├── .config ├── constraints.txt ├── dictionary.txt ├── pydoclint-baseline.txt ├── requirements-docs.in ├── requirements-test.in └── requirements.in ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ack.yml │ ├── push.yml │ ├── release.yml │ └── tox.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .sonarcloud.properties ├── .taplo.toml ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── codecov.yml ├── cspell.config.yaml ├── docs ├── configuration.md ├── contributor_guide.md ├── faq.md ├── galaxy.yml ├── index.md ├── installation.md ├── integration.ini ├── sanity.ini ├── tox-ansible.ini ├── unit.ini └── user_guide.md ├── mkdocs.yml ├── pyproject.toml ├── src └── tox_ansible │ ├── __init__.py │ └── plugin.py ├── tests ├── __init__.py ├── conftest.py ├── fixtures │ ├── integration │ │ ├── test_basic │ │ │ ├── galaxy.yml │ │ │ ├── tox-ansible.ini │ │ │ └── tox.ini │ │ └── test_user_provided │ │ │ ├── galaxy.yml │ │ │ └── tox-ansible.ini │ └── unit │ │ └── test_type │ │ └── tox-ansible.ini ├── integration │ ├── __init__.py │ ├── test_basic.py │ └── test_user_provided.py └── unit │ ├── __init__.py │ ├── test_plugin.py │ └── test_type.py ├── tools └── report-coverage └── tox.ini /.config/constraints.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # tox run -e deps 3 | ansible-compat==25.5.0 # via pytest-ansible 4 | astroid==3.3.10 # via pylint 5 | attrs==25.3.0 # via jsonschema, referencing 6 | babel==2.17.0 # via mkdocs-material 7 | backrefs==5.8 # via mkdocs-material 8 | beautifulsoup4==4.13.4 # via linkchecker, mkdocs-htmlproofer-plugin 9 | cachetools==6.0.0 # via tox 10 | cairocffi==1.7.1 # via cairosvg 11 | cairosvg==2.7.1 ; python_full_version < '3.11' # via mkdocs-ansible 12 | cairosvg==2.8.2 ; python_full_version >= '3.11' # via mkdocs-ansible 13 | certifi==2025.4.26 # via requests 14 | cffi==1.17.1 # via cairocffi, cryptography 15 | cfgv==3.4.0 # via pre-commit 16 | chardet==5.2.0 # via tox 17 | charset-normalizer==3.4.2 # via requests 18 | click==8.2.1 # via mkdocs, pydoclint 19 | colorama==0.4.6 # via click, griffe, mkdocs, mkdocs-material, pylint, pytest, tox 20 | coverage==7.8.2 # via tox-ansible (pyproject.toml) 21 | cryptography==45.0.3 # via ansible-core 22 | csscompressor==0.9.5 # via mkdocs-minify-plugin 23 | cssselect2==0.8.0 # via cairosvg 24 | defusedxml==0.7.1 # via cairosvg 25 | dill==0.4.0 # via pylint 26 | distlib==0.3.9 # via virtualenv 27 | dnspython==2.7.0 # via linkchecker 28 | docstring-parser-fork==0.0.12 # via pydoclint 29 | execnet==2.1.1 # via pytest-xdist 30 | filelock==3.18.0 # via tox, virtualenv 31 | ghp-import==2.1.0 # via mkdocs 32 | griffe==1.7.3 # via mkdocstrings-python 33 | hjson==3.1.0 # via mkdocs-macros-plugin, super-collections 34 | htmlmin2==0.1.13 # via mkdocs-minify-plugin 35 | identify==2.6.12 # via pre-commit 36 | idna==3.10 # via requests 37 | iniconfig==2.1.0 # via pytest 38 | isort==6.0.1 # via pylint 39 | jinja2==3.1.6 # via ansible-core, mkdocs, mkdocs-macros-plugin, mkdocs-material, mkdocstrings 40 | jsmin==3.0.1 # via mkdocs-minify-plugin 41 | jsonschema==4.24.0 # via ansible-compat 42 | jsonschema-specifications==2025.4.1 # via jsonschema 43 | linkchecker==10.5.0 # via mkdocs-ansible 44 | markdown==3.8 # via markdown-include, mkdocs, mkdocs-autorefs, mkdocs-htmlproofer-plugin, mkdocs-material, mkdocstrings, pymdown-extensions 45 | markdown-exec==1.10.3 # via mkdocs-ansible 46 | markdown-include==0.8.1 # via mkdocs-ansible 47 | markupsafe==3.0.2 # via jinja2, mkdocs, mkdocs-autorefs, mkdocstrings 48 | mccabe==0.7.0 # via pylint 49 | mergedeep==1.3.4 # via mkdocs, mkdocs-get-deps 50 | mkdocs==1.6.1 # via mkdocs-ansible, mkdocs-autorefs, mkdocs-gen-files, mkdocs-htmlproofer-plugin, mkdocs-macros-plugin, mkdocs-material, mkdocs-minify-plugin, mkdocstrings 51 | mkdocs-ansible==25.5.0 ; python_full_version < '3.11' # via tox-ansible (pyproject.toml) 52 | mkdocs-ansible==25.6.0 ; python_full_version >= '3.11' # via tox-ansible (pyproject.toml) 53 | mkdocs-autorefs==1.4.2 # via mkdocstrings, mkdocstrings-python 54 | mkdocs-gen-files==0.5.0 # via mkdocs-ansible 55 | mkdocs-get-deps==0.2.0 # via mkdocs 56 | mkdocs-htmlproofer-plugin==1.3.0 # via mkdocs-ansible 57 | mkdocs-macros-plugin==1.3.7 # via mkdocs-ansible 58 | mkdocs-material==9.6.14 # via mkdocs-ansible 59 | mkdocs-material-extensions==1.3.1 # via mkdocs-ansible, mkdocs-material 60 | mkdocs-minify-plugin==0.8.0 # via mkdocs-ansible 61 | mkdocstrings==0.29.1 # via mkdocs-ansible, mkdocstrings-python 62 | mkdocstrings-python==1.16.12 # via mkdocs-ansible 63 | mypy==1.16.0 # via tox-ansible (pyproject.toml) 64 | mypy-extensions==1.1.0 # via mypy 65 | nodeenv==1.9.1 # via pre-commit 66 | packaging==25.0 # via ansible-compat, ansible-core, mkdocs, mkdocs-macros-plugin, pyproject-api, pytest, pytest-ansible, tox 67 | paginate==0.5.7 # via mkdocs-material 68 | pathspec==0.12.1 # via mkdocs, mkdocs-macros-plugin, mypy 69 | pillow==11.2.1 # via cairosvg, mkdocs-ansible 70 | platformdirs==4.3.8 # via mkdocs-get-deps, pylint, tox, virtualenv 71 | pluggy==1.6.0 # via pytest, tox 72 | pre-commit==4.2.0 # via tox-ansible (pyproject.toml) 73 | pycparser==2.22 # via cffi 74 | pydoclint==0.6.6 # via tox-ansible (pyproject.toml) 75 | pygments==2.19.1 # via mkdocs-material 76 | pylint==3.3.7 # via tox-ansible (pyproject.toml) 77 | pymdown-extensions==10.15 # via markdown-exec, mkdocs-ansible, mkdocs-material, mkdocstrings 78 | pyproject-api==1.9.1 # via tox 79 | pytest==8.3.5 # via pytest-ansible, pytest-xdist, tox-ansible (pyproject.toml) 80 | pytest-ansible==25.5.0 # via tox-ansible (pyproject.toml) 81 | pytest-xdist==3.7.0 # via tox-ansible (pyproject.toml) 82 | python-dateutil==2.9.0.post0 # via ghp-import, mkdocs-macros-plugin 83 | pyyaml==6.0.2 # via ansible-compat, ansible-core, mkdocs, mkdocs-get-deps, mkdocs-macros-plugin, pre-commit, pymdown-extensions, pyyaml-env-tag, tox-ansible (pyproject.toml) 84 | pyyaml-env-tag==1.1 # via mkdocs 85 | referencing==0.36.2 # via jsonschema, jsonschema-specifications 86 | requests==2.32.3 # via linkchecker, mkdocs-htmlproofer-plugin, mkdocs-material 87 | rpds-py==0.25.1 # via jsonschema, referencing 88 | ruff==0.11.13 # via tox-ansible (pyproject.toml) 89 | six==1.17.0 # via python-dateutil 90 | soupsieve==2.7 # via beautifulsoup4 91 | subprocess-tee==0.4.2 # via ansible-compat 92 | super-collections==0.5.3 # via mkdocs-macros-plugin 93 | termcolor==3.1.0 # via mkdocs-macros-plugin 94 | tinycss2==1.4.0 # via cairosvg, cssselect2 95 | toml-sort==0.24.2 # via tox-ansible (pyproject.toml) 96 | tomlkit==0.13.3 # via pylint, toml-sort 97 | tox==4.26.0 # via tox-ansible (pyproject.toml) 98 | types-pyyaml==6.0.12.20250516 # via tox-ansible (pyproject.toml) 99 | urllib3==2.4.0 # via requests 100 | virtualenv==20.31.2 # via pre-commit, tox 101 | watchdog==6.0.0 # via mkdocs 102 | webencodings==0.5.1 # via cssselect2, tinycss2 103 | 104 | # The following packages were excluded from the output: 105 | # ansible-core 106 | # exceptiongroup 107 | # resolvelib 108 | # tomli 109 | # typing-extensions 110 | -------------------------------------------------------------------------------- /.config/dictionary.txt: -------------------------------------------------------------------------------- 1 | autouse 2 | bthornto 3 | calver 4 | caplog 5 | capsys 6 | ccshared 7 | cflags 8 | cppflags 9 | cpython 10 | delenv 11 | devel 12 | endgroup 13 | envdir 14 | envlist 15 | envtmpdir 16 | fileh 17 | fixturenames 18 | ldflags 19 | libera 20 | metafunc 21 | passenv 22 | pythonhashseed 23 | pythonioencoding 24 | setenv 25 | sysroot 26 | testenv 27 | tmpdir 28 | toxfile 29 | xdist 30 | -------------------------------------------------------------------------------- /.config/pydoclint-baseline.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/tox-ansible/abf6109fa79904c88eb2baf3a0ead0bfcf4d31fe/.config/pydoclint-baseline.txt -------------------------------------------------------------------------------- /.config/requirements-docs.in: -------------------------------------------------------------------------------- 1 | mkdocs-ansible>=0.2.0 2 | -------------------------------------------------------------------------------- /.config/requirements-test.in: -------------------------------------------------------------------------------- 1 | ansible-core 2 | coverage[toml] 3 | mypy 4 | pre-commit 5 | pydoclint 6 | pylint 7 | pytest 8 | pytest-xdist 9 | ruff 10 | toml-sort 11 | tox 12 | types-PyYAML 13 | -------------------------------------------------------------------------------- /.config/requirements.in: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-ansible>=3.1.0 3 | pytest-xdist 4 | pyyaml 5 | tox>=4.15.1 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansible/devtools 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | Please see the official 4 | [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html). 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: pip 5 | directory: /.config/ 6 | schedule: 7 | day: sunday 8 | interval: weekly 9 | labels: 10 | - dependabot-deps-updates 11 | - skip-changelog 12 | groups: 13 | dependencies: 14 | patterns: 15 | - "*" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | labels: 21 | - "dependencies" 22 | - "skip-changelog" 23 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # see https://github.com/ansible/team-devtools 3 | _extends: ansible/team-devtools 4 | -------------------------------------------------------------------------------- /.github/workflows/ack.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/ack.yml 2 | name: ack 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request_target: 10 | types: [opened, labeled, unlabeled, synchronize] 11 | 12 | jobs: 13 | ack: 14 | uses: ansible/team-devtools/.github/workflows/ack.yml@token_revised 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/push.yml 3 | name: push 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - "releases/**" 9 | - "stable/**" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | ack: 14 | uses: ansible/team-devtools/.github/workflows/push.yml@main 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | release: 10 | environment: release 11 | runs-on: ubuntu-24.04 12 | permissions: 13 | id-token: write 14 | 15 | env: 16 | FORCE_COLOR: 1 17 | PY_COLORS: 1 18 | 19 | steps: 20 | - name: Switch to using Python 3.12 by default 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.12" 24 | 25 | - name: Install tox 26 | run: python3 -m pip install --user "tox>=4.0.0" 27 | 28 | - name: Check out src from Git 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 # needed by setuptools-scm 32 | 33 | - name: Build dists 34 | run: python3 -m tox -e pkg 35 | 36 | - name: Publish to pypi.org 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | 39 | forum_post: 40 | needs: release 41 | runs-on: ubuntu-24.04 42 | 43 | steps: 44 | - name: Retreive the forum post script from team-devtools 45 | run: curl -O https://raw.githubusercontent.com/ansible/team-devtools/main/.github/workflows/forum_post.py 46 | 47 | - name: Run the forum post script 48 | run: python3 forum_post.py ${{ github.event.repository.full_name }} ${{ github.event.release.tag_name }} ${{ secrets.FORUM_KEY }} ${{ secrets.FORUM_USER }} 49 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tox 3 | 4 | on: 5 | merge_group: 6 | branches: 7 | - "main" 8 | push: 9 | branches: 10 | - "main" 11 | pull_request: 12 | branches: 13 | - "main" 14 | - "releases/**" 15 | - "stable/**" 16 | schedule: 17 | - cron: "0 0 * * *" 18 | workflow_call: 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | tox: 26 | uses: ansible/team-devtools/.github/workflows/tox.yml@main 27 | with: 28 | max_python: "3.13" 29 | jobs_producing_coverage: 8 30 | 31 | smoke-matrix: 32 | # Uncomment the below line when all tests start passing 33 | needs: tox 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{github.event.pull_request.head.ref}} 39 | repository: ${{ github.event.pull_request.head.repo.full_name }} 40 | 41 | - name: Set up Python 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: "3.11" 45 | 46 | - name: Install tox-ansible, includes tox 47 | run: python3 -m pip install . 48 | 49 | - name: Generate matrix 50 | id: generate-matrix 51 | working-directory: tests/fixtures/unit/test_type 52 | run: | 53 | python3 -m tox --ansible --gh-matrix --conf tox-ansible.ini 54 | 55 | outputs: 56 | envlist: ${{ steps.generate-matrix.outputs.envlist }} 57 | 58 | smoke-test: 59 | needs: smoke-matrix 60 | strategy: 61 | fail-fast: false 62 | matrix: 63 | entry: ${{ fromJson(needs.smoke-matrix.outputs.envlist) }} 64 | name: ${{ matrix.entry.name }} 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | with: 69 | ref: ${{ github.event.pull_request.head.sha }} 70 | fetch-depth: 0 71 | 72 | - name: Set up Python 73 | uses: actions/setup-python@v5 74 | with: 75 | # tox-ansible requires python 3.10 but it can be used to test with 76 | # older versions of python. 77 | python-version: | 78 | ${{ matrix.entry.python }} 79 | 3.10 80 | 81 | - name: Install tox-ansible, includes tox 82 | run: python3 -m pip install . 83 | 84 | - name: Install ansible-creator 85 | run: python3 -m pip install ansible-creator 86 | 87 | - name: Create test directory to scaffold ansible collection 88 | run: mkdir example 89 | 90 | - name: Scaffold an ansible collection using ansible-creator 91 | working-directory: example 92 | run: ansible-creator init collection "foo.bar" 93 | 94 | - name: Track files created by ansible-creator 95 | working-directory: example 96 | run: git add . 97 | 98 | - name: Run tox test 99 | working-directory: example 100 | run: python3 -m tox --ansible -e ${{ matrix.entry.name }} --conf tox-ansible.ini 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | 104 | smoke-check: 105 | needs: smoke-test 106 | runs-on: ubuntu-latest 107 | steps: 108 | - run: >- 109 | python -c "assert set([ 110 | '${{ needs.smoke-test.result }}', 111 | ]) == {'success'}" 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | coverage.lcov 47 | coverage.xml 48 | junit.xml 49 | nosetests.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | 165 | 166 | # In contrast to the entries above this line which largely come from 167 | # untracked sources, the following have been inidividually rationalized 168 | # and should all have detailed explanations 169 | 170 | # Version created and populated by setuptools_scm 171 | /src/*/_version.py 172 | 173 | .DS_Store 174 | _readthedocs 175 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | # format compatible with commitlint 4 | autoupdate_commit_msg: "chore: pre-commit autoupdate" 5 | autoupdate_schedule: monthly 6 | autofix_commit_msg: "chore: auto fixes from pre-commit.com hooks" 7 | 8 | skip: 9 | # https://github.com/pre-commit-ci/issues/issues/55 10 | - lock 11 | - deps 12 | repos: 13 | - repo: https://github.com/rbubley/mirrors-prettier 14 | rev: v3.5.3 15 | hooks: 16 | - id: prettier 17 | always_run: true 18 | additional_dependencies: 19 | - prettier 20 | - prettier-plugin-sort-json 21 | - repo: https://github.com/streetsidesoftware/cspell-cli 22 | rev: v9.0.1 23 | hooks: 24 | - id: cspell 25 | name: Spell check with cspell 26 | - repo: https://github.com/pre-commit/pre-commit-hooks.git 27 | rev: v5.0.0 28 | hooks: 29 | - id: check-added-large-files 30 | - id: check-merge-conflict 31 | - id: check-symlinks 32 | - id: debug-statements 33 | - id: detect-private-key 34 | - id: end-of-file-fixer 35 | - id: trailing-whitespace 36 | - id: mixed-line-ending 37 | - id: fix-byte-order-marker 38 | - id: check-executables-have-shebangs 39 | - id: check-merge-conflict 40 | - id: debug-statements 41 | language_version: python3 42 | 43 | - repo: https://github.com/pappasam/toml-sort 44 | rev: v0.24.2 45 | hooks: 46 | - id: toml-sort-fix 47 | alias: toml 48 | 49 | - repo: https://github.com/tox-dev/tox-ini-fmt 50 | rev: 1.5.0 51 | hooks: 52 | - id: tox-ini-fmt 53 | 54 | - repo: https://github.com/astral-sh/ruff-pre-commit 55 | rev: v0.11.12 56 | hooks: 57 | - id: ruff 58 | args: 59 | - --fix 60 | - --exit-non-zero-on-fix 61 | types_or: [python, pyi] 62 | - id: ruff-format # must be after ruff 63 | types_or: [python, pyi] 64 | 65 | - repo: https://github.com/pre-commit/mirrors-mypy 66 | rev: v1.16.0 67 | hooks: 68 | - id: mypy 69 | additional_dependencies: 70 | - pytest 71 | - tox 72 | - types-PyYAML 73 | 74 | - repo: https://github.com/jsh9/pydoclint 75 | rev: "0.6.7" 76 | hooks: 77 | - id: pydoclint 78 | 79 | - repo: https://github.com/pycqa/pylint.git 80 | rev: v3.3.7 81 | hooks: 82 | - id: pylint 83 | args: 84 | - --output-format=colorized 85 | additional_dependencies: 86 | - pytest 87 | - tox 88 | - pyyaml 89 | 90 | # Keep last due to being considerably slower than the others: 91 | - repo: local 92 | hooks: 93 | - id: deps 94 | # To run it execute: `pre-commit run pip-compile-upgrade --hook-stage manual` 95 | name: Upgrade constraints files and requirements 96 | files: ^(pyproject\.toml|\.config/.*)$ 97 | always_run: true 98 | language: python 99 | language_version: "3.10" # minimal we support officially https://github.com/astral-sh/uv/issues/3883 100 | entry: python3 -m uv pip compile -q --all-extras --universal --output-file=.config/constraints.txt pyproject.toml --upgrade 101 | pass_filenames: false 102 | stages: 103 | - manual 104 | additional_dependencies: 105 | - uv>=0.5.25 106 | - id: lock 107 | name: Check constraints files and requirements 108 | files: ^(pyproject\.toml|\.config/.*)$ 109 | language: python 110 | language_version: "3.10" # minimal we support officially https://github.com/astral-sh/uv/issues/3883 111 | entry: python3 -m uv pip compile -q --all-extras --universal --output-file=.config/constraints.txt pyproject.toml 112 | pass_filenames: false 113 | additional_dependencies: 114 | - uv>=0.5.25 115 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | mkdocs: 5 | fail_on_warning: true 6 | configuration: mkdocs.yml 7 | 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.11" 12 | commands: 13 | - pip install --user tox 14 | - python3 -m tox -e docs 15 | python: 16 | install: 17 | - method: pip 18 | path: tox 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | submodules: 24 | include: all 25 | recursive: true 26 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.python.version=3.10, 3.11, 3.12, 3.13 2 | sonar.sources=src/ 3 | sonar.tests=tests/ 4 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | # cspell: disable-next-line 3 | # compatibility between toml-sort-fix pre-commit hook and panekj.even-betterer-toml extension 4 | align_comments = false 5 | array_trailing_comma = false 6 | compact_arrays = true 7 | compact_entries = false 8 | compact_inline_tables = true 9 | inline_table_expand = false 10 | reorder_keys = true 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "charliermarsh.ruff", 4 | "esbenp.prettier-vscode", 5 | "gruntfuggly.triggertaskonsave", 6 | "markis.code-coverage", 7 | "ms-python.debugpy", 8 | "ms-python.mypy-type-checker", 9 | "ms-python.pylint", 10 | "ms-python.python", 11 | "sonarsource.sonarlint-vscode", 12 | "streetsidesoftware.code-spell-checker", 13 | "tamasfe.even-better-toml" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[jsonc]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[python]": { 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit", 8 | "source.organizeImports": "explicit" 9 | }, 10 | "editor.defaultFormatter": "charliermarsh.ruff", 11 | "editor.formatOnSave": true 12 | }, 13 | "flake8.importStrategy": "fromEnvironment", 14 | "markiscodecoverage.searchCriteria": ".cache/.coverage/lcov.info", 15 | "mypy-type-checker.args": ["--config-file=${workspaceFolder}/pyproject.toml"], 16 | "mypy-type-checker.importStrategy": "fromEnvironment", 17 | "mypy-type-checker.reportingScope": "workspace", 18 | "pylint.importStrategy": "fromEnvironment", 19 | "python.testing.pytestArgs": ["tests"], 20 | "python.testing.pytestEnabled": true, 21 | "python.testing.unittestEnabled": false, 22 | "triggerTaskOnSave.tasks": { 23 | "pydoclint": ["*.py"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "pydoclint", 8 | "type": "shell", 9 | "command": "pydoclint", 10 | "args": ["."], 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "problemMatcher": { 15 | "owner": "pydoclint", 16 | "fileLocation": ["relative", "${workspaceFolder}"], 17 | "pattern": { 18 | "regexp": "^(.*?):(\\d+):\\s(.*?):\\s(.*)$", 19 | "file": 1, 20 | "line": 2, 21 | "code": 3, 22 | "message": 4 23 | } 24 | }, 25 | "group": { 26 | "kind": "none", 27 | "isDefault": true 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bradley A. Thornton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tox-ansible 2 | 3 | ## Introduction 4 | 5 | `tox-ansible` is a utility designed to simplify the testing of Ansible content collections. 6 | 7 | Implemented as a `tox` plugin, `tox-ansible` provides a simple way to test Ansible content collections across multiple Python interpreters and Ansible versions. 8 | 9 | `tox-ansible` uses familiar python testing tools to perform the actual testing. It uses `tox` to create and manage the testing environments, `ansible-test sanity` to run the sanity tests, and `pytest` to run the unit and integration tests. This eliminated the black box nature of other approaches and allowed for more control over the testing process. 10 | 11 | When used on a local development system, each of the environments are left intact after a test run. This allows for easy debugging of failed tests for a given test type, python interpreter and Ansible version. 12 | 13 | By using `tox` to create and manage the testing environments, Test outcomes should always be the same on a local development system as they are in a CI/CD pipeline. 14 | 15 | `tox` virtual environments are created in the `.tox` directory. These are easily deleted and recreated if needed. 16 | 17 | ## Talk to us 18 | 19 | Need help or want to discuss the project? See our [Contributor guide](https://ansible.readthedocs.io/projects/tox-ansible/contributor_guide/#talk-to-us) join the conversation! 20 | 21 | ## Installation 22 | 23 | Install from pypi: 24 | 25 | ```bash 26 | pip install tox-ansible 27 | ``` 28 | 29 | ## Usage 30 | 31 | From the root of your collection, create an empty `tox-ansible.ini` file and list the available environments: 32 | 33 | ```bash 34 | touch tox-ansible.ini 35 | tox list --ansible --conf tox-ansible.ini 36 | ``` 37 | 38 | A list of dynamically generated Ansible environments will be displayed: 39 | 40 | ``` 41 | 42 | default environments: 43 | ... 44 | integration-py3.11-2.14 -> Integration tests for ansible.scm using ansible-core 2.14 and python 3.11 45 | integration-py3.11-devel -> Integration tests for ansible.scm using ansible-core devel and python 3.11 46 | integration-py3.11-milestone -> Integration tests for ansible.scm using ansible-core milestone and python 3.11 47 | ... 48 | sanity-py3.11-2.14 -> Sanity tests for ansible.scm using ansible-core 2.14 and python 3.11 49 | sanity-py3.11-devel -> Sanity tests for ansible.scm using ansible-core devel and python 3.11 50 | sanity-py3.11-milestone -> Sanity tests for ansible.scm using ansible-core milestone and python 3.11 51 | ... 52 | unit-py3.11-2.14 -> Unit tests for ansible.scm using ansible-core 2.14 and python 3.11 53 | unit-py3.11-devel -> Unit tests for ansible.scm using ansible-core devel and python 3.11 54 | unit-py3.11-milestone -> Unit tests for ansible.scm using ansible-core milestone and python 3.11 55 | ``` 56 | 57 | These represent the available testing environments. Each denotes the type of tests that will be run, the Python interpreter used to run the tests, and the Ansible version used to run the tests. 58 | 59 | To run tests with a single environment, simply run the following command: 60 | 61 | ```bash 62 | tox -e sanity-py3.11-2.14 --ansible --conf tox-ansible.ini 63 | ``` 64 | 65 | To run tests with multiple environments, simply add the environment names to the command: 66 | 67 | ```bash 68 | tox -e sanity-py3.11-2.14,unit-py3.11-2.14 --ansible --conf tox-ansible.ini 69 | ``` 70 | 71 | To run all tests of a specific type in all available environments, use the factor `-f` flag: 72 | 73 | ```bash 74 | tox -f unit --ansible -p auto --conf tox-ansible.ini 75 | ``` 76 | 77 | To run all tests across all available environments: 78 | 79 | ```bash 80 | tox --ansible -p auto --conf tox-ansible.ini 81 | ``` 82 | 83 | Note: The `-p auto` flag will run multiple tests in parallel. 84 | Note: The specific Python interpreter will need to be pre-installed on your system, e.g.: 85 | 86 | ```bash 87 | sudo dnf install python3.10 88 | ``` 89 | 90 | To review the specific commands and configuration for each of the integration, sanity, and unit factors: 91 | 92 | ```bash 93 | tox config --ansible --conf tox-ansible.ini 94 | ``` 95 | 96 | Generate specific GitHub action matrix as per scope mentioned with `--matrix-scope`: 97 | 98 | ```bash 99 | tox --ansible --gh-matrix --matrix-scope unit --conf tox-ansible.ini 100 | ``` 101 | 102 | A list of dynamically generated Ansible environments will be displayed specifically for unit tests: 103 | 104 | ``` 105 | [ 106 | { 107 | "description": "Unit tests using ansible 2.9 and python 3.8", 108 | "factors": [ 109 | "unit", 110 | "py3.8", 111 | "2.9" 112 | ], 113 | "name": "unit-py3.8-2.9", 114 | "python": "3.8" 115 | }, 116 | ... 117 | { 118 | "description": "Unit tests using ansible-core milestone and python 3.12", 119 | "factors": [ 120 | "unit", 121 | "py3.12", 122 | "milestone" 123 | ], 124 | "name": "unit-py3.12-milestone", 125 | "python": "3.12" 126 | } 127 | ] 128 | ``` 129 | 130 | ## Configuration 131 | 132 | `tox-ansible` should be configured using a `tox-ansible.ini` file. Using a `tox-ansible.ini` file allows for the introduction of the `tox-ansible` plugin to a repository that may already have an existing `tox` configuration without conflicts. If no configuration overrides are needed, the `tox-ansible.ini` file may be empty but should be present. In addition to all `tox` supported keywords the `ansible` section and `skip` keyword are available: 133 | 134 | ```ini 135 | # tox-ansible.ini 136 | [ansible] 137 | skip = 138 | 2.9 139 | devel 140 | ``` 141 | 142 | This will skip tests in any environment that uses Ansible 2.9 or the devel branch. The list of strings is used for a simple string in string comparison of environment names. Here is the [guide] to override `tox-ansible` environment configuration. 143 | 144 | [guide]: https://ansible.readthedocs.io/projects/tox-ansible/configuration/#overriding-the-configuration 145 | 146 | ## Release process 147 | 148 | `tox-ansible` is released with [CalVer] scheme version numbers. The particular scheme we are using is `YY.MM.MICRO`, meaning that a release in March 2025 will be named `25.3.0`, and if a patch (ie, non-feature) release is required for that release, it will be named 25.3.1, even if it is released in April. The month will not increment until a new version with features or other significant changes is released. More details about calver release process can be seen [here]. 149 | 150 | [here]: https://ansible.readthedocs.io/projects/team-devtools/guides/calver/ 151 | [CalVer]: https://calver.org/ 152 | 153 | ## Note to version 1.x users 154 | 155 | Users of tox-ansible v1 should use the stable/1.x branch because the default branch is a rewrite of the plugin for tox 4.0+ which is not backward compatible with the old plugin. 156 | 157 | Version 1 of the plugin had native support for molecule. Please see the "Running molecule scenarios" above for an alternative approach. 158 | 159 | ## License 160 | 161 | MIT 162 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | patch: true 5 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | dictionaryDefinitions: 2 | - name: words 3 | path: .config/dictionary.txt 4 | addWords: true 5 | dictionaries: 6 | - bash 7 | - networking-terms 8 | - python 9 | - words 10 | - "!aws" 11 | - "!backwards-compatibility" 12 | - "!cryptocurrencies" 13 | - "!cpp" 14 | ignorePaths: 15 | - .config/requirements* 16 | - \.* 17 | - cspell.config.yaml 18 | - mkdocs.yml 19 | - pyproject.toml 20 | - tox.ini 21 | 22 | languageSettings: 23 | - languageId: python 24 | allowCompoundWords: false 25 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | `tox-ansible` should be configured using a `tox-ansible.ini` file. Using a `tox-ansible.ini` file allows for the introduction of the `tox-ansible` plugin to a repository that may already have an existing `tox` configuration without conflicts. If no configuration overrides are needed, the `tox-ansible.ini` file may be empty but should be present. In addition to all `tox` supported keywords the `ansible` section and `skip` keyword are available: 4 | 5 | ```ini 6 | # tox-ansible.ini 7 | [ansible] 8 | skip = 9 | 2.9 10 | devel 11 | ``` 12 | 13 | This will skip tests in any environment that uses Ansible 2.9 or the devel branch. The list of strings is used for a simple string in string comparison of environment names. 14 | 15 | ### Overriding the configuration 16 | 17 | Any configuration in either the `[testenv]` section or an environment section `[testenv:integration-py3.11-{devel,milestone}]` can override some or all of the `tox-ansible` environment configurations. 18 | 19 | For example 20 | 21 | ```ini 22 | 23 | [testenv] 24 | commands_pre = 25 | true 26 | 27 | [testenv:integration-py3.11-{devel,milestone}] 28 | commands = 29 | true 30 | ``` 31 | 32 | will result in: 33 | 34 | ```ini 35 | # tox-ansible.ini 36 | [testenv:integration-py3.11-devel] 37 | commands_pre = true 38 | commands = true 39 | ``` 40 | 41 | Used without caution, this configuration can result in unexpected behavior, and possible false positive or false negative test results. 42 | -------------------------------------------------------------------------------- /docs/contributor_guide.md: -------------------------------------------------------------------------------- 1 | # Contributor Guide 2 | 3 | To contribute to `tox-ansible`, please use pull requests on a branch of your own fork. 4 | 5 | After [creating your fork on GitHub], you can do: 6 | 7 | ```shell-session 8 | $ git clone --recursive git@github.com:your-name/tox-ansible 9 | $ cd tox-ansible 10 | $ git checkout -b your-branch-name 11 | # DO SOME CODING HERE 12 | $ git add your new files 13 | $ git commit -v 14 | $ git push origin your-branch-name 15 | ``` 16 | 17 | You will then be able to create a pull request from your commit. 18 | 19 | Prerequisites: 20 | 21 | 1. All fixes to core functionality (i.e. anything except docs or examples) should 22 | be accompanied by tests that fail prior to your change and succeed afterwards. 23 | 24 | 2. Before sending a PR, make sure that `tox -e lint` passes. 25 | 26 | Feel free to raise issues in the repo if you feel unable to contribute a code 27 | fix. 28 | 29 | Possible security bugs should be reported via email to . 30 | 31 | ## Talk to us 32 | 33 | ### Code of Conduct 34 | 35 | Please read and adhere to the [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all your interactions with the Ansible community. 36 | 37 | ### Forum 38 | 39 | Join the [Ansible Forum](https://forum.ansible.com) as a single starting point and our default communication platform for questions and help, development discussions, events, and much more. [Register](https://forum.ansible.com/signup?) to join the community. Search by categories and tags to find interesting topics or start a new one; subscribe only to topics you need! 40 | 41 | - [Get Help](https://forum.ansible.com/c/help/6): get help or help others. Please add appropriate tags if you start new discussions, for example `devtools`. 42 | - [Posts tagged with 'devtools'](https://forum.ansible.com/tag/devtools): subscribe to participate in project-related conversations. 43 | - [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn) used to announce releases and important changes. 44 | - [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts. 45 | - [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events. 46 | 47 | See `Navigating the Ansible forum `\_ for some practical advice on finding your way around. 48 | 49 | ### Matrix 50 | 51 | For real-time interactions, conversations in the community happen over the Matrix protocol in the [#devtools:ansible.com](https://matrix.to/#/#devtools:ansible.com). 52 | 53 | For more information, see the community-hosted [Matrix FAQ](https://hackmd.io/@ansible-community/community-matrix-faq). 54 | 55 | [creating your fork on github]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects 56 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | > Need help or want to discuss the project? See our [Contributor guide](https://ansible.readthedocs.io/projects/tox-ansible/contributor_guide/#talk-to-us) join the conversation! 4 | 5 | ## How does it work? 6 | 7 | `tox` will, by default, create a Python virtual environment for a given environment. `tox-ansible` adds Ansible collection specific build and test logic to tox. The collection is copied into the virtual environment created by tox, built, and installed into the virtual environment. The installation of the collection will include the collection's collection dependencies. `tox-ansible` will also install any Python dependencies from a `test-requirements.txt` (or `requirements-test.txt`) and `requirements.txt` file. The virtual environment's temporary directory is used, so the copy, build and install steps are performed with each test run ensuring the current collection code is used. 8 | 9 | `tox-ansible` also sets the `ANSIBLE_COLLECTIONS_PATH` environment variable to point to the virtual environment's temporary directory. This ensures that the collection under test is used when running tests. The `pytest-ansible-units` pytest plugin injects the `ANSIBLE_COLLECTIONS_PATH` environment variable into the collection loader so ansible-core can locate the collection. 10 | 11 | `pytest` is used to run both the `unit` and `integration tests`. 12 | `ansible-test sanity` is used to run the `sanity` tests. 13 | 14 | For full configuration examples for each of the sanity, integration, and unit tests including the commands being run and the environment variables being set and passed, see the following: 15 | 16 | - [integration](https://github.com/ansible/tox-ansible/blob/main/docs/integration.ini) 17 | - [sanity](https://github.com/ansible/tox-ansible/blob/main/docs/sanity.ini) 18 | - [unit](https://github.com/ansible/tox-ansible/blob/main/docs/unit.ini) 19 | 20 | See the [tox documentation](https://tox.readthedocs.io/en/latest/) for more information on tox. 21 | -------------------------------------------------------------------------------- /docs/galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | authors: 3 | - Ansible Network Community (ansible-network) 4 | dependencies: 5 | "ansible.utils": ">=2.6.1" 6 | license_file: LICENSE 7 | name: sample 8 | namespace: ansible 9 | description: A sample galaxy.yml 10 | readme: README.md 11 | repository: https://github.com/ansible-collections/ansible.sample 12 | issues: https://github.com/ansible-collections/ansible.sample/issues 13 | tags: 14 | - application 15 | - cloud 16 | - database 17 | - infrastructure 18 | - linux 19 | - monitoring 20 | - networking 21 | - security 22 | - storage 23 | - tools 24 | - windows 25 | version: 1.0.0 26 | build_ignore: 27 | - .github 28 | - .gitignore 29 | - .isort.cfg 30 | - .pre-commit-config.yaml 31 | - mypy.ini 32 | - pyproject.toml 33 | - tox.ini 34 | - toxfile.py 35 | - .vscode 36 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Tox Ansible Documentation 2 | 3 | > Need help or want to discuss the project? See our [Contributor guide](https://ansible.readthedocs.io/projects/tox-ansible/contributor_guide/#talk-to-us) to join the conversation! 4 | 5 | ## About Tox Ansible 6 | 7 | `tox-ansible` is a utility designed to simplify the testing of ansible content collections. 8 | 9 | Implemented as `tox` plugin, `tox-ansible` provides a simple way to test ansible content collections across multiple python interpreter and ansible versions. 10 | 11 | `tox-ansible` uses familiar python testing tools to perform the actual testing. It uses `tox` to create and manage the testing environments, `ansible-test sanity` to run the sanity tests, and `pytest` to run the unit and integration tests. This eliminated the black box nature of other approaches and allows for more control over the testing process. 12 | 13 | When used on a local development system, each of the environments are left intact after a test run. This allows for easy debugging of failed tests for a given test type, python interpreter and ansible version. 14 | 15 | By using `tox` to create and manage the testing environments, Test outcomes should always be the same on a local development system as they are in a CI/CD pipeline. 16 | 17 | `tox` virtual environments are created in the `.tox` directory. These are easily deleted and recreated if needed. 18 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > Need help or want to discuss the project? See our [Contributor guide](https://ansible.readthedocs.io/projects/tox-ansible/contributor_guide/#talk-to-us) to join the conversation! 4 | 5 | Getting started with tox-ansible is as simple as: 6 | 7 | ```bash 8 | pip install tox-ansible 9 | ``` 10 | 11 | ## Dependencies 12 | 13 | `tox-ansible` will install additional dependencies if necessary: 14 | 15 | - `tox` version 4.0 or greater. 16 | - `pytest-ansible` version 3.1.0 or greater. 17 | - `pytest` 18 | - `pytest-xdist` 19 | - `pyyaml` 20 | -------------------------------------------------------------------------------- /docs/integration.ini: -------------------------------------------------------------------------------- 1 | [testenv:integration-py3.11-devel] 2 | type = VirtualEnvRunner 3 | set_env = 4 | ANSIBLE_COLLECTIONS_PATH=/home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collections/ 5 | PIP_DISABLE_PIP_VERSION_CHECK=1 6 | PYTHONHASHSEED=3119075902 7 | PYTHONIOENCODING=utf-8 8 | base = testenv 9 | runner = virtualenv 10 | description = Integration tests using ansible-core devel and python 3.11 11 | depends = 12 | env_name = integration-py3.11-devel 13 | labels = 14 | env_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel 15 | env_tmp_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp 16 | env_log_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/log 17 | suicide_timeout = 0.0 18 | interrupt_timeout = 0.3 19 | terminate_timeout = 0.2 20 | platform = 21 | pass_env = 22 | CC 23 | CCSHARED 24 | CFLAGS 25 | CPPFLAGS 26 | CURL_CA_BUNDLE 27 | CXX 28 | GITHUB_TOKEN 29 | HOME 30 | LANG 31 | LANGUAGE 32 | LDFLAGS 33 | LD_LIBRARY_PATH 34 | PIP_* 35 | PKG_CONFIG 36 | PKG_CONFIG_PATH 37 | PKG_CONFIG_SYSROOT_DIR 38 | REQUESTS_CA_BUNDLE 39 | SSL_CERT_FILE 40 | TMPDIR 41 | VIRTUALENV_* 42 | http_proxy 43 | https_proxy 44 | no_proxy 45 | parallel_show_output = False 46 | recreate = False 47 | allowlist_externals = 48 | bash 49 | cp 50 | git 51 | rm 52 | rsync 53 | mkdir 54 | cd 55 | echo 56 | pip_pre = False 57 | install_command = python -I -m pip install '{packages}' 58 | constrain_package_deps = False 59 | use_frozen_constraints = False 60 | list_dependencies_command = python3 -m pip freeze --all 61 | commands_pre = 62 | mkdir /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collection_build 63 | bash -c 'cd /home/bthornto/github/tox-ansible/docs && rsync -r --cvs-exclude --filter=":- .gitignore" . /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collection_build' 64 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collection_build && ansible-galaxy collection build && ansible-galaxy collection install ansible-sample-*.tar.gz -p /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collections' 65 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collections/ansible_collections/ansible/sample && git config --global init.defaultBranch main && git init .' 66 | commands = python3 -m pytest --ansible-unit-inject-only /home/bthornto/github/tox-ansible/docs/tests/integration 67 | commands_post = 68 | change_dir = /home/bthornto/github/tox-ansible/docs 69 | args_are_paths = True 70 | ignore_errors = False 71 | ignore_outcome = False 72 | base_python = py3.11 73 | env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/lib/python3.11/site-packages 74 | env_bin_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/bin 75 | env_python = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/bin/python 76 | py_dot_ver = 3.11 77 | py_impl = cpython 78 | deps = 79 | pytest 80 | pytest-xdist 81 | git+https://github.com/ansible-community/pytest-ansible.git 82 | https://github.com/ansible/ansible/archive/devel.tar.gz 83 | system_site_packages = False 84 | always_copy = False 85 | download = False 86 | skip_install = True 87 | package = skip 88 | -------------------------------------------------------------------------------- /docs/sanity.ini: -------------------------------------------------------------------------------- 1 | [testenv:sanity-py3.11-devel] 2 | type = VirtualEnvRunner 3 | set_env = 4 | ANSIBLE_COLLECTIONS_PATH=/home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collections/ 5 | PIP_DISABLE_PIP_VERSION_CHECK=1 6 | PYTHONHASHSEED=3868722208 7 | PYTHONIOENCODING=utf-8 8 | base = testenv 9 | runner = virtualenv 10 | description = Sanity tests using ansible-core devel and python 3.11 11 | depends = 12 | env_name = sanity-py3.11-devel 13 | labels = 14 | env_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel 15 | env_tmp_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp 16 | env_log_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/log 17 | suicide_timeout = 0.0 18 | interrupt_timeout = 0.3 19 | terminate_timeout = 0.2 20 | platform = 21 | pass_env = 22 | CC 23 | CCSHARED 24 | CFLAGS 25 | CPPFLAGS 26 | CURL_CA_BUNDLE 27 | CXX 28 | GITHUB_TOKEN 29 | HOME 30 | LANG 31 | LANGUAGE 32 | LDFLAGS 33 | LD_LIBRARY_PATH 34 | PIP_* 35 | PKG_CONFIG 36 | PKG_CONFIG_PATH 37 | PKG_CONFIG_SYSROOT_DIR 38 | REQUESTS_CA_BUNDLE 39 | SSL_CERT_FILE 40 | TMPDIR 41 | VIRTUALENV_* 42 | http_proxy 43 | https_proxy 44 | no_proxy 45 | parallel_show_output = False 46 | recreate = False 47 | allowlist_externals = 48 | bash 49 | cp 50 | git 51 | rm 52 | rsync 53 | mkdir 54 | cd 55 | echo 56 | pip_pre = False 57 | install_command = python -I -m pip install '{packages}' 58 | constrain_package_deps = False 59 | use_frozen_constraints = False 60 | list_dependencies_command = python3 -m pip freeze --all 61 | commands_pre = 62 | mkdir /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collection_build 63 | bash -c 'cd /home/bthornto/github/tox-ansible/docs && rsync -r --cvs-exclude --filter=":- .gitignore" . /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collection_build' 64 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collection_build && ansible-galaxy collection build && ansible-galaxy collection install ansible-sample-*.tar.gz -p /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collections' 65 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collections/ansible_collections/ansible/sample && git config --global init.defaultBranch main && git init .' 66 | commands = bash -c 'cd /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collections/ansible_collections/ansible/sample && ansible-test sanity --local --requirements --python 3.11' 67 | commands_post = 68 | change_dir = /home/bthornto/github/tox-ansible/docs 69 | args_are_paths = True 70 | ignore_errors = False 71 | ignore_outcome = False 72 | base_python = py3.11 73 | env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/lib/python3.11/site-packages 74 | env_bin_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/bin 75 | env_python = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/bin/python 76 | py_dot_ver = 3.11 77 | py_impl = cpython 78 | deps = https://github.com/ansible/ansible/archive/devel.tar.gz 79 | system_site_packages = False 80 | always_copy = False 81 | download = False 82 | skip_install = True 83 | package = skip 84 | -------------------------------------------------------------------------------- /docs/tox-ansible.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/tox-ansible/abf6109fa79904c88eb2baf3a0ead0bfcf4d31fe/docs/tox-ansible.ini -------------------------------------------------------------------------------- /docs/unit.ini: -------------------------------------------------------------------------------- 1 | [testenv:unit-py3.11-devel] 2 | type = VirtualEnvRunner 3 | set_env = 4 | ANSIBLE_COLLECTIONS_PATH=/home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections/ 5 | PIP_DISABLE_PIP_VERSION_CHECK=1 6 | PYTHONHASHSEED=1318243417 7 | PYTHONIOENCODING=utf-8 8 | base = testenv 9 | runner = virtualenv 10 | description = Unit tests using ansible-core devel and python 3.11 11 | depends = 12 | env_name = unit-py3.11-devel 13 | labels = 14 | env_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel 15 | env_tmp_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp 16 | env_log_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/log 17 | suicide_timeout = 0.0 18 | interrupt_timeout = 0.3 19 | terminate_timeout = 0.2 20 | platform = 21 | pass_env = 22 | CC 23 | CCSHARED 24 | CFLAGS 25 | CPPFLAGS 26 | CURL_CA_BUNDLE 27 | CXX 28 | GITHUB_TOKEN 29 | HOME 30 | LANG 31 | LANGUAGE 32 | LDFLAGS 33 | LD_LIBRARY_PATH 34 | PIP_* 35 | PKG_CONFIG 36 | PKG_CONFIG_PATH 37 | PKG_CONFIG_SYSROOT_DIR 38 | REQUESTS_CA_BUNDLE 39 | SSL_CERT_FILE 40 | TMPDIR 41 | VIRTUALENV_* 42 | http_proxy 43 | https_proxy 44 | no_proxy 45 | parallel_show_output = False 46 | recreate = False 47 | allowlist_externals = 48 | bash 49 | cp 50 | git 51 | rm 52 | rsync 53 | mkdir 54 | cd 55 | echo 56 | pip_pre = False 57 | install_command = python -I -m pip install '{packages}' 58 | constrain_package_deps = False 59 | use_frozen_constraints = False 60 | list_dependencies_command = python3 -m pip freeze --all 61 | commands_pre = 62 | mkdir /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collection_build 63 | bash -c 'cd /home/bthornto/github/tox-ansible/docs && rsync -r --cvs-exclude --filter=":- .gitignore" . /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collection_build' 64 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collection_build && ansible-galaxy collection build && ansible-galaxy collection install ansible-sample-*.tar.gz -p /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections' 65 | bash -c 'cd /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections/ansible_collections/ansible/sample && git config --global init.defaultBranch main && git init .' 66 | commands = bash -c 'cd /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections/ && python3 -m pytest --ansible-unit-inject-only /home/bthornto/github/tox-ansible/docs/tests/unit' 67 | commands_post = 68 | change_dir = /home/bthornto/github/tox-ansible/docs 69 | args_are_paths = True 70 | ignore_errors = False 71 | ignore_outcome = False 72 | base_python = py3.11 73 | env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/lib/python3.11/site-packages 74 | env_bin_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/bin 75 | env_python = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/bin/python 76 | py_dot_ver = 3.11 77 | py_impl = cpython 78 | deps = 79 | pytest 80 | pytest-xdist 81 | git+https://github.com/ansible-community/pytest-ansible.git 82 | https://github.com/ansible/ansible/archive/devel.tar.gz 83 | system_site_packages = False 84 | always_copy = False 85 | download = False 86 | skip_install = True 87 | package = skip 88 | -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # Usage of tox-ansible 2 | 3 | > Need help or want to discuss the project? See our [Contributor guide](https://ansible.readthedocs.io/projects/tox-ansible/contributor_guide/#talk-to-us) to learn how to join the conversation! 4 | 5 | From the root of your collection, create an empty `tox-ansible.ini` file and list the available environments: 6 | 7 | ```bash 8 | touch tox-ansible.ini 9 | tox list --ansible --conf tox-ansible.ini 10 | ``` 11 | 12 | A list of dynamically generated Ansible environments will be displayed: 13 | 14 | ``` 15 | 16 | default environments: 17 | ... 18 | integration-py3.11-2.14 -> Integration tests for ansible.scm using ansible-core 2.14 and python 3.11 19 | integration-py3.11-devel -> Integration tests for ansible.scm using ansible-core devel and python 3.11 20 | integration-py3.11-milestone -> Integration tests for ansible.scm using ansible-core milestone and python 3.11 21 | ... 22 | sanity-py3.11-2.14 -> Sanity tests for ansible.scm using ansible-core 2.14 and python 3.11 23 | sanity-py3.11-devel -> Sanity tests for ansible.scm using ansible-core devel and python 3.11 24 | sanity-py3.11-milestone -> Sanity tests for ansible.scm using ansible-core milestone and python 3.11 25 | ... 26 | unit-py3.11-2.14 -> Unit tests for ansible.scm using ansible-core 2.14 and python 3.11 27 | unit-py3.11-devel -> Unit tests for ansible.scm using ansible-core devel and python 3.11 28 | unit-py3.11-milestone -> Unit tests for ansible.scm using ansible-core milestone and python 3.11 29 | ``` 30 | 31 | These represent the available testing environments. Each denotes the type of tests that will be run, the Python interpreter used to run the tests, and the Ansible version used to run the tests. 32 | 33 | To run tests with a single environment, simply run the following command: 34 | 35 | ```bash 36 | tox -e sanity-py3.11-2.14 --ansible --conf tox-ansible.ini 37 | ``` 38 | 39 | To run tests with multiple environments, simply add the environment names to the command: 40 | 41 | ```bash 42 | tox -e sanity-py3.11-2.14,unit-py3.11-2.14 --ansible --conf tox-ansible.ini 43 | ``` 44 | 45 | To run all tests of a specific type in all available environments, use the factor `-f` flag: 46 | 47 | ```bash 48 | tox -f unit --ansible -p auto --conf tox-ansible.ini 49 | ``` 50 | 51 | To run all tests across all available environments: 52 | 53 | ```bash 54 | tox --ansible -p auto --conf tox-ansible.ini 55 | ``` 56 | 57 | Note: The `-p auto` flag will run multiple tests in parallel. 58 | Note: The specific Python interpreter will need to be pre-installed on your system, e.g.: 59 | 60 | ```bash 61 | sudo dnf install python3.9 62 | ``` 63 | 64 | To review the specific commands and configuration for each of the integration, sanity, and unit factors: 65 | 66 | ```bash 67 | tox config --ansible --conf tox-ansible.ini 68 | ``` 69 | 70 | Generate specific GitHub action matrix as per scope mentioned with `--matrix-scope`: 71 | 72 | ```bash 73 | tox --ansible --gh-matrix --matrix-scope unit --conf tox-ansible.ini 74 | ``` 75 | 76 | A list of dynamically generated Ansible environments will be displayed specifically for unit tests: 77 | 78 | ``` 79 | [ 80 | { 81 | "description": "Unit tests using ansible 2.9 and python 3.8", 82 | "factors": [ 83 | "unit", 84 | "py3.8", 85 | "2.9" 86 | ], 87 | "name": "unit-py3.8-2.9", 88 | "python": "3.8" 89 | }, 90 | ... 91 | { 92 | "description": "Unit tests using ansible-core milestone and python 3.12", 93 | "factors": [ 94 | "unit", 95 | "py3.12", 96 | "milestone" 97 | ], 98 | "name": "unit-py3.12-milestone", 99 | "python": "3.12" 100 | } 101 | ] 102 | ``` 103 | 104 | ## Passing command line arguments to ansible-test / pytest 105 | 106 | The behavior of the `ansible-test` (for `sanity-*` environments) or `pytest` (for `unit-*` and `integration-*` environments) commands can be customized by passing further command line arguments to it, e.g., by invoking `tox` like this: 107 | 108 | ```bash 109 | tox -f sanity --ansible --conf tox-ansible.ini -- --test validate-modules -vvv 110 | ``` 111 | 112 | The arguments after the `--` will be passed to the `ansible-test` command. Thus in this example, only the `validate-modules` sanity test will run, but with an increased verbosity. 113 | 114 | Same can be done to pass arguments to the `pytest` commands for the `unit-*` and `integration-*` environments: 115 | 116 | ```bash 117 | tox -e unit-py3.13-2.18 --ansible --conf tox-ansible.ini -- --junit-xml=tests/output/junit/unit.xml 118 | ``` 119 | 120 | ## Usage in a CI/CD pipeline 121 | 122 | The repo contains a GitHub workflow that can be used in a GitHub actions CI/CD pipeline. The workflow will run all tests across all available environments unless limited by the `skip` option in the `tox-ansible.ini` file. 123 | 124 | Each environment will be run in a separate job. The workflow will run all jobs in parallel. 125 | 126 | The GitHub matrix is dynamically created by `tox-ansible` using the `--gh-matrix` and `--ansible` flags. The list of environments is converted to a list of entries in json format and added to the file specified by the "GITHUB_OUTPUT" environment variable. The workflow will read this file and use it to create the matrix. 127 | 128 | A sample use of the GitHub workflow might look like this: 129 | 130 | ```yaml 131 | name: Test collection 132 | 133 | concurrency: 134 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 135 | cancel-in-progress: true 136 | 137 | on: 138 | pull_request: 139 | branches: [main] 140 | workflow_dispatch: 141 | 142 | jobs: 143 | tox-ansible: 144 | uses: ansible/tox-ansible/.github/workflows/run.yml@main 145 | ``` 146 | 147 | Sample `json` 148 | 149 | ```json 150 | [ 151 | // ... 152 | { 153 | "description": "Integration tests using ansible-core devel and python 3.11", 154 | "factors": ["integration", "py3.11", "devel"], 155 | "name": "integration-py3.11-devel", 156 | "python": "3.11" 157 | } 158 | // ... 159 | ] 160 | ``` 161 | 162 | ## Testing molecule scenarios 163 | 164 | Although the `tox-ansible` plugin does not have functionality specific to molecule, it can be a powerful tool to run `molecule` scenarios across a matrix of Ansible and Python versions. 165 | 166 | This can be accomplished by presenting molecule scenarios as integration tests available through `pytest` using the [pytest-ansible](https://github.com/ansible-community/pytest-ansible) plugin, which is installed when `tox-ansible` is installed. 167 | 168 | Assuming the following collection directory structure: 169 | 170 | ```bash 171 | namespace.name 172 | ├── extensions 173 | │ ├── molecule 174 | │ │ ├── playbook 175 | │ │ │ ├── create.yml 176 | │ │ │ ├── converge.yml 177 | │ │ │ ├── molecule.yml 178 | │ │ │ └── verify.yml 179 | │ │ ├── plugins 180 | │ │ │ ├── create.yml 181 | │ │ │ ├── converge.yml 182 | │ │ │ ├── molecule.yml 183 | │ │ │ └── verify.yml 184 | │ │ ├── targets 185 | │ │ │ ├── create.yml 186 | │ │ │ ├── converge.yml 187 | │ │ │ ├── molecule.yml 188 | │ │ │ └── verify.yml 189 | ├── playbooks 190 | │ └── site.yaml 191 | ├── plugins 192 | │ ├── action 193 | │ │ └── action_plugin.py 194 | │ ├── modules 195 | │ │ └── module.py 196 | ├── tests 197 | │ ├── integration 198 | │ │ │── targets 199 | │ │ │ ├── success 200 | │ │ │ │ └── tasks 201 | │ │ │ │ └── main.yaml 202 | │ │ └── test_integration.py 203 | ├── tox-ansible.ini 204 | └── tox.ini 205 | ``` 206 | 207 | Individual `molecule` scenarios can be added to the collection's extension directory to test playbooks, roles, and integration targets. 208 | 209 | In order to present each `molecule` scenario as an individual `pytest` test a new `helper` file is added. 210 | 211 | ```python 212 | # tests/integration/test_integration.py 213 | 214 | """Tests for molecule scenarios.""" 215 | from __future__ import absolute_import, division, print_function 216 | 217 | from pytest_ansible.molecule import MoleculeScenario 218 | 219 | 220 | def test_integration(molecule_scenario: MoleculeScenario) -> None: 221 | """Run molecule for each scenario. 222 | 223 | :param molecule_scenario: The molecule scenario object 224 | """ 225 | proc = molecule_scenario.test() 226 | assert proc.returncode == 0 227 | ``` 228 | 229 | The `molecule_scenario` fixture parametrizes the `molecule` scenarios found within the collection and creates an individual `pytest` test for each which will be run during any `integration-*` environment. 230 | 231 | This approach provides the flexibility of running the `molecule` scenarios directly with `molecule`, `pytest` or `tox`. Additionally, presented as native `pytest` tests, the `molecule` scenarios should show in the `pytest` test tree in the user's IDE. 232 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: Tox Ansible Documentation 3 | site_url: https://ansible.readthedocs.io/projects/tox-ansible/ 4 | repo_url: https://github.com/ansible/tox-ansible 5 | edit_uri: blob/main/docs/ 6 | copyright: Copyright © 2024 Red Hat, Inc. 7 | docs_dir: docs 8 | strict: true 9 | 10 | theme: 11 | name: ansible 12 | features: 13 | - content.code.copy 14 | - content.action.edit 15 | - navigation.expand 16 | - navigation.sections 17 | - navigation.instant 18 | - navigation.indexes 19 | - navigation.tracking 20 | - toc.integrate 21 | 22 | nav: 23 | - User Guide: 24 | - Home: index.md 25 | - Installation: installation.md 26 | - Configuration: configuration.md 27 | - User Guide: user_guide.md 28 | - FAQ: faq.md 29 | - Contributor Guide: contributor_guide.md 30 | 31 | plugins: 32 | - autorefs 33 | - markdown-exec 34 | - material/search: 35 | separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 36 | - material/tags 37 | - mkdocstrings: 38 | handlers: 39 | python: 40 | paths: [src] 41 | options: 42 | # Sphinx is for historical reasons, but we could consider switching if needed 43 | # https://mkdocstrings.github.io/griffe/docstrings/ 44 | docstring_style: sphinx 45 | merge_init_into_class: yes 46 | show_submodules: yes 47 | import: 48 | - url: https://docs.ansible.com/ansible/latest/objects.inv 49 | domains: [py, std] 50 | 51 | markdown_extensions: 52 | - markdown_include.include: 53 | base_path: docs 54 | - admonition 55 | - def_list 56 | - footnotes 57 | - pymdownx.highlight: 58 | anchor_linenums: true 59 | - pymdownx.inlinehilite 60 | - pymdownx.superfences 61 | - pymdownx.magiclink: 62 | repo_url_shortener: true 63 | repo_url_shorthand: true 64 | social_url_shorthand: true 65 | social_url_shortener: true 66 | user: facelessuser 67 | repo: pymdown-extensions 68 | normalize_issue_symbols: true 69 | - pymdownx.tabbed: 70 | alternate_style: true 71 | - toc: 72 | toc_depth: 2 73 | permalink: true 74 | - pymdownx.superfences: 75 | custom_fences: 76 | - name: mermaid 77 | class: mermaid 78 | format: !!python/name:pymdownx.superfences.fence_code_format 79 | - name: python 80 | class: python 81 | validator: !!python/name:markdown_exec.validator 82 | format: !!python/name:markdown_exec.formatter 83 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools >= 65.3.0", # required by pyproject+setuptools_scm integration and editable installs 5 | "setuptools_scm[toml] >= 7.0.5" # required for "no-local-version" scheme 6 | ] 7 | 8 | [project] 9 | authors = [{"email" = "bthornto@redhat.com", "name" = "Bradley A. Thornton"}] 10 | classifiers = [ 11 | 'Development Status :: 5 - Production/Stable', 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Operating System :: OS Independent', 15 | 'Topic :: Software Development :: Testing', 16 | 'Topic :: Software Development :: Quality Assurance', 17 | 'Topic :: Utilities', 18 | 'Programming Language :: Python', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python :: 3 :: Only', 21 | 'Programming Language :: Python :: 3.10', 22 | 'Programming Language :: Python :: 3.11', 23 | 'Programming Language :: Python :: 3.12', 24 | 'Programming Language :: Python :: 3.13', 25 | 'Programming Language :: Python :: Implementation :: CPython', 26 | 'Programming Language :: Python :: Implementation :: PyPy' 27 | ] 28 | description = "A radical approach to testing ansible content" 29 | dynamic = ["dependencies", "optional-dependencies", "version"] 30 | keywords = ["ansible", "collections", "tox"] 31 | license = {text = "MIT"} 32 | maintainers = [{"email" = "info@ansible.com", "name" = "Ansible by Red Hat"}] 33 | name = "tox-ansible" 34 | readme = "README.md" 35 | requires-python = ">=3.10" 36 | 37 | [project.entry-points.tox] 38 | tox-ansible = "tox_ansible.plugin" 39 | 40 | [project.urls] 41 | changelog = "https://github.com/ansible/tox-ansible/releases" 42 | documentation = "https://ansible.readthedocs.io/projects/tox-ansible/" 43 | homepage = "https://github.com/ansible/tox-ansible" 44 | repository = "https://github.com/ansible/tox-ansible" 45 | 46 | [tool.coverage.report] 47 | exclude_also = ["if TYPE_CHECKING:", "pragma: no cover"] 48 | fail_under = 81 49 | ignore_errors = true 50 | show_missing = true 51 | skip_covered = true 52 | skip_empty = true 53 | sort = "Cover" 54 | 55 | [tool.coverage.run] 56 | branch = false # https://github.com/nedbat/coveragepy/issues/605 57 | concurrency = ["multiprocessing", "thread"] 58 | omit = ["_version.py"] 59 | parallel = true 60 | source_pkgs = ["tox_ansible"] 61 | 62 | [tool.mypy] 63 | cache_dir = "./.cache/.mypy" 64 | files = ["src", "tests"] 65 | strict = true 66 | 67 | [tool.pydoclint] 68 | allow-init-docstring = true 69 | arg-type-hints-in-docstring = false 70 | auto-regenerate-baseline = true 71 | baseline = ".config/pydoclint-baseline.txt" 72 | check-return-types = false 73 | exclude = '\.cache|\.eggs|\.git|\.mypy_cache|\.tox|build|dist|out|site|\.?venv' 74 | should-document-private-class-attributes = true 75 | show-filenames-in-every-violation-message = true 76 | skip-checking-short-docstrings = false 77 | style = "google" 78 | 79 | [tool.pylint] 80 | 81 | [tool.pylint.format] 82 | max-line-length = 100 83 | 84 | [tool.pylint.master] 85 | good-names = "i,j,k,ex,Run,_,f,fh" 86 | ignore = [ 87 | "_version.py" # built by setuptools_scm 88 | ] 89 | jobs = 0 90 | no-docstring-rgx = "__.*__" 91 | 92 | [tool.pylint.messages_control] 93 | disable = [ 94 | "unknown-option-value", 95 | # https://gist.github.com/cidrblock/ec3412bacfeb34dbc2d334c1d53bef83 96 | "C0103", # invalid-name / ruff N815 97 | "C0105", # typevar-name-incorrect-variance / ruff PLC0105 98 | "C0112", # empty-docstring / ruff D419 99 | "C0113", # unneeded-not / ruff SIM208 100 | "C0114", # missing-module-docstring / ruff D100 101 | "C0115", # missing-class-docstring / ruff D101 102 | "C0116", # missing-function-docstring / ruff D103 103 | "C0121", # singleton-comparison / ruff PLC0121 104 | "C0123", # unidiomatic-typecheck / ruff E721 105 | "C0131", # typevar-double-variance / ruff PLC0131 106 | "C0132", # typevar-name-mismatch / ruff PLC0132 107 | "C0198", # bad-docstring-quotes / ruff Q002 108 | "C0199", # docstring-first-line-empty / ruff D210 109 | "C0201", # consider-iterating-dictionary / ruff SIM118 110 | "C0202", # bad-classmethod-argument / ruff PLC0202 111 | "C0205", # single-string-used-for-slots / ruff PLC0205 112 | "C0208", # use-sequence-for-iteration / ruff PLC0208 113 | "C0301", # line-too-long / ruff E501 114 | "C0303", # trailing-whitespace / ruff W291 115 | "C0304", # missing-final-newline / ruff W292 116 | "C0321", # multiple-statements / ruff PLC0321 117 | "C0410", # multiple-imports / ruff E401 118 | "C0411", # wrong-import-order / ruff I001 119 | "C0412", # ungrouped-imports / ruff I001 120 | "C0413", # wrong-import-position / ruff E402 121 | "C0414", # useless-import-alias / ruff PLC0414 122 | "C0415", # import-outside-toplevel / ruff PLC0415 123 | "C0501", # consider-using-any-or-all / ruff PLC0501 124 | "C1901", # compare-to-empty-string / ruff PLC1901 125 | "C2201", # misplaced-comparison-constant / ruff SIM300 126 | "C2401", # non-ascii-name / ruff PLC2401 127 | "C2403", # non-ascii-module-import / ruff PLC2403 128 | "C2701", # import-private-name / ruff PLC2701 129 | "C2801", # unnecessary-dunder-call / ruff PLC2801 130 | "C3001", # unnecessary-lambda-assignment / ruff E731 131 | "C3002", # unnecessary-direct-lambda-call / ruff PLC3002 132 | "E0001", # syntax-error / ruff E999 133 | "E0100", # init-is-generator / ruff PLE0100 134 | "E0101", # return-in-init / ruff PLE0101 135 | "E0102", # function-redefined / ruff F811 136 | "E0103", # not-in-loop / ruff PLE0103 137 | "E0104", # return-outside-function / ruff F706 138 | "E0105", # yield-outside-function / ruff F704 139 | "E0107", # nonexistent-operator / ruff B002 140 | "E0112", # too-many-star-expressions / ruff F622 141 | "E0115", # nonlocal-and-global / ruff PLE0115 142 | "E0116", # continue-in-finally / ruff PLE0116 143 | "E0117", # nonlocal-without-binding / ruff PLE0117 144 | "E0118", # used-prior-global-declaration / ruff PLE0118 145 | "E0211", # no-method-argument / ruff N805 146 | "E0213", # no-self-argument / ruff N805 147 | "E0237", # assigning-non-slot / ruff PLE0237 148 | "E0241", # duplicate-bases / ruff PLE0241 149 | "E0302", # unexpected-special-method-signature / ruff PLE0302 150 | "E0303", # invalid-length-returned / ruff PLE0303 151 | "E0304", # invalid-bool-returned / ruff PLE0304 152 | "E0305", # invalid-index-returned / ruff PLE0305 153 | "E0308", # invalid-bytes-returned / ruff PLE0308 154 | "E0309", # invalid-hash-returned / ruff PLE0309 155 | "E0402", # relative-beyond-top-level / ruff TID252 156 | "E0602", # undefined-variable / ruff F821 157 | "E0603", # undefined-all-variable / ruff F822 158 | "E0604", # invalid-all-object / ruff PLE0604 159 | "E0605", # invalid-all-format / ruff PLE0605 160 | "E0643", # potential-index-error / ruff PLE0643 161 | "E0704", # misplaced-bare-raise / ruff PLE0704 162 | "E0711", # notimplemented-raised / ruff F901 163 | "E1132", # repeated-keyword / ruff PLE1132 164 | "E1142", # await-outside-async / ruff PLE1142 165 | "E1205", # logging-too-many-args / ruff PLE1205 166 | "E1206", # logging-too-few-args / ruff PLE1206 167 | "E1300", # bad-format-character / ruff PLE1300 168 | "E1301", # truncated-format-string / ruff F501 169 | "E1302", # mixed-format-string / ruff F506 170 | "E1303", # format-needs-mapping / ruff F502 171 | "E1304", # missing-format-string-key / ruff F524 172 | "E1305", # too-many-format-args / ruff F522 173 | "E1306", # too-few-format-args / ruff F524 174 | "E1307", # bad-string-format-type / ruff PLE1307 175 | "E1310", # bad-str-strip-call / ruff PLE1310 176 | "E1519", # singledispatch-method / ruff PLE1519 177 | "E1520", # singledispatchmethod-function / ruff PLE5120 178 | "E1700", # yield-inside-async-function / ruff PLE1700 179 | "E2502", # bidirectional-unicode / ruff PLE2502 180 | "E2510", # invalid-character-backspace / ruff PLE2510 181 | "E2512", # invalid-character-sub / ruff PLE2512 182 | "E2513", # invalid-character-esc / ruff PLE2513 183 | "E2514", # invalid-character-nul / ruff PLE2514 184 | "E2515", # invalid-character-zero-width-space / ruff PLE2515 185 | "E4703", # modified-iterating-set / ruff PLE4703 186 | "R0123", # literal-comparison / ruff F632 187 | "R0124", # comparison-with-itself / ruff PLR0124 188 | "R0133", # comparison-of-constants / ruff PLR0133 189 | "R0202", # no-classmethod-decorator / ruff PLR0202 190 | "R0203", # no-staticmethod-decorator / ruff PLR0203 191 | "R0205", # useless-object-inheritance / ruff UP004 192 | "R0206", # property-with-parameters / ruff PLR0206 193 | "R0904", # too-many-public-methods / ruff PLR0904 194 | "R0911", # too-many-return-statements / ruff PLR0911 195 | "R0912", # too-many-branches / ruff PLR0912 196 | "R0913", # too-many-arguments / ruff PLR0913 197 | "R0914", # too-many-locals / ruff PLR0914 198 | "R0915", # too-many-statements / ruff PLR0915 199 | "R0916", # too-many-boolean-expressions / ruff PLR0916 200 | "R1260", # too-complex / ruff C901 201 | "R1701", # consider-merging-isinstance / ruff PLR1701 202 | "R1702", # too-many-nested-blocks / ruff PLR1702 203 | "R1703", # simplifiable-if-statement / ruff SIM108 204 | "R1704", # redefined-argument-from-local / ruff PLR1704 205 | "R1705", # no-else-return / ruff RET505 206 | "R1706", # consider-using-ternary / ruff PLR1706 207 | "R1707", # trailing-comma-tuple / ruff COM818 208 | "R1710", # inconsistent-return-statements / ruff PLR1710 209 | "R1711", # useless-return / ruff PLR1711 210 | "R1714", # consider-using-in / ruff PLR1714 211 | "R1715", # consider-using-get / ruff SIM401 212 | "R1717", # consider-using-dict-comprehension / ruff C402 213 | "R1718", # consider-using-set-comprehension / ruff C401 214 | "R1719", # simplifiable-if-expression / ruff PLR1719 215 | "R1720", # no-else-raise / ruff RET506 216 | "R1721", # unnecessary-comprehension / ruff C416 217 | "R1722", # consider-using-sys-exit / ruff PLR1722 218 | "R1723", # no-else-break / ruff RET508 219 | "R1724", # no-else-continue / ruff RET507 220 | "R1725", # super-with-arguments / ruff UP008 221 | "R1728", # consider-using-generator / ruff C417 222 | "R1729", # use-a-generator / ruff C419 223 | "R1730", # consider-using-min-builtin / ruff PLR1730 224 | "R1731", # consider-using-max-builtin / ruff PLR1730 225 | "R1732", # consider-using-with / ruff SIM115 226 | "R1733", # unnecessary-dict-index-lookup / ruff PLR1733 227 | "R1734", # use-list-literal / ruff C405 228 | "R1735", # use-dict-literal / ruff C406 229 | "R1736", # unnecessary-list-index-lookup / ruff PLR1736 230 | "R2004", # magic-value-comparison / ruff PLR2004 231 | "R2044", # empty-comment / ruff PLR2044 232 | "R5501", # else-if-used / ruff PLR5501 233 | "R6002", # consider-using-alias / ruff UP006 234 | "R6003", # consider-alternative-union-syntax / ruff UP007 235 | "R6104", # consider-using-augmented-assign / ruff PLR6104 236 | "R6201", # use-set-for-membership / ruff PLR6201 237 | "R6301", # no-self-use / ruff PLR6301 238 | "W0102", # dangerous-default-value / ruff B006 239 | "W0104", # pointless-statement / ruff B018 240 | "W0106", # expression-not-assigned / ruff B018 241 | "W0107", # unnecessary-pass / ruff PIE790 242 | "W0108", # unnecessary-lambda / ruff PLW0108 243 | "W0109", # duplicate-key / ruff F601 244 | "W0120", # useless-else-on-loop / ruff PLW0120 245 | "W0122", # exec-used / ruff S102 246 | "W0123", # eval-used / ruff PGH001 247 | "W0127", # self-assigning-variable / ruff PLW0127 248 | "W0129", # assert-on-string-literal / ruff PLW0129 249 | "W0130", # duplicate-value / ruff B033 250 | "W0131", # named-expr-without-context / ruff PLW0131 251 | "W0133", # pointless-exception-statement / ruff PLW0133 252 | "W0150", # lost-exception / ruff B012 253 | "W0160", # consider-ternary-expression / ruff SIM108 254 | "W0177", # nan-comparison / ruff PLW0117 255 | "W0199", # assert-on-tuple / ruff F631 256 | "W0211", # bad-staticmethod-argument / ruff PLW0211 257 | "W0212", # protected-access / ruff SLF001 258 | "W0245", # super-without-brackets / ruff PLW0245 259 | "W0301", # unnecessary-semicolon / ruff E703 260 | "W0401", # wildcard-import / ruff F403 261 | "W0404", # reimported / ruff F811 262 | "W0406", # import-self / ruff PLW0406 263 | "W0410", # misplaced-future / ruff F404 264 | "W0511", # fixme / ruff PLW0511 265 | "W0602", # global-variable-not-assigned / ruff PLW0602 266 | "W0603", # global-statement / ruff PLW0603 267 | "W0604", # global-at-module-level / ruff PLW0604 268 | "W0611", # unused-import / ruff F401 269 | "W0612", # unused-variable / ruff F841 270 | "W0613", # unused-argument / ruff ARG001 271 | "W0622", # redefined-builtin / ruff A001 272 | "W0640", # cell-var-from-loop / ruff B023 273 | "W0702", # bare-except / ruff E722 274 | "W0705", # duplicate-except / ruff B014 275 | "W0706", # try-except-raise / ruff TRY302 276 | "W0707", # raise-missing-from / ruff TRY200 277 | "W0711", # binary-op-exception / ruff PLW0711 278 | "W0718", # broad-exception-caught / ruff PLW0718 279 | "W0719", # broad-exception-raised / ruff TRY002 280 | "W1113", # keyword-arg-before-vararg / ruff B026 281 | "W1201", # logging-not-lazy / ruff G 282 | "W1202", # logging-format-interpolation / ruff G 283 | "W1203", # logging-fstring-interpolation / ruff G 284 | "W1300", # bad-format-string-key / ruff PLW1300 285 | "W1301", # unused-format-string-key / ruff F504 286 | "W1302", # bad-format-string / ruff PLW1302 287 | "W1303", # missing-format-argument-key / ruff F524 288 | "W1304", # unused-format-string-argument / ruff F507 289 | "W1305", # format-combined-specification / ruff F525 290 | "W1308", # duplicate-string-formatting-argument / ruff PLW1308 291 | "W1309", # f-string-without-interpolation / ruff F541 292 | "W1310", # format-string-without-interpolation / ruff F541 293 | "W1401", # anomalous-backslash-in-string / ruff W605 294 | "W1404", # implicit-str-concat / ruff ISC001 295 | "W1405", # inconsistent-quotes / ruff Q000 296 | "W1406", # redundant-u-string-prefix / ruff UP025 297 | "W1501", # bad-open-mode / ruff PLW1501 298 | "W1508", # invalid-envvar-default / ruff PLW1508 299 | "W1509", # subprocess-popen-preexec-fn / ruff PLW1509 300 | "W1510", # subprocess-run-check / ruff PLW1510 301 | "W1514", # unspecified-encoding / ruff PLW1514 302 | "W1515", # forgotten-debug-statement / ruff T100 303 | "W1518", # method-cache-max-size-none / ruff B019 304 | "W1641", # eq-without-hash / ruff PLW1641 305 | "W2101", # useless-with-lock / ruff PLW2101 306 | "W2402", # non-ascii-file-name / ruff N999 307 | "W2901", # redefined-loop-name / ruff PLW2901 308 | "W3201", # bad-dunder-name / ruff PLW3201 309 | "W3301", # nested-min-max / ruff PLW3301 310 | "duplicate-code", 311 | "too-few-public-methods", 312 | "too-many-instance-attributes" 313 | ] 314 | enable = [ 315 | "useless-suppression" 316 | ] 317 | fail-on = [ 318 | "useless-suppression" 319 | ] 320 | 321 | [tool.pytest.ini_options] 322 | addopts = "-ra --showlocals --durations=10" 323 | cache_dir = "./.cache/.pytest" 324 | junit_family = "legacy" # see https://docs.codecov.com/docs/test-analytics 325 | testpaths = "tests" 326 | tmp_path_retention_policy = "failed" 327 | verbosity_assertions = 2 328 | 329 | [tool.ruff] 330 | builtins = ["__"] 331 | cache-dir = "./.cache/.ruff" 332 | fix = true 333 | line-length = 100 334 | target-version = "py310" 335 | 336 | [tool.ruff.lint] 337 | ignore = [ 338 | "COM812", # conflicts with ISC001 on format 339 | "ISC001" # conflicts with COM812 on format 340 | ] 341 | select = ["ALL"] 342 | 343 | [tool.ruff.lint.flake8-pytest-style] 344 | parametrize-values-type = "tuple" 345 | 346 | [tool.ruff.lint.isort] 347 | known-first-party = ["tox_ansible"] 348 | lines-after-imports = 2 # Ensures consistency for cases when there's variable vs function/class definitions after imports 349 | lines-between-types = 1 # Separate import/from with 1 line 350 | required-imports = ["from __future__ import annotations"] 351 | 352 | [tool.ruff.lint.per-file-ignores] 353 | "_version.py" = ["SIM108"] 354 | "tests/**" = ["SLF001", "S101", "S602", "T201"] 355 | 356 | [tool.ruff.lint.pydocstyle] 357 | convention = "google" 358 | 359 | [tool.setuptools.dynamic] 360 | dependencies = {file = [".config/requirements.in"]} 361 | optional-dependencies.docs = {file = [".config/requirements-docs.in"]} 362 | optional-dependencies.test = {file = [".config/requirements-test.in"]} 363 | 364 | [tool.setuptools_scm] 365 | # To prevent accidental pick of mobile version tags such 'v6' 366 | git_describe_command = [ 367 | "git", 368 | "describe", 369 | "--dirty", 370 | "--long", 371 | "--tags", 372 | "--match", 373 | "v*.*" 374 | ] 375 | local_scheme = "no-local-version" 376 | tag_regex = "^(?Pv)?(?P\\d+[^\\+]*)(?P.*)?$" 377 | write_to = "src/tox_ansible/_version.py" 378 | 379 | [tool.tomlsort] 380 | in_place = true 381 | sort_inline_tables = true 382 | sort_table_keys = true 383 | 384 | [tool.uv.pip] 385 | annotation-style = "line" 386 | custom-compile-command = "tox run -e deps" 387 | no-emit-package = [ 388 | "ansible-core", 389 | "exceptiongroup", 390 | "pip", 391 | "resolvelib", 392 | "ruamel-yaml-clib", 393 | "tomli", 394 | "typing_extensions", 395 | "uv" 396 | ] 397 | -------------------------------------------------------------------------------- /src/tox_ansible/__init__.py: -------------------------------------------------------------------------------- 1 | """The tox-ansible package.""" 2 | -------------------------------------------------------------------------------- /src/tox_ansible/plugin.py: -------------------------------------------------------------------------------- 1 | # cspell:ignore envlist 2 | """tox plugin to emit a github matrix.""" 3 | 4 | from __future__ import annotations 5 | 6 | import json 7 | import logging 8 | import os 9 | import re 10 | import sys 11 | import uuid 12 | 13 | from dataclasses import asdict, dataclass, field 14 | from pathlib import Path 15 | from typing import TYPE_CHECKING, TypeVar 16 | 17 | import yaml 18 | 19 | from tox.config.loader.memory import MemoryLoader 20 | from tox.config.loader.section import Section 21 | from tox.config.loader.str_convert import StrConvert 22 | from tox.config.sets import ConfigSet, CoreConfigSet, EnvConfigSet 23 | from tox.plugin import impl 24 | from tox.tox_env.python.api import PY_FACTORS_RE 25 | 26 | 27 | if TYPE_CHECKING: 28 | from tox.config.cli.parser import ToxParser 29 | from tox.config.types import EnvList 30 | from tox.session.state import State 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | ALLOWED_EXTERNALS = [ 35 | "bash", 36 | "sh", 37 | "cp", 38 | "git", 39 | "rm", 40 | "mkdir", 41 | "cd", 42 | "echo", 43 | "dirname", 44 | ] 45 | ENV_LIST = """ 46 | {integration, sanity, unit}-py3.10-{2.17} 47 | {integration, sanity, unit}-py3.11-{2.17, 2.18, milestone, devel} 48 | {integration, sanity, unit}-py3.12-{2.17, 2.18, milestone, devel} 49 | {integration, sanity, unit}-py3.13-{2.18, milestone, devel} 50 | """ 51 | TOX_WORK_DIR = Path() 52 | # Without the minimal pytest-ansible condition, installation may fail in some 53 | # cases (pip, uv). 54 | OUR_DEPS = [ 55 | "pytest>=7.4.3", # Oct 2023 56 | "pytest-xdist>=3.4.0", # Nov 2023 57 | "pytest-ansible>=v4.1.1", # latest version still supporting py39 (Oct 2023) 58 | ] 59 | 60 | T = TypeVar("T", bound=ConfigSet) 61 | 62 | 63 | class AnsibleConfigSet(ConfigSet): 64 | """The ansible configuration.""" 65 | 66 | def register_config(self) -> None: 67 | """Register the ansible configuration.""" 68 | self.add_config( 69 | "skip", 70 | of_type=list[str], 71 | default=[], 72 | desc="ansible configuration", 73 | ) 74 | 75 | 76 | @dataclass 77 | class AnsibleTestConf: # pylint: disable=too-many-instance-attributes 78 | """Ansible test configuration. 79 | 80 | Attributes: 81 | description: The description of the test. 82 | deps: The dependencies for the test. 83 | setenv: The set environment variables for the test. 84 | skip_install: Skip the installation. 85 | allowlist_externals: The allowed external commands. 86 | commands_pre: The pre-run commands. 87 | commands: The commands to run. 88 | passenv: The pass environment 89 | """ 90 | 91 | description: str 92 | deps: str 93 | setenv: str 94 | skip_install: bool 95 | allowlist_externals: list[str] = field(default_factory=list) 96 | commands_pre: list[str] = field(default_factory=list) 97 | commands: list[str] = field(default_factory=list) 98 | passenv: list[str] = field(default_factory=list) 99 | 100 | 101 | def custom_sort(string: str) -> tuple[int, ...]: 102 | """Convert a env name into a tuple of ints. 103 | 104 | In the case of a string, use the ord() of the first two characters. 105 | 106 | Args: 107 | string: The string to sort. 108 | 109 | Returns: 110 | The tuple of converted values. 111 | """ 112 | parts = re.split(r"\.|-|py", string) 113 | converted = [] 114 | for part in parts: 115 | if not part: 116 | continue 117 | try: 118 | converted.append(int(part)) 119 | except ValueError: 120 | num_part = "".join((str(ord(char)).rjust(3, "0")) for char in part[0:2]) 121 | converted.append(int(num_part)) 122 | return tuple(converted) 123 | 124 | 125 | @impl 126 | def tox_add_option(parser: ToxParser) -> None: 127 | """Add the --gh-matrix option to the tox CLI. 128 | 129 | Args: 130 | parser: The tox CLI parser. 131 | """ 132 | parser.add_argument( 133 | "--matrix-scope", 134 | default="all", 135 | choices=["all", "sanity", "integration", "unit"], 136 | help="Emit a github matrix specific to scope mentioned", 137 | ) 138 | 139 | parser.add_argument( 140 | "--gh-matrix", 141 | action="store_true", 142 | default=False, 143 | help="Emit a github matrix", 144 | ) 145 | 146 | parser.add_argument( 147 | "--ansible", 148 | action="store_true", 149 | default=False, 150 | help="Enable ansible testing", 151 | ) 152 | 153 | 154 | @impl 155 | def tox_add_core_config( 156 | core_conf: CoreConfigSet, # noqa: ARG001 # pylint: disable=unused-argument 157 | state: State, 158 | ) -> None: 159 | """Dump the environment list and exit. 160 | 161 | Args: 162 | core_conf: The core configuration object. 163 | state: The state object. 164 | """ 165 | if state.conf.options.gh_matrix and not state.conf.options.ansible: 166 | err = "The --gh-matrix option requires --ansible" 167 | logger.critical(err) 168 | sys.exit(1) 169 | 170 | if not state.conf.options.ansible: 171 | return 172 | 173 | if state.conf.src_path.name == "tox.ini": 174 | msg = ( 175 | "Using a default tox.ini file with tox-ansible plugin is not recommended." 176 | " Consider using a tox-ansible.ini file and specify it on the command line" 177 | " (`tox --ansible -c tox-ansible.ini`) to avoid unintentionally overriding" 178 | " the tox-ansible environment configurations." 179 | ) 180 | logger.warning(msg) 181 | 182 | global TOX_WORK_DIR # pylint: disable=global-statement # noqa: PLW0603 183 | TOX_WORK_DIR = state.conf.work_dir 184 | env_list = add_ansible_matrix(state) 185 | 186 | if not state.conf.options.gh_matrix: 187 | return 188 | 189 | generate_gh_matrix(env_list=env_list, section=state.conf.options.matrix_scope) 190 | sys.exit(0) 191 | 192 | 193 | @impl 194 | def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: 195 | """Add the test requirements and ansible-core to the virtual environment. 196 | 197 | Args: 198 | env_conf: The environment configuration object. 199 | state: The state object. 200 | """ 201 | if not state.conf.options.ansible: 202 | return 203 | 204 | factors = env_conf.name.split("-") 205 | expected_factors = 3 206 | if len(factors) != expected_factors or factors[0] not in [ 207 | "integration", 208 | "sanity", 209 | "unit", 210 | ]: 211 | return 212 | 213 | galaxy_path = TOX_WORK_DIR / "galaxy.yml" 214 | c_name, c_namespace = get_collection_name(galaxy_path=galaxy_path) 215 | pos_args = state.conf.pos_args(to_path=None) 216 | 217 | conf = AnsibleTestConf( 218 | allowlist_externals=ALLOWED_EXTERNALS, 219 | commands_pre=conf_commands_pre( 220 | c_name=c_name, 221 | c_namespace=c_namespace, 222 | env_conf=env_conf, 223 | ), 224 | commands=conf_commands( 225 | c_name=c_name, 226 | c_namespace=c_namespace, 227 | env_conf=env_conf, 228 | pos_args=pos_args, 229 | test_type=factors[0], 230 | ), 231 | description=desc_for_env(env_conf.name), 232 | deps=conf_deps(env_conf=env_conf, test_type=factors[0]), 233 | passenv=conf_passenv(), 234 | setenv=conf_setenv(env_conf), 235 | skip_install=True, 236 | ) 237 | loader = MemoryLoader(**asdict(conf)) 238 | env_conf.loaders.append(loader) 239 | 240 | 241 | def desc_for_env(env: str) -> str: 242 | """Generate a description for an environment. 243 | 244 | Args: 245 | env: The environment name. 246 | 247 | Returns: 248 | The environment description. 249 | """ 250 | test_type, python, core = env.split("-") 251 | ansible_pkg = "ansible-core" 252 | 253 | return f"{test_type.capitalize()} tests using {ansible_pkg} {core} and python {python[2:]}" 254 | 255 | 256 | def in_action() -> bool: 257 | """Check if running on Github Actions platform. 258 | 259 | Returns: 260 | True if running on Github Actions platform. 261 | """ 262 | return os.environ.get("GITHUB_ACTIONS") == "true" 263 | 264 | 265 | def add_ansible_matrix(state: State) -> EnvList: 266 | """Add the ansible matrix to the state. 267 | 268 | Args: 269 | state: The state object. 270 | 271 | Returns: 272 | The environment list. 273 | """ 274 | ansible_config = state.conf.get_section_config( 275 | Section(None, "ansible"), 276 | base=[], 277 | of_type=AnsibleConfigSet, 278 | for_env=None, 279 | ) 280 | env_list = StrConvert().to_env_list(ENV_LIST) 281 | env_list.envs = [ 282 | env for env in env_list.envs if all(skip not in env for skip in ansible_config["skip"]) 283 | ] 284 | env_list.envs = sorted(env_list.envs, key=custom_sort) 285 | state.conf.core.loaders.append( 286 | MemoryLoader(env_list=env_list), 287 | ) 288 | return env_list 289 | 290 | 291 | def _check_num_candidates(candidates: list[str], env_name: str) -> None: 292 | """Check the number of candidates. 293 | 294 | Args: 295 | candidates: The candidates. 296 | env_name: The environment name. 297 | """ 298 | if len(candidates) > 1: 299 | err = f"Multiple python versions found in {env_name}" 300 | logger.critical(err) 301 | sys.exit(1) 302 | if len(candidates) == 0: 303 | err = f"No python version found in {env_name}" 304 | logger.critical(err) 305 | sys.exit(1) 306 | 307 | 308 | def _gen_version(candidates: list[str]) -> str: 309 | """Generate the version from the candidates. 310 | 311 | Args: 312 | candidates: The candidates. 313 | 314 | Returns: 315 | The version. 316 | """ 317 | if "." in candidates[0]: 318 | return candidates[0] 319 | return f"{candidates[0][0]}.{candidates[0][1:]}" 320 | 321 | 322 | def generate_gh_matrix(env_list: EnvList, section: str) -> None: 323 | """Generate the github matrix. 324 | 325 | Args: 326 | env_list: The environment list. 327 | section: The test section to be generated. 328 | """ 329 | results = [] 330 | 331 | for env_name in env_list.envs: 332 | if section != "all" and not env_name.startswith(section): 333 | continue 334 | candidates = [] 335 | factors = env_name.split("-") 336 | for factor in factors: 337 | match = PY_FACTORS_RE.match(factor) 338 | if match: 339 | candidates.append(match[2]) 340 | 341 | _check_num_candidates(candidates=candidates, env_name=env_name) 342 | version = _gen_version(candidates=candidates) 343 | 344 | results.append( 345 | { 346 | "description": desc_for_env(env_name), 347 | "factors": factors, 348 | "name": env_name, 349 | "python": version, 350 | }, 351 | ) 352 | 353 | gh_output = os.getenv("GITHUB_OUTPUT") 354 | if not gh_output and not in_action(): 355 | value = json.dumps(results, indent=2, sort_keys=True) 356 | print(value) # noqa: T201 357 | return 358 | 359 | if not gh_output: 360 | err = "GITHUB_OUTPUT environment variable not set" 361 | logger.critical(err) 362 | sys.exit(1) 363 | 364 | value = json.dumps(results) 365 | 366 | if "\n" in value: 367 | eof = f"EOF-{uuid.uuid4()}" 368 | encoded = f"envlist<<{eof}\n{value}\n{eof}\n" 369 | else: 370 | encoded = f"envlist={value}\n" 371 | 372 | with Path(gh_output).open("a", encoding="utf-8") as fileh: 373 | fileh.write(encoded) 374 | 375 | 376 | def get_collection_name(galaxy_path: Path) -> tuple[str, str]: 377 | """Extract collection information from the galaxy.yml file. 378 | 379 | Args: 380 | galaxy_path: The path to the galaxy.yml file. 381 | 382 | Returns: 383 | The collection name and namespace. 384 | """ 385 | try: 386 | with galaxy_path.open() as galaxy_file: 387 | galaxy = yaml.safe_load(galaxy_file) 388 | except FileNotFoundError: 389 | err = f"Unable to find galaxy.yml file at {galaxy_path}" 390 | logger.critical(err) 391 | sys.exit(1) 392 | 393 | try: 394 | c_name = galaxy["name"] 395 | c_namespace = galaxy["namespace"] 396 | except KeyError as exc: 397 | err = f"Unable to find {exc} in galaxy.yml" 398 | logger.critical(err) 399 | sys.exit(1) 400 | return c_name, c_namespace 401 | 402 | 403 | def conf_commands( 404 | c_name: str, 405 | c_namespace: str, 406 | env_conf: EnvConfigSet, 407 | pos_args: tuple[str, ...] | None, 408 | test_type: str, 409 | ) -> list[str]: 410 | """Build the commands for the tox environment. 411 | 412 | Args: 413 | c_name: The collection name. 414 | c_namespace: The collection namespace. 415 | env_conf: The tox environment configuration object. 416 | pos_args: Positional arguments passed to tox command. 417 | test_type: The test type, either "integration", "unit", or "sanity". 418 | 419 | Returns: 420 | The commands to run. 421 | """ 422 | if test_type in ["integration", "unit"]: 423 | return conf_commands_for_integration_unit( 424 | pos_args=pos_args, 425 | test_type=test_type, 426 | ) 427 | if test_type == "sanity": 428 | return conf_commands_for_sanity( 429 | c_name=c_name, 430 | c_namespace=c_namespace, 431 | env_conf=env_conf, 432 | pos_args=pos_args, 433 | ) 434 | err = f"Unknown test type {test_type}" 435 | logger.critical(err) 436 | sys.exit(1) 437 | 438 | 439 | def conf_commands_for_integration_unit( 440 | pos_args: tuple[str, ...] | None, 441 | test_type: str, 442 | ) -> list[str]: 443 | """Build the commands for integration and unit tests. 444 | 445 | Args: 446 | pos_args: Positional arguments passed to tox command. 447 | test_type: The test type, either "integration" or "unit". 448 | 449 | Returns: 450 | The commands to run. 451 | """ 452 | args = f" {' '.join(pos_args)} " if pos_args else " " 453 | 454 | # Use pytest ansible unit inject only to inject the collection path 455 | # into the collection finder 456 | command = f"python3 -m pytest --ansible-unit-inject-only{args}{TOX_WORK_DIR}/tests/{test_type}" 457 | return [command] 458 | 459 | 460 | def conf_commands_for_sanity( 461 | c_name: str, 462 | c_namespace: str, 463 | env_conf: EnvConfigSet, 464 | pos_args: tuple[str, ...] | None, 465 | ) -> list[str]: 466 | """Add commands for sanity tests. 467 | 468 | Args: 469 | c_name: The collection name. 470 | c_namespace: The collection namespace. 471 | env_conf: The tox environment configuration object. 472 | pos_args: Positional arguments passed to tox command. 473 | 474 | Returns: 475 | The commands to run. 476 | """ 477 | commands = [] 478 | envtmpdir = env_conf["envtmpdir"] 479 | 480 | args = f" {' '.join(pos_args)}" if pos_args else "" 481 | 482 | py_ver = env_conf.name.split("-")[1].replace("py", "") 483 | 484 | command = f"ansible-test sanity --local --requirements --python {py_ver}{args}" 485 | ch_dir = f"cd {envtmpdir}/collections/ansible_collections/{c_namespace}/{c_name}" 486 | full_command = f"bash -c '{ch_dir} && {command}'" 487 | commands.append(full_command) 488 | return commands 489 | 490 | 491 | def conf_commands_pre( 492 | env_conf: EnvConfigSet, 493 | c_name: str, 494 | c_namespace: str, 495 | ) -> list[str]: 496 | """Build and install the collection. 497 | 498 | Args: 499 | env_conf: The tox environment configuration object. 500 | c_name: The collection name. 501 | c_namespace: The collection namespace. 502 | 503 | Returns: 504 | The commands to pre run. 505 | """ 506 | commands = [] 507 | 508 | # Define some directories" 509 | envtmpdir = env_conf["envtmpdir"] 510 | collections_root = f"{envtmpdir}/collections" 511 | collection_installed_at = f"{collections_root}/ansible_collections/{c_namespace}/{c_name}" 512 | galaxy_build_dir = f"{envtmpdir}/collection_build" 513 | end_group = "echo ::endgroup::" 514 | 515 | if in_action(): 516 | group = "echo ::group::Make the galaxy build dir" 517 | commands.append(group) 518 | commands.append(f"mkdir {galaxy_build_dir}") 519 | if in_action(): 520 | commands.append(end_group) 521 | 522 | if in_action(): 523 | group = "echo ::group::Copy the collection to the galaxy build dir" 524 | commands.append(group) 525 | cd_tox_dir = f"cd {TOX_WORK_DIR}" 526 | copy_script = ( 527 | f"for file in $(git ls-files 2> /dev/null || ls); do\n\t" 528 | f"mkdir -p {galaxy_build_dir}/$(dirname $file);\n\t" 529 | f"cp -r $file {galaxy_build_dir}/$file;\n" 530 | "done" 531 | ) 532 | full_cmd = f"sh -c '{cd_tox_dir} && {copy_script}'" 533 | commands.append(full_cmd) 534 | if in_action(): 535 | commands.append(end_group) 536 | 537 | if in_action(): 538 | group = "echo ::group::Build and install the collection" 539 | commands.append(group) 540 | cd_build_dir = f"cd {galaxy_build_dir}" 541 | build_cmd = "ansible-galaxy collection build" 542 | tar_file = f"{c_namespace}-{c_name}-*.tar.gz" 543 | install_cmd = f"ansible-galaxy collection install {tar_file} -p {collections_root}" 544 | full_cmd = f"bash -c '{cd_build_dir} && {build_cmd} && {install_cmd}'" 545 | commands.append(full_cmd) 546 | if in_action(): 547 | commands.append(end_group) 548 | 549 | if in_action(): 550 | group = "echo ::group::Initialize the collection to avoid ansible #68499" 551 | commands.append(group) 552 | cd_install_dir = f"cd {collection_installed_at}" 553 | git_cfg = "git config --global init.defaultBranch main" 554 | git_init = "git init ." 555 | full_cmd = f"bash -c '{cd_install_dir} && {git_cfg} && {git_init}'" 556 | commands.append(full_cmd) 557 | if in_action(): 558 | commands.append(end_group) 559 | 560 | return commands 561 | 562 | 563 | def conf_deps(env_conf: EnvConfigSet, test_type: str) -> str: 564 | """Add dependencies to the tox environment. 565 | 566 | Args: 567 | env_conf: The tox environment configuration object. 568 | test_type: The test type, either "integration", "unit", or "sanity". 569 | 570 | Returns: 571 | The dependencies. 572 | """ 573 | deps = [] 574 | if test_type in ["integration", "unit"]: 575 | deps.extend(OUR_DEPS) 576 | try: 577 | with (TOX_WORK_DIR / "test-requirements.txt").open() as fileh: 578 | deps.extend(fileh.read().splitlines()) 579 | except FileNotFoundError: 580 | pass 581 | try: 582 | with (TOX_WORK_DIR / "requirements-test.txt").open() as fileh: 583 | deps.extend(fileh.read().splitlines()) 584 | except FileNotFoundError: 585 | pass 586 | try: 587 | with (TOX_WORK_DIR / "requirements.txt").open() as fileh: 588 | deps.extend(fileh.read().splitlines()) 589 | except FileNotFoundError: 590 | pass 591 | 592 | ansible_version = env_conf.name.split("-")[2] 593 | base_url = "https://github.com/ansible/ansible/archive/" 594 | if ansible_version in ["devel", "milestone"]: 595 | ansible_package = f"{base_url}{ansible_version}.tar.gz" 596 | else: 597 | ansible_package = f"{base_url}stable-{ansible_version}.tar.gz" 598 | deps.append(ansible_package) 599 | return "\n".join(deps) 600 | 601 | 602 | def conf_passenv() -> list[str]: 603 | """Build the pass environment variables for the tox environment. 604 | 605 | Returns: 606 | The pass environment variables. 607 | """ 608 | passenv = [] 609 | passenv.append("GITHUB_TOKEN") 610 | return passenv 611 | 612 | 613 | def conf_setenv(env_conf: EnvConfigSet) -> str: 614 | """Build the set environment variables for the tox environment. 615 | 616 | Set the XDG_CACHE_HOME to the environment directory to isolate it 617 | 618 | Args: 619 | env_conf: The tox environment configuration object. 620 | 621 | Returns: 622 | The set environment variables. 623 | """ 624 | envvar_name = "ANSIBLE_COLLECTIONS_PATH" 625 | envtmpdir = env_conf["envtmpdir"] 626 | 627 | setenv = [ 628 | f"{envvar_name}={envtmpdir}/collections/", 629 | f"XDG_CACHE_HOME={env_conf['env_dir']}/.cache", 630 | ] 631 | return "\n".join(setenv) 632 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Global testing fixtures. 2 | 3 | The root package import below happens before the pytest workers are forked, so it 4 | picked up by the initial coverage process for a source match. 5 | 6 | Without it, coverage reports the following false positive error: 7 | 8 | CoverageWarning: No data was collected. (no-data-collected) 9 | 10 | This works in conjunction with the coverage source_pkg set to the package such that 11 | a `coverage run --debug trace` shows the source package and file match. 12 | 13 | <...> 14 | Imported source package '' as '/**/src//__init__.py' 15 | <...> 16 | Tracing '/**/src//__init__.py' 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import configparser 22 | import os 23 | import subprocess 24 | import sys 25 | 26 | from dataclasses import dataclass 27 | from pathlib import Path 28 | from typing import TYPE_CHECKING 29 | 30 | import pytest 31 | 32 | import tox_ansible # noqa: F401 33 | 34 | 35 | if TYPE_CHECKING: 36 | from collections.abc import Sequence 37 | 38 | from _pytest.python import Metafunc 39 | 40 | GH_MATRIX_LENGTH = 27 41 | 42 | 43 | def run( 44 | args: Sequence[str | Path] | str | Path, 45 | *, 46 | cwd: Path, 47 | check: bool = False, 48 | shell: bool = True, 49 | env: subprocess._ENV | None = None, 50 | ) -> subprocess.CompletedProcess[str]: 51 | """Utility function to run a command. 52 | 53 | Args: 54 | args: The command to run 55 | cwd: The current working directory 56 | check: Whether to raise an exception if the command fails 57 | shell: Whether to run the command in a shell 58 | env: The environment to run the command in 59 | 60 | Returns: 61 | A CompletedProcess with the result of the command 62 | """ 63 | return subprocess.run( 64 | args=args, 65 | capture_output=True, 66 | check=check, 67 | cwd=str(cwd), 68 | shell=shell, 69 | text=True, 70 | env=env, 71 | ) 72 | 73 | 74 | @pytest.fixture(scope="session") 75 | def tox_bin() -> Path: 76 | """Provide the path to the tox binary. 77 | 78 | Returns: 79 | Path to the tox binary 80 | """ 81 | return Path(sys.executable).parent / "tox" 82 | 83 | 84 | @pytest.fixture(scope="session") 85 | def matrix_length() -> int: 86 | """Provide the length of the gh matrix. 87 | 88 | Returns: 89 | Length of the matrix 90 | """ 91 | return GH_MATRIX_LENGTH 92 | 93 | 94 | @pytest.fixture(scope="module") 95 | def module_fixture_dir(request: pytest.FixtureRequest) -> Path: 96 | """Provide a module specific fixture directory. 97 | 98 | Args: 99 | request: pytest fixture request 100 | 101 | Returns: 102 | Path to the module specific fixture directory 103 | """ 104 | cwd = Path(__file__).parent 105 | fixture_dir = cwd / "fixtures" 106 | return fixture_dir / request.path.relative_to(cwd).parent / request.path.stem 107 | 108 | 109 | @pytest.fixture(autouse=True) 110 | def _tox_in_tox(monkeypatch: pytest.MonkeyPatch) -> None: 111 | """Enable tox-in-tox. 112 | 113 | Args: 114 | monkeypatch: pytest fixture to patch modules 115 | """ 116 | monkeypatch.delenv("TOX_ENV_NAME", raising=False) 117 | monkeypatch.delenv("TOX_WORK_DIR", raising=False) 118 | monkeypatch.delenv("TOX_ENV_DIR", raising=False) 119 | 120 | 121 | @dataclass 122 | class BasicEnvironment: 123 | """An structure for an environment. 124 | 125 | Attributes: 126 | name: The name of the environment 127 | config: The configuration entry for the environment 128 | """ 129 | 130 | name: str 131 | config: dict[str, str] 132 | 133 | 134 | def pytest_generate_tests(metafunc: Metafunc) -> None: 135 | """Parametrize the basic environments and there configurations. 136 | 137 | Args: 138 | metafunc: Metadata for the test 139 | """ 140 | if "basic_environment" in metafunc.fixturenames: 141 | cwd = Path(__file__).parent 142 | basic_dir = cwd / "fixtures" / "integration" / "test_basic" 143 | try: 144 | cmd = ( 145 | f"{sys.executable} -m tox config --ansible " 146 | f"--root {basic_dir} --conf tox-ansible.ini" 147 | ) 148 | env = os.environ 149 | env.pop("TOX_ENV_DIR", None) 150 | env.pop("TOX_ENV_NAME", None) 151 | env.pop("TOX_WORK_DIR", None) 152 | 153 | proc = run( 154 | args=cmd, 155 | check=True, 156 | cwd=basic_dir, 157 | env=env, 158 | ) 159 | except subprocess.CalledProcessError as exc: 160 | print(exc.stdout) 161 | print(exc.stderr) 162 | pytest.fail(exc.stderr) 163 | 164 | configs = configparser.ConfigParser() 165 | configs.read_string(proc.stdout) 166 | 167 | assert configs.sections() 168 | 169 | environment_configs = [ 170 | BasicEnvironment(name=name, config=dict(configs[name])) for name in configs.sections() 171 | ] 172 | 173 | metafunc.parametrize( 174 | "basic_environment", 175 | environment_configs, 176 | ids=[x.replace(":", "-") for x in configs.sections()], 177 | ) 178 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_basic/galaxy.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | namespace: test 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_basic/tox-ansible.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_basic/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_user_provided/galaxy.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | namespace: test 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_user_provided/tox-ansible.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | 5 | [testenv] 6 | deps = 7 | root 8 | set_env = 9 | root = 1 10 | commands_pre = 11 | root 12 | commands = 13 | root 14 | allowlist_externals = 15 | root 16 | 17 | [testenv:integration-py3.11-devel] 18 | pass_env = 19 | specific 20 | 21 | [ansible] 22 | skip = 23 | milestone 24 | -------------------------------------------------------------------------------- /tests/fixtures/unit/test_type/tox-ansible.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | 5 | [testenv] 6 | deps = 7 | root 8 | set_env = 9 | root = 1 10 | commands_pre = 11 | root 12 | commands = 13 | root 14 | allowlist_externals = 15 | root 16 | 17 | [testenv:integration-py3.11-devel] 18 | pass_env = 19 | specific 20 | 21 | [ansible] 22 | skip = 23 | devel 24 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests.""" 2 | -------------------------------------------------------------------------------- /tests/integration/test_basic.py: -------------------------------------------------------------------------------- 1 | """Basic tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | import subprocess 7 | 8 | from typing import TYPE_CHECKING 9 | 10 | import pytest 11 | 12 | from tests.conftest import run 13 | 14 | 15 | if TYPE_CHECKING: 16 | from pathlib import Path 17 | 18 | from ..conftest import BasicEnvironment # noqa: TID252 19 | 20 | 21 | def test_ansible_environments(module_fixture_dir: Path, tox_bin: Path) -> None: 22 | """Test that the ansible environments are available. 23 | 24 | Args: 25 | module_fixture_dir: pytest fixture to get the fixtures directory 26 | tox_bin: pytest fixture to get the tox binary 27 | """ 28 | cmd = (str(tox_bin), "-l", "--ansible", "--conf", f"{module_fixture_dir}/tox-ansible.ini") 29 | try: 30 | proc = run(cmd, cwd=module_fixture_dir, check=True, shell=False) 31 | except subprocess.CalledProcessError as exc: 32 | print(exc.stdout) 33 | print(exc.stderr) 34 | pytest.fail(exc.stderr) 35 | assert "integration" in proc.stdout 36 | assert "sanity" in proc.stdout 37 | assert "unit" in proc.stdout 38 | 39 | 40 | def test_gh_matrix( 41 | module_fixture_dir: Path, 42 | monkeypatch: pytest.MonkeyPatch, 43 | tox_bin: Path, 44 | ) -> None: 45 | """Test that the ansible github matrix generation. 46 | 47 | Remove the GITHUB environment variable to test the default output. 48 | 49 | Args: 50 | module_fixture_dir: pytest fixture to get the fixtures directory 51 | monkeypatch: pytest fixture to patch modules 52 | tox_bin: pytest fixture to get the tox binary 53 | """ 54 | monkeypatch.delenv("GITHUB_ACTIONS", raising=False) 55 | monkeypatch.delenv("GITHUB_OUTPUT", raising=False) 56 | 57 | cmd = (tox_bin, "--ansible", "--gh-matrix", "--conf", f"{module_fixture_dir}/tox-ansible.ini") 58 | proc = run( 59 | cmd, 60 | cwd=module_fixture_dir, 61 | check=True, 62 | shell=False, 63 | ) 64 | structured = json.loads(proc.stdout) 65 | assert isinstance(structured, list) 66 | assert structured 67 | for entry in structured: 68 | assert tuple(sorted(entry)) == ("description", "factors", "name", "python") 69 | assert isinstance(entry["description"], str) 70 | assert isinstance(entry["factors"], list) 71 | assert isinstance(entry["name"], str) 72 | assert isinstance(entry["python"], str) 73 | 74 | 75 | def test_environment_config( 76 | basic_environment: BasicEnvironment, 77 | ) -> None: 78 | """Test that the ansible environment configurations are generated. 79 | 80 | Ensure the environment configurations are generated and look for information unlikely to change 81 | as a basic smoke test. 82 | 83 | Args: 84 | basic_environment: A dict representing the environment configuration 85 | """ 86 | assert "py3" in basic_environment.name 87 | 88 | config = basic_environment.config 89 | 90 | assert config["allowlist_externals"] 91 | assert config["commands_pre"] 92 | assert config["commands"] 93 | assert config["pass_env"] 94 | 95 | assert "https://github.com/ansible/ansible/archive" in config["deps"] 96 | assert "XDG_CACHE_HOME" in config["set_env"] 97 | 98 | 99 | def test_no_ansible_flag(module_fixture_dir: Path, tox_bin: Path) -> None: 100 | """Test exit plugin w/o ansible-flag. 101 | 102 | Args: 103 | module_fixture_dir: pytest fixture to get the fixtures directory 104 | tox_bin: pytest fixture to get the tox binary 105 | 106 | """ 107 | cmd = (tox_bin, "--root", str(module_fixture_dir), "--conf", "tox-ansible.ini") 108 | proc = run( 109 | cmd, 110 | cwd=module_fixture_dir, 111 | check=True, 112 | ) 113 | assert "py: OK" in proc.stdout 114 | 115 | 116 | def test_no_ansible_flag_gh(module_fixture_dir: Path, tox_bin: Path) -> None: 117 | """Test that the ansible flag is required for gh_matrix. 118 | 119 | Args: 120 | module_fixture_dir: pytest fixture to get the fixtures directory 121 | tox_bin: pytest fixture to get the tox binary 122 | 123 | """ 124 | cmd = ( 125 | str(tox_bin), 126 | "--gh-matrix", 127 | "--root", 128 | str(module_fixture_dir), 129 | "--conf", 130 | str(module_fixture_dir / "tox-ansible.ini"), 131 | ) 132 | 133 | proc = run( 134 | cmd, 135 | cwd=module_fixture_dir, 136 | check=False, 137 | shell=False, 138 | ) 139 | assert proc.returncode == 1 140 | assert "The --gh-matrix option requires --ansible" in proc.stdout 141 | 142 | 143 | def test_tox_ini_msg( 144 | module_fixture_dir: Path, 145 | tox_bin: Path, 146 | ) -> None: 147 | """Test that a recommendation is provided to not use a tox.ini. 148 | 149 | Args: 150 | module_fixture_dir: pytest fixture to get the fixtures directory 151 | tox_bin: pytest fixture to get the tox binary 152 | 153 | """ 154 | cmd = (tox_bin, "--ansible", "--root", str(module_fixture_dir), "-e", "non-existent") 155 | with pytest.raises(subprocess.CalledProcessError) as exc: 156 | run( 157 | cmd, 158 | cwd=module_fixture_dir, 159 | check=True, 160 | shell=False, 161 | ) 162 | expected = "Using a default tox.ini file with tox-ansible plugin is not recommended" 163 | assert expected in exc.value.stdout 164 | 165 | 166 | def test_setting_matrix_scope( 167 | module_fixture_dir: Path, 168 | tox_bin: Path, 169 | monkeypatch: pytest.MonkeyPatch, 170 | ) -> None: 171 | """Test setting the matrix scope to a specific section. 172 | 173 | Args: 174 | module_fixture_dir: pytest fixture to get the fixtures directory 175 | tox_bin: pytest fixture to get the tox binary 176 | monkeypatch: pytest fixture to patch modules 177 | """ 178 | monkeypatch.delenv("GITHUB_ACTIONS", raising=False) 179 | monkeypatch.delenv("GITHUB_OUTPUT", raising=False) 180 | monkeypatch.chdir(module_fixture_dir) 181 | 182 | cmd = ( 183 | tox_bin, 184 | "--ansible", 185 | "--gh-matrix", 186 | "--matrix-scope", 187 | "integration", 188 | "--conf", 189 | "tox-ansible.ini", 190 | ) 191 | proc = run( 192 | cmd, 193 | cwd=module_fixture_dir, 194 | check=False, 195 | shell=False, 196 | ) 197 | structured = json.loads(proc.stdout) 198 | assert isinstance(structured, list) 199 | assert all(entry["name"].startswith("integration") for entry in structured) 200 | 201 | 202 | def test_action_not_output( 203 | module_fixture_dir: Path, 204 | tox_bin: Path, 205 | monkeypatch: pytest.MonkeyPatch, 206 | ) -> None: 207 | """Test for exit when action is set but not output. 208 | 209 | Args: 210 | module_fixture_dir: pytest fixture to get the fixtures directory 211 | tox_bin: pytest fixture to get the tox binary 212 | monkeypatch: pytest fixture to patch modules 213 | """ 214 | monkeypatch.setenv("GITHUB_ACTIONS", "true") 215 | monkeypatch.delenv("GITHUB_OUTPUT", raising=False) 216 | monkeypatch.chdir(module_fixture_dir) 217 | 218 | cmd = (tox_bin, "--ansible", "--gh-matrix", "--conf", "tox-ansible.ini") 219 | 220 | proc = run( 221 | cmd, 222 | cwd=module_fixture_dir, 223 | check=False, 224 | shell=False, 225 | ) 226 | assert "GITHUB_OUTPUT environment variable not set" in proc.stdout 227 | -------------------------------------------------------------------------------- /tests/integration/test_user_provided.py: -------------------------------------------------------------------------------- 1 | """User provided configuration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | import os 7 | import subprocess 8 | 9 | from configparser import ConfigParser 10 | from typing import TYPE_CHECKING 11 | 12 | import pytest 13 | 14 | from tests.conftest import run 15 | 16 | 17 | if TYPE_CHECKING: 18 | from pathlib import Path 19 | 20 | 21 | def test_user_provided( 22 | module_fixture_dir: Path, 23 | tox_bin: Path, 24 | ) -> None: 25 | """Test supplemental user configuration. 26 | 27 | Args: 28 | module_fixture_dir: pytest fixture for module fixture directory 29 | tox_bin: pytest fixture for tox binary 30 | """ 31 | try: 32 | proc = run( 33 | f"{tox_bin} config --ansible --root {module_fixture_dir} --conf tox-ansible.ini", 34 | cwd=module_fixture_dir, 35 | check=True, 36 | ) 37 | except subprocess.CalledProcessError as exc: 38 | print(exc.stdout) 39 | print(exc.stderr) 40 | pytest.fail(exc.stderr) 41 | cfg_parser = ConfigParser() 42 | cfg_parser.read_string(proc.stdout) 43 | for env_name in cfg_parser.sections(): 44 | assert cfg_parser.get(env_name, "allowlist_externals") == "root" 45 | assert cfg_parser.get(env_name, "commands_pre") == "root" 46 | assert cfg_parser.get(env_name, "commands") == "root" 47 | assert cfg_parser.get(env_name, "deps") == "root" 48 | assert "root" in cfg_parser.get(env_name, "set_env") 49 | assert "milestone" not in env_name 50 | assert "specific" in cfg_parser.get("testenv:integration-py3.11-devel", "pass_env") 51 | 52 | 53 | def test_user_provided_matrix_success( 54 | matrix_length: int, 55 | module_fixture_dir: Path, 56 | monkeypatch: pytest.MonkeyPatch, 57 | tox_bin: Path, 58 | ) -> None: 59 | """Test supplemental user configuration for matrix generation. 60 | 61 | Args: 62 | matrix_length: pytest fixture for matrix length 63 | module_fixture_dir: pytest fixture for module fixture directory 64 | monkeypatch: pytest fixture to patch modules 65 | tox_bin: pytest fixture for tox binary 66 | """ 67 | monkeypatch.delenv("GITHUB_ACTIONS", raising=False) 68 | monkeypatch.delenv("GITHUB_OUTPUT", raising=False) 69 | proc = run( 70 | f"{tox_bin} --ansible --root {module_fixture_dir} --gh-matrix --conf tox-ansible.ini", 71 | cwd=module_fixture_dir, 72 | check=True, 73 | env=os.environ, 74 | ) 75 | matrix = json.loads(proc.stdout) 76 | matrix_len = matrix_length 77 | assert len(matrix) == matrix_len 78 | for entry in matrix: 79 | assert entry["description"] 80 | assert entry["factors"] 81 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for tox-ansible.""" 2 | -------------------------------------------------------------------------------- /tests/unit/test_plugin.py: -------------------------------------------------------------------------------- 1 | """Unit plugin tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | import io 6 | import json 7 | import re 8 | 9 | from pathlib import Path 10 | 11 | import pytest 12 | import yaml 13 | 14 | from tox.config.cli.parse import Options 15 | from tox.config.cli.parser import Parsed 16 | from tox.config.loader.memory import MemoryLoader 17 | from tox.config.main import Config 18 | from tox.config.source import discover_source 19 | from tox.config.types import EnvList 20 | from tox.report import ToxHandler 21 | from tox.session.state import State 22 | 23 | from tox_ansible.plugin import ( 24 | conf_commands, 25 | conf_commands_pre, 26 | conf_deps, 27 | generate_gh_matrix, 28 | get_collection_name, 29 | tox_add_env_config, 30 | ) 31 | 32 | 33 | def test_commands_pre(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: 34 | """Test pre-command generation. 35 | 36 | Args: 37 | monkeypatch: Pytest fixture. 38 | tmp_path: Pytest fixture. 39 | """ 40 | monkeypatch.setenv("GITHUB_ACTIONS", "true") 41 | 42 | ini_file = tmp_path / "tox.ini" 43 | ini_file.touch() 44 | source = discover_source(ini_file, None) 45 | 46 | conf = Config.make( 47 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 48 | pos_args=[], 49 | source=source, 50 | ).get_env("py39") 51 | 52 | conf.add_config( 53 | keys=["env_tmp_dir", "envtmpdir"], 54 | of_type=Path, 55 | default=tmp_path, 56 | desc="", 57 | ) 58 | 59 | result = conf_commands_pre(env_conf=conf, c_name="test", c_namespace="test") 60 | number_commands = 12 61 | assert len(result) == number_commands, result 62 | 63 | 64 | def test_check_num_candidates_2(caplog: pytest.LogCaptureFixture) -> None: 65 | """Test the number of candidates check. 66 | 67 | Args: 68 | caplog: Pytest fixture. 69 | """ 70 | environment_list = EnvList(envs=["integration-py3.13-py3.13"]) 71 | with pytest.raises(SystemExit, match="1"): 72 | generate_gh_matrix(environment_list, "all") 73 | logs = caplog.text 74 | assert "Multiple python versions found" in logs 75 | 76 | 77 | def test_check_num_candidates_0(caplog: pytest.LogCaptureFixture) -> None: 78 | """Test the number of candidates check. 79 | 80 | Args: 81 | caplog: Pytest fixture. 82 | """ 83 | environment_list = EnvList(envs=["integration-foo-foo"]) 84 | with pytest.raises(SystemExit, match="1"): 85 | generate_gh_matrix(environment_list, "all") 86 | logs = caplog.text 87 | assert "No python version found" in logs 88 | 89 | 90 | @pytest.mark.parametrize("python", ("py313", "py3.13")) 91 | def test_gen_version_matrix(python: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 92 | """Test the version matrix generation. 93 | 94 | Args: 95 | python: Python version. 96 | tmp_path: Pytest fixture. 97 | monkeypatch: Pytest fixture. 98 | """ 99 | av = "2.18" 100 | environment_list = EnvList(envs=[f"integration-{python}-{av}"]) 101 | monkeypatch.setenv("GITHUB_ACTIONS", "true") 102 | gh_output = tmp_path / "matrix.json" 103 | monkeypatch.setenv("GITHUB_OUTPUT", str(gh_output)) 104 | generate_gh_matrix(environment_list, "all") 105 | result = gh_output.read_text() 106 | json_string = re.match(r"envlist=(?P.*)$", result) 107 | assert json_string 108 | json_result = json.loads(json_string.group("json")) 109 | assert json_result[0] == { 110 | "description": f"Integration tests using ansible-core {av} and python {python[2:]}", 111 | "factors": ["integration", python, av], 112 | "name": f"integration-{python}-{av}", 113 | "python": "3.13", 114 | } 115 | 116 | 117 | def test_gen_version_matrix_with_nl(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 118 | """Test the version matrix generation when it contains a newline. 119 | 120 | Args: 121 | tmp_path: Pytest fixture. 122 | monkeypatch: Pytest fixture. 123 | """ 124 | environment_list = EnvList(envs=["integration-py3.13-2.18"]) 125 | monkeypatch.setenv("GITHUB_ACTIONS", "true") 126 | gh_output = tmp_path / "matrix.json" 127 | monkeypatch.setenv("GITHUB_OUTPUT", str(gh_output)) 128 | 129 | json_dumps = json.dumps 130 | 131 | def json_dumps_mock(result: list[dict[str, str]]) -> str: 132 | """Mock json.dumps. 133 | 134 | Args: 135 | result: Result. 136 | 137 | Returns: 138 | str: JSON string. 139 | """ 140 | return json_dumps(result, indent=4) 141 | 142 | monkeypatch.setattr("json.dumps", json_dumps_mock) 143 | 144 | generate_gh_matrix(environment_list, "all") 145 | result = gh_output.read_text() 146 | assert result.startswith("envlist< None: 153 | """Test the collection name retrieval when the file is missing. 154 | 155 | Args: 156 | tmp_path: Pytest fixture. 157 | caplog: Pytest fixture for log capture 158 | """ 159 | with pytest.raises(SystemExit, match="1"): 160 | get_collection_name(tmp_path / "galaxy.yml") 161 | logs = caplog.text 162 | assert "Unable to find galaxy.yml" in logs 163 | 164 | 165 | def test_get_collection_name_broken( 166 | tmp_path: Path, 167 | caplog: pytest.LogCaptureFixture, 168 | ) -> None: 169 | """Test the collection name retrieval when the file is missing. 170 | 171 | Args: 172 | tmp_path: Pytest fixture. 173 | caplog: Pytest fixture for log capture 174 | """ 175 | galaxy_file = tmp_path / "galaxy.yml" 176 | contents = {"namespace": "test", "no_name": "test"} 177 | galaxy_file.write_text(yaml.dump(contents)) 178 | with pytest.raises(SystemExit, match="1"): 179 | get_collection_name(tmp_path / "galaxy.yml") 180 | logs = caplog.text 181 | assert "Unable to find 'name' in galaxy.yml" in logs 182 | 183 | 184 | def test_get_collection_name_success( 185 | tmp_path: Path, 186 | ) -> None: 187 | """Test the collection name retrieval when the file is missing. 188 | 189 | Args: 190 | tmp_path: Pytest fixture. 191 | """ 192 | galaxy_file = tmp_path / "galaxy.yml" 193 | contents = {"namespace": "test", "name": "test"} 194 | galaxy_file.write_text(yaml.dump(contents)) 195 | name, namespace = get_collection_name(tmp_path / "galaxy.yml") 196 | assert name == "test" 197 | assert namespace == "test" 198 | 199 | 200 | def test_conf_commands_unit(tmp_path: Path) -> None: 201 | """Test the conf_commands function. 202 | 203 | Args: 204 | tmp_path: Pytest fixture. 205 | """ 206 | ini_file = tmp_path / "tox.ini" 207 | ini_file.touch() 208 | source = discover_source(ini_file, None) 209 | 210 | conf = Config.make( 211 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 212 | pos_args=[], 213 | source=source, 214 | ).get_env("unit-py3.13-2.18") 215 | 216 | result = conf_commands( 217 | env_conf=conf, 218 | c_name="test", 219 | c_namespace="test", 220 | test_type="unit", 221 | pos_args=None, 222 | ) 223 | assert len(result) == 1 224 | assert result[0] == "python3 -m pytest --ansible-unit-inject-only ./tests/unit" 225 | 226 | 227 | def test_conf_commands_sanity(tmp_path: Path) -> None: 228 | """Test the conf_commands function. 229 | 230 | Args: 231 | tmp_path: Pytest fixture. 232 | """ 233 | ini_file = tmp_path / "tox.ini" 234 | ini_file.touch() 235 | source = discover_source(ini_file, None) 236 | 237 | conf = Config.make( 238 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 239 | pos_args=[], 240 | source=source, 241 | ).get_env("sanity-py3.13-2.18") 242 | 243 | conf.add_config( 244 | keys=["env_tmp_dir", "envtmpdir"], 245 | of_type=Path, 246 | default=tmp_path, 247 | desc="", 248 | ) 249 | 250 | result = conf_commands( 251 | env_conf=conf, 252 | c_name="test", 253 | c_namespace="test", 254 | test_type="sanity", 255 | pos_args=None, 256 | ) 257 | assert len(result) == 1 258 | assert "ansible-test sanity" in result[0] 259 | 260 | 261 | def test_conf_commands_integration(tmp_path: Path) -> None: 262 | """Test the conf_commands function. 263 | 264 | Args: 265 | tmp_path: Pytest fixture. 266 | """ 267 | ini_file = tmp_path / "tox.ini" 268 | ini_file.touch() 269 | source = discover_source(ini_file, None) 270 | 271 | conf = Config.make( 272 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 273 | pos_args=[], 274 | source=source, 275 | ).get_env("integration-py3.13-2.18") 276 | 277 | result = conf_commands( 278 | env_conf=conf, 279 | c_name="test", 280 | c_namespace="test", 281 | test_type="integration", 282 | pos_args=None, 283 | ) 284 | assert len(result) == 1 285 | assert result[0] == "python3 -m pytest --ansible-unit-inject-only ./tests/integration" 286 | 287 | 288 | def test_conf_commands_invalid(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: 289 | """Test the conf_commands function. 290 | 291 | Args: 292 | tmp_path: Pytest fixture. 293 | caplog: Pytest fixture for log capture 294 | """ 295 | ini_file = tmp_path / "tox.ini" 296 | ini_file.touch() 297 | source = discover_source(ini_file, None) 298 | 299 | conf = Config.make( 300 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 301 | pos_args=[], 302 | source=source, 303 | ).get_env("invalid-py3.13-2.18") 304 | 305 | with pytest.raises(SystemExit, match="1"): 306 | conf_commands( 307 | env_conf=conf, 308 | c_name="test", 309 | c_namespace="test", 310 | test_type="invalid", 311 | pos_args=None, 312 | ) 313 | 314 | logs = caplog.text 315 | assert "Unknown test type" in logs 316 | 317 | 318 | def test_conf_deps(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 319 | """Test the conf_commands function. 320 | 321 | Args: 322 | tmp_path: Pytest fixture. 323 | monkeypatch: Pytest fixture for patching. 324 | """ 325 | ini_file = tmp_path / "tox.ini" 326 | ini_file.touch() 327 | source = discover_source(ini_file, None) 328 | 329 | (tmp_path / "test-requirements.txt").write_text("test-requirement") 330 | (tmp_path / "requirements.txt").write_text("requirement") 331 | (tmp_path / "requirements-test.txt").write_text("requirement-test") 332 | monkeypatch.setattr("tox_ansible.plugin.TOX_WORK_DIR", tmp_path) 333 | 334 | conf = Config.make( 335 | Parsed(work_dir=tmp_path, override=[], config_file=ini_file, root_dir=tmp_path), 336 | pos_args=[], 337 | source=source, 338 | ).get_env("unit-py3.13-2.18") 339 | 340 | result = conf_deps(env_conf=conf, test_type="unit") 341 | assert "test-requirement" in result 342 | assert "requirement" in result 343 | assert "requirement-test" in result 344 | 345 | 346 | def test_tox_add_env_config_valid(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 347 | """Test the tox_add_env_config function. 348 | 349 | Args: 350 | tmp_path: Pytest fixture for temporary directory. 351 | monkeypatch: Pytest fixture for patching. 352 | """ 353 | ini_file = tmp_path / "tox.ini" 354 | ini_file.touch() 355 | (tmp_path / "galaxy.yml").write_text("namespace: test\nname: test") 356 | monkeypatch.chdir(tmp_path) 357 | source = discover_source(ini_file, None) 358 | parsed = Parsed( 359 | work_dir=tmp_path, 360 | override=[], 361 | config_file=ini_file, 362 | root_dir=tmp_path, 363 | ansible=True, 364 | ) 365 | 366 | env_conf = Config.make( 367 | parsed=parsed, 368 | pos_args=[], 369 | source=source, 370 | ).get_env("unit-py3.13-2.18") 371 | 372 | env_conf.add_config( 373 | keys=["env_tmp_dir", "envtmpdir"], 374 | of_type=Path, 375 | default=tmp_path, 376 | desc="", 377 | ) 378 | 379 | env_conf.add_config( 380 | keys=["env_dir", "envdir"], 381 | of_type=Path, 382 | default=tmp_path, 383 | desc="", 384 | ) 385 | 386 | output = io.BytesIO() 387 | wrapper = io.TextIOWrapper( 388 | buffer=output, 389 | encoding="utf-8", 390 | line_buffering=True, 391 | ) 392 | 393 | state = State( 394 | options=Options( 395 | parsed=parsed, 396 | pos_args="", 397 | source=source, 398 | cmd_handlers={}, 399 | log_handler=ToxHandler(level=0, is_colored=False, out_err=(wrapper, wrapper)), 400 | ), 401 | args=[], 402 | ) 403 | 404 | tox_add_env_config(env_conf, state) 405 | 406 | assert isinstance(env_conf.loaders[0], MemoryLoader) 407 | assert ( 408 | env_conf.loaders[0].raw["description"] 409 | == "Unit tests using ansible-core 2.18 and python 3.13" 410 | ) 411 | 412 | 413 | def test_tox_add_env_config_invalid(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: 414 | """Test the tox_add_env_config function. 415 | 416 | Args: 417 | tmp_path: Pytest fixture for temporary directory. 418 | monkeypatch: Pytest fixture for patching. 419 | """ 420 | ini_file = tmp_path / "tox.ini" 421 | ini_file.touch() 422 | (tmp_path / "galaxy.yml").write_text("namespace: test\nname: test") 423 | monkeypatch.chdir(tmp_path) 424 | source = discover_source(ini_file, None) 425 | parsed = Parsed( 426 | work_dir=tmp_path, 427 | override=[], 428 | config_file=ini_file, 429 | root_dir=tmp_path, 430 | ansible=True, 431 | ) 432 | 433 | env_conf = Config.make( 434 | parsed=parsed, 435 | pos_args=[], 436 | source=source, 437 | ).get_env("insanity-py3.13-2.18") 438 | 439 | output = io.BytesIO() 440 | wrapper = io.TextIOWrapper( 441 | buffer=output, 442 | encoding="utf-8", 443 | line_buffering=True, 444 | ) 445 | 446 | state = State( 447 | options=Options( 448 | parsed=parsed, 449 | pos_args="", 450 | source=source, 451 | cmd_handlers={}, 452 | log_handler=ToxHandler(level=0, is_colored=False, out_err=(wrapper, wrapper)), 453 | ), 454 | args=[], 455 | ) 456 | 457 | tox_add_env_config(env_conf, state) 458 | assert not env_conf.loaders 459 | -------------------------------------------------------------------------------- /tests/unit/test_type.py: -------------------------------------------------------------------------------- 1 | """Test the use of a generic type with tox. 2 | 3 | These are specific to PR https://github.com/tox-dev/tox/pull/3288 4 | 5 | When merged the 2nd test can be switched to a SystemExit like test #1 6 | The types override for List/list and the related ruff noqa's in plugin.py can be removed. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import json 12 | import runpy 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | import pytest 17 | 18 | 19 | if TYPE_CHECKING: 20 | from pathlib import Path 21 | 22 | 23 | def test_type_current( 24 | capsys: pytest.CaptureFixture[str], 25 | monkeypatch: pytest.MonkeyPatch, 26 | module_fixture_dir: Path, 27 | ) -> None: 28 | """Test the current runtime for a gh matrix. 29 | 30 | Args: 31 | capsys: pytest fixture to capture stdout and stderr 32 | monkeypatch: pytest fixture to patch modules 33 | module_fixture_dir: pytest fixture to provide a module specific fixture directory 34 | """ 35 | matrix_length = 27 36 | monkeypatch.delenv("GITHUB_ACTIONS", raising=False) 37 | monkeypatch.delenv("GITHUB_OUTPUT", raising=False) 38 | monkeypatch.chdir(module_fixture_dir) 39 | args = [ 40 | "--ansible", 41 | "--gh-matrix", 42 | "--conf", 43 | "tox-ansible.ini", 44 | ] 45 | out = runpy.run_module("tox") 46 | with pytest.raises(SystemExit): 47 | out["run"](args=args) 48 | captured = capsys.readouterr() 49 | matrix = json.loads(captured.out) 50 | assert len(matrix) == matrix_length 51 | for entry in matrix: 52 | assert entry["description"] 53 | assert entry["factors"] 54 | -------------------------------------------------------------------------------- /tools/report-coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | coverage combine -q "--data-file=${TOX_ENV_DIR}/.coverage" "${TOX_ENV_DIR}"/.coverage.* 4 | coverage xml "--data-file=${TOX_ENV_DIR}/.coverage" -o "${TOX_ENV_DIR}/coverage.xml" --ignore-errors --fail-under=0 5 | COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage lcov --fail-under=0 --ignore-errors -q 6 | COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage report --fail-under=0 --ignore-errors 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.23.2 4 | tox-uv>=1.20.2 5 | env_list = 6 | py 7 | deps 8 | docs 9 | lint 10 | milestone 11 | pkg 12 | skip_missing_interpreters = true 13 | 14 | [testenv] 15 | description = Run pytest under {basepython} 16 | package = editable 17 | extras = 18 | test 19 | pass_env = 20 | CI 21 | CONTAINER_* 22 | DOCKER_* 23 | GITHUB_* 24 | HOME 25 | PYTEST_* 26 | SSH_AUTH_SOCK 27 | TERM 28 | USER 29 | set_env = 30 | !milestone: PIP_CONSTRAINT = {toxinidir}/.config/constraints.txt 31 | !milestone: UV_CONSTRAINT = {toxinidir}/.config/constraints.txt 32 | COVERAGE_COMBINED = {envdir}/.coverage 33 | COVERAGE_FILE = {env:COVERAGE_FILE:{envdir}/.coverage.{envname}} 34 | COVERAGE_PROCESS_START = {toxinidir}/pyproject.toml 35 | FORCE_COLOR = 1 36 | PRE_COMMIT_COLOR = always 37 | TERM = xterm-256color 38 | commands_pre = 39 | sh -c "rm -f {envdir}/.coverage* 2>/dev/null || true" 40 | commands = 41 | python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")' 42 | coverage run -m pytest {posargs:-n auto --junitxml=./junit.xml} 43 | commands_post = 44 | py{,310,311,312,313}: ./tools/report-coverage 45 | allowlist_externals = 46 | ./tools/report-coverage 47 | git 48 | rm 49 | sh 50 | 51 | [testenv:deps] 52 | description = Bump all dependencies 53 | skip_install = true 54 | deps = 55 | {[testenv:lint]deps} 56 | extras = 57 | set_env = 58 | PIP_CONSTRAINT = /dev/null 59 | UV_CONSTRAINT = /dev/null 60 | commands_pre = 61 | commands = 62 | pre-commit run --all-files --show-diff-on-failure --hook-stage manual deps 63 | pre-commit run --all-files --show-diff-on-failure lock 64 | pre-commit autoupdate 65 | tox -e lint 66 | git diff --exit-code 67 | env_dir = {toxworkdir}/lint 68 | 69 | [testenv:docs] 70 | description = Builds docs 71 | package = editable 72 | skip_install = false 73 | extras = 74 | docs 75 | set_env = 76 | DYLD_FALLBACK_LIBRARY_PATH = /opt/homebrew/lib:{env:LD_LIBRARY_PATH} 77 | NO_COLOR = 1 78 | TERM = dump 79 | commands = 80 | mkdocs build {posargs:--strict --site-dir=_readthedocs/html/} 81 | 82 | [testenv:lint] 83 | description = Enforce quality standards under {basepython} 84 | skip_install = true 85 | deps = 86 | pre-commit 87 | pre-commit-uv>=4.1.4 88 | uv>=0.5.25 89 | set_env = 90 | PIP_CONSTRAINT = /dev/null 91 | UV_CONSTRAINT = /dev/null 92 | commands = 93 | pre-commit run --show-diff-on-failure --all-files 94 | 95 | [testenv:milestone] 96 | description = 97 | Run tests with ansible-core milestone branch and without dependencies constraints 98 | deps = 99 | ansible-core@ https://github.com/ansible/ansible/archive/milestone.tar.gz 100 | set_env = 101 | {[testenv]set_env} 102 | PIP_CONSTRAINT = /dev/null 103 | UV_CONSTRAINT = /dev/null 104 | 105 | [testenv:pkg] 106 | description = 107 | Do packaging/distribution 108 | skip_install = true 109 | deps = 110 | build>=0.9 111 | twine >= 4.0.2 # pyup: ignore 112 | set_env = 113 | commands = 114 | rm -rfv {toxinidir}/dist/ 115 | python -m build --outdir {toxinidir}/dist/ {toxinidir} 116 | sh -c "python -m twine check --strict {toxinidir}/dist/*" 117 | --------------------------------------------------------------------------------