├── .flake8 ├── .github ├── release-drafter.yml └── workflows │ ├── check-build.yml │ ├── code_checks.yml │ ├── label-verifier.yml │ ├── python-publish.yml │ ├── release-draft.yml │ ├── test.yml │ └── validate.yml ├── .gitignore ├── .version_check └── check.py ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── images ├── banner.png └── icon.png ├── requirements.txt ├── testit-adapter-behave ├── README.md ├── pytest.ini ├── requirements.txt ├── setup.py ├── src │ └── testit_adapter_behave │ │ ├── __init__.py │ │ ├── formatter.py │ │ ├── listener.py │ │ ├── models │ │ ├── __init__.py │ │ ├── label.py │ │ ├── option.py │ │ ├── tags.py │ │ ├── test_result_step.py │ │ └── url_link.py │ │ ├── scenario_parser.py │ │ ├── tags_parser.py │ │ └── utils.py └── tests │ └── test_formatter.py ├── testit-adapter-nose ├── README.md ├── pytest.ini ├── setup.py ├── src │ └── testit_adapter_nose │ │ ├── __init__.py │ │ ├── listener.py │ │ ├── plugin.py │ │ └── utils.py └── tests │ └── test_plugin.py ├── testit-adapter-pytest ├── README.md ├── pytest.ini ├── requirements.txt ├── setup.py ├── src │ └── testit_adapter_pytest │ │ ├── __init__.py │ │ ├── fixture_context.py │ │ ├── listener.py │ │ ├── models │ │ ├── __init__.py │ │ └── executable_test.py │ │ ├── plugin.py │ │ └── utils.py └── tests │ └── test_listener.py ├── testit-adapter-robotframework ├── README.md ├── pytest.ini ├── requirements.txt ├── setup.py ├── src │ └── testit_adapter_robotframework │ │ ├── TMSLibrary.py │ │ ├── __init__.py │ │ ├── listeners.py │ │ ├── models.py │ │ └── utils.py └── tests │ └── test_listeners.py └── testit-python-commons ├── README.md ├── pytest.ini ├── requirements.txt ├── setup.py ├── src ├── testit.py └── testit_python_commons │ ├── __init__.py │ ├── app_properties.py │ ├── client │ ├── __init__.py │ ├── api_client.py │ ├── client_configuration.py │ └── converter.py │ ├── decorators.py │ ├── dynamic_methods.py │ ├── models │ ├── __init__.py │ ├── adapter_mode.py │ ├── fixture.py │ ├── link.py │ ├── link_type.py │ ├── outcome_type.py │ ├── step_result.py │ ├── test_result.py │ └── test_result_with_all_fixture_step_results_model.py │ ├── services │ ├── __init__.py │ ├── adapter_manager.py │ ├── adapter_manager_configuration.py │ ├── fixture_manager.py │ ├── fixture_storage.py │ ├── logger.py │ ├── plugin_manager.py │ ├── retry.py │ ├── step_manager.py │ ├── step_result_storage.py │ └── utils.py │ └── step.py └── tests ├── conftest.py ├── services ├── test_adapter_manager.py ├── test_fixture_manager.py ├── test_plugin_manager.py ├── test_step_manager.py └── test_utils.py ├── test_app_properties.py └── test_dynamic_methods.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 8 4 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_PATCH_VERSION' 2 | tag-template: '$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'enhancement' 7 | 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'bug' 11 | 12 | - title: '🧾 Documentation' 13 | labels: 14 | - 'type:documentation' 15 | 16 | - title: '🧪 Tests' 17 | labels: 18 | - 'tests' 19 | 20 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 21 | template: | 22 | ## Changes 23 | $CHANGES 24 | -------------------------------------------------------------------------------- /.github/workflows/check-build.yml: -------------------------------------------------------------------------------- 1 | name: Check build like on publish 2 | 3 | on: 4 | - pull_request 5 | 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | package: [ 17 | testit-adapter-behave, 18 | testit-adapter-nose, 19 | testit-adapter-pytest, 20 | testit-adapter-robotframework, 21 | testit-python-commons 22 | ] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: '3.x' 30 | - name: Check version 31 | run: | 32 | paths=($(ls | grep 'testit')) 33 | for str in ${paths[@]}; do 34 | VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' $str/setup.py) 35 | python .version_check/check.py $VERSION 36 | done 37 | 38 | - name: Install dependencies for ${{ matrix.package }} 39 | working-directory: ${{ matrix.package }} 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install build 43 | 44 | - name: Build package 45 | working-directory: ${{ matrix.package }} 46 | run: | 47 | python -m build -s -------------------------------------------------------------------------------- /.github/workflows/code_checks.yml: -------------------------------------------------------------------------------- 1 | name: Code checks 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | linters: 8 | name: Auto check 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macos-latest] 13 | python-version: ['3.11'] 14 | package: [ 15 | testit-adapter-behave, 16 | testit-adapter-nose, 17 | testit-adapter-pytest, 18 | testit-adapter-robotframework, 19 | testit-python-commons 20 | ] 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Install Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install requirements for ${{ matrix.package }} 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install -r requirements.txt 33 | 34 | # - name: Run flake8 [${{ matrix.package }}] 35 | # working-directory: ${{ matrix.package }} 36 | # run: flake8 --ignore=C901 . 37 | 38 | # - name: Tests [${{ matrix.package }}] 39 | # working-directory: ${{ matrix.package }} 40 | # run: pytest --basetemp=tmp -------------------------------------------------------------------------------- /.github/workflows/label-verifier.yml: -------------------------------------------------------------------------------- 1 | name: "Verify type labels" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | triage: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: zwaldowski/match-label-action@v4 12 | with: 13 | allowed_multiple: enhancement, bug, documentation, tests -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [ published ] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | package: [ 26 | testit-adapter-behave, 27 | testit-adapter-nose, 28 | testit-adapter-pytest, 29 | testit-adapter-robotframework, 30 | testit-python-commons 31 | ] 32 | 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set up Python 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: '3.x' 39 | - name: Check version 40 | run: | 41 | paths=($(ls | grep 'testit')) 42 | for str in ${paths[@]}; do 43 | VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' $str/setup.py) 44 | python .version_check/check.py $VERSION 45 | done 46 | - name: Install dependencies for ${{ matrix.package }} 47 | working-directory: ${{ matrix.package }} 48 | run: | 49 | python -m pip install --upgrade pip 50 | pip install build 51 | - name: Build package 52 | working-directory: ${{ matrix.package }} 53 | run: | 54 | python -m build -s 55 | - name: Publish package for ${{ matrix.package }} 56 | uses: pypa/gh-action-pypi-publish@release/v1 57 | with: 58 | skip-existing: true 59 | user: __token__ 60 | packages-dir: ${{ matrix.package }}/dist/ 61 | password: ${{ secrets.PYPI_API_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/release-draft.yml: -------------------------------------------------------------------------------- 1 | name: "Create draft release" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | update_draft_release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: release-drafter/release-drafter@v5 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | run-name: "#${{ github.run_number }} test by ${{ github.triggering_actor }}" 3 | on: 4 | pull_request: 5 | paths: 6 | - "testit-adapter-behave/**" 7 | - "testit-adapter-nose/**" 8 | - "testit-adapter-pytest/**" 9 | - "testit-adapter-robotframework/**" 10 | - "testit-python-commons/**" 11 | - "requirements.txt" 12 | - ".github/**/test.yml" 13 | env: 14 | DOTNET_VERSION: 8 15 | GITHUB_PAT: ${{ secrets.SERVICE_ACCOUNT_TOKEN }} 16 | PYTHON_VERSION: 3.12 17 | TEMP_FILE: tmp/output.txt 18 | TMS_ADAPTER_MODE: 1 19 | TMS_CERT_VALIDATION: false 20 | TMS_PRIVATE_TOKEN: ${{ secrets.TESTIT_PRIVATE_TOKEN }} 21 | TMS_URL: ${{ secrets.TESTIT_URL }} 22 | jobs: 23 | test: 24 | name: ${{ matrix.project_name }} 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | include: 30 | - adapter_name: testit-adapter-behave 31 | configuration_id: BEHAVE_CONFIGURATION_ID 32 | project_id: BEHAVE_PROJECT_ID 33 | project_name: behave 34 | test_command: 'behave -f testit_adapter_behave.formatter:AdapterFormatter' 35 | - adapter_name: testit-adapter-nose 36 | configuration_id: NOSE_CONFIGURATION_ID 37 | project_id: NOSE_PROJECT_ID 38 | project_name: nose 39 | test_command: 'nose2 --testit' 40 | - adapter_name: testit-adapter-pytest 41 | configuration_id: PYTEST_CONFIGURATION_ID 42 | project_id: PYTEST_PROJECT_ID 43 | project_name: pytest 44 | test_command: 'pytest --testit' 45 | - adapter_name: testit-adapter-robotframework 46 | configuration_id: ROBOT_FRAMEWORK_CONFIGURATION_ID 47 | project_id: ROBOT_FRAMEWORK_PROJECT_ID 48 | project_name: robotFramework 49 | test_command: 'robot -v testit tests' 50 | env: 51 | TMS_CONFIGURATION_ID: ${{ secrets[matrix.configuration_id] }} 52 | TMS_PROJECT_ID: ${{ secrets[matrix.project_id] }} 53 | TMS_TEST_RUN_NAME: ${{ matrix.project_name }} TestRun 54 | steps: 55 | - name: Checkout adapters-python 56 | uses: actions/checkout@v4 57 | - name: Checkout api-validator-dotnet 58 | uses: actions/checkout@v4 59 | with: 60 | repository: testit-tms/api-validator-dotnet 61 | token: ${{ env.GITHUB_PAT }} 62 | path: api-validator-dotnet 63 | - name: Checkout python-examples 64 | uses: actions/checkout@v4 65 | with: 66 | repository: testit-tms/python-examples 67 | path: python-examples 68 | - name: Setup dotnet 69 | uses: actions/setup-dotnet@v4 70 | with: 71 | dotnet-version: ${{ env.DOTNET_VERSION }} 72 | - name: Setup python 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ env.PYTHON_VERSION }} 76 | - name: Create TestRun 77 | run: | 78 | pip3 install testit-cli 79 | testit testrun create --token ${{ env.TMS_PRIVATE_TOKEN }} --output ${{ env.TEMP_FILE }} 80 | echo "TMS_TEST_RUN_ID=$(<${{ env.TEMP_FILE }})" >> $GITHUB_ENV 81 | pip3 uninstall -y -r <(pip freeze) 82 | - name: Setup environment 83 | run: | 84 | dotnet build --configuration Debug --property WarningLevel=0 api-validator-dotnet 85 | pip3 install -r python-examples/${{ matrix.project_name }}/requirements_ci.txt 86 | pip3 install ./testit-python-commons 87 | pip3 install ./${{ matrix.adapter_name }} 88 | - name: Test 89 | run: | 90 | cd python-examples/${{ matrix.project_name }} 91 | eval "${{ matrix.test_command }}" || exit 0 92 | - name: Validate 93 | run: | 94 | dotnet test --configuration Debug --no-build --logger:"console;verbosity=detailed" api-validator-dotnet 95 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | on: 3 | pull_request: 4 | paths: 5 | - "testit-adapter-behave/**" 6 | - "testit-adapter-nose/**" 7 | - "testit-adapter-pytest/**" 8 | - "testit-adapter-robotframework/**" 9 | - "testit-python-commons/**" 10 | - "requirements.txt" 11 | - ".github/workflows/validate.yml" 12 | push: 13 | branches: 14 | - main 15 | env: 16 | PYTHON_VERSION: 3.12 17 | 18 | jobs: 19 | validate: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | package: [ 25 | testit-adapter-behave, 26 | testit-adapter-nose, 27 | testit-adapter-pytest, 28 | testit-adapter-robotframework, 29 | testit-python-commons 30 | ] 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ env.PYTHON_VERSION }} 39 | 40 | - name: Cache pip dependencies 41 | uses: actions/cache@v4 42 | with: 43 | path: ~/.cache/pip 44 | key: ${{ runner.os }}-pip-${{ matrix.package }}-${{ hashFiles('**/requirements.txt') }} 45 | restore-keys: ${{ runner.os }}-pip-${{ matrix.package }}- 46 | 47 | - name: Install base dependencies 48 | run: python -m pip install --upgrade pip 49 | 50 | - name: Install package dependencies 51 | working-directory: ${{ matrix.package }} 52 | run: | 53 | if [ -f requirements.txt ]; then 54 | pip install -r requirements.txt 55 | echo "Successfully installed dependencies from requirements.txt for ${{ matrix.package }}." 56 | else 57 | echo "requirements.txt not found in ${{ matrix.package }}, skipping 'pip install -r requirements.txt'." 58 | fi 59 | 60 | - name: Install testit-python-commons 61 | if: matrix.package != 'testit-python-commons' 62 | run: pip install ./testit-python-commons 63 | 64 | - name: Install package in development mode 65 | working-directory: ${{ matrix.package }} 66 | run: pip install -e . 67 | 68 | - name: Install test dependencies 69 | run: pip install pytest pytest-mock pytest-cov==4.1.0 coveralls==3.3.1 coverage==6.5.0 70 | 71 | - name: Test 72 | working-directory: ${{ matrix.package }} 73 | shell: bash 74 | run: | 75 | echo "Running pytest for ${{ matrix.package }}..." 76 | pytest tests/ -v --tb=short --no-header --cov=src --cov-report=term-missing --cov-report=xml 77 | echo "Unit tests completed for ${{ matrix.package }}" 78 | 79 | - name: Publish coverage to Coveralls.io 80 | if: success() 81 | uses: coverallsapp/github-action@v2 82 | with: 83 | github-token: ${{ secrets.GITHUB_TOKEN }} 84 | base-path: ./${{ matrix.package }}/src 85 | flag-name: ${{ matrix.package }} 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Automatically generated by `hgimportsvn` 2 | .svn 3 | .hgsvn 4 | 5 | # Ignore local virtualenvs 6 | lib/ 7 | bin/ 8 | include/ 9 | .Python/ 10 | 11 | # These lines are suggested according to the svn:ignore property 12 | # Feel free to enable them by uncommenting them 13 | *.pyc 14 | *.pyo 15 | *.swp 16 | *.class 17 | *.orig 18 | *~ 19 | .hypothesis/ 20 | 21 | # autogenerated 22 | src/_pytest/_version.py 23 | # setuptools 24 | .eggs/ 25 | 26 | doc/*/_build 27 | doc/*/.doctrees 28 | doc/*/_changelog_towncrier_draft.rst 29 | build/ 30 | dist/ 31 | *.egg-info 32 | htmlcov/ 33 | issue/ 34 | env/ 35 | .env/ 36 | .venv/ 37 | venv/ 38 | /pythonenv*/ 39 | 3rdparty/ 40 | .tox 41 | .cache 42 | .pytest_cache 43 | .mypy_cache 44 | .coverage 45 | .coverage.* 46 | coverage.xml 47 | .ropeproject 48 | .idea 49 | .hypothesis 50 | .pydevproject 51 | .project 52 | .settings 53 | .vscode 54 | 55 | # generated by pip 56 | pip-wheel-metadata/ 57 | 58 | # pytest debug logs generated via --debug 59 | pytestdebug.log 60 | 61 | .vs/ 62 | -------------------------------------------------------------------------------- /.version_check/check.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | if len(sys.argv) < 2: 5 | print("Usage: python check.py ") 6 | sys.exit(1) 7 | 8 | 9 | def pipi_is_canonical(version): 10 | return re.match(r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$', version) is not None 11 | 12 | def check_version(version): 13 | print(f"Received version: {param}") 14 | if not pipi_is_canonical(version): 15 | raise Exception("Version " + version + " is not canonical for pypi") 16 | else: 17 | print("Version OK") 18 | 19 | 20 | param = sys.argv[1] # First argument after script name 21 | check_version(param) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All participants of this repository are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with this repository. 4 | 5 | ## The Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## The Standards 10 | 11 | Examples of behaviour that contributes to creating a positive environment include: 12 | 13 | - Using welcoming and inclusive language 14 | - Being respectful of differing viewpoints and experiences 15 | - Gracefully accepting constructive criticism 16 | - Referring to people by their preferred pronouns and using gender-neutral pronouns when uncertain 17 | 18 | Examples of unacceptable behaviour by participants include: 19 | 20 | - Trolling, insulting/derogatory comments, public or private harassment 21 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 22 | - Not being respectful to reasonable communication boundaries, such as 'leave me alone,' 'go away,' or 'I’m not discussing this with you.' 23 | - The usage of sexualised language or imagery and unwelcome sexual attention or advances 24 | - Swearing, usage of strong or disturbing language 25 | - Demonstrating the graphics or any other content you know may be considered disturbing 26 | - Starting and/or participating in arguments related to politics 27 | - Assuming or promoting any kind of inequality including but not limited to: age, body size, disability, ethnicity, gender identity and expression, nationality and race, personal appearance, religion, or sexual identity and orientation 28 | - Attacking personal tastes 29 | - Other conduct which you know could reasonably be considered inappropriate in a professional setting. 30 | 31 | ## Enforcement 32 | 33 | Violations of the Code of Conduct may be reported by sending an email to [support@testit.software](mailto:support@testit.software). All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately. 34 | 35 | We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Attribution 38 | 39 | This Code of Conduct is adapted from [dev.to](https://dev.to/code-of-conduct). 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Test IT 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test IT Python Integrations 2 | The repository contains new versions of adapters for python test frameworks. 3 | 4 | Pytest: [![Release 5 | Status](https://img.shields.io/pypi/v/testit-adapter-pytest?style=plastic)](https://pypi.python.org/pypi/testit-adapter-pytest) 6 | [![Downloads](https://img.shields.io/pypi/dm/testit-adapter-pytest?style=plastic)](https://pypi.python.org/pypi/testit-adapter-pytest) 7 | 8 | Behave: [![Release 9 | Status](https://img.shields.io/pypi/v/testit-adapter-behave?style=plastic)](https://pypi.python.org/pypi/testit-adapter-behave) 10 | [![Downloads](https://img.shields.io/pypi/dm/testit-adapter-behave?style=plastic)](https://pypi.python.org/pypi/testit-adapter-behave) 11 | 12 | Nose: [![Release 13 | Status](https://img.shields.io/pypi/v/testit-adapter-nose?style=plastic)](https://pypi.python.org/pypi/testit-adapter-nose) 14 | [![Downloads](https://img.shields.io/pypi/dm/testit-adapter-nose?style=plastic)](https://pypi.python.org/pypi/testit-adapter-nose) 15 | 16 | Robotframework: [![Release 17 | Status](https://img.shields.io/pypi/v/testit-adapter-robotframework?style=plastic)](https://pypi.python.org/pypi/testit-adapter-robotframework) 18 | [![Downloads](https://img.shields.io/pypi/dm/testit-adapter-robotframework?style=plastic)](https://pypi.python.org/pypi/testit-adapter-robotframework) 19 | 20 | Commons: [![Release 21 | Status](https://img.shields.io/pypi/v/testit-python-commons?style=plastic)](https://pypi.python.org/pypi/testit-python-commons) 22 | [![Downloads](https://img.shields.io/pypi/dm/testit-python-commons?style=plastic)](https://pypi.python.org/pypi/testit-python-commons) 23 | 24 | 25 | ## Compatibility 26 | 27 | | Test IT | Behave | Nose | Pytest | RobotFramework | 28 | |---------|---------------|---------------|---------------|----------------| 29 | | 3.5 | 2.0 | 2.0 | 2.0 | 2.0 | 30 | | 4.0 | 2.1 | 2.1 | 2.1 | 2.1 | 31 | | 4.5 | 2.5 | 2.5 | 2.5 | 2.5 | 32 | | 4.6 | 2.8 | 2.8 | 2.8 | 2.8 | 33 | | 5.0 | 3.2 | 3.2 | 3.2 | 3.2 | 34 | | 5.2 | 3.3 | 3.3 | 3.3 | 3.3 | 35 | | 5.3 | 3.6.1.post530 | 3.6.1.post530 | 3.6.1.post530 | 3.6.1.post530 | 36 | | Cloud | 3.6.1 | 3.6.1 | 3.6.1 | 3.6.1 | 37 | 38 | Supported test frameworks : 39 | 1. [Pytest](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-pytest) 40 | 2. [Behave](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-behave) 41 | 3. [RobotFramework](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-robotframework) 42 | 4. [Nose](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-nose) 43 | 44 | # 🚀 Warning 45 | Since 3.0.0 version: 46 | - If the externalId annotation is not specified, then its contents will be a hash of a fully qualified method name. 47 | 48 | 49 | Coverage Status 50 | 51 | -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/images/banner.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/images/icon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs~=20.3.0 2 | behave~=1.2.6 3 | setuptools~=47.1.0 4 | 5 | pytest 6 | pytest-xdist 7 | 8 | flake8 9 | flake8-builtins 10 | pep8-naming 11 | flake8-variables-names 12 | flake8-import-order 13 | mypy 14 | 15 | attrs 16 | robotframework 17 | 18 | pytest 19 | pytest-xdist 20 | -------------------------------------------------------------------------------- /testit-adapter-behave/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = tests src 3 | testpaths = tests -------------------------------------------------------------------------------- /testit-adapter-behave/requirements.txt: -------------------------------------------------------------------------------- 1 | attrs~=20.3.0 2 | behave~=1.2.6 3 | setuptools~=47.1.0 4 | -------------------------------------------------------------------------------- /testit-adapter-behave/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | VERSION = "3.6.1" 4 | 5 | setup( 6 | name='testit-adapter-behave', 7 | version=VERSION, 8 | description='Behave adapter for Test IT', 9 | long_description=open('README.md', "r").read(), 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/testit-tms/adapters-python/', 12 | author='Integration team', 13 | author_email='integrations@testit.software', 14 | license='Apache-2.0', 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: 3.12', 24 | ], 25 | py_modules=['testit_adapter_behave'], 26 | packages=find_packages(where='src'), 27 | package_dir={'': 'src'}, 28 | install_requires=[ 29 | 'behave', 30 | 'testit-python-commons==' + VERSION, 31 | 'attrs'], 32 | ) 33 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-behave/src/testit_adapter_behave/__init__.py -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/formatter.py: -------------------------------------------------------------------------------- 1 | from behave.formatter.base import Formatter 2 | 3 | from testit_python_commons.services import TmsPluginManager 4 | 5 | from .listener import AdapterListener 6 | from .utils import filter_out_scenarios, parse_userdata 7 | 8 | 9 | class AdapterFormatter(Formatter): 10 | __adapter_launch_is_started = False 11 | __tests_for_launch = None 12 | 13 | def __init__(self, stream_opener, config): 14 | super(AdapterFormatter, self).__init__(stream_opener, config) 15 | 16 | option = parse_userdata(config.userdata) 17 | 18 | self.__listener = AdapterListener( 19 | TmsPluginManager.get_adapter_manager(option), 20 | TmsPluginManager.get_step_manager()) 21 | 22 | TmsPluginManager.get_plugin_manager().register(self.__listener) 23 | 24 | def start_adapter_launch(self): 25 | self.__listener.start_launch() 26 | 27 | self.__tests_for_launch = self.__listener.get_tests_for_launch() 28 | self.__adapter_launch_is_started = True 29 | 30 | def uri(self, uri): 31 | if not self.__adapter_launch_is_started: 32 | self.start_adapter_launch() 33 | 34 | def feature(self, feature): 35 | feature.scenarios = filter_out_scenarios( 36 | self.__tests_for_launch, 37 | feature.scenarios) 38 | 39 | def scenario(self, scenario): 40 | self.__listener.get_scenario(scenario) 41 | 42 | def match(self, match): 43 | self.__listener.get_step_parameters(match) 44 | 45 | def result(self, step): 46 | self.__listener.get_step_result(step) 47 | 48 | def close_stream(self): 49 | self.__listener.stop_launch() 50 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/listener.py: -------------------------------------------------------------------------------- 1 | import testit_python_commons.services as adapter 2 | from testit_python_commons.models.outcome_type import OutcomeType 3 | from testit_python_commons.services import ( 4 | AdapterManager, 5 | StepManager) 6 | 7 | from .models.test_result_step import get_test_result_step_model 8 | from .scenario_parser import ( 9 | parse_scenario, 10 | parse_status) 11 | from .utils import ( 12 | convert_step_to_step_result_model, 13 | convert_executable_test_to_test_result_model) 14 | 15 | 16 | class AdapterListener(object): 17 | __executable_test = None 18 | __background_steps_count = 0 19 | __steps_count = 0 20 | 21 | def __init__(self, adapter_manager: AdapterManager, step_manager: StepManager): 22 | self.__adapter_manager = adapter_manager 23 | self.__step_manager = step_manager 24 | 25 | def start_launch(self): 26 | test_run_id = self.__adapter_manager.get_test_run_id() 27 | 28 | self.__adapter_manager.set_test_run_id(test_run_id) 29 | 30 | def stop_launch(self): 31 | self.__adapter_manager.write_tests() 32 | 33 | def get_tests_for_launch(self): 34 | return self.__adapter_manager.get_autotests_for_launch() 35 | 36 | def get_scenario(self, scenario): 37 | self.__executable_test = parse_scenario(scenario) 38 | self.__background_steps_count = len(scenario.background_steps) 39 | self.__steps_count = len(scenario.steps) 40 | 41 | def set_scenario(self): 42 | self.__adapter_manager.write_test( 43 | convert_executable_test_to_test_result_model(self.__executable_test)) 44 | 45 | def get_step_parameters(self, match): 46 | scope = self.get_scope() 47 | 48 | executable_step = get_test_result_step_model() 49 | 50 | for argument in match.arguments: 51 | name = argument.name if argument.name else 'param' + str(match.arguments.index(argument)) 52 | executable_step['description'] += f'{name} = {argument.original} ' 53 | executable_step['parameters'][name] = argument.original 54 | 55 | self.__executable_test[scope].append(executable_step) 56 | 57 | def get_step_result(self, result): 58 | scope = self.get_scope() 59 | outcome = parse_status(result.status) 60 | 61 | self.__executable_test[scope][-1]['title'] = result.name 62 | self.__executable_test[scope][-1]['outcome'] = outcome 63 | self.__executable_test[scope][-1]['duration'] = round(result.duration * 1000) 64 | self.__executable_test['duration'] += result.duration * 1000 65 | 66 | # TODO: Add to python-commons 67 | nested_step_results = self.__step_manager.get_steps_tree() 68 | executable_step = self.__executable_test[scope][-1] 69 | # TODO: Fix in python-commons 70 | result_scope = f'{scope}Results' if scope == 'setUp' else 'stepResults' 71 | self.__executable_test[result_scope].append( 72 | convert_step_to_step_result_model( 73 | executable_step, 74 | nested_step_results)) 75 | 76 | if outcome != OutcomeType.PASSED: 77 | self.__executable_test['traces'] = result.error_message 78 | self.__executable_test['outcome'] = outcome 79 | self.set_scenario() 80 | return 81 | 82 | if scope == 'setUp': 83 | self.__background_steps_count -= 1 84 | return 85 | 86 | self.__steps_count -= 1 87 | 88 | if self.__steps_count == 0: 89 | self.__executable_test['outcome'] = outcome 90 | self.set_scenario() 91 | 92 | def get_scope(self): 93 | if self.__background_steps_count != 0: 94 | return 'setUp' 95 | 96 | return 'steps' 97 | 98 | @adapter.hookimpl 99 | def add_link(self, link): 100 | if self.__executable_test: 101 | self.__executable_test['resultLinks'].append(link) 102 | 103 | @adapter.hookimpl 104 | def add_message(self, test_message): 105 | if self.__executable_test: 106 | self.__executable_test['message'] = str(test_message) 107 | 108 | @adapter.hookimpl 109 | def add_attachments(self, attach_paths: list or tuple): 110 | if self.__executable_test: 111 | self.__executable_test['attachments'] += self.__adapter_manager.load_attachments(attach_paths) 112 | 113 | @adapter.hookimpl 114 | def create_attachment(self, body, name: str): 115 | if self.__executable_test: 116 | self.__executable_test['attachments'] += self.__adapter_manager.create_attachment(body, name) 117 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-behave/src/testit_adapter_behave/models/__init__.py -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/label.py: -------------------------------------------------------------------------------- 1 | # TODO: Add model to python-commons; implement via attrs 2 | def get_label_model(label): 3 | return { 4 | 'name': label 5 | } 6 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/option.py: -------------------------------------------------------------------------------- 1 | from attr import attrib, attrs 2 | 3 | 4 | # TODO: Add model to python-commons 5 | @attrs(kw_only=True) 6 | class Option(object): 7 | set_url = attrib(default=None) 8 | set_private_token = attrib(default=None) 9 | set_project_id = attrib(default=None) 10 | set_configuration_id = attrib(default=None) 11 | set_test_run_id = attrib(default=None) 12 | set_test_run_name = attrib(default=None) 13 | set_tms_proxy = attrib(default=None) 14 | set_adapter_mode = attrib(default=None) 15 | set_config_file = attrib(default=None) 16 | set_cert_validation = attrib(default=None) 17 | set_automatic_creation_test_cases = attrib(default=None) 18 | set_automatic_updation_links_to_test_cases = attrib(default=None) 19 | set_import_realtime = attrib(default=None) 20 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/tags.py: -------------------------------------------------------------------------------- 1 | class TagType: 2 | EXTERNAL_ID = 'ExternalId=' 3 | DISPLAY_NAME = 'DisplayName=' 4 | LINKS = 'Links=' 5 | TITLE = 'Title=' 6 | WORK_ITEM_IDS = 'WorkItemIds=' 7 | DESCRIPTION = 'Description=' 8 | LABELS = 'Labels=' 9 | NAMESPACE = 'NameSpace=' 10 | CLASSNAME = 'ClassName=' 11 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/test_result_step.py: -------------------------------------------------------------------------------- 1 | # TODO: Add model to python-commons; implement via attrs 2 | def get_test_result_step_model(): 3 | return { 4 | 'title': None, 5 | 'description': '', 6 | 'outcome': None, 7 | 'duration': 0, 8 | 'steps': [], 9 | 'parameters': {}, 10 | 'attachments': [], 11 | # TODO: Add to python-commons 12 | # 'started_on': '', 13 | # 'completed_on': None 14 | } 15 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/models/url_link.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.models.link import Link 2 | 3 | 4 | def get_url_to_link_model(url: str) -> Link: 5 | return Link() \ 6 | .set_url(url) 7 | 8 | 9 | def get_dict_to_link_model(link: dict) -> Link: 10 | return Link() \ 11 | .set_url(link['url']) \ 12 | .set_title(link.get('title', None)) \ 13 | .set_link_type(link.get('type', None)) \ 14 | .set_description(link.get('description', None)) 15 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/scenario_parser.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from enum import Enum 3 | 4 | from testit_python_commons.models.outcome_type import OutcomeType 5 | 6 | from .models.tags import TagType 7 | from .tags_parser import parse_tags 8 | 9 | STATUS = { 10 | 'passed': OutcomeType.PASSED, 11 | 'failed': OutcomeType.FAILED, 12 | 'skipped': OutcomeType.SKIPPED, 13 | 'untested': OutcomeType.SKIPPED, 14 | 'undefined': OutcomeType.BLOCKED 15 | } 16 | 17 | 18 | def parse_scenario(scenario): 19 | tags = parse_tags(scenario.tags + scenario.feature.tags) 20 | 21 | # TODO: Add model to python-commons; implement via attrs 22 | executable_test = { 23 | 'externalID': tags[TagType.EXTERNAL_ID] if 24 | TagType.EXTERNAL_ID in tags and tags[TagType.EXTERNAL_ID] else get_scenario_external_id(scenario), 25 | 'autoTestName': tags[TagType.DISPLAY_NAME] if 26 | TagType.DISPLAY_NAME in tags and tags[TagType.DISPLAY_NAME] else get_scenario_name(scenario), 27 | 'outcome': None, 28 | 'steps': [], 29 | 'stepResults': [], 30 | 'setUp': [], 31 | 'setUpResults': [], 32 | 'tearDown': [], 33 | 'tearDownResults': [], 34 | 'resultLinks': [], 35 | 'duration': 0, 36 | 'traces': None, 37 | 'message': None, 38 | 'namespace': get_scenario_namespace(scenario), 39 | 'classname': None, 40 | 'attachments': [], 41 | 'parameters': get_scenario_parameters(scenario), 42 | # TODO: Make optional in Converter python-commons 43 | 'properties': {}, 44 | 'title': None, 45 | 'description': None, 46 | 'links': [], 47 | 'labels': [], 48 | 'workItemsID': [], 49 | "externalKey": get_scenario_name(scenario) 50 | # TODO: Add to python-commons 51 | # 'started_on': '', 52 | # 'completed_on': None 53 | } 54 | 55 | if TagType.TITLE in tags: 56 | executable_test['title'] = tags[TagType.TITLE] 57 | 58 | if TagType.DESCRIPTION in tags: 59 | executable_test['description'] = tags[TagType.DESCRIPTION] 60 | 61 | if TagType.LINKS in tags: 62 | executable_test['links'] = tags[TagType.LINKS] 63 | 64 | if TagType.LABELS in tags: 65 | executable_test['labels'] = tags[TagType.LABELS] 66 | 67 | if TagType.NAMESPACE in tags: 68 | executable_test['namespace'] = tags[TagType.NAMESPACE] 69 | 70 | if TagType.CLASSNAME in tags: 71 | executable_test['classname'] = tags[TagType.CLASSNAME] 72 | 73 | if TagType.WORK_ITEM_IDS in tags: 74 | # TODO: Fix in python-commons to "workItemIds" 75 | executable_test['workItemsID'] = tags[TagType.WORK_ITEM_IDS] 76 | 77 | return executable_test 78 | 79 | 80 | def parse_status(status): 81 | return STATUS[status.name] 82 | 83 | 84 | def get_scenario_name(scenario): 85 | return scenario.name if scenario.name else scenario.keyword 86 | 87 | 88 | def get_scenario_external_id(scenario): 89 | from .utils import get_hash 90 | 91 | return get_hash(scenario.feature.filename + scenario.name) 92 | 93 | 94 | def get_scenario_namespace(scenario): 95 | return scenario.feature.filename 96 | 97 | 98 | def get_scenario_parameters(scenario): 99 | row = scenario._row 100 | 101 | return {name: value for name, value in zip(row.headings, row.cells)} if row else {} 102 | 103 | 104 | def get_scenario_status(scenario): 105 | for step in scenario.all_steps: 106 | if get_step_status(step) != 'passed': 107 | return get_step_status(step) 108 | return OutcomeType.PASSED 109 | 110 | 111 | def get_scenario_status_details(scenario): 112 | for step in scenario.all_steps: 113 | if get_step_status(step) != 'passed': 114 | return get_step_status_details(step) 115 | 116 | 117 | def get_step_status(result): 118 | if result.exception: 119 | return get_status(result.exception) 120 | else: 121 | if isinstance(result.status, Enum): 122 | return STATUS.get(result.status.name, None) 123 | else: 124 | return STATUS.get(result.status, None) 125 | 126 | 127 | def get_status(exception): 128 | if exception and isinstance(exception, AssertionError): 129 | return OutcomeType.FAILED 130 | elif exception: 131 | return OutcomeType.BLOCKED 132 | return OutcomeType.PASSED 133 | 134 | 135 | def get_step_status_details(result): 136 | if result.exception: 137 | trace = "\n".join(result.exc_traceback) if type(result.exc_traceback) == list else \ 138 | ''.join(traceback.format_tb(result.exc_traceback)) 139 | return trace 140 | 141 | 142 | def get_step_table(step): 143 | table = [','.join(step.table.headings)] 144 | [table.append(','.join(list(row))) for row in step.table.rows] 145 | return '\n'.join(table) 146 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/tags_parser.py: -------------------------------------------------------------------------------- 1 | from .models.label import get_label_model 2 | from .models.tags import TagType 3 | from .models.url_link import get_url_to_link_model, get_dict_to_link_model 4 | 5 | 6 | def parse_tags(tags): 7 | parsed_tags = { 8 | TagType.LINKS: [], 9 | TagType.LABELS: [], 10 | TagType.WORK_ITEM_IDS: [] 11 | } 12 | 13 | for tag in tags: 14 | if TagType.EXTERNAL_ID in tag: 15 | parsed_tags[TagType.EXTERNAL_ID] = tag[len(TagType.EXTERNAL_ID):] 16 | 17 | elif TagType.DISPLAY_NAME in tag: 18 | parsed_tags[TagType.DISPLAY_NAME] = tag[len(TagType.DISPLAY_NAME):] 19 | 20 | elif TagType.LINKS in tag: 21 | parsed_tags[TagType.LINKS].extend( 22 | parse_links( 23 | tag[len(TagType.LINKS):])) 24 | 25 | elif TagType.TITLE in tag: 26 | parsed_tags[TagType.TITLE] = tag[len(TagType.TITLE):] 27 | 28 | elif TagType.WORK_ITEM_IDS in tag: 29 | parsed_tags[TagType.WORK_ITEM_IDS].extend( 30 | parse_massive( 31 | tag[len(TagType.WORK_ITEM_IDS):])) 32 | 33 | elif TagType.DESCRIPTION in tag: 34 | parsed_tags[TagType.DESCRIPTION] = tag[len(TagType.DESCRIPTION):] 35 | 36 | elif TagType.LABELS in tag: 37 | parsed_tags[TagType.LABELS].extend( 38 | parse_labels( 39 | tag[len(TagType.LABELS):])) 40 | 41 | elif TagType.NAMESPACE in tag: 42 | parsed_tags[TagType.NAMESPACE] = tag[len(TagType.NAMESPACE):] 43 | 44 | elif TagType.CLASSNAME in tag: 45 | parsed_tags[TagType.CLASSNAME] = tag[len(TagType.CLASSNAME):] 46 | 47 | return parsed_tags 48 | 49 | 50 | def parse_massive(tag: str): 51 | return tag.split(',') 52 | 53 | 54 | def parse_labels(tag): 55 | parsed_labels = [] 56 | 57 | for label in parse_massive(tag): 58 | parsed_labels.append(get_label_model(label)) 59 | 60 | return parsed_labels 61 | 62 | 63 | def parse_links(tag: str): 64 | parsed_links = [] 65 | json_links = parse_json(tag) 66 | 67 | if not json_links: 68 | for url in parse_massive(tag): 69 | parsed_links.append(get_url_to_link_model(url)) 70 | 71 | if isinstance(json_links, tuple): 72 | for url in json_links: 73 | parsed_links.append(get_url_to_link_model(url)) 74 | 75 | if isinstance(json_links, dict): 76 | parsed_links.append(get_dict_to_link_model(json_links)) 77 | 78 | return parsed_links 79 | 80 | 81 | def parse_json(json_string: str): 82 | try: 83 | return eval(json_string) 84 | except Exception: 85 | return 86 | -------------------------------------------------------------------------------- /testit-adapter-behave/src/testit_adapter_behave/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import logging 3 | import re 4 | import typing 5 | 6 | from testit_python_commons.models.step_result import StepResult 7 | from testit_python_commons.models.test_result import TestResult 8 | 9 | from .models.option import Option 10 | from .models.tags import TagType 11 | from .scenario_parser import get_scenario_external_id, get_scenario_parameters 12 | from .tags_parser import parse_tags 13 | 14 | 15 | def parse_userdata(userdata): 16 | if not userdata: 17 | return 18 | 19 | option = Option() 20 | 21 | if 'tmsUrl' in userdata: 22 | option.set_url = userdata['tmsUrl'] 23 | 24 | if 'tmsPrivateToken' in userdata: 25 | option.set_private_token = userdata['tmsPrivateToken'] 26 | 27 | if 'tmsProjectId' in userdata: 28 | option.set_project_id = userdata['tmsProjectId'] 29 | 30 | if 'tmsConfigurationId' in userdata: 31 | option.set_configuration_id = userdata['tmsConfigurationId'] 32 | 33 | if 'tmsTestRunId' in userdata: 34 | option.set_test_run_id = userdata['tmsTestRunId'] 35 | 36 | if 'tmsTestRunName' in userdata: 37 | option.set_test_run_name = userdata['tmsTestRunName'] 38 | 39 | if 'tmsProxy' in userdata: 40 | option.set_tms_proxy = userdata['tmsProxy'] 41 | 42 | if 'tmsAdapterMode' in userdata: 43 | option.set_adapter_mode = userdata['tmsAdapterMode'] 44 | 45 | if 'tmsConfigFile' in userdata: 46 | option.set_config_file = userdata['tmsConfigFile'] 47 | 48 | if 'tmsCertValidation' in userdata: 49 | option.set_cert_validation = userdata['tmsCertValidation'] 50 | 51 | if 'tmsAutomaticCreationTestCases' in userdata: 52 | option.set_automatic_creation_test_cases = userdata['tmsAutomaticCreationTestCases'] 53 | 54 | if 'tmsAutomaticUpdationLinksToTestCases' in userdata: 55 | option.set_automatic_updation_links_to_test_cases = userdata['tmsAutomaticUpdationLinksToTestCases'] 56 | 57 | if 'tmsImportRealtime' in userdata: 58 | option.set_import_realtime = userdata['tmsImportRealtime'] 59 | 60 | return option 61 | 62 | 63 | def filter_out_scenarios(tests_for_launch, scenarios): 64 | if tests_for_launch: 65 | included_scenarios = [] 66 | for i in range(len(scenarios)): 67 | if scenarios[i].keyword == 'Scenario Outline' and hasattr(scenarios[i], 'scenarios'): 68 | scenarios_outline = filter_out_scenarios(tests_for_launch, scenarios[i].scenarios) 69 | 70 | if len(scenarios_outline) != 0: 71 | for unmatched_scenario in set(scenarios[i].scenarios).symmetric_difference(set(scenarios_outline)): 72 | scenarios[i].scenarios.remove(unmatched_scenario) 73 | 74 | included_scenarios.append(scenarios[i]) 75 | else: 76 | if validate_scenario(scenarios[i], tests_for_launch): 77 | included_scenarios.append(scenarios[i]) 78 | 79 | scenarios = included_scenarios 80 | 81 | return scenarios 82 | 83 | 84 | def validate_scenario(scenario, tests_for_launch) -> bool: 85 | tags = parse_tags(scenario.tags + scenario.feature.tags) 86 | external_id = tags[TagType.EXTERNAL_ID] if \ 87 | TagType.EXTERNAL_ID in tags and tags[TagType.EXTERNAL_ID] else get_scenario_external_id(scenario) 88 | 89 | if scenario.keyword == 'Scenario Outline': 90 | external_id = param_attribute_collector(external_id, get_scenario_parameters(scenario)) 91 | 92 | return external_id in tests_for_launch 93 | 94 | 95 | def param_attribute_collector(attribute, run_param): 96 | result = attribute 97 | param_keys = re.findall(r"\{'<(.*?)>'\}", attribute) 98 | if len(param_keys) > 0: 99 | for param_key in param_keys: 100 | root_key = param_key 101 | id_keys = re.findall(r'\[(.*?)\]', param_key) 102 | if len(id_keys) == 0: 103 | if root_key in run_param: 104 | result = result.replace("{'<" + root_key + ">'}", str(run_param[root_key])) 105 | else: 106 | logging.error(f"Parameter {root_key} not found") 107 | elif len(id_keys) == 1: 108 | base_key = root_key.replace("[" + id_keys[0] + "]", "") 109 | id_key = id_keys[0].strip("\'\"") 110 | if id_key.isdigit() and int(id_key) in range(len(run_param[base_key])): 111 | val_key = int(id_key) 112 | elif id_key.isalnum() and not id_key.isdigit() and id_key in run_param[base_key].keys(): 113 | val_key = id_key 114 | else: 115 | raise SystemExit(f"Not key: {root_key} in run parameters or other keys problem") 116 | result = result.replace("{'<" + root_key + ">'}", str(run_param[base_key][val_key])) 117 | else: 118 | raise SystemExit("For type tuple, list, dict) support only one level!") 119 | elif len(param_keys) == 0: 120 | result = attribute 121 | else: 122 | raise SystemExit("Collecting parameters error!") 123 | return result 124 | 125 | 126 | def convert_executable_test_to_test_result_model(executable_test: dict) -> TestResult: 127 | return TestResult()\ 128 | .set_external_id(executable_test['externalID'])\ 129 | .set_autotest_name(executable_test['autoTestName'])\ 130 | .set_step_results(executable_test['stepResults'])\ 131 | .set_setup_results(executable_test['setUpResults'])\ 132 | .set_teardown_results(executable_test['tearDownResults'])\ 133 | .set_duration(executable_test['duration'])\ 134 | .set_outcome(executable_test['outcome'])\ 135 | .set_traces(executable_test['traces'])\ 136 | .set_attachments(executable_test['attachments'])\ 137 | .set_parameters(executable_test['parameters'])\ 138 | .set_properties(executable_test['properties'])\ 139 | .set_namespace(executable_test['namespace'])\ 140 | .set_classname(executable_test['classname'])\ 141 | .set_title(executable_test['title'])\ 142 | .set_description(executable_test['description'])\ 143 | .set_links(executable_test['links'])\ 144 | .set_result_links(executable_test['resultLinks'])\ 145 | .set_labels(executable_test['labels'])\ 146 | .set_work_item_ids(executable_test['workItemsID'])\ 147 | .set_message(executable_test['message'])\ 148 | .set_external_key(executable_test['externalKey']) 149 | 150 | 151 | def convert_step_to_step_result_model(step: dict, nested_step_results: typing.List[StepResult]) -> StepResult: 152 | step_result_model = StepResult()\ 153 | .set_title(step['title'])\ 154 | .set_description(step['description'])\ 155 | .set_outcome(step['outcome'])\ 156 | .set_duration(step['duration'])\ 157 | .set_attachments(step['attachments']) 158 | 159 | if 'parameters' in step: 160 | step_result_model.set_parameters(step['parameters']) 161 | 162 | if nested_step_results: 163 | step_result_model.set_step_results(nested_step_results) 164 | 165 | return step_result_model 166 | 167 | 168 | def get_hash(value: str): 169 | md = hashlib.sha256(bytes(value, encoding='utf-8')) 170 | return md.hexdigest() 171 | -------------------------------------------------------------------------------- /testit-adapter-behave/tests/test_formatter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_mock import MockerFixture 3 | 4 | from testit_adapter_behave.formatter import AdapterFormatter 5 | from testit_python_commons.services import TmsPluginManager 6 | 7 | 8 | class TestAdapterFormatter: 9 | def test_uri_starts_launch_if_not_started(self, mocker: MockerFixture): 10 | # Arrange 11 | mock_config = mocker.Mock() 12 | mock_config.userdata = {} 13 | 14 | mock_adapter_manager = mocker.Mock() 15 | mock_step_manager = mocker.Mock() 16 | mock_plugin_manager_instance = mocker.Mock() 17 | 18 | mocker.patch.object(TmsPluginManager, 'get_adapter_manager', return_value=mock_adapter_manager) 19 | mocker.patch.object(TmsPluginManager, 'get_step_manager', return_value=mock_step_manager) 20 | mocker.patch.object(TmsPluginManager, 'get_plugin_manager', return_value=mock_plugin_manager_instance) 21 | 22 | # Mock AdapterListener 23 | mock_adapter_listener_instance = mocker.Mock() 24 | mock_adapter_listener_class = mocker.patch( 25 | 'testit_adapter_behave.formatter.AdapterListener', 26 | return_value=mock_adapter_listener_instance 27 | ) 28 | 29 | formatter = AdapterFormatter(stream_opener=mocker.Mock(), config=mock_config) 30 | 31 | # Ensure launch is not started initially 32 | formatter._AdapterFormatter__adapter_launch_is_started = False 33 | mocker.patch.object(formatter, 'start_adapter_launch') # Mock the method to check if it's called 34 | 35 | # Act 36 | formatter.uri("some_uri") 37 | 38 | # Assert 39 | formatter.start_adapter_launch.assert_called_once() 40 | mock_adapter_listener_class.assert_called_once_with(mock_adapter_manager, mock_step_manager) 41 | mock_plugin_manager_instance.register.assert_called_once_with(mock_adapter_listener_instance) 42 | 43 | def test_uri_does_not_start_launch_if_already_started(self, mocker: MockerFixture): 44 | # Arrange 45 | mock_config = mocker.Mock() 46 | mock_config.userdata = {} 47 | 48 | mocker.patch.object(TmsPluginManager, 'get_adapter_manager') 49 | mocker.patch.object(TmsPluginManager, 'get_step_manager') 50 | mocker.patch.object(TmsPluginManager, 'get_plugin_manager', return_value=mocker.Mock()) 51 | 52 | mocker.patch( 53 | 'testit_adapter_behave.formatter.AdapterListener' 54 | ) 55 | 56 | formatter = AdapterFormatter(stream_opener=mocker.Mock(), config=mock_config) 57 | 58 | # Ensure launch is already started 59 | formatter._AdapterFormatter__adapter_launch_is_started = True 60 | mocker.patch.object(formatter, 'start_adapter_launch') 61 | 62 | # Act 63 | formatter.uri("some_uri") 64 | 65 | # Assert 66 | formatter.start_adapter_launch.assert_not_called() -------------------------------------------------------------------------------- /testit-adapter-nose/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = tests src 3 | testpaths = tests -------------------------------------------------------------------------------- /testit-adapter-nose/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = "3.6.1" 4 | 5 | setup( 6 | name='testit-adapter-nose', 7 | version=VERSION, 8 | description='Nose adapter for Test IT', 9 | long_description=open('README.md', "r").read(), 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/testit-tms/adapters-python/', 12 | author='Integration team', 13 | author_email='integrations@testit.software', 14 | license='Apache-2.0', 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: 3.12', 24 | ], 25 | py_modules=['testit_adapter_nose'], 26 | packages=find_packages(where='src'), 27 | package_dir={'': 'src'}, 28 | install_requires=['attrs', 'nose2', 'testit-python-commons==' + VERSION], 29 | entry_points={ 30 | 'nose.plugins.0.10': [ 31 | 'testit_adapter_nose = testit_adapter_nose.plugin:TmsPlugin', 32 | ] 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /testit-adapter-nose/src/testit_adapter_nose/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-nose/src/testit_adapter_nose/__init__.py -------------------------------------------------------------------------------- /testit-adapter-nose/src/testit_adapter_nose/listener.py: -------------------------------------------------------------------------------- 1 | import testit_python_commons.services as adapter 2 | from testit_python_commons.services import ( 3 | AdapterManager, 4 | StepManager) 5 | from .utils import ( 6 | form_test, 7 | get_outcome, 8 | convert_executable_test_to_test_result_model 9 | ) 10 | 11 | 12 | class AdapterListener(object): 13 | __executable_test = None 14 | 15 | def __init__(self, adapter_manager: AdapterManager, step_manager: StepManager, top_level_directory: str): 16 | self.__adapter_manager = adapter_manager 17 | self.__step_manager = step_manager 18 | self.__top_level_directory = top_level_directory 19 | 20 | def start_launch(self): 21 | test_run_id = self.__adapter_manager.get_test_run_id() 22 | 23 | self.__adapter_manager.set_test_run_id(test_run_id) 24 | 25 | def stop_launch(self): 26 | self.__adapter_manager.write_tests() 27 | 28 | def get_tests_for_launch(self): 29 | return self.__adapter_manager.get_autotests_for_launch() 30 | 31 | def start_test(self, test): 32 | self.__executable_test = form_test(test, self.__top_level_directory) 33 | 34 | def set_outcome(self, event): 35 | outcome, message, trace = get_outcome(event) 36 | 37 | self.__executable_test['outcome'] = outcome 38 | self.__executable_test['traces'] = trace 39 | 40 | if not self.__executable_test['message']: 41 | self.__executable_test['message'] = message 42 | 43 | def stop_test(self): 44 | test_results_steps = self.__step_manager.get_steps_tree() 45 | self.__executable_test['stepResults'] = test_results_steps 46 | 47 | self.__adapter_manager.write_test( 48 | convert_executable_test_to_test_result_model(self.__executable_test)) 49 | self.__executable_test = None 50 | 51 | @adapter.hookimpl 52 | def add_link(self, link): 53 | if self.__executable_test: 54 | self.__executable_test['resultLinks'].append(link) 55 | 56 | @adapter.hookimpl 57 | def add_message(self, test_message): 58 | if self.__executable_test: 59 | self.__executable_test['message'] = str(test_message) 60 | 61 | @adapter.hookimpl 62 | def add_attachments(self, attach_paths: list or tuple): 63 | if self.__executable_test: 64 | self.__executable_test['attachments'] += self.__adapter_manager.load_attachments(attach_paths) 65 | 66 | @adapter.hookimpl 67 | def create_attachment(self, body, name: str): 68 | if self.__executable_test: 69 | self.__executable_test['attachments'] += self.__adapter_manager.create_attachment(body, name) 70 | -------------------------------------------------------------------------------- /testit-adapter-nose/src/testit_adapter_nose/plugin.py: -------------------------------------------------------------------------------- 1 | from nose2.events import Plugin 2 | 3 | from testit_python_commons.services import TmsPluginManager 4 | 5 | from .listener import AdapterListener 6 | 7 | 8 | class TmsPlugin(Plugin): 9 | configSection = 'testit' 10 | commandLineSwitch = (None, 'testit', 'TMS adapter for Nose') 11 | __listener = None 12 | __tests_for_launch = None 13 | __top_level_directory = None 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(TmsPlugin, self).__init__(*args, **kwargs) 17 | 18 | def handleDir(self, event): 19 | if not self.__top_level_directory: 20 | self.__top_level_directory = event.topLevelDirectory 21 | 22 | def startTestRun(self, event): 23 | self.__listener = AdapterListener( 24 | TmsPluginManager.get_adapter_manager(), 25 | TmsPluginManager.get_step_manager(), 26 | self.__top_level_directory) 27 | 28 | TmsPluginManager.get_plugin_manager().register(self.__listener) 29 | 30 | self.__listener.start_launch() 31 | self.__tests_for_launch = self.__listener.get_tests_for_launch() 32 | 33 | def afterTestRun(self, event): 34 | self.__listener.stop_launch() 35 | 36 | def startTest(self, event): 37 | self.__listener.start_test(event.test) 38 | 39 | def stopTest(self, event): 40 | self.__listener.stop_test() 41 | 42 | def testOutcome(self, event): 43 | self.__listener.set_outcome(event) 44 | -------------------------------------------------------------------------------- /testit-adapter-nose/tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest_mock import MockerFixture 3 | 4 | from testit_adapter_nose.plugin import TmsPlugin 5 | 6 | 7 | class TestTmsPlugin: 8 | def test_handleDir(self, mocker: MockerFixture): 9 | plugin = TmsPlugin() 10 | mock_event = mocker.Mock() 11 | mock_event.topLevelDirectory = "/test/directory" 12 | 13 | assert plugin._TmsPlugin__top_level_directory is None 14 | 15 | plugin.handleDir(mock_event) 16 | 17 | assert plugin._TmsPlugin__top_level_directory == "/test/directory" 18 | 19 | # Calling it again should not change the value 20 | mock_event_another = mocker.Mock() 21 | mock_event_another.topLevelDirectory = "/another/directory" 22 | plugin.handleDir(mock_event_another) 23 | 24 | assert plugin._TmsPlugin__top_level_directory == "/test/directory" -------------------------------------------------------------------------------- /testit-adapter-pytest/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = tests src 3 | testpaths = tests -------------------------------------------------------------------------------- /testit-adapter-pytest/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-xdist 3 | 4 | flake8 5 | flake8-builtins 6 | pep8-naming 7 | flake8-variables-names 8 | flake8-import-order 9 | mypy 10 | -------------------------------------------------------------------------------- /testit-adapter-pytest/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | VERSION = "3.6.1" 4 | 5 | setup( 6 | name='testit-adapter-pytest', 7 | version=VERSION, 8 | description='Pytest adapter for Test IT', 9 | long_description=open('README.md', "r").read(), 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/testit-tms/adapters-python/', 12 | author='Integration team', 13 | author_email='integrations@testit.software', 14 | license='Apache-2.0', 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.8', 18 | 'Programming Language :: Python :: 3.9', 19 | 'Programming Language :: Python :: 3.10', 20 | 'Programming Language :: Python :: 3.11', 21 | 'Programming Language :: Python :: 3.12', 22 | ], 23 | py_modules=['testit_adapter_pytest'], 24 | packages=find_packages(where='src'), 25 | package_dir={'': 'src'}, 26 | install_requires=['pytest', 'pytest-xdist', 'attrs', 'testit-python-commons==' + VERSION], 27 | entry_points={'pytest11': ['testit_adapter_pytest = testit_adapter_pytest.plugin']} 28 | ) 29 | -------------------------------------------------------------------------------- /testit-adapter-pytest/src/testit_adapter_pytest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-pytest/src/testit_adapter_pytest/__init__.py -------------------------------------------------------------------------------- /testit-adapter-pytest/src/testit_adapter_pytest/fixture_context.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from testit_python_commons.services import Utils, TmsPluginManager 4 | 5 | 6 | class FixtureContext: 7 | def __init__(self, fixture_function, parent_uuid=None, name=None): 8 | self._fixture_function = fixture_function 9 | self._parent_uuid = parent_uuid 10 | self.__title = name if name else fixture_function.__name__ 11 | self.__description = fixture_function.__doc__ 12 | self._uuid = uuid4() 13 | self.parameters = None 14 | 15 | def __call__(self, *args, **kwargs): 16 | self.parameters = Utils.get_function_parameters(self._fixture_function, *args, **kwargs) 17 | 18 | with self: 19 | return self._fixture_function(*args, **kwargs) 20 | 21 | def __enter__(self): 22 | TmsPluginManager.get_plugin_manager().hook.start_fixture( 23 | parent_uuid=self._parent_uuid, 24 | uuid=self._uuid, 25 | title=self.__title, 26 | parameters=self.parameters) 27 | 28 | def __exit__(self, exc_type, exc_val, exc_tb): 29 | TmsPluginManager.get_plugin_manager().hook.stop_fixture( 30 | uuid=self._uuid, 31 | exc_type=exc_type, 32 | exc_val=exc_val, 33 | exc_tb=exc_tb) 34 | -------------------------------------------------------------------------------- /testit-adapter-pytest/src/testit_adapter_pytest/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-pytest/src/testit_adapter_pytest/models/__init__.py -------------------------------------------------------------------------------- /testit-adapter-pytest/src/testit_adapter_pytest/models/executable_test.py: -------------------------------------------------------------------------------- 1 | from attr import attrs, attrib 2 | from attr import Factory 3 | 4 | 5 | @attrs 6 | class ExecutableTest: 7 | external_id = attrib() 8 | name = attrib() 9 | steps = attrib(default=Factory(list)) 10 | step_results = attrib(default=Factory(list)) 11 | setup_steps = attrib(default=Factory(list)) 12 | setup_step_results = attrib(default=Factory(list)) 13 | teardown_steps = attrib(default=Factory(list)) 14 | teardown_step_results = attrib(default=Factory(list)) 15 | result_links = attrib(default=Factory(list)) 16 | duration = attrib(default=None) 17 | outcome = attrib(default=None) 18 | failure_reason_names = attrib(default=Factory(list)) 19 | traces = attrib(default=None) 20 | attachments = attrib(default=Factory(list)) 21 | parameters = attrib(default=Factory(dict)) 22 | properties = attrib(default=Factory(dict)) 23 | namespace = attrib(default=None) 24 | classname = attrib(default=None) 25 | title = attrib(default=None) 26 | description = attrib(default=None) 27 | links = attrib(default=Factory(list)) 28 | labels = attrib(default=Factory(list)) 29 | work_item_ids = attrib(default=Factory(list)) 30 | message = attrib(default=None) 31 | node_id = attrib(default=None) 32 | -------------------------------------------------------------------------------- /testit-adapter-pytest/src/testit_adapter_pytest/plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from testit_adapter_pytest.listener import TmsListener 4 | 5 | from testit_python_commons.services import TmsPluginManager 6 | 7 | 8 | def pytest_addoption(parser): 9 | parser.getgroup('testit').addoption( 10 | '--testit', 11 | action='store_true', 12 | dest="tms_report", 13 | help='Pytest plugin for Test IT' 14 | ) 15 | parser.getgroup('testit').addoption( 16 | '--tmsUrl', 17 | action="store", 18 | dest="set_url", 19 | metavar="https://demo.testit.software", 20 | help='Set location of the TMS instance' 21 | ) 22 | parser.getgroup('testit').addoption( 23 | '--tmsPrivateToken', 24 | action="store", 25 | dest="set_private_token", 26 | metavar="T2lKd2pLZGI4WHRhaVZUejNl", 27 | help='Set API secret key' 28 | ) 29 | parser.getgroup('testit').addoption( 30 | '--tmsProjectId', 31 | action="store", 32 | dest="set_project_id", 33 | metavar="15dbb164-c1aa-4cbf-830c-8c01ae14f4fb", 34 | help='Set project ID' 35 | ) 36 | parser.getgroup('testit').addoption( 37 | '--tmsConfigurationId', 38 | action="store", 39 | dest="set_configuration_id", 40 | metavar="d354bdac-75dc-4e3d-84d4-71186c0dddfc", 41 | help='Set configuration ID' 42 | ) 43 | parser.getgroup('testit').addoption( 44 | '--tmsTestRunId', 45 | action="store", 46 | dest="set_test_run_id", 47 | metavar="5236eb3f-7c05-46f9-a609-dc0278896464", 48 | help='Set test run ID (optional)' 49 | ) 50 | parser.getgroup('debug').addoption( 51 | '--tmsProxy', 52 | action="store", 53 | dest="set_tms_proxy", 54 | metavar='{"http":"http://localhost:8888","https":"http://localhost:8888"}', 55 | help='Set proxy for sending requests (optional)' 56 | ) 57 | parser.getgroup('testit').addoption( 58 | '--tmsTestRunName', 59 | action="store", 60 | dest="set_test_run_name", 61 | metavar="Custom name of test run", 62 | help='Set custom name of test run (optional)' 63 | ) 64 | parser.getgroup('testit').addoption( 65 | '--tmsAdapterMode', 66 | action="store", 67 | dest="set_adapter_mode", 68 | metavar="1", 69 | help=""" 70 | Set adapter mode with test run (optional): 71 | 0 - with filtering autotests by launch\'s suite in TMS (Default) 72 | 1 - without filtering autotests by launch\'s suite in TMS 73 | 2 - create new test run in TMS 74 | """ 75 | ) 76 | parser.getgroup('testit').addoption( 77 | '--tmsConfigFile', 78 | action="store", 79 | dest="set_config_file", 80 | metavar="tmsConfigFile", 81 | help='Set custom name of configuration file' 82 | ) 83 | parser.getgroup('testit').addoption( 84 | '--tmsCertValidation', 85 | action="store", 86 | dest="set_cert_validation", 87 | metavar="false", 88 | help='Set custom name of configuration file' 89 | ) 90 | parser.getgroup('testit').addoption( 91 | '--tmsAutomaticCreationTestCases', 92 | action="store", 93 | dest="set_automatic_creation_test_cases", 94 | metavar="false", 95 | help=""" 96 | Set mode of automatic creation test cases (optional): 97 | true - create a test case linked to the created autotest (not to the updated autotest) 98 | false - not create a test case (Default) 99 | """ 100 | ) 101 | parser.getgroup('testit').addoption( 102 | '--tmsAutomaticUpdationLinksToTestCases', 103 | action="store", 104 | dest="set_automatic_updation_links_to_test_cases", 105 | metavar="false", 106 | help=""" 107 | Set mode of automatic updation links to test cases (optional): 108 | true - update links to test cases 109 | false - not update links to test cases (Default) 110 | """ 111 | ) 112 | parser.getgroup('testit').addoption( 113 | '--tmsImportRealtime', 114 | action="store", 115 | dest="set_import_realtime", 116 | metavar="false", 117 | help=""" 118 | Set mode of import type selection when launching autotests (optional): 119 | true - the adapter will create/update each autotest in real time (Default) 120 | false - the adapter will create/update multiple autotests 121 | """ 122 | ) 123 | 124 | 125 | @pytest.mark.tryfirst 126 | def pytest_cmdline_main(config): 127 | if config.option.tms_report: 128 | listener = TmsListener( 129 | TmsPluginManager.get_adapter_manager(config.option), 130 | TmsPluginManager.get_step_manager(), 131 | TmsPluginManager.get_fixture_manager()) 132 | 133 | config.pluginmanager.register(listener) 134 | TmsPluginManager.get_plugin_manager().register(listener) 135 | -------------------------------------------------------------------------------- /testit-adapter-pytest/tests/test_listener.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import MagicMock 3 | 4 | from testit_adapter_pytest.listener import TmsListener 5 | 6 | 7 | class TestTmsListener: 8 | def test_add_link_appends_link(self, mocker): 9 | # Arrange 10 | mock_adapter_manager = mocker.MagicMock() 11 | mock_step_manager = mocker.MagicMock() 12 | mock_fixture_manager = mocker.MagicMock() 13 | 14 | listener = TmsListener( 15 | adapter_manager=mock_adapter_manager, 16 | step_manager=mock_step_manager, 17 | fixture_manager=mock_fixture_manager 18 | ) 19 | 20 | # Mock the __executable_test attribute 21 | mock_executable_test = mocker.MagicMock() 22 | mock_executable_test.result_links = [] 23 | listener._TmsListener__executable_test = mock_executable_test 24 | 25 | test_link = {"url": "http://example.com", "title": "Example"} 26 | 27 | # Act 28 | listener.add_link(test_link) 29 | 30 | # Assert 31 | assert len(mock_executable_test.result_links) == 1 32 | assert mock_executable_test.result_links[0] == test_link 33 | 34 | def test_add_link_does_nothing_if_no_executable_test(self, mocker): 35 | # Arrange 36 | mock_adapter_manager = mocker.MagicMock() 37 | mock_step_manager = mocker.MagicMock() 38 | mock_fixture_manager = mocker.MagicMock() 39 | 40 | listener = TmsListener( 41 | adapter_manager=mock_adapter_manager, 42 | step_manager=mock_step_manager, 43 | fixture_manager=mock_fixture_manager 44 | ) 45 | listener._TmsListener__executable_test = None # Ensure __executable_test is None 46 | 47 | test_link = {"url": "http://example.com", "title": "Example"} 48 | 49 | # Act 50 | listener.add_link(test_link) 51 | 52 | # Assert 53 | # No direct assertion on links, but we ensure no error and __executable_test remains None 54 | # If __executable_test had a result_links attribute, we would check it's still empty. 55 | # For this case, the main check is that no AttributeError occurs. 56 | assert listener._TmsListener__executable_test is None -------------------------------------------------------------------------------- /testit-adapter-robotframework/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = tests src 3 | testpaths = tests -------------------------------------------------------------------------------- /testit-adapter-robotframework/requirements.txt: -------------------------------------------------------------------------------- 1 | attrs 2 | robotframework -------------------------------------------------------------------------------- /testit-adapter-robotframework/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | VERSION = "3.6.1" 4 | 5 | setup( 6 | name='testit-adapter-robotframework', 7 | version=VERSION, 8 | description='Robot Framework adapter for Test IT', 9 | long_description=open('README.md', "r").read(), 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/testit-tms/adapters-python/', 12 | author='Integration team', 13 | author_email='integrations@testit.software', 14 | license='Apache-2.0', 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: 3.12', 24 | ], 25 | py_modules=['testit_adapter_robotframework'], 26 | packages=find_packages(where='src'), 27 | package_dir={'': 'src'}, 28 | install_requires=['attrs', 'robotframework', 'testit-python-commons==' + VERSION] 29 | ) 30 | -------------------------------------------------------------------------------- /testit-adapter-robotframework/src/testit_adapter_robotframework/TMSLibrary.py: -------------------------------------------------------------------------------- 1 | from robot.libraries.BuiltIn import BuiltIn 2 | 3 | from testit_python_commons.services import TmsPluginManager 4 | 5 | from .listeners import AutotestAdapter, TestRunAdapter 6 | from .models import Option 7 | 8 | 9 | def enabled(func): 10 | def wrapped(self, *args, **kwargs): 11 | if self.enabled: 12 | return func(self, *args, **kwargs) 13 | else: 14 | raise ImportError("TestIt module should be enabled. Use '-v testit' CLI option") 15 | 16 | return wrapped 17 | 18 | 19 | class TMSLibrary: 20 | """Library for exporting result to TestIt. 21 | 22 | = Table of contents = 23 | 24 | %TOC% 25 | 26 | = Usage = 27 | 28 | This library has several keyword, for example `Add Link`, adding links to result of test in TestIt 29 | 30 | = Examples = 31 | 32 | | `Add Message` | My message | | | 33 | | `Add Link` | http://ya.ru | | | 34 | | `Add Attachments` | image.png | log.txt | video.gif | 35 | """ 36 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 37 | ROBOT_LIBRARY_VERSION = '1.0' 38 | 39 | def __init__(self): 40 | built_in = BuiltIn() 41 | self.enabled = built_in.get_variable_value("${testit}", None) is not None 42 | if self.enabled: 43 | cli_params = ["tmsUrl", "tmsPrivateToken", "tmsProjectId", "tmsConfigurationId", "tmsTestRunId", 44 | "tmsProxy", "tmsTestRunName", "tmsAdapterMode", "tmsConfigFile", "tmsCertValidation", 45 | "tmsAutomaticCreationTestCases", "tmsAutomaticUpdationLinksToTestCases", "tmsImportRealtime"] 46 | option = Option(**{param: built_in.get_variable_value(f'${{{param}}}', None) for param in cli_params}) 47 | self.adapter_manager = TmsPluginManager.get_adapter_manager(option) 48 | pabot_index = built_in.get_variable_value('${PABOTQUEUEINDEX}', None) 49 | if pabot_index is not None: 50 | try: 51 | from pabot import PabotLib 52 | pabot = PabotLib() 53 | if int(pabot_index) == 0: 54 | test_run_id = self.adapter_manager.get_test_run_id() 55 | pabot.set_parallel_value_for_key('test_run_id', test_run_id) 56 | else: 57 | while True: 58 | test_run_id = pabot.get_parallel_value_for_key('test_run_id') 59 | if test_run_id: 60 | break 61 | self.adapter_manager.set_test_run_id(test_run_id) 62 | except RuntimeError: 63 | raise SystemExit 64 | else: 65 | self.adapter_manager.set_test_run_id(self.adapter_manager.get_test_run_id()) 66 | self.ROBOT_LIBRARY_LISTENER = [AutotestAdapter(self.adapter_manager), TestRunAdapter(self.adapter_manager)] 67 | 68 | @enabled 69 | def add_link(self, url, type='Defect', title=None, description=None): # noqa: A002,VNE003 70 | """ 71 | Adds link to current test. 72 | 73 | Valid link types are ``Defect``, ``Issue``, ``Related``, ``BlockedBy``, ``Requirement``, ``Repository``. 74 | 75 | """ 76 | from testit_python_commons.models.link import Link 77 | 78 | link = Link()\ 79 | .set_url(url)\ 80 | .set_title(title)\ 81 | .set_link_type(type)\ 82 | .set_description(description) 83 | self.ROBOT_LIBRARY_LISTENER[0].active_test.resultLinks.append(link) 84 | 85 | @enabled 86 | def add_links(self, *links): 87 | """ 88 | Adds several links to current test. 89 | 90 | Every link should be a dict with ``url`` key. See `Add Link` keyword for more information. 91 | 92 | """ 93 | for link in links: 94 | if isinstance(link, dict): 95 | self.add_link(**link) 96 | 97 | @enabled 98 | def add_attachments(self, *paths): 99 | """ 100 | Adds several attachments to current test. 101 | 102 | """ 103 | attachments = self.adapter_manager.load_attachments(paths) 104 | self.ROBOT_LIBRARY_LISTENER[0].active_test.attachments.extend(attachments) 105 | 106 | @enabled 107 | def add_attachment(self, text, filename=None): 108 | """ 109 | Adds attachment to current test 110 | 111 | """ 112 | attachment = self.adapter_manager.create_attachment(text, filename) 113 | self.ROBOT_LIBRARY_LISTENER[0].active_test.attachments.extend(attachment) 114 | 115 | @enabled 116 | def add_message(self, message): 117 | """ 118 | Adds error message to current test 119 | """ 120 | self.ROBOT_LIBRARY_LISTENER[0].active_test.message = message 121 | -------------------------------------------------------------------------------- /testit-adapter-robotframework/src/testit_adapter_robotframework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-adapter-robotframework/src/testit_adapter_robotframework/__init__.py -------------------------------------------------------------------------------- /testit-adapter-robotframework/src/testit_adapter_robotframework/listeners.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from robot.api import SuiteVisitor, logger 4 | from robot.libraries.BuiltIn import BuiltIn 5 | 6 | from .models import Autotest 7 | from .utils import STATUSES, convert_time, convert_executable_test_to_test_result_model, get_hash 8 | 9 | 10 | class AutotestAdapter: 11 | ROBOT_LISTENER_API_VERSION = 2 12 | 13 | def __init__(self, adapter_manager): 14 | self.adapter_manager = adapter_manager 15 | self.active_test = None 16 | 17 | @staticmethod 18 | def get_test_title(attrs): 19 | title = attrs['type'] 20 | return "\t".join( 21 | attrs['assign'] + [title if title not in ["SETUP", "TEARDOWN", "KEYWORD"] else "", 22 | attrs['kwname']] + attrs['args']) 23 | 24 | @staticmethod 25 | def parse_arguments(args): 26 | parameters = {} 27 | for arg in args: 28 | variables = re.findall(r'\${[a-zA-Z-_\\ \d]*}', arg) 29 | for arg_var in variables: 30 | value = str(BuiltIn().get_variable_value(arg_var)) 31 | if len(value) > 2000: 32 | value = value[:2000] 33 | parameters[arg_var] = value 34 | return parameters if parameters else None 35 | 36 | def start_test(self, name, attributes): 37 | self.active_test = Autotest(autoTestName=name) 38 | self.active_test.add_attributes(attributes) 39 | BuiltIn().remove_tags("testit*") 40 | 41 | def start_keyword(self, name, attributes): 42 | if self.active_test: 43 | title = self.get_test_title(attributes) 44 | parameters = self.parse_arguments(attributes['args']) 45 | self.active_test.add_step(attributes['type'], title, attributes['doc'], parameters) 46 | 47 | def end_keyword(self, name, attributes): 48 | if self.active_test: 49 | title = self.get_test_title(attributes) 50 | start = convert_time(attributes['starttime']) 51 | end = convert_time(attributes['endtime']) 52 | duration = attributes['elapsedtime'] 53 | outcome = STATUSES[attributes['status']] 54 | self.active_test.add_step_result(title, start, end, duration, outcome, self.active_test.attachments) 55 | self.active_test.attachments = [] 56 | 57 | def end_test(self, name, attributes): 58 | if self.active_test: 59 | self.active_test.outcome = STATUSES[attributes['status']] 60 | self.active_test.started_on = convert_time(attributes['starttime']) 61 | self.active_test.completed_on = convert_time(attributes['endtime']) 62 | if not self.active_test.message: 63 | if self.active_test.outcome == 'Failed': 64 | for step in (self.active_test.setUpResults + self.active_test.stepResults + 65 | self.active_test.tearDownResults): 66 | if step.outcome == 'Failed': 67 | self.active_test.message = f"Failed on step: '{step.title}'" 68 | break 69 | self.active_test.traces = attributes['message'] 70 | self.active_test.duration = attributes['elapsedtime'] 71 | self.adapter_manager.write_test( 72 | convert_executable_test_to_test_result_model(self.active_test.order())) 73 | 74 | def close(self): 75 | self.adapter_manager.write_tests() 76 | 77 | 78 | class TestRunAdapter: 79 | ROBOT_LISTENER_API_VERSION = 3 80 | 81 | def __init__(self, adapter_manager): 82 | self.adapter_manager = adapter_manager 83 | 84 | def start_suite(self, suite, result): 85 | tests = self.adapter_manager.get_autotests_for_launch() 86 | if tests is not None: 87 | selector = ExcludeTests(*tests) 88 | suite.visit(selector) 89 | 90 | 91 | class ExcludeTests(SuiteVisitor): 92 | 93 | def __init__(self, *tests): 94 | self.tests = tests 95 | if not len(self.tests): 96 | logger.error('No tests to run!') 97 | raise SystemExit 98 | 99 | def start_suite(self, suite): 100 | suite.tests = [t for t in suite.tests if self._is_included(t)] 101 | 102 | def _is_included(self, test): 103 | tags = test.tags 104 | external_id = get_hash(test.longname) 105 | for tag in tags: 106 | if str(tag).lower().startswith('testit.externalid'): 107 | external_id = tag.split(':', 1)[-1].strip() 108 | return external_id in self.tests 109 | 110 | def end_suite(self, suite): 111 | suite.suites = [s for s in suite.suites if s.test_count > 0] 112 | 113 | def visit_test(self, test): 114 | pass 115 | -------------------------------------------------------------------------------- /testit-adapter-robotframework/src/testit_adapter_robotframework/models.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | 4 | from attr import Factory, asdict, attrib, s 5 | 6 | from robot.api import logger 7 | from testit_python_commons.models.link import Link 8 | 9 | from .utils import get_hash 10 | 11 | 12 | LinkTypes = ['Related', 'BlockedBy', 'Defect', 'Issue', 'Requirement', 'Repository'] 13 | 14 | 15 | def link_type_check(self, attribute, value): 16 | if value.title() not in LinkTypes: 17 | raise ValueError(f"Incorrect Link type: {value}") 18 | 19 | 20 | def url_check(self, attribute, value): 21 | if not bool(re.match( 22 | r"(https?|ftp)://" 23 | r"(\w+(-\w+)*\.)?" 24 | r"((\w+(-\w+)*)\.(\w+))" 25 | r"(\.\w+)*" 26 | r"([\w\-._~/]*)*(? TestResult: 16 | return TestResult()\ 17 | .set_external_id(executable_test['externalID'])\ 18 | .set_autotest_name(executable_test['autoTestName'])\ 19 | .set_step_results( 20 | step_results_to_autotest_steps_model(executable_test['stepResults']))\ 21 | .set_setup_results( 22 | step_results_to_autotest_steps_model(executable_test['setUpResults']))\ 23 | .set_teardown_results( 24 | step_results_to_autotest_steps_model(executable_test['tearDownResults']))\ 25 | .set_duration(executable_test['duration'])\ 26 | .set_outcome(executable_test['outcome'])\ 27 | .set_traces(executable_test['traces'])\ 28 | .set_attachments(executable_test['attachments'])\ 29 | .set_parameters(executable_test['parameters'])\ 30 | .set_properties(executable_test['properties'])\ 31 | .set_namespace(executable_test['namespace'])\ 32 | .set_classname(executable_test['classname'])\ 33 | .set_title(executable_test['title'])\ 34 | .set_description(executable_test['description'])\ 35 | .set_links(executable_test['links'])\ 36 | .set_result_links(executable_test['resultLinks'])\ 37 | .set_labels(executable_test['labels'])\ 38 | .set_work_item_ids(executable_test['workItemsID'])\ 39 | .set_message(executable_test['message'])\ 40 | .set_external_key(executable_test['externalKey']) 41 | 42 | 43 | def step_results_to_autotest_steps_model(step_results: dict) -> typing.List[StepResult]: 44 | autotest_model_steps = [] 45 | 46 | for step_result in step_results: 47 | step_result_model = StepResult()\ 48 | .set_title(step_result['title'])\ 49 | .set_description(step_result['description'])\ 50 | .set_outcome(step_result['outcome'])\ 51 | .set_duration(step_result['duration'])\ 52 | .set_attachments(step_result['attachments']) 53 | 54 | if 'parameters' in step_result: 55 | step_result_model.set_parameters(step_result['parameters']) 56 | 57 | if 'step_results' in step_result: 58 | step_result_model.set_step_results( 59 | step_results_to_autotest_steps_model(step_result['step_results'])) 60 | 61 | autotest_model_steps.append(step_result_model) 62 | 63 | return autotest_model_steps 64 | 65 | 66 | def get_hash(value: str): 67 | md = hashlib.sha256(bytes(value, encoding='utf-8')) 68 | return md.hexdigest() 69 | 70 | 71 | STATUSES = {'FAIL': 'Failed', 'PASS': 'Passed', 'SKIP': 'Skipped', 'NOT RUN': 'Skipped'} 72 | -------------------------------------------------------------------------------- /testit-adapter-robotframework/tests/test_listeners.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pytest_mock import MockerFixture 3 | from unittest.mock import MagicMock 4 | 5 | 6 | class TestAutotestAdapter: 7 | def test_parse_arguments(self, mocker: MockerFixture): 8 | mock_builtin_class = mocker.Mock() 9 | mock_builtin_instance = mocker.Mock() 10 | mock_builtin_class.return_value = mock_builtin_instance 11 | 12 | mock_builtin_instance.get_variable_value.side_effect = [ 13 | "value1", 14 | "value2", 15 | "a" * 2500 # A value longer than 2000 characters 16 | ] 17 | 18 | def parse_arguments(args): 19 | parameters = {} 20 | for arg in args: 21 | variables = re.findall(r'\${[a-zA-Z-_\\ \d]*}', arg) 22 | for arg_var in variables: 23 | value = str(mock_builtin_instance.get_variable_value(arg_var)) 24 | if len(value) > 2000: 25 | value = value[:2000] 26 | parameters[arg_var] = value 27 | return parameters if parameters else None 28 | 29 | # Arrange 30 | args = ["${var1}", "prefix_${var2}_suffix", "${var3_with_long_value}"] 31 | 32 | # Act 33 | parameters = parse_arguments(args) 34 | 35 | # Assert 36 | assert parameters == { 37 | "${var1}": "value1", 38 | "${var2}": "value2", 39 | "${var3_with_long_value}": "a" * 2000 # Should be truncated 40 | } 41 | 42 | expected_calls = [ 43 | mocker.call("${var1}"), 44 | mocker.call("${var2}"), 45 | mocker.call("${var3_with_long_value}") 46 | ] 47 | mock_builtin_instance.get_variable_value.assert_has_calls(expected_calls, any_order=True) -------------------------------------------------------------------------------- /testit-python-commons/README.md: -------------------------------------------------------------------------------- 1 | # How to enable debug logging? 2 | 1. Add in **connection_config.ini** file from the root directory of the project: 3 | ``` 4 | [debug] 5 | __DEV = true 6 | ``` 7 | -------------------------------------------------------------------------------- /testit-python-commons/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = tests src 3 | testpaths = tests -------------------------------------------------------------------------------- /testit-python-commons/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-xdist 3 | 4 | flake8 5 | flake8-builtins 6 | pep8-naming 7 | flake8-variables-names 8 | flake8-import-order 9 | mypy 10 | -------------------------------------------------------------------------------- /testit-python-commons/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | VERSION = "3.6.1" 4 | 5 | setup( 6 | name='testit-python-commons', 7 | version=VERSION, 8 | description='Python commons for Test IT', 9 | long_description=open('README.md', "r").read(), 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/testit-tms/adapters-python/', 12 | author='Integration team', 13 | author_email='integrations@testit.software', 14 | license='Apache-2.0', 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.6', 18 | 'Programming Language :: Python :: 3.7', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Programming Language :: Python :: 3.9', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: 3.12', 24 | ], 25 | py_modules=['testit', 'testit_python_commons'], 26 | packages=find_packages(where='src'), 27 | package_dir={'': 'src'}, 28 | install_requires=['pluggy', 'testit-api-client==6.0.1.post530'] 29 | ) 30 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.decorators import ( 2 | className, 3 | description, 4 | displayName, 5 | externalID, 6 | externalId, 7 | labels, 8 | link, 9 | links, 10 | nameSpace, 11 | title, 12 | workItemID, 13 | workItemIds 14 | ) 15 | from testit_python_commons.dynamic_methods import ( 16 | addAttachments, 17 | addLink, 18 | addLinks, 19 | addMessage, 20 | addWorkItemIds, 21 | addDisplayName, 22 | addNameSpace, 23 | addClassName, 24 | addExternalId, 25 | addTitle, 26 | addDescription, 27 | addLabels, 28 | addParameter, 29 | attachments, 30 | message 31 | ) 32 | from testit_python_commons.models import LinkType 33 | from testit_python_commons.step import step 34 | 35 | __all__ = [ 36 | 'externalID', 37 | 'externalId', 38 | 'displayName', 39 | 'nameSpace', 40 | 'className', 41 | 'workItemID', 42 | 'workItemIds', 43 | 'title', 44 | 'description', 45 | 'labels', 46 | 'link', 47 | 'links', 48 | 'addLink', 49 | 'addLinks', 50 | 'attachments', 51 | 'addAttachments', 52 | 'message', 53 | 'addMessage', 54 | 'addWorkItemIds', 55 | 'addDisplayName', 56 | 'addNameSpace', 57 | 'addClassName', 58 | 'addExternalId', 59 | 'addTitle', 60 | 'addDescription', 61 | 'addLabels', 62 | 'addParameter', 63 | 'step', 64 | 'LinkType' 65 | ] 66 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-python-commons/src/testit_python_commons/__init__.py -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/app_properties.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import logging 3 | import os 4 | import re 5 | import warnings 6 | import uuid 7 | from urllib.parse import urlparse 8 | 9 | from testit_python_commons.models.adapter_mode import AdapterMode 10 | 11 | 12 | class AppProperties: 13 | __project_metadata_file = 'pyproject.toml' 14 | __properties_file = 'connection_config.ini' 15 | __available_extensions = ['.ini', '.toml'] 16 | 17 | __env_prefix = 'TMS' 18 | 19 | @staticmethod 20 | def load_properties(option=None): 21 | properties = AppProperties.load_file_properties( 22 | option.set_config_file if hasattr(option, 'set_config_file') else None) 23 | 24 | properties.update(AppProperties.load_env_properties()) 25 | 26 | if option: 27 | properties.update(AppProperties.load_cli_properties(option)) 28 | 29 | AppProperties.__check_properties(properties) 30 | 31 | return properties 32 | 33 | @classmethod 34 | def load_file_properties(cls, file_name: str = None): 35 | properties = {} 36 | 37 | path = os.path.abspath('') 38 | root = path[:path.index(os.sep)] 39 | 40 | if file_name: 41 | _, extension = os.path.splitext(file_name) 42 | if extension not in cls.__available_extensions: 43 | raise FileNotFoundError( 44 | f'{file_name} is not a valid file. Available extensions: {cls.__available_extensions}' 45 | ) 46 | cls.__properties_file = file_name 47 | 48 | if os.environ.get(f'{cls.__env_prefix}_CONFIG_FILE'): 49 | cls.__properties_file = os.environ.get(f'{cls.__env_prefix}_CONFIG_FILE') 50 | 51 | if os.path.isfile(cls.__project_metadata_file): 52 | # https://peps.python.org/pep-0621/ 53 | cls.__properties_file = cls.__project_metadata_file 54 | 55 | while not os.path.isfile( 56 | path + os.sep + cls.__properties_file) and path != root: 57 | path = path[:path.rindex(os.sep)] 58 | 59 | path = path + os.sep + cls.__properties_file 60 | 61 | if os.path.isfile(path): 62 | parser = configparser.RawConfigParser() 63 | 64 | parser.read(path, encoding="utf-8") 65 | 66 | if parser.has_section('testit'): 67 | for key, value in parser.items('testit'): 68 | properties[key] = cls.__search_in_environ(value) 69 | 70 | if parser.has_section('debug'): 71 | if parser.has_option('debug', 'tmsproxy'): 72 | properties['tmsproxy'] = cls.__search_in_environ( 73 | parser.get('debug', 'tmsproxy')) 74 | 75 | if parser.has_option('debug', '__dev'): 76 | properties['logs'] = cls.__search_in_environ( 77 | parser.get('debug', '__dev')).lower() 78 | 79 | if 'privatetoken' in properties: 80 | warnings.warn( 81 | 'The configuration file specifies a private token. It is not safe.' 82 | ' Use TMS_PRIVATE_TOKEN environment variable', 83 | category=Warning, 84 | stacklevel=2) 85 | warnings.simplefilter('default', Warning) 86 | 87 | return properties 88 | 89 | @classmethod 90 | def load_cli_properties(cls, option): 91 | cli_properties = {} 92 | 93 | if hasattr(option, 'set_url') and cls.__check_property_value(option.set_url): 94 | cli_properties['url'] = option.set_url 95 | 96 | if hasattr(option, 'set_private_token') and cls.__check_property_value(option.set_private_token): 97 | cli_properties['privatetoken'] = option.set_private_token 98 | 99 | if hasattr(option, 'set_project_id') and cls.__check_property_value(option.set_project_id): 100 | cli_properties['projectid'] = option.set_project_id 101 | 102 | if hasattr(option, 'set_configuration_id') and cls.__check_property_value(option.set_configuration_id): 103 | cli_properties['configurationid'] = option.set_configuration_id 104 | 105 | if hasattr(option, 'set_test_run_id') and cls.__check_property_value(option.set_test_run_id): 106 | cli_properties['testrunid'] = option.set_test_run_id 107 | 108 | if hasattr(option, 'set_test_run_name') and cls.__check_property_value(option.set_test_run_name): 109 | cli_properties['testrunname'] = option.set_test_run_name 110 | 111 | if hasattr(option, 'set_tms_proxy') and cls.__check_property_value(option.set_tms_proxy): 112 | cli_properties['tmsproxy'] = option.set_tms_proxy 113 | 114 | if hasattr(option, 'set_adapter_mode') and cls.__check_property_value(option.set_adapter_mode): 115 | cli_properties['adaptermode'] = option.set_adapter_mode 116 | 117 | if hasattr(option, 'set_cert_validation') and cls.__check_property_value(option.set_cert_validation): 118 | cli_properties['certvalidation'] = option.set_cert_validation 119 | 120 | if hasattr(option, 'set_automatic_creation_test_cases') and cls.__check_property_value( 121 | option.set_automatic_creation_test_cases): 122 | cli_properties['automaticcreationtestcases'] = option.set_automatic_creation_test_cases 123 | 124 | if hasattr(option, 'set_automatic_updation_links_to_test_cases') and cls.__check_property_value( 125 | option.set_automatic_updation_links_to_test_cases): 126 | cli_properties['automaticupdationlinkstotestcases'] = option.set_automatic_updation_links_to_test_cases 127 | 128 | if hasattr(option, 'set_import_realtime') and cls.__check_property_value( 129 | option.set_import_realtime): 130 | cli_properties['importrealtime'] = option.set_import_realtime 131 | 132 | return cli_properties 133 | 134 | @classmethod 135 | def load_env_properties(cls): 136 | env_properties = {} 137 | 138 | if f'{cls.__env_prefix}_URL' in os.environ.keys() and cls.__check_property_value( 139 | os.environ.get(f'{cls.__env_prefix}_URL')): 140 | env_properties['url'] = os.environ.get(f'{cls.__env_prefix}_URL') 141 | 142 | if f'{cls.__env_prefix}_PRIVATE_TOKEN' in os.environ.keys() and cls.__check_property_value( 143 | os.environ.get(f'{cls.__env_prefix}_PRIVATE_TOKEN')): 144 | env_properties['privatetoken'] = os.environ.get(f'{cls.__env_prefix}_PRIVATE_TOKEN') 145 | 146 | if f'{cls.__env_prefix}_PROJECT_ID' in os.environ.keys() and cls.__check_property_value( 147 | os.environ.get(f'{cls.__env_prefix}_PROJECT_ID')): 148 | env_properties['projectid'] = os.environ.get(f'{cls.__env_prefix}_PROJECT_ID') 149 | 150 | if f'{cls.__env_prefix}_CONFIGURATION_ID' in os.environ.keys() and cls.__check_property_value( 151 | os.environ.get(f'{cls.__env_prefix}_CONFIGURATION_ID')): 152 | env_properties['configurationid'] = os.environ.get(f'{cls.__env_prefix}_CONFIGURATION_ID') 153 | 154 | if f'{cls.__env_prefix}_TEST_RUN_ID' in os.environ.keys() and cls.__check_property_value( 155 | os.environ.get(f'{cls.__env_prefix}_TEST_RUN_ID')): 156 | env_properties['testrunid'] = os.environ.get(f'{cls.__env_prefix}_TEST_RUN_ID') 157 | 158 | if f'{cls.__env_prefix}_TEST_RUN_NAME' in os.environ.keys() and cls.__check_property_value( 159 | os.environ.get(f'{cls.__env_prefix}_TEST_RUN_NAME')): 160 | env_properties['testrunname'] = os.environ.get(f'{cls.__env_prefix}_TEST_RUN_NAME') 161 | 162 | if f'{cls.__env_prefix}_PROXY' in os.environ.keys() and cls.__check_property_value( 163 | os.environ.get(f'{cls.__env_prefix}_PROXY')): 164 | env_properties['tmsproxy'] = os.environ.get(f'{cls.__env_prefix}_PROXY') 165 | 166 | if f'{cls.__env_prefix}_ADAPTER_MODE' in os.environ.keys() and cls.__check_property_value( 167 | os.environ.get(f'{cls.__env_prefix}_ADAPTER_MODE')): 168 | env_properties['adaptermode'] = os.environ.get(f'{cls.__env_prefix}_ADAPTER_MODE') 169 | 170 | if f'{cls.__env_prefix}_CERT_VALIDATION' in os.environ.keys() and cls.__check_property_value( 171 | os.environ.get(f'{cls.__env_prefix}_CERT_VALIDATION')): 172 | env_properties['certvalidation'] = os.environ.get(f'{cls.__env_prefix}_CERT_VALIDATION') 173 | 174 | if f'{cls.__env_prefix}_AUTOMATIC_CREATION_TEST_CASES' in os.environ.keys() and cls.__check_property_value( 175 | os.environ.get(f'{cls.__env_prefix}_AUTOMATIC_CREATION_TEST_CASES')): 176 | env_properties['automaticcreationtestcases'] = os.environ.get( 177 | f'{cls.__env_prefix}_AUTOMATIC_CREATION_TEST_CASES') 178 | 179 | if f'{cls.__env_prefix}_AUTOMATIC_UPDATION_LINKS_TO_TEST_CASES' in os.environ.keys() and cls.__check_property_value( 180 | os.environ.get(f'{cls.__env_prefix}_AUTOMATIC_UPDATION_LINKS_TO_TEST_CASES')): 181 | env_properties['automaticupdationlinkstotestcases'] = os.environ.get( 182 | f'{cls.__env_prefix}_AUTOMATIC_UPDATION_LINKS_TO_TEST_CASES') 183 | 184 | if f'{cls.__env_prefix}_IMPORT_REALTIME' in os.environ.keys() and cls.__check_property_value( 185 | os.environ.get(f'{cls.__env_prefix}_IMPORT_REALTIME')): 186 | env_properties['importrealtime'] = os.environ.get( 187 | f'{cls.__env_prefix}_IMPORT_REALTIME') 188 | 189 | return env_properties 190 | 191 | @classmethod 192 | def __check_properties(cls, properties: dict): 193 | adapter_mode = properties.get('adaptermode') 194 | 195 | if adapter_mode == AdapterMode.NEW_TEST_RUN: 196 | try: 197 | uuid.UUID(str(properties.get('testrunid'))) 198 | logging.error('Adapter mode "2" is enabled. Config should not contains test run id!') 199 | raise SystemExit 200 | except ValueError: 201 | pass 202 | 203 | elif adapter_mode in ( 204 | AdapterMode.RUN_ALL_TESTS, 205 | AdapterMode.USE_FILTER, 206 | None): 207 | try: 208 | uuid.UUID(str(properties.get('testrunid'))) 209 | except ValueError: 210 | logging.error(f'Adapter mode "{adapter_mode if adapter_mode else "0"}" is enabled. ' 211 | f'The test run ID is needed, but it was not found!') 212 | raise SystemExit 213 | else: 214 | logging.error(f'Unknown adapter mode "{adapter_mode}"!') 215 | raise SystemExit 216 | 217 | try: 218 | uuid.UUID(str(properties.get('projectid'))) 219 | except ValueError: 220 | logging.error('Project ID was not found!') 221 | raise SystemExit 222 | 223 | try: 224 | url = urlparse(properties.get('url')) 225 | if not all([url.scheme, url.netloc]): 226 | raise AttributeError 227 | except AttributeError: 228 | logging.error('URL is invalid!') 229 | raise SystemExit 230 | 231 | if not cls.__check_property_value(properties.get('privatetoken')): 232 | logging.error('Private token was not found!') 233 | raise SystemExit 234 | 235 | try: 236 | uuid.UUID(str(properties.get('configurationid'))) 237 | except ValueError: 238 | logging.error('Configuration ID was not found!') 239 | raise SystemExit 240 | 241 | if not cls.__check_property_value(properties.get('certvalidation')): 242 | properties['certvalidation'] = 'true' 243 | 244 | if not cls.__check_property_value(properties.get('automaticcreationtestcases')): 245 | properties['automaticcreationtestcases'] = 'false' 246 | 247 | if not cls.__check_property_value(properties.get('automaticupdationlinkstotestcases')): 248 | properties['automaticupdationlinkstotestcases'] = 'false' 249 | 250 | if not cls.__check_property_value(properties.get('importrealtime')): 251 | properties['importrealtime'] = 'true' 252 | 253 | @staticmethod 254 | def __search_in_environ(var_name: str): 255 | if re.fullmatch(r'{[a-zA-Z_]\w*}', var_name) and var_name[1:-1] in os.environ: 256 | return os.environ[var_name[1:-1]] 257 | 258 | return var_name 259 | 260 | @staticmethod 261 | def __check_property_value(value: str): 262 | if value is not None and value != "": 263 | return True 264 | 265 | return False 266 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/client/__init__.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.client.api_client import ( 2 | ApiClientWorker, 3 | ClientConfiguration 4 | ) 5 | 6 | __all__ = [ 7 | 'ApiClientWorker', 8 | 'ClientConfiguration' 9 | ] 10 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/client/client_configuration.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.services.logger import adapter_logger 2 | from testit_python_commons.services.utils import Utils 3 | 4 | 5 | class ClientConfiguration: 6 | __project_id = None 7 | __test_run_id = None 8 | 9 | def __init__(self, app_properties: dict): 10 | if app_properties.get('projectid'): 11 | self.__project_id = Utils.uuid_check(app_properties.get('projectid')) 12 | 13 | if app_properties.get('testrunid'): 14 | self.__test_run_id = Utils.uuid_check(app_properties.get('testrunid')) 15 | 16 | self.__url = Utils.url_check(app_properties.get('url')) 17 | self.__private_token = app_properties.get('privatetoken') 18 | self.__configuration_id = Utils.uuid_check(app_properties.get('configurationid')) 19 | self.__test_run_name = app_properties.get('testrunname') 20 | self.__tms_proxy = app_properties.get('tmsproxy') 21 | self.__adapter_mode = app_properties.get('adaptermode') 22 | self.__cert_validation = app_properties.get('certvalidation') 23 | self.__automatic_updation_links_to_test_cases = app_properties.get('automaticupdationlinkstotestcases') 24 | 25 | @adapter_logger 26 | def get_url(self): 27 | return self.__url 28 | 29 | @adapter_logger 30 | def get_private_token(self): 31 | return self.__private_token 32 | 33 | @adapter_logger 34 | def get_project_id(self): 35 | return self.__project_id 36 | 37 | @adapter_logger 38 | def set_project_id(self, project_id: str): 39 | self.__project_id = project_id 40 | 41 | @adapter_logger 42 | def get_configuration_id(self): 43 | return self.__configuration_id 44 | 45 | @adapter_logger 46 | def get_test_run_id(self): 47 | return self.__test_run_id 48 | 49 | @adapter_logger 50 | def set_test_run_id(self, test_run_id: str): 51 | self.__test_run_id = test_run_id 52 | 53 | @adapter_logger 54 | def get_test_run_name(self): 55 | return self.__test_run_name 56 | 57 | @adapter_logger 58 | def get_proxy(self): 59 | return self.__tms_proxy 60 | 61 | @adapter_logger 62 | def get_mode(self): 63 | return self.__adapter_mode 64 | 65 | def get_cert_validation(self): 66 | return self.__cert_validation 67 | 68 | def get_automatic_updation_links_to_test_cases(self): 69 | return self.__automatic_updation_links_to_test_cases 70 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/decorators.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import types 4 | from functools import wraps 5 | 6 | from testit_python_commons.services.logger import adapter_logger 7 | from testit_python_commons.services.utils import Utils 8 | 9 | 10 | def inner(function): 11 | def set_properties(kwargs): 12 | if not hasattr(function, 'test_properties') and kwargs: 13 | function.test_properties = {} 14 | 15 | for key, value in kwargs.items(): 16 | if hasattr(function, 17 | 'callspec') and key not in function.callspec.params: 18 | function.test_properties[key] = str(value) 19 | 20 | @wraps(function) 21 | def sync_wrapper(*args, **kwargs): 22 | set_properties(kwargs) 23 | function(*args, **kwargs) 24 | 25 | @wraps(function) 26 | async def async_wrapper(*args, **kwargs): 27 | set_properties(kwargs) 28 | await function(*args, **kwargs) 29 | 30 | if isinstance(function, types.FunctionType): 31 | if asyncio.iscoroutinefunction(function): 32 | return async_wrapper 33 | 34 | return sync_wrapper 35 | 36 | return function 37 | 38 | 39 | @Utils.deprecated('Use "workItemIds" instead.') 40 | @adapter_logger 41 | def workItemID(*test_workitems_id: int or str): # noqa: N802 42 | def outer(function): 43 | function.test_workitems_id = [] 44 | for test_workitem_id in test_workitems_id: 45 | function.test_workitems_id.append(str(test_workitem_id)) 46 | return inner(function) 47 | 48 | return outer 49 | 50 | 51 | @adapter_logger 52 | def workItemIds(*test_workitems_id: int or str): # noqa: N802 53 | def outer(function): # noqa: N802 54 | function.test_workitems_id = [] 55 | for test_workitem_id in test_workitems_id: 56 | function.test_workitems_id.append(str(test_workitem_id)) 57 | return inner(function) 58 | 59 | return outer 60 | 61 | 62 | @adapter_logger 63 | def displayName(test_displayname: str): # noqa: N802 64 | def outer(function): 65 | function.test_displayname = test_displayname 66 | return inner(function) 67 | 68 | return outer 69 | 70 | 71 | @adapter_logger 72 | def nameSpace(test_namespace: str): # noqa: N802 73 | def outer(function): 74 | function.test_namespace = test_namespace 75 | return inner(function) 76 | 77 | return outer 78 | 79 | 80 | @adapter_logger 81 | def className(test_classname: str): # noqa: N802 82 | def outer(function): 83 | function.test_classname = test_classname 84 | return inner(function) 85 | 86 | return outer 87 | 88 | 89 | @Utils.deprecated('Use "externalId" instead.') 90 | @adapter_logger 91 | def externalID(test_external_id: str): # noqa: N802 92 | def outer(function): 93 | function.test_external_id = test_external_id 94 | return inner(function) 95 | 96 | return outer 97 | 98 | 99 | @adapter_logger 100 | def externalId(test_external_id: str): # noqa: N802 101 | def outer(function): 102 | function.test_external_id = test_external_id 103 | return inner(function) 104 | 105 | return outer 106 | 107 | 108 | @adapter_logger 109 | def title(test_title: str): 110 | def outer(function): 111 | function.test_title = test_title 112 | return inner(function) 113 | 114 | return outer 115 | 116 | 117 | @adapter_logger 118 | def description(test_description: str): 119 | def outer(function): 120 | function.test_description = test_description 121 | return inner(function) 122 | 123 | return outer 124 | 125 | 126 | @adapter_logger 127 | def labels(*test_labels: str): 128 | def outer(function): 129 | function.test_labels = test_labels 130 | return inner(function) 131 | 132 | return outer 133 | 134 | 135 | @Utils.deprecated('Use "links" instead.') 136 | @adapter_logger 137 | def link(url: str, title: str = None, type: str = None, description: str = None): # noqa: A002,VNE003 138 | def outer(function): 139 | if not hasattr(function, 'test_links'): 140 | function.test_links = [] 141 | 142 | function.test_links.append( 143 | Utils.convert_link_dict_to_link_model({ 144 | "url": url, 145 | "title": title, 146 | "type": type, 147 | "description": description})) 148 | 149 | return inner(function) 150 | 151 | return outer 152 | 153 | 154 | @adapter_logger 155 | def links(url: str = None, title: str = None, type: str = None, # noqa: A002,VNE003 156 | description: str = None, links: list or tuple = None): 157 | def outer(function): 158 | if not hasattr(function, 'test_links'): 159 | function.test_links = [] 160 | 161 | if url: 162 | function.test_links.append( 163 | Utils.convert_link_dict_to_link_model({ 164 | "url": url, 165 | "title": title, 166 | "type": type, 167 | "description": description})) 168 | elif links and (isinstance(links, list) or isinstance(links, tuple)): 169 | for link in links: 170 | if isinstance(link, dict) and 'url' in link: 171 | function.test_links.append( 172 | Utils.convert_link_dict_to_link_model(link)) 173 | else: 174 | logging.warning(f'Link ({link}) can\'t be processed!') 175 | else: 176 | logging.warning(f'Links for {function.__name__} can\'t be processed!\nPlease, set "url" or "links"!') 177 | return inner(function) 178 | 179 | return outer 180 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/dynamic_methods.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from testit_python_commons.services import TmsPluginManager 4 | from testit_python_commons.services.logger import adapter_logger 5 | from testit_python_commons.services.utils import Utils 6 | 7 | 8 | @Utils.deprecated('Use "addLinks" instead.') 9 | @adapter_logger 10 | def addLink(url: str, title: str = None, type: str = None, description: str = None): # noqa: A002,VNE003,N802 11 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_link'): 12 | TmsPluginManager.get_plugin_manager().hook \ 13 | .add_link( 14 | link=Utils.convert_link_dict_to_link_model({ 15 | "url": url, 16 | "title": title, 17 | "type": type, 18 | "description": description})) 19 | 20 | 21 | @adapter_logger 22 | def addLinks(url: str = None, title: str = None, type: str = None, description: str = None, # noqa: A002,VNE003,N802 23 | links: list or tuple = None): 24 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_link'): 25 | if url: 26 | TmsPluginManager.get_plugin_manager().hook \ 27 | .add_link( 28 | link=Utils.convert_link_dict_to_link_model({ 29 | "url": url, 30 | "title": title, 31 | "type": type, 32 | "description": description})) 33 | elif links and (isinstance(links, list) or isinstance(links, tuple)): 34 | for link in links: 35 | if isinstance(link, dict) and 'url' in link: 36 | TmsPluginManager.get_plugin_manager().hook \ 37 | .add_link(link=Utils.convert_link_dict_to_link_model(link)) 38 | else: 39 | logging.warning(f'Link ({link}) can\'t be processed!') 40 | else: 41 | logging.warning("Links can't be processed!\nPlease, set 'url' or 'links'!") 42 | 43 | 44 | @Utils.deprecated('Use "addMessage" instead.') 45 | @adapter_logger 46 | def message(test_message: str): 47 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_message'): 48 | TmsPluginManager.get_plugin_manager().hook \ 49 | .add_message(test_message=test_message) 50 | 51 | 52 | @adapter_logger 53 | def addMessage(test_message: str): # noqa: N802 54 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_message'): 55 | TmsPluginManager.get_plugin_manager().hook \ 56 | .add_message(test_message=test_message) 57 | 58 | 59 | @Utils.deprecated('Use "addAttachments" instead.') 60 | @adapter_logger 61 | def attachments(*attachments_paths): 62 | active_step = TmsPluginManager.get_step_manager().get_active_step() 63 | 64 | if active_step: 65 | attachment_ids = TmsPluginManager.get_adapter_manager().load_attachments(attachments_paths) 66 | 67 | active_step.set_attachments(active_step.get_attachments() + attachment_ids) 68 | else: 69 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_attachments'): 70 | TmsPluginManager.get_plugin_manager().hook \ 71 | .add_attachments(attach_paths=attachments_paths) 72 | 73 | 74 | @adapter_logger 75 | def addAttachments(data, is_text: bool = False, name: str = None): # noqa: N802 76 | active_step = TmsPluginManager.get_step_manager().get_active_step() 77 | 78 | if active_step: 79 | add_attachments_to_step(active_step, data, is_text, name) 80 | else: 81 | add_attachments_to_test(data, is_text, name) 82 | 83 | 84 | @adapter_logger 85 | def add_attachments_to_step(step, data, is_text: bool = False, name: str = None): 86 | if is_text: 87 | attachment_ids = TmsPluginManager.get_adapter_manager().create_attachment(data, name) 88 | else: 89 | if isinstance(data, str): 90 | attachment_ids = TmsPluginManager.get_adapter_manager().load_attachments([data]) 91 | elif isinstance(data, tuple) or isinstance(data, list): 92 | attachment_ids = TmsPluginManager.get_adapter_manager().load_attachments(data) 93 | else: 94 | logging.warning(f'File ({data}) not found!') 95 | return 96 | 97 | step.set_attachments(step.get_attachments() + attachment_ids) 98 | 99 | 100 | @adapter_logger 101 | def add_attachments_to_test(data, is_text: bool = False, name: str = None): 102 | if is_text and hasattr(TmsPluginManager.get_plugin_manager().hook, 'create_attachment'): 103 | TmsPluginManager.get_plugin_manager().hook \ 104 | .create_attachment( 105 | body=data, 106 | name=name) 107 | elif hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_attachments'): 108 | if isinstance(data, str): 109 | TmsPluginManager.get_plugin_manager().hook \ 110 | .add_attachments(attach_paths=[data]) 111 | elif isinstance(data, tuple) or isinstance(data, list): 112 | TmsPluginManager.get_plugin_manager().hook \ 113 | .add_attachments(attach_paths=data) 114 | else: 115 | logging.warning(f'({data}) is not path!') 116 | 117 | 118 | @adapter_logger 119 | def addWorkItemIds(*test_work_item_ids: int or str): # noqa: N802 120 | if not hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_work_item_id'): 121 | return 122 | 123 | for test_work_item_id in test_work_item_ids: 124 | TmsPluginManager.get_plugin_manager().hook.add_work_item_id(test_work_item_id=str(test_work_item_id)) 125 | 126 | 127 | @adapter_logger 128 | def addDisplayName(test_display_name: str): # noqa: N802 129 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_display_name'): 130 | TmsPluginManager.get_plugin_manager().hook \ 131 | .add_display_name(test_display_name=str(test_display_name)) 132 | 133 | 134 | @adapter_logger 135 | def addNameSpace(test_namespace: str): # noqa: N802 136 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_namespace'): 137 | TmsPluginManager.get_plugin_manager().hook \ 138 | .add_namespace(test_namespace=str(test_namespace)) 139 | 140 | 141 | @adapter_logger 142 | def addClassName(test_classname: str): # noqa: N802 143 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_classname'): 144 | TmsPluginManager.get_plugin_manager().hook \ 145 | .add_classname(test_classname=str(test_classname)) 146 | 147 | 148 | @adapter_logger 149 | def addExternalId(test_external_id: str): # noqa: N802 150 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_external_id'): 151 | TmsPluginManager.get_plugin_manager().hook \ 152 | .add_external_id(test_external_id=str(test_external_id)) 153 | 154 | 155 | @adapter_logger 156 | def addTitle(test_title: str): 157 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_title'): 158 | TmsPluginManager.get_plugin_manager().hook \ 159 | .add_title(test_title=str(test_title)) 160 | 161 | 162 | @adapter_logger 163 | def addDescription(test_description: str): 164 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_description'): 165 | TmsPluginManager.get_plugin_manager().hook \ 166 | .add_description(test_description=str(test_description)) 167 | 168 | 169 | @adapter_logger 170 | def addLabels(*test_labels: str): 171 | if not hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_label'): 172 | return 173 | 174 | for test_label in test_labels: 175 | TmsPluginManager.get_plugin_manager().hook.add_label(test_label=str(test_label)) 176 | 177 | @adapter_logger 178 | def addParameter(name: str, value: str): 179 | if hasattr(TmsPluginManager.get_plugin_manager().hook, 'add_parameter'): 180 | TmsPluginManager.get_plugin_manager().hook \ 181 | .add_parameter(name=str(name), value=str(value)) 182 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/__init__.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.models.link_type import LinkType 2 | 3 | __all__ = [ 4 | 'LinkType' 5 | ] 6 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/adapter_mode.py: -------------------------------------------------------------------------------- 1 | class AdapterMode: 2 | USE_FILTER = '0' 3 | RUN_ALL_TESTS = '1' 4 | NEW_TEST_RUN = '2' 5 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/fixture.py: -------------------------------------------------------------------------------- 1 | from attr import attrs, attrib 2 | from attr import Factory 3 | 4 | 5 | @attrs 6 | class FixtureResult: 7 | title = attrib(default=None) 8 | outcome = attrib(default=None) 9 | description = attrib(default=None) 10 | message = attrib(default=None) 11 | stacktrace = attrib(default=None) 12 | steps = attrib(default=None) 13 | attachments = attrib(default=Factory(list)) 14 | parameters = attrib(default=Factory(list)) 15 | start = attrib(default=None) 16 | stop = attrib(default=None) 17 | 18 | 19 | @attrs 20 | class FixturesContainer: 21 | uuid = attrib(default=None) 22 | external_ids = attrib(default=Factory(list)) 23 | befores = attrib(default=Factory(list)) 24 | afters = attrib(default=Factory(list)) 25 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/link.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.models.link_type import LinkType 2 | from testit_python_commons.services.logger import adapter_logger 3 | 4 | 5 | class Link: 6 | __url: str = None 7 | __title: str = None 8 | __link_type: LinkType = None 9 | __description: str = None 10 | 11 | @adapter_logger 12 | def set_url(self, url: str): 13 | self.__url = url 14 | 15 | return self 16 | 17 | @adapter_logger 18 | def get_url(self) -> str: 19 | return self.__url 20 | 21 | @adapter_logger 22 | def set_title(self, title: str): 23 | self.__title = title 24 | 25 | return self 26 | 27 | @adapter_logger 28 | def get_title(self) -> str: 29 | return self.__title 30 | 31 | @adapter_logger 32 | def set_link_type(self, link_type: LinkType): 33 | self.__link_type = link_type 34 | 35 | return self 36 | 37 | @adapter_logger 38 | def get_link_type(self) -> LinkType: 39 | return self.__link_type 40 | 41 | @adapter_logger 42 | def set_description(self, description: str): 43 | self.__description = description 44 | 45 | return self 46 | 47 | @adapter_logger 48 | def get_description(self) -> str: 49 | return self.__description 50 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/link_type.py: -------------------------------------------------------------------------------- 1 | class LinkType: 2 | RELATED = 'Related' 3 | BLOCKED_BY = 'BlockedBy' 4 | DEFECT = 'Defect' 5 | ISSUE = 'Issue' 6 | REQUIREMENT = 'Requirement' 7 | REPOSITORY = 'Repository' 8 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/outcome_type.py: -------------------------------------------------------------------------------- 1 | class OutcomeType: 2 | PASSED = 'Passed' 3 | FAILED = 'Failed' 4 | SKIPPED = 'Skipped' 5 | BLOCKED = 'Blocked' 6 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/step_result.py: -------------------------------------------------------------------------------- 1 | class StepResult: 2 | __title: str = None 3 | __outcome: str = None 4 | __description: str = None 5 | __duration: int = None 6 | __started_on: str = None 7 | __completed_on: str = None 8 | __step_results: list = None 9 | __attachments: list = None 10 | __parameters: dict = None 11 | 12 | def __init__(self): 13 | self.__step_results = [] 14 | self.__attachments = [] 15 | self.__parameters = {} 16 | 17 | def set_title(self, title: str): 18 | self.__title = title 19 | 20 | return self 21 | 22 | def get_title(self) -> str: 23 | return self.__title 24 | 25 | def set_outcome(self, outcome: str): 26 | self.__outcome = outcome 27 | 28 | return self 29 | 30 | def get_outcome(self) -> str: 31 | return self.__outcome 32 | 33 | def set_description(self, description: str): 34 | self.__description = description 35 | 36 | return self 37 | 38 | def get_description(self) -> str: 39 | return self.__description 40 | 41 | def set_duration(self, duration: int): 42 | self.__duration = duration 43 | 44 | return self 45 | 46 | def get_duration(self) -> int: 47 | return self.__duration 48 | 49 | def set_started_on(self, started_on: str): 50 | self.__started_on = started_on 51 | 52 | return self 53 | 54 | def get_started_on(self) -> str: 55 | return self.__started_on 56 | 57 | def set_completed_on(self, completed_on: str): 58 | self.__completed_on = completed_on 59 | 60 | return self 61 | 62 | def get_completed_on(self) -> str: 63 | return self.__completed_on 64 | 65 | def set_step_results(self, step_results: list): 66 | self.__step_results = step_results 67 | 68 | return self 69 | 70 | def get_step_results(self) -> list: 71 | return self.__step_results 72 | 73 | def set_attachments(self, attachments: list): 74 | self.__attachments = attachments 75 | 76 | return self 77 | 78 | def get_attachments(self) -> list: 79 | return self.__attachments 80 | 81 | def set_parameters(self, parameters: dict): 82 | self.__parameters = parameters 83 | 84 | return self 85 | 86 | def get_parameters(self) -> dict: 87 | return self.__parameters 88 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/test_result.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from testit_python_commons.models.link import Link 4 | from testit_python_commons.models.step_result import StepResult 5 | from testit_python_commons.services.logger import adapter_logger 6 | 7 | 8 | class TestResult: 9 | __external_id: str = None 10 | __autotest_name: str = None 11 | __outcome: str = None 12 | __title: str = None 13 | __description: str = None 14 | __duration: int = None 15 | __started_on: str = None 16 | __completed_on: str = None 17 | __namespace: str = None 18 | __classname: str = None 19 | __message: str = None 20 | __traces: str = None 21 | __step_results: typing.List[StepResult] = [] 22 | __setup_results: typing.List[StepResult] = [] 23 | __teardown_results: typing.List[StepResult] = [] 24 | __links: typing.List[Link] = [] 25 | __result_links: typing.List[Link] = [] 26 | __attachments: typing.List[str] = [] 27 | __labels: typing.List[str] = [] 28 | __work_item_ids: typing.List[str] = [] 29 | __parameters: dict = {} 30 | __properties: dict = {} 31 | __automatic_creation_test_cases: bool = False 32 | __external_key: str = None 33 | 34 | @adapter_logger 35 | def set_external_id(self, external_id: str): 36 | self.__external_id = external_id 37 | 38 | return self 39 | 40 | @adapter_logger 41 | def get_external_id(self) -> str: 42 | return self.__external_id 43 | 44 | @adapter_logger 45 | def set_autotest_name(self, autotest_name: str): 46 | self.__autotest_name = autotest_name 47 | 48 | return self 49 | 50 | @adapter_logger 51 | def get_autotest_name(self) -> str: 52 | return self.__autotest_name 53 | 54 | @adapter_logger 55 | def set_outcome(self, outcome: str): 56 | self.__outcome = outcome 57 | 58 | return self 59 | 60 | @adapter_logger 61 | def get_outcome(self) -> str: 62 | return self.__outcome 63 | 64 | @adapter_logger 65 | def set_title(self, title: str): 66 | self.__title = title 67 | 68 | return self 69 | 70 | @adapter_logger 71 | def get_title(self) -> str: 72 | return self.__title 73 | 74 | @adapter_logger 75 | def set_description(self, description: str): 76 | self.__description = description 77 | 78 | return self 79 | 80 | @adapter_logger 81 | def get_description(self) -> str: 82 | return self.__description 83 | 84 | @adapter_logger 85 | def set_duration(self, duration: int): 86 | self.__duration = duration 87 | 88 | return self 89 | 90 | @adapter_logger 91 | def get_duration(self) -> int: 92 | return self.__duration 93 | 94 | @adapter_logger 95 | def set_started_on(self, started_on: str): 96 | self.__started_on = started_on 97 | 98 | return self 99 | 100 | @adapter_logger 101 | def get_started_on(self) -> str: 102 | return self.__started_on 103 | 104 | @adapter_logger 105 | def set_completed_on(self, completed_on: str): 106 | self.__completed_on = completed_on 107 | 108 | return self 109 | 110 | @adapter_logger 111 | def get_completed_on(self) -> str: 112 | return self.__completed_on 113 | 114 | @adapter_logger 115 | def set_namespace(self, namespace: str): 116 | self.__namespace = namespace 117 | 118 | return self 119 | 120 | @adapter_logger 121 | def get_namespace(self) -> str: 122 | return self.__namespace 123 | 124 | @adapter_logger 125 | def set_classname(self, classname: str): 126 | self.__classname = classname 127 | 128 | return self 129 | 130 | @adapter_logger 131 | def get_classname(self) -> str: 132 | return self.__classname 133 | 134 | @adapter_logger 135 | def set_message(self, message: str): 136 | self.__message = message 137 | 138 | return self 139 | 140 | @adapter_logger 141 | def get_message(self) -> str: 142 | return self.__message 143 | 144 | @adapter_logger 145 | def set_traces(self, traces: str): 146 | self.__traces = traces 147 | 148 | return self 149 | 150 | @adapter_logger 151 | def get_traces(self) -> str: 152 | return self.__traces 153 | 154 | @adapter_logger 155 | def set_step_results(self, step_results: list): 156 | self.__step_results = step_results 157 | 158 | return self 159 | 160 | @adapter_logger 161 | def get_step_results(self) -> typing.List[StepResult]: 162 | return self.__step_results 163 | 164 | @adapter_logger 165 | def set_setup_results(self, setup_results: list): 166 | self.__setup_results = setup_results 167 | 168 | return self 169 | 170 | @adapter_logger 171 | def get_setup_results(self) -> typing.List[StepResult]: 172 | return self.__setup_results 173 | 174 | @adapter_logger 175 | def set_teardown_results(self, teardown_results: list): 176 | self.__teardown_results = teardown_results 177 | 178 | return self 179 | 180 | @adapter_logger 181 | def get_teardown_results(self) -> typing.List[StepResult]: 182 | return self.__teardown_results 183 | 184 | @adapter_logger 185 | def set_links(self, links: list): 186 | self.__links = links 187 | 188 | return self 189 | 190 | @adapter_logger 191 | def get_links(self) -> list: 192 | return self.__links 193 | 194 | @adapter_logger 195 | def set_result_links(self, result_links: list): 196 | self.__result_links = result_links 197 | 198 | return self 199 | 200 | @adapter_logger 201 | def get_result_links(self) -> list: 202 | return self.__result_links 203 | 204 | @adapter_logger 205 | def set_attachments(self, attachments: list): 206 | self.__attachments = attachments 207 | 208 | return self 209 | 210 | @adapter_logger 211 | def get_attachments(self) -> list: 212 | return self.__attachments 213 | 214 | @adapter_logger 215 | def set_labels(self, labels: list): 216 | self.__labels = labels 217 | 218 | return self 219 | 220 | @adapter_logger 221 | def get_labels(self) -> list: 222 | return self.__labels 223 | 224 | @adapter_logger 225 | def set_work_item_ids(self, work_item_ids: list): 226 | self.__work_item_ids = work_item_ids 227 | 228 | return self 229 | 230 | @adapter_logger 231 | def get_work_item_ids(self) -> list: 232 | return self.__work_item_ids 233 | 234 | @adapter_logger 235 | def set_parameters(self, parameters: dict): 236 | self.__parameters = parameters 237 | 238 | return self 239 | 240 | @adapter_logger 241 | def get_parameters(self) -> dict: 242 | return self.__parameters 243 | 244 | @adapter_logger 245 | def set_properties(self, properties: dict): 246 | self.__properties = properties 247 | 248 | return self 249 | 250 | @adapter_logger 251 | def get_properties(self) -> dict: 252 | return self.__properties 253 | 254 | @adapter_logger 255 | def set_automatic_creation_test_cases(self, automatic_creation_test_cases: bool): 256 | self.__automatic_creation_test_cases = automatic_creation_test_cases 257 | 258 | return self 259 | 260 | @adapter_logger 261 | def get_automatic_creation_test_cases(self) -> bool: 262 | return self.__automatic_creation_test_cases 263 | 264 | @adapter_logger 265 | def set_external_key(self, external_key: str): 266 | self.__external_key = external_key 267 | 268 | return self 269 | 270 | @adapter_logger 271 | def get_external_key(self) -> str: 272 | return self.__external_key 273 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/models/test_result_with_all_fixture_step_results_model.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from testit_python_commons.models.step_result import StepResult 4 | 5 | 6 | class TestResultWithAllFixtureStepResults: 7 | def __init__(self, test_result_id: str): 8 | self.__test_result_id = test_result_id 9 | self.__setup_results = [] 10 | self.__teardown_results = [] 11 | 12 | def get_test_result_id(self) -> str: 13 | return self.__test_result_id 14 | 15 | def set_setup_results(self, setup_results: typing.List[StepResult]): 16 | self.__setup_results += setup_results 17 | 18 | return self 19 | 20 | def get_setup_results(self) -> typing.List[StepResult]: 21 | return self.__setup_results 22 | 23 | def set_teardown_results(self, teardown_results: typing.List[StepResult]): 24 | self.__teardown_results = teardown_results + self.__teardown_results 25 | 26 | return self 27 | 28 | def get_teardown_results(self) -> typing.List[StepResult]: 29 | return self.__teardown_results 30 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/__init__.py: -------------------------------------------------------------------------------- 1 | from pluggy import HookimplMarker 2 | 3 | from testit_python_commons.services.adapter_manager import AdapterManager 4 | from testit_python_commons.services.plugin_manager import TmsPluginManager 5 | from testit_python_commons.services.fixture_manager import FixtureManager 6 | from testit_python_commons.services.step_manager import StepManager 7 | from testit_python_commons.services.utils import Utils 8 | 9 | hookimpl = HookimplMarker("testit") 10 | 11 | __all__ = [ 12 | 'AdapterManager', 13 | 'TmsPluginManager', 14 | 'FixtureManager', 15 | 'StepManager', 16 | 'Utils', 17 | 'hookimpl' 18 | ] 19 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/adapter_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | 4 | from testit_python_commons.client.api_client import ApiClientWorker 5 | from testit_python_commons.client.client_configuration import ClientConfiguration 6 | from testit_python_commons.models.adapter_mode import AdapterMode 7 | from testit_python_commons.models.test_result import TestResult 8 | from testit_python_commons.services.fixture_manager import FixtureManager 9 | from testit_python_commons.services.adapter_manager_configuration import AdapterManagerConfiguration 10 | from testit_python_commons.services.logger import adapter_logger 11 | from testit_python_commons.services.utils import Utils 12 | 13 | 14 | class AdapterManager: 15 | def __init__( 16 | self, 17 | adapter_configuration: AdapterManagerConfiguration, 18 | client_configuration: ClientConfiguration, 19 | fixture_manager: FixtureManager): 20 | self.__config = adapter_configuration 21 | self.__api_client = ApiClientWorker(client_configuration) 22 | self.__fixture_manager = fixture_manager 23 | self.__test_result_map = {} 24 | self.__test_results = [] 25 | 26 | @adapter_logger 27 | def set_test_run_id(self, test_run_id: str): 28 | self.__config.set_test_run_id(test_run_id) 29 | self.__api_client.set_test_run_id(test_run_id) 30 | 31 | @adapter_logger 32 | def get_test_run_id(self): 33 | if self.__config.get_mode() != AdapterMode.NEW_TEST_RUN: 34 | return self.__config.get_test_run_id() 35 | 36 | return self.__api_client.create_test_run() 37 | 38 | @adapter_logger 39 | def get_autotests_for_launch(self): 40 | if self.__config.get_mode() == AdapterMode.USE_FILTER: 41 | return self.__api_client.get_autotests_by_test_run_id() 42 | 43 | return 44 | 45 | @adapter_logger 46 | def write_test(self, test_result: TestResult): 47 | if self.__config.should_import_realtime(): 48 | self.__write_test_realtime(test_result) 49 | 50 | return 51 | 52 | self.__test_results.append(test_result) 53 | 54 | @adapter_logger 55 | def __write_test_realtime(self, test_result: TestResult): 56 | test_result.set_automatic_creation_test_cases( 57 | self.__config.should_automatic_creation_test_cases()) 58 | 59 | self.__test_result_map[test_result.get_external_id()] = self.__api_client.write_test(test_result) 60 | 61 | @adapter_logger 62 | def write_tests(self): 63 | if self.__config.should_import_realtime(): 64 | self.__load_setup_and_teardown_step_results() 65 | 66 | return 67 | 68 | self.__write_tests_after_all() 69 | 70 | @adapter_logger 71 | def __load_setup_and_teardown_step_results(self): 72 | self.__api_client.update_test_results(self.__fixture_manager.get_all_items(), self.__test_result_map) 73 | 74 | @adapter_logger 75 | def __write_tests_after_all(self): 76 | fixtures = self.__fixture_manager.get_all_items() 77 | 78 | self.__api_client.write_tests(self.__test_results, fixtures) 79 | 80 | @adapter_logger 81 | def load_attachments(self, attach_paths: list or tuple): 82 | return self.__api_client.load_attachments(attach_paths) 83 | 84 | @adapter_logger 85 | def create_attachment(self, body, name: str): 86 | if name is None: 87 | name = str(uuid.uuid4()) + '-attachment.txt' 88 | 89 | path = os.path.join(os.path.abspath(''), name) 90 | 91 | with open(path, 'wb') as attached_file: 92 | attached_file.write( 93 | Utils.convert_body_of_attachment(body)) 94 | 95 | attachment_id = self.__api_client.load_attachments((path,)) 96 | 97 | os.remove(path) 98 | 99 | return attachment_id 100 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/adapter_manager_configuration.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.models.adapter_mode import AdapterMode 2 | from testit_python_commons.services.logger import adapter_logger 3 | from testit_python_commons.services.utils import Utils 4 | 5 | 6 | class AdapterManagerConfiguration: 7 | __test_run_id = None 8 | 9 | def __init__(self, app_properties: dict): 10 | if app_properties.get('testrunid'): 11 | self.__test_run_id = Utils.uuid_check(app_properties.get('testrunid')) 12 | 13 | self.__adapter_mode = app_properties.get('adaptermode', AdapterMode.USE_FILTER) 14 | self.__automatic_creation_test_cases = Utils.convert_value_str_to_bool( 15 | app_properties.get('automaticcreationtestcases')) 16 | self.__import_realtime = Utils.convert_value_str_to_bool(app_properties.get('importrealtime')) 17 | 18 | @adapter_logger 19 | def get_test_run_id(self): 20 | return self.__test_run_id 21 | 22 | @adapter_logger 23 | def set_test_run_id(self, test_run_id: str): 24 | self.__test_run_id = test_run_id 25 | 26 | @adapter_logger 27 | def get_mode(self): 28 | return self.__adapter_mode 29 | 30 | @adapter_logger 31 | def should_automatic_creation_test_cases(self) -> bool: 32 | return self.__automatic_creation_test_cases 33 | 34 | @adapter_logger 35 | def should_import_realtime(self) -> bool: 36 | return self.__import_realtime 37 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/fixture_manager.py: -------------------------------------------------------------------------------- 1 | from testit_python_commons.services.fixture_storage import ThreadContextFixtures 2 | from testit_python_commons.models.step_result import StepResult 3 | 4 | 5 | class FixtureManager: 6 | def __init__(self): 7 | self._items = ThreadContextFixtures() 8 | self._orphan_items = [] 9 | 10 | def _update_item(self, uuid, **kwargs): 11 | item = self._items[uuid] if uuid else self._items[next(reversed(self._items))] 12 | for name, value in kwargs.items(): 13 | attr = getattr(item, name) 14 | if isinstance(attr, list): 15 | attr.append(value) 16 | else: 17 | setattr(item, name, value) 18 | 19 | def _last_executable(self): 20 | for _uuid in reversed(self._items): 21 | if isinstance(self._items[_uuid], StepResult): 22 | return _uuid 23 | 24 | def get_item(self, uuid): 25 | return self._items.get(uuid) 26 | 27 | def get_last_item(self, item_type=None): 28 | for _uuid in reversed(self._items): 29 | if item_type is None: 30 | return self._items.get(_uuid) 31 | if type(self._items[_uuid]) == item_type: 32 | return self._items.get(_uuid) 33 | 34 | def start_group(self, uuid, group): 35 | self._items[uuid] = group 36 | 37 | def stop_group(self, uuid, **kwargs): 38 | self._update_item(uuid, **kwargs) 39 | 40 | def update_group(self, uuid, **kwargs): 41 | self._update_item(uuid, **kwargs) 42 | 43 | def start_before_fixture(self, parent_uuid, uuid, fixture): 44 | self._items.get(parent_uuid).befores.append(fixture) 45 | self._items[uuid] = fixture 46 | 47 | def stop_before_fixture(self, uuid, **kwargs): 48 | self._update_item(uuid, **kwargs) 49 | self._items.pop(uuid) 50 | 51 | def start_after_fixture(self, parent_uuid, uuid, fixture): 52 | self._items.get(parent_uuid).afters.append(fixture) 53 | self._items[uuid] = fixture 54 | 55 | def stop_after_fixture(self, uuid, **kwargs): 56 | self._update_item(uuid, **kwargs) 57 | self._items.pop(uuid) 58 | 59 | def get_all_items(self): 60 | return self._items.get_all() 61 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/fixture_storage.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from collections import OrderedDict, defaultdict 3 | 4 | 5 | class ThreadContextFixtures: 6 | _thread_context = defaultdict(OrderedDict) 7 | _init_thread: threading.Thread 8 | 9 | @property 10 | def thread_context(self): 11 | context = self._thread_context[threading.current_thread()] 12 | if not context and threading.current_thread() is not self._init_thread: 13 | uuid, last_item = next(reversed(self._thread_context[self._init_thread].items())) 14 | context[uuid] = last_item 15 | return context 16 | 17 | def __init__(self, *args, **kwargs): 18 | self._init_thread = threading.current_thread() 19 | super().__init__(*args, **kwargs) 20 | 21 | def __setitem__(self, key, value): 22 | self.thread_context.__setitem__(key, value) 23 | 24 | def __getitem__(self, item): 25 | return self.thread_context.__getitem__(item) 26 | 27 | def __iter__(self): 28 | return self.thread_context.__iter__() 29 | 30 | def __reversed__(self): 31 | return self.thread_context.__reversed__() 32 | 33 | def get(self, key): 34 | return self.thread_context.get(key) 35 | 36 | def pop(self, key): 37 | return self.thread_context.pop(key) 38 | 39 | def get_all(self): 40 | return dict(self.thread_context.items()) 41 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/logger.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from testit_python_commons.services.plugin_manager import TmsPluginManager 4 | 5 | 6 | def adapter_logger(function): 7 | @wraps(function) 8 | def wrapper(*args, **kwargs): 9 | from testit_python_commons.services.utils import Utils 10 | 11 | logger = TmsPluginManager.get_logger() 12 | 13 | parameters = Utils.get_function_parameters(function, *args, **kwargs) 14 | 15 | message = f'Method "{function.__name__}" started' 16 | 17 | if parameters: 18 | message += f' with parameters: {parameters}' 19 | 20 | logger.debug(message) 21 | 22 | result = function(*args, **kwargs) 23 | 24 | message = f'Method "{function.__name__}" finished' 25 | 26 | if result is not None: 27 | message += f' with result: {result}' 28 | 29 | logger.debug(message) 30 | 31 | return result 32 | return wrapper 33 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pluggy import PluginManager 4 | 5 | from testit_python_commons.services.step_manager import StepManager 6 | from testit_python_commons.app_properties import AppProperties 7 | 8 | 9 | class TmsPluginManager: 10 | __plugin_manager = None 11 | __adapter_manager = None 12 | __fixture_manager = None 13 | __step_manager = None 14 | __logger = None 15 | 16 | @classmethod 17 | def get_plugin_manager(cls): 18 | if cls.__plugin_manager is None: 19 | cls.__plugin_manager = PluginManager('testit') 20 | 21 | return cls.__plugin_manager 22 | 23 | @classmethod 24 | def get_adapter_manager(cls, option=None): 25 | if cls.__adapter_manager is None: 26 | from testit_python_commons.services.adapter_manager import AdapterManager 27 | from testit_python_commons.client.client_configuration import ClientConfiguration 28 | from testit_python_commons.services.adapter_manager_configuration import AdapterManagerConfiguration 29 | 30 | app_properties = AppProperties.load_properties(option) 31 | 32 | cls.get_logger(app_properties.get('logs') == 'true') 33 | 34 | client_configuration = ClientConfiguration(app_properties) 35 | adapter_configuration = AdapterManagerConfiguration(app_properties) 36 | fixture_manager = cls.get_fixture_manager() 37 | 38 | cls.__adapter_manager = AdapterManager(adapter_configuration, client_configuration, fixture_manager) 39 | 40 | return cls.__adapter_manager 41 | 42 | @classmethod 43 | def get_fixture_manager(cls): 44 | if cls.__fixture_manager is None: 45 | from testit_python_commons.services.fixture_manager import FixtureManager 46 | 47 | cls.__fixture_manager = FixtureManager() 48 | 49 | return cls.__fixture_manager 50 | 51 | @classmethod 52 | def get_step_manager(cls) -> StepManager: 53 | if cls.__step_manager is None: 54 | cls.__step_manager = StepManager() 55 | 56 | return cls.__step_manager 57 | 58 | @classmethod 59 | def get_logger(cls, debug: bool = False): 60 | if cls.__logger is None: 61 | if debug: 62 | logging.basicConfig(format='\n%(levelname)s (%(asctime)s): %(message)s', level=logging.DEBUG) 63 | 64 | cls.__logger = logging.getLogger('TmsLogger') 65 | 66 | return cls.__logger 67 | 68 | @classmethod 69 | def __getattr__(cls, attribute): 70 | return getattr(cls.get_plugin_manager(), attribute) 71 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/retry.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import time 4 | 5 | import testit_api_client 6 | 7 | 8 | def retry(func): 9 | def retry_wrapper(*args, **kwargs): 10 | attempts = 0 11 | retries = 10 12 | 13 | while attempts < retries: 14 | try: 15 | return func(*args, **kwargs) 16 | except testit_api_client.exceptions.ApiException as e: 17 | sleep_time = random.randrange(0, 100) 18 | time.sleep(sleep_time/100) 19 | attempts += 1 20 | 21 | logging.error(e) 22 | 23 | return retry_wrapper 24 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/step_manager.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from testit_python_commons.models.step_result import StepResult 4 | from testit_python_commons.services.step_result_storage import StepResultStorage 5 | 6 | 7 | class StepManager: 8 | __steps_tree: typing.List[StepResult] = [] 9 | 10 | def __init__(self): 11 | self.__storage = StepResultStorage() 12 | 13 | def start_step(self, step: StepResult): 14 | if self.__storage.get_count(): 15 | parent_step: StepResult = self.__storage.get_last() 16 | 17 | step_results_from_parent_step = parent_step.get_step_results() 18 | step_results_from_parent_step.append(step) 19 | parent_step.set_step_results(step_results_from_parent_step) 20 | 21 | self.__storage.add(step) 22 | 23 | def stop_step(self): 24 | if self.__storage.get_count() == 1: 25 | self.__steps_tree.append(self.__storage.get_last()) 26 | 27 | self.__storage.remove_last() 28 | 29 | def get_active_step(self) -> StepResult: 30 | return self.__storage.get_last() 31 | 32 | def get_steps_tree(self) -> typing.List[StepResult]: 33 | steps_tree = self.__steps_tree.copy() 34 | self.__steps_tree.clear() 35 | 36 | return steps_tree 37 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/step_result_storage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from testit_python_commons.models.step_result import StepResult 5 | 6 | 7 | class StepResultStorage: 8 | __storage: typing.List[StepResult] = [] 9 | 10 | def add(self, step_result: StepResult): 11 | self.__storage.append(step_result) 12 | 13 | def get_last(self): 14 | if not self.__storage: 15 | return 16 | 17 | return self.__storage[-1] 18 | 19 | def remove_last(self): 20 | try: 21 | self.__storage.pop() 22 | except Exception as exc: 23 | logging.error(f'Cannot remove last step from storage. Storage is empty. {exc}') 24 | 25 | def get_count(self) -> int: 26 | return len(self.__storage) 27 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/services/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import re 4 | import warnings 5 | 6 | from testit_python_commons.models.link import Link 7 | from testit_python_commons.services.logger import adapter_logger 8 | 9 | 10 | class Utils: 11 | @staticmethod 12 | def uuid_check(uuid: str): 13 | if not re.fullmatch(r'[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}', uuid): 14 | logging.error(f'The wrong {uuid}!') 15 | raise SystemExit 16 | 17 | return uuid 18 | 19 | @staticmethod 20 | def url_check(url: str): 21 | if not re.fullmatch( 22 | r"^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)" 23 | r"([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$", 24 | url): 25 | logging.error('The wrong URL!') 26 | raise SystemExit 27 | 28 | if url[-1] == '/': 29 | return url[:-1] 30 | 31 | return url 32 | 33 | @staticmethod 34 | def deprecated(message): 35 | def deprecated_decorator(func): # noqa: N802 36 | def deprecated_func(*args, **kwargs): 37 | warnings.warn( 38 | '"{}" is no longer acceptable to compute time between versions.\n{}'.format(func.__name__, message), 39 | category=DeprecationWarning, 40 | stacklevel=2) 41 | warnings.simplefilter('default', DeprecationWarning) 42 | return func(*args, **kwargs) 43 | 44 | return deprecated_func 45 | 46 | return deprecated_decorator 47 | 48 | @staticmethod 49 | def get_function_parameters(function, *args, **kwargs): 50 | parameters = {} 51 | args_default_values = inspect.getfullargspec(function).defaults 52 | 53 | if args or args_default_values: 54 | all_keys = inspect.getfullargspec(function).args 55 | all_args = list(args) 56 | 57 | if args_default_values: 58 | all_args += list(args_default_values[len(args) - (len(all_keys) - len(args_default_values)):]) 59 | 60 | method_args = [arg_name for arg_name in all_keys if arg_name not in list(kwargs)] 61 | 62 | if len(method_args) == len(all_args): 63 | for index in range(0, len(method_args)): 64 | parameters[method_args[index]] = str(all_args[index]) 65 | 66 | if kwargs: 67 | for key, parameter in kwargs.items(): 68 | parameters[key] = str(parameter) 69 | 70 | return parameters 71 | 72 | @staticmethod 73 | def exclude_self_parameter(all_parameters: dict) -> dict: 74 | if all_parameters.get('self') is not None: 75 | all_parameters.pop('self') 76 | return all_parameters 77 | 78 | @staticmethod 79 | def collect_parameters_in_string_attribute(attribute: str, all_parameters: dict) -> str: 80 | param_keys = [] 81 | 82 | if attribute: 83 | param_keys = re.findall(r"\{(.*?)\}", attribute) 84 | 85 | if len(param_keys) > 0: 86 | for param_key in param_keys: 87 | parameter = all_parameters.get(param_key) 88 | 89 | if parameter is not None: 90 | attribute = attribute.replace("{" + param_key + "}", str(parameter)) 91 | 92 | return attribute 93 | 94 | @staticmethod 95 | @adapter_logger 96 | def convert_link_dict_to_link_model(link_dict: dict) -> Link: 97 | link_model = Link() 98 | link_model.set_url(link_dict['url']) 99 | 100 | if 'title' in link_dict: 101 | link_model.set_title(link_dict['title']) 102 | 103 | if 'type' in link_dict: 104 | link_model.set_link_type(link_dict['type']) 105 | 106 | if 'description' in link_dict: 107 | link_model.set_description(link_dict['description']) 108 | 109 | return link_model 110 | 111 | @staticmethod 112 | @adapter_logger 113 | def convert_body_of_attachment(body): 114 | if isinstance(body, bytes): 115 | return body 116 | 117 | return str(body).encode('utf-8') 118 | 119 | @staticmethod 120 | @adapter_logger 121 | def convert_value_str_to_bool(value: str) -> bool: 122 | if value: 123 | return value == 'true' 124 | 125 | return False 126 | -------------------------------------------------------------------------------- /testit-python-commons/src/testit_python_commons/step.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | from functools import wraps 4 | from typing import Any, Callable, TypeVar 5 | 6 | from testit_python_commons.models.step_result import StepResult 7 | from testit_python_commons.services import ( 8 | TmsPluginManager, 9 | Utils 10 | ) 11 | 12 | 13 | Func = TypeVar("Func", bound=Callable[..., Any]) 14 | 15 | 16 | def step(*args, **kwargs): 17 | if callable(args[0]): 18 | function = args[0] 19 | return StepContext(function.__name__, None, {})(function) 20 | else: 21 | title = get_title(args, kwargs) 22 | description = get_description(args, kwargs) 23 | 24 | return StepContext(title, description, {}) 25 | 26 | 27 | def get_title(args: tuple, kwargs: dict): 28 | if 'title' in kwargs: 29 | return kwargs['title'] 30 | 31 | if len(args) > 0: 32 | if isinstance(args[0], str): 33 | return args[0] 34 | 35 | logging.error(f'Cannot to get step title: {args[1]}. The title must be of string type.') 36 | 37 | 38 | def get_description(args: tuple, kwargs: dict): 39 | if 'description' in kwargs: 40 | return kwargs['description'] 41 | 42 | if len(args) > 1: 43 | if isinstance(args[1], str): 44 | return args[1] 45 | logging.error(f'Cannot to get step description: {args[1]}. The description must be of string type.') 46 | 47 | 48 | class StepContext: 49 | def __init__(self, title, description, parameters): 50 | self.__title = title 51 | self.__description = description 52 | self.__parameters = parameters 53 | 54 | def __enter__(self): 55 | self.__start_time = round(datetime.utcnow().timestamp() * 1000) 56 | self.__step_result = StepResult() 57 | 58 | self.__title = Utils.collect_parameters_in_string_attribute(self.__title, self.__parameters) 59 | self.__description = Utils.collect_parameters_in_string_attribute(self.__description, self.__parameters) 60 | 61 | self.__step_result\ 62 | .set_title(self.__title)\ 63 | .set_description(self.__description)\ 64 | .set_parameters( 65 | Utils.exclude_self_parameter(self.__parameters) 66 | ) 67 | 68 | logging.debug(f'Step "{self.__title}" was started') 69 | 70 | TmsPluginManager.get_step_manager().start_step(self.__step_result) 71 | 72 | def __exit__(self, exc_type, exc_val, exc_tb): 73 | outcome = 'Failed' if exc_type \ 74 | else TmsPluginManager.get_plugin_manager().hook.get_pytest_check_outcome()[0] if \ 75 | hasattr(TmsPluginManager.get_plugin_manager().hook, 'get_pytest_check_outcome') \ 76 | else 'Passed' 77 | duration = round(datetime.utcnow().timestamp() * 1000) - self.__start_time 78 | 79 | self.__step_result\ 80 | .set_outcome(outcome)\ 81 | .set_duration(duration) 82 | 83 | TmsPluginManager.get_step_manager().stop_step() 84 | 85 | def __call__(self, function: Func) -> Func: 86 | @wraps(function) 87 | def impl(*args, **kwargs): 88 | __tracebackhide__ = True 89 | parameters = Utils.get_function_parameters(function, *args, **kwargs) 90 | 91 | title = self.__title if self.__title else function.__name__ 92 | 93 | with StepContext(title, self.__description, parameters): 94 | return function(*args, **kwargs) 95 | 96 | return impl 97 | -------------------------------------------------------------------------------- /testit-python-commons/tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testit-tms/adapters-python/50f78210918a3213cc5b82c3aef8ac3532a46189/testit-python-commons/tests/conftest.py -------------------------------------------------------------------------------- /testit-python-commons/tests/services/test_adapter_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import uuid 3 | 4 | from testit_python_commons.services.adapter_manager import AdapterManager 5 | from testit_python_commons.services.adapter_manager_configuration import AdapterManagerConfiguration 6 | from testit_python_commons.client.client_configuration import ClientConfiguration 7 | from testit_python_commons.services.fixture_manager import FixtureManager 8 | from testit_python_commons.models.adapter_mode import AdapterMode 9 | 10 | class TestAdapterManager: 11 | @pytest.fixture 12 | def mock_adapter_config(self, mocker): 13 | return mocker.create_autospec(AdapterManagerConfiguration) 14 | 15 | @pytest.fixture 16 | def mock_client_config(self, mocker): 17 | return mocker.create_autospec(ClientConfiguration) 18 | 19 | @pytest.fixture 20 | def mock_fixture_manager(self, mocker): 21 | return mocker.create_autospec(FixtureManager) 22 | 23 | @pytest.fixture 24 | def mock_api_client_worker(self, mocker, mock_client_config): 25 | mock = mocker.patch( 26 | 'testit_python_commons.services.adapter_manager.ApiClientWorker', 27 | autospec=True) 28 | return mock.return_value 29 | 30 | @pytest.fixture 31 | def adapter_manager(self, mock_adapter_config, mock_client_config, mock_fixture_manager, mock_api_client_worker): 32 | manager = AdapterManager( 33 | adapter_configuration=mock_adapter_config, 34 | client_configuration=mock_client_config, 35 | fixture_manager=mock_fixture_manager 36 | ) 37 | manager._AdapterManager__api_client = mock_api_client_worker 38 | return manager 39 | 40 | def test_set_test_run_id(self, adapter_manager, mock_adapter_config, mock_api_client_worker): 41 | test_run_id = str(uuid.uuid4()) 42 | adapter_manager.set_test_run_id(test_run_id) 43 | mock_adapter_config.set_test_run_id.assert_called_once_with(test_run_id) 44 | mock_api_client_worker.set_test_run_id.assert_called_once_with(test_run_id) 45 | 46 | def test_get_test_run_id_new_test_run_mode(self, adapter_manager, mock_adapter_config, mock_api_client_worker): 47 | test_run_id = str(uuid.uuid4()) 48 | mock_adapter_config.get_mode.return_value = AdapterMode.NEW_TEST_RUN 49 | mock_api_client_worker.create_test_run.return_value = test_run_id 50 | 51 | test_run_result_id = adapter_manager.get_test_run_id() 52 | 53 | assert test_run_id == test_run_result_id 54 | mock_adapter_config.get_mode.assert_called_once() 55 | mock_api_client_worker.create_test_run.assert_called_once() 56 | mock_adapter_config.get_test_run_id.assert_not_called() 57 | 58 | def test_get_autotests_for_launch_use_filter_mode(self, adapter_manager, mock_adapter_config, mock_api_client_worker): 59 | mock_adapter_config.get_mode.return_value = AdapterMode.USE_FILTER 60 | expected_autotests = [str(uuid.uuid4()), str(uuid.uuid4())] 61 | mock_api_client_worker.get_autotests_by_test_run_id.return_value = expected_autotests 62 | 63 | autotests = adapter_manager.get_autotests_for_launch() 64 | 65 | assert autotests == expected_autotests 66 | mock_adapter_config.get_mode.assert_called_once() 67 | mock_api_client_worker.get_autotests_by_test_run_id.assert_called_once() 68 | 69 | def test_write_tests_realtime_import_enabled(self, adapter_manager, mock_adapter_config, 70 | mock_api_client_worker, mock_fixture_manager): 71 | mock_adapter_config.should_import_realtime.return_value = True 72 | all_fixture_items = ["setup1", "teardown1"] 73 | mock_fixture_manager.get_all_items.return_value = all_fixture_items 74 | adapter_manager._AdapterManager__test_result_map = {"test1": "result1"} 75 | 76 | adapter_manager.write_tests() 77 | 78 | mock_adapter_config.should_import_realtime.assert_called_once() 79 | mock_fixture_manager.get_all_items.assert_called_once() 80 | mock_api_client_worker.update_test_results.assert_called_once_with( 81 | all_fixture_items, {"test1": "result1"} 82 | ) 83 | mock_api_client_worker.write_tests.assert_not_called() 84 | 85 | def test_load_attachments(self, adapter_manager, mock_api_client_worker): 86 | attach_paths = ["path/to/attachment1.txt", "path/to/attachment2.jpg"] 87 | expected_result = ["id1", "id2"] 88 | mock_api_client_worker.load_attachments.return_value = expected_result 89 | 90 | result = adapter_manager.load_attachments(attach_paths) 91 | 92 | assert result == expected_result 93 | mock_api_client_worker.load_attachments.assert_called_once_with(attach_paths) 94 | 95 | def test_create_attachment_with_name(self, adapter_manager, mock_api_client_worker, mocker): 96 | mock_os_path_join = mocker.patch("os.path.join") 97 | mock_os_path_abspath = mocker.patch("os.path.abspath", return_value="/abs/path") 98 | mock_open = mocker.patch("builtins.open", mocker.mock_open()) 99 | mock_os_remove = mocker.patch("os.remove") 100 | mock_utils_convert = mocker.patch( 101 | "testit_python_commons.services.adapter_manager.Utils.convert_body_of_attachment", 102 | return_value=b"converted_body" 103 | ) 104 | 105 | file_body = "Hello Attachment" 106 | file_name = "custom_attachment.txt" 107 | expected_file_path = "/abs/path/custom_name.txt" 108 | mock_os_path_join.return_value = expected_file_path 109 | expected_attachment_id = str(uuid.uuid4()) 110 | mock_api_client_worker.load_attachments.return_value = expected_attachment_id 111 | 112 | attachment_id = adapter_manager.create_attachment(file_body, file_name) 113 | 114 | mock_os_path_abspath.assert_called_once_with('') 115 | mock_os_path_join.assert_called_once_with("/abs/path", file_name) 116 | mock_open.assert_called_once_with(expected_file_path, 'wb') 117 | mock_open().write.assert_called_once_with(b"converted_body") 118 | mock_utils_convert.assert_called_once_with(file_body) 119 | mock_api_client_worker.load_attachments.assert_called_once_with((expected_file_path,)) 120 | mock_os_remove.assert_called_once_with(expected_file_path) 121 | assert attachment_id == expected_attachment_id -------------------------------------------------------------------------------- /testit-python-commons/tests/services/test_fixture_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import uuid 3 | 4 | from testit_python_commons.services.fixture_manager import FixtureManager 5 | from testit_python_commons.models.step_result import StepResult 6 | 7 | class TestFixtureManager: 8 | @pytest.fixture 9 | def fixture_manager(self): 10 | manager = FixtureManager() 11 | manager._items.thread_context.clear() 12 | return manager 13 | 14 | @pytest.fixture 15 | def mock_fixture(self, mocker): 16 | fixture = mocker.MagicMock() 17 | return fixture 18 | 19 | def test_get_item_existing(self, fixture_manager, mock_fixture): 20 | test_uuid = str(uuid.uuid4()) 21 | fixture_manager._items[test_uuid] = mock_fixture 22 | 23 | result = fixture_manager.get_item(test_uuid) 24 | assert result == mock_fixture 25 | 26 | def test_get_last_item_no_type(self, fixture_manager, mock_fixture, mocker): 27 | uuid1 = str(uuid.uuid4()) 28 | uuid2 = str(uuid.uuid4()) 29 | fixture_manager._items[uuid1] = mocker.MagicMock() 30 | fixture_manager._items[uuid2] = mock_fixture 31 | 32 | result = fixture_manager.get_last_item() 33 | assert result == mock_fixture 34 | 35 | def test_get_last_item_with_type(self, fixture_manager, mocker): 36 | uuid1 = str(uuid.uuid4()) 37 | uuid2 = str(uuid.uuid4()) 38 | uuid3 = str(uuid.uuid4()) 39 | 40 | step_result = StepResult() 41 | fixture_manager._items[uuid1] = mocker.MagicMock() 42 | fixture_manager._items[uuid2] = step_result 43 | fixture_manager._items[uuid3] = mocker.MagicMock() 44 | 45 | result = fixture_manager.get_last_item(StepResult) 46 | assert result == step_result 47 | 48 | def test_start_group(self, fixture_manager, mock_fixture): 49 | test_uuid = str(uuid.uuid4()) 50 | fixture_manager.start_group(test_uuid, mock_fixture) 51 | assert fixture_manager._items[test_uuid] == mock_fixture 52 | 53 | def test_stop_group(self, fixture_manager, mock_fixture): 54 | test_uuid = str(uuid.uuid4()) 55 | fixture_manager._items[test_uuid] = mock_fixture 56 | fixture_manager.stop_group(test_uuid, status="completed", duration=100) 57 | 58 | assert mock_fixture.status == "completed" 59 | assert mock_fixture.duration == 100 60 | 61 | def test_update_group(self, fixture_manager, mock_fixture): 62 | test_uuid = str(uuid.uuid4()) 63 | fixture_manager._items[test_uuid] = mock_fixture 64 | fixture_manager.update_group(test_uuid, name="UpdatedName") 65 | 66 | assert mock_fixture.name == "UpdatedName" 67 | 68 | def test_start_before_fixture(self, fixture_manager, mock_fixture, mocker): 69 | parent_uuid = str(uuid.uuid4()) 70 | fixture_uuid = str(uuid.uuid4()) 71 | 72 | mock_group = mocker.MagicMock() 73 | mock_group.befores = [] 74 | fixture_manager._items[parent_uuid] = mock_group 75 | 76 | fixture_manager.start_before_fixture(parent_uuid, fixture_uuid, mock_fixture) 77 | assert mock_fixture in mock_group.befores 78 | assert fixture_manager._items[fixture_uuid] == mock_fixture 79 | 80 | def test_stop_before_fixture(self, fixture_manager, mock_fixture): 81 | fixture_uuid = str(uuid.uuid4()) 82 | fixture_manager._items[fixture_uuid] = mock_fixture 83 | 84 | fixture_manager.stop_before_fixture(fixture_uuid, status="passed") 85 | 86 | assert mock_fixture.status == "passed" 87 | assert fixture_uuid not in fixture_manager._items 88 | 89 | def test_start_after_fixture(self, fixture_manager, mock_fixture, mocker): 90 | parent_uuid = str(uuid.uuid4()) 91 | fixture_uuid = str(uuid.uuid4()) 92 | 93 | mock_group = mocker.MagicMock() 94 | mock_group.afters = [] 95 | fixture_manager._items[parent_uuid] = mock_group 96 | 97 | fixture_manager.start_after_fixture(parent_uuid, fixture_uuid, mock_fixture) 98 | 99 | assert mock_fixture in mock_group.afters 100 | assert fixture_manager._items[fixture_uuid] == mock_fixture 101 | 102 | def test_stop_after_fixture(self, fixture_manager, mock_fixture): 103 | fixture_uuid = str(uuid.uuid4()) 104 | fixture_manager._items[fixture_uuid] = mock_fixture 105 | 106 | fixture_manager.stop_after_fixture(fixture_uuid, status="failed", error="Test error") 107 | 108 | assert mock_fixture.status == "failed" 109 | assert mock_fixture.error == "Test error" 110 | assert fixture_uuid not in fixture_manager._items 111 | 112 | def test_get_all_items(self, fixture_manager, mocker): 113 | expected_items = { 114 | str(uuid.uuid4()): mocker.MagicMock(), 115 | str(uuid.uuid4()): mocker.MagicMock() 116 | } 117 | 118 | for key, value in expected_items.items(): 119 | fixture_manager._items[key] = value 120 | 121 | result = fixture_manager.get_all_items() 122 | 123 | assert result == expected_items -------------------------------------------------------------------------------- /testit-python-commons/tests/services/test_plugin_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import logging 3 | 4 | from testit_python_commons.services.plugin_manager import TmsPluginManager 5 | from testit_python_commons.services.fixture_manager import FixtureManager 6 | 7 | @pytest.fixture(autouse=True) 8 | def reset_tms_plugin_manager_singletons(): 9 | TmsPluginManager._TmsPluginManager__plugin_manager = None 10 | TmsPluginManager._TmsPluginManager__adapter_manager = None 11 | TmsPluginManager._TmsPluginManager__fixture_manager = None 12 | TmsPluginManager._TmsPluginManager__step_manager = None 13 | TmsPluginManager._TmsPluginManager__logger = None 14 | 15 | class TestTmsPluginManager: 16 | def test_get_plugin_manager_creates_instance(self): 17 | from pluggy import PluginManager 18 | pm = TmsPluginManager.get_plugin_manager() 19 | assert pm is not None 20 | assert isinstance(pm, PluginManager) 21 | assert pm.project_name == 'testit' 22 | 23 | def test_get_fixture_manager_creates_instance(self): 24 | fm = TmsPluginManager.get_fixture_manager() 25 | assert fm is not None 26 | assert isinstance(fm, FixtureManager) 27 | 28 | def test_get_step_manager_creates_instance(self): 29 | from testit_python_commons.services.step_manager import StepManager 30 | sm = TmsPluginManager.get_step_manager() 31 | assert sm is not None 32 | assert isinstance(sm, StepManager) 33 | 34 | def test_get_logger_creates_instance(self): 35 | logger = TmsPluginManager.get_logger() 36 | assert logger is not None 37 | assert isinstance(logger, logging.Logger) 38 | assert logger.name == 'TmsLogger' 39 | 40 | def test_get_adapter_manager_creates_instance(self, mocker): 41 | mocks = self._setup_adapter_manager_dependencies_mocks(mocker, logs_enabled_str='false') 42 | 43 | test_option = "test_config_option" 44 | adapter_manager_instance = TmsPluginManager.get_adapter_manager(option=test_option) 45 | 46 | assert adapter_manager_instance is mocks['adapter_manager_ctor'].return_value 47 | assert TmsPluginManager._TmsPluginManager__adapter_manager is mocks['adapter_manager_ctor'].return_value 48 | 49 | mocks['load_properties'].assert_called_once_with(test_option) 50 | mocks['app_properties_instance'].get.assert_called_once_with('logs') 51 | mocks['get_logger'].assert_called_once_with(False) 52 | 53 | mocks['client_configuration_ctor'].assert_called_once_with(mocks['app_properties_instance']) 54 | mocks['adapter_manager_configuration_ctor'].assert_called_once_with(mocks['app_properties_instance']) 55 | 56 | mocks['adapter_manager_ctor'].assert_called_once_with( 57 | mocks['adapter_manager_configuration_ctor'].return_value, 58 | mocks['client_configuration_ctor'].return_value, 59 | mocks['fixture_manager_instance'] 60 | ) 61 | 62 | def _setup_adapter_manager_dependencies_mocks(self, mocker, logs_enabled_str: str = 'false'): 63 | mock_app_properties_instance = mocker.MagicMock() 64 | mock_app_properties_instance.get.return_value = logs_enabled_str 65 | 66 | mock_load_properties = mocker.patch( 67 | 'testit_python_commons.app_properties.AppProperties.load_properties', 68 | return_value=mock_app_properties_instance) 69 | 70 | mock_get_logger = mocker.patch.object(TmsPluginManager, 'get_logger') 71 | 72 | mock_fixture_manager_instance = mocker.MagicMock(spec=FixtureManager) 73 | mocker.patch.object(TmsPluginManager, 'get_fixture_manager', return_value=mock_fixture_manager_instance) 74 | 75 | mock_client_configuration_ctor = mocker.patch( 76 | 'testit_python_commons.client.client_configuration.ClientConfiguration') 77 | mock_adapter_manager_configuration_ctor = mocker.patch( 78 | 'testit_python_commons.services.adapter_manager_configuration.AdapterManagerConfiguration') 79 | mock_adapter_manager_ctor = mocker.patch('testit_python_commons.services.adapter_manager.AdapterManager') 80 | 81 | return { 82 | 'load_properties': mock_load_properties, 83 | 'app_properties_instance': mock_app_properties_instance, 84 | 'get_logger': mock_get_logger, 85 | 'fixture_manager_instance': mock_fixture_manager_instance, 86 | 'client_configuration_ctor': mock_client_configuration_ctor, 87 | 'adapter_manager_configuration_ctor': mock_adapter_manager_configuration_ctor, 88 | 'adapter_manager_ctor': mock_adapter_manager_ctor 89 | } -------------------------------------------------------------------------------- /testit-python-commons/tests/services/test_step_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from testit_python_commons.services.step_manager import StepManager 4 | from testit_python_commons.models.step_result import StepResult 5 | from testit_python_commons.services.step_result_storage import StepResultStorage 6 | 7 | class TestStepManager: 8 | @pytest.fixture 9 | def step_manager(self, mock_step_result_storage): 10 | return StepManager() 11 | 12 | @pytest.fixture 13 | def mock_step_result(self, mocker): 14 | step = mocker.Mock(spec=StepResult) 15 | step.get_step_results.return_value = [] 16 | step.set_step_results.return_value = step 17 | return step 18 | 19 | @pytest.fixture 20 | def mock_step_result_storage(self, mocker): 21 | mock_storage = mocker.Mock(spec=StepResultStorage) 22 | mocker.patch('testit_python_commons.services.step_manager.StepResultStorage', return_value=mock_storage) 23 | return mock_storage 24 | 25 | def test_start_step_with_empty_storage(self, step_manager, mock_step_result, mock_step_result_storage): 26 | mock_step_result_storage.get_count.return_value = 0 27 | step_manager.start_step(mock_step_result) 28 | mock_step_result_storage.get_count.assert_called_once() 29 | mock_step_result_storage.add.assert_called_once_with(mock_step_result) 30 | mock_step_result.get_step_results.assert_not_called() 31 | 32 | def test_stop_step_when_not_last_step(self, step_manager, mock_step_result_storage): 33 | mock_step_result_storage.get_count.return_value = 2 34 | step_manager.stop_step() 35 | mock_step_result_storage.get_count.assert_called_once() 36 | mock_step_result_storage.remove_last.assert_called_once() 37 | mock_step_result_storage.get_last.assert_not_called() 38 | 39 | def test_get_active_step_returns_last_step(self, step_manager, mock_step_result, mock_step_result_storage): 40 | mock_step_result_storage.get_last.return_value = mock_step_result 41 | result = step_manager.get_active_step() 42 | mock_step_result_storage.get_last.assert_called_once() 43 | assert result == mock_step_result 44 | 45 | def test_get_steps_tree_returns_copy_and_clears(self, step_manager, mocker): 46 | mock_step1 = mocker.Mock(spec=StepResult) 47 | mock_step2 = mocker.Mock(spec=StepResult) 48 | step_manager._StepManager__steps_tree = [mock_step1, mock_step2] 49 | result = step_manager.get_steps_tree() 50 | assert len(result) == 2 51 | assert mock_step1 in result 52 | assert mock_step2 in result 53 | 54 | assert len(step_manager._StepManager__steps_tree) == 0 -------------------------------------------------------------------------------- /testit-python-commons/tests/services/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import warnings 3 | import inspect 4 | import uuid 5 | import random 6 | import string 7 | 8 | from testit_python_commons.services.utils import Utils 9 | from testit_python_commons.models.link import Link 10 | 11 | class TestUtils: 12 | #uuid_check 13 | def test_uuid_check_valid_uuid(self): 14 | valid_uuid = str(uuid.uuid4()) 15 | result = Utils.uuid_check(valid_uuid) 16 | assert result == valid_uuid 17 | 18 | def test_uuid_check_invalid_uuid_raises_system_exit(self): 19 | invalid_uuid = f"invalid-{random.randint(1000, 9999)}-format" 20 | with pytest.raises(SystemExit): 21 | Utils.uuid_check(invalid_uuid) 22 | 23 | def test_uuid_check_empty_string_raises_system_exit(self): 24 | with pytest.raises(SystemExit): 25 | Utils.uuid_check("") 26 | 27 | def test_uuid_check_wrong_length_raises_system_exit(self): 28 | valid_uuid = str(uuid.uuid4()) 29 | wrong_length_uuid = valid_uuid[:-1] 30 | with pytest.raises(SystemExit): 31 | Utils.uuid_check(wrong_length_uuid) 32 | 33 | #url_check 34 | @pytest.mark.parametrize("protocol", ["http", "https", "ftp"]) 35 | def test_url_check_valid_url(self, protocol): 36 | domain = ''.join(random.choices(string.ascii_lowercase, k=8)) 37 | valid_url = f"{protocol}://{domain}.com" 38 | result = Utils.url_check(valid_url) 39 | assert result == valid_url 40 | 41 | def test_url_check_invalid_url_raises_system_exit(self): 42 | invalid_url = f"not-a-valid-url-{random.randint(100, 999)}" 43 | with pytest.raises(SystemExit): 44 | Utils.url_check(invalid_url) 45 | 46 | def test_url_check_empty_string_raises_system_exit(self): 47 | with pytest.raises(SystemExit): 48 | Utils.url_check("") 49 | 50 | #deprecated 51 | def test_deprecated_decorator_shows_warning(self): 52 | message = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) 53 | 54 | @Utils.deprecated(message) 55 | def test_function(): 56 | return f"test_result_{random.randint(1, 100)}" 57 | 58 | with warnings.catch_warnings(record=True) as w: 59 | warnings.simplefilter("always") 60 | result = test_function() 61 | 62 | assert len(w) == 1 63 | assert issubclass(w[0].category, DeprecationWarning) 64 | assert "test_function" in str(w[0].message) 65 | assert message in str(w[0].message) 66 | assert "test_result_" in result 67 | 68 | #get_function_parameters 69 | def test_get_function_parameters_with_args_only(self): 70 | def sample_function(arg1, arg2): 71 | pass 72 | 73 | args_value = { 74 | "arg1": f"value_{random.randint(1, 50)}", 75 | "arg2": f"data_{random.randint(51, 100)}" 76 | } 77 | 78 | result = Utils.get_function_parameters(sample_function, args_value["arg1"], args_value["arg2"]) 79 | assert result == args_value 80 | 81 | def test_get_function_parameters_with_kwargs_only(self): 82 | def sample_function(arg1, arg2): 83 | pass 84 | 85 | args_value = { 86 | "arg1": f"keyword_{random.randint(1, 25)}", 87 | "arg2": f"parameter_{random.randint(26, 50)}" 88 | } 89 | 90 | result = Utils.get_function_parameters(sample_function, arg1=args_value["arg1"], arg2=args_value["arg2"]) 91 | assert result == args_value 92 | 93 | def test_get_function_parameters_empty_function(self): 94 | def sample_function(): 95 | pass 96 | 97 | result = Utils.get_function_parameters(sample_function) 98 | assert result == {} 99 | 100 | #exclude_self_parameter 101 | def test_exclude_self_parameter_removes_self(self): 102 | parameters = { 103 | "self": f"instance_{random.randint(1, 100)}", 104 | "arg1": f"arg_value_{random.randint(101, 200)}", 105 | "arg2": f"param_value_{random.randint(201, 300)}" 106 | } 107 | 108 | result = Utils.exclude_self_parameter(parameters) 109 | assert result == {k: parameters[k] for k in ["arg1", "arg2"]} 110 | 111 | def test_exclude_self_parameter_no_self_present(self): 112 | parameters = { 113 | "arg1": f"random_value_{random.randint(1, 50)}", 114 | "arg2": f"another_value_{random.randint(51, 100)}" 115 | } 116 | 117 | result = Utils.exclude_self_parameter(parameters) 118 | assert result == parameters 119 | 120 | def test_exclude_self_parameter_empty_dict(self): 121 | result = Utils.exclude_self_parameter({}) 122 | assert result == {} 123 | 124 | #collect_parameters_in_string_attribute 125 | def test_collect_parameters_in_string_attribute_with_placeholders(self): 126 | attribute = "Tests {value1} - {value2}" 127 | parameters = { 128 | "value1": ''.join(random.choices(string.ascii_letters, k=8)), 129 | "value2": str(random.randint(18, 80)) 130 | } 131 | 132 | result = Utils.collect_parameters_in_string_attribute(attribute, parameters) 133 | assert result == f"Tests {parameters['value1']} - {parameters['value2']}" 134 | 135 | def test_collect_parameters_in_string_attribute_no_placeholders(self): 136 | greeting = f"Tests {random.randint(1, 1000)}" 137 | parameters = { 138 | "value": ''.join(random.choices(string.ascii_letters, k=6)) 139 | } 140 | 141 | result = Utils.collect_parameters_in_string_attribute(greeting, parameters) 142 | assert result == greeting 143 | 144 | def test_collect_parameters_in_string_attribute_missing_parameter(self): 145 | attribute = "Tests {value} - {value_missing}" 146 | parameters = { 147 | "value": ''.join(random.choices(string.ascii_letters, k=7)) 148 | } 149 | 150 | result = Utils.collect_parameters_in_string_attribute(attribute, parameters) 151 | assert result == f"Tests {parameters['value']} - {{value_missing}}" 152 | 153 | def test_collect_parameters_in_string_attribute_empty_string(self): 154 | parameters = { 155 | "value": ''.join(random.choices(string.ascii_letters, k=5)) 156 | } 157 | 158 | result = Utils.collect_parameters_in_string_attribute("", parameters) 159 | assert result == "" 160 | 161 | #convert_link_dict_to_link_model 162 | def test_convert_link_dict_to_link_model_minimal(self, mocker): 163 | mock_link = mocker.Mock(spec=Link) 164 | mocker.patch('testit_python_commons.services.utils.Link', return_value=mock_link) 165 | 166 | domain = ''.join(random.choices(string.ascii_lowercase, k=12)) 167 | link_dict = {"url": f"https://{domain}.example.com"} 168 | result = Utils.convert_link_dict_to_link_model(link_dict) 169 | 170 | mock_link.set_url.assert_called_once_with(link_dict["url"]) 171 | assert result == mock_link 172 | 173 | def test_convert_link_dict_to_link_model_full(self, mocker): 174 | mock_link = mocker.Mock(spec=Link) 175 | mocker.patch('testit_python_commons.services.utils.Link', return_value=mock_link) 176 | 177 | domain = ''.join(random.choices(string.ascii_lowercase, k=10)) 178 | link_dict = { 179 | "url": f"https://{domain}.test.org", 180 | "title": ''.join(random.choices(string.ascii_letters, k=8)), 181 | "type": random.choice(["external", "internal", "documentation", "api"]), 182 | "description": ''.join(random.choices(string.ascii_lowercase, k=6)) 183 | } 184 | result = Utils.convert_link_dict_to_link_model(link_dict) 185 | 186 | mock_link.set_url.assert_called_once_with(link_dict["url"]) 187 | mock_link.set_title.assert_called_once_with(link_dict["title"]) 188 | mock_link.set_link_type.assert_called_once_with(link_dict["type"]) 189 | mock_link.set_description.assert_called_once_with(link_dict["description"]) 190 | assert result == mock_link 191 | 192 | #convert_body_of_attachment 193 | def test_convert_body_of_attachment_bytes_input(self): 194 | content = ''.join(random.choices(string.ascii_letters + string.digits, k=15)) 195 | body = content.encode('utf-8') 196 | result = Utils.convert_body_of_attachment(body) 197 | 198 | assert result == body 199 | assert isinstance(result, bytes) 200 | 201 | def test_convert_body_of_attachment_string_input(self): 202 | body = f"test content {random.randint(1000, 9999)}" 203 | result = Utils.convert_body_of_attachment(body) 204 | expected = body.encode('utf-8') 205 | 206 | assert result == expected 207 | assert isinstance(result, bytes) 208 | 209 | def test_convert_body_of_attachment_integer_input(self): 210 | body = random.randint(100, 999999) 211 | result = Utils.convert_body_of_attachment(body) 212 | expected = str(body).encode('utf-8') 213 | 214 | assert result == expected 215 | assert isinstance(result, bytes) 216 | 217 | #convert_value_str_to_bool 218 | def test_convert_value_str_to_bool_true_string(self): 219 | result = Utils.convert_value_str_to_bool("true") 220 | assert result is True 221 | 222 | def test_convert_value_str_to_bool_false_string(self): 223 | result = Utils.convert_value_str_to_bool("false") 224 | assert result is False 225 | 226 | def test_convert_value_str_to_bool_other_string(self): 227 | random_string = ''.join(random.choices(string.ascii_letters, k=5)) 228 | result = Utils.convert_value_str_to_bool(random_string) 229 | assert result is False 230 | 231 | def test_convert_value_str_to_bool_empty_string(self): 232 | result = Utils.convert_value_str_to_bool("") 233 | assert result is False 234 | 235 | def test_convert_value_str_to_bool_none_value(self): 236 | result = Utils.convert_value_str_to_bool(None) 237 | assert result is False -------------------------------------------------------------------------------- /testit-python-commons/tests/test_app_properties.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import os 4 | import uuid 5 | import pytest 6 | 7 | from testit_python_commons.app_properties import AppProperties 8 | 9 | class TestAppProperties: 10 | __properties_file = AppProperties._AppProperties__properties_file 11 | __env_prefix = AppProperties._AppProperties__env_prefix 12 | 13 | @pytest.fixture 14 | def create_config_file(self, tmp_path): 15 | def _create_config_file(filename, content): 16 | file_path = tmp_path / filename 17 | file_path.write_text(content, encoding="utf-8") 18 | return file_path 19 | 20 | return _create_config_file 21 | 22 | @pytest.fixture 23 | def properties(self): 24 | return { 25 | 'url': "https://www.example.com", 26 | 'privatetoken': ''.join(random.choices(string.ascii_letters + string.digits, k=24)), 27 | 'projectid': str(uuid.uuid4()), 28 | 'configurationid': str(uuid.uuid4()), 29 | 'testrunid': str(uuid.uuid4()), 30 | 'testrunname': ''.join(random.choices(string.ascii_letters + string.digits, k=10)), 31 | 'adaptermode': "2", 32 | 'tmsproxy': ''.join(random.choices(string.ascii_letters + string.digits, k=3)), 33 | 'certvalidation': "false", 34 | 'automaticcreationtestcases': "true", 35 | 'automaticupdationlinkstotestcases': "true", 36 | 'importrealtime': "false" 37 | } 38 | 39 | def test_load_file_properties_ini_file(self, create_config_file, tmp_path, mocker): 40 | mocker.patch.object(os.path, 'abspath', return_value=str(tmp_path)) 41 | mocker.patch.dict(os.environ, clear=True) 42 | 43 | properties_testit = { 44 | 'url': 'https://www.example.com', 45 | 'privatetoken': ''.join(random.choices(string.ascii_letters + string.digits, k=24)), 46 | 'projectid': str(uuid.uuid4()), 47 | 'configurationid': str(uuid.uuid4()), 48 | 'testrunid': str(uuid.uuid4()), 49 | 'testrunname': ''.join(random.choices(string.ascii_letters + string.digits, k=10)), 50 | 'adaptermode': "2" 51 | } 52 | 53 | properties_debug = { 54 | 'tmsproxy': ''.join(random.choices(string.ascii_letters + string.digits, k=3)), 55 | 'logs': 'true' 56 | } 57 | 58 | config_content = "[testit]\n" 59 | for property in properties_testit: 60 | config_content += "%s=%s\n" % (property, properties_testit[property]) 61 | config_content += "[debug]\n" 62 | config_content += "tmsProxy=%s\n" % properties_debug['tmsproxy'] 63 | config_content += "__dev=%s\n" % properties_debug['logs'] 64 | 65 | create_config_file(self.__properties_file, config_content) 66 | mocker.patch('os.path.isfile', lambda p: p == str(tmp_path / self.__properties_file)) 67 | assert AppProperties.load_file_properties() == {**properties_testit, **properties_debug} 68 | 69 | def test_load_env_properties_all_set(self, properties, mocker): 70 | env_vars = { 71 | f"{self.__env_prefix}_URL": properties["url"], 72 | f"{self.__env_prefix}_PRIVATE_TOKEN": properties["privatetoken"], 73 | f"{self.__env_prefix}_PROJECT_ID": properties["projectid"], 74 | f"{self.__env_prefix}_CONFIGURATION_ID": properties["configurationid"], 75 | f"{self.__env_prefix}_TEST_RUN_ID": properties["testrunid"], 76 | f"{self.__env_prefix}_TEST_RUN_NAME": properties["testrunname"], 77 | f"{self.__env_prefix}_ADAPTER_MODE": properties["adaptermode"], 78 | f"{self.__env_prefix}_PROXY": properties["tmsproxy"], 79 | f"{self.__env_prefix}_CERT_VALIDATION": properties["certvalidation"], 80 | f"{self.__env_prefix}_AUTOMATIC_CREATION_TEST_CASES": properties["automaticcreationtestcases"], 81 | f"{self.__env_prefix}_AUTOMATIC_UPDATION_LINKS_TO_TEST_CASES": properties["automaticupdationlinkstotestcases"], 82 | f"{self.__env_prefix}_IMPORT_REALTIME": properties["importrealtime"], 83 | } 84 | mocker.patch.dict(os.environ, env_vars, clear=True) 85 | 86 | assert AppProperties.load_env_properties() == properties 87 | 88 | def test_load_cli_properties_all_set(self, properties, mocker): 89 | mock_options = mocker.MagicMock() 90 | mock_options.set_url = properties["url"] 91 | mock_options.set_private_token = properties["privatetoken"] 92 | mock_options.set_project_id = properties["projectid"] 93 | mock_options.set_configuration_id = properties["configurationid"] 94 | mock_options.set_test_run_id = properties["testrunid"] 95 | mock_options.set_test_run_name = properties["testrunname"] 96 | mock_options.set_adapter_mode = properties["adaptermode"] 97 | mock_options.set_tms_proxy = properties["tmsproxy"] 98 | mock_options.set_cert_validation = properties["certvalidation"] 99 | mock_options.set_automatic_creation_test_cases = properties["automaticcreationtestcases"] 100 | mock_options.set_automatic_updation_links_to_test_cases = properties["automaticupdationlinkstotestcases"] 101 | mock_options.set_import_realtime = properties["importrealtime"] 102 | 103 | assert AppProperties.load_cli_properties(mock_options) == properties 104 | 105 | 106 | -------------------------------------------------------------------------------- /testit-python-commons/tests/test_dynamic_methods.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import pytest 4 | 5 | from testit_python_commons import dynamic_methods 6 | from testit_python_commons.services import TmsPluginManager, Utils 7 | from testit_python_commons.models.link_type import LinkType 8 | from testit_python_commons.models.link import Link 9 | 10 | 11 | class TestDynamicMethods: 12 | @pytest.fixture 13 | def link_model(self) -> Link: 14 | return Utils.convert_link_dict_to_link_model({ 15 | "url": "https://www.example.com", 16 | "title": ''.join(random.choices(string.ascii_letters + string.digits, k=10)), 17 | "type": LinkType.ISSUE, 18 | "description": ''.join(random.choices(string.ascii_letters + string.digits, k=10)) 19 | }) 20 | 21 | @pytest.fixture 22 | def mock_plugin_manager_hook(self, mocker): 23 | mock_plugin_manager = mocker.patch.object(TmsPluginManager, 'get_plugin_manager') 24 | mock_hook = mock_plugin_manager.return_value.hook 25 | 26 | return mock_hook 27 | 28 | def test_add_link_deprecated(self, link_model, mock_plugin_manager_hook, mocker): 29 | mock_convert = mocker.patch.object(Utils, 'convert_link_dict_to_link_model') 30 | mock_convert.return_value = link_model 31 | 32 | dynamic_methods.addLink( 33 | url=link_model.get_url(), 34 | title=link_model.get_title(), 35 | type=link_model.get_link_type(), 36 | description=link_model.get_description() 37 | ) 38 | 39 | mock_plugin_manager_hook.add_link.assert_called_once_with(link=link_model) 40 | mock_convert.assert_called_once_with({ 41 | "url": link_model.get_url(), 42 | "title": link_model.get_title(), 43 | "type": link_model.get_link_type(), 44 | "description": link_model.get_description() 45 | }) 46 | 47 | def test_add_links_with_url(self, link_model, mock_plugin_manager_hook, mocker): 48 | mock_convert = mocker.patch.object(Utils, 'convert_link_dict_to_link_model') 49 | mock_convert.return_value = link_model 50 | 51 | dynamic_methods.addLinks( 52 | url=link_model.get_url(), 53 | title=link_model.get_title(), 54 | type=link_model.get_link_type(), 55 | description=link_model.get_description() 56 | ) 57 | 58 | mock_plugin_manager_hook.add_link.assert_called_once_with(link=link_model) 59 | mock_convert.assert_called_once_with({ 60 | "url": link_model.get_url(), 61 | "title": link_model.get_title(), 62 | "type": link_model.get_link_type(), 63 | "description": link_model.get_description() 64 | }) 65 | 66 | def test_add_links_with_list(self, link_model, mock_plugin_manager_hook, mocker): 67 | mock_convert = mocker.patch.object(Utils, 'convert_link_dict_to_link_model') 68 | mock_convert.return_value = link_model 69 | 70 | link_data = { 71 | "url": link_model.get_url(), 72 | "title": link_model.get_title(), 73 | "type": link_model.get_link_type(), 74 | "description": link_model.get_description() 75 | } 76 | dynamic_methods.addLinks(links=[link_data]) 77 | 78 | mock_plugin_manager_hook.add_link.assert_called_once_with(link=link_model) 79 | mock_convert.assert_called_once_with(link_data) 80 | 81 | def test_add_message(self, mock_plugin_manager_hook): 82 | test_message = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) 83 | dynamic_methods.addMessage(test_message) 84 | mock_plugin_manager_hook.add_message.assert_called_once_with(test_message=test_message) --------------------------------------------------------------------------------