From 5bf495a6a707c986f669ca208cc3ffcfa86b3385 Mon Sep 17 00:00:00 2001 From: "Bradley A. Thornton" Date: Tue, 9 May 2023 08:36:04 -0700 Subject: [PATCH] New Version 2 codebase (#169) initial v2 code commit --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .config/dictionary.txt | 17 + .config/requirements-dev.txt | 6 + .config/requirements-test.txt | 6 + .config/requirements.in | 5 + .coveragerc | 8 - .darglint | 7 + .flake8 | 56 ++ .github/CODEOWNERS | 1 - .github/dependabot.yml | 12 - .github/labels.yml | 38 -- .github/release-drafter.yml | 27 - .github/workflows/release-drafter.yml | 18 - .github/workflows/run.yml | 70 +++ .github/workflows/tox.yml | 239 ++------ .gitignore | 177 +++++- .pre-commit-config.yaml | 109 ++-- .pylintrc | 20 - .yamllint | 14 - CHANGELOG | 9 - LICENSE | 22 +- README.md | 427 ++++++-------- TODO.txt | 3 - codecov.yml | 1 - cspell.config.yaml | 33 ++ docs/galaxy.yml | 35 ++ docs/integration.ini | 87 +++ docs/sanity.ini | 83 +++ .../tasks/main.yml => docs/tox-ansible.ini | 0 docs/unit.ini | 87 +++ galaxy.yml | 27 - mypy.ini | 11 - pyproject.toml | 113 +++- requirements.txt | 29 + setup.cfg | 56 -- setup.py | 7 - src/tox_ansible/__init__.py | 1 + src/tox_ansible/_yaml.py | 11 - src/tox_ansible/ansible/__init__.py | 204 ------- src/tox_ansible/ansible/scenario.py | 76 --- src/tox_ansible/filter/__init__.py | 18 - src/tox_ansible/filter/base_filter.py | 27 - src/tox_ansible/filter/by_driver.py | 16 - src/tox_ansible/filter/by_scenario.py | 21 - src/tox_ansible/hooks.py | 86 --- src/tox_ansible/matrix/__init__.py | 53 -- src/tox_ansible/matrix/axes.py | 15 - src/tox_ansible/options.py | 114 ---- src/tox_ansible/plugin.py | 539 ++++++++++++++++++ src/tox_ansible/tox_ansible_test_case.py | 90 --- src/tox_ansible/tox_base_case.py | 71 --- src/tox_ansible/tox_helper.py | 150 ----- src/tox_ansible/tox_lint_case.py | 62 -- src/tox_ansible/tox_molecule_case.py | 139 ----- src/tox_ansible/utils.py | 20 - tests/__init__.py | 1 + tests/conftest.py | 29 + tests/fixtures/collection/galaxy.yml | 5 - tests/fixtures/collection/molecule.yml | 3 - .../collection/molecule/one/molecule.yml | 2 - .../collection/molecule/two/molecule.yml | 2 - .../complex/molecule/default/molecule.yml | 2 - .../molecule/name_mismatch/molecule.yml | 4 - .../complex/molecule/openstack/molecule.yml | 2 - .../collection/roles/no_tests/tasks/main.yml | 0 .../simple/molecule/default/converge.yml | 5 - .../simple/molecule/default/molecule.yml | 5 - .../collection/roles/simple/tasks/main.yml | 2 - tests/fixtures/collection/tox.ini | 13 - tests/fixtures/expand_collection/galaxy.yml | 5 - .../ignored/molecule/not_found/molecule.yml | 0 .../expand_collection/molecule_one.yml | 3 - .../expand_collection/molecule_two.yml | 3 - .../simple/molecule/default/molecule.yml | 2 - .../roles/simple/molecule/symlink | 1 - .../roles/simple/tasks/main.yml | 0 tests/fixtures/expand_collection/tox.ini | 21 - .../expand_collection_comma/galaxy.yml | 5 - .../ignored/molecule/not_found/molecule.yml | 0 .../simple/molecule/default/molecule.yml | 2 - .../roles/simple/molecule/symlink | 1 - .../roles/simple/tasks/main.yml | 0 .../fixtures/expand_collection_comma/tox.ini | 18 - .../expand_collection_newlines/galaxy.yml | 3 - .../ignored/molecule/not_found/molecule.yml | 0 .../simple/molecule/default/molecule.yml | 2 - .../roles/simple/molecule/symlink | 1 - .../roles/simple/tasks/main.yml | 0 .../expand_collection_newlines/tox.ini | 22 - tests/fixtures/has_deps/galaxy.yml | 5 - .../has_deps/molecule/one/molecule.yml | 2 - .../has_deps/molecule/two/molecule.yml | 2 - .../simple/molecule/default/converge.yml | 4 - .../simple/molecule/default/molecule.yml | 5 - .../has_deps/roles/simple/tasks/main.yml | 2 - tests/fixtures/has_deps/tox.ini | 16 - .../integration/test_basic/galaxy.yml | 2 + .../integration/test_basic/tox-ansible.ini | 3 + .../integration/test_user_provided/galaxy.yml | 2 + .../test_user_provided/tox-ansible.ini | 19 + .../is_precommit/.pre-commit-config.yaml | 0 tests/fixtures/is_precommit/galaxy.yml | 5 - tests/fixtures/is_precommit/molecule.yml | 3 - .../is_precommit/molecule/one/molecule.yml | 2 - .../is_precommit/molecule/two/molecule.yml | 2 - .../complex/molecule/default/molecule.yml | 2 - .../molecule/name_mismatch/molecule.yml | 4 - .../complex/molecule/openstack/molecule.yml | 2 - .../is_precommit/roles/complex/tasks/main.yml | 0 .../roles/no_tests/tasks/main.yml | 0 .../simple/molecule/default/converge.yml | 5 - .../simple/molecule/default/molecule.yml | 5 - .../is_precommit/roles/simple/tasks/main.yml | 2 - tests/fixtures/is_precommit/tox.ini | 13 - .../.config/molecule/config.yml | 3 - .../not_collection/molecule/one/molecule.yml | 2 - .../not_collection/molecule/two/molecule.yml | 2 - tests/fixtures/not_collection/tox.ini | 36 -- .../molecule/dont_find_me/molecule.yml | 3 - tests/fixtures/nothing/tox.ini | 5 - tests/fixtures/simplified/galaxy.yml | 5 - .../simplified/molecule/default/molecule.yml | 2 - .../simplified/molecule/two/molecule.yml | 2 - .../myrole/molecule/another/converge.yml | 5 - .../myrole/molecule/another/molecule.yml | 5 - .../myrole/molecule/default/converge.yml | 5 - .../myrole/molecule/default/molecule.yml | 5 - tests/fixtures/simplified/tox.ini | 11 - tests/integration/__init__.py | 1 + tests/integration/test_basic.py | 62 ++ tests/integration/test_user_provided.py | 38 ++ tests/sanity/ignore-2.10.txt | 0 tests/sanity/ignore-2.9.txt | 0 tests/sanity/requirements.txt | 0 tests/test_ansible.py | 80 --- tests/test_by_driver.py | 18 - tests/test_by_scenario.py | 17 - tests/test_everything.py | 171 ------ tests/test_filter.py | 26 - tests/test_matrix.py | 65 --- tests/test_options.py | 114 ---- tests/test_scenario.py | 85 --- tests/test_tox_helper.py | 8 - tests/test_tox_lint_case.py | 109 ---- tests/test_tox_molecule_case.py | 179 ------ tests/test_utils.py | 15 - tests/util.py | 111 ---- tox.ini | 115 ++-- 147 files changed, 1863 insertions(+), 3286 deletions(-) create mode 100644 .config/dictionary.txt create mode 100644 .config/requirements-dev.txt create mode 100644 .config/requirements-test.txt create mode 100644 .config/requirements.in delete mode 100644 .coveragerc create mode 100644 .darglint create mode 100644 .flake8 delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/dependabot.yml delete mode 100644 .github/labels.yml delete mode 100644 .github/release-drafter.yml delete mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/run.yml delete mode 100644 .pylintrc delete mode 100644 .yamllint delete mode 100644 CHANGELOG delete mode 100644 TODO.txt delete mode 100644 codecov.yml create mode 100644 cspell.config.yaml create mode 100644 docs/galaxy.yml create mode 100644 docs/integration.ini create mode 100644 docs/sanity.ini rename tests/fixtures/collection/roles/complex/tasks/main.yml => docs/tox-ansible.ini (100%) create mode 100644 docs/unit.ini delete mode 100644 galaxy.yml delete mode 100644 mypy.ini create mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 src/tox_ansible/_yaml.py delete mode 100644 src/tox_ansible/ansible/__init__.py delete mode 100644 src/tox_ansible/ansible/scenario.py delete mode 100644 src/tox_ansible/filter/__init__.py delete mode 100644 src/tox_ansible/filter/base_filter.py delete mode 100644 src/tox_ansible/filter/by_driver.py delete mode 100644 src/tox_ansible/filter/by_scenario.py delete mode 100644 src/tox_ansible/hooks.py delete mode 100644 src/tox_ansible/matrix/__init__.py delete mode 100644 src/tox_ansible/matrix/axes.py delete mode 100644 src/tox_ansible/options.py create mode 100644 src/tox_ansible/plugin.py delete mode 100644 src/tox_ansible/tox_ansible_test_case.py delete mode 100644 src/tox_ansible/tox_base_case.py delete mode 100644 src/tox_ansible/tox_helper.py delete mode 100644 src/tox_ansible/tox_lint_case.py delete mode 100644 src/tox_ansible/tox_molecule_case.py delete mode 100644 src/tox_ansible/utils.py create mode 100644 tests/conftest.py delete mode 100644 tests/fixtures/collection/galaxy.yml delete mode 100644 tests/fixtures/collection/molecule.yml delete mode 100644 tests/fixtures/collection/molecule/one/molecule.yml delete mode 100644 tests/fixtures/collection/molecule/two/molecule.yml delete mode 100644 tests/fixtures/collection/roles/complex/molecule/default/molecule.yml delete mode 100644 tests/fixtures/collection/roles/complex/molecule/name_mismatch/molecule.yml delete mode 100644 tests/fixtures/collection/roles/complex/molecule/openstack/molecule.yml delete mode 100644 tests/fixtures/collection/roles/no_tests/tasks/main.yml delete mode 100644 tests/fixtures/collection/roles/simple/molecule/default/converge.yml delete mode 100644 tests/fixtures/collection/roles/simple/molecule/default/molecule.yml delete mode 100644 tests/fixtures/collection/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/collection/tox.ini delete mode 100644 tests/fixtures/expand_collection/galaxy.yml delete mode 100644 tests/fixtures/expand_collection/ignored/molecule/not_found/molecule.yml delete mode 100644 tests/fixtures/expand_collection/molecule_one.yml delete mode 100644 tests/fixtures/expand_collection/molecule_two.yml delete mode 100644 tests/fixtures/expand_collection/roles/simple/molecule/default/molecule.yml delete mode 120000 tests/fixtures/expand_collection/roles/simple/molecule/symlink delete mode 100644 tests/fixtures/expand_collection/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/expand_collection/tox.ini delete mode 100644 tests/fixtures/expand_collection_comma/galaxy.yml delete mode 100644 tests/fixtures/expand_collection_comma/ignored/molecule/not_found/molecule.yml delete mode 100644 tests/fixtures/expand_collection_comma/roles/simple/molecule/default/molecule.yml delete mode 120000 tests/fixtures/expand_collection_comma/roles/simple/molecule/symlink delete mode 100644 tests/fixtures/expand_collection_comma/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/expand_collection_comma/tox.ini delete mode 100644 tests/fixtures/expand_collection_newlines/galaxy.yml delete mode 100644 tests/fixtures/expand_collection_newlines/ignored/molecule/not_found/molecule.yml delete mode 100644 tests/fixtures/expand_collection_newlines/roles/simple/molecule/default/molecule.yml delete mode 120000 tests/fixtures/expand_collection_newlines/roles/simple/molecule/symlink delete mode 100644 tests/fixtures/expand_collection_newlines/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/expand_collection_newlines/tox.ini delete mode 100644 tests/fixtures/has_deps/galaxy.yml delete mode 100644 tests/fixtures/has_deps/molecule/one/molecule.yml delete mode 100644 tests/fixtures/has_deps/molecule/two/molecule.yml delete mode 100644 tests/fixtures/has_deps/roles/simple/molecule/default/converge.yml delete mode 100644 tests/fixtures/has_deps/roles/simple/molecule/default/molecule.yml delete mode 100644 tests/fixtures/has_deps/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/has_deps/tox.ini create mode 100644 tests/fixtures/integration/test_basic/galaxy.yml create mode 100644 tests/fixtures/integration/test_basic/tox-ansible.ini create mode 100644 tests/fixtures/integration/test_user_provided/galaxy.yml create mode 100644 tests/fixtures/integration/test_user_provided/tox-ansible.ini delete mode 100644 tests/fixtures/is_precommit/.pre-commit-config.yaml delete mode 100644 tests/fixtures/is_precommit/galaxy.yml delete mode 100644 tests/fixtures/is_precommit/molecule.yml delete mode 100644 tests/fixtures/is_precommit/molecule/one/molecule.yml delete mode 100644 tests/fixtures/is_precommit/molecule/two/molecule.yml delete mode 100644 tests/fixtures/is_precommit/roles/complex/molecule/default/molecule.yml delete mode 100644 tests/fixtures/is_precommit/roles/complex/molecule/name_mismatch/molecule.yml delete mode 100644 tests/fixtures/is_precommit/roles/complex/molecule/openstack/molecule.yml delete mode 100644 tests/fixtures/is_precommit/roles/complex/tasks/main.yml delete mode 100644 tests/fixtures/is_precommit/roles/no_tests/tasks/main.yml delete mode 100644 tests/fixtures/is_precommit/roles/simple/molecule/default/converge.yml delete mode 100644 tests/fixtures/is_precommit/roles/simple/molecule/default/molecule.yml delete mode 100644 tests/fixtures/is_precommit/roles/simple/tasks/main.yml delete mode 100644 tests/fixtures/is_precommit/tox.ini delete mode 100644 tests/fixtures/not_collection/.config/molecule/config.yml delete mode 100644 tests/fixtures/not_collection/molecule/one/molecule.yml delete mode 100644 tests/fixtures/not_collection/molecule/two/molecule.yml delete mode 100644 tests/fixtures/not_collection/tox.ini delete mode 100644 tests/fixtures/nothing/molecule/dont_find_me/molecule.yml delete mode 100644 tests/fixtures/nothing/tox.ini delete mode 100644 tests/fixtures/simplified/galaxy.yml delete mode 100644 tests/fixtures/simplified/molecule/default/molecule.yml delete mode 100644 tests/fixtures/simplified/molecule/two/molecule.yml delete mode 100644 tests/fixtures/simplified/roles/myrole/molecule/another/converge.yml delete mode 100644 tests/fixtures/simplified/roles/myrole/molecule/another/molecule.yml delete mode 100644 tests/fixtures/simplified/roles/myrole/molecule/default/converge.yml delete mode 100644 tests/fixtures/simplified/roles/myrole/molecule/default/molecule.yml delete mode 100644 tests/fixtures/simplified/tox.ini create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_basic.py create mode 100644 tests/integration/test_user_provided.py delete mode 100644 tests/sanity/ignore-2.10.txt delete mode 100644 tests/sanity/ignore-2.9.txt delete mode 100644 tests/sanity/requirements.txt delete mode 100644 tests/test_ansible.py delete mode 100644 tests/test_by_driver.py delete mode 100644 tests/test_by_scenario.py delete mode 100644 tests/test_everything.py delete mode 100644 tests/test_filter.py delete mode 100644 tests/test_matrix.py delete mode 100644 tests/test_options.py delete mode 100644 tests/test_scenario.py delete mode 100644 tests/test_tox_helper.py delete mode 100644 tests/test_tox_lint_case.py delete mode 100644 tests/test_tox_molecule_case.py delete mode 100644 tests/test_utils.py delete mode 100644 tests/util.py diff --git a/.config/dictionary.txt b/.config/dictionary.txt new file mode 100644 index 0000000..bb6ab0c --- /dev/null +++ b/.config/dictionary.txt @@ -0,0 +1,17 @@ +autouse +basepython +cidrblock +delenv +devel +endgroup +envtmpdir +envtmpdir +fileh +nocolor +passenv +setenv +testenv +toxfile +toxinidir +workdir +xdist diff --git a/.config/requirements-dev.txt b/.config/requirements-dev.txt new file mode 100644 index 0000000..9b208e9 --- /dev/null +++ b/.config/requirements-dev.txt @@ -0,0 +1,6 @@ +black +pytest +ruff +pre-commit +mypy +types-pyyaml diff --git a/.config/requirements-test.txt b/.config/requirements-test.txt new file mode 100644 index 0000000..e6403ad --- /dev/null +++ b/.config/requirements-test.txt @@ -0,0 +1,6 @@ +coverage +devpi_process +pre-commit +pytest +pytest_mock +pytest-xdist diff --git a/.config/requirements.in b/.config/requirements.in new file mode 100644 index 0000000..e5124a4 --- /dev/null +++ b/.config/requirements.in @@ -0,0 +1,5 @@ +pytest +pytest-ansible>=3.1.0 +pytest-xdist +pyyaml +tox>4.4.8 diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 7870f1f..0000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -source = src -relative_files = True - -[report] -omit = - src/tox_ansible/hooks.py - src/tox_ansible/tox_helper.py diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..8e68aa3 --- /dev/null +++ b/.darglint @@ -0,0 +1,7 @@ +[darglint] +# NOTE: All `darglint` styles except for `sphinx` hit ridiculously low +# NOTE: performance on some of the in-project Python modules. +# Refs: +# * https://github.com/terrencepreilly/darglint/issues/186 +docstring_style = sphinx +strictness = full diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2a79090 --- /dev/null +++ b/.flake8 @@ -0,0 +1,56 @@ +[flake8] + +builtins = _ + +# Print the total number of errors: +count = true + +# Don't even try to analyze these: +extend-exclude = + # No need to traverse egg info dir + *.egg-info, + # tool cache dirs + *_cache + # project env vars + .env, + # GitHub configs + .github, + # Cache files of MyPy + .mypy_cache, + # Cache files of pytest + .pytest_cache, + # Temp dir of pytest-testmon + .tmontmp, + # Occasional virtualenv dir + .venv + # VS Code + .vscode, + # Temporary build dir + build, + # This contains sdists and wheels of ansible-navigator that we don't want to check + dist, + # Metadata of `pip wheel` cmd is autogenerated + pip-wheel-metadata, + # adjacent venv + venv + # project tox directory + .tox + +# IMPORTANT: avoid using ignore option, always use extend-ignore instead +# Completely and unconditionally ignore the following errors: +extend-ignore = F, E203 + +# Accessibility/large fonts and PEP8 unfriendly: +max-line-length = 100 + +# Allow certain violations in certain files: +# Please keep both sections of this list sorted, as it will be easier for others to find and add entries in the future +per-file-ignores = + # The following ignores have been researched and should be considered permanent + # each should be preceeded with an explanation of each of the error codes + # If other ignores are added for a specific file in the section following this, + # these will need to be added to that line as well. + + +# Count the number of occurrences of each error/warning code and print a report: +statistics = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 758dd5f..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @greg-hellings @ssbarnea diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 7d854c6..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Documentation: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem -version: 2 - -updates: - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily - - package-ecosystem: pip - directory: "/" - schedule: - interval: daily diff --git a/.github/labels.yml b/.github/labels.yml deleted file mode 100644 index a203ea8..0000000 --- a/.github/labels.yml +++ /dev/null @@ -1,38 +0,0 @@ -# Format and labels used aim to match those used by Ansible project -# https://github.com/marketplace/actions/github-labeler -- name: bug - color: "fbca04" - description: "This issue/PR relates to a bug." -- name: deprecated - color: "fef2c0" - description: "This issue/PR relates to a deprecated module." -- name: docs - color: "4071a5" - description: "This issue/PR relates to or includes documentation." -- name: enhancement - color: "ededed" - description: "This issue/PR relates to a feature request." -- name: feature - color: "006b75" - description: "This issue/PR relates to a feature request." -- name: major - color: "c6476b" - description: "Marks an important and likely breaking change." -- name: packaging - color: "4071a5" - description: "Packaging category" -- name: performance - color: "555555" - description: "Relates to product or testing performance." -- name: skip-changelog - color: "eeeeee" - description: "Can be missed from the changelog." -- name: stale - color: "eeeeee" - description: "Not updated in long time, will be closed soon." -- name: wontfix - color: "eeeeee" - description: "This will not be worked on" -- name: test - color: "0e8a16" - description: "This PR relates to tests, QA, CI." diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index a444deb..0000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Format and labels used aim to match those used by Ansible project -categories: - - title: 'Major Changes' - labels: - - 'major' # c6476b - - title: 'Minor Changes' - labels: - - 'feature' # 006b75 - - 'enhancement' # ededed - - 'performance' # 555555 - - title: 'Bugfixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' # fbca04 - - 'docs' # 4071a5 - - 'packaging' # 4071a5 - - 'test' # #0e8a16 - - title: 'Deprecations' - labels: - - 'deprecated' # fef2c0 -exclude-labels: - - 'skip-changelog' -template: | - ## Changes - - $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index a7b7332..0000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - - 'releases/**' - - 'stable/**' - -jobs: - update_release_draft: - runs-on: ubuntu-20.04 - steps: - # Drafts your next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml new file mode 100644 index 0000000..88616d2 --- /dev/null +++ b/.github/workflows/run.yml @@ -0,0 +1,70 @@ +name: tox-ansible + +on: + pull_request: + branches: + - main + - "releases/**" + - "stable/**" + workflow_call: + +jobs: + tox-matrix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.5.0 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{ github.event.pull_request.head.repo.full_name }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install tox-ansible, includes tox + run: python -m pip install git+https://github.com/tox-dev/tox-ansible.git@uplift_v2 + + - name: Generate matrix + id: generate-matrix + run: | + python -m tox --ansible --gh-matrix --conf tox-ansible.ini + + outputs: + envlist: ${{ steps.generate-matrix.outputs.envlist }} + + test: + needs: tox-matrix + strategy: + fail-fast: false + matrix: + entry: ${{ fromJson(needs.tox-matrix.outputs.envlist) }} + name: ${{ matrix.entry.name }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.5.0 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.entry.python }} + + - name: Install deps + run: python -m pip install -r test-requirements.txt + + - name: Run tox test + run: python -m tox --ansible -e ${{ matrix.entry.name }} --conf tox-ansible.ini + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + test_passed: + needs: test + runs-on: ubuntu-latest + steps: + - run: >- + python -c "assert set([ + '${{ needs.test.result }}', + ]) == {'success'}" diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 27d3b3e..d436202 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -1,208 +1,81 @@ name: tox +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + on: - create: # is used for publishing to PyPI and TestPyPI - tags: # any tag regardless of its name, no branches + create: # is used for publishing to PyPI and TestPyPI + tags: # any tag regardless of its name, no branches - "**" - push: # only publishes pushes to the main branch to TestPyPI - branches: # any integration branch but not tag + push: # only publishes pushes to the main branch to TestPyPI + branches: # any integration branch but not tag - "main" pull_request: - release: - types: - - published # It seems that you can publish directly without creating - - prereleased - schedule: - - cron: 1 0 * * * # Run daily at 0:01 UTC jobs: - lint: - runs-on: ubuntu-20.04 + pre: + name: pre + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.generate_matrix.outputs.matrix }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - name: Determine matrix + id: generate_matrix + uses: coactions/dynamic-matrix@v1 with: - python-version: "3.10" - cache: pip - - name: run lint - run: | - python3 -m pip install -U tox - python3 -m tox -e lint + min_python: "3.9" + max_python: "3.11" + default_python: "3.11" # used by jobs in other_names + other_names: | + lint - packaging: + tox: + name: ${{ matrix.name }} / python ${{ matrix.python_version }} runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - name: run packaging - run: | - python3 -m pip install -U tox - python3 -m tox -e packaging - - test: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} + needs: pre strategy: fail-fast: false - matrix: - include: - - name: py36 - tox_env: py36 - os: ubuntu-20.04 - python-version: "3.6" - - name: py37 - tox_env: py37 - os: ubuntu-20.04 - python-version: "3.7" - - name: py38 - tox_env: py38 - os: ubuntu-20.04 - python-version: "3.8" - - name: py39 - tox_env: py39 - os: ubuntu-20.04 - python-version: "3.9" - - name: py310 - tox_env: py310 - os: ubuntu-20.04 - python-version: "3.10" - - name: py311 - tox_env: py311 - os: ubuntu-20.04 - python-version: "3.11" - - name: py38@macos - tox_env: py38 - os: macOS-latest - python-version: 3.8 + matrix: ${{ fromJson(needs.pre.outputs.matrix) }} + steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} - cache: pip - - name: run test - run: | - python3 -m pip install tox - python3 -m tox -e ${{ matrix.tox_env }} - env: - COVERAGE_FILE: .coverage.${{ matrix.python-version }} - - name: upload artifacts - uses: actions/upload-artifact@v3 + fetch-depth: 0 # needed by setuptools-scm + + - name: Set up Python ${{ matrix.python_version }} + uses: actions/setup-python@v4 with: - name: coverage-results - path: .coverage.${{ matrix.python-version }} + python-version: ${{ matrix.python_version }} - # Disabled until we can really start working on adding support for tox4 - # test-devel: - # needs: lint - # runs-on: ubuntu-20.04 - # strategy: - # fail-fast: false - # matrix: - # python: ["3.6"] - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/setup-python@v4 - # with: - # python-version: ${{ matrix.python-version }} - # - name: run test (devel) - # run: | - # python -m pip install tox - # tox -e py$(printf "${{ matrix.version }}" | tr -d '.')-devel + - name: Install tox + run: python3 -m pip install --upgrade "tox>=4.0.2" - coverage: - needs: test - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - cache: pip - - uses: actions/download-artifact@v3 - with: - name: coverage-results - - name: run coverage + - name: Log Python info (${{ matrix.python_version }}) run: | - python3 -m pip install tox codecov - python3 -m tox -e coverage - codecov -X pycov -X gcov - env: - CODECOV_TOKEN: ${{ secrets.codecov_token }} + command -v python + python --version --version + python3 -m pip freeze --all - publish: - needs: - - coverage - - lint - - packaging - if: startsWith(github.ref, 'refs/tags/') # Only release during tags - runs-on: ubuntu-20.04 + - name: "tox -e ${{ matrix.passed_name }}" + continue-on-error: ${{ matrix.devel || false }} + run: python3 -m tox -e ${{ matrix.passed_name }} - env: - PY_COLORS: 1 - TOXENV: packaging + # - name: Upload coverage data + # if: ${{ startsWith(matrix.passed_name, 'py') }} + # uses: codecov/codecov-action@v3 + # with: + # name: ${{ matrix.passed_name }} + # fail_ci_if_error: false # see https://github.com/codecov/codecov-action/issues/598 + # token: ${{ secrets.CODECOV_TOKEN }} + # verbose: true # optional (default = false) + tox_passed: + needs: tox + runs-on: ubuntu-latest steps: - - name: Check out src from Git - uses: actions/checkout@v3 - with: - # Get shallow Git history (default) for release events - # but have a complete clone for any other workflows. - # Both options fetch tags but since we're going to remove - # one from HEAD in non-create-tag workflows, we need full - # history for them. - fetch-depth: >- - ${{ - ( - ( - github.event_name == 'create' && - github.event.ref_type == 'tag' - ) || - github.event_name == 'release' - ) && - 1 || 0 - }} - - name: Switch to using Python 3.9 by default - uses: actions/setup-python@v4 - with: - python-version: "3.9" - cache: pip - - name: Install tox - run: python3 -m pip install --user tox - - name: Drop Git tags from HEAD for non-tag-create and non-release events - if: >- - ( - github.event_name != 'create' || - github.event.ref_type != 'tag' - ) && - github.event_name != 'release' - run: >- - git tag --points-at HEAD - | - xargs git tag --delete - - name: Build dists - run: python3 -m tox - - name: Publish to test.pypi.org - if: >- - ( - github.event_name == 'push' && - github.ref == format( - 'refs/heads/{0}', github.event.repository.default_branch - ) - ) || - ( - github.event_name == 'create' && - github.event.ref_type == 'tag' - ) - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.testpypi_password }} - repository_url: https://test.pypi.org/legacy/ - - name: Publish to pypi.org - if: >- # "create" workflows run separately from "push" & "pull_request" - github.event_name == 'release' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.pypi_password }} + - run: >- + python -c "assert set([ + '${{ needs.tox.result }}', + ]) == {'success'}" diff --git a/.gitignore b/.gitignore index 1ce2a04..b58ef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,163 @@ -# Vim temporary files -.*.swp -.*.swo -# Python temporary files -__pycache__ -*.pyc -*.pyo -*.egg-info -# Tox directory -.tox -# Other generated files +# Dynamically created +_version.py + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage .coverage.* -htmlcov -build -dist -.idea -.eggs +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fba61d2..3c18d30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,50 +1,93 @@ +--- repos: - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-merge-conflict + - id: check-symlinks + - id: debug-statements + - id: end-of-file-fixer + - id: no-commit-to-branch + args: [--branch, main] + - id: trailing-whitespace + + - repo: https://github.com/asottile/add-trailing-comma.git + rev: v2.4.0 + hooks: + - id: add-trailing-comma + args: + - --py36-plus + + - repo: https://github.com/Lucas-C/pre-commit-hooks.git + rev: v1.5.1 + hooks: + - id: remove-tabs + + - repo: https://github.com/pre-commit/mirrors-prettier + # keep it before yamllint + rev: v3.0.0-alpha.6 hooks: - - id: isort + - id: prettier + always_run: true + additional_dependencies: + - prettier + - prettier-plugin-toml + - prettier-plugin-sort-json + - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - language_version: python3 - - repo: https://github.com/pre-commit/pre-commit-hooks.git - rev: v4.4.0 + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: "1.3.0" hooks: - - id: end-of-file-fixer - - id: trailing-whitespace - - id: mixed-line-ending - - id: check-byte-order-marker - - id: check-executables-have-shebangs - - id: check-merge-conflict - - id: debug-statements - - repo: https://github.com/PyCQA/flake8 + - id: tox-ini-fmt + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.0.263" + hooks: + - id: ruff + args: + - "--exit-non-zero-on-fix" + + - repo: https://github.com/streetsidesoftware/cspell-cli + rev: v6.31.0 + hooks: + - id: cspell + name: Spell check with cspell + + # untill ruff has parity with darglint + - repo: https://github.com/PyCQA/flake8.git rev: 6.0.0 hooks: - id: flake8 - - repo: https://github.com/adrienverge/yamllint.git - rev: v1.30.0 - hooks: - - id: yamllint - files: \.(yaml|yml)$ - types: [file, yaml] - entry: yamllint --strict - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 - hooks: - - id: mypy - # empty args needed in order to match mypy cli behavior - args: [] - entry: mypy src/ - pass_filenames: false + language_version: python3 additional_dependencies: - - py>=1.9.0 - - types-PyYAML - - repo: https://github.com/pre-commit/mirrors-pylint - rev: v3.0.0a5 + - darglint + - flake8-docstrings + + - repo: https://github.com/pycqa/pylint.git + rev: v3.0.0a6 hooks: - id: pylint + args: + - --output-format=colorized additional_dependencies: - pytest + - tox - pyyaml + + - repo: https://github.com/pre-commit/mirrors-mypy.git + rev: v1.2.0 + hooks: + - id: mypy + additional_dependencies: + - pytest - tox + - types-PyYAML + args: + - src + - tests + - --python-version=3.8 + pass_filenames: false diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 5e2e09c..0000000 --- a/.pylintrc +++ /dev/null @@ -1,20 +0,0 @@ -[MESSAGES CONTROL] - -disable = - # TODO(ssbarnea): remove temporary skips adding during initial adoption: - invalid-name, - missing-class-docstring, - missing-function-docstring, - missing-module-docstring, - no-member, - redefined-outer-name, - too-few-public-methods, - unused-argument, - useless-object-inheritance, - -[REPORTS] -output-format = colorized - -[IMPORTS] -preferred-modules = - ansible:, diff --git a/.yamllint b/.yamllint deleted file mode 100644 index 24f7577..0000000 --- a/.yamllint +++ /dev/null @@ -1,14 +0,0 @@ -extends: default -ignore: | - .tox -rules: - braces: - max-spaces-inside: 1 - level: error - brackets: - max-spaces-inside: 1 - level: error - truthy: - check-keys: false - document-start: disable - line-length: disable diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 95c4a48..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,9 +0,0 @@ -0.11.0 - 3 June 2020 -* Add support for podman driver -* Fix support for config replacement values -* Add {posargs} to the end of default molecule command -* Continue refactoring from getters/setters to properties where appropriate - -0.10.0 - 21 April 2020 -* Add support for ec2 driver -* Add requirements.txt support for each scenario to include its own support diff --git a/LICENSE b/LICENSE index 29d007a..d1fdd08 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,21 @@ -Copyright 2020 +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2023 Bradley A. Thornton -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d2bf655..b395074 100644 --- a/README.md +++ b/README.md @@ -1,320 +1,217 @@ -[![CI/CD Builds](https://github.com/tox-dev/tox-ansible/workflows/tox/badge.svg)](https://github.com/tox-dev/tox-ansible/actions) -[![codecov](https://codecov.io/gh/tox-dev/tox-ansible/branch/main/graph/badge.svg)](https://codecov.io/gh/tox-dev/tox-ansible) -[![PyPI version](https://badge.fury.io/py/tox-ansible.svg)](https://badge.fury.io/py/tox-ansible) +# tox-ansible -tox-ansible -=========== +## Note to version 1.x users -This plugin for tox auto-generates tox environments for running -quality assurance tools like ansible-test or molecule. Optionally, you can -decide to filter the environments down to only a subset of them. -The tool is rather tightly integrated for the official [Molecule](https://github.com/ansible/molecule) -testing tool that integrates with [Ansible](https://github.com/ansible/ansible). +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. -ansible-test ------------- +The rewritten plugin will feature additional options for facilitating running tests of any repository with Ansible content: -This plugin saves you this trouble by allowing you the freedom to run -these commands tranparently. For example you can run `tox -e sanity` which -will install the collection, change current directory and execute -`ansible-test sanity --python X.Y`. You can even add posargs that end up being -passed to the executed command, like `tox -e sanity -- --help`. +Ability to run molecule scenarios (planned) -By default, tox-ansible will also limit execution of ansible-test to the -current python version used by tox. In addition, tox-ansible will try to -automatically determine the best environment to run these tests in -(docker if present on the host, virtualenv, etc.). +Close-to-zero configuration goals (in progress) -You can disable this auto-detection feature in the `tox.ini`, see below for details. +Focus on testing collections (initial implementation) -```shell -$ tox -va -default environments: -sanity -> Auto-generated for: ansible-test sanity -``` +## Introduction -Only those enviroments that are detected will be listed. At least sanity will -always be visible as it does not require adding new files. +`tox-ansible` is a utility designed to simplify the testing of ansible content collections. +Implemented as `tox` plugin, `tox-ansible` provides a simple way to test ansible content collections across multiple python interpreter and ansible versions. -More details ------------- +`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. -This plugin is designed to support auto-discovery of Molecule scenarios. When it has done so, the plugin -will then create a tox environment, if one does not already -exist, that contains factors matching against the scenario path. For example, if you have scenarios -that live at `molecule/scenario`, `roles/somerole/molecule/default`, and `roles/otherrole/molecule/default`, -then tox environments will be named `["scenario", "roles-somerole-default", "roles-otherrole-default"]`. +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. -Additional configuration options exist to expand this matrix automatically. For instance, you can have -it auto-generate version with tox factors for different versions of python (e.g. -['py27-user-default', 'py38-user-default']). Additional options can also be added for different versions -of Ansible (e.g. ['ansible27-user-default', 'ansible28-user-default']) +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. -There are also options to filter the list of environments executed. The execution can be filtered to -limit itself to only scenarios with a particular name, to only certain Molecule drivers, or a combination -of the two options. Of course, tox can still be used to execute only one environment by passing the -name directly via e.g. `tox -e roles-myrole-scenario`. +`tox` virtual environments are created in the `.tox` directory. These are easily deleted and recreated if needed. -If an environment already exists that matches the generated environment name, then this plugin -will not override settings specified directly in the tox.ini for that environment. Thus, if you need to customize -a particular run, then you can do so, but still take advantage of the filtering options and -auto-generation of the environments for other scenarios and options. Dependencies defined in the standard -way in tox.ini for any name collision environments will be augmented with the ones needed for -running Molecule and lint. +## Installation -Configuration -============= +Install from pypi: + +```bash +pip install tox-ansible +``` -tox.ini -------- +## Dependencies -Any values in the `envlist` will be left in the default envlist by this plugin. So if you want to have -several envs that do things other than run `molecule ...` commands, you can configure those directly -in the tox.ini file. +`tox-ansible` will install additional dependencies if necessary: -To add global options to the molecule commands, add the arguments in a line list to the "[ansible]" -section key "molecule\_opts". +- `tox` version 4.0 or greater. +- `pytest-ansible` version 3.1.0 or greater. +- `pytest` +- `pytest-xdist` +- `pyyaml` -To test each scenario with specified versions of either Ansible or Python, you can add version -numbers to the keys `ansible` and `python` under the `[ansible]` section of the ini. These versions -take the same format as the `envlist` version familiar to Python users. So, if you want to test on -Ansible 2.9, 2.10, and 3.0 as well as with Python 2.7 and 3.8 then you can add this snippet (values can -be separated by a mix of commas and newlines): +## Usage -```ini -[ansible] -ansible = 2.{9,10},3.0 -python = 2.7,3.8 -# To change how tox env name is build for scanrios, you can use vars like: -# $path - paths under which molecule file is hosted (can be empty string) -# $parent - only the parent folder under which is hosted (can be empty string) -# $name - this is the name of the scenario (folder under molecule/) -# $nondefault_name - same as name but when scenario is named 'default' it becomes empty string -# -# scenario_format = $path-$role-$name +From the root of your collection, create an empty `tox-ansible.ini` file and list the available environments: +```bash +touch tox-ansible.ini +tox list --ansible --conf tox-ansible.ini ``` -If you find the default environment names generated for scenarios too long, -you can configure `scenario_format = $parent-$nondefault_name` which should -produce very short names, regardless if your scenarios are in repository root -or under the roles. That works nicely as long you do not have duplicate -scenario names. - -To pass a configuration file to "[ansible-lint](https://github.com/ansible-community/ansible-lint)", -add the option "ansible\_lint\_config". Similarly to pass a config file option to -"[yamllint](https://github.com/adrienverge/yamllint)", set the option "yamllint\_config" in -the "[ansible]" section. Flake8 can be configured per its normal segment in your tox.ini file. All -three of these commands are run as part of the "lint\_all" environment that this plugin creates. - -requirements.txt ----------------- - -If a particular scenario requires a select set of Python packages to be installed in the virtualenv with -molecule and the like, you can add a "requirements.txt" file to the molecule scenario directory, and that -will be appended to the list of built-in scenario requirements. - -Examples -======== - -## Basic Example - -The following Collections structure - -
.
-├── galaxy.yml
-├── molecule
-│   ├── one
-│   │   └── molecule.yml
-│   └── two
-│       └── molecule.yml
-├── roles
-│   ├── my_role
-│   │   └── molecule
-│   │       ├── otherscenario
-│   │       │   └── molecule.yml
-│   │       └── somescenario
-│   │           └── molecule.yml
-│   └── other_role
-│       └── molecule
-│           ├── basic
-│           │   └── molecule.yml
-│           ├── default
-│           │   └── molecule.yml
-│           └── somescenario
-│               └── molecule.yml
-└── tox.ini
-
- -With the following tox.ini file: +A list of dynamically generated ansible environments will be displayed: -```ini -[tox] -envlist = ``` -Tox-ansible will auto-generate the following environments: + +default environments: +... +integration-py3.11-2.14 -> Integration tests for ansible.scm using ansible-core 2.14 and python 3.11 +integration-py3.11-devel -> Integration tests for ansible.scm using ansible-core devel and python 3.11 +integration-py3.11-milestone -> Integration tests for ansible.scm using ansible-core milestone and python 3.11 +... +sanity-py3.11-2.14 -> Sanity tests for ansible.scm using ansible-core 2.14 and python 3.11 +sanity-py3.11-devel -> Sanity tests for ansible.scm using ansible-core devel and python 3.11 +sanity-py3.11-milestone -> Sanity tests for ansible.scm using ansible-core milestone and python 3.11 +... +unit-py3.11-2.14 -> Unit tests for ansible.scm using ansible-core 2.14 and python 3.11 +unit-py3.11-devel -> Unit tests for ansible.scm using ansible-core devel and python 3.11 +unit-py3.11-milestone -> Unit tests for ansible.scm using ansible-core milestone and python 3.11 +``` + +These represent the testing environments that are available. 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. + +To run tests with a single environment, simply run the following command: ```bash -$ tox -l -lint_all -one -python -roles-my_role-otherscenario -roles-my_role-somescenario -roles-other_role-basic -roles-other_role-default -roles-other_role-somescenario -two +tox -e sanity-py3.11-2.14 --ansible --conf tox-ansible.ini ``` -Note that the "python" environment is a default behavior of Tox, if there are no -environments specified in the config file. To suppress it, specify at least one element -in the envlist entry within tox.ini +To run tests with multiple environments, simply add the environment names to the command: + +```bash +tox -e sanity-py3.11-2.14,unit-py3.11-2.14 --ansible --conf tox-ansible.ini +``` -### tox.ini examples -The `ansible_test_platform` option controls the platform (docker, venv, python version) that ansible-test targets run in. -By default, this is set to `auto` for automatic detection. You can also set this option -to `docker` or `venv` explicitly, or disable the Python and platform auto-detection by setting -this option to `posargs`: +To run all tests of a specific type in all available environments, use the factor `-f` flag: -```ini -[ansible] -ansible_test_platform = posargs # Disable auto-detection +```bash +tox -f unit --ansible -p auto --conf tox-ansible.ini ``` -Note that if you do this, you will have to add your own platform parameters to ansible-test via posargs, -as discussed above. For example, to use a separate container for the controller and target hosts, -you can use the following command: +To run all tests across all available environments: -`$ tox -e integration -- --controller docker:default --target docker:centos7` +```bash +tox --ansible -p auto --conf tox-ansible.ini +``` +Note: The `-p auto` flag will run multiple tests in parallel. +Note: The specific python interpreter will need to be pre-installed on you system, e.g.: -To add arguments to every molecule invocation, add the -following segment to tox.ini. Each argument needs to be on a separate line, which allows -spaces and other special characters to be used without needing shell-style escapes: -```ini -[ansible] -molecule_opts = - --debug +```bash +sudo dnf install python3.9 ``` -If you use a global molecule configuration file at the project level -(`/.config/molecule/config.yml`), it will be detected -automatically and will be the reference in order to determine the default driver -name used for your molecule scenarios. +To review the specific commands and configuration for each of the integration, sanity, and unit factors: -If you want pass one or multiple base configuration file(s) to -"[molecule](https://github.com/ansible-community/molecule)", add the option -"molecule\_config\_files" to the Ansible section and list them as follows. -```ini -[ansible] -molecule_opts = - --debug -molecule_config_files = - {toxinidir}/tests/molecule_one.yml - {toxinidir}/tests/molecule_two.yml +```bash +tox config --ansible --conf tox-ansible.ini ``` -Sometimes there are paths you will want to ignore running tests in. Particularly if you -install other roles or collections underneath of your source tree. You can ignore these paths -with the following tox.ini bit: +## Configuration + +`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 is available: + ```ini +# tox-ansible.ini [ansible] -ignore_path = - dist - generated_paths_to_ignore +skip = + 2.9 + devel ``` -This field is very simple, and should list folder names, anywhere in the tree, to ignore. -It does not do specialized glob matching or sub-path matching at this time. Anything living under -any folder whose name appears in this list will be ignored. -To test with ansible versions 2.7.\*, 2.8.\*, and 2.9.\* across every role and scenario: +This will skip tests in any environment that use ansible 2.9 or the devel branch. The list of strings are used for a simple string in string comparison of environment names. + +## Overriding the configuration + +Any configuration in either the `[testenv]` section or am environment section `[testenv:integration-py3.11-{devel,milestone}]` can override some or all of the `tox-ansible` environment configurations. + +For example + ```ini -[ansible] -ansible = 2.{7,8,9} -``` -Now the output will look like this: -```bash -$ tox -l -ansible27-lint_all -ansible27-one -ansible27-roles-my_role-otherscenario -ansible27-roles-my_role-somescenario -ansible27-roles-other_role-basic -ansible27-roles-other_role-default -ansible27-roles-other_role-somescenario -ansible27-two -ansible28-lint_all -ansible28-one -ansible28-roles-my_role-otherscenario -ansible28-roles-my_role-somescenario -ansible28-roles-other_role-basic -ansible28-roles-other_role-default -ansible28-roles-other_role-somescenario -ansible28-two -ansible29-lint_all -ansible29-one -ansible29-roles-my_role-otherscenario -ansible29-roles-my_role-somescenario -ansible29-roles-other_role-basic -ansible29-roles-other_role-default -ansible29-roles-other_role-somescenario -ansible29-two -python +[testenv] +commands_pre = + true + +[testenv:integration-py3.11-{devel,milestone}] +commands = + true ``` -If you want multiple Python versions, you can also specify that: +will result in: ```ini -[ansible] -python = 2.7,3.8 +# tox-ansible.ini +[testenv:integration-py3.11-devel] +commands_pre = true +commands = true ``` -```bash -$ tox -l -py27-lint_all -py27-one -py27-roles-my_role-otherscenario -py27-roles-my_role-somescenario -py27-roles-other_role-basic -py27-roles-other_role-default -py27-roles-other_role-somescenario -py27-two -py38-lint_all -py38-one -py38-roles-my_role-otherscenario -py38-roles-my_role-somescenario -py38-roles-other_role-basic -py38-roles-other_role-default -py38-roles-other_role-somescenario -py38-two -python +Used without caution, this configuration can result in unexpected behavior, and possible false positive or false negative test results. + +## Usage in a CI/CD pipeline + +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. + +Each environment will be run in a separate job. The workflow will run all jobs in parallel. + +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 the file specified by the "GITHUB_OUTPUT" environment variable. The workflow will read this file and use it to create the matrix. + +A sample use of the github workflow might look like this: + +```yaml +name: Test collection + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + tox-ansible: + uses: tox-dev/tox-ansible/.github/workflows/run.yml@main ``` -Under the hood --------------- - -The plugin will glob the current directory and look for any files matching the glob pattern -`molecule/*/molecule.yml` and make the assumption that these represent Molecule scenarios. - -It then generates new environments for any discovered scenarios that do not already exist -in the tox environment list. These names will include the full path to the scenario folder -with the exception of the `molecule` directory name. So a scenario rooted at `roles/foo/molecule/bar` -will be named `roles-foo-bar`. Similarly one that lives at `molecule/bar` will be named just `bar`. - -Generated environments are added to the default execution envlist with a dependency on -Molecule. This list will be expanded by any configured matrix axes with appropriate dependencies and -configurations made. Each one will execute the command "molecule test -s {scenario}" if it passes the -filter step. - -Environments are configured with the following values, by default, unless they are explicitly specified -in the tox.ini file: -* dependencies -* commands -* working directory -* basepython (if specified in the `[ansible]` expand matrix) -By use of the defined factors in a name, some values can be given in the general tox environment config -section, but the above values will be explicitly specified. So do not rely on setting those values -through the use of factor expansion in a generic section. +Sample `json` + +```json +[ + // ... + { + "description": "Integration tests using ansible-core devel and python 3.11", + "factors": ["integration", "py3.11", "devel"], + "name": "integration-py3.11-devel", + "python": "3.11" + } + // ... +] +``` + +## How does it work? + +`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` 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. + +`tox-ansible` also sets the `ANSIBLE_COLLECTIONS_PATHS` 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_PATHS` environment variable into the collection loader so ansible-core can locate the collection. + +`pytest` is ued to run both the `unit` and `integration tests`. +`ansible-test sanity` is used to run the `sanity` tests. + +For a full configuration examples for each of the sanity, integration, and unit tests including the commands being run and the environments variables being set and passed, see the following: + +- [integration](docs/integration.ini) +- [sanity](docs/sanity.ini) +- [unit](docs/unit.ini) + +See the [tox documentation](https://tox.readthedocs.io/en/latest/) for more information on tox. + +## License + +MIT diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 92ed93b..0000000 --- a/TODO.txt +++ /dev/null @@ -1,3 +0,0 @@ -* Molecule 3.0 support - Now that Molecule 3.0 has been released, it's time to update this to handle -the separation of non-Docker drivers from the core testing diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 69cb760..0000000 --- a/codecov.yml +++ /dev/null @@ -1 +0,0 @@ -comment: false diff --git a/cspell.config.yaml b/cspell.config.yaml new file mode 100644 index 0000000..1d7f78f --- /dev/null +++ b/cspell.config.yaml @@ -0,0 +1,33 @@ +dictionaryDefinitions: + - name: words + path: .config/dictionary.txt + addWords: true +dictionaries: + - bash + - networking-terms + - python + - words + - "!aws" + - "!backwards-compatibility" + - "!cryptocurrencies" + - "!cpp" +ignorePaths: + # All dot files in the root + - \.* + # This file + - cspell.config.yaml + # The mypy configuration file + - mypy.ini + # The shared file for tool configuration + - pyproject.toml + # requirements files + - .config/requirements* + - requirements.txt + # The tox configuration file + - tox.ini + # Sample tox configurations + - docs/*.ini + +languageSettings: + - languageId: python + allowCompoundWords: false diff --git a/docs/galaxy.yml b/docs/galaxy.yml new file mode 100644 index 0000000..5dac889 --- /dev/null +++ b/docs/galaxy.yml @@ -0,0 +1,35 @@ +--- +authors: + - Ansible Network Community (ansible-network) +dependencies: + "ansible.utils": ">=2.6.1" +license_file: LICENSE +name: sample +namespace: ansible +description: A sample galaxy.yml +readme: README.md +repository: https://github.com/ansible-collections/ansible.sample +issues: https://github.com/ansible-collections/ansible.sample/issues +tags: + - application + - cloud + - database + - infrastructure + - linux + - monitoring + - networking + - security + - storage + - tools + - windows +version: 1.0.0 +build_ignore: + - .github + - .gitignore + - .isort.cfg + - .pre-commit-config.yaml + - mypy.ini + - pyproject.toml + - tox.ini + - toxfile.py + - .vscode diff --git a/docs/integration.ini b/docs/integration.ini new file mode 100644 index 0000000..83e2c22 --- /dev/null +++ b/docs/integration.ini @@ -0,0 +1,87 @@ +[testenv:integration-py3.11-devel] +type = VirtualEnvRunner +set_env = + ANSIBLE_COLLECTIONS_PATHS=/home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collections/ + PIP_DISABLE_PIP_VERSION_CHECK=1 + PYTHONHASHSEED=3119075902 + PYTHONIOENCODING=utf-8 +base = testenv +runner = virtualenv +description = Integration tests using ansible-core devel and python 3.11 +depends = +env_name = integration-py3.11-devel +labels = +env_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel +env_tmp_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp +env_log_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/log +suicide_timeout = 0.0 +interrupt_timeout = 0.3 +terminate_timeout = 0.2 +platform = +pass_env = + CC + CCSHARED + CFLAGS + CPPFLAGS + CURL_CA_BUNDLE + CXX + GITHUB_TOKEN + HOME + LANG + LANGUAGE + LDFLAGS + LD_LIBRARY_PATH + PIP_* + PKG_CONFIG + PKG_CONFIG_PATH + PKG_CONFIG_SYSROOT_DIR + REQUESTS_CA_BUNDLE + SSL_CERT_FILE + TMPDIR + VIRTUALENV_* + http_proxy + https_proxy + no_proxy +parallel_show_output = False +recreate = False +allowlist_externals = + bash + cp + git + rm + rsync + mkdir + cd + echo +pip_pre = False +install_command = python -I -m pip install '{packages}' +constrain_package_deps = False +use_frozen_constraints = False +list_dependencies_command = python -m pip freeze --all +commands_pre = + mkdir /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/tmp/collection_build + 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' + 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' + 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 .' +commands = python -m pytest --ansible-unit-inject-only /home/bthornto/github/tox-ansible/docs/tests/integration +commands_post = +change_dir = /home/bthornto/github/tox-ansible/docs +args_are_paths = True +ignore_errors = False +ignore_outcome = False +base_python = py3.11 +env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/lib/python3.11/site-packages +env_bin_dir = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/bin +env_python = /home/bthornto/github/tox-ansible/docs/integration-py3.11-devel/bin/python +py_dot_ver = 3.11 +py_impl = cpython +deps = + pytest + pytest-xdist + git+https://github.com/ansible-community/pytest-ansible.git + https://github.com/ansible/ansible/archive/devel.tar.gz +system_site_packages = False +always_copy = False +download = False +skip_install = True +package = skip diff --git a/docs/sanity.ini b/docs/sanity.ini new file mode 100644 index 0000000..2b7dc36 --- /dev/null +++ b/docs/sanity.ini @@ -0,0 +1,83 @@ +[testenv:sanity-py3.11-devel] +type = VirtualEnvRunner +set_env = + ANSIBLE_COLLECTIONS_PATHS=/home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collections/ + PIP_DISABLE_PIP_VERSION_CHECK=1 + PYTHONHASHSEED=3868722208 + PYTHONIOENCODING=utf-8 +base = testenv +runner = virtualenv +description = Sanity tests using ansible-core devel and python 3.11 +depends = +env_name = sanity-py3.11-devel +labels = +env_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel +env_tmp_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp +env_log_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/log +suicide_timeout = 0.0 +interrupt_timeout = 0.3 +terminate_timeout = 0.2 +platform = +pass_env = + CC + CCSHARED + CFLAGS + CPPFLAGS + CURL_CA_BUNDLE + CXX + GITHUB_TOKEN + HOME + LANG + LANGUAGE + LDFLAGS + LD_LIBRARY_PATH + PIP_* + PKG_CONFIG + PKG_CONFIG_PATH + PKG_CONFIG_SYSROOT_DIR + REQUESTS_CA_BUNDLE + SSL_CERT_FILE + TMPDIR + VIRTUALENV_* + http_proxy + https_proxy + no_proxy +parallel_show_output = False +recreate = False +allowlist_externals = + bash + cp + git + rm + rsync + mkdir + cd + echo +pip_pre = False +install_command = python -I -m pip install '{packages}' +constrain_package_deps = False +use_frozen_constraints = False +list_dependencies_command = python -m pip freeze --all +commands_pre = + mkdir /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/tmp/collection_build + 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' + 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' + 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 .' +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' +commands_post = +change_dir = /home/bthornto/github/tox-ansible/docs +args_are_paths = True +ignore_errors = False +ignore_outcome = False +base_python = py3.11 +env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/lib/python3.11/site-packages +env_bin_dir = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/bin +env_python = /home/bthornto/github/tox-ansible/docs/sanity-py3.11-devel/bin/python +py_dot_ver = 3.11 +py_impl = cpython +deps = https://github.com/ansible/ansible/archive/devel.tar.gz +system_site_packages = False +always_copy = False +download = False +skip_install = True +package = skip diff --git a/tests/fixtures/collection/roles/complex/tasks/main.yml b/docs/tox-ansible.ini similarity index 100% rename from tests/fixtures/collection/roles/complex/tasks/main.yml rename to docs/tox-ansible.ini diff --git a/docs/unit.ini b/docs/unit.ini new file mode 100644 index 0000000..f03d14f --- /dev/null +++ b/docs/unit.ini @@ -0,0 +1,87 @@ +[testenv:unit-py3.11-devel] +type = VirtualEnvRunner +set_env = + ANSIBLE_COLLECTIONS_PATHS=/home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections/ + PIP_DISABLE_PIP_VERSION_CHECK=1 + PYTHONHASHSEED=1318243417 + PYTHONIOENCODING=utf-8 +base = testenv +runner = virtualenv +description = Unit tests using ansible-core devel and python 3.11 +depends = +env_name = unit-py3.11-devel +labels = +env_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel +env_tmp_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp +env_log_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/log +suicide_timeout = 0.0 +interrupt_timeout = 0.3 +terminate_timeout = 0.2 +platform = +pass_env = + CC + CCSHARED + CFLAGS + CPPFLAGS + CURL_CA_BUNDLE + CXX + GITHUB_TOKEN + HOME + LANG + LANGUAGE + LDFLAGS + LD_LIBRARY_PATH + PIP_* + PKG_CONFIG + PKG_CONFIG_PATH + PKG_CONFIG_SYSROOT_DIR + REQUESTS_CA_BUNDLE + SSL_CERT_FILE + TMPDIR + VIRTUALENV_* + http_proxy + https_proxy + no_proxy +parallel_show_output = False +recreate = False +allowlist_externals = + bash + cp + git + rm + rsync + mkdir + cd + echo +pip_pre = False +install_command = python -I -m pip install '{packages}' +constrain_package_deps = False +use_frozen_constraints = False +list_dependencies_command = python -m pip freeze --all +commands_pre = + mkdir /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collection_build + 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' + 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' + 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 .' +commands = bash -c 'cd /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/tmp/collections/ && python -m pytest --ansible-unit-inject-only /home/bthornto/github/tox-ansible/docs/tests/unit' +commands_post = +change_dir = /home/bthornto/github/tox-ansible/docs +args_are_paths = True +ignore_errors = False +ignore_outcome = False +base_python = py3.11 +env_site_packages_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/lib/python3.11/site-packages +env_bin_dir = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/bin +env_python = /home/bthornto/github/tox-ansible/docs/unit-py3.11-devel/bin/python +py_dot_ver = 3.11 +py_impl = cpython +deps = + pytest + pytest-xdist + git+https://github.com/ansible-community/pytest-ansible.git + https://github.com/ansible/ansible/archive/devel.tar.gz +system_site_packages = False +always_copy = False +download = False +skip_install = True +package = skip diff --git a/galaxy.yml b/galaxy.yml deleted file mode 100644 index 334c56b..0000000 --- a/galaxy.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -# Used only for testing in order to make the plugin believe we are inside -# a collection repository (so it adds the extra test environments). -name: foo -namespace: bar -version: 0.0.1 -authors: - - Red Hat -readme: README.md - -build_ignore: - - .ansible - - .cache - - .github - - .gitignore - - .pytest_cache - - .vscode - - .tox - - dist - - tox.ini - - .eggs - - .mypy_cache - - "*.egg-info" - - modules - - module_utils - - infrared_plugin - - scripts diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index e3d459c..0000000 --- a/mypy.ini +++ /dev/null @@ -1,11 +0,0 @@ -[mypy] -python_version = 3.6 -color_output = True -error_summary = True -disallow_untyped_calls = True -warn_redundant_casts = True -no_implicit_optional = True - -# 3rd party ignores, to remove once they add hints -[mypy-tox.*] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index aeb4cdf..641bd30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,111 @@ [build-system] +build-backend = "setuptools.build_meta" requires = [ - "setuptools >= 42.0.0", # required by pyproject+setuptools_scm integration - "setuptools_scm[toml] >= 3.5.0", # required for "no-local-version" scheme - "setuptools_scm_git_archive >= 1.0", - "wheel", + "setuptools>=63", # required by pyproject+setuptools_scm integration + "setuptools_scm[toml]>=7.0.5", # required for "no-local-version" scheme + ] -build-backend = "setuptools.build_meta" + +[project] +name = "tox-ansible" +description = "A radical approach to testing ansible content" +readme = "README.md" +keywords = ["ansible", "collections", "tox"] +license = { text = "MIT" } +maintainers = [{ "name" = "Ansible by Red Hat", "email" = "info@ansible.com" }] +authors = [{ "name" = "Bradley A. Thornton", "email" = "bthornto@redhat.com" }] +requires-python = ">=3.8" +classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Quality Assurance', + 'Topic :: Utilities', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', +] +dynamic = ["dependencies", "optional-dependencies", "version"] + +[project.entry-points.tox] +tox-ansible = "tox_ansible.plugin" + +[tool.setuptools.dynamic] +optional-dependencies.dev = { file = [".config/requirements-dev.txt"] } +optional-dependencies.test = { file = [".config/requirements-test.txt"] } +dependencies = { file = [".config/requirements.in"] } + +[tool.setuptools_scm] +local_scheme = "no-local-version" +write_to = "src/tox_ansible/_version.py" [tool.black] -skip-string-normalization = false +line-length = 100 + +[tool.pylint] + +[tool.pylint.format] +max-line-length = 100 + +[tool.pylint.master] +# pylint defaults + fh for with open .. as (f|fh) +good-names = "i,j,k,ex,Run,_,f,fh" +jobs = 0 +no-docstring-rgx = "__.*__" + +[tool.pylint.messages_control] +# missing module docstring will be picked up by ruff +# could not do infile b/c older version of pylint didn't have it +# and ansible-test sanity uses older version in earlier ansible +disable = [ + "duplicate-code", + "fixme", + "missing-module-docstring", + "too-few-public-methods", + "unsubscriptable-object", +] +enable = [ + "useless-suppression", # Identify unneeded pylint disable statements + +] + +[tool.pytest.ini_options] +addopts = "-n=auto --dist=loadfile --maxfail=10 --durations=30 --showlocals" + +[tool.ruff] +fix = true +line-length = 100 +builtins = ["__"] +select = ["ALL"] +# ARG = flake8 argument, overlap with pylint +# FBT = flake8 boolean, silly +# TID = flake8 tidy imports, need relative for pylint +# C901 = flake8 complexity, fixme +# PLR0915 = too many statments, fixme +# RET504 = prefer less complex return statements +ignore = ["ARG", "FBT", "TID", "C901", "PLR0915", "RET504"] +target-version = "py38" + +[tool.ruff.isort] +lines-after-imports = 2 # Ensures consistency for cases when there's variable vs function/class definitions after imports +lines-between-types = 1 # Separate import/from with 1 line + +[tool.ruff.per-file-ignores] +# S101 Allow assert in tests +# S602 Allow shell in test +# T201 Allow print in tests +"tests/**" = ["S101", "S602", "T201"] + +[tool.ruff.pydocstyle] +convention = "pep257" -[tool.isort] -profile = "black" +[tool.ruff.flake8-pytest-style] +parametrize-values-type = "tuple" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eae1c3a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --extra=docs --extra=test --no-annotate --output-file=requirements.txt +# +cachetools==5.3.0 +cfgv==3.3.1 +chardet==5.1.0 +colorama==0.4.6 +distlib==0.3.6 +execnet==1.9.0 +filelock==3.12.0 +identify==2.5.23 +iniconfig==2.0.0 +nodeenv==1.7.0 +packaging==23.1 +platformdirs==3.5.0 +pluggy==1.0.0 +pre-commit==3.2.2 +pyproject-api==1.5.1 +pytest==7.3.1 +pytest-xdist==3.2.1 +pyyaml==6.0 +tox==4.5.1 +virtualenv==20.23.0 + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5156eab..0000000 --- a/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -[metadata] -name = tox-ansible -url = https://github.com/ansible-community/tox-ansible -project_urls = - Bug Tracker = https://github.com/ansible-community/tox-ansible/issues - Release Management = https://github.com/ansible-community/tox-ansible/releases - CI = https://github.com/ansible-community/tox-ansible/actions - Source Code = https://github.com/ansible-community/tox-ansible -description = Auto-generate environments for molecule role testing -long_description = file: README.md -long_description_content_type = text/markdown -author = Greg Hellings -author_email = greg.hellings@gmail.com -maintainer = Greg Hellings -maintainer_email = greg.hellings@gmail.com -license = MIT -; license_file = LICENSE -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - Framework :: tox - License :: OSI Approved :: GNU General Public License v3 (GPLv3) - Programming Language :: Python - Programming Language :: Python :: 3 - Topic :: Software Development :: Testing -keywords = - ansible - tox - tox-plugin - -[options] -use_scm_version = True -python_requires = >=3.6 -package_dir = - = src -packages = find: -include_package_data = True -zip_safe = False - -# These are required during `setup.py` run: -setup_requires = - setuptools_scm>=1.15.0 - setuptools_scm_git_archive>=1.0 - -# These are required in actual runtime: -install_requires = - tox >= 3.18 # allowlist_externals - py >= 1.9.0 - pyyaml - -[options.entry_points] -tox = - ansible-collection = tox_ansible.hooks - -[options.packages.find] -where = src diff --git a/setup.py b/setup.py deleted file mode 100644 index 201a702..0000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -import setuptools - -if __name__ == "__main__": - setuptools.setup( - use_scm_version={"local_scheme": "no-local-version"}, - setup_requires=["setuptools_scm[toml]>=3.5.0"], - ) diff --git a/src/tox_ansible/__init__.py b/src/tox_ansible/__init__.py index e69de29..d676831 100644 --- a/src/tox_ansible/__init__.py +++ b/src/tox_ansible/__init__.py @@ -0,0 +1 @@ +"""The tox-ansible package.""" diff --git a/src/tox_ansible/_yaml.py b/src/tox_ansible/_yaml.py deleted file mode 100644 index b5d866d..0000000 --- a/src/tox_ansible/_yaml.py +++ /dev/null @@ -1,11 +0,0 @@ -from yaml import load - -try: - from yaml import CLoader as Loader -except ImportError: # pragma: no cover - from yaml import Loader # type: ignore - - -def load_yaml(filename: str): - with open(filename, "r", encoding="utf-8") as c: - return load(c.read(), Loader=Loader) diff --git a/src/tox_ansible/ansible/__init__.py b/src/tox_ansible/ansible/__init__.py deleted file mode 100644 index 19ff515..0000000 --- a/src/tox_ansible/ansible/__init__.py +++ /dev/null @@ -1,204 +0,0 @@ -import copy -import glob -import logging -import sys -from os import path -from typing import Any, Dict - -from tox_ansible._yaml import load_yaml -from tox_ansible.tox_helper import Tox - -from ..tox_ansible_test_case import ToxAnsibleTestCase -from ..tox_lint_case import ToxLintCase -from ..tox_molecule_case import ToxMoleculeCase -from ..utils import use_docker -from .scenario import Scenario - -LOCAL_CONFIG_FILE = ".config/molecule/config.yml" - - -class Ansible(object): - """A generalized class that handles interactions between the plugin and - Ansible structures. It will determine if we are in an Ansible folder - structure, what kind, and fetch the relevant information back from the - filesystem for the caller.""" - - def __init__(self, base="", options=None): - """Create an Ansible object to introspect on whether the given - directory is an Ansible structure or not. Currently aware of a - collection and a role. - - :param base: Path to the folder. Defaults to being relative to - os.path.curdir, but can be absolute""" - if path.isabs(base): - self.directory = base - else: - self.directory = path.abspath(path.join(path.curdir, base)) - self._scenarios = None - self.options = options - self.tox = Tox() - - def molecule_config_files(self): - """Determine if there is a global molecule configuration file at the - project level. If there are molecule base configuration file(s) passed - as an option in the tox.ini file, those will take precedence over the - global configuration file at the project level. - - :return: A list of absolute path of molecule base configuration file. - None, otherwise.""" - global_molecule_config = path.join(self.directory, LOCAL_CONFIG_FILE) - - if self.options.molecule_config_files: - return self.options.molecule_config_files - - if path.isfile(global_molecule_config): - return [global_molecule_config] - - return None - - @property - def molecule_config(self): - """Reads all the molecule base configuration files present and adds them - in the self.molecule_config field. - - :return: A list of one or multiple dictionaries including the content of - the molecule base configuration file(s) - """ - configs = [] - config_files_list = self.molecule_config_files() - if config_files_list: - for config_file in config_files_list: - configs.append(load_yaml(config_file)) - - return configs - - @property - def scenarios(self): - """Recursively searches the potential Ansible directory and looks for any - scenario directories found. - - :return: An array of Scenario objects, empty if none are found""" - # Don't walk the filesystem more often than necessary - if self._scenarios is not None: - return self._scenarios - self._scenarios = [] - - # we resolve symlinks and use a set in order to avoid duplicating - # scenarios when symlinks are used. - files = { - path.realpath(f) - for f in glob.glob( - f"{self.directory}/**/molecule/*/molecule.yml", recursive=True - ) - } - - for file in files: - # Check scenario path - base_dir = path.dirname(file) - # Find if it's anywhere in the ignore list - tree = base_dir.split("/") - ignored = False - for branch in tree: - if branch in self.options.ignore_paths: - ignored = True - if not ignored: - self._scenarios.append( - Scenario( - path.relpath(base_dir, self.directory), - self.molecule_config, - ) - ) - return self._scenarios - - @property - def tox_cases(self): - """Returns a list of TestCase objects that can be queried to create - the structure of a test environment. - - :return: List of TestCase objects""" - - # pylint: disable=fixme - # TODO(ssbarnea): Detect and enable only those tests that do exist - # to avoid confusing tox user. - docker_present = use_docker() - platform = self.options.ansible_test_platform - if (platform == "auto" and docker_present) or platform == "docker": - opts = ["--docker", "default"] - elif platform in ("auto", "venv"): - opts = ["--venv"] - else: - opts = [] - - # We use --venv because otherwise we risk getting errors from the - # system environment, especially as one of tests performed is - # 'pip check'. - ANSIBLE_TEST_COMMANDS: Dict[str, Dict[str, Any]] = { - "integration": { - "args": ["--requirements", *opts], - "requires": "tests/integration", - }, - "network-integration": { - "args": ["--requirements", *opts], - "requires": "tests/network-integration", - }, - # sanity tests do not need presence of sanity check or even tests - # folder - "sanity": {"args": ["--requirements", *opts], "requires": ""}, - "shell": {"args": ["--requirements", *opts]}, - "units": { - "args": ["--requirements", *opts], - "requires": "tests/unit", - }, - "windows-integration": { - "args": ["--requirements", *opts], - "requires": "tests/windows-integration", - }, - # special commands (not supported by us yet) - "coverage": {"requires": "tests/unit"}, - "env": {}, - } - - # Append posargs if any to each command - for value in ANSIBLE_TEST_COMMANDS.values(): - if "args" not in value: - value["args"] = copy.deepcopy(self.tox.posargs) - else: - value["args"].extend(self.tox.posargs) - if ( - "--python" not in self.tox.posargs - and self.options.ansible_test_platform == "auto" - ): - value["args"].extend( - ["--python", f"{sys.version_info[0]}.{sys.version_info[1]}"] - ) - - tox_cases = [] - drivers = {s.driver for s in self.scenarios if s.driver} - for scenario in self.scenarios: - tox_cases.append(ToxMoleculeCase(scenario, drivers=drivers)) - - # if we are inside a collection, we also enable ansible-test support - galaxy_file = path.join(self.directory, "galaxy.yml") - if path.isfile(galaxy_file): - galaxy_config = load_yaml(galaxy_file) - for command in ANSIBLE_TEST_COMMANDS.items(): - if not path.exists( - path.join( - self.directory, - command[1].get("requires", ""), - ) - ): - continue - try: - tox_cases.append( - ToxAnsibleTestCase( - command[0], - args=command[1]["args"], - galaxy_config=galaxy_config, - ) - ) - except RuntimeError as exc: - logging.warning(str(exc)) - - tox_cases.append(ToxLintCase(tox_cases)) - return tox_cases diff --git a/src/tox_ansible/ansible/scenario.py b/src/tox_ansible/ansible/scenario.py deleted file mode 100644 index 75c2a7d..0000000 --- a/src/tox_ansible/ansible/scenario.py +++ /dev/null @@ -1,76 +0,0 @@ -from os import path - -from tox_ansible._yaml import load_yaml - - -class Scenario(object): - """Knows about scenarios.""" - - def __init__(self, directory, global_config=None): - self.directory = directory - self.scenario_file = path.join(self.directory, "molecule.yml") - self.global_config = global_config - - def __str__(self): - return f"{self.name}" - - @property - def run_dir(self): - """The directory this scenario should be run from""" - return path.dirname(path.dirname(self.directory)) - - @property - def config(self): - """Reads the molecule.yml file. Adds it to the self.config - field.""" - return load_yaml(self.scenario_file) - - @property - def name(self): - """Determines the name of the scenario.""" - return path.basename(self.directory) - - @property - def driver(self): - """Reads the driver for this scenario, if one is defined. - If there is a driver found in the scenario configuration and if there - is a global configuration, the driver coming from the scenario - will be returned. Otherwise, the global driver. - - :return: Driver name defined in molecule.yml or None - :raise: A RuntimeError if the driver name is present in multiple - molecule base configuration files given as options in the - tox.ini file. Or if no driver configuration has been found. - """ - if self.config and "driver" in self.config and "name" in self.config["driver"]: - return self.config["driver"]["name"] - - if self.global_config: - drivers_found_number = len( - [i for i, d in enumerate(self.global_config) if "driver" in d] - ) - if drivers_found_number == 0: - return None - - if drivers_found_number == 1: - return self.global_config[-1].get("driver")["name"] - - if drivers_found_number > 1: - raise RuntimeError( - "Driver configuration is present in multiple " - "molecule base configuration files." - ) - - return None - - @property - def requirements(self): - """Checks for the existence of a requirements.txt file in the current - scenario directory, so that a scenario can include custom requirements - without needing to specify them in tox.ini if it doesn't want to. - - :return: path to requirements.txt, if it exists. None otherwise""" - requirements_txt = path.join(self.directory, "requirements.txt") - if path.isfile(requirements_txt): - return requirements_txt - return None diff --git a/src/tox_ansible/filter/__init__.py b/src/tox_ansible/filter/__init__.py deleted file mode 100644 index 44e67a7..0000000 --- a/src/tox_ansible/filter/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from .by_driver import ByDriver -from .by_scenario import ByScenario - - -class Filter(object): - """Filters an envlist""" - - def __init__(self, options): - self._filters = [ - ByScenario(options.scenario), - ByDriver(options.driver), - ] - - def filter(self, envlist): - results = envlist - for f in self._filters: - results = f.filter(results) - return results diff --git a/src/tox_ansible/filter/base_filter.py b/src/tox_ansible/filter/base_filter.py deleted file mode 100644 index 0005d8b..0000000 --- a/src/tox_ansible/filter/base_filter.py +++ /dev/null @@ -1,27 +0,0 @@ -class BaseFilter(object): - """A base class that filters a given envlist based on the requirements of - the given _filter variable. This should be a callable function, but not - a member method of any baseclasses. That is, you should define it to not - require the "self" keyword. See one of the existing implementation classes - that exists herein to see exactly how that happens.""" - - def filter(self, envlist): - """ - Calling this method without properly defining the _filter method will - cause an error. That function must be defined as something which can - be passed to the builtin method "filter" and should expect to be handed - the list of objects we're filtering and their names as a tuple of the - form ('envname', ) from tox. Any environments that - were modified or created as part of this plugin should have an extra - attribute named "scenario" that can be queried. - - :param envlist: A dictionary of the environments and the configs that - will be filtered - :return: The list of environments filtered based on the defined - conditions.""" - # By default we're going to add the list of names we're looking for to - # the self.names attribute. If that is an empty string, then we should - # not filter on that value - if hasattr(self, "names") and len(self.names) == 0: - return envlist - return dict(filter(self._filter, envlist.items())) diff --git a/src/tox_ansible/filter/by_driver.py b/src/tox_ansible/filter/by_driver.py deleted file mode 100644 index 6489a28..0000000 --- a/src/tox_ansible/filter/by_driver.py +++ /dev/null @@ -1,16 +0,0 @@ -from .base_filter import BaseFilter - - -class ByDriver(BaseFilter): - """Filters environments based on what Molecule driver is configured to run - the scenario.""" - - def __init__(self, names): - self.names = names - - def _filter(e): - if hasattr(e[1], "tox_case") and hasattr(e[1].tox_case, "scenario"): - return e[1].tox_case.scenario.driver in self.names - return False - - self._filter = _filter diff --git a/src/tox_ansible/filter/by_scenario.py b/src/tox_ansible/filter/by_scenario.py deleted file mode 100644 index 8ec0354..0000000 --- a/src/tox_ansible/filter/by_scenario.py +++ /dev/null @@ -1,21 +0,0 @@ -from .base_filter import BaseFilter - - -class ByScenario(BaseFilter): - """Filters to only scenarios that match a given name.""" - - def __init__(self, names): - """ - :param envlist: The dictionary of tox environment configs per the base - class argument - :param names: A list of scenario names to match. Only environments - from scenarios whose names are in the given list will be returned.""" - self.names = names - - def _filter(e): - if hasattr(e[1], "tox_case") and hasattr(e[1].tox_case, "scenario"): - # Can still reference "self" because of nesting - return e[1].tox_case.scenario.name in self.names - return False - - self._filter = _filter diff --git a/src/tox_ansible/hooks.py b/src/tox_ansible/hooks.py deleted file mode 100644 index 7c90559..0000000 --- a/src/tox_ansible/hooks.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Tox hook implementations.""" -from __future__ import print_function - -import logging - -try: - from tox import hookimpl - - from tox_ansible import tox_helper - from tox_ansible.ansible import Ansible - from tox_ansible.filter import Filter - from tox_ansible.options import ( - DRIVER_ENV_NAME, - DRIVER_OPTION_NAME, - SCENARIO_ENV_NAME, - SCENARIO_OPTION_NAME, - Options, - ) - - @hookimpl - def tox_addoption(parser): - """Add options to filter down to only executing the given roles and - scenarios.""" - parser.add_argument( - "--ansible-scenario", - dest=SCENARIO_OPTION_NAME, - action="append", - help="Only execute scenarios with the given names (env " - + f"{SCENARIO_ENV_NAME})", - ) - parser.add_argument( - "--ansible-driver", - dest=DRIVER_OPTION_NAME, - action="append", - help="Only execute scenarios with the given driver (env " - + f"{DRIVER_ENV_NAME})", - ) - - @hookimpl - def tox_configure(config): - """If the current folder includes a file named `galaxy.yml`, then look for - a roles directory and generate environments for every (role, scenario) - combo that is discovered therein.""" - tox = tox_helper.Tox(config) - options = Options(tox) - - # Only execute inside of a collection, otherwise we have nothing to do - if options.disabled: - return - - ansible = Ansible(options=options, base=tox.toxinidir) - - # Create any test cases that are discovered in the directory structure and - # expand them per any configured matrix axes in the config file - tox_cases = ansible.tox_cases - tox_cases = options.expand_matrix(tox_cases) - - # Add them to the envconfig list before testing for explicit calls, because - # we want the user to be able to specifically state an auto-generated - # test, if they want to - tox.add_envconfigs(tox_cases, options) - - # Don't filter down or add to envlist if an environment has been - # specified by the user - if hasattr(config, "envlist_explicit") and config.envlist_explicit: - return - - # Add the items we generated to the envlist to be executed by default - # Set, because there might be dupes - config.envlist = list(set(config.envlist + config.ansible_envlist)) - - # Since the user hasn't been explicit about which environments to execute - # against, then we add the ones we've generated to the list, and then we - # filter it - if options.do_filter(): - envfilter = Filter(options) - # Actually modify the envconfigs, because we don't care about ones we - # won't be executing - config.envconfigs = envfilter.filter(config.envconfigs) - config.envlist = list(config.envconfigs.keys()) - config.envlist.sort() - config.envlist_default = config.envlist - -except ImportError: - # tox4 - logging.error("tox-ansible disabled itself as it does not support tox4 yet.") diff --git a/src/tox_ansible/matrix/__init__.py b/src/tox_ansible/matrix/__init__.py deleted file mode 100644 index 4a23b8a..0000000 --- a/src/tox_ansible/matrix/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -class Matrix(object): - def __init__(self): - self.axes = [] - - def add_axis(self, axis): - """Add an extension axis to this matrix. Axes can be found in the - axes.py file and are subclasses of the MatrixAxisBase class. - - :param axis: An expansion axis to add to this matrix.""" - self.axes.append(axis) - - def expand(self, cases): - for axis in self.axes: - cases = axis.expand(cases) - return cases - - -class MatrixAxisBase(object): - """Expands a list of a particular test case by creating copies with the - appropriately named factors and replacing the base case. - - ***THIS IS AN ABSTRACT BASE CLASS***""" - - def __init__(self, versions): - """Initialize a matrix to expand a particular version. - - :param versions: A list of versions to expand this matrix""" - self.versions = versions - - def expand(self, tox_cases): - """Expand the list of test cases by multiplying it by this matrix for - the configured field. - - :param tox_cases: An iterable of the currently existing test cases - :return: A list of the test cases, copied and expanded by this - particular matrix factor.""" - results = [] - for tox_case in tox_cases: - for version in self.versions: - results.append(self.expand_one(tox_case, version)) - return results - - def expand_one(self, tox_case, version): - """Do the actual expansion on a particular test case. - - ***MUST BE OVERRIDDEN BY CHILD CLASSES*** - - :param: tox_case: the test case to be expanded by this particular - axis - :param version: the version of the test case to be expanded in this - step - :return: the resultant new version of the test case""" - raise NotImplementedError diff --git a/src/tox_ansible/matrix/axes.py b/src/tox_ansible/matrix/axes.py deleted file mode 100644 index 39c7ba3..0000000 --- a/src/tox_ansible/matrix/axes.py +++ /dev/null @@ -1,15 +0,0 @@ -from . import MatrixAxisBase - - -class AnsibleAxis(MatrixAxisBase): - """See documentation on the Matrix base class""" - - def expand_one(self, tox_case, version): - return tox_case.expand_ansible(version) - - -class PythonAxis(MatrixAxisBase): - """See documentation on the Matrix base class""" - - def expand_one(self, tox_case, version): - return tox_case.expand_python(version) diff --git a/src/tox_ansible/options.py b/src/tox_ansible/options.py deleted file mode 100644 index 933301f..0000000 --- a/src/tox_ansible/options.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from itertools import chain - -from tox.config import _split_env - -from .matrix import Matrix -from .matrix.axes import AnsibleAxis, PythonAxis - -SCENARIO_OPTION_NAME = "ansible_scenario" -DRIVER_OPTION_NAME = "ansible_driver" - -SCENARIO_ENV_NAME = "TOX_" + SCENARIO_OPTION_NAME.upper() -DRIVER_ENV_NAME = "TOX_" + DRIVER_OPTION_NAME.upper() - -INI_SECTION = "ansible" -INI_PYTHON_VERSIONS = "python" -INI_ANSIBLE_VERSIONS = "ansible" -INI_ANSIBLE_TEST_PLATFORM = "ansible_test_platform" -INI_ANSIBLE_TEST_PLATFORM_DEFAULT = "auto" -INI_ANSIBLE_TEST_PLATFORM_CHOICES = ["auto", "posargs", "docker", "venv"] -INI_MOLECULE_GLOBAL_OPTS = "molecule_opts" -INI_IGNORE_PATHS = "ignore_path" -INI_ANSIBLE_LINT_CONFIG = "ansible_lint_config" -INI_YAMLLINT_CONFIG = "yamllint_config" -INI_MOLECULE_CONFIG_FILES = "molecule_config_files" -INI_SCENARIO_FORMAT = "scenario_format" -INI_SCENARIO_FORMAT_DEFAULT = "$path-$parent-$name" -INI_DISABLED = "disabled" -INI_DISABLED_DEFAULT = False - - -# pylint: disable=too-many-instance-attributes -class Options(object): - """Represents the options, and performs the logic around them.""" - - def __init__(self, tox): - self.tox = tox - self.reader = tox.get_reader(INI_SECTION) - opts = tox.opts - self.scenario = self._parse_opt(opts, SCENARIO_OPTION_NAME, SCENARIO_ENV_NAME) - self.driver = self._parse_opt(opts, DRIVER_OPTION_NAME, DRIVER_ENV_NAME) - self.matrix = Matrix() - self.ansible_lint = self.reader.getstring(INI_ANSIBLE_LINT_CONFIG) - self.yamllint = self.reader.getstring(INI_YAMLLINT_CONFIG) - self.molecule_config_files = self.reader.getlist( - INI_MOLECULE_CONFIG_FILES, sep="\n" - ) - self.scenario_format = self.reader.getstring( - INI_SCENARIO_FORMAT, INI_SCENARIO_FORMAT_DEFAULT - ) - - ansible = self.reader.getlist(INI_ANSIBLE_VERSIONS) - ansible = _split_env(ansible) - if ansible: - self.matrix.add_axis(AnsibleAxis(ansible)) - self.ansible_test_platform = self.reader.getstring( - INI_ANSIBLE_TEST_PLATFORM, INI_ANSIBLE_TEST_PLATFORM_DEFAULT - ) - if self.ansible_test_platform not in INI_ANSIBLE_TEST_PLATFORM_CHOICES: - raise ValueError( - "Invalid value for 'ansible_test_platform': " - f"{self.ansible_test_platform}. " - "Supported values are: " - f"{' '.join(INI_ANSIBLE_TEST_PLATFORM_CHOICES)}" - ) - - pythons = self.reader.getlist(INI_PYTHON_VERSIONS) - pythons = _split_env(pythons) - if pythons: - self.matrix.add_axis(PythonAxis(pythons)) - - def expand_matrix(self, tox_cases): - """Expand the tox_cases list if there are any matrix factors defined - to be expanded. Return the list of resulting types. - - :param tox_cases: An iterable of test case objects - :return: A list of test cases expanded per any configured matrix - values""" - return self.matrix.expand(tox_cases) - - def do_filter(self): - """Determine if we should be filtering or not. Only do so if there are - arguments to do the filtering around. Otherwise, we don't want to leave - no environments to execute against.""" - return len(self.scenario) != 0 or len(self.driver) != 0 - - @property - def global_opts(self): - opts = self.reader.getlist(INI_MOLECULE_GLOBAL_OPTS, sep="\n") - return opts - - @property - def ignore_paths(self): - paths = self.reader.getlist(INI_IGNORE_PATHS, sep="\n") - return paths - - @property - def disabled(self): - return self.reader.getbool(INI_DISABLED, INI_DISABLED_DEFAULT) - - def _parse_opt(self, option, opt, env): - if ( - isinstance(option, dict) - and opt in option.keys() - and option[opt] is not None - ): - values = list(map(lambda a: a.split(","), option[opt])) - values = list(chain.from_iterable(values)) - return values - - if env in os.environ: - return os.environ[env].split(",") - - return [] diff --git a/src/tox_ansible/plugin.py b/src/tox_ansible/plugin.py new file mode 100644 index 0000000..124d350 --- /dev/null +++ b/src/tox_ansible/plugin.py @@ -0,0 +1,539 @@ +# cspell:ignore envlist +"""tox plugin to emit a github matrix.""" + +import json +import logging +import os +import re +import sys +import uuid + +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import List, Tuple, TypeVar + +import yaml + +from tox.config.cli.parser import ToxParser +from tox.config.loader.memory import MemoryLoader +from tox.config.loader.section import Section +from tox.config.loader.str_convert import StrConvert +from tox.config.sets import ConfigSet, CoreConfigSet, EnvConfigSet +from tox.config.types import EnvList +from tox.plugin import impl +from tox.session.state import State +from tox.tox_env.python.api import PY_FACTORS_RE + + +ALLOWED_EXTERNALS = [ + "bash", + "cp", + "git", + "rm", + "rsync", + "mkdir", + "cd", + "echo", +] +ENV_LIST = """ +{integration, sanity, unit}-py3.8-{2.9, 2.12, 2.13} +{integration, sanity, unit}-py3.9-{2.12, 2.13, 2.14, milestone, devel} +{integration, sanity, unit}-py3.10-{2.12, 2.13, 2.14, milestone, devel} +{integration, sanity, unit}-py3.11-{2.14, milestone, devel} +""" +TOX_WORK_DIR = Path() +OUR_DEPS = [ + "pytest", + "pytest-xdist", + "git+https://github.com/ansible-community/pytest-ansible.git", +] + +T = TypeVar("T", bound=ConfigSet) + + +class AnsibleConfigSet(ConfigSet): + """The ansible configuration.""" + + def register_config(self: T) -> None: + """Register the ansible configuration.""" + self.add_config( + "skip", + of_type=List[str], + default=[], + desc="ansible configuration", + ) + + +@dataclass +class AnsibleTestConf: # pylint: disable=too-many-instance-attributes + """Ansible test configuration.""" + + description: str + deps: str + setenv: str + skip_install: bool + allowlist_externals: List[str] = field(default_factory=list) + commands_pre: List[str] = field(default_factory=list) + commands: List[str] = field(default_factory=list) + passenv: List[str] = field(default_factory=list) + + +def custom_sort(string: str) -> Tuple[int, ...]: + """Convert a env name into a tuple of ints. + + In the case of a string, use the ord() of the first two characters. + + :param string: The string to sort. + :return: The tuple of converted values. + """ + parts = re.split(r"\.|-|py", string) + converted = [] + for part in parts: + if not part: + continue + try: + converted.append(int(part)) + except ValueError: + num_part = "".join((str(ord(char)).rjust(3, "0")) for char in part[0:2]) + converted.append(int(num_part)) + return tuple(converted) + + +@impl +def tox_add_option(parser: ToxParser) -> None: + """Add the --gh-matrix option to the tox CLI. + + :param parser: The tox CLI parser. + """ + parser.add_argument( + "--gh-matrix", + action="store_true", + default=False, + help="Emit a github matrix", + ) + + parser.add_argument( + "--ansible", + action="store_true", + default=False, + help="Enable ansible testing", + ) + + +@impl +def tox_add_core_config( + core_conf: CoreConfigSet, # pylint: disable=unused-argument + state: State, +) -> None: + """Dump the environment list and exit. + + :param core_conf: The core configuration object. + :param state: The state object. + """ + if state.conf.options.gh_matrix and not state.conf.options.ansible: + err = "The --gh-matrix option requires --ansible" + logging.critical(err) + sys.exit(1) + + if not state.conf.options.ansible: + return + + if state.conf.src_path.name == "tox.ini": + msg = ( + "Using a default tox.ini file with tox-ansible plugin is not recommended." + " Consider using a tox-ansible.ini file and specify it on the command line" + " (`tox --ansible -c tox-ansible.ini`) to avoid unintentionally overriding" + " the tox-ansible environment configurations." + ) + logging.warning(msg) + + global TOX_WORK_DIR # pylint: disable=global-statement # noqa: PLW0603 + TOX_WORK_DIR = state.conf.work_dir + env_list = add_ansible_matrix(state) + + if not state.conf.options.gh_matrix: + return + + generate_gh_matrix(env_list=env_list) + sys.exit(0) + + +@impl +def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: + """Add the test requirements and ansible-core to the virtual environment. + + :param env_conf: The environment configuration object. + :param state: The state object. + """ + if not state.conf.options.ansible: + return + + factors = env_conf.name.split("-") + expected_factors = 3 + if len(factors) != expected_factors or factors[0] not in [ + "integration", + "sanity", + "unit", + ]: + return + + galaxy_path = TOX_WORK_DIR / "galaxy.yml" + c_name, c_namespace = get_collection_name(galaxy_path=galaxy_path) + + conf = AnsibleTestConf( + allowlist_externals=ALLOWED_EXTERNALS, + commands_pre=conf_commands_pre( + c_name=c_name, + c_namespace=c_namespace, + env_conf=env_conf, + ), + commands=conf_commands( + c_name=c_name, + c_namespace=c_namespace, + env_conf=env_conf, + test_type=factors[0], + ), + description=desc_for_env(env_conf.name), + deps=conf_deps(env_conf=env_conf, test_type=factors[0]), + passenv=conf_passenv(), + setenv=conf_setenv(env_conf), + skip_install=True, + ) + loader = MemoryLoader(**asdict(conf)) + env_conf.loaders.append(loader) + + +def desc_for_env(env: str) -> str: + """Generate a description for an environment. + + :param env: The environment name. + :return: The environment description. + """ + test_type, python, core = env.split("-") + ansible_pkg = "ansible" if core == "2.9" else "ansible-core" + + description = ( + f"{test_type.capitalize()} tests using {ansible_pkg} {core} and python {python[2:]}" + ) + return description + + +def in_action() -> bool: + """Check if running on Github Actions platform. + + :return: True if running on Github Actions platform + """ + return os.environ.get("GITHUB_ACTIONS") == "true" + + +def add_ansible_matrix(state: State) -> EnvList: + """Add the ansible matrix to the state. + + :param state: The state object. + :return: The environment list. + """ + ansible_config = state.conf.get_section_config( + Section(None, "ansible"), + base=[], + of_type=AnsibleConfigSet, + for_env=None, + ) + env_list = StrConvert().to_env_list(ENV_LIST) + env_list.envs = [ + env for env in env_list.envs if all(skip not in env for skip in ansible_config["skip"]) + ] + env_list.envs = sorted(env_list.envs, key=custom_sort) + state.conf.core.loaders.append( + MemoryLoader(env_list=env_list), + ) + return env_list + + +def generate_gh_matrix(env_list: EnvList) -> None: + """Generate the github matrix. + + :param env_list: The environment list. + """ + results = [] + + for env_name in env_list.envs: + candidates = [] + factors = env_name.split("-") + for factor in factors: + match = PY_FACTORS_RE.match(factor) + if match: + candidates.append(match[2]) + if len(candidates) > 1: + err = f"Multiple python versions found in {env_name}" + logging.critical(err) + sys.exit(1) + if len(candidates) == 0: + err = f"No python versions found in {env_name}" + logging.critical(err) + sys.exit(1) + if "." in candidates[0]: + version = candidates[0] + else: + version = f"{candidates[0][0]}.{candidates[0][1:]}" + results.append( + { + "description": desc_for_env(env_name), + "factors": factors, + "name": env_name, + "python": version, + }, + ) + + gh_output = os.getenv("GITHUB_OUTPUT") + if not gh_output and not in_action(): + value = json.dumps(results, indent=2, sort_keys=True) + print(value) # noqa: T201 + return + + if not gh_output: + err = "GITHUB_OUTPUT environment variable not set" + logging.critical(err) + sys.exit(1) + + value = json.dumps(results) + + if "\n" in value: + eof = f"EOF-{uuid.uuid4()}" + encoded = f"envlist<<{eof}\n{value}\n{eof}\n" + else: + encoded = f"envlist={value}\n" + + with Path(gh_output).open("a", encoding="utf-8") as fileh: + fileh.write(encoded) + + +def get_collection_name(galaxy_path: Path) -> Tuple[str, str]: + """Extract collection information from the galaxy.yml file. + + :param galaxy_path: The path to the galaxy.yml file. + :return: The collection name. + """ + try: + with galaxy_path.open() as galaxy_file: + galaxy = yaml.safe_load(galaxy_file) + except FileNotFoundError: + err = f"Unable to find galaxy.yml file at {galaxy_path}" + logging.critical(err) + sys.exit(1) + + try: + c_name = galaxy["name"] + c_namespace = galaxy["namespace"] + except KeyError as exc: + err = f"Unable to find {exc} in galaxy.yml" + logging.critical(err) + sys.exit(1) + return c_name, c_namespace + + +def conf_commands( + c_name: str, + c_namespace: str, + env_conf: EnvConfigSet, + test_type: str, +) -> List[str]: + """Build the commands for the tox environment. + + :param c_name: The collection name. + :param c_namespace: The collection namespace. + :param test_type: The test type, either "integration", "unit", or "sanity". + :param env_conf: The tox environment configuration object. + :return: The commands to run. + """ + if test_type in ["integration", "unit"]: + return conf_commands_for_integration_unit( + env_conf=env_conf, + test_type=test_type, + ) + if test_type == "sanity": + return conf_commands_for_sanity( + c_name=c_name, + c_namespace=c_namespace, + env_conf=env_conf, + ) + err = f"Unknown test type {test_type}" + logging.critical(err) + sys.exit(1) + + +def conf_commands_for_integration_unit( + env_conf: EnvConfigSet, + test_type: str, +) -> List[str]: + """Build the commands for integration and unit tests. + + :param env_conf: The tox environment configuration object. + :param test_type: The test type, either "integration" or "unit". + :return: The command to run. + """ + commands = [] + envtmpdir = env_conf["envtmpdir"] + # Use pytest ansible unit inject only to inject the collection path + # into the collection finder + command = f"python -m pytest {TOX_WORK_DIR}/tests/{test_type}" + unit_ch_dir = f"{envtmpdir}/collections/" + if test_type == "unit": + commands.append(f"bash -c 'cd {unit_ch_dir} && {command}'") + else: + commands.append(command) + return commands + + +def conf_commands_for_sanity( + c_name: str, + c_namespace: str, + env_conf: EnvConfigSet, +) -> List[str]: + """Add commands for sanity tests. + + :param c_name: The collection name. + :param c_namespace: The collection namespace. + :param env_conf: The tox environment configuration object. + :return: The commands to run. + """ + commands = [] + envtmpdir = env_conf["envtmpdir"] + + py_ver = env_conf["basepython"][0].replace("py", "") + if "." not in py_ver: + py_ver = f"{py_ver[0]}.{py_ver[1:]}" + + command = f"ansible-test sanity --local --requirements --python {py_ver}" + ch_dir = f"cd {envtmpdir}/collections/ansible_collections/{c_namespace}/{c_name}" + full_command = f"bash -c '{ch_dir} && {command}'" + commands.append(full_command) + return commands + + +def conf_commands_pre( + env_conf: EnvConfigSet, + c_name: str, + c_namespace: str, +) -> List[str]: + """Build and install the collection. + + :param env_conf: The tox environment configuration object. + :param c_name: The collection name. + :param c_namespace: The collection namespace. + :return: The commands to pre run. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + commands = [] + + # Define some directories" + envtmpdir = env_conf["envtmpdir"] + collections_root = f"{envtmpdir}/collections" + collection_installed_at = f"{collections_root}/ansible_collections/{c_namespace}/{c_name}" + galaxy_build_dir = f"{envtmpdir}/collection_build" + end_group = "echo ::endgroup::" + + if in_action(): + group = "echo ::group::Make the galaxy build dir" + commands.append(group) + commands.append(f"mkdir {galaxy_build_dir}") + if in_action(): + commands.append(end_group) + + if in_action(): + group = "echo ::group::Copy the collection to the galaxy build dir" + commands.append(group) + cd_tox_dir = f"cd {TOX_WORK_DIR}" + rsync_cmd = f'rsync -r --cvs-exclude --filter=":- .gitignore" . {galaxy_build_dir}' + full_cmd = f"bash -c '{cd_tox_dir} && {rsync_cmd}'" + commands.append(full_cmd) + if in_action(): + commands.append(end_group) + + if in_action(): + group = "echo ::group::Build and install the collection" + commands.append(group) + cd_build_dir = f"cd {galaxy_build_dir}" + build_cmd = "ansible-galaxy collection build" + tar_file = f"{c_namespace}-{c_name}-*.tar.gz" + install_cmd = f"ansible-galaxy collection install {tar_file} -p {collections_root}" + full_cmd = f"bash -c '{cd_build_dir} && {build_cmd} && {install_cmd}'" + commands.append(full_cmd) + if in_action(): + commands.append(end_group) + + if in_action(): + group = "echo ::group::Initialize the collection to avoid ansible #68499" + commands.append(group) + cd_install_dir = f"cd {collection_installed_at}" + git_cfg = "git config --global init.defaultBranch main" + git_init = "git init ." + full_cmd = f"bash -c '{cd_install_dir} && {git_cfg} && {git_init}'" + commands.append(full_cmd) + if in_action(): + commands.append(end_group) + + if env_conf.name == "sanity-py3.8-2.9": + # Avoid "Setuptools is replacing distutils" + if in_action(): + group = "echo ::group::Use old setuptools for sanity-py3.8-2.9" + commands.append(group) + pip_install = "pip install setuptools==57.5.0" + commands.append(pip_install) + if in_action(): + commands.append(end_group) + + return commands + + +def conf_deps(env_conf: EnvConfigSet, test_type: str) -> str: + """Add dependencies to the tox environment. + + :param env_conf: The environment configuration. + :param test_type: The test type. + :return: The dependencies. + """ + deps = [] + if test_type in ["integration", "unit"]: + deps.extend(OUR_DEPS) + try: + with (TOX_WORK_DIR / "test-requirements.txt").open() as fileh: + deps.extend(fileh.read().splitlines()) + except FileNotFoundError: + pass + try: + with (TOX_WORK_DIR / "requirements.txt").open() as fileh: + deps.extend(fileh.read().splitlines()) + except FileNotFoundError: + pass + + ansible_version = env_conf.name.split("-")[2] + base_url = "https://github.com/ansible/ansible/archive/" + if ansible_version in ["devel", "milestone"]: + ansible_package = f"{base_url}{ansible_version}.tar.gz" + else: + ansible_package = f"{base_url}stable-{ansible_version}.tar.gz" + deps.append(ansible_package) + return "\n".join(deps) + + +def conf_passenv() -> List[str]: + """Build the pass environment variables for the tox environment. + + :return: The pass environment variables. + """ + passenv = [] + passenv.append("GITHUB_TOKEN") + return passenv + + +def conf_setenv(env_conf: EnvConfigSet) -> str: + """Build the set environment variables for the tox environment. + + :param env_conf: The environment configuration. + :return: The set environment variables. + """ + envtmpdir = env_conf["envtmpdir"] + setenv = [] + setenv.append(f"ANSIBLE_COLLECTIONS_PATHS={envtmpdir}/collections/") + return "\n".join(setenv) diff --git a/src/tox_ansible/tox_ansible_test_case.py b/src/tox_ansible/tox_ansible_test_case.py deleted file mode 100644 index 977b0e0..0000000 --- a/src/tox_ansible/tox_ansible_test_case.py +++ /dev/null @@ -1,90 +0,0 @@ -import os - -from .tox_base_case import ToxBaseCase - -DEFAULT_DESCRIPTION = "Auto-generated for: molecule test -s {scenario_name}" - - -# pylint: disable=too-many-instance-attributes -class ToxAnsibleTestCase(ToxBaseCase): - def __init__(self, command, name_parts=None, args=None, galaxy_config=None): - """Create the base test case. - - :param scenario: The scenario that this test case should run""" - _galaxy_fields = ("name", "namespace", "version") - self._cases = [] - self.command = command - self.args = args or [] - self.galaxy_config = galaxy_config or {} - if not galaxy_config or any(k not in galaxy_config for k in _galaxy_fields): - raise RuntimeError( - "Invalid galaxy.yml content, missing one of " - f"required keys {', '.join(_galaxy_fields)} but we got {galaxy_config}" - ) - - self.namespace = galaxy_config["namespace"] - self.collection = galaxy_config["name"] - self.version = galaxy_config["version"] - - if self.command == "coverage": - self.args = ["--venv", "--requirements"] - - self._dependencies = [ - # to build collections we want to be sure we use latest version - # of ansible core, as older versions like 2.9 do not support - # ignore patterns. Install can be done with older versions. - "ansible-core>=2.11.3", - ] - self._name_parts = name_parts or [] - super().__init__() - - def get_name(self, fmt=""): - return self.command - - @property - def description(self): - return f"Auto-generated for: ansible-test {self.command} {' '.join(self.args)}" - - @property - def dependencies(self): - return self._dependencies - - @property - def working_dir(self): - """Get the directory where the test should be executed. - - :return: Path where the test case should be executed from""" - - return os.path.expanduser( - "~/.ansible/collections/ansible_collections/" - f"{self.namespace}/{self.collection}" - ) - - def get_commands(self, options): - if self.command != "coverage": - commands = [ - ["ansible-test", self.command, *self.args], - ] - else: - commands = [ - # avoid ansible-test failure to erase missing location... - ["mkdir", "-p", "tests/output/coverage"], - ["ansible-test", self.command, "erase", *self.args], - ["ansible-test", "units", "--coverage", *self.args], - ["ansible-test", "integration", "--coverage", *self.args], - ["ansible-test", self.command, "report", *self.args], - ] - - return [ - [ - "bash", - "-c", - ( - f"cd {self._config.toxinidir} && " - "ansible-galaxy collection build -v -f --output-path dist/ && " - "ansible-galaxy collection install -f " - f"dist/{self.namespace}-{self.collection}-{self.version}.tar.gz" - ), - ], - *commands, - ] diff --git a/src/tox_ansible/tox_base_case.py b/src/tox_ansible/tox_base_case.py deleted file mode 100644 index eba744e..0000000 --- a/src/tox_ansible/tox_base_case.py +++ /dev/null @@ -1,71 +0,0 @@ -import copy - -from .tox_helper import Tox - - -class ToxBaseCase(object): - def __init__(self): - # Some of the matrix fields we might care about later. They should be - # copied in the "expand" method below, if you add any more to this - # area in the future - self.python = None - self.ansible = None - self._config = Tox() - - @property - def basepython(self): - """The python version that should be used to execute this, if a - particular one is requested. If not, then leave it up to the system - default. The name of the executable is arrived at simply by appending - the configured python Version to the word "python". So if you have - specified python as "3.9" then this method will yield "python3.9" - - :return: Python executable to execute against, if any, else None""" - if self.python is None: - return None - return "python" + self.python - - def _expand(self, name): - """Create a copy of this role, expanded with the additional name field - and other such niceties. - - :param name: An additional field to be added to the name factors - :return: A copy of this object with the additional name factor""" - clone = copy.copy(self) - clone._name_parts.insert(0, name) # pylint: disable=protected-access - clone.python = self.python - clone.ansible = self.ansible - return clone - - def expand_python(self, version): - """Create a copy of this Test Case, but add a factor to the name to - reflect a particular version of Python - - :param version: String representation of Python version (e.g. '2.7') - :return: A copy of this test case expanded to specify the given version - of python""" - copy = self._expand("py" + version.replace(".", "")) - copy.python = version - - return copy - - def expand_ansible(self, version): - """Create a copy of this Test CAse, but add a factor to the name to - reflect a particular version of Ansible - - :param version: String representation of Ansible version (e.g. 2.8) - :return: A copy of this test case expanded to specify the given version - of Ansible""" - copy = self._expand("ansible" + version.replace(".", "")) - copy.ansible = version - - return copy - - def __copy__(self): - obj = type(self).__new__(self.__class__) - for k, v in self.__dict__.items(): - if k in ("scenario", "_config", "_drivers"): - obj.__dict__[k] = v - else: - obj.__dict__[k] = copy.copy(v) - return obj diff --git a/src/tox_ansible/tox_helper.py b/src/tox_ansible/tox_helper.py deleted file mode 100644 index e6d1c0f..0000000 --- a/src/tox_ansible/tox_helper.py +++ /dev/null @@ -1,150 +0,0 @@ -from os.path import abspath, isfile, join - -import py -from tox.config import DepOption, ParseIni, SectionReader, testenvprefix - -passenv_list = ( - # ansible-test does not work without HOME directory - "HOME", - # both ansible-test and molecule may fail to run if these are not passed: - "DOCKER_HOST", - "CONTAINER_HOST", - "SSH_AUTH_SOCK", -) - - -class Tox(object): - instance = None - """A class that handles interacting with the specific internals of the tox - world for the plugin.""" - - def __new__(cls, *args): - if cls.instance is None: - cls.instance = super(Tox, cls).__new__(cls) - return cls.instance - - def __init__(self, config=None): - """Initialize this object - - :param config: the tox config object""" - if config is not None: - self.config = config - - def get_reader(self, section, prefix=None): - """Creates a SectionReader and configures it with known and reasonable - substitution values based on the config. - - :param section: Config section name to read from - :param prefix: Any applicable prefix to the ini section name. Default - None""" - # pylint: disable=protected-access - reader = SectionReader(section, self.config._cfg, prefix=prefix) - distshare_default = join(str(self.config.homedir), ".tox", "distshare") - reader.addsubstitutions( - toxinidir=self.config.toxinidir, - homedir=self.config.homedir, - toxworkdir=self.config.toxworkdir, - ) - self.config.distdir = reader.getpath( - "distdir", join(str(self.config.toxworkdir), "dist") - ) - reader.addsubstitutions(distdir=self.config.distdir) - self.config.distshare = reader.getpath("distshare", distshare_default) - reader.addsubstitutions(distshare=self.config.distshare) - return reader - - @property - def posargs(self): - """Returns any configured posargs from the tox world""" - return self.config.option.args - - @property - def toxinidir(self): - """Returns the configured toxinidir for working with base directory paths""" - return self.config.toxinidir - - @property - def opts(self): - """Return the options as a dictionary-style object. - - :return: A dictionary of the command line options""" - return vars(self.config.option) - - def add_envconfigs(self, tox_cases, options): - """Modifies the list of envconfigs in tox to add any that were - generated by this plugin. - - :param tox_cases: An iterable of test cases to create environments - from""" - # Stripped down version of parseini.__init__ for making a generated - # envconfig - prefix = "tox" if self.config.toxinipath.basename == "setup.cfg" else None - reader = self.get_reader("tox", prefix=prefix) - make_envconfig = ParseIni.make_envconfig - skip_install = False - # Python 2 fix - make_envconfig = getattr(make_envconfig, "__func__", make_envconfig) - - if not isfile(join(self.config.toxinidir, "setup.py")) and not isfile( - join(self.config.toxinidir, "pyproject.toml") - ): - skip_install = True - - # Store the generated ansible envlist - self.config.ansible_envlist = [] - for tox_case in tox_cases: - tox_case_name = tox_case.get_name(fmt=options.scenario_format) - section = testenvprefix + tox_case_name - # pylint: disable=protected-access - config = make_envconfig( - self.config, tox_case_name, section, reader._subs, self.config - ) - config.tox_case = tox_case - # We do not want to create new environments for each command we run - config.envdir = py._path.local.LocalPath(abspath(join(".tox", "ansible"))) - - if skip_install: - config.skip_install = skip_install - self.customize_envconfig(config, options) - for v in passenv_list: - if v not in config.passenv: - config.passenv.add(v) - # We do not want to install packages - config.skipsdist = True - - self.config.envconfigs[tox_case_name] = config - self.config.ansible_envlist.append(tox_case_name) - - def customize_envconfig(self, config, options): - """Writes the fields of the envconfig that need to be given default - molecule related values. - - :param config: the constructed envconfig for this to customize""" - tox_case = config.tox_case - if not config.description: - config.description = tox_case.description - # We do not want to use commands from existing default environment - # as these may likely be others tests. Generated environments will - # have their own internal commands. - config.commands = tox_case.get_commands(options) - # Default deps to install molecule, etc - do = DepOption() - processed_deps = do.postprocess(config, tox_case.dependencies) - if config.deps: - processed_deps = config.deps + processed_deps - config.deps = processed_deps - # Cannot run in {toxinidir}, which is default - if not config.envdir or config.envdir == self.config.toxinidir: - config.envdir = self.config.toxworkdir.join("ansible") - # Need to run molecule from the role directory - if not config.changedir or config.changedir == self.config.toxinidir: - config.changedir = py.path.local(tox_case.working_dir) - if not config.basepython and tox_case.python is not None: - config.basepython = tox_case.basepython - - if hasattr(config, "whitelist_externals"): - allowlist = "whitelist_externals" - else: - allowlist = "allowlist_externals" - if not getattr(config, allowlist): - config.allowlist_externals = ["bash"] diff --git a/src/tox_ansible/tox_lint_case.py b/src/tox_ansible/tox_lint_case.py deleted file mode 100644 index 3af95b5..0000000 --- a/src/tox_ansible/tox_lint_case.py +++ /dev/null @@ -1,62 +0,0 @@ -from copy import copy -from pathlib import Path - -from .tox_base_case import ToxBaseCase -from .tox_helper import Tox - -BASH = "cd {} && molecule {} lint -s {}" - - -class ToxLintCase(ToxBaseCase): - description = "Auto-generated lint for ansible cases" - - def __init__(self, cases, name_parts=None): - self._cases = copy(cases) - self._name_parts = name_parts or [] - self._config = Tox() - super().__init__() - - def get_commands(self, options): - if self.is_precommit: - cmds = [["pre-commit", "run", "--all"]] - else: - cmds = [] - # Construct the ansible-lint command - ansible_lint = ["ansible-lint", "-R"] - if options.ansible_lint: - ansible_lint.append("-c") - ansible_lint.append(options.ansible_lint) - cmds.append(ansible_lint) - - # Construct the yamllint command - if options.yamllint: - yamllint = ["yamllint", "-c", options.yamllint, "."] - cmds.append(yamllint) - - # Construct the flake8 invocation - flake8 = ["flake8", "."] - cmds.append(flake8) - - return cmds - - @property - def is_precommit(self) -> bool: - """Determines if this repository is configured to use pre-commit - or not.""" - p = Path(self.working_dir) / ".pre-commit-config.yaml" - return p.exists() - - @property - def working_dir(self): - return self._config.toxinidir - - @property - def dependencies(self): - if self.is_precommit: - deps = set(["pre-commit"]) - else: - deps = set(["flake8", "ansible-lint", "yamllint", "ansible"]) - return deps - - def get_name(self, fmt=""): - return "-".join(self._name_parts + ["lint_all"]) diff --git a/src/tox_ansible/tox_molecule_case.py b/src/tox_ansible/tox_molecule_case.py deleted file mode 100644 index 91be551..0000000 --- a/src/tox_ansible/tox_molecule_case.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -from functools import lru_cache -from string import Template -from typing import Iterable, List, Optional - -from .options import INI_SCENARIO_FORMAT_DEFAULT -from .tox_base_case import ToxBaseCase -from .tox_helper import Tox - -DRIVER_DEPENDENCIES = { - # Because of the close relationship of these two, it's not uncommon to run one - # scenario using the other driver if your system does not support one or the - # other. So we'll choose to install only one of them - "containers": ["molecule-containers"], - "docker": ["molecule-docker", "molecule-podman"], - "podman": ["molecule-podman", "molecule-docker"], - "openstack": ["molecule-openstack", "openstacksdk", "os-client-config"], - "ec2": ["molecule-ec2", "boto", "boto3"], - "vagrant": ["molecule-vagrant", "python-vagrant"], -} - - -DEFAULT_DESCRIPTION = "Auto-generated for: {cwd_cmd}molecule test -s {scenario_name}" - - -class ToxMoleculeCase(ToxBaseCase): - """Represents a generalized Test Case for an Ansible structure.""" - - def __init__(self, scenario, name_parts=None, drivers: Optional[List[str]] = None): - """Create the base test case. - - :param scenario: The scenario that this test case should run""" - self.scenario = scenario - self._dependencies = [ - "molecule", - "ansible-lint", - "yamllint", - "flake8", - "pytest", - "testinfra", - ] - self._name_parts = name_parts or [] - if drivers: - self._drivers = drivers - else: - self._drivers = [] - super().__init__() # type: ignore - - def get_commands(self, options): - """Get the commands that this scenario should execute. - - :param options: The options object for this plugin - :return: the default commands to run to execute this test case, if the - user does not configure them explicitly""" - molecule = ["molecule"] - molecule.extend(options.global_opts) - - if options.molecule_config_files: - for config_file in options.molecule_config_files: - molecule.extend(["-c", config_file]) - - molecule.extend(["test", "-s", self.scenario.name]) - tox = Tox() - molecule.extend(tox.posargs) - return [molecule] - - @property - def working_dir(self): - """Get the directory where the test should be executed. - - :return: Path where the test case should be executed from""" - return os.path.dirname(os.path.dirname(self.scenario.directory)) - - @property - def dependencies(self) -> Iterable: - """The dependencies for this particular test case. - - :return: A list of the pip dependencies for this test case""" - dependencies = self._dependencies - if self.ansible is not None: - dependencies.append(f"ansible=={self.ansible}.*") - else: - dependencies.append("ansible") - for driver in self._drivers: - if driver in DRIVER_DEPENDENCIES: - dependencies.extend(DRIVER_DEPENDENCIES[driver]) - elif driver != "delegated": - dependencies.append("molecule-" + driver) - # Scenarios can specify a requirements.txt - if self.scenario.requirements is not None: - dependencies.append("-r" + self.scenario.requirements) - return dependencies - - @lru_cache() - def get_name(self, fmt=""): - """The name of this test case. The name is made up of various factors - from the tox world, joined together by hyphens. Some of them are based - on the role and scenario names. Others are based on factors such as - the python version or ansible version. - - :return: The tox-friendly name of this test scenario - """ - scenario_path = self.scenario.directory.split(os.path.sep) - parts = list(filter(lambda x: x != "molecule" and "." not in x, scenario_path)) - - name = parts[-1] - - parent = "" - path = "" - if len(parts) >= 3: - path = "-".join(parts[:-2]) - parent = parts[-2] - elif len(parts) == 2: - parent = parts[-2] - - nondefault_name = name if (name != "default" or not parent) else "" - - if not fmt: - fmt = INI_SCENARIO_FORMAT_DEFAULT - - formatted_name = Template(fmt).safe_substitute( - path=path, parent=parent, name=name, nondefault_name=nondefault_name - ) - formatted_name = "-".join(self._name_parts + [formatted_name]) - - # remove any leading or trailing dashes - formatted_name = formatted_name.strip("-") - # remove double dashes - while "--" in formatted_name: - formatted_name = formatted_name.replace("--", "-") - - return formatted_name - - @property - def description(self): - cwd_cmd = f"cd {self.scenario.run_dir} && " if self.scenario.run_dir else "" - return DEFAULT_DESCRIPTION.format( - scenario_name=self.scenario.name, cwd_cmd=cwd_cmd - ) diff --git a/src/tox_ansible/utils.py b/src/tox_ansible/utils.py deleted file mode 100644 index a2adf9c..0000000 --- a/src/tox_ansible/utils.py +++ /dev/null @@ -1,20 +0,0 @@ -import subprocess -from functools import lru_cache - - -@lru_cache() -def use_docker() -> bool: - """Return true if we should use docker.""" - # docker version command works even if service is not accesible, but - # info should pass only if we have a running service. - try: - result = subprocess.run( - ["docker", "info"], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - return result.returncode == 0 - except FileNotFoundError: - pass - return False diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..d420712 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7cf0b51 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +"""Global testing fixtures.""" + +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="module") +def module_fixture_dir(request: pytest.FixtureRequest) -> Path: + """Provide a module specific fixture directory. + + :param request: pytest fixture request + :return: path to the module specific fixture directory + """ + cwd = Path(__file__).parent + fixture_dir = cwd / "fixtures" + test_dir = fixture_dir / request.path.relative_to(cwd).parent / request.path.stem + return test_dir + + +@pytest.fixture(autouse=True) +def _tox_in_tox(monkeypatch: pytest.MonkeyPatch) -> None: + """Enable tox-in-tox. + + :param monkeypatch: pytest fixture to patch modules + """ + monkeypatch.delenv("TOX_ENV_NAME", raising=False) + monkeypatch.delenv("TOX_WORK_DIR", raising=False) + monkeypatch.delenv("TOX_ENV_DIR", raising=False) diff --git a/tests/fixtures/collection/galaxy.yml b/tests/fixtures/collection/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/collection/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/collection/molecule.yml b/tests/fixtures/collection/molecule.yml deleted file mode 100644 index 4b7cf61..0000000 --- a/tests/fixtures/collection/molecule.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -driver: - name: podman diff --git a/tests/fixtures/collection/molecule/one/molecule.yml b/tests/fixtures/collection/molecule/one/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/collection/molecule/one/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/collection/molecule/two/molecule.yml b/tests/fixtures/collection/molecule/two/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/collection/molecule/two/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/collection/roles/complex/molecule/default/molecule.yml b/tests/fixtures/collection/roles/complex/molecule/default/molecule.yml deleted file mode 100644 index 88483f7..0000000 --- a/tests/fixtures/collection/roles/complex/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: docker diff --git a/tests/fixtures/collection/roles/complex/molecule/name_mismatch/molecule.yml b/tests/fixtures/collection/roles/complex/molecule/name_mismatch/molecule.yml deleted file mode 100644 index af656de..0000000 --- a/tests/fixtures/collection/roles/complex/molecule/name_mismatch/molecule.yml +++ /dev/null @@ -1,4 +0,0 @@ -scenario: - name: real_name -driver: - name: docker diff --git a/tests/fixtures/collection/roles/complex/molecule/openstack/molecule.yml b/tests/fixtures/collection/roles/complex/molecule/openstack/molecule.yml deleted file mode 100644 index 0c7863d..0000000 --- a/tests/fixtures/collection/roles/complex/molecule/openstack/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: openstack diff --git a/tests/fixtures/collection/roles/no_tests/tasks/main.yml b/tests/fixtures/collection/roles/no_tests/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/collection/roles/simple/molecule/default/converge.yml b/tests/fixtures/collection/roles/simple/molecule/default/converge.yml deleted file mode 100644 index 1127127..0000000 --- a/tests/fixtures/collection/roles/simple/molecule/default/converge.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: test - hosts: localhost - gather_facts: false - roles: - - simple diff --git a/tests/fixtures/collection/roles/simple/molecule/default/molecule.yml b/tests/fixtures/collection/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index 40f03c9..0000000 --- a/tests/fixtures/collection/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,5 +0,0 @@ -driver: - name: delegated -platforms: - - name: localhost - hostname: localhost diff --git a/tests/fixtures/collection/roles/simple/tasks/main.yml b/tests/fixtures/collection/roles/simple/tasks/main.yml deleted file mode 100644 index 133237d..0000000 --- a/tests/fixtures/collection/roles/simple/tasks/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: say hello - debug: msg='tox-ansible is the best' diff --git a/tests/fixtures/collection/tox.ini b/tests/fixtures/collection/tox.ini deleted file mode 100644 index 31196a8..0000000 --- a/tests/fixtures/collection/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -skipdist = true -envlist = lint_all - -[ansible] -molecule_opts = - --debug -molecule_config_files = - {toxinidir}/molecule.yml - -[testenv] -usedevelop = false -skip_install = true diff --git a/tests/fixtures/expand_collection/galaxy.yml b/tests/fixtures/expand_collection/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/expand_collection/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/expand_collection/ignored/molecule/not_found/molecule.yml b/tests/fixtures/expand_collection/ignored/molecule/not_found/molecule.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection/molecule_one.yml b/tests/fixtures/expand_collection/molecule_one.yml deleted file mode 100644 index 31d22a5..0000000 --- a/tests/fixtures/expand_collection/molecule_one.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -driver: - name: openstack diff --git a/tests/fixtures/expand_collection/molecule_two.yml b/tests/fixtures/expand_collection/molecule_two.yml deleted file mode 100644 index 406ae59..0000000 --- a/tests/fixtures/expand_collection/molecule_two.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -provisioner: - name: ansible diff --git a/tests/fixtures/expand_collection/roles/simple/molecule/default/molecule.yml b/tests/fixtures/expand_collection/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index 88483f7..0000000 --- a/tests/fixtures/expand_collection/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: docker diff --git a/tests/fixtures/expand_collection/roles/simple/molecule/symlink b/tests/fixtures/expand_collection/roles/simple/molecule/symlink deleted file mode 120000 index 331d858..0000000 --- a/tests/fixtures/expand_collection/roles/simple/molecule/symlink +++ /dev/null @@ -1 +0,0 @@ -default \ No newline at end of file diff --git a/tests/fixtures/expand_collection/roles/simple/tasks/main.yml b/tests/fixtures/expand_collection/roles/simple/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection/tox.ini b/tests/fixtures/expand_collection/tox.ini deleted file mode 100644 index 05ed2ce..0000000 --- a/tests/fixtures/expand_collection/tox.ini +++ /dev/null @@ -1,21 +0,0 @@ -[tox] -skipdist = true -envlist = derp - -[ansible] -ansible = 2.{8,9} -python = 2.7,3.8 -molecule_opts = - --debug -molecule_config_files = - {toxinidir}/molecule_one.yml - {toxinidir}/molecule_two.yml -ignore_path = - ignored -ansible_lint_config = some/path.config -yamllint = some/yamllint.config -disabled = false - -[testenv] -usedevelop = false -skip_install = true diff --git a/tests/fixtures/expand_collection_comma/galaxy.yml b/tests/fixtures/expand_collection_comma/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/expand_collection_comma/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/expand_collection_comma/ignored/molecule/not_found/molecule.yml b/tests/fixtures/expand_collection_comma/ignored/molecule/not_found/molecule.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection_comma/roles/simple/molecule/default/molecule.yml b/tests/fixtures/expand_collection_comma/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index 88483f7..0000000 --- a/tests/fixtures/expand_collection_comma/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: docker diff --git a/tests/fixtures/expand_collection_comma/roles/simple/molecule/symlink b/tests/fixtures/expand_collection_comma/roles/simple/molecule/symlink deleted file mode 120000 index 331d858..0000000 --- a/tests/fixtures/expand_collection_comma/roles/simple/molecule/symlink +++ /dev/null @@ -1 +0,0 @@ -default \ No newline at end of file diff --git a/tests/fixtures/expand_collection_comma/roles/simple/tasks/main.yml b/tests/fixtures/expand_collection_comma/roles/simple/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection_comma/tox.ini b/tests/fixtures/expand_collection_comma/tox.ini deleted file mode 100644 index b172abb..0000000 --- a/tests/fixtures/expand_collection_comma/tox.ini +++ /dev/null @@ -1,18 +0,0 @@ -[tox] -skipdist = true -envlist = derp - -[ansible] -ansible = 2.8,2.9 -python = 2.7,3.8 -molecule_opts = - --debug -ignore_path = - ignored -ansible_lint_config = some/path.config -yamllint = some/yamllint.config -disabled = false - -[testenv] -usedevelop = false -skip_install = true diff --git a/tests/fixtures/expand_collection_newlines/galaxy.yml b/tests/fixtures/expand_collection_newlines/galaxy.yml deleted file mode 100644 index 613cfec..0000000 --- a/tests/fixtures/expand_collection_newlines/galaxy.yml +++ /dev/null @@ -1,3 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 diff --git a/tests/fixtures/expand_collection_newlines/ignored/molecule/not_found/molecule.yml b/tests/fixtures/expand_collection_newlines/ignored/molecule/not_found/molecule.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection_newlines/roles/simple/molecule/default/molecule.yml b/tests/fixtures/expand_collection_newlines/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index 88483f7..0000000 --- a/tests/fixtures/expand_collection_newlines/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: docker diff --git a/tests/fixtures/expand_collection_newlines/roles/simple/molecule/symlink b/tests/fixtures/expand_collection_newlines/roles/simple/molecule/symlink deleted file mode 120000 index 331d858..0000000 --- a/tests/fixtures/expand_collection_newlines/roles/simple/molecule/symlink +++ /dev/null @@ -1 +0,0 @@ -default \ No newline at end of file diff --git a/tests/fixtures/expand_collection_newlines/roles/simple/tasks/main.yml b/tests/fixtures/expand_collection_newlines/roles/simple/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/expand_collection_newlines/tox.ini b/tests/fixtures/expand_collection_newlines/tox.ini deleted file mode 100644 index edc04aa..0000000 --- a/tests/fixtures/expand_collection_newlines/tox.ini +++ /dev/null @@ -1,22 +0,0 @@ -[tox] -skipdist = true -envlist = derp - -[ansible] -ansible = - 2.8 - 2.9 -python = - 2.7 - 3.8 -molecule_opts = - --debug -ignore_path = - ignored -ansible_lint_config = some/path.config -yamllint = some/yamllint.config -disabled = false - -[testenv] -usedevelop = false -skip_install = true diff --git a/tests/fixtures/has_deps/galaxy.yml b/tests/fixtures/has_deps/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/has_deps/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/has_deps/molecule/one/molecule.yml b/tests/fixtures/has_deps/molecule/one/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/has_deps/molecule/one/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/has_deps/molecule/two/molecule.yml b/tests/fixtures/has_deps/molecule/two/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/has_deps/molecule/two/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/has_deps/roles/simple/molecule/default/converge.yml b/tests/fixtures/has_deps/roles/simple/molecule/default/converge.yml deleted file mode 100644 index 99fcfef..0000000 --- a/tests/fixtures/has_deps/roles/simple/molecule/default/converge.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: test - hosts: all - roles: - - simple diff --git a/tests/fixtures/has_deps/roles/simple/molecule/default/molecule.yml b/tests/fixtures/has_deps/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index dffef1c..0000000 --- a/tests/fixtures/has_deps/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,5 +0,0 @@ -driver: - name: podman -platforms: - - name: simple - image: fedora:latest diff --git a/tests/fixtures/has_deps/roles/simple/tasks/main.yml b/tests/fixtures/has_deps/roles/simple/tasks/main.yml deleted file mode 100644 index 133237d..0000000 --- a/tests/fixtures/has_deps/roles/simple/tasks/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: say hello - debug: msg='tox-ansible is the best' diff --git a/tests/fixtures/has_deps/tox.ini b/tests/fixtures/has_deps/tox.ini deleted file mode 100644 index 7caf87c..0000000 --- a/tests/fixtures/has_deps/tox.ini +++ /dev/null @@ -1,16 +0,0 @@ -[tox] -skipdist = true -envlist = lint_all - -[ansible] -disabled = false - -[testenv] -usedevelop = false -skip_install = true -deps = - otherdep - -[testenv:lint_all] -deps = - notadep==1 diff --git a/tests/fixtures/integration/test_basic/galaxy.yml b/tests/fixtures/integration/test_basic/galaxy.yml new file mode 100644 index 0000000..279ce54 --- /dev/null +++ b/tests/fixtures/integration/test_basic/galaxy.yml @@ -0,0 +1,2 @@ +name: test +namespace: test diff --git a/tests/fixtures/integration/test_basic/tox-ansible.ini b/tests/fixtures/integration/test_basic/tox-ansible.ini new file mode 100644 index 0000000..db47471 --- /dev/null +++ b/tests/fixtures/integration/test_basic/tox-ansible.ini @@ -0,0 +1,3 @@ +[tox] +requires = + tox>=4.2 diff --git a/tests/fixtures/integration/test_user_provided/galaxy.yml b/tests/fixtures/integration/test_user_provided/galaxy.yml new file mode 100644 index 0000000..279ce54 --- /dev/null +++ b/tests/fixtures/integration/test_user_provided/galaxy.yml @@ -0,0 +1,2 @@ +name: test +namespace: test diff --git a/tests/fixtures/integration/test_user_provided/tox-ansible.ini b/tests/fixtures/integration/test_user_provided/tox-ansible.ini new file mode 100644 index 0000000..ecfa15e --- /dev/null +++ b/tests/fixtures/integration/test_user_provided/tox-ansible.ini @@ -0,0 +1,19 @@ +[tox] +requires = + tox>=4.2 + +[testenv] +deps = + root +set_env = + root = 1 +commands_pre = + root +commands = + root +allowlist_externals = + root + +[testenv:integration-py3.11-devel] +pass_env = + specific diff --git a/tests/fixtures/is_precommit/.pre-commit-config.yaml b/tests/fixtures/is_precommit/.pre-commit-config.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/is_precommit/galaxy.yml b/tests/fixtures/is_precommit/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/is_precommit/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/is_precommit/molecule.yml b/tests/fixtures/is_precommit/molecule.yml deleted file mode 100644 index 4b7cf61..0000000 --- a/tests/fixtures/is_precommit/molecule.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -driver: - name: podman diff --git a/tests/fixtures/is_precommit/molecule/one/molecule.yml b/tests/fixtures/is_precommit/molecule/one/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/is_precommit/molecule/one/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/is_precommit/molecule/two/molecule.yml b/tests/fixtures/is_precommit/molecule/two/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/is_precommit/molecule/two/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/is_precommit/roles/complex/molecule/default/molecule.yml b/tests/fixtures/is_precommit/roles/complex/molecule/default/molecule.yml deleted file mode 100644 index 88483f7..0000000 --- a/tests/fixtures/is_precommit/roles/complex/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: docker diff --git a/tests/fixtures/is_precommit/roles/complex/molecule/name_mismatch/molecule.yml b/tests/fixtures/is_precommit/roles/complex/molecule/name_mismatch/molecule.yml deleted file mode 100644 index af656de..0000000 --- a/tests/fixtures/is_precommit/roles/complex/molecule/name_mismatch/molecule.yml +++ /dev/null @@ -1,4 +0,0 @@ -scenario: - name: real_name -driver: - name: docker diff --git a/tests/fixtures/is_precommit/roles/complex/molecule/openstack/molecule.yml b/tests/fixtures/is_precommit/roles/complex/molecule/openstack/molecule.yml deleted file mode 100644 index 0c7863d..0000000 --- a/tests/fixtures/is_precommit/roles/complex/molecule/openstack/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: openstack diff --git a/tests/fixtures/is_precommit/roles/complex/tasks/main.yml b/tests/fixtures/is_precommit/roles/complex/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/is_precommit/roles/no_tests/tasks/main.yml b/tests/fixtures/is_precommit/roles/no_tests/tasks/main.yml deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/is_precommit/roles/simple/molecule/default/converge.yml b/tests/fixtures/is_precommit/roles/simple/molecule/default/converge.yml deleted file mode 100644 index 1127127..0000000 --- a/tests/fixtures/is_precommit/roles/simple/molecule/default/converge.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: test - hosts: localhost - gather_facts: false - roles: - - simple diff --git a/tests/fixtures/is_precommit/roles/simple/molecule/default/molecule.yml b/tests/fixtures/is_precommit/roles/simple/molecule/default/molecule.yml deleted file mode 100644 index 40f03c9..0000000 --- a/tests/fixtures/is_precommit/roles/simple/molecule/default/molecule.yml +++ /dev/null @@ -1,5 +0,0 @@ -driver: - name: delegated -platforms: - - name: localhost - hostname: localhost diff --git a/tests/fixtures/is_precommit/roles/simple/tasks/main.yml b/tests/fixtures/is_precommit/roles/simple/tasks/main.yml deleted file mode 100644 index 133237d..0000000 --- a/tests/fixtures/is_precommit/roles/simple/tasks/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: say hello - debug: msg='tox-ansible is the best' diff --git a/tests/fixtures/is_precommit/tox.ini b/tests/fixtures/is_precommit/tox.ini deleted file mode 100644 index 31196a8..0000000 --- a/tests/fixtures/is_precommit/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -skipdist = true -envlist = lint_all - -[ansible] -molecule_opts = - --debug -molecule_config_files = - {toxinidir}/molecule.yml - -[testenv] -usedevelop = false -skip_install = true diff --git a/tests/fixtures/not_collection/.config/molecule/config.yml b/tests/fixtures/not_collection/.config/molecule/config.yml deleted file mode 100644 index 4b7cf61..0000000 --- a/tests/fixtures/not_collection/.config/molecule/config.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -driver: - name: podman diff --git a/tests/fixtures/not_collection/molecule/one/molecule.yml b/tests/fixtures/not_collection/molecule/one/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/not_collection/molecule/one/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/not_collection/molecule/two/molecule.yml b/tests/fixtures/not_collection/molecule/two/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/not_collection/molecule/two/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/not_collection/tox.ini b/tests/fixtures/not_collection/tox.ini deleted file mode 100644 index 7db51cb..0000000 --- a/tests/fixtures/not_collection/tox.ini +++ /dev/null @@ -1,36 +0,0 @@ -[tox] -skipdist = true -envlist = py27, py3{5,6,7,8}, py{27,38}-tox{2,3}0, lint, coverage - -[ansible] -disabled = false - -[testenv] -usedevelop = true -deps = - testfixtures - coverage - six - py - tox30: tox==3.0 - tox20: tox==2.0 - py27: mock -commands = - coverage run -m unittest discover {posargs} - -[testenv:coverage] -parallel_show_output = true -depends = py27, py3{5,6,7,8}, py{27,38}-tox{2,3}0 -setenv = -commands = - coverage combine - coverage report -m - -[testenv:lint] -skip_install = true -deps = - flake8 -commands = - flake8 src - flake8 tests - flake8 setup.py diff --git a/tests/fixtures/nothing/molecule/dont_find_me/molecule.yml b/tests/fixtures/nothing/molecule/dont_find_me/molecule.yml deleted file mode 100644 index 5ee881d..0000000 --- a/tests/fixtures/nothing/molecule/dont_find_me/molecule.yml +++ /dev/null @@ -1,3 +0,0 @@ -# Intentionally left blank -# If tox-ansible tries to find and parse this -# file, then it will die a horrible death diff --git a/tests/fixtures/nothing/tox.ini b/tests/fixtures/nothing/tox.ini deleted file mode 100644 index 66daf28..0000000 --- a/tests/fixtures/nothing/tox.ini +++ /dev/null @@ -1,5 +0,0 @@ -[tox] -envlist = manual - -[ansible] -disabled = true diff --git a/tests/fixtures/simplified/galaxy.yml b/tests/fixtures/simplified/galaxy.yml deleted file mode 100644 index 1468d0a..0000000 --- a/tests/fixtures/simplified/galaxy.yml +++ /dev/null @@ -1,5 +0,0 @@ -namespace: example -name: foo -version: 0.0.1 -authors: [] -readme: '' diff --git a/tests/fixtures/simplified/molecule/default/molecule.yml b/tests/fixtures/simplified/molecule/default/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/simplified/molecule/default/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/simplified/molecule/two/molecule.yml b/tests/fixtures/simplified/molecule/two/molecule.yml deleted file mode 100644 index cfebe70..0000000 --- a/tests/fixtures/simplified/molecule/two/molecule.yml +++ /dev/null @@ -1,2 +0,0 @@ -driver: - name: delegated diff --git a/tests/fixtures/simplified/roles/myrole/molecule/another/converge.yml b/tests/fixtures/simplified/roles/myrole/molecule/another/converge.yml deleted file mode 100644 index bc85a08..0000000 --- a/tests/fixtures/simplified/roles/myrole/molecule/another/converge.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: test - hosts: localhost - gather_facts: false - roles: - - myrole diff --git a/tests/fixtures/simplified/roles/myrole/molecule/another/molecule.yml b/tests/fixtures/simplified/roles/myrole/molecule/another/molecule.yml deleted file mode 100644 index 40f03c9..0000000 --- a/tests/fixtures/simplified/roles/myrole/molecule/another/molecule.yml +++ /dev/null @@ -1,5 +0,0 @@ -driver: - name: delegated -platforms: - - name: localhost - hostname: localhost diff --git a/tests/fixtures/simplified/roles/myrole/molecule/default/converge.yml b/tests/fixtures/simplified/roles/myrole/molecule/default/converge.yml deleted file mode 100644 index bc85a08..0000000 --- a/tests/fixtures/simplified/roles/myrole/molecule/default/converge.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: test - hosts: localhost - gather_facts: false - roles: - - myrole diff --git a/tests/fixtures/simplified/roles/myrole/molecule/default/molecule.yml b/tests/fixtures/simplified/roles/myrole/molecule/default/molecule.yml deleted file mode 100644 index 40f03c9..0000000 --- a/tests/fixtures/simplified/roles/myrole/molecule/default/molecule.yml +++ /dev/null @@ -1,5 +0,0 @@ -driver: - name: delegated -platforms: - - name: localhost - hostname: localhost diff --git a/tests/fixtures/simplified/tox.ini b/tests/fixtures/simplified/tox.ini deleted file mode 100644 index 88e83e0..0000000 --- a/tests/fixtures/simplified/tox.ini +++ /dev/null @@ -1,11 +0,0 @@ -[tox] -skipdist = true -envlist = lint_all - -[testenv] -usedevelop = false -skip_install = true - -[ansible] -scenario_format = $parent-$nondefault_name -disabled = false diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..c210fac --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests.""" diff --git a/tests/integration/test_basic.py b/tests/integration/test_basic.py new file mode 100644 index 0000000..6026e75 --- /dev/null +++ b/tests/integration/test_basic.py @@ -0,0 +1,62 @@ +"""Basic tests.""" +import json +import subprocess + +from pathlib import Path + +import pytest + + +def test_ansible_environments(module_fixture_dir: Path) -> None: + """Test that the ansible environments are available. + + :param module_fixture_dir: pytest fixture to get the fixtures directory + """ + try: + proc = subprocess.run( + f"tox -l --ansible --root {module_fixture_dir} --conf tox-ansible.ini", + capture_output=True, + cwd=str(module_fixture_dir), + text=True, + check=True, + shell=True, + ) + except subprocess.CalledProcessError as exc: + print(exc.stdout) + print(exc.stderr) + pytest.fail(exc.stderr) + assert "integration" in proc.stdout + assert "sanity" in proc.stdout + assert "unit" in proc.stdout + + +def test_gh_matrix( + module_fixture_dir: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that the ansible github matrix generation. + + Remove the GITHUB environment variable to test the default output. + + :param module_fixture_dir: pytest fixture to get the fixtures directory + :param monkeypatch: pytest fixture to patch modules + """ + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + monkeypatch.delenv("GITHUB_OUTPUT", raising=False) + proc = subprocess.run( + f"tox --ansible --gh-matrix --root {module_fixture_dir} --conf tox-ansible.ini", + capture_output=True, + cwd=str(module_fixture_dir), + text=True, + check=True, + shell=True, + ) + structured = json.loads(proc.stdout) + assert isinstance(structured, list) + assert structured + for entry in structured: + assert tuple(sorted(entry)) == ("description", "factors", "name", "python") + assert isinstance(entry["description"], str) + assert isinstance(entry["factors"], list) + assert isinstance(entry["name"], str) + assert isinstance(entry["python"], str) diff --git a/tests/integration/test_user_provided.py b/tests/integration/test_user_provided.py new file mode 100644 index 0000000..9c02d42 --- /dev/null +++ b/tests/integration/test_user_provided.py @@ -0,0 +1,38 @@ +"""User provided configuration.""" +import subprocess + +from configparser import ConfigParser +from pathlib import Path + +import pytest + + +def test_user_provided( + module_fixture_dir: Path, +) -> None: + """Test supplemental user configuration. + + :param module_fixture_dir: pytest fixture for module fixture directory + """ + try: + proc = subprocess.run( + f"tox config --ansible --root {module_fixture_dir} --conf tox-ansible.ini", + capture_output=True, + cwd=str(module_fixture_dir), + text=True, + check=True, + shell=True, + ) + except subprocess.CalledProcessError as exc: + print(exc.stdout) + print(exc.stderr) + pytest.fail(exc.stderr) + cfg_parser = ConfigParser() + cfg_parser.read_string(proc.stdout) + for env_name in cfg_parser.sections(): + assert cfg_parser.get(env_name, "allowlist_externals") == "root" + assert cfg_parser.get(env_name, "commands_pre") == "root" + assert cfg_parser.get(env_name, "commands") == "root" + assert cfg_parser.get(env_name, "deps") == "root" + assert "root" in cfg_parser.get(env_name, "set_env") + assert "specific" in cfg_parser.get("testenv:integration-py3.11-devel", "pass_env") diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/sanity/requirements.txt b/tests/sanity/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_ansible.py b/tests/test_ansible.py deleted file mode 100644 index 8fbeaea..0000000 --- a/tests/test_ansible.py +++ /dev/null @@ -1,80 +0,0 @@ -import os - -import pytest - -from tox_ansible.ansible import Ansible - - -@pytest.mark.parametrize( - "folder", - [ - ("tests/fixtures/collection"), - ("tests/fixtures/expand_collection"), - ("tests/fixtures/expand_collection_newlines"), - ("tests/fixtures/not_collection"), - ("tests/fixtures/has_deps"), - ("tests/fixtures/nothing"), - ], -) -def test_with_scenarios(mocker, folder): - ansible = Ansible(base=folder) - ansible.options = mocker.Mock() - ansible.options.ignore_paths = [] - ansible.options.molecule_config_files = [] - # ansible._scenarios = scenarios # pylint: disable=protected-access - assert ansible.directory == os.path.realpath(folder) - - -def test_with_full_path(): - ansible = Ansible("/dev") - assert ansible.directory == "/dev" - - -def test_scenarios_correct(mocker): - ansible = Ansible("tests/fixtures/collection") - ansible.options = mocker.Mock() - ansible.options.ignore_paths = [] - ansible.options.molecule_config_files = [] - assert len(ansible.scenarios) == 6 - # Second call to be sure we are not walking twice - assert len(ansible.scenarios) == 6 - - -def test_ignore_paths(mocker): - ansible = Ansible("tests/fixtures/collection") - ansible.options = mocker.Mock() - # We're ignoring one of them - ansible.options.ignore_paths = ["one"] - ansible.options.molecule_config_files = ["tests/fixtures/collection/molecule.yml"] - assert len(ansible.scenarios) == 5 - assert len(ansible.molecule_config) == 1 - - -def test_scenarios_with_global_molecule_config(mocker): - ansible = Ansible("tests/fixtures/not_collection") - global_config = [os.path.join(ansible.directory, ".config/molecule/config.yml")] - ansible.options = mocker.Mock() - ansible.options.ignore_paths = [] - ansible.options.molecule_config_files = [] - assert ansible.molecule_config_files() == global_config - - -def test_scenarios_with_base_molecule_config(mocker): - ansible = Ansible("tests/fixtures/collection") - base_configs = [os.path.join(ansible.directory, "molecule.yml")] - ansible.options = mocker.Mock() - ansible.options.ignore_paths = [] - ansible.options.molecule_config_files = base_configs - assert ansible.molecule_config_files() == base_configs - - -def test_scenarios_with_multiple_base_molecule_config(mocker): - ansible = Ansible("tests/fixtures/expand_collection") - base_configs = [ - os.path.join(ansible.directory, "molecule_one.yml"), - os.path.join(ansible.directory, "molecule_two.yml"), - ] - ansible.options = mocker.Mock() - ansible.options.ignore_paths = [] - ansible.options.molecule_config_files = base_configs - assert ansible.molecule_config_files() == base_configs diff --git a/tests/test_by_driver.py b/tests/test_by_driver.py deleted file mode 100644 index c605545..0000000 --- a/tests/test_by_driver.py +++ /dev/null @@ -1,18 +0,0 @@ -from unittest import TestCase -from unittest.mock import Mock - -from tox_ansible.filter.by_driver import ByDriver - - -class TestByDriver(TestCase): - def test_no_case(self): - by_driver = ByDriver(["yes"]) - envlist = { - "yes": Mock(tox_case=Mock(scenario=Mock(driver="yes"))), - "no": Mock(tox_case=Mock(scenario=Mock(driver="no"))), - "also_no": Mock(spec=[]), - } - filtered = by_driver.filter(envlist) - self.assertIn("yes", filtered) - self.assertNotIn("no", filtered) - self.assertNotIn("alsono", filtered) diff --git a/tests/test_by_scenario.py b/tests/test_by_scenario.py deleted file mode 100644 index c32055e..0000000 --- a/tests/test_by_scenario.py +++ /dev/null @@ -1,17 +0,0 @@ -from collections import namedtuple -from unittest import TestCase -from unittest.mock import Mock - -from tox_ansible.filter.by_scenario import ByScenario - - -class TestByScenario(TestCase): - def test_by_scenario(self): - Scenario = namedtuple("Scenario", "name") - scenario = Scenario(name="affirmative") - envlist = {"yes": Mock(tox_case=Mock(scenario=scenario)), "no": Mock(spec=[])} - scenarios = ["affirmative", "other"] - by_scenario = ByScenario(scenarios) - filtered = by_scenario.filter(envlist) - self.assertIn("yes", filtered) - self.assertNotIn("no", filtered) diff --git a/tests/test_everything.py b/tests/test_everything.py deleted file mode 100644 index d9ef63e..0000000 --- a/tests/test_everything.py +++ /dev/null @@ -1,171 +0,0 @@ -from __future__ import print_function - -import contextlib -import os -import shutil -import subprocess -import sys -from unittest.mock import patch - -import pytest - -EXPECTED = { - "tests/fixtures/collection": "\n".join( - [ - "env", - "lint_all", - "one", - "roles-complex-default", - "roles-complex-name_mismatch", - "roles-complex-openstack", - "roles-simple-default", - "sanity", - "shell", - "two", - ] - ), - "tests/fixtures/expand_collection": "\n".join( - [ - "derp", - "env", - "py27-ansible28-lint_all", - "py27-ansible28-roles-simple-default", - "py27-ansible29-lint_all", - "py27-ansible29-roles-simple-default", - "py38-ansible28-lint_all", - "py38-ansible28-roles-simple-default", - "py38-ansible29-lint_all", - "py38-ansible29-roles-simple-default", - "sanity", - "shell", - ] - ), - "tests/fixtures/not_collection": "\n".join( - [ - "coverage", - "lint", - "lint_all", - "one", - "py27", - "py27-tox20", - "py27-tox30", - "py35", - "py36", - "py37", - "py38", - "py38-tox20", - "py38-tox30", - "two", - ] - ), - "tests/fixtures/simplified": "\n".join( - [ - "default", - "env", - "lint_all", - "myrole", - "myrole-another", - "sanity", - "shell", - "two", - ] - ), - "tests/fixtures/nothing": "manual", -} -EXPECTED["tests/fixtures/expand_collection_comma"] = EXPECTED[ - "tests/fixtures/expand_collection" -] -EXPECTED["tests/fixtures/expand_collection_newlines"] = EXPECTED[ - "tests/fixtures/expand_collection" -] - -EXPECTED_ARGS = { - "default": """roles-complex-default -roles-simple-default""", - "openstack": "roles-complex-openstack", - "simple": "roles-simple-default", -} - - -@contextlib.contextmanager -def cd(directory): - cwd = os.getcwd() - os.chdir(directory) - try: - yield - finally: - os.chdir(cwd) - - -def run_tox(args, capture): - tox = ["tox"] - tox.extend(args) - try: - subprocess.run(tox, check=True) - except SystemExit as s: - if s.code != 0: - raise - out, err = capture.readouterr() - return out.strip(), err.strip() - - -@pytest.mark.parametrize( - "directory", - [ - ("tests/fixtures/collection"), - ("tests/fixtures/expand_collection"), - ("tests/fixtures/expand_collection_comma"), - ("tests/fixtures/expand_collection_newlines"), - ("tests/fixtures/not_collection"), - ("tests/fixtures/nothing"), - ("tests/fixtures/simplified"), - ], -) -def test_run_tox(directory, capfd): - with cd(directory): - out, _ = run_tox(["-l"], capfd) - assert out == EXPECTED[directory] - - -def test_no_results(capfd): - with cd("tests/fixtures/collection"): - out, err = run_tox(["-l", "--ansible-driver", "derp"], capfd) - assert out == "" - assert err == "" - - -def test_tox_ini_deps_preserved(capfd): - with cd("tests/fixtures/has_deps"): - lint_out, _ = run_tox(["--showconfig", "-e", "lint_all"], capfd) - simple_out, _ = run_tox(["--showconfig", "-e", "roles-simple-default"], capfd) - deps = [m for m in lint_out.split("\n") if m.startswith("deps = ")][0] - assert "notadep==1" in deps - deps = [m for m in simple_out.split("\n") if m.startswith("deps = ")][0] - assert "otherdep" in deps - - -@pytest.mark.parametrize( - "target,value", - [("scenario", "default"), ("driver", "openstack")], -) -def test_run_tox_with_args(target, value, capfd): - args = ["-l", f"--ansible-{target}", value] - with cd("tests/fixtures/collection"): - cli, _ = run_tox(args, capfd) - with patch.dict("os.environ", {f"TOX_ANSIBLE_{target.upper()}": value}): - env, _ = run_tox(["-l"], capfd) - assert cli == EXPECTED_ARGS[value] - assert env == EXPECTED_ARGS[value] - - -def test_run_with_test_command(capfd): - if "linux" in sys.platform: - with cd("tests/fixtures/collection"): - try: - shutil.rmtree(".tox") - except FileNotFoundError: - pass - cli, _ = run_tox(["-e", "roles-simple-default"], capfd) - assert ( - "roles-simple-default: commands succeeded" in cli - ), f"Important text missing from {cli}" diff --git a/tests/test_filter.py b/tests/test_filter.py deleted file mode 100644 index 383fe72..0000000 --- a/tests/test_filter.py +++ /dev/null @@ -1,26 +0,0 @@ -# For rennamed/moved assert(Count|Item)Equal -from collections import namedtuple -from unittest import TestCase -from unittest.mock import Mock - -from tox_ansible.filter import Filter - - -class TestFilter(TestCase): - def test_filter(self): - # Mock struggles with an attribute named "name" - Scenario = namedtuple("Scenario", "name") - opts = Mock(scenario=["default"], driver=[]) - default = Scenario(name="default") - other = Scenario(name="other") - envlist = { - "one-default": Mock(tox_case=Mock(scenario=default)), - "one-other": Mock(tox_case=Mock(scenario=other)), - "two-default": Mock(tox_case=Mock(scenario=default)), - "three-default": Mock(tox_case=Mock(scenario=default)), - } - expected = ["one-default", "two-default", "three-default"] - - f = Filter(opts) - results = f.filter(envlist) - self.assertCountEqual(results.keys(), expected) diff --git a/tests/test_matrix.py b/tests/test_matrix.py deleted file mode 100644 index 6a70edd..0000000 --- a/tests/test_matrix.py +++ /dev/null @@ -1,65 +0,0 @@ -from copy import copy -from unittest import TestCase - -from tox_ansible.matrix import Matrix, MatrixAxisBase -from tox_ansible.matrix.axes import AnsibleAxis, PythonAxis -from tox_ansible.tox_lint_case import ToxLintCase -from tox_ansible.tox_molecule_case import ToxMoleculeCase - -try: - from unittest import mock -except ImportError: - import mock - - -class TestMatrix(TestCase): - def test_empty_matrix(self): - cases = [ToxMoleculeCase(mock.Mock()), ToxLintCase([])] - original = copy(cases) - matrix = Matrix() - after_cases = matrix.expand(cases) - self.assertEqual(after_cases, original) - - def test_python_axis(self): - axis = PythonAxis(["2.7"]) - case = mock.Mock() - axis.expand([case]) - case.expand_python.assert_called_once_with("2.7") - - def test_ansible_axis(self): - axis = AnsibleAxis(["2.10"]) - case = mock.Mock() - axis.expand([case]) - case.expand_ansible.assert_called_once_with("2.10") - - def test_matrix_calls_axis(self): - matrix = Matrix() - axis = mock.Mock() - matrix.add_axis(axis) - cases = [mock.Mock()] - matrix.expand(cases) - axis.expand.assert_called_once_with(cases) - - def test_base_has_abstract_method(self): - base = MatrixAxisBase([]) - with self.assertRaises(NotImplementedError): - base.expand_one(0, 0) - - def test_matrix_leaves_out_bare_lint_all(self): - matrix = Matrix() - matrix.add_axis(PythonAxis(["2.7", "3.8"])) - cases = [ToxLintCase([])] - expanded = matrix.expand(cases) - self.assertEqual(2, len(expanded)) - - def test_matrix_both_axes(self): - matrix = Matrix() - matrix.add_axis(PythonAxis(["2.7"])) - matrix.add_axis(AnsibleAxis(["2.8"])) - case = mock.Mock() - case.expand_python.return_value = case - case.expand_ansible.return_value = case - matrix.expand([case]) - self.assertEqual(2, len(matrix.axes)) - case.expand_python.assert_called_once_with("2.7") - case.expand_ansible.assert_called_once_with("2.8") diff --git a/tests/test_options.py b/tests/test_options.py deleted file mode 100644 index b9ec6d0..0000000 --- a/tests/test_options.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from configparser import ConfigParser -from pathlib import Path - -import pytest - -from tox_ansible.options import ( - INI_IGNORE_PATHS, - INI_MOLECULE_GLOBAL_OPTS, - SCENARIO_ENV_NAME, - SCENARIO_OPTION_NAME, - Options, -) -from tox_ansible.tox_helper import Tox - - -@pytest.fixture -def good_config(mocker): - c = mocker.Mock() - reader = mocker.Mock() - c.get_reader.return_value = reader - reader.getlist.return_value = ["2.10", "3.9"] - reader.getstring.return_value = "auto" - c.opts = {SCENARIO_OPTION_NAME: ["explicit_scenario,other_scenario"]} - return c - - -@pytest.fixture -def opts(mocker, good_config): - opts = Options(good_config) - return opts - - -@pytest.fixture -def bad_config(mocker): - c = mocker.Mock() - reader = mocker.Mock() - c.get_reader.return_value = reader - reader.getlist.return_value = ["2.11"] - reader.getstring.return_value = "invalid" - return c - - -def test_do_filter_scenario(opts): - opts.role = [] - opts.scenario = ["default"] - opts.driver = [] - assert opts.do_filter() - - -def test_do_filter_driver(opts): - opts.role = [] - opts.scenario = [] - opts.driver = ["openstack"] - assert opts.do_filter() - - -def test_do_filter_none_is_false(opts): - opts.role = [] - opts.scenario = [] - opts.driver = [] - assert not opts.do_filter() - - -def test_options_expand_matrix(opts, mocker): - opts.matrix = mocker.Mock() - opts.expand_matrix([]) - opts.matrix.expand.assert_called_once_with([]) - - -@pytest.mark.parametrize( - "folder,expected", - [ - (Path("tests/fixtures/collection"), False), - (Path("tests/fixtures/expand_collection"), False), - (Path("tests/fixtures/expand_collection_newlines"), False), - (Path("tests/fixtures/has_deps"), False), - (Path("tests/fixtures/not_collection"), False), - (Path("tests/fixtures/nothing"), True), - ], -) -def test_disabled(mocker, folder, expected): - c = ConfigParser() - c.read(folder / "tox.ini") - config = mocker.Mock() - config._cfg = c # pylint: disable=protected-access - tox = Tox(config) - options = Options(tox) - assert options.disabled == expected - - -def test_options_are_valid(bad_config): - with pytest.raises(ValueError): - Options(bad_config) - - -def test_global_opts(opts): - assert opts.global_opts == ["2.10", "3.9"] - opts.reader.getlist.assert_called_with(INI_MOLECULE_GLOBAL_OPTS, sep="\n") - assert opts.ignore_paths == ["2.10", "3.9"] - opts.reader.getlist.assert_called_with(INI_IGNORE_PATHS, sep="\n") - - -def test_environment_cli_set_option(mocker, good_config): - mocker.patch.dict(os.environ, {SCENARIO_ENV_NAME: "my_test_scenario"}) - # Tests with both env and CLI - cli_opts = Options(good_config) - # Tests with only env - good_config.opts = {} - env_opts = Options(good_config) - # CLI options take precedence - assert cli_opts.scenario == ["explicit_scenario", "other_scenario"] - # Env options dropback - assert env_opts.scenario == ["my_test_scenario"] diff --git a/tests/test_scenario.py b/tests/test_scenario.py deleted file mode 100644 index 2879c16..0000000 --- a/tests/test_scenario.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest - -from tox_ansible.ansible.scenario import Scenario - - -@pytest.fixture -def openstack(tmp_path): - d = tmp_path / "default" - d.mkdir() - f = d / "molecule.yml" - f.write_text("driver:\n name: openstack") - r = d / "requirements.txt" - r.write_text("") - return d - - -@pytest.fixture -def surprise(tmp_path): - d = tmp_path / "surprise" - d.mkdir() - f = d / "molecule.yml" - f.write_text("driver:\n name: surprise") - return d - - -@pytest.fixture -def no_driver(tmp_path): - d = tmp_path / "no_driver" - d.mkdir() - f = d / "molecule.yml" - f.write_text("") - return d - - -def test_scenario_name_introspect(openstack): - s = Scenario(openstack) - assert s.name == "default" - assert str(s) == "default" - assert s.driver == "openstack" - assert s.requirements is not None - - -def test_scenario_name_explicit(surprise): - s = Scenario(surprise) - assert s.name == "surprise" - assert str(s) == "surprise" - assert s.driver == "surprise" - assert s.requirements is None - - -def test_no_driver(no_driver): - s = Scenario(no_driver) - assert s.name == "no_driver" - assert s.driver is None - - -def test_no_driver_with_empty_global(no_driver): - s = Scenario(no_driver) - s.global_config = [{}] - assert s.name == "no_driver" - assert s.driver is None - - -def test_no_driver_with_multiple_globals(no_driver): - s = Scenario(no_driver) - s.global_config = [{"driver": {}}, {"driver": {}}] - with pytest.raises(RuntimeError) as info: - s.driver # pylint: disable=pointless-statement - assert "Driver configuration is present" in str(info.value) - - -def test_driver_with_global_config(surprise): - global_config = [{"driver": {"name": "podman"}}] - s = Scenario(surprise, global_config) - assert s.name == "surprise" - assert str(s) == "surprise" - assert s.driver == "surprise" - assert s.requirements is None - - -def test_no_driver_with_global_config(no_driver): - global_config = [{"driver": {"name": "podman"}}] - s = Scenario(no_driver, global_config) - assert s.name == "no_driver" - assert s.driver == "podman" diff --git a/tests/test_tox_helper.py b/tests/test_tox_helper.py deleted file mode 100644 index b329dcd..0000000 --- a/tests/test_tox_helper.py +++ /dev/null @@ -1,8 +0,0 @@ -from unittest import TestCase - -from tox_ansible.tox_helper import Tox - - -class TestTox(TestCase): - def test_tox_helper_basic_works(self): - Tox({}) diff --git a/tests/test_tox_lint_case.py b/tests/test_tox_lint_case.py deleted file mode 100644 index bf5dd18..0000000 --- a/tests/test_tox_lint_case.py +++ /dev/null @@ -1,109 +0,0 @@ -from pathlib import Path -from unittest.mock import Mock - -import pytest - -from tox_ansible.ansible.scenario import Scenario -from tox_ansible.tox_lint_case import ToxLintCase - - -@pytest.fixture -def precommit(): - p = Path(__file__).parent - return p / "fixtures" / "is_precommit" - - -@pytest.fixture -def no_precommit(): - p = Path(__file__).parent - return p / "fixtures" / "collection" - - -def test_names_are_correct(mocker): - tc = ToxLintCase([]) - deps = set(["ansible-lint", "flake8", "yamllint", "ansible"]) - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value="/home", - ) - assert tc.get_name() == "lint_all" - assert tc.working_dir == "/home" - assert tc.dependencies == deps - - -def test_pre_commit_is_correct(mocker, precommit, no_precommit): - tc = ToxLintCase([]) - deps = set(["pre-commit"]) - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value=precommit, - ) - assert tc.is_precommit - assert tc.dependencies == deps - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value=no_precommit, - ) - assert not tc.is_precommit - - -def test_expand_python(): - tc = ToxLintCase([]) - out = tc.expand_python("2.7") - assert out.get_name() == "py27-lint_all" - - -def test_expand_ansible(): - tc = ToxLintCase([]) - out = tc.expand_ansible("2.10") - assert out.get_name() == "ansible210-lint_all" - - -def test_commands_are_correct(mocker, no_precommit, precommit): - options = Mock() - options.global_opts = [] - options.ansible_lint = None - options.yamllint = None - case1 = Mock(scenario=Scenario("molecule/s1")) - case2 = Mock(scenario=Scenario("something/roles/r1/molecule/s2")) - case3 = Mock(scenario=Scenario("roles/r2/molecule/s3")) - bummer = ToxLintCase([]) - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value=no_precommit, - ) - tc = ToxLintCase([case1, case2, case3, bummer]) - cmds = tc.get_commands(options) - expected = [["ansible-lint", "-R"], ["flake8", "."]] - assert expected == cmds - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value=precommit, - ) - assert tc.get_commands(options) == [["pre-commit", "run", "--all"]] - - -def test_lint_options_correct(mocker, no_precommit): - options = mocker.Mock() - options.global_opts = [] - options.ansible_lint = "some/path" - options.yamllint = "some/yaml.path" - bummer = ToxLintCase([]) - mocker.patch( - "tox_ansible.tox_lint_case.Tox.toxinidir", - new_callable=mocker.PropertyMock, - return_value=no_precommit, - ) - tc = ToxLintCase([bummer]) - cmds = tc.get_commands(options) - expected = [ - ["ansible-lint", "-R", "-c", "some/path"], - ["yamllint", "-c", "some/yaml.path", "."], - ["flake8", "."], - ] - assert expected == cmds diff --git a/tests/test_tox_molecule_case.py b/tests/test_tox_molecule_case.py deleted file mode 100644 index f5866a3..0000000 --- a/tests/test_tox_molecule_case.py +++ /dev/null @@ -1,179 +0,0 @@ -import pytest - -# pylint: disable=import-error -from tox_ansible.ansible.scenario import Scenario -from tox_ansible.options import Options -from tox_ansible.tox_helper import Tox -from tox_ansible.tox_molecule_case import ToxMoleculeCase - - -@pytest.fixture -def config(mocker): - return mocker.PropertyMock(return_value={}) - - -@pytest.fixture -def scenario(mocker): - s = Scenario("molecule/my_test") - return s - - -@pytest.fixture -def long_scenario(mocker): - s = Scenario("roles/somedir/subdir/molecule/foo") - return s - - -@pytest.fixture -def odd_scenario(mocker): - s = Scenario("somedir/molecule/scenario") - return s - - -@pytest.fixture -def opts(mocker): - config = mocker.Mock() - reader = mocker.Mock() - config.get_reader.return_value = reader - reader.getlist.return_value = ["2.10", "3.9"] - reader.getstring.return_value = "auto" - return Options(config) - - -def test_case_is_simple(config, opts, scenario, mocker): - mocker.patch.object( - Options, "global_opts", new_callable=mocker.PropertyMock, return_value=[] - ) - mocker.patch.object( - Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[] - ) - t = ToxMoleculeCase(scenario) - opts.molecule_config_files = [] - assert t.get_name() == "my_test" - assert t.working_dir == "" - cmds = [["molecule", "test", "-s", scenario.name]] - assert t.get_commands(opts) == cmds - assert t.basepython is None - - -def test_case_is_simple_with_config_files(config, opts, scenario, mocker): - base_configs = [ - "/home/jdoe/my_ansible_collections/tests/molecule_one.yml", - "/home/jdoe/my_ansible_collections/tests/molecule_one.yml", - ] - mocker.patch.object( - Options, "global_opts", new_callable=mocker.PropertyMock, return_value=[] - ) - mocker.patch.object( - Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[] - ) - t = ToxMoleculeCase(scenario) - opts.molecule_config_files = base_configs - assert t.get_name() == "my_test" - assert t.working_dir == "" - cmds = [ - [ - "molecule", - "-c", - base_configs[0], - "-c", - base_configs[-1], - "test", - "-s", - scenario.name, - ] - ] - assert t.get_commands(opts) == cmds - assert t.basepython is None - - -def test_case_has_global_opts(mocker, scenario, opts, config): - mocker.patch.object( - Options, - "global_opts", - new_callable=mocker.PropertyMock, - return_value=["-c", "derp"], - ) - mocker.patch.object( - Tox, "posargs", new_callable=mocker.PropertyMock, return_value=[] - ) - t = ToxMoleculeCase(scenario) - opts.molecule_config_files = [] - cmds = [["molecule", "-c", "derp", "test", "-s", scenario.name]] - assert t.get_commands(opts) == cmds - - -def test_case_expand_ansible(scenario): - t = ToxMoleculeCase(scenario) - ts = t.expand_ansible("2.7") - assert ts.ansible == "2.7" - assert ts.get_name() == "ansible27-my_test" - assert "ansible==2.7.*" in ts.dependencies - assert ts.basepython is None - assert "Auto-generated for: molecule test -s my_test" == ts.description - - -def test_case_expand_python(scenario): - t = ToxMoleculeCase(scenario) - ts = t.expand_python("4.1") - assert ts.python == "4.1" - assert ts.get_name() == "py41-my_test" - assert ts.basepython == "python4.1" - - -def test_case_expand_twice(scenario): - t = ToxMoleculeCase(scenario) - t1 = t.expand_python("4.1") - t2 = t1.expand_ansible("1.0") - assert t2.get_name() == "ansible10-py41-my_test" - - -def test_case_includes_docker_deps(scenario): - t = ToxMoleculeCase(scenario, drivers=["docker"]) - assert "molecule-docker" in t.dependencies - assert "molecule-podman" in t.dependencies - - -def test_case_includes_openstack_deps(scenario): - t = ToxMoleculeCase(scenario, drivers=["openstack"]) - assert "openstacksdk" in t.dependencies - assert "moelcule-podman" not in t.dependencies - - -def test_case_ignores_delegated_driver(scenario): - t = ToxMoleculeCase(scenario, drivers=["delegated"]) - assert "molecule-delegated" not in t.dependencies - - -def test_case_handles_unknown_driver(scenario): - t = ToxMoleculeCase(scenario, drivers=["derpy"]) - assert "molecule-derpy" in t.dependencies - - -def test_case_for_multiple_drivers(scenario): - t = ToxMoleculeCase(scenario, drivers=["docker", "podman", "vagrant"]) - assert "molecule-docker" in t.dependencies - assert "molecule-podman" in t.dependencies - assert "molecule-vagrant" in t.dependencies - assert len(list(filter(lambda r: "-r" in r, t.dependencies))) == 0 - - -def test_case_handles_requirements(mocker, scenario): - t = ToxMoleculeCase(scenario, drivers=["derp"]) - mocker.patch( - "tox_ansible.ansible.scenario.Scenario.requirements", - new_callable=mocker.PropertyMock, - return_value="some_reqs.txt", - ) - print(t.dependencies) - assert "-rsome_reqs.txt" in t.dependencies - - -def test_long_name(long_scenario): - t = ToxMoleculeCase(long_scenario, drivers=["empty"]) - assert t.get_name() == "roles-somedir-subdir-foo" - - -def test_odd_name(odd_scenario): - t = ToxMoleculeCase(odd_scenario, drivers=[]) - assert t.get_name() == "somedir-scenario" diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 49b3f74..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,15 +0,0 @@ -from tox_ansible.utils import use_docker - - -def test_docker_present(mocker): - use_docker.cache_clear() - rc = mocker.Mock() - rc.returncode = 0 - mocker.patch("tox_ansible.utils.subprocess.run", return_value=rc) - assert use_docker() - - -def test_docker_absent(mocker): - use_docker.cache_clear() - mocker.patch("tox_ansible.utils.subprocess.run", side_effect=FileNotFoundError) - assert not use_docker() diff --git a/tests/util.py b/tests/util.py deleted file mode 100644 index e905564..0000000 --- a/tests/util.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import shutil -import subprocess -import tempfile -from textwrap import dedent -from unittest import TestCase - -from tox.config import PARALLEL_ENV_VAR_KEY_PUBLIC as TOX_PARALLEL_ENV - -GALAXY_SAMPLE = """ -namespace: example -name: foo -version: 0.0.1 -""" - - -class ToxAnsible_TestCase(TestCase): - """A TestCase for testing tox configs. - - This class creates a temporary directory, writing the provided contents to - the tox config file and any scenario files. - - The test case also provides the `.tox_call()` method for calling tox with - the config, and `.tox_envlist()`, which is useful for testing the expected - env list. - - Note that the test case doesn't change the working directory or environment - - Attributes: - ini_contents: contentst of tox.ini - ini_filename: file to create - defaults to tox.ini - ini_filepath: Full path to tox.ini - generated during test setup - roles: A list of tuples. [('rolename', [('scenario1', {...}), ...])] - Each tuple is (rolename, scenariolist). The scenariolist is a list - of tuples that is (scenarioname, molecule.yml). - """ - - ini_contents = None - ini_filename = "tox.ini" - roles = [] - - @classmethod - def setUpClass(cls): - super().setUpClass() - - assert ( - cls.ini_contents is not None - ), "`{cls.__module__}.{cls.__name__}.ini_contents` has not been set".format( - cls=cls - ) - - # Create a tmpdir so that we aren't working somewhere that matters - cls._temp_dir = tempfile.mkdtemp() - cls.ini_filepath = os.path.join(cls._temp_dir, cls.ini_filename) - - # Write out the tox.ini file - with open(cls.ini_filepath, "w", encoding="utf-8") as ini_file: - ini_file.write(dedent(cls.ini_contents)) - - # Write the galaxy.yml file - blank for now - with open( - os.path.join(cls._temp_dir, "galaxy.yml"), "w", encoding="utf-8" - ) as galaxy_yml: - galaxy_yml.write(GALAXY_SAMPLE) - - # Create role and scenario dirs - for role, scenarios in cls.roles: - tasks = os.path.join(cls._temp_dir, "roles", role, "tasks") - os.makedirs(tasks) - # It's not a role if it doesn't have a tasks/main.yml - with open(os.path.join(tasks, "main.yml"), "w", encoding="utf-8") as tasks: - tasks.write("") - # Create the molecule scenarios - for scenario, molecule in scenarios: - d = os.path.join(cls._temp_dir, "roles", role, "molecule", scenario) - os.makedirs(d) - with open( - os.path.join(d, "molecule.yml"), "w", encoding="utf-8" - ) as mol_file: - mol_file.write(dedent(molecule)) - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls._temp_dir) - super().tearDownClass() - - def _tox_call(self, arguments): - # Remove TOX_PARALLEL_ENV from the subprocess environment variables - # See: https://github.com/tox-dev/tox/issues/1275 - env = os.environ.copy() - env.pop(TOX_PARALLEL_ENV, None) - - # pylint: disable=consider-using-with - proc = subprocess.Popen( - arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env - ) - stdout, stderr = proc.communicate() - - return proc.returncode, stdout.decode("utf-8"), stderr.decode("utf-8") - - def tox_call(self, arguments): - base = ["tox", "-c", self.ini_filepath] - return self._tox_call(base + arguments) - - def tox_envlist(self, arguments=None): - arguments = arguments if arguments else [] - returncode, stdout, stderr = self.tox_call(["-l"] + arguments) - - self.assertEqual(returncode, 0, stderr) - - return stdout.strip().splitlines() diff --git a/tox.ini b/tox.ini index 0ce32ea..4288a3e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,68 +1,87 @@ [tox] -minversion = 3.18.0 -skipsdist = true -ignore_path = tests -envlist = - py3{6,7,8,9,10,11} - py3{6,7,8,9,10,11}-devel - lint - packaging - coverage - -[ansible] -ansible = 2.9 2.10 +requires = + tox>=4.2 +no_package = true +skip_missing_interpreters = true +work_dir = {env:TOX_WORK_DIR:.tox} [testenv] -usedevelop = true +description = Run pytest under {basepython} ({envpython}) deps = - testfixtures - coverage - pytest - pytest-cov - pytest-mock - devel: tox>=4.0.0a3 -passenv = + --editable .[test] +pass_env = + CI + CONTAINER_* DOCKER_* - CONTAINERS_* -setenv = - COVERAGE_FILE={env:COVERAGE_FILE:.coverage.{basepython}} + GITHUB_* + HOME + PYTEST_* + SSH_AUTH_SOCK + TERM + USER +set_env = + COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} + COVERAGE_PROCESS_START = {toxinidir}/pyproject.toml + FORCE_COLOR = 1 + PIP_CONSTRAINT = {toxinidir}/requirements.txt + PRE_COMMIT_COLOR = always + TERM = xterm-256color commands = - pytest --cov=src --cov-report=term-missing {posargs:.} + coverage run -m pytest {posargs} + sh -c "coverage combine -q .tox/.coverage.* && coverage xml || true && coverage report" allowlist_externals = - bash + coverage + sh +envlist = lint, py, packaging, report, clean + +[testenv:clean] +description = Erase coverage data +skip_install = true +deps = + coverage[toml] +commands = + coverage erase -[testenv:coverage] -parallel_show_output = true -depends = py3{6,7,8,9} -setenv = +[testenv:report] +description = Produce coverage report +skip_install = true +deps = + coverage[toml] commands = - coverage combine - coverage report -m + coverage report + cat .tox/.tmp/.mypy/index.txt [testenv:lint] -skip_install = true +description = Enforce quality standards under {basepython} ({envpython}) deps = - pre_commit + --editable . + pre-commit commands = - python -m pre_commit run {posargs:--all} + pre-commit run --show-diff-on-failure --all-files +install_command = pip install {opts} {packages} [testenv:packaging] -usedevelop = false +description = + Build package, verify metadata, install package skip_install = true deps = - collective.checkdocs >= 0.2 - pep517 >= 0.5.0 - twine >= 2.0.0 + build>=0.7 + twine commands = - bash -c "rm -rf {toxinidir}/dist/ {toxinidir}/build/ && mkdir -p {toxinidir}/dist/" - python -m pep517.build \ - --source \ - --binary \ - --out-dir {toxinidir}/dist/ \ + {envpython} -c 'import os.path, shutil, sys; \ + dist_dir = os.path.join("{toxinidir}", "dist"); \ + os.path.isdir(dist_dir) or sys.exit(0); \ + print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ + shutil.rmtree(dist_dir)' + {envpython} -m build \ + --outdir {toxinidir}/dist/ \ {toxinidir} - twine check dist/* + twine check --strict {toxinidir}/dist/* + sh -c "python3 -m pip install {toxinidir}/dist/*.whl" + tox --version | grep tox-ansible + pip uninstall -y tox-ansible +allowlist_externals = + sh + twine -[flake8] -exclude = .tox/,.venv/,dist/,build/,.eggs/ -# To match black -max-line-length = 88 +[base]