├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── data ├── 20240221T141700Z_sv_300_2112_10_one_moving_object.osi └── 20240618T122540Z_sv_370_244_20_minimal_valid_example.osi ├── doc ├── description.adoc ├── howtocontribute.adoc ├── setup.adoc ├── usage.adoc └── writing-rules.adoc ├── osivalidator ├── __init__.py ├── __main__.py ├── linked_proto_field.py ├── osi_general_validator.py ├── osi_id_manager.py ├── osi_rules.py ├── osi_rules_checker.py ├── osi_rules_implementations.py └── osi_validator_logger.py ├── requirements.txt ├── requirements_develop.txt ├── rules2yml.py ├── setup.py └── tests ├── __init__.py ├── test_osi_general_validator.py ├── test_validation_rules.py └── tests_rules ├── __init__.py ├── test_check_if.py ├── test_first_element_of.py ├── test_is_different_to.py ├── test_is_equal_to.py ├── test_is_globally_unique.py ├── test_is_greater_than.py ├── test_is_greater_than_or_equal_to.py ├── test_is_iso_country_code.py ├── test_is_less_than.py ├── test_is_less_than_or_equal_to.py ├── test_is_optional.py ├── test_is_set.py ├── test_last_element_of.py └── test_refers_to.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | #### Describe how to reproduce the bug 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | #### Describe the expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | #### Show some screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | #### Describe the OS you are using 27 | - OS: [e.g. iOS, Ubuntu 18.04, Windows 10, ...] 28 | - Language: [e.g. C++, Python, ...] 29 | - Version [e.g. 3.6.8] 30 | 31 | #### Additional context 32 | Add any other context about the bug here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the feature 11 | Is your feature request related to a problem? 12 | A clear and concise description of what the problem is. 13 | Example: I am always frustrated when [...]. 14 | 15 | #### Describe the solution you would like 16 | A clear and concise description of what you want to happen. 17 | 18 | #### Describe alternatives you have considered 19 | A clear and concise description of any alternative solutions or features you have considered. 20 | 21 | #### Describe the backwards compatibility 22 | How does the feature impact the backwards compatibility of the current major version of OSI-Validation? 23 | If the suggested feature would be implemented would there be an issue with the previous versions of OSI-Validation? What part of OSI would break or improve by this feature? 24 | 25 | #### Additional context 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question if you do not understand something or just want clarifications. 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the problem 11 | I want to do ... in my application, but what I am actually getting is.... 12 | Here is a screenshot that illustrates the problem: 13 | 14 | `image.png` 15 | 16 | #### Describe what you have already tried 17 | This is the most simplified version of my code that I have been able to get, which still produces the problem I described above. 18 | 19 | // my code 20 | ... 21 | 22 | Basically, what the code is doing is.... 23 | Changing ... does not work because it gives the following error: 24 | 25 | 26 | Some error 27 | 28 | 29 | #### Describe your research 30 | [The documentation here] mentioned ... but did not provide a clear example of how that is done. 31 | [This Stack Overflow question] describes a similar problem, but mine is different because.... 32 | 33 | #### Ask your question 34 | So my basic question is, how do I...? 35 | 36 | #### Additional context 37 | Add any other context or screenshots about the question here. 38 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Reference to a related issue in the repository 2 | Add a reference to a related issue in the repository. 3 | 4 | #### Add a description 5 | Add a description of the changes proposed in the pull request. 6 | 7 | **Some questions to ask**: 8 | What is this change? 9 | What does it fix? 10 | Is this a bug fix or a feature? Does it break any existing functionality or force me to update to a new version? 11 | How has it been tested? 12 | 13 | #### Mention a member 14 | Add @mentions of the person or team responsible for reviewing proposed changes. 15 | 16 | #### Check the checklist 17 | 18 | - [ ] My code and comments follow the [contributors guidelines](https://opensimulationinterface.github.io/osi-documentation/osi/howtocontribute.html) of this project. 19 | - [ ] I have performed a self-review of my own code. 20 | - [ ] I have made corresponding changes to the [documentation](https://github.com/OpenSimulationInterface/osi-documentation) for osi-validation. 21 | - [ ] My changes generate no new warnings. 22 | - [ ] I have added tests that prove my fix is effective or that my feature works. 23 | - [ ] New and existing unit tests / travis ci pass locally with my changes. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | linter: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 3.8 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.8" 24 | 25 | - name: Upgrade pip 26 | run: python -m pip install --upgrade pip 27 | 28 | - name: Install dependencies 29 | run: pip install -r requirements_develop.txt 30 | 31 | - name: Check black format 32 | run: black --check --diff --exclude "(open-simulation-interface|proto2cpp|.venv)" . 33 | 34 | - name: Check dead code with vulture 35 | run: vulture *.py tests/ osivalidator/ --min-confidence 100 36 | 37 | build-validator: 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | runs-on: [ubuntu-latest] 42 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 43 | 44 | name: "🐍 ${{ matrix.python-version }} • ${{ matrix.runs-on }}" 45 | runs-on: ${{ matrix.runs-on }} 46 | 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | with: 51 | submodules: recursive 52 | lfs: true 53 | 54 | - name: Set up Python ${{ matrix.python-version }} 55 | uses: actions/setup-python@v5 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | 59 | - name: Set up Virtual Environment 60 | run: | 61 | python -m venv .venv 62 | source .venv/bin/activate 63 | python -m pip install --upgrade pip 64 | pip install -r requirements_develop.txt 65 | 66 | - name: Generate parsed rules 67 | run: | 68 | source .venv/bin/activate 69 | pip install -r requirements.txt 70 | python rules2yml.py -d rules 71 | 72 | - name: Check rule correctness with unittests 73 | run: | 74 | source .venv/bin/activate 75 | python -m unittest discover tests 76 | 77 | - name: Run osi-validator 78 | run: | 79 | source .venv/bin/activate 80 | pip install . 81 | osivalidator --data data/20240618T122540Z_sv_370_244_20_minimal_valid_example.osi -r rules 82 | osivalidator --data data/20240618T122540Z_sv_370_244_20_minimal_valid_example.osi -r rules --parallel 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | .idea/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | .venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | vpython/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # Visual Studio Code 110 | .vscode 111 | 112 | # Madeup extension for OSI byteencoded string 113 | *.osibytes 114 | *.sosibytes 115 | .DS_Store 116 | 117 | output_logs/ 118 | tests/overtake_right_straight_SensorView.txt 119 | 120 | # OSI Data Messages 121 | osi_message_data/ 122 | 123 | # OSI Rules 124 | rules/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "open-simulation-interface"] 2 | path = open-simulation-interface 3 | url = https://github.com/OpenSimulationInterface/open-simulation-interface.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | OSI Validation 2 | =============================== 3 | 4 | Copyright (C) 2024 BMW AG 5 | 6 | This Source Code Form is subject to the terms of the Mozilla Public 7 | License, v. 2.0. If a copy of the MPL was not distributed with this 8 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | 10 | Mozilla Public License Version 2.0 11 | ================================== 12 | 13 | 1. Definitions 14 | -------------- 15 | 16 | 1.1. "Contributor" 17 | means each individual or legal entity that creates, contributes to 18 | the creation of, or owns Covered Software. 19 | 20 | 1.2. "Contributor Version" 21 | means the combination of the Contributions of others (if any) used 22 | by a Contributor and that particular Contributor's Contribution. 23 | 24 | 1.3. "Contribution" 25 | means Covered Software of a particular Contributor. 26 | 27 | 1.4. "Covered Software" 28 | means Source Code Form to which the initial Contributor has attached 29 | the notice in Exhibit A, the Executable Form of such Source Code 30 | Form, and Modifications of such Source Code Form, in each case 31 | including portions thereof. 32 | 33 | 1.5. "Incompatible With Secondary Licenses" 34 | means 35 | 36 | (a) that the initial Contributor has attached the notice described 37 | in Exhibit B to the Covered Software; or 38 | 39 | (b) that the Covered Software was made available under the terms of 40 | version 1.1 or earlier of the License, but not also under the 41 | terms of a Secondary License. 42 | 43 | 1.6. "Executable Form" 44 | means any form of the work other than Source Code Form. 45 | 46 | 1.7. "Larger Work" 47 | means a work that combines Covered Software with other material, in 48 | a separate file or files, that is not Covered Software. 49 | 50 | 1.8. "License" 51 | means this document. 52 | 53 | 1.9. "Licensable" 54 | means having the right to grant, to the maximum extent possible, 55 | whether at the time of the initial grant or subsequently, any and 56 | all of the rights conveyed by this License. 57 | 58 | 1.10. "Modifications" 59 | means any of the following: 60 | 61 | (a) any file in Source Code Form that results from an addition to, 62 | deletion from, or modification of the contents of Covered 63 | Software; or 64 | 65 | (b) any new file in Source Code Form that contains any Covered 66 | Software. 67 | 68 | 1.11. "Patent Claims" of a Contributor 69 | means any patent claim(s), including without limitation, method, 70 | process, and apparatus claims, in any patent Licensable by such 71 | Contributor that would be infringed, but for the grant of the 72 | License, by the making, using, selling, offering for sale, having 73 | made, import, or transfer of either its Contributions or its 74 | Contributor Version. 75 | 76 | 1.12. "Secondary License" 77 | means either the GNU General Public License, Version 2.0, the GNU 78 | Lesser General Public License, Version 2.1, the GNU Affero General 79 | Public License, Version 3.0, or any later versions of those 80 | licenses. 81 | 82 | 1.13. "Source Code Form" 83 | means the form of the work preferred for making modifications. 84 | 85 | 1.14. "You" (or "Your") 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, "You" includes any entity that 88 | controls, is controlled by, or is under common control with You. For 89 | purposes of this definition, "control" means (a) the power, direct 90 | or indirect, to cause the direction or management of such entity, 91 | whether by contract or otherwise, or (b) ownership of more than 92 | fifty percent (50%) of the outstanding shares or beneficial 93 | ownership of such entity. 94 | 95 | 2. License Grants and Conditions 96 | -------------------------------- 97 | 98 | 2.1. Grants 99 | 100 | Each Contributor hereby grants You a world-wide, royalty-free, 101 | non-exclusive license: 102 | 103 | (a) under intellectual property rights (other than patent or trademark) 104 | Licensable by such Contributor to use, reproduce, make available, 105 | modify, display, perform, distribute, and otherwise exploit its 106 | Contributions, either on an unmodified basis, with Modifications, or 107 | as part of a Larger Work; and 108 | 109 | (b) under Patent Claims of such Contributor to make, use, sell, offer 110 | for sale, have made, import, and otherwise transfer either its 111 | Contributions or its Contributor Version. 112 | 113 | 2.2. Effective Date 114 | 115 | The licenses granted in Section 2.1 with respect to any Contribution 116 | become effective for each Contribution on the date the Contributor first 117 | distributes such Contribution. 118 | 119 | 2.3. Limitations on Grant Scope 120 | 121 | The licenses granted in this Section 2 are the only rights granted under 122 | this License. No additional rights or licenses will be implied from the 123 | distribution or licensing of Covered Software under this License. 124 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 125 | Contributor: 126 | 127 | (a) for any code that a Contributor has removed from Covered Software; 128 | or 129 | 130 | (b) for infringements caused by: (i) Your and any other third party's 131 | modifications of Covered Software, or (ii) the combination of its 132 | Contributions with other software (except as part of its Contributor 133 | Version); or 134 | 135 | (c) under Patent Claims infringed by Covered Software in the absence of 136 | its Contributions. 137 | 138 | This License does not grant any rights in the trademarks, service marks, 139 | or logos of any Contributor (except as may be necessary to comply with 140 | the notice requirements in Section 3.4). 141 | 142 | 2.4. Subsequent Licenses 143 | 144 | No Contributor makes additional grants as a result of Your choice to 145 | distribute the Covered Software under a subsequent version of this 146 | License (see Section 10.2) or under the terms of a Secondary License (if 147 | permitted under the terms of Section 3.3). 148 | 149 | 2.5. Representation 150 | 151 | Each Contributor represents that the Contributor believes its 152 | Contributions are its original creation(s) or it has sufficient rights 153 | to grant the rights to its Contributions conveyed by this License. 154 | 155 | 2.6. Fair Use 156 | 157 | This License is not intended to limit any rights You have under 158 | applicable copyright doctrines of fair use, fair dealing, or other 159 | equivalents. 160 | 161 | 2.7. Conditions 162 | 163 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 164 | in Section 2.1. 165 | 166 | 3. Responsibilities 167 | ------------------- 168 | 169 | 3.1. Distribution of Source Form 170 | 171 | All distribution of Covered Software in Source Code Form, including any 172 | Modifications that You create or to which You contribute, must be under 173 | the terms of this License. You must inform recipients that the Source 174 | Code Form of the Covered Software is governed by the terms of this 175 | License, and how they can obtain a copy of this License. You may not 176 | attempt to alter or restrict the recipients' rights in the Source Code 177 | Form. 178 | 179 | 3.2. Distribution of Executable Form 180 | 181 | If You distribute Covered Software in Executable Form then: 182 | 183 | (a) such Covered Software must also be made available in Source Code 184 | Form, as described in Section 3.1, and You must inform recipients of 185 | the Executable Form how they can obtain a copy of such Source Code 186 | Form by reasonable means in a timely manner, at a charge no more 187 | than the cost of distribution to the recipient; and 188 | 189 | (b) You may distribute such Executable Form under the terms of this 190 | License, or sublicense it under different terms, provided that the 191 | license for the Executable Form does not attempt to limit or alter 192 | the recipients' rights in the Source Code Form under this License. 193 | 194 | 3.3. Distribution of a Larger Work 195 | 196 | You may create and distribute a Larger Work under terms of Your choice, 197 | provided that You also comply with the requirements of this License for 198 | the Covered Software. If the Larger Work is a combination of Covered 199 | Software with a work governed by one or more Secondary Licenses, and the 200 | Covered Software is not Incompatible With Secondary Licenses, this 201 | License permits You to additionally distribute such Covered Software 202 | under the terms of such Secondary License(s), so that the recipient of 203 | the Larger Work may, at their option, further distribute the Covered 204 | Software under the terms of either this License or such Secondary 205 | License(s). 206 | 207 | 3.4. Notices 208 | 209 | You may not remove or alter the substance of any license notices 210 | (including copyright notices, patent notices, disclaimers of warranty, 211 | or limitations of liability) contained within the Source Code Form of 212 | the Covered Software, except that You may alter any license notices to 213 | the extent required to remedy known factual inaccuracies. 214 | 215 | 3.5. Application of Additional Terms 216 | 217 | You may choose to offer, and to charge a fee for, warranty, support, 218 | indemnity or liability obligations to one or more recipients of Covered 219 | Software. However, You may do so only on Your own behalf, and not on 220 | behalf of any Contributor. You must make it absolutely clear that any 221 | such warranty, support, indemnity, or liability obligation is offered by 222 | You alone, and You hereby agree to indemnify every Contributor for any 223 | liability incurred by such Contributor as a result of warranty, support, 224 | indemnity or liability terms You offer. You may include additional 225 | disclaimers of warranty and limitations of liability specific to any 226 | jurisdiction. 227 | 228 | 4. Inability to Comply Due to Statute or Regulation 229 | --------------------------------------------------- 230 | 231 | If it is impossible for You to comply with any of the terms of this 232 | License with respect to some or all of the Covered Software due to 233 | statute, judicial order, or regulation then You must: (a) comply with 234 | the terms of this License to the maximum extent possible; and (b) 235 | describe the limitations and the code they affect. Such description must 236 | be placed in a text file included with all distributions of the Covered 237 | Software under this License. Except to the extent prohibited by statute 238 | or regulation, such description must be sufficiently detailed for a 239 | recipient of ordinary skill to be able to understand it. 240 | 241 | 5. Termination 242 | -------------- 243 | 244 | 5.1. The rights granted under this License will terminate automatically 245 | if You fail to comply with any of its terms. However, if You become 246 | compliant, then the rights granted under this License from a particular 247 | Contributor are reinstated (a) provisionally, unless and until such 248 | Contributor explicitly and finally terminates Your grants, and (b) on an 249 | ongoing basis, if such Contributor fails to notify You of the 250 | non-compliance by some reasonable means prior to 60 days after You have 251 | come back into compliance. Moreover, Your grants from a particular 252 | Contributor are reinstated on an ongoing basis if such Contributor 253 | notifies You of the non-compliance by some reasonable means, this is the 254 | first time You have received notice of non-compliance with this License 255 | from such Contributor, and You become compliant prior to 30 days after 256 | Your receipt of the notice. 257 | 258 | 5.2. If You initiate litigation against any entity by asserting a patent 259 | infringement claim (excluding declaratory judgment actions, 260 | counter-claims, and cross-claims) alleging that a Contributor Version 261 | directly or indirectly infringes any patent, then the rights granted to 262 | You by any and all Contributors for the Covered Software under Section 263 | 2.1 of this License shall terminate. 264 | 265 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 266 | end user license agreements (excluding distributors and resellers) which 267 | have been validly granted by You or Your distributors under this License 268 | prior to termination shall survive termination. 269 | 270 | ************************************************************************ 271 | * * 272 | * 6. Disclaimer of Warranty * 273 | * ------------------------- * 274 | * * 275 | * Covered Software is provided under this License on an "as is" * 276 | * basis, without warranty of any kind, either expressed, implied, or * 277 | * statutory, including, without limitation, warranties that the * 278 | * Covered Software is free of defects, merchantable, fit for a * 279 | * particular purpose or non-infringing. The entire risk as to the * 280 | * quality and performance of the Covered Software is with You. * 281 | * Should any Covered Software prove defective in any respect, You * 282 | * (not any Contributor) assume the cost of any necessary servicing, * 283 | * repair, or correction. This disclaimer of warranty constitutes an * 284 | * essential part of this License. No use of any Covered Software is * 285 | * authorized under this License except under this disclaimer. * 286 | * * 287 | ************************************************************************ 288 | 289 | ************************************************************************ 290 | * * 291 | * 7. Limitation of Liability * 292 | * -------------------------- * 293 | * * 294 | * Under no circumstances and under no legal theory, whether tort * 295 | * (including negligence), contract, or otherwise, shall any * 296 | * Contributor, or anyone who distributes Covered Software as * 297 | * permitted above, be liable to You for any direct, indirect, * 298 | * special, incidental, or consequential damages of any character * 299 | * including, without limitation, damages for lost profits, loss of * 300 | * goodwill, work stoppage, computer failure or malfunction, or any * 301 | * and all other commercial damages or losses, even if such party * 302 | * shall have been informed of the possibility of such damages. This * 303 | * limitation of liability shall not apply to liability for death or * 304 | * personal injury resulting from such party's negligence to the * 305 | * extent applicable law prohibits such limitation. Some * 306 | * jurisdictions do not allow the exclusion or limitation of * 307 | * incidental or consequential damages, so this exclusion and * 308 | * limitation may not apply to You. * 309 | * * 310 | ************************************************************************ 311 | 312 | 8. Litigation 313 | ------------- 314 | 315 | Any litigation relating to this License may be brought only in the 316 | courts of a jurisdiction where the defendant maintains its principal 317 | place of business and such litigation shall be governed by laws of that 318 | jurisdiction, without reference to its conflict-of-law provisions. 319 | Nothing in this Section shall prevent a party's ability to bring 320 | cross-claims or counter-claims. 321 | 322 | 9. Miscellaneous 323 | ---------------- 324 | 325 | This License represents the complete agreement concerning the subject 326 | matter hereof. If any provision of this License is held to be 327 | unenforceable, such provision shall be reformed only to the extent 328 | necessary to make it enforceable. Any law or regulation which provides 329 | that the language of a contract shall be construed against the drafter 330 | shall not be used to construe this License against a Contributor. 331 | 332 | 10. Versions of the License 333 | --------------------------- 334 | 335 | 10.1. New Versions 336 | 337 | Mozilla Foundation is the license steward. Except as provided in Section 338 | 10.3, no one other than the license steward has the right to modify or 339 | publish new versions of this License. Each version will be given a 340 | distinguishing version number. 341 | 342 | 10.2. Effect of New Versions 343 | 344 | You may distribute the Covered Software under the terms of the version 345 | of the License under which You originally received the Covered Software, 346 | or under the terms of any subsequent version published by the license 347 | steward. 348 | 349 | 10.3. Modified Versions 350 | 351 | If you create software not governed by this License, and you want to 352 | create a new license for such software, you may create and use a 353 | modified version of this License if you rename the license and remove 354 | any references to the name of the license steward (except to note that 355 | such modified license differs from this License). 356 | 357 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 358 | Licenses 359 | 360 | If You choose to distribute Source Code Form that is Incompatible With 361 | Secondary Licenses under the terms of this version of the License, the 362 | notice described in Exhibit B of this License must be attached. 363 | 364 | Exhibit A - Source Code Form License Notice 365 | ------------------------------------------- 366 | 367 | This Source Code Form is subject to the terms of the Mozilla Public 368 | License, v. 2.0. If a copy of the MPL was not distributed with this 369 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 370 | 371 | If it is not possible or desirable to put the notice in a particular 372 | file, then You may include the notice in a location (such as a LICENSE 373 | file in a relevant directory) where a recipient would be likely to look 374 | for such a notice. 375 | 376 | You may add additional accurate notices of copyright ownership. 377 | 378 | Exhibit B - "Incompatible With Secondary Licenses" Notice 379 | --------------------------------------------------------- 380 | 381 | This Source Code Form is "Incompatible With Secondary Licenses", as 382 | defined by the Mozilla Public License, v. 2.0. 383 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSI Validator 2 | [![CI](https://github.com/OpenSimulationInterface/osi-validation/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/OpenSimulationInterface/osi-validation/actions/workflows/ci.yml) 3 | 4 | > **_NOTE:_** This tool is not part of the official OSI standard. It has its own release schedule. The OSI CCB is not responsible for this software but MUST be notified about pull requests. 5 | 6 | OSI Validator checks the compliance of OSI messages with predefined rules. 7 | These rules can be generated from the OSI .proto files with [rules2yml.py](https://github.com/OpenSimulationInterface/osi-validation/blob/master/rules2yml.py). 8 | After the rules are generated, they can be customized by the user. 9 | The full documentation on the validator and customization of the rules is available [here](https://github.com/OpenSimulationInterface/osi-validation/tree/master/doc). 10 | 11 | ## Usage 12 | 13 | ```bash 14 | usage: osivalidator [-h] --data DATA [--rules RULES] [--type {SensorView,GroundTruth,SensorData}] [--output OUTPUT] [--timesteps TIMESTEPS] [--debug] [--verbose] [--parallel] [--format {None}] 15 | [--blast BLAST] [--buffer BUFFER] 16 | 17 | Validate data defined at the input 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | --data DATA Path to the file with OSI-serialized data. 22 | --rules RULES, -r RULES 23 | Directory with text files containig rules. 24 | --type {SensorView,GroundTruth,SensorData}, -t {SensorView,GroundTruth,SensorData} 25 | Name of the type used to serialize data. 26 | --output OUTPUT, -o OUTPUT 27 | Output folder of the log files. 28 | --timesteps TIMESTEPS 29 | Number of timesteps to analyze. If -1, all. 30 | --debug Set the debug mode to ON. 31 | --verbose, -v Set the verbose mode to ON. 32 | --parallel, -p (Ignored) Set parallel mode to ON. 33 | --format {None}, -f {None} 34 | (Ignored) Set the format type of the trace. 35 | --blast BLAST, -bl BLAST 36 | Set the maximum in-memory storage count of OSI messages during validation. 37 | --buffer BUFFER, -bu BUFFER 38 | (Ignored) Set the buffer size to retrieve OSI messages from trace file. Set it to 0 if you do not want to use buffering at all. 39 | ``` 40 | 41 | ## Installation 42 | 43 | OSI Validator has been developed with Python 3.8 within a virtual environment on Ubuntu 20.04. See [this documentation](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/) for Python virtual environments. 44 | Check for compatibility with your system the [github actions](https://github.com/OpenSimulationInterface/osi-validation/actions) CI workflow. 45 | Currently supported are Python 3.8, 3.9, 3.10, 3.11 and 3.12 with the latest Ubuntu version. 46 | Check the installation prerequisites of the [Open Simulation Interface](https://github.com/OpenSimulationInterface/open-simulation-interface#installation) 47 | 48 | ### Local Linux (recommended) 49 | 50 | ```bash 51 | $ git clone https://github.com/OpenSimulationInterface/osi-validation.git 52 | $ cd osi-validation 53 | $ git submodule update --init 54 | $ python3 -m venv .venv 55 | $ source .venv/bin/activate 56 | (.venv) $ python3 -m pip install --upgrade pip 57 | (.venv) $ python3 -m pip install -r requirements_develop.txt 58 | (.venv) $ cd open-simulation-interface && python3 -m pip install . && cd .. 59 | (.venv) $ python3 -m pip install -r requirements.txt 60 | (.venv) $ python3 rules2yml.py -d rules 61 | (.venv) $ python3 -m pip install . 62 | ``` 63 | 64 | ### Local Windows (Git bash) 65 | 66 | ```bash 67 | $ git clone https://github.com/OpenSimulationInterface/osi-validation.git 68 | $ cd osi-validation 69 | $ git submodule update --init 70 | $ python -m venv .venv 71 | $ source .venv/Scripts/activate 72 | (.venv) $ python -m pip install --upgrade pip 73 | (.venv) $ python -m pip install -r requirements_develop.txt 74 | (.venv) $ cd open-simulation-interface && python -m pip install . && cd .. 75 | (.venv) $ python -m pip install -r requirements.txt 76 | (.venv) $ python rules2yml.py -d rules 77 | (.venv) $ python -m pip install . 78 | ``` 79 | 80 | ## Example command 81 | 82 | ```bash 83 | $ osivalidator --data data/20240221T141700Z_sv_300_2112_10_one_moving_object.osi --rules rules/ 84 | ``` 85 | -------------------------------------------------------------------------------- /data/20240221T141700Z_sv_300_2112_10_one_moving_object.osi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSimulationInterface/osi-validation/c5a77fea085f63d256f1ade4198a6969d640f940/data/20240221T141700Z_sv_300_2112_10_one_moving_object.osi -------------------------------------------------------------------------------- /data/20240618T122540Z_sv_370_244_20_minimal_valid_example.osi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSimulationInterface/osi-validation/c5a77fea085f63d256f1ade4198a6969d640f940/data/20240618T122540Z_sv_370_244_20_minimal_valid_example.osi -------------------------------------------------------------------------------- /doc/description.adoc: -------------------------------------------------------------------------------- 1 | = General description 2 | 3 | The OSI Validator is a validation framework for the field values of OSI 4 | messages. It recursively checks the compliance of an OSI message of type 5 | `+SensorData+`, `+SensorView+` or `+GroundTruth+` according to 6 | predefined rules. It was developed due to the need of validation of a 7 | huge amount of OSI messages which cannot be validated manually. 8 | 9 | setup usage writing-rules 10 | -------------------------------------------------------------------------------- /doc/howtocontribute.adoc: -------------------------------------------------------------------------------- 1 | = Contributors' Guidelines 2 | 3 | == Introduction 4 | 5 | The purpose of this document is to help contributors get started with 6 | the OSI Validation codebase. 7 | 8 | == Reporting issues 9 | 10 | The simplest way to contribute to OSI Validation is to report issues 11 | that you may find with the project on 12 | https://github.com/OpenSimulationInterface/osi-validation[github]. 13 | Everyone can create issues. Always make sure to search the existing 14 | issues before reporting a new one. Issues may be created to discuss: 15 | 16 | * https://github.com/OpenSimulationInterface/osi-validation/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=[Feature 17 | requests or Ideas] 18 | * https://github.com/OpenSimulationInterface/osi-validation/issues/new?assignees=&labels=bug&template=bug_report.md&title=[Bugs] 19 | * https://github.com/OpenSimulationInterface/osi-validation/issues/new?assignees=&labels=question&template=question.md&title=[Questions] 20 | * https://github.com/OpenSimulationInterface/osi-validation/issues/new[Other] 21 | 22 | If practicable issues should be closed by a referenced pull request or 23 | commit 24 | (https://help.github.com/en/articles/closing-issues-using-keywords[here] 25 | you can find keywords to close issues automatically). To help developers 26 | and maintainers we provide a 27 | https://github.com/OpenSimulationInterface/osi-validation/blob/master/.github/pull_request_template.md[pull 28 | request template] which will be generated each time you create a new 29 | pull request. 30 | 31 | See 32 | https://opensimulationinterface.github.io/osi-documentation/osi/howtocontribute.html#our-git-workflow[here] 33 | for more on our git workflow. 34 | 35 | == Documentation 36 | 37 | * Documentation changes can be performed by anyone. 38 | * Consider adding stuff to the 39 | https://github.com/OpenSimulationInterface/osi-documentation[osi-documentation] 40 | or directly to the 41 | https://github.com/OpenSimulationInterface/osi-validation/tree/master/doc[doc] 42 | folder in the repository. 43 | * When new changes are made directly to the 44 | https://github.com/OpenSimulationInterface/osi-documentation[osi-documentation] 45 | repo the documentation will be rebuild and the new changes can be seen. 46 | When making documentation changes in the 47 | https://github.com/OpenSimulationInterface/osi-validation/tree/master/doc[doc] 48 | folder of the repository the changes will be visible when the daily 49 | chron job of osi-documentation is executed. 50 | -------------------------------------------------------------------------------- /doc/setup.adoc: -------------------------------------------------------------------------------- 1 | = Installation Guide 2 | 3 | The OSI Validator is being developed with Python 3.6 within a virtual 4 | environment. It is recommended to use the same python version for 5 | validation tasks. 6 | 7 | == Setup for linux users 8 | 9 | This setup guide is for users who want to just use the validator. 10 | 11 | Clone the repository osi-validation: 12 | 13 | [source,bash] 14 | ---- 15 | git clone https://github.com/OpenSimulationInterface/osi-validation.git 16 | ---- 17 | 18 | Change directory to osi-validation: 19 | 20 | [source,bash] 21 | ---- 22 | cd osi-validation 23 | ---- 24 | 25 | Clone the submodules: 26 | 27 | [source,bash] 28 | ---- 29 | git submodule update --init 30 | ---- 31 | 32 | Install the open-simulation-interface: 33 | 34 | [source,bash] 35 | ---- 36 | cd open-simulation-interface 37 | pip install . 38 | ---- 39 | 40 | Install osi-validation into the global root directory: 41 | 42 | [source,bash] 43 | ---- 44 | cd ..; sudo pip3 install . 45 | ---- 46 | 47 | Now you can run the validator on an example trace file (`+trace.osi+`) 48 | by calling: 49 | 50 | [source,bash] 51 | ---- 52 | osivalidator --data trace.osi 53 | ---- 54 | 55 | === OSI Validator Binary 56 | 57 | After the installation of all the dependencies it is possible to compile 58 | the osi-validator into one binary file (size ~ 9.1 Mb) for easier 59 | distribution and usage. For that we use 60 | https://www.pyinstaller.org/[pyinstaller]: 61 | 62 | [source,bash] 63 | ---- 64 | pip install pyinstaller 65 | pyinstaller osivalidator/osi_general_validator.py --onefile 66 | ---- 67 | 68 | After the compilation you can find the binary in the `+dist+` directory. 69 | You can use the binary the normal way you would use the command line 70 | interface: 71 | 72 | [source,bash] 73 | ---- 74 | ./dist/osi_general_validator --help 75 | ---- 76 | 77 | == Setup for linux developers 78 | 79 | This setup guide is for developers who want to contribute to the OSI 80 | Validator. 81 | 82 | Clone repository osi-validation: 83 | 84 | [source,bash] 85 | ---- 86 | git clone https://github.com/OpenSimulationInterface/osi-validation.git 87 | ---- 88 | 89 | Change directory: 90 | 91 | [source,bash] 92 | ---- 93 | cd osi-validation 94 | ---- 95 | 96 | Clone the submodules: 97 | 98 | [source,bash] 99 | ---- 100 | git submodule update --init 101 | ---- 102 | 103 | It is best practice to use a virtual environment in python. It has 104 | various advantages such as the ability to install modules locally, 105 | export a working environment, and execute a Python program in that 106 | environment so that you don't mess around with your global python 107 | environment. Install virtual environment: 108 | 109 | [source,bash] 110 | ---- 111 | sudo apt-get install virtualenv 112 | ---- 113 | 114 | Create virtual environment: 115 | 116 | [source,bash] 117 | ---- 118 | virtualenv -p /usr/bin/python3 venv 119 | ---- 120 | 121 | Activate your virtual environment: 122 | 123 | [source,bash] 124 | ---- 125 | source venv/bin/activate 126 | ---- 127 | 128 | Install open-simulation-interface: 129 | 130 | [source,bash] 131 | ---- 132 | cd open-simulation-interface 133 | pip install . 134 | ---- 135 | 136 | Now you can run the validator on an example trace file (`+trace.osi+`) 137 | by calling: 138 | 139 | [source,bash] 140 | ---- 141 | python osivalidator/osi_general_validator.py --data trace.osi 142 | ---- 143 | 144 | The advantage to call the osi-validator this way for developers is that 145 | you do not need to reinstall the application when you made changes to 146 | the code. 147 | 148 | == Setup for windows users 149 | 150 | In Progress ... 151 | 152 | == Setup for windows developers 153 | 154 | In Progress ... 155 | -------------------------------------------------------------------------------- /doc/usage.adoc: -------------------------------------------------------------------------------- 1 | = Usage 2 | 3 | == Example 4 | 5 | After the installation you can call the command `+osivalidator+` in your 6 | terminal which has the following usage: 7 | 8 | [source,bash] 9 | ---- 10 | usage: osivalidator [-h] --data DATA [--rules RULES] [--type {SensorView,GroundTruth,SensorData}] [--output OUTPUT] [--timesteps TIMESTEPS] [--debug] [--verbose] [--parallel] [--format {None}] 11 | [--blast BLAST] [--buffer BUFFER] 12 | 13 | Validate data defined at the input 14 | 15 | mandatory arguments: 16 | --data DATA Path to the file with OSI-serialized data. 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | --rules RULES, -r RULES 21 | Directory with text files containig rules. 22 | --type {SensorView,GroundTruth,SensorData}, -t {SensorView,GroundTruth,SensorData} 23 | Name of the type used to serialize data. 24 | --output OUTPUT, -o OUTPUT 25 | Output folder of the log files. 26 | --timesteps TIMESTEPS 27 | Number of timesteps to analyze. If -1, all. 28 | --debug Set the debug mode to ON. 29 | --verbose, -v Set the verbose mode to ON. 30 | --parallel, -p (Ignored) Set parallel mode to ON. 31 | --format {None}, -f {None} 32 | (Ignored) Set the format type of the trace. 33 | --blast BLAST, -bl BLAST 34 | Set the maximum in-memory storage count of OSI messages during validation. 35 | --buffer BUFFER, -bu BUFFER 36 | (Ignored) Set the buffer size to retrieve OSI messages from trace file. Set it to 0 if you do not want to use buffering at all. 37 | ---- 38 | 39 | To run the validation first you need an OSI trace file which consists of 40 | multiple OSI messages. In the directory `+data+` of the repository we 41 | already provide an example trace file which is called 42 | `+20240221T141700Z_sv_300_2112_10_one_moving_object.osi+`. 43 | 44 | To validate the trace file you simply call `+osivalidator+` and provide 45 | the path to the trace: 46 | 47 | [source,bash] 48 | ---- 49 | osivalidator --data data/20240221T141700Z_sv_300_2112_10_one_moving_object.osi 50 | ---- 51 | 52 | To validate trace files with rules defined in the comments of 53 | `+*.proto+` files in the open-simulation-interface repository first you 54 | need to generate them and then specify them: 55 | 56 | [source,bash] 57 | ---- 58 | python rules2yml.py # Generates the rule directory 59 | osivalidator --data data/20240221T141700Z_sv_300_2112_10_one_moving_object.osi --rules rules -p 60 | ---- 61 | 62 | The rules2yml.py generates a yml file for each OSI proto file containing the rules specified in OSI. 63 | The yml files are located in the specified rules folder given as an input parameter. 64 | Additionally, the script generates respective yml schema files to validate the rule yml files in /schema. 65 | The schema files contain the message names of the original OSI proto file and a list of applicable rules. 66 | If a rule has an associated value, e.g. a string or a number, the type of the value is also checked. 67 | When executing osivalidator, all rule files are validated against their respective schema. 68 | 69 | If needed, the rules folder can be copied and modified for specific use cases, e.g. by adding or removing certain rules. 70 | This way, osivalidation can be used with different sets of rules. 71 | 72 | After successfully running the validation the following output is 73 | generated: 74 | 75 | [source,bash] 76 | ---- 77 | Instantiate logger ... 78 | Reading data ... 79 | Retrieving messages in osi trace file until 1290 ... 80 | 10 messages has been discovered in 8.273124694824219e-05 s 81 | Collect validation rules ... 82 | 83 | Caching ... 84 | Caching done! 85 | Warnings (8) 86 | Ranges of timestamps Message 87 | ---------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------- 88 | [0, 9] SensorView.version.is_set(None) does not comply in SensorView 89 | [0, 9] SensorView.sensor_id.is_set(None) does not comply in SensorView 90 | [0, 9] SensorView.mounting_position.is_set(None) does not comply in SensorView 91 | [0, 9] SensorView.host_vehicle_id.is_set(None) does not comply in SensorView 92 | [0, 9] MovingObject.vehicle_attributes.check_if.is_set(None) does not comply in SensorView.global_ground_truth.moving_object 93 | [0, 9] MovingObject.vehicle_attributes.check_if([{'is_equal_to': 2, 'target': 'this.type'}]) does not comply in SensorView.global_ground_truth.moving_object 94 | [0, 9] Reference unresolved: GroundTruth to MovingObject (ID: 113) 95 | [1, 9] MovingObject.pedestrian_attributes.check_if.is_equal_to(3) does not comply in SensorView.global_ground_truth.moving_object.type 96 | ---- 97 | 98 | The Output is a report of how many errors (here 0) and warnings (here 99 | 8) were found in the osi-message according to the defined rules in your 100 | specified rules directory. The rules can be found under the tag 101 | `+\rules+` in the *.proto files from the 102 | https://github.com/OpenSimulationInterface/open-simulation-interface[osi 103 | github]. 104 | 105 | For each error and warning there is a description on which timestamp it was found, the path to the rule and the path to the 106 | osi-message is provided. The general format is: 107 | 108 | [source,bash] 109 | ---- 110 | Errors (NUMBER_ERRORS) 111 | Ranges of timestamps Message 112 | -------------------------------- -------------------------------------------------------- 113 | [START_TIMESTAMP, END_TIMESTAMP] PATH_TO_RULE(VALUE) does not comply in PATH_TO_OSI_FIELD 114 | 115 | Warnings (NUMBER_WARNINGS) 116 | Ranges of timestamps Message 117 | -------------------------------- -------------------------------------------------------- 118 | [START_TIMESTAMP, END_TIMESTAMP] PATH_TO_RULE(VALUE) does not comply in PATH_TO_OSI_FIELD 119 | ---- 120 | 121 | The osivalidator will end the execution with the exit code 1, if warnings or errors are generated. 122 | If the trace file is valid and no warning or errors occurred, the execution is ended with exit code 0. 123 | 124 | == Understanding Validation Output 125 | 126 | To better understand the validation output let us use the example 127 | above and describe the meaning of the lines. First of all one should 128 | know that the rules to the fields are checked in a 129 | https://en.wikipedia.org/wiki/Depth-first_search[depth-first-search] 130 | (DFS) traversal manner. The validation starts with the top-level message `+SensorView+`, `+SensorData+` or `+GroundTruth+` 131 | and goes in depth if the message is set. For example the message 132 | below is checked but does not go further in depth because it is not 133 | set (indicated by `+is_set(None)+`): 134 | 135 | [source,bash] 136 | ---- 137 | [0, 9] SensorView.version.is_set(None) does not comply in SensorView 138 | ---- 139 | 140 | Some rules have conditions. 141 | The vehicle_attributes of a moving object for example only have to be set, when the moving object is of type vehicle. 142 | The following warning indicates, that it is checked that the moving object is of type vehicle (enum value 2) but the vehicle_attributes are not set. 143 | 144 | [source,bash] 145 | ---- 146 | [0, 9] MovingObject.vehicle_attributes.check_if.is_set(None) does not comply in SensorView.global_ground_truth.moving_object 147 | [0, 9] MovingObject.vehicle_attributes.check_if([{'is_equal_to': 2, 'target': 'this.type'}]) does not comply in SensorView.global_ground_truth.moving_object 148 | ---- 149 | 150 | == Custom Rules 151 | 152 | Currently, the following rules exist: 153 | 154 | [source,python] 155 | ---- 156 | is_greater_than: 1 157 | is_greater_than_or_equal_to: 1 158 | is_less_than_or_equal_to: 10 159 | is_less_than: 2 160 | is_equal: 1 161 | is_different: 2 162 | is_globally_unique: 163 | refers_to: MovingObject 164 | is_iso_country_code: 165 | first_element: {is_equal: 0.13, is_greater_than: 0.13} 166 | last_element: {is_equal: 0.13, is_greater_than: 0.13} 167 | check_if: [{is_equal: 2, is_greater_than: 3, target: this.y}, {do_check: {is_equal: 1, is_less_than: 3}}] 168 | is_set: 169 | ---- 170 | 171 | These rules can be added manually to the rules *.yml files like in the 172 | example of the environmental conditions below (see 173 | `+how-to-write-rules+` for more): 174 | 175 | [source,yaml] 176 | ---- 177 | EnvironmentalConditions: 178 | ambient_illumination: 179 | time_of_day: 180 | unix_timestamp: 181 | atmospheric_pressure: 182 | - is_greater_than_or_equal_to: 80000 183 | - is_less_than_or_equal_to: 120000 184 | temperature: 185 | - is_greater_than_or_equal_to: 170 186 | - is_less_than_or_equal_to: 340 187 | relative_humidity: 188 | - is_greater_than_or_equal_to: 0 189 | - is_less_than_or_equal_to: 100 190 | precipitation: 191 | fog: 192 | TimeOfDay: 193 | seconds_since_midnight: 194 | - is_greater_than_or_equal_to: 0 195 | - is_less_than: 86400 196 | ---- 197 | 198 | Further custom rules can be implemented into the osi-validator (see 199 | https://github.com/OpenSimulationInterface/osi-validation/blob/master/osivalidator/osi_rules_implementations.py[rules 200 | implementation] for more). 201 | -------------------------------------------------------------------------------- /doc/writing-rules.adoc: -------------------------------------------------------------------------------- 1 | = How to write rules 2 | 3 | == Folder structure 4 | 5 | Official rules for individual OSI fields are defined in the `+*.proto+` files of 6 | https://github.com/OpenSimulationInterface/open-simulation-interface[OSI]. 7 | The first step to using and editing rules is to extract them from the proto files with `+rules2yml.py+`. 8 | It has the following options: 9 | [source,bash] 10 | ---- 11 | usage: python3 rules2yml.py [-h] [--dir DIR] [--full-osi] 12 | 13 | Export the rules of *.proto files into the *.yml format so it can be used by 14 | the validator. 15 | 16 | options: 17 | -h, --help show this help message and exit 18 | --dir DIR, -d DIR Name of the directory where the yml rules will be stored. 19 | --full-osi, -f Add is_set rule to all fields that do not contain it already. 20 | ---- 21 | 22 | The following example command will generate the yml rule files in a folder `rules` and add the `is_set` rule to every OSI field that does not have this rule anyways. 23 | This will result in a rule base to check for a full interface. 24 | 25 | [source,bash] 26 | ---- 27 | python3 rules2yml.py --dir rules --full-osi 28 | ---- 29 | 30 | The resulting yml rule files can be freely edited and are explained in the following in more detail. 31 | 32 | == File structure 33 | 34 | Below you can see an example of the osi_detectedlane.yml rule file for 35 | https://github.com/OpenSimulationInterface/open-simulation-interface/blob/master/osi_detectedlane.proto[osi_detectedlane.proto] without the `--full-osi` option. 36 | 37 | [source,YAML] 38 | ---- 39 | DetectedLane: 40 | header: 41 | CandidateLane: 42 | probability: 43 | - is_greater_than_or_equal_to: 0 44 | - is_less_than_or_equal_to: 1 45 | classification: 46 | 47 | DetectedLaneBoundary: 48 | header: 49 | boundary_line: 50 | position: 51 | ---- 52 | 53 | Each root at the top level represent a root message type 54 | `+DetectedLane+` or `+DetectedLaneBoundary+`. The children of each root 55 | message represent its fields if they are not camel-case. For example 56 | `+header+` is a field of `+DetectedLane+` or `+header+` and 57 | `+boundary_line+` are fields of `+DetectedLaneBoundary+`. 58 | `+CandidateLane+` is a submessage of the message `+DetectedLane+`. Each 59 | field has a `+sequence+` (starting with an hyphen `+-+`) of rules that 60 | apply to that specific field. For example the probability of the message 61 | `+CandidateLane+` is between 0 and 1. 62 | 63 | == Rules 64 | 65 | The rules can either be with or without any parameters. 66 | 67 | [source,YAML] 68 | ---- 69 | is_greater_than_or_equal_to: 0.0 70 | is_equal: 1 71 | ---- 72 | 73 | In the case a rule has a parameter, it is written as a 74 | [.title-ref]#mapping# ( a [.title-ref]#scalar# followed by a colon ":"). 75 | What comes after the colon depends on the rule used. For instance, the 76 | rule `+is_greater_than_or_equal_to+` accept a double. 77 | 78 | The available rules and their usage are explained in the osi-validator 79 | class 80 | https://github.com/OpenSimulationInterface/osi-validation/blob/master/osivalidator/osi_rules_implementations.py[osi_rules_implementations]. 81 | See also examples for available rules which can be defined in `+*.yml+` 82 | files below: 83 | 84 | [source,YAML] 85 | ---- 86 | is_set: 87 | is_greater_than: 1 88 | is_greater_than_or_equal_to: 1 89 | is_less_than_or_equal_to: 10 90 | is_less_than: 2 91 | is_equal: 1 92 | is_different: 2 93 | is_globally_unique: 94 | refers_to: Lane 95 | is_iso_country_code: 96 | first_element: {is_equal: 0.13, is_greater_than: 0.13} 97 | last_element: {is_equal: 0.13, is_greater_than: 0.13} 98 | check_if: [{is_equal: 2, is_greater_than: 3, target: this.y}, {do_check: {is_equal: 1, is_less_than: 3}}] 99 | ---- 100 | 101 | == Severity 102 | 103 | When an attribute does not comply with a rule, a warning is thrown. An 104 | error will be thrown if an exclamation mark is written at the end of the 105 | verb of a rule. 106 | 107 | === Example 108 | 109 | [source,YAML] 110 | ---- 111 | is_greater_than!: 0 112 | ---- 113 | -------------------------------------------------------------------------------- /osivalidator/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OSI Validator 3 | """ 4 | -------------------------------------------------------------------------------- /osivalidator/__main__.py: -------------------------------------------------------------------------------- 1 | import osi_general_validator 2 | 3 | osi_general_validator.main() 4 | -------------------------------------------------------------------------------- /osivalidator/linked_proto_field.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class describes a wrapper on protobuf fields that add a link 3 | with parent message and bind message to some additional information. 4 | """ 5 | 6 | from google.protobuf.message import Message 7 | from google.protobuf.json_format import MessageToDict 8 | 9 | import os, sys 10 | 11 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 12 | import osi_rules 13 | 14 | 15 | class LinkedProtoField: 16 | """ 17 | This class describes a wrapper on protobuf fields that add a link 18 | with parent message and bind message to some additional information. 19 | 20 | The Protobuf's RepeatedCompositeContainer that describes repeated field are 21 | replaced with Python lists. 22 | The field information (parent message and field name) for the repeated 23 | field are here bounded to each element of the list. 24 | """ 25 | 26 | def __init__(self, value, name=None, parent=None): 27 | self.name = name 28 | self.value = value 29 | self.parent = parent 30 | self.path = name if parent is None else parent.path + "." + name 31 | 32 | self._dict = None 33 | self._fields = None 34 | self._message_type = None 35 | 36 | @property 37 | def is_message(self): 38 | """Return true if the field contain a message""" 39 | return isinstance(self.value, Message) 40 | 41 | @property 42 | def message_type(self): 43 | """ 44 | Return a path to the message type in OSI3 as a ProtoMessagePath 45 | """ 46 | if not self._message_type: 47 | field_type_desc = self.value.DESCRIPTOR 48 | message_type = [] 49 | while field_type_desc is not None: 50 | message_type.insert(0, field_type_desc.name) 51 | field_type_desc = field_type_desc.containing_type 52 | self._message_type = osi_rules.ProtoMessagePath(message_type) 53 | return self._message_type 54 | 55 | @property 56 | def fields(self): 57 | """ 58 | Overloading of protobuf ListFields function that return 59 | a list of LinkedProtoFields. 60 | 61 | Only works if the field is composite, raise an AttributeError otherwise. 62 | """ 63 | if self._fields is None: 64 | self._fields = { 65 | field_tuple[0].name: self.get_field(field_tuple[0].name) 66 | for field_tuple in self.value.ListFields() 67 | } 68 | 69 | return self._fields.values() 70 | 71 | @property 72 | def all_field_descriptors(self): 73 | """ 74 | List all the fields descriptors, i.e even the one that are not set 75 | """ 76 | return self.value.DESCRIPTOR.fields 77 | 78 | def to_dict(self): 79 | """ 80 | Return the dict version of the protobuf message. 81 | 82 | Compute the dict only once, then store it and retrieve it. 83 | """ 84 | if self._dict is None: 85 | self._dict = MessageToDict(self.value) 86 | 87 | return self._dict 88 | 89 | def get_field(self, field_name): 90 | """ 91 | If the LinkedProtoField wraps a message, return the field of this 92 | message. Otherwise, raise an AttributeError. 93 | """ 94 | if self._fields is not None: 95 | return self._fields[field_name] 96 | 97 | field = getattr(self.value, field_name) 98 | 99 | if ( 100 | hasattr(self.value, "DESCRIPTOR") 101 | and self.value.DESCRIPTOR.fields_by_name[field_name].label == 3 102 | ): 103 | return [ 104 | LinkedProtoField(u_field, parent=self, name=field_name) 105 | for u_field in field 106 | ] 107 | 108 | return LinkedProtoField(field, parent=self, name=field_name) 109 | 110 | def has_field(self, field_name): 111 | """ 112 | Check if a protobuf message has an attribute/field even if this 113 | is a repeated field. 114 | 115 | If it is a repeated field, this function returns false if there is no 116 | element into it. 117 | """ 118 | try: 119 | return self.value.HasField(field_name) 120 | except ValueError: 121 | try: 122 | return len(getattr(self.value, field_name)) > 0 123 | except AttributeError: 124 | return False 125 | 126 | def query(self, path, parent=False): 127 | """ 128 | Return a LinkedProtoField from a path. 129 | 130 | Example of path: ./global_ground_truth/moving_object 131 | """ 132 | cursor = self 133 | components = path.split(".") 134 | if parent: 135 | if len(components) > 1: 136 | components.pop() 137 | else: 138 | components.append("parent") 139 | 140 | for path_component in components: 141 | if path_component == "this": 142 | cursor = cursor 143 | elif path_component == "parent": 144 | cursor = cursor.parent 145 | else: 146 | cursor = cursor.get_field(path_component) 147 | 148 | return cursor 149 | 150 | def __repr__(self): 151 | return self.value.__repr__() 152 | -------------------------------------------------------------------------------- /osivalidator/osi_general_validator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main class and entry point of the OSI Validator. 3 | """ 4 | 5 | import argparse 6 | from tqdm import tqdm 7 | from osi3trace.osi_trace import OSITrace 8 | import os, sys 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 11 | 12 | # Import local files 13 | try: 14 | import osi_rules 15 | import osi_validator_logger 16 | import osi_rules_checker 17 | import linked_proto_field 18 | except Exception as e: 19 | print( 20 | "Make sure you have installed the requirements with 'pip install -r requirements.txt'!" 21 | ) 22 | print(e) 23 | 24 | 25 | def check_positive_int(value): 26 | ivalue = int(value) 27 | if ivalue < 0: 28 | raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value) 29 | return ivalue 30 | 31 | 32 | def command_line_arguments(): 33 | """Define and handle command line interface""" 34 | 35 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 36 | 37 | parser = argparse.ArgumentParser( 38 | description="Validate data defined at the input", prog="osivalidator" 39 | ) 40 | parser.add_argument( 41 | "--data", 42 | help="Path to the file with OSI-serialized data.", 43 | type=str, 44 | required=True, 45 | ) 46 | parser.add_argument( 47 | "--rules", 48 | "-r", 49 | help="Directory with yml files containing rules. ", 50 | default=os.path.join(dir_path, "rules"), 51 | type=str, 52 | ) 53 | parser.add_argument( 54 | "--type", 55 | "-t", 56 | help="Name of the type used to serialize data. Default is SensorView.", 57 | choices=[ 58 | "SensorView", 59 | "SensorViewConfiguration", 60 | "GroundTruth", 61 | "HostVehicleData", 62 | "SensorData", 63 | "TrafficUpdate", 64 | "TrafficCommandUpdate", 65 | "TrafficCommand", 66 | "MotionRequest", 67 | "StreamingUpdate", 68 | ], 69 | type=str, 70 | required=False, 71 | ) 72 | parser.add_argument( 73 | "--output", 74 | "-o", 75 | help="Output folder of the log files.", 76 | default="output_logs", 77 | type=str, 78 | required=False, 79 | ) 80 | parser.add_argument( 81 | "--timesteps", 82 | help="Number of timesteps to analyze. If -1, all.", 83 | type=int, 84 | default=-1, 85 | required=False, 86 | ) 87 | parser.add_argument( 88 | "--debug", help="Set the debug mode to ON.", action="store_true" 89 | ) 90 | parser.add_argument( 91 | "--verbose", "-v", help="Set the verbose mode to ON.", action="store_true" 92 | ) 93 | parser.add_argument( 94 | "--parallel", 95 | "-p", 96 | help="(Ignored) Set parallel mode to ON.", 97 | default=False, 98 | required=False, 99 | action="store_true", 100 | ) 101 | parser.add_argument( 102 | "--format", 103 | "-f", 104 | help="(Ignored) Set the format type of the trace.", 105 | choices=[None], 106 | default=None, 107 | type=str, 108 | required=False, 109 | ) 110 | parser.add_argument( 111 | "--blast", 112 | "-bl", 113 | help="Set the maximum in-memory storage count of OSI messages during validation.", 114 | default=500, 115 | type=check_positive_int, 116 | required=False, 117 | ) 118 | parser.add_argument( 119 | "--buffer", 120 | "-bu", 121 | help="(Ignored) Set the buffer size to retrieve OSI messages from trace file. Set it to 0 if you do not want to use buffering at all.", 122 | default=0, 123 | type=check_positive_int, 124 | required=False, 125 | ) 126 | 127 | return parser.parse_args() 128 | 129 | 130 | LOGS = [] 131 | LOGGER = osi_validator_logger.OSIValidatorLogger() 132 | VALIDATION_RULES = osi_rules.OSIRules() 133 | 134 | 135 | def detect_message_type(path: str): 136 | """Automatically detect the message type from the file name. 137 | If it cannot be detected, the function return SensorView as default. 138 | 139 | Args: 140 | path (str): Path incl. filename of the trace file 141 | 142 | Returns: 143 | Str: Message type as string, e.g. SensorData, SensorView etc. 144 | """ 145 | filename = os.path.basename(path) 146 | if filename.find("_sd_") != -1: 147 | return "SensorData" 148 | if filename.find("_sv_") != -1: 149 | return "SensorView" 150 | if filename.find("_svc_") != -1: 151 | return "SensorViewConfiguration" 152 | if filename.find("_gt_") != -1: 153 | return "GroundTruth" 154 | if filename.find("_tu_") != -1: 155 | return "TrafficUpdate" 156 | if filename.find("_tcu_") != -1: 157 | return "TrafficCommandUpdate" 158 | if filename.find("_tc_") != -1: 159 | return "TrafficCommand" 160 | if filename.find("_hvd_") != -1: 161 | return "HostVehicleData" 162 | if filename.find("_mr_") != -1: 163 | return "MotionRequest" 164 | if filename.find("_su_") != -1: 165 | return "StreamingUpdate" 166 | return "SensorView" 167 | 168 | 169 | def main(): 170 | """Main method""" 171 | 172 | # Handling of command line arguments 173 | args = command_line_arguments() 174 | 175 | if not args.type: 176 | args.type = detect_message_type(args.data) 177 | 178 | # Instantiate Logger 179 | print("Instantiate logger ...") 180 | directory = args.output 181 | if not os.path.exists(directory): 182 | os.makedirs(directory) 183 | 184 | LOGGER.init(args.debug, args.verbose, directory) 185 | 186 | # Read data 187 | print("Reading data ...") 188 | trace = OSITrace(path=args.data, type_name=args.type) 189 | 190 | # Collect Validation Rules 191 | print("Collect validation rules ...") 192 | try: 193 | VALIDATION_RULES.from_yaml_directory(args.rules) 194 | except Exception as e: 195 | trace.close() 196 | print("Error collecting validation rules:", e) 197 | exit(1) 198 | 199 | # Pass all timesteps or the number specified 200 | if args.timesteps != -1: 201 | max_timestep = args.timesteps 202 | LOGGER.info(None, f"Pass the {max_timestep} first timesteps") 203 | else: 204 | LOGGER.info(None, "Pass all timesteps") 205 | max_timestep = None 206 | 207 | total_length = os.path.getsize(args.data) 208 | current_pos = 0 209 | 210 | with tqdm(total=total_length, unit="B", unit_scale=True, unit_divisor=1024) as pbar: 211 | for index, message in enumerate(trace): 212 | if index % args.blast == 0: 213 | LOGS = [] 214 | if max_timestep and index >= max_timestep: 215 | pbar.update(total_length - current_pos) 216 | break 217 | try: 218 | process_message(message, index, args.type) 219 | except Exception as e: 220 | print(str(e)) 221 | new_pos = trace.file.tell() 222 | pbar.update(new_pos - current_pos) 223 | current_pos = new_pos 224 | 225 | trace.close() 226 | display_results() 227 | if get_num_logs() > 0: 228 | exit(1) 229 | 230 | 231 | def process_message(message, timestep, data_type): 232 | """Process one message""" 233 | rule_checker = osi_rules_checker.OSIRulesChecker(LOGGER) 234 | timestamp = rule_checker.set_timestamp(message.timestamp, timestep) 235 | 236 | LOGGER.log_messages[timestep] = [] 237 | LOGGER.debug_messages[timestep] = [] 238 | LOGGER.info(None, f"Analyze message of timestamp {timestamp}", False) 239 | 240 | # Check common rules 241 | getattr(rule_checker, "check_children")( 242 | linked_proto_field.LinkedProtoField(message, name=data_type), 243 | VALIDATION_RULES.get_rules().get_type(data_type), 244 | ) 245 | 246 | LOGS.extend(LOGGER.log_messages[timestep]) 247 | 248 | 249 | # Synthetize Logs 250 | def display_results(): 251 | return LOGGER.synthetize_results(LOGS) 252 | 253 | 254 | def get_num_logs(): 255 | return len(LOGS) 256 | 257 | 258 | if __name__ == "__main__": 259 | main() 260 | -------------------------------------------------------------------------------- /osivalidator/osi_id_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the OSIIDManager which manage IDs and check for references 3 | and unicity according to the OSI KPIs. 4 | """ 5 | 6 | from collections import Counter 7 | 8 | 9 | class OSIIDManager: 10 | """Manage the ID of OSI Messages for verification of unicity and references""" 11 | 12 | def __init__(self, logger): 13 | # id => [object1, object2...] 14 | # Each object (message) should have a different type 15 | # Ideally, there is only one object per id 16 | # to be resolved at the end 17 | self._index = dict() 18 | 19 | # [(referer_obj, id, expected_type, condition), ...] 20 | # one tuple per reference 21 | # to be resolved at the end 22 | self._references = [] 23 | 24 | self.logger = logger 25 | 26 | def get_all_messages_by_id(self, message_id, message_t=None): 27 | """Retrieve all the message by giving an id""" 28 | return list( 29 | filter(lambda m: message_t_filter(m, message_t), self._index[message_id]) 30 | ) 31 | 32 | def get_message_by_id(self, message_id, message_t=None): 33 | """Retrieve only one message by giving an id""" 34 | return next( 35 | filter(lambda m: message_t_filter(m, message_t), self._index[message_id]) 36 | ) 37 | 38 | def register_message(self, message_id, message): 39 | """Register one message in the ID manager""" 40 | if message_id in self._index: 41 | self._index[message_id].append(message) 42 | else: 43 | self._index[message_id] = [message] 44 | return True 45 | 46 | def refer(self, referer, identifier, expected_type, condition=None): 47 | """Add a reference 48 | Condition is a function that will be applied on the found object if the 49 | reference is resolved 50 | """ 51 | self._references.append((referer, identifier, expected_type, condition)) 52 | 53 | def resolve_unicity(self, timestamp): 54 | """Check for double ID""" 55 | for identifier, objects in self._index.items(): 56 | types_counter = None 57 | if len(objects) > 1: 58 | types_list = list(map(type, objects)) 59 | types_str_list = ", ".join(map(lambda o: o.__name__, types_list)) 60 | self.logger.warning( 61 | timestamp, 62 | f"Several objects of type {types_str_list} have the ID " 63 | + str(identifier), 64 | ) 65 | 66 | if len(objects) != len(set(types_list)): 67 | types_counter = list(map(str, Counter(list(types_list)))) 68 | 69 | obj_list_str = ", ".join( 70 | map( 71 | lambda t, c=types_counter: c[t] + " " + t.__name__, 72 | filter(lambda t, c=types_counter: c[t] != 1, types_counter), 73 | ) 74 | ) 75 | 76 | self.logger.error( 77 | timestamp, 78 | f"Several objects of the same type have the ID " 79 | + str(identifier) 80 | + ":" 81 | + obj_list_str, 82 | ) 83 | 84 | def resolve_references(self, timestamp): 85 | """Check if references are compliant""" 86 | for reference in self._references: 87 | referer, identifier, expected_type, condition = reference 88 | try: 89 | found_object = next( 90 | filter( 91 | lambda o, et=expected_type: type(o).__name__ == et, 92 | self._index[identifier], 93 | ) 94 | ) 95 | except (StopIteration, KeyError): 96 | self.logger.error( 97 | timestamp, 98 | f"Reference unresolved: {referer.DESCRIPTOR.name} " 99 | + f"to {expected_type} " 100 | + f"(ID: {identifier})", 101 | ) 102 | else: 103 | self.logger.debug( 104 | timestamp, 105 | f"Reference resolved: {expected_type} " + f"(ID: {identifier})", 106 | ) 107 | if condition is not None: 108 | if condition(found_object): 109 | self.logger.debug(timestamp, f"Condition OK") 110 | else: 111 | self.logger.error(timestamp, f"Condition not OK") 112 | 113 | def reset(self): 114 | """Erase all data in the ID manager""" 115 | self._index = {} 116 | self._references = [] 117 | 118 | 119 | def message_t_filter(message, message_t): 120 | """Check if a message is of type message_t""" 121 | if message_t is not None: 122 | return isinstance(message, message_t) 123 | return True 124 | -------------------------------------------------------------------------------- /osivalidator/osi_rules.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all the useful classes to describe the tree of the 3 | validation rules tree. 4 | """ 5 | 6 | import os, sys 7 | 8 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 9 | 10 | from copy import deepcopy 11 | from enum import Enum 12 | 13 | from ruamel.yaml import YAML 14 | from pathlib import Path 15 | import yamale 16 | 17 | import osi_rules_implementations 18 | 19 | 20 | class OSIRules: 21 | """This class collects validation rules""" 22 | 23 | def __init__(self): 24 | self.rules = TypeRulesContainer() 25 | self.nested_fields = { 26 | "dimension", 27 | "position", 28 | "velocity", 29 | "acceleration", 30 | "orientation", 31 | "orientation_rate", 32 | "orientation_acceleration", 33 | } 34 | 35 | def validate_rules_yml(self, file=None): 36 | """Validate rule yml files against schema.""" 37 | 38 | # Read schema file 39 | directory = os.path.dirname(file) 40 | filename, file_extension = os.path.splitext(os.path.basename(file)) 41 | schema_file = directory + os.sep + "schema" + os.sep + filename + "_schema.yml" 42 | if os.path.exists(schema_file): 43 | schema = yamale.make_schema(schema_file) 44 | else: 45 | print(f"WARNING: No schema file found for {file}.\n") 46 | return 47 | 48 | # Create a Data object 49 | data = yamale.make_data(file) 50 | 51 | # Validate data against the schema. Throws a ValueError if data is invalid. 52 | try: 53 | yamale.validate(schema, data) 54 | except yamale.yamale_error.YamaleError as exc: 55 | print(exc.message) 56 | return False 57 | 58 | return True 59 | 60 | def from_yaml_directory(self, path=None): 61 | """Collect validation rules found in the directory.""" 62 | 63 | if not path: 64 | dir_path = dir_path = os.path.dirname(os.path.realpath(__file__)) 65 | path = os.path.join(dir_path, "rules") 66 | 67 | exts = (".yml", ".yaml") 68 | rule_file_errors = dict() 69 | for filename in os.listdir(path): 70 | if filename.startswith("osi_") and filename.endswith(exts): 71 | if self.validate_rules_yml(os.path.join(path, filename)): 72 | self.from_yaml_file(os.path.join(path, filename)) 73 | else: 74 | print(f"WARNING: Invalid rule file: {filename}.\n") 75 | rule_file_errors[filename] = rule_file_errors.get(filename, 0) + 1 76 | 77 | if rule_file_errors: 78 | print(f"Errors per file: {rule_file_errors}") 79 | raise Exception("Errors were found in the OSI rule files.") 80 | 81 | def from_yaml_file(self, path): 82 | """Import from a file""" 83 | yaml = YAML(typ="safe") 84 | self.from_dict(rules_dict=yaml.load(Path(path))) 85 | 86 | def from_yaml(self, yaml_content): 87 | """Import from a string""" 88 | yaml = YAML(typ="safe") 89 | self.from_dict(rules_dict=yaml.load(yaml_content)) 90 | 91 | def get_rules(self): 92 | """Return the rules""" 93 | return self.rules 94 | 95 | def from_dict(self, rules_dict=None, rules_container=None): 96 | """Translate dict rules into objects rules""" 97 | 98 | rules_container = rules_container or self.rules 99 | 100 | for key, value in rules_dict.items(): 101 | if key[0].isupper() and isinstance(value, dict): # it's a nested type 102 | new_message_t = rules_container.add_type(MessageTypeRules(name=key)) 103 | if value is not None: 104 | self.from_dict(value, new_message_t) 105 | 106 | elif key in self.nested_fields and isinstance(value, dict): 107 | new_message_t = rules_container.add_type( 108 | MessageTypeRules(name=key), 109 | path=f"{rules_container.type_name}.{key}", 110 | ) 111 | if value is not None: 112 | self.from_dict(value, new_message_t) 113 | 114 | elif isinstance(value, list): # it's a field 115 | field = rules_container.add_field(FieldRules(name=key)) 116 | for rule_dict in value: # iterate over rules 117 | field.add_rule(Rule(dictionary=rule_dict)) 118 | 119 | elif value is not None: 120 | sys.stderr.write( 121 | "must be dict or list, got " + type(rules_dict).__name__ + "\n" 122 | ) 123 | 124 | 125 | class ProtoMessagePath: 126 | """Represents a path to a message object""" 127 | 128 | def __init__(self, path=None): 129 | if path and not all(isinstance(component, str) for component in path): 130 | sys.stderr.write("Path must be str list, found " + str(path) + "\n") 131 | self.path = deepcopy(path) or [] 132 | 133 | def __repr__(self): 134 | return ".".join(self.path) 135 | 136 | def __getitem__(self, parent): 137 | return self.path[parent] 138 | 139 | def pretty_html(self): 140 | """Return a pretty html version of the message path""" 141 | return ( 142 | ".".join(map(lambda l: "" + l + "", self.path[:-1])) 143 | + "." 144 | + self.path[-1] 145 | ) 146 | 147 | def child_path(self, child): 148 | """Return a new path for the child""" 149 | new_path = deepcopy(self) 150 | new_path.path.append(child) 151 | 152 | return new_path 153 | 154 | 155 | class OSIRuleNode: 156 | """Represents any node in the tree of OSI rules""" 157 | 158 | def __init__(self, path=None): 159 | self._path = path 160 | self.root = None 161 | 162 | @property 163 | def path(self): 164 | """Return the path of the node""" 165 | return self._path 166 | 167 | @path.setter 168 | def path(self, path): 169 | new_path = ProtoMessagePath(path=path.path) 170 | self._path = new_path 171 | 172 | 173 | class TypeRulesContainer(OSIRuleNode): 174 | """This class defines either a MessageType or a list of MessageTypes""" 175 | 176 | def __init__(self, nested_types=None, root=None): 177 | super().__init__(path=ProtoMessagePath()) 178 | self.nested_types = nested_types or dict() 179 | self.root = root if root else self 180 | 181 | def add_type(self, message_type, path=None): 182 | """Add a message type in the TypeContainer""" 183 | message_type.path = ( 184 | self.path.child_path(message_type.type_name) 185 | if path is None 186 | else ProtoMessagePath(path=path.split(".")) 187 | ) 188 | message_type.root = self.root 189 | self.nested_types[message_type.type_name] = message_type 190 | return message_type 191 | 192 | def add_type_from_path(self, path, fields=None): 193 | """Add a message type in the TypeContainer by giving a path 194 | 195 | The path must be a list and the last element of the list is the name of 196 | the message type. 197 | 198 | If the message type already exists, it is not added. 199 | """ 200 | 201 | try: 202 | return self.get_type(path) 203 | except KeyError: 204 | pass 205 | 206 | name = path[-1] 207 | new_message_t = MessageTypeRules(name=name, fields=fields) 208 | 209 | child = self 210 | for node in path[:-1]: 211 | try: 212 | child = child.nested_types[node] 213 | except KeyError: 214 | child = child.add_type(MessageTypeRules(node)) 215 | 216 | child.add_type(new_message_t) 217 | 218 | return new_message_t 219 | 220 | def get_type(self, message_path): 221 | """Get a MessageType by name or path""" 222 | if isinstance(message_path, ProtoMessagePath): 223 | message_t = self 224 | for component in message_path.path: 225 | try: 226 | message_t = message_t.nested_types[component] 227 | except KeyError: 228 | raise KeyError("Type not found: " + str(message_path)) 229 | return message_t 230 | if isinstance(message_path, str): 231 | return self.nested_types[message_path] 232 | 233 | sys.stderr.write("Type must be ProtoMessagePath or str" + "\n") 234 | 235 | def __getitem__(self, name): 236 | return self.nested_types[name] 237 | 238 | def __repr__(self): 239 | return f"TypeContainer({len(self.nested_types)}):\n" + ",".join( 240 | map(str, self.nested_types) 241 | ) 242 | 243 | 244 | class MessageTypeRules(TypeRulesContainer): 245 | """This class manages the fields of a Message Type""" 246 | 247 | def __init__(self, name, fields=None, root=None): 248 | super().__init__(root=root) 249 | self.type_name = name 250 | self.fields = dict() 251 | if isinstance(fields, list): 252 | for field in fields: 253 | self.fields[field.field_name] = field 254 | elif isinstance(fields, dict): 255 | self.fields = fields 256 | 257 | def add_field(self, field): 258 | """Add a field with or without rules to a Message Type""" 259 | field.path = self.path.child_path(field.field_name) 260 | field.root = self.root 261 | self.fields[field.field_name] = field 262 | return field 263 | 264 | def get_field(self, field_name): 265 | return self.fields[field_name] 266 | 267 | def __getitem__(self, field_name): 268 | return self.get_field(field_name) 269 | 270 | def __repr__(self): 271 | return ( 272 | f"{self.type_name}:" 273 | + f"MessageType({len(self.fields)}):{self.fields}," 274 | + f"Nested types({len(self.nested_types)})" 275 | + (":" + ",".join(self.nested_types.keys()) if self.nested_types else "") 276 | ) 277 | 278 | 279 | class FieldRules(OSIRuleNode): 280 | """This class manages the rules of a Field in a Message Type""" 281 | 282 | def __init__(self, name, rules=None, path=None, root=None): 283 | super().__init__() 284 | self.rules = dict() 285 | self.field_name = name 286 | 287 | if path: 288 | self.path = path 289 | 290 | if root: 291 | self.root = root 292 | 293 | if isinstance(rules, list): 294 | for rule in rules: 295 | self.add_rule(rule) 296 | 297 | def add_rule(self, rule): 298 | """Add a new rule to a field with the parameters. 299 | For example: 300 | 301 | .. code-block:: python 302 | 303 | self.add_rule(Rule(verb="is_less_than_or_equal_to", params=2)) 304 | 305 | The rule can also be a dictionary containing one key (the rule verb) with one 306 | value (the parameter). 307 | For example: 308 | 309 | .. code-block:: python 310 | 311 | self.add_rule({"is_less_than_or_equal_to": 2}) 312 | """ 313 | rule.path = self.path.child_path(rule.verb) 314 | rule.root = self.root 315 | self.rules[rule.verb] = rule 316 | 317 | def has_rule(self, rule): 318 | """Check if a field has the rule ``rule``""" 319 | return rule in self.rules 320 | 321 | def list_rules(self): 322 | """List the rules of a field""" 323 | return self.rules 324 | 325 | def get_rule(self, verb): 326 | """Return the rule object for the verb rule_verb in this field.""" 327 | return self.rules[verb] 328 | 329 | def __getitem__(self, verb): 330 | return self.get_rule(verb) 331 | 332 | def __repr__(self): 333 | nested_rules = [self.rules[r] for r in self.rules] 334 | return f"{self.field_name}:Field({len(self.rules)}):{nested_rules}" 335 | 336 | 337 | class Rule(OSIRuleNode): 338 | """This class manages one rule""" 339 | 340 | def __init__(self, **kwargs): 341 | super().__init__() 342 | self.severity = kwargs.get("severity", Severity.ERROR) 343 | self.path = kwargs.get("path", ProtoMessagePath()) 344 | self.field_name = kwargs.get("field_name") 345 | 346 | self.params = kwargs.get("params", None) 347 | self.extra_params = kwargs.get("extra_params", None) 348 | self.target = kwargs.get("target", None) 349 | self.verb = kwargs.get("verb", None) 350 | 351 | dictionary = kwargs.get("dictionary", None) 352 | if dictionary: 353 | self.from_dict(dictionary) 354 | 355 | if not hasattr(osi_rules_implementations, self.verb): 356 | sys.stderr.write(self.verb + " rule does not exist\n") 357 | 358 | def from_dict(self, rule_dict: dict): 359 | """Instantiate Rule object from a dictionary""" 360 | try: 361 | (verb, params), *extra_params = rule_dict.items() 362 | self.verb = verb 363 | self.params = params 364 | self.extra_params = dict(extra_params) 365 | self.target = self.extra_params.pop("target", None) 366 | 367 | return True 368 | except AttributeError: 369 | sys.stderr.write("rule must be YAML mapping, got: " + str(rule_dict) + "\n") 370 | return False 371 | 372 | @property 373 | def path(self): 374 | return self._path 375 | 376 | @property 377 | def targeted_field(self): 378 | if self.target: 379 | return self.target.split(".")[-1] 380 | return self.field_name 381 | 382 | @path.setter 383 | def path(self, path): 384 | self._path = path 385 | if len(self.path.path) >= 2 and isinstance(self.path, ProtoMessagePath): 386 | self.field_name = self.path.path[-2] 387 | elif not hasattr(self, "field_name"): 388 | self.field_name = "UnknownField" 389 | 390 | def __repr__(self): 391 | return f"{self.verb}({self.params}) target={self.target}" 392 | 393 | def __eq__(self, other): 394 | return ( 395 | self.verb == other.verb 396 | and self.params == other.params 397 | and self.severity == other.severity 398 | ) 399 | 400 | 401 | class Severity(Enum): 402 | """Description of the severity of the raised error if a rule does not comply.""" 403 | 404 | INFO = 20 405 | WARN = 30 406 | ERROR = 40 407 | -------------------------------------------------------------------------------- /osivalidator/osi_rules_checker.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all the rules which a message or an attribute of a message 3 | from an OSI trace can comply. 4 | """ 5 | 6 | from types import MethodType 7 | import os, sys 8 | 9 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 10 | 11 | import osi_rules 12 | import osi_validator_logger 13 | import osi_id_manager 14 | import osi_rules_implementations 15 | 16 | 17 | class OSIRulesChecker: 18 | """This class contains all the available rules to write OSI requirements and 19 | the necessary methods to check their compliance. 20 | 21 | The rule methods are marked with \*Rule\*. 22 | """ 23 | 24 | def __init__(self, logger=None): 25 | self.logger = logger or osi_validator_logger.OSIValidatorLogger() 26 | self.id_manager = osi_id_manager.OSIIDManager(logger) 27 | self.timestamp = self.timestamp_ns = -1 28 | 29 | for module_name in dir(osi_rules_implementations): 30 | method = getattr(osi_rules_implementations, module_name) 31 | if getattr(method, "is_rule", False): 32 | setattr(self, module_name, MethodType(method, self)) 33 | 34 | # Rules implementation 35 | def log(self, severity, message): 36 | """ 37 | Wrapper for the logger of the Validation Software 38 | """ 39 | if isinstance(severity, osi_rules.Severity): 40 | severity_method = osi_validator_logger.SEVERITY[severity] 41 | elif isinstance(severity, str): 42 | severity_method = severity 43 | else: 44 | raise TypeError("type not accepted: must be Severity enum or str") 45 | 46 | return getattr(self.logger, severity_method)(self.timestamp, message) 47 | 48 | def set_timestamp(self, timestamp, ts_id): 49 | """Set the timestamp for the analysis""" 50 | self.timestamp_ns = int(timestamp.nanos + timestamp.seconds * 10e9) 51 | self.timestamp = ts_id 52 | return self.timestamp, ts_id 53 | 54 | def check_rule(self, parent_field, rule): 55 | """Check if a field comply with a rule given the \*parent\* field""" 56 | try: 57 | rule_method = getattr(self, rule.verb) 58 | except AttributeError: 59 | raise AttributeError("Rule " + rule.verb + " not implemented yet\n") 60 | 61 | if rule.target is not None: 62 | parent_field = parent_field.query(rule.target, parent=True) 63 | 64 | if getattr(rule_method, "pre_check", False): 65 | # We do NOT know if the child exists 66 | checked_field = parent_field 67 | elif parent_field.has_field(rule.targeted_field): 68 | # We DO know that the child exists 69 | checked_field = parent_field.get_field(rule.targeted_field) 70 | else: 71 | checked_field = None 72 | 73 | if checked_field is not None: 74 | return rule_method(checked_field, rule) 75 | 76 | return False 77 | -------------------------------------------------------------------------------- /osivalidator/osi_rules_implementations.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the implementation of each rule to be used in the 3 | requirements or in the Doxygen documentation. 4 | 5 | All these rules are bounded into "OSIRulesChecker", so they have access to all 6 | its attributes and methods. 7 | """ 8 | 9 | from functools import wraps 10 | from iso3166 import countries 11 | import os, sys 12 | 13 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 14 | import osi_rules 15 | 16 | 17 | def add_default_rules_to_subfields(message, type_rules): 18 | """Add default rules to fields of message fields (subfields)""" 19 | for descriptor in message.all_field_descriptors: 20 | field_rules = ( 21 | type_rules.get_field(descriptor.name) 22 | if descriptor.name in type_rules.fields 23 | else type_rules.add_field(osi_rules.FieldRules(descriptor.name)) 24 | ) 25 | 26 | if descriptor.message_type: 27 | field_rules.add_rule(osi_rules.Rule(verb="check_children")) 28 | 29 | 30 | # DECORATORS 31 | # These functions are no rule implementation, but decorators to characterize 32 | # rules. These decorators can also be used to make grouped checks like 33 | # benchmarks. 34 | 35 | 36 | def pre_check(func): 37 | """Decorator for rules that need to be checked before knowing that the field 38 | exists or not 39 | """ 40 | func.pre_check = True 41 | return func 42 | 43 | 44 | def repeated_selector(func): 45 | """Decorator for selector-rules that take""" 46 | func.repeated_selector = True 47 | return func 48 | 49 | 50 | def rule_implementation(func): 51 | """Decorator to label rules method implementations""" 52 | func.is_rule = True 53 | 54 | @wraps(func) 55 | def wrapper(self, field, rule, **kwargs): 56 | if isinstance(rule, osi_rules.FieldRules): 57 | rule = rule.rules[func.__name__] 58 | 59 | if isinstance(field, list) and not getattr(func, "repeated_selector", False): 60 | result = all([func(self, unique_field, rule) for unique_field in field]) 61 | else: 62 | result = func(self, field, rule, **kwargs) 63 | 64 | if not result and isinstance(rule, osi_rules.Rule): 65 | if isinstance(field, list): 66 | path = field[0].path 67 | else: 68 | path = field.path 69 | self.log( 70 | rule.severity, 71 | str(rule.path) 72 | + "(" 73 | + str(rule.params) 74 | + ")" 75 | + " does not comply in " 76 | + str(path), 77 | ) 78 | 79 | return result 80 | 81 | return wrapper 82 | 83 | 84 | # RULES 85 | # TODO Refactor this code into a seperate class so it can be easy parsed by sphinx 86 | 87 | # Special type matching for paths 88 | type_match = { 89 | "moving_object.base": "BaseMoving", 90 | "stationary_object.base": "BaseStationary", 91 | } 92 | 93 | 94 | @rule_implementation 95 | def check_children(self, field, rule): 96 | """Check if a field message is valid, that is all the inner rules of the 97 | message in the field are complying. 98 | 99 | :param params: none 100 | """ 101 | path_list = field.path.split(".") 102 | type_structure = None 103 | if len(path_list) > 2: 104 | grandparent = path_list[-3] 105 | parent = path_list[-2] 106 | child = path_list[-1] 107 | type_structure = type_match.get(grandparent + "." + parent) 108 | 109 | if type_structure and child in rule.root.get_type(type_structure).nested_types: 110 | subfield_rules = rule.root.get_type(type_structure).nested_types[child] 111 | # subfield_rules = rule.root.get_type(field.message_type) 112 | elif isinstance(rule, osi_rules.MessageTypeRules): 113 | subfield_rules = rule 114 | else: 115 | subfield_rules = rule.root.get_type(field.message_type) 116 | 117 | # Add default rules for each subfield that can be validated (default) 118 | add_default_rules_to_subfields(field, subfield_rules) 119 | 120 | # loop over the fields in the rules 121 | for subfield_rules in subfield_rules.fields.values(): 122 | for subfield_rule in subfield_rules.rules.values(): 123 | self.check_rule(field, subfield_rule) 124 | 125 | # Resolve ID and references 126 | if not field.parent: 127 | self.id_manager.resolve_unicity(self.timestamp) 128 | self.id_manager.resolve_references(self.timestamp) 129 | return True 130 | 131 | 132 | @rule_implementation 133 | def is_less_than_or_equal_to(self, field, rule): 134 | """Check if a number is under or equal a maximum. 135 | 136 | :param params: the maximum (float) 137 | """ 138 | return field.value <= rule.params 139 | 140 | 141 | @rule_implementation 142 | def is_less_than(self, field, rule): 143 | """Check if a number is under a maximum. 144 | 145 | :param params: the maximum (float) 146 | """ 147 | return field.value < rule.params 148 | 149 | 150 | @rule_implementation 151 | def is_greater_than_or_equal_to(self, field, rule): 152 | """Check if a number is over or equal a minimum. 153 | 154 | :param params: the minimum (float) 155 | """ 156 | return field.value >= rule.params 157 | 158 | 159 | @rule_implementation 160 | def is_greater_than(self, field, rule): 161 | """Check if a number is over a minimum. 162 | 163 | :param params: the minimum (float) 164 | """ 165 | return field.value > rule.params 166 | 167 | 168 | @rule_implementation 169 | def is_equal_to(self, field, rule): 170 | """Check if a number equals the parameter. 171 | 172 | :param params: the equality to check (float or bool) 173 | 174 | Example: 175 | ``` 176 | - is_equal_to: 1 177 | ``` 178 | """ 179 | return field.value == rule.params 180 | 181 | 182 | @rule_implementation 183 | def is_different_to(self, field, rule): 184 | """Check if a number is different from the parameter. 185 | 186 | :param params: the inequality to check (float or bool) 187 | 188 | Example: 189 | ``` 190 | - is_different_to: 1 191 | ``` 192 | """ 193 | return field.value != rule.params 194 | 195 | 196 | @rule_implementation 197 | def is_globally_unique(self, field, rule): 198 | """Register an ID in the OSI ID manager to later perform a ID 199 | consistency validation. 200 | 201 | Must be set to an Identifier. 202 | 203 | :param params: none 204 | """ 205 | 206 | object_of_id = field.parent.value 207 | identifier = field.value.value 208 | 209 | return self.id_manager.register_message(identifier, object_of_id) 210 | 211 | 212 | @rule_implementation 213 | def refers_to(self, field, rule): 214 | """Add a reference to another message by ID. 215 | 216 | :param params: Type name of the referred object (string) 217 | """ 218 | expected_type = rule.params 219 | 220 | referer = field.parent.value 221 | identifier = field.value.value 222 | condition = None 223 | self.id_manager.refer(referer, identifier, expected_type, condition) 224 | return True 225 | 226 | 227 | @rule_implementation 228 | def is_iso_country_code(self, field, rule): 229 | """Check if a string is a ISO country code. 230 | 231 | :param params: none 232 | """ 233 | iso_code = field 234 | try: 235 | countries.get(iso_code) 236 | return True 237 | except KeyError: 238 | return False 239 | 240 | 241 | @rule_implementation 242 | @repeated_selector 243 | def first_element(self, field, rule): 244 | """Check rule for first message of a repeated field. 245 | 246 | :param params: dictionary of rules to be checked for the first message 247 | (mapping) 248 | """ 249 | statement_true = True 250 | nested_fields_rules = rule.params 251 | 252 | # Convert parsed yaml file to dictonary rules 253 | nested_fields_rules_list = [] 254 | for key_field, nested_rule in nested_fields_rules.items(): 255 | nested_rule[0].update({"target": "this." + key_field}) 256 | nested_fields_rules_list.append(nested_rule[0]) 257 | 258 | rules_checker_list = [] 259 | for nested_fields_rule in nested_fields_rules_list: 260 | statement_rule = osi_rules.Rule( 261 | dictionary=nested_fields_rule, 262 | field_name=rule.field_name, 263 | severity=osi_rules.Severity.ERROR, 264 | ) 265 | statement_rule.path = rule.path.child_path(statement_rule.verb) 266 | statement_true = self.check_rule(field[0], statement_rule) and statement_true 267 | rules_checker_list.append(statement_true) 268 | 269 | return all(rules_checker_list) 270 | 271 | 272 | @rule_implementation 273 | @repeated_selector 274 | def last_element(self, field, rule): 275 | """Check rule for last message of a repeated field. 276 | 277 | :param field: Field to which the rule needs to comply 278 | :param rule: dictionary of rules to be checked for the last message 279 | (mapping) 280 | """ 281 | statement_true = True 282 | nested_fields_rules = rule.params 283 | 284 | # Convert parsed yaml file to dictonary rules 285 | nested_fields_rules_list = [] 286 | for key_field, nested_rule in nested_fields_rules.items(): 287 | nested_rule[0].update({"target": "this." + key_field}) 288 | nested_fields_rules_list.append(nested_rule[0]) 289 | 290 | rules_checker_list = [] 291 | for nested_fields_rule in nested_fields_rules_list: 292 | statement_rule = osi_rules.Rule( 293 | dictionary=nested_fields_rule, 294 | field_name=rule.field_name, 295 | severity=osi_rules.Severity.ERROR, 296 | ) 297 | statement_rule.path = rule.path.child_path(statement_rule.verb) 298 | statement_true = self.check_rule(field[-1], statement_rule) and statement_true 299 | rules_checker_list.append(statement_true) 300 | 301 | return all(rules_checker_list) 302 | 303 | 304 | @rule_implementation 305 | def is_optional(self, field, rule): 306 | """This rule set the is_set one on a "Warning" severity. 307 | 308 | :param params: none 309 | """ 310 | return True 311 | 312 | 313 | @rule_implementation 314 | @pre_check 315 | def is_set(self, field, rule): 316 | """Check if a field is set or if a repeated field has at least one element. 317 | 318 | :param params: none 319 | """ 320 | return field.has_field(rule.field_name) 321 | 322 | 323 | @rule_implementation 324 | @pre_check 325 | def check_if(self, field, rule): 326 | """ 327 | Evaluate rules if some statements are verified: 328 | 329 | :param params: statements 330 | :param extra_params: `do_check`: rules to validate if statements are true 331 | 332 | Structure: 333 | 334 | a_field: 335 | - check_if: 336 | {params: statements} 337 | do_check: 338 | {extra_params: rules to validate if statements are true} 339 | 340 | 341 | Example: 342 | 343 | a_field: 344 | - check_if: 345 | - is_set: # Statements 346 | target: parent.environment.temperature 347 | - another_statement: statement parameter 348 | do_check: # Check that will be performed only if the statements are True 349 | - is_less_than_or_equal_to: 0.5 350 | - is_greater_than_or_equal_to: 0 351 | 352 | """ 353 | statements = rule.params 354 | do_checks = rule.extra_params["do_check"] 355 | statement_true = True 356 | 357 | # Check if all the statements are true 358 | for statement in statements: 359 | statement_rule = osi_rules.Rule( 360 | dictionary=statement, 361 | field_name=rule.field_name, 362 | severity=osi_rules.Severity.INFO, 363 | ) 364 | statement_rule.path = rule.path.child_path(statement_rule.verb) 365 | statement_true = self.check_rule(field, statement_rule) and statement_true 366 | 367 | # If the statements are true, check the do_check rules 368 | if not statement_true: 369 | return True 370 | 371 | return all( 372 | ( 373 | self.check_rule( 374 | field, 375 | osi_rules.Rule( 376 | path=rule.path.child_path(next(iter(check.keys()))), 377 | dictionary=check, 378 | field_name=rule.field_name, 379 | ), 380 | ) 381 | for check in do_checks 382 | ) 383 | ) 384 | -------------------------------------------------------------------------------- /osivalidator/osi_validator_logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module which contains OSIValidatorLogger and some logging filters who wrap the 3 | Python logging module. 4 | """ 5 | 6 | import logging 7 | import time 8 | 9 | import itertools 10 | import textwrap 11 | from tabulate import tabulate 12 | 13 | from functools import wraps 14 | 15 | import os, sys 16 | 17 | sys.path.append(os.path.join(os.path.dirname(__file__), ".")) 18 | import osi_rules 19 | 20 | 21 | def log(func): 22 | """Wrapper for logging function""" 23 | 24 | @wraps(func) 25 | def wrapper(self, timestamp, msg, *args, **kwargs): 26 | if timestamp not in self.log_messages: 27 | self.log_messages[timestamp] = [] 28 | return func(self, timestamp, msg, *args, **kwargs) 29 | 30 | return wrapper 31 | 32 | 33 | class WarningFilter(logging.Filter): 34 | """Filter for the logger which take INFO and WARNING messages""" 35 | 36 | def filter(self, record): 37 | return record.levelno in [20, 30] 38 | 39 | 40 | class ErrorFilter(logging.Filter): 41 | """Filter for the logger which take INFO and ERROR messages""" 42 | 43 | def filter(self, record): 44 | return record.levelno in [20, 40] 45 | 46 | 47 | class InfoFilter(logging.Filter): 48 | """Filter which only take INFO messages""" 49 | 50 | def filter(self, record): 51 | return record.levelno == 20 52 | 53 | 54 | class OSIValidatorLogger: 55 | """Wrapper for the Python logger""" 56 | 57 | def __init__(self, debug=False): 58 | self.log_messages = dict() 59 | self.debug_messages = dict() 60 | self.debug_mode = debug 61 | self.logger = logging.getLogger(__name__) 62 | self.formatter = logging.Formatter("%(levelname)-7s -- %(message)s") 63 | self.logger.setLevel(logging.DEBUG if debug else logging.INFO) 64 | self._is_cli_output_set = False 65 | self.conn = None 66 | self.dbname = None 67 | 68 | def init_cli_output(self, verbose): 69 | """Initialize the CLI output""" 70 | if self._is_cli_output_set: 71 | self.logger.handlers.pop() 72 | if verbose: 73 | handler_all = logging.StreamHandler(sys.stdout) 74 | handler_all.setFormatter(self.formatter) 75 | handler_all.setLevel(logging.DEBUG) 76 | self.logger.addHandler(handler_all) 77 | else: 78 | # If verbose mode is OFF, only log INFOS 79 | handler_info = logging.StreamHandler(sys.stdout) 80 | handler_info.setFormatter(self.formatter) 81 | handler_info.addFilter(InfoFilter()) 82 | handler_info.setLevel(logging.INFO) 83 | self.logger.addHandler(handler_info) 84 | self._is_cli_output_set = True 85 | 86 | def init(self, debug, verbose, output_path, files=False): 87 | """Initialize the OSI Validator Logger. Useful to reinitialize the object.""" 88 | self.debug_mode = debug 89 | self.init_logging_storage(files, output_path) 90 | self.init_cli_output(verbose) 91 | 92 | def init_logging_storage(self, files, output_path): 93 | """Initialize (create or set handler) for the specified logging storage""" 94 | timestamp = time.time() 95 | self._init_logging_to_files(timestamp, output_path) 96 | 97 | def _init_logging_to_files(self, timestamp, output_path): 98 | # Add handlers for files 99 | error_file_path = os.path.join(output_path, f"error_{timestamp}.log") 100 | warning_file_path = os.path.join(output_path, f"warn_{timestamp}.log") 101 | 102 | # Log errors in a file 103 | handler_error = logging.FileHandler(error_file_path, mode="a", encoding="utf-8") 104 | 105 | # Log warnings in another file 106 | handler_warning = logging.FileHandler( 107 | warning_file_path, mode="a", encoding="utf-8" 108 | ) 109 | 110 | # Set formatters 111 | handler_error.setFormatter(self.formatter) 112 | handler_warning.setFormatter(self.formatter) 113 | 114 | # Filter 115 | handler_error.addFilter(ErrorFilter()) 116 | handler_warning.addFilter(WarningFilter()) 117 | 118 | # Set level to DEBUG 119 | handler_error.setLevel(logging.DEBUG) 120 | handler_warning.setLevel(logging.DEBUG) 121 | 122 | self.logger.addHandler(handler_error) 123 | self.logger.addHandler(handler_warning) 124 | 125 | @log 126 | def debug(self, timestamp, msg, *args, **kwargs): 127 | """Wrapper for python debug logger""" 128 | if self.debug_mode: 129 | self.debug_messages[timestamp].append((10, timestamp, msg)) 130 | msg = "[TS " + str(timestamp) + "]" + msg 131 | return self.logger.debug(msg, *args, **kwargs) 132 | 133 | @log 134 | def warning(self, timestamp, msg, *args, **kwargs): 135 | """Wrapper for python warning logger""" 136 | self.log_messages[timestamp].append((30, timestamp, msg)) 137 | msg = "[TS " + str(timestamp) + "]" + msg 138 | return self.logger.warning(msg, *args, **kwargs) 139 | 140 | @log 141 | def error(self, timestamp, msg, *args, **kwargs): 142 | """Wrapper for python error logger""" 143 | self.log_messages[timestamp].append((40, timestamp, msg)) 144 | msg = "[TS " + str(timestamp) + "]" + msg 145 | return self.logger.error(msg, *args, **kwargs) 146 | 147 | @log 148 | def info(self, timestamp, msg, *args, **kwargs): 149 | """Wrapper for python info logger""" 150 | if kwargs.get("pass_to_logger"): 151 | return self.logger.info(msg, *args, **kwargs) 152 | return 0 153 | 154 | def synthetize_results(self, messages): 155 | """Aggregate the sqlite log and output a synthetized version of the 156 | result""" 157 | 158 | def ranges(i): 159 | group = itertools.groupby(enumerate(i), lambda x_y: x_y[1] - x_y[0]) 160 | for _, second in group: 161 | second = list(second) 162 | yield second[0][1], second[-1][1] 163 | 164 | def format_ranges(ran): 165 | if ran[0] == ran[1]: 166 | return str(ran[0]) 167 | return f"[{ran[0]}, {ran[1]}]" 168 | 169 | def process_timestamps(distinct_messages): 170 | results = [] 171 | range_dict = {message[2]: [] for message in distinct_messages} 172 | for message in distinct_messages: 173 | if not message[1] in range_dict[message[2]]: 174 | range_dict[message[2]].append(message[1]) 175 | 176 | for message_key, timestamps in range_dict.items(): 177 | # Timestamps need to be sorted before the ranges can be determined 178 | ts_ranges = ", ".join(map(format_ranges, ranges(sorted(timestamps)))) 179 | results.append( 180 | [wrapper_ranges.fill(ts_ranges), wrapper.fill(message_key)] 181 | ) 182 | return results 183 | 184 | wrapper_ranges = textwrap.TextWrapper(width=40) 185 | wrapper = textwrap.TextWrapper(width=200) 186 | return print_synthesis("Warnings", process_timestamps(messages)) 187 | 188 | 189 | def print_synthesis(title, ranges_messages_table): 190 | """Print the (range, messages) table in a nice way, precessed with title and 191 | the number of messages""" 192 | headers = ["Ranges of timestamps", "Message"] 193 | title_string = title + " (" + str(len(ranges_messages_table)) + ") " 194 | table_string = tabulate(ranges_messages_table, headers=headers) 195 | print(title_string) 196 | print(table_string) 197 | return title_string + "\n" + table_string 198 | 199 | 200 | SEVERITY = { 201 | osi_rules.Severity.INFO: "info", 202 | osi_rules.Severity.ERROR: "error", 203 | osi_rules.Severity.WARN: "warning", 204 | } 205 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm>=4.66.1 2 | yamale>=5.0.0 3 | tabulate>=0.9.0 4 | ruamel.yaml>=0.18.5 5 | defusedxml>=0.7.1 6 | iso3166>=2.1.1 7 | protobuf>=4.24.4 8 | open-simulation-interface @ git+https://github.com/OpenSimulationInterface/open-simulation-interface.git@master 9 | -------------------------------------------------------------------------------- /requirements_develop.txt: -------------------------------------------------------------------------------- 1 | setuptools>=69.0.3 2 | wheel>=0.42.0 3 | vulture>=2.10 4 | black==23.12.1 5 | -------------------------------------------------------------------------------- /rules2yml.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import re 4 | from glob import * 5 | import os 6 | from ruamel.yaml import YAML 7 | 8 | 9 | def command_line_arguments(): 10 | """Define and handle command line interface""" 11 | 12 | dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 13 | 14 | parser = argparse.ArgumentParser( 15 | description="Export the rules of *.proto files into the *.yml format so it can be used by the validator.", 16 | prog="python3 rules2yml.py", 17 | ) 18 | parser.add_argument( 19 | "--dir", 20 | "-d", 21 | help="Name of the directory where the yml rules will be stored.", 22 | default="rules", 23 | required=False, 24 | type=str, 25 | ) 26 | parser.add_argument( 27 | "--full-osi", 28 | "-f", 29 | help="Add is_set rule to all fields that do not contain it already.", 30 | action="store_true", 31 | required=False, 32 | ) 33 | 34 | return parser.parse_args() 35 | 36 | 37 | def gen_yml_rules(dir_name="rules", full_osi=False): 38 | with open(r"open-simulation-interface/rules.yml") as file: 39 | yaml = YAML(typ="safe") 40 | rules_dict = yaml.load(file) 41 | 42 | if not os.path.exists(dir_name): 43 | os.makedirs(dir_name) 44 | if not os.path.exists(dir_name + "/schema"): 45 | os.makedirs(dir_name + "/schema") 46 | 47 | for file in glob("open-simulation-interface/*.proto*"): 48 | filename = file.split("open-simulation-interface/")[1].split(".proto")[0] 49 | 50 | if os.path.exists(f"{dir_name}/schema/{filename}_schema.yml"): 51 | continue 52 | 53 | with open(f"{dir_name}/schema/{filename}_schema.yml", "a") as schema_file: 54 | with open(file, "rt") as fin: 55 | isEnum = False 56 | numMessage = 0 57 | saveStatement = "" 58 | prevMainField = False # boolean, that the previous field has children 59 | 60 | for line in fin: 61 | if file.find(".proto") != -1: 62 | # Search for comment ("//"). 63 | matchComment = re.search("//", line) 64 | if matchComment is not None: 65 | statement = line[: matchComment.start()] 66 | else: 67 | statement = line 68 | 69 | # Add part of the statement from last line. 70 | statement = saveStatement + " " + statement 71 | 72 | # New line is not necessary. Remove for a better output. 73 | statement = statement.replace("\n", "") 74 | 75 | # Is statement complete 76 | matchSep = re.search(r"[{};]", statement) 77 | if matchSep is None: 78 | saveStatement = statement 79 | statement = "" 80 | else: 81 | saveStatement = statement[matchSep.end() :] 82 | statement = statement[: matchSep.end()] 83 | 84 | # Search for "enum". 85 | matchEnum = re.search(r"\benum\b", statement) 86 | if matchEnum is not None: 87 | isEnum = True 88 | 89 | # Search for a closing brace. 90 | matchClosingBrace = re.search("}", statement) 91 | if isEnum is True and matchClosingBrace is not None: 92 | isEnum = False 93 | continue 94 | 95 | # Check if not inside an enum. 96 | if isEnum is False: 97 | # Search for "message". 98 | matchMessage = re.search(r"\bmessage\b", statement) 99 | if matchMessage is not None: 100 | # a new message or a new nested message 101 | numMessage += 1 102 | endOfLine = statement[matchMessage.end() :] 103 | matchName = re.search(r"\b\w[\S]*\b", endOfLine) 104 | if matchName is not None and prevMainField is False: 105 | # Check previous main field to exclude empty fields from sensor specific file 106 | matchNameConv = re.search( 107 | r"\b[A-Z][a-zA-Z0-9]*\b", 108 | endOfLine[matchName.start() : matchName.end()], 109 | ) 110 | schema_file.write( 111 | 2 * (numMessage - 1) * " " 112 | + f"{matchNameConv.group(0)}:\n" 113 | ) 114 | prevMainField = True 115 | 116 | elif re.search(r"\bextend\b", statement) is not None: 117 | # treat extend as message 118 | numMessage += 1 119 | 120 | # Search for a closing brace. 121 | matchClosingBrace = re.search("}", statement) 122 | if numMessage > 0 and matchClosingBrace is not None: 123 | numMessage -= 1 124 | 125 | if matchComment is None and len(saveStatement) == 0: 126 | if numMessage > 0 or isEnum == True: 127 | if statement.find(";") != -1: 128 | field = statement.strip().split()[2] 129 | schema_file.write( 130 | (2 * numMessage) * " " 131 | + f"{field}: any(list(include('rules', required=False)), null(), required=False)\n" 132 | ) 133 | prevMainField = False 134 | schema_file.write( 135 | "---\n" 136 | "rules:\n" 137 | " is_greater_than: num(required=False)\n" 138 | " is_greater_than_or_equal_to: num(required=False)\n" 139 | " is_less_than_or_equal_to: num(required=False)\n" 140 | " is_less_than: num(required=False)\n" 141 | " is_equal_to: any(num(), bool(), required=False)\n" 142 | " is_different_to: num(required=False)\n" 143 | " is_globally_unique: str(required=False)\n" 144 | " refers_to: str(required=False)\n" 145 | " is_iso_country_code: str(required=False)\n" 146 | " is_set: str(required=False)\n" 147 | " check_if: list(include('rules', required=False),required=False)\n" 148 | " do_check: any(required=False)\n" 149 | " target: any(required=False)\n" 150 | " first_element: any(required=False)\n" 151 | " last_element: any(required=False)" 152 | ) 153 | 154 | for file in glob("open-simulation-interface/*.proto*"): 155 | filename = file.split("open-simulation-interface/")[1].split(".proto")[0] 156 | 157 | if os.path.exists(f"{dir_name}/{filename}.yml"): 158 | continue 159 | 160 | with open(f"{dir_name}/{filename}.yml", "a") as yml_file: 161 | with open(file, "rt") as fin: 162 | isEnum = False 163 | numMessage = 0 164 | shiftCounter = False 165 | saveStatement = "" 166 | prevMainField = False # boolean, that the previous field has children 167 | rules = [] 168 | 169 | for line in fin: 170 | if file.find(".proto") != -1: 171 | # Search for comment ("//"). 172 | matchComment = re.search("//", line) 173 | if matchComment is not None: 174 | statement = line[: matchComment.start()] 175 | comment = line[matchComment.end() :] 176 | else: 177 | statement = line 178 | comment = "" 179 | 180 | # Add part of the statement from last line. 181 | statement = saveStatement + " " + statement 182 | saveStatement = "" 183 | 184 | # New line is not necessary. Remove for a better output. 185 | statement = statement.replace("\n", "") 186 | comment = comment.replace("\n", "") 187 | 188 | # Is statement complete 189 | matchSep = re.search(r"[{};]", statement) 190 | if matchSep is None: 191 | saveStatement = statement 192 | statement = "" 193 | else: 194 | saveStatement = statement[matchSep.end() :] 195 | statement = statement[: matchSep.end()] 196 | 197 | if isEnum is True: 198 | matchName = re.search(r"\b\w[\S:]+\b", statement) 199 | if matchName is not None: 200 | checkName = statement[ 201 | matchName.start() : matchName.end() 202 | ] 203 | 204 | # Search for "enum". 205 | matchEnum = re.search(r"\benum\b", statement) 206 | if matchEnum is not None: 207 | isEnum = True 208 | # print(f"Matched enum {isEnum}") 209 | endOfLine = statement[matchEnum.end() :] 210 | matchName = re.search(r"\b\w[\S]*\b", endOfLine) 211 | if matchName is not None: 212 | # Test case 8: Check name - no special char 213 | matchNameConv = re.search( 214 | r"\b[A-Z][a-zA-Z0-9]*\b", 215 | endOfLine[matchName.start() : matchName.end()], 216 | ) 217 | 218 | # Search for a closing brace. 219 | matchClosingBrace = re.search("}", statement) 220 | if isEnum is True and matchClosingBrace is not None: 221 | isEnum = False 222 | continue 223 | 224 | # Check if not inside an enum. 225 | if isEnum is False: 226 | # Search for "message". 227 | matchMessage = re.search(r"\bmessage\b", statement) 228 | if matchMessage is not None: 229 | # a new message or a new nested message 230 | numMessage += 1 231 | endOfLine = statement[matchMessage.end() :] 232 | matchName = re.search(r"\b\w[\S]*\b", endOfLine) 233 | if matchName is not None and prevMainField is False: 234 | # Test case 10: Check name - no special char - 235 | # start with a capital letter 236 | matchNameConv = re.search( 237 | r"\b[A-Z][a-zA-Z0-9]*\b", 238 | endOfLine[matchName.start() : matchName.end()], 239 | ) 240 | # print(matchNameConv.group(0)) 241 | yml_file.write( 242 | 2 * (numMessage - 1) * " " 243 | + f"{matchNameConv.group(0)}:\n" 244 | ) 245 | prevMainField = True 246 | 247 | elif re.search(r"\bextend\b", statement) is not None: 248 | # treat extend as message 249 | numMessage += 1 250 | 251 | else: 252 | # Check field names 253 | if numMessage > 0: 254 | matchName = re.search(r"\b\w[\S]*\b\s*=", statement) 255 | if matchName is not None: 256 | checkName = statement[ 257 | matchName.start() : matchName.end() - 1 258 | ] 259 | # Check field message type (remove field name) 260 | type = statement.replace(checkName, "") 261 | matchName = re.search(r"\b\w[\S\.]*\s*=", type) 262 | 263 | # Search for a closing brace. 264 | matchClosingBrace = re.search("}", statement) 265 | if numMessage > 0 and matchClosingBrace is not None: 266 | numMessage -= 1 267 | 268 | if matchComment is not None: 269 | if comment != "": 270 | for rulename, ruleregex in rules_dict.items(): 271 | if re.search(ruleregex, comment): 272 | rules.append(comment) 273 | shiftCounter = True 274 | 275 | elif len(saveStatement) == 0: 276 | if numMessage > 0 or isEnum == True: 277 | if statement.find(";") != -1: 278 | field = statement.strip().split()[2] 279 | yml_file.write( 280 | (2 * numMessage) * " " + f"{field}:\n" 281 | ) 282 | prevMainField = False 283 | 284 | # If option --full-osi is enabled: 285 | # Check if is_set is already a rule for the current field, if not, add it. 286 | if full_osi and not any( 287 | "is_set" in rule for rule in rules 288 | ): 289 | yml_file.write( 290 | (2 * numMessage + 2) * " " 291 | + f"- is_set:\n" 292 | ) 293 | 294 | if shiftCounter: 295 | for rule in rules: 296 | rule_list = rule.split() 297 | # Check if syntax 298 | if "check_if" in rule_list: 299 | yml_file.write( 300 | (2 * numMessage + 2) * " " 301 | + f"- {rule_list[0]}:\n" 302 | ) 303 | yml_file.write( 304 | (2 * numMessage + 4) * " " 305 | + f"- {rule_list[2]}: {rule_list[3]}\n" 306 | ) 307 | yml_file.write( 308 | (2 * numMessage + 6) * " " 309 | + f"target: {rule_list[1]}\n" 310 | ) 311 | yml_file.write( 312 | (2 * numMessage + 4) * " " 313 | + f"{rule_list[5]}:\n" 314 | ) 315 | yml_file.write( 316 | (2 * numMessage + 4) * " " 317 | + f"- {rule_list[6]}:\n" 318 | ) 319 | 320 | # First element syntax 321 | elif "first_element" in rule_list: 322 | yml_file.write( 323 | (2 * numMessage + 2) * " " 324 | + f"- {rule_list[0]}:\n" 325 | ) 326 | yml_file.write( 327 | (2 * numMessage + 6) * " " 328 | + f"{rule_list[1]}:\n" 329 | ) 330 | yml_file.write( 331 | (2 * numMessage + 8) * " " 332 | + f"- {rule_list[2]}: {rule_list[3]}\n" 333 | ) 334 | 335 | # Last element syntax 336 | elif "last_element" in rule_list: 337 | yml_file.write( 338 | (2 * numMessage + 2) * " " 339 | + f"- {rule_list[0]}:\n" 340 | ) 341 | yml_file.write( 342 | (2 * numMessage + 6) * " " 343 | + f"{rule_list[1]}:\n" 344 | ) 345 | yml_file.write( 346 | (2 * numMessage + 8) * " " 347 | + f"- {rule_list[2]}: {rule_list[3]}\n" 348 | ) 349 | 350 | # Standalone rules 351 | elif any( 352 | list_item 353 | in [ 354 | "is_globally_unique", 355 | "is_set", 356 | "is_iso_country_code", 357 | ] 358 | for list_item in rule_list 359 | ): 360 | yml_file.write( 361 | (2 * numMessage + 2) * " " 362 | + f"-{rule}:\n" 363 | ) 364 | # Values or parameters of rules 365 | else: 366 | yml_file.write( 367 | (2 * numMessage + 2) * " " 368 | + f"-{rule}\n" 369 | ) 370 | 371 | shiftCounter = False 372 | rules = [] 373 | 374 | 375 | def main(): 376 | # Handling of command line arguments 377 | args = command_line_arguments() 378 | gen_yml_rules(args.dir, args.full_osi) 379 | 380 | 381 | if __name__ == "__main__": 382 | main() 383 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup module of OSI Validation Software 3 | """ 4 | import glob 5 | import sys 6 | import os 7 | import setuptools 8 | 9 | AUTHOR = "BMW AG" 10 | 11 | 12 | if __name__ == "__main__": 13 | with open("README.md", "r") as fh: 14 | README = fh.read() 15 | 16 | python_version = f"{sys.version_info.major}.{sys.version_info.minor}" 17 | data_files_path = os.path.join( 18 | "lib", f"python{python_version}", "site-packages", "rules" 19 | ) 20 | 21 | setuptools.setup( 22 | name="OSI Validation", 23 | version="1.1.0", 24 | author=AUTHOR, 25 | description="Validator for OSI messages", 26 | long_description=README, 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/OpenSimulationInterface/osi-validation", 29 | packages=setuptools.find_packages(), 30 | classifiers=[ 31 | "Programming Language :: Python :: 3.8", 32 | "License :: MPL-2.0", 33 | "Operating System :: OS Independent", 34 | ], 35 | data_files=[ 36 | ( 37 | "open-simulation-interface", 38 | glob.glob("open-simulation-interface/*.proto"), 39 | ), 40 | ( 41 | data_files_path, 42 | glob.glob("rules/*.yml"), 43 | ), 44 | ], 45 | include_package_data=True, 46 | install_requires=[ 47 | "tqdm>=4.66.1", 48 | "tabulate>=0.9.0", 49 | "ruamel.yaml>=0.18.5", 50 | "defusedxml>=0.7.1", 51 | "iso3166>=2.1.1", 52 | "protobuf==4.24.4", 53 | "open-simulation-interface @ git+https://github.com/OpenSimulationInterface/open-simulation-interface.git@v3.7.0-rc1", 54 | ], 55 | entry_points={ 56 | "console_scripts": ["osivalidator=osivalidator.osi_general_validator:main"], 57 | }, 58 | ) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSimulationInterface/osi-validation/c5a77fea085f63d256f1ade4198a6969d640f940/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_osi_general_validator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from osivalidator.osi_general_validator import detect_message_type 3 | 4 | 5 | class TestDetectMessageType(unittest.TestCase): 6 | def test_detect_message_type_sensor_data(self): 7 | path = "path/to/file_sd_123.osi" 8 | message_type = detect_message_type(path) 9 | self.assertEqual(message_type, "SensorData") 10 | 11 | def test_detect_message_type_sensor_view(self): 12 | path = "path/to/file_sv_123.osi" 13 | message_type = detect_message_type(path) 14 | self.assertEqual(message_type, "SensorView") 15 | 16 | def test_detect_message_type_sensor_view_config(self): 17 | path = "path/to/file_svc_123.osi" 18 | message_type = detect_message_type(path) 19 | self.assertEqual(message_type, "SensorViewConfiguration") 20 | 21 | def test_detect_message_type_ground_truth(self): 22 | path = "path/to/file_gt_123.osi" 23 | message_type = detect_message_type(path) 24 | self.assertEqual(message_type, "GroundTruth") 25 | 26 | def test_detect_message_type_traffic_update(self): 27 | path = "path/to/file_tu_123.osi" 28 | message_type = detect_message_type(path) 29 | self.assertEqual(message_type, "TrafficUpdate") 30 | 31 | def test_detect_message_type_traffic_command_update(self): 32 | path = "path/to/file_tcu_123.osi" 33 | message_type = detect_message_type(path) 34 | self.assertEqual(message_type, "TrafficCommandUpdate") 35 | 36 | def test_detect_message_type_traffic_command(self): 37 | path = "path/to/file_tc_123.osi" 38 | message_type = detect_message_type(path) 39 | self.assertEqual(message_type, "TrafficCommand") 40 | 41 | def test_detect_message_type_host_vehicle_data(self): 42 | path = "path/to/file_hvd_123.osi" 43 | message_type = detect_message_type(path) 44 | self.assertEqual(message_type, "HostVehicleData") 45 | 46 | def test_detect_message_type_motion_request(self): 47 | path = "path/to/file_mr_123.osi" 48 | message_type = detect_message_type(path) 49 | self.assertEqual(message_type, "MotionRequest") 50 | 51 | def test_detect_message_type_streaming_update(self): 52 | path = "path/to/file_su_123.osi" 53 | message_type = detect_message_type(path) 54 | self.assertEqual(message_type, "StreamingUpdate") 55 | 56 | def test_detect_message_type_unknown(self): 57 | path = "path/to/unknown_file.osi" 58 | message_type = detect_message_type(path) 59 | self.assertEqual(message_type, "SensorView") 60 | 61 | def test_detect_message_type_empty_path(self): 62 | path = "" 63 | message_type = detect_message_type(path) 64 | self.assertEqual(message_type, "SensorView") 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /tests/test_validation_rules.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | from glob import * 5 | import os 6 | import shutil 7 | import yamale 8 | from osivalidator.osi_rules import ( 9 | Rule, 10 | TypeRulesContainer, 11 | ProtoMessagePath, 12 | OSIRules, 13 | OSIRuleNode, 14 | ) 15 | from rules2yml import gen_yml_rules 16 | 17 | 18 | class TestValidationRules(unittest.TestCase): 19 | 20 | """Test class for OSIValidationRules class""" 21 | 22 | def test_from_directory(self): 23 | """Test import from directory""" 24 | ovr = OSIRules() 25 | ovr.from_yaml_directory("rules") 26 | test_path = ProtoMessagePath(["LaneBoundary", "BoundaryPoint"]) 27 | ovr_container = ovr.rules.get_type(test_path) 28 | self.assertIsInstance(ovr_container.path, ProtoMessagePath) 29 | self.assertEqual(test_path.path, ovr_container.path.path) 30 | 31 | def test_creation_node(self): 32 | """Test creation of a node in OSI rules""" 33 | node = OSIRuleNode("foo") 34 | self.assertEqual(node.path, "foo") 35 | 36 | def test_add_type_from_path(self): 37 | """Test the adding of a Message type from a path in the rule tree""" 38 | container = TypeRulesContainer() 39 | path = ProtoMessagePath(["foo", "bar", "type"]) 40 | container.add_type_from_path(path) 41 | typecontainer = container.get_type(path) 42 | self.assertIsInstance(typecontainer.path, ProtoMessagePath) 43 | self.assertEqual(path.path, typecontainer.path.path) 44 | 45 | def test_parse_yaml(self): 46 | """Test the YAML parsing""" 47 | raw = """ 48 | HostVehicleData: 49 | location: 50 | - is_set: 51 | location_rmse: 52 | - is_set: 53 | """ 54 | validation_rules = OSIRules() 55 | validation_rules.from_yaml(raw) 56 | rules = validation_rules.rules 57 | field = rules["HostVehicleData"].get_field("location") 58 | rule_check = Rule( 59 | verb="is_set", 60 | field_name="location", 61 | path=ProtoMessagePath(["HostVehicleData", "location", "is_set"]), 62 | ) 63 | 64 | self.assertEqual(field["is_set"], rule_check) 65 | 66 | def test_yaml_generation(self): 67 | gen_yml_rules("unit_test_rules/") 68 | 69 | num_proto_files = len(glob("open-simulation-interface/*.proto*")) 70 | num_rule_files = len(glob("unit_test_rules/*.yml")) 71 | num_rule_schema_files = len(glob("unit_test_rules/schema/*.yml")) 72 | self.assertEqual(num_proto_files, num_rule_files) 73 | self.assertEqual(num_rule_files, num_rule_schema_files) 74 | 75 | # clean up 76 | if os.path.isdir("unit_test_rules"): 77 | shutil.rmtree("unit_test_rules") 78 | 79 | def test_yaml_schema_fail(self): 80 | gen_yml_rules("unit_test_rules/") 81 | 82 | # alter exemplary rule for fail check 83 | raw_sensorspecific = """RadarSpecificObjectData: 84 | rcs: 85 | LidarSpecificObjectData: 86 | maximum_measurement_distance_sensor: 87 | - is_greater_than_or_equal_to: 0 88 | probability: 89 | - is_less_than_or_equal_to: x 90 | - is_greater_than_or_equal_to: 0 91 | trilateration_status: 92 | trend: 93 | signalway: 94 | Signalway: 95 | sender_id: 96 | receiver_id: 97 | """ 98 | 99 | os.remove("unit_test_rules/osi_sensorspecific.yml") 100 | with open("unit_test_rules/osi_sensorspecific.yml", "w") as rule_file: 101 | rule_file.write(raw_sensorspecific) 102 | 103 | validation_rules = OSIRules() 104 | validation_output = validation_rules.validate_rules_yml( 105 | "unit_test_rules/osi_sensorspecific.yml" 106 | ) 107 | self.assertEqual(validation_output, False) 108 | 109 | # clean up 110 | if os.path.isdir("unit_test_rules"): 111 | shutil.rmtree("unit_test_rules") 112 | 113 | 114 | if __name__ == "__main__": 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /tests/tests_rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSimulationInterface/osi-validation/c5a77fea085f63d256f1ade4198a6969d640f940/tests/tests_rules/__init__.py -------------------------------------------------------------------------------- /tests/tests_rules/test_check_if.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for test class of Check-if rule implementation 3 | 4 | Test-tree: 5 | 6 | Vector3d 7 | | 8 | ------------- 9 | | | | 10 | x y z 11 | 1 2 None 12 | 13 | Complying tests: 14 | - if y == 3, check x == 2 15 | - if y == 2, check x == 1 16 | - if y == 2, check x is_set == True 17 | 18 | Not complying test: 19 | - if y == 2, check x == 2 20 | - if y == 2, check z is_set == False 21 | 22 | """ 23 | 24 | import unittest 25 | from osivalidator.linked_proto_field import LinkedProtoField 26 | from osivalidator.osi_rules import Rule 27 | from osivalidator.osi_rules_checker import OSIRulesChecker 28 | 29 | from osi3.osi_common_pb2 import Vector3d 30 | 31 | 32 | class TestCheckIf(unittest.TestCase): 33 | """Test class of OSI Rule check_if class""" 34 | 35 | def setUp(self): 36 | self.FRC = OSIRulesChecker() 37 | 38 | pb_VECTOR3D = Vector3d() 39 | pb_VECTOR3D.x = 1 40 | pb_VECTOR3D.y = 2 41 | self.VECTOR3D = LinkedProtoField(pb_VECTOR3D, "Vector3D") 42 | 43 | def test_comply1(self): 44 | rule = Rule( 45 | verb="check_if", 46 | field_name="x", 47 | params=[{"is_equal_to": 3, "target": "this.y"}], 48 | extra_params={"do_check": [{"is_equal_to": 2}]}, 49 | ) 50 | compliance = self.FRC.check_if(self.VECTOR3D, rule) 51 | self.assertTrue(compliance) 52 | 53 | def test_comply2(self): 54 | rule = Rule( 55 | verb="check_if", 56 | field_name="x", 57 | params=[{"is_equal_to": 2, "target": "this.y"}], 58 | extra_params={"do_check": [{"is_equal_to": 1}]}, 59 | ) 60 | compliance = self.FRC.check_if(self.VECTOR3D, rule) 61 | self.assertTrue(compliance) 62 | 63 | def test_comply_is_set(self): 64 | rule = Rule( 65 | verb="check_if", 66 | field_name="x", 67 | params=[{"is_equal_to": 2, "target": "this.y"}], 68 | extra_params={"do_check": [{"is_set": None}]}, 69 | ) 70 | compliance = self.FRC.check_if(self.VECTOR3D, rule) 71 | self.assertTrue(compliance) 72 | 73 | def test_not_comply(self): 74 | rule = Rule( 75 | verb="check_if", 76 | field_name="x", 77 | params=[{"is_equal_to": 2, "target": "this.y"}], 78 | extra_params={"do_check": [{"is_equal_to": 2}]}, 79 | ) 80 | compliance = self.FRC.check_if(self.VECTOR3D, rule) 81 | self.assertFalse(compliance) 82 | 83 | def test_not_comply_is_set_if(self): 84 | rule = Rule( 85 | verb="check_if", 86 | field_name="z", 87 | params=[{"is_equal_to": 2, "target": "this.y"}], 88 | extra_params={"do_check": [{"is_set": None}]}, 89 | ) 90 | compliance = self.FRC.check_if(self.VECTOR3D, rule) 91 | self.assertFalse(compliance) 92 | -------------------------------------------------------------------------------- /tests/tests_rules/test_first_element_of.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osi3.osi_sensorview_pb2 import SensorView 6 | 7 | from osivalidator.osi_rules_checker import OSIRulesChecker 8 | from osivalidator.linked_proto_field import LinkedProtoField 9 | from osivalidator.osi_rules import Rule, TypeRulesContainer, ProtoMessagePath 10 | 11 | 12 | class TestFirstElement(unittest.TestCase): 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | sv1 = SensorView() 17 | linked_sv1 = LinkedProtoField(sv1, name="SensorView") 18 | gt1 = sv1.global_ground_truth 19 | linked_gt1 = LinkedProtoField( 20 | gt1, name="global_ground_truth", parent=linked_sv1 21 | ) 22 | 23 | gtlb1 = gt1.lane_boundary.add() 24 | linked_gtlb1 = LinkedProtoField(gtlb1, name="lane_boundary", parent=linked_gt1) 25 | 26 | bladd1 = gtlb1.boundary_line.add() 27 | bladd1.position.x = 1699.2042678176733 28 | bladd1.position.y = 100.16895580204906 29 | bladd1.position.z = 0.0 30 | bladd1.width = 0.13 31 | bladd1.height = 0.0 32 | 33 | self.lb1 = LinkedProtoField(bladd1, name="boundary_line", parent=linked_gtlb1) 34 | self.lb1.path = "SensorView.global_ground_truth.lane_boundary.boundary_line" 35 | # self.lb1.parent = 36 | 37 | sv2 = SensorView() 38 | linked_sv2 = LinkedProtoField(sv2, name="SensorView") 39 | 40 | gt2 = sv2.global_ground_truth 41 | linked_gt2 = LinkedProtoField( 42 | gt2, name="global_ground_truth", parent=linked_sv2 43 | ) 44 | 45 | gtlb2 = gt2.lane_boundary.add() 46 | linked_gtlb2 = LinkedProtoField(gtlb2, name="lane_boundary", parent=linked_gt2) 47 | 48 | bladd2 = gtlb2.boundary_line.add() 49 | bladd2.position.x = 1699.2042678176733 50 | bladd2.position.y = 100.16895580204906 51 | bladd2.position.z = 0.0 52 | bladd2.width = 0.14 53 | bladd2.height = 0.13 54 | self.lb2 = LinkedProtoField(bladd2, name="boundary_line", parent=linked_gtlb2) 55 | self.lb2.path = "SensorView.global_ground_truth.lane_boundary.boundary_line" 56 | 57 | def tearDown(self): 58 | del self.FRC 59 | 60 | def test_comply_first_element(self): 61 | field_list = [self.lb1, self.lb2] 62 | container = TypeRulesContainer() 63 | proto_path = ProtoMessagePath(["LaneBoundary", "BoundaryPoint"]) 64 | container.add_type_from_path(proto_path) 65 | container.add_type_from_path(ProtoMessagePath(["Vector3d"])) 66 | 67 | rule = Rule( 68 | verb="first_element", 69 | params={"width": [{"is_equal_to": 0.13}], "height": [{"is_equal_to": 0.0}]}, 70 | path=proto_path, 71 | extra_params=dict(), 72 | field_name="boundary_line", 73 | ) 74 | rule.root = container 75 | compliance = self.FRC.first_element(field_list, rule) 76 | self.assertTrue(compliance) 77 | 78 | def test_not_comply_first_element(self): 79 | field_list = [self.lb1, self.lb2] 80 | container = TypeRulesContainer() 81 | proto_path = ProtoMessagePath(["LaneBoundary", "BoundaryPoint"]) 82 | container.add_type_from_path(proto_path) 83 | container.add_type_from_path(ProtoMessagePath(["Vector3d"])) 84 | 85 | rule = Rule( 86 | verb="first_element", 87 | params={ 88 | "width": [{"is_equal_to": 0.11}], 89 | "height": [{"is_equal_to": 0.13}], 90 | }, 91 | path=proto_path, 92 | extra_params=dict(), 93 | field_name="boundary_line", 94 | ) 95 | rule.root = container 96 | compliance = self.FRC.first_element(field_list, rule) 97 | self.assertFalse(compliance) 98 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_different_to.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsDifferentTo(unittest.TestCase): 11 | """Test class of OSIDataContainer class""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply(self): 20 | field = LinkedProtoField(value=3) 21 | rule = Rule(verb="is_different_to", params=2) 22 | compliance = self.FRC.is_different_to(field, rule) 23 | self.assertTrue(compliance) 24 | 25 | def test_not_comply(self): 26 | field = LinkedProtoField(value=2) 27 | rule = Rule(verb="is_different_to", params=2) 28 | compliance = self.FRC.is_different_to(field, rule) 29 | self.assertFalse(compliance) 30 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_equal_to.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsEqual(unittest.TestCase): 11 | """Test class of OSIDataContainer class""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply(self): 20 | field = LinkedProtoField(value=2) 21 | rule = Rule(verb="is_less_than_or_equal_to", params=2) 22 | compliance = self.FRC.is_equal_to(field, rule) 23 | self.assertTrue(compliance) 24 | 25 | def test_not_comply(self): 26 | field = LinkedProtoField(value=3) 27 | rule = Rule(verb="is_less_than_or_equal_to", params=2) 28 | compliance = self.FRC.is_equal_to(field, rule) 29 | self.assertFalse(compliance) 30 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_globally_unique.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | from osivalidator.osi_rules_checker import OSIRulesChecker 5 | from osi3.osi_sensorview_pb2 import SensorView 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule, TypeRulesContainer, ProtoMessagePath 8 | 9 | 10 | class TestIsGlobalUnique(unittest.TestCase): 11 | def setUp(self): 12 | self.FRC = OSIRulesChecker() 13 | 14 | sv = SensorView() 15 | linked_sv = LinkedProtoField(sv, name="SensorView") 16 | sid = sv.sensor_id 17 | sid.value = 0 18 | self.linked_sid = LinkedProtoField(sid, name="sensor_id", parent=linked_sv) 19 | 20 | sv2 = SensorView() 21 | linked_sv2 = LinkedProtoField(sv2, name="SensorView") 22 | sid2 = sv2.sensor_id 23 | sid2.value = 2 24 | self.linked_sid2 = LinkedProtoField(sid2, name="sensor_id", parent=linked_sv2) 25 | 26 | def tearDown(self): 27 | del self.FRC 28 | del self.linked_sid 29 | del self.linked_sid2 30 | 31 | def test_comply_is_globally_unique(self): 32 | """ 33 | Test if the ID Manager has unique indices 34 | """ 35 | container = TypeRulesContainer() 36 | proto_path = ProtoMessagePath(["SensorView", "sensor_id", "is_globally_unique"]) 37 | container.add_type_from_path(proto_path) 38 | 39 | rule = Rule( 40 | verb="is_globally_unique", 41 | field_name="sensor_id", 42 | extra_params=dict(), 43 | path=proto_path, 44 | ) 45 | rule.root = container 46 | self.FRC.is_globally_unique(self.linked_sid, rule) 47 | self.FRC.is_globally_unique(self.linked_sid2, rule) 48 | self.FRC.is_globally_unique(self.linked_sid2, rule) 49 | index_dict = self.FRC.id_manager._index 50 | self.assertEqual(2, len(index_dict)) 51 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_greater_than.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsGreaterThan(unittest.TestCase): 11 | """Test for rule is_greater_than""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply_greater(self): 20 | field_params_rule_params = [ 21 | [2, 1], 22 | [0, -1], 23 | [1, 0], 24 | [1, -1], 25 | [-1, -2], 26 | [-1.3, -1.5], 27 | [0.9, -1.3], 28 | ] 29 | 30 | for fr_param in field_params_rule_params: 31 | with self.subTest(fr_param=fr_param): 32 | self.assertTrue( 33 | self.FRC.is_greater_than( 34 | LinkedProtoField(value=fr_param[0]), 35 | Rule(verb="is_greater_than", params=fr_param[1]), 36 | ) 37 | ) 38 | 39 | def test_not_comply_greater(self): 40 | field_params_rule_params = [ 41 | [2, 1], 42 | [0, -1], 43 | [1, 0], 44 | [1, -1], 45 | [-1, -2], 46 | [-1.3, -1.5], 47 | [0.9, -1.3], 48 | ] 49 | 50 | for fr_param in field_params_rule_params: 51 | with self.subTest(fr_param=fr_param): 52 | self.assertFalse( 53 | self.FRC.is_greater_than( 54 | LinkedProtoField(value=fr_param[1]), 55 | Rule(verb="is_greater_than", params=fr_param[0]), 56 | ) 57 | ) 58 | 59 | def test_not_comply_equal(self): 60 | field_params_rule_params = [[3, 3], [0, 0], [-1, -1], [-1.5, -1.5], [2.3, 2.3]] 61 | 62 | for fr_param in field_params_rule_params: 63 | with self.subTest(fr_param=fr_param): 64 | self.assertFalse( 65 | self.FRC.is_greater_than( 66 | LinkedProtoField(value=fr_param[1]), 67 | Rule(verb="is_greater_than", params=fr_param[0]), 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_greater_than_or_equal_to.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsGreaterThanOrEqualTo(unittest.TestCase): 11 | """Test for rule is_greater_than_or_equal_to""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply_greater(self): 20 | field_params_rule_params = [ 21 | [2, 1], 22 | [0, -1], 23 | [1, 0], 24 | [1, -1], 25 | [-1, -2], 26 | [-1.3, -1.5], 27 | [0.9, -1.3], 28 | ] 29 | 30 | for fr_param in field_params_rule_params: 31 | with self.subTest(fr_param=fr_param): 32 | self.assertTrue( 33 | self.FRC.is_greater_than_or_equal_to( 34 | LinkedProtoField(value=fr_param[0]), 35 | Rule(verb="is_greater_than_or_equal_to", params=fr_param[1]), 36 | ) 37 | ) 38 | 39 | def test_not_comply_greater(self): 40 | field_params_rule_params = [ 41 | [2, 1], 42 | [0, -1], 43 | [1, 0], 44 | [1, -1], 45 | [-1, -2], 46 | [-1.3, -1.5], 47 | [0.9, -1.3], 48 | ] 49 | 50 | for fr_param in field_params_rule_params: 51 | with self.subTest(fr_param=fr_param): 52 | self.assertFalse( 53 | self.FRC.is_greater_than_or_equal_to( 54 | LinkedProtoField(value=fr_param[1]), 55 | Rule(verb="is_greater_than_or_equal_to", params=fr_param[0]), 56 | ) 57 | ) 58 | 59 | def test_comply_equal(self): 60 | field_params_rule_params = [[3, 3], [0, 0], [-1, -1], [-1.5, -1.5], [2.3, 2.3]] 61 | 62 | for fr_param in field_params_rule_params: 63 | with self.subTest(fr_param=fr_param): 64 | self.assertTrue( 65 | self.FRC.is_greater_than_or_equal_to( 66 | LinkedProtoField(value=fr_param[1]), 67 | Rule(verb="is_greater_than_or_equal_to", params=fr_param[0]), 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_iso_country_code.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | from osivalidator.osi_rules_checker import OSIRulesChecker 5 | 6 | 7 | class TestIsIsoCountryCode(unittest.TestCase): 8 | def setUp(self): 9 | self.FRC = OSIRulesChecker() 10 | 11 | def tearDown(self): 12 | del self.FRC 13 | 14 | def test_comply_iso_country_code(self): 15 | self.assertTrue(self.FRC.is_iso_country_code("DEU", None)) 16 | 17 | def test_not_comply_iso_country_code(self): 18 | self.assertFalse(self.FRC.is_iso_country_code("1234", None)) 19 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_less_than.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsLessThan(unittest.TestCase): 11 | """Test for rule is_less_than""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply_less(self): 20 | field_params_rule_params = [ 21 | [2, 1], 22 | [0, -1], 23 | [1, 0], 24 | [1, -1], 25 | [-1, -2], 26 | [-1.3, -1.5], 27 | [0.9, -1.3], 28 | ] 29 | 30 | for fr_param in field_params_rule_params: 31 | with self.subTest(fr_param=fr_param): 32 | self.assertTrue( 33 | self.FRC.is_less_than( 34 | LinkedProtoField(value=fr_param[1]), 35 | Rule(verb="is_less_than", params=fr_param[0]), 36 | ) 37 | ) 38 | 39 | def test_not_comply_less(self): 40 | field_params_rule_params = [ 41 | [2, 1], 42 | [0, -1], 43 | [1, 0], 44 | [1, -1], 45 | [-1, -2], 46 | [-1.3, -1.5], 47 | [0.9, -1.3], 48 | ] 49 | 50 | for fr_param in field_params_rule_params: 51 | with self.subTest(fr_param=fr_param): 52 | self.assertFalse( 53 | self.FRC.is_less_than( 54 | LinkedProtoField(value=fr_param[0]), 55 | Rule(verb="is_less_than", params=fr_param[1]), 56 | ) 57 | ) 58 | 59 | def test_not_comply_equal(self): 60 | field_params_rule_params = [[3, 3], [0, 0], [-1, -1], [-1.5, -1.5], [2.3, 2.3]] 61 | 62 | for fr_param in field_params_rule_params: 63 | with self.subTest(fr_param=fr_param): 64 | self.assertFalse( 65 | self.FRC.is_less_than( 66 | LinkedProtoField(value=fr_param[1]), 67 | Rule(verb="is_less_than", params=fr_param[0]), 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_less_than_or_equal_to.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osivalidator.osi_rules import Rule 8 | 9 | 10 | class TestIsLessThanOrEqualTo(unittest.TestCase): 11 | """Test for rule is_less_than_or_equal_to""" 12 | 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | def tearDown(self): 17 | del self.FRC 18 | 19 | def test_comply_less(self): 20 | field_params_rule_params = [ 21 | [2, 1], 22 | [0, -1], 23 | [1, 0], 24 | [1, -1], 25 | [-1, -2], 26 | [-1.3, -1.5], 27 | [0.9, -1.3], 28 | ] 29 | 30 | for fr_param in field_params_rule_params: 31 | with self.subTest(fr_param=fr_param): 32 | self.assertTrue( 33 | self.FRC.is_less_than_or_equal_to( 34 | LinkedProtoField(value=fr_param[1]), 35 | Rule(verb="is_less_than_or_equal_to", params=fr_param[0]), 36 | ) 37 | ) 38 | 39 | def test_not_comply_less(self): 40 | field_params_rule_params = [ 41 | [2, 1], 42 | [0, -1], 43 | [1, 0], 44 | [1, -1], 45 | [-1, -2], 46 | [-1.3, -1.5], 47 | [0.9, -1.3], 48 | ] 49 | 50 | for fr_param in field_params_rule_params: 51 | with self.subTest(fr_param=fr_param): 52 | self.assertFalse( 53 | self.FRC.is_less_than_or_equal_to( 54 | LinkedProtoField(value=fr_param[0]), 55 | Rule(verb="is_less_than_or_equal_to", params=fr_param[1]), 56 | ) 57 | ) 58 | 59 | def test_comply_equal(self): 60 | field_params_rule_params = [[3, 3], [0, 0], [-1, -1], [-1.5, -1.5], [2.3, 2.3]] 61 | 62 | for fr_param in field_params_rule_params: 63 | with self.subTest(fr_param=fr_param): 64 | self.assertTrue( 65 | self.FRC.is_less_than_or_equal_to( 66 | LinkedProtoField(value=fr_param[1]), 67 | Rule(verb="is_less_than_or_equal_to", params=fr_param[0]), 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_optional.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from osivalidator.osi_rules_checker import OSIRulesChecker 4 | from osivalidator.linked_proto_field import LinkedProtoField 5 | from osivalidator.osi_rules import Rule 6 | 7 | 8 | class TestIsOptional(unittest.TestCase): 9 | """ 10 | This Unit test just checks if the rule is_optional is set to True 11 | because in protobuf 3 every message field is optional 12 | """ 13 | 14 | def setUp(self): 15 | self.FRC = OSIRulesChecker() 16 | 17 | def tearDown(self): 18 | del self.FRC 19 | 20 | def test_comply_is_optional(self): 21 | self.assertTrue( 22 | self.FRC.is_optional( 23 | LinkedProtoField(value=1), Rule(verb="is_optional", params=None) 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /tests/tests_rules/test_is_set.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for test class of is_set rule implementation 3 | 4 | Test-tree: 5 | 6 | Orientation3d 7 | | 8 | ------------- 9 | | | | 10 | roll pitch yaw 11 | 1 None 2 12 | 13 | Complying tests: 14 | - set roll and check is_set == True 15 | - set yaw and check is_set == True 16 | 17 | Not complying test: 18 | - don't set pitch check is_set == False 19 | 20 | """ 21 | 22 | import unittest 23 | 24 | from osivalidator.osi_rules_checker import OSIRulesChecker 25 | from osivalidator.linked_proto_field import LinkedProtoField 26 | from osivalidator.osi_rules import Rule 27 | from osi3.osi_common_pb2 import Orientation3d 28 | 29 | 30 | class TestIsSet(unittest.TestCase): 31 | def setUp(self): 32 | self.FRC = OSIRulesChecker() 33 | 34 | pb_ORIENTATION3D = Orientation3d() 35 | pb_ORIENTATION3D.roll = 1 36 | # pb_ORIENTATION3D.pitch = 2 --> pitch is not set 37 | pb_ORIENTATION3D.yaw = 2 38 | self.ORIENTATION3D = LinkedProtoField(pb_ORIENTATION3D, "Orientation3d") 39 | 40 | def tearDown(self): 41 | del self.FRC 42 | del self.ORIENTATION3D 43 | 44 | def test_comply_is_set(self): 45 | rule = Rule(verb="is_set", field_name="roll") 46 | compliance = self.FRC.is_set(self.ORIENTATION3D, rule) 47 | self.assertTrue(compliance) 48 | 49 | rule = Rule(verb="is_set", field_name="yaw") 50 | compliance = self.FRC.is_set(self.ORIENTATION3D, rule) 51 | self.assertTrue(compliance) 52 | 53 | def test_comply_is_not_set(self): 54 | rule = Rule(verb="is_set", field_name="pitch") 55 | compliance = self.FRC.is_set(self.ORIENTATION3D, rule) 56 | self.assertFalse(compliance) 57 | -------------------------------------------------------------------------------- /tests/tests_rules/test_last_element_of.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osi3.osi_sensorview_pb2 import SensorView 6 | 7 | from osivalidator.osi_rules_checker import OSIRulesChecker 8 | from osivalidator.linked_proto_field import LinkedProtoField 9 | from osivalidator.osi_rules import Rule, TypeRulesContainer, ProtoMessagePath 10 | 11 | 12 | class TestFirstElement(unittest.TestCase): 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | sv1 = SensorView() 17 | linked_sv1 = LinkedProtoField(sv1, name="SensorView") 18 | gt1 = sv1.global_ground_truth 19 | linked_gt1 = LinkedProtoField( 20 | gt1, name="global_ground_truth", parent=linked_sv1 21 | ) 22 | 23 | gtlb1 = gt1.lane_boundary.add() 24 | linked_gtlb1 = LinkedProtoField(gtlb1, name="lane_boundary", parent=linked_gt1) 25 | 26 | bladd1 = gtlb1.boundary_line.add() 27 | bladd1.position.x = 1699.2042678176733 28 | bladd1.position.y = 100.16895580204906 29 | bladd1.position.z = 0.0 30 | bladd1.width = 0.13 31 | bladd1.height = 0.0 32 | 33 | self.lb1 = LinkedProtoField(bladd1, name="boundary_line", parent=linked_gtlb1) 34 | self.lb1.path = "SensorView.global_ground_truth.lane_boundary.boundary_line" 35 | # self.lb1.parent = 36 | 37 | sv2 = SensorView() 38 | linked_sv2 = LinkedProtoField(sv2, name="SensorView") 39 | 40 | gt2 = sv2.global_ground_truth 41 | linked_gt2 = LinkedProtoField( 42 | gt2, name="global_ground_truth", parent=linked_sv2 43 | ) 44 | 45 | gtlb2 = gt2.lane_boundary.add() 46 | linked_gtlb2 = LinkedProtoField(gtlb2, name="lane_boundary", parent=linked_gt2) 47 | 48 | bladd2 = gtlb2.boundary_line.add() 49 | bladd2.position.x = 1699.2042678176733 50 | bladd2.position.y = 100.16895580204906 51 | bladd2.position.z = 0.0 52 | bladd2.width = 0.14 53 | bladd2.height = 0.13 54 | self.lb2 = LinkedProtoField(bladd2, name="boundary_line", parent=linked_gtlb2) 55 | self.lb2.path = "SensorView.global_ground_truth.lane_boundary.boundary_line" 56 | 57 | def tearDown(self): 58 | del self.FRC 59 | 60 | def test_comply_last_element(self): 61 | field_list = [self.lb1, self.lb2] 62 | container = TypeRulesContainer() 63 | proto_path = ProtoMessagePath(["LaneBoundary", "BoundaryPoint"]) 64 | container.add_type_from_path(proto_path) 65 | container.add_type_from_path(ProtoMessagePath(["Vector3d"])) 66 | 67 | rule = Rule( 68 | verb="last_element", 69 | params={ 70 | "width": [{"is_equal_to": 0.14}], 71 | "height": [{"is_equal_to": 0.13}], 72 | }, 73 | path=proto_path, 74 | extra_params=dict(), 75 | field_name="boundary_line", 76 | ) 77 | rule.root = container 78 | compliance = self.FRC.last_element(field_list, rule) 79 | self.assertTrue(compliance) 80 | 81 | def test_not_comply_last_element(self): 82 | field_list = [self.lb1, self.lb2] 83 | container = TypeRulesContainer() 84 | proto_path = ProtoMessagePath(["LaneBoundary", "BoundaryPoint"]) 85 | container.add_type_from_path(proto_path) 86 | container.add_type_from_path(ProtoMessagePath(["Vector3d"])) 87 | 88 | rule = Rule( 89 | verb="last_element", 90 | params={ 91 | "width": [{"is_equal_to": 0.11}], 92 | "height": [{"is_equal_to": 0.13}], 93 | }, 94 | path=proto_path, 95 | extra_params=dict(), 96 | field_name="boundary_line", 97 | ) 98 | rule.root = container 99 | compliance = self.FRC.last_element(field_list, rule) 100 | self.assertFalse(compliance) 101 | -------------------------------------------------------------------------------- /tests/tests_rules/test_refers_to.py: -------------------------------------------------------------------------------- 1 | """Module for test class of OSIValidationRules class""" 2 | 3 | import unittest 4 | 5 | from osivalidator.osi_rules_checker import OSIRulesChecker 6 | from osivalidator.linked_proto_field import LinkedProtoField 7 | from osi3.osi_sensorview_pb2 import SensorView 8 | from osi3.osi_groundtruth_pb2 import GroundTruth 9 | from osivalidator.osi_rules import Rule, ProtoMessagePath, TypeRulesContainer 10 | 11 | 12 | class TestRefersTo(unittest.TestCase): 13 | def setUp(self): 14 | self.FRC = OSIRulesChecker() 15 | 16 | sv1 = SensorView() 17 | linked_sv1 = LinkedProtoField(sv1, name="SensorView") 18 | 19 | gt1 = sv1.global_ground_truth 20 | linked_gt1 = LinkedProtoField( 21 | gt1, name="global_ground_truth", parent=linked_sv1 22 | ) 23 | 24 | gt1.host_vehicle_id.value = 0 25 | hvid1 = gt1.host_vehicle_id 26 | self.linked_hvid1 = LinkedProtoField( 27 | hvid1, name="host_vehicle_id", parent=linked_gt1 28 | ) 29 | 30 | sv2 = SensorView() 31 | linked_sv2 = LinkedProtoField(sv2, name="SensorView") 32 | 33 | gt2 = sv2.global_ground_truth 34 | linked_gt2 = LinkedProtoField( 35 | gt2, name="global_ground_truth", parent=linked_sv2 36 | ) 37 | 38 | gt2.host_vehicle_id.value = 1 39 | hvid1 = gt2.host_vehicle_id 40 | self.linked_hvid2 = LinkedProtoField( 41 | hvid1, name="host_vehicle_id", parent=linked_gt2 42 | ) 43 | 44 | def tearDown(self): 45 | del self.FRC 46 | del self.linked_hvid1 47 | del self.linked_hvid2 48 | 49 | def test_comply_refers_to(self): 50 | """ 51 | Check if the message object is referenced correctly 52 | """ 53 | container = TypeRulesContainer() 54 | proto_path = ProtoMessagePath(["GroundTruth", "host_vehicle_id", "refers_to"]) 55 | container.add_type_from_path(proto_path) 56 | 57 | rule = Rule( 58 | verb="refers_to", 59 | params="MovingObject", 60 | extra_params=dict(), 61 | path=proto_path, 62 | field_name="host_vehicle_id", 63 | ) 64 | rule.root = container 65 | self.FRC.refers_to(self.linked_hvid1, rule) 66 | self.FRC.refers_to(self.linked_hvid2, rule) 67 | self.FRC.refers_to(self.linked_hvid1, rule) 68 | 69 | references_list = self.FRC.id_manager._references 70 | 71 | print(references_list) 72 | 73 | # Check the instance type of the reference 74 | self.assertIsInstance(references_list[0][0], GroundTruth) 75 | 76 | # Check the id assignment of the reference to the object 77 | self.assertEqual(references_list[0][0].host_vehicle_id.value, 0) 78 | self.assertEqual(references_list[0][1], 0) 79 | self.assertEqual(references_list[0][2], "MovingObject") 80 | --------------------------------------------------------------------------------