├── .config ├── ansible-lint-ignore.txt ├── ansible-lint.yaml ├── constraints.txt ├── dictionary.txt ├── molecule.spec ├── pydoclint-baseline.txt ├── requirements-docs.txt ├── requirements-test.txt ├── requirements-testinfra.txt └── requirements.in ├── .env ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── security_bug_report.md ├── dependabot.yml ├── patchback.yml ├── release-drafter.yml └── workflows │ ├── ack.yml │ ├── push.yml │ ├── redirects.yml │ ├── release.yml │ └── tox.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .readthedocs.yml ├── .sonarcloud.properties ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── ansible.cfg ├── codecov.yml ├── collections ├── README.md └── requirements.yml ├── cspell.config.yaml ├── docs ├── _static │ └── images │ │ ├── favicon.ico │ │ ├── logo.png │ │ └── logo_big.png ├── asset │ ├── molecule.png │ ├── molecule.psd │ ├── mollie.png │ └── mollie.psd ├── ci.md ├── configuration.md ├── contributing.md ├── examples │ ├── docker.md │ ├── kubevirt.md │ └── podman.md ├── faq.md ├── getting-started.md ├── google04e29a42ae6e6cbc.html ├── guides │ ├── custom-image.md │ ├── docker-rootless.md │ ├── inside-a-container.md │ ├── monolith.md │ ├── parallel.md │ ├── podman-inside-docker.md │ ├── sharing.md │ └── systemd-container.md ├── images │ └── collection_structure.png ├── index.md ├── installation.md ├── next.md ├── redirects.yml └── usage.md ├── mise.toml ├── mkdocs.yml ├── molecule ├── pyproject.toml ├── src └── molecule │ ├── __init__.py │ ├── __main__.py │ ├── api.py │ ├── app.py │ ├── command │ ├── __init__.py │ ├── base.py │ ├── check.py │ ├── cleanup.py │ ├── converge.py │ ├── create.py │ ├── dependency.py │ ├── destroy.py │ ├── drivers.py │ ├── idempotence.py │ ├── init │ │ ├── __init__.py │ │ ├── base.py │ │ ├── init.py │ │ └── scenario.py │ ├── list.py │ ├── login.py │ ├── matrix.py │ ├── prepare.py │ ├── reset.py │ ├── side_effect.py │ ├── syntax.py │ ├── test.py │ └── verify.py │ ├── config.py │ ├── console.py │ ├── constants.py │ ├── data │ ├── __init__.py │ ├── driver.json │ ├── init-scenario.yml │ ├── molecule.json │ └── templates │ │ └── scenario │ │ ├── converge.yml.j2 │ │ ├── create.yml.j2 │ │ ├── destroy.yml.j2 │ │ └── molecule.yml.j2 │ ├── dependency │ ├── __init__.py │ ├── ansible_galaxy │ │ ├── __init__.py │ │ ├── base.py │ │ ├── collections.py │ │ └── roles.py │ ├── base.py │ └── shell.py │ ├── driver │ ├── __init__.py │ ├── base.py │ └── delegated.py │ ├── exceptions.py │ ├── interpolation.py │ ├── logger.py │ ├── model │ ├── __init__.py │ └── schema_v3.py │ ├── platforms.py │ ├── provisioner │ ├── __init__.py │ ├── ansible.py │ ├── ansible_playbook.py │ ├── ansible_playbooks.py │ └── base.py │ ├── py.typed │ ├── scenario.py │ ├── scenarios.py │ ├── shell.py │ ├── state.py │ ├── status.py │ ├── text.py │ ├── types.py │ ├── util.py │ └── verifier │ ├── __init__.py │ ├── ansible.py │ ├── base.py │ └── testinfra.py ├── tests ├── __init__.py ├── conftest.py ├── fixtures │ ├── integration │ │ └── test_command │ │ │ ├── molecule │ │ │ ├── default │ │ │ │ ├── converge.yml │ │ │ │ ├── create.yml │ │ │ │ ├── destroy.yml │ │ │ │ └── molecule.yml │ │ │ ├── docker │ │ │ │ ├── converge.yml │ │ │ │ ├── create.yml │ │ │ │ ├── destroy.yml │ │ │ │ ├── molecule.yml │ │ │ │ ├── requirements.yml │ │ │ │ └── tasks │ │ │ │ │ └── create-fail.yml │ │ │ ├── kubevirt │ │ │ │ ├── converge.yml │ │ │ │ ├── create.yml │ │ │ │ ├── destroy.yml │ │ │ │ ├── molecule.yml │ │ │ │ ├── requirements.yml │ │ │ │ └── tasks │ │ │ │ │ ├── create_vm.yml │ │ │ │ │ └── create_vm_dictionary.yml │ │ │ ├── podman │ │ │ │ ├── converge.yml │ │ │ │ ├── create.yml │ │ │ │ ├── destroy.yml │ │ │ │ ├── molecule.yml │ │ │ │ ├── requirements.yml │ │ │ │ └── tasks │ │ │ │ │ └── create-fail.yml │ │ │ └── smoke │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ │ └── scenarios │ │ │ ├── cleanup │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── cleanup.yml │ │ │ │ ├── converge.yml │ │ │ │ ├── molecule.yml │ │ │ │ └── tests │ │ │ │ └── test_cleanup.py │ │ │ ├── dependency │ │ │ └── molecule │ │ │ │ ├── ansible-galaxy │ │ │ │ ├── converge.yml │ │ │ │ ├── molecule.yml │ │ │ │ └── requirements.yml │ │ │ │ └── shell │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ │ ├── driver │ │ │ ├── delegated │ │ │ │ ├── meta │ │ │ │ │ └── main.yml │ │ │ │ └── molecule │ │ │ │ │ └── default │ │ │ │ │ ├── cleanup.yml │ │ │ │ │ ├── converge.yml │ │ │ │ │ ├── create.yml │ │ │ │ │ ├── destroy.yml │ │ │ │ │ ├── molecule.yml │ │ │ │ │ ├── prepare.yml │ │ │ │ │ ├── side_effect.yml │ │ │ │ │ └── verify.yml │ │ │ └── delegated_invalid_role_name_with_role_name_check_equals_to_1 │ │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ ├── create.yml │ │ │ │ ├── destroy.yml │ │ │ │ └── molecule.yml │ │ │ ├── host_group_vars │ │ │ ├── group_vars │ │ │ │ └── example │ │ │ │ │ └── all.yml │ │ │ ├── host_vars │ │ │ │ └── extra-host │ │ │ ├── hosts │ │ │ └── molecule │ │ │ │ ├── default │ │ │ │ ├── converge.yml │ │ │ │ ├── group_vars │ │ │ │ │ └── example │ │ │ │ │ │ └── all.yml │ │ │ │ └── molecule.yml │ │ │ │ └── links │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ │ ├── idempotence │ │ │ ├── molecule │ │ │ │ └── raises │ │ │ │ │ ├── converge.yml │ │ │ │ │ └── molecule.yml │ │ │ └── tasks │ │ │ │ └── main.yml │ │ │ ├── interpolation │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ │ ├── overrride_driver │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ │ ├── side_effect │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ ├── molecule.yml │ │ │ │ ├── side_effect.yml │ │ │ │ └── tests │ │ │ │ └── test_side_effect.py │ │ │ ├── test_destroy_strategy │ │ │ └── molecule │ │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ ├── molecule.yml │ │ │ │ └── tests │ │ │ │ └── test_destroy_strategy.py │ │ │ └── verifier │ │ │ ├── .pre-commit-config.yaml │ │ │ └── molecule │ │ │ ├── testinfra-pre-commit │ │ │ ├── converge.yml │ │ │ ├── molecule.yml │ │ │ └── tests │ │ │ │ └── test_testinfra_pre_commit.py │ │ │ └── testinfra │ │ │ ├── converge.yml │ │ │ ├── molecule.yml │ │ │ ├── roles │ │ │ └── molecule │ │ │ │ └── tasks │ │ │ │ └── main.yml │ │ │ ├── shared │ │ │ └── test_shared.py │ │ │ └── tests │ │ │ └── test_testinfra.py │ └── resources │ │ ├── .yamllint │ │ ├── broken-collection │ │ └── galaxy.yml │ │ ├── molecule.yml │ │ ├── playbooks │ │ └── delegated │ │ │ ├── create.yml │ │ │ ├── destroy.yml │ │ │ └── inventory │ │ │ └── group_vars │ │ │ └── all │ │ │ └── all.yml │ │ ├── roles │ │ └── molecule │ │ │ ├── meta │ │ │ └── main.yml │ │ │ └── tasks │ │ │ └── main.yml │ │ ├── sample-collection │ │ ├── CHANGELOG.md │ │ ├── galaxy.yml │ │ ├── meta │ │ │ └── runtime.yml │ │ ├── molecule │ │ │ └── default │ │ │ │ ├── converge.yml │ │ │ │ └── molecule.yml │ │ └── roles │ │ │ └── get_rich │ │ │ └── tasks │ │ │ └── main.yml │ │ └── schema_instance_files │ │ ├── invalid │ │ └── molecule_delegated.yml │ │ └── valid │ │ └── molecule.yml ├── integration │ ├── __init__.py │ ├── conftest.py │ └── test_command.py └── unit │ ├── __init__.py │ ├── command │ ├── __init__.py │ ├── conftest.py │ ├── init │ │ ├── __init__.py │ │ └── test_scenario.py │ ├── test_base.py │ ├── test_check.py │ ├── test_cleanup.py │ ├── test_converge.py │ ├── test_create.py │ ├── test_dependency.py │ ├── test_destroy.py │ ├── test_idempotence.py │ ├── test_list.py │ ├── test_login.py │ ├── test_matrix.py │ ├── test_prepare.py │ ├── test_side_effect.py │ ├── test_syntax.py │ ├── test_test.py │ └── test_verify.py │ ├── conftest.py │ ├── dependency │ ├── __init__.py │ ├── ansible_galaxy │ │ ├── __init__.py │ │ ├── test_collections.py │ │ └── test_roles.py │ └── test_shell.py │ ├── driver │ ├── __init__.py │ └── test_delegated.py │ ├── lint │ └── __init__.py │ ├── model │ ├── __init__.py │ └── v2 │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_dependency_section.py │ │ ├── test_driver_section.py │ │ ├── test_platforms_section.py │ │ ├── test_provisioner_section.py │ │ ├── test_scenario_section.py │ │ ├── test_schema.py │ │ └── test_verifier_section.py │ ├── provisioner │ ├── __init__.py │ ├── test_ansible.py │ ├── test_ansible_playbook.py │ └── test_ansible_playbooks.py │ ├── test_api.py │ ├── test_config.py │ ├── test_interpolation.py │ ├── test_logger.py │ ├── test_platforms.py │ ├── test_scenario.py │ ├── test_scenarios.py │ ├── test_scenarios_ordered.py │ ├── test_shell.py │ ├── test_state.py │ ├── test_status.py │ ├── test_text.py │ ├── test_util.py │ └── verifier │ ├── __init__.py │ ├── test_ansible.py │ └── test_testinfra.py ├── tools ├── get-version.sh ├── opts.txt ├── report-coverage ├── test-setup.sh └── update-version.sh └── tox.ini /.config/ansible-lint-ignore.txt: -------------------------------------------------------------------------------- 1 | # This file contains ignores rule violations for ansible-lint 2 | src/molecule/test/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/meta/main.yml role-name 3 | src/molecule/test/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/meta/main.yml schema[meta] 4 | -------------------------------------------------------------------------------- /.config/ansible-lint.yaml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - .tox 3 | - .venv 4 | - mkdocs.yml # using tags 5 | # not really playbooks: 6 | kinds: 7 | - "yaml": "**/.config/ansible-lint.yaml" 8 | - "yaml": "**/.github/*.yml" 9 | - "yaml": "**/.pre-commit-config.yaml" 10 | - "yaml": "**/.readthedocs.yml" 11 | - "yaml": "**/codecov.yml" 12 | - "yaml": "**/cspell.config.yaml" 13 | - "yaml": "**/docs/*.yml" 14 | -------------------------------------------------------------------------------- /.config/dictionary.txt: -------------------------------------------------------------------------------- 1 | anyconfig 2 | argnames 3 | argvalues 4 | autouse 5 | autovivification 6 | booleaness 7 | cacheprovider 8 | caplog 9 | capsys 10 | cfmpr 11 | cgroupfs 12 | cgroupns 13 | clicolor 14 | cloudinitdisk 15 | connopts 16 | containerdisk 17 | credssp 18 | decentral 19 | dedriver 20 | delenv 21 | dependee 22 | dequènes 23 | devel 24 | dind 25 | dotglob 26 | emptydisk 27 | endgroup 28 | endraw 29 | enoent 30 | envlist 31 | envname 32 | ericsysmin 33 | euxo 34 | exitstatus 35 | failover 36 | fileglob 37 | followlinks 38 | fooo 39 | frob 40 | geerlingguy 41 | getfixturevalue 42 | goss 43 | helloworld 44 | hookwrapper 45 | hostvars 46 | htmldir 47 | indentless 48 | jinaj 49 | jodewey 50 | jpvanhal 51 | jsonfile 52 | keypair 53 | kubevirt 54 | libcloud 55 | libera 56 | libpod 57 | lifecycle 58 | lockf 59 | makereport 60 | maxsplit 61 | metacopy 62 | mfoo 63 | minversion 64 | modifyitems 65 | mountopt 66 | multibranch 67 | mygroups 68 | nitz 69 | nocows 70 | nodestroy 71 | nodev 72 | nopasswd 73 | notest 74 | ntlm 75 | overlayfs 76 | parallelizable 77 | passenv 78 | passwordless 79 | prepender 80 | prerun 81 | printenv 82 | profisioner 83 | pycolors 84 | Qalthos 85 | quux 86 | reget 87 | reraised 88 | resetall 89 | rolebinding 90 | rrequirements 91 | runcmd 92 | saez 93 | schemafile 94 | seccomp 95 | selectattr 96 | serviceaccount 97 | sessionfinish 98 | setenv 99 | signoff 100 | skipsdist 101 | ssbarnea 102 | stestr 103 | stopall 104 | stty 105 | superitem 106 | sysexit 107 | templater 108 | testenv 109 | testinfra 110 | testrole 111 | testvar 112 | timedout 113 | tmpfs 114 | topdown 115 | tryfirst 116 | ulimits 117 | ultral 118 | unittests 119 | urandom 120 | usefixtures 121 | usermod 122 | vfoo 123 | virt 124 | virt's 125 | virtenv 126 | virtualmachines 127 | xdist 128 | zzyzx 129 | -------------------------------------------------------------------------------- /.config/molecule.spec: -------------------------------------------------------------------------------- 1 | # All tests require Internet access 2 | # to test in mock use: --enable-network --with check 3 | # to test in a privileged environment use: 4 | # --with check --with privileged_tests 5 | %bcond_with check 6 | %bcond_with privileged_tests 7 | 8 | Name: molecule 9 | Version: VERSION_PLACEHOLDER 10 | Release: 1%{?dist} 11 | Summary: Molecule testing framework for Ansible content 12 | 13 | License: MIT 14 | URL: https://github.com/ansible-community/molecule 15 | Source0: %{pypi_source} 16 | 17 | BuildArch: noarch 18 | 19 | BuildRequires: pyproject-rpm-macros 20 | BuildRequires: python%{python3_pkgversion}-build 21 | BuildRequires: python%{python3_pkgversion}-devel 22 | BuildRequires: python%{python3_pkgversion}-pip 23 | BuildRequires: python%{python3_pkgversion}-setuptools 24 | BuildRequires: python%{python3_pkgversion}-setuptools_scm 25 | BuildRequires: python%{python3_pkgversion}-wheel 26 | %if %{with check} 27 | # These are required for tests: 28 | BuildRequires: python%{python3_pkgversion}-pyyaml 29 | BuildRequires: python%{python3_pkgversion}-pytest 30 | BuildRequires: python%{python3_pkgversion}-pytest-xdist 31 | BuildRequires: python%{python3_pkgversion}-libselinux 32 | BuildRequires: git 33 | %endif 34 | # Named based on fedora 35: 35 | Requires: ansible-core 36 | Requires: python3-enrich 37 | Requires: python3-packaging 38 | Requires: python3-pyyaml 39 | Requires: python3-rich 40 | 41 | # generate_buildrequires 42 | # pyproject_buildrequires 43 | 44 | %description 45 | Molecule provides support for testing with multiple instances, operating 46 | systems and distributions, virtualization providers, test frameworks and 47 | testing scenarios. 48 | 49 | %prep 50 | %autosetup 51 | 52 | 53 | %build 54 | %pyproject_wheel 55 | 56 | 57 | %install 58 | %pyproject_install 59 | 60 | 61 | %if %{with check} 62 | %check 63 | PYTHONPATH=%{buildroot}%{python3_sitelib} \ 64 | pytest-3 \ 65 | -v \ 66 | --disable-pytest-warnings \ 67 | --numprocesses=auto \ 68 | %if %{with privileged_tests} 69 | tests 70 | %else 71 | tests/unit 72 | %endif 73 | %endif 74 | 75 | 76 | %files 77 | %{python3_sitelib}/molecule/ 78 | %{python3_sitelib}/molecule-*.dist-info/ 79 | %{_bindir}/molecule 80 | %license LICENSE 81 | %doc docs/* README.md 82 | 83 | %changelog 84 | Available at https://github.com/ansible-community/molecule/releases 85 | -------------------------------------------------------------------------------- /.config/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs-ansible>=24.3.0 2 | linkchecker>=10.4.0 3 | -------------------------------------------------------------------------------- /.config/requirements-test.txt: -------------------------------------------------------------------------------- 1 | ansi2html >= 1.8.0 2 | ansible-lint >= 6.12.1 3 | coverage[toml] 4 | docker >= 7.1.0 # testing 5 | filelock >= 3.9.0 6 | mypy 7 | pexpect >= 4.9.0, < 5 8 | pre-commit 9 | pydoclint 10 | pylint 11 | pytest 12 | pytest-mock >= 3.10.0 13 | pytest-plus >= 0.7.0 14 | pytest-xdist 15 | pytest-instafail 16 | requests != 2.32.0 # https://github.com/docker/docker-py/issues/3256 17 | ruff 18 | toml-sort 19 | tox 20 | types-jsonschema 21 | types-pexpect 22 | types-pyyaml 23 | -------------------------------------------------------------------------------- /.config/requirements-testinfra.txt: -------------------------------------------------------------------------------- 1 | pytest-testinfra>=8.1.0 2 | -------------------------------------------------------------------------------- /.config/requirements.in: -------------------------------------------------------------------------------- 1 | ansible-compat >= 25.1.4 2 | ansible-core >= 2.15.0 3 | click >= 8.0, < 9 4 | click-help-colors 5 | enrich >= 1.2.7 6 | jsonschema >= 4.9.1 7 | Jinja2 >= 2.11.3 8 | packaging 9 | pluggy >= 0.7.1, < 2.0 10 | PyYAML >= 5.1 11 | rich >= 9.5.1 12 | wcmatch>=8.1.2 # MIT 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # PWD is supposed to be the directory container the .envrc file based on the 3 | # specs. This trick should isolate our testing from user environment. 4 | ANSIBLE_HOME=$PWD/.ansible 5 | ANSIBLE_CONFIG=$PWD/ansible.cfg 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansible/devtools 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | Please see the official 4 | [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html). 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema 3 | name: Bug Report 4 | description: > 5 | Please test the main branch before creating new issues. Do not use this 6 | form to ask for support. 7 | # title: "[Bug]: " 8 | labels: ["new"] 9 | projects: ["ansible/86"] 10 | # assignees: 11 | # - octocat 12 | body: 13 | - type: checkboxes 14 | id: checklist 15 | attributes: 16 | label: Prerequisites 17 | description: > 18 | Create new issues only if all checks were made and you checked [FAQ](https://ansible.readthedocs.io/projects/molecule/faq/) and the 19 | [discussions forum](https://github.com/ansible/molecule/discussions). 20 | options: 21 | - label: This was not already reported in the past (duplicate check) 22 | required: true 23 | - label: It does reproduce it with code from main branch (latest unreleased version) 24 | required: true 25 | - label: I include a minimal example for reproducing the bug 26 | required: true 27 | - label: The bug is not trivial, as for those a direct pull-request is preferred 28 | required: true 29 | - label: Running `pip check` does not report any conflicts 30 | required: true 31 | - label: I was able to reproduce the issue on a different machine 32 | required: true 33 | - label: The issue is not specific to any driver other than 'default' one 34 | required: true 35 | 36 | - type: markdown 37 | attributes: 38 | value: | 39 | Also check [FAQ](https://ansible.readthedocs.io/projects/molecule/faq/) and the 40 | [discussions forum](https://github.com/ansible/molecule/discussions) before creating a new issue. 41 | 42 | - type: textarea 43 | id: environment 44 | attributes: 45 | label: Environment 46 | description: > 47 | Include all output of `molecule --version` and 48 | anything else that might be relevant, such **operating system**. 49 | validations: 50 | required: true 51 | 52 | - type: textarea 53 | id: what-happened 54 | attributes: 55 | label: What happened 56 | description: > 57 | Also tell us, what did you **expect** to happen? 58 | Please give some details of the feature being requested or what 59 | should happen if providing a bug report. 60 | placeholder: Tell us what you see! 61 | validations: 62 | required: true 63 | 64 | - type: textarea 65 | id: reproducer 66 | attributes: 67 | label: Reproducing example 68 | render: yml 69 | description: > 70 | Please write a [minimum complete verifiable YAML example](https://stackoverflow.com/help/mcve) that is reproduces the issue without 71 | causing other side effects. If the issue cannot be reproduce with a single 72 | YAML file, please paste a link to [gist](https://gist.github.com/) or to a full git repository that contain this example. 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Propose a new feature or behavior change 4 | url: https://github.com/ansible/molecule/discussions/categories/ideas 5 | about: Always check for existing threads before adding a new one 6 | - name: Ask a question on Discussions area 7 | url: https://github.com/ansible/molecule/discussions 8 | about: Recommended place for questions, debates, consultation. 9 | - name: Ask a question on StackOverflow 10 | url: https://stackoverflow.com/questions/tagged/molecule 11 | about: For well defined questions and answers use StackOverflow 12 | - name: Molecule Plugins (drivers) 13 | about: This includes some drivers such azure, docker, ec2, gce, podman and vagrant 14 | url: https://github.com/ansible-community/molecule-plugins/ 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security bug report 3 | about: How to report security vulnerabilities 4 | --- 5 | 6 | For all security related bugs, email security@ansible.com instead of using this issue tracker and you will receive a prompt response. 7 | 8 | For more information, see https://docs.ansible.com/ansible/latest/community/reporting_bugs_and_features.html 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: pip 5 | directory: /.config/ 6 | schedule: 7 | day: sunday 8 | interval: weekly 9 | labels: 10 | - dependabot-deps-updates 11 | - skip-changelog 12 | groups: 13 | dependencies: 14 | patterns: 15 | - "*" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | labels: 21 | - "dependencies" 22 | - "skip-changelog" 23 | -------------------------------------------------------------------------------- /.github/patchback.yml: -------------------------------------------------------------------------------- 1 | --- 2 | backport_branch_prefix: patchback/backports/ 3 | backport_label_prefix: backport- 4 | target_branch_prefix: stable/ 5 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # see https://github.com/ansible/team-devtools 3 | _extends: ansible/team-devtools 4 | -------------------------------------------------------------------------------- /.github/workflows/ack.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/ack.yml 2 | name: ack 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request_target: 10 | types: [opened, labeled, unlabeled, synchronize] 11 | 12 | jobs: 13 | ack: 14 | uses: ansible/team-devtools/.github/workflows/ack.yml@token_revised 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/ansible/team-devtools/blob/main/.github/workflows/push.yml 3 | name: push 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - "releases/**" 9 | - "stable/**" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | ack: 14 | uses: ansible/team-devtools/.github/workflows/push.yml@main 15 | -------------------------------------------------------------------------------- /.github/workflows/redirects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Sync RTD redirects 3 | name: redirects 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - docs/redirects.yml 11 | - .github/workflows/redirects.yml 12 | 13 | # Manually triggered using GitHub's UI 14 | workflow_dispatch: 15 | 16 | jobs: 17 | docs: 18 | environment: release 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-python@v5 23 | 24 | - name: Upgrade Python toolchain 25 | run: python3 -m pip install --upgrade pip setuptools wheel 26 | 27 | - name: Install readthedocs-cli 28 | run: python3 -m pip install readthedocs-cli 29 | 30 | - name: Sync redirects 31 | run: rtd projects ansible-lint redirects sync -f docs/redirects.yml --wet-run 32 | env: 33 | RTD_TOKEN: ${{ secrets.RTD_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | 4 | on: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | release: 10 | environment: release 11 | runs-on: ubuntu-24.04 12 | permissions: 13 | id-token: write 14 | 15 | env: 16 | FORCE_COLOR: 1 17 | PY_COLORS: 1 18 | 19 | steps: 20 | - name: Switch to using Python 3.12 by default 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.12" 24 | 25 | - name: Install tox 26 | run: python3 -m pip install --user "tox>=4.0.0" 27 | 28 | - name: Check out src from Git 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 # needed by setuptools-scm 32 | 33 | - name: Build dists 34 | run: python3 -m tox -e pkg 35 | 36 | - name: Publish to pypi.org 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | 39 | forum_post: 40 | needs: release 41 | runs-on: ubuntu-24.04 42 | 43 | steps: 44 | - name: Retreive the forum post script from team-devtools 45 | run: curl -O https://raw.githubusercontent.com/ansible/team-devtools/main/.github/workflows/forum_post.py 46 | 47 | - name: Run the forum post script 48 | run: python3 forum_post.py ${{ github.event.repository.full_name }} ${{ github.event.release.tag_name }} ${{ secrets.FORUM_KEY }} ${{ secrets.FORUM_USER }} 49 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tox 3 | 4 | on: 5 | merge_group: 6 | branches: 7 | - "main" 8 | push: 9 | branches: 10 | - "main" 11 | pull_request: 12 | branches: 13 | - "main" 14 | workflow_call: 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | tox: 22 | uses: ansible/team-devtools/.github/workflows/tox.yml@main 23 | with: 24 | default_python: "3.10" # for lint 25 | max_python: "3.13" 26 | jobs_producing_coverage: 8 27 | other_names_also: "eco" 28 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | 3 | # Needed by dependabot, see https://github.com/dependabot/dependabot-core/issues/1455 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | mkdocs: 5 | fail_on_warning: true 6 | configuration: mkdocs.yml 7 | 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.12" 12 | commands: 13 | - pip install --user tox 14 | - python3 -m tox -e docs 15 | python: 16 | install: 17 | - method: pip 18 | path: tox 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | submodules: 24 | include: all 25 | recursive: true 26 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.python.version=3.10, 3.11, 3.12, 3.13 2 | sonar.sources=src/ 3 | sonar.tests=tests/ 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "charliermarsh.ruff", 4 | "esbenp.prettier-vscode", 5 | "gruntfuggly.triggertaskonsave", 6 | "markis.code-coverage", 7 | "ms-python.debugpy", 8 | "ms-python.mypy-type-checker", 9 | "ms-python.pylint", 10 | "ms-python.python", 11 | "sonarsource.sonarlint-vscode", 12 | "streetsidesoftware.code-spell-checker" 13 | ], 14 | "unwantedRecommendations": [ 15 | "ms-python.isort", 16 | "ms-python.flake8", 17 | "ms-toolsai.jupyter", 18 | "ms-python.black-formatter" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[jsonc]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[python]": { 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit", 8 | "source.organizeImports": "explicit" 9 | }, 10 | "editor.defaultFormatter": "charliermarsh.ruff", 11 | "editor.formatOnSave": true 12 | }, 13 | "flake8.importStrategy": "fromEnvironment", 14 | "markiscodecoverage.searchCriteria": ".cache/.coverage/lcov.info", 15 | "mypy-type-checker.args": ["--config-file=${workspaceFolder}/pyproject.toml"], 16 | "mypy-type-checker.importStrategy": "fromEnvironment", 17 | "mypy-type-checker.reportingScope": "workspace", 18 | "pylint.importStrategy": "fromEnvironment", 19 | "python.testing.pytestArgs": ["tests"], 20 | "python.testing.pytestEnabled": true, 21 | "python.testing.unittestEnabled": false, 22 | "triggerTaskOnSave.tasks": { 23 | "pydoclint": ["*.py"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "pydoclint", 8 | "type": "shell", 9 | "command": "pydoclint", 10 | "args": ["."], 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "problemMatcher": { 15 | "owner": "pydoclint", 16 | "fileLocation": ["relative", "${workspaceFolder}"], 17 | "pattern": { 18 | "regexp": "^(.*?):(\\d+):\\s(.*?):\\s(.*)$", 19 | "file": 1, 20 | "line": 2, 21 | "code": 3, 22 | "message": 4 23 | } 24 | }, 25 | "group": { 26 | "kind": "none", 27 | "isDefault": true 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2018 Cisco Systems, Inc. 4 | Copyright (c) 2018 Red Hat, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About Ansible Molecule 2 | 3 | [![PyPI Package](https://img.shields.io/pypi/v/molecule)](https://pypi.org/project/molecule/) 4 | [![Documentation Status](https://readthedocs.org/projects/molecule/badge/?version=latest)](https://ansible.readthedocs.io/projects/molecule) 5 | [![image](https://github.com/ansible-community/molecule/workflows/tox/badge.svg)](https://github.com/ansible-community/molecule/actions) 6 | [![Python Black Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) 7 | [![Ansible Code of Conduct](https://img.shields.io/badge/Code%20of%20Conduct-silver.svg)](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) 8 | [![Discussions](https://img.shields.io/badge/Discussions-silver.svg)](https://forum.ansible.com/tag/molecule) 9 | [![Repository License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 10 | 11 | Molecule project is designed to aid in the development and testing of 12 | [Ansible](https://ansible.com) roles. 13 | 14 | Molecule provides support for testing with multiple instances, operating 15 | systems and distributions, virtualization providers, test frameworks and 16 | testing scenarios. 17 | 18 | Molecule encourages an approach that results in consistently developed 19 | roles that are well-written, easily understood and maintained. 20 | 21 | Molecule supports only the latest two major versions of Ansible (N/N-1). 22 | 23 | Once installed, the command line can be called using any of the methods 24 | below: 25 | 26 | ```bash 27 | molecule ... 28 | python3 -m molecule ... # python module calling method 29 | ``` 30 | 31 | # Documentation 32 | 33 | Read the documentation and more at . 34 | 35 | # Get Involved 36 | 37 | See the [Talk to us](https://ansible.readthedocs.io/projects/molecule/contributing/#talk-to-us) section of the documentation to ask questions, find help, and join the conversation. 38 | 39 | For complete details, see the 40 | [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). 41 | 42 | If you want to get moving fast and make a quick patch: 43 | 44 | ```bash 45 | $ git clone https://github.com/ansible-community/molecule && cd molecule 46 | $ python3 -m venv .venv && source .venv/bin/activate 47 | $ python3 -m pip install -U setuptools pip tox 48 | ``` 49 | 50 | And you're ready to make your changes! 51 | 52 | # Authors 53 | 54 | Molecule project was created by [Retr0h](https://github.com/retr0h) and 55 | it is now community-maintained as part of the 56 | [Ansible](https://ansible.com) by Red Hat project. 57 | 58 | # License 59 | 60 | The 61 | [MIT](https://github.com/ansible-community/molecule/blob/main/LICENSE) 62 | License. 63 | 64 | The logo is licensed under the [Creative Commons NoDerivatives 4.0 65 | License](https://creativecommons.org/licenses/by-nd/4.0/). 66 | 67 | If you have some other use in mind, contact us. 68 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | # User during testing to ensure local user config does not affect testing 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | patch: true 5 | -------------------------------------------------------------------------------- /collections/README.md: -------------------------------------------------------------------------------- 1 | These collections are here for ansible-lint so the ansible-lint pre-commit action can install them prior to linting the repository. 2 | -------------------------------------------------------------------------------- /collections/requirements.yml: -------------------------------------------------------------------------------- 1 | # This file is only used for testing purposes as it helps ansible-lint to 2 | # install dependencies when these are missing. 3 | collections: 4 | - name: community.crypto 5 | - name: community.docker 6 | version: ">=3.10.4" 7 | - name: containers.podman 8 | - name: kubernetes.core 9 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | dictionaryDefinitions: 2 | - name: words 3 | path: .config/dictionary.txt 4 | addWords: true 5 | dictionaries: 6 | - bash 7 | - networking-terms 8 | - python 9 | - words 10 | - "!aws" 11 | - "!backwards-compatibility" 12 | - "!cryptocurrencies" 13 | - "!cpp" 14 | ignorePaths: 15 | - .config/requirements* 16 | - \.* 17 | - cspell.config.yaml 18 | - mkdocs.yml 19 | - pyproject.toml 20 | - tox.ini 21 | 22 | languageSettings: 23 | - languageId: python 24 | allowCompoundWords: false 25 | -------------------------------------------------------------------------------- /docs/_static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/_static/images/favicon.ico -------------------------------------------------------------------------------- /docs/_static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/_static/images/logo.png -------------------------------------------------------------------------------- /docs/_static/images/logo_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/_static/images/logo_big.png -------------------------------------------------------------------------------- /docs/asset/molecule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/asset/molecule.png -------------------------------------------------------------------------------- /docs/asset/molecule.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/asset/molecule.psd -------------------------------------------------------------------------------- /docs/asset/mollie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/asset/mollie.png -------------------------------------------------------------------------------- /docs/asset/mollie.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/asset/mollie.psd -------------------------------------------------------------------------------- /docs/examples/docker.md: -------------------------------------------------------------------------------- 1 | # Using docker containers 2 | 3 | Below you can see a scenario that is using docker containers as test hosts. 4 | When you run `molecule test --scenario-name docker` the `create`, `converge` and 5 | `destroy` steps will be run one after another. 6 | 7 | This example is using Ansible playbooks and it does not need any molecule 8 | plugins to run. You can fully control which test requirements you need to be 9 | installed. 10 | 11 | ## Config playbook 12 | 13 | ```yaml title="molecule.yml" 14 | {!tests/fixtures/integration/test_command/molecule/docker/molecule.yml!} 15 | ``` 16 | 17 | ```yaml title="requirements.yml" 18 | {!tests/fixtures/integration/test_command/molecule/docker/requirements.yml!} 19 | ``` 20 | 21 | ## Create playbook 22 | 23 | ```yaml title="create.yml" 24 | {!tests/fixtures/integration/test_command/molecule/docker/create.yml!} 25 | ``` 26 | 27 | ```yaml title="tasks/create-fail.yml" 28 | {!tests/fixtures/integration/test_command/molecule/docker/tasks/create-fail.yml!} 29 | ``` 30 | 31 | ## Converge playbook 32 | 33 | ```yaml title="converge.yml" 34 | {!tests/fixtures/integration/test_command/molecule/docker/converge.yml!} 35 | ``` 36 | 37 | ## Destroy playbook 38 | 39 | ```yaml title="destroy.yml" 40 | {!tests/fixtures/integration/test_command/molecule/docker/destroy.yml!} 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/examples/podman.md: -------------------------------------------------------------------------------- 1 | # Using podman containers 2 | 3 | Below you can see a scenario that is using podman containers as test hosts. 4 | When you run `molecule test --scenario-name podman` the `create`, `converge` and 5 | `destroy` steps will be run one after another. 6 | 7 | This example is using Ansible playbooks and it does not need any molecule 8 | plugins to run. You can fully control which test requirements you need to be 9 | installed. 10 | 11 | ## Config playbook 12 | 13 | ```yaml title="molecule.yml" 14 | {!tests/fixtures/integration/test_command/molecule/podman/molecule.yml!} 15 | ``` 16 | 17 | ```yaml title="requirements.yml" 18 | {!tests/fixtures/integration/test_command/molecule/podman/requirements.yml!} 19 | ``` 20 | 21 | ## Create playbook 22 | 23 | ```yaml title="create.yml" 24 | {!tests/fixtures/integration/test_command/molecule/podman/create.yml!} 25 | ``` 26 | 27 | ```yaml title="tasks/create-fail.yml" 28 | {!tests/fixtures/integration/test_command/molecule/podman/tasks/create-fail.yml!} 29 | ``` 30 | 31 | ## Converge playbook 32 | 33 | ```yaml title="converge.yml" 34 | {!tests/fixtures/integration/test_command/molecule/podman/converge.yml!} 35 | ``` 36 | 37 | ## Destroy playbook 38 | 39 | ```yaml title="destroy.yml" 40 | {!tests/fixtures/integration/test_command/molecule/podman/destroy.yml!} 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/google04e29a42ae6e6cbc.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google04e29a42ae6e6cbc.html 2 | -------------------------------------------------------------------------------- /docs/guides/custom-image.md: -------------------------------------------------------------------------------- 1 | ## Customizing the Docker Image Used by a Scenario/Platform 2 | 3 | The docker driver supports using pre-built images and `docker build` 4 | -ing local customizations for each scenario's platform. The Docker 5 | image used by a scenario is governed by the following configuration 6 | items: 7 | 8 | 1. `platforms[*].image`: Docker image name:tag to use as base image. 9 | 10 | 2. `platforms[*].pre_build_image`: Whether to customize base image or 11 | use as-is[^1]. 12 | 13 | > - When `true`, use the specified `platform[].image` as-is. 14 | > - When `false`, exec `docker build` to customize base image 15 | > using either: 16 | > 17 | > > - Dockerfile specified by `platforms[*].dockerfile` or 18 | > > - Dockerfile rendered from `Dockerfile.j2` template (in 19 | > > scenario dir) 20 | 21 | The `Dockerfile.j2` template is generated at 22 | `molecule init scenario`-time when `--driver-name` is `docker`. The 23 | template can be customized as needed to create the desired modifications 24 | to the Docker image used in the scenario. 25 | 26 | Note: `platforms[*].pre_build_image` defaults to `true` in each 27 | scenario's generated `molecule.yml` file. 28 | 29 | [^1]: 30 | [Implementation in molecule docker 31 | driver](https://github.com/ansible-community/molecule-plugins/blob/main/src/molecule_plugins/docker/playbooks/create.yml) 32 | -------------------------------------------------------------------------------- /docs/guides/docker-rootless.md: -------------------------------------------------------------------------------- 1 | ## Docker With Non-Privileged User 2 | 3 | The Molecule Docker driver executes Ansible playbooks as the 4 | root user. If your workflow requires adding support for running as a 5 | non-privileged user, then adapt `molecule.yml` and `Dockerfile.j2` as 6 | follows. 7 | 8 | Note: The `Dockerfile` templating and image building processes are only 9 | done for scenarios with `pre_build_image = False`, which is not the 10 | default setting in generated `molecule.yml` files. 11 | 12 | To modify the Docker image to support running as a normal user: 13 | 14 | Append the following code block to the end of `Dockerfile.j2`. It 15 | creates an `ansible` user with passwordless sudo privileges. Note the 16 | variable `SUDO_GROUP` depends on the target distribution as Debian uses `sudo` 17 | instead of `wheel` group. 18 | 19 | ```docker 20 | {% raw %} 21 | # Create `ansible` user with sudo permissions and membership in `DEPLOY_GROUP` 22 | # This template gets rendered using `loop: "{{ molecule_yml.platforms }}"`, so 23 | # each `item` is an element of platforms list from the molecule.yml file for this scenario. 24 | ENV ANSIBLE_USER=ansible DEPLOY_GROUP=deployer 25 | ENV SUDO_GROUP={{'sudo' if 'debian' in item.image else 'wheel' }} 26 | RUN set -xe \ 27 | && groupadd -r ${ANSIBLE_USER} \ 28 | && groupadd -r ${DEPLOY_GROUP} \ 29 | && useradd -m -g ${ANSIBLE_USER} ${ANSIBLE_USER} \ 30 | && usermod -aG ${SUDO_GROUP} ${ANSIBLE_USER} \ 31 | && usermod -aG ${DEPLOY_GROUP} ${ANSIBLE_USER} \ 32 | && sed -i "/^%${SUDO_GROUP}/s/ALL\$/NOPASSWD:ALL/g" /etc/sudoers 33 | {% endraw %} 34 | ``` 35 | 36 | Modify `provisioner.inventory` in `molecule.yml` as follows: 37 | 38 | ```yaml 39 | platforms: 40 | - name: instance 41 | image: quay.io/centos/centos:stream8 42 | # … 43 | ``` 44 | 45 | ```yaml 46 | provisioner: 47 | name: ansible 48 | # … 49 | inventory: 50 | host_vars: 51 | # setting for the platform instance named 'instance' 52 | instance: 53 | ansible_user: ansible 54 | ``` 55 | 56 | Make sure to use your **platform instance name**. In this case 57 | `instance`. 58 | 59 | An example for a different platform instance name: 60 | 61 | ```yaml 62 | platforms: 63 | - name: centos8 64 | image: quay.io/centos/centos:stream8 65 | # … 66 | ``` 67 | 68 | ```yaml 69 | provisioner: 70 | name: ansible 71 | # … 72 | inventory: 73 | host_vars: 74 | # setting for the platform instance named 'centos8' 75 | centos8: 76 | ansible_user: ansible 77 | ``` 78 | 79 | To test it, add the following task to `tasks/main.yml`. It fails, 80 | because the non-privileged user is not allowed to create a folder in 81 | `/opt/`. This needs to be performed using `sudo`. 82 | 83 | To perform the task using `sudo`, uncomment `become: yes`. Now the task 84 | will succeed. 85 | 86 | ```yaml 87 | - name: Create apps dir 88 | file: 89 | path: /opt/examples 90 | owner: ansible 91 | group: deployer 92 | mode: 0775 93 | state: directory 94 | # become: yes 95 | ``` 96 | 97 | Don't forget to run `molecule destroy` if image has already been 98 | created. 99 | -------------------------------------------------------------------------------- /docs/guides/inside-a-container.md: -------------------------------------------------------------------------------- 1 | ## Running inside a container 2 | 3 | Molecule is built into a Docker image by the [Ansible Dev Tools](https://ansible.readthedocs.io/projects/dev-tools/container/) project. 4 | 5 | Any questions or bugs related to use of Molecule from within a container 6 | should be addressed by the Ansible Dev Tools 7 | project. 8 | -------------------------------------------------------------------------------- /docs/guides/monolith.md: -------------------------------------------------------------------------------- 1 | ## Monolith Repo 2 | 3 | Molecule is generally used to test playbooks or roles in isolation. 4 | However, it can also test them from a monolith repo. 5 | 6 | ```bash 7 | $ tree monolith-repo -L 3 --prune 8 | monolith-repo 9 | ├── library 10 | │ └── foo.py 11 | ├── plugins 12 | │ └── filters 13 | │ └── foo.py 14 | └── roles 15 | ├── bar 16 | │ └── README.md 17 | ├── baz 18 | │ └── README.md 19 | └── foo 20 | └── README.md 21 | ``` 22 | 23 | The role initialized with Molecule (baz in this case) would simply 24 | reference the dependent roles via its `converge.yml` or meta 25 | dependencies. 26 | 27 | Molecule can test complex scenarios leveraging this technique. 28 | 29 | ```bash 30 | $ cd monolith-repo/roles/baz 31 | $ molecule test 32 | ``` 33 | 34 | Molecule is simply setting the `ANSIBLE_*` environment variables. To 35 | view the environment variables set during a Molecule operation pass the 36 | `--debug` flag. 37 | 38 | ```bash 39 | $ molecule --debug test 40 | 41 | DEBUG: ANSIBLE ENVIRONMENT 42 | --- 43 | ANSIBLE_CONFIG: /private/tmp/monolith-repo/roles/baz/molecule/default/.molecule/ansible.cfg 44 | ``` 45 | 46 | Molecule can be customized any number of ways. Updating the 47 | provisioner's env section in `molecule.yml` to suit the needs of the 48 | developer and layout of the project. 49 | 50 | ```yaml 51 | provisioner: 52 | name: ansible 53 | env: 54 | ANSIBLE_$VAR: $VALUE 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/guides/parallel.md: -------------------------------------------------------------------------------- 1 | ## Running Molecule processes in parallel mode 2 | 3 | !!! warning 4 | 5 | This functionality should be considered experimental. It is part of 6 | ongoing work towards enabling parallelizable functionality across all 7 | moving parts in the execution of the Molecule feature set. 8 | 9 | !!! note 10 | 11 | Only the following sequences support parallelizable functionality: 12 | 13 | > - `check_sequence`: `molecule check --parallel` 14 | > - `destroy_sequence`: `molecule destroy --parallel` 15 | > - `test_sequence`: `molecule test --parallel` 16 | 17 | It is currently only available for use with the Docker driver. 18 | 19 | When Molecule receives the `--parallel` flag it will generate a 20 | [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) for 21 | the duration of the testing sequence and will use that unique identifier 22 | to cache the run-time state for that process. The parallel Molecule 23 | processes cached state and created instances will therefore not 24 | interfere with each other. 25 | 26 | Molecule uses a new and separate caching folder for this in the 27 | `$HOME/.cache/molecule_parallel` location. Molecule exposes a new 28 | environment variable `MOLECULE_PARALLEL` which can enable this 29 | functionality. 30 | 31 | It is possible to run Molecule processes in parallel using another tool 32 | to orchestrate the parallelization (such as [GNU 33 | Parallel](https://www.gnu.org/software/parallel/) or 34 | [Pytest](https://docs.pytest.org/en/latest/)). If you do so, make sure 35 | Molecule knows it is running in parallel mode by specifying the 36 | `--parallel` flag to your command(s) to avoid concurrency issues. 37 | -------------------------------------------------------------------------------- /docs/guides/podman-inside-docker.md: -------------------------------------------------------------------------------- 1 | # Podman inside Docker 2 | 3 | Sometimes your CI system comes prepared to run with Docker but you want 4 | to test podman into it. This `prepare.yml` playbook would let podman run 5 | inside a privileged Docker host by adding some required settings: 6 | 7 | ```yaml 8 | - name: prepare 9 | hosts: podman-in-docker 10 | tasks: 11 | - name: install fuse-overlayfs 12 | package: 13 | name: 14 | - fuse-overlayfs 15 | 16 | - name: create containers config dir 17 | file: 18 | group: root 19 | mode: a=rX,u+w 20 | owner: root 21 | path: /etc/containers 22 | state: directory 23 | 24 | - name: make podman use fuse-overlayfs storage 25 | copy: 26 | content: | 27 | # See man 5 containers-storage.conf for more information 28 | [storage] 29 | driver = "overlay" 30 | [storage.options.overlay] 31 | mount_program = "/usr/bin/fuse-overlayfs" 32 | mountopt = "nodev,metacopy=on" 33 | dest: /etc/containers/storage.conf 34 | group: root 35 | mode: a=r,u+w 36 | owner: root 37 | 38 | - name: make podman use cgroupfs cgroup manager 39 | copy: 40 | content: | 41 | # See man 5 libpod.conf for more information 42 | cgroup_manager = "cgroupfs" 43 | dest: /etc/containers/libpod.conf 44 | group: root 45 | mode: a=r,u+w 46 | owner: root 47 | ``` 48 | 49 | Another option is to configure the same settings directly into the 50 | `molecule.yml` definition: 51 | 52 | ```yaml 53 | driver: 54 | name: podman 55 | platforms: 56 | - name: podman-in-docker 57 | # ... other options 58 | cgroup_manager: cgroupfs 59 | storage_opt: overlay.mount_program=/usr/bin/fuse-overlayfs 60 | storage_driver: overlay 61 | ``` 62 | 63 | At the time of writing, [Gitlab CI shared runners run privileged Docker 64 | hosts](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) and 65 | are suitable for these workarounds. 66 | -------------------------------------------------------------------------------- /docs/guides/sharing.md: -------------------------------------------------------------------------------- 1 | ## Sharing Across Scenarios 2 | 3 | Playbooks and tests can be shared across scenarios. 4 | 5 | ```bash 6 | $ tree shared-tests 7 | shared-tests 8 | ├── molecule 9 | │   ├── centos 10 | │   │   └── molecule.yml 11 | │   ├── resources 12 | │   │   ├── playbooks 13 | │   │   │   ├── Dockerfile.j2 (optional) 14 | │   │   │   ├── create.yml 15 | │   │   │   ├── destroy.yml 16 | │   │   │   ├── converge.yml # <-- previously called playbook.yml 17 | │   │   │   └── prepare.yml 18 | │   │   └── tests 19 | │   │   └── test_default.py 20 | │   ├── ubuntu 21 | │   │   └── molecule.yml 22 | │   └── ubuntu-upstart 23 | │   └── molecule.yml 24 | ``` 25 | 26 | Tests and playbooks can be shared across scenarios. 27 | 28 | In this example the `tests` directory lives in a shared 29 | location and `molecule.yml` points to the shared tests. 30 | 31 | ```yaml 32 | verifier: 33 | name: testinfra 34 | directory: ../resources/tests/ 35 | ``` 36 | 37 | In this second example the actions `create`, 38 | `destroy`, `converge` and `prepare` 39 | are loaded from a shared directory. 40 | 41 | ```yaml 42 | provisioner: 43 | name: ansible 44 | playbooks: 45 | create: ../resources/playbooks/create.yml 46 | destroy: ../resources/playbooks/destroy.yml 47 | converge: ../resources/playbooks/converge.yml 48 | prepare: ../resources/playbooks/prepare.yml 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/guides/systemd-container.md: -------------------------------------------------------------------------------- 1 | ## Systemd Container 2 | 3 | To start a service which requires systemd, [in a non-privileged 4 | container](https://developers.redhat.com/blog/2016/09/13/running-systemd-in-a-non-privileged-container/), 5 | configure `molecule.yml` with a systemd compliant image, tmpfs, volumes, 6 | and command as follows. 7 | 8 | ```yaml 9 | platforms: 10 | - name: instance 11 | image: quay.io/centos/centos:stream8 12 | command: /sbin/init 13 | tmpfs: 14 | - /run 15 | - /tmp 16 | volumes: 17 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 18 | ``` 19 | 20 | When needed, such security profiles can be reused (for example [the one 21 | available in 22 | Fedora](https://src.fedoraproject.org/rpms/docker/raw/88fa030b904d7af200b150e10ea4a700f759cca4/f/seccomp.json)): 23 | 24 | ```yaml 25 | platforms: 26 | - name: instance 27 | image: debian:stretch 28 | command: /sbin/init 29 | security_opts: 30 | - seccomp=path/to/seccomp.json 31 | tmpfs: 32 | - /run 33 | - /tmp 34 | volumes: 35 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 36 | ``` 37 | 38 | The developer can also opt to [start the container with extended 39 | privileges](https://blog.docker.com/2013/09/docker-can-now-run-within-docker/), 40 | by either giving it `SYS_ADMIN` capabilities or running it in 41 | `privileged` mode. 42 | 43 | !!! warning 44 | 45 | Use caution when using `privileged` mode or `SYS_ADMIN` capabilities as 46 | it grants the container elevated access to the underlying system. 47 | 48 | To limit the scope of the extended privileges, grant `SYS_ADMIN` 49 | capabilities along with the same image, command, and volumes as shown in 50 | the `non-privileged` example. 51 | 52 | ```yaml 53 | platforms: 54 | - name: instance 55 | image: quay.io/centos/centos:stream8 56 | command: /sbin/init 57 | capabilities: 58 | - SYS_ADMIN 59 | volumes: 60 | - /sys/fs/cgroup:/sys/fs/cgroup:ro 61 | ``` 62 | 63 | To start the container in `privileged` mode, set the privileged flag 64 | along with the same image and command as shown in the `non-privileged` 65 | example. 66 | 67 | ```yaml 68 | platforms: 69 | - name: instance 70 | image: quay.io/centos/centos:stream8 71 | command: /sbin/init 72 | privileged: True 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/images/collection_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/docs/images/collection_structure.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | --- 6 | 7 | # About Ansible Molecule 8 | 9 | Molecule project is designed to aid in the development and testing of 10 | [Ansible](https://ansible.com) roles. 11 | 12 | Molecule provides support for testing with multiple instances, operating 13 | systems and distributions, virtualization providers, test frameworks and 14 | testing scenarios. 15 | 16 | Molecule encourages an approach that results in consistently developed 17 | roles that are well-written, easily understood and maintained. 18 | 19 | Molecule supports only the latest two major versions of Ansible (N/N-1). 20 | 21 | Once installed, the command line can be called using any of the methods 22 | below: 23 | 24 | ```bash 25 | molecule ... 26 | python3 -m molecule ... # python module calling method 27 | ``` 28 | 29 | # External Resources 30 | 31 | Below you can see a list of useful articles and presentations, recently 32 | updated being listed first: 33 | 34 | - [Ansible Collections: Role Tests with 35 | Molecule](https://ericsysmin.com/2020/04/30/ansible-collections-role-tests-with-molecule/) 36 | @ericsysmin 37 | - [Molecule v3 Slides](https://sbarnea.com/slides/molecule/#/) 38 | @ssbarnea. 39 | - [Testing your Ansible roles with 40 | Molecule](https://www.jeffgeerling.com/blog/2018/testing-your-ansible-roles-molecule) 41 | @geerlingguy 42 | - [How to test Ansible and don't go 43 | nuts](https://www.goncharov.xyz/it/ansible-testing-en.html) 44 | @ultral 45 | -------------------------------------------------------------------------------- /docs/next.md: -------------------------------------------------------------------------------- 1 | # Molecule Next 2 | 3 | Molecule "next" is the future major version of molecule, one that is currently 4 | available from the main branch. One of the main goals of the new version is to 5 | reduce the amount of magic and just rely on ansible core features. 6 | 7 | # Implemented changes 8 | 9 | - `roles-path` and `collections-paths` are no longer configurable for 10 | dependencies. Users are expected to make use of [ansible.cfg](https://docs.ansible.com/ansible/latest/reference_appendices/config.html) file to 11 | alter them when needed. 12 | 13 | - `molecule init` command is now only available to create a scenario 14 | using `molecule init scenario`. 15 | Users will no longer be able to create a role. 16 | Instead, users can make use of [ansible-galaxy](https://docs.ansible.com/ansible/latest/galaxy/dev_guide.html) to create a collection or role. 17 | 18 | - From v6, `testinfra` is now an optional dependency. 19 | It will be removed in the next major release(v7). 20 | 21 | # Planned changes 22 | 23 | - Refactoring how dependencies are installed 24 | - Bringing ephemeral directory under scenario folder instead of the current 25 | inconvenient location under `~/.cache/molecule/...` 26 | - Addition of a minimal `ansible.cfg` file under the scenario folder that can 27 | be used to tell Ansible from where to load testing content. This is to replace 28 | current Molecule magic around roles, collections and library paths and 29 | test inventory location. Once done you will be able to run molecule playbooks with Ansible directly without 30 | having to define these folders. 31 | -------------------------------------------------------------------------------- /docs/redirects.yml: -------------------------------------------------------------------------------- 1 | # Authoritative list of redirects we have configured in RTD, 2 | # https://pypi.org/project/readthedocs-cli/ 3 | --- 4 | - type: page 5 | from_url: /en/latest/ 6 | to_url: / 7 | - type: page 8 | from_url: /en/latest/getting-started/ 9 | to_url: /getting-started/ 10 | # Keep this last or it will match all the other rules 11 | - type: sphinx_htmldir 12 | from_url: "" 13 | to_url: "" 14 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Command Line Reference 2 | 3 | ## Special commands 4 | 5 | - drivers 6 | - init 7 | - list 8 | - login 9 | - matrix 10 | - reset 11 | 12 | ## Valid actions 13 | 14 | - check 15 | - cleanup 16 | 17 | - This action has cleanup and is not enabled by default. 18 | See the provisioner's documentation for further details. 19 | 20 | - **converge** : Converge will execute the sequence necessary to converge the instances. 21 | - create \*\* driver 22 | - dependency 23 | - destroy \*\* all, parallel, driver 24 | - idempotence 25 | - prepare \*\* force 26 | - side-effect 27 | - syntax 28 | - test - \*\* - Test command will execute the sequence necessary to test the instances. 29 | - verify 30 | 31 | ### -s, --scenario-name 32 | 33 | ### --parallel / --no-parallel 34 | 35 | ### Passing extra arguments to the provisioner 36 | 37 | ``` 38 | ... -- -vvv --tags foo,bar 39 | 40 | Providing additional command line arguments to the `ansible-playbook` 41 | command. Use this option with care, as there is no sanitation or 42 | validation of input. Options passed on the CLI override options 43 | provided in provisioner's `options` section of `molecule.yml`. 44 | ``` 45 | 46 | ## molecule init 47 | 48 | ### molecule init scenario 49 | 50 | ## molecule list 51 | 52 | List command shows information about current scenarios. 53 | 54 | ``` 55 | molecule list 56 | ``` 57 | 58 | ## molecule login 59 | 60 | ## molecule matrix 61 | 62 | Matrix will display the subcommand's ordered list of actions, which can 63 | be changed in 64 | [scenario](configuration.md#scenario) 65 | configuration. 66 | 67 | ## Test sequence commands 68 | 69 | We can tell Molecule to create an instance with: 70 | 71 | ```bash 72 | molecule create 73 | ``` 74 | 75 | We can verify that Molecule has created the instance and they're up and 76 | running with: 77 | 78 | ```bash 79 | molecule list 80 | ``` 81 | 82 | Now, let's add a task to our role under `tasks/main.yml` file like so: 83 | 84 | ```yaml 85 | - name: Molecule Hello World! 86 | ansible.builtin.debug: 87 | msg: Hello, World! 88 | ``` 89 | 90 | We can then tell Molecule to test our role against our instance with: 91 | 92 | ```bash 93 | molecule converge 94 | ``` 95 | 96 | If we want to manually inspect the instance afterward, we can run: 97 | 98 | ```bash 99 | molecule login 100 | ``` 101 | 102 | We now have a free hand to experiment with the instance state. 103 | 104 | Finally, we can exit the instance and destroy it with: 105 | 106 | ```bash 107 | molecule destroy 108 | ``` 109 | 110 | !!! note 111 | 112 | If Molecule reports any errors, it can be useful to pass the `--debug` 113 | option to get more verbose output. 114 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [settings] 2 | idiomatic_version_file_enable_tools = [] 3 | -------------------------------------------------------------------------------- /molecule: -------------------------------------------------------------------------------- 1 | tests/fixtures/integration/test_command/molecule -------------------------------------------------------------------------------- /src/molecule/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Red Hat, Inc. 2 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to 6 | # deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | # sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | """Molecule version information.""" 22 | 23 | from __future__ import annotations 24 | 25 | 26 | try: 27 | from ._version import ( # type: ignore[import-not-found, unused-ignore] 28 | __version__, 29 | version, 30 | ) 31 | except ImportError: # pragma: no cover 32 | __version__ = "0.1.dev1" 33 | version = __version__ 34 | 35 | __all__ = ("__version__", "version") 36 | -------------------------------------------------------------------------------- /src/molecule/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Red Hat, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Molecule CLI main entry point.""" 21 | 22 | from __future__ import annotations 23 | 24 | from molecule.shell import main 25 | 26 | 27 | if __name__ == "__main__": 28 | main() # pylint: disable=no-value-for-parameter 29 | -------------------------------------------------------------------------------- /src/molecule/app.py: -------------------------------------------------------------------------------- 1 | """Molecule Application Module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from functools import lru_cache 6 | from pathlib import Path 7 | from subprocess import CalledProcessError, CompletedProcess 8 | from typing import TYPE_CHECKING 9 | 10 | from ansible_compat.runtime import Runtime 11 | 12 | from molecule.util import print_environment_vars 13 | 14 | 15 | if TYPE_CHECKING: 16 | from pathlib import Path 17 | 18 | 19 | class App: 20 | """App class that keep runtime status.""" 21 | 22 | def __init__(self, path: Path) -> None: 23 | """Create a new app instance. 24 | 25 | Args: 26 | path: The path to the project. 27 | """ 28 | self.runtime = Runtime(project_dir=path, isolated=False) 29 | 30 | def run_command( # noqa: PLR0913 31 | self, 32 | cmd: str | list[str], 33 | env: dict[str, str] | None = None, 34 | cwd: Path | None = None, 35 | *, 36 | debug: bool = False, 37 | echo: bool = False, # noqa: ARG002 38 | quiet: bool = False, # noqa: ARG002 39 | check: bool = False, 40 | ) -> CompletedProcess[str]: 41 | """Execute the given command and returns None. 42 | 43 | Args: 44 | cmd: A list of strings containing the command to run. 45 | env: A dict containing the shell's environment. 46 | cwd: An optional Path to the working directory. 47 | debug: An optional bool to toggle debug output. 48 | echo: An optional bool to toggle command echo. 49 | quiet: An optional bool to toggle command output. 50 | check: An optional bool to toggle command error checking. 51 | 52 | Returns: 53 | A completed process object. 54 | 55 | Raises: 56 | CalledProcessError: If return code is nonzero and check is True. 57 | """ 58 | if debug: 59 | print_environment_vars(env) 60 | 61 | result = self.runtime.run( 62 | args=cmd, 63 | env=env, 64 | cwd=cwd, 65 | tee=True, 66 | set_acp=False, 67 | ) 68 | if result.returncode != 0 and check: 69 | raise CalledProcessError( 70 | returncode=result.returncode, 71 | cmd=result.args, 72 | output=result.stdout, 73 | stderr=result.stderr, 74 | ) 75 | return result 76 | 77 | 78 | @lru_cache 79 | def get_app(path: Path) -> App: 80 | """Return the app instance. 81 | 82 | Args: 83 | path: The path to the project. 84 | 85 | Returns: 86 | App: The app instance. 87 | """ 88 | return App(path) 89 | -------------------------------------------------------------------------------- /src/molecule/command/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | # 3 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | # DEALINGS IN THE SOFTWARE. 22 | 23 | # NOTE(retr0h): Importing into the ``molecule.command`` namespace, to prevent 24 | # collisions (e.g. ``list``). The CLI usage may conflict with reserved words 25 | # or builtins. 26 | from __future__ import annotations 27 | 28 | from molecule.command import ( 29 | base, # noqa: F401 30 | check, # noqa: F401 31 | cleanup, # noqa: F401 32 | converge, # noqa: F401 33 | create, # noqa: F401 34 | dependency, # noqa: F401 35 | destroy, # noqa: F401 36 | drivers, # noqa: F401 37 | idempotence, # noqa: F401 38 | list, # noqa: A004, F401 39 | login, # noqa: F401 40 | matrix, # noqa: F401 41 | prepare, # noqa: F401 42 | reset, # noqa: F401 43 | side_effect, # noqa: F401 44 | syntax, # noqa: F401 45 | test, # noqa: F401 46 | verify, # noqa: F401 47 | ) 48 | from molecule.command.init import init # noqa: F401 49 | -------------------------------------------------------------------------------- /src/molecule/command/dependency.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Dependency Command Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | from typing import TYPE_CHECKING 27 | 28 | import click 29 | 30 | from molecule.command import base 31 | 32 | 33 | if TYPE_CHECKING: 34 | from molecule.types import CommandArgs, MoleculeArgs 35 | 36 | 37 | LOG = logging.getLogger(__name__) 38 | 39 | 40 | class Dependency(base.Base): 41 | """Dependency Command Class.""" 42 | 43 | def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 44 | """Execute the actions necessary to perform a `molecule dependency`. 45 | 46 | Args: 47 | action_args: Arguments for this command. Unused. 48 | """ 49 | if self._config.dependency: 50 | self._config.dependency.execute() 51 | 52 | 53 | @base.click_command_ex() 54 | @click.pass_context 55 | @base.click_command_options 56 | def dependency( 57 | ctx: click.Context, 58 | /, 59 | scenario_name: list[str] | None, 60 | exclude: list[str], 61 | *, 62 | __all: bool, 63 | report: bool, 64 | shared_inventory: bool, 65 | ) -> None: # pragma: no cover 66 | """Manage the role's dependencies. 67 | 68 | \f 69 | Args: 70 | ctx: Click context object holding commandline arguments. 71 | scenario_name: Name of the scenario to target. 72 | exclude: Name of the scenarios to avoid targeting. 73 | __all: Whether molecule should target scenario_name or all scenarios. 74 | report: Whether to show an after-run summary report. 75 | shared_inventory: Whether the inventory should be shared between scenarios. 76 | """ # noqa: D301 77 | args: MoleculeArgs = ctx.obj.get("args") 78 | subcommand = base._get_subcommand(__name__) # noqa: SLF001 79 | command_args: CommandArgs = { 80 | "subcommand": subcommand, 81 | "report": report, 82 | "shared_inventory": shared_inventory, 83 | } 84 | 85 | if __all: 86 | scenario_name = None 87 | 88 | base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude) 89 | -------------------------------------------------------------------------------- /src/molecule/command/drivers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Create Command Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | import click 27 | 28 | from molecule import api 29 | from molecule.command import base 30 | from molecule.console import console 31 | 32 | 33 | LOG = logging.getLogger(__name__) 34 | 35 | 36 | @base.click_command_ex() 37 | @click.pass_context 38 | @click.option( 39 | "--format", 40 | "-f", 41 | type=click.Choice(["simple", "plain"]), 42 | default="simple", 43 | help="Change output format. (simple)", 44 | ) 45 | def drivers( 46 | ctx: click.Context, # noqa: ARG001 47 | format: str, # noqa: A002 48 | ) -> None: # pragma: no cover 49 | """List drivers. 50 | 51 | \f 52 | Args: 53 | ctx: Click context object holding commandline arguments. 54 | format: Output format to use. 55 | """ # noqa: D301 56 | drivers = [] # pylint: disable=redefined-outer-name 57 | for driver in api.drivers().values(): 58 | description = str(driver) 59 | if format == "plain": 60 | description = f"{driver!s:16s}[logging.level.notset] {driver.title} Version {driver.version} from {driver.module} python module.)[/logging.level.notset]" 61 | drivers.append([driver, description]) 62 | console.print(description) 63 | -------------------------------------------------------------------------------- /src/molecule/command/init/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /src/molecule/command/init/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Base class used by init command.""" 21 | 22 | from __future__ import annotations 23 | 24 | import abc 25 | import logging 26 | 27 | from pathlib import Path 28 | 29 | from molecule.exceptions import MoleculeError 30 | 31 | 32 | LOG = logging.getLogger(__name__) 33 | 34 | 35 | class Base(abc.ABC): 36 | """Init Command Base Class.""" 37 | 38 | @abc.abstractmethod 39 | def execute(self, action_args: list[str] | None = None) -> None: 40 | """Abstract method to execute the command. 41 | 42 | Args: 43 | action_args: An optional list of arguments to pass to the action. 44 | """ 45 | 46 | def _validate_template_dir(self, template_dir: str) -> None: 47 | if not Path(template_dir).is_dir(): 48 | msg = f"The specified template directory ({template_dir!s}) does not exist" 49 | raise MoleculeError(message=msg) 50 | -------------------------------------------------------------------------------- /src/molecule/command/init/init.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Base class used by init command.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | from molecule.command import base 27 | from molecule.command.init import scenario 28 | 29 | 30 | LOG = logging.getLogger(__name__) 31 | 32 | 33 | @base.click_group_ex() 34 | def init() -> None: # pragma: no cover 35 | """Initialize a new scenario.""" 36 | 37 | 38 | init.add_command(scenario.scenario) 39 | -------------------------------------------------------------------------------- /src/molecule/command/reset.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Lint Command Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | from typing import TYPE_CHECKING 27 | 28 | import click 29 | 30 | from molecule.api import drivers 31 | from molecule.command import base 32 | 33 | 34 | if TYPE_CHECKING: 35 | from molecule.types import CommandArgs, MoleculeArgs 36 | 37 | 38 | LOG = logging.getLogger(__name__) 39 | 40 | 41 | @base.click_command_ex() 42 | @click.pass_context 43 | @click.option( 44 | "--scenario-name", 45 | "-s", 46 | default=base.MOLECULE_DEFAULT_SCENARIO_NAME, 47 | help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})", 48 | ) 49 | def reset( 50 | ctx: click.Context, 51 | scenario_name: str, 52 | ) -> None: # pragma: no cover 53 | """Reset molecule temporary folders. 54 | 55 | \f 56 | Args: 57 | ctx: Click context object holding commandline arguments. 58 | scenario_name: Name of the scenario to target. 59 | """ # noqa: D301 60 | args: MoleculeArgs = ctx.obj.get("args") 61 | subcommand = base._get_subcommand(__name__) # noqa: SLF001 62 | command_args: CommandArgs = {"subcommand": subcommand} 63 | 64 | base.execute_cmdline_scenarios([scenario_name], args, command_args) 65 | for driver in drivers().values(): 66 | driver.reset() 67 | -------------------------------------------------------------------------------- /src/molecule/command/syntax.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Syntax Command Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | from typing import TYPE_CHECKING 27 | 28 | import click 29 | 30 | from molecule.command import base 31 | 32 | 33 | if TYPE_CHECKING: 34 | from molecule.types import CommandArgs, MoleculeArgs 35 | 36 | 37 | LOG = logging.getLogger(__name__) 38 | 39 | 40 | class Syntax(base.Base): 41 | """Syntax Command Class.""" 42 | 43 | def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 44 | """Execute the actions necessary to perform a `molecule syntax`. 45 | 46 | Args: 47 | action_args: Arguments for this command. Unused. 48 | """ 49 | if self._config.provisioner: 50 | self._config.provisioner.syntax() 51 | 52 | 53 | @base.click_command_ex() 54 | @click.pass_context 55 | @base.click_command_options 56 | def syntax( 57 | ctx: click.Context, 58 | /, 59 | scenario_name: list[str] | None, 60 | exclude: list[str], 61 | *, 62 | __all: bool, 63 | report: bool, 64 | shared_inventory: bool, 65 | ) -> None: # pragma: no cover 66 | """Use the provisioner to syntax check the role. 67 | 68 | \f 69 | Args: 70 | ctx: Click context object holding commandline arguments. 71 | scenario_name: Name of the scenario to target. 72 | exclude: Name of the scenarios to avoid targeting. 73 | __all: Whether molecule should target scenario_name or all scenarios. 74 | report: Whether to show an after-run summary report. 75 | shared_inventory: Whether the inventory should be shared between scenarios. 76 | """ # noqa: D301 77 | args: MoleculeArgs = ctx.obj.get("args") 78 | subcommand = base._get_subcommand(__name__) # noqa: SLF001 79 | command_args: CommandArgs = { 80 | "subcommand": subcommand, 81 | "report": report, 82 | "shared_inventory": shared_inventory, 83 | } 84 | 85 | if __all: 86 | scenario_name = None 87 | 88 | base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude) 89 | -------------------------------------------------------------------------------- /src/molecule/command/verify.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Verify Command Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import logging 25 | 26 | from typing import TYPE_CHECKING 27 | 28 | import click 29 | 30 | from molecule.command import base 31 | 32 | 33 | if TYPE_CHECKING: 34 | from molecule.types import CommandArgs, MoleculeArgs 35 | 36 | 37 | LOG = logging.getLogger(__name__) 38 | 39 | 40 | class Verify(base.Base): 41 | """Verify Command Class.""" 42 | 43 | def execute(self, action_args: list[str] | None = None) -> None: 44 | """Execute the actions necessary to perform a `molecule verify`. 45 | 46 | Args: 47 | action_args: Arguments for this command. 48 | """ 49 | self._config.verifier.execute(action_args) 50 | 51 | 52 | @base.click_command_ex() 53 | @click.pass_context 54 | @base.click_command_options 55 | def verify( 56 | ctx: click.Context, 57 | /, 58 | scenario_name: list[str] | None, 59 | exclude: list[str], 60 | *, 61 | __all: bool, 62 | report: bool, 63 | shared_inventory: bool, 64 | ) -> None: # pragma: no cover 65 | """Run automated tests against instances. 66 | 67 | \f 68 | Args: 69 | ctx: Click context object holding commandline arguments. 70 | scenario_name: Name of the scenario to target. 71 | exclude: Name of the scenarios to avoid targeting. 72 | __all: Whether molecule should target scenario_name or all scenarios. 73 | report: Whether to show an after-run summary report. 74 | shared_inventory: Whether the inventory should be shared between scenarios. 75 | """ # noqa: D301 76 | args: MoleculeArgs = ctx.obj.get("args") 77 | subcommand = base._get_subcommand(__name__) # noqa: SLF001 78 | command_args: CommandArgs = { 79 | "subcommand": subcommand, 80 | "report": report, 81 | "shared_inventory": shared_inventory, 82 | } 83 | 84 | if __all: 85 | scenario_name = None 86 | 87 | base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude) 88 | -------------------------------------------------------------------------------- /src/molecule/console.py: -------------------------------------------------------------------------------- 1 | """Console and terminal utilities.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | 8 | from typing import Any 9 | 10 | from enrich.console import Console 11 | from rich.style import Style 12 | from rich.theme import Theme 13 | 14 | 15 | theme = Theme( 16 | { 17 | "info": "dim cyan", 18 | "warning": "magenta", 19 | "danger": "bold red", 20 | "scenario": "green", 21 | "action": "green", 22 | "section_title": "bold cyan", 23 | "logging.level.notset": Style(dim=True), 24 | "logging.level.debug": Style(color="white", dim=True), 25 | "logging.level.info": Style(color="blue"), 26 | "logging.level.warning": Style(color="red"), 27 | "logging.level.error": Style(color="red", bold=True), 28 | "logging.level.critical": Style(color="red", bold=True), 29 | "logging.level.success": Style(color="green", bold=True), 30 | }, 31 | ) 32 | 33 | 34 | # Based on Ansible implementation 35 | def to_bool(a: object) -> bool: 36 | """Return a bool for the arg. 37 | 38 | Args: 39 | a: A value to coerce to bool. 40 | 41 | Returns: 42 | A bool representation of a. 43 | """ 44 | if a is None or isinstance(a, bool): 45 | return bool(a) 46 | if isinstance(a, str): 47 | a = a.lower() 48 | return a in ("yes", "on", "1", "true", 1) 49 | 50 | 51 | def should_do_markup() -> bool: 52 | """Decide about use of ANSI colors. 53 | 54 | Returns: 55 | Whether the output should be colored. 56 | """ 57 | py_colors = None 58 | 59 | # https://xkcd.com/927/ 60 | for v in ["PY_COLORS", "CLICOLOR", "FORCE_COLOR", "ANSIBLE_FORCE_COLOR"]: 61 | value = os.environ.get(v, None) 62 | if value is not None: 63 | py_colors = to_bool(value) 64 | break 65 | 66 | # If deliberately disabled colors 67 | if os.environ.get("NO_COLOR", None): 68 | return False 69 | 70 | # User configuration requested colors 71 | if py_colors is not None: 72 | return to_bool(py_colors) 73 | 74 | term = os.environ.get("TERM", "") 75 | if "xterm" in term: 76 | return True 77 | 78 | if term == "dumb": 79 | return False 80 | 81 | # Use tty detection logic as last resort because there are numerous 82 | # factors that can make isatty return a misleading value, including: 83 | # - stdin.isatty() is the only one returning true, even on a real terminal 84 | # - stderr returning false if user user uses a error stream coloring solution 85 | return sys.stdout.isatty() 86 | 87 | 88 | # Define ANSIBLE_FORCE_COLOR if markup is enabled and another value is not 89 | # already given. This assures that Ansible subprocesses are still colored, 90 | # even if they do not run with a real TTY. 91 | if should_do_markup(): 92 | os.environ["ANSIBLE_FORCE_COLOR"] = os.environ.get("ANSIBLE_FORCE_COLOR", "1") 93 | 94 | console_options: dict[str, Any] = {"emoji": False, "theme": theme, "soft_wrap": True} 95 | 96 | console = Console( 97 | force_terminal=should_do_markup(), 98 | theme=theme, 99 | record=True, 100 | redirect=True, 101 | ) 102 | console_options_stderr = console_options.copy() 103 | console_options_stderr["stderr"] = True 104 | console_stderr: Console = Console(**console_options_stderr) 105 | -------------------------------------------------------------------------------- /src/molecule/constants.py: -------------------------------------------------------------------------------- 1 | """Constants used by molecule.""" 2 | 3 | from __future__ import annotations 4 | 5 | 6 | RC_SUCCESS = 0 7 | RC_TIMEOUT = 3 8 | RC_SETUP_ERROR = 4 # Broken setup, like missing Ansible 9 | RC_UNKNOWN_ERROR = 5 # Unexpected errors for which we do not have more specific codes, yet 10 | 11 | 12 | MOLECULE_HEADER = "# Molecule managed" 13 | -------------------------------------------------------------------------------- /src/molecule/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Module containing molecule data files.""" 2 | -------------------------------------------------------------------------------- /src/molecule/data/driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "$defs": { 3 | "MoleculeDriverModel": { 4 | "properties": { 5 | "name": { 6 | "enum": ["delegated", "default"], 7 | "title": "Name", 8 | "type": "string" 9 | }, 10 | "options": { 11 | "$ref": "#/$defs/MoleculeDriverOptionsModel" 12 | }, 13 | "safe_files": { 14 | "items": { 15 | "type": "string" 16 | }, 17 | "title": "SafeFiles", 18 | "type": "array" 19 | }, 20 | "ssh_connection_options": { 21 | "items": { 22 | "type": "string" 23 | }, 24 | "title": "SshConnectionOptions", 25 | "type": "array" 26 | } 27 | }, 28 | "title": "MoleculeDriverModel", 29 | "type": "object" 30 | }, 31 | "MoleculeDriverOptionsModel": { 32 | "additionalProperties": false, 33 | "properties": { 34 | "ansible_connection_options": { 35 | "additionalProperties": { 36 | "type": "string" 37 | }, 38 | "title": "Ansible Connection Options", 39 | "type": "object" 40 | }, 41 | "login_cmd_template": { 42 | "title": "Login Cmd Template", 43 | "type": "string" 44 | }, 45 | "managed": { 46 | "title": "Managed", 47 | "type": "boolean" 48 | } 49 | }, 50 | "title": "MoleculeDriverOptionsModel", 51 | "type": "object" 52 | }, 53 | "MoleculePlatformModel": { 54 | "additionalProperties": true, 55 | "properties": { 56 | "box": { 57 | "title": "Box", 58 | "type": "string" 59 | }, 60 | "children": { 61 | "items": { 62 | "type": "string" 63 | }, 64 | "type": "array" 65 | }, 66 | "groups": { 67 | "items": { 68 | "type": "string" 69 | }, 70 | "title": "Groups", 71 | "type": "array" 72 | }, 73 | "hostname": { 74 | "title": "Hostname", 75 | "type": ["string", "boolean"] 76 | }, 77 | "name": { 78 | "title": "Name", 79 | "type": "string" 80 | } 81 | }, 82 | "required": ["name"], 83 | "title": "MoleculePlatformModel", 84 | "type": "object" 85 | } 86 | }, 87 | "$id": "https://raw.githubusercontent.com/ansible-community/molecule/main/src/molecule/driver/driver.json", 88 | "$schema": "http://json-schema.org/draft-07/schema", 89 | "examples": ["molecule/*/molecule.yml"], 90 | "properties": { 91 | "driver": { 92 | "$ref": "#/$defs/MoleculeDriverModel" 93 | }, 94 | "platforms": { 95 | "items": { 96 | "$ref": "#/$defs/MoleculePlatformModel" 97 | }, 98 | "title": "Platforms", 99 | "type": "array" 100 | } 101 | }, 102 | "required": ["driver"], 103 | "title": "Molecule Delegated Driver Schema", 104 | "type": "object" 105 | } 106 | -------------------------------------------------------------------------------- /src/molecule/data/init-scenario.yml: -------------------------------------------------------------------------------- 1 | - name: Create a new molecule scenario 2 | hosts: localhost 3 | gather_facts: false 4 | vars_prompt: 5 | - name: scenario_name 6 | prompt: What is the scenario name? 7 | vars: 8 | dest: "{{ ('molecule/' + (scenario_name | default('default'))) | realpath }}" 9 | tasks: 10 | - name: Check if destination folder exists 11 | ansible.builtin.file: 12 | path: "{{ dest }}" 13 | state: directory 14 | mode: "0700" 15 | - name: Check if destination folder is empty 16 | ansible.builtin.find: 17 | paths: "{{ dest }}" 18 | register: dest_content 19 | - name: Fail if destination folder is not empty 20 | when: dest_content.matched > 0 21 | ansible.builtin.fail: 22 | msg: Refused to expand templates as destination folder '{{ dest }}' as it already has content in it. 23 | - name: Expand templates 24 | vars: 25 | dest_file: "{{ dest }}/{{ item | basename | regex_replace('\\.j2$', '') }}" 26 | ansible.builtin.template: 27 | src: "{{ item }}" 28 | dest: "{{ dest_file }}" 29 | mode: "0644" 30 | with_fileglob: 31 | - templates/scenario/*.j2 32 | loop_control: 33 | label: "{{ dest_file | relpath }}" 34 | when: 35 | - ((driver_name in ['containers', 'docker', 'podman']) and 36 | (item | regex_search('create|destroy') is none) or 37 | (driver_name not in ['containers', 'docker', 'podman'])) 38 | -------------------------------------------------------------------------------- /src/molecule/data/templates/scenario/converge.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Replace this task with one that validates your content 7 | ansible.builtin.debug: 8 | msg: "This is the effective test" 9 | -------------------------------------------------------------------------------- /src/molecule/data/templates/scenario/create.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | {% raw -%} 3 | - name: Create 4 | hosts: localhost 5 | connection: local 6 | gather_facts: false 7 | # no_log: "{{ molecule_no_log }}" 8 | tasks: 9 | # TODO: Developer must implement and populate 'server' variable 10 | 11 | - name: Create instance config 12 | when: server.changed | default(false) | bool # noqa no-handler 13 | block: 14 | - name: Populate instance config dict # noqa jinja 15 | ansible.builtin.set_fact: 16 | instance_conf_dict: {} 17 | # instance': "{{ }}", 18 | # address': "{{ }}", 19 | # user': "{{ }}", 20 | # port': "{{ }}", 21 | # 'identity_file': "{{ }}", } 22 | with_items: "{{ server.results }}" 23 | register: instance_config_dict 24 | 25 | - name: Convert instance config dict to a list 26 | ansible.builtin.set_fact: 27 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 28 | 29 | - name: Dump instance config 30 | ansible.builtin.copy: 31 | content: | 32 | # Molecule managed 33 | 34 | {{ instance_conf | to_json | from_json | to_yaml }} 35 | dest: "{{ molecule_instance_config }}" 36 | mode: "0600" 37 | {%- endraw %} 38 | -------------------------------------------------------------------------------- /src/molecule/data/templates/scenario/destroy.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | {% raw -%} 3 | - name: Destroy 4 | hosts: localhost 5 | connection: local 6 | gather_facts: false 7 | # no_log: "{{ molecule_no_log }}" 8 | tasks: 9 | # Developer must implement. 10 | 11 | # Mandatory configuration for Molecule to function. 12 | 13 | - name: Populate instance config 14 | ansible.builtin.set_fact: 15 | instance_conf: {} 16 | 17 | - name: Dump instance config 18 | ansible.builtin.copy: 19 | content: | 20 | # Molecule managed 21 | 22 | {{ instance_conf | to_json | from_json | to_yaml }} 23 | dest: "{{ molecule_instance_config }}" 24 | mode: "0600" 25 | when: server.changed | default(false) | bool # noqa no-handler 26 | {%- endraw %} 27 | -------------------------------------------------------------------------------- /src/molecule/data/templates/scenario/molecule.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: {{ driver_name }} 4 | platforms: 5 | - name: instance 6 | {% if driver_name in ['containers', 'docker', 'podman'] %} 7 | image: quay.io/centos/centos:stream8 8 | pre_build_image: true 9 | {% else %} 10 | # you might want to add your own variables here based on what provisioning 11 | # you are doing like: 12 | # image: quay.io/centos/centos:stream8 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /src/molecule/dependency/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /src/molecule/dependency/ansible_galaxy/collections.py: -------------------------------------------------------------------------------- 1 | """Ansible Galaxy dependencies for lists of collections.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING, cast 9 | 10 | from molecule import util 11 | from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase 12 | 13 | 14 | if TYPE_CHECKING: 15 | from collections.abc import MutableMapping 16 | 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | 21 | class Collections(AnsibleGalaxyBase): 22 | """Collection-specific Ansible Galaxy dependency handling. 23 | 24 | Attributes: 25 | FILTER_OPTS: Keys to remove from the dictionary returned by options(). 26 | COMMANDS: Arguments to send to ansible-galaxy to install the appropriate type of content. 27 | """ 28 | 29 | FILTER_OPTS = ("role-file",) 30 | COMMANDS = ("collection", "install") 31 | 32 | @property 33 | def default_options(self) -> MutableMapping[str, str | bool]: 34 | """Default options for this dependency. 35 | 36 | Returns: 37 | Default options for this dependency. 38 | """ 39 | general = super().default_options 40 | specific = util.merge_dicts( 41 | general, 42 | { 43 | "requirements-file": str( 44 | Path( 45 | self._config.scenario.directory, 46 | "collections.yml", 47 | ), 48 | ), 49 | }, 50 | ) 51 | 52 | return specific # noqa: RET504 53 | 54 | @property 55 | def requirements_file(self) -> str: 56 | """Path to requirements file. 57 | 58 | Returns: 59 | Path to the requirements file for this dependency. 60 | """ 61 | return cast("str", self.options["requirements-file"]) 62 | -------------------------------------------------------------------------------- /src/molecule/dependency/ansible_galaxy/roles.py: -------------------------------------------------------------------------------- 1 | """Ansible Galaxy dependencies for lists of roles.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING, cast 9 | 10 | from molecule import util 11 | from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase 12 | 13 | 14 | if TYPE_CHECKING: 15 | from collections.abc import MutableMapping 16 | 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | 21 | class Roles(AnsibleGalaxyBase): 22 | """Role-specific Ansible Galaxy dependency handling. 23 | 24 | Attributes: 25 | FILTER_OPTS: Keys to remove from the dictionary returned by options(). 26 | COMMANDS: Arguments to send to ansible-galaxy to install the appropriate type of content. 27 | """ 28 | 29 | FILTER_OPTS = ("requirements-file",) 30 | COMMANDS = ("install",) 31 | 32 | @property 33 | def default_options(self) -> MutableMapping[str, str | bool]: 34 | """Default options for this dependency. 35 | 36 | Returns: 37 | Default options for this dependency. 38 | """ 39 | general = super().default_options 40 | specific = util.merge_dicts( 41 | general, 42 | { 43 | "role-file": str( 44 | Path( 45 | self._config.scenario.directory, 46 | "requirements.yml", 47 | ), 48 | ), 49 | }, 50 | ) 51 | return specific # noqa: RET504 52 | 53 | @property 54 | def requirements_file(self) -> str: 55 | """Path to requirements file. 56 | 57 | Returns: 58 | Path to the requirements file for this dependency. 59 | """ 60 | return cast("str", self.options["role-file"]) 61 | -------------------------------------------------------------------------------- /src/molecule/driver/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /src/molecule/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom Molecule exceptions.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from typing import TYPE_CHECKING 8 | 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Sequence 12 | from warnings import WarningMessage 13 | 14 | 15 | LOG = logging.getLogger(__name__) 16 | 17 | 18 | class MoleculeError(Exception): 19 | """Generic Molecule error.""" 20 | 21 | def __init__( 22 | self, 23 | message: str = "", 24 | code: int = 1, 25 | warns: Sequence[WarningMessage] = (), 26 | ) -> None: 27 | """Custom exception to handle scenario run failures. 28 | 29 | Args: 30 | message: The message to display about the problem. 31 | code: Exit code to use when exiting. 32 | warns: Warnings about the problem to issue. 33 | """ 34 | super().__init__() 35 | 36 | if message: 37 | LOG.critical(message, extra={"highlighter": False}) 38 | 39 | for warn in warns: 40 | LOG.warning(warn.__dict__["message"].args[0]) 41 | 42 | self.code = code 43 | 44 | 45 | class ScenarioFailureError(MoleculeError): 46 | """Details about a scenario that failed.""" 47 | -------------------------------------------------------------------------------- /src/molecule/model/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /src/molecule/provisioner/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /src/molecule/provisioner/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Provisioner Base Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | import abc 25 | 26 | from typing import TYPE_CHECKING 27 | 28 | 29 | if TYPE_CHECKING: 30 | from molecule.config import Config 31 | 32 | 33 | class Base(abc.ABC): 34 | """Provisioner Base Class.""" 35 | 36 | def __init__(self, config: Config) -> None: 37 | """Initialize code for all :ref:`Provisioner` classes. 38 | 39 | Args: 40 | config: An instance of a Molecule config. 41 | """ 42 | self._config = config 43 | 44 | @property 45 | @abc.abstractmethod 46 | def default_options(self) -> dict[str, str | bool]: # pragma: no cover 47 | """Get default CLI arguments provided to ``cmd``. 48 | 49 | Returns: 50 | The default CLI arguments. 51 | """ 52 | 53 | @property 54 | @abc.abstractmethod 55 | def default_env(self) -> dict[str, str]: # pragma: no cover 56 | """Get default env variables provided to ``cmd``. 57 | 58 | Returns: 59 | The default env variables. 60 | """ 61 | 62 | @property 63 | @abc.abstractmethod 64 | def name(self) -> str: # pragma: no cover 65 | """Name of the provisioner. 66 | 67 | Returns: 68 | The provisioner's name. 69 | """ 70 | -------------------------------------------------------------------------------- /src/molecule/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/src/molecule/py.typed -------------------------------------------------------------------------------- /src/molecule/status.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Status Module.""" 21 | 22 | from __future__ import annotations 23 | 24 | from typing import NamedTuple 25 | 26 | 27 | class Status(NamedTuple): 28 | """Scenario status information. 29 | 30 | Attributes: 31 | instance_name: Name of the instance. 32 | driver_name: Name of the driver being used. 33 | provisioner_name: Name of the provisioner being used. 34 | scenario_name: Name of the scenario being run. 35 | created: Has the scenario been created. 36 | converged: Has the scenario converged. 37 | """ 38 | 39 | instance_name: str 40 | driver_name: str 41 | provisioner_name: str 42 | scenario_name: str 43 | created: str 44 | converged: str 45 | -------------------------------------------------------------------------------- /src/molecule/verifier/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Replace this task with one that validates your content 7 | ansible.builtin.debug: 8 | msg: "This is the effective test" 9 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | # TODO: Developer must implement and populate 'server' variable 9 | 10 | - name: Create instance config 11 | when: server.changed | default(false) | bool # noqa no-handler 12 | block: 13 | - name: Populate instance config dict # noqa jinja 14 | ansible.builtin.set_fact: 15 | instance_conf_dict: 16 | { 17 | "instance": "{{ }}", 18 | "address": "{{ }}", 19 | "user": "{{ }}", 20 | "port": "{{ }}", 21 | "identity_file": "{{ }}", 22 | } 23 | with_items: "{{ server.results }}" 24 | register: instance_config_dict 25 | 26 | - name: Convert instance config dict to a list 27 | ansible.builtin.set_fact: 28 | instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" 29 | 30 | - name: Dump instance config 31 | ansible.builtin.copy: 32 | content: | 33 | # Molecule managed 34 | 35 | {{ instance_conf | to_json | from_json | to_yaml }} 36 | dest: "{{ molecule_instance_config }}" 37 | mode: "0600" 38 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | no_log: "{{ molecule_no_log }}" 7 | tasks: 8 | # Developer must implement. 9 | 10 | # Mandatory configuration for Molecule to function. 11 | 12 | - name: Populate instance config 13 | ansible.builtin.set_fact: 14 | instance_conf: {} 15 | 16 | - name: Dump instance config 17 | ansible.builtin.copy: 18 | content: | 19 | # Molecule managed 20 | 21 | {{ instance_conf | to_json | from_json | to_yaml }} 22 | dest: "{{ molecule_instance_config }}" 23 | mode: "0600" 24 | when: server.changed | default(false) | bool # noqa no-handler 25 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platforms: 3 | - name: instance 4 | # you might want to add your own variables here based on what provisioning 5 | # you are doing like: 6 | # image: quay.io/centos/centos:stream8 7 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/converge.yml: -------------------------------------------------------------------------------- 1 | - name: Fail if molecule group is missing 2 | hosts: localhost 3 | tasks: 4 | - name: Print some info 5 | ansible.builtin.debug: 6 | msg: "{{ groups }}" 7 | 8 | - name: Assert group existence 9 | ansible.builtin.assert: 10 | that: "'molecule' in groups" 11 | fail_msg: | 12 | molecule group was not found inside inventory groups: {{ groups }} 13 | 14 | - name: Converge 15 | hosts: molecule 16 | # We disable gather facts because it would fail due to our container not 17 | # having python installed. This will not prevent use from running 'raw' 18 | # commands. Most molecule users are expected to use containers that already 19 | # have python installed in order to avoid notable delays installing it. 20 | gather_facts: false 21 | tasks: 22 | - name: Check uname 23 | ansible.builtin.raw: uname -a 24 | register: result 25 | changed_when: false 26 | 27 | - name: Print some info 28 | ansible.builtin.assert: 29 | that: result.stdout is ansible.builtin.search("^Linux") 30 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/create.yml: -------------------------------------------------------------------------------- 1 | - name: Create 2 | hosts: localhost 3 | gather_facts: false 4 | vars: 5 | molecule_inventory: 6 | all: 7 | hosts: {} 8 | molecule: {} 9 | tasks: 10 | - name: Create a container 11 | community.docker.docker_container: 12 | name: "{{ item.name }}" 13 | image: "{{ item.image }}" 14 | state: started 15 | command: sleep 1d 16 | log_driver: json-file 17 | register: result 18 | loop: "{{ molecule_yml.platforms }}" 19 | 20 | - name: Print some info 21 | ansible.builtin.debug: 22 | msg: "{{ result.results }}" 23 | 24 | - name: Fail if container is not running 25 | when: > 26 | item.container.State.ExitCode != 0 or 27 | not item.container.State.Running 28 | ansible.builtin.include_tasks: 29 | file: tasks/create-fail.yml 30 | loop: "{{ result.results }}" 31 | loop_control: 32 | label: "{{ item.container.Name }}" 33 | 34 | - name: Add container to molecule_inventory 35 | vars: 36 | inventory_partial_yaml: | 37 | all: 38 | children: 39 | molecule: 40 | hosts: 41 | "{{ item.name }}": 42 | ansible_connection: community.docker.docker 43 | ansible.builtin.set_fact: 44 | molecule_inventory: > 45 | {{ molecule_inventory | combine(inventory_partial_yaml | from_yaml, recursive=true) }} 46 | loop: "{{ molecule_yml.platforms }}" 47 | loop_control: 48 | label: "{{ item.name }}" 49 | 50 | - name: Dump molecule_inventory 51 | ansible.builtin.copy: 52 | content: | 53 | {{ molecule_inventory | to_yaml }} 54 | dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 55 | mode: "0600" 56 | 57 | - name: Force inventory refresh 58 | ansible.builtin.meta: refresh_inventory 59 | 60 | - name: Fail if molecule group is missing 61 | ansible.builtin.assert: 62 | that: "'molecule' in groups" 63 | fail_msg: | 64 | molecule group was not found inside inventory groups: {{ groups }} 65 | run_once: true # noqa: run-once[task] 66 | 67 | # we want to avoid errors like "Failed to create temporary directory" 68 | - name: Validate that inventory was refreshed 69 | hosts: molecule 70 | gather_facts: false 71 | tasks: 72 | - name: Check uname 73 | ansible.builtin.raw: uname -a 74 | register: result 75 | changed_when: false 76 | 77 | - name: Display uname info 78 | ansible.builtin.debug: 79 | msg: "{{ result.stdout }}" 80 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/destroy.yml: -------------------------------------------------------------------------------- 1 | - name: Destroy molecule containers 2 | hosts: molecule 3 | gather_facts: false 4 | tasks: 5 | - name: Stop and remove container 6 | delegate_to: localhost 7 | community.docker.docker_container: 8 | name: "{{ inventory_hostname }}" 9 | state: absent 10 | auto_remove: true 11 | 12 | - name: Remove dynamic molecule inventory 13 | hosts: localhost 14 | gather_facts: false 15 | tasks: 16 | - name: Remove dynamic inventory file 17 | ansible.builtin.file: 18 | path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 19 | state: absent 20 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/molecule.yml: -------------------------------------------------------------------------------- 1 | dependency: 2 | name: galaxy 3 | options: 4 | requirements-file: requirements.yml 5 | platforms: 6 | - name: molecule-ubuntu 7 | image: ubuntu:18.04 8 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - name: community.docker 3 | version: ">=3.10.4" 4 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/docker/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | - name: Retrieve container log 2 | ansible.builtin.command: 3 | cmd: >- 4 | {% raw %} 5 | docker logs 6 | {% endraw %} 7 | {{ item.stdout_lines[0] }} 8 | changed_when: false 9 | register: logfile_cmd 10 | 11 | - name: Display container log 12 | ansible.builtin.fail: 13 | msg: "{{ logfile_cmd.stderr }}" 14 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/kubevirt/converge.yml: -------------------------------------------------------------------------------- 1 | - name: Fail if molecule group is missing 2 | hosts: localhost 3 | tasks: 4 | - name: Print some info 5 | ansible.builtin.debug: 6 | msg: "{{ groups }}" 7 | 8 | - name: Assert group existence 9 | ansible.builtin.assert: 10 | that: "'molecule' in groups" 11 | fail_msg: | 12 | molecule group was not found inside inventory groups: {{ groups }} 13 | 14 | - name: Converge 15 | hosts: molecule 16 | # We disable gather facts because it would fail due to our container not 17 | # having python installed. This will not prevent use from running 'raw' 18 | # commands. Most molecule users are expected to use containers that already 19 | # have python installed in order to avoid notable delays installing it. 20 | gather_facts: false 21 | tasks: 22 | - name: Check uname 23 | ansible.builtin.raw: uname -a 24 | register: result 25 | changed_when: false 26 | 27 | - name: Print some info 28 | ansible.builtin.assert: 29 | that: result.stdout is ansible.builtin.match("Linux") 30 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/kubevirt/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | tasks: 7 | - name: Delete VM Instance in KubeVirt 8 | kubernetes.core.k8s: 9 | state: absent 10 | kind: VirtualMachine 11 | name: "{{ vm.name }}" 12 | namespace: "{{ vm.namespace }}" 13 | loop: "{{ molecule_yml.platforms }}" 14 | loop_control: 15 | loop_var: vm 16 | 17 | - name: Delete NodePort Service in KubeVirt 18 | kubernetes.core.k8s: 19 | state: absent 20 | kind: Service 21 | name: "{{ vm.name }}" 22 | namespace: "{{ vm.namespace }}" 23 | loop: "{{ molecule_yml.platforms }}" 24 | loop_control: 25 | loop_var: vm 26 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/kubevirt/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | role-file: requirements.yml 7 | platforms: 8 | - name: rhel9 9 | image: registry.redhat.io/rhel9/rhel-guest-image 10 | namespace: 11 | ssh_service: 12 | type: NodePort 13 | ansible_user: cloud-user 14 | memory: 1Gi 15 | - name: rhel8 16 | image: registry.redhat.io/rhel8/rhel-guest-image 17 | namespace: 18 | ssh_service: 19 | type: NodePort 20 | ansible_user: cloud-user 21 | memory: 1Gi 22 | provisioner: 23 | name: ansible 24 | config_options: 25 | defaults: 26 | interpreter_python: auto_silent 27 | callback_whitelist: profile_tasks, timer, yaml 28 | ssh_connection: 29 | pipelining: false 30 | log: true 31 | verifier: 32 | name: ansible 33 | scenario: 34 | test_sequence: 35 | - dependency 36 | - destroy 37 | - syntax 38 | - create 39 | - converge 40 | - idempotence 41 | - side_effect 42 | - verify 43 | - destroy 44 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/kubevirt/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: kubernetes.core 4 | - name: community.crypto 5 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/kubevirt/tasks/create_vm_dictionary.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create VM dictionary 3 | vars: 4 | # This variable block is setting the `ssh_service_address` variable. 5 | # It first checks if the service type of the SSH service is 'NodePort'. 6 | # If it is, it retrieves the 'nodePort' from the services results. 7 | ssh_service_address: >- 8 | {%- set svc_type = vm.ssh_service.type | default(None) -%} 9 | {%- if svc_type == 'NodePort' -%} 10 | {{ (node_port_services.results | selectattr('vm.name', '==', vm.name) | first)['resources'][0]['spec']['ports'][0]['nodePort'] }} 11 | {%- endif -%} 12 | ansible.builtin.set_fact: 13 | # Here, the task is updating the `molecule_systems` dictionary with new VM information. 14 | # If `molecule_systems` doesn't exist, it is created as an empty dictionary. 15 | # Then it is combined with a new dictionary for the current VM, containing ansible connection details. 16 | molecule_systems: >- 17 | {{ 18 | molecule_systems | default({}) | combine({ 19 | vm.name: { 20 | 'ansible_user': vm.ansible_user, 21 | 'ansible_host': nodeport_host, 22 | 'ansible_ssh_port': ssh_service_address, 23 | 'ansible_ssh_private_key_file': temporary_ssh_key_path 24 | } 25 | }) 26 | }} 27 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/converge.yml: -------------------------------------------------------------------------------- 1 | - name: Fail if molecule group is missing 2 | hosts: localhost 3 | tasks: 4 | - name: Print some info 5 | ansible.builtin.debug: 6 | msg: "{{ groups }}" 7 | 8 | - name: Assert group existence 9 | ansible.builtin.assert: 10 | that: "'molecule' in groups" 11 | fail_msg: | 12 | molecule group was not found inside inventory groups: {{ groups }} 13 | 14 | - name: Converge 15 | hosts: molecule 16 | # We disable gather facts because it would fail due to our container not 17 | # having python installed. This will not prevent use from running 'raw' 18 | # commands. Most molecule users are expected to use containers that already 19 | # have python installed in order to avoid notable delays installing it. 20 | gather_facts: false 21 | tasks: 22 | - name: Check uname 23 | ansible.builtin.raw: uname -a 24 | register: result 25 | changed_when: false 26 | 27 | - name: Print some info 28 | ansible.builtin.assert: 29 | that: result.stdout is ansible.builtin.search("^Linux") 30 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/create.yml: -------------------------------------------------------------------------------- 1 | - name: Create 2 | hosts: localhost 3 | gather_facts: false 4 | vars: 5 | molecule_inventory: 6 | all: 7 | hosts: {} 8 | children: 9 | molecule: 10 | hosts: {} 11 | 12 | tasks: 13 | - name: Create a container 14 | containers.podman.podman_container: 15 | name: "{{ item.name }}" 16 | image: "{{ item.image }}" 17 | privileged: "{{ item.privileged | default(omit) }}" 18 | volumes: "{{ item.volumes | default(omit) }}" 19 | capabilities: "{{ item.capabilities | default(omit) }}" 20 | systemd: "{{ item.systemd | default(omit) }}" 21 | state: started 22 | command: "{{ item.command | default('sleep 1d') }}" 23 | # bash -c "while true; do sleep 10000; done" 24 | log_driver: json-file 25 | register: result 26 | loop: "{{ molecule_yml.platforms }}" 27 | 28 | - name: Print some info 29 | ansible.builtin.debug: 30 | msg: "{{ result.results }}" 31 | 32 | - name: Fail if container is not running 33 | when: > 34 | item.container.State.ExitCode != 0 or 35 | not item.container.State.Running 36 | ansible.builtin.include_tasks: 37 | file: tasks/create-fail.yml 38 | loop: "{{ result.results }}" 39 | loop_control: 40 | label: "{{ item.container.Name }}" 41 | 42 | - name: Add container to molecule_inventory 43 | vars: 44 | inventory_partial_yaml: | 45 | all: 46 | children: 47 | molecule: 48 | hosts: 49 | "{{ item.name }}": 50 | ansible_connection: containers.podman.podman 51 | ansible.builtin.set_fact: 52 | molecule_inventory: > 53 | {{ molecule_inventory | combine(inventory_partial_yaml | from_yaml, recursive=true) }} 54 | loop: "{{ molecule_yml.platforms }}" 55 | loop_control: 56 | label: "{{ item.name }}" 57 | 58 | - name: Dump molecule_inventory 59 | ansible.builtin.copy: 60 | content: | 61 | {{ molecule_inventory | to_yaml }} 62 | dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 63 | mode: "0600" 64 | 65 | - name: Force inventory refresh 66 | ansible.builtin.meta: refresh_inventory 67 | 68 | - name: Fail if molecule group is missing 69 | ansible.builtin.assert: 70 | that: "'molecule' in groups" 71 | fail_msg: | 72 | molecule group was not found inside inventory groups: {{ groups }} 73 | run_once: true # noqa: run-once[task] 74 | 75 | # we want to avoid errors like "Failed to create temporary directory" 76 | - name: Validate that inventory was refreshed 77 | hosts: molecule 78 | gather_facts: false 79 | tasks: 80 | - name: Check uname 81 | ansible.builtin.raw: uname -a 82 | register: result 83 | changed_when: false 84 | 85 | - name: Display uname info 86 | ansible.builtin.debug: 87 | msg: "{{ result.stdout }}" 88 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/destroy.yml: -------------------------------------------------------------------------------- 1 | - name: Destroy molecule containers 2 | hosts: molecule 3 | gather_facts: false 4 | tasks: 5 | - name: Stop and remove container 6 | delegate_to: localhost 7 | containers.podman.podman_container: 8 | name: "{{ inventory_hostname }}" 9 | state: absent 10 | rm: true 11 | - name: Remove potentially stopped container 12 | delegate_to: localhost 13 | ansible.builtin.command: 14 | cmd: podman container rm --ignore {{ inventory_hostname }} 15 | changed_when: false 16 | 17 | - name: Remove dynamic molecule inventory 18 | hosts: localhost 19 | gather_facts: false 20 | tasks: 21 | - name: Remove dynamic inventory file 22 | ansible.builtin.file: 23 | path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 24 | state: absent 25 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/molecule.yml: -------------------------------------------------------------------------------- 1 | dependency: 2 | name: galaxy 3 | options: 4 | requirements-file: requirements.yml 5 | platforms: 6 | - name: molecule-ubuntu 7 | image: ubuntu:18.04 8 | driver: 9 | options: 10 | managed: false 11 | login_cmd_template: "podman exec -ti {instance} bash" 12 | ansible_connection_options: 13 | ansible_connection: podman 14 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - containers.podman 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/podman/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | - name: Retrieve container log 2 | ansible.builtin.command: 3 | cmd: >- 4 | {% raw %} 5 | podman logs 6 | {% endraw %} 7 | {{ item.stdout_lines[0] }} 8 | # podman inspect --format='{{.HostConfig.LogConfig.Path}}' 9 | changed_when: false 10 | register: logfile_cmd 11 | 12 | - name: Display container log 13 | ansible.builtin.fail: 14 | msg: "{{ logfile_cmd.stderr }}" 15 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/smoke/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: localhost 4 | connection: local 5 | tasks: 6 | - name: Assert CWD is the same as playbook_dir 7 | ansible.builtin.assert: 8 | that: 9 | - playbook_dir == pwd 10 | msg: > 11 | Molecule should automatically chdir to scenario directory {{ playbook_dir }} 12 | but we found that it runs from {{ pwd }} instead." 13 | vars: 14 | pwd: "{{ lookup('pipe', 'pwd') }}" 15 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/molecule/smoke/molecule.yml: -------------------------------------------------------------------------------- 1 | platforms: 2 | - name: localhost 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/cleanup/molecule/default/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup external hosts 3 | hosts: localhost 4 | connection: local 5 | tasks: 6 | - name: Debug 7 | ansible.builtin.debug: 8 | msg: "Cleaned up" 9 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/cleanup/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Touch a file 7 | ansible.builtin.command: touch /tmp/cleanup 8 | args: 9 | creates: /tmp/cleanup 10 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/cleanup/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | command: /sbin/init 10 | privileged: true 11 | provisioner: 12 | name: ansible 13 | playbooks: 14 | cleanup: cleanup.yml 15 | verifier: 16 | name: ansible 17 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/cleanup/molecule/default/tests/test_cleanup.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | import testinfra.utils.ansible_runner 8 | 9 | 10 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 11 | os.environ["MOLECULE_INVENTORY_FILE"], 12 | ).get_hosts("all") 13 | 14 | 15 | def test_hosts_file(host): # type: ignore[no-untyped-def] # noqa: ANN201 16 | """Validate host file.""" 17 | f = host.file("/etc/hosts") 18 | 19 | assert f.exists 20 | assert f.user == "root" 21 | assert f.group == "root" 22 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/dependency/molecule/ansible-galaxy/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Install requirements for collection community.molecule 7 | ansible.builtin.pip: 8 | name: molecule 9 | delegate_to: localhost 10 | 11 | - name: Validate that collection was installed 12 | ansible.builtin.debug: 13 | msg: "{{ 'foo' | community.molecule.header }}" 14 | 15 | - name: Test installed role 16 | ansible.builtin.include_role: 17 | name: ssbarnea.ansible_role_helloworld 18 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/dependency/molecule/ansible-galaxy/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | role-file: molecule/ansible-galaxy/requirements.yml 6 | requirements-file: molecule/ansible-galaxy/requirements.yml 7 | driver: 8 | name: default 9 | platforms: 10 | - name: instance 11 | image: ${TEST_BASE_IMAGE} 12 | provisioner: 13 | name: ansible 14 | scenario: 15 | name: ansible-galaxy 16 | verifier: 17 | name: ansible 18 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/dependency/molecule/ansible-galaxy/requirements.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.ansible.com/ansible/latest/galaxy/user_guide.html#installing-roles-and-collections-from-the-same-requirements-yml-file 2 | collections: 3 | - community.molecule 4 | roles: 5 | - name: ssbarnea.ansible_role_helloworld 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/dependency/molecule/shell/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Install requirements for collection community.molecule 7 | ansible.builtin.pip: 8 | name: molecule 9 | delegate_to: localhost 10 | 11 | - name: Validate that collection was installed 12 | ansible.builtin.debug: 13 | msg: "{{ 'foo' | community.molecule.header }}" 14 | 15 | - name: Test installed role 16 | ansible.builtin.include_role: 17 | name: ssbarnea.ansible_role_helloworld 18 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/dependency/molecule/shell/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: shell 4 | command: > 5 | bash -c " 6 | ansible-galaxy collection install community.molecule --force && 7 | ansible-galaxy role install ssbarnea.ansible_role_helloworld 8 | " 9 | driver: 10 | name: default 11 | platforms: 12 | - name: instance 13 | image: ${TEST_BASE_IMAGE} 14 | provisioner: 15 | name: ansible 16 | scenario: 17 | name: shell 18 | verifier: 19 | name: ansible 20 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | author: Molecule Developer 6 | description: Role to test ansible_compat installation of role 7 | namespace: molecule 8 | role_name: delegated_test 9 | license: GPL 10 | min_ansible_version: "2.15" 11 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cleanup 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: all 4 | gather_facts: false 5 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: all 4 | gather_facts: false 5 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: default 4 | platforms: 5 | - name: instance 6 | provisioner: 7 | name: ansible 8 | inventory: 9 | hosts: 10 | all: 11 | hosts: 12 | instance: 13 | ansible_host: localhost 14 | default_sequence: 15 | - converge 16 | test_sequence: 17 | # - prepare 18 | - converge 19 | # - verify 20 | verify_sequence: 21 | - converge 22 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/side_effect.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Side Effect 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated/molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # noqa: role-name 3 | dependencies: [] 4 | 5 | galaxy_info: 6 | author: Molecule Developer 7 | description: Role to test ansible_compat installation of role 8 | namespace: molecule 9 | role_name: delegated-test 10 | license: GPL 11 | min_ansible_version: "2.15" 12 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: false 6 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: all 4 | gather_facts: false 5 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: all 4 | gather_facts: false 5 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_name_check: 1 3 | driver: 4 | name: default 5 | platforms: 6 | - name: instance 7 | provisioner: 8 | name: ansible 9 | inventory: 10 | hosts: 11 | all: 12 | hosts: 13 | instance: 14 | ansible_host: localhost 15 | default_sequence: 16 | - converge 17 | test_sequence: 18 | # - prepare 19 | - converge 20 | # - verify 21 | verify_sequence: 22 | - converge 23 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/group_vars/example/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | host_group_vars_group_vars_linked: true 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/host_vars/extra-host: -------------------------------------------------------------------------------- 1 | --- 2 | host_group_vars_extra_host_linked: true 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/hosts: -------------------------------------------------------------------------------- 1 | extra-host 2 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Molecule test fixture 3 | hosts: instance 4 | gather_facts: false 5 | tasks: 6 | - name: Host vars host_var for host host-group-vars from molecule.yml 7 | ansible.builtin.debug: 8 | var: host_group_vars_host_molecule_yml 9 | 10 | - name: Host vars from host_vars existing directory 11 | ansible.builtin.debug: 12 | var: host_group_vars_host_vars_dir 13 | 14 | - name: Group vars group_var for group example from molecule.yml 15 | ansible.builtin.debug: 16 | var: 17 | - host_group_vars_example_group_one_molecule_yml 18 | - host_group_vars_example_group_two_molecule_yml 19 | 20 | - name: Group vars from group_vars existing directory 21 | ansible.builtin.debug: 22 | var: host_group_vars_group_vars_dir 23 | 24 | - name: Group vars group_var from child group example_1 from molecule.yml 25 | ansible.builtin.debug: 26 | var: host_group_vars_example_1_child_group_molecule_yml 27 | 28 | - name: Variable from extra_host from molecule.yml 29 | ansible.builtin.debug: 30 | var: hostvars['extra_host']['host_group_vars_extra_host_molecule_yml'] 31 | 32 | - name: Molecule test fixture 33 | hosts: example 34 | gather_facts: false 35 | tasks: 36 | - name: Dummy converge of example group 37 | ansible.builtin.debug: 38 | var: ansible_host 39 | 40 | - name: Molecule test fixture 41 | hosts: example_1 42 | gather_facts: false 43 | tasks: 44 | - name: Dummy converge of child example_1 group 45 | ansible.builtin.debug: 46 | var: ansible_host 47 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/molecule/default/group_vars/example/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | host_group_vars_group_vars_dir: true 3 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | groups: 10 | - example 11 | children: 12 | - example_1 13 | provisioner: 14 | name: ansible 15 | inventory: 16 | hosts: 17 | all: 18 | hosts: 19 | extra_host: 20 | host_group_vars_extra_host_molecule_yml: true 21 | host_vars: 22 | instance: 23 | host_group_vars_host_molecule_yml: true 24 | group_vars: 25 | example: 26 | host_group_vars_example_group_one_molecule_yml: true 27 | host_group_vars_example_group_two_molecule_yml: true 28 | example_1: 29 | host_group_vars_example_1_child_group_molecule_yml: true 30 | verifier: 31 | name: ansible 32 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/molecule/links/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Molecule test fixture 3 | hosts: example 4 | gather_facts: false 5 | tasks: 6 | - name: Host vars from host_vars links 7 | ansible.builtin.debug: 8 | var: host_group_vars_host_vars_linked 9 | 10 | - name: Group vars from group_vars links 11 | ansible.builtin.debug: 12 | var: host_group_vars_group_vars_linked 13 | 14 | - name: Variable from extra inventory link 15 | ansible.builtin.debug: 16 | var: hostvars['extra-host']['host_group_vars_extra_host_linked'] 17 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/host_group_vars/molecule/links/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | groups: 10 | - example 11 | children: 12 | - example_1 13 | provisioner: 14 | name: ansible 15 | inventory: 16 | links: 17 | hosts: ../../hosts 18 | host_vars: ../../host_vars 19 | group_vars: ../../group_vars 20 | scenario: 21 | name: links 22 | verifier: 23 | name: testinfra 24 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/idempotence/molecule/raises/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | roles: [] 7 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/idempotence/molecule/raises/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | # commented on purpose to validate that molecule will inherit it from 12 | # parent folder name: 13 | # scenario: 14 | # name: raises 15 | verifier: 16 | name: ansible 17 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/idempotence/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Steps provided by @saez0pub 3 | # Taken from https://github.com/ansible-community/molecule/issues/835 4 | 5 | - name: Create /tmp/test1 6 | ansible.builtin.file: 7 | name: /tmp/test1 8 | state: directory 9 | mode: "0600" 10 | 11 | - name: Replace /tmp/test1 by /tmp/test2 12 | ansible.builtin.shell: | 13 | set -euxo pipefail 14 | ls -ld /tmp/test1 | grep root 15 | changed_when: false 16 | 17 | - name: Fix /tmp/test1 perms 18 | ansible.builtin.file: 19 | name: /tmp/test1 20 | state: directory 21 | owner: lp 22 | group: lp 23 | mode: "0600" 24 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/interpolation/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Dummy task 7 | ansible.builtin.command: /bin/true 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/interpolation/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: ${DRIVER_NAME} 6 | platforms: 7 | - name: $INSTANCE_NAME 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | verifier: 12 | name: ansible 13 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/overrride_driver/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Dummy task 7 | ansible.builtin.command: /bin/true 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/overrride_driver/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | verifier: 12 | name: ansible 13 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/side_effect/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Check for test file 7 | ansible.builtin.stat: 8 | path: /tmp/testfile 9 | register: test_file 10 | 11 | - name: Create the test file 12 | ansible.builtin.file: 13 | path: /tmp/testfile 14 | state: touch 15 | mode: "0644" 16 | when: not test_file.stat.exists 17 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/side_effect/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | command: /sbin/init 10 | privileged: true 11 | provisioner: 12 | name: ansible 13 | playbooks: 14 | side_effect: side_effect.yml 15 | verifier: 16 | name: ansible 17 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/side_effect/molecule/default/side_effect.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Side Effect 3 | hosts: all 4 | gather_facts: false 5 | no_log: "{{ molecule_no_log }}" 6 | tasks: 7 | - name: Delete the test file as a planned side effect 8 | ansible.builtin.file: 9 | path: /tmp/testfile 10 | state: absent 11 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/side_effect/molecule/default/tests/test_side_effect.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | import testinfra.utils.ansible_runner 8 | 9 | 10 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 11 | os.environ["MOLECULE_INVENTORY_FILE"], 12 | ).get_hosts("all") 13 | 14 | 15 | def test_side_effect_removed_file(host): # type: ignore[no-untyped-def] # noqa: ANN201 16 | """Validate that file was removed.""" 17 | assert not host.file("/tmp/testfile").exists # noqa: S108 18 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/test_destroy_strategy/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | gather_facts: false 5 | become: true 6 | tasks: 7 | - name: Force a converge failure 8 | ansible.builtin.command: /bin/false 9 | changed_when: false 10 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/test_destroy_strategy/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | verifier: 12 | name: ansible 13 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/test_destroy_strategy/molecule/default/tests/test_destroy_strategy.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | import testinfra.utils.ansible_runner 8 | 9 | 10 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 11 | os.environ["MOLECULE_INVENTORY_FILE"], 12 | ).get_hosts("all") 13 | 14 | 15 | def test_hostname(host): # type: ignore[no-untyped-def] # noqa: ANN201 16 | """Validate hostname.""" 17 | assert host.check_output("hostname -s") == "instance" 18 | 19 | 20 | def test_etc_molecule_directory(host): # type: ignore[no-untyped-def] # noqa: ANN201 21 | """Validate molecule directory.""" 22 | f = host.file("/etc/molecule") 23 | 24 | assert f.is_directory 25 | assert f.user == "root" 26 | assert f.group == "root" 27 | assert f.mode == 0o755 # noqa: PLR2004 28 | 29 | 30 | def test_etc_molecule_ansible_hostname_file(host): # type: ignore[no-untyped-def] # noqa: ANN201 31 | """Validate molecule instance file.""" 32 | f = host.file("/etc/molecule/instance") 33 | 34 | assert f.is_file 35 | assert f.user == "root" 36 | assert f.group == "root" 37 | assert f.mode == 0o644 # noqa: PLR2004 38 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: flake8 6 | name: flake8 7 | entry: python3 -m flake8 --max-line-length=120 8 | language: system 9 | types: [python] 10 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra-pre-commit/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | tasks: 5 | - name: Create /tmp/molecule 6 | ansible.builtin.file: 7 | dest: /etc/molecule 8 | group: root 9 | owner: root 10 | mode: "0755" 11 | state: directory 12 | 13 | - name: Create /etc/molecule/{{ ansible_hostname }} 14 | ansible.builtin.copy: 15 | dest: "/etc/molecule/{{ ansible_hostname }}" 16 | group: root 17 | owner: root 18 | mode: "0644" 19 | content: "{{ ansible_hostname }}" 20 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra-pre-commit/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | scenario: 12 | name: testinfra-pre-commit 13 | verifier: 14 | name: testinfra 15 | options: 16 | vvv: true 17 | additional_files_or_dirs: 18 | - ../shared/test_*.py 19 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra-pre-commit/tests/test_testinfra_pre_commit.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | 6 | def test_ansible_hostname(host): # type: ignore[no-untyped-def] # noqa: ANN201 7 | """Validate hostname.""" 8 | f = host.file("/tmp/molecule/instance-1") # noqa: S108 9 | assert not f.exists 10 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | gather_facts: false 4 | hosts: all 5 | roles: 6 | - molecule 7 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | image: ${TEST_BASE_IMAGE} 9 | provisioner: 10 | name: ansible 11 | env: 12 | ANSIBLE_ROLES_PATH: ../../../../resources/roles/ 13 | scenario: 14 | name: testinfra 15 | verifier: 16 | name: testinfra 17 | options: 18 | vvv: true 19 | additional_files_or_dirs: 20 | - ../shared/test_*.py 21 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/roles/molecule/tasks/main.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/molecule/bfb18224c98e7ad4cc0c49b03a8398df1f550a01/tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/roles/molecule/tasks/main.yml -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/shared/test_shared.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | import testinfra.utils.ansible_runner 8 | 9 | 10 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 11 | os.environ["MOLECULE_INVENTORY_FILE"], 12 | ).get_hosts("all") 13 | 14 | 15 | def test_ansible_hostname(host): # type: ignore[no-untyped-def] # noqa: ANN201 16 | """Validate hostname.""" 17 | f = host.file("/tmp/molecule/instance-1") # noqa: S108 18 | 19 | assert not f.exists 20 | -------------------------------------------------------------------------------- /tests/fixtures/integration/test_command/scenarios/verifier/molecule/testinfra/tests/test_testinfra.py: -------------------------------------------------------------------------------- 1 | """Testinfra tests.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | import testinfra.utils.ansible_runner 8 | 9 | 10 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 11 | os.environ["MOLECULE_INVENTORY_FILE"], 12 | ).get_hosts("all") 13 | 14 | 15 | def test_ansible_hostname(host): # type: ignore[no-untyped-def] # noqa: ANN201 16 | """Validate hostname.""" 17 | f = host.file("/tmp/molecule/instance-1") # noqa: S108 18 | 19 | assert not f.exists 20 | -------------------------------------------------------------------------------- /tests/fixtures/resources/.yamllint: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | braces: 5 | max-spaces-inside: 1 6 | level: error 7 | brackets: 8 | max-spaces-inside: 1 9 | level: error 10 | line-length: disable 11 | truthy: disable 12 | -------------------------------------------------------------------------------- /tests/fixtures/resources/broken-collection/galaxy.yml: -------------------------------------------------------------------------------- 1 | name: baddies 2 | name_space: acme 3 | version: 1.0.0 4 | -------------------------------------------------------------------------------- /tests/fixtures/resources/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: [] 7 | provisioner: 8 | name: ansible 9 | scenario: 10 | name: ansible-galaxy 11 | verifier: 12 | name: ansible 13 | -------------------------------------------------------------------------------- /tests/fixtures/resources/playbooks/delegated/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | tasks: [] 7 | -------------------------------------------------------------------------------- /tests/fixtures/resources/playbooks/delegated/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy 3 | hosts: localhost 4 | connection: local 5 | gather_facts: false 6 | tasks: [] 7 | -------------------------------------------------------------------------------- /tests/fixtures/resources/playbooks/delegated/inventory/group_vars/all/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | base_dir: /tmp 3 | -------------------------------------------------------------------------------- /tests/fixtures/resources/roles/molecule/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Ansible Molecule by Red Hat 4 | description: Test Molecule role 5 | license: MIT 6 | min_ansible_version: "2.15" 7 | platforms: 8 | - name: GenericLinux 9 | versions: 10 | - all 11 | galaxy_tags: [] 12 | dependencies: [] 13 | -------------------------------------------------------------------------------- /tests/fixtures/resources/roles/molecule/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create /etc/molecule 3 | ansible.builtin.file: 4 | dest: /etc/molecule 5 | group: root 6 | owner: root 7 | mode: "0755" 8 | state: directory 9 | 10 | - name: Create /etc/molecule/{{ ansible_host }} 11 | ansible.builtin.copy: 12 | dest: "/etc/molecule/{{ ansible_host }}" 13 | group: root 14 | owner: root 15 | mode: "0644" 16 | content: "{{ ansible_host }}" 17 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/galaxy.yml: -------------------------------------------------------------------------------- 1 | name: goodies 2 | namespace: acme 3 | version: 1.0.0 4 | readme: README.md 5 | authors: 6 | - Red Hat 7 | description: Acme Goodies Collection 8 | build_ignore: 9 | - "*.egg-info" 10 | - .ansible 11 | - .DS_Store 12 | - .eggs 13 | - .gitignore 14 | - .mypy_cache 15 | - .pytest_cache 16 | - .stestr 17 | - .stestr.conf 18 | - .tox 19 | - .vscode 20 | - MANIFEST.in 21 | - build 22 | - dist 23 | - doc 24 | - report.html 25 | - setup.cfg 26 | - setup.py 27 | - "tests/unit/*.*" 28 | - README.rst 29 | - tox.ini 30 | 31 | repository: https://opendev.org/openstack/tripleo-repos 32 | license_file: LICENSE 33 | tags: 34 | - tools 35 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/meta/runtime.yml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: localhost 4 | tasks: 5 | - name: "Include sample role from current collection" 6 | ansible.builtin.include_role: 7 | name: acme.goodies.get_rich 8 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: default 6 | platforms: 7 | - name: instance 8 | provisioner: 9 | name: ansible 10 | verifier: 11 | name: ansible 12 | -------------------------------------------------------------------------------- /tests/fixtures/resources/sample-collection/roles/get_rich/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Some task inside foo.bar collection 2 | ansible.builtin.debug: 3 | msg: "hello world!" 4 | -------------------------------------------------------------------------------- /tests/fixtures/resources/schema_instance_files/invalid/molecule_delegated.yml: -------------------------------------------------------------------------------- 1 | driver: 2 | name: default 3 | platforms: 4 | - name: foo 5 | children: 2 # invalid, must be list of strings 6 | -------------------------------------------------------------------------------- /tests/fixtures/resources/schema_instance_files/valid/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: shell 4 | enabled: true 5 | command: path/to/command --flag1 subcommand --flag2 6 | options: 7 | ignore-certs: true 8 | ignore-errors: true 9 | env: 10 | FOO: bar 11 | 12 | driver: 13 | name: default 14 | options: 15 | managed: false 16 | login_cmd_template: ... 17 | ansible_connection_options: 18 | ansible_connection: ssh 19 | 20 | log: true 21 | 22 | platforms: 23 | - name: ubi8 24 | hostname: ubi8 25 | children: [] # list of strings 26 | unknown_property_foo: bar # unknown properties should be allowed for drivers 27 | groups: 28 | - ubi8 29 | # vagrant ones 30 | box: foo/bar 31 | 32 | - name: ubi7 33 | hostname: ubi7 34 | children: ["ubi8"] 35 | groups: 36 | - ubi7 37 | 38 | provisioner: 39 | playbooks: 40 | prepare: prepare.yml 41 | inventory: 42 | hosts: 43 | all: 44 | hosts: 45 | ubi8: 46 | ansible_python_interpreter: /usr/bin/python3 47 | ubi7: 48 | selinux: permissive 49 | ubi8: 50 | selinux: enforced 51 | name: ansible 52 | log: true 53 | env: 54 | ANSIBLE_STDOUT_CALLBACK: yaml 55 | config_options: 56 | defaults: 57 | fact_caching: jsonfile 58 | fact_caching_connection: /tmp/molecule/facts 59 | 60 | scenario: 61 | test_sequence: 62 | - destroy 63 | - create 64 | - prepare 65 | - converge 66 | - check 67 | - verify 68 | - destroy 69 | 70 | verifier: 71 | name: testinfra 72 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests.""" 2 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # Copyright (c) 2018 Red Hat, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to 6 | # deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | # sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | from __future__ import annotations 22 | 23 | from typing import TYPE_CHECKING 24 | 25 | import pytest 26 | 27 | from molecule import logger 28 | from molecule.app import get_app 29 | 30 | 31 | if TYPE_CHECKING: 32 | from collections.abc import Iterator 33 | from pathlib import Path 34 | 35 | 36 | LOG = logger.get_logger(__name__) 37 | 38 | 39 | @pytest.fixture(name="with_scenario") 40 | def _with_scenario( # noqa: PLR0913 41 | request: pytest.FixtureRequest, 42 | monkeypatch: pytest.MonkeyPatch, 43 | test_ephemeral_dir_env: dict[str, str], 44 | scenario_to_test: str, 45 | scenario_name: str, 46 | test_fixture_dir: Path, 47 | ) -> Iterator[None]: 48 | """Yield to test with a scenario, changes dir and does cleanup. 49 | 50 | Args: 51 | request: The pytest request object. 52 | monkeypatch: The pytest monkeypatch object. 53 | test_ephemeral_dir_env: The ephemeral directory environment variables. 54 | scenario_to_test: The scenario to test. 55 | scenario_name: The scenario name. 56 | test_fixture_dir: The test fixture directory. 57 | 58 | Yields: 59 | None: 60 | """ 61 | scenario_directory = test_fixture_dir / "scenarios" / scenario_to_test 62 | 63 | monkeypatch.chdir(scenario_directory) 64 | 65 | yield 66 | if request.node.rep_call.failed: 67 | return 68 | if scenario_name: 69 | msg = f"CLEANUP: Destroying instances for {scenario_name}" 70 | LOG.info(msg) 71 | cmd = ["molecule", "destroy", "--scenario-name", scenario_name] 72 | assert ( 73 | get_app(scenario_directory).run_command(cmd=cmd, env=test_ephemeral_dir_env).returncode 74 | == 0 75 | ) 76 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/command/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/command/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | 25 | @pytest.fixture 26 | def command_patched_ansible_create(mocker): # type: ignore[no-untyped-def] # noqa: ANN201, D103 27 | return mocker.patch("molecule.provisioner.ansible.Ansible.create") 28 | 29 | 30 | @pytest.fixture 31 | def command_driver_delegated_section_data(): # type: ignore[no-untyped-def] # noqa: ANN201, D103 32 | x = { 33 | "driver": { 34 | "name": "default", 35 | "options": { 36 | "managed": False, 37 | }, 38 | }, 39 | } 40 | # if "DOCKER_HOST" in os.environ: 41 | # x["driver"]["options"]["ansible_docker_extra_args"] = "-H={}".format( 42 | return x # noqa: RET504 43 | 44 | 45 | @pytest.fixture 46 | def command_driver_delegated_managed_section_data(): # type: ignore[no-untyped-def] # noqa: ANN201, D103 47 | return {"driver": {"name": "default", "managed": True}} 48 | -------------------------------------------------------------------------------- /tests/unit/command/init/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/command/test_check.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | import pytest 25 | 26 | from molecule.command import check 27 | 28 | 29 | if TYPE_CHECKING: 30 | from unittest.mock import MagicMock, Mock 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | @pytest.fixture 38 | def _patched_ansible_check(mocker: MockerFixture) -> MagicMock: 39 | return mocker.patch("molecule.provisioner.ansible.Ansible.check") 40 | 41 | 42 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 43 | # config.Config._validate from executing. Thus preventing odd side-effects 44 | # throughout patched.assert_called unit tests. 45 | def test_check_execute( # noqa: D103 46 | mocker: MockerFixture, 47 | caplog: pytest.LogCaptureFixture, 48 | _patched_ansible_check: Mock, # noqa: PT019 49 | patched_config_validate: Mock, 50 | config_instance: config.Config, 51 | ) -> None: 52 | c = check.Check(config_instance) 53 | c.execute() 54 | 55 | assert "default" in caplog.text 56 | assert "check" in caplog.text 57 | -------------------------------------------------------------------------------- /tests/unit/command/test_cleanup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import os 23 | 24 | from typing import TYPE_CHECKING 25 | 26 | import pytest 27 | 28 | from molecule import config, util 29 | from molecule.command import cleanup 30 | 31 | 32 | if TYPE_CHECKING: 33 | from typing import Literal 34 | from unittest.mock import MagicMock, Mock 35 | 36 | from pytest_mock import MockerFixture 37 | 38 | from molecule.types import ProvisionerData 39 | 40 | 41 | @pytest.fixture 42 | def _command_provisioner_section_with_cleanup_data() -> dict[ 43 | Literal["provisioner"], 44 | ProvisionerData, 45 | ]: 46 | return {"provisioner": {"name": "ansible", "playbooks": {"cleanup": "cleanup.yml"}}} 47 | 48 | 49 | @pytest.fixture 50 | def _patched_ansible_cleanup(mocker: MockerFixture) -> MagicMock: 51 | return mocker.patch("molecule.provisioner.ansible.Ansible.cleanup") 52 | 53 | 54 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 55 | # config.Config._validate from executing. Thus preventing odd side-effects 56 | # throughout patched.assert_called unit tests. 57 | @pytest.mark.parametrize( 58 | "config_instance", 59 | ["_command_provisioner_section_with_cleanup_data"], # noqa: PT007 60 | indirect=True, 61 | ) 62 | def test_cleanup_execute( # noqa: D103 63 | mocker: MockerFixture, 64 | _patched_ansible_cleanup: Mock, # noqa: PT019 65 | caplog: pytest.LogCaptureFixture, 66 | patched_config_validate: Mock, 67 | config_instance: config.Config, 68 | ) -> None: 69 | pb = os.path.join(config_instance.scenario.directory, "cleanup.yml") # noqa: PTH118 70 | util.write_file(pb, "") 71 | 72 | cu = cleanup.Cleanup(config_instance) 73 | cu.execute() 74 | 75 | assert "cleanup" in caplog.text 76 | 77 | _patched_ansible_cleanup.assert_called_once_with() 78 | 79 | 80 | def test_cleanup_execute_skips_when_playbook_not_configured( # noqa: D103 81 | caplog: pytest.LogCaptureFixture, 82 | _patched_ansible_cleanup: Mock, # noqa: PT019 83 | config_instance: config.Config, 84 | ) -> None: 85 | cu = cleanup.Cleanup(config_instance) 86 | cu.execute() 87 | 88 | msg = "Skipping, cleanup playbook not configured." 89 | assert msg in caplog.text 90 | 91 | assert not _patched_ansible_cleanup.called 92 | -------------------------------------------------------------------------------- /tests/unit/command/test_converge.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING, Any 23 | 24 | from click.testing import CliRunner 25 | 26 | from molecule.command import converge 27 | from molecule.shell import main 28 | 29 | 30 | if TYPE_CHECKING: 31 | from unittest.mock import Mock 32 | 33 | import pytest 34 | 35 | from pytest_mock import MockerFixture 36 | 37 | from molecule import config 38 | 39 | 40 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 41 | # config.Config._validate from executing. Thus preventing odd side-effects 42 | # throughout patched.assert_called unit tests. 43 | def test_converge_execute( # noqa: D103 44 | mocker: MockerFixture, 45 | caplog: pytest.LogCaptureFixture, 46 | patched_ansible_converge: Mock, 47 | patched_config_validate: Any, # noqa: ANN401 48 | config_instance: config.Config, 49 | ) -> None: 50 | c = converge.Converge(config_instance) 51 | c.execute() 52 | 53 | assert "default" in caplog.text 54 | assert "converge" in caplog.text 55 | 56 | patched_ansible_converge.assert_called_once_with() 57 | 58 | assert config_instance.state.converged 59 | 60 | 61 | def test_ansible_args_passed_to_scenarios_get_configs(mocker: MockerFixture) -> None: # noqa: D103 62 | # Scenarios patch is needed to safely invoke CliRunner 63 | # in the test environment and block scenario execution 64 | mocker.patch("molecule.scenarios.Scenarios") 65 | patched_get_configs = mocker.patch("molecule.command.base.get_configs") 66 | 67 | runner = CliRunner() 68 | args = ("converge", "--", "-e", "testvar=testvalue") 69 | ansible_args = args[2:] 70 | runner.invoke(main, args, obj={}) 71 | 72 | # call index [0][2] is the 3rd positional argument to get_configs, 73 | # which should be the tuple of parsed ansible_args from the CLI 74 | assert patched_get_configs.call_args[0][2] == ansible_args 75 | -------------------------------------------------------------------------------- /tests/unit/command/test_create.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | import pytest 25 | 26 | from molecule.command import create 27 | 28 | 29 | if TYPE_CHECKING: 30 | from unittest.mock import MagicMock, Mock 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | @pytest.fixture 38 | def _patched_create_setup(mocker: MockerFixture) -> MagicMock: 39 | return mocker.patch("molecule.command.create.Create._setup") 40 | 41 | 42 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 43 | # config.Config._validate from executing. Thus preventing odd side-effects 44 | # throughout patched.assert_called unit tests. 45 | @pytest.mark.skip(reason="create not running for delegated") 46 | def test_create_execute( # noqa: D103 47 | mocker: MockerFixture, 48 | caplog: pytest.LogCaptureFixture, 49 | command_patched_ansible_create: Mock, 50 | patched_config_validate: Mock, 51 | config_instance: config.Config, 52 | ) -> None: 53 | c = create.Create(config_instance) 54 | c.execute() 55 | 56 | assert "default" in caplog.text 57 | assert "converge" in caplog.text 58 | 59 | assert config_instance.state.driver == "default" 60 | 61 | command_patched_ansible_create.assert_called_once_with() 62 | 63 | assert config_instance.state.created 64 | 65 | 66 | @pytest.mark.skip(reason="create not running for delegated") 67 | def test_execute_skips_when_instances_already_created( # noqa: D103 68 | caplog: pytest.LogCaptureFixture, 69 | command_patched_ansible_create: Mock, 70 | config_instance: config.Config, 71 | ) -> None: 72 | config_instance.state.change_state("created", True) # noqa: FBT003 73 | c = create.Create(config_instance) 74 | c.execute() 75 | 76 | msg = "Skipping, instances already created." 77 | assert msg in caplog.text 78 | 79 | assert not command_patched_ansible_create.called 80 | -------------------------------------------------------------------------------- /tests/unit/command/test_dependency.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | from molecule.command import dependency 25 | 26 | 27 | if TYPE_CHECKING: 28 | from unittest.mock import Mock 29 | 30 | import pytest 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 38 | # config.Config._validate from executing. Thus preventing odd side-effects 39 | # throughout patched.assert_called unit tests. 40 | def test_dependency_execute( # noqa: D103 41 | mocker: MockerFixture, 42 | caplog: pytest.LogCaptureFixture, 43 | patched_ansible_galaxy: Mock, 44 | patched_config_validate: Mock, 45 | config_instance: config.Config, 46 | ) -> None: 47 | d = dependency.Dependency(config_instance) 48 | d.execute() 49 | 50 | patched_ansible_galaxy.assert_called_once_with() 51 | 52 | assert "default" in caplog.text 53 | assert "dependency" in caplog.text 54 | -------------------------------------------------------------------------------- /tests/unit/command/test_destroy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | import pytest 25 | 26 | from molecule.command import destroy 27 | 28 | 29 | if TYPE_CHECKING: 30 | from unittest.mock import MagicMock, Mock 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | @pytest.fixture 38 | def _patched_ansible_destroy(mocker: MockerFixture) -> MagicMock: 39 | return mocker.patch("molecule.provisioner.ansible.Ansible.destroy") 40 | 41 | 42 | @pytest.fixture 43 | def _patched_destroy_setup(mocker: MockerFixture) -> MagicMock: 44 | return mocker.patch("molecule.command.destroy.Destroy._setup") 45 | 46 | 47 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 48 | # config.Config._validate from executing. Thus preventing odd side-effects 49 | # throughout patched.assert_called unit tests. 50 | @pytest.mark.skip(reason="destroy not running for delegated") 51 | def test_destroy_execute( # noqa: D103 52 | mocker: MockerFixture, 53 | caplog: pytest.LogCaptureFixture, 54 | patched_config_validate: Mock, 55 | _patched_ansible_destroy: Mock, # noqa: PT019 56 | config_instance: config.Config, 57 | ) -> None: 58 | d = destroy.Destroy(config_instance) 59 | d.execute() 60 | 61 | assert "destroy" in caplog.text 62 | 63 | assert "verify" in caplog.text 64 | 65 | _patched_ansible_destroy.assert_called_once_with() 66 | 67 | assert not config_instance.state.converged 68 | assert not config_instance.state.created 69 | 70 | 71 | @pytest.mark.parametrize( 72 | "config_instance", 73 | ["command_driver_delegated_section_data"], # noqa: PT007 74 | indirect=True, 75 | ) 76 | def test_execute_skips_when_destroy_strategy_is_never( # noqa: D103 77 | _patched_destroy_setup: Mock, # noqa: PT019 78 | caplog: pytest.LogCaptureFixture, 79 | _patched_ansible_destroy: Mock, # noqa: PT019 80 | config_instance: config.Config, 81 | ) -> None: 82 | config_instance.command_args = {"destroy": "never"} 83 | 84 | d = destroy.Destroy(config_instance) 85 | d.execute() 86 | 87 | msg = "Skipping, '--destroy=never' requested." 88 | assert msg in caplog.text 89 | 90 | assert not _patched_ansible_destroy.called 91 | -------------------------------------------------------------------------------- /tests/unit/command/test_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | from molecule.command import list # noqa: A004 25 | from molecule.status import Status 26 | 27 | 28 | if TYPE_CHECKING: 29 | import pytest 30 | 31 | from molecule import config 32 | 33 | 34 | def test_list_execute( # noqa: D103 35 | capsys: pytest.CaptureFixture[str], 36 | config_instance: config.Config, 37 | ) -> None: 38 | l = list.List(config_instance) # noqa: E741 39 | x = [ 40 | Status( 41 | instance_name="instance-1", 42 | driver_name="default", 43 | provisioner_name="ansible", 44 | scenario_name="default", 45 | created="false", 46 | converged="false", 47 | ), 48 | Status( 49 | instance_name="instance-2", 50 | driver_name="default", 51 | provisioner_name="ansible", 52 | scenario_name="default", 53 | created="false", 54 | converged="false", 55 | ), 56 | ] 57 | 58 | assert x == l.execute() 59 | -------------------------------------------------------------------------------- /tests/unit/command/test_matrix.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Unittests for matrix command.""" 21 | -------------------------------------------------------------------------------- /tests/unit/command/test_syntax.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | import pytest 25 | 26 | from molecule.command import syntax 27 | 28 | 29 | if TYPE_CHECKING: 30 | from unittest.mock import MagicMock, Mock 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | @pytest.fixture 38 | def _patched_ansible_syntax(mocker: MockerFixture) -> MagicMock: 39 | return mocker.patch("molecule.provisioner.ansible.Ansible.syntax") 40 | 41 | 42 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 43 | # config.Config._validate from executing. Thus preventing odd side-effects 44 | # throughout patched.assert_called unit tests. 45 | def test_syntax_execute( # noqa: D103 46 | mocker: MockerFixture, 47 | caplog: pytest.LogCaptureFixture, 48 | _patched_ansible_syntax: Mock, # noqa: PT019 49 | patched_config_validate: Mock, 50 | config_instance: config.Config, 51 | ) -> None: 52 | s = syntax.Syntax(config_instance) 53 | s.execute() 54 | 55 | assert "default" in caplog.text 56 | assert "syntax" in caplog.text 57 | 58 | _patched_ansible_syntax.assert_called_once_with() 59 | -------------------------------------------------------------------------------- /tests/unit/command/test_test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Unittests for test command.""" 21 | -------------------------------------------------------------------------------- /tests/unit/command/test_verify.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | from molecule.command import verify 25 | 26 | 27 | if TYPE_CHECKING: 28 | from unittest.mock import Mock 29 | 30 | import pytest 31 | 32 | from pytest_mock import MockerFixture 33 | 34 | from molecule import config 35 | 36 | 37 | # NOTE(retr0h): The use of the `patched_config_validate` fixture, disables 38 | # config.Config._validate from executing. Thus preventing odd side-effects 39 | # throughout patched.assert_called unit tests. 40 | def test_verify_execute( # noqa: D103 41 | mocker: MockerFixture, 42 | caplog: pytest.LogCaptureFixture, 43 | patched_default_verifier: Mock, 44 | patched_config_validate: Mock, 45 | config_instance: config.Config, 46 | ) -> None: 47 | v = verify.Verify(config_instance) 48 | v.execute() 49 | 50 | assert "default" in caplog.text 51 | assert "verify" in caplog.text 52 | -------------------------------------------------------------------------------- /tests/unit/dependency/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/dependency/ansible_galaxy/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/driver/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/lint/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/model/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/model/v2/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/model/v2/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | """Unittest for v2 config format.""" 21 | 22 | from __future__ import annotations 23 | 24 | from typing import TYPE_CHECKING 25 | 26 | import pytest 27 | 28 | from molecule import util 29 | 30 | 31 | if TYPE_CHECKING: 32 | from pathlib import Path 33 | from typing import Any 34 | 35 | 36 | @pytest.fixture(name="molecule_file") 37 | def fixture_molecule_file(resources_folder_path: Path) -> Path: 38 | """Return path to molecule file. 39 | 40 | Args: 41 | resources_folder_path: Path to the resources folder. 42 | 43 | Returns: 44 | Path to molecule file. 45 | """ 46 | return resources_folder_path / "molecule.yml" 47 | 48 | 49 | @pytest.fixture 50 | def config(molecule_file: Path, request: pytest.FixtureRequest) -> dict[str, Any]: 51 | """Return merged molecule file data. 52 | 53 | Args: 54 | molecule_file: Path to molecule file. 55 | request: Pytest fixture request. 56 | 57 | Returns: 58 | Merged molecule file data. 59 | """ 60 | with molecule_file.open() as f: 61 | d = util.safe_load(f) 62 | if hasattr(request, "param"): 63 | if isinstance(request.getfixturevalue(request.param), str): 64 | d2 = util.safe_load(request.getfixturevalue(request.param)) 65 | else: 66 | d2 = request.getfixturevalue(request.param) 67 | d = util.merge_dicts(d, d2) 68 | 69 | return d # type: ignore[no-any-return] 70 | 71 | 72 | @pytest.fixture 73 | def _model_platforms_delegated_section_data() -> str: 74 | return """ 75 | --- 76 | platforms: 77 | - name: instance 78 | """.strip() 79 | -------------------------------------------------------------------------------- /tests/unit/model/v2/test_platforms_section.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule.model import schema_v3 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "config", 29 | ["_model_platforms_delegated_section_data"], # noqa: PT007 30 | indirect=True, 31 | ) 32 | def test_platforms_delegated(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 33 | assert not schema_v3.validate(config) 34 | -------------------------------------------------------------------------------- /tests/unit/model/v2/test_scenario_section.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule.model import schema_v3 25 | 26 | 27 | @pytest.fixture 28 | def _model_scenario_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 29 | return { 30 | "scenario": { 31 | "name": "foo", 32 | "check_sequence": ["check"], 33 | "converge_sequence": ["converge"], 34 | "create_sequence": ["create"], 35 | "destroy_sequence": ["destroy"], 36 | "test_sequence": ["test"], 37 | }, 38 | } 39 | 40 | 41 | @pytest.mark.parametrize("config", ["_model_scenario_section_data"], indirect=True) # noqa: PT007 42 | def test_scenario(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 43 | assert not schema_v3.validate(config) 44 | 45 | 46 | @pytest.fixture 47 | def _model_scenario_errors_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 48 | return {"scenario": {"name": 0}} 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "config", 53 | ["_model_scenario_errors_section_data"], # noqa: PT007 54 | indirect=True, 55 | ) 56 | def test_scenario_has_errors(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 57 | x = ["0 is not of type 'string'"] 58 | 59 | assert x == schema_v3.validate(config) 60 | -------------------------------------------------------------------------------- /tests/unit/model/v2/test_schema.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import typing 23 | 24 | from molecule.model import schema_v3 25 | from tests.conftest import run 26 | 27 | 28 | if typing.TYPE_CHECKING: 29 | from pathlib import Path 30 | 31 | 32 | def test_base_config(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 33 | assert not schema_v3.validate(config) 34 | 35 | 36 | def test_molecule_schema(resources_folder_path: Path) -> None: 37 | """Test the molecule schema. 38 | 39 | Args: 40 | resources_folder_path: Path to the resources folder. 41 | """ 42 | cmd = [ 43 | "uv", 44 | "tool", 45 | "run", 46 | "check-jsonschema", 47 | "-v", 48 | "--schemafile", 49 | "src/molecule/data/molecule.json", 50 | f"{resources_folder_path}/schema_instance_files/valid/molecule.yml", 51 | ] 52 | assert run(cmd).returncode == 0 53 | 54 | cmd = [ 55 | "uv", 56 | "tool", 57 | "run", 58 | "check-jsonschema", 59 | "-v", 60 | "--schemafile", 61 | "src/molecule/data/driver.json", 62 | f"{resources_folder_path}/schema_instance_files/invalid/molecule_delegated.yml", 63 | ] 64 | assert run(cmd).returncode != 0 65 | -------------------------------------------------------------------------------- /tests/unit/model/v2/test_verifier_section.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule.model import schema_v3 25 | 26 | 27 | @pytest.fixture 28 | def _model_verifier_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 29 | return { 30 | "verifier": { 31 | "name": "testinfra", 32 | "enabled": True, 33 | "directory": "foo", 34 | "options": {"foo": "bar"}, 35 | "env": {"FOO": "foo", "FOO_BAR": "foo_bar"}, 36 | "additional_files_or_dirs": ["foo"], 37 | }, 38 | } 39 | 40 | 41 | @pytest.mark.parametrize("config", ["_model_verifier_section_data"], indirect=True) # noqa: PT007 42 | def test_verifier(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 43 | assert not schema_v3.validate(config) 44 | 45 | 46 | @pytest.fixture 47 | def _model_verifier_errors_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 48 | return { 49 | "verifier": { 50 | "name": 0, 51 | }, 52 | } 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "config", 57 | ["_model_verifier_errors_section_data"], # noqa: PT007 58 | indirect=True, 59 | ) 60 | def test_verifier_has_errors(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 61 | x = ["0 is not one of ['ansible', 'goss', 'inspec', 'testinfra']"] 62 | 63 | assert x == schema_v3.validate(config) 64 | 65 | 66 | @pytest.fixture 67 | def _model_verifier_allows_testinfra_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 68 | return {"verifier": {"name": "testinfra"}} 69 | 70 | 71 | @pytest.fixture 72 | def _model_verifier_allows_ansible_section_data(): # type: ignore[no-untyped-def] # noqa: ANN202 73 | return {"verifier": {"name": "ansible"}} 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "config", 78 | [ # noqa: PT007 79 | ("_model_verifier_allows_testinfra_section_data"), 80 | ("_model_verifier_allows_ansible_section_data"), 81 | ], 82 | indirect=True, 83 | ) 84 | def test_verifier_allows_name(config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 85 | assert not schema_v3.validate(config) 86 | -------------------------------------------------------------------------------- /tests/unit/provisioner/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tests/unit/test_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Red Hat, Inc. # noqa: D100 2 | # Copyright (c) 2015-2018 Cisco Systems, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to 6 | # deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | # sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | from __future__ import annotations 22 | 23 | from molecule import api 24 | 25 | 26 | def test_api_drivers() -> None: # noqa: D103 27 | results = api.drivers() 28 | 29 | for result in results.values(): 30 | assert isinstance(result, api.Driver) 31 | 32 | assert "default" in results 33 | 34 | 35 | def test_api_verifiers() -> None: # noqa: D103 36 | x = ["testinfra", "ansible"] 37 | 38 | assert all(elem in api.verifiers() for elem in x) 39 | -------------------------------------------------------------------------------- /tests/unit/test_platforms.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule import config, platforms 25 | 26 | 27 | @pytest.fixture 28 | def _instance(config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN202 29 | return platforms.Platforms(config_instance) 30 | 31 | 32 | def test_instances_property(_instance): # type: ignore[no-untyped-def] # noqa: ANN201, PT019, D103 33 | x = [ 34 | {"groups": ["foo", "bar"], "name": "instance-1", "children": ["child1"]}, 35 | {"groups": ["baz", "foo"], "name": "instance-2", "children": ["child2"]}, 36 | ] 37 | 38 | assert x == _instance.instances 39 | 40 | 41 | @pytest.fixture(name="platform_name") 42 | def fixture_platform_name(request, config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN201, D103 43 | return platforms.Platforms(config_instance, platform_name=request.param) 44 | 45 | 46 | @pytest.mark.parametrize("platform_name", ["instance-1"], indirect=True) # noqa: PT007 47 | def test_instances_property_with_platform_name_instance_1(platform_name): # type: ignore[no-untyped-def] # noqa: ANN201, D103 48 | x = [ 49 | {"groups": ["foo", "bar"], "name": "instance-1", "children": ["child1"]}, 50 | ] 51 | 52 | assert x == platform_name.instances 53 | 54 | 55 | @pytest.mark.parametrize("platform_name", ["instance-2"], indirect=True) # noqa: PT007 56 | def test_instances_property_with_platform_name_instance_2(platform_name): # type: ignore[no-untyped-def] # noqa: ANN201, D103 57 | x = [ 58 | {"groups": ["baz", "foo"], "name": "instance-2", "children": ["child2"]}, 59 | ] 60 | 61 | assert x == platform_name.instances 62 | -------------------------------------------------------------------------------- /tests/unit/test_scenarios_ordered.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Marc Dequènes (Duck) # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import copy 23 | 24 | import pytest 25 | 26 | from molecule import config, scenarios 27 | 28 | 29 | @pytest.fixture 30 | def _instance(config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN202 31 | config_instance_1 = copy.deepcopy(config_instance) 32 | config_instance_1.config["scenario"]["name"] = "two" 33 | config_instance_1.molecule_file = config_instance_1.molecule_file.replace( 34 | "default", 35 | "02_foo", 36 | ) 37 | 38 | config_instance_2 = copy.deepcopy(config_instance) 39 | config_instance_2.config["scenario"]["name"] = "one" 40 | config_instance_2.molecule_file = config_instance_2.molecule_file.replace( 41 | "default", 42 | "01_foo", 43 | ) 44 | 45 | config_instance_3 = copy.deepcopy(config_instance) 46 | config_instance_3.config["scenario"]["name"] = "three" 47 | config_instance_3.molecule_file = config_instance_3.molecule_file.replace( 48 | "default", 49 | "03_foo", 50 | ) 51 | 52 | return scenarios.Scenarios( 53 | [config_instance_1, config_instance_2, config_instance_3], 54 | ) 55 | 56 | 57 | def test_all_ordered(_instance): # type: ignore[no-untyped-def] # noqa: ANN201, PT019, D103 58 | result = _instance.all 59 | 60 | assert len(result) == 3 # noqa: PLR2004 61 | assert result[0].name == "one" 62 | assert result[1].name == "two" 63 | assert result[2].name == "three" 64 | -------------------------------------------------------------------------------- /tests/unit/test_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule import shell 25 | 26 | 27 | def test_shell() -> None: # noqa: D103 28 | with pytest.raises(SystemExit): 29 | shell.main() # pylint: disable=no-value-for-parameter 30 | -------------------------------------------------------------------------------- /tests/unit/test_status.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2018 Cisco Systems, Inc. # noqa: D100 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | from __future__ import annotations 21 | 22 | import pytest 23 | 24 | from molecule.status import Status 25 | 26 | 27 | @pytest.fixture 28 | def _instance() -> Status: 29 | s = Status( 30 | instance_name="instance", 31 | driver_name="driver", 32 | provisioner_name="provisioner", 33 | scenario_name="scenario", 34 | created="True", 35 | converged="False", 36 | ) 37 | 38 | return s # noqa: RET504 39 | 40 | 41 | def test__instance_name_attribute(_instance: Status) -> None: # noqa: PT019, D103 42 | assert _instance.instance_name == "instance" 43 | 44 | 45 | def test_status_driver_name_attribute(_instance: Status) -> None: # noqa: PT019, D103 46 | assert _instance.driver_name == "driver" 47 | 48 | 49 | def test_status_provisioner_name_attribute(_instance: Status) -> None: # noqa: PT019, D103 50 | assert _instance.provisioner_name == "provisioner" 51 | 52 | 53 | def test_status_scenario_name_attribute(_instance: Status) -> None: # noqa: PT019, D103 54 | assert _instance.scenario_name == "scenario" 55 | 56 | 57 | def test_status_created_attribute(_instance: Status) -> None: # noqa: PT019, D103 58 | assert _instance.created == "True" 59 | 60 | 61 | def test_status_converged_attribute(_instance: Status) -> None: # noqa: PT019, D103 62 | assert _instance.converged == "False" 63 | -------------------------------------------------------------------------------- /tests/unit/test_text.py: -------------------------------------------------------------------------------- 1 | """Test the molecule text module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from molecule.text import ( 6 | camelize, 7 | strip_ansi_color, 8 | strip_ansi_escape, 9 | title, 10 | underscore, 11 | ) 12 | 13 | 14 | def test_camelize() -> None: # noqa: D103 15 | assert camelize("foo") == "Foo" 16 | assert camelize("foo_bar") == "FooBar" 17 | assert camelize("foo_bar_baz") == "FooBarBaz" 18 | 19 | 20 | def test_strip_ansi_color() -> None: # noqa: D103 21 | s = "foo\x1b[0m\x1b[0m\x1b[0m\n\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m" 22 | 23 | assert strip_ansi_color(s) == "foo\n" 24 | 25 | 26 | def test_strip_ansi_escape() -> None: # noqa: D103 27 | string = "ls\r\n\x1b[00m\x1b[01;31mfoo\x1b[00m\r\n\x1b[01;31m" 28 | 29 | assert strip_ansi_escape(string) == "ls\r\nfoo\r\n" 30 | 31 | 32 | def test_title() -> None: # noqa: D103 33 | assert title("foo") == "Foo" 34 | assert title("foo_bar") == "Foo Bar" 35 | 36 | 37 | def test_underscore() -> None: # noqa: D103 38 | assert underscore("Foo") == "foo" 39 | assert underscore("FooBar") == "foo_bar" 40 | assert underscore("FooBarBaz") == "foo_bar_baz" 41 | -------------------------------------------------------------------------------- /tests/unit/verifier/__init__.py: -------------------------------------------------------------------------------- 1 | # noqa: D104 2 | -------------------------------------------------------------------------------- /tools/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | { 4 | python3 -c "import setuptools_scm" || python3 -m pip install --user setuptools-scm 5 | } 1>&2 # redirect stdout to stderr to avoid polluting the output 6 | python3 -m setuptools_scm | \ 7 | sed 's/Guessed Version\([^+]\+\).*/\1/' 8 | -------------------------------------------------------------------------------- /tools/opts.txt: -------------------------------------------------------------------------------- 1 | distro 2 | -------------------------------------------------------------------------------- /tools/report-coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # cspell: ignore exuo 3 | set -euo pipefail 4 | coverage combine -q "--data-file=${TOX_ENV_DIR}/.coverage" "${TOX_ENV_DIR}"/.coverage.* 5 | coverage xml "--data-file=${TOX_ENV_DIR}/.coverage" -o "${TOX_ENV_DIR}/coverage.xml" --ignore-errors --fail-under=0 6 | COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage lcov --fail-under=0 --ignore-errors -q 7 | COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage report --ignore-errors 8 | -------------------------------------------------------------------------------- /tools/test-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | # Used by Zuul CI to perform extra bootstrapping 4 | 5 | # Bumping system tox because version from CentOS 7 is too old 6 | # We are not using pip --user due to few bugs in tox role which does not allow 7 | # us to override how is called. Once these are addressed we will switch back 8 | # non-sudo mode. 9 | PYTHON=$(command -v python3 python | head -n1) 10 | 11 | sudo "$PYTHON" -m pip install "tox>=3.14.6" 12 | # 13 | # Bootstrapping of services needed for testing, like docker or podman, is done 14 | # via pre.yaml playbook from ansible-tox-molecule job: 15 | # https://github.com/ansible/ansible-zuul-jobs/blob/master/playbooks/ansible-tox-molecule/pre.yaml 16 | # 17 | # test-setup.sh runs before that playbook. 18 | -------------------------------------------------------------------------------- /tools/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR=$(dirname "$0") 3 | VERSION=$(./tools/get-version.sh) 4 | mkdir -p "${DIR}/../dist" 5 | sed -e "s/VERSION_PLACEHOLDER/${VERSION}/" \ 6 | "${DIR}/../.config/molecule.spec" \ 7 | > "${DIR}/../dist/molecule.spec" 8 | --------------------------------------------------------------------------------