diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml new file mode 100644 index 0000000..5335fb8 --- /dev/null +++ b/.github/workflows/flake8.yaml @@ -0,0 +1,20 @@ +--- +name: Flake8 +on: pull_request +jobs: + flake8: + name: Check code with Flake8 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Setup environment + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run Flake8 + run: flake8 . --ignore=E266,E261,E265,W503 --max-line-length=120 --count diff --git a/.github/workflows/nose2.yaml b/.github/workflows/nose2.yaml new file mode 100644 index 0000000..d537aa5 --- /dev/null +++ b/.github/workflows/nose2.yaml @@ -0,0 +1,21 @@ +--- +name: Nose2 +on: pull_request +jobs: + nose2: + name: Run unit tests with nose2 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Setup environment + run: | + python -m pip install --upgrade pip + pip install nose2 pyyaml pandas numpy allantools + - name: Run nose2 + # run: python -m nose2 + run: python -m nose2 -s postprocess # only do postprocessing code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d8c32b --- /dev/null +++ b/.gitignore @@ -0,0 +1,195 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c731b8a..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "vse-sync-pp"] - path = vse-sync-pp - url = git@github.com:redhat-partner-solutions/vse-sync-pp.git diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..80081d3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +[MASTER] + +load-plugins=pylint.extensions.no_self_use +init-hook='import sys; sys.path.append("src")' diff --git a/Containerfile b/Containerfile index 8e9b410..925c933 100644 --- a/Containerfile +++ b/Containerfile @@ -1,17 +1,3 @@ -# This needs access to private git repos, so uses the `--ssh` option of `podman build`. -# e.g. `podman build --ssh=default .` -# -# You must have an SSH key which is authorized for the github account, and that does -# NOT require a password. It must be added to your github account. -# -# If git clone fails with an error like this: -# agent key RSA SHA256:BSM91TfbWaV/8f0dYA6Itp7xd7mxVcCc8Ineu2YK9bc returned incorrect signature type -# -# make sure your SSH key does not require a password. -# I also had to create a key of type ED25519 for it to work, RSA did not work. -# `ssh-keygen -t ed25519` -# - FROM registry.access.redhat.com/ubi9/ubi-minimal:latest RUN microdnf install -y git golang python3 python3-pip tar python3-yaml jq ruby @@ -25,21 +11,15 @@ ENV VSE_DIR=/usr/vse RUN mkdir -p ${VSE_DIR} WORKDIR ${VSE_DIR} -RUN mkdir -p -m 0600 ~/.ssh && \ - ssh-keyscan -H github.com >> ~/.ssh/known_hosts - -RUN --mount=type=ssh git clone --depth=1 git@github.com:redhat-partner-solutions/testdrive.git +RUN git clone -v --depth=1 https://github.com/redhat-partner-solutions/testdrive.git -RUN --mount=type=ssh git clone --depth=1 git@github.com:redhat-partner-solutions/vse-sync-test-report.git +RUN git clone -v --depth=1 https://github.com/redhat-partner-solutions/vse-sync-test-report.git -RUN --mount=type=ssh git clone --depth=1 git@github.com:redhat-partner-solutions/vse-sync-test.git -WORKDIR ${VSE_DIR}/vse-sync-test -RUN --mount=type=ssh git submodule update --init --recursive +RUN git clone -v --depth=1 https://github.com/redhat-partner-solutions/vse-sync-test.git -WORKDIR ${VSE_DIR} -RUN --mount=type=ssh git clone git@github.com:redhat-partner-solutions/vse-sync-collection-tools.git +RUN git clone -v https://github.com/redhat-partner-solutions/vse-sync-collection-tools.git WORKDIR ${VSE_DIR}/vse-sync-collection-tools RUN go mod vendor WORKDIR ${VSE_DIR} -CMD ["./vse-sync-test/hack/e2e.sh", "-d", "2000s", "/usr/vse/kubeconfig"] +CMD ["./vse-sync-test/cmd/e2e.sh", "-d", "2000s", "/usr/vse/kubeconfig"] diff --git a/LICENSE b/LICENSE index d159169..1b04da3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,339 +1,15 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. +SPDX-License-Identifier: GPL-2.0-or-later + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; either version 2 of the License, or (at your +option) any later version. +. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +. +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA, search GPL-2.0-or-later or +see . diff --git a/README.adoc b/README.adoc index b3ea915..fce9453 100644 --- a/README.adoc +++ b/README.adoc @@ -104,7 +104,7 @@ For example to run analysis over an existing dataset stored in `~/tmp/data/` the [source,shell] ---- -podman run -v ~/kubeconfig-cormorant:/usr/vse/kubeconfig -v ~/tmp/data:/usr/vse/data quay.io/redhat-partner-solutions/vse-sync-test:latest ./vse-sync-test/hack/e2e.sh +podman run -v ~/kubeconfig-cormorant:/usr/vse/kubeconfig -v ~/tmp/data:/usr/vse/data quay.io/redhat-partner-solutions/vse-sync-test:latest ./vse-sync-test/cmd/e2e.sh ---- == Development Setup @@ -127,11 +127,11 @@ $ tree -L 1 - To clean up a previous run and do a new test run: [source,shell] ---- -$ rm -r data/*; ./vse-sync-test/hack/e2e.sh -d 2000s ~/kubeconfig-cormorant +$ rm -r data/*; ./vse-sync-test/cmd/e2e.sh -d 2000s ~/kubeconfig-cormorant ---- - Omit the kubeconfig to skip data collection and re-analyse existing data in `data/collected/` : [source,shell] ---- -$ rm -r data/artefacts; rm data/*; ./vse-sync-test/hack/e2e.sh +$ rm -r data/artefacts; rm data/*; ./vse-sync-test/cmd/e2e.sh ---- diff --git a/hack/e2e.sh b/cmd/e2e.sh similarity index 99% rename from hack/e2e.sh rename to cmd/e2e.sh index 66b6432..6ec250f 100755 --- a/hack/e2e.sh +++ b/cmd/e2e.sh @@ -15,7 +15,7 @@ ANALYSERPATH=$TESTROOT/vse-sync-test REPORTGENPATH=$TESTROOT/vse-sync-test-report REPORTPRIVSUTGENPATH=$TESTROOT/vse-sync-sut TDPATH=$TESTROOT/testdrive/src -PPPATH=$ANALYSERPATH/vse-sync-pp/src +PPPATH=$ANALYSERPATH/postprocess/src OUTPUTDIR=$TESTROOT/data DATADIR=$OUTPUTDIR/collected # Raw collected data/logs @@ -143,7 +143,6 @@ audit_container() { { "vse-sync-collection-tools": $(audit_repo $COLLECTORPATH), "vse-sync-test": $(audit_repo $ANALYSERPATH), - "vse-sync-pp": $(audit_repo $PPPATH), "testdrive": $(audit_repo $TDPATH), "vse-sync-test-report": $(audit_repo $REPORTGENPATH) } diff --git a/postprocess/LICENSE b/postprocess/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/postprocess/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/postprocess/MANIFEST.in b/postprocess/MANIFEST.in new file mode 100644 index 0000000..c1a7121 --- /dev/null +++ b/postprocess/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include README.md diff --git a/postprocess/README.adoc b/postprocess/README.adoc new file mode 100644 index 0000000..79c57ee --- /dev/null +++ b/postprocess/README.adoc @@ -0,0 +1,92 @@ += vse-sync-pp + +This repository contains python modules for analyzing and generating synchronization test results by means of consuming input data coming from: + +* ptp4l, ts2phc, and phc2sys logs coming from PTP operator logs + +* collector files generated by collector tools developed in https://github.com/redhat-partner-solutions/vse-sync-collection-tools[collectors] + +These are the main python modules: + +* link:src/vse_sync_pp/demux.py[demux]: Demultiplex gathered data messages of a given identifier coming from a single multiplexed data source. This module consumes directly data coming from collectors. For each demultiplexed data message of a given identifier print the canonical data produced by the parser as JSON. The module leverages `parse` module to generate its output. + +* link:src/vse_sync_pp/parse.py[parse]: Parse data messages of a given identifier from a single data source. When used standalone (without `demux` module) the data source comes from PTP operator logs. For each parsed data message print the canonical data produced by the parser as JSON. + +* link:src/vse_sync_pp/analyze.py[analyze]: Analyze data messages from a single source. Analyze data parsed from the log messages in input. Print the test result and data analysis as JSON. + +* link:src/vse_sync_pp/plot.py[plot]: plot data parsed from data messages coming from a single source. The data parsed from incoming data messages is plotted to an image file. + +== Running + +=== Demux collector data from file + +To see the collector demuxers available: + + python3 -m vse_sync_pp.demux --help + +To filter a specific collector from file aggregating several collectors: + + python3 -m vse_sync_pp.demux + +To filter a collector presented on stdin: + + python3 -m vse_sync_pp.demux - + +=== Parse a log file + +To see the parsers available: + + python3 -m vse_sync_pp.parse --help + +To parse an existing log file: + + python3 -m vse_sync_pp.parse + +To parse data presented on stdin: + + python3 -m vse_sync_pp.parse - + +To output timestamps relative to the timestamp of the first accepted log line: + + python3 -m vse_sync_pp.parse --relative + +=== Plot unfiltered log data + +To see the parsers available: + + python3 -m vse_sync_pp.plot --help + +To plot data from existing log file `` to ``: + + python3 -m vse_sync_pp.plot + +To plot data presented on stdin: + + python3 -m vse_sync_pp.plot - + +To plot already parsed (canonical) data: + + python3 -m vse_sync_pp.plot --canonical + +=== Analyze unfiltered log data + +To see the analyzers available: + + python3 -m vse_sync_pp.analyze --help + +To analyze data from existing log file: + + python3 -m vse_sync_pp.analyze + +To analyze data presented on stdin: + + python3 -m vse_sync_pp.analyze - + +To analyze already parsed (canonical) data: + + python3 -m vse_sync_pp.analyze --canonical + +== Contributing to the repo + +See the link:doc/CONTRIBUTING.adoc[contribution guide] for detailed instructions +on how to get started contributing to this repo. diff --git a/postprocess/config/prtca.yaml b/postprocess/config/prtca.yaml new file mode 100644 index 0000000..8e33656 --- /dev/null +++ b/postprocess/config/prtca.yaml @@ -0,0 +1,6 @@ +--- +requirements: G.8272/PRTC-A +parameters: + transient-period/s: 300 + min-test-duration/s: 1000 + time-error-limit/%: 10 diff --git a/postprocess/config/prtcb.yaml b/postprocess/config/prtcb.yaml new file mode 100644 index 0000000..dc290e0 --- /dev/null +++ b/postprocess/config/prtcb.yaml @@ -0,0 +1,6 @@ +--- +requirements: G.8272/PRTC-B +parameters: + transient-period/s: 300 + min-test-duration/s: 1000 + time-error-limit/%: 10 diff --git a/postprocess/config/ran.yaml b/postprocess/config/ran.yaml new file mode 100644 index 0000000..79eba63 --- /dev/null +++ b/postprocess/config/ran.yaml @@ -0,0 +1,6 @@ +--- +requirements: workload/RAN +parameters: + transient-period/s: 300 + min-test-duration/s: 1000 + time-error-limit/%: 10 diff --git a/postprocess/doc/CONTRIBUTING.adoc b/postprocess/doc/CONTRIBUTING.adoc new file mode 100644 index 0000000..03343c4 --- /dev/null +++ b/postprocess/doc/CONTRIBUTING.adoc @@ -0,0 +1,61 @@ += Contributing + +You can contribute to the modules in this repo in several ways. In terms of a "good PR" there is one important requirement to fulfill: + +1. A PR that provides a new class or an updated class in `parsers` module requires its respective set of unit tests. + +2. A PR that provides a new class or an updated class in `analyzers` module requires its respective set of unit tests. + +== Parsers + +Collector and/or ptp operator logs parsers are contributed into `src/vse_sync_pp/parsers/`. + +Each log file parser must be represented by a separate class that: + +* is provided in an appropriate module, with module named by: + * user space process; + * kernel module; or, + * functional area +* is registered in `vse_sync_pp.parse.PARSERS` +* has unit test cases built by class `ParserTestBuilder` + * (`tests/vse_sync_pp/parsers/test_parser.py`) + +== Analyzers + +Log file analyzers are contributed into `src/vse_sync_pp/analyzers/`. + +Each log file analyzer must be represented by a separate class that: + +* inherits from Analyzer class +* is provided in an appropriate module +* is registered in `vse_sync_pp.analyze.ANALYZERS` +* has unit test cases built by class `AnalyzerTestBuilder` + * (`tests/vse_sync_pp/analyzers/test_analyzer.py`) +* an example of `Analyzer` is `TimeErrorAnalyzerBase` class + +=== TimeErrorAnalyzerBase + +`TimeErrorAnalyzerBase` is a class used to calculate the time error of a given kind of clock. Derived clases with name `TimeErrorAnalyzer` override details specifics of the time error calculation for eahc clock type under evaluation. For example, each clock type has a specific value of locked clock classes: + +Derived class `TimeErrorAnalyzer` for the DPLL clock marks the state Locked as `3`: + +[source, python] +--- +locked = frozenset({3}) +--- + +whereas derived class `TimeErrorAnalyzer` for PTP Hardware Clock marks Locked state as `s2`: + +[source, python] +--- +locked = frozenset({'s2'}) +--- + +== Demuxers + +Contributions to demux module can be done by proposing changes based on `src/vse_sync_pp/demux.py`. + +== Plots + +Contributions to plot module can be done by proposing changes based on +`src/vse_sync_pp/plot.py`. diff --git a/postprocess/pyproject.toml b/postprocess/pyproject.toml new file mode 100644 index 0000000..250c8b5 --- /dev/null +++ b/postprocess/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] + +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/postprocess/setup.cfg b/postprocess/setup.cfg new file mode 100644 index 0000000..f9da265 --- /dev/null +++ b/postprocess/setup.cfg @@ -0,0 +1,41 @@ +[metadata] + +name = vse-sync-pp +version = 0.1.0 +author = Red Hat Telco Solutions +#author_email = +license = GNU General Public License v2 or later (GPLv2+) +description = Synchronization post-processing +long_description = file: README.md +long_description_content_type = text/markdown +#url = +project_urls = + Source Code = https://github.com/redhat-partner-solutions/vse-sync-pp +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) + +[options] + +package_dir = + =src +packages = find: +python_requires = + >=3.10 +#install_requires = + +[options.packages.find] + +where = src + +[tox:tox] + +min_version = 4.0 + +[testenv] + +description = run unit tests +deps = + nose2 + coverage +commands = nose2 -v -C --coverage src/ diff --git a/postprocess/src/vse_sync_pp/__init__.py b/postprocess/src/vse_sync_pp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postprocess/src/vse_sync_pp/analyze.py b/postprocess/src/vse_sync_pp/analyze.py new file mode 100644 index 0000000..b490b70 --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyze.py @@ -0,0 +1,64 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze log messages from a single source.""" + +from argparse import ArgumentParser +import sys + +from .common import ( + open_input, + print_loj, +) + +from .parsers import PARSERS +from .analyzers import ( + ANALYZERS, + Config, +) + + +def main(): + """Analyze log messages from a single source. + + Analyze data parsed from the log messages in input. Print the test result + and data analysis as JSON. + """ + aparser = ArgumentParser(description=main.__doc__) + aparser.add_argument( + '--canonical', action='store_true', + help="input contains canonical data", + ) + aparser.add_argument( + '--config', + help="YAML file specifying test requirements and parameters", + ) + aparser.add_argument( + 'input', + help="input file, or '-' to read from stdin", + ) + aparser.add_argument( + 'analyzer', choices=tuple(ANALYZERS), + help="analyzer to run over input", + ) + args = aparser.parse_args() + config = Config.from_yaml(args.config) if args.config else Config() + analyzer = ANALYZERS[args.analyzer](config) + parser = PARSERS[analyzer.parser]() + with open_input(args.input) as fid: + method = parser.canonical if args.canonical else parser.parse + for parsed in method(fid): + analyzer.collect(parsed) + dct = { + 'result': analyzer.result, + 'timestamp': analyzer.timestamp, + 'duration': analyzer.duration, + 'reason': analyzer.reason, + 'analysis': analyzer.analysis, + } + # Python exits with error code 1 on EPIPE + if not print_loj(dct): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/postprocess/src/vse_sync_pp/analyzers/__init__.py b/postprocess/src/vse_sync_pp/analyzers/__init__.py new file mode 100644 index 0000000..cf5851e --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/__init__.py @@ -0,0 +1,31 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyzers""" + +from .analyzer import Config # noqa + +from . import ( + gnss, + ppsdpll, + ts2phc, + phc2sys, + pmc, +) + +ANALYZERS = { + cls.id_: cls for cls in ( + gnss.TimeErrorAnalyzer, + ppsdpll.TimeErrorAnalyzer, + ts2phc.TimeErrorAnalyzer, + phc2sys.TimeErrorAnalyzer, + pmc.ClockStateAnalyzer, + gnss.TimeDeviationAnalyzer, + ppsdpll.TimeDeviationAnalyzer, + ts2phc.TimeDeviationAnalyzer, + phc2sys.TimeDeviationAnalyzer, + gnss.MaxTimeIntervalErrorAnalyzer, + ppsdpll.MaxTimeIntervalErrorAnalyzer, + ts2phc.MaxTimeIntervalErrorAnalyzer, + phc2sys.MaxTimeIntervalErrorAnalyzer, + ) +} diff --git a/postprocess/src/vse_sync_pp/analyzers/analyzer.py b/postprocess/src/vse_sync_pp/analyzers/analyzer.py new file mode 100644 index 0000000..e5bc250 --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/analyzer.py @@ -0,0 +1,498 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Common analyzer functionality""" + +import yaml +from pandas import DataFrame +from datetime import (datetime, timezone) + +import allantools +import numpy as np + +from scipy import signal as scipy_signal + +from ..requirements import REQUIREMENTS + + +class Config(): + """Analyzer configuration""" + def __init__(self, filename=None, requirements=None, parameters=None): + self._filename = filename + self._requirements = requirements + self._parameters = parameters + + def _reason(self, reason): + """Return `reason`, extended if this config is from a file.""" + if self._filename is None: + return reason + return reason + f' in config file {self._filename}' + + def requirement(self, key): + """Return the value at `key` in this configuration's requirements. + + Raise :class:`KeyError` if a value cannot be returned. + """ + try: + return REQUIREMENTS[self._requirements][key] + except KeyError as exc: + if self._requirements is None: + reason = 'no requirements specified' + elif self._requirements not in REQUIREMENTS: + reason = f'unknown requirements {self._requirements}' + else: + reason = f'unknown requirement {key} in {self._requirements}' + raise KeyError(self._reason(reason)) from exc + + def parameter(self, key): + """Return the value at `key` in this configuration's parameters. + + Raise :class:`KeyError` if a value cannot be returned. + """ + try: + return self._parameters[key] + except TypeError as exc: + reason = 'no parameters specified' + raise KeyError(self._reason(reason)) from exc + except KeyError as exc: + reason = f'unknown parameter {key}' + raise KeyError(self._reason(reason)) from exc + + @classmethod + def from_yaml(cls, filename, encoding='utf-8'): + """Build configuration from YAML file at `filename`""" + with open(filename, encoding=encoding) as fid: + dct = dict(yaml.safe_load(fid.read())) + return cls(filename, dct.get('requirements'), dct.get('parameters')) + + +class CollectionIsClosed(Exception): + """Data collection was closed while collecting data""" + # empty + + +class Analyzer(): + """A base class providing common analyzer functionality""" + def __init__(self, config): + self._config = config + self._rows = [] + self._data = None + self._result = None + self._reason = None + self._timestamp = None + self._duration = None + self._analysis = None + + def collect(self, *rows): + """Collect data from `rows`""" + if self._rows is None: + raise CollectionIsClosed() + self._rows += rows + + def prepare(self, rows): + """Return (columns, records) from collected data `rows` + + `columns` is a sequence of column names + `records` is a sequence of rows prepared for test analysis + + If `records` is an empty sequence, then `columns` is also empty. + """ + return (rows[0]._fields, rows) if rows else ((), ()) + + def close(self): + """Close data collection""" + if self._data is None: + (columns, records) = self.prepare(self._rows) + self._data = DataFrame.from_records(records, columns=columns) + self._rows = None + + def _test(self): + """Close data collection and test collected data""" + if self._result is None: + self.close() + (self._result, self._reason) = self.test(self._data) + + def _explain(self): + """Close data collection and explain collected data""" + if self._analysis is None: + self.close() + self._analysis = self.explain(self._data) + self._timestamp = self._analysis.pop('timestamp', None) + self._duration = self._analysis.pop('duration', None) + + def _timestamp_from_dec(self, dec): + """Return an absolute timestamp or decimal timestamp from `dec`. + + If `dec` is large enough to represent 2023 (the year this was coded), + or a later year, then assume it represents an absolute date-time. (This + is >53 years if counting seconds from zero.) Otherwise assume relative + time. + + >>> today = datetime.now() + >>> today + datetime.datetime(2023, 9, 8, 16, 49, 54, 735285) + >>> ts = today.timestamp() + >>> ts + 1694188194.735285 + >>> ts / (365 * 24 * 60 * 60) + 53.72235523640554 + """ + # `dtv` is a timezone-aware datetime value with resolution of seconds + dtv = datetime.fromtimestamp(int(dec), tz=timezone.utc) + if datetime.now().year - dtv.year <= 1: + # absolute date-time + return dtv.isoformat() + # relative time + return dec + + @staticmethod + def _check_missing_samples(data, result, reason): + if reason is None: + if len(data.timestamp.diff().astype(float).round(0).tail(-1).unique()) > 1: + return (False, "missing test samples") + return result, reason + + @property + def result(self): + """The boolean result from this analyzer's test of the collected data""" + self._test() + return self._result + + @property + def reason(self): + """A string qualifying :attr:`result` (or None if unqualified)""" + self._test() + return self._reason + + @property + def timestamp(self): + """The ISO 8601 date-time timestamp, when the test started""" + self._explain() + return self._timestamp + + @property + def duration(self): + """The test duration in seconds""" + self._explain() + return self._duration + + @property + def analysis(self): + """A structured analysis of the collected data""" + self._explain() + return self._analysis + + @staticmethod + def _statistics(data, units, ndigits=3): + """Return a dict of statistics for `data`, rounded to `ndigits`""" + def _round(val): + """Return `val` as native Python type or Decimal, rounded to `ndigits`""" + try: + return round(val.item(), ndigits) + except AttributeError: + return round(val, ndigits) + min_ = data.min() + max_ = data.max() + return { + 'units': units, + 'min': _round(min_), + 'max': _round(max_), + 'range': _round(max_ - min_), + 'mean': _round(data.mean()), + 'stddev': _round(data.std()), + 'variance': _round(data.var()), + } + + def test(self, data): + """This analyzer's test of the collected `data`. + + Return a 2-tuple (result, reason) where `result` is a boolean or the + string "error" and `reason` is a string or None. + + A boolean `result` indicates test success or failure: `result` "error" + indicates that a result could not be produced. + + A string `reason` qualifies `result`; when None `result` is unqualified. + """ + raise NotImplementedError + + def explain(self, data): + """Return a structured analysis of the collected `data`""" + raise NotImplementedError + + +class TimeErrorAnalyzerBase(Analyzer): + """Analyze time error. + + Derived classes must override class attribute `locked`, specifying a + frozenset of values representing locked states. + """ + locked = frozenset() + + def __init__(self, config): + super().__init__(config) + # required system time output accuracy + accuracy = config.requirement('time-error-in-locked-mode/ns') + # limit on inaccuracy at observation point + limit = config.parameter('time-error-limit/%') + # exclusive upper bound on absolute time error for any sample + self._unacceptable = accuracy * (limit / 100) + # samples in the initial transient period are ignored + self._transient = config.parameter('transient-period/s') + # minimum test duration for a valid test + self._duration_min = config.parameter('min-test-duration/s') + + def prepare(self, rows): + idx = 0 + try: + tstart = rows[0].timestamp + self._transient + except IndexError: + pass + else: + while idx < len(rows): + if tstart <= rows[idx].timestamp: + break + idx += 1 + return super().prepare(rows[idx:]) + + def test(self, data): + if len(data) == 0: + return ("error", "no data") + if frozenset(data.state.unique()).difference(self.locked): # pylint: disable=no-member + return (False, "loss of lock") + terr_min = data.terror.min() + terr_max = data.terror.max() + if self._unacceptable <= max(abs(terr_min), abs(terr_max)): + return (False, "unacceptable time error") + if data.iloc[-1].timestamp - data.iloc[0].timestamp < self._duration_min: + return (False, "short test duration") + if len(data) - 1 < self._duration_min: + return (False, "short test samples") + return (True, None) + + def explain(self, data): + if len(data) == 0: + return {} + return { + 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), + 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, + 'terror': self._statistics(data.terror, 'ns'), + } + + +def calculate_limit(accuracy, limit_percentage, tau): + """Calculate upper limit based on tau + + `accuracy` is the list of functions to calculate upper limits + `limit_percentage` is the unaccuracy percentage + `tau` is the observation window interval + + Return the upper limit value based on `tau` + """ + for (low, high), f in accuracy.items(): + if ((low is None or tau > low) and (tau <= high)): + return f(tau) * (limit_percentage / 100) + + +def out_of_range(taus, samples, accuracy, limit): + """Check if the input samples are out of range. + + `taus` list of observation windows intervals + `samples` are input samples + `accuracy` contains the list of upper bound limit functions + `limit` is the percentage to apply the upper limit + + Return `True` if any value in `samples` is out of range + """ + for tau, sample in zip(taus, samples): + mask = calculate_limit(accuracy, limit, tau) + if mask <= sample: + return True + return False + + +def calculate_filter(input_signal, transient, sample_rate): + """Calculate digital low-pass filter from `input_signal` + + scipy_signal.butter input arguments: + order of the filter: 1 + critical Frequency in half cycles per sample: + (for digital filters is normalized from 0 to 1 where 1 is Nyquist frequency) + cutoff Frequency: 0.1Hz + sample_rate in samples per second + btype is band type or type of filter + analog=False since it is always a digital filter + scipy_signal.butter return arguments: + `numerator` coefficient vector and `denominator coefficient vector of the butterworth digital filter + """ + numerator, denominator = scipy_signal.butter(1, 0.1 / (sample_rate / 2), btype="low", analog=False, output="ba") + lpf_signal = scipy_signal.filtfilt(numerator, denominator, input_signal.terror) + lpf_signal = lpf_signal[transient:len(lpf_signal)] + return lpf_signal + + +class TimeIntervalErrorAnalyzerBase(Analyzer): + """Analyze Time Interval Error (also referred to as Wander). + + Derived classes calculate specific Time Interval Error metric focused on measuring + the change of Time Error. + """ + locked = frozenset() + + def __init__(self, config): + super().__init__(config) + # samples in the initial transient period are ignored + self._transient = config.parameter('transient-period/s') + # minimum test duration for a valid test + self._duration_min = config.parameter('min-test-duration/s') + # limit initial tau observation windows from 1 to 10k taus + taus_below_10k = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, + 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, + 4000, 5000, 6000, 7000, 8000, 9000, 10000]) + # observation window upper limit to 100k samples in 5k increments + taus_above_10k = np.arange(15000, 100000, 5000) + # `taus_list` contains range limit of obervation window intervals for which to compute the statistic + self._taus_list = np.concatenate((taus_below_10k, taus_above_10k)) + self._rate = None + self._lpf_signal = None + + def prepare(self, rows): + idx = 0 + try: + tstart = rows[0].timestamp + self._transient + except IndexError: + pass + else: + while idx < len(rows): + if tstart <= rows[idx].timestamp: + break + idx += 1 + return super().prepare(rows[idx:]) + + @staticmethod + def calculate_rate(data): + # calculate sample rate using 100 samples + cumdelta = 0 + + for i in range(1, 100 if len(data) > 100 else len(data)): + cumdelta = cumdelta + data.iloc[i].timestamp - data.iloc[i - 1].timestamp + return round((1 / (cumdelta / 100))) + + def _test_common(self, data): + if len(data) == 0: + return ("error", "no data") + if frozenset(data.state.unique()).difference(self.locked): + return (False, "loss of lock") + if data.iloc[-1].timestamp - data.iloc[0].timestamp < self._duration_min: + return (False, "short test duration") + if len(data) - 1 < self._duration_min: + return (False, "short test samples") + return None + + def _explain_common(self, data): + if len(data) == 0: + return {} + if self._rate is None: + self._rate = self.calculate_rate(self._data) + if self._lpf_signal is None: + self._lpf_signal = calculate_filter(data, self._transient, self._rate) + return None + + def toplot(self): + self.close() + self._generate_taus() + yield from zip(self._taus, self._samples) + + def _generate_taus(self): + if self._rate is None: + self._rate = self.calculate_rate(self._data) + if self._lpf_signal is None: + self._lpf_signal = calculate_filter(self._data, self._transient, self._rate) + return None + + +class TimeDeviationAnalyzerBase(TimeIntervalErrorAnalyzerBase): + """Analyze Time Deviation (TDEV). + + Derived classes must override class attribute `locked`, specifying a + frozenset of values representing locked states. + """ + def __init__(self, config): + super().__init__(config) + # required system time deviation output + self._accuracy = config.requirement('time-deviation-in-locked-mode/ns') + # limit of inaccuracy at observation point + self._limit = config.parameter('time-deviation-limit/%') + # list of observation windows intervals to calculate TDEV + # `_taus` is a subset of `taus_list` + self._taus = None + # TDEV samples + self._samples = None + + def _generate_taus(self): + super()._generate_taus() + if self._samples is None: + self._taus, self._samples, errors, ns = allantools.tdev(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + + def test(self, data): + result = self._test_common(data) + if result is None: + self._generate_taus() + if out_of_range(self._taus, self._samples, self._accuracy, self._limit): + return (False, "unacceptable time deviation") + return (True, None) + return result + + def explain(self, data): + analysis = self._explain_common(data) + if analysis is None: + self._generate_taus() + return { + 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), + 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, + 'tdev': self._statistics(self._samples, 'ns'), + } + return analysis + + +class MaxTimeIntervalErrorAnalyzerBase(TimeIntervalErrorAnalyzerBase): + """Analyze Maximum Time Interval Error (MTIE). + + Derived classes must override class attribute `locked`, specifying a + frozenset of values representing locked states. + """ + def __init__(self, config): + super().__init__(config) + # required system maximum time interval error output in ns + self._accuracy = config.requirement('maximum-time-interval-error-in-locked-mode/ns') + # limit of inaccuracy at observation point + self._limit = config.parameter('maximum-time-interval-error-limit/%') + # list of observation windows intervals to calculate MTIE + # `_taus` will be a subset of `taus_list` + self._taus = None + # MTIE samples + self._samples = None + + def _generate_taus(self): + super()._generate_taus() + if self._samples is None: + self._taus, self._samples, errors, ns = allantools.mtie(self._lpf_signal, rate=self._rate, data_type="phase", taus=self._taus_list) # noqa + + def test(self, data): + result = self._test_common(data) + if result is None: + self._generate_taus() + if out_of_range(self._taus, self._samples, self._accuracy, self._limit): + return (False, "unacceptable mtie") + return (True, None) + return result + + def explain(self, data): + analysis = self._explain_common(data) + if analysis is None: + self._generate_taus() + return { + 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), + 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, + 'mtie': self._statistics(self._samples, 'ns'), + } + return analysis diff --git a/postprocess/src/vse_sync_pp/analyzers/gnss.py b/postprocess/src/vse_sync_pp/analyzers/gnss.py new file mode 100644 index 0000000..7bc597d --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/gnss.py @@ -0,0 +1,37 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze GNSS log messages""" + +from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase + + +class TimeErrorAnalyzer(TimeErrorAnalyzerBase): + """Analyze time error""" + id_ = 'gnss/time-error' + parser = id_ + # 'state' values are assumed to be u-blox gpsFix values + # 0 = no fix + # 1 = dead reckoning only + # 2 = 2D-Fix + # 3 = 3D-Fix + # 4 = GPS + dead reckoning combined + # 5 = time only fix + locked = frozenset({3, 4, 5}) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'gnss/time-deviation' + parser = 'gnss/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({3, 4, 5}) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze time deviation""" + id_ = 'gnss/mtie' + parser = 'gnss/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({3, 4, 5}) diff --git a/postprocess/src/vse_sync_pp/analyzers/phc2sys.py b/postprocess/src/vse_sync_pp/analyzers/phc2sys.py new file mode 100644 index 0000000..1f6dda7 --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/phc2sys.py @@ -0,0 +1,37 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze phc2sys log messages""" + +from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase + + +class TimeErrorAnalyzer(TimeErrorAnalyzerBase): + """Analyze time error""" + id_ = 'phc2sys/time-error' + parser = id_ + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'phc2sys/time-deviation' + parser = 'phc2sys/time-error' + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze max time interval error""" + id_ = 'phc2sys/mtie' + parser = 'phc2sys/time-error' + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) diff --git a/postprocess/src/vse_sync_pp/analyzers/pmc.py b/postprocess/src/vse_sync_pp/analyzers/pmc.py new file mode 100644 index 0000000..45492d4 --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/pmc.py @@ -0,0 +1,188 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze PMC log messages""" + +from .analyzer import Analyzer +import copy + +STATE_FREERUN = 248 +STATE_LOCKED = 6 +STATE_HOLDOVER_IN_SPEC = 7 +STATE_HOLDOVER_OUT_OF_SPEC1 = 140 +STATE_HOLDOVER_OUT_OF_SPEC2 = 150 +STATE_HOLDOVER_OUT_OF_SPEC3 = 160 +CLOCK_ACCURACY_NANO_SECONDS100 = "0x21" +CLOCK_ACCURACY_UNKNOWN = "0xFE" +OFFSET_SCALED_LOG_VARIANCE_CONNECTED = "0x4E5D" +OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED = "0xFFFF" + +STATE_NAMES = { + STATE_FREERUN: "FREERUN", + STATE_LOCKED: "LOCKED", + STATE_HOLDOVER_IN_SPEC: "HOLDOVER_IN_SPEC", + STATE_HOLDOVER_OUT_OF_SPEC1: "HOLDOVER_OUT_SPEC1", + STATE_HOLDOVER_OUT_OF_SPEC2: "HOLDOVER_OUT_SPEC2", + STATE_HOLDOVER_OUT_OF_SPEC3: "HOLDOVER_OUT_SPEC3", +} + +STATE_TRANSITION = { + STATE_FREERUN: [STATE_FREERUN, STATE_LOCKED], + STATE_LOCKED: [STATE_LOCKED, STATE_HOLDOVER_IN_SPEC], + STATE_HOLDOVER_IN_SPEC: [STATE_LOCKED, + STATE_HOLDOVER_IN_SPEC, + STATE_HOLDOVER_OUT_OF_SPEC1, + STATE_HOLDOVER_OUT_OF_SPEC2, + STATE_HOLDOVER_OUT_OF_SPEC3], + STATE_HOLDOVER_OUT_OF_SPEC1: [STATE_LOCKED, + STATE_HOLDOVER_OUT_OF_SPEC1, + STATE_HOLDOVER_OUT_OF_SPEC2, + STATE_HOLDOVER_OUT_OF_SPEC3], + STATE_HOLDOVER_OUT_OF_SPEC2: [STATE_LOCKED, + STATE_HOLDOVER_OUT_OF_SPEC1, + STATE_HOLDOVER_OUT_OF_SPEC2, + STATE_HOLDOVER_OUT_OF_SPEC3], + STATE_HOLDOVER_OUT_OF_SPEC3: [STATE_LOCKED, + STATE_HOLDOVER_OUT_OF_SPEC1, + STATE_HOLDOVER_OUT_OF_SPEC2, + STATE_HOLDOVER_OUT_OF_SPEC3], +} + +BASE_CLOCK_CLASS_COUNT = { + "count": 0, + "transitions": { + STATE_FREERUN: 0, + STATE_LOCKED: 0, + STATE_HOLDOVER_IN_SPEC: 0, + STATE_HOLDOVER_OUT_OF_SPEC1: 0, + STATE_HOLDOVER_OUT_OF_SPEC2: 0, + STATE_HOLDOVER_OUT_OF_SPEC3: 0, + }, +} + +CLOCK_ACCURACY_FOR_CLOCK_CLASS = { + STATE_FREERUN: CLOCK_ACCURACY_UNKNOWN, + STATE_LOCKED: CLOCK_ACCURACY_NANO_SECONDS100, + STATE_HOLDOVER_IN_SPEC: CLOCK_ACCURACY_UNKNOWN, + STATE_HOLDOVER_OUT_OF_SPEC1: CLOCK_ACCURACY_UNKNOWN, + STATE_HOLDOVER_OUT_OF_SPEC2: CLOCK_ACCURACY_UNKNOWN, + STATE_HOLDOVER_OUT_OF_SPEC3: CLOCK_ACCURACY_UNKNOWN, +} + +OFFSET_SCALED_LOG_VARIANCE_FOR_CLOCK_CLASS = { + STATE_FREERUN: OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED, + STATE_LOCKED: OFFSET_SCALED_LOG_VARIANCE_CONNECTED, + STATE_HOLDOVER_IN_SPEC: OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED, + STATE_HOLDOVER_OUT_OF_SPEC1: OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED, + STATE_HOLDOVER_OUT_OF_SPEC2: OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED, + STATE_HOLDOVER_OUT_OF_SPEC3: OFFSET_SCALED_LOG_VARIANCE_NOT_CONNECTED, +} + + +def is_illegal_transition(current_state, new_state): + return new_state not in STATE_TRANSITION[current_state] + + +def is_illegal_clock_accuracy(state, clock_accuracy): + return clock_accuracy.upper() != CLOCK_ACCURACY_FOR_CLOCK_CLASS[state].upper() + + +def is_illegal_offset_scaled_log_variance(state, offset_scaled_log_variance): + return offset_scaled_log_variance.upper() != OFFSET_SCALED_LOG_VARIANCE_FOR_CLOCK_CLASS[state].upper() + + +def get_named_clock_class_result(clock_class_count): + named_clock_class_count = {STATE_NAMES[k]: v for (k, v) in clock_class_count.items()} + for clock_class in clock_class_count.values(): + clock_class["transitions"] = {STATE_NAMES[k]: v for (k, v) in clock_class["transitions"].items()} + return named_clock_class_count + + +class ClockStateAnalyzer(Analyzer): + """Analyze clock state + """ + id_ = 'phc/gm-settings' + parser = id_ + + def __init__(self, config): + super().__init__(config) + # minimum test duration for a valid test + self._duration_min = config.parameter('min-test-duration/s') + self.transition_count = 0 + self.clock_class_count = { + STATE_FREERUN: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + STATE_LOCKED: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + STATE_HOLDOVER_IN_SPEC: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + STATE_HOLDOVER_OUT_OF_SPEC1: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + STATE_HOLDOVER_OUT_OF_SPEC2: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + STATE_HOLDOVER_OUT_OF_SPEC3: copy.deepcopy(BASE_CLOCK_CLASS_COUNT), + } + + def prepare(self, rows): + idx = 0 + try: + tstart = rows[0].timestamp + except IndexError: + pass + else: + while idx < len(rows): + if tstart <= rows[idx].timestamp: + break + idx += 1 + return super().prepare(rows[idx:]) + + def test(self, data): + if len(data) == 0: + return ("error", "no data") + + if data.iloc[-1].timestamp - data.iloc[0].timestamp < self._duration_min: + return (False, "short test duration") + if len(data) - 1 < self._duration_min: + return (False, "short test samples") + + state = None + illegal_transition = False + illegal_clock_accuracy = False + illegal_offset_scaled_log_variance = False + for index, row in data.iterrows(): + clock_class = row['clock_class'] + clock_accuracy = row['clockAccuracy'] + offset_scaled_log_variance = row['offsetScaledLogVariance'] + + if (state is None) and (clock_class in STATE_TRANSITION): + state = clock_class + else: + if clock_class != state: + self.transition_count += 1 + + if clock_class not in STATE_TRANSITION: + return (False, f"wrong clock class {clock_class}") + + if is_illegal_transition(state, clock_class): + illegal_transition = True + + self.clock_class_count[state]["transitions"][clock_class] += 1 + state = clock_class + self.clock_class_count[clock_class]["count"] += 1 + + if is_illegal_clock_accuracy(state, clock_accuracy): + illegal_clock_accuracy = True + if is_illegal_offset_scaled_log_variance(state, offset_scaled_log_variance): + illegal_offset_scaled_log_variance = True + if illegal_transition: + return (False, "illegal state transition") + if illegal_clock_accuracy: + return (False, "illegal clock accuracy") + if illegal_offset_scaled_log_variance: + return (False, "illegal offset scaled log variance") + return (True, None) + + def explain(self, data): + if len(data) == 0: + return {} + + return { + 'timestamp': self._timestamp_from_dec(data.iloc[0].timestamp), + 'duration': data.iloc[-1].timestamp - data.iloc[0].timestamp, + 'clock_class_count': get_named_clock_class_result(self.clock_class_count), + 'total_transitions': self.transition_count, + } diff --git a/postprocess/src/vse_sync_pp/analyzers/ppsdpll.py b/postprocess/src/vse_sync_pp/analyzers/ppsdpll.py new file mode 100644 index 0000000..2453884 --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/ppsdpll.py @@ -0,0 +1,54 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze ppsdpll log messages""" + +from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase + + +class TimeErrorAnalyzer(TimeErrorAnalyzerBase): + """Analyze DPLL time error""" + id_ = 'ppsdpll/time-error' + parser = 'dpll/time-error' + # 'state' unlocked + # -1 = DPLL_UNKNOWN + # 0 = DPLL_INVALID + # 1 = DPLL_FREERUN + # 'state' locked and operational + # 2 = DPLL_LOCKED + # 3 = DPLL_LOCKED HOLDOVER ACQUIRED + # 'state' unlocked but operational + # 4 = DPLL_HOLDOVER + locked = frozenset({2, 3}) + + def prepare(self, rows): + return super().prepare([ + r._replace(terror=float(r.terror)) for r in rows + ]) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze DPLL time deviation""" + id_ = 'ppsdpll/time-deviation' + parser = 'dpll/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({2, 3}) + + def prepare(self, rows): + return super().prepare([ + r._replace(terror=float(r.terror)) for r in rows + ]) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze DPLL max time interval error""" + id_ = 'ppsdpll/mtie' + parser = 'dpll/time-error' + # see 'state' values in `TimeErrorAnalyzer` comments + locked = frozenset({2, 3}) + + def prepare(self, rows): + return super().prepare([ + r._replace(terror=float(r.terror)) for r in rows + ]) diff --git a/postprocess/src/vse_sync_pp/analyzers/ts2phc.py b/postprocess/src/vse_sync_pp/analyzers/ts2phc.py new file mode 100644 index 0000000..b1bc7fa --- /dev/null +++ b/postprocess/src/vse_sync_pp/analyzers/ts2phc.py @@ -0,0 +1,36 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Analyze ts2phc log messages""" +from .analyzer import TimeErrorAnalyzerBase +from .analyzer import TimeDeviationAnalyzerBase +from .analyzer import MaxTimeIntervalErrorAnalyzerBase + + +class TimeErrorAnalyzer(TimeErrorAnalyzerBase): + """Analyze time error""" + id_ = 'ts2phc/time-error' + parser = id_ + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) + + +class TimeDeviationAnalyzer(TimeDeviationAnalyzerBase): + """Analyze time deviation""" + id_ = 'ts2phc/time-deviation' + parser = 'ts2phc/time-error' + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) + + +class MaxTimeIntervalErrorAnalyzer(MaxTimeIntervalErrorAnalyzerBase): + """Analyze max time interval error""" + id_ = 'ts2phc/mtie' + parser = 'ts2phc/time-error' + locked = frozenset({'s2'}) + + def test(self, data): + return self._check_missing_samples(data, *super().test(data)) diff --git a/postprocess/src/vse_sync_pp/common.py b/postprocess/src/vse_sync_pp/common.py new file mode 100644 index 0000000..41be59e --- /dev/null +++ b/postprocess/src/vse_sync_pp/common.py @@ -0,0 +1,54 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Common code for command line tools""" + +import sys +from contextlib import nullcontext + +import json +from decimal import Decimal +import numpy + + +def open_input(filename, encoding='utf-8', **kwargs): + """Return a context manager for reading from `filename`. + + If `filename` is '-' then read from stdin instead of `filename`. + """ + if filename == '-': + return nullcontext(sys.stdin) + return open(filename, encoding=encoding, **kwargs) + + +class JsonEncoder(json.JSONEncoder): + """A JSON encoder accepting :class:`Decimal` values + and arrays `numpy.ndarray` values + """ + def default(self, o): + """Return a commonly serializable value from `o`""" + if isinstance(o, Decimal): + return float(o) + if isinstance(o, numpy.ndarray): + return o.tolist() + return super().default(o) + + +def print_loj(val, encoder_cls=JsonEncoder, flush=True): + """Print value `val` as a line of JSON and, optionally, `flush` stdout. + + If SIGPIPE is received then set `sys.stdout` to None and return False: + otherwise return True. + + The Python recommendation suggests this clean up on SIGPIPE: + https://docs.python.org/3/library/signal.html#note-on-sigpipe + + However this code uses setting `sys.stdout` to None as per: + https://stackoverflow.com/questions/26692284/\ + how-to-prevent-brokenpipeerror-when-doing-a-flush-in-python + """ + try: + print(json.dumps(val, cls=encoder_cls), flush=flush) + return True + except BrokenPipeError: + sys.stdout = None + return False diff --git a/postprocess/src/vse_sync_pp/demux.py b/postprocess/src/vse_sync_pp/demux.py new file mode 100644 index 0000000..5002dc9 --- /dev/null +++ b/postprocess/src/vse_sync_pp/demux.py @@ -0,0 +1,43 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Demultiplex log messages from a single multiplexed source.""" + +from argparse import ArgumentParser +import sys + +from .common import ( + open_input, + print_loj, +) + +from .parsers import PARSERS +from .source import muxed + + +def main(): + """Demultiplex log messages from a single multiplexed source. + + Demultiplex log messages for the specified parser from the multiplexed + content in input. For each demultiplexed log message print the canonical + data produced by the parser as JSON. + """ + aparser = ArgumentParser(description=main.__doc__) + aparser.add_argument( + 'input', + help="input file, or '-' to read from stdin", + ) + aparser.add_argument( + 'parser', choices=tuple(PARSERS), + help="data to demultiplex from input", + ) + args = aparser.parse_args() + parser = PARSERS[args.parser]() + with open_input(args.input) as fid: + for (_, data) in muxed(fid, {parser.id_: parser}): + # Python exits with error code 1 on EPIPE + if not print_loj(data): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/postprocess/src/vse_sync_pp/heatmap.py b/postprocess/src/vse_sync_pp/heatmap.py new file mode 100644 index 0000000..bb70391 --- /dev/null +++ b/postprocess/src/vse_sync_pp/heatmap.py @@ -0,0 +1,65 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Heatmap data parsed from log messages from a single source.""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + + +class Heatmap(): + """ + Heatmap to visualize data values as a 2D grid of colored squares. + Input parameters: + x_ticks - x axis ticks + y_ticks - y axis ticks + title - heatmap title + unallowed_cells - a list of cells to be colored in red representing a bad relationship + between the two axis variables. + row/column format: ([0,1], [1,1]) + colorbar_label - colorbar label + xlabel - x axis label + ylabel - y axis label + """ + def __init__(self, x_ticks, y_ticks, title, unallowed_cells, + colorbar_label, xlabel, ylabel): + self._x_ticks = x_ticks + self._y_ticks = y_ticks + self._title = title + self._unallowed_cells = unallowed_cells + self._colorbar_label = colorbar_label + self._xlabel = xlabel + self._ylabel = ylabel + + def plot(self, data, filename): + np_data = np.array(data) + fig, ax = plt.subplots() + im = ax.imshow(np_data, cmap="cividis") + + # Add a colorbar for reference + cbar = plt.colorbar(im) + cbar.set_label(self._colorbar_label) + + # Show all ticks and label them with the respective list entries + ax.set_xticks(np.arange(len(self._x_ticks)), labels=self._x_ticks) + ax.set_yticks(np.arange(len(self._y_ticks)), labels=self._y_ticks) + + # Rotate the tick labels and set their alignment. + plt.setp(ax.get_xticklabels(), rotation=45, ha="right", + rotation_mode="anchor") + plt.xlabel(self._xlabel) + plt.ylabel(self._ylabel) + + # Loop over data dimensions and create text annotations. + for i in range(len(self._x_ticks)): + for j in range(len(self._y_ticks)): + ax.text(j, i, np_data[i, j], + ha="center", va="center", color="white") + + for row, col in self._unallowed_cells: + if np_data[row][col] >= 1: + rect = patches.Rectangle((col - 0.5, row - 0.5), 1, 1, linewidth=1, edgecolor='none', facecolor='red') + ax.add_patch(rect) + ax.set_title(self._title) + fig.tight_layout() + plt.savefig(filename) diff --git a/postprocess/src/vse_sync_pp/parse.py b/postprocess/src/vse_sync_pp/parse.py new file mode 100644 index 0000000..e8ba436 --- /dev/null +++ b/postprocess/src/vse_sync_pp/parse.py @@ -0,0 +1,45 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse log messages from a single source.""" + +from argparse import ArgumentParser +import sys + +from .common import ( + open_input, + print_loj, +) + +from .parsers import PARSERS + + +def main(): + """Parse log messages from a single source. + + Parse log messages using the specified parser. For each parsed log message + print the canonical data produced by the parser as JSON. + """ + aparser = ArgumentParser(description=main.__doc__) + aparser.add_argument( + '-r', '--relative', action='store_true', + help="print timestamps relative to the first line's timestamp", + ) + aparser.add_argument( + 'input', + help="input file, or '-' to read from stdin", + ) + aparser.add_argument( + 'parser', choices=tuple(PARSERS), + help="data to parse from input", + ) + args = aparser.parse_args() + parser = PARSERS[args.parser]() + with open_input(args.input) as fid: + for data in parser.parse(fid, relative=args.relative): + # Python exits with error code 1 on EPIPE + if not print_loj(data): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/postprocess/src/vse_sync_pp/parsers/__init__.py b/postprocess/src/vse_sync_pp/parsers/__init__.py new file mode 100644 index 0000000..8681f1c --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/__init__.py @@ -0,0 +1,19 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parsers""" + +from . import dpll +from . import gnss +from . import ts2phc +from . import phc2sys +from . import pmc + +PARSERS = { + cls.id_: cls for cls in ( + dpll.TimeErrorParser, + gnss.TimeErrorParser, + ts2phc.TimeErrorParser, + phc2sys.TimeErrorParser, + pmc.ClockClassParser, + ) +} diff --git a/postprocess/src/vse_sync_pp/parsers/dpll.py b/postprocess/src/vse_sync_pp/parsers/dpll.py new file mode 100644 index 0000000..d7f2bff --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/dpll.py @@ -0,0 +1,28 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse dpll log messages""" + +from collections import namedtuple + +from .parser import (Parser, parse_timestamp, parse_decimal) + + +class TimeErrorParser(Parser): + """Parse Time Error from a dpll CSV sample""" + id_ = 'dpll/time-error' + elems = ('timestamp', 'eecstate', 'state', 'terror') + y_name = 'terror' + parsed = namedtuple('Parsed', elems) + + def make_parsed(self, elems): + if len(elems) < len(self.elems): + raise ValueError(elems) + timestamp = parse_timestamp(elems[0]) + eecstate = int(elems[1]) + state = int(elems[2]) + terror = parse_decimal(elems[3]) + return self.parsed(timestamp, eecstate, state, terror) + + def parse_line(self, line): + # DPLL samples come from a fixed format CSV file + return self.make_parsed(line.split(',')) diff --git a/postprocess/src/vse_sync_pp/parsers/gnss.py b/postprocess/src/vse_sync_pp/parsers/gnss.py new file mode 100644 index 0000000..8cffbf2 --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/gnss.py @@ -0,0 +1,34 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse GNSS log messages""" + +from collections import namedtuple + +from .parser import (Parser, parse_timestamp) + + +class TimeErrorParser(Parser): + """Parse time error from a GNSS CSV sample""" + id_ = 'gnss/time-error' + # 'state' values are assumed to be u-blox gpsFix values + # 0 = no fix + # 1 = dead reckoning only + # 2 = 2D-Fix + # 3 = 3D-Fix + # 4 = GPS + dead reckoning combined + # 5 = time only fix + elems = ('timestamp', 'state', 'terror') + y_name = 'terror' + parsed = namedtuple('Parsed', elems) + + def make_parsed(self, elems): + if len(elems) < len(self.elems): + raise ValueError(elems) + timestamp = parse_timestamp(elems[0]) + state = int(elems[1]) + terror = int(elems[2]) + return self.parsed(timestamp, state, terror) + + def parse_line(self, line): + # GNSS samples come from a fixed format CSV file + return self.make_parsed(line.split(',')) diff --git a/postprocess/src/vse_sync_pp/parsers/parser.py b/postprocess/src/vse_sync_pp/parsers/parser.py new file mode 100644 index 0000000..1dddcde --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/parser.py @@ -0,0 +1,122 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Common parser functionality""" + +import json +import re +from datetime import (datetime, timezone) +from decimal import (Decimal, InvalidOperation) + +# sufficient regex to extract the whole decimal fraction part +RE_ISO8601_DECFRAC = re.compile( + r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.(\d+)(.*)$' +) + + +def parse_timestamp_abs(val): + """Return a :class:`Decimal` from `val`, an absolute timestamp string. + + Accepted absolute timestamp strings are ISO 8601 format with restrictions. + The string must: explicitly specify UTC timezone, supply time in seconds, + specify a decimal fractional part with a decimal mark of '.'. + + Return None if `val` is not a string or is not an ISO 8601 format string. + Raise :class:`ValueError` otherwise. + """ + try: + dtv = datetime.fromisoformat(val) + except TypeError: + return None + except ValueError: + # before Python 3.11 `fromisoformat` fails if UTC denoted by 'Z' and/or + # the decimal fraction has more than 6 digits + match = RE_ISO8601_DECFRAC.match(val) + if match is None: + return None + # parse without decimal fraction, with 'Z' substituted + tzv = '+00:00' if match.group(3) == 'Z' else match.group(3) + dtv = datetime.fromisoformat(match.group(1) + tzv) + else: + match = RE_ISO8601_DECFRAC.match(val) + if match is None: + raise ValueError(val) + if dtv.tzinfo != timezone.utc: + raise ValueError(val) + # `dtv` may truncate decimal fraction: use decimal fraction from `val` + return Decimal(f'{int(dtv.timestamp())}.{match.group(2)}') + + +def parse_decimal(val): + """Return a :class:`Decimal` from `val` or raise :class:`ValueError`""" + try: + return Decimal(val) + except InvalidOperation as exc: + raise ValueError(val) from exc + + +def parse_timestamp(val): + """Return a :class:`Decimal` from absolute or relative timestamp `val`""" + return parse_timestamp_abs(val) or parse_decimal(val) + + +def relative_timestamp(parsed, tzero): + """Return relative timestamp with respect to `tzero` coming from `parsed`""" + timestamp = getattr(parsed, 'timestamp', None) + if timestamp is not None: + if tzero is None: + tzero = timestamp + parsed = parsed._replace(timestamp=timestamp - tzero) + return tzero, parsed + + +class Parser(): + """A base class providing common parser functionality""" + def make_parsed(self, elems): + """Return a namedtuple value from parsed iterable `elems`. + + Raise :class:`ValueError` if a value cannot be formed from `elems`. + """ + raise ValueError(elems) + + def parse_line(self, line): + """Parse `line`. + + If `line` is accepted, return a namedtuple value. + If `line` is rejected, raise :class:`ValueError`. + Otherwise the `line` is discarded, return None. + """ + return None + + def parse(self, file, relative=False): + """Parse lines from `file` object. + + This method is a generator yielding a namedtuple value for each + accepted line in `file`. If `relative` is truthy, then present all + timestamps relative to the first accepted line's timestamp. + """ + tzero = None + for line in file: + parsed = self.parse_line(line) + if parsed is not None: + if relative: + tzero, parsed = relative_timestamp(parsed, tzero) + yield parsed + + def canonical(self, file, relative=False): + """Parse canonical data from `file` object. + + The canonical representation is JSON-encoded parsed data, with one + parsed item per line in `file`. If `relative` is truthy, then present + all timestamps relative to the first accepted line's timestamp. + + This method is a generator yielding a namedtuple value for each line in + `file`. + """ + tzero = None + for line in file: + obj = json.loads(line, parse_float=Decimal) + parsed = self.make_parsed(obj) + if parsed is not None: + if relative: + tzero, parsed = relative_timestamp(parsed, tzero) + yield parsed diff --git a/postprocess/src/vse_sync_pp/parsers/phc2sys.py b/postprocess/src/vse_sync_pp/parsers/phc2sys.py new file mode 100644 index 0000000..d7e4fa8 --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/phc2sys.py @@ -0,0 +1,55 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse phc2sys log messages""" + +import re +from collections import namedtuple + +from .parser import (Parser, parse_decimal) + + +class TimeErrorParser(Parser): + """Parse time error from a phc2sys log message""" + id_ = 'phc2sys/time-error' + elems = ('timestamp', 'terror', 'state', 'delay') + y_name = 'terror' + parsed = namedtuple('Parsed', elems) + + @staticmethod + def build_regexp(): + """Return a regular expression string for parsing phc2sys log file lines""" + return r'\s'.join((r'^phc2sys' + + r'\[([1-9][0-9]*\.[0-9]{3})\]:' # timestamp + + r'(?:\s\[ptp4l\.\d\..*\])?', # configuration file name + r'CLOCK_REALTIME phc offset\s*', + r'(-?[0-9]+)', # time error + r'(\S+)', # state + r'freq\s*', + r'([-+]?[0-9]+)', # frequency error + r'delay\s*', + r'(-?[0-9]+)' # delay + + r'\s*.*$')) + + def __init__(self): + super().__init__() + self._regexp = re.compile(self.build_regexp()) + + def make_parsed(self, elems): + if len(elems) < len(self.elems): + raise ValueError(elems) + timestamp = parse_decimal(elems[0]) + terror = int(elems[1]) + state = str(elems[2]) + delay = int(elems[3]) + return self.parsed(timestamp, terror, state, delay) + + def parse_line(self, line): + matched = self._regexp.match(line) + if matched: + return self.make_parsed(( + matched.group(1), + matched.group(2), + matched.group(3), + matched.group(5), + )) + return None diff --git a/postprocess/src/vse_sync_pp/parsers/pmc.py b/postprocess/src/vse_sync_pp/parsers/pmc.py new file mode 100644 index 0000000..6786806 --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/pmc.py @@ -0,0 +1,28 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse PMC log messages""" + +from collections import namedtuple + +from .parser import (Parser, parse_timestamp) + + +class ClockClassParser(Parser): + """Parse clock class samples""" + id_ = 'phc/gm-settings' + elems = ('timestamp', 'clock_class', 'clockAccuracy', 'offsetScaledLogVariance') + y_name = 'clock_class' + parsed = namedtuple('Parsed', elems) + + def make_parsed(self, elems): + if len(elems) < len(self.elems): + raise ValueError(elems) + timestamp = parse_timestamp(elems[0]) + clock_class = int(elems[1]) + clock_accuracy = str(elems[2]).rstrip() + offset_scaled_log_variance = str(elems[3]).rstrip() + return self.parsed(timestamp, clock_class, clock_accuracy, offset_scaled_log_variance) + + def parse_line(self, line): + # PMC samples come from a CSV file + return self.make_parsed(line.split(',')) diff --git a/postprocess/src/vse_sync_pp/parsers/ts2phc.py b/postprocess/src/vse_sync_pp/parsers/ts2phc.py new file mode 100644 index 0000000..9182416 --- /dev/null +++ b/postprocess/src/vse_sync_pp/parsers/ts2phc.py @@ -0,0 +1,57 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Parse ts2phc log messages""" + +import re +from collections import namedtuple + +from .parser import (Parser, parse_decimal) + + +class TimeErrorParser(Parser): + """Parse time error from a ts2phc log message""" + id_ = 'ts2phc/time-error' + elems = ('timestamp', 'interface', 'terror', 'state') + y_name = 'terror' + parsed = namedtuple('Parsed', elems) + + @staticmethod + def build_regexp(interface=None): + """Return a regular expression string for parsing log file lines. + + If `interface` then only parse lines for the specified interface. + """ + return r''.join(( + r'^ts2phc' + + r'\[([1-9][0-9]*\.[0-9]{3})\]:', # timestamp + r'(?:\s\[ts2phc\.\d\..*\])?', # configuration file name + fr'\s({interface})' if interface else r'\s(\S+)', # interface + r'\smaster offset\s*', + r'\s(-?[0-9]+)', # time error + r'\s(\S+)', # state + r'.*$', + )) + + def __init__(self, interface=None): + super().__init__() + self._regexp = re.compile(self.build_regexp(interface)) + + def make_parsed(self, elems): + if len(elems) < len(self.elems): + raise ValueError(elems) + timestamp = parse_decimal(elems[0]) + interface = str(elems[1]) + terror = int(elems[2]) + state = str(elems[3]) + return self.parsed(timestamp, interface, terror, state) + + def parse_line(self, line): + matched = self._regexp.match(line) + if matched: + return self.make_parsed(( + matched.group(1), + matched.group(2), + matched.group(3), + matched.group(4), + )) + return None diff --git a/postprocess/src/vse_sync_pp/plot.py b/postprocess/src/vse_sync_pp/plot.py new file mode 100644 index 0000000..1594a4e --- /dev/null +++ b/postprocess/src/vse_sync_pp/plot.py @@ -0,0 +1,123 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Plot data parsed from log messages from a single source.""" + +from argparse import ArgumentParser + +import numpy as np +import matplotlib.pyplot as plt +from collections import namedtuple + +from .common import open_input + +from .parsers import PARSERS + + +Axis = namedtuple("Axis", ["desc", "attr", "scale", "scale_kwargs"], defaults=[None, None, None, None]) +TIMESERIES = Axis("Time (s)", "timestamp") + + +class Plotter(): + """Rudimentary plotter of data values against timestamp""" + def __init__(self, x, y): + self._x = x + self._y = y + self._x_data = [] + self._y_data = [] + + @staticmethod + def _extract_attr(axis, data): + return getattr(data, axis.attr) + + def _set_yscale(self, ax): + if self._y.scale is not None: + ax.set_yscale(self._y.scale, **(self._y.scale_kwargs or {})) + elif any((abs(v) > 10 for v in self._y_data)): + ax.set_yscale("symlog", linthresh=10) + + def append(self, data): + """Append x and y data points extracted from `data`""" + self._x_data.append(self._extract_attr(self._x, data)) + self._y_data.append(self._extract_attr(self._y, data)) + + def _plot_scatter(self, ax): + ax.axhline(0, color='black') + self._set_yscale(ax) + if self._x.scale is not None: + ax.set_xscale(self._x.scale, **(self._x.scale_kwargs or {})) + ax.plot(self._x_data, self._y_data, '.') + ax.grid() + ax.set_title(f'{self._x.desc} vs {self._y.desc}') + + def _plot_hist(self, ax): + counts, bins = np.histogram( + np.array(self._y_data, dtype=float), + bins='scott' + ) + ax.hist(bins[:-1], bins, weights=counts) + self._set_yscale(ax) + if self._x.scale is not None: + ax.set_xscale(self._x.scale, **(self._x.scale_kwargs or {})) + ax.set_title(f'Histogram of {self._y.desc}') + + def plot(self, filename): + """Plot data to `filename`""" + fig, (ax1, ax2) = plt.subplots(2, constrained_layout=True) + fig.set_size_inches(10, 8) + self._plot_scatter(ax1) + self._plot_hist(ax2) + ax3 = ax2.twinx() + ax3.set_ylabel('CDF') + ax3.ecdf(np.array(self._y_data, dtype=float), color="black", linewidth=2) + plt.savefig(filename) + return fig, (ax1, ax2, ax3) + + def plot_scatter(self, filename): + fig, ax = plt.subplots(1, constrained_layout=True) + fig.set_size_inches(10, 4) + self._plot_scatter(ax) + plt.savefig(filename) + return fig, ax + + def plot_histogram(self, filename): + fig, ax = plt.subplots(1, constrained_layout=True) + fig.set_size_inches(10, 4) + self._plot_hist(ax) + plt.savefig(filename) + return fig, ax + + +def main(): + """Plot data parsed from log messages from a single source. + + Plot data parsed from the log messages in input to an image file. + """ + aparser = ArgumentParser(description=main.__doc__) + aparser.add_argument( + '-c', '--canonical', action='store_true', + help="input contains canonical data", + ) + aparser.add_argument( + 'input', + help="input file, or '-' to read from stdin", + ) + aparser.add_argument( + 'parser', choices=tuple(PARSERS), + help="data to parse from input", + ) + aparser.add_argument( + 'output', + help="output image filename", + ) + args = aparser.parse_args() + parser = PARSERS[args.parser]() + plotter = Plotter(TIMESERIES, Axis(parser.y_name, parser.y_name)) + with open_input(args.input) as fid: + method = parser.canonical if args.canonical else parser.parse + for parsed in method(fid): + plotter.append(parsed) + plotter.plot(args.output) + + +if __name__ == '__main__': + main() diff --git a/postprocess/src/vse_sync_pp/requirements.py b/postprocess/src/vse_sync_pp/requirements.py new file mode 100644 index 0000000..de25da7 --- /dev/null +++ b/postprocess/src/vse_sync_pp/requirements.py @@ -0,0 +1,39 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Requirements specified in ITU-T G.8272/Y.1367""" + +REQUIREMENTS = { + 'G.8272/PRTC-A': { + 'maximum-time-interval-error-in-locked-mode/ns': { + (None, 273): lambda t: 0.275 * t + 25, + (274, 100000): lambda t: 100 + }, + 'time-deviation-in-locked-mode/ns': { + (None, 100): lambda t: 3, + (101, 1000): lambda t: 0.03 * t, + (1001, 100000): lambda t: 30 + }, + 'time-error-in-locked-mode/ns': 100, + }, + 'G.8272/PRTC-B': { + 'maximum-time-interval-error-in-locked-mode/ns': { + (None, 54.5): lambda t: 0.275 * t + 25, + (54.5, 100000): lambda t: 40 + }, + 'time-deviation-in-locked-mode/ns': { + (None, 100): lambda t: 1, + (101, 500): lambda t: 0.01 * t, + (501, 100000): lambda t: 5 + }, + 'time-error-in-locked-mode/ns': 40, + }, + 'workload/RAN': { + 'time-error-in-locked-mode/ns': 100, + 'time-deviation-in-locked-mode/ns': { + (None, 1000000): lambda t: 100 + }, + 'maximum-time-interval-error-in-locked-mode/ns': { + (None, 100000): lambda t: 1 + } + }, +} diff --git a/postprocess/src/vse_sync_pp/sequence.py b/postprocess/src/vse_sync_pp/sequence.py new file mode 100644 index 0000000..0724bde --- /dev/null +++ b/postprocess/src/vse_sync_pp/sequence.py @@ -0,0 +1,141 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Sequence log messages from multiple sources.""" + +from argparse import ArgumentParser +import sys +from sys import stdin + +from collections import namedtuple +import yaml + +from .common import print_loj + +from .parsers import PARSERS +from .source import ( + logged, + muxed, +) + + +def build_emit(available, include=(), exclude=()): + """Return a frozenset of ids to emit. + + If `include` is empty, then return ids in `available` but not in `exclude`. + Otherwise, return ids in `available` and `include` but not in `exclude`. + """ + ava = frozenset(available) + inc = frozenset(include) + exc = frozenset(exclude) + return ava.intersection(inc or ava).difference(exc) + + +def build_sources(parsers, filename, encoding='utf-8'): + """Generator yielding (id_, data) generators for sources in `filename`""" + parsers = {id_: cls() for (id_, cls) in parsers.items()} + with open(filename, encoding=encoding) as fid: + for obj in yaml.safe_load_all(fid.read()): + source = obj['source'] + contains = obj['contains'] + file = stdin if source == '-' else open(source, encoding=encoding) + if contains == 'muxed': + yield muxed(file, parsers) + else: + yield logged(file, parsers[contains]) + + +# tuple of the most recent values generated by source +# timestamp must be first and numeric +Head = namedtuple('Head', ('timestamp', 'id_', 'data', 'source')) + + +def build_head(source): + """Return a :class:`Head` value or None from the next item in `source`.""" + try: + (id_, data) = next(source) + return Head(data.timestamp, id_, data, source) + except StopIteration: + return None + + +def build_heads(sources): + """Return a list of :class:`Head` values from the first items in `sources`. + + The returned list is sorted by timestamp in ascending order. + """ + heads = [] + for source in sources: + head = build_head(source) + if head: + heads.append(head) + return sorted(heads) + + +def insert_head(heads, head): + """Return `heads` with `head` inserted in ascending timestamp order. + + If `head` is None then do not insert `head`. + """ + if head is None: + return heads + idx = 0 + while idx < len(heads): + if head.timestamp < heads[idx].timestamp: + break + idx += 1 + heads.insert(idx, head) + return heads + + +def main(): + """Sequence log messages from multiple sources to stdout. + + Log messages are read from files specified in the YAML file supplied in + command line argument `sources`. This YAML file may contain multiple + documents, with each document containing a single object with 'source' and + 'contains' pairs. 'source' specifies a file to read from or '-' to read from + stdin; 'contains' specifies the content in the file, either the id of a + supported parser (as listed in command line help for options `--include` + and `--exclude`) or 'muxed' for multiplexed content. + + Each line in multiplexed content must be a JSON-encoded object with 'id' and + 'data' pairs. 'id' must be the id of a supported parser; 'data' a value + conforming to the canonical form produced by that parser. + + The output written to stdout is multiplexed content, with lines sequenced as + follows. An initial set of candidate messages to write is constructed by + parsing the first log message from each source. The message with the lowest + timestamp in this set is written to stdout before being replaced by the next + log message from its source. This process is repeated until all sources are + empty. (For the avoidance of doubt, log messages within a single source are + not sequenced by this tool: they are processed in file order.) + """ + aparser = ArgumentParser(description=main.__doc__) + aparser.add_argument( + '--include', choices=PARSERS, nargs='*', default=(), + help='restrict output messages to these parsers', + ) + aparser.add_argument( + '--exclude', choices=PARSERS, nargs='*', default=(), + help='never output messages for these parsers (overriding)', + ) + aparser.add_argument( + 'sources', + help='YAML file specifying sources of log messages', + ) + args = aparser.parse_args() + emit = build_emit(PARSERS, args.include, args.exclude) + sources = tuple(build_sources(PARSERS, args.sources)) + heads = build_heads(sources) + while heads: + first = heads.pop(0) + if first.id_ in emit: + obj = {'id': first.id_, 'data': first.data} + # Python exits with error code 1 on EPIPE + if not print_loj(obj): + sys.exit(1) + heads = insert_head(heads, build_head(first.source)) + + +if __name__ == '__main__': + main() diff --git a/postprocess/src/vse_sync_pp/source.py b/postprocess/src/vse_sync_pp/source.py new file mode 100644 index 0000000..a73bace --- /dev/null +++ b/postprocess/src/vse_sync_pp/source.py @@ -0,0 +1,61 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Log message sources.""" + +import json +from decimal import Decimal + + +def logged(file, parser): + """Generator yielding (id_, data) for lines in `file` parsed by `parser`. + + `id_` is the value of the corresponding parser attribute; + `data` is the canonical data produced by `parser` for a line. + + `file` is closed just before returning. + """ + while True: + line = file.readline() + if line == '': + file.close() + return + data = parser.parse_line(line.rstrip()) + if data is not None: + yield (parser.id_, data) + + +def muxed(file, parsers): + """Generator yielding (id_, data) for multiplexed content in `file`. + + Each line in `file` must be a JSON-encoded object with pairs at 'id' and + 'data'. If there is no parser for the value at 'id' in `parsers`, then the + line is discarded: otherwise a pair is generated. + + `id_` is the value at 'id'; + `data` the values within data must be the equivalent to canonical data + produced by the parser at `id_` in `parsers` for the value at 'data'. + They can be in the form of a JSON array or a JSON object. + If the data is a JSON array then the ordering must match the parser output. + If the data is a JSON object then the names must match the parser output. + + + `file` is closed just before returning. + """ + while True: + line = file.readline() + if line == '': + file.close() + return + obj = json.loads(line.rstrip(), parse_float=Decimal) + id_ = obj['id'] + try: + parser = parsers[id_] + except KeyError: + pass + else: + if isinstance(obj['data'], dict): + data = tuple(obj['data'][name] for name in parser.elems) + else: + data = obj['data'] + parsed = parser.make_parsed(data) + yield (id_, parsed) diff --git a/postprocess/tests/__init__.py b/postprocess/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postprocess/tests/vse_sync_pp/__init__.py b/postprocess/tests/vse_sync_pp/__init__.py new file mode 100644 index 0000000..cf2de07 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/__init__.py @@ -0,0 +1,12 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Common test functions""" + + +def make_fqname(subject): + """Return the fully-qualified name of test `subject`.""" + # subject is assumed to be a class + return '.'.join(( + subject.__module__, + subject.__name__, + )) diff --git a/postprocess/tests/vse_sync_pp/analyzers/__init__.py b/postprocess/tests/vse_sync_pp/analyzers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postprocess/tests/vse_sync_pp/analyzers/config.yaml b/postprocess/tests/vse_sync_pp/analyzers/config.yaml new file mode 100644 index 0000000..ba01f84 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/config.yaml @@ -0,0 +1,4 @@ +requirements: G.8272/PRTC-A +parameters: + foo: bar + baz: 8 diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_analyzer.py b/postprocess/tests/vse_sync_pp/analyzers/test_analyzer.py new file mode 100644 index 0000000..fd4c310 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_analyzer.py @@ -0,0 +1,210 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers""" + +from unittest import TestCase +from os.path import join as joinpath +from os.path import dirname + +from nose2.tools import params + +from vse_sync_pp.analyzers.analyzer import ( + Config, + CollectionIsClosed, +) + +from .. import make_fqname + + +class TestConfig(TestCase): + """Tests for vse_sync_pp.analyzers.analyzer.Config""" + def test_requirement_errors(self): + """Test vse_sync_pp.analyzers.analyzer.Config.requirement errors""" + # no filename, no requirements + config = Config() + with self.assertRaises(KeyError) as ctx: + config.requirement('foo') + self.assertEqual( + str(ctx.exception), + "'no requirements specified'", + ) + # filename, no requirements + config = Config('bar') + with self.assertRaises(KeyError) as ctx: + config.requirement('baz') + self.assertEqual( + str(ctx.exception), + "'no requirements specified in config file bar'", + ) + # no filename, unknown requirements + config = Config(requirements='quux') + with self.assertRaises(KeyError) as ctx: + config.requirement('corge') + self.assertEqual( + str(ctx.exception), + "'unknown requirements quux'", + ) + # filename, unknown requirements + config = Config('quuz', 'thud') + with self.assertRaises(KeyError) as ctx: + config.requirement('wibble') + self.assertEqual( + str(ctx.exception), + "'unknown requirements thud in config file quuz'", + ) + # no filename, unknown requirement + config = Config(requirements='G.8272/PRTC-A') + with self.assertRaises(KeyError) as ctx: + config.requirement('xxyyz') + self.assertEqual( + str(ctx.exception), + "'unknown requirement xxyyz in G.8272/PRTC-A'", + ) + # filename, unknown requirement + config = Config(filename='foobarbaz', requirements='G.8272/PRTC-A') + with self.assertRaises(KeyError) as ctx: + config.requirement('quod') + self.assertEqual( + str(ctx.exception), + "'unknown requirement quod in G.8272/PRTC-A" + " in config file foobarbaz'", + ) + + def test_requirement_success(self): + """Test vse_sync_pp.analyzers.analyzer.Config.requirement success""" + config = Config(requirements='G.8272/PRTC-A') + key = 'time-error-in-locked-mode/ns' + self.assertEqual(config.requirement(key), 100) + + def test_parameter_errors(self): + """Test vse_sync_pp.analyzers.analyzer.Config.parameter errors""" + # no filename, no parameters + config = Config() + with self.assertRaises(KeyError) as ctx: + config.parameter('foo') + self.assertEqual( + str(ctx.exception), + "'no parameters specified'", + ) + # filename, no parameters + config = Config('bar') + with self.assertRaises(KeyError) as ctx: + config.parameter('baz') + self.assertEqual( + str(ctx.exception), + "'no parameters specified in config file bar'", + ) + # no filename, unknown parameters + config = Config(parameters={'quux': 3}) + with self.assertRaises(KeyError) as ctx: + config.parameter('corge') + self.assertEqual( + str(ctx.exception), + "'unknown parameter corge'", + ) + # filename, unknown parameters + config = Config('quuz', parameters={'thud': 7}) + with self.assertRaises(KeyError) as ctx: + config.parameter('wibble') + self.assertEqual( + str(ctx.exception), + "'unknown parameter wibble in config file quuz'", + ) + + def test_parameter_success(self): + """Test vse_sync_pp.analyzers.analyzer.Config.parameter success""" + config = Config(parameters={'xxyyz': 'success'}) + self.assertEqual(config.parameter('xxyyz'), 'success') + + def test_yaml(self): + """Test vse_sync_pp.analyzers.analyzer.Config.from_yaml""" + filename = joinpath(dirname(__file__), 'config.yaml') + config = Config.from_yaml(filename) + self.assertEqual( + config.requirement('time-error-in-locked-mode/ns'), + 100, + ) + self.assertEqual(config.parameter('foo'), 'bar') + self.assertEqual(config.parameter('baz'), 8) + + +class AnalyzerTestBuilder(type): + """Build tests for vse_sync_pp.analyzers + + Specify this class as metaclass and provide: + `constructor` - a callable returning the analyzer to test + `id_` - the expected value of analyzer class attribute `id_` + `parser` - the expected value of analyzer class attribute `parser` + `expect` - dict of requirements, parameters, rows, result, reason, + timestamp, duration, analysis giving test config, input data, + expected outputs + """ + def __new__(cls, name, bases, dct): + constructor = dct['constructor'] + fqname = make_fqname(constructor) + dct.update({ + 'test_id': cls.make_test_id( + constructor, fqname, + dct['id_'], + ), + 'test_parser': cls.make_test_parser( + constructor, fqname, + dct['parser'], + ), + 'test_result': cls.make_test_result( + constructor, fqname, + dct['expect'], + ), + }) + return super().__new__(cls, name, bases, dct) + + # make functions for use as TestCase methods + @staticmethod + def make_test_id(constructor, fqname, id_): + """Make a function testing id_ class attribute value""" + def method(self): + """Test analyzer id_ class attribute value""" + self.assertEqual(constructor.id_, id_) + method.__doc__ = f'Test {fqname} id_ class attribute value' + return method + + @staticmethod + def make_test_parser(constructor, fqname, parser): + """Make a function testing parser class attribute value""" + def method(self): + """Test analyzer parser class attribute value""" + self.assertEqual(constructor.parser, parser) + method.__doc__ = f'Test {fqname} parser class attribute value' + return method + + @staticmethod + def make_test_result(constructor, fqname, expect): + """Make a function testing analyzer test result and analysis""" + @params(*expect) + def method(self, dct): + """Test analyzer test result and analysis""" + requirements = dct['requirements'] + parameters = dct['parameters'] + rows = dct['rows'] + result = dct['result'] + reason = dct['reason'] + timestamp = dct['timestamp'] + duration = dct['duration'] + analysis = dct['analysis'] + config = Config(None, requirements, parameters) + analyzer = constructor(config) + analyzer.collect(*rows) + self.assertEqual(analyzer.result, result) + self.assertEqual(analyzer.reason, reason) + self.assertEqual(analyzer.timestamp, timestamp) + self.assertEqual(analyzer.duration, duration) + self.assertEqual(analyzer.analysis, analysis) + with self.assertRaises(CollectionIsClosed): + analyzer.collect(*rows) + self.assertEqual(analyzer.result, result) + self.assertEqual(analyzer.reason, reason) + self.assertEqual(analyzer.timestamp, timestamp) + self.assertEqual(analyzer.duration, duration) + self.assertEqual(analyzer.analysis, analysis) + method.__doc__ = f'Test {fqname} analyzer test result and analysis' + return method diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_gnss.py b/postprocess/tests/vse_sync_pp/analyzers/test_gnss.py new file mode 100644 index 0000000..5979463 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_gnss.py @@ -0,0 +1,672 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers.gnss""" + +from unittest import TestCase +from collections import namedtuple +from decimal import Decimal +import math + +from vse_sync_pp.analyzers.gnss import ( + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer +) + +from .test_analyzer import AnalyzerTestBuilder + +TERR = namedtuple('TERR', ('timestamp', 'terror', 'state')) + + +class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.gnss.TimeErrorAnalyzer""" + constructor = TimeErrorAnalyzer + id_ = 'gnss/time-error' + parser = 'gnss/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + # state 1 causes failure + TERR(Decimal(1), 0, 1), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 10, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + # terror of 10 is unacceptable + TERR(Decimal(3), 10, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': False, + 'reason': "unacceptable time error", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 10, + 'range': 10, + 'mean': 2, + 'stddev': round(math.sqrt(20), 3), + 'variance': 20.0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(3), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + # oops, missing sample + TERR(Decimal(5), 0, 5), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 1), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.gnss.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'gnss/mtie' + parser = 'gnss/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + # state s1 causes failure + TERR(Decimal(1), 0, 1), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + # oops, missing sample + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 1), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.gnss.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'gnss/time-deviation' + parser = 'gnss/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + # state s1 causes failure + TERR(Decimal(1), 0, 1), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 5), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + # oops, missing sample + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + TERR(Decimal(21), 0, 5), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 5), + TERR(Decimal(2), 0, 5), + TERR(Decimal(3), 0, 5), + TERR(Decimal(4), 0, 5), + TERR(Decimal(5), 0, 5), + TERR(Decimal(6), 0, 5), + TERR(Decimal(7), 0, 5), + TERR(Decimal(8), 0, 5), + TERR(Decimal(9), 0, 5), + TERR(Decimal(10), 0, 5), + TERR(Decimal(11), 0, 5), + TERR(Decimal(12), 0, 5), + TERR(Decimal(13), 0, 5), + TERR(Decimal(14), 0, 5), + TERR(Decimal(15), 0, 5), + TERR(Decimal(16), 0, 5), + TERR(Decimal(17), 0, 5), + TERR(Decimal(18), 0, 5), + TERR(Decimal(19), 0, 5), + TERR(Decimal(20), 0, 5), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_phc2sys.py b/postprocess/tests/vse_sync_pp/analyzers/test_phc2sys.py new file mode 100644 index 0000000..c9ca2fb --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_phc2sys.py @@ -0,0 +1,683 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers.phc2sys""" + +from unittest import TestCase +from collections import namedtuple +from decimal import Decimal +import math + +from vse_sync_pp.analyzers.phc2sys import ( + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer +) + +from .test_analyzer import AnalyzerTestBuilder + +TERR = namedtuple('TERR', ('timestamp', 'terror', 'state', 'delay')) + + +class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.phc2sys.TimeErrorAnalyzer""" + constructor = TimeErrorAnalyzer + id_ = 'phc2sys/time-error' + parser = 'phc2sys/time-error' + expect = ( + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + # state s1 causes failure + TERR(Decimal(1), 0, 's1', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 10, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + # terror of 10 is unacceptable + TERR(Decimal(3), 10, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': False, + 'reason': "unacceptable time error", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 10, + 'range': 10, + 'mean': 2, + 'stddev': round(math.sqrt(20), 3), + 'variance': 20.0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(3), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + ), + 'result': False, + 'reason': "missing test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(5), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'phc2sys/mtie' + parser = 'phc2sys/time-error' + expect = ( + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + # state s1 causes failure + TERR(Decimal(1), 0, 's1', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'phc2sys/time-deviation' + parser = 'phc2sys/time-error' + expect = ( + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + # state s1 causes failure + TERR(Decimal(1), 0, 's1', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + # oops, missing sample + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + TERR(Decimal(21), 0, 's2', 620), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'workload/RAN', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1', 620), + TERR(Decimal(1), 0, 's2', 620), + TERR(Decimal(2), 0, 's2', 620), + TERR(Decimal(3), 0, 's2', 620), + TERR(Decimal(4), 0, 's2', 620), + TERR(Decimal(5), 0, 's2', 620), + TERR(Decimal(6), 0, 's2', 620), + TERR(Decimal(7), 0, 's2', 620), + TERR(Decimal(8), 0, 's2', 620), + TERR(Decimal(9), 0, 's2', 620), + TERR(Decimal(10), 0, 's2', 620), + TERR(Decimal(11), 0, 's2', 620), + TERR(Decimal(12), 0, 's2', 620), + TERR(Decimal(13), 0, 's2', 620), + TERR(Decimal(14), 0, 's2', 620), + TERR(Decimal(15), 0, 's2', 620), + TERR(Decimal(16), 0, 's2', 620), + TERR(Decimal(17), 0, 's2', 620), + TERR(Decimal(18), 0, 's2', 620), + TERR(Decimal(19), 0, 's2', 620), + TERR(Decimal(20), 0, 's2', 620), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_pmc.py b/postprocess/tests/vse_sync_pp/analyzers/test_pmc.py new file mode 100644 index 0000000..faf055b --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_pmc.py @@ -0,0 +1,2535 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers.pmc""" + +from unittest import TestCase +from collections import namedtuple +from decimal import Decimal + +from vse_sync_pp.analyzers.pmc import ClockStateAnalyzer + +from .test_analyzer import AnalyzerTestBuilder + +CLOCK_CLASS = namedtuple('CLOCK_CLASS', ('timestamp', 'clock_class', 'clockAccuracy', 'offsetScaledLogVariance')) + + +class TestClockStateAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.pmc.ClockStateAnalyzer""" + constructor = ClockStateAnalyzer + id_ = 'phc/gm-settings' + parser = 'phc/gm-settings' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal('0'), 248, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal('1'), 12, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "wrong clock class 12", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 7, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 140, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 1, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 150, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 1, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 160, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 1 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 140, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 1, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 150, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 1, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 160, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 1 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 7, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 140, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 150, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 160, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 140, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 7, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 150, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 7, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 160, '0xFE', '0xFFFF'), + # wrong state transition + CLOCK_CLASS(Decimal(1), 7, '0xFE', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal state transition", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 1 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 248, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 7, '0xFE', '0xFFFF'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 7, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 140, '0xFE', '0xFFFF'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 140, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 1, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 150, '0xFE', '0xFFFF'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 150, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 1, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 160, '0xFE', '0xFFFF'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 160, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 1 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong clock accuracy + CLOCK_CLASS(Decimal(1), 6, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal clock accuracy", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 1, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 6, '0x21', '0x4E5D'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 6, '0x21', '0xFFFF'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 1, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 1, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 7, '0xFE', '0xFFFF'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 7, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 1, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 140, '0xFE', '0xFFFF'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 140, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 1, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 150, '0xFE', '0xFFFF'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 150, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 1, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 160, '0xFE', '0xFFFF'), + # wrong offset scaled log variance + CLOCK_CLASS(Decimal(1), 160, '0xFE', '0x4E5D'), + ), + 'result': False, + 'reason': "illegal offset scaled log variance", + 'timestamp': Decimal(0), + 'duration': Decimal(1), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 0, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 1 + } + } + }, + "total_transitions": 0 + } + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'min-test-duration/s': 1, + }, + 'rows': ( + CLOCK_CLASS(Decimal(0), 248, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(1), 248, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(2), 6, '0x21', '0x4E5D'), + CLOCK_CLASS(Decimal(3), 7, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(4), 140, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(5), 6, '0x21', '0x4E5D'), + CLOCK_CLASS(Decimal(6), 7, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(7), 150, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(8), 6, '0x21', '0x4E5D'), + CLOCK_CLASS(Decimal(9), 7, '0xFE', '0xFFFF'), + CLOCK_CLASS(Decimal(10), 160, '0xFE', '0xFFFF'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(0), + 'duration': Decimal(10), + 'analysis': { + "clock_class_count": { + "FREERUN": { + "count": 1, + "transitions": { + "FREERUN": 1, + "LOCKED": 1, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "LOCKED": { + "count": 3, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 3, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_IN_SPEC": { + "count": 3, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 1, + "HOLDOVER_OUT_SPEC2": 1, + "HOLDOVER_OUT_SPEC3": 1 + } + }, + "HOLDOVER_OUT_SPEC1": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 1, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC2": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 1, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + }, + "HOLDOVER_OUT_SPEC3": { + "count": 1, + "transitions": { + "FREERUN": 0, + "LOCKED": 0, + "HOLDOVER_IN_SPEC": 0, + "HOLDOVER_OUT_SPEC1": 0, + "HOLDOVER_OUT_SPEC2": 0, + "HOLDOVER_OUT_SPEC3": 0 + } + } + }, + "total_transitions": 9 + } + }, + ) diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_ppsdpll.py b/postprocess/tests/vse_sync_pp/analyzers/test_ppsdpll.py new file mode 100644 index 0000000..59c3d80 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_ppsdpll.py @@ -0,0 +1,736 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers.ppsdpll""" + +from unittest import TestCase +from collections import namedtuple +from decimal import Decimal + +from vse_sync_pp.analyzers.ppsdpll import ( + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer +) + +from .test_analyzer import AnalyzerTestBuilder + + +DPLLS = namedtuple('DPLLS', ('timestamp', 'eecstate', 'state', 'terror',)) + + +class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ppsdpll.TimeErrorAnalyzer""" + constructor = TimeErrorAnalyzer + id_ = 'ppsdpll/time-error' + parser = 'dpll/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 3, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + # oops going into freerun + DPLLS(Decimal('1876879.28'), 3, 1, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(1), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 1.0, + 'max': 1.0, + 'range': 0.0, + 'mean': 1.0, + 'stddev': 0.0, + 'variance': 0.0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + # state 2 does not cause failure + DPLLS(Decimal('1876878.28'), 3, 2, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(1), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 1, + 'max': 1, + 'range': 0, + 'mean': 1, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 10, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876877.28'), 3, 3, Decimal(-40)), + # terror of -40 is unacceptable + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(-40)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(-39)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(-38)), + ), + 'result': False, + 'reason': "unacceptable time error", + 'timestamp': Decimal('1876878.28'), + 'duration': Decimal(2), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': -40.0, + 'max': -38.0, + 'range': 2.0, + 'mean': -39.0, + 'stddev': 1.0, + 'variance': 1.0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': ( + DPLLS(Decimal('1876877.28'), 3, 3, Decimal(37)), + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(37)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(38)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(39)), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal('1876878.28'), + 'duration': Decimal(2), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 37.0, + 'max': 39.0, + 'range': 2.0, + 'mean': 38.0, + 'stddev': 1.0, + 'variance': 1.0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + # oops, lost sample + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(3), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 1, + 'max': 1, + 'range': 0, + 'mean': 1, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1695047659.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047660.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047661.996251'), 3, 3, Decimal(1)), + DPLLS(Decimal('1695047662.996251'), 3, 3, Decimal(1)), + ), + 'result': True, + 'reason': None, + 'timestamp': '2023-09-18T14:34:20+00:00', + 'duration': Decimal(2), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 1, + 'max': 1, + 'range': 0, + 'mean': 1, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ppsdpll.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'ppsdpll/mtie' + parser = 'dpll/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 3, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + # oops going into freerun + DPLLS(Decimal('1876879.28'), 3, 1, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 10, + }, + 'rows': ( + # state 2 does not cause failure + DPLLS(Decimal('1876878.28'), 3, 2, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(0)), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 23, + }, + 'rows': ( + DPLLS(Decimal('1876877.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(0)), + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal('1876878.28'), + 'duration': Decimal(22), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(0)), + # oops, lost samples + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(0)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(0)), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + DPLLS(Decimal('1695047659.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047660.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047661.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047662.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047663.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047664.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047665.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047666.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047667.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047668.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047669.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047670.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047671.996251'), 3, 3, Decimal(0)), + DPLLS(Decimal('1695047672.996251'), 3, 3, Decimal(0)), + ), + 'result': True, + 'reason': None, + 'timestamp': '2023-09-18T14:34:20+00:00', + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ppsdpll.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'ppsdpll/time-deviation' + parser = 'dpll/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 3, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-B', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 3, + 'min-test-duration/s': 1, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + # state s1 causes failure + DPLLS(Decimal('1876879.28'), 3, 1, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + # oops, missing sample + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + DPLLS(Decimal('1876878.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876879.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876880.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876881.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876882.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876883.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876884.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876885.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876886.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876887.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876888.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876889.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876890.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876891.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876892.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876893.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876894.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876895.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876896.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876897.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876898.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876899.28'), 3, 3, Decimal(1)), + DPLLS(Decimal('1876900.28'), 3, 3, Decimal(1)), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal('1876879.28'), + 'duration': Decimal(21), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/postprocess/tests/vse_sync_pp/analyzers/test_ts2phc.py b/postprocess/tests/vse_sync_pp/analyzers/test_ts2phc.py new file mode 100644 index 0000000..cc4a8ef --- /dev/null +++ b/postprocess/tests/vse_sync_pp/analyzers/test_ts2phc.py @@ -0,0 +1,683 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.analyzers.ts2phc""" + +from unittest import TestCase +from collections import namedtuple +from decimal import Decimal +import math + +from vse_sync_pp.analyzers.ts2phc import ( + TimeErrorAnalyzer, + TimeDeviationAnalyzer, + MaxTimeIntervalErrorAnalyzer +) + +from .test_analyzer import AnalyzerTestBuilder + +TERR = namedtuple('TERR', ('timestamp', 'terror', 'state')) + + +class TestTimeErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.TimeErrorAnalyzer""" + constructor = TimeErrorAnalyzer + id_ = 'ts2phc/time-error' + parser = 'ts2phc/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + # state s1 causes failure + TERR(Decimal(1), 0, 's1'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 10, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + # terror of 10 is unacceptable + TERR(Decimal(3), 10, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': False, + 'reason': "unacceptable time error", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 10, + 'range': 10, + 'mean': 2, + 'stddev': round(math.sqrt(20), 3), + 'variance': 20.0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(3), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + ), + 'result': False, + 'reason': "missing test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(5), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 4, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(4), + 'analysis': { + 'terror': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestMaxTimeIntervalErrorAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.MaxTimeIntervalErrorAnalyzer""" + constructor = MaxTimeIntervalErrorAnalyzer + id_ = 'ts2phc/mtie' + parser = 'ts2phc/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + # state s1 causes failure + TERR(Decimal(1), 0, 's1'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 15, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 12, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(12), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'maximum-time-interval-error-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 11, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + # minimum to compute valid MTIE + 'duration': Decimal(11), + 'analysis': { + 'mtie': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) + + +class TestTimeDeviationAnalyzer(TestCase, metaclass=AnalyzerTestBuilder): + """Test cases for vse_sync_pp.analyzers.ts2phc.TimeDeviationAnalyzer""" + constructor = TimeDeviationAnalyzer + id_ = 'ts2phc/time-deviation' + parser = 'ts2phc/time-error' + expect = ( + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 1, + }, + 'rows': (), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 6, + 'min-test-duration/s': 1, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + ), + 'result': "error", + 'reason': "no data", + 'timestamp': None, + 'duration': None, + 'analysis': {}, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + # state s1 causes failure + TERR(Decimal(1), 0, 's1'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + ), + 'result': False, + 'reason': "loss of lock", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 25, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + # oops, window too short + ), + 'result': False, + 'reason': "short test duration", + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + 'min-test-duration/s': 20, + }, + 'rows': ( + TERR(Decimal(0), 0, 's2'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + # oops, missing sample + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + TERR(Decimal(21), 0, 's2'), + ), + 'result': False, + 'reason': "short test samples", + 'timestamp': Decimal(1), + 'duration': Decimal(20), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + { + 'requirements': 'G.8272/PRTC-A', + 'parameters': { + 'time-deviation-limit/%': 100, + 'transient-period/s': 1, + # minimum to compute valid TDEV + 'min-test-duration/s': 19, + }, + 'rows': ( + TERR(Decimal(0), 0, 's1'), + TERR(Decimal(1), 0, 's2'), + TERR(Decimal(2), 0, 's2'), + TERR(Decimal(3), 0, 's2'), + TERR(Decimal(4), 0, 's2'), + TERR(Decimal(5), 0, 's2'), + TERR(Decimal(6), 0, 's2'), + TERR(Decimal(7), 0, 's2'), + TERR(Decimal(8), 0, 's2'), + TERR(Decimal(9), 0, 's2'), + TERR(Decimal(10), 0, 's2'), + TERR(Decimal(11), 0, 's2'), + TERR(Decimal(12), 0, 's2'), + TERR(Decimal(13), 0, 's2'), + TERR(Decimal(14), 0, 's2'), + TERR(Decimal(15), 0, 's2'), + TERR(Decimal(16), 0, 's2'), + TERR(Decimal(17), 0, 's2'), + TERR(Decimal(18), 0, 's2'), + TERR(Decimal(19), 0, 's2'), + TERR(Decimal(20), 0, 's2'), + ), + 'result': True, + 'reason': None, + 'timestamp': Decimal(1), + 'duration': Decimal(19), + 'analysis': { + 'tdev': { + 'units': 'ns', + 'min': 0, + 'max': 0, + 'range': 0, + 'mean': 0, + 'stddev': 0, + 'variance': 0, + }, + }, + }, + ) diff --git a/postprocess/tests/vse_sync_pp/parsers/__init__.py b/postprocess/tests/vse_sync_pp/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postprocess/tests/vse_sync_pp/parsers/test_dpll.py b/postprocess/tests/vse_sync_pp/parsers/test_dpll.py new file mode 100644 index 0000000..16b25d2 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_dpll.py @@ -0,0 +1,50 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers.dpll""" + +from unittest import TestCase +from decimal import Decimal + +from vse_sync_pp.parsers.dpll import ( + TimeErrorParser, +) + +from .test_parser import ParserTestBuilder + + +class TestTimeErrorParser(TestCase, metaclass=ParserTestBuilder): + """Test cases for vse_sync_pp.parsers.dpll.TimeErrorParser""" + constructor = TimeErrorParser + id_ = 'dpll/time-error' + elems = ('timestamp', 'eecstate', 'state', 'terror') + accept = ( + ( + '1876878.28,3,3,-0.79,-3.21', + (Decimal('1876878.28'), 3, 3, Decimal('-0.79')), + ), + ( + '1876878.28,3,3,-0.79', + (Decimal('1876878.28'), 3, 3, Decimal('-0.79')), + ), + ) + reject = ( + 'foo bar baz', + '3,3,-0.79', + 'quux,3,3,-0.79', + '1876878.28,quux,3,-0.79', + '1876878.28,3,quux,-0.79', + '1876878.28,3,3,quux', + ) + discard = () + file = ( + '\n'.join(( + '1876878.28,3,3,-0.79', + '1876879.29,3,3,-1.05', + '1876879.29,3,3,-0.79,-3.21', + )), + ( + (Decimal('1876878.28'), 3, 3, Decimal('-0.79')), + (Decimal('1876879.29'), 3, 3, Decimal('-1.05')), + (Decimal('1876879.29'), 3, 3, Decimal('-0.79')), + ), + ) diff --git a/postprocess/tests/vse_sync_pp/parsers/test_gnss.py b/postprocess/tests/vse_sync_pp/parsers/test_gnss.py new file mode 100644 index 0000000..a2a4d42 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_gnss.py @@ -0,0 +1,56 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers.gnss""" + +from unittest import TestCase +from decimal import Decimal + +from vse_sync_pp.parsers.gnss import ( + TimeErrorParser, +) + +from .test_parser import ParserTestBuilder + + +class TestTimeErrorParser(TestCase, metaclass=ParserTestBuilder): + """Test cases for vse_sync_pp.parsers.gnss.TimeErrorParser""" + constructor = TimeErrorParser + id_ = 'gnss/time-error' + elems = ('timestamp', 'state', 'terror') + accept = ( + ('681011.839,5,-3', + (Decimal('681011.839'), 5, -3)), + ('2023-06-16T17:01:11.131Z,1,400', + (Decimal('1686934871.131'), 1, 400)), + ('2023-06-16T17:01:11.131282-00:00,2,399', + (Decimal('1686934871.131282'), 2, 399)), + ('2023-06-16T17:01:11.131282269+00:00,3,398', + (Decimal('1686934871.131282269'), 3, 398)), + ('681011.839,5,-3,-2.72', + (Decimal('681011.839'), 5, -3)), + ) + reject = ( + 'foo bar baz', + '1876878.28,3', + 'quux,3,3', + '1876878.28,quux,3', + '1876878.28,3,quux', + '2023-06-16T17:01Z,5,-3', + '2023-06-16T17:01:00Z,5,-3', + '2023-06-16T17:01:00.123+01:00,5,-3', + '2023-06-16T17:01:00,123+00:00,5,-3', + ) + discard = () + file = ( + '\n'.join(( + '847914.839,3,4', + '847915.839,5,-1', + '847915.839,5,-1,-1', + + )), + ( + (Decimal('847914.839'), 3, 4), + (Decimal('847915.839'), 5, -1), + (Decimal('847915.839'), 5, -1), + ), + ) diff --git a/postprocess/tests/vse_sync_pp/parsers/test_parser.py b/postprocess/tests/vse_sync_pp/parsers/test_parser.py new file mode 100644 index 0000000..e47a9c0 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_parser.py @@ -0,0 +1,196 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers""" + +import json +from io import StringIO + +from unittest import TestCase +from nose2.tools import params + +from vse_sync_pp.common import JsonEncoder +from vse_sync_pp.parsers.parser import Parser + +from .. import make_fqname + + +class TestParser(TestCase): + """Test cases for vse_sync_pp.parsers.parser.Parser""" + def test_make_parsed(self): + """Test vse_sync_pp.parsers.parser.Parser.make_parsed""" + with self.assertRaises(ValueError): + Parser().make_parsed(()) + + def test_parse_line(self): + """Test vse_sync_pp.parsers.parser.Parser.parse_line""" + self.assertIsNone(Parser().parse_line('foo bar baz')) + + +class ParserTestBuilder(type): + """Build tests for vse_sync_pp.parsers + + Specify this class as metaclass and provide: + `constructor` - a callable returning the parser to test + `id_` - the expected value of parser class attribute `id_` + `elems` - the expected value of parser class attribute `elems` + `accept` - a sequence of 2-tuples (line, expect) the parser must accept + `reject` - a sequence of lines the parser must reject with ValueError + `discard` - a sequence of lines the parser must discard + `file` - a 2-tuple (lines, expect) the parser must parse `expect` from + `lines` presented as a file object + """ + def __new__(cls, name, bases, dct): + constructor = dct['constructor'] + fqname = make_fqname(constructor) + dct.update({ + 'test_id': cls.make_test_id( + constructor, fqname, + dct['id_'], + ), + 'test_elems': cls.make_test_elems( + constructor, fqname, + dct['elems'], + ), + 'test_make_parsed': cls.make_test_make_parsed( + constructor, fqname, + dct['accept'][0][1], + ), + 'test_accept': cls.make_test_accept( + constructor, fqname, + dct['elems'], dct['accept'], + ), + 'test_reject': cls.make_test_reject( + constructor, fqname, + dct['reject'], + ), + 'test_discard': cls.make_test_discard( + constructor, fqname, + dct['discard'], + ), + 'test_file': cls.make_test_file( + constructor, fqname, + dct['file'][0], dct['file'][1], + ), + 'test_canonical': cls.make_test_canonical( + constructor, fqname, + dct['file'][1], + ), + }) + return super().__new__(cls, name, bases, dct) + + # make functions for use as TestCase methods + @staticmethod + def make_test_id(constructor, fqname, id_): + """Make a function testing id_ class attribute value""" + def method(self): + """Test parser id_ class attribute value""" + self.assertEqual(constructor.id_, id_) + method.__doc__ = f'Test {fqname} id_ class attribute value' + return method + + @staticmethod + def make_test_elems(constructor, fqname, elems): + """Make a function testing elems class attribute value""" + def method(self): + """Test parser elems class attribute value""" + self.assertEqual(constructor.elems, elems) + self.assertIn('timestamp', elems) + self.assertIn(constructor.y_name, elems) + method.__doc__ = f'Test {fqname} elems class attribute value' + return method + + @staticmethod + def make_test_make_parsed(constructor, fqname, expect): + """Make a function testing parser makes parsed""" + def method(self): + """Test parser makes parsed""" + parser = constructor() + self.assertEqual(parser.make_parsed(expect), expect) + with self.assertRaises(ValueError): + parser.make_parsed(expect[:-1]) + method.__doc__ = f'Test {fqname} make parsed' + return method + + @staticmethod + def make_test_accept(constructor, fqname, elems, accept): + """Make a function testing parser accepts line""" + @params(*accept) + def method(self, line, expect): + """Test parser accepts line""" + parser = constructor() + parsed = parser.parse_line(line) + # test parsed value as a tuple + self.assertEqual(expect, parsed) + # test parsed value as a namedtuple + for (idx, name) in enumerate(elems): + self.assertEqual(expect[idx], getattr(parsed, name)) + method.__doc__ = f'Test {fqname} accepts line' + return method + + @staticmethod + def make_test_reject(constructor, fqname, reject): + """Make a function testing parser rejects line""" + @params(*reject) + def method(self, line): + """Test parser rejects line""" + parser = constructor() + with self.assertRaises(ValueError): + parser.parse_line(line) + method.__doc__ = f'Test {fqname} rejects line' + return method + + @staticmethod + def make_test_discard(constructor, fqname, discard): + """Make a function testing parser discards line""" + @params(*discard) + def method(self, line): + """Test parser discards line""" + parser = constructor() + parsed = parser.parse_line(line) + self.assertIsNone(parsed) + method.__doc__ = f'Test {fqname} discards line' + return method + + @staticmethod + def make_test_file(constructor, fqname, lines, expect): + """Make a function testing parser parses `expect` from `lines`""" + def method(self): + """Test parser parses file""" + ### parse presented timestamps + parser = constructor() + parsed = parser.parse(StringIO(lines)) + for pair in zip(parsed, expect, strict=True): + self.assertEqual(pair[0], pair[1]) + ### parse relative timestamps + tidx = parser.elems.index('timestamp') + + def relative(expect): + """Generator yielding items with relative timestamps""" + tzero = None + for item in expect: + ritem = list(item) + if tzero is None: + tzero = item[tidx] + ritem[tidx] = item[tidx] - tzero + yield tuple(ritem) + parser = constructor() + parsed = parser.parse(StringIO(lines), relative=True) + for pair in zip(parsed, relative(expect), strict=True): + self.assertEqual(pair[0], pair[1]) + method.__doc__ = f'Test {fqname} parses file' + return method + + @staticmethod + def make_test_canonical(constructor, fqname, expect): + """Make a function testing parser parses `expect` from `expect`""" + def method(self): + """Test parser parses canonical""" + lines = '\n'.join(( + json.dumps(e, cls=JsonEncoder) for e in expect + )) + '\n' + parser = constructor() + parsed = parser.canonical(StringIO(lines)) + for pair in zip(parsed, expect, strict=True): + self.assertEqual(pair[0], pair[1]) + method.__doc__ = f'Test {fqname} parses canonical' + return method diff --git a/postprocess/tests/vse_sync_pp/parsers/test_phc2sys.py b/postprocess/tests/vse_sync_pp/parsers/test_phc2sys.py new file mode 100644 index 0000000..33ebc45 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_phc2sys.py @@ -0,0 +1,58 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers.phc2sys""" + +from unittest import TestCase +from decimal import Decimal + +from vse_sync_pp.parsers.phc2sys import ( + TimeErrorParser, +) + +from .test_parser import ParserTestBuilder + + +class TestTimeErrorParser(TestCase, metaclass=ParserTestBuilder): + """Test cases for vse_sync_pp.parsers.phc2sys.TimeErrorParser""" + constructor = TimeErrorParser + id_ = 'phc2sys/time-error' + elems = ('timestamp', 'terror', 'state', 'delay') + accept = ( + ('phc2sys[681011.839]: [ptp4l.0.config] ' + 'CLOCK_REALTIME phc offset 8 s2 freq +6339 delay 502', + (Decimal('681011.839'), 8, 's2', 502),), + ('phc2sys[847916.839]: ' + 'CLOCK_REALTIME phc offset 4 s2 freq +639 delay 102', + (Decimal('847916.839'), 4, 's2', 102),), + ('phc2sys[681011.839]: [ptp4l.0.config] ' + 'CLOCK_REALTIME phc offset 8 s2 freq -6339 delay 502', + (Decimal('681011.839'), 8, 's2', 502),), + ('phc2sys[681012.839]: [ptp4l.1.config] ' + 'CLOCK_REALTIME phc offset 8 s2 freq -6339 delay 502', + (Decimal('681012.839'), 8, 's2', 502),), + ('phc2sys[847916.839]: ' + 'CLOCK_REALTIME phc offset 4 s2 freq -639 delay 102', + (Decimal('847916.839'), 4, 's2', 102),), + ) + reject = () + discard = ( + 'foo bar baz', + ) + file = ( + '\n'.join(( + 'foo', + 'phc2sys[847914.839]: [ptp4l.0.config] ' + 'CLOCK_REALTIME phc offset 1 s2 freq +639 delay 102', + 'bar', + 'phc2sys[847915.839]: [ptp4l.0.config] ' + 'CLOCK_REALTIME phc offset 0 s2 freq +639 delay 102', + 'baz', + 'phc2sys[847916.839]: ' + 'CLOCK_REALTIME phc offset 4 s2 freq -639 delay 102', + )), + ( + (Decimal('847914.839'), 1, 's2', 102), + (Decimal('847915.839'), 0, 's2', 102), + (Decimal('847916.839'), 4, 's2', 102), + ), + ) diff --git a/postprocess/tests/vse_sync_pp/parsers/test_pmc.py b/postprocess/tests/vse_sync_pp/parsers/test_pmc.py new file mode 100644 index 0000000..6aed0af --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_pmc.py @@ -0,0 +1,58 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers.pmc""" + +from unittest import TestCase +from decimal import Decimal + +from vse_sync_pp.parsers.pmc import ( + ClockClassParser, +) + +from .test_parser import ParserTestBuilder + + +class TestClockClassParser(TestCase, metaclass=ParserTestBuilder): + """Test cases for vse_sync_pp.parsers.pmc.TestClockClassParser""" + constructor = ClockClassParser + id_ = 'phc/gm-settings' + elems = ('timestamp', 'clock_class', 'clockAccuracy', 'offsetScaledLogVariance') + accept = ( + #FreeRun class id: 248 + ('681011.839,248,0xFE,0xFFFF', + (Decimal('681011.839'), 248, '0xFE', '0xFFFF'),), + #Locked class id: 6 + ('2023-06-16T17:01:11.131Z,6,0x21,0x4E5D', + (Decimal('1686934871.131'), 6, '0x21', '0x4E5D'),), + #Holdover class ids: 7,140,150,160 + ('2023-06-16T17:01:11.131282-00:00,7,0xFE,0xFFFF', + (Decimal('1686934871.131282'), 7, '0xFE', '0xFFFF'),), + ('2023-06-16T17:01:11.131282269+00:00,140,0xFE,0xFFFF', + (Decimal('1686934871.131282269'), 140, '0xFE', '0xFFFF'),), + ('681011.839,150,0xFE,0xFFFF', + (Decimal('681011.839'), 150, '0xFE', '0xFFFF'),), + ('681011.839,160,0xFE,0xFFFF', + (Decimal('681011.839'), 160, '0xFE', '0xFFFF'),), + ) + reject = ( + 'foo bar baz quux corge', + 'quux,3,3,xy,xyz', + '1876878.28,quux,3,2,baz', + '2023-06-16T17:01Z,5,-3,foo,2', + '2023-06-16T17:01:00Z,5,-3,-1,-2', + '2023-06-16T17:01:00.123+01:00,5,-3,foo,bar', + '2023-06-16T17:01:00,123+00:00,5,-3,foo,bar', + ) + discard = () + file = ( + '\n'.join(( + '847914.839,248,0xFE,0xFFFF', + '847915.839,6,0x21,0x4E5D', + '847916.839,7,0xFE,0xFFFF', + )), + ( + (Decimal('847914.839'), 248, '0xFE', '0xFFFF'), + (Decimal('847915.839'), 6, '0x21', '0x4E5D'), + (Decimal('847916.839'), 7, '0xFE', '0xFFFF'), + ), + ) diff --git a/postprocess/tests/vse_sync_pp/parsers/test_ts2phc.py b/postprocess/tests/vse_sync_pp/parsers/test_ts2phc.py new file mode 100644 index 0000000..5fb50f7 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/parsers/test_ts2phc.py @@ -0,0 +1,49 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.parsers.ts2phc""" + +from unittest import TestCase +from decimal import Decimal + +from vse_sync_pp.parsers.ts2phc import ( + TimeErrorParser, +) + +from .test_parser import ParserTestBuilder + + +class TestTimeErrorParser(TestCase, metaclass=ParserTestBuilder): + """Test cases for vse_sync_pp.parsers.ts2phc.TimeErrorParser""" + constructor = TimeErrorParser + id_ = 'ts2phc/time-error' + elems = ('timestamp', 'interface', 'terror', 'state') + accept = ( + ('ts2phc[681011.839]: [ts2phc.0.config] ' + 'ens7f1 master offset 0 s2 freq -0', + (Decimal('681011.839'), 'ens7f1', 0, 's2'),), + ('ts2phc[681011.839]: [ts2phc.2.config] ' + 'ens7f1 master offset 0 s2 freq -0', + (Decimal('681011.839'), 'ens7f1', 0, 's2'),), + ('ts2phc[681011.839]: ' + 'ens7f1 master offset 0 s2 freq -0', + (Decimal('681011.839'), 'ens7f1', 0, 's2'),), + ) + reject = () + discard = ( + 'foo bar baz', + ) + file = ( + '\n'.join(( + 'foo', + 'ts2phc[847914.839]: [ts2phc.0.config] ' + 'ens7f1 master offset 1 s2 freq +1', + 'bar', + 'ts2phc[847915.839]: [ts2phc.0.config] ' + 'ens7f1 master offset 0 s2 freq -0', + 'baz', + )), + ( + (Decimal('847914.839'), 'ens7f1', 1, 's2'), + (Decimal('847915.839'), 'ens7f1', 0, 's2'), + ), + ) diff --git a/postprocess/tests/vse_sync_pp/test_common.py b/postprocess/tests/vse_sync_pp/test_common.py new file mode 100644 index 0000000..26319a3 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/test_common.py @@ -0,0 +1,25 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.common""" + +import json +from decimal import Decimal + +from unittest import TestCase + +from vse_sync_pp.common import JsonEncoder + + +class TestJsonEncoder(TestCase): + """Test cases for vse_sync_pp.common.JsonEncoder""" + def test_success(self): + """Test vse_sync_pp.common.JsonEncoder encodes Decimal""" + self.assertEqual( + json.dumps(Decimal('123.456'), cls=JsonEncoder), + '123.456', + ) + + def test_error(self): + """Test vse_sync_pp.common.JsonEncoder rejects instance""" + with self.assertRaises(TypeError): + json.dumps(self, cls=JsonEncoder) diff --git a/postprocess/tests/vse_sync_pp/test_muxed.py b/postprocess/tests/vse_sync_pp/test_muxed.py new file mode 100644 index 0000000..148e02f --- /dev/null +++ b/postprocess/tests/vse_sync_pp/test_muxed.py @@ -0,0 +1,96 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.source.muxed""" + +import json +from collections import namedtuple +from decimal import Decimal +from io import StringIO +from itertools import zip_longest +from unittest import TestCase + +from vse_sync_pp.parsers import PARSERS +from vse_sync_pp.source import muxed + +CaseValue = namedtuple("CaseValue", "input,expected") +NO_PARSER = CaseValue( + { + "id": "im-not-a-parser", + "data": ["I", "should", "not", "be"], + }, + None, +) +DPLL_LIST = CaseValue( + { + "id": "dpll/time-error", + "data": ["1876878.28", "3", "3", "-0.79"], + }, + ("dpll/time-error", (Decimal("1876878.28"), 3, 3, Decimal("-0.79"))), +) +DPLL_DICT = CaseValue( + { + "id": "dpll/time-error", + "data": { + "timestamp": "1876878.28", + "eecstate": "3", + "state": "3", + "terror": "-0.79", + }, + }, + ("dpll/time-error", (Decimal("1876878.28"), 3, 3, Decimal("-0.79"))), +) +GNSS_LIST = CaseValue( + { + "id": "gnss/time-error", + "data": ["681011.839", "5", "2", "-3"], + }, + ("gnss/time-error", (Decimal("681011.839"), 5, 2)), +) + +GNSS_DICT = CaseValue( + { + "id": "gnss/time-error", + "data": { + "timestamp": "681011.839", + "state": "5", + "terror": "2", + "ferror": "-3", + }, + }, + ("gnss/time-error", (Decimal("681011.839"), 5, 2)), +) + + +class TestMuxed(TestCase): + def _test(self, *cases): + file = StringIO("\n".join(json.dumps(c.input) for c in cases)) + + parsers = {} + for case in cases: + key = case.input["id"] + if key in PARSERS: + parsers[key] = PARSERS[key]() + + actual_values = muxed(file, parsers) + expected = [c.expected for c in cases if c.expected is not None] + + for actual, expected in zip_longest(actual_values, expected): + self.assertEqual(actual, expected) + + def test_filter(self): + """Check that lines with no parser are ignored""" + self._test(NO_PARSER, DPLL_LIST, GNSS_LIST) + + def test_tuple(self): + """Check that lines with json arrays are ingested properly""" + self._test(DPLL_LIST, GNSS_LIST) + + def test_dict(self): + """Check that lines with json objects are ingested properly""" + self._test(DPLL_DICT, GNSS_DICT) + + def test_mixed(self): + """Check that muxed can process a mixture of lines with json + arrays and json objects + """ + self._test(DPLL_DICT, GNSS_LIST) diff --git a/postprocess/tests/vse_sync_pp/test_requirements.py b/postprocess/tests/vse_sync_pp/test_requirements.py new file mode 100644 index 0000000..4071c54 --- /dev/null +++ b/postprocess/tests/vse_sync_pp/test_requirements.py @@ -0,0 +1,46 @@ +### SPDX-License-Identifier: GPL-2.0-or-later + +"""Test cases for vse_sync_pp.requirements""" + +### ensure values in REQUIREMENTS have to be changed in two places + +from unittest import TestCase + +from vse_sync_pp.requirements import REQUIREMENTS + + +class TestRequirements(TestCase): + """Test cases for vse_sync_pp.requirements.REQUIREMENTS""" + def test_g8272_prtc_a(self): + """Test G.8272/PRTC-A requirement values""" + + self.assertEqual(REQUIREMENTS['G.8272/PRTC-A']['time-error-in-locked-mode/ns'], 100) + + (interval1, func1), (interval2, func2) = REQUIREMENTS['G.8272/PRTC-A']['maximum-time-interval-error-in-locked-mode/ns'].items() # noqa + + self.assertEqual(func1(100), 52.5) + self.assertEqual(func2(300), 100) + + (interval1, func1), (interval2, func2), (interval3, func3) = REQUIREMENTS['G.8272/PRTC-A']['time-deviation-in-locked-mode/ns'].items() # noqa + self.assertEqual(func1(100), 3) + self.assertEqual(func2(150), 4.5) + self.assertEqual(func3(550), 30) + + def test_g8272_prtc_b(self): + """Test G.8272/PRTC-B requirement values""" + + self.assertEqual(REQUIREMENTS['G.8272/PRTC-B']['time-error-in-locked-mode/ns'], 40) + + (interval1, func1), (interval2, func2) = REQUIREMENTS['G.8272/PRTC-B']['maximum-time-interval-error-in-locked-mode/ns'].items() # noqa + + self.assertEqual(func1(100), 52.5) + self.assertEqual(func2(300), 40) + + (interval1, func1), (interval2, func2), (interval3, func3) = REQUIREMENTS['G.8272/PRTC-B']['time-deviation-in-locked-mode/ns'].items() # noqa + self.assertEqual(func1(100), 1) + self.assertEqual(func2(150), 1.5) + self.assertEqual(func3(550), 5) + + def test_workload_RAN(self): + """Test workload/RAN requirement values""" + self.assertEqual(REQUIREMENTS['workload/RAN']['time-error-in-locked-mode/ns'], 100) diff --git a/src/vse_sync_pp b/src/vse_sync_pp deleted file mode 120000 index d850ddb..0000000 --- a/src/vse_sync_pp +++ /dev/null @@ -1 +0,0 @@ -../vse-sync-pp/src/vse_sync_pp/ \ No newline at end of file diff --git a/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/plot.py b/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/plot.py index f23a031..2739ff8 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/plot.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/plot.py @@ -20,6 +20,7 @@ from vse_sync_pp.parsers.dpll import TimeErrorParser from vse_sync_pp.plot import Plotter, Axis, TIMESERIES + def main(): """Plot test data and print files output as JSON to stdout @@ -46,5 +47,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/testimpl.py b/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/testimpl.py index 3f9dd28..7ed5fd4 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/testimpl.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/1PPS-to-DPLL/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -46,6 +47,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -56,5 +58,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/plot.py b/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/plot.py index 9af698a..d533966 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/plot.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/plot.py @@ -20,6 +20,7 @@ from vse_sync_pp.parsers.gnss import TimeErrorParser from vse_sync_pp.plot import Plotter, Axis, TIMESERIES + def main(): """Plot test data and print files output as JSON to stdout @@ -46,5 +47,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py b/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py index 5f32837..6c0fd58 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -46,6 +47,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -56,5 +58,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/plot.py b/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/plot.py index a84ac0f..4f21cb3 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/plot.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/plot.py @@ -20,6 +20,7 @@ from vse_sync_pp.parsers.ts2phc import TimeErrorParser from vse_sync_pp.plot import Plotter, Axis, TIMESERIES + def main(): """Plot test data and print files output as JSON to stdout @@ -46,5 +47,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/testimpl.py b/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/testimpl.py index 7d8b9ad..c1d70e6 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/testimpl.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/DPLL-to-PHC/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -54,5 +56,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/plot.py b/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/plot.py index 3cd4fda..0d2f5a8 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/plot.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/plot.py @@ -20,6 +20,7 @@ from vse_sync_pp.parsers.phc2sys import TimeErrorParser from vse_sync_pp.plot import Plotter, Axis, TIMESERIES + def main(): """Plot test data and print files output as JSON to stdout @@ -46,5 +47,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/testimpl.py b/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/testimpl.py index 0051269..f3578eb 100755 --- a/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/testimpl.py +++ b/tests/sync/G.8272/time-error-in-locked-mode/PHC-to-SYS/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -54,5 +56,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/plot.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/plot.py index a19b30f..3ac5ac0 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/plot.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.dpll import TimeErrorParser from vse_sync_pp.analyzers.ppsdpll import MaxTimeIntervalErrorAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered MTIE (ns)", "mtie")) - Parsed = namedtuple('Parsed',('tau','mtie')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'mtie')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -65,5 +67,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/testimpl.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/testimpl.py index 3206bc6..7d783ff 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/testimpl.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/1PPS-to-DPLL/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, config, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, config, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -55,5 +57,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/plot.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/plot.py index f621271..b4258b0 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/plot.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.gnss import TimeErrorParser from vse_sync_pp.analyzers.gnss import MaxTimeIntervalErrorAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered MTIE (ns)", "mtie")) - Parsed = namedtuple('Parsed',('tau','mtie')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'mtie')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -65,5 +67,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py index f1501dc..efeda59 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, config, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, config, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -55,5 +57,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/plot.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/plot.py index 6c077a0..2f58de7 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/plot.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.ts2phc import TimeErrorParser from vse_sync_pp.analyzers.ts2phc import MaxTimeIntervalErrorAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered MTIE (ns)", "tdev")) - Parsed = namedtuple('Parsed',('tau','tdev')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'tdev')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -53,7 +55,7 @@ def main(): analyzer = MaxTimeIntervalErrorAnalyzer(Config.from_yaml(CONFIG)) with open_input(args.input) as fid: analyzer.collect(*parser.parse(fid)) - + # plot data output = f'{args.prefix}.png' plot_data(analyzer, output) @@ -65,5 +67,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/testimpl.py b/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/testimpl.py index c9c4d91..b6dc59b 100755 --- a/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/testimpl.py +++ b/tests/sync/G.8272/wander-MTIE-in-locked-mode/DPLL-to-PHC/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, config, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, config, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -55,5 +57,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/plot.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/plot.py index fe35744..7b94cbd 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/plot.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.dpll import TimeErrorParser from vse_sync_pp.analyzers.ppsdpll import TimeDeviationAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered TDEV (ns)", "tdev")) - Parsed = namedtuple('Parsed',('tau','tdev')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'tdev')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -63,5 +65,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/testimpl.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/testimpl.py index 6509a7f..5384d1a 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/testimpl.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/1PPS-to-DPLL/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -46,6 +47,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -56,5 +58,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/plot.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/plot.py index 60677d6..b22531b 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/plot.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.gnss import TimeErrorParser from vse_sync_pp.analyzers.gnss import TimeDeviationAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered TDEV (ns)", "tdev")) - Parsed = namedtuple('Parsed',('tau','tdev')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'tdev')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -53,7 +55,7 @@ def main(): analyzer = TimeDeviationAnalyzer(Config.from_yaml(CONFIG)) with open_input(args.input) as fid: analyzer.collect(*parser.canonical(fid)) - + # plot data output = f'{args.prefix}.png' plot_data(analyzer, output) @@ -65,5 +67,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py index 4846d63..b2a1dbd 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/Constellation-to-GNSS-receiver/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, encoding='utf-8'): """A reference implementation for tests under: @@ -46,6 +47,7 @@ def refimpl(filename, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -56,5 +58,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/plot.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/plot.py index d539637..d9154a2 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/plot.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/plot.py @@ -20,7 +20,7 @@ ) from collections import namedtuple -from vse_sync_pp.plot import Plotter, Axis, TIMESERIES +from vse_sync_pp.plot import Plotter, Axis from vse_sync_pp.parsers.ts2phc import TimeErrorParser from vse_sync_pp.analyzers.ts2phc import TimeDeviationAnalyzer @@ -28,14 +28,16 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def plot_data(analyzer, output): """Plot data""" plotter = Plotter(Axis("tau observation window (s)", "tau", "log"), Axis("filtered TDEV (ns)", "tdev")) - Parsed = namedtuple('Parsed',('tau','tdev')) - for tau,sample in analyzer.toplot(): - plotter.append(Parsed(tau, sample)) + Parsed = namedtuple('Parsed', ('tau', 'tdev')) + for tau, sample in analyzer.toplot(): + plotter.append(Parsed(tau, sample)) plotter.plot_scatter(output) + def main(): """Plot test data and print files output as JSON to stdout @@ -53,7 +55,7 @@ def main(): analyzer = TimeDeviationAnalyzer(Config.from_yaml(CONFIG)) with open_input(args.input) as fid: analyzer.collect(*parser.parse(fid)) - + # plot data output = f'{args.prefix}.png' plot_data(analyzer, output) @@ -65,5 +67,6 @@ def main(): if not print_loj([item]): sys.exit(1) + if __name__ == '__main__': main() diff --git a/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/testimpl.py b/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/testimpl.py index 46c5c5f..95df1c1 100755 --- a/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/testimpl.py +++ b/tests/sync/G.8272/wander-TDEV-in-locked-mode/DPLL-to-PHC/testimpl.py @@ -25,6 +25,7 @@ CONFIG = joinpath(dirname(__file__), 'config.yaml') + def refimpl(filename, config, encoding='utf-8'): """A reference implementation for tests under: @@ -44,6 +45,7 @@ def refimpl(filename, config, encoding='utf-8'): 'analysis': analyzer.analysis, } + def main(): """Run this test and print test output as JSON to stdout""" aparser = ArgumentParser(description=main.__doc__) @@ -55,5 +57,6 @@ def main(): if not print_loj(output): sys.exit(1) + if __name__ == '__main__': main() diff --git a/vse-sync-pp b/vse-sync-pp deleted file mode 160000 index 73b94e3..0000000 --- a/vse-sync-pp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73b94e3809f2fbab189ba507a7f879b220997493