├── .github ├── CODEOWNERS └── workflows │ ├── python-publish.yml │ └── unit-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── docs └── releasenotes │ └── 0.3.0.rst ├── noxfile.py ├── pyproject.toml ├── setup.py ├── sherlock ├── __init__.py ├── __main__.py ├── complexity.py ├── config.py ├── core.py ├── exceptions.py ├── file_utils.py ├── model.py ├── report │ ├── __init__.py │ ├── html.py │ ├── html.template │ ├── json.py │ └── print.py ├── version.py └── visitor.py └── tests ├── __init__.py ├── atest ├── __init__.py ├── aliased_library │ ├── __init__.py │ ├── test_aliased_library.py │ └── test_data │ │ ├── Library.py │ │ └── test.robot ├── complicated_structure │ ├── __init__.py │ ├── test_complicated_structure.py │ └── test_data │ │ ├── libraries │ │ └── MyLibrary │ │ │ ├── MyLibrary.py │ │ │ └── __init__.py │ │ ├── resources │ │ ├── common │ │ │ └── test.resource │ │ └── test.resource │ │ └── tests │ │ └── test.robot ├── dict_variables │ ├── __init__.py │ ├── test_data │ │ └── test.robot │ └── test_dict_variables.py ├── duplicated_imports │ ├── __init__.py │ ├── test_data │ │ ├── test.resource │ │ └── test.robot │ └── test_duplicated_imports.py ├── duplicated_names_dotted │ ├── __init__.py │ ├── test_data │ │ ├── Library1.py │ │ ├── Library2.py │ │ ├── test.resource │ │ └── test.robot │ └── test_duplicated_names_dotted.py ├── empty_keywords │ ├── __init__.py │ ├── test_data │ │ ├── Library.py │ │ ├── test.resource │ │ └── test.robot │ └── test_empty_keywords.py ├── external_import │ ├── __init__.py │ ├── test_data │ │ ├── ext_libs │ │ │ └── Library.py │ │ └── tests │ │ │ └── test.robot │ └── test_external_import.py ├── external_library │ ├── __init__.py │ ├── test_data │ │ ├── test.robot │ │ └── test_data.txt │ └── test_external_library.py ├── import_with_variable │ ├── __init__.py │ ├── test_data │ │ └── tests │ │ │ ├── a.resource │ │ │ ├── b.resource │ │ │ ├── c.resource │ │ │ ├── test_a.robot │ │ │ └── test_b.robot │ └── test_import_with_variable.py ├── invalid_library │ ├── __init__.py │ ├── test_data │ │ ├── Library.py │ │ ├── LibraryAccessRunning.py │ │ ├── LibraryWithArgs.py │ │ ├── LibraryWithFailingInit.py │ │ └── test.robot │ └── test_invalid_library.py ├── keyword_in_res_and_suite │ ├── __init__.py │ ├── test_data │ │ ├── kw.resource │ │ └── test.robot │ └── test_keyword_in_res_and_suite.py ├── library_from_resource │ ├── __init__.py │ ├── test_data │ │ ├── MyStuff.py │ │ ├── imports.resource │ │ └── test.robot │ └── test_library_from_resource.py ├── multiple_sources │ ├── __init__.py │ ├── test_data │ │ ├── resource1 │ │ │ └── file.resource │ │ ├── resource2 │ │ │ └── file2.resource │ │ └── test.robot │ └── test_multiple_sources.py ├── nested_modules │ ├── __init__.py │ ├── test_data │ │ ├── pages │ │ │ ├── Page1 │ │ │ │ └── __init__.py │ │ │ ├── Page2 │ │ │ │ └── __init__.py │ │ │ └── __init__.py │ │ └── test.robot │ └── test_nested_modules.py ├── pythonpath │ ├── __init__.py │ ├── test_data │ │ ├── nested │ │ │ └── Library.py │ │ └── test.robot │ └── test_python_path.py ├── recursive_import │ ├── __init__.py │ ├── test_data │ │ ├── Library.py │ │ ├── Library2.py │ │ ├── base.resource │ │ ├── sub_resource.resource │ │ └── suite.robot │ └── test_recursive_import.py ├── resolve_variables │ ├── __init__.py │ ├── test_data │ │ ├── Library1.py │ │ ├── Library2.py │ │ └── test.robot │ └── test_resolve_variables.py ├── resource_outside │ ├── __init__.py │ ├── test_data │ │ ├── resource1 │ │ │ └── file.resource │ │ └── tests │ │ │ ├── resource2 │ │ │ └── file2.resource │ │ │ └── test.robot │ └── test_resource_outside.py ├── run_keywords │ ├── __init__.py │ ├── test_data │ │ └── test.robot │ └── test_run_keywords.py ├── serarch_order │ ├── __init__.py │ ├── search_order_1 │ │ ├── a.resource │ │ └── suite.robot │ ├── search_order_2 │ │ ├── a.resource │ │ ├── b.resource │ │ └── suite.robot │ ├── search_order_3 │ │ ├── a.resource │ │ ├── b.resource │ │ └── suite.robot │ ├── search_order_4 │ │ ├── a.resource │ │ ├── b.resource │ │ ├── from_b.resource │ │ └── suite.robot │ ├── search_order_5 │ │ ├── a.resource │ │ ├── b.resource │ │ └── suite.robot │ └── test_search_order.py ├── suite_setups │ ├── __init__.py │ ├── test_data │ │ └── tests │ │ │ ├── __init__.robot │ │ │ ├── resource.robot │ │ │ └── tests.robot │ └── test_suite_setups.py ├── test_example_workflow.py └── very_long_and_short_name │ ├── __init__.py │ ├── test_data │ ├── test.resource │ └── test.robot │ └── test_very_long_and_short_name.py ├── packages ├── stable4 │ └── requirements.txt ├── stable5 │ └── requirements.txt └── stable6 │ └── requirements.txt ├── test_data ├── configs │ ├── empty_pyproject │ │ └── pyproject.toml │ ├── nested_pyproject │ │ ├── 1 │ │ │ └── 2 │ │ │ │ └── test.robot │ │ ├── 2 │ │ │ └── pyproject.toml │ │ └── pyproject.toml │ ├── no_sherlock_section_pyproject │ │ └── pyproject.toml │ ├── pyproject │ │ ├── pyproject.toml │ │ └── pyproject_other.toml │ ├── pyproject_invalid │ │ └── pyproject.toml │ ├── pyproject_missing_key │ │ └── pyproject.toml │ ├── pyproject_nested_config │ │ └── pyproject.toml │ └── pyproject_pythonpath │ │ └── pyproject.toml ├── gitignore │ └── .gitignore ├── libs │ ├── KeywordFour.py │ └── MyPythonLibrary.py ├── resources │ ├── resourceA.resource │ ├── resourceB.robot │ └── resourceC.resource └── tests │ ├── nested │ └── test.robot │ └── test.robot └── utest ├── __init__.py ├── complexity_models.py ├── test_cli.py ├── test_complexity.py ├── test_config.py └── test_file_utils.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bhirsz -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install setuptools wheel twine 23 | python -m pip install robotframework 24 | - name: Build and publish 25 | env: 26 | TWINE_USERNAME: __token__ 27 | TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }} 28 | run: | 29 | python setup.py sdist bdist_wheel 30 | twine upload dist/* 31 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run unit tests for given OSes 2 | 3 | name: Unit tests 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | include: 17 | - os: 'windows-latest' 18 | python-version: '3.9' 19 | rf-version: 'stable5' 20 | - os: 'ubuntu-latest' 21 | python-version: '3.8' 22 | rf-version: 'stable4' 23 | - os: 'ubuntu-latest' 24 | python-version: '3.8' 25 | rf-version: 'stable5' 26 | - os: 'ubuntu-latest' 27 | python-version: '3.9' 28 | rf-version: 'stable5' 29 | - os: 'ubuntu-latest' 30 | python-version: '3.10' 31 | rf-version: 'stable6' 32 | runs-on: ${{ matrix.os }} 33 | 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install -r tests/packages/${{ matrix.rf-version }}/requirements.txt 45 | pip install .[dev] 46 | - name: Run unit tests with coverage 47 | run: 48 | coverage run -m pytest 49 | - name: Codecov 50 | uses: codecov/codecov-action@v3.1.4 51 | with: 52 | name: codecov-sherlock 53 | if: always() 54 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # IPython 61 | profile_default/ 62 | ipython_config.py 63 | 64 | # pyenv 65 | .python-version 66 | 67 | # pipenv 68 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 69 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 70 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 71 | # install all needed dependencies. 72 | #Pipfile.lock 73 | 74 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 75 | __pypackages__/ 76 | 77 | # Environments 78 | .env 79 | .venv 80 | env/ 81 | venv/ 82 | ENV/ 83 | env.bak/ 84 | venv.bak/ 85 | 86 | # mypy 87 | .mypy_cache/ 88 | .dmypy.json 89 | dmypy.json 90 | 91 | # Pycharm files 92 | .idea/ 93 | 94 | actual/ 95 | 96 | *.html 97 | *.xml 98 | sherlock.log -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.11.5 4 | hooks: 5 | - id: isort 6 | name: isort (python) 7 | 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sherlock 2 | The tool for analyzing Robot Framework code in terms of not used code, code complexity or performance issues. 3 | 4 | > **Note** 5 | > 6 | > The tool is in the ***Alpha*** state, which means it may be unstable and should be used at your own risk. Some 7 | > features may be broken and there are still many things to be developed. If you find anything unexpected, or you have ideas for improvements 8 | > not listed in GitHub issues, please open an new issue. 9 | 10 | ## Installation 11 | 12 | You can install the latest version of Sherlock simply by running: 13 | ```commandline 14 | pip install -U robotframework-sherlock 15 | ``` 16 | 17 | Sherlock requires Python 3.8+. 18 | 19 | ## Usage 20 | 21 | Sherlock can prepare analysis based on your source code alone. However, it's currently highly recommended to also include 22 | output of test execution. 23 | 24 | Run Sherlock with: 25 | ```commandline 26 | sherlock --output 27 | ``` 28 | 29 | To analyze external library/resource use ``--resource`` option: 30 | ```commandline 31 | sherlock --output output.xml --resource SeleniumLibrary src/ 32 | ``` 33 | ```commandline 34 | sherlock --output output.xml --resource external_repository_used_in_tests/ src/ 35 | ``` 36 | 37 | ## Reports 38 | Sherlock by default prints the output. You can configure what reports are produced by sherlock using ``--report`` option: 39 | ```commandline 40 | sherlock --report print 41 | ``` 42 | ```commandline 43 | sherlock --report html 44 | ``` 45 | ``--report`` accepts comma separated list of reports: 46 | ```commandline 47 | sherlock --report print,html,json 48 | ``` 49 | 50 | ## BuiltIn library 51 | 52 | To show analysis of BuiltIn libraries use ``--include-builtin`` flag: 53 | ```commandline 54 | sherlock --include-builtin src/ 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/releasenotes/0.3.0.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Sherlock 0.3.0 3 | ================ 4 | 5 | This release focus on fixing critical issues with Robot Framework compatibility and improves core features of Sherlock. 6 | 7 | You can install the latest available version by running:: 8 | 9 | pip install --upgrade robotframework-sherlock 10 | 11 | or to install exactly this version:: 12 | 13 | pip install robotframework-sherlock==0.3.0 14 | 15 | Loading libraries is now more resilient (#56) 16 | -------------------------------------------- 17 | 18 | Invalid library imports or missing dependencies does not stop Sherlock execution now. Import errors should be now 19 | displayed in the report:: 20 | 21 | Directory: test_data 22 | ├── Library: Library.py 23 | │ Import errors: 24 | │ D:\test.robot: Library 'Library' expected 0 arguments, got 1. 25 | ├── Library: LibraryWithArgs.py 26 | │ Import errors: 27 | │ D:\test.robot: Library 'LibraryWithArgs' expected 1 argument, got 0. 28 | 29 | External resource can be loaded from the directory (#54) 30 | -------------------------------------------------------- 31 | 32 | It is now possible to load external resource/libraries for analysis from the directory:: 33 | 34 | sherlock --resource external_libs/ tests 35 | 36 | New --pythonpath option to enable searching in extra paths (#46) 37 | ---------------------------------------------------------------- 38 | 39 | New ``--pythonpath`` option was added to allow specifying extra paths to be searched when looking for the test library. 40 | The supported syntax is the same as Robot Framework ``--pythonpath`` syntax. 41 | 42 | Sherlock now accepts single file as path argument 43 | ------------------------------------------------- 44 | 45 | It is now possible to pass single file for Sherlock analysis:: 46 | 47 | sherlock test.robot 48 | 49 | In such case Sherlock will use file parent directory as a base. 50 | 51 | Reports are now dynamically loaded 52 | ---------------------------------- 53 | 54 | Refactored reports to work as Robocop reports. It opens possibility for external reports in the future or integration 55 | between other tools. 56 | 57 | Fixes 58 | ===== 59 | 60 | Sherlock with Robot Framework 6.0 61 | --------------------------------- 62 | 63 | Sherlock shall now support Robot Framework 6.0+. 64 | 65 | Dictionary values with equals sign (#92) 66 | ---------------------------------------- 67 | 68 | Dictionary values with equals sign should be parsable by Sherlock now:: 69 | 70 | *** Variables *** 71 | &{MY_DICT}= key=string with = sign 72 | 73 | Other 74 | ===== 75 | 76 | - Sherlock now requires at least Python 3.8 to run 77 | - improved configuration validations. Sherlock will now display human readable error if configured with invalid value 78 | - unexpected exceptions during parsing the resource files will now contain source file path 79 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | DEFAULT_PYTHON_VERSION = "3.9" 4 | UNIT_TEST_PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10"] 5 | nox.options.sessions = [ 6 | "unit", 7 | ] 8 | 9 | 10 | @nox.session(python=UNIT_TEST_PYTHON_VERSIONS) 11 | def unit(session): 12 | session.install(".[dev]") 13 | session.run("pytest", "tests") 14 | 15 | 16 | @nox.session(python=DEFAULT_PYTHON_VERSION) 17 | def coverage(session): 18 | session.install(".[dev]") 19 | session.install("coverage") 20 | session.run("coverage", "run", "-m", "pytest") 21 | session.run("coverage", "html") 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | [tool.isort] 4 | profile = "black" 5 | line_length = 120 6 | [tool.coverage.run] 7 | omit = ["*tests*"] 8 | source = ["sherlock"] 9 | [tool.coverage.report] 10 | exclude_lines = [ 11 | "pragma: no cover", 12 | "if __name__ == .__main__.:", 13 | "raise NotImplementedError" 14 | ] 15 | fail_under = 90 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import find_packages, setup 4 | 5 | PACKAGE = "sherlock" 6 | HERE = pathlib.Path(__file__).parent 7 | README = (HERE / "README.md").read_text() 8 | with open(HERE / PACKAGE / "version.py") as f: 9 | __version__ = f.read().split("=")[1].strip().strip('"') 10 | CLASSIFIERS = """ 11 | Development Status :: 2 - Pre-Alpha 12 | License :: OSI Approved :: Apache Software License 13 | Operating System :: OS Independent 14 | Programming Language :: Python 15 | Programming Language :: Python :: 3.7 16 | Programming Language :: Python :: 3.8 17 | Programming Language :: Python :: 3.9 18 | Framework :: Robot Framework 19 | Framework :: Robot Framework :: Tool 20 | Topic :: Software Development :: Testing 21 | Topic :: Software Development :: Quality Assurance 22 | Topic :: Utilities 23 | Intended Audience :: Developers 24 | """.strip().splitlines() 25 | KEYWORDS = "robotframework automation testautomation testing qa" 26 | DESCRIPTION = "Robot Framework code analysis tool" 27 | 28 | setup( 29 | name=f"robotframework-{PACKAGE}", 30 | version=__version__, 31 | description=DESCRIPTION, 32 | long_description=README, 33 | long_description_content_type="text/markdown", 34 | url=f"https://github.com/bhirsz/robotframework-{PACKAGE}", 35 | download_url=f"https://pypi.org/project/robotframework-{PACKAGE}", 36 | author="Bartlomiej Hirsz, Mateusz Nojek", 37 | author_email="bartek.hirsz@gmail.com, matnojek@gmail.com", 38 | license="Apache License 2.0", 39 | platforms="any", 40 | classifiers=CLASSIFIERS, 41 | keywords=KEYWORDS, 42 | packages=find_packages(), 43 | package_data={"": ["*.template"]}, 44 | python_requires=">=3.8", 45 | install_requires=[ 46 | "robotframework>=4.1", 47 | "toml>=0.10.2", 48 | "pathspec==0.9.*", 49 | "jinja2", 50 | "tabulate==0.8.9", 51 | "rich>=10.12.0", 52 | ], 53 | extras_require={ 54 | "dev": ["coverage", "pytest", "robotframework-templateddata"], 55 | # "doc": ["sphinx", "sphinx_rtd_theme"], 56 | }, 57 | entry_points={"console_scripts": [f"{PACKAGE}={PACKAGE}:run_cli"]}, 58 | ) 59 | -------------------------------------------------------------------------------- /sherlock/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from sherlock.core import Sherlock 4 | from sherlock.exceptions import SherlockFatalError 5 | 6 | 7 | def run_cli(): 8 | try: 9 | runner = Sherlock() 10 | runner.run() 11 | except SherlockFatalError as err: 12 | print(f"Error: {err}") 13 | sys.exit(1) 14 | except Exception as err: 15 | message = ( 16 | "\nFatal exception occurred. You can create an issue at " 17 | "https://github.com/bhirsz/robotframework-sherlock/issues . Thanks!" # TODO change url when migrate 18 | ) 19 | err.args = (str(err.args[0]) + message,) + err.args[1:] 20 | raise err 21 | -------------------------------------------------------------------------------- /sherlock/__main__.py: -------------------------------------------------------------------------------- 1 | from sherlock import run_cli 2 | 3 | if __name__ == "__main__": 4 | run_cli() 5 | -------------------------------------------------------------------------------- /sherlock/complexity.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from robot.api.parsing import ModelVisitor 4 | 5 | 6 | class PathNode: 7 | def __init__(self, name): 8 | self.name = name 9 | 10 | 11 | class ComplexityChecker(ModelVisitor): 12 | def __init__(self): 13 | self.nodes = defaultdict(list) 14 | self.graph = None 15 | self.tail = None 16 | 17 | def connect(self, node1, node2): 18 | self.nodes[node1].append(node2) 19 | self.nodes[node2] = [] 20 | 21 | def complexity(self): 22 | """mccabe V-E+2""" 23 | edges = sum(len(n) for n in self.nodes.values()) 24 | nodes = len(self.nodes) 25 | return edges - nodes + 2 26 | 27 | def append_path_node(self, name): 28 | if not self.tail: 29 | return 30 | path_node = PathNode(name) 31 | self.connect(self.tail, path_node) 32 | self.tail = path_node 33 | return path_node 34 | 35 | def visit_Keyword(self, node): # noqa 36 | name = f"{node.name}:{node.lineno}:{node.col_offset}" 37 | path_node = PathNode(name) 38 | self.tail = path_node 39 | self.generic_visit(node) 40 | 41 | def visit_KeywordCall(self, node): # noqa 42 | name = f"KeywordCall {node.lineno}" 43 | self.append_path_node(name) 44 | 45 | def visit_For(self, node): # noqa 46 | name = f"FOR {node.lineno}" 47 | path_node = self.append_path_node(name) 48 | self._parse_subgraph(node, path_node) 49 | 50 | def visit_If(self, node): # noqa 51 | name = f"IF {node.lineno}" 52 | path_node = self.append_path_node(name) 53 | self._parse_subgraph(node, path_node) 54 | 55 | def _parse_subgraph(self, node, path_node): 56 | loose_ends = [] 57 | self.tail = path_node 58 | self.generic_visit(node) 59 | loose_ends.append(self.tail) 60 | if getattr(node, "orelse", False): 61 | self.tail = path_node 62 | self.generic_visit(node.orelse) 63 | loose_ends.append(self.tail) 64 | else: 65 | loose_ends.append(path_node) 66 | if path_node: 67 | bottom = PathNode("") 68 | for loose_end in loose_ends: 69 | self.connect(loose_end, bottom) 70 | self.tail = bottom 71 | -------------------------------------------------------------------------------- /sherlock/config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import glob 3 | import os 4 | import string 5 | from pathlib import Path 6 | from typing import List 7 | 8 | import toml 9 | from robot.conf import RobotSettings 10 | 11 | from sherlock.exceptions import SherlockFatalError 12 | from sherlock.file_utils import find_file_in_project_root, find_project_root, get_gitignore 13 | from sherlock.version import __version__ 14 | 15 | BUILT_IN = "BuiltIn" 16 | ROBOT_DEFAULT_OUTPUT = "output.xml" 17 | 18 | 19 | class CommaSeparated(argparse.Action): 20 | def __call__(self, parser, namespace, values, option_string=None): 21 | setattr(namespace, self.dest, values.split(",")) 22 | 23 | 24 | class PythonPath(argparse.Action): 25 | def __call__(self, parser, namespace, values, option_string=None): 26 | container = getattr(namespace, self.dest) 27 | paths = _process_pythonpath([values]) 28 | container.extend(paths) 29 | 30 | 31 | def _process_pythonpath(paths): 32 | return [ 33 | os.path.abspath(globbed) 34 | for path in paths 35 | for split in _split_pythonpath(path) 36 | for globbed in glob.glob(split) or [split] 37 | ] 38 | 39 | 40 | def _split_pythonpath(path): 41 | path = path.replace("/", os.sep) 42 | if ";" in path: 43 | yield from path.split(";") 44 | elif os.sep == "/": 45 | yield from path.split(":") 46 | else: 47 | drive = "" 48 | for item in path.split(":"): 49 | if drive: 50 | if item.startswith("\\"): 51 | yield f"{drive}:{item}" 52 | drive = "" 53 | continue 54 | yield drive 55 | drive = "" 56 | if len(item) == 1 and item in string.ascii_letters: 57 | drive = item 58 | else: 59 | yield item 60 | if drive: 61 | yield drive 62 | 63 | 64 | class Config: 65 | def __init__(self, from_cli=True): 66 | self.path = Path.cwd() 67 | self.output = None 68 | self.log_output = None 69 | self.report: List[str] = ["print"] 70 | self.variable = [] 71 | self.variablefile = [] 72 | self.pythonpath = [] 73 | self.robot_settings = None 74 | self.include_builtin = False 75 | self.root = Path.cwd() 76 | self.default_gitignore = None 77 | self.resource: List[str] = [] 78 | if from_cli: 79 | self.parse_cli() 80 | self.init_variables() 81 | 82 | def parse_cli(self): 83 | parser = self._create_parser() 84 | parsed_args = parser.parse_args() 85 | 86 | self.set_root(parsed_args) 87 | 88 | default = self.get_defaults_from_config(parsed_args) 89 | if default: 90 | # set only options not already set from cli 91 | self.set_parsed_opts(default) 92 | 93 | self.set_parsed_opts(dict(**vars(parsed_args))) 94 | self.validate_test_path() 95 | self.validate_output() 96 | 97 | def validate_test_path(self): 98 | if self.path.is_file(): 99 | self.path = self.path.parent 100 | if not self.path.exists(): 101 | raise SherlockFatalError(f"Path to source code does not exist: '{self.path.resolve()}'") 102 | 103 | def validate_output(self): 104 | if self.output is None: 105 | output = (self.path if self.path.is_dir() else self.path.parent) / ROBOT_DEFAULT_OUTPUT 106 | if output.is_file(): # TODO document this 107 | self.output = output 108 | elif not self.output.is_file(): 109 | raise SherlockFatalError( 110 | f"Reading Robot Framework output file failed. No such file: '{self.output}'" 111 | ) from None 112 | 113 | def set_root(self, parsed_args): 114 | self.root = find_project_root((getattr(parsed_args, "path", Path.cwd()),)) 115 | if self.root: 116 | self.default_gitignore = get_gitignore(self.root) 117 | 118 | def _create_parser(self) -> argparse.ArgumentParser: 119 | parser = argparse.ArgumentParser( 120 | prog="sherlock", 121 | description=f"Code complexity analyser for Robot Framework. Version: {__version__}\n", 122 | argument_default=argparse.SUPPRESS, 123 | ) 124 | 125 | parser.add_argument( 126 | "path", metavar="SOURCE", default=self.path, nargs="?", type=Path, help="Path to source code" 127 | ) 128 | parser.add_argument( 129 | "-o", 130 | "--output", 131 | type=Path, 132 | help="Path to Robot Framework output file", 133 | ) 134 | parser.add_argument( 135 | "-r", "--resource", action="append", help="Path/name of the library or resource to be included in analysis" 136 | ) 137 | parser.add_argument( 138 | "-rp", 139 | "--report", 140 | action=CommaSeparated, 141 | help="Generate reports after analysis. Use comma separated list for multiple reports. " 142 | "Available reports: print (default), html, json", 143 | ) 144 | parser.add_argument( 145 | "-c", 146 | "--config", 147 | help="Path to TOML configuration file", 148 | ) 149 | parser.add_argument( 150 | "-v", "--variable", help="Set Robot variable in Sherlock namespace", metavar="name:value", action="append" 151 | ) 152 | parser.add_argument( 153 | "-V", 154 | "--variablefile", 155 | help="Set Robot variable in Sherlock namespace from Python or YAML file", 156 | metavar="path", 157 | action="append", 158 | ) 159 | parser.add_argument( 160 | "--include-builtin", 161 | help="Use this flag to include BuiltIn libraries in analysis", 162 | action="store_true", 163 | ) 164 | parser.add_argument( 165 | "-P", 166 | "--pythonpath", 167 | help="Additional locations where to search libraries. Multiple paths can be given by separating them with " 168 | "a semicolon (`;`) or by using this option several times.", 169 | action=PythonPath, 170 | default=self.pythonpath, 171 | ) 172 | parser.add_argument( 173 | "--log-output", 174 | type=argparse.FileType("w"), 175 | help="Path to output log", 176 | ) 177 | return parser 178 | 179 | def set_parsed_opts(self, namespace): 180 | for key, value in namespace.items(): 181 | if key in self.__dict__: 182 | self.__dict__[key] = value 183 | 184 | def get_defaults_from_config(self, parsed_args): 185 | if "config" in parsed_args: 186 | config_path = parsed_args.config 187 | if not Path(config_path).is_file(): 188 | raise SherlockFatalError(f"Configuration file '{config_path}' does not exist") from None 189 | else: 190 | config_path = find_file_in_project_root("pyproject.toml", self.root) 191 | if not config_path.is_file(): 192 | return {} 193 | return TomlConfigParser(config_path=config_path, look_up=self.__dict__).get_config() 194 | 195 | def init_variables(self): 196 | self.robot_settings = RobotSettings({"variable": self.variable, "variablefile": self.variablefile}) 197 | 198 | 199 | class TomlConfigParser: 200 | def __init__(self, config_path, look_up): 201 | self.config_path = str(config_path) 202 | self.look_up = look_up 203 | 204 | def read_from_file(self): 205 | try: 206 | config = toml.load(self.config_path) 207 | except toml.TomlDecodeError as err: 208 | raise SherlockFatalError(f"Failed to decode {self.config_path}: {err}") from None 209 | return config.get("tool", {}).get("sherlock", {}) 210 | 211 | def get_config(self): 212 | read_config = {} 213 | config = self.read_from_file() 214 | if not config: 215 | return read_config 216 | 217 | toml_data = {key.replace("-", "_"): value for key, value in config.items()} 218 | for key, value in toml_data.items(): 219 | if key == "log_output": 220 | read_config[key] = argparse.FileType("w")(value) 221 | elif key == "report": 222 | read_config[key] = value.split(",") if isinstance(value, str) else value 223 | elif key == "config": 224 | raise SherlockFatalError("Nesting configuration files is not allowed") 225 | elif key == "output": 226 | read_config[key] = Path(value) 227 | elif key == "include_builtin": 228 | read_config[key] = str(value).lower() in ("true", "1", "yes", "t", "y") # TODO tests 229 | elif key == "pythonpath": 230 | read_config[key] = _process_pythonpath(value) 231 | elif key in self.look_up: 232 | read_config[key] = value 233 | else: 234 | raise SherlockFatalError(f"Option '{key}' is not supported in configuration file") 235 | return read_config 236 | -------------------------------------------------------------------------------- /sherlock/core.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from typing import List, Optional 4 | 5 | from robot.api import ExecutionResult, TestSuiteBuilder 6 | 7 | from sherlock.config import BUILT_IN, Config 8 | from sherlock.model import Library, Resource, Tree 9 | from sherlock.report import get_reports 10 | from sherlock.visitor import StructureVisitor 11 | 12 | 13 | class Sherlock: 14 | def __init__(self, config: Optional[Config] = None): 15 | self.config = Config() if config is None else config 16 | self.reports = get_reports(self.config.report) 17 | self.resources = dict() 18 | self.directory = None 19 | self.packages: List[Tree] = [] 20 | self.from_output = bool(self.config.output) 21 | 22 | def run(self): 23 | self.log("Sherlock analysis of Robot Framework code:\n") 24 | root = self.config.path 25 | self.log(f"Using {root.resolve()} as source repository") 26 | if self.config.pythonpath: 27 | sys.path = self.config.pythonpath + sys.path 28 | 29 | if self.from_output: 30 | suite = ExecutionResult(self.config.output).suite 31 | self.log(f"Loaded {self.config.output.resolve()} output file") 32 | else: 33 | suite = TestSuiteBuilder().build(self.config.path) 34 | 35 | tree = self.map_resources_for_path(root) 36 | self.packages.append(tree) 37 | self.packages.append(self.create_builtin_tree()) 38 | self.packages.extend(self.map_resources()) 39 | 40 | code_visitor = StructureVisitor(self.resources, self.from_output, self.config.robot_settings) 41 | suite.visit(code_visitor) 42 | for error in code_visitor.errors: 43 | self.log(error) 44 | 45 | self.report() 46 | 47 | def log(self, line: str): 48 | print(line, file=self.config.log_output) 49 | 50 | def report(self): 51 | for tree in self.packages: 52 | if not self.config.include_builtin and tree.name == BUILT_IN: 53 | continue 54 | for report in self.reports.values(): 55 | report.get_report(tree, tree.name, self.config.root) 56 | 57 | def map_resources_for_path(self, root: Path): 58 | tree = Tree.from_directory(path=root, gitignore=self.config.default_gitignore) 59 | self.resources.update({path: resource for path, resource in tree.get_resources()}) 60 | return tree 61 | 62 | def map_resources(self): 63 | for resource in self.config.resource: 64 | resource = Path(resource) 65 | resolved = resource.resolve() # TODO search in pythonpaths etc, iterate over directories if not file 66 | if resolved.is_dir(): 67 | yield self.map_resources_for_path(resolved) 68 | else: 69 | if not resolved.exists() or resolved.suffix == ".py": 70 | res_model = Library(resolved) 71 | else: 72 | res_model = Resource(resolved) 73 | self.resources[str(resolved)] = res_model 74 | tree = Tree(name=resource.name) 75 | tree.children.append(res_model) 76 | yield tree 77 | 78 | def create_builtin_tree(self): 79 | built_in = Library(BUILT_IN) 80 | built_in.load_library([], [], "builtin") 81 | built_in.filter_not_used = True 82 | built_in.builtin = True 83 | 84 | self.resources[BUILT_IN] = built_in 85 | tree = Tree(name=BUILT_IN) 86 | tree.children.append(built_in) 87 | return tree 88 | -------------------------------------------------------------------------------- /sherlock/exceptions.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | import re 3 | from collections import defaultdict 4 | 5 | 6 | class RecommendationFinder: 7 | def find_similar(self, name, candidates): 8 | norm_name = self.normalize(name) 9 | norm_cand = self.get_normalized_candidates(candidates) 10 | matches = [] 11 | for norm in norm_name: 12 | matches += self.find(norm, norm_cand.keys()) 13 | if not matches: 14 | return "" 15 | matches = self.get_original_candidates(matches, norm_cand) 16 | suggestion = "Did you mean:\n" 17 | suggestion += "\n".join(f" {match}" for match in matches) 18 | return suggestion 19 | 20 | def find(self, name, candidates, max_matches=5): 21 | """Return a list of close matches to `name` from `candidates`.""" 22 | if not name or not candidates: 23 | return [] 24 | cutoff = self._calculate_cutoff(name) 25 | return difflib.get_close_matches(name, candidates, n=max_matches, cutoff=cutoff) 26 | 27 | @staticmethod 28 | def _calculate_cutoff(string, min_cutoff=0.5, max_cutoff=0.85, step=0.03): 29 | """The longer the string the bigger required cutoff.""" 30 | cutoff = min_cutoff + len(string) * step 31 | return min(cutoff, max_cutoff) 32 | 33 | @staticmethod 34 | def normalize(name): 35 | """ 36 | Return tuple where first element is string created from sorted words in name, 37 | and second element is name without `-` and `_`. 38 | """ 39 | norm = re.split("[-_ ]+", name) 40 | return " ".join(sorted(norm)), name.replace("-", "").replace("_", "") 41 | 42 | @staticmethod 43 | def get_original_candidates(candidates, norm_candidates): 44 | """Map found normalized candidates to unique original candidates.""" 45 | return sorted(list(set(c for cand in candidates for c in norm_candidates[cand]))) 46 | 47 | def get_normalized_candidates(self, candidates): 48 | """ 49 | Thanks for normalizing and sorting we can find cases like this-is, thisis, this-is1 instead of is-this. 50 | Normalized names form dictionary that point to original names - we're using list because several names can 51 | have one common normalized name. 52 | Different normalization methods try to imitate possible mistakes done when typing name - different order, 53 | missing `-` etc. 54 | """ 55 | norm = defaultdict(list) 56 | for cand in candidates: 57 | for norm_cand in self.normalize(cand): 58 | norm[norm_cand].append(cand) 59 | return norm 60 | 61 | 62 | class SherlockFatalError(ValueError): 63 | pass 64 | 65 | 66 | class ConfigGeneralError(SherlockFatalError): 67 | pass 68 | 69 | 70 | class InvalidReportName(ConfigGeneralError): 71 | def __init__(self, report, reports): 72 | report_names = sorted(list(reports.keys())) 73 | msg = ( 74 | f"Provided report '{report}' does not exist. " 75 | f"Use comma separated list of values from: {','.join(report_names)}. " 76 | ) 77 | similar = RecommendationFinder().find_similar(report, report_names) 78 | msg += similar 79 | super().__init__(msg) 80 | -------------------------------------------------------------------------------- /sherlock/file_utils.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from pathlib import Path 3 | from typing import List 4 | 5 | from pathspec import PathSpec 6 | 7 | INCLUDE_EXT = frozenset({".robot", ".resource", ".py"}) 8 | INIT_EXT = ("__init__.robot", "__init__.py") 9 | 10 | 11 | @lru_cache() 12 | def get_gitignore(root: Path) -> PathSpec: 13 | """Return a PathSpec matching gitignore content if present.""" 14 | gitignore = root / ".gitignore" 15 | lines: List[str] = [] 16 | if gitignore.is_file(): 17 | with gitignore.open(encoding="utf-8") as gf: 18 | lines = gf.readlines() 19 | return PathSpec.from_lines("gitwildmatch", lines) 20 | 21 | 22 | def find_project_root(paths) -> Path: 23 | """Return a directory containing .git or pyproject.toml. 24 | That directory will be a common parent of all files and directories 25 | passed in `paths`. 26 | If no directory in the tree contains a marker that would specify it's the 27 | project root, the root of the file system is returned. 28 | """ 29 | if paths is None: 30 | return Path.cwd() 31 | 32 | path_srcs = [Path(Path.cwd(), path).resolve() for path in paths] 33 | 34 | # A list of lists of parents for each 'path'. 'path' is included as a 35 | # "parent" of itself if it is a directory 36 | src_parents = [list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs] 37 | 38 | common_base = max( 39 | set.intersection(*(set(parents) for parents in src_parents)), 40 | key=lambda path: path.parts, 41 | ) 42 | 43 | for directory in (common_base, *common_base.parents): 44 | if (directory / ".git").exists() or (directory / "pyproject.toml").is_file(): 45 | return directory 46 | return directory 47 | 48 | 49 | def find_file_in_project_root(config_name, root): 50 | for parent in (root, *root.parents): 51 | if (parent / ".git").exists() or (parent / config_name).is_file(): 52 | return parent / config_name 53 | return parent / config_name 54 | -------------------------------------------------------------------------------- /sherlock/model.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import math 3 | import textwrap 4 | from pathlib import Path 5 | from typing import Optional 6 | 7 | import robot.errors 8 | from pathspec import PathSpec 9 | from robot.api import get_model 10 | from robot.running.arguments import EmbeddedArguments 11 | from robot.running.testlibraries import TestLibrary 12 | from robot.utils import NormalizedDict 13 | from robot.variables import Variables 14 | 15 | from sherlock.complexity import ComplexityChecker 16 | from sherlock.file_utils import INCLUDE_EXT 17 | 18 | DIRECTORY_TYPE = "Directory" 19 | RESOURCE_TYPE = "Resource" 20 | LIBRARY_TYPE = "Library" 21 | SUITE_TYPE = "Suite" 22 | 23 | 24 | class KeywordStats: 25 | def __init__(self, name, parent, node=None): 26 | self.name = name 27 | self.parent = parent 28 | self.used = 0 29 | self.node = node 30 | self.complexity = self.get_complexity() 31 | self.timings = KeywordTimings() 32 | 33 | @property 34 | def status(self): 35 | # TODO fail (fail), warning statuses (skip) 36 | if not self.used: 37 | return "label" 38 | return "pass" 39 | 40 | def __str__(self): 41 | # TODO reduce since resource prints table now 42 | s = f"{self.name}\n" 43 | s += f" Used: {self.used}\n" 44 | if self.complexity: 45 | s += f" Complexity: {self.complexity}\n" 46 | if self.used: 47 | s += " Elapsed time:\n" 48 | s += textwrap.indent(str(self.timings), " ") 49 | return s 50 | 51 | def get_complexity(self): 52 | if not self.node: 53 | return None 54 | checker = ComplexityChecker() 55 | checker.visit(self.node) 56 | return checker.complexity() 57 | 58 | 59 | class KeywordTimings: 60 | def __init__(self): 61 | self._max = 0 62 | self._min = math.inf 63 | self._avg = 0 64 | self._total = 0 65 | self._count = 0 66 | 67 | def add_timing(self, elapsed): 68 | self._count += 1 69 | self._max = max(self._max, elapsed) 70 | self._min = min(self._min, elapsed) 71 | self._total += elapsed 72 | self._avg = math.floor(self._total / self._count) 73 | 74 | def format_time(self, milliseconds): 75 | if not self._count: 76 | return "0" 77 | seconds = milliseconds / 1000 78 | return str(round(seconds)) 79 | 80 | @property 81 | def max(self): 82 | return self.format_time(self._max) 83 | 84 | @max.setter 85 | def max(self, value): 86 | self._max = value 87 | 88 | @property 89 | def min(self): 90 | return self.format_time(self._min) 91 | 92 | @min.setter 93 | def min(self, value): 94 | self._min = value 95 | 96 | @property 97 | def avg(self): 98 | return self.format_time(self._avg) 99 | 100 | @avg.setter 101 | def avg(self, value): 102 | self._avg = value 103 | 104 | @property 105 | def total(self): 106 | return self.format_time(self._total) 107 | 108 | @total.setter 109 | def total(self, value): 110 | self._total = value 111 | 112 | def __add__(self, other): 113 | timing = KeywordTimings() 114 | timing.max = self._max 115 | timing.min = self._min 116 | timing._count = self._count 117 | timing.avg = self._avg 118 | timing.total = self._total 119 | 120 | if other._count: 121 | timing.add_timing(other._total) 122 | return timing 123 | 124 | def __radd__(self, other): 125 | return self.__add__(other) 126 | 127 | 128 | class ResourceVisitor(ast.NodeVisitor): 129 | def __init__(self, parent): 130 | self.parent = parent 131 | self.normal_keywords = NormalizedDict(ignore="_") 132 | self.embedded_keywords = dict() 133 | self.variables = Variables() 134 | self.resources = [] 135 | self.libraries = dict() 136 | self.has_tests = False 137 | 138 | def visit_TestCase(self, node): # noqa 139 | self.has_tests = True 140 | 141 | def visit_Keyword(self, node): # noqa 142 | embedded = EmbeddedArguments(node.name) 143 | if embedded and embedded.args: 144 | self.embedded_keywords[node.name] = (KeywordStats(node.name, parent=self.parent, node=node), embedded.name) 145 | else: 146 | self.normal_keywords[node.name] = KeywordStats( 147 | node.name, parent=self.parent, node=node 148 | ) # TODO: handle duplications 149 | 150 | def visit_ResourceImport(self, node): # noqa 151 | if node.name: 152 | self.resources.append(node.name) 153 | 154 | def visit_LibraryImport(self, node): # noqa 155 | if node.name: 156 | self.libraries[(node.name, node.alias)] = node.args 157 | 158 | def visit_Variable(self, node): # noqa 159 | if not node.name or node.errors: 160 | return 161 | if node.name[0] == "$": 162 | self.variables[node.name] = node.value[0] if node.value else "" 163 | elif node.name[0] == "@": 164 | self.variables[node.name] = list(node.value) 165 | elif node.name[0] == "&": 166 | self.variables[node.name] = self.set_dict(node.value) 167 | 168 | @staticmethod 169 | def set_dict(values): 170 | ret = {} 171 | for value in values: 172 | key, val = value.split("=", maxsplit=1) if "=" in value else (value, "") 173 | ret[key] = val 174 | return ret 175 | 176 | 177 | class KeywordStore: 178 | def __init__(self, normal, embedded): 179 | self._normal = normal 180 | self._embedded = embedded 181 | 182 | def __iter__(self): 183 | yield from self._normal.values() 184 | for kw_stat, pattern in self._embedded.values(): 185 | yield kw_stat 186 | 187 | def find_kw(self, kw_name): 188 | found = [] 189 | try: 190 | kw_stat = self._normal[kw_name] 191 | found.append(kw_stat) 192 | except KeyError: 193 | for name, (kw_stat, template) in self._embedded.items(): 194 | if self.match_embedded(kw_name, template): 195 | found.append(kw_stat) 196 | return found 197 | 198 | @staticmethod 199 | def match_embedded(name, pattern): 200 | raise NotImplemented 201 | 202 | 203 | class KeywordLibraryStore(KeywordStore): 204 | def __init__(self, test_library, parent): 205 | normal = NormalizedDict(ignore="_") 206 | for kw in test_library.handlers._normal: 207 | normal[kw] = KeywordStats(kw, parent=parent) 208 | embedded = {handler.name: (KeywordStats(handler.name, handler)) for handler in test_library.handlers._embedded} 209 | super().__init__(normal, embedded) 210 | 211 | @staticmethod 212 | def match_embedded(name, pattern): 213 | return pattern.matches(name) 214 | 215 | 216 | class KeywordResourceStore(KeywordStore): 217 | def __init__(self, normal, embedded, parent): 218 | keyword_stats = {name: KeywordStats(name, parent) for name in normal.values()} 219 | for kw_stat, pattern in embedded.values(): 220 | keyword_stats[kw_stat.name] = kw_stat 221 | super().__init__(normal, embedded) 222 | 223 | @staticmethod 224 | def match_embedded(name, pattern): 225 | return pattern.match(name) 226 | 227 | 228 | class File: 229 | def __init__(self, path): 230 | self.path = path 231 | self.name = str(Path(path).name) 232 | self.name_no_ext = str(Path(path).stem) 233 | self.keywords = [] 234 | self.errors = set() 235 | 236 | def get_resources(self): 237 | return str(self.path), self 238 | 239 | def get_type(self): 240 | raise NotImplemented 241 | 242 | def __str__(self): 243 | s = f"{self.get_type()}: {self.name}\n" 244 | if self.errors: 245 | s += "Import errors:\n" 246 | s += textwrap.indent("".join(self.errors), " ") 247 | return s 248 | 249 | 250 | class Library(File): 251 | def __init__(self, path): 252 | super().__init__(path) 253 | self.type = LIBRARY_TYPE 254 | self.loaded = False 255 | self.filter_not_used = False 256 | self.builtin = False 257 | 258 | def get_type(self): 259 | return self.type 260 | 261 | def load_library(self, args, scope_variables, defined_in_file): 262 | if self.keywords: 263 | return 264 | error = False 265 | if scope_variables is not None and args: 266 | replaced_args = [] 267 | for arg in args: 268 | try: 269 | replaced_args.append(scope_variables.replace_string(arg)) 270 | except robot.errors.VariableError as err: 271 | error = True 272 | self.errors.add( 273 | f"Failed to load library with an error: {err} You can provide Robot variables " 274 | f"to Sherlock using -v/--variable name:value cli option.\n" 275 | ) 276 | else: 277 | replaced_args = args 278 | if error: 279 | return 280 | name = str(self.path) 281 | self.init_library(name, replaced_args, defined_in_file) 282 | 283 | def init_library(self, name, replaced_args, defined_in_file): 284 | try: 285 | library = TestLibrary(name, replaced_args) 286 | self.name = library.orig_name 287 | self.keywords = KeywordLibraryStore(library, name) 288 | except robot.errors.DataError as err: 289 | self.errors.add(f"{defined_in_file}: {err}") 290 | 291 | def search(self, name): 292 | if not self.keywords: 293 | return [] 294 | return self.keywords.find_kw(name) 295 | 296 | 297 | class Resource(File): 298 | def __init__(self, path: Path): 299 | super().__init__(path) 300 | self.type = RESOURCE_TYPE 301 | self.name = path.name # TODO Resolve chaos with names and paths 302 | self.directory = str(path.parent) 303 | 304 | visitor = self.load_model_from_resource(path) 305 | self.keywords = KeywordResourceStore(visitor.normal_keywords, visitor.embedded_keywords, str(path)) 306 | self.has_tests = visitor.has_tests 307 | self.variables = visitor.variables 308 | self.current_variables = None 309 | 310 | self.resources = visitor.resources 311 | self.libraries = visitor.libraries 312 | 313 | @staticmethod 314 | def load_model_from_resource(path): 315 | model = get_model(str(path), data_only=True, curdir=str(path.cwd())) 316 | visitor = ResourceVisitor(str(path)) 317 | try: 318 | visitor.visit(model) 319 | except Exception as err: 320 | print(f"Fatal error when parsing the file: {path}") 321 | raise err 322 | return visitor 323 | 324 | def get_type(self): 325 | return SUITE_TYPE if self.has_tests else RESOURCE_TYPE 326 | 327 | def search(self, name, lib_name): 328 | if lib_name and lib_name != self.name_no_ext: 329 | return [] 330 | return self.keywords.find_kw(name) 331 | 332 | 333 | class Tree: 334 | def __init__(self, name): 335 | self.name = name 336 | self.type = DIRECTORY_TYPE 337 | self.path = "" 338 | self.errors = set() 339 | self.children = [] 340 | 341 | @classmethod 342 | def from_directory(cls, path: Path, gitignore: Optional[PathSpec] = None): 343 | tree = cls(str(path.name)) 344 | tree.path = path 345 | 346 | for child in path.iterdir(): 347 | gitignore_pattern = f"{child}/" if child.is_dir() else str(child) 348 | if gitignore is not None and gitignore.match_file(gitignore_pattern): 349 | continue 350 | child = child.resolve() 351 | if child.is_dir(): 352 | library_init = Library(child) if tree.has_init(child) else None 353 | 354 | child_tree = cls.from_directory(path=child, gitignore=gitignore) 355 | if child_tree.children: # if the directory is empty (no libraries or resources) skip it 356 | if library_init: 357 | child_tree.children.append(library_init) 358 | tree.children.append(child_tree) 359 | elif library_init: 360 | tree.children.append(library_init) 361 | elif child.is_file(): 362 | if child.suffix not in INCLUDE_EXT or child.name == "__init__.py": 363 | continue 364 | if child.suffix == ".py": # TODO better mapping 365 | tree.children.append(Library(child)) 366 | else: 367 | tree.children.append(Resource(child)) 368 | return tree 369 | 370 | @staticmethod 371 | def has_init(directory): 372 | return any(child.name == "__init__.py" for child in directory.iterdir()) # FIXME 373 | 374 | def get_type(self): 375 | return self.type 376 | 377 | def get_resources(self): 378 | for resource in self.children: 379 | if resource.type == DIRECTORY_TYPE: 380 | yield from resource.get_resources() 381 | else: 382 | yield resource.get_resources() 383 | 384 | def __str__(self): 385 | return f"Directory: {self.name}" 386 | -------------------------------------------------------------------------------- /sherlock/report/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import inspect 3 | from pathlib import Path 4 | 5 | import sherlock.exceptions 6 | 7 | 8 | class Report: 9 | def get_report(self, tree, tree_name, path_root): 10 | raise NotImplementedError 11 | 12 | 13 | def _import_module_from_file(file_path): 14 | """Import Python file as module. 15 | 16 | importlib does not support importing Python files directly, and we need to create module specification first.""" 17 | spec = importlib.util.spec_from_file_location(file_path.stem, file_path) 18 | mod = importlib.util.module_from_spec(spec) 19 | spec.loader.exec_module(mod) 20 | return mod 21 | 22 | 23 | def modules_from_paths(paths, recursive=True): 24 | for path in paths: 25 | path_object = Path(path) 26 | if path_object.is_dir(): 27 | if not recursive or path_object.name in {".git", "__pycache__"}: 28 | continue 29 | yield from modules_from_paths([file for file in path_object.iterdir()]) 30 | elif path_object.suffix == ".py": 31 | yield _import_module_from_file(path_object) 32 | 33 | 34 | def load_reports(): 35 | """ 36 | Load all valid reports. 37 | Report is considered valid if it inherits from `Report` class 38 | and contains both `name` and `description` attributes. 39 | """ 40 | reports = {} 41 | for module in modules_from_paths([Path(__file__).parent]): 42 | classes = inspect.getmembers(module, inspect.isclass) 43 | for report_class in classes: 44 | if not issubclass(report_class[1], Report): 45 | continue 46 | report = report_class[1]() 47 | if not hasattr(report, "name") or not hasattr(report, "description"): 48 | continue 49 | reports[report.name] = report 50 | return reports 51 | 52 | 53 | def get_reports(configured_reports): 54 | """ 55 | Returns dictionary with list of valid, enabled reports (listed in `configured_reports` set of str). 56 | """ 57 | reports = load_reports() 58 | enabled_reports = {} 59 | for report in configured_reports: 60 | if report not in reports: 61 | raise sherlock.exceptions.InvalidReportName(report, reports) 62 | elif report not in enabled_reports: 63 | enabled_reports[report] = reports[report] 64 | return enabled_reports 65 | -------------------------------------------------------------------------------- /sherlock/report/html.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from pathlib import Path 3 | 4 | from jinja2 import Template 5 | 6 | import sherlock.report 7 | from sherlock.file_utils import INIT_EXT 8 | from sherlock.model import DIRECTORY_TYPE, LIBRARY_TYPE, RESOURCE_TYPE, SUITE_TYPE, KeywordTimings 9 | 10 | 11 | class KeywordResult: 12 | def __init__(self, element_id, name, used, complexity, status, timings): 13 | self.element_id = element_id 14 | self.name = name 15 | self.used = used 16 | self.complexity = complexity 17 | self.status = status 18 | self.timings = timings 19 | 20 | 21 | class HtmlResultModel: 22 | def __init__(self, element_id, model): 23 | self.element_id = element_id 24 | self.type = model.get_type().upper() 25 | self.name = model.name 26 | self.path = model.path 27 | self.show = "BuiltIn" not in self.name 28 | self.children = [] 29 | self.keywords = [] 30 | self.timings = KeywordTimings() 31 | self.errors = model.errors 32 | self.fill_keywords(model) 33 | self.fill_children(model) 34 | 35 | @property 36 | def status(self): 37 | status = "label" 38 | if self.errors: 39 | return "fail" 40 | for child in chain(self.keywords, self.children): 41 | if child.status in ("skip", "fail"): 42 | return child.status 43 | if child.status == "pass": 44 | status = "pass" 45 | if self.type == SUITE_TYPE.upper() and not self.keywords: 46 | return "pass" 47 | return status 48 | 49 | def fill_keywords(self, model): 50 | if model.type not in (RESOURCE_TYPE, LIBRARY_TYPE, SUITE_TYPE) or not model.keywords: 51 | return 52 | for index, kw in enumerate(model.keywords): 53 | self.timings += kw.timings 54 | new_id = f"{self.element_id}-k{index}" 55 | self.keywords.append( 56 | KeywordResult( 57 | element_id=new_id, 58 | name=kw.name, 59 | used=kw.used, 60 | complexity=kw.complexity, 61 | status=kw.status, 62 | timings=kw.timings, 63 | ) 64 | ) 65 | 66 | @staticmethod 67 | def get_children_with_init_first(model): 68 | children = [child for child in model.children] 69 | for index, child in enumerate(children): 70 | if child.name in INIT_EXT: 71 | yield children.pop(index) 72 | for child in children: 73 | yield child 74 | 75 | def fill_children(self, model): 76 | if model.type != DIRECTORY_TYPE: 77 | return 78 | for index, child in enumerate(self.get_children_with_init_first(model)): 79 | new_id = f"{self.element_id}-r{index}" 80 | model = HtmlResultModel(new_id, child) 81 | self.timings += model.timings 82 | self.children.append(model) 83 | 84 | 85 | class HTMLReport(sherlock.report.Report): 86 | name: str = "html" 87 | description: str = "HTML report" 88 | 89 | def get_report(self, directory, name, output_dir): 90 | result = HtmlResultModel("d0", directory) 91 | with open(Path(__file__).parent / "html.template") as f: 92 | template = Template(f.read()).render(tree=[result]) 93 | with open(output_dir / f"sherlock_{name}.html", "w") as f: 94 | f.write(template) 95 | -------------------------------------------------------------------------------- /sherlock/report/html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 284 | 335 | 336 | 337 | 345 | 346 | 347 | 348 | 351 | {% for directory in tree recursive %} 352 | {%- if directory.show -%} 353 |
354 |
355 |
356 | {%- if directory.timings is not none%} 357 | {{ directory.timings.total }} s 358 | {% endif %} 359 | {{ directory.type }} 360 | {{ directory.name }} 361 |
362 |
363 |
364 | 477 |
478 | {%- endif -%} 479 | {%- endfor -%} 480 | 481 | 482 | 483 | -------------------------------------------------------------------------------- /sherlock/report/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import sherlock.report 4 | from sherlock.model import DIRECTORY_TYPE 5 | 6 | 7 | def directory_to_json(directory): 8 | ret = {"name": str(directory.name), "type": directory.type} 9 | if directory.type == DIRECTORY_TYPE: # TODO Directory can have keywords (__init__.py) 10 | ret["children"] = [directory_to_json(resource) for resource in directory.children] 11 | else: 12 | if directory.keywords is None: 13 | ret["keywords"] = [] 14 | else: 15 | ret["keywords"] = [ 16 | {"name": kw.name, "used": kw.used, "complexity": kw.complexity, "status": "pass"} # TODO 17 | for kw in directory.keywords 18 | ] 19 | return ret 20 | 21 | 22 | class JsonReport(sherlock.report.Report): 23 | name: str = "json" 24 | description: str = "JSON report" 25 | 26 | def get_report(self, tree, tree_name, path_root): 27 | ret = directory_to_json(tree) 28 | with open(path_root / f"sherlock_{tree_name}.json", "w") as f: 29 | json.dump(ret, f, indent=4) 30 | -------------------------------------------------------------------------------- /sherlock/report/print.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console, Group 2 | from rich.markup import escape 3 | from rich.table import Table 4 | from rich.text import Text 5 | from rich.tree import Tree 6 | 7 | import sherlock.report 8 | from sherlock.model import DIRECTORY_TYPE, KeywordTimings 9 | 10 | 11 | def timings_to_table(timings): 12 | timings_table = Table(title="Elapsed time") 13 | for col in ["Total elapsed [s]", "Shortest execution [s]", "Longest execution [s]", "Average execution [s]"]: 14 | timings_table.add_column(col) 15 | timings_table.add_row(timings.total, timings.min, timings.max, timings.avg) 16 | return timings_table 17 | 18 | 19 | def keywords_to_table(keywords): 20 | has_complexity = any(kw.complexity for kw in keywords) 21 | table = Table(title="Keywords:") 22 | table.add_column("Name", justify="left", no_wrap=True) 23 | table.add_column("Executions") 24 | if has_complexity: 25 | table.add_column("Complexity") 26 | table.add_column("Average time [s]") 27 | table.add_column("Total time [s]") 28 | for kw in keywords: 29 | name = kw.name if kw.used else f"[cyan]{kw.name}" 30 | row = [name, str(kw.used)] 31 | if has_complexity: 32 | row.append(str(kw.complexity)) 33 | if kw.used: 34 | row.extend([str(kw.timings.avg), str(kw.timings.total)]) 35 | else: 36 | row.extend(["", ""]) 37 | table.add_row(*row) 38 | return table 39 | 40 | 41 | def log_directory(directory, tree): 42 | for resource in directory.children: 43 | if resource.type == DIRECTORY_TYPE: 44 | style = "dim" if resource.name.startswith("__") else "" 45 | branch = tree.add( 46 | f"[bold magenta][link file://{resource.name}]{escape(resource.name)}", 47 | style=style, 48 | guide_style=style, 49 | ) 50 | log_directory(resource, branch) 51 | else: 52 | text = Text(str(resource)) 53 | keywords = [kw for kw in resource.keywords] 54 | if keywords: 55 | timings = sum((kw.timings for kw in keywords if kw.used), KeywordTimings()) 56 | timings_table = timings_to_table(timings) 57 | keywords_table = keywords_to_table(keywords) 58 | 59 | tree.add(Group(text, timings_table, keywords_table)) 60 | else: 61 | tree.add(text) 62 | 63 | 64 | class PrintReport(sherlock.report.Report): 65 | name: str = "print" 66 | description: str = "Simple printed report" 67 | 68 | def get_report(self, directory, tree_name, path_root): 69 | tree = Tree( 70 | f"[link file://{directory}]{directory}", 71 | guide_style="bold bright_blue", 72 | ) 73 | log_directory(directory, tree) 74 | console = Console() 75 | console.print() 76 | console.print(tree) 77 | -------------------------------------------------------------------------------- /sherlock/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.0" 2 | -------------------------------------------------------------------------------- /sherlock/visitor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import OrderedDict 3 | from pathlib import Path 4 | 5 | import robot.errors 6 | from robot.api import SuiteVisitor 7 | from robot.errors import DataError 8 | from robot.utils import find_file 9 | from robot.variables.scopes import VariableScopes 10 | from robot.variables.variables import Variables 11 | 12 | from sherlock.model import LIBRARY_TYPE 13 | 14 | 15 | def _normalize_library_path(library): 16 | path = library.replace("/", os.sep) 17 | if os.path.exists(path): 18 | return os.path.abspath(path) 19 | return library 20 | 21 | 22 | def _get_library_name(name, directory): 23 | if not _is_library_by_path(name): 24 | return name 25 | try: 26 | return find_file(name, directory, LIBRARY_TYPE) 27 | except robot.errors.DataError as err: 28 | print(f"Failed to import library: {err}") 29 | return None 30 | 31 | 32 | def _is_library_by_path(path): 33 | return path.lower().endswith((".py", "/", os.sep)) 34 | 35 | 36 | class StructureVisitor(SuiteVisitor): 37 | def __init__(self, resources, from_output, robot_settings): 38 | self.resources = resources 39 | self.from_output = from_output 40 | self.variables = VariableScopes(robot_settings) 41 | self.suite_resource = None 42 | self.imported_resources = OrderedDict() 43 | self.imported_libraries = OrderedDict() 44 | 45 | self.suite_errors = set() 46 | self.errors = [] 47 | 48 | def init_imports(self, resource, variables=None): 49 | # TODO read variables from suite and add it to self.variables 50 | current_variables = Variables() 51 | # global vars 52 | current_variables.update( 53 | self.variables if hasattr(self.variables, "store") else self.variables.current 54 | ) # FIXME 55 | # resource vars 56 | current_variables.update(resource.variables.copy()) 57 | # vars from resources importing given resource 58 | if variables: 59 | current_variables.update(variables.copy()) 60 | for res in resource.resources: 61 | try: 62 | res = current_variables.replace_string(res) 63 | except DataError as err: 64 | pass 65 | # TODO 66 | # self._raise_replacing_vars_failed(import_setting, err) 67 | res = str(Path(resource.directory, res).resolve()) # FIXME 68 | if res in self.resources and res not in self.imported_resources: 69 | self.imported_resources[res] = self.resources[res] 70 | self.init_imports(self.resources[res], current_variables) 71 | 72 | for (lib, alias), args in resource.libraries.items(): 73 | library = current_variables.replace_string(lib) 74 | library = _normalize_library_path(library) 75 | library = _get_library_name(library, resource.directory) 76 | if library is None: 77 | continue 78 | if library in self.resources: 79 | self.resources[library].load_library(args, current_variables, resource.path) 80 | lib_name = alias or self.resources[library].name 81 | self.imported_libraries[lib_name] = self.resources[library] 82 | 83 | def visit_suite(self, suite): 84 | self.imported_resources = OrderedDict() 85 | self.imported_libraries = OrderedDict() 86 | if str(suite.source) in self.resources: 87 | suite_resource = self.resources[str(suite.source)] 88 | elif Path(suite.source).is_dir() and str(Path(suite.source) / "__init__.robot") in self.resources: 89 | suite_resource = self.resources[str(Path(suite.source) / "__init__.robot")] 90 | else: 91 | suite_resource = "" 92 | # raise SherlockFatalError(f"Could not find definition of '{suite.source}' suite") # TODO 93 | if suite_resource: 94 | self.suite_resource = suite_resource 95 | self.init_imports(suite_resource) 96 | suite.setup.visit(self) 97 | suite.tests.visit(self) 98 | 99 | # TODO improve error handling 100 | # if self.suite_errors: 101 | # self.errors.append(f"\nErrors in {suite.source}:") 102 | # self.errors.extend(list(self.suite_errors)) 103 | 104 | suite.teardown.visit(self) 105 | suite.suites.visit(self) 106 | 107 | def visit_keyword(self, kw): 108 | name = kw.kwname if self.from_output else kw.name 109 | lib_name = kw.libname if self.from_output else None 110 | found = self.search_def(name, lib_name) 111 | if not found: 112 | self.suite_errors.add(f"Keyword '{name}' definition not found") 113 | elif len(found) > 1: 114 | s = f"Keyword '{name}' matches following resources/libraries:\n" 115 | self.suite_errors.add(s) 116 | else: 117 | found[0].used += 1 118 | if self.from_output: 119 | found[0].timings.add_timing(kw.elapsedtime) 120 | if hasattr(kw, "body"): 121 | kw.body.visit(self) 122 | if getattr(kw, "teardown", None): 123 | kw.teardown.visit(self) 124 | 125 | def search_def(self, kw_name, lib_name): 126 | found = [] 127 | if self.suite_resource: 128 | found += self.suite_resource.search(kw_name, lib_name) 129 | if found: 130 | return found 131 | for res_name, resource in self.imported_resources.items(): 132 | found += resource.search(kw_name, lib_name) 133 | if found: 134 | return found 135 | for lib_name_or_alias, library in self.imported_libraries.items(): 136 | if lib_name and lib_name != lib_name_or_alias: 137 | continue 138 | found += library.search(kw_name) 139 | if not found: 140 | return self.resources["BuiltIn"].search(kw_name) 141 | return found 142 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/__init__.py -------------------------------------------------------------------------------- /tests/atest/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | from pathlib import Path 4 | 5 | from sherlock.config import Config, _process_pythonpath 6 | from sherlock.core import Sherlock 7 | 8 | 9 | def run_sherlock(robot_output, source, report=None, resource=None, pythonpath=None): 10 | config = Config(from_cli=False) 11 | config.output = robot_output 12 | config.path = source 13 | if report is not None: 14 | config.report = report 15 | if resource is not None: 16 | config.resource = resource 17 | if pythonpath is not None: 18 | config.pythonpath = _process_pythonpath([pythonpath]) 19 | config.validate_test_path() 20 | 21 | sherlock = Sherlock(config=config) 22 | sherlock.run() # TODO create special report readable by tests? 23 | return sherlock 24 | 25 | 26 | def get_output(output): 27 | with open(output) as f: 28 | data = json.load(f) 29 | Path(output).unlink() 30 | return data 31 | 32 | 33 | def sort_by_name(collection): 34 | return sorted(collection, key=lambda x: x["name"]) 35 | 36 | 37 | def match_tree(expected, actual): 38 | if expected["name"] != actual["name"]: 39 | print(f"Expected name '{expected['name']}' does not match actual name '{actual['name']}'") 40 | return False 41 | 42 | if "keywords" in expected: 43 | if len(expected["keywords"]) != len(actual["keywords"]): 44 | print( 45 | f"Expected number of keywords in {expected['name']}: {len(expected['keywords'])} " 46 | f"does not match actual: {len(actual['keywords'])}" 47 | ) 48 | return False 49 | expected["keywords"] = sort_by_name(expected["keywords"]) 50 | actual["keywords"] = sort_by_name(actual["keywords"]) 51 | for exp_keyword, act_keyword in zip(expected["keywords"], actual["keywords"]): 52 | if "used" not in exp_keyword: 53 | act_keyword.pop("used", None) 54 | if "complexity" not in exp_keyword: 55 | act_keyword.pop("complexity", None) 56 | if "status" not in exp_keyword: 57 | act_keyword.pop("status", None) 58 | if exp_keyword != act_keyword: 59 | return False 60 | 61 | if "children" in expected: 62 | if len(expected["children"]) != len(actual["children"]): 63 | print( 64 | f"Expected length of children tree: {len(expected['children'])} " 65 | f"does not match actual: {len(actual['children'])}" 66 | ) 67 | return False 68 | expected["children"] = sort_by_name(expected["children"]) 69 | actual["children"] = sort_by_name(actual["children"]) 70 | if not all( 71 | match_tree(exp_child, act_child) for exp_child, act_child in zip(expected["children"], actual["children"]) 72 | ): 73 | return False 74 | 75 | if "type" in expected: 76 | if expected["type"] != actual["type"]: 77 | print(f"Resource type does not match: {expected['type']} != {actual['type']}") 78 | return False 79 | return True 80 | 81 | 82 | class Tree: 83 | def __init__(self, name, keywords=None, children=None, res_type=None): 84 | self.name = name 85 | self.keywords = keywords 86 | self.children = children 87 | self.res_type = res_type 88 | 89 | def to_json(self): 90 | ret = {"name": self.name} 91 | if self.keywords is not None: 92 | ret["keywords"] = [kw.to_json() for kw in self.keywords] 93 | if self.children is not None: 94 | ret["children"] = [child.to_json() for child in self.children] 95 | if self.res_type is not None: 96 | ret["type"] = self.res_type 97 | return ret 98 | 99 | def __str__(self): 100 | return str(self.to_json()) 101 | 102 | 103 | class Keyword: 104 | def __init__(self, name, used=None, complexity=None): 105 | self.name = name 106 | self.used = used 107 | self.complexity = complexity 108 | 109 | def to_json(self): 110 | ret = {"name": self.name} 111 | if self.used is not None: 112 | ret["used"] = self.used 113 | if self.complexity is not None: 114 | ret["complexity"] = self.complexity 115 | return ret 116 | 117 | 118 | class AcceptanceTest: 119 | ROOT = Path() 120 | TEST_PATH = "test.robot" 121 | 122 | def run_robot(self, pythonpath=None): 123 | source = self.ROOT / self.TEST_PATH 124 | cmd = f"robot --outputdir {self.ROOT}" 125 | if pythonpath: 126 | cmd += f" --pythonpath {pythonpath}" 127 | cmd += f" {source}" 128 | subprocess.run(cmd.split(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 129 | 130 | def run_sherlock(self, source=None, resource=None, report=None, pythonpath=None, run_robot=True): 131 | if report is None: 132 | report = ["json"] 133 | if run_robot: 134 | self.run_robot(pythonpath) 135 | robot_output = self.ROOT / "output.xml" 136 | else: 137 | robot_output = None 138 | if source: 139 | source = source if source.is_dir() else source.parent 140 | else: 141 | source = self.ROOT 142 | run_sherlock(robot_output=robot_output, source=source, report=report, resource=resource, pythonpath=pythonpath) 143 | data = get_output(f"sherlock_{source.name}.json") 144 | return data 145 | 146 | @staticmethod 147 | def should_match_tree(expected_tree, actual): 148 | expected = expected_tree.to_json() 149 | assert match_tree(expected, actual) 150 | -------------------------------------------------------------------------------- /tests/atest/aliased_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/aliased_library/__init__.py -------------------------------------------------------------------------------- /tests/atest/aliased_library/test_aliased_library.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestDuplicatedNamesDotted(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="Library", keywords=[Keyword(name="Keyword 1", used=2)]), 15 | Tree(name="test.robot", keywords=[]), 16 | ], 17 | ) 18 | self.should_match_tree(expected, data) 19 | -------------------------------------------------------------------------------- /tests/atest/aliased_library/test_data/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def keyword1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/aliased_library/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library Library.py 3 | Library Library.py WITH NAME Aliased 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Library.Keyword 1 9 | Aliased.Keyword 1 10 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/complicated_structure/__init__.py -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_complicated_structure.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, run_sherlock 4 | 5 | 6 | class TestComplicatedStructure(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | TEST_PATH = "tests" 9 | 10 | def test(self): 11 | self.run_robot() 12 | run_sherlock(robot_output=self.ROOT / "output.xml", source=self.ROOT, report=["html"]) 13 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_data/libraries/MyLibrary/MyLibrary.py: -------------------------------------------------------------------------------- 1 | class MyLibrary: 2 | def python_keyword(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_data/libraries/MyLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | from .MyLibrary import MyLibrary 2 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_data/resources/common/test.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Robot Keyword 2 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_data/resources/test.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library ..${/}libraries${/}MyLibrary/ 3 | Resource common${/}test.resource 4 | 5 | 6 | *** Keywords *** 7 | Robot Keyword 8 | No Operation 9 | -------------------------------------------------------------------------------- /tests/atest/complicated_structure/test_data/tests/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ..${/}resources${/}test.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Python Keyword 8 | Robot Keyword 9 | FOR ${a} IN RANGE 3 10 | Robot Keyword 2 11 | END 12 | -------------------------------------------------------------------------------- /tests/atest/dict_variables/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/dict_variables/__init__.py -------------------------------------------------------------------------------- /tests/atest/dict_variables/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Variables *** 2 | &{DICT VAR} key=value 3 | ... key2=value 4 | @{LIST} 1 2 3 5 | ${VAR} ENV 6 | ${VAR2} ENV2 # comment 7 | VAR 3 8 | &{CONTAINS_OTHER} &{MANY} 9 | &{INVALID} key 10 | &{EQUAL_SIGN}= key=string with = sign 11 | &{EQUAL_SIGN} = key=string with = sign 12 | &{EMPTY} 13 | 14 | *** Test Cases *** 15 | Test 16 | Keyword 1 17 | 18 | *** Keywords *** 19 | Keyword 1 20 | No Operation 21 | -------------------------------------------------------------------------------- /tests/atest/dict_variables/test_dict_variables.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestDictVariables(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock(source=self.ROOT / "test.robot") 11 | expected = Tree( 12 | name="test_data", 13 | res_type="Directory", 14 | children=[ 15 | Tree( 16 | name="test.robot", res_type="Resource", keywords=[Keyword(name="Keyword 1", used=1, complexity=1)] 17 | ), 18 | ], 19 | ) 20 | self.should_match_tree(expected, data) 21 | -------------------------------------------------------------------------------- /tests/atest/duplicated_imports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/duplicated_imports/__init__.py -------------------------------------------------------------------------------- /tests/atest/duplicated_imports/test_data/test.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/duplicated_imports/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource test.resource 3 | Resource test.resource 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 1 9 | Keyword 1 10 | -------------------------------------------------------------------------------- /tests/atest/duplicated_imports/test_duplicated_imports.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from tests.atest import AcceptanceTest, Keyword, Tree 6 | 7 | 8 | class TestDuplicatedImports(AcceptanceTest): 9 | ROOT = Path(__file__).parent / "test_data" 10 | 11 | @pytest.mark.parametrize("run_robot", [True, False]) 12 | def test(self, run_robot): 13 | data = self.run_sherlock(run_robot=run_robot) 14 | expected = Tree( 15 | name="test_data", 16 | children=[ 17 | Tree(name="test.resource", keywords=[Keyword(name="Keyword 1", used=2, complexity=1)]), 18 | Tree(name="test.robot", keywords=[]), 19 | ], 20 | ) 21 | self.should_match_tree(expected, data) 22 | -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/duplicated_names_dotted/__init__.py -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/test_data/Library1.py: -------------------------------------------------------------------------------- 1 | class Library1: 2 | def keyword1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/test_data/Library2.py: -------------------------------------------------------------------------------- 1 | class Library2: 2 | def keyword1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/test_data/test.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library Library1.py 3 | Library Library2.py 4 | Resource test.resource 5 | 6 | 7 | *** Test Cases *** 8 | Test 9 | Library1.Keyword 1 10 | Library2.Keyword 1 11 | library1.Keyword 1 12 | FOR ${i} IN RANGE 21 13 | Keyword 1 14 | END 15 | -------------------------------------------------------------------------------- /tests/atest/duplicated_names_dotted/test_duplicated_names_dotted.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestDuplicatedNamesDotted(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="Library1", keywords=[Keyword(name="Keyword 1", used=2)]), 15 | Tree(name="Library2", keywords=[Keyword(name="Keyword 1", used=1)]), 16 | Tree(name="test.resource", keywords=[Keyword(name="Keyword 1", used=21, complexity=1)]), 17 | Tree(name="test.robot", keywords=[]), 18 | ], 19 | ) 20 | self.should_match_tree(expected, data) 21 | -------------------------------------------------------------------------------- /tests/atest/empty_keywords/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/empty_keywords/__init__.py -------------------------------------------------------------------------------- /tests/atest/empty_keywords/test_data/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def not_used(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/empty_keywords/test_data/test.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | -------------------------------------------------------------------------------- /tests/atest/empty_keywords/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource test.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Keyword 1 8 | Keyword 1 9 | 10 | *** Keywords *** 11 | Keyword 1 12 | No Operation 13 | -------------------------------------------------------------------------------- /tests/atest/empty_keywords/test_empty_keywords.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestEmptyKeywords(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock(report=["json", "print"]) 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="test.resource", keywords=[]), 15 | # Library is never imported anywhere, so we can't check keywords 16 | Tree(name="Library.py", keywords=[]), 17 | Tree(name="test.robot", keywords=[Keyword(name="Keyword 1", used=2, complexity=1)]), 18 | ], 19 | ) 20 | self.should_match_tree(expected, data) 21 | -------------------------------------------------------------------------------- /tests/atest/external_import/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/external_import/__init__.py -------------------------------------------------------------------------------- /tests/atest/external_import/test_data/ext_libs/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def keyword1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/external_import/test_data/tests/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library ../ext_libs/Library.py 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Keyword 1 8 | -------------------------------------------------------------------------------- /tests/atest/external_import/test_external_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree, get_output 4 | 5 | 6 | class TestExternalImport(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" / "tests" 8 | 9 | def test(self): 10 | ext_libs_path = str(Path(__file__).parent / "test_data" / "ext_libs") 11 | data = self.run_sherlock(resource=[ext_libs_path]) 12 | expected = Tree( 13 | name="tests", 14 | children=[Tree(name="test.robot", keywords=[])], 15 | ) 16 | self.should_match_tree(expected, data) 17 | external_data = get_output("sherlock_ext_libs.json") 18 | external_expected = Tree( 19 | name="ext_libs", children=[Tree(name="Library", keywords=[Keyword(name="Keyword 1", used=1)])] 20 | ) 21 | self.should_match_tree(external_expected, external_data) 22 | -------------------------------------------------------------------------------- /tests/atest/external_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/external_library/__init__.py -------------------------------------------------------------------------------- /tests/atest/external_library/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library TemplatedData 3 | 4 | *** Test Cases *** 5 | Test 6 | ${data} Get Templated Data From Path test_data.txt 7 | 8 | Other test 9 | ${data} Get Templated Data From Path test_data.txt 10 | -------------------------------------------------------------------------------- /tests/atest/external_library/test_data/test_data.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/external_library/test_data/test_data.txt -------------------------------------------------------------------------------- /tests/atest/external_library/test_external_library.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from tests.atest import AcceptanceTest, Keyword, Tree, get_output 6 | 7 | 8 | @pytest.mark.skip("does not work in CI - need to investigate") # TODO 9 | class TestExternalLibrary(AcceptanceTest): 10 | ROOT = Path(__file__).parent / "test_data" 11 | 12 | def test_external_not_in_resource_option(self): 13 | data = self.run_sherlock() 14 | expected = Tree( 15 | name="test_data", 16 | children=[ 17 | Tree(name="test.robot", keywords=[]), 18 | ], 19 | ) 20 | self.should_match_tree(expected, data) 21 | 22 | def test_external_in_resource_option(self): 23 | self.run_sherlock(resource=["TemplatedData"]) 24 | data = get_output("sherlock_TemplatedData.json") 25 | expected = Tree( 26 | name="TemplatedData", 27 | children=[ 28 | Tree( 29 | name="TemplatedData", 30 | keywords=[ 31 | Keyword(name="Get Templated Data", used=0), 32 | Keyword(name="Get Templated Data From Path", used=2), 33 | Keyword(name="Normalize"), 34 | Keyword(name="Return Data With Type"), 35 | ], 36 | ), 37 | ], 38 | ) 39 | self.should_match_tree(expected, data) 40 | -------------------------------------------------------------------------------- /tests/atest/import_with_variable/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/import_with_variable/__init__.py -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_data/tests/a.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ${VARIABLE}.resource 3 | -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_data/tests/b.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | Internal Keyword 4 | 5 | Internal Keyword 6 | Log I am in b -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_data/tests/c.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | Internal Keyword 4 | 5 | Internal Keyword 6 | Log I am in c 7 | -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_data/tests/test_a.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | 4 | 5 | *** Variables *** 6 | ${VARIABLE} b 7 | 8 | *** Test Cases *** 9 | Test 10 | Keyword 1 11 | 12 | 13 | *** Keywords *** 14 | Internal Keyword 15 | Log I am called from b.resource 16 | -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_data/tests/test_b.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | 4 | 5 | *** Variables *** 6 | ${VARIABLE} c 7 | 8 | *** Test Cases *** 9 | Test 10 | Keyword 1 11 | -------------------------------------------------------------------------------- /tests/atest/import_with_variable/test_import_with_variable.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestImportWithVariable(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | TEST_PATH = "tests" 9 | 10 | def test(self): 11 | data = self.run_sherlock() 12 | expected = Tree( 13 | name="test_data", 14 | children=[ 15 | Tree( 16 | name="tests", 17 | children=[ 18 | Tree(name="a.resource", keywords=[]), 19 | Tree( 20 | name="b.resource", 21 | keywords=[Keyword(name="Internal Keyword", used=0), Keyword(name="Keyword 1", used=1)], 22 | ), 23 | Tree( 24 | name="c.resource", 25 | keywords=[Keyword(name="Internal Keyword", used=1), Keyword(name="Keyword 1", used=1)], 26 | ), 27 | Tree(name="test_a.robot", keywords=[Keyword(name="Internal Keyword", used=1)]), 28 | Tree(name="test_b.robot", keywords=[]), 29 | ], 30 | ) 31 | ], 32 | ) 33 | self.should_match_tree(expected, data) 34 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/invalid_library/__init__.py -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_data/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def not_used(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_data/LibraryAccessRunning.py: -------------------------------------------------------------------------------- 1 | from robot.libraries.BuiltIn import BuiltIn 2 | 3 | 4 | class LibraryAccessRunning: 5 | def __init__(self): 6 | self.lib_instance = BuiltIn().get_library_instance("Selenium") 7 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_data/LibraryWithArgs.py: -------------------------------------------------------------------------------- 1 | class LibraryWithArgs: 2 | def __init__(self, arg): 3 | self.arg = arg 4 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_data/LibraryWithFailingInit.py: -------------------------------------------------------------------------------- 1 | class LibraryWithFailingInit: 2 | def __init__(self): 3 | raise ValueError("Invalid value") 4 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library IDontExist.py 3 | Library LibraryWithArgs.py # missing required arg 4 | Library Library.py arg # library that does not have arguments 5 | Library LibraryWithFailingInit.py 6 | Library LibraryAccessRunning.py 7 | 8 | 9 | *** Test Cases *** 10 | Test 11 | Keyword 1 12 | 13 | *** Keywords *** 14 | Keyword 1 15 | No Operation 16 | -------------------------------------------------------------------------------- /tests/atest/invalid_library/test_invalid_library.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestInvalidLibrary(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock(report=["json", "print"]) 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="Library.py", keywords=[]), 15 | Tree(name="LibraryWithArgs.py", keywords=[]), 16 | Tree(name="LibraryWithFailingInit.py", keywords=[]), 17 | Tree(name="LibraryAccessRunning.py", keywords=[]), 18 | Tree(name="test.robot", keywords=[Keyword(name="Keyword 1", used=1, complexity=1)]), 19 | ], 20 | ) 21 | self.should_match_tree(expected, data) 22 | -------------------------------------------------------------------------------- /tests/atest/keyword_in_res_and_suite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/keyword_in_res_and_suite/__init__.py -------------------------------------------------------------------------------- /tests/atest/keyword_in_res_and_suite/test_data/kw.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Ambiguous Name 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/keyword_in_res_and_suite/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource kw.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Ambiguous Name 8 | kw.Ambiguous Name 9 | 10 | *** Keywords *** 11 | Ambiguous Name 12 | No Operation 13 | -------------------------------------------------------------------------------- /tests/atest/keyword_in_res_and_suite/test_keyword_in_res_and_suite.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestKeywordInResAndSuite(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="kw.resource", keywords=[Keyword(name="Ambiguous Name", used=1)]), 15 | Tree(name="test.robot", keywords=[Keyword(name="Ambiguous Name", used=1)]), 16 | ], 17 | ) 18 | self.should_match_tree(expected, data) 19 | -------------------------------------------------------------------------------- /tests/atest/library_from_resource/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/library_from_resource/__init__.py -------------------------------------------------------------------------------- /tests/atest/library_from_resource/test_data/MyStuff.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class MyStuff: 5 | def my_keyword(self): 6 | time.sleep(2) 7 | 8 | def not_used(self): 9 | pass 10 | 11 | def third_keyword(self): 12 | time.sleep(1) 13 | -------------------------------------------------------------------------------- /tests/atest/library_from_resource/test_data/imports.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library MyStuff.py 3 | -------------------------------------------------------------------------------- /tests/atest/library_from_resource/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource imports.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | My Keyword 8 | Third Keyword 9 | 10 | Test2 11 | Third Keyword 12 | -------------------------------------------------------------------------------- /tests/atest/library_from_resource/test_library_from_resource.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from tests.atest import AcceptanceTest, Keyword, Tree 6 | 7 | 8 | @pytest.mark.skip(reason="Library import with sleeps for timing tests") 9 | class TestLibraryFromResource(AcceptanceTest): 10 | ROOT = Path(__file__).parent / "test_data" 11 | 12 | def test(self): 13 | data = self.run_sherlock() 14 | expected = Tree( 15 | name="test_data", 16 | children=[ 17 | Tree(name="imports.resource", keywords=[]), 18 | Tree( 19 | name="MyStuff", 20 | keywords=[ 21 | Keyword(name="My Keyword", used=1), 22 | Keyword(name="Not Used", used=0), 23 | Keyword(name="Third Keyword", used=2), 24 | ], 25 | ), 26 | Tree(name="test.robot", keywords=[]), 27 | ], 28 | ) 29 | self.should_match_tree(expected, data) 30 | -------------------------------------------------------------------------------- /tests/atest/multiple_sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/multiple_sources/__init__.py -------------------------------------------------------------------------------- /tests/atest/multiple_sources/test_data/resource1/file.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/multiple_sources/test_data/resource2/file2.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 2 3 | No Operation 4 | 5 | Keyword 3 6 | No Operation 7 | -------------------------------------------------------------------------------- /tests/atest/multiple_sources/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource resource1${/}file.resource 3 | Resource resource2${/}file2.resource 4 | 5 | *** Test Cases *** 6 | Test 7 | Keyword 1 8 | Keyword 2 9 | -------------------------------------------------------------------------------- /tests/atest/multiple_sources/test_multiple_sources.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestMultipleSources(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | # def test_two_sources(self, path_to_test_data): 10 | # robot_output = path_to_test_data / "output.xml" 11 | # source1 = path_to_test_data / "resource1" 12 | # source2 = path_to_test_data / "resource2" # TODO 13 | # run_sherlock(robot_output=robot_output, source=[source1, source2], report=["json"]) 14 | 15 | def test_single_source(self): 16 | source = self.ROOT / "resource1" 17 | data = self.run_sherlock(source=source) 18 | # TODO used 0 since it doesn't know it was used by test.robot 19 | expected = Tree( 20 | name="resource1", 21 | children=[Tree(name="file.resource", keywords=[Keyword(name="Keyword 1", used=0, complexity=1)])], 22 | ) 23 | self.should_match_tree(expected, data) 24 | -------------------------------------------------------------------------------- /tests/atest/nested_modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/nested_modules/__init__.py -------------------------------------------------------------------------------- /tests/atest/nested_modules/test_data/pages/Page1/__init__.py: -------------------------------------------------------------------------------- 1 | class Page1: 2 | def keyword_1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/nested_modules/test_data/pages/Page2/__init__.py: -------------------------------------------------------------------------------- 1 | class Page2: 2 | def keyword_2(self): 3 | pass 4 | 5 | def keyword_3(self): 6 | pass 7 | -------------------------------------------------------------------------------- /tests/atest/nested_modules/test_data/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/nested_modules/test_data/pages/__init__.py -------------------------------------------------------------------------------- /tests/atest/nested_modules/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library pages${/}Page1${/} 3 | Library pages${/}Page2${/} 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 1 9 | Keyword 2 10 | -------------------------------------------------------------------------------- /tests/atest/nested_modules/test_nested_modules.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestNestedModules(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | res_type="Directory", 14 | children=[ 15 | Tree( 16 | name="pages", 17 | res_type="Directory", 18 | children=[ 19 | Tree(name="Page1", res_type="Library", keywords=[Keyword(name="Keyword 1", used=1)]), 20 | Tree( 21 | name="Page2", 22 | res_type="Library", 23 | keywords=[Keyword(name="Keyword 2", used=1), Keyword(name="Keyword 3", used=0)], 24 | ), 25 | Tree(name="pages", res_type="Library", keywords=[]), 26 | ], 27 | ), 28 | Tree(name="test.robot", res_type="Resource", keywords=[]), 29 | ], 30 | ) 31 | self.should_match_tree(expected, data) 32 | -------------------------------------------------------------------------------- /tests/atest/pythonpath/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/pythonpath/__init__.py -------------------------------------------------------------------------------- /tests/atest/pythonpath/test_data/nested/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def keyword1(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/pythonpath/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library Library.py 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Library.Keyword 1 8 | -------------------------------------------------------------------------------- /tests/atest/pythonpath/test_python_path.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestPythonPath(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | cwd = Path(__file__).parent / "test_data" / "nested" 11 | data = self.run_sherlock(pythonpath=str(cwd)) 12 | expected = Tree( 13 | name="test_data", 14 | children=[ 15 | Tree( 16 | name="nested", 17 | res_type="Directory", 18 | children=[Tree(name="Library", keywords=[Keyword(name="Keyword 1", used=1)])], 19 | ), 20 | Tree(name="test.robot", keywords=[]), 21 | ], 22 | ) 23 | self.should_match_tree(expected, data) 24 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/recursive_import/__init__.py -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_data/Library.py: -------------------------------------------------------------------------------- 1 | class Library: 2 | def my_keyword(self): 3 | pass 4 | 5 | def not_used(self): 6 | pass 7 | 8 | def third_keyword(self): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_data/Library2.py: -------------------------------------------------------------------------------- 1 | class Library2: 2 | def keyword_from_lib2(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_data/base.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library Library.py 3 | Resource sub_resource.resource 4 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_data/sub_resource.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource base.resource 3 | Library Library2.py 4 | 5 | *** Keywords *** 6 | Sub resource keyword 7 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_data/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource base.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | My Keyword 8 | Third Keyword 9 | 10 | Test2 11 | Third Keyword 12 | -------------------------------------------------------------------------------- /tests/atest/recursive_import/test_recursive_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestRecursiveImport(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | TEST_PATH = "" 9 | 10 | def test(self): 11 | data = self.run_sherlock() 12 | expected = Tree( 13 | name="test_data", 14 | children=[ 15 | Tree(name="base.resource", res_type="Resource", keywords=[]), 16 | Tree( 17 | name="Library", 18 | res_type="Library", 19 | keywords=[ 20 | Keyword(name="My Keyword", used=1), 21 | Keyword(name="Not Used", used=0), 22 | Keyword(name="Third Keyword", used=2), 23 | ], 24 | ), 25 | Tree( 26 | name="Library2", 27 | res_type="Library", 28 | keywords=[ 29 | Keyword(name="Keyword From Lib2", used=0), 30 | ], 31 | ), 32 | Tree( 33 | name="sub_resource.resource", 34 | res_type="Resource", 35 | keywords=[Keyword(name="Sub resource keyword", used=0)], 36 | ), 37 | Tree(name="suite.robot", keywords=[]), 38 | ], 39 | ) 40 | self.should_match_tree(expected, data) 41 | -------------------------------------------------------------------------------- /tests/atest/resolve_variables/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/resolve_variables/__init__.py -------------------------------------------------------------------------------- /tests/atest/resolve_variables/test_data/Library1.py: -------------------------------------------------------------------------------- 1 | class Library1: 2 | def __init__(self, arg): 3 | self.arg = arg 4 | 5 | def keyword1(self): 6 | pass 7 | -------------------------------------------------------------------------------- /tests/atest/resolve_variables/test_data/Library2.py: -------------------------------------------------------------------------------- 1 | from robot.api import logger 2 | 3 | 4 | class Library2: 5 | def __init__(self, arg): 6 | self.arg = arg 7 | 8 | def keyword1(self): 9 | logger.info(f"Variable value: {self.arg}") 10 | -------------------------------------------------------------------------------- /tests/atest/resolve_variables/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library ${library}.py ${VARIABLE} 3 | 4 | 5 | *** Variables *** 6 | ${LIBRARY} Library1 7 | ${VARIABLE} ${LIBRARY} 8 | 9 | *** Test Cases *** 10 | Test 11 | Keyword 1 12 | -------------------------------------------------------------------------------- /tests/atest/resolve_variables/test_resolve_variables.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestResolveVariables(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree(name="Library1", keywords=[Keyword(name="Keyword 1", used=1)]), 15 | Tree(name="Library2.py", keywords=[]), 16 | Tree(name="test.robot", keywords=[]), 17 | ], 18 | ) 19 | self.should_match_tree(expected, data) 20 | -------------------------------------------------------------------------------- /tests/atest/resource_outside/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/resource_outside/__init__.py -------------------------------------------------------------------------------- /tests/atest/resource_outside/test_data/resource1/file.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | No Operation 4 | -------------------------------------------------------------------------------- /tests/atest/resource_outside/test_data/tests/resource2/file2.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 2 3 | No Operation 4 | 5 | Keyword 3 6 | No Operation 7 | -------------------------------------------------------------------------------- /tests/atest/resource_outside/test_data/tests/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ..${/}resource1${/}file.resource 3 | Resource resource2${/}file2.resource 4 | 5 | *** Test Cases *** 6 | Test 7 | Keyword 1 8 | Keyword 2 9 | -------------------------------------------------------------------------------- /tests/atest/resource_outside/test_resource_outside.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree, get_output 4 | 5 | 6 | class TestResourceOutside(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" / "tests" 8 | 9 | def test_resource_outside(self): 10 | # FIXME when path is directory, PermissionError is raised 11 | resource = self.ROOT.parent / "resource1" / "file.resource" 12 | data = self.run_sherlock(resource=[resource]) 13 | expected = Tree( 14 | name="tests", 15 | children=[ 16 | Tree( 17 | name="resource2", 18 | children=[ 19 | Tree( 20 | name="file2.resource", 21 | keywords=[ 22 | Keyword(name="Keyword 2", used=1, complexity=1), 23 | Keyword(name="Keyword 3", used=0, complexity=1), 24 | ], 25 | ) 26 | ], 27 | ), 28 | Tree(name="test.robot", keywords=[]), 29 | ], 30 | ) 31 | self.should_match_tree(expected, data) 32 | data = get_output("sherlock_file.resource.json") 33 | # TODO fix trees for single files 34 | expected = Tree( 35 | name="file.resource", 36 | children=[Tree(name="file.resource", keywords=[Keyword(name="Keyword 1", used=1, complexity=1)])], 37 | ) 38 | self.should_match_tree(expected, data) 39 | -------------------------------------------------------------------------------- /tests/atest/run_keywords/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/run_keywords/__init__.py -------------------------------------------------------------------------------- /tests/atest/run_keywords/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Suite Setup Run Keyword keyword 1 3 | Suite Teardown Run Keywords Keyword 1 AND Keyword 2 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Run Keyword If ${TRUE} Keyword 1 9 | Run Keyword And Ignore Error Keyword 3 10 | 11 | *** Keywords *** 12 | Keyword 1 13 | No Operation 14 | 15 | Keyword 2 16 | Keyword 1 17 | 18 | Keyword 3 19 | No Operation 20 | -------------------------------------------------------------------------------- /tests/atest/run_keywords/test_run_keywords.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestRunKeywords(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | expected = Tree( 12 | name="test_data", 13 | children=[ 14 | Tree( 15 | name="test.robot", 16 | keywords=[ 17 | Keyword(name="Keyword 1", used=4), 18 | Keyword(name="Keyword 2", used=1), 19 | Keyword(name="Keyword 3", used=1), 20 | ], 21 | ), 22 | ], 23 | ) 24 | self.should_match_tree(expected, data) 25 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/serarch_order/__init__.py -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_1/a.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 3 | Duplicated 4 | 5 | Duplicated 6 | Log From resource 7 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_1/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | Keyword 8 | 9 | 10 | *** Keywords *** 11 | Duplicated 12 | Log From suite 13 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_2/a.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Duplicated 3 | Log From a.resource 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_2/b.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 3 | Duplicated 4 | 5 | Duplicated 6 | Log From b.resource 7 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_2/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | Resource b.resource 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 9 | 10 | 11 | *** Keywords *** 12 | Duplicated 13 | Log From suite 14 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_3/a.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Duplicated in resource 3 | Log From a.resource 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_3/b.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Duplicated in resource 3 | Log From b.resource 4 | 5 | Keyword 6 | a.Duplicated in resource 7 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_3/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | Resource b.resource 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 9 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_4/a.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | 1 3 | Log From a.resource 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_4/b.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource from_b.resource 3 | 4 | *** Keywords *** 5 | Keyword 6 | from_b.1 7 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_4/from_b.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | 1 3 | Log from_b.resource 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_4/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | Resource b.resource 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 9 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_5/a.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 3 | Something that a.resource needs 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_5/b.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Something that a.resource needs 3 | Log b.resource 4 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/search_order_5/suite.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource a.resource 3 | Resource b.resource 4 | 5 | 6 | *** Test Cases *** 7 | Test 8 | Keyword 9 | -------------------------------------------------------------------------------- /tests/atest/serarch_order/test_search_order.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestSearchOrder1(AcceptanceTest): 7 | ROOT = Path(Path(__file__).parent, "search_order_1") 8 | TEST_PATH = "" 9 | 10 | def test(self): 11 | data = self.run_sherlock() 12 | expected = Tree( 13 | name="search_order_1", 14 | children=[ 15 | Tree( 16 | name="a.resource", 17 | res_type="Resource", 18 | keywords=[Keyword(name="Duplicated", used=0), Keyword(name="Keyword", used=1)], 19 | ), 20 | Tree(name="suite.robot", keywords=[Keyword(name="Duplicated", used=1)]), 21 | ], 22 | ) 23 | self.should_match_tree(expected, data) 24 | 25 | 26 | class TestSearchOrder2(AcceptanceTest): 27 | ROOT = Path(Path(__file__).parent, "search_order_2") 28 | TEST_PATH = "" 29 | 30 | def test(self): 31 | data = self.run_sherlock() 32 | expected = Tree( 33 | name="search_order_2", 34 | children=[ 35 | Tree(name="a.resource", res_type="Resource", keywords=[Keyword(name="Duplicated", used=0)]), 36 | Tree( 37 | name="b.resource", 38 | res_type="Resource", 39 | keywords=[Keyword(name="Duplicated", used=0), Keyword(name="Keyword", used=1)], 40 | ), 41 | Tree(name="suite.robot", keywords=[Keyword(name="Duplicated", used=1)]), 42 | ], 43 | ) 44 | self.should_match_tree(expected, data) 45 | 46 | 47 | class TestSearchOrder3(AcceptanceTest): 48 | ROOT = Path(Path(__file__).parent, "search_order_3") 49 | TEST_PATH = "" 50 | 51 | def test(self): 52 | data = self.run_sherlock() 53 | expected = Tree( 54 | name="search_order_3", 55 | children=[ 56 | Tree(name="a.resource", res_type="Resource", keywords=[Keyword(name="Duplicated in resource", used=1)]), 57 | Tree( 58 | name="b.resource", 59 | res_type="Resource", 60 | keywords=[Keyword(name="Duplicated in resource", used=0), Keyword(name="Keyword", used=1)], 61 | ), 62 | Tree(name="suite.robot", keywords=[]), 63 | ], 64 | ) 65 | self.should_match_tree(expected, data) 66 | 67 | 68 | class TestSearchOrder4(AcceptanceTest): 69 | ROOT = Path(Path(__file__).parent, "search_order_4") 70 | TEST_PATH = "" 71 | 72 | def test(self): 73 | data = self.run_sherlock() 74 | expected = Tree( 75 | name="search_order_4", 76 | children=[ 77 | Tree(name="a.resource", res_type="Resource", keywords=[Keyword(name="1", used=0)]), 78 | Tree(name="b.resource", res_type="Resource", keywords=[Keyword(name="Keyword", used=1)]), 79 | Tree(name="from_b.resource", res_type="Resource", keywords=[Keyword(name="1", used=1)]), 80 | Tree(name="suite.robot", keywords=[]), 81 | ], 82 | ) 83 | self.should_match_tree(expected, data) 84 | 85 | 86 | class TestSearchOrder5(AcceptanceTest): 87 | ROOT = Path(Path(__file__).parent, "search_order_5") 88 | TEST_PATH = "" 89 | 90 | def test(self): 91 | data = self.run_sherlock() 92 | expected = Tree( 93 | name="search_order_5", 94 | children=[ 95 | Tree(name="a.resource", res_type="Resource", keywords=[Keyword(name="Keyword", used=1)]), 96 | Tree( 97 | name="b.resource", 98 | res_type="Resource", 99 | keywords=[Keyword(name="Something that a.resource needs", used=1)], 100 | ), 101 | Tree(name="suite.robot", keywords=[]), 102 | ], 103 | ) 104 | self.should_match_tree(expected, data) 105 | -------------------------------------------------------------------------------- /tests/atest/suite_setups/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/suite_setups/__init__.py -------------------------------------------------------------------------------- /tests/atest/suite_setups/test_data/tests/__init__.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Test Timeout 1min 3 | Suite Setup Suite Keyword 4 | Suite Teardown Suite Keyword 5 | 6 | *** Keywords *** 7 | Suite Keyword 8 | Log Here I Am! 9 | -------------------------------------------------------------------------------- /tests/atest/suite_setups/test_data/tests/resource.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Suite Keyword 3 | FOR ${var} IN RANGE 2 4 | Other Keyword 5 | END 6 | [Teardown] Other Keyword 7 | 8 | Other Keyword 9 | Log Here I Am! 10 | -------------------------------------------------------------------------------- /tests/atest/suite_setups/test_data/tests/tests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource resource.robot 3 | Suite Setup Suite Keyword 4 | Suite Teardown Suite Keyword 5 | 6 | *** Test Cases *** 7 | Empty Test 8 | [Teardown] Internal Keyword 9 | No Operation 10 | 11 | 12 | *** Keywords *** 13 | Internal Keyword 14 | No Operation 15 | -------------------------------------------------------------------------------- /tests/atest/suite_setups/test_suite_setups.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestSuiteSetups(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | TEST_PATH = "tests" 9 | 10 | def test(self): 11 | data = self.run_sherlock() 12 | expected = Tree( 13 | name="test_data", 14 | children=[ 15 | Tree( 16 | name="tests", 17 | children=[ 18 | Tree( 19 | name="resource.robot", 20 | keywords=[Keyword(name="Other Keyword", used=6), Keyword(name="Suite Keyword", used=2)], 21 | ), 22 | Tree(name="tests.robot", keywords=[Keyword(name="Internal Keyword", used=1)]), 23 | Tree(name="__init__.robot", keywords=[Keyword(name="Suite Keyword", used=2)]), 24 | ], 25 | ) 26 | ], 27 | ) 28 | self.should_match_tree(expected, data) 29 | -------------------------------------------------------------------------------- /tests/atest/test_example_workflow.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from . import AcceptanceTest, run_sherlock 6 | 7 | 8 | class TestExampleWorkflow(AcceptanceTest): 9 | ROOT = Path(__file__).parent.parent / "test_data" 10 | TEST_PATH = "tests" 11 | 12 | def test_with_xml_output(self): 13 | robot_output = self.ROOT / "output.xml" 14 | source = self.ROOT / self.TEST_PATH 15 | self.run_robot() 16 | run_sherlock(robot_output=robot_output, source=source) 17 | 18 | def test_without_xml_output(self): 19 | source = self.ROOT / self.TEST_PATH 20 | run_sherlock(robot_output=None, source=source) 21 | 22 | @pytest.mark.parametrize("report_type", ["json", "html"]) 23 | def test_with_report(self, report_type): 24 | robot_output = self.ROOT / "output.xml" 25 | source = self.ROOT / self.TEST_PATH 26 | self.run_robot() 27 | runner = run_sherlock(robot_output=robot_output, source=source, report=[report_type]) 28 | output = runner.config.root / f"sherlock_tests.{report_type}" 29 | assert output.is_file() 30 | output.unlink() 31 | -------------------------------------------------------------------------------- /tests/atest/very_long_and_short_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/atest/very_long_and_short_name/__init__.py -------------------------------------------------------------------------------- /tests/atest/very_long_and_short_name/test_data/test.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | This Is Very Long Name That Should Be Displayed Properly 3 | [Arguments] ${a} 4 | Log ${a} 5 | 6 | Short 7 | No Operation 8 | -------------------------------------------------------------------------------- /tests/atest/very_long_and_short_name/test_data/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource test.resource 3 | 4 | 5 | *** Test Cases *** 6 | Test 7 | This Is Very Long Name That Should Be Displayed Properly ${1} 8 | Short 9 | -------------------------------------------------------------------------------- /tests/atest/very_long_and_short_name/test_very_long_and_short_name.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.atest import AcceptanceTest, Keyword, Tree 4 | 5 | 6 | class TestVeryLongAndShortName(AcceptanceTest): 7 | ROOT = Path(__file__).parent / "test_data" 8 | 9 | def test(self): 10 | data = self.run_sherlock() 11 | # TODO assert 12 | -------------------------------------------------------------------------------- /tests/packages/stable4/requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework==4.1.3 -------------------------------------------------------------------------------- /tests/packages/stable5/requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework==5.0.1 -------------------------------------------------------------------------------- /tests/packages/stable6/requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework==6.1 -------------------------------------------------------------------------------- /tests/test_data/configs/empty_pyproject/pyproject.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/test_data/configs/empty_pyproject/pyproject.toml -------------------------------------------------------------------------------- /tests/test_data/configs/nested_pyproject/1/2/test.robot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/test_data/configs/nested_pyproject/1/2/test.robot -------------------------------------------------------------------------------- /tests/test_data/configs/nested_pyproject/2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | report = "print,html" -------------------------------------------------------------------------------- /tests/test_data/configs/nested_pyproject/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | report = "print" -------------------------------------------------------------------------------- /tests/test_data/configs/no_sherlock_section_pyproject/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.other] 2 | some-opion = ["smth"] 3 | -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | output = "output.xml" 3 | log_output = "sherlock.log" 4 | report = "print,html" 5 | path = ["file1.robot", "dir/"] 6 | variable = [ 7 | "first:value", 8 | "second:value" 9 | ] -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject/pyproject_other.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | path = ["file1.robot", "dir/"] -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject_invalid/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | output = "output.xml" 3 | log_output = sherlock.log 4 | report = "print,html" 5 | path = ["file1.robot", "dir/"] -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject_missing_key/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | output = "output.xml" 3 | log_output = "sherlock.log" 4 | report = "print,html" 5 | path = ["file1.robot", "dir/"] 6 | some_key = "value" -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject_nested_config/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | output = "output.xml" 3 | log_output = "sherlock.log" 4 | report = "print,html" 5 | path = ["file1.robot", "dir/"] 6 | config = "smth.toml" -------------------------------------------------------------------------------- /tests/test_data/configs/pyproject_pythonpath/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.sherlock] 2 | report = "html" 3 | pythonpath = [ 4 | "test_libs" 5 | ] -------------------------------------------------------------------------------- /tests/test_data/gitignore/.gitignore: -------------------------------------------------------------------------------- 1 | *.resource -------------------------------------------------------------------------------- /tests/test_data/libs/KeywordFour.py: -------------------------------------------------------------------------------- 1 | class KeywordFour: 2 | def keyword_4(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/test_data/libs/MyPythonLibrary.py: -------------------------------------------------------------------------------- 1 | from robot.api.deco import not_keyword 2 | 3 | 4 | class MyPythonLibrary: 5 | def __init__(self, arg): 6 | self.arg = arg 7 | 8 | def python_keyword(self, a): 9 | pass 10 | 11 | def im_not_used(self): 12 | pass 13 | 14 | @not_keyword 15 | def should_be_hidden(self): 16 | pass 17 | -------------------------------------------------------------------------------- /tests/test_data/resources/resourceA.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | Log 1 4 | 5 | Keyword 2 6 | Keyword 3 7 | 8 | Keyword 3 9 | Log 3 10 | No Operation 11 | ${condition} Set Variable ${True} 12 | IF ${condition} 13 | IF ${condition} 14 | Should Be True ${True} 15 | ELSE IF False 16 | Should Be False ${False} 17 | END 18 | END 19 | IF ${condition} 20 | IF ${condition} 21 | Should Be True ${True} 22 | ELSE IF False 23 | Should Be False ${False} 24 | END 25 | END 26 | 27 | Keyword 4 28 | Log 4 29 | 30 | Run Keyword With ${variable} 31 | Log ${variable} 32 | -------------------------------------------------------------------------------- /tests/test_data/resources/resourceB.robot: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword4 3 | Log 4 4 | -------------------------------------------------------------------------------- /tests/test_data/resources/resourceC.resource: -------------------------------------------------------------------------------- 1 | *** Keywords *** 2 | Keyword 1 3 | Log 1 4 | 5 | Keyword 2 6 | Keyword 3 7 | 8 | Keyword 3 9 | Log 3 10 | 11 | Keyword 4 12 | Log 4 13 | 14 | Run Keyword With ${variable} 15 | Log ${variable} 16 | -------------------------------------------------------------------------------- /tests/test_data/tests/nested/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ..${/}..${/}resources${/}resourceA.resource 3 | Resource idontexist${B}.resource 4 | Library ..${/}..${/}libs${/}MyPythonLibrary.py test # imported but not used 5 | 6 | *** Variables *** 7 | ${B} 5 8 | 9 | *** Test Cases *** 10 | Test A 11 | FOR ${a} IN RANGE ${B} 12 | Keyword 1 13 | Keyword 2 14 | Keyword 4 15 | END 16 | Internal Keyword 17 | 18 | *** Keywords *** 19 | Internal_keyword 20 | Log Internal 21 | -------------------------------------------------------------------------------- /tests/test_data/tests/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Resource ..${/}resources${/}resourceA.resource 3 | Resource ..${/}resources${/}resourceB.robot # keyword 4 redefined 4 | Library ..${/}libs${/}MyPythonLibrary.py test 5 | Library ..${/}libs${/}KeywordFour.py # keyword 4 redefined 6 | 7 | 8 | *** Test Cases *** 9 | Test A 10 | Keyword 1 11 | Keyword 2 12 | Keyword 4 13 | 14 | Test B 15 | Keyword 1 16 | Keyword 4 17 | 18 | Test C 19 | Python Keyword a 20 | # Python Keyword call without args etc? 21 | 22 | Test Embedded 23 | Run Keyword With abcd 24 | Run Keyword With 1 25 | run keyword with abc 26 | -------------------------------------------------------------------------------- /tests/utest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-sherlock/eae11f65eb10dd8c7ea9825292fe59eff0baa962/tests/utest/__init__.py -------------------------------------------------------------------------------- /tests/utest/complexity_models.py: -------------------------------------------------------------------------------- 1 | model_1complexity = """ 2 | *** Keywords *** 3 | Keyword 4 | Keyword Call 5 | Another Keyword Call 6 | # Comment 7 | Keyword With 8 | ... ${multiline} 9 | ... ${args} 10 | 11 | """ 12 | 13 | model_3complexity = """ 14 | *** Keywords *** 15 | Keyword 16 | Keyword Call 17 | Another Keyword Call 18 | # Comment 19 | IF condition 20 | Keyword Call 21 | ELSE 22 | Keyword Call 23 | END 24 | Keyword With 25 | ... ${multiline} 26 | ... ${args} 27 | 28 | """ 29 | 30 | model_5complexity = """ 31 | *** Keywords *** 32 | Keyword 33 | Keyword Call 34 | Another Keyword Call 35 | # Comment 36 | IF condition 37 | Keyword Call 38 | ELSE 39 | Keyword Call 40 | FOR ${var} IN RANGE 10 41 | Log ${var} 42 | END 43 | END 44 | Keyword With 45 | ... ${multiline} 46 | ... ${args} 47 | 48 | """ 49 | -------------------------------------------------------------------------------- /tests/utest/test_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tempfile 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | import sherlock.config 9 | from sherlock.config import Config 10 | from sherlock.core import Sherlock 11 | from sherlock.exceptions import SherlockFatalError 12 | 13 | 14 | class TestCli: 15 | def test_invalid_output(self, tmp_path): 16 | with patch.object( 17 | sys, 18 | "argv", 19 | f"sherlock --output idontexist.xml --report html {tmp_path}".split(), 20 | ), pytest.raises( 21 | SherlockFatalError, match="Reading Robot Framework output file failed. No such file: 'idontexist.xml'" 22 | ): 23 | Config() 24 | 25 | def test_no_output(self, tmp_path): 26 | with patch.object( 27 | sys, 28 | "argv", 29 | f"sherlock --report html {tmp_path}".split(), 30 | ): 31 | Config() 32 | 33 | def test_output(self): 34 | with tempfile.NamedTemporaryFile() as fp, patch.object( 35 | sys, 36 | "argv", 37 | f"sherlock --output {fp.name} .".split(), 38 | ): 39 | config = Config() 40 | assert config.output == Path(fp.name) 41 | 42 | def test_default_output(self): 43 | with tempfile.NamedTemporaryFile() as fp, patch.object( 44 | sherlock.config, "ROBOT_DEFAULT_OUTPUT", fp.name 45 | ), patch.object( 46 | sys, 47 | "argv", 48 | f"sherlock {Path(fp.name).parent}".split(), 49 | ): 50 | config = Config() 51 | assert config.output == config.path / fp.name 52 | 53 | def test_default_source(self): 54 | with patch.object(sys, "argv", ["sherlock"]): 55 | config = Config() 56 | assert config.path == Path.cwd() 57 | 58 | def test_invalid_source(self): 59 | full_invalid_path = Path("idontexist").resolve() 60 | with patch.object(sys, "argv", ["sherlock", "idontexist"]), pytest.raises(SherlockFatalError) as err: 61 | Config() 62 | assert f"Path to source code does not exist: '{full_invalid_path}'" in str(err.value) 63 | 64 | def test_invalid_report(self): 65 | with patch.object(sys, "argv", "sherlock --report print,invalid".split(),), pytest.raises( 66 | SherlockFatalError, 67 | match="Provided report 'invalid' does not exist. Use comma separated list of values from: html,json,print", 68 | ): 69 | Sherlock() 70 | 71 | def test_invalid_report_similar(self): 72 | with patch.object(sys, "argv", "sherlock --report printt".split(),), pytest.raises( 73 | SherlockFatalError, 74 | match="Provided report 'printt' does not exist. Use comma separated list of values from: html,json,print. " 75 | "Did you mean:\n print", 76 | ): 77 | Sherlock() 78 | 79 | def test_default_report(self): 80 | with patch.object(sys, "argv", "sherlock".split()): 81 | config = Config() 82 | assert config.report == ["print"] 83 | -------------------------------------------------------------------------------- /tests/utest/test_complexity.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from robot.api import get_model 3 | 4 | from sherlock.model import KeywordStats 5 | 6 | from .complexity_models import model_1complexity, model_3complexity, model_5complexity 7 | 8 | 9 | class TestComplexity: 10 | @pytest.mark.parametrize( 11 | "string_model, complexity", [(model_1complexity, 1), (model_3complexity, 3), (model_5complexity, 5)] 12 | ) 13 | def test_complexity(self, string_model, complexity): 14 | model = get_model(string_model) 15 | kw_stat = KeywordStats(name="Dummy", parent="Dummy", node=model) 16 | assert kw_stat.complexity == complexity 17 | 18 | def test_complexity_without_model(self): 19 | kw_stat = KeywordStats(name="Dummy", parent="Dummy", node=None) 20 | assert kw_stat.complexity is None 21 | 22 | def test_complexity_print(self): 23 | model = get_model(model_1complexity) 24 | kw_stat = KeywordStats(name="Dummy", parent="Dummy", node=model) 25 | assert "Dummy\n Used: 0\n Complexity: 1\n" == str(kw_stat) 26 | 27 | def test_complexity_without_model_print(self): 28 | kw_stat = KeywordStats(name="Dummy", parent="Dummy", node=None) 29 | assert "Dummy\n Used: 0\n" == str(kw_stat) 30 | -------------------------------------------------------------------------------- /tests/utest/test_config.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import io 3 | import os 4 | import sys 5 | import tempfile 6 | from pathlib import Path 7 | from unittest.mock import patch 8 | 9 | import pytest 10 | 11 | from sherlock.config import Config, TomlConfigParser 12 | from sherlock.exceptions import SherlockFatalError 13 | 14 | TEST_DATA_DIR = Path(__file__).parent.parent / "test_data" 15 | 16 | 17 | @contextlib.contextmanager 18 | def working_directory(path): 19 | """Changes working directory and returns to previous on exit""" 20 | prev_cwd = Path.cwd() 21 | os.chdir(path) 22 | try: 23 | yield 24 | finally: 25 | os.chdir(prev_cwd) 26 | 27 | 28 | class TestConfig: 29 | def test_load_args_from_cli(self, tmp_path): 30 | with tempfile.NamedTemporaryFile() as fp, patch.object( 31 | sys, 32 | "argv", 33 | f"sherlock --output {fp.name} --log-output sherlock.log --report html {tmp_path}".split(), 34 | ): 35 | config = Config() 36 | assert config.path == Path(tmp_path) 37 | assert config.output == Path(fp.name) 38 | assert isinstance(config.log_output, io.TextIOWrapper) 39 | assert config.report == ["html"] 40 | 41 | def test_load_args_from_cli_no_pyproject(self, tmp_path): 42 | with tempfile.NamedTemporaryFile() as fp, working_directory(Path.home()), patch.object( 43 | sys, 44 | "argv", 45 | f"sherlock --output {fp.name} --log-output sherlock.log --report html {tmp_path}".split(), 46 | ): 47 | config = Config() 48 | assert config.path == Path(tmp_path) 49 | assert config.output == Path(fp.name) 50 | assert isinstance(config.log_output, io.TextIOWrapper) 51 | assert config.report == ["html"] 52 | 53 | def test_load_args_from_cli_overwrite_config(self): 54 | config_dir = TEST_DATA_DIR / "configs" / "pyproject" 55 | with tempfile.NamedTemporaryFile() as fp, working_directory(config_dir), patch.object( 56 | sys, "argv", f"sherlock --output {fp.name} {config_dir}".split() 57 | ): 58 | config = Config() 59 | assert config.path == Path(config_dir) 60 | assert config.output == Path(fp.name) 61 | assert isinstance(config.log_output, io.TextIOWrapper) 62 | assert config.report == ["print", "html"] 63 | 64 | def test_load_args_from_cli_config_option(self, tmp_path): 65 | config_dir = TEST_DATA_DIR / "configs" / "pyproject" 66 | cmd = f"sherlock --config pyproject_other.toml {tmp_path}".split() 67 | with working_directory(config_dir), patch.object(sys, "argv", cmd): 68 | config = Config() 69 | assert config.path == Path(tmp_path) 70 | assert config.log_output is None 71 | assert config.report == ["print"] 72 | 73 | def test_load_args_from_config_missing_file(self, tmp_path): 74 | cmd = f"sherlock --config idontexist.toml {tmp_path}".split() 75 | with patch.object(sys, "argv", cmd), pytest.raises(SherlockFatalError) as err: 76 | Config() 77 | assert "Configuration file 'idontexist.toml' does not exist" in str(err) 78 | 79 | @pytest.mark.parametrize( 80 | "python_path, exp_python_path", 81 | [ 82 | (".", [str(Path.cwd())]), 83 | (f".;{Path('test_libraries')}", [str(Path.cwd()), str(Path.cwd() / "test_libraries")]), 84 | ], 85 | ) 86 | def test_pythonpath(self, tmp_path, python_path, exp_python_path): 87 | cmd = f"sherlock --pythonpath {python_path} {tmp_path}".split() 88 | with patch.object(sys, "argv", cmd): 89 | config = Config() 90 | assert sorted(exp_python_path) == sorted(config.pythonpath) 91 | 92 | 93 | class TestTomlParser: 94 | def test_read_toml_data(self): 95 | config_path = TEST_DATA_DIR / "configs" / "pyproject" / "pyproject.toml" 96 | config = TomlConfigParser(config_path=config_path, look_up={}).read_from_file() 97 | assert config and isinstance(config, dict) 98 | 99 | def test_read_toml_data_empty_pyproject(self): 100 | config_path = TEST_DATA_DIR / "configs" / "empty_pyproject" / "pyproject.toml" 101 | config = TomlConfigParser(config_path=config_path, look_up={}).read_from_file() 102 | assert not config and isinstance(config, dict) 103 | 104 | def test_read_toml_data_no_sherlock_section(self): 105 | config_path = TEST_DATA_DIR / "configs" / "no_sherlock_section_pyproject" / "pyproject.toml" 106 | config = TomlConfigParser(config_path=config_path, look_up={}).read_from_file() 107 | assert not config and isinstance(config, dict) 108 | 109 | def test_get_config(self): 110 | config_path = TEST_DATA_DIR / "configs" / "pyproject" / "pyproject.toml" 111 | look_up = Config(from_cli=False).__dict__ 112 | config = TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 113 | assert isinstance(config["log_output"], io.TextIOWrapper) 114 | config["log_output"] = None 115 | assert config == { 116 | "output": Path("output.xml"), 117 | "log_output": None, 118 | "report": ["print", "html"], 119 | "path": ["file1.robot", "dir/"], 120 | "variable": ["first:value", "second:value"], 121 | } 122 | 123 | def test_get_config_empty(self): 124 | config_path = TEST_DATA_DIR / "configs" / "empty_pyproject" / "pyproject.toml" 125 | look_up = Config(from_cli=False).__dict__ 126 | config = TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 127 | assert not config and isinstance(config, dict) 128 | 129 | def test_get_config_missing_key(self): 130 | config_path = TEST_DATA_DIR / "configs" / "pyproject_missing_key" / "pyproject.toml" 131 | look_up = Config(from_cli=False).__dict__ 132 | with pytest.raises(SherlockFatalError) as err: 133 | TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 134 | assert "Option 'some_key' is not supported in configuration file" in str(err) 135 | 136 | def test_get_config_nested_configuration(self): 137 | config_path = TEST_DATA_DIR / "configs" / "pyproject_nested_config" / "pyproject.toml" 138 | look_up = Config(from_cli=False).__dict__ 139 | with pytest.raises(SherlockFatalError) as err: 140 | TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 141 | assert "Nesting configuration files is not allowed" in str(err) 142 | 143 | def test_get_config_invalid_toml(self): 144 | config_path = TEST_DATA_DIR / "configs" / "pyproject_invalid" / "pyproject.toml" 145 | look_up = Config(from_cli=False).__dict__ 146 | with pytest.raises(SherlockFatalError) as err: 147 | TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 148 | assert ( 149 | rf"Failed to decode {config_path}: This float doesn't have a leading digit (line 3 column 1 char 38)" 150 | in err.value.args[0] 151 | ) 152 | 153 | def test_get_config_python_path(self): 154 | config_path = TEST_DATA_DIR / "configs" / "pyproject_pythonpath" / "pyproject.toml" 155 | look_up = Config(from_cli=False).__dict__ 156 | config = TomlConfigParser(config_path=config_path, look_up=look_up).get_config() 157 | assert config == {"pythonpath": [str(Path.cwd() / "test_libs")], "report": ["html"]} 158 | -------------------------------------------------------------------------------- /tests/utest/test_file_utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from sherlock.file_utils import find_file_in_project_root, find_project_root, get_gitignore 8 | 9 | TEST_DATA_DIR = Path(__file__).parent.parent / "test_data" 10 | 11 | 12 | @contextlib.contextmanager 13 | def working_directory(path): 14 | """Changes working directory and returns to previous on exit""" 15 | prev_cwd = Path.cwd() 16 | os.chdir(path) 17 | try: 18 | yield 19 | finally: 20 | os.chdir(prev_cwd) 21 | 22 | 23 | class TestFileUtils: 24 | def test_find_project_root(self): 25 | source = TEST_DATA_DIR / "configs" / "pyproject" 26 | root = find_project_root((source,)) 27 | assert root == source 28 | 29 | def test_find_project_root_without_sources(self): 30 | source = TEST_DATA_DIR / "configs" / "pyproject" 31 | with working_directory(source): 32 | root = find_project_root(None) 33 | assert root == source 34 | 35 | def test_find_project_root_multiple_sources(self): 36 | common = TEST_DATA_DIR / "configs" / "nested_pyproject" 37 | sources = (common / "1" / "2" / "test.robot", common / "2") 38 | root = find_project_root(sources) 39 | assert root == common 40 | 41 | def test_find_project_root_no_pyproject(self): 42 | source = Path.home() 43 | root = find_project_root((source,)) 44 | assert root == Path(source.parts[0]) 45 | 46 | def test_find_file_in_project_root(self): 47 | source = TEST_DATA_DIR / "configs" / "nested_pyproject" / "2" 48 | file = find_file_in_project_root("pyproject.toml", source) 49 | assert file == source / "pyproject.toml" 50 | 51 | def test_find_file_in_project_root_no_file(self): 52 | source = TEST_DATA_DIR / "configs" / "no_pyproject" 53 | file = find_file_in_project_root("pyproject.toml", source) 54 | # if file is not found it will find it in parents - in this case top of the repository 55 | assert file == TEST_DATA_DIR.parent.parent / "pyproject.toml" 56 | 57 | def test_find_file_in_project_root_outside_repo(self): 58 | source = Path.home() 59 | file = find_file_in_project_root("pyproject.toml", source) 60 | # if file is not found anywhere, it will return Path({root}:/{file}) 61 | assert file == Path(source.parts[0]) / "pyproject.toml" 62 | 63 | def test_get_gitignore(self): 64 | gitignore = TEST_DATA_DIR / "gitignore" 65 | spec = get_gitignore(gitignore) 66 | assert spec.match_file("file.resource") 67 | assert not spec.match_file("file.robot") 68 | --------------------------------------------------------------------------------