diff options
author | cos <cos> | 2024-04-20 09:15:12 +0200 |
---|---|---|
committer | cos <cos> | 2024-04-20 09:44:47 +0200 |
commit | bbed591285e1882d05198e518f75873af58939f5 (patch) | |
tree | c25b33652187f7070d0c2467663c11d6cd4e2326 /test | |
parent | c4f015da4ac75017b97c24ef6601bdd98872e60f (diff) | |
download | debian-ansible-core-upstream/failed-recreation-attempt.zip |
Attempt to recreate upstream branch state from tar filesupstream/failed-recreation-attempt
Unfortunately this was a too naive approach, and the result fails to
build.
Work around that version control is behind the actual package version in
trixie. As is obvious from the lacking commits in the salsa repository
and also visible on https://tracker.debian.org/pkg/ansible-core with the
report from vcswatch stating: VCS repository is not up to date.
This commit contains all changes from ansible-core_2.14.13.orig.tar.gz
to ansible-core_2.16.5.orig.tar.gz, which should hopefully be a squashed
representation on the same set of changes on the uploader's unpushed git
tree.
Diffstat (limited to 'test')
533 files changed, 5466 insertions, 3242 deletions
diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json index 243a5e43..36f402fc 100644 --- a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json +++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json @@ -17,7 +17,7 @@ "version": "0.1.1231", "readme": "README.md", "license_file": "COPYING", - "homepage": "", + "homepage": "" }, "file_manifest_file": { "format": 1, diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py index caec2ed6..dfc12710 100644 --- a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py +++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py @@ -20,7 +20,6 @@ DOCUMENTATION = ''' required: True ''' -from ansible.errors import AnsibleParserError from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable diff --git a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py index d4569869..639d3c6b 100644 --- a/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py +++ b/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py @@ -32,7 +32,8 @@ RETURN = """ version_added: 1.0.0 """ -from ansible.module_utils.common._collections_compat import Sequence +from collections.abc import Sequence + from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleError diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json index 243a5e43..36f402fc 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json @@ -17,7 +17,7 @@ "version": "0.1.1231", "readme": "README.md", "license_file": "COPYING", - "homepage": "", + "homepage": "" }, "file_manifest_file": { "format": 1, diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py index cbb8f0fb..1870b8ea 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py @@ -19,7 +19,6 @@ DOCUMENTATION = ''' required: True ''' -from ansible.errors import AnsibleParserError from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py index 79b7a704..aaaecb80 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py @@ -3,12 +3,17 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: randommodule short_description: A random module description: - A random module. + - See O(foo.bar.baz#role:main:foo=bar) for how this is used in the P(foo.bar.baz#role)'s C(main) entrypoint. + - See L(the docsite,https://docs.ansible.com/ansible-core/devel/) for more information on ansible-core. + - This module is not related to the M(ansible.builtin.copy) module. HORIZONTALLINE You might also be interested + in R(ansible_python_interpreter, ansible_python_interpreter). + - Sometimes you have M(broken markup) that will result in error messages. author: - Ansible Core Team version_added: 1.0.0 @@ -18,22 +23,22 @@ deprecated: removed_in: '3.0.0' options: test: - description: Some text. + description: Some text. Consider not using O(ignore:foo=bar). type: str version_added: 1.2.0 sub: - description: Suboptions. + description: Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this. type: dict suboptions: subtest: - description: A suboption. + description: A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\foo\(1\).txt). type: int version_added: 1.1.0 # The following is the wrong syntax, and should not get processed # by add_collection_to_versions_and_dates() options: subtest2: - description: Another suboption. + description: Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\),d\\]). type: float version_added: 1.1.0 # The following is not supported in modules, and should not get processed @@ -65,7 +70,7 @@ seealso: EXAMPLES = ''' ''' -RETURN = ''' +RETURN = r''' z_last: description: A last result. type: str @@ -75,7 +80,8 @@ z_last: m_middle: description: - This should be in the middle. - - Has some more data + - Has some more data. + - Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value). type: dict returned: success and 1st of month contains: @@ -86,7 +92,7 @@ m_middle: version_added: 1.4.0 a_first: - description: A first result. + description: A first result. Use RV(a_first=foo\(bar\\baz\)bam). type: str returned: success ''' diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml index cc60945e..ebfea2af 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml @@ -8,6 +8,25 @@ DOCUMENTATION: description: does not matter type: raw required: true + seealso: + - module: ansible.builtin.test + - module: testns.testcol.fakemodule + description: A fake module + - plugin: testns.testcol.noop + plugin_type: lookup + - plugin: testns.testcol.grouped + plugin_type: filter + description: A grouped filter. + - plugin: ansible.builtin.combine + plugin_type: filter + - plugin: ansible.builtin.file + plugin_type: lookup + description: Read a file on the controller. + - link: https://docs.ansible.com + name: Ansible docsite + description: See also the Ansible docsite. + - ref: foo_bar + description: Some foo bar. EXAMPLES: | {{ 'anything' is yolo }} diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json index 02ec289f..e930d7d8 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json @@ -17,7 +17,7 @@ "version": "1.2.0", "readme": "README.md", "license_file": "COPYING", - "homepage": "", + "homepage": "" }, "file_manifest_file": { "format": 1, diff --git a/test/integration/targets/ansible-doc/randommodule-text.output b/test/integration/targets/ansible-doc/randommodule-text.output index 602d66ec..ca361346 100644 --- a/test/integration/targets/ansible-doc/randommodule-text.output +++ b/test/integration/targets/ansible-doc/randommodule-text.output @@ -1,6 +1,13 @@ > TESTNS.TESTCOL.RANDOMMODULE (./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py) - A random module. + A random module. See `foo=bar' (of role foo.bar.baz, main + entrypoint) for how this is used in the [foo.bar.baz]'s `main' + entrypoint. See the docsite <https://docs.ansible.com/ansible- + core/devel/> for more information on ansible-core. This module + is not related to the [ansible.builtin.copy] module. + ------------- You might also be interested in + ansible_python_interpreter. Sometimes you have [broken markup] + that will result in error messages. ADDED IN: version 1.0.0 of testns.testcol @@ -14,7 +21,8 @@ DEPRECATED: OPTIONS (= is mandatory): - sub - Suboptions. + Suboptions. Contains `sub.subtest', which can be set to `123'. + You can use `TEST_ENV' to set this. set_via: env: - deprecated: @@ -29,7 +37,8 @@ OPTIONS (= is mandatory): OPTIONS: - subtest2 - Another suboption. + Another suboption. Useful when [ansible.builtin.shuffle] + is used with value `[a,b,),d\]'. default: null type: float added in: version 1.1.0 @@ -39,14 +48,15 @@ OPTIONS (= is mandatory): SUBOPTIONS: - subtest - A suboption. + A suboption. Not compatible to `path=c:\foo(1).txt' (of + module ansible.builtin.copy). default: null type: int added in: version 1.1.0 of testns.testcol - test - Some text. + Some text. Consider not using `foo=bar'. default: null type: str added in: version 1.2.0 of testns.testcol @@ -93,13 +103,15 @@ EXAMPLES: RETURN VALUES: - a_first - A first result. + A first result. Use `a_first=foo(bar\baz)bam'. returned: success type: str - m_middle This should be in the middle. - Has some more data + Has some more data. + Check out `m_middle.suboption' and compare it to `a_first=foo' + and `value' (of lookup plugin community.general.foo). returned: success and 1st of month type: dict diff --git a/test/integration/targets/ansible-doc/randommodule.output b/test/integration/targets/ansible-doc/randommodule.output index cf036000..f40202a8 100644 --- a/test/integration/targets/ansible-doc/randommodule.output +++ b/test/integration/targets/ansible-doc/randommodule.output @@ -12,14 +12,18 @@ "why": "Test deprecation" }, "description": [ - "A random module." + "A random module.", + "See O(foo.bar.baz#role:main:foo=bar) for how this is used in the P(foo.bar.baz#role)'s C(main) entrypoint.", + "See L(the docsite,https://docs.ansible.com/ansible-core/devel/) for more information on ansible-core.", + "This module is not related to the M(ansible.builtin.copy) module. HORIZONTALLINE You might also be interested in R(ansible_python_interpreter, ansible_python_interpreter).", + "Sometimes you have M(broken markup) that will result in error messages." ], "filename": "./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py", "has_action": false, "module": "randommodule", "options": { "sub": { - "description": "Suboptions.", + "description": "Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this.", "env": [ { "deprecated": { @@ -34,14 +38,14 @@ ], "options": { "subtest2": { - "description": "Another suboption.", + "description": "Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\\),d\\\\]).", "type": "float", "version_added": "1.1.0" } }, "suboptions": { "subtest": { - "description": "A suboption.", + "description": "A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\\\foo\\(1\\).txt).", "type": "int", "version_added": "1.1.0", "version_added_collection": "testns.testcol" @@ -50,7 +54,7 @@ "type": "dict" }, "test": { - "description": "Some text.", + "description": "Some text. Consider not using O(ignore:foo=bar).", "type": "str", "version_added": "1.2.0", "version_added_collection": "testns.testcol" @@ -103,7 +107,7 @@ "metadata": null, "return": { "a_first": { - "description": "A first result.", + "description": "A first result. Use RV(a_first=foo\\(bar\\\\baz\\)bam).", "returned": "success", "type": "str" }, @@ -123,7 +127,8 @@ }, "description": [ "This should be in the middle.", - "Has some more data" + "Has some more data.", + "Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value)." ], "returned": "success and 1st of month", "type": "dict" diff --git a/test/integration/targets/ansible-doc/runme.sh b/test/integration/targets/ansible-doc/runme.sh index f51fa8a4..b525766c 100755 --- a/test/integration/targets/ansible-doc/runme.sh +++ b/test/integration/targets/ansible-doc/runme.sh @@ -1,36 +1,74 @@ #!/usr/bin/env bash -set -eux +# always set sane error behaviors, enable execution tracing later if sufficient verbosity requested +set -eu + +verbosity=0 + +# default to silent output for naked grep; -vvv+ will adjust this +export GREP_OPTS=-q + +# shell tracing output is very large from this script; only enable if >= -vvv was passed +while getopts :v opt +do case "$opt" in + v) ((verbosity+=1)) ;; + *) ;; + esac +done + +if (( verbosity >= 3 )); +then + set -x; + export GREP_OPTS= ; +fi + +echo "running playbook-backed docs tests" ansible-playbook test.yml -i inventory "$@" # test keyword docs -ansible-doc -t keyword -l | grep 'vars_prompt: list of variables to prompt for.' -ansible-doc -t keyword vars_prompt | grep 'description: list of variables to prompt for.' -ansible-doc -t keyword asldkfjaslidfhals 2>&1 | grep 'Skipping Invalid keyword' +ansible-doc -t keyword -l | grep $GREP_OPTS 'vars_prompt: list of variables to prompt for.' +ansible-doc -t keyword vars_prompt | grep $GREP_OPTS 'description: list of variables to prompt for.' +ansible-doc -t keyword asldkfjaslidfhals 2>&1 | grep $GREP_OPTS 'Skipping Invalid keyword' # collections testing ( unset ANSIBLE_PLAYBOOK_DIR cd "$(dirname "$0")" -# test module docs from collection + +echo "test fakemodule docs from collection" # we use sed to strip the module path from the first line current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule | sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/')" expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/' fakemodule.output)" test "$current_out" == "$expected_out" +echo "test randommodule docs from collection" # we use sed to strip the plugin path from the first line, and fix-urls.py to unbreak and replace URLs from stable-X branches current_out="$(ansible-doc --playbook-dir ./ testns.testcol.randommodule | sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/' | python fix-urls.py)" expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/' randommodule-text.output)" test "$current_out" == "$expected_out" -# ensure we do work with valid collection name for list -ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep -v "Invalid collection name" +echo "test yolo filter docs from collection" +# we use sed to strip the plugin path from the first line, and fix-urls.py to unbreak and replace URLs from stable-X branches +current_out="$(ansible-doc --playbook-dir ./ testns.testcol.yolo --type test | sed '1 s/\(^> TESTNS\.TESTCOL\.YOLO\).*(.*)$/\1/' | python fix-urls.py)" +expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.YOLO\).*(.*)$/\1/' yolo-text.output)" +test "$current_out" == "$expected_out" + +echo "ensure we do work with valid collection name for list" +ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep $GREP_OPTS -v "Invalid collection name" -# ensure we dont break on invalid collection name for list -ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep "Invalid collection name" +echo "ensure we dont break on invalid collection name for list" +ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep $GREP_OPTS "Invalid collection name" -# test listing diff plugin types from collection +echo "filter list with more than one collection (1/2)" +output=$(ansible-doc --list testns.testcol3 testns.testcol4 --playbook-dir ./ 2>&1 | wc -l) +test "$output" -eq 2 + +echo "filter list with more than one collection (2/2)" +output=$(ansible-doc --list testns.testcol testns.testcol4 --playbook-dir ./ 2>&1 | wc -l) +test "$output" -eq 5 + +echo "testing ansible-doc output for various plugin types" for ptype in cache inventory lookup vars filter module do # each plugin type adds 1 from collection @@ -50,20 +88,20 @@ do elif [ "${ptype}" == "lookup" ]; then expected_names=("noop"); elif [ "${ptype}" == "vars" ]; then expected_names=("noop_vars_plugin"); fi fi - # ensure we ONLY list from the collection + echo "testing collection-filtered list for plugin ${ptype}" justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol|wc -l) test "$justcol" -eq "$expected" - # ensure the right names are displayed + echo "validate collection plugin name display for plugin ${ptype}" list_result=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol) metadata_result=$(ansible-doc --metadata-dump --no-fail-on-errors -t ${ptype} --playbook-dir ./ testns.testcol) for name in "${expected_names[@]}"; do - echo "${list_result}" | grep "testns.testcol.${name}" - echo "${metadata_result}" | grep "testns.testcol.${name}" + echo "${list_result}" | grep $GREP_OPTS "testns.testcol.${name}" + echo "${metadata_result}" | grep $GREP_OPTS "testns.testcol.${name}" done - # ensure we get error if passinginvalid collection, much less any plugins - ansible-doc -l -t ${ptype} testns.testcol 2>&1 | grep "unable to locate collection" + # ensure we get error if passing invalid collection, much less any plugins + ansible-doc -l -t ${ptype} bogus.boguscoll 2>&1 | grep $GREP_OPTS "unable to locate collection" # TODO: do we want per namespace? # ensure we get 1 plugins when restricting namespace @@ -73,20 +111,28 @@ done #### test role functionality -# Test role text output +echo "testing role text output" # we use sed to strip the role path from the first line current_role_out="$(ansible-doc -t role -r ./roles test_role1 | sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/')" expected_role_out="$(sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/' fakerole.output)" test "$current_role_out" == "$expected_role_out" +echo "testing multiple role entrypoints" # Two collection roles are defined, but only 1 has a role arg spec with 2 entry points output=$(ansible-doc -t role -l --playbook-dir . testns.testcol | wc -l) test "$output" -eq 2 +echo "test listing roles with multiple collection filters" +# Two collection roles are defined, but only 1 has a role arg spec with 2 entry points +output=$(ansible-doc -t role -l --playbook-dir . testns.testcol2 testns.testcol | wc -l) +test "$output" -eq 2 + +echo "testing standalone roles" # Include normal roles (no collection filter) output=$(ansible-doc -t role -l --playbook-dir . | wc -l) test "$output" -eq 3 +echo "testing role precedence" # Test that a role in the playbook dir with the same name as a role in the # 'roles' subdir of the playbook dir does not appear (lower precedence). output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from roles subdir") @@ -94,7 +140,7 @@ test "$output" -eq 1 output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from playbook dir" || true) test "$output" -eq 0 -# Test entry point filter +echo "testing role entrypoint filter" current_role_out="$(ansible-doc -t role --playbook-dir . testns.testcol.testrole -e alternate| sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/')" expected_role_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/' fakecollrole.output)" test "$current_role_out" == "$expected_role_out" @@ -103,10 +149,16 @@ test "$current_role_out" == "$expected_role_out" #### test add_collection_to_versions_and_dates() +echo "testing json output" current_out="$(ansible-doc --json --playbook-dir ./ testns.testcol.randommodule | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')" expected_out="$(sed 's/ *"filename": "[^"]*",$//' randommodule.output)" test "$current_out" == "$expected_out" +echo "testing json output 2" +current_out="$(ansible-doc --json --playbook-dir ./ testns.testcol.yolo --type test | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')" +expected_out="$(sed 's/ *"filename": "[^"]*",$//' yolo.output)" +test "$current_out" == "$expected_out" + current_out="$(ansible-doc --json --playbook-dir ./ -t cache testns.testcol.notjsonfile | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')" expected_out="$(sed 's/ *"filename": "[^"]*",$//' notjsonfile.output)" test "$current_out" == "$expected_out" @@ -119,8 +171,9 @@ current_out="$(ansible-doc --json --playbook-dir ./ -t vars testns.testcol.noop_ expected_out="$(sed 's/ *"filename": "[^"]*",$//' noop_vars_plugin.output)" test "$current_out" == "$expected_out" +echo "testing metadata dump" # just ensure it runs -ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir /dev/null >/dev/null +ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir /dev/null 1>/dev/null 2>&1 # create broken role argument spec mkdir -p broken-docs/collections/ansible_collections/testns/testcol/roles/testrole/meta @@ -144,71 +197,72 @@ argument_specs: EOF # ensure that --metadata-dump does not fail when --no-fail-on-errors is supplied -ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --no-fail-on-errors --playbook-dir broken-docs testns.testcol >/dev/null +ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --no-fail-on-errors --playbook-dir broken-docs testns.testcol 1>/dev/null 2>&1 # ensure that --metadata-dump does fail when --no-fail-on-errors is not supplied output=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir broken-docs testns.testcol 2>&1 | grep -c 'ERROR!' || true) test "${output}" -eq 1 -# ensure we list the 'legacy plugins' + +echo "testing legacy plugin listing" [ "$(ansible-doc -M ./library -l ansible.legacy |wc -l)" -gt "0" ] -# playbook dir should work the same +echo "testing legacy plugin list via --playbook-dir" [ "$(ansible-doc -l ansible.legacy --playbook-dir ./|wc -l)" -gt "0" ] -# see that we show undocumented when missing docs +echo "testing undocumented plugin output" [ "$(ansible-doc -M ./library -l ansible.legacy |grep -c UNDOCUMENTED)" == "6" ] -# ensure filtering works and does not include any 'test_' modules +echo "testing filtering does not include any 'test_' modules" [ "$(ansible-doc -M ./library -l ansible.builtin |grep -c test_)" == 0 ] [ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |grep -c test_)" == 0 ] -# ensure filtering still shows modules +echo "testing filtering still shows modules" count=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc -l ansible.builtin |wc -l) [ "${count}" -gt "0" ] [ "$(ansible-doc -M ./library -l ansible.builtin |wc -l)" == "${count}" ] [ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |wc -l)" == "${count}" ] -# produce 'sidecar' docs for test +echo "testing sidecar docs for jinja plugins" [ "$(ansible-doc -t test --playbook-dir ./ testns.testcol.yolo| wc -l)" -gt "0" ] [ "$(ansible-doc -t filter --playbook-dir ./ donothing| wc -l)" -gt "0" ] [ "$(ansible-doc -t filter --playbook-dir ./ ansible.legacy.donothing| wc -l)" -gt "0" ] -# no docs and no sidecar -ansible-doc -t filter --playbook-dir ./ nodocs 2>&1| grep -c 'missing documentation' || true +echo "testing no docs and no sidecar" +ansible-doc -t filter --playbook-dir ./ nodocs 2>&1| grep $GREP_OPTS -c 'missing documentation' || true -# produce 'sidecar' docs for module +echo "testing sidecar docs for module" [ "$(ansible-doc -M ./library test_win_module| wc -l)" -gt "0" ] [ "$(ansible-doc --playbook-dir ./ test_win_module| wc -l)" -gt "0" ] -# test 'double DOCUMENTATION' use +echo "testing duplicate DOCUMENTATION" [ "$(ansible-doc --playbook-dir ./ double_doc| wc -l)" -gt "0" ] -# don't break on module dir +echo "testing don't break on module dir" ansible-doc --list --module-path ./modules > /dev/null -# ensure we dedupe by fqcn and not base name +echo "testing dedupe by fqcn and not base name" [ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64decode')" -eq "3" ] -# ensure we don't show duplicates for plugins that only exist in ansible.builtin when listing ansible.legacy plugins +echo "testing no duplicates for plugins that only exist in ansible.builtin when listing ansible.legacy plugins" [ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64encode')" -eq "1" ] -# with playbook dir, legacy should override -ansible-doc -t filter split --playbook-dir ./ |grep histerical +echo "testing with playbook dir, legacy should override" +ansible-doc -t filter split --playbook-dir ./ |grep $GREP_OPTS histerical pyc_src="$(pwd)/filter_plugins/other.py" pyc_1="$(pwd)/filter_plugins/split.pyc" pyc_2="$(pwd)/library/notaplugin.pyc" trap 'rm -rf "$pyc_1" "$pyc_2"' EXIT -# test pyc files are not used as adjacent documentation +echo "testing pyc files are not used as adjacent documentation" python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_1')" -ansible-doc -t filter split --playbook-dir ./ |grep histerical +ansible-doc -t filter split --playbook-dir ./ |grep $GREP_OPTS histerical -# test pyc files are not listed as plugins +echo "testing pyc files are not listed as plugins" python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_2')" test "$(ansible-doc -l -t module --playbook-dir ./ 2>&1 1>/dev/null |grep -c "notaplugin")" == 0 -# without playbook dir, builtin should return -ansible-doc -t filter split |grep -v histerical +echo "testing without playbook dir, builtin should return" +ansible-doc -t filter split 2>&1 |grep $GREP_OPTS -v histerical diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt b/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt index 110009e3..69218290 100644 --- a/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt +++ b/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt @@ -1,6 +1,11 @@ MANIFEST.json FILES.json README.rst +GPL +LICENSES/ +LICENSES/MIT.txt +.reuse/ +.reuse/dep5 changelogs/ docs/ playbooks/ @@ -88,6 +93,7 @@ plugins/test/bar.yml plugins/test/baz.yaml plugins/test/test.py plugins/vars/bar.yml +plugins/vars/bar.yml.license plugins/vars/baz.yaml plugins/vars/test.py roles/foo/ diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml b/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml index 8f0ada0b..140bf2a7 100644 --- a/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml +++ b/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml @@ -2,6 +2,7 @@ namespace: ns name: col version: 1.0.0 readme: README.rst +license_file: GPL authors: - Ansible manifest: diff --git a/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py b/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py index 913a6f79..60c43cc7 100644 --- a/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py +++ b/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py @@ -5,8 +5,12 @@ paths = [ 'ns-col-1.0.0.tar.gz', 'foo.txt', 'README.rst', + 'GPL', + 'LICENSES/MIT.txt', + '.reuse/dep5', 'artifacts/.gitkeep', 'plugins/vars/bar.yml', + 'plugins/vars/bar.yml.license', 'plugins/vars/baz.yaml', 'plugins/vars/test.py', 'plugins/vars/docs.md', diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml index dab599b1..f0e78ca0 100644 --- a/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml +++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml @@ -5,7 +5,7 @@ - name: Test installing collections from git repositories environment: - ANSIBLE_COLLECTIONS_PATHS: "{{ galaxy_dir }}/collections" + ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/collections" vars: cleanup: True galaxy_dir: "{{ galaxy_dir }}" diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml index f22f9844..91ed9124 100644 --- a/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml +++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml @@ -14,6 +14,8 @@ command: 'ansible-galaxy collection install {{ artifact_path }} -p {{ alt_install_path }} --no-deps' vars: artifact_path: "{{ galaxy_dir }}/ansible_test-collection_1-1.0.0.tar.gz" + environment: + ANSIBLE_COLLECTIONS_PATH: "" - name: check if the files and folders in build_ignore were respected stat: diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml index dd307d72..520dbe5c 100644 --- a/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml +++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml @@ -22,7 +22,12 @@ lineinfile: path: '{{ scm_path }}/namespace_2/collection_2/galaxy.yml' regexp: '^dependencies' - line: "dependencies: {'git+file://{{ scm_path }}/namespace_1/.git#collection_1/': 'master'}" + # NOTE: The committish is set to `HEAD` here because Git's default has + # NOTE: changed to `main` and it behaves differently in + # NOTE: different envs with different Git versions. + line: >- + dependencies: + {'git+file://{{ scm_path }}/namespace_1/.git#collection_1/': 'HEAD'} - name: Commit the changes shell: git add ./; git commit -m 'add collection' diff --git a/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py b/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py index 53c29f77..c1f5e1d7 100644 --- a/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py +++ b/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py @@ -84,7 +84,8 @@ def invoke_api(module, url, method='GET', data=None, status_codes=None): resp, info = fetch_url(module, url, method=method, data=data, headers=headers) if info['status'] not in status_codes: - module.fail_json(url=url, **info) + info['url'] = url + module.fail_json(**info) data = to_text(resp.read()) if data: @@ -105,7 +106,7 @@ def delete_pulp_distribution(distribution, module): def delete_pulp_orphans(module): """ Deletes any orphaned pulp objects. """ - orphan_uri = module.params['pulp_api'] + '/pulp/api/v3/orphans/' + orphan_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/orphans/' task_info = invoke_api(module, orphan_uri, method='DELETE', status_codes=[202]) wait_pulp_task(task_info['task'], module) @@ -125,25 +126,39 @@ def get_galaxy_namespaces(module): return [n['name'] for n in ns_info['data']] -def get_pulp_distributions(module): +def get_pulp_distributions(module, distribution): """ Gets a list of all the pulp distributions. """ - distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/' - distro_info = invoke_api(module, distro_uri) + distro_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/distributions/ansible/ansible/' + distro_info = invoke_api(module, distro_uri + '?name=' + distribution) return [module.params['pulp_api'] + r['pulp_href'] for r in distro_info['results']] -def get_pulp_repositories(module): +def get_pulp_repositories(module, repository): """ Gets a list of all the pulp repositories. """ - repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/' - repo_info = invoke_api(module, repo_uri) + repo_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/repositories/ansible/ansible/' + repo_info = invoke_api(module, repo_uri + '?name=' + repository) return [module.params['pulp_api'] + r['pulp_href'] for r in repo_info['results']] +def get_repo_collections(repository, module): + collections_uri = module.params['galaxy_ng_server'] + 'v3/plugin/ansible/content/' + repository + '/collections/index/' + # status code 500 isn't really expected, an unhandled exception is causing this instead of a 404 + # See https://issues.redhat.com/browse/AAH-2329 + info = invoke_api(module, collections_uri + '?limit=100&offset=0', status_codes=[200, 500]) + if not info: + return [] + return [module.params['pulp_api'] + c['href'] for c in info['data']] + + +def delete_repo_collection(collection, module): + task_info = invoke_api(module, collection, method='DELETE', status_codes=[202]) + wait_pulp_task(task_info['task'], module) + + def new_galaxy_namespace(name, module): """ Creates a new namespace in Galaxy NG. """ - ns_uri = module.params['galaxy_ng_server'] + 'v3/_ui/namespaces/' - data = {'name': name, 'groups': [{'name': 'system:partner-engineers', 'object_permissions': - ['add_namespace', 'change_namespace', 'upload_to_namespace']}]} + ns_uri = module.params['galaxy_ng_server'] + 'v3/namespaces/ ' + data = {'name': name, 'groups': []} ns_info = invoke_api(module, ns_uri, method='POST', data=data, status_codes=[201]) return ns_info['id'] @@ -151,16 +166,17 @@ def new_galaxy_namespace(name, module): def new_pulp_repository(name, module): """ Creates a new pulp repository. """ - repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/' - data = {'name': name} + repo_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/repositories/ansible/ansible/' + # retain_repo_versions to work around https://issues.redhat.com/browse/AAH-2332 + data = {'name': name, 'retain_repo_versions': '1024'} repo_info = invoke_api(module, repo_uri, method='POST', data=data, status_codes=[201]) - return module.params['pulp_api'] + repo_info['pulp_href'] + return repo_info['pulp_href'] def new_pulp_distribution(name, base_path, repository, module): """ Creates a new pulp distribution for a repository. """ - distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/' + distro_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/distributions/ansible/ansible/' data = {'name': name, 'base_path': base_path, 'repository': repository} task_info = invoke_api(module, distro_uri, method='POST', data=data, status_codes=[202]) task_info = wait_pulp_task(task_info['task'], module) @@ -194,8 +210,15 @@ def main(): ) module.params['force_basic_auth'] = True - [delete_pulp_distribution(d, module) for d in get_pulp_distributions(module)] - [delete_pulp_repository(r, module) for r in get_pulp_repositories(module)] + # It may be due to the process of cleaning up orphans, but we cannot delete the namespace + # while a collection still exists, so this is just a new safety to nuke all collections + # first + for repository in module.params['repositories']: + [delete_repo_collection(c, module) for c in get_repo_collections(repository, module)] + + for repository in module.params['repositories']: + [delete_pulp_distribution(d, module) for d in get_pulp_distributions(module, repository)] + [delete_pulp_repository(r, module) for r in get_pulp_repositories(module, repository)] delete_pulp_orphans(module) [delete_galaxy_namespace(n, module) for n in get_galaxy_namespaces(module)] diff --git a/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py b/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py index f4a51c4b..423edd9e 100644 --- a/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py +++ b/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py @@ -77,6 +77,7 @@ RETURN = ''' # ''' +import datetime import os import subprocess import tarfile @@ -84,13 +85,13 @@ import tempfile import yaml from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from functools import partial from multiprocessing import dummy as threading from multiprocessing import TimeoutError -COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT = 120 +COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT = 180 def publish_collection(module, collection): @@ -104,6 +105,7 @@ def publish_collection(module, collection): collection_dir = os.path.join(module.tmpdir, "%s-%s-%s" % (namespace, name, version)) b_collection_dir = to_bytes(collection_dir, errors='surrogate_or_strict') os.mkdir(b_collection_dir) + os.mkdir(os.path.join(b_collection_dir, b'meta')) with open(os.path.join(b_collection_dir, b'README.md'), mode='wb') as fd: fd.write(b"Collection readme") @@ -120,6 +122,8 @@ def publish_collection(module, collection): } with open(os.path.join(b_collection_dir, b'galaxy.yml'), mode='wb') as fd: fd.write(to_bytes(yaml.safe_dump(galaxy_meta), errors='surrogate_or_strict')) + with open(os.path.join(b_collection_dir, b'meta/runtime.yml'), mode='wb') as fd: + fd.write(b'requires_ansible: ">=1.0.0"') with tempfile.NamedTemporaryFile(mode='wb') as temp_fd: temp_fd.write(b"data") @@ -246,7 +250,8 @@ def run_module(): supports_check_mode=False ) - result = dict(changed=True, results=[]) + start = datetime.datetime.now() + result = dict(changed=True, results=[], start=str(start)) pool = threading.Pool(4) publish_func = partial(publish_collection, module) @@ -263,7 +268,9 @@ def run_module(): r['build']['rc'] + r['publish']['rc'] for r in result['results'] )) - module.exit_json(failed=failed, **result) + end = datetime.datetime.now() + delta = end - start + module.exit_json(failed=failed, end=str(end), delta=str(delta), **result) def main(): diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/build.yml b/test/integration/targets/ansible-galaxy-collection/tasks/build.yml index 8140d468..83e9acc9 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/build.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/build.yml @@ -1,4 +1,29 @@ --- +- name: create a dangling symbolic link inside collection directory + ansible.builtin.file: + src: '/non-existent-path/README.md' + dest: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/docs/README.md' + state: link + force: yes + +- name: build basic collection based on current directory with dangling symlink + command: ansible-galaxy collection build {{ galaxy_verbosity }} + args: + chdir: '{{ galaxy_dir }}/scratch/ansible_test/my_collection' + register: fail_symlink_build + ignore_errors: yes + +- name: assert that build fails due to dangling symlink + assert: + that: + - fail_symlink_build.failed + - '"Failed to find the target path" in fail_symlink_build.stderr' + +- name: remove dangling symbolic link + ansible.builtin.file: + path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/docs/README.md' + state: absent + - name: build basic collection based on current directory command: ansible-galaxy collection build {{ galaxy_verbosity }} args: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/download.yml b/test/integration/targets/ansible-galaxy-collection/tasks/download.yml index b651a73e..a554c277 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/download.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/download.yml @@ -5,7 +5,7 @@ state: directory - name: download collection with multiple dependencies with --no-deps - command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 --no-deps -s pulp_v2 {{ galaxy_verbosity }} + command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 --no-deps -s galaxy_ng {{ galaxy_verbosity }} register: download_collection args: chdir: '{{ galaxy_dir }}/download' @@ -34,7 +34,7 @@ - (download_collection_actual.files[1].path | basename) in ['requirements.yml', 'parent_dep-parent_collection-1.0.0.tar.gz'] - name: download collection with multiple dependencies - command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 -s pulp_v2 {{ galaxy_verbosity }} + command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 -s galaxy_ng {{ galaxy_verbosity }} register: download_collection args: chdir: '{{ galaxy_dir }}/download' diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml index eb471f8e..d861cb4d 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml @@ -1,5 +1,5 @@ # resolvelib>=0.6.0 added an 'incompatibilities' parameter to find_matches -# If incompatibilities aren't removed from the viable candidates, this example causes infinite resursion +# If incompatibilities aren't removed from the viable candidates, this example causes infinite recursion - name: test resolvelib removes incompatibilites in find_matches and errors quickly (prevent infinite recursion) block: - name: create collection dir diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/init.yml b/test/integration/targets/ansible-galaxy-collection/tasks/init.yml index 17a000db..46198fef 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/init.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/init.yml @@ -5,6 +5,12 @@ chdir: '{{ galaxy_dir }}/scratch' register: init_relative +- name: create required runtime.yml + copy: + content: | + requires_ansible: '>=1.0.0' + dest: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/meta/runtime.yml' + - name: get result of create default skeleton find: path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection' @@ -92,6 +98,65 @@ - (init_custom_path_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta'] - (init_custom_path_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta'] +- name: test using a custom skeleton for collection init + block: + - name: create skeleton directories + file: + path: "{{ galaxy_dir }}/scratch/skeleton/{{ item }}" + state: directory + loop: + - custom_skeleton + - custom_skeleton/plugins + - inventory + + - name: create files + file: + path: "{{ galaxy_dir }}/scratch/skeleton/{{ item }}" + state: touch + loop: + - inventory/foo.py + - galaxy.yml + + - name: create symlinks + file: + path: "{{ galaxy_dir }}/scratch/skeleton/{{ item.link }}" + src: "{{ galaxy_dir }}/scratch/skeleton/{{ item.source }}" + state: link + loop: + - link: custom_skeleton/plugins/inventory + source: inventory + - link: custom_skeleton/galaxy.yml + source: galaxy.yml + + - name: initialize a collection using the skeleton + command: ansible-galaxy collection init ansible_test.my_collection {{ init_path }} {{ skeleton }} + vars: + init_path: '--init-path {{ galaxy_dir }}/scratch/skeleton' + skeleton: '--collection-skeleton {{ galaxy_dir }}/scratch/skeleton/custom_skeleton' + + - name: stat expected collection contents + stat: + path: "{{ galaxy_dir }}/scratch/skeleton/ansible_test/my_collection/{{ item }}" + register: stat_result + loop: + - plugins + - plugins/inventory + - galaxy.yml + - plugins/inventory/foo.py + + - assert: + that: + - stat_result.results[0].stat.isdir + - stat_result.results[1].stat.islnk + - stat_result.results[2].stat.islnk + - stat_result.results[3].stat.isreg + + always: + - name: cleanup + file: + path: "{{ galaxy_dir }}/scratch/skeleton" + state: absent + - name: create collection for ignored files and folders command: ansible-galaxy collection init ansible_test.ignore args: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/install.yml b/test/integration/targets/ansible-galaxy-collection/tasks/install.yml index cca83c7b..92378266 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/install.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/install.yml @@ -165,10 +165,13 @@ failed_when: - '"Could not satisfy the following requirements" not in fail_dep_mismatch.stderr' - '" fail_dep2.name:<0.0.5 (dependency of fail_namespace.fail_collection:2.1.2)" not in fail_dep_mismatch.stderr' + - 'pre_release_hint not in fail_dep_mismatch.stderr' + vars: + pre_release_hint: 'Hint: Pre-releases are not installed by default unless the specific version is given. To enable pre-releases, use --pre.' - name: Find artifact url for namespace3.name uri: - url: '{{ test_server }}{{ vX }}collections/namespace3/name/versions/1.0.0/' + url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/namespace3/name/versions/1.0.0/' user: '{{ pulp_user }}' password: '{{ pulp_password }}' force_basic_auth: true @@ -218,7 +221,7 @@ state: absent - assert: - that: error == expected_error + that: expected_error in error vars: error: "{{ result.stderr | regex_replace('\\n', ' ') }}" expected_error: >- @@ -258,12 +261,14 @@ ignore_errors: yes register: result - - debug: msg="Actual - {{ error }}" + - debug: + msg: "Actual - {{ error }}" - - debug: msg="Expected - {{ expected_error }}" + - debug: + msg: "Expected - {{ expected_error }}" - assert: - that: error == expected_error + that: expected_error in error always: - name: clean up collection skeleton and artifact file: @@ -295,7 +300,7 @@ - name: Find artifact url for namespace4.name uri: - url: '{{ test_server }}{{ vX }}collections/namespace4/name/versions/1.0.0/' + url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/namespace4/name/versions/1.0.0/' user: '{{ pulp_user }}' password: '{{ pulp_password }}' force_basic_auth: true @@ -325,10 +330,11 @@ environment: ANSIBLE_GALAXY_SERVER_LIST: undefined -- when: not requires_auth +# pulp_v2 doesn't require auth +- when: v2|default(false) block: - name: install a collection with an empty server list - {{ test_id }} - command: ansible-galaxy collection install namespace5.name -s '{{ test_server }}' {{ galaxy_verbosity }} + command: ansible-galaxy collection install namespace5.name -s '{{ test_server }}' --api-version 2 {{ galaxy_verbosity }} register: install_empty_server_list environment: ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' @@ -571,7 +577,6 @@ - namespace8 - namespace9 -# SIVEL - name: assert invalid signature is not fatal with ansible-galaxy install --ignore-errors - {{ test_id }} assert: that: @@ -646,6 +651,7 @@ - namespace8 - namespace9 +# test --ignore-signature-status-code extends ANSIBLE_GALAXY_IGNORE_SIGNATURE_STATUS_CODES env var - name: install collections with only one valid signature by ignoring the other errors command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} --ignore-signature-status-code FAILURE register: install_req @@ -686,6 +692,60 @@ vars: install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}" +# test --ignore-signature-status-code passed multiple times +- name: reinstall collections with only one valid signature by ignoring the other errors + command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} {{ ignore_errors }} + register: install_req + vars: + req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml" + cli_opts: "-s {{ test_name }} --keyring {{ keyring }} --force" + keyring: "{{ gpg_homedir }}/pubring.kbx" + ignore_errors: "--ignore-signature-status-code BADSIG --ignore-signature-status-code FAILURE" + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + +- name: assert invalid signature is not fatal with ansible-galaxy install - {{ test_name }} + assert: + that: + - install_req is success + - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout' + - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout' + - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr' + - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout' + - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout' + vars: + install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}" + +# test --ignore-signature-status-code passed once with a list +- name: reinstall collections with only one valid signature by ignoring the other errors + command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} {{ ignore_errors }} + register: install_req + vars: + req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml" + cli_opts: "-s {{ test_name }} --keyring {{ keyring }} --force" + keyring: "{{ gpg_homedir }}/pubring.kbx" + ignore_errors: "--ignore-signature-status-codes BADSIG FAILURE" + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + +- name: assert invalid signature is not fatal with ansible-galaxy install - {{ test_name }} + assert: + that: + - install_req is success + - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout' + - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout' + - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr' + - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout' + - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout' + vars: + install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}" + - name: clean up collections from last test file: path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name' @@ -697,44 +757,45 @@ - namespace8 - namespace9 -# Uncomment once pulp container is at pulp>=0.5.0 -#- name: install cache.cache at the current latest version -# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' -vvv -# environment: -# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' -# -#- set_fact: -# cache_version_build: '{{ (cache_version_build | int) + 1 }}' -# -#- name: publish update for cache.cache test -# setup_collections: -# server: galaxy_ng -# collections: -# - namespace: cache -# name: cache -# version: 1.0.{{ cache_version_build }} -# -#- name: make sure the cache version list is ignored on a collection version change - {{ test_id }} -# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' --force -vvv -# register: install_cached_update -# environment: -# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' -# -#- name: get result of cache version list is ignored on a collection version change - {{ test_id }} -# slurp: -# path: '{{ galaxy_dir }}/ansible_collections/cache/cache/MANIFEST.json' -# register: install_cached_update_actual -# -#- name: assert cache version list is ignored on a collection version change - {{ test_id }} -# assert: -# that: -# - '"Installing ''cache.cache:1.0.{{ cache_version_build }}'' to" in install_cached_update.stdout' -# - (install_cached_update_actual.content | b64decode | from_json).collection_info.version == '1.0.' ~ cache_version_build +- when: not v2|default(false) + block: + - name: install cache.cache at the current latest version + command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' -vvv + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + + - set_fact: + cache_version_build: '{{ (cache_version_build | int) + 1 }}' + + - name: publish update for cache.cache test + setup_collections: + server: galaxy_ng + collections: + - namespace: cache + name: cache + version: 1.0.{{ cache_version_build }} + + - name: make sure the cache version list is ignored on a collection version change - {{ test_id }} + command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' --force -vvv + register: install_cached_update + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + + - name: get result of cache version list is ignored on a collection version change - {{ test_id }} + slurp: + path: '{{ galaxy_dir }}/ansible_collections/cache/cache/MANIFEST.json' + register: install_cached_update_actual + + - name: assert cache version list is ignored on a collection version change - {{ test_id }} + assert: + that: + - '"Installing ''cache.cache:1.0.{{ cache_version_build }}'' to" in install_cached_update.stdout' + - (install_cached_update_actual.content | b64decode | from_json).collection_info.version == '1.0.' ~ cache_version_build - name: install collection with symlink - {{ test_id }} command: ansible-galaxy collection install symlink.symlink -s '{{ test_name }}' {{ galaxy_verbosity }} environment: - ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' register: install_symlink - find: @@ -772,6 +833,56 @@ - install_symlink_actual.results[5].stat.islnk - install_symlink_actual.results[5].stat.lnk_target == '../REÅDMÈ.md' + +# Testing an install from source to check that symlinks to directories +# are preserved (see issue https://github.com/ansible/ansible/issues/78442) +- name: symlink_dirs collection install from source test + block: + + - name: create symlink_dirs collection + command: ansible-galaxy collection init symlink_dirs.symlink_dirs --init-path "{{ galaxy_dir }}/scratch" + + - name: create directory in collection + file: + path: "{{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/folderA" + state: directory + + - name: create symlink to folderA + file: + dest: "{{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/folderB" + src: ./folderA + state: link + force: yes + + - name: install symlink_dirs collection from source + command: ansible-galaxy collection install {{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/ + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + register: install_symlink_dirs + + - name: get result of install collection with symlink_dirs - {{ test_id }} + stat: + path: '{{ galaxy_dir }}/ansible_collections/symlink_dirs/symlink_dirs/{{ path }}' + register: install_symlink_dirs_actual + loop_control: + loop_var: path + loop: + - folderA + - folderB + + - name: assert install collection with symlink_dirs - {{ test_id }} + assert: + that: + - '"Installing ''symlink_dirs.symlink_dirs:1.0.0'' to" in install_symlink_dirs.stdout' + - install_symlink_dirs_actual.results[0].stat.isdir + - install_symlink_dirs_actual.results[1].stat.islnk + - install_symlink_dirs_actual.results[1].stat.lnk_target == './folderA' + always: + - name: clean up symlink_dirs collection directory + file: + path: "{{ galaxy_dir }}/scratch/symlink_dirs" + state: absent + - name: remove install directory for the next test because parent_dep.parent_collection was installed - {{ test_id }} file: path: '{{ galaxy_dir }}/ansible_collections' @@ -780,7 +891,7 @@ - name: install collection and dep compatible with multiple requirements - {{ test_id }} command: ansible-galaxy collection install parent_dep.parent_collection parent_dep2.parent_collection environment: - ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' register: install_req - name: assert install collections with ansible-galaxy install - {{ test_id }} @@ -802,7 +913,7 @@ - name: install a collection to the same installation directory - {{ test_id }} command: ansible-galaxy collection install namespace1.name1 environment: - ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' register: install_req - name: assert installed collections with ansible-galaxy install - {{ test_id }} @@ -1009,7 +1120,7 @@ args: chdir: '{{ galaxy_dir }}/scratch' environment: - ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections' + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' register: install_concrete_pre - name: get result of install collections with concrete pre-release dep - {{ test_id }} diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml b/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml index 74c99838..f3b9777c 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml @@ -25,6 +25,14 @@ regexp: "^dependencies:*" line: "dependencies: {'ns.coll2': '>=1.0.0'}" + - name: create required runtime.yml + copy: + dest: "{{ galaxy_dir }}/offline/setup/ns/{{ item }}/meta/runtime.yml" + content: "requires_ansible: '>=1.0.0'" + loop: + - coll1 + - coll2 + - name: build both collections command: ansible-galaxy collection build {{ init_dir }}/ns/{{ item }} args: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/list.yml b/test/integration/targets/ansible-galaxy-collection/tasks/list.yml index b8d63492..1c93d54b 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/list.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/list.yml @@ -1,4 +1,4 @@ -- name: initialize collection structure +- name: initialize dev collection structure command: ansible-galaxy collection init {{ item }} --init-path "{{ galaxy_dir }}/dev/ansible_collections" {{ galaxy_verbosity }} loop: - 'dev.collection1' @@ -8,6 +8,13 @@ - 'dev.collection5' - 'dev.collection6' +- name: initialize prod collection structure + command: ansible-galaxy collection init {{ item }} --init-path "{{ galaxy_dir }}/prod/ansible_collections" {{ galaxy_verbosity }} + loop: + - 'prod.collection1' + - 'prod.collection2' + - 'prod.collection3' + - name: replace the default version of the collections lineinfile: path: "{{ galaxy_dir }}/dev/ansible_collections/dev/{{ item.name }}/galaxy.yml" @@ -53,13 +60,13 @@ - assert: that: - - "'dev.collection1 *' in list_result.stdout" + - "'dev.collection1 *' in list_result.stdout" # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey - - "'dev.collection2 placeholder' in list_result.stdout" - - "'dev.collection3 *' in list_result.stdout" - - "'dev.collection4 *' in list_result.stdout" - - "'dev.collection5 *' in list_result.stdout" - - "'dev.collection6 *' in list_result.stdout" + - "'dev.collection2 placeholder' in list_result.stdout" + - "'dev.collection3 *' in list_result.stdout" + - "'dev.collection4 *' in list_result.stdout" + - "'dev.collection5 *' in list_result.stdout" + - "'dev.collection6 *' in list_result.stdout" - name: list collections in human format command: ansible-galaxy collection list --format human @@ -69,12 +76,12 @@ - assert: that: - - "'dev.collection1 *' in list_result_human.stdout" + - "'dev.collection1 *' in list_result_human.stdout" # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey - - "'dev.collection2 placeholder' in list_result_human.stdout" - - "'dev.collection3 *' in list_result_human.stdout" - - "'dev.collection5 *' in list_result.stdout" - - "'dev.collection6 *' in list_result.stdout" + - "'dev.collection2 placeholder' in list_result_human.stdout" + - "'dev.collection3 *' in list_result_human.stdout" + - "'dev.collection5 *' in list_result.stdout" + - "'dev.collection6 *' in list_result.stdout" - name: list collections in yaml format command: ansible-galaxy collection list --format yaml @@ -84,6 +91,12 @@ - assert: that: + - yaml_result[galaxy_dir ~ '/dev/ansible_collections'] != yaml_result[galaxy_dir ~ '/prod/ansible_collections'] + vars: + yaml_result: '{{ list_result_yaml.stdout | from_yaml }}' + +- assert: + that: - "item.value | length == 6" - "item.value['dev.collection1'].version == '*'" - "item.value['dev.collection2'].version == 'placeholder'" @@ -91,6 +104,7 @@ - "item.value['dev.collection5'].version == '*'" - "item.value['dev.collection6'].version == '*'" with_dict: "{{ list_result_yaml.stdout | from_yaml }}" + when: "'dev' in item.key" - name: list collections in json format command: ansible-galaxy collection list --format json @@ -107,6 +121,7 @@ - "item.value['dev.collection5'].version == '*'" - "item.value['dev.collection6'].version == '*'" with_dict: "{{ list_result_json.stdout | from_json }}" + when: "'dev' in item.key" - name: list single collection in json format command: "ansible-galaxy collection list {{ item.key }} --format json" @@ -137,7 +152,7 @@ register: list_result_error ignore_errors: True environment: - ANSIBLE_COLLECTIONS_PATH: "" + ANSIBLE_COLLECTIONS_PATH: "i_dont_exist" - assert: that: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/main.yml b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml index 724c861e..e17d6aa1 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/main.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/main.yml @@ -72,13 +72,12 @@ vars: test_name: '{{ item.name }}' test_server: '{{ item.server }}' - vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}' + test_api_server: '{{ item.api_server|default(item.server) }}' loop: - name: pulp_v2 - server: '{{ pulp_server }}published/api/' - - name: pulp_v3 - server: '{{ pulp_server }}published/api/' - v3: true + api_server: '{{ galaxy_ng_server }}' + server: '{{ pulp_server }}primary/api/' + v2: true - name: galaxy_ng server: '{{ galaxy_ng_server }}' v3: true @@ -108,8 +107,9 @@ test_id: '{{ item.name }}' test_name: '{{ item.name }}' test_server: '{{ item.server }}' - vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}' + test_api_server: '{{ item.api_server|default(item.server) }}' requires_auth: '{{ item.requires_auth|default(false) }}' + v2: '{{ item.v2|default(false) }}' args: apply: environment: @@ -120,10 +120,9 @@ v3: true requires_auth: true - name: pulp_v2 - server: '{{ pulp_server }}published/api/' - - name: pulp_v3 - server: '{{ pulp_server }}published/api/' - v3: true + server: '{{ pulp_server }}primary/api/' + api_server: '{{ galaxy_ng_server }}' + v2: true - name: test installing and downloading collections with the range of supported resolvelib versions include_tasks: supported_resolvelib.yml @@ -135,6 +134,17 @@ loop_control: loop_var: resolvelib_version +- name: test choosing pinned pre-releases anywhere in the dependency tree + # This is a regression test for the case when the end-user does not + # explicitly allow installing pre-release collection versions, but their + # precise pins are still selected if met among the dependencies, either + # direct or transitive. + include_tasks: pinned_pre_releases_in_deptree.yml + +- name: test installing prereleases via scm direct requests + # In this test suite because the bug relies on the dep having versions on a Galaxy server + include_tasks: virtual_direct_requests.yml + - name: publish collection with a dep on another server setup_collections: server: secondary @@ -176,13 +186,13 @@ in install_cross_dep.stdout # pulp_v2 is highest in the list so it will find it there first - >- - "'parent_dep.parent_collection:1.0.0' obtained from server pulp_v2" + "'parent_dep.parent_collection:1.0.0' obtained from server galaxy_ng" in install_cross_dep.stdout - >- - "'child_dep.child_collection:0.9.9' obtained from server pulp_v2" + "'child_dep.child_collection:0.9.9' obtained from server galaxy_ng" in install_cross_dep.stdout - >- - "'child_dep.child_dep2:1.2.2' obtained from server pulp_v2" + "'child_dep.child_dep2:1.2.2' obtained from server galaxy_ng" in install_cross_dep.stdout - (install_cross_dep_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0' - (install_cross_dep_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0' @@ -204,10 +214,9 @@ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}' ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg' vars: - test_api_fallback: 'pulp_v2' - test_api_fallback_versions: 'v1, v2' - test_name: 'galaxy_ng' - test_server: '{{ galaxy_ng_server }}' + test_api_fallback: 'galaxy_ng' + test_api_fallback_versions: 'v3, pulp-v3, v1' + test_name: 'pulp_v2' - name: run ansible-galaxy collection list tests include_tasks: list.yml diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml b/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml index 241eae60..1be16ae9 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml @@ -5,9 +5,12 @@ chdir: '{{ galaxy_dir }}' register: publish_collection +- name: ensure we can download the published collection - {{ test_name }} + command: ansible-galaxy collection install -s {{ test_name }} -p "{{ remote_tmp_dir }}/publish/{{ test_name }}" ansible_test.my_collection==1.0.0 {{ galaxy_verbosity }} + - name: get result of publish collection - {{ test_name }} uri: - url: '{{ test_server }}{{ vX }}collections/ansible_test/my_collection/versions/1.0.0/' + url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/ansible_test/my_collection/versions/1.0.0/' return_content: yes user: '{{ pulp_user }}' password: '{{ pulp_password }}' diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml b/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml index 763c5a19..bff36892 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml @@ -20,11 +20,11 @@ - include_tasks: install.yml vars: - test_name: pulp_v3 + test_name: galaxy_ng test_id: '{{ test_name }} (resolvelib {{ resolvelib_version }})' - test_server: '{{ pulp_server }}published/api/' - vX: "v3/" - requires_auth: false + test_server: '{{ galaxy_ng_server }}' + test_api_server: '{{ galaxy_ng_server }}' + requires_auth: true args: apply: environment: diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml b/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml index 893ea803..debd70bc 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml @@ -142,7 +142,7 @@ - directory - name: install a collection - command: ansible-galaxy collection install namespace1.name1:0.0.1 {{ galaxy_verbosity }} + command: ansible-galaxy collection install namespace1.name1==0.0.1 {{ galaxy_verbosity }} register: result failed_when: - '"namespace1.name1:0.0.1 was installed successfully" not in result.stdout_lines' diff --git a/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml b/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml index dfe3d0f7..0fe2f82d 100644 --- a/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml +++ b/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml @@ -3,6 +3,11 @@ args: chdir: '{{ galaxy_dir }}/scratch' +- name: created required runtime.yml + copy: + content: 'requires_ansible: ">=1.0.0"' + dest: '{{ galaxy_dir }}/scratch/ansible_test/verify/meta/runtime.yml' + - name: build the collection command: ansible-galaxy collection build scratch/ansible_test/verify args: @@ -31,6 +36,9 @@ - name: verify the collection against the first valid server command: ansible-galaxy collection verify ansible_test.verify:1.0.0 -vvvv {{ galaxy_verbosity }} register: verify + vars: + # This sets a specific precedence that the tests are expecting + ANSIBLE_GALAXY_SERVER_LIST: offline,secondary,pulp_v2,galaxy_ng - assert: that: diff --git a/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 b/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 index 9bff527b..a242979d 100644 --- a/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 +++ b/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 @@ -1,28 +1,22 @@ [galaxy] # Ensures subsequent unstable reruns don't use the cached information causing another failure cache_dir={{ remote_tmp_dir }}/galaxy_cache -server_list=offline,pulp_v2,pulp_v3,galaxy_ng,secondary +server_list=offline,galaxy_ng,secondary,pulp_v2 [galaxy_server.offline] url={{ offline_server }} [galaxy_server.pulp_v2] -url={{ pulp_server }}published/api/ -username={{ pulp_user }} -password={{ pulp_password }} - -[galaxy_server.pulp_v3] -url={{ pulp_server }}published/api/ -v3=true +url={{ pulp_server }}primary/api/ username={{ pulp_user }} password={{ pulp_password }} +api_version=2 [galaxy_server.galaxy_ng] -url={{ galaxy_ng_server }} +url={{ galaxy_ng_server }}content/primary/ token={{ galaxy_ng_token.json.token }} [galaxy_server.secondary] -url={{ pulp_server }}secondary/api/ -v3=true +url={{ galaxy_ng_server }}content/secondary/ username={{ pulp_user }} password={{ pulp_password }} diff --git a/test/integration/targets/ansible-galaxy-collection/vars/main.yml b/test/integration/targets/ansible-galaxy-collection/vars/main.yml index 175d6696..066d2678 100644 --- a/test/integration/targets/ansible-galaxy-collection/vars/main.yml +++ b/test/integration/targets/ansible-galaxy-collection/vars/main.yml @@ -9,17 +9,20 @@ supported_resolvelib_versions: - "0.6.0" - "0.7.0" - "0.8.0" + - "0.9.0" + - "1.0.1" unsupported_resolvelib_versions: - "0.2.0" # Fails on import - "0.5.1" pulp_repositories: - - published + - primary - secondary publish_namespaces: - ansible_test + - secondary collection_list: # Scenario to test out pre-release being ignored unless explicitly set and version pagination. @@ -162,3 +165,41 @@ collection_list: name: parent dependencies: namespace1.name1: '*' + + # non-prerelease is published to test that installing + # the pre-release from SCM doesn't accidentally prefer indirect + # dependencies from Galaxy + - namespace: test_prereleases + name: collection2 + version: 1.0.0 + + - namespace: dev_and_stables_ns + name: dev_and_stables_name + version: 1.2.3-dev0 + - namespace: dev_and_stables_ns + name: dev_and_stables_name + version: 1.2.4 + + - namespace: ns_with_wildcard_dep + name: name_with_wildcard_dep + version: 5.6.7-beta.3 + dependencies: + dev_and_stables_ns.dev_and_stables_name: >- + * + - namespace: ns_with_dev_dep + name: name_with_dev_dep + version: 6.7.8 + dependencies: + dev_and_stables_ns.dev_and_stables_name: 1.2.3-dev0 + + - namespace: rc_meta_ns_with_transitive_dev_dep + name: rc_meta_name_with_transitive_dev_dep + version: 2.4.5-rc5 + dependencies: + ns_with_dev_dep.name_with_dev_dep: >- + * + - namespace: meta_ns_with_transitive_wildcard_dep + name: meta_name_with_transitive_wildcard_dep + version: 4.5.6 + dependencies: + ns_with_wildcard_dep.name_with_wildcard_dep: 5.6.7-beta.3 diff --git a/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py b/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py index cfd908c1..48766638 100755 --- a/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py +++ b/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py @@ -2,6 +2,7 @@ """Create a role archive which overwrites an arbitrary file.""" import argparse +import os import pathlib import tarfile import tempfile @@ -18,6 +19,15 @@ def main() -> None: create_archive(args.archive, args.content, args.target) +def generate_files_from_path(path): + if os.path.isdir(path): + for subpath in os.listdir(path): + _path = os.path.join(path, subpath) + yield from generate_files_from_path(_path) + elif os.path.isfile(path): + yield pathlib.Path(path) + + def create_archive(archive_path: pathlib.Path, content_path: pathlib.Path, target_path: pathlib.Path) -> None: with ( tarfile.open(name=archive_path, mode='w') as role_archive, @@ -35,10 +45,15 @@ def create_archive(archive_path: pathlib.Path, content_path: pathlib.Path, targe role_archive.add(meta_main_path) role_archive.add(symlink_path) - content_tarinfo = role_archive.gettarinfo(content_path, str(symlink_path)) + for path in generate_files_from_path(content_path): + if path == content_path: + arcname = str(symlink_path) + else: + arcname = os.path.join(temp_dir_path, path) - with content_path.open('rb') as content_file: - role_archive.addfile(content_tarinfo, content_file) + content_tarinfo = role_archive.gettarinfo(path, arcname) + with path.open('rb') as file_content: + role_archive.addfile(content_tarinfo, file_content) if __name__ == '__main__': diff --git a/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml b/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml index c70e8998..1c17daf7 100644 --- a/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml +++ b/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml @@ -23,6 +23,9 @@ command: cmd: ansible-galaxy role install --roles-path '{{ remote_tmp_dir }}/dir-traversal/roles' dangerous.tar chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + environment: + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False ignore_errors: true register: galaxy_install_dangerous @@ -42,3 +45,86 @@ - dangerous_overwrite_content.content|default('')|b64decode == '' - not dangerous_overwrite_stat.stat.exists - galaxy_install_dangerous is failed + - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))" + +- name: remove tarfile for next test + file: + path: '{{ item }}' + state: absent + loop: + - '{{ remote_tmp_dir }}/dir-traversal/source/dangerous.tar' + - '{{ remote_tmp_dir }}/dir-traversal/roles/dangerous.tar' + +- name: build dangerous dir traversal role that includes .. in the symlink path + script: + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + cmd: create-role-archive.py dangerous.tar content.txt {{ remote_tmp_dir }}/dir-traversal/source/../target/target-file-to-overwrite.txt + executable: '{{ ansible_playbook_python }}' + +- name: install dangerous role + command: + cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles dangerous.tar' + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + environment: + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + ignore_errors: true + register: galaxy_install_dangerous + +- name: check for overwritten file + stat: + path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt' + register: dangerous_overwrite_stat + +- name: get overwritten content + slurp: + path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt' + register: dangerous_overwrite_content + when: dangerous_overwrite_stat.stat.exists + +- assert: + that: + - dangerous_overwrite_content.content|default('')|b64decode == '' + - not dangerous_overwrite_stat.stat.exists + - galaxy_install_dangerous is failed + - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))" + +- name: remove tarfile for next test + file: + path: '{{ remote_tmp_dir }}/dir-traversal/source/dangerous.tar' + state: absent + +- name: build dangerous dir traversal role that includes .. in the relative symlink path + script: + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + cmd: create-role-archive.py dangerous_rel.tar content.txt ../context.txt + +- name: install dangerous role with relative symlink + command: + cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles dangerous_rel.tar' + chdir: '{{ remote_tmp_dir }}/dir-traversal/source' + environment: + ANSIBLE_NOCOLOR: True + ANSIBLE_FORCE_COLOR: False + ignore_errors: true + register: galaxy_install_dangerous + +- name: check for symlink outside role + stat: + path: "{{ remote_tmp_dir | realpath }}/dir-traversal/roles/symlink" + register: symlink_outside_role + +- assert: + that: + - not symlink_outside_role.stat.exists + - galaxy_install_dangerous is failed + - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))" + +- name: remove test directories + file: + path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}' + state: absent + loop: + - source + - target + - roles diff --git a/test/integration/targets/ansible-galaxy-role/tasks/main.yml b/test/integration/targets/ansible-galaxy-role/tasks/main.yml index b39df11c..5f88a557 100644 --- a/test/integration/targets/ansible-galaxy-role/tasks/main.yml +++ b/test/integration/targets/ansible-galaxy-role/tasks/main.yml @@ -25,10 +25,18 @@ - name: Valid role archive command: "tar cf {{ remote_tmp_dir }}/valid-role.tar {{ remote_tmp_dir }}/role.d" -- name: Invalid file - copy: - content: "" +- name: Add invalid symlink + file: + state: link + src: "~/invalid" dest: "{{ remote_tmp_dir }}/role.d/tasks/~invalid.yml" + force: yes + +- name: Add another invalid symlink + file: + state: link + src: "/" + dest: "{{ remote_tmp_dir }}/role.d/tasks/invalid$name.yml" - name: Valid requirements file copy: @@ -61,3 +69,4 @@ command: ansible-galaxy role remove invalid-testrole - import_tasks: dir-traversal.yml +- import_tasks: valid-role-symlinks.yml diff --git a/test/integration/targets/ansible-galaxy/files/testserver.py b/test/integration/targets/ansible-galaxy/files/testserver.py index 13598507..8cca6a83 100644 --- a/test/integration/targets/ansible-galaxy/files/testserver.py +++ b/test/integration/targets/ansible-galaxy/files/testserver.py @@ -1,20 +1,15 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import sys +import http.server +import socketserver import ssl if __name__ == '__main__': - if sys.version_info[0] >= 3: - import http.server - import socketserver - Handler = http.server.SimpleHTTPRequestHandler - httpd = socketserver.TCPServer(("", 4443), Handler) - else: - import BaseHTTPServer - import SimpleHTTPServer - Handler = SimpleHTTPServer.SimpleHTTPRequestHandler - httpd = BaseHTTPServer.HTTPServer(("", 4443), Handler) + Handler = http.server.SimpleHTTPRequestHandler + context = ssl.SSLContext() + context.load_cert_chain(certfile='./cert.pem', keyfile='./key.pem') + httpd = socketserver.TCPServer(("", 4443), Handler) + httpd.socket = context.wrap_socket(httpd.socket, server_side=True) - httpd.socket = ssl.wrap_socket(httpd.socket, certfile='./cert.pem', keyfile='./key.pem', server_side=True) httpd.serve_forever() diff --git a/test/integration/targets/ansible-galaxy/runme.sh b/test/integration/targets/ansible-galaxy/runme.sh index 7d966e29..fcd826c3 100755 --- a/test/integration/targets/ansible-galaxy/runme.sh +++ b/test/integration/targets/ansible-galaxy/runme.sh @@ -61,10 +61,13 @@ f_ansible_galaxy_create_role_repo_post() git add . git commit -m "local testing ansible galaxy role" + # NOTE: `HEAD` is used because the newer Git versions create + # NOTE: `main` by default and the older ones differ. We + # NOTE: want to avoid hardcoding them. git archive \ --format=tar \ --prefix="${repo_name}/" \ - master > "${repo_tar}" + HEAD > "${repo_tar}" # Configure basic (insecure) HTTPS-accessible repository galaxy_local_test_role_http_repo="${galaxy_webserver_root}/${galaxy_local_test_role}.git" if [[ ! -d "${galaxy_local_test_role_http_repo}" ]]; then @@ -354,7 +357,7 @@ pushd "${galaxy_testdir}" popd # ${galaxy_testdir} f_ansible_galaxy_status \ - "role info non-existant role" + "role info non-existent role" mkdir -p "${role_testdir}" pushd "${role_testdir}" diff --git a/test/integration/targets/ansible-inventory/files/valid_sample.yml b/test/integration/targets/ansible-inventory/files/valid_sample.yml index 477f82f2..b8e7b882 100644 --- a/test/integration/targets/ansible-inventory/files/valid_sample.yml +++ b/test/integration/targets/ansible-inventory/files/valid_sample.yml @@ -4,4 +4,4 @@ all: hosts: something: foo: bar - ungrouped: {}
\ No newline at end of file + ungrouped: {} diff --git a/test/integration/targets/ansible-inventory/tasks/main.yml b/test/integration/targets/ansible-inventory/tasks/main.yml index 84ac2c3c..c3459c12 100644 --- a/test/integration/targets/ansible-inventory/tasks/main.yml +++ b/test/integration/targets/ansible-inventory/tasks/main.yml @@ -145,3 +145,10 @@ loop_control: loop_var: toml_package when: toml_package is not contains 'tomllib' or (toml_package is contains 'tomllib' and ansible_facts.python.version_info >= [3, 11]) + + +- include_tasks: "{{item}}_output.yml" + loop: + - json + - yaml + - toml diff --git a/test/integration/targets/ansible-pull/runme.sh b/test/integration/targets/ansible-pull/runme.sh index 582e8099..b591b283 100755 --- a/test/integration/targets/ansible-pull/runme.sh +++ b/test/integration/targets/ansible-pull/runme.sh @@ -36,7 +36,8 @@ function pass_tests { fi # test for https://github.com/ansible/ansible/issues/13681 - if grep -E '127\.0\.0\.1.*ok' "${temp_log}"; then + # match play default output stats, was matching limit + docker + if grep -E '127\.0\.0\.1\s*: ok=' "${temp_log}"; then cat "${temp_log}" echo "Found host 127.0.0.1 in output. Only localhost should be present." exit 1 @@ -86,5 +87,7 @@ ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" "$@" multi_play pass_tests_multi +ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" conn_secret.yml --connection-password-file "${repo_dir}/secret_connection_password" "$@" + # fail if we try do delete /var/tmp ANSIBLE_CONFIG='' ansible-pull -d var/tmp -U "${repo_dir}" --purge "$@" diff --git a/test/integration/targets/ansible-runner/aliases b/test/integration/targets/ansible-runner/aliases index 13e7d785..f4caffd1 100644 --- a/test/integration/targets/ansible-runner/aliases +++ b/test/integration/targets/ansible-runner/aliases @@ -1,5 +1,4 @@ shippable/posix/group5 context/controller -skip/osx skip/macos skip/freebsd diff --git a/test/integration/targets/ansible-runner/files/adhoc_example1.py b/test/integration/targets/ansible-runner/files/adhoc_example1.py index ab24bcad..fe7f9446 100644 --- a/test/integration/targets/ansible-runner/files/adhoc_example1.py +++ b/test/integration/targets/ansible-runner/files/adhoc_example1.py @@ -2,7 +2,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import json -import os import sys import ansible_runner diff --git a/test/integration/targets/ansible-test-cloud-openshift/aliases b/test/integration/targets/ansible-test-cloud-openshift/aliases index 6e32db7b..b714e82c 100644 --- a/test/integration/targets/ansible-test-cloud-openshift/aliases +++ b/test/integration/targets/ansible-test-cloud-openshift/aliases @@ -1,4 +1,4 @@ cloud/openshift shippable/generic/group1 -disabled # disabled due to requirements conflict: botocore 1.20.6 has requirement urllib3<1.27,>=1.25.4, but you have urllib3 1.24.3. +disabled # the container crashes when using a non-default network on some docker hosts (such as Ubuntu 20.04) context/controller diff --git a/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml b/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml index c3b51904..6acb67dc 100644 --- a/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml +++ b/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml @@ -1,6 +1,13 @@ +- name: Load kubeconfig + include_vars: "{{ lookup('env', 'K8S_AUTH_KUBECONFIG') }}" + +- name: Verify endpoints exist + assert: + that: clusters + - name: Verify endpoints respond uri: - url: "{{ item }}" + url: "{{ item.cluster.server }}" validate_certs: no with_items: - - https://openshift-origin:8443/ + - "{{ clusters }}" diff --git a/test/integration/targets/ansible-test-container/runme.py b/test/integration/targets/ansible-test-container/runme.py index 8ff48e0d..3c86b6dd 100755 --- a/test/integration/targets/ansible-test-container/runme.py +++ b/test/integration/targets/ansible-test-container/runme.py @@ -996,7 +996,7 @@ class AptBootstrapper(Bootstrapper): @classmethod def install_podman(cls) -> bool: """Return True if podman will be installed.""" - return not (os_release.id == 'ubuntu' and os_release.version_id == '20.04') + return not (os_release.id == 'ubuntu' and os_release.version_id in {'20.04', '22.04'}) @classmethod def install_docker(cls) -> bool: @@ -1053,13 +1053,14 @@ class ApkBootstrapper(Bootstrapper): # crun added as podman won't install it as dep if runc is present # but we don't want runc as it fails # The edge `crun` package installed below requires ip6tables, and in - # edge, the `iptables` package includes `ip6tables`, but in 3.16 they - # are separate. + # edge, the `iptables` package includes `ip6tables`, but in 3.18 they + # are separate. Remove `ip6tables` once we update to 3.19. packages = ['docker', 'podman', 'openssl', 'crun', 'ip6tables'] run_command('apk', 'add', *packages) - # 3.16 only contains crun 1.4.5, to get 1.9.2 to resolve the run/shm issue, install crun from edge - run_command('apk', 'upgrade', '-U', '--repository=http://dl-cdn.alpinelinux.org/alpine/edge/community', 'crun') + # 3.18 only contains crun 1.8.4, to get 1.9.2 to resolve the run/shm issue, install crun from 3.19 + # Remove once we update to 3.19 + run_command('apk', 'upgrade', '-U', '--repository=http://dl-cdn.alpinelinux.org/alpine/v3.19/community', 'crun') run_command('service', 'docker', 'start') run_command('modprobe', 'tun') diff --git a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py index f59b9091..f662b97d 100644 --- a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py +++ b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py @@ -16,10 +16,10 @@ RETURN = '''#''' from ansible.plugins.lookup import LookupBase # noinspection PyUnresolvedReferences -from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded +from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded # pylint: disable=unused-import try: - import demo + import demo # pylint: disable=unused-import except ImportError: pass else: diff --git a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py index 22b4236a..38860b03 100644 --- a/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py +++ b/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py @@ -16,10 +16,10 @@ RETURN = '''#''' from ansible.plugins.lookup import LookupBase # noinspection PyUnresolvedReferences -from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded +from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded # pylint: disable=unused-import try: - import demo + import demo # pylint: disable=unused-import except ImportError: pass else: diff --git a/test/integration/targets/ansible-test-sanity-import/runme.sh b/test/integration/targets/ansible-test-sanity-import/runme.sh index a12e3e3f..a49a71a0 100755 --- a/test/integration/targets/ansible-test-sanity-import/runme.sh +++ b/test/integration/targets/ansible-test-sanity-import/runme.sh @@ -1,7 +1,29 @@ #!/usr/bin/env bash +set -eu + +# Create test scenarios at runtime that do not pass sanity tests. +# This avoids the need to create ignore entries for the tests. + +mkdir -p ansible_collections/ns/col/plugins/lookup + +( + cd ansible_collections/ns/col/plugins/lookup + + echo "import sys; sys.stdout.write('unwanted stdout')" > stdout.py # stdout: unwanted stdout + echo "import sys; sys.stderr.write('unwanted stderr')" > stderr.py # stderr: unwanted stderr +) + source ../collection/setup.sh +# Run regular import sanity tests. + +ansible-test sanity --test import --color --failure-ok --lint --python "${ANSIBLE_TEST_PYTHON_VERSION}" "${@}" 1> actual-stdout.txt 2> actual-stderr.txt +diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt +grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt + +# Run import sanity tests which require modifications to the source directory. + vendor_dir="$(python -c 'import pathlib, ansible._vendor; print(pathlib.Path(ansible._vendor.__file__).parent)')" cleanup() { diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml index c2575422..4ca20efb 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml @@ -17,6 +17,9 @@ DOCUMENTATION: default: foo author: - Ansible Core Team + seealso: + - plugin: ns.col.import_order_lookup + plugin_type: lookup EXAMPLES: | - name: example for sidecar diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md index bf1003fa..d158a987 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md @@ -1,3 +1,4 @@ README ------ + This is a simple collection used to test failures with ``ansible-test sanity --test validate-modules``. diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md index bbdd5138..9c1c1c34 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md @@ -1,3 +1,4 @@ README ------ + This is a simple PowerShell-only collection used to verify that ``ansible-test`` works on a collection. diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt index 95f12f39..ca6e52a3 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt +++ b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt @@ -1,5 +1,26 @@ +plugins/lookup/import_order_lookup.py:5:0: import-before-documentation: Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN. +plugins/modules/check_mode_attribute_1.py:0:0: attributes-check-mode: The module does not declare support for check mode, but the check_mode attribute's support value is 'full' and not 'none' +plugins/modules/check_mode_attribute_2.py:0:0: attributes-check-mode: The module does not declare support for check mode, but the check_mode attribute's support value is 'partial' and not 'none' +plugins/modules/check_mode_attribute_3.py:0:0: attributes-check-mode: The module does declare support for check mode, but the check_mode attribute's support value is 'none' +plugins/modules/check_mode_attribute_4.py:0:0: attributes-check-mode-details: The module declares it does not fully support check mode, but has no details on what exactly that means +plugins/modules/import_order.py:8:0: import-before-documentation: Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN. plugins/modules/invalid_yaml_syntax.py:0:0: deprecation-mismatch: "meta/runtime.yml" and DOCUMENTATION.deprecation do not agree. plugins/modules/invalid_yaml_syntax.py:0:0: missing-documentation: No DOCUMENTATION provided plugins/modules/invalid_yaml_syntax.py:8:15: documentation-syntax-error: DOCUMENTATION is not valid YAML plugins/modules/invalid_yaml_syntax.py:12:15: invalid-examples: EXAMPLES is not valid YAML plugins/modules/invalid_yaml_syntax.py:16:15: return-syntax-error: RETURN is not valid YAML +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.0: While parsing "V(C\(" at index 1: Unnecessarily escaped "(" @ data['options']['a11']['suboptions']['b1']['description'][0]. Got 'V(C\\(foo\\)).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.2: While parsing "P(foo.bar#baz)" at index 1: Plugin name "foo.bar" is not a FQCN @ data['options']['a11']['suboptions']['b1']['description'][2]. Got 'P(foo.bar#baz).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.3: While parsing "P(foo.bar.baz)" at index 1: Parameter "foo.bar.baz" is not of the form FQCN#type @ data['options']['a11']['suboptions']['b1']['description'][3]. Got 'P(foo.bar.baz).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.4: Directive "P(foo.bar.baz#woof)" must contain a valid plugin type; found "woof" @ data['options']['a11']['suboptions']['b1']['description'][4]. Got 'P(foo.bar.baz#woof).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.5: While parsing "E(foo\(" at index 1: Unnecessarily escaped "(" @ data['options']['a11']['suboptions']['b1']['description'][5]. Got 'E(foo\\(bar).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a2.description: While parsing "V(C\(" at index 1: Unnecessarily escaped "(" for dictionary value @ data['options']['a2']['description']. Got 'V(C\\(foo\\)).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a4.description: While parsing "P(foo.bar#baz)" at index 1: Plugin name "foo.bar" is not a FQCN for dictionary value @ data['options']['a4']['description']. Got 'P(foo.bar#baz).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a5.description: While parsing "P(foo.bar.baz)" at index 1: Parameter "foo.bar.baz" is not of the form FQCN#type for dictionary value @ data['options']['a5']['description']. Got 'P(foo.bar.baz).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a6.description: Directive "P(foo.bar.baz#woof)" must contain a valid plugin type; found "woof" for dictionary value @ data['options']['a6']['description']. Got 'P(foo.bar.baz#woof).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a7.description: While parsing "E(foo\(" at index 1: Unnecessarily escaped "(" for dictionary value @ data['options']['a7']['description']. Got 'E(foo\\(bar).' +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(bar)" contains a non-existing option "bar" +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(bar=bam)" contains a non-existing option "bar" +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(foo.bar=1)" contains a non-existing option "foo.bar" +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "RV(bam)" contains a non-existing return value "bam" +plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "RV(does.not.exist=true)" contains a non-existing return value "does.not.exist" diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh b/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh index e0299969..5e2365ab 100755 --- a/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh +++ b/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh @@ -6,7 +6,17 @@ set -eux ansible-test sanity --test validate-modules --color --truncate 0 --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt -grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt +grep -F -f "${TEST_DIR}/expected.txt" actual-stderr.txt + +cd ../col +ansible-test sanity --test runtime-metadata + +cd ../failure +if ansible-test sanity --test runtime-metadata 2>&1 | tee out.txt; then + echo "runtime-metadata in failure should be invalid" + exit 1 +fi +grep out.txt -e 'extra keys not allowed' cd ../ps_only diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md index d8138d3b..67b8a83b 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md @@ -1,3 +1,4 @@ README ------ + This is a simple collection used to verify that ``ansible-test`` works on a collection. diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml index fee22ad8..76ead137 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml @@ -2,4 +2,11 @@ requires_ansible: '>=2.11' # force ansible-doc to check the Ansible version (re plugin_routing: modules: hi: - redirect: hello + redirect: ns.col2.hello + hiya: + redirect: ns.col2.package.subdir.hiya + module_utils: + hi: + redirect: ansible_collections.ns.col2.plugins.module_utils + hello: + redirect: ns.col2.hiya diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py index 580f9d87..16e0bc88 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py @@ -19,9 +19,9 @@ EXAMPLES = ''' RETURN = ''' # ''' from ansible.plugins.lookup import LookupBase -from ansible import constants +from ansible import constants # pylint: disable=unused-import -import lxml +import lxml # pylint: disable=unused-import class LookupModule(LookupBase): diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py index dbb479a7..5cdd0966 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py @@ -19,7 +19,7 @@ EXAMPLES = ''' RETURN = ''' # ''' from ansible.plugins.lookup import LookupBase -from ansible import constants +from ansible import constants # pylint: disable=unused-import class LookupModule(LookupBase): diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py index e79613bb..8780e356 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py @@ -19,7 +19,7 @@ EXAMPLES = ''' RETURN = '''''' from ansible.module_utils.basic import AnsibleModule -from ansible import constants # intentionally trigger pylint ansible-bad-module-import error +from ansible import constants # intentionally trigger pylint ansible-bad-module-import error # pylint: disable=unused-import def main(): diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py index 2e35cf85..e34d1c37 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py @@ -5,4 +5,4 @@ __metaclass__ = type # This is not an allowed import, but since this file is in a plugins/ subdirectory that is not checked, # the import sanity test will not complain. -import lxml +import lxml # pylint: disable=unused-import diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py index 82215438..a5d896f7 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py @@ -4,12 +4,12 @@ __metaclass__ = type import tempfile try: - import urllib2 # intentionally trigger pylint ansible-bad-import error + import urllib2 # intentionally trigger pylint ansible-bad-import error # pylint: disable=unused-import except ImportError: urllib2 = None try: - from urllib2 import Request # intentionally trigger pylint ansible-bad-import-from error + from urllib2 import Request # intentionally trigger pylint ansible-bad-import-from error # pylint: disable=unused-import except ImportError: Request = None diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt index e1b3f4ca..dcbe827c 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt @@ -1,6 +1,7 @@ plugins/modules/bad.py import plugins/modules/bad.py pylint:ansible-bad-module-import plugins/lookup/bad.py import +plugins/plugin_utils/check_pylint.py pylint:disallowed-name tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from diff --git a/test/integration/targets/ansible-test-sanity/runme.sh b/test/integration/targets/ansible-test-sanity/runme.sh index 233db741..92584958 100755 --- a/test/integration/targets/ansible-test-sanity/runme.sh +++ b/test/integration/targets/ansible-test-sanity/runme.sh @@ -1,5 +1,11 @@ #!/usr/bin/env bash +set -eux + +ansible-test sanity --color --allow-disabled -e "${@}" + +set +x + source ../collection/setup.sh set -x diff --git a/test/integration/targets/ansible-test/venv-pythons.py b/test/integration/targets/ansible-test/venv-pythons.py index b380f147..97998bcd 100755 --- a/test/integration/targets/ansible-test/venv-pythons.py +++ b/test/integration/targets/ansible-test/venv-pythons.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Return target Python options for use with ansible-test.""" +import argparse import os import shutil import subprocess @@ -10,6 +11,11 @@ from ansible import release def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--only-versions', action='store_true') + + options = parser.parse_args() + ansible_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(release.__file__)))) source_root = os.path.join(ansible_root, 'test', 'lib') @@ -33,6 +39,10 @@ def main(): print(f'{executable} - {"fail" if process.returncode else "pass"}', file=sys.stderr) if not process.returncode: + if options.only_versions: + args.append(python_version) + continue + args.extend(['--target-python', f'venv/{python_version}']) print(' '.join(args)) diff --git a/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml b/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml index 71dbacc0..2365d47d 100644 --- a/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml +++ b/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml @@ -20,4 +20,4 @@ # 3366323866663763660a323766383531396433663861656532373663373134376263383263316261 # 3137 -# $ ansible-playbook -i inventory --vault-password-file=vault-secret tasks.yml +# $ ansible-playbook -i inventory --vault-password-file=vault-secret tasks.yml diff --git a/test/integration/targets/ansible-vault/runme.sh b/test/integration/targets/ansible-vault/runme.sh index 50720ea9..98399eca 100755 --- a/test/integration/targets/ansible-vault/runme.sh +++ b/test/integration/targets/ansible-vault/runme.sh @@ -47,6 +47,18 @@ echo $? # view the vault encrypted password file ansible-vault view "$@" --vault-id vault-password encrypted-vault-password +# check if ansible-vault fails when destination is not writable +NOT_WRITABLE_DIR="${MYTMPDIR}/not_writable" +TEST_FILE_EDIT4="${NOT_WRITABLE_DIR}/testfile" +mkdir "${NOT_WRITABLE_DIR}" +touch "${TEST_FILE_EDIT4}" +chmod ugo-w "${NOT_WRITABLE_DIR}" +ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE_EDIT4}" < /dev/null > log 2>&1 && : +grep "not writable" log && : +WRONG_RC=$? +echo "rc was $WRONG_RC (1 is expected)" +[ $WRONG_RC -eq 1 ] + # encrypt with a password from a vault encrypted password file and multiple vault-ids # should fail because we dont know which vault id to use to encrypt with ansible-vault encrypt "$@" --vault-id vault-password --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}" && : @@ -574,3 +586,23 @@ ansible-playbook realpath.yml "$@" --vault-password-file symlink/get-password-sy # using symlink ansible-playbook symlink.yml "$@" --vault-password-file script/vault-secret.sh 2>&1 |grep "${ER}" + +### SALT TESTING ### +# prep files for encryption +for salted in test1 test2 test3 +do + echo 'this is salty' > "salted_${salted}" +done + +# encrypt files +ANSIBLE_VAULT_ENCRYPT_SALT=salty ansible-vault encrypt salted_test1 --vault-password-file example1_password "$@" +ANSIBLE_VAULT_ENCRYPT_SALT=salty ansible-vault encrypt salted_test2 --vault-password-file example1_password "$@" +ansible-vault encrypt salted_test3 --vault-password-file example1_password "$@" + +# should be the same +out=$(diff salted_test1 salted_test2) +[ "${out}" == "" ] + +# shoudl be diff +out=$(diff salted_test1 salted_test3 || true) +[ "${out}" != "" ] diff --git a/test/integration/targets/ansible-vault/test_vault.yml b/test/integration/targets/ansible-vault/test_vault.yml index 7f8ed115..c21d49a6 100644 --- a/test/integration/targets/ansible-vault/test_vault.yml +++ b/test/integration/targets/ansible-vault/test_vault.yml @@ -1,6 +1,6 @@ - hosts: testhost gather_facts: False vars: - - output_dir: . + output_dir: . roles: - { role: test_vault, tags: test_vault} diff --git a/test/integration/targets/ansible-vault/test_vaulted_template.yml b/test/integration/targets/ansible-vault/test_vaulted_template.yml index b495211d..6a16ec86 100644 --- a/test/integration/targets/ansible-vault/test_vaulted_template.yml +++ b/test/integration/targets/ansible-vault/test_vaulted_template.yml @@ -1,6 +1,6 @@ - hosts: testhost gather_facts: False vars: - - output_dir: . + output_dir: . roles: - { role: test_vaulted_template, tags: test_vaulted_template} diff --git a/test/integration/targets/ansible/aliases b/test/integration/targets/ansible/aliases index 8278ec8b..c7f2050a 100644 --- a/test/integration/targets/ansible/aliases +++ b/test/integration/targets/ansible/aliases @@ -1,2 +1,3 @@ shippable/posix/group3 context/controller +needs/target/support-callback_plugins diff --git a/test/integration/targets/ansible/ansible-testé.cfg b/test/integration/targets/ansible/ansible-testé.cfg index 09af947f..a0e4e8d7 100644 --- a/test/integration/targets/ansible/ansible-testé.cfg +++ b/test/integration/targets/ansible/ansible-testé.cfg @@ -1,3 +1,3 @@ [defaults] remote_user = admin -collections_paths = /tmp/collections +collections_path = /tmp/collections diff --git a/test/integration/targets/ansible/runme.sh b/test/integration/targets/ansible/runme.sh index e9e72a9f..d6780214 100755 --- a/test/integration/targets/ansible/runme.sh +++ b/test/integration/targets/ansible/runme.sh @@ -14,9 +14,9 @@ ANSIBLE_REMOTE_USER=administrator ansible-config dump| grep 'DEFAULT_REMOTE_USER ansible-config list | grep 'DEFAULT_REMOTE_USER' # Collection -ansible-config view -c ./ansible-testé.cfg | grep 'collections_paths = /tmp/collections' +ansible-config view -c ./ansible-testé.cfg | grep 'collections_path = /tmp/collections' ansible-config dump -c ./ansible-testé.cfg | grep 'COLLECTIONS_PATHS([^)]*) =' -ANSIBLE_COLLECTIONS_PATHS=/tmp/collections ansible-config dump| grep 'COLLECTIONS_PATHS([^)]*) =' +ANSIBLE_COLLECTIONS_PATH=/tmp/collections ansible-config dump| grep 'COLLECTIONS_PATHS([^)]*) =' ansible-config list | grep 'COLLECTIONS_PATHS' # 'view' command must fail when config file is missing or has an invalid file extension @@ -34,7 +34,7 @@ ansible localhost -m debug -a var=playbook_dir --playbook-dir=/doesnotexist/tmp env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"' # test adhoc callback triggers -ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout - +ANSIBLE_CALLBACK_PLUGINS=../support-callback_plugins/callback_plugins ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout - # CB_WANTS_IMPLICIT isn't anything in Ansible itself. # Our test cb plugin just accepts it. It lets us avoid copypasting the whole diff --git a/test/integration/targets/apt/aliases b/test/integration/targets/apt/aliases index 5f892f9c..20c87093 100644 --- a/test/integration/targets/apt/aliases +++ b/test/integration/targets/apt/aliases @@ -1,6 +1,5 @@ shippable/posix/group2 destructive skip/freebsd -skip/osx skip/macos skip/rhel diff --git a/test/integration/targets/apt/tasks/apt.yml b/test/integration/targets/apt/tasks/apt.yml index d273eda7..a0bc1992 100644 --- a/test/integration/targets/apt/tasks/apt.yml +++ b/test/integration/targets/apt/tasks/apt.yml @@ -372,7 +372,7 @@ - libcaca-dev - libslang2-dev -# https://github.com/ansible/ansible/issues/38995 +# # https://github.com/ansible/ansible/issues/38995 - name: build-dep for a package apt: name: tree @@ -524,6 +524,55 @@ - "allow_change_held_packages_no_update is not changed" - "allow_change_held_packages_hello_version.stdout == allow_change_held_packages_hello_version_again.stdout" +# Remove pkg on hold +- name: Put hello on hold + shell: apt-mark hold hello + +- name: Get hold list + shell: apt-mark showhold + register: hello_hold + +- name: Check that the package hello is on the hold list + assert: + that: + - "'hello' in hello_hold.stdout" + +- name: Try removing package hello + apt: + name: hello + state: absent + register: package_removed + ignore_errors: true + +- name: verify the package is not removed with dpkg + shell: dpkg -l hello + register: dpkg_result + +- name: Verify that package was not removed + assert: + that: + - package_removed is failed + - dpkg_result is success + +- name: Try removing package (allow_change_held_packages=yes) + apt: + name: hello + state: absent + allow_change_held_packages: yes + register: package_removed + +- name: verify the package is removed with dpkg + shell: dpkg -l hello + register: dpkg_result + ignore_errors: true + +- name: Verify that package removal was succesfull + assert: + that: + - package_removed is success + - dpkg_result is failed + - package_removed.changed + # Virtual package - name: Install a virtual package apt: diff --git a/test/integration/targets/apt/tasks/repo.yml b/test/integration/targets/apt/tasks/repo.yml index d4cce78a..b1d08afa 100644 --- a/test/integration/targets/apt/tasks/repo.yml +++ b/test/integration/targets/apt/tasks/repo.yml @@ -400,6 +400,8 @@ - { upgrade_type: safe, force_apt_get: True } - { upgrade_type: full, force_apt_get: True } + - include_tasks: "upgrade_scenarios.yml" + - name: Remove aptitude if not originally present apt: pkg: aptitude diff --git a/test/integration/targets/apt_key/aliases b/test/integration/targets/apt_key/aliases index a820ec90..97f534a8 100644 --- a/test/integration/targets/apt_key/aliases +++ b/test/integration/targets/apt_key/aliases @@ -1,5 +1,4 @@ shippable/posix/group1 skip/freebsd -skip/osx skip/macos skip/rhel diff --git a/test/integration/targets/apt_key/tasks/main.yml b/test/integration/targets/apt_key/tasks/main.yml index ffb89b22..7aee56a7 100644 --- a/test/integration/targets/apt_key/tasks/main.yml +++ b/test/integration/targets/apt_key/tasks/main.yml @@ -21,7 +21,7 @@ - import_tasks: 'apt_key_inline_data.yml' when: ansible_distribution in ('Ubuntu', 'Debian') - + - import_tasks: 'file.yml' when: ansible_distribution in ('Ubuntu', 'Debian') diff --git a/test/integration/targets/apt_repository/aliases b/test/integration/targets/apt_repository/aliases index 34e2b540..b4fe8dba 100644 --- a/test/integration/targets/apt_repository/aliases +++ b/test/integration/targets/apt_repository/aliases @@ -1,6 +1,5 @@ destructive shippable/posix/group1 skip/freebsd -skip/osx skip/macos skip/rhel diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml index 9c15e647..2ddf4140 100644 --- a/test/integration/targets/apt_repository/tasks/apt.yml +++ b/test/integration/targets/apt_repository/tasks/apt.yml @@ -152,6 +152,11 @@ - 'result.changed' - 'result.state == "present"' - 'result.repo == test_ppa_spec' + - '"sources_added" in result' + - 'result.sources_added | length == 1' + - '"git" in result.sources_added[0]' + - '"sources_removed" in result' + - 'result.sources_removed | length == 0' - result_cache is not changed - name: 'examine apt cache mtime' @@ -167,6 +172,17 @@ apt_repository: repo='{{test_ppa_spec}}' state=absent register: result +- assert: + that: + - 'result.changed' + - 'result.state == "absent"' + - 'result.repo == test_ppa_spec' + - '"sources_added" in result' + - 'result.sources_added | length == 0' + - '"sources_removed" in result' + - 'result.sources_removed | length == 1' + - '"git" in result.sources_removed[0]' + # When installing a repo with the spec, the key is *NOT* added - name: 'ensure ppa key is absent (expect: pass)' apt_key: id='{{test_ppa_key}}' state=absent @@ -224,7 +240,7 @@ - assert: that: - result is failed - - result.msg == 'Please set argument \'repo\' to a non-empty value' + - result.msg.startswith("argument 'repo' is of type <class 'NoneType'> and we were unable to convert to str") - name: Test apt_repository with an empty value for repo apt_repository: diff --git a/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml b/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml index 726de111..62960ccd 100644 --- a/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml +++ b/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml @@ -4,4 +4,4 @@ - name: Delete existing repo file: path: "{{ test_repo_path }}" - state: absent
\ No newline at end of file + state: absent diff --git a/test/integration/targets/argspec/library/argspec.py b/test/integration/targets/argspec/library/argspec.py index b6d6d110..2d86d77b 100644 --- a/test/integration/targets/argspec/library/argspec.py +++ b/test/integration/targets/argspec/library/argspec.py @@ -23,6 +23,10 @@ def main(): 'type': 'str', 'choices': ['absent', 'present'], }, + 'default_value': { + 'type': 'bool', + 'default': True, + }, 'path': {}, 'content': {}, 'mapping': { @@ -246,7 +250,7 @@ def main(): ('state', 'present', ('path', 'content'), True), ), mutually_exclusive=( - ('path', 'content'), + ('path', 'content', 'default_value',), ), required_one_of=( ('required_one_of_one', 'required_one_of_two'), diff --git a/test/integration/targets/become/tasks/main.yml b/test/integration/targets/become/tasks/main.yml index 4a2ce64b..c05824d7 100644 --- a/test/integration/targets/become/tasks/main.yml +++ b/test/integration/targets/become/tasks/main.yml @@ -11,8 +11,8 @@ ansible_become_user: "{{ become_test_config.user }}" ansible_become_method: "{{ become_test_config.method }}" ansible_become_password: "{{ become_test_config.password | default(None) }}" - loop: "{{ - (become_methods | selectattr('skip', 'undefined') | list)+ + loop: "{{ + (become_methods | selectattr('skip', 'undefined') | list)+ (become_methods | selectattr('skip', 'defined') | rejectattr('skip') | list) }}" loop_control: diff --git a/test/integration/targets/blockinfile/tasks/main.yml b/test/integration/targets/blockinfile/tasks/main.yml index 054e5549..f26cb165 100644 --- a/test/integration/targets/blockinfile/tasks/main.yml +++ b/test/integration/targets/blockinfile/tasks/main.yml @@ -31,6 +31,7 @@ - import_tasks: add_block_to_existing_file.yml - import_tasks: create_file.yml +- import_tasks: create_dir.yml - import_tasks: preserve_line_endings.yml - import_tasks: block_without_trailing_newline.yml - import_tasks: file_without_trailing_newline.yml @@ -39,3 +40,5 @@ - import_tasks: insertafter.yml - import_tasks: insertbefore.yml - import_tasks: multiline_search.yml +- import_tasks: append_newline.yml +- import_tasks: prepend_newline.yml diff --git a/test/integration/targets/blocks/unsafe_failed_task.yml b/test/integration/targets/blocks/unsafe_failed_task.yml index adfa492a..e74327b9 100644 --- a/test/integration/targets/blocks/unsafe_failed_task.yml +++ b/test/integration/targets/blocks/unsafe_failed_task.yml @@ -1,7 +1,7 @@ - hosts: localhost gather_facts: false vars: - - data: {} + data: {} tasks: - block: - name: template error diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout index 71a4ef9e..ed455756 100644 --- a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout +++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout @@ -43,6 +43,7 @@ fatal: [testhost]: FAILED! => TASK [Skipped task] ************************************************************ skipping: [testhost] => changed: false + false_condition: false skip_reason: Conditional result was False TASK [Task with var in name (foo bar)] ***************************************** @@ -120,6 +121,7 @@ ok: [testhost] => (item=debug-3) => msg: debug-3 skipping: [testhost] => (item=debug-4) => ansible_loop_var: item + false_condition: item != 4 item: 4 fatal: [testhost]: FAILED! => msg: One or more items failed @@ -199,9 +201,11 @@ skipping: [testhost] => TASK [debug] ******************************************************************* skipping: [testhost] => (item=1) => ansible_loop_var: item + false_condition: false item: 1 skipping: [testhost] => (item=2) => ansible_loop_var: item + false_condition: false item: 2 skipping: [testhost] => msg: All items skipped diff --git a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout index 7a99cc74..3a121a5f 100644 --- a/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout +++ b/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout @@ -45,6 +45,7 @@ fatal: [testhost]: FAILED! => TASK [Skipped task] ************************************************************ skipping: [testhost] => changed: false + false_condition: false skip_reason: Conditional result was False TASK [Task with var in name (foo bar)] ***************************************** @@ -126,6 +127,7 @@ ok: [testhost] => (item=debug-3) => msg: debug-3 skipping: [testhost] => (item=debug-4) => ansible_loop_var: item + false_condition: item != 4 item: 4 fatal: [testhost]: FAILED! => msg: One or more items failed @@ -206,9 +208,11 @@ skipping: [testhost] => TASK [debug] ******************************************************************* skipping: [testhost] => (item=1) => ansible_loop_var: item + false_condition: false item: 1 skipping: [testhost] => (item=2) => ansible_loop_var: item + false_condition: false item: 2 skipping: [testhost] => msg: All items skipped diff --git a/test/integration/targets/check_mode/check_mode.yml b/test/integration/targets/check_mode/check_mode.yml index a5777506..ebf1c5b5 100644 --- a/test/integration/targets/check_mode/check_mode.yml +++ b/test/integration/targets/check_mode/check_mode.yml @@ -1,7 +1,7 @@ - name: Test that check works with check_mode specified in roles hosts: testhost vars: - - output_dir: . + output_dir: . roles: - { role: test_always_run, tags: test_always_run } - { role: test_check_mode, tags: test_check_mode } diff --git a/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml b/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml index f926d144..ce9ecbf4 100644 --- a/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml +++ b/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml @@ -25,8 +25,8 @@ register: foo - name: verify that the file was marked as changed in check mode - assert: - that: + assert: + that: - "template_result is changed" - "not foo.stat.exists" @@ -44,7 +44,7 @@ check_mode: no - name: verify that the file was not changed - assert: - that: + assert: + that: - "checkmode_disabled is changed" - "template_result2 is not changed" diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py index fc19a99d..77f80502 100644 --- a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.plugins.connection import ConnectionBase DOCUMENTATION = """ diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py index b945eb68..6f3a19d7 100644 --- a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py @@ -2,10 +2,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import sys - -from ..module_utils import bogusmu # pylint: disable=relative-beyond-top-level +from ..module_utils import bogusmu # pylint: disable=relative-beyond-top-level,unused-import def main(): diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py index 59cb3c5e..6f2320d3 100644 --- a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py @@ -2,10 +2,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import sys - -from ..module_utils import missing_redirect_target_collection # pylint: disable=relative-beyond-top-level +from ..module_utils import missing_redirect_target_collection # pylint: disable=relative-beyond-top-level,unused-import def main(): diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py index 31ffd17c..de5c2e58 100644 --- a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py +++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py @@ -2,10 +2,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import sys - -from ..module_utils import missing_redirect_target_module # pylint: disable=relative-beyond-top-level +from ..module_utils import missing_redirect_target_module # pylint: disable=relative-beyond-top-level,unused-import def main(): diff --git a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py index ae6941f3..92696481 100644 --- a/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py +++ b/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py @@ -19,7 +19,6 @@ DOCUMENTATION = ''' required: True ''' -from ansible.errors import AnsibleParserError from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable diff --git a/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py b/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py index 23cce104..f1242e14 100644 --- a/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py +++ b/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py @@ -14,7 +14,6 @@ DOCUMENTATION = ''' - Enable in configuration. ''' -from ansible import constants as C from ansible.plugins.callback import CallbackBase diff --git a/test/integration/targets/command_nonexisting/tasks/main.yml b/test/integration/targets/command_nonexisting/tasks/main.yml index d21856e7..e54ecb3f 100644 --- a/test/integration/targets/command_nonexisting/tasks/main.yml +++ b/test/integration/targets/command_nonexisting/tasks/main.yml @@ -1,4 +1,4 @@ - command: commandthatdoesnotexist --would-be-awkward register: res changed_when: "'changed' in res.stdout" - failed_when: "res.stdout != '' or res.stderr != ''"
\ No newline at end of file + failed_when: "res.stdout != '' or res.stderr != ''" diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml index 1f4aa5d7..2cc365db 100644 --- a/test/integration/targets/command_shell/tasks/main.yml +++ b/test/integration/targets/command_shell/tasks/main.yml @@ -284,6 +284,30 @@ that: - "command_result6.stdout == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'" +- name: check default var expansion + command: /bin/sh -c 'echo "\$TEST"' + environment: + TEST: z + register: command_result7 + +- name: assert vars were expanded + assert: + that: + - command_result7.stdout == '\\z' + +- name: check disabled var expansion + command: /bin/sh -c 'echo "\$TEST"' + args: + expand_argument_vars: false + environment: + TEST: z + register: command_result8 + +- name: assert vars were not expanded + assert: + that: + - command_result8.stdout == '$TEST' + ## ## shell ## @@ -546,3 +570,21 @@ - command_strip.stderr == 'hello \n ' - command_no_strip.stdout== 'hello \n \r\n' - command_no_strip.stderr == 'hello \n \r\n' + +- name: run shell with expand_argument_vars + shell: echo 'hi' + args: + expand_argument_vars: false + register: shell_expand_failure + ignore_errors: true + +- name: assert shell with expand_arguments_vars failed + assert: + that: + - shell_expand_failure is failed + - "shell_expand_failure.msg == 'Unsupported parameters for (shell) module: expand_argument_vars'" + +- name: Run command that backgrounds, to ensure no hang + shell: '{{ role_path }}/scripts/yoink.sh &' + delegate_to: localhost + timeout: 5 diff --git a/test/integration/targets/conditionals/play.yml b/test/integration/targets/conditionals/play.yml index 455818c9..56ec8438 100644 --- a/test/integration/targets/conditionals/play.yml +++ b/test/integration/targets/conditionals/play.yml @@ -665,3 +665,29 @@ - item loop: - 1 == 1 + + - set_fact: + sentinel_file: '{{ lookup("env", "OUTPUT_DIR")}}/LOOKUP_SIDE_EFFECT.txt' + + - name: ensure sentinel file is absent + file: + path: '{{ sentinel_file }}' + state: absent + - name: get an untrusted var that's a valid Jinja expression with a side-effect + shell: | + echo "lookup('pipe', 'echo bang > \"$SENTINEL_FILE\" && cat \"$SENTINEL_FILE\"')" + environment: + SENTINEL_FILE: '{{ sentinel_file }}' + register: untrusted_expr + - name: use a conditional with an inline template that refers to the untrusted expression + debug: + msg: look at some seemingly innocuous stuff + when: '"foo" in {{ untrusted_expr.stdout }}' + ignore_errors: true + - name: ensure the untrusted expression side-effect has not executed + stat: + path: '{{ sentinel_file }}' + register: sentinel_stat + - assert: + that: + - not sentinel_stat.stat.exists diff --git a/test/integration/targets/connection_delegation/aliases b/test/integration/targets/connection_delegation/aliases index 6c965663..0ce76011 100644 --- a/test/integration/targets/connection_delegation/aliases +++ b/test/integration/targets/connection_delegation/aliases @@ -1,6 +1,5 @@ shippable/posix/group3 context/controller skip/freebsd # No sshpass -skip/osx # No sshpass skip/macos # No sshpass skip/rhel # No sshpass diff --git a/test/integration/targets/connection_paramiko_ssh/test_connection.inventory b/test/integration/targets/connection_paramiko_ssh/test_connection.inventory index a3f34ab7..cd17c090 100644 --- a/test/integration/targets/connection_paramiko_ssh/test_connection.inventory +++ b/test/integration/targets/connection_paramiko_ssh/test_connection.inventory @@ -2,6 +2,6 @@ paramiko_ssh-pipelining ansible_ssh_pipelining=true paramiko_ssh-no-pipelining ansible_ssh_pipelining=false [paramiko_ssh:vars] -ansible_host=localhost +ansible_host={{ 'localhost'|string }} ansible_connection=paramiko_ssh ansible_python_interpreter="{{ ansible_playbook_python }}" diff --git a/test/integration/targets/connection_psrp/tests.yml b/test/integration/targets/connection_psrp/tests.yml index dabbf407..08832b14 100644 --- a/test/integration/targets/connection_psrp/tests.yml +++ b/test/integration/targets/connection_psrp/tests.yml @@ -6,6 +6,9 @@ gather_facts: no tasks: + - name: reboot the host + ansible.windows.win_reboot: + - name: test complex objects in raw output # until PyYAML is upgraded to 4.x we need to use the \U escape for a unicode codepoint # and enclose in a quote to it translates the \U @@ -29,15 +32,8 @@ - raw_out.stdout_lines[4] == "winrm" - raw_out.stdout_lines[5] == "string - \U0001F4A9" - # Become only works on Server 2008 when running with basic auth, skip this host for now as it is too complicated to - # override the auth protocol in the tests. - - name: check if we running on Server 2008 - win_shell: '[System.Environment]::OSVersion.Version -ge [Version]"6.1"' - register: os_version - - name: test out become with psrp win_whoami: - when: os_version|bool register: whoami_out become: yes become_method: runas @@ -47,7 +43,6 @@ assert: that: - whoami_out.account.sid == "S-1-5-18" - when: os_version|bool - name: test out async with psrp win_shell: Start-Sleep -Seconds 2; Write-Output abc diff --git a/test/integration/targets/connection_winrm/tests.yml b/test/integration/targets/connection_winrm/tests.yml index 78f92a49..b086a3ad 100644 --- a/test/integration/targets/connection_winrm/tests.yml +++ b/test/integration/targets/connection_winrm/tests.yml @@ -6,6 +6,9 @@ gather_facts: no tasks: + - name: reboot the host + ansible.windows.win_reboot: + - name: setup remote tmp dir import_role: name: ../../setup_remote_tmp_dir diff --git a/test/integration/targets/copy/tasks/main.yml b/test/integration/targets/copy/tasks/main.yml index b86c56ac..601312fa 100644 --- a/test/integration/targets/copy/tasks/main.yml +++ b/test/integration/targets/copy/tasks/main.yml @@ -84,6 +84,7 @@ - import_tasks: check_mode.yml # https://github.com/ansible/ansible/issues/57618 + # https://github.com/ansible/ansible/issues/79749 - name: Test diff contents copy: content: 'Ansible managed\n' @@ -95,6 +96,7 @@ that: - 'diff_output.diff[0].before == ""' - '"Ansible managed" in diff_output.diff[0].after' + - '"file.txt" in diff_output.diff[0].after_header' - name: tests with remote_src and non files import_tasks: src_remote_file_is_not_file.yml diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml index d6c8e63c..40ea9de3 100644 --- a/test/integration/targets/copy/tasks/tests.yml +++ b/test/integration/targets/copy/tasks/tests.yml @@ -420,6 +420,80 @@ - "stat_results2.stat.mode == '0547'" # +# test copying an empty dir to a dest dir with remote_src=True +# + +- name: create empty test dir + file: + path: '{{ remote_dir }}/testcase_empty_dir' + state: directory + +- name: test copying an empty dir to a dir that does not exist (dest ends with slash) + copy: + src: '{{ remote_dir }}/testcase_empty_dir/' + remote_src: yes + dest: '{{ remote_dir }}/testcase_empty_dir_dest/' + register: copy_result + +- name: get stat of newly created dir + stat: + path: '{{ remote_dir }}/testcase_empty_dir_dest' + register: stat_result + +- assert: + that: + - copy_result.changed + - stat_result.stat.exists + - stat_result.stat.isdir + +- name: test no change is made running the task twice + copy: + src: '{{ remote_dir }}/testcase_empty_dir/' + remote_src: yes + dest: '{{ remote_dir }}/testcase_empty_dir_dest/' + register: copy_result + failed_when: copy_result is changed + +- name: remove to test dest with no trailing slash + file: + path: '{{ remote_dir }}/testcase_empty_dir_dest/' + state: absent + +- name: test copying an empty dir to a dir that does not exist (both src/dest have no trailing slash) + copy: + src: '{{ remote_dir }}/testcase_empty_dir' + remote_src: yes + dest: '{{ remote_dir }}/testcase_empty_dir_dest' + register: copy_result + +- name: get stat of newly created dir + stat: + path: '{{ remote_dir }}/testcase_empty_dir_dest' + register: stat_result + +- assert: + that: + - copy_result.changed + - stat_result.stat.exists + - stat_result.stat.isdir + +- name: test no change is made running the task twice + copy: + src: '{{ remote_dir }}/testcase_empty_dir/' + remote_src: yes + dest: '{{ remote_dir }}/testcase_empty_dir_dest/' + register: copy_result + failed_when: copy_result is changed + +- name: clean up src and dest + file: + path: "{{ item }}" + state: absent + loop: + - '{{ remote_dir }}/testcase_empty_dir' + - '{{ remote_dir }}/testcase_empty_dir_dest' + +# # test recursive copy local_follow=False, no trailing slash # @@ -2284,3 +2358,81 @@ that: - fail_copy_directory_with_enc_file is failed - fail_copy_directory_with_enc_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file' + +# +# Test for issue 74536: recursively copy all nested directories with remote_src=yes and src='dir/' when dest exists +# +- vars: + src: '{{ remote_dir }}/testcase_74536' + block: + - name: create source dir with 3 nested subdirs + file: + path: '{{ src }}/a/b1/c1' + state: directory + + - name: copy the source dir with a trailing slash + copy: + src: '{{ src }}/' + remote_src: yes + dest: '{{ src }}_dest/' + register: copy_result + failed_when: copy_result is not changed + + - name: remove the source dir to recreate with different subdirs + file: + path: '{{ src }}' + state: absent + + - name: recreate source dir + file: + path: "{{ item }}" + state: directory + loop: + - '{{ src }}/a/b1/c2' + - '{{ src }}/a/b2/c3' + + - name: copy the source dir containing new subdirs into the existing dest dir + copy: + src: '{{ src }}/' + remote_src: yes + dest: '{{ src }}_dest/' + register: copy_result + + - name: stat each directory that should exist + stat: + path: '{{ item }}' + register: stat_result + loop: + - '{{ src }}_dest' + - '{{ src }}_dest/a' + - '{{ src }}_dest/a/b1' + - '{{ src }}_dest/a/b2' + - '{{ src }}_dest/a/b1/c1' + - '{{ src }}_dest/a/b1/c2' + - '{{ src }}_dest/a/b2/c3' + + - debug: msg="{{ stat_result }}" + + - assert: + that: + - copy_result is changed + # all paths exist + - stat_result.results | map(attribute='stat') | map(attribute='exists') | unique == [true] + # all paths are dirs + - stat_result.results | map(attribute='stat') | map(attribute='isdir') | unique == [true] + + - name: copy the src again to verify no changes will be made + copy: + src: '{{ src }}/' + remote_src: yes + dest: '{{ src }}_dest/' + register: copy_result + failed_when: copy_result is changed + + - name: clean up src and dest + file: + path: '{{ item }}' + state: absent + loop: + - '{{ src }}' + - '{{ src }}_dest' diff --git a/test/integration/targets/cron/aliases b/test/integration/targets/cron/aliases index f2f9ac9d..f3703f85 100644 --- a/test/integration/targets/cron/aliases +++ b/test/integration/targets/cron/aliases @@ -1,4 +1,3 @@ destructive shippable/posix/group1 -skip/osx skip/macos diff --git a/test/integration/targets/debconf/tasks/main.yml b/test/integration/targets/debconf/tasks/main.yml index d3d63cdf..f9236268 100644 --- a/test/integration/targets/debconf/tasks/main.yml +++ b/test/integration/targets/debconf/tasks/main.yml @@ -33,4 +33,44 @@ - 'debconf_test0.current is defined' - '"tzdata/Zones/Etc" in debconf_test0.current' - 'debconf_test0.current["tzdata/Zones/Etc"] == "UTC"' - when: ansible_distribution in ('Ubuntu', 'Debian') + + - name: install debconf-utils + apt: + name: debconf-utils + state: present + register: debconf_utils_deb_install + + - name: Check if password is set + debconf: + name: ddclient + question: ddclient/password + value: "MySecretValue" + vtype: password + register: debconf_test1 + + - name: validate results for test 1 + assert: + that: + - debconf_test1.changed + + - name: Change password again + debconf: + name: ddclient + question: ddclient/password + value: "MySecretValue" + vtype: password + no_log: yes + register: debconf_test2 + + - name: validate results for test 1 + assert: + that: + - not debconf_test2.changed + always: + - name: uninstall debconf-utils + apt: + name: debconf-utils + state: absent + when: debconf_utils_deb_install is changed + + when: ansible_distribution in ('Ubuntu', 'Debian')
\ No newline at end of file diff --git a/test/integration/targets/delegate_to/delegate_local_from_root.yml b/test/integration/targets/delegate_to/delegate_local_from_root.yml index c9be4ff2..b44f83bd 100644 --- a/test/integration/targets/delegate_to/delegate_local_from_root.yml +++ b/test/integration/targets/delegate_to/delegate_local_from_root.yml @@ -3,7 +3,7 @@ gather_facts: false remote_user: root tasks: - - name: ensure we copy w/o errors due to remote user not being overriden + - name: ensure we copy w/o errors due to remote user not being overridden copy: src: testfile dest: "{{ playbook_dir }}" diff --git a/test/integration/targets/delegate_to/runme.sh b/test/integration/targets/delegate_to/runme.sh index 1bdf27cf..e0dcc746 100755 --- a/test/integration/targets/delegate_to/runme.sh +++ b/test/integration/targets/delegate_to/runme.sh @@ -76,3 +76,7 @@ ansible-playbook test_delegate_to_lookup_context.yml -i inventory -v "$@" ansible-playbook delegate_local_from_root.yml -i inventory -v "$@" -e 'ansible_user=root' ansible-playbook delegate_with_fact_from_delegate_host.yml "$@" ansible-playbook delegate_facts_loop.yml -i inventory -v "$@" +ansible-playbook test_random_delegate_to_with_loop.yml -i inventory -v "$@" + +# Run playbook multiple times to ensure there are no false-negatives +for i in $(seq 0 10); do ansible-playbook test_random_delegate_to_without_loop.yml -i inventory -v "$@"; done; diff --git a/test/integration/targets/delegate_to/test_delegate_to.yml b/test/integration/targets/delegate_to/test_delegate_to.yml index dcfa9d03..eb601e02 100644 --- a/test/integration/targets/delegate_to/test_delegate_to.yml +++ b/test/integration/targets/delegate_to/test_delegate_to.yml @@ -1,9 +1,9 @@ - hosts: testhost3 vars: - - template_role: ./roles/test_template - - output_dir: "{{ playbook_dir }}" - - templated_var: foo - - templated_dict: { 'hello': 'world' } + template_role: ./roles/test_template + output_dir: "{{ playbook_dir }}" + templated_var: foo + templated_dict: { 'hello': 'world' } tasks: - name: Test no delegate_to setup: @@ -57,6 +57,25 @@ - name: remove test file file: path={{ output_dir }}/tmp.txt state=absent + - name: Use omit to thwart delegation + ping: + delegate_to: "{{ jenkins_install_key_on|default(omit) }}" + register: d_omitted + + - name: Use empty to thwart delegation should fail + ping: + delegate_to: "{{ jenkins_install_key_on }}" + when: jenkins_install_key_on != "" + vars: + jenkins_install_key_on: '' + ignore_errors: true + register: d_empty + + - name: Ensure previous 2 tests actually did what was expected + assert: + that: + - d_omitted is success + - d_empty is failed - name: verify delegation with per host vars hosts: testhost6 diff --git a/test/integration/targets/dnf/aliases b/test/integration/targets/dnf/aliases index d6f27b8e..b12f3547 100644 --- a/test/integration/targets/dnf/aliases +++ b/test/integration/targets/dnf/aliases @@ -1,6 +1,4 @@ destructive shippable/posix/group1 -skip/power/centos skip/freebsd -skip/osx skip/macos diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml index ec1c36f8..9845f3db 100644 --- a/test/integration/targets/dnf/tasks/dnf.yml +++ b/test/integration/targets/dnf/tasks/dnf.yml @@ -224,7 +224,7 @@ - assert: that: - dnf_result is success - - dnf_result.results|length == 2 + - dnf_result.results|length >= 2 - "dnf_result.results[0].startswith('Removed: ')" - "dnf_result.results[1].startswith('Removed: ')" @@ -427,6 +427,10 @@ - shell: 'dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"' register: shell_dnf_result +- dnf: + name: "@Custom Group" + state: absent + # GROUP UPGRADE - this will go to the same method as group install # but through group_update - it is its invocation we're testing here # see commit 119c9e5d6eb572c4a4800fbe8136095f9063c37b @@ -446,6 +450,10 @@ # cleanup until https://github.com/ansible/ansible/issues/27377 is resolved - shell: dnf -y group install "Custom Group" && dnf -y group remove "Custom Group" +- dnf: + name: "@Custom Group" + state: absent + - name: try to install non existing group dnf: name: "@non-existing-group" @@ -551,30 +559,35 @@ - "'No package non-existent-rpm available' in dnf_result['failures'][0]" - "'Failed to install some of the specified packages' in dnf_result['msg']" -- name: use latest to install httpd +- name: ensure sos isn't installed dnf: - name: httpd + name: sos + state: absent + +- name: use latest to install sos + dnf: + name: sos state: latest register: dnf_result -- name: verify httpd was installed +- name: verify sos was installed assert: that: - - "'changed' in dnf_result" + - dnf_result is changed -- name: uninstall httpd +- name: uninstall sos dnf: - name: httpd + name: sos state: removed -- name: update httpd only if it exists +- name: update sos only if it exists dnf: - name: httpd + name: sos state: latest update_only: yes register: dnf_result -- name: verify httpd not installed +- name: verify sos not installed assert: that: - "not dnf_result is changed" @@ -655,6 +668,28 @@ - "'changed' in dnf_result" - "'results' in dnf_result" +# Install RPM from url with update_only +- name: install from url with update_only + dnf: + name: "file://{{ pkg_path }}" + state: latest + update_only: true + disable_gpg_check: true + register: dnf_result + +- name: verify installation + assert: + that: + - "dnf_result is success" + - "not dnf_result is changed" + - "dnf_result is not failed" + +- name: verify dnf module outputs + assert: + that: + - "'changed' in dnf_result" + - "'results' in dnf_result" + - name: Create a temp RPM file which does not contain nevra information file: name: "/tmp/non_existent_pkg.rpm" diff --git a/test/integration/targets/dnf/tasks/main.yml b/test/integration/targets/dnf/tasks/main.yml index 65b77ceb..4941e2c3 100644 --- a/test/integration/targets/dnf/tasks/main.yml +++ b/test/integration/targets/dnf/tasks/main.yml @@ -61,6 +61,7 @@ when: - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('29', '>=')) or (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>=')) + - not dnf5|default(false) tags: - dnf_modularity @@ -69,5 +70,6 @@ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>=')) - include_tasks: cacheonly.yml - when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or - (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>=')) + when: + - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or + (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>=')) diff --git a/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml index 503cb4c3..f54c0a83 100644 --- a/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml +++ b/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml @@ -240,7 +240,8 @@ - name: Do an "upgrade" to an older version of broken-a, allow_downgrade=false dnf: name: - - broken-a-1.2.3-1* + #- broken-a-1.2.3-1* + - broken-a-1.2.3-1.el7.x86_64 state: latest allow_downgrade: false check_mode: true diff --git a/test/integration/targets/dnf/tasks/test_sos_removal.yml b/test/integration/targets/dnf/tasks/test_sos_removal.yml index 0d70cf78..5e161dbb 100644 --- a/test/integration/targets/dnf/tasks/test_sos_removal.yml +++ b/test/integration/targets/dnf/tasks/test_sos_removal.yml @@ -15,5 +15,5 @@ that: - sos_rm is successful - sos_rm is changed - - "'Removed: sos-' ~ sos_version ~ '-' ~ sos_release in sos_rm.results[0]" - - sos_rm.results|length == 1 + - sos_rm.results|select("contains", "Removed: sos-{{ sos_version }}-{{ sos_release }}")|length > 0 + - sos_rm.results|length > 0 diff --git a/test/integration/targets/dpkg_selections/aliases b/test/integration/targets/dpkg_selections/aliases index c0d5684b..9c44d752 100644 --- a/test/integration/targets/dpkg_selections/aliases +++ b/test/integration/targets/dpkg_selections/aliases @@ -1,6 +1,5 @@ shippable/posix/group1 destructive skip/freebsd -skip/osx skip/macos skip/rhel diff --git a/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml b/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml index 080db262..016d7716 100644 --- a/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml +++ b/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml @@ -87,3 +87,15 @@ apt: name: hello state: absent + +- name: Try to select non-existent package + dpkg_selections: + name: kernel + selection: hold + ignore_errors: yes + register: result + +- name: Check that module fails for non-existent package + assert: + that: + - "'Failed to find package' in result.msg" diff --git a/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py b/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py index c0c5ccd5..28227fce 100644 --- a/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py +++ b/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import pkg_resources +import pkg_resources # pylint: disable=unused-import from ansible.plugins.lookup import LookupBase diff --git a/test/integration/targets/environment/test_environment.yml b/test/integration/targets/environment/test_environment.yml index 43f9c74e..f295cf3c 100644 --- a/test/integration/targets/environment/test_environment.yml +++ b/test/integration/targets/environment/test_environment.yml @@ -7,8 +7,8 @@ - hosts: testhost vars: - - test1: - key1: val1 + test1: + key1: val1 environment: PATH: '{{ansible_env.PATH + ":/lola"}}' lola: 'ido' @@ -41,9 +41,9 @@ - hosts: testhost vars: - - test1: - key1: val1 - - test2: + test1: + key1: val1 + test2: key1: not1 other1: val2 environment: "{{test1}}" diff --git a/test/integration/targets/error_from_connection/connection_plugins/dummy.py b/test/integration/targets/error_from_connection/connection_plugins/dummy.py index 59a81a1b..d322fe0d 100644 --- a/test/integration/targets/error_from_connection/connection_plugins/dummy.py +++ b/test/integration/targets/error_from_connection/connection_plugins/dummy.py @@ -11,7 +11,6 @@ DOCUMENTATION = """ version_added: "2.0" options: {} """ -import ansible.constants as C from ansible.errors import AnsibleError from ansible.plugins.connection import ConnectionBase diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml index 7bf18c5e..2aef5957 100644 --- a/test/integration/targets/expect/tasks/main.yml +++ b/test/integration/targets/expect/tasks/main.yml @@ -148,6 +148,15 @@ - "echo_result.stdout_lines[-2] == 'foobar'" - "echo_result.stdout_lines[-1] == 'bar'" +- name: test timeout is valid as null + expect: + command: "{{ansible_python_interpreter}} {{test_command_file}}" + responses: + foo: bar + echo: true + timeout: null # wait indefinitely + timeout: 2 # but shouldn't be waiting long + - name: test response list expect: command: "{{ansible_python_interpreter}} {{test_command_file}} foo foo" diff --git a/test/integration/targets/facts_linux_network/aliases b/test/integration/targets/facts_linux_network/aliases index 100ce23a..c9e1dc55 100644 --- a/test/integration/targets/facts_linux_network/aliases +++ b/test/integration/targets/facts_linux_network/aliases @@ -1,7 +1,6 @@ needs/privileged shippable/posix/group1 skip/freebsd -skip/osx skip/macos context/target destructive diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml index 8a6b5b7b..d0bf9bdc 100644 --- a/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml +++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml @@ -28,6 +28,15 @@ register: failed_fetch_dest_dir ignore_errors: true +- name: Test unreachable + fetch: + src: "{{ remote_tmp_dir }}/orig" + dest: "{{ output_dir }}" + register: unreachable_fetch + ignore_unreachable: true + vars: + ansible_user: wrong + - name: Ensure fetch failed assert: that: @@ -39,3 +48,4 @@ - failed_fetch_no_access.msg is search('file is not readable') - failed_fetch_dest_dir is failed - failed_fetch_dest_dir.msg is search('dest is an existing directory') + - unreachable_fetch is unreachable diff --git a/test/integration/targets/file/tasks/link_rewrite.yml b/test/integration/targets/file/tasks/link_rewrite.yml index b0e1af3e..2416c2ca 100644 --- a/test/integration/targets/file/tasks/link_rewrite.yml +++ b/test/integration/targets/file/tasks/link_rewrite.yml @@ -16,11 +16,11 @@ dest: "{{ tempdir.path }}/somelink" state: link -- stat: +- stat: path: "{{ tempdir.path }}/somelink" register: link -- stat: +- stat: path: "{{ tempdir.path }}/somefile" register: file @@ -32,12 +32,12 @@ - file: path: "{{ tempdir.path }}/somelink" mode: 0644 - -- stat: + +- stat: path: "{{ tempdir.path }}/somelink" register: link -- stat: +- stat: path: "{{ tempdir.path }}/somefile" register: file diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml index a5bd68d7..c1b4c791 100644 --- a/test/integration/targets/file/tasks/main.yml +++ b/test/integration/targets/file/tasks/main.yml @@ -779,7 +779,7 @@ register: touch_result_in_check_mode_fails_not_existing_group - assert: - that: + that: - touch_result_in_check_mode_not_existing.changed - touch_result_in_check_mode_preserve_access_time.changed - touch_result_in_check_mode_change_only_mode.changed diff --git a/test/integration/targets/filter_core/tasks/main.yml b/test/integration/targets/filter_core/tasks/main.yml index 2d084191..9d287a18 100644 --- a/test/integration/targets/filter_core/tasks/main.yml +++ b/test/integration/targets/filter_core/tasks/main.yml @@ -454,6 +454,38 @@ - password_hash_2 is failed - "'not support' in password_hash_2.msg" +- name: install passlib if needed + pip: + name: passlib + state: present + register: installed_passlib + +- name: test using passlib with an unsupported hash type + set_fact: + foo: '{{"hey"|password_hash("msdcc")}}' + ignore_errors: yes + register: unsupported_hash_type + +- name: remove passlib if it was installed + pip: + name: passlib + state: absent + when: installed_passlib.changed + +- assert: + that: + - unsupported_hash_type.msg == msg + vars: + msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512" + +- name: test password_hash can work with bcrypt without passlib installed + debug: + msg: "{{ 'somestring'|password_hash('bcrypt') }}" + register: crypt_bcrypt + # Some implementations of crypt do not fail outright and return some short value. + failed_when: crypt_bcrypt is failed or (crypt_bcrypt.msg|length|int) != 60 + when: ansible_facts.os_family in ['RedHat', 'Debian'] + - name: Verify to_uuid throws on weird namespace set_fact: foo: '{{"hey"|to_uuid(namespace=22)}}' diff --git a/test/integration/targets/filter_encryption/base.yml b/test/integration/targets/filter_encryption/base.yml index 8bf25f77..1479f734 100644 --- a/test/integration/targets/filter_encryption/base.yml +++ b/test/integration/targets/filter_encryption/base.yml @@ -2,6 +2,7 @@ gather_facts: true vars: data: secret + data2: 'foo: bar\n' dvault: '{{ "secret"|vault("test")}}' password: test s_32: '{{(2**31-1)}}' @@ -21,6 +22,15 @@ is_64: '{{ "64" in ansible_facts["architecture"] }}' salt: '{{ is_64|bool|ternary(s_64, s_32)|random(seed=inventory_hostname)}}' vaultedstring: '{{ is_64|bool|ternary(vaultedstring_64, vaultedstring_32) }}' + # command line vaulted data2 + vaulted_id: !vault | + $ANSIBLE_VAULT;1.2;AES256;test1 + 36383733336533656264393332663131613335333332346439356164383935656234663631356430 + 3533353537343834333538356366376233326364613362640a623832636339363966336238393039 + 35316562626335306534356162623030613566306235623863373036626531346364626166656134 + 3063376436656635330a363636376131663362633731313964353061663661376638326461393736 + 3863 + vaulted_to_id: "{{data2|vault('test1@secret', vault_id='test1')}}" tasks: - name: check vaulting @@ -35,3 +45,5 @@ that: - vaultedstring|unvault(password) == data - vault|unvault(password) == data + - vaulted_id|unvault('test1@secret', vault_id='test1') + - vaulted_to_id|unvault('test1@secret', vault_id='test1') diff --git a/test/integration/targets/filter_mathstuff/tasks/main.yml b/test/integration/targets/filter_mathstuff/tasks/main.yml index 019f00e4..33fcae82 100644 --- a/test/integration/targets/filter_mathstuff/tasks/main.yml +++ b/test/integration/targets/filter_mathstuff/tasks/main.yml @@ -64,44 +64,44 @@ that: - '[1,2,3]|intersect([4,5,6]) == []' - '[1,2,3]|intersect([3,4,5,6]) == [3]' - - '[1,2,3]|intersect([3,2,1]) == [1,2,3]' - - '(1,2,3)|intersect((4,5,6))|list == []' - - '(1,2,3)|intersect((3,4,5,6))|list == [3]' + - '[1,2,3]|intersect([3,2,1]) | sort == [1,2,3]' + - '(1,2,3)|intersect((4,5,6)) == []' + - '(1,2,3)|intersect((3,4,5,6)) == [3]' - '["a","A","b"]|intersect(["B","c","C"]) == []' - '["a","A","b"]|intersect(["b","B","c","C"]) == ["b"]' - - '["a","A","b"]|intersect(["b","A","a"]) == ["a","A","b"]' - - '("a","A","b")|intersect(("B","c","C"))|list == []' - - '("a","A","b")|intersect(("b","B","c","C"))|list == ["b"]' + - '["a","A","b"]|intersect(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]' + - '("a","A","b")|intersect(("B","c","C")) == []' + - '("a","A","b")|intersect(("b","B","c","C")) == ["b"]' - name: Verify difference tags: difference assert: that: - - '[1,2,3]|difference([4,5,6]) == [1,2,3]' - - '[1,2,3]|difference([3,4,5,6]) == [1,2]' + - '[1,2,3]|difference([4,5,6]) | sort == [1,2,3]' + - '[1,2,3]|difference([3,4,5,6]) | sort == [1,2]' - '[1,2,3]|difference([3,2,1]) == []' - - '(1,2,3)|difference((4,5,6))|list == [1,2,3]' - - '(1,2,3)|difference((3,4,5,6))|list == [1,2]' - - '["a","A","b"]|difference(["B","c","C"]) == ["a","A","b"]' - - '["a","A","b"]|difference(["b","B","c","C"]) == ["a","A"]' + - '(1,2,3)|difference((4,5,6)) | sort == [1,2,3]' + - '(1,2,3)|difference((3,4,5,6)) | sort == [1,2]' + - '["a","A","b"]|difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","a","b"]' + - '["a","A","b"]|difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","a"]' - '["a","A","b"]|difference(["b","A","a"]) == []' - - '("a","A","b")|difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","a","b"]' - - '("a","A","b")|difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","a"]' + - '("a","A","b")|difference(("B","c","C")) | sort(case_sensitive=True) == ["A","a","b"]' + - '("a","A","b")|difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","a"]' - name: Verify symmetric_difference tags: symmetric_difference assert: that: - - '[1,2,3]|symmetric_difference([4,5,6]) == [1,2,3,4,5,6]' - - '[1,2,3]|symmetric_difference([3,4,5,6]) == [1,2,4,5,6]' + - '[1,2,3]|symmetric_difference([4,5,6]) | sort == [1,2,3,4,5,6]' + - '[1,2,3]|symmetric_difference([3,4,5,6]) | sort == [1,2,4,5,6]' - '[1,2,3]|symmetric_difference([3,2,1]) == []' - - '(1,2,3)|symmetric_difference((4,5,6))|list == [1,2,3,4,5,6]' - - '(1,2,3)|symmetric_difference((3,4,5,6))|list == [1,2,4,5,6]' - - '["a","A","b"]|symmetric_difference(["B","c","C"]) == ["a","A","b","B","c","C"]' - - '["a","A","b"]|symmetric_difference(["b","B","c","C"]) == ["a","A","B","c","C"]' + - '(1,2,3)|symmetric_difference((4,5,6)) | sort == [1,2,3,4,5,6]' + - '(1,2,3)|symmetric_difference((3,4,5,6)) | sort == [1,2,4,5,6]' + - '["a","A","b"]|symmetric_difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '["a","A","b"]|symmetric_difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","c"]' - '["a","A","b"]|symmetric_difference(["b","A","a"]) == []' - - '("a","A","b")|symmetric_difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]' - - '("a","A","b")|symmetric_difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","c"]' + - '("a","A","b")|symmetric_difference(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '("a","A","b")|symmetric_difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","c"]' - name: Verify union tags: union @@ -112,11 +112,11 @@ - '[1,2,3]|union([3,2,1]) == [1,2,3]' - '(1,2,3)|union((4,5,6))|list == [1,2,3,4,5,6]' - '(1,2,3)|union((3,4,5,6))|list == [1,2,3,4,5,6]' - - '["a","A","b"]|union(["B","c","C"]) == ["a","A","b","B","c","C"]' - - '["a","A","b"]|union(["b","B","c","C"]) == ["a","A","b","B","c","C"]' - - '["a","A","b"]|union(["b","A","a"]) == ["a","A","b"]' - - '("a","A","b")|union(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]' - - '("a","A","b")|union(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '["a","A","b"]|union(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '["a","A","b"]|union(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '["a","A","b"]|union(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]' + - '("a","A","b")|union(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' + - '("a","A","b")|union(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]' - name: Verify min tags: min diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml index 89c62b9b..9c4a960f 100644 --- a/test/integration/targets/find/tasks/main.yml +++ b/test/integration/targets/find/tasks/main.yml @@ -374,3 +374,6 @@ - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list' - '"checksum" in result.files[0]' + +- name: Run mode tests + import_tasks: mode.yml diff --git a/test/integration/targets/fork_safe_stdio/aliases b/test/integration/targets/fork_safe_stdio/aliases index e968db72..7761837e 100644 --- a/test/integration/targets/fork_safe_stdio/aliases +++ b/test/integration/targets/fork_safe_stdio/aliases @@ -1,3 +1,3 @@ shippable/posix/group3 context/controller -skip/macos +needs/target/test_utils diff --git a/test/integration/targets/fork_safe_stdio/runme.sh b/test/integration/targets/fork_safe_stdio/runme.sh index 4438c3fe..863582f3 100755 --- a/test/integration/targets/fork_safe_stdio/runme.sh +++ b/test/integration/targets/fork_safe_stdio/runme.sh @@ -7,7 +7,7 @@ echo "testing for stdio deadlock on forked workers (10s timeout)..." # Enable a callback that trips deadlocks on forked-child stdout, time out after 10s; forces running # in a pty, since that tends to be much slower than raw file I/O and thus more likely to trigger the deadlock. # Redirect stdout to /dev/null since it's full of non-printable garbage we don't want to display unless it failed -ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py timeout 10s ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$? +ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py ../test_utils/scripts/timeout.py -- 10 ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$? if [ $RC != 0 ]; then echo "failed; likely stdout deadlock. dumping raw output (may be very large)" diff --git a/test/integration/targets/gathering_facts/library/file_utils.py b/test/integration/targets/gathering_facts/library/file_utils.py index 58538029..38fa9265 100644 --- a/test/integration/targets/gathering_facts/library/file_utils.py +++ b/test/integration/targets/gathering_facts/library/file_utils.py @@ -1,9 +1,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import sys - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.facts.utils import ( get_file_content, diff --git a/test/integration/targets/gathering_facts/runme.sh b/test/integration/targets/gathering_facts/runme.sh index c1df560c..a90de0f0 100755 --- a/test/integration/targets/gathering_facts/runme.sh +++ b/test/integration/targets/gathering_facts/runme.sh @@ -25,3 +25,17 @@ ansible-playbook test_module_defaults.yml "$@" --tags default_fact_module ANSIBLE_FACTS_MODULES='ansible.legacy.setup' ansible-playbook test_module_defaults.yml "$@" --tags custom_fact_module ansible-playbook test_module_defaults.yml "$@" --tags networking + +# test it works by default +ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ "$@" + +# test that gather_facts will timeout parallel modules that dont support gather_timeout when using gather_Timeout +ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=1 parallel=true' "$@" 2>&1 |grep 'Timeout exceeded' + +# test that gather_facts parallel w/o timing out +ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=30 parallel=true' "$@" 2>&1 |grep -v 'Timeout exceeded' + + +# test parallelism +ANSIBLE_FACTS_MODULES='dummy1,dummy2,dummy3' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=30 parallel=true' "$@" 2>&1 +rm "${OUTPUT_DIR}/canary.txt" diff --git a/test/integration/targets/get_url/tasks/main.yml b/test/integration/targets/get_url/tasks/main.yml index 09814c70..c26cc08b 100644 --- a/test/integration/targets/get_url/tasks/main.yml +++ b/test/integration/targets/get_url/tasks/main.yml @@ -398,6 +398,8 @@ port: '{{ http_port }}' state: started +- include_tasks: hashlib.yml + - name: download src with sha1 checksum url in check mode get_url: url: 'http://localhost:{{ http_port }}/27617.txt' diff --git a/test/integration/targets/get_url/tasks/use_netrc.yml b/test/integration/targets/get_url/tasks/use_netrc.yml index e1852a81..234c904a 100644 --- a/test/integration/targets/get_url/tasks/use_netrc.yml +++ b/test/integration/targets/get_url/tasks/use_netrc.yml @@ -22,7 +22,7 @@ register: response_failed - name: Parse token from msg.txt - set_fact: + set_fact: token: "{{ (response_failed['content'] | b64decode | from_json).token }}" - name: assert Test Bearer authorization is failed with netrc @@ -48,7 +48,7 @@ register: response - name: Parse token from msg.txt - set_fact: + set_fact: token: "{{ (response['content'] | b64decode | from_json).token }}" - name: assert Test Bearer authorization is successfull with use_netrc=False @@ -64,4 +64,4 @@ state: absent with_items: - "{{ remote_tmp_dir }}/netrc" - - "{{ remote_tmp_dir }}/msg.txt"
\ No newline at end of file + - "{{ remote_tmp_dir }}/msg.txt" diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml index e0585ca3..20f1b4e9 100644 --- a/test/integration/targets/git/tasks/depth.yml +++ b/test/integration/targets/git/tasks/depth.yml @@ -95,14 +95,16 @@ repo: 'file://{{ repo_dir|expanduser }}/shallow' dest: '{{ checkout_dir }}' depth: 1 - version: master + version: >- + {{ git_default_branch }} - name: DEPTH | run a second time (now fetch, not clone) git: repo: 'file://{{ repo_dir|expanduser }}/shallow' dest: '{{ checkout_dir }}' depth: 1 - version: master + version: >- + {{ git_default_branch }} register: git_fetch - name: DEPTH | ensure the fetch succeeded @@ -120,7 +122,8 @@ repo: 'file://{{ repo_dir|expanduser }}/shallow' dest: '{{ checkout_dir }}' depth: 1 - version: master + version: >- + {{ git_default_branch }} - name: DEPTH | switch to older branch with depth=1 (uses fetch) git: diff --git a/test/integration/targets/git/tasks/forcefully-fetch-tag.yml b/test/integration/targets/git/tasks/forcefully-fetch-tag.yml index 47c37478..db35e048 100644 --- a/test/integration/targets/git/tasks/forcefully-fetch-tag.yml +++ b/test/integration/targets/git/tasks/forcefully-fetch-tag.yml @@ -11,7 +11,7 @@ git add leet; git commit -m uh-oh; git tag -f herewego; - git push --tags origin master + git push --tags origin '{{ git_default_branch }}' args: chdir: "{{ repo_dir }}/tag_force_push_clone1" @@ -26,7 +26,7 @@ git add leet; git commit -m uh-oh; git tag -f herewego; - git push -f --tags origin master + git push -f --tags origin '{{ git_default_branch }}' args: chdir: "{{ repo_dir }}/tag_force_push_clone1" diff --git a/test/integration/targets/git/tasks/gpg-verification.yml b/test/integration/targets/git/tasks/gpg-verification.yml index 8c8834a9..bd57ed1d 100644 --- a/test/integration/targets/git/tasks/gpg-verification.yml +++ b/test/integration/targets/git/tasks/gpg-verification.yml @@ -37,8 +37,10 @@ environment: - GNUPGHOME: "{{ git_gpg_gpghome }}" shell: | - set -e + set -eEu + git init + touch an_empty_file git add an_empty_file git commit --no-gpg-sign --message "Commit, and don't sign" @@ -48,11 +50,11 @@ git tag --annotate --message "This is not a signed tag" unsigned_annotated_tag HEAD git commit --allow-empty --gpg-sign --message "Commit, and sign" git tag --sign --message "This is a signed tag" signed_annotated_tag HEAD - git checkout -b some_branch/signed_tip master + git checkout -b some_branch/signed_tip '{{ git_default_branch }}' git commit --allow-empty --gpg-sign --message "Commit, and sign" - git checkout -b another_branch/unsigned_tip master + git checkout -b another_branch/unsigned_tip '{{ git_default_branch }}' git commit --allow-empty --no-gpg-sign --message "Commit, and don't sign" - git checkout master + git checkout '{{ git_default_branch }}' args: chdir: "{{ git_gpg_source }}" diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml index 0e0cf684..409bbae2 100644 --- a/test/integration/targets/git/tasks/localmods.yml +++ b/test/integration/targets/git/tasks/localmods.yml @@ -1,6 +1,17 @@ # test for https://github.com/ansible/ansible-modules-core/pull/5505 - name: LOCALMODS | prepare old git repo - shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1" + shell: | + set -eEu + + rm -rf localmods + mkdir localmods + cd localmods + + git init + + echo "1" > a + git add a + git commit -m "1" args: chdir: "{{repo_dir}}" @@ -55,7 +66,18 @@ # localmods and shallow clone - name: LOCALMODS | prepare old git repo - shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1" + shell: | + set -eEu + + rm -rf localmods + mkdir localmods + cd localmods + + git init + + echo "1" > a + git add a + git commit -m "1" args: chdir: "{{repo_dir}}" diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml index ed06eab5..c990251f 100644 --- a/test/integration/targets/git/tasks/main.yml +++ b/test/integration/targets/git/tasks/main.yml @@ -16,27 +16,37 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see <http://www.gnu.org/licenses/>. -- import_tasks: setup.yml -- import_tasks: setup-local-repos.yml +# NOTE: Moving `$HOME` to tmp dir allows this integration test be +# NOTE: non-destructive. There is no other way to instruct Git use a custom +# NOTE: config path. There are new `$GIT_CONFIG_KEY_{COUNT,KEY,VALUE}` vars +# NOTE: for setting specific configuration values but those are only available +# NOTE: since Git v2.31 which is why we cannot rely on them yet. -- import_tasks: formats.yml -- import_tasks: missing_hostkey.yml -- import_tasks: missing_hostkey_acceptnew.yml -- import_tasks: no-destination.yml -- import_tasks: specific-revision.yml -- import_tasks: submodules.yml -- import_tasks: change-repo-url.yml -- import_tasks: depth.yml -- import_tasks: single-branch.yml -- import_tasks: checkout-new-tag.yml -- include_tasks: gpg-verification.yml - when: +- block: + - import_tasks: setup.yml + - import_tasks: setup-local-repos.yml + + - import_tasks: formats.yml + - import_tasks: missing_hostkey.yml + - import_tasks: missing_hostkey_acceptnew.yml + - import_tasks: no-destination.yml + - import_tasks: specific-revision.yml + - import_tasks: submodules.yml + - import_tasks: change-repo-url.yml + - import_tasks: depth.yml + - import_tasks: single-branch.yml + - import_tasks: checkout-new-tag.yml + - include_tasks: gpg-verification.yml + when: - not gpg_version.stderr - gpg_version.stdout - not (ansible_os_family == 'RedHat' and ansible_distribution_major_version is version('7', '<')) -- import_tasks: localmods.yml -- import_tasks: reset-origin.yml -- import_tasks: ambiguous-ref.yml -- import_tasks: archive.yml -- import_tasks: separate-git-dir.yml -- import_tasks: forcefully-fetch-tag.yml + - import_tasks: localmods.yml + - import_tasks: reset-origin.yml + - import_tasks: ambiguous-ref.yml + - import_tasks: archive.yml + - import_tasks: separate-git-dir.yml + - import_tasks: forcefully-fetch-tag.yml + environment: + HOME: >- + {{ remote_tmp_dir }} diff --git a/test/integration/targets/git/tasks/missing_hostkey.yml b/test/integration/targets/git/tasks/missing_hostkey.yml index 136c5d5d..d8a2a818 100644 --- a/test/integration/targets/git/tasks/missing_hostkey.yml +++ b/test/integration/targets/git/tasks/missing_hostkey.yml @@ -35,7 +35,8 @@ git: repo: '{{ repo_format3 }}' dest: '{{ checkout_dir }}' - version: 'master' + version: >- + {{ git_default_branch }} accept_hostkey: false # should already have been accepted key_file: '{{ github_ssh_private_key }}' ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts' diff --git a/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml index 3fd19067..338ae081 100644 --- a/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml +++ b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml @@ -55,7 +55,8 @@ git: repo: '{{ repo_format3 }}' dest: '{{ checkout_dir }}' - version: 'master' + version: >- + {{ git_default_branch }} accept_newhostkey: false # should already have been accepted key_file: '{{ github_ssh_private_key }}' ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts' diff --git a/test/integration/targets/git/tasks/reset-origin.yml b/test/integration/targets/git/tasks/reset-origin.yml index 8fddd4b1..cb497c44 100644 --- a/test/integration/targets/git/tasks/reset-origin.yml +++ b/test/integration/targets/git/tasks/reset-origin.yml @@ -12,7 +12,14 @@ state: directory - name: RESET-ORIGIN | Initialise the repo with a file named origin,see github.com/ansible/ansible/pull/22502 - shell: git init; echo "PR 22502" > origin; git add origin; git commit -m "PR 22502" + shell: | + set -eEu + + git init + + echo "PR 22502" > origin + git add origin + git commit -m "PR 22502" args: chdir: "{{ repo_dir }}/origin" diff --git a/test/integration/targets/git/tasks/setup-local-repos.yml b/test/integration/targets/git/tasks/setup-local-repos.yml index 584a1693..4626f102 100644 --- a/test/integration/targets/git/tasks/setup-local-repos.yml +++ b/test/integration/targets/git/tasks/setup-local-repos.yml @@ -9,15 +9,32 @@ - "{{ repo_dir }}/tag_force_push" - name: SETUP-LOCAL-REPOS | prepare minimal git repo - shell: git init; echo "1" > a; git add a; git commit -m "1" + shell: | + set -eEu + + git init + + echo "1" > a + git add a + git commit -m "1" args: chdir: "{{ repo_dir }}/minimal" - name: SETUP-LOCAL-REPOS | prepare git repo for shallow clone shell: | - git init; - echo "1" > a; git add a; git commit -m "1"; git tag earlytag; git branch earlybranch; - echo "2" > a; git add a; git commit -m "2"; + set -eEu + + git init + + echo "1" > a + git add a + git commit -m "1" + git tag earlytag + git branch earlybranch + + echo "2" > a + git add a + git commit -m "2" args: chdir: "{{ repo_dir }}/shallow" @@ -29,7 +46,10 @@ - name: SETUP-LOCAL-REPOS | prepare tmp git repo with two branches shell: | + set -eEu + git init + echo "1" > a; git add a; git commit -m "1" git checkout -b test_branch; echo "2" > a; git commit -m "2 on branch" a git checkout -b new_branch; echo "3" > a; git commit -m "3 on new branch" a @@ -40,6 +60,9 @@ # We make the repo here for consistency with the other repos, # but we finish setting it up in forcefully-fetch-tag.yml. - name: SETUP-LOCAL-REPOS | prepare tag_force_push git repo - shell: git init --bare + shell: | + set -eEu + + git init --bare args: chdir: "{{ repo_dir }}/tag_force_push" diff --git a/test/integration/targets/git/tasks/setup.yml b/test/integration/targets/git/tasks/setup.yml index 06511053..982c03ff 100644 --- a/test/integration/targets/git/tasks/setup.yml +++ b/test/integration/targets/git/tasks/setup.yml @@ -28,10 +28,44 @@ register: gpg_version - name: SETUP | set git global user.email if not already set - shell: git config --global user.email || git config --global user.email "noreply@example.com" + shell: git config --global user.email 'noreply@example.com' - name: SETUP | set git global user.name if not already set - shell: git config --global user.name || git config --global user.name "Ansible Test Runner" + shell: git config --global user.name 'Ansible Test Runner' + +- name: SETUP | set git global init.defaultBranch + shell: >- + git config --global init.defaultBranch '{{ git_default_branch }}' + +- name: SETUP | set git global init.templateDir + # NOTE: This custom Git repository template emulates the `init.defaultBranch` + # NOTE: setting on Git versions below 2.28. + # NOTE: Ref: https://superuser.com/a/1559582. + # NOTE: Other workarounds mentioned there, like invoking + # NOTE: `git symbolic-ref HEAD refs/heads/main` after each `git init` turned + # NOTE: out to have mysterious side effects that break the tests in surprising + # NOTE: ways. + shell: | + set -eEu + + git config --global \ + init.templateDir '{{ remote_tmp_dir }}/git-templates/git.git' + + mkdir -pv '{{ remote_tmp_dir }}/git-templates' + set +e + GIT_TEMPLATES_DIR=$(\ + 2>/dev/null \ + ls -1d \ + '/Library/Developer/CommandLineTools/usr/share/git-core/templates' \ + '/usr/local/share/git-core/templates' \ + '/usr/share/git-core/templates' \ + ) + set -e + >&2 echo "Found Git's default templates directory: ${GIT_TEMPLATES_DIR}" + cp -r "${GIT_TEMPLATES_DIR}" '{{ remote_tmp_dir }}/git-templates/git.git' + + echo 'ref: refs/heads/{{ git_default_branch }}' \ + > '{{ remote_tmp_dir }}/git-templates/git.git/HEAD' - name: SETUP | create repo_dir file: diff --git a/test/integration/targets/git/tasks/single-branch.yml b/test/integration/targets/git/tasks/single-branch.yml index 5cfb4d5b..ca8457ac 100644 --- a/test/integration/targets/git/tasks/single-branch.yml +++ b/test/integration/targets/git/tasks/single-branch.yml @@ -52,7 +52,8 @@ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches' dest: '{{ checkout_dir }}' single_branch: yes - version: master + version: >- + {{ git_default_branch }} register: single_branch_3 - name: SINGLE_BRANCH | Clone example git repo using single_branch with version again @@ -60,7 +61,8 @@ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches' dest: '{{ checkout_dir }}' single_branch: yes - version: master + version: >- + {{ git_default_branch }} register: single_branch_4 - name: SINGLE_BRANCH | List revisions diff --git a/test/integration/targets/git/tasks/specific-revision.yml b/test/integration/targets/git/tasks/specific-revision.yml index 26fa7cf3..f1fe41d5 100644 --- a/test/integration/targets/git/tasks/specific-revision.yml +++ b/test/integration/targets/git/tasks/specific-revision.yml @@ -162,7 +162,14 @@ path: "{{ checkout_dir }}" - name: SPECIFIC-REVISION | prepare origina repo - shell: git init; echo "1" > a; git add a; git commit -m "1" + shell: | + set -eEu + + git init + + echo "1" > a + git add a + git commit -m "1" args: chdir: "{{ checkout_dir }}" @@ -191,7 +198,14 @@ force: yes - name: SPECIFIC-REVISION | create new commit in original - shell: git init; echo "2" > b; git add b; git commit -m "2" + shell: | + set -eEu + + git init + + echo "2" > b + git add b + git commit -m "2" args: chdir: "{{ checkout_dir }}" diff --git a/test/integration/targets/git/vars/main.yml b/test/integration/targets/git/vars/main.yml index b38531f3..55c7c438 100644 --- a/test/integration/targets/git/vars/main.yml +++ b/test/integration/targets/git/vars/main.yml @@ -41,6 +41,7 @@ repo_update_url_2: 'https://github.com/ansible-test-robinro/git-test-new' known_host_files: - "{{ lookup('env','HOME') }}/.ssh/known_hosts" - '/etc/ssh/ssh_known_hosts' +git_default_branch: main git_version_supporting_depth: 1.9.1 git_version_supporting_ls_remote: 1.7.5 git_version_supporting_single_branch: 1.7.10 diff --git a/test/integration/targets/group/tasks/main.yml b/test/integration/targets/group/tasks/main.yml index eb8126dd..21235240 100644 --- a/test/integration/targets/group/tasks/main.yml +++ b/test/integration/targets/group/tasks/main.yml @@ -16,25 +16,4 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see <http://www.gnu.org/licenses/>. -- name: ensure test groups are deleted before the test - group: - name: '{{ item }}' - state: absent - loop: - - ansibullgroup - - ansibullgroup2 - - ansibullgroup3 - -- block: - - name: run tests - include_tasks: tests.yml - - always: - - name: remove test groups after test - group: - name: '{{ item }}' - state: absent - loop: - - ansibullgroup - - ansibullgroup2 - - ansibullgroup3
\ No newline at end of file +- import_tasks: tests.yml diff --git a/test/integration/targets/group/tasks/tests.yml b/test/integration/targets/group/tasks/tests.yml index f9a81220..eb92cd1d 100644 --- a/test/integration/targets/group/tasks/tests.yml +++ b/test/integration/targets/group/tasks/tests.yml @@ -1,343 +1,412 @@ --- -## -## group add -## - -- name: create group (check mode) - group: - name: ansibullgroup - state: present - register: create_group_check - check_mode: True - -- name: get result of create group (check mode) - script: 'grouplist.sh "{{ ansible_distribution }}"' - register: create_group_actual_check - -- name: assert create group (check mode) - assert: - that: - - create_group_check is changed - - '"ansibullgroup" not in create_group_actual_check.stdout_lines' - -- name: create group - group: - name: ansibullgroup - state: present - register: create_group - -- name: get result of create group - script: 'grouplist.sh "{{ ansible_distribution }}"' - register: create_group_actual - -- name: assert create group - assert: - that: - - create_group is changed - - create_group.gid is defined - - '"ansibullgroup" in create_group_actual.stdout_lines' - -- name: create group (idempotent) +- name: ensure test groups are deleted before the test group: - name: ansibullgroup - state: present - register: create_group_again + name: '{{ item }}' + state: absent + loop: + - ansibullgroup + - ansibullgroup2 + - ansibullgroup3 -- name: assert create group (idempotent) - assert: - that: - - not create_group_again is changed +- block: + ## + ## group add + ## -## -## group check -## + - name: create group (check mode) + group: + name: ansibullgroup + state: present + register: create_group_check + check_mode: true -- name: run existing group check tests - group: - name: "{{ create_group_actual.stdout_lines|random }}" - state: present - with_sequence: start=1 end=5 - register: group_test1 - -- name: validate results for testcase 1 - assert: - that: - - group_test1.results is defined - - group_test1.results|length == 5 - -- name: validate change results for testcase 1 - assert: - that: - - not group_test1 is changed - -## -## group add with gid -## - -- name: get the next available gid - script: gidget.py - args: - executable: '{{ ansible_python_interpreter }}' - register: gid - -- name: create a group with a gid (check mode) - group: - name: ansibullgroup2 - gid: '{{ gid.stdout_lines[0] }}' - state: present - register: create_group_gid_check - check_mode: True - -- name: get result of create a group with a gid (check mode) - script: 'grouplist.sh "{{ ansible_distribution }}"' - register: create_group_gid_actual_check - -- name: assert create group with a gid (check mode) - assert: - that: - - create_group_gid_check is changed - - '"ansibullgroup2" not in create_group_gid_actual_check.stdout_lines' - -- name: create a group with a gid - group: - name: ansibullgroup2 - gid: '{{ gid.stdout_lines[0] }}' - state: present - register: create_group_gid - -- name: get gid of created group - command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('ansibullgroup2').gr_gid)\"" - register: create_group_gid_actual - -- name: assert create group with a gid - assert: - that: - - create_group_gid is changed - - create_group_gid.gid | int == gid.stdout_lines[0] | int - - create_group_gid_actual.stdout | trim | int == gid.stdout_lines[0] | int - -- name: create a group with a gid (idempotent) - group: - name: ansibullgroup2 - gid: '{{ gid.stdout_lines[0] }}' - state: present - register: create_group_gid_again + - name: get result of create group (check mode) + script: 'grouplist.sh "{{ ansible_distribution }}"' + register: create_group_actual_check -- name: assert create group with a gid (idempotent) - assert: - that: - - not create_group_gid_again is changed - - create_group_gid_again.gid | int == gid.stdout_lines[0] | int + - name: assert create group (check mode) + assert: + that: + - create_group_check is changed + - '"ansibullgroup" not in create_group_actual_check.stdout_lines' -- block: - - name: create a group with a non-unique gid + - name: create group group: - name: ansibullgroup3 - gid: '{{ gid.stdout_lines[0] }}' - non_unique: true + name: ansibullgroup state: present - register: create_group_gid_non_unique + register: create_group - - name: validate gid required with non_unique + - name: get result of create group + script: 'grouplist.sh "{{ ansible_distribution }}"' + register: create_group_actual + + - name: assert create group + assert: + that: + - create_group is changed + - create_group.gid is defined + - '"ansibullgroup" in create_group_actual.stdout_lines' + + - name: create group (idempotent) group: - name: foo - non_unique: true - register: missing_gid - ignore_errors: true + name: ansibullgroup + state: present + register: create_group_again - - name: assert create group with a non unique gid + - name: assert create group (idempotent) assert: that: - - create_group_gid_non_unique is changed - - create_group_gid_non_unique.gid | int == gid.stdout_lines[0] | int - - missing_gid is failed - when: ansible_facts.distribution not in ['MacOSX', 'Alpine'] + - not create_group_again is changed -## -## group remove -## + ## + ## group check + ## -- name: delete group (check mode) - group: - name: ansibullgroup - state: absent - register: delete_group_check - check_mode: True + - name: run existing group check tests + group: + name: "{{ create_group_actual.stdout_lines|random }}" + state: present + with_sequence: start=1 end=5 + register: group_test1 -- name: get result of delete group (check mode) - script: grouplist.sh "{{ ansible_distribution }}" - register: delete_group_actual_check + - name: validate results for testcase 1 + assert: + that: + - group_test1.results is defined + - group_test1.results|length == 5 -- name: assert delete group (check mode) - assert: - that: - - delete_group_check is changed - - '"ansibullgroup" in delete_group_actual_check.stdout_lines' + - name: validate change results for testcase 1 + assert: + that: + - not group_test1 is changed -- name: delete group - group: - name: ansibullgroup - state: absent - register: delete_group + ## + ## group add with gid + ## -- name: get result of delete group - script: grouplist.sh "{{ ansible_distribution }}" - register: delete_group_actual + - name: get the next available gid + script: get_free_gid.py + args: + executable: '{{ ansible_python_interpreter }}' + register: gid -- name: assert delete group - assert: - that: - - delete_group is changed - - '"ansibullgroup" not in delete_group_actual.stdout_lines' + - name: create a group with a gid (check mode) + group: + name: ansibullgroup2 + gid: '{{ gid.stdout_lines[0] }}' + state: present + register: create_group_gid_check + check_mode: true -- name: delete group (idempotent) - group: - name: ansibullgroup - state: absent - register: delete_group_again - -- name: assert delete group (idempotent) - assert: - that: - - not delete_group_again is changed - -- name: Ensure lgroupadd is present - action: "{{ ansible_facts.pkg_mgr }}" - args: - name: libuser - state: present - when: ansible_facts.system in ['Linux'] and ansible_distribution != 'Alpine' and ansible_os_family != 'Suse' - tags: - - user_test_local_mode - -- name: Ensure lgroupadd is present - Alpine - command: apk add -U libuser - when: ansible_distribution == 'Alpine' - tags: - - user_test_local_mode - -# https://github.com/ansible/ansible/issues/56481 -- block: - - name: Test duplicate GID with local=yes - group: - name: "{{ item }}" - gid: 1337 - local: yes - loop: - - group1_local_test - - group2_local_test - ignore_errors: yes - register: local_duplicate_gid_result - - - assert: - that: - - local_duplicate_gid_result['results'][0] is success - - local_duplicate_gid_result['results'][1]['msg'] == "GID '1337' already exists with group 'group1_local_test'" - always: - - name: Cleanup + - name: get result of create a group with a gid (check mode) + script: 'grouplist.sh "{{ ansible_distribution }}"' + register: create_group_gid_actual_check + + - name: assert create group with a gid (check mode) + assert: + that: + - create_group_gid_check is changed + - '"ansibullgroup2" not in create_group_gid_actual_check.stdout_lines' + + - name: create a group with a gid group: - name: group1_local_test - state: absent - # only applicable to Linux, limit further to CentOS where 'luseradd' is installed - when: ansible_distribution == 'CentOS' + name: ansibullgroup2 + gid: '{{ gid.stdout_lines[0] }}' + state: present + register: create_group_gid -# https://github.com/ansible/ansible/pull/59769 -- block: - - name: create a local group with a gid - group: - name: group1_local_test - gid: 1337 - local: yes - state: present - register: create_local_group_gid - - - name: get gid of created local group - command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_local_test').gr_gid)\"" - register: create_local_group_gid_actual - - - name: assert create local group with a gid - assert: + - name: get gid of created group + script: "get_gid_for_group.py ansibullgroup2" + args: + executable: '{{ ansible_python_interpreter }}' + register: create_group_gid_actual + + - name: assert create group with a gid + assert: that: - - create_local_group_gid is changed - - create_local_group_gid.gid | int == 1337 | int - - create_local_group_gid_actual.stdout | trim | int == 1337 | int - - - name: create a local group with a gid (idempotent) - group: - name: group1_local_test - gid: 1337 - state: present - register: create_local_group_gid_again - - - name: assert create local group with a gid (idempotent) - assert: + - create_group_gid is changed + - create_group_gid.gid | int == gid.stdout_lines[0] | int + - create_group_gid_actual.stdout | trim | int == gid.stdout_lines[0] | int + + - name: create a group with a gid (idempotent) + group: + name: ansibullgroup2 + gid: '{{ gid.stdout_lines[0] }}' + state: present + register: create_group_gid_again + + - name: assert create group with a gid (idempotent) + assert: that: - - not create_local_group_gid_again is changed - - create_local_group_gid_again.gid | int == 1337 | int - always: - - name: Cleanup create local group with a gid + - not create_group_gid_again is changed + - create_group_gid_again.gid | int == gid.stdout_lines[0] | int + + - block: + - name: create a group with a non-unique gid + group: + name: ansibullgroup3 + gid: '{{ gid.stdout_lines[0] }}' + non_unique: true + state: present + register: create_group_gid_non_unique + + - name: validate gid required with non_unique + group: + name: foo + non_unique: true + register: missing_gid + ignore_errors: true + + - name: assert create group with a non unique gid + assert: + that: + - create_group_gid_non_unique is changed + - create_group_gid_non_unique.gid | int == gid.stdout_lines[0] | int + - missing_gid is failed + when: ansible_facts.distribution not in ['MacOSX', 'Alpine'] + + ## + ## group remove + ## + + - name: delete group (check mode) group: - name: group1_local_test + name: ansibullgroup state: absent - # only applicable to Linux, limit further to CentOS where 'luseradd' is installed - when: ansible_distribution == 'CentOS' + register: delete_group_check + check_mode: true -# https://github.com/ansible/ansible/pull/59772 -- block: - - name: create group with a gid - group: - name: group1_test - gid: 1337 - local: no - state: present - register: create_group_gid - - - name: get gid of created group - command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_test').gr_gid)\"" - register: create_group_gid_actual - - - name: assert create group with a gid - assert: - that: - - create_group_gid is changed - - create_group_gid.gid | int == 1337 | int - - create_group_gid_actual.stdout | trim | int == 1337 | int - - - name: create local group with the same gid - group: - name: group1_test - gid: 1337 - local: yes - state: present - register: create_local_group_gid - - - name: assert create local group with a gid - assert: + - name: get result of delete group (check mode) + script: 'grouplist.sh "{{ ansible_distribution }}"' + register: delete_group_actual_check + + - name: assert delete group (check mode) + assert: that: - - create_local_group_gid.gid | int == 1337 | int - always: - - name: Cleanup create group with a gid + - delete_group_check is changed + - '"ansibullgroup" in delete_group_actual_check.stdout_lines' + + - name: delete group group: - name: group1_test - local: no + name: ansibullgroup state: absent - - name: Cleanup create local group with the same gid + register: delete_group + + - name: get result of delete group + script: 'grouplist.sh "{{ ansible_distribution }}"' + register: delete_group_actual + + - name: assert delete group + assert: + that: + - delete_group is changed + - '"ansibullgroup" not in delete_group_actual.stdout_lines' + + - name: delete group (idempotent) group: - name: group1_test - local: yes + name: ansibullgroup state: absent - # only applicable to Linux, limit further to CentOS where 'lgroupadd' is installed - when: ansible_distribution == 'CentOS' + register: delete_group_again -# create system group + - name: assert delete group (idempotent) + assert: + that: + - not delete_group_again is changed -- name: remove group - group: - name: ansibullgroup - state: absent + - name: Ensure lgroupadd is present + action: "{{ ansible_facts.pkg_mgr }}" + args: + name: libuser + state: present + when: ansible_facts.system in ['Linux'] and ansible_distribution != 'Alpine' and ansible_os_family != 'Suse' + tags: + - user_test_local_mode + + - name: Ensure lgroupadd is present - Alpine + command: apk add -U libuser + when: ansible_distribution == 'Alpine' + tags: + - user_test_local_mode + + # https://github.com/ansible/ansible/issues/56481 + - block: + - name: Test duplicate GID with local=yes + group: + name: "{{ item }}" + gid: 1337 + local: true + loop: + - group1_local_test + - group2_local_test + ignore_errors: true + register: local_duplicate_gid_result + + - assert: + that: + - local_duplicate_gid_result['results'][0] is success + - local_duplicate_gid_result['results'][1]['msg'] == "GID '1337' already exists with group 'group1_local_test'" + always: + - name: Cleanup + group: + name: group1_local_test + state: absent + # only applicable to Linux, limit further to CentOS where 'luseradd' is installed + when: ansible_distribution == 'CentOS' + + # https://github.com/ansible/ansible/pull/59769 + - block: + - name: create a local group with a gid + group: + name: group1_local_test + gid: 1337 + local: true + state: present + register: create_local_group_gid + + - name: get gid of created local group + script: "get_gid_for_group.py group1_local_test" + args: + executable: '{{ ansible_python_interpreter }}' + register: create_local_group_gid_actual + + - name: assert create local group with a gid + assert: + that: + - create_local_group_gid is changed + - create_local_group_gid.gid | int == 1337 | int + - create_local_group_gid_actual.stdout | trim | int == 1337 | int + + - name: create a local group with a gid (idempotent) + group: + name: group1_local_test + gid: 1337 + state: present + register: create_local_group_gid_again + + - name: assert create local group with a gid (idempotent) + assert: + that: + - not create_local_group_gid_again is changed + - create_local_group_gid_again.gid | int == 1337 | int + always: + - name: Cleanup create local group with a gid + group: + name: group1_local_test + state: absent + # only applicable to Linux, limit further to CentOS where 'luseradd' is installed + when: ansible_distribution == 'CentOS' + + # https://github.com/ansible/ansible/pull/59772 + - block: + - name: create group with a gid + group: + name: group1_test + gid: 1337 + local: false + state: present + register: create_group_gid + + - name: get gid of created group + script: "get_gid_for_group.py group1_test" + args: + executable: '{{ ansible_python_interpreter }}' + register: create_group_gid_actual + + - name: assert create group with a gid + assert: + that: + - create_group_gid is changed + - create_group_gid.gid | int == 1337 | int + - create_group_gid_actual.stdout | trim | int == 1337 | int + + - name: create local group with the same gid + group: + name: group1_test + gid: 1337 + local: true + state: present + register: create_local_group_gid + + - name: assert create local group with a gid + assert: + that: + - create_local_group_gid.gid | int == 1337 | int + always: + - name: Cleanup create group with a gid + group: + name: group1_test + local: false + state: absent + - name: Cleanup create local group with the same gid + group: + name: group1_test + local: true + state: absent + # only applicable to Linux, limit further to CentOS where 'lgroupadd' is installed + when: ansible_distribution == 'CentOS' + + # https://github.com/ansible/ansible/pull/78172 + - block: + - name: Create a group + group: + name: groupdeltest + state: present + + - name: Create user with primary group of groupdeltest + user: + name: groupdeluser + group: groupdeltest + state: present + + - name: Show we can't delete the group usually + group: + name: groupdeltest + state: absent + ignore_errors: true + register: failed_delete + + - name: assert we couldn't delete the group + assert: + that: + - failed_delete is failed + + - name: force delete the group + group: + name: groupdeltest + force: true + state: absent + + always: + - name: Cleanup user + user: + name: groupdeluser + state: absent + + - name: Cleanup group + group: + name: groupdeltest + state: absent + when: ansible_distribution not in ["MacOSX", "Alpine", "FreeBSD"] + + # create system group + + - name: remove group + group: + name: ansibullgroup + state: absent -- name: create system group - group: - name: ansibullgroup - state: present - system: yes + - name: create system group + group: + name: ansibullgroup + state: present + system: true + + always: + - name: remove test groups after test + group: + name: '{{ item }}' + state: absent + loop: + - ansibullgroup + - ansibullgroup2 + - ansibullgroup3 diff --git a/test/integration/targets/handlers/runme.sh b/test/integration/targets/handlers/runme.sh index 76fc99d8..368ca44d 100755 --- a/test/integration/targets/handlers/runme.sh +++ b/test/integration/targets/handlers/runme.sh @@ -50,6 +50,9 @@ for strategy in linear free; do [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags force_false_in_play --force-handlers \ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_B" ] + # https://github.com/ansible/ansible/pull/80898 + [ "$(ansible-playbook 80880.yml -i inventory.handlers -vv "$@" 2>&1)" ] + unset ANSIBLE_STRATEGY done @@ -66,6 +69,9 @@ done # Notify handler listen ansible-playbook test_handlers_listen.yml -i inventory.handlers -v "$@" +# https://github.com/ansible/ansible/issues/82363 +ansible-playbook test_multiple_handlers_with_recursive_notification.yml -i inventory.handlers -v "$@" + # Notify inexistent handlers results in error set +e result="$(ansible-playbook test_handlers_inexistent_notify.yml -i inventory.handlers "$@" 2>&1)" @@ -181,3 +187,24 @@ grep out.txt -e "ERROR! Using a block as a handler is not supported." ansible-playbook test_block_as_handler-import.yml "$@" 2>&1 | tee out.txt grep out.txt -e "ERROR! Using a block as a handler is not supported." + +ansible-playbook test_include_role_handler_once.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'handler ran')" = "1" ] + +ansible-playbook test_listen_role_dedup.yml "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'a handler from a role')" = "1" ] + +ansible localhost -m include_role -a "name=r1-dep_chain-vars" "$@" + +ansible-playbook test_include_tasks_in_include_role.yml "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'handler ran')" = "1" ] + +ansible-playbook test_run_once.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'handler ran once')" = "1" ] + +ansible-playbook 82241.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'included_task_from_tasks_dir')" = "1" ] + +ansible-playbook nested_flush_handlers_failure_force.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'flush_handlers_rescued')" = "1" ] +[ "$(grep out.txt -ce 'flush_handlers_always')" = "2" ] diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml index 6fc4e85a..97636d9d 100644 --- a/test/integration/targets/include_vars/tasks/main.yml +++ b/test/integration/targets/include_vars/tasks/main.yml @@ -208,6 +208,21 @@ - "config.key2.b == 22" - "config.key3 == 3" +- name: Include a vars dir with hash variables + include_vars: + dir: "{{ role_path }}/vars2/hashes/" + hash_behaviour: merge + +- name: Verify that the hash is merged after vars files are accumulated + assert: + that: + - "config | length == 3" + - "config.key0 is undefined" + - "config.key1 == 1" + - "config.key2 | length == 1" + - "config.key2.b == 22" + - "config.key3 == 3" + - include_vars: file: no_auto_unsafe.yml register: baz @@ -215,3 +230,40 @@ - assert: that: - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText" + +- name: setup test following symlinks + delegate_to: localhost + block: + - name: create directory to test following symlinks + file: + path: "{{ role_path }}/test_symlink" + state: directory + + - name: create symlink to the vars2 dir + file: + src: "{{ role_path }}/vars2" + dest: "{{ role_path }}/test_symlink/symlink" + state: link + +- name: include vars by following the symlink + include_vars: + dir: "{{ role_path }}/test_symlink" + register: follow_sym + +- assert: + that: follow_sym.ansible_included_var_files | sort == [hash1, hash2] + vars: + hash1: "{{ role_path }}/test_symlink/symlink/hashes/hash1.yml" + hash2: "{{ role_path }}/test_symlink/symlink/hashes/hash2.yml" + +- name: Test include_vars includes everything to the correct depth + ansible.builtin.include_vars: + dir: "{{ role_path }}/files/test_depth" + depth: 3 + name: test_depth_var + register: test_depth + +- assert: + that: + - "test_depth.ansible_included_var_files|length == 8" + - "test_depth_var.keys()|length == 8" diff --git a/test/integration/targets/include_vars/vars/services/service_vars.yml b/test/integration/targets/include_vars/vars/services/service_vars.yml index 96b05d6c..bcac7646 100644 --- a/test/integration/targets/include_vars/vars/services/service_vars.yml +++ b/test/integration/targets/include_vars/vars/services/service_vars.yml @@ -1,2 +1,2 @@ --- -service_name: 'my_custom_service'
\ No newline at end of file +service_name: 'my_custom_service' diff --git a/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml b/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml index 2c04fee5..cd82eca5 100644 --- a/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml +++ b/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml @@ -1,3 +1,3 @@ --- service_name_fqcn: 'my_custom_service' -service_name_tmpl_fqcn: '{{ service_name_fqcn }}'
\ No newline at end of file +service_name_tmpl_fqcn: '{{ service_name_fqcn }}' diff --git a/test/integration/targets/include_when_parent_is_dynamic/tasks.yml b/test/integration/targets/include_when_parent_is_dynamic/tasks.yml index 6831245c..d500f0df 100644 --- a/test/integration/targets/include_when_parent_is_dynamic/tasks.yml +++ b/test/integration/targets/include_when_parent_is_dynamic/tasks.yml @@ -9,4 +9,4 @@ # perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic # this file was loaded using include_tasks, which is dynamic, so this include should also be dynamic -- include: syntax_error.yml +- include_tasks: syntax_error.yml diff --git a/test/integration/targets/include_when_parent_is_static/tasks.yml b/test/integration/targets/include_when_parent_is_static/tasks.yml index a234a3dd..50dd2341 100644 --- a/test/integration/targets/include_when_parent_is_static/tasks.yml +++ b/test/integration/targets/include_when_parent_is_static/tasks.yml @@ -9,4 +9,4 @@ # perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic # this file was loaded using import_tasks, which is static, so this include should also be static -- include: syntax_error.yml +- import_tasks: syntax_error.yml diff --git a/test/integration/targets/includes/include_on_playbook_should_fail.yml b/test/integration/targets/includes/include_on_playbook_should_fail.yml index 953459dc..c9b1e81a 100644 --- a/test/integration/targets/includes/include_on_playbook_should_fail.yml +++ b/test/integration/targets/includes/include_on_playbook_should_fail.yml @@ -1 +1 @@ -- include: test_includes3.yml +- include_tasks: test_includes3.yml diff --git a/test/integration/targets/includes/roles/test_includes/handlers/main.yml b/test/integration/targets/includes/roles/test_includes/handlers/main.yml index 7d3e625f..453fa96d 100644 --- a/test/integration/targets/includes/roles/test_includes/handlers/main.yml +++ b/test/integration/targets/includes/roles/test_includes/handlers/main.yml @@ -1 +1 @@ -- include: more_handlers.yml +- import_tasks: more_handlers.yml diff --git a/test/integration/targets/includes/roles/test_includes/tasks/main.yml b/test/integration/targets/includes/roles/test_includes/tasks/main.yml index 83ca468b..2ba1ae63 100644 --- a/test/integration/targets/includes/roles/test_includes/tasks/main.yml +++ b/test/integration/targets/includes/roles/test_includes/tasks/main.yml @@ -17,47 +17,9 @@ # along with Ansible. If not, see <http://www.gnu.org/licenses/>. -- include: included_task1.yml a=1 b=2 c=3 - -- name: verify non-variable include params - assert: - that: - - "ca == '1'" - - "cb == '2'" - - "cc == '3'" - -- set_fact: - a: 101 - b: 102 - c: 103 - -- include: included_task1.yml a={{a}} b={{b}} c=103 - -- name: verify variable include params - assert: - that: - - "ca == 101" - - "cb == 102" - - "cc == 103" - -# Test that strings are not turned into numbers -- set_fact: - a: "101" - b: "102" - c: "103" - -- include: included_task1.yml a={{a}} b={{b}} c=103 - -- name: verify variable include params - assert: - that: - - "ca == '101'" - - "cb == '102'" - - "cc == '103'" - # now try long form includes -- include: included_task1.yml +- include_tasks: included_task1.yml vars: a: 201 b: 202 diff --git a/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml b/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml index 5ae7882f..d7bcf8eb 100644 --- a/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml +++ b/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml @@ -1,9 +1,9 @@ - name: this needs to be here debug: msg: "hello" -- include: inner.yml +- include_tasks: inner.yml with_items: - '1' -- ansible.builtin.include: inner_fqcn.yml +- ansible.builtin.include_tasks: inner_fqcn.yml with_items: - '1' diff --git a/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml index 7bc19faa..c06d3feb 100644 --- a/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml +++ b/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml @@ -1,6 +1,6 @@ - name: this needs to be here debug: msg: "hello" -- include: inner.yml +- include_tasks: inner.yml with_items: - '1' diff --git a/test/integration/targets/includes/runme.sh b/test/integration/targets/includes/runme.sh index e619feaf..8622cf66 100755 --- a/test/integration/targets/includes/runme.sh +++ b/test/integration/targets/includes/runme.sh @@ -10,7 +10,7 @@ echo "EXPECTED ERROR: Ensure we fail if using 'include' to include a playbook." set +e result="$(ansible-playbook -i ../../inventory include_on_playbook_should_fail.yml -v "$@" 2>&1)" set -e -grep -q "ERROR! 'include' is not a valid attribute for a Play" <<< "$result" +grep -q "ERROR! 'include_tasks' is not a valid attribute for a Play" <<< "$result" ansible-playbook includes_loop_rescue.yml --extra-vars strategy=linear "$@" ansible-playbook includes_loop_rescue.yml --extra-vars strategy=free "$@" diff --git a/test/integration/targets/includes/test_includes2.yml b/test/integration/targets/includes/test_includes2.yml index a32e8513..da6b914f 100644 --- a/test/integration/targets/includes/test_includes2.yml +++ b/test/integration/targets/includes/test_includes2.yml @@ -13,8 +13,8 @@ - role: test_includes tags: test_includes tasks: - - include: roles/test_includes/tasks/not_a_role_task.yml - - include: roles/test_includes/tasks/empty.yml + - include_tasks: roles/test_includes/tasks/not_a_role_task.yml + - include_tasks: roles/test_includes/tasks/empty.yml - assert: that: - "ca == 33000" diff --git a/test/integration/targets/includes/test_includes3.yml b/test/integration/targets/includes/test_includes3.yml index 0b4c6312..f3c4964e 100644 --- a/test/integration/targets/includes/test_includes3.yml +++ b/test/integration/targets/includes/test_includes3.yml @@ -1,6 +1,6 @@ - hosts: testhost tasks: - - include: test_includes4.yml + - include_tasks: test_includes4.yml with_items: ["a"] loop_control: loop_var: r diff --git a/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py b/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py index 7ca445a3..43cad4fc 100644 --- a/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py +++ b/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py @@ -14,7 +14,7 @@ DOCUMENTATION = ''' ''' from ansible.errors import AnsibleParserError -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.plugins.inventory import BaseInventoryPlugin, Constructable diff --git a/test/integration/targets/inventory_ini/inventory.ini b/test/integration/targets/inventory_ini/inventory.ini index a0c99ade..a5de4211 100644 --- a/test/integration/targets/inventory_ini/inventory.ini +++ b/test/integration/targets/inventory_ini/inventory.ini @@ -1,3 +1,5 @@ +gitlab-runner-01 ansible_host=gitlab-runner-01.internal.example.net ansible_user=root + [local] testhost ansible_connection=local ansible_become=no ansible_become_user=ansibletest1 diff --git a/test/integration/targets/inventory_ini/runme.sh b/test/integration/targets/inventory_ini/runme.sh index 81bf1475..919e1884 100755 --- a/test/integration/targets/inventory_ini/runme.sh +++ b/test/integration/targets/inventory_ini/runme.sh @@ -3,3 +3,6 @@ set -eux ansible-playbook -v -i inventory.ini test_ansible_become.yml + +ansible-inventory -v -i inventory.ini --list 2> out +test "$(grep -c 'SyntaxWarning' out)" -eq 0 diff --git a/test/integration/targets/iptables/aliases b/test/integration/targets/iptables/aliases index 7d66ecf8..73df8aad 100644 --- a/test/integration/targets/iptables/aliases +++ b/test/integration/targets/iptables/aliases @@ -1,5 +1,4 @@ shippable/posix/group2 skip/freebsd -skip/osx skip/macos skip/docker diff --git a/test/integration/targets/iptables/tasks/chain_management.yml b/test/integration/targets/iptables/tasks/chain_management.yml index 03551228..dae4103a 100644 --- a/test/integration/targets/iptables/tasks/chain_management.yml +++ b/test/integration/targets/iptables/tasks/chain_management.yml @@ -45,6 +45,26 @@ - result is not failed - '"FOOBAR-CHAIN" in result.stdout' +- name: add rule to foobar chain + become: true + iptables: + chain: FOOBAR-CHAIN + source: 0.0.0.0 + destination: 0.0.0.0 + jump: DROP + comment: "FOOBAR-CHAIN RULE" + +- name: get the state of the iptable rules after rule is added to foobar chain + become: true + shell: "{{ iptables_bin }} -L" + register: result + +- name: assert rule is present in foobar chain + assert: + that: + - result is not failed + - '"FOOBAR-CHAIN RULE" in result.stdout' + - name: flush the foobar chain become: true iptables: @@ -68,4 +88,3 @@ that: - result is not failed - '"FOOBAR-CHAIN" not in result.stdout' - - '"FOOBAR-RULE" not in result.stdout' diff --git a/test/integration/targets/known_hosts/defaults/main.yml b/test/integration/targets/known_hosts/defaults/main.yml index b1b56ac7..cd438430 100644 --- a/test/integration/targets/known_hosts/defaults/main.yml +++ b/test/integration/targets/known_hosts/defaults/main.yml @@ -3,4 +3,4 @@ example_org_rsa_key: > example.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAglyZmHHWskQ9wkh8LYbIqzvg99/oloneH7BaZ02ripJUy/2Zynv4tgUfm9fdXvAb1XXCEuTRnts9FBer87+voU0FPRgx3CfY9Sgr0FspUjnm4lqs53FIab1psddAaS7/F7lrnjl6VqBtPwMRQZG7qlml5uogGJwYJHxX0PGtsdoTJsM= example_org_ed25519_key: > - example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzlnSq5ESxLgW0avvPk3j7zLV59hcAPkxrMNdnZMKP2
\ No newline at end of file + example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzlnSq5ESxLgW0avvPk3j7zLV59hcAPkxrMNdnZMKP2 diff --git a/test/integration/targets/known_hosts/tasks/main.yml b/test/integration/targets/known_hosts/tasks/main.yml index dc00dedd..d5ffec4d 100644 --- a/test/integration/targets/known_hosts/tasks/main.yml +++ b/test/integration/targets/known_hosts/tasks/main.yml @@ -99,7 +99,7 @@ # https://github.com/ansible/ansible/issues/78598 # test removing nonexistent host key when the other keys exist for the host - name: remove different key - known_hosts: + known_hosts: name: example.org key: "{{ example_org_ed25519_key }}" state: absent diff --git a/test/integration/targets/lookup_config/tasks/main.yml b/test/integration/targets/lookup_config/tasks/main.yml index 356d2f80..e5699d34 100644 --- a/test/integration/targets/lookup_config/tasks/main.yml +++ b/test/integration/targets/lookup_config/tasks/main.yml @@ -42,6 +42,7 @@ - name: remote user and port for ssh connection set_fact: ssh_user_and_port: '{{q("config", "remote_user", "port", plugin_type="connection", plugin_name="ssh")}}' + ssh_user_and_port_and_origin: '{{q("config", "remote_user", "port", plugin_type="connection", plugin_name="ssh", show_origin=True)}}' vars: ansible_ssh_user: lola ansible_ssh_port: 2022 @@ -71,4 +72,5 @@ - lookup_config_7 is failed - '"Invalid setting" in lookup_config_7.msg' - ssh_user_and_port == ['lola', 2022] + - "ssh_user_and_port_and_origin == [['lola', 'var: ansible_ssh_user'], [2022, 'var: ansible_ssh_port']]" - yolo_remote == ["yolo"] diff --git a/test/integration/targets/lookup_fileglob/issue72873/test.yml b/test/integration/targets/lookup_fileglob/issue72873/test.yml index 218ee58d..92d93d45 100644 --- a/test/integration/targets/lookup_fileglob/issue72873/test.yml +++ b/test/integration/targets/lookup_fileglob/issue72873/test.yml @@ -5,7 +5,7 @@ dir: files tasks: - file: path='{{ dir }}' state=directory - + - file: path='setvars.bat' state=touch # in current directory! - file: path='{{ dir }}/{{ item }}' state=touch @@ -20,11 +20,11 @@ - name: Get working order results and sort them set_fact: - working: '{{ query("fileglob", "setvars.bat", "{{ dir }}/*.[ch]") | sort }}' + working: '{{ query("fileglob", "setvars.bat", dir ~ "/*.[ch]") | sort }}' - name: Get broken order results and sort them set_fact: - broken: '{{ query("fileglob", "{{ dir }}/*.[ch]", "setvars.bat") | sort }}' + broken: '{{ query("fileglob", dir ~ "/*.[ch]", "setvars.bat") | sort }}' - assert: that: diff --git a/test/integration/targets/lookup_first_found/tasks/main.yml b/test/integration/targets/lookup_first_found/tasks/main.yml index 9aeaf1d1..ba248bd5 100644 --- a/test/integration/targets/lookup_first_found/tasks/main.yml +++ b/test/integration/targets/lookup_first_found/tasks/main.yml @@ -94,3 +94,56 @@ - assert: that: - foo is defined + +# TODO: no 'terms' test +- name: test first_found lookup with no terms + set_fact: + no_terms: "{{ query('first_found', files=['missing1', 'hosts', 'missing2'], paths=['/etc'], errors='ignore') }}" + +- assert: + that: "no_terms|first == '/etc/hosts'" + +- name: handle templatable dictionary entries + block: + + - name: Load variables specific for OS family + assert: + that: + - "item is file" + - "item|basename == 'itworks.yml'" + with_first_found: + - files: + - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml" # invalid var, should be skipped + - "{{ansible_lsb.id}}-{{ansible_lsb.major_release}}.yml" # does not exist, but should try + - "{{ansible_distribution}}-{{ansible_distribution_major_version}}.yml" # does not exist, but should try + - itworks.yml + - ishouldnotbefound.yml # this exist, but should not be found + paths: + - "{{role_path}}/vars" + + - name: Load variables specific for OS family, but now as list of dicts, same options as above + assert: + that: + - "item is file" + - "item|basename == 'itworks.yml'" + with_first_found: + - files: + - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml" + paths: + - "{{role_path}}/vars" + - files: + - "{{ansible_lsb.id}}-{{ansible_lsb.major_release}}.yml" + paths: + - "{{role_path}}/vars" + - files: + - "{{ansible_distribution}}-{{ansible_distribution_major_version}}.yml" + paths: + - "{{role_path}}/vars" + - files: + - itworks.yml + paths: + - "{{role_path}}/vars" + - files: + - ishouldnotbefound.yml + paths: + - "{{role_path}}/vars" diff --git a/test/integration/targets/lookup_sequence/tasks/main.yml b/test/integration/targets/lookup_sequence/tasks/main.yml index bd0a4d80..e64801d3 100644 --- a/test/integration/targets/lookup_sequence/tasks/main.yml +++ b/test/integration/targets/lookup_sequence/tasks/main.yml @@ -195,4 +195,4 @@ - ansible_failed_task.name == "EXPECTED FAILURE - test bad format string message" - ansible_failed_result.msg == expected vars: - expected: "bad formatting string: d"
\ No newline at end of file + expected: "bad formatting string: d" diff --git a/test/integration/targets/lookup_together/tasks/main.yml b/test/integration/targets/lookup_together/tasks/main.yml index 71365a15..115c9e52 100644 --- a/test/integration/targets/lookup_together/tasks/main.yml +++ b/test/integration/targets/lookup_together/tasks/main.yml @@ -26,4 +26,4 @@ - assert: that: - ansible_failed_task.name == "EXPECTED FAILURE - test empty list" - - ansible_failed_result.msg == "with_together requires at least one element in each list"
\ No newline at end of file + - ansible_failed_result.msg == "with_together requires at least one element in each list" diff --git a/test/integration/targets/lookup_url/aliases b/test/integration/targets/lookup_url/aliases index ef37fce1..19b7d98f 100644 --- a/test/integration/targets/lookup_url/aliases +++ b/test/integration/targets/lookup_url/aliases @@ -1,4 +1,11 @@ destructive shippable/posix/group3 needs/httptester -skip/macos/12.0 # This test crashes Python due to https://wefearchange.org/2018/11/forkmacos.rst.html +skip/macos # This test crashes Python due to https://wefearchange.org/2018/11/forkmacos.rst.html +# Example failure: +# +# TASK [lookup_url : Test that retrieving a url works] *************************** +# objc[15394]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. +# objc[15394]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in t +# he fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug. +# ERROR! A worker was found in a dead state diff --git a/test/integration/targets/lookup_url/meta/main.yml b/test/integration/targets/lookup_url/meta/main.yml index 374b5fdf..6853708f 100644 --- a/test/integration/targets/lookup_url/meta/main.yml +++ b/test/integration/targets/lookup_url/meta/main.yml @@ -1,2 +1,2 @@ -dependencies: +dependencies: - prepare_http_tests diff --git a/test/integration/targets/lookup_url/tasks/main.yml b/test/integration/targets/lookup_url/tasks/main.yml index 2fb227ad..83fd5db6 100644 --- a/test/integration/targets/lookup_url/tasks/main.yml +++ b/test/integration/targets/lookup_url/tasks/main.yml @@ -1,6 +1,6 @@ - name: Test that retrieving a url works set_fact: - web_data: "{{ lookup('url', 'https://{{ httpbin_host }}/get?one') }}" + web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/get?one') }}" - name: Assert that the url was retrieved assert: @@ -9,7 +9,7 @@ - name: Test that retrieving a url with invalid cert fails set_fact: - web_data: "{{ lookup('url', 'https://{{ badssl_host }}/') }}" + web_data: "{{ lookup('url', 'https://' ~ badssl_host ~ '/') }}" ignore_errors: True register: url_invalid_cert @@ -20,12 +20,12 @@ - name: Test that retrieving a url with invalid cert with validate_certs=False works set_fact: - web_data: "{{ lookup('url', 'https://{{ badssl_host }}/', validate_certs=False) }}" + web_data: "{{ lookup('url', 'https://' ~ badssl_host ~ '/', validate_certs=False) }}" register: url_no_validate_cert - assert: that: - - "'{{ badssl_host_substring }}' in web_data" + - badssl_host_substring in web_data - vars: url: https://{{ httpbin_host }}/get @@ -52,3 +52,27 @@ - name: Test use_netrc=False import_tasks: use_netrc.yml + +- vars: + ansible_lookup_url_agent: ansible-test-lookup-url-agent + block: + - name: Test user agent + set_fact: + web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/user-agent') }}" + + - name: Assert that user agent is set + assert: + that: + - ansible_lookup_url_agent in web_data['user-agent'] + +- vars: + ansible_lookup_url_force_basic_auth: yes + block: + - name: Test force basic auth + set_fact: + web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/headers', username='abc') }}" + + - name: Assert that Authorization header is set + assert: + that: + - "'Authorization' in web_data.headers" diff --git a/test/integration/targets/lookup_url/tasks/use_netrc.yml b/test/integration/targets/lookup_url/tasks/use_netrc.yml index 68dc8934..b90d05dc 100644 --- a/test/integration/targets/lookup_url/tasks/use_netrc.yml +++ b/test/integration/targets/lookup_url/tasks/use_netrc.yml @@ -10,7 +10,7 @@ - name: test Url lookup with ~/.netrc forced Basic auth set_fact: - web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}) }}" + web_data: "{{ lookup('ansible.builtin.url', 'https://' ~ httpbin_host ~ '/bearer', headers={'Authorization':'Bearer foobar'}) }}" ignore_errors: yes - name: assert test Url lookup with ~/.netrc forced Basic auth @@ -18,11 +18,11 @@ that: - "web_data.token.find('v=' ~ 'Zm9vOmJhcg==') == -1" fail_msg: "Was expecting 'foo:bar' in base64, but received: {{ web_data }}" - success_msg: "Expected Basic authentication even Bearer headers were sent" + success_msg: "Expected Basic authentication even Bearer headers were sent" - name: test Url lookup with use_netrc=False set_fact: - web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}, use_netrc='False') }}" + web_data: "{{ lookup('ansible.builtin.url', 'https://' ~ httpbin_host ~ '/bearer', headers={'Authorization':'Bearer foobar'}, use_netrc='False') }}" - name: assert test Url lookup with netrc=False used Bearer authentication assert: @@ -34,4 +34,4 @@ - name: Clean up. Removing ~/.netrc file: path: ~/.netrc - state: absent
\ No newline at end of file + state: absent diff --git a/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml index 09322a9d..bd892de9 100644 --- a/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml +++ b/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml @@ -1,4 +1,4 @@ plugin_routing: connection: redirected_dummy: - redirect: ns.name.dummy
\ No newline at end of file + redirect: ns.name.dummy diff --git a/test/integration/targets/loop-connection/main.yml b/test/integration/targets/loop-connection/main.yml index fbffe309..ba60e649 100644 --- a/test/integration/targets/loop-connection/main.yml +++ b/test/integration/targets/loop-connection/main.yml @@ -30,4 +30,4 @@ - assert: that: - connected_test.results[0].stderr == "ran - 1" - - connected_test.results[1].stderr == "ran - 2"
\ No newline at end of file + - connected_test.results[1].stderr == "ran - 2" diff --git a/test/integration/targets/missing_required_lib/library/missing_required_lib.py b/test/integration/targets/missing_required_lib/library/missing_required_lib.py index 480ea001..8c7ba884 100644 --- a/test/integration/targets/missing_required_lib/library/missing_required_lib.py +++ b/test/integration/targets/missing_required_lib/library/missing_required_lib.py @@ -8,7 +8,7 @@ __metaclass__ = type from ansible.module_utils.basic import AnsibleModule, missing_required_lib try: - import ansible_missing_lib + import ansible_missing_lib # pylint: disable=unused-import HAS_LIB = True except ImportError as e: HAS_LIB = False diff --git a/test/integration/targets/module_defaults/action_plugins/debug.py b/test/integration/targets/module_defaults/action_plugins/debug.py index 2584fd3d..0c43201c 100644 --- a/test/integration/targets/module_defaults/action_plugins/debug.py +++ b/test/integration/targets/module_defaults/action_plugins/debug.py @@ -20,7 +20,7 @@ __metaclass__ = type from ansible.errors import AnsibleUndefinedVariable from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.plugins.action import ActionBase diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py index 0d39f26d..174f3725 100644 --- a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py +++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py @@ -5,7 +5,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.plugins.action.normal import ActionModule as ActionBase -from ansible.utils.vars import merge_hash class ActionModule(ActionBase): diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py index 20284fd1..7ba24348 100644 --- a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py +++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py @@ -5,7 +5,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.plugins.action.normal import ActionModule as ActionBase -from ansible.utils.vars import merge_hash class ActionModule(ActionBase): diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py index b0e1904b..67050fbd 100644 --- a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py +++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py @@ -5,7 +5,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.plugins.action.normal import ActionModule as ActionBase -from ansible.utils.vars import merge_hash class ActionModule(ActionBase): diff --git a/test/integration/targets/module_no_log/aliases b/test/integration/targets/module_no_log/aliases index 9e84f636..afa1c9c3 100644 --- a/test/integration/targets/module_no_log/aliases +++ b/test/integration/targets/module_no_log/aliases @@ -1,5 +1,4 @@ shippable/posix/group3 context/controller skip/freebsd # not configured to log user.info to /var/log/syslog -skip/osx # not configured to log user.info to /var/log/syslog skip/macos # not configured to log user.info to /var/log/syslog diff --git a/test/integration/targets/module_no_log/tasks/main.yml b/test/integration/targets/module_no_log/tasks/main.yml index cf9e5802..bf024105 100644 --- a/test/integration/targets/module_no_log/tasks/main.yml +++ b/test/integration/targets/module_no_log/tasks/main.yml @@ -59,3 +59,41 @@ # 2) the AnsibleModule.log method is not working - good_message in grep.stdout - bad_message not in grep.stdout + +- name: Ensure we do not obscure what we should not + block: + - module_that_has_secret: + secret: u + notsecret: u + register: ouch + ignore_errors: true + + - name: no log wont obscure booleans when True, but still hide in msg + assert: + that: + - ouch['changed'] is boolean + - "'*' in ouch['msg']" + + - module_that_has_secret: + secret: a + notsecret: b + register: ouch + ignore_errors: true + + - name: no log wont obscure booleans when False, but still hide in msg + assert: + that: + - ouch['changed'] is boolean + - "'*' in ouch['msg']" + + - module_that_has_secret: + secret: True + notsecret: False + register: ouch + ignore_errors: true + + - name: no log does not hide bool values + assert: + that: + - ouch['changed'] is boolean + - "'*' not in ouch['msg']" diff --git a/test/integration/targets/module_utils/library/test.py b/test/integration/targets/module_utils/library/test.py index fb6c8a81..857d3d8e 100644 --- a/test/integration/targets/module_utils/library/test.py +++ b/test/integration/targets/module_utils/library/test.py @@ -11,8 +11,8 @@ import ansible.module_utils.foo0 results['foo0'] = ansible.module_utils.foo0.data # Test depthful import with no from -import ansible.module_utils.bar0.foo -results['bar0'] = ansible.module_utils.bar0.foo.data +import ansible.module_utils.bar0.foo3 +results['bar0'] = ansible.module_utils.bar0.foo3.data # Test import of module_utils/foo1.py from ansible.module_utils import foo1 @@ -72,12 +72,12 @@ from ansible.module_utils.spam8.ham import eggs results['spam8'] = (bacon.data, eggs) # Test that import of module_utils/qux1/quux.py using as works -from ansible.module_utils.qux1 import quux as one -results['qux1'] = one.data +from ansible.module_utils.qux1 import quux as two +results['qux1'] = two.data # Test that importing qux2/quux.py and qux2/quuz.py using as works -from ansible.module_utils.qux2 import quux as one, quuz as two -results['qux2'] = (one.data, two.data) +from ansible.module_utils.qux2 import quux as three, quuz as four +results['qux2'] = (three.data, four.data) # Test depth from ansible.module_utils.a.b.c.d.e.f.g.h import data diff --git a/test/integration/targets/module_utils/library/test_failure.py b/test/integration/targets/module_utils/library/test_failure.py index efb3ddae..ab80ceae 100644 --- a/test/integration/targets/module_utils/library/test_failure.py +++ b/test/integration/targets/module_utils/library/test_failure.py @@ -6,9 +6,9 @@ results = {} # Test that we are rooted correctly # Following files: # module_utils/yak/zebra/foo.py -from ansible.module_utils.zebra import foo +from ansible.module_utils.zebra import foo4 -results['zebra'] = foo.data +results['zebra'] = foo4.data from ansible.module_utils.basic import AnsibleModule AnsibleModule(argument_spec=dict()).exit_json(**results) diff --git a/test/integration/targets/module_utils/module_utils_test.yml b/test/integration/targets/module_utils/module_utils_test.yml index 4e948bd6..352bc582 100644 --- a/test/integration/targets/module_utils/module_utils_test.yml +++ b/test/integration/targets/module_utils/module_utils_test.yml @@ -47,7 +47,7 @@ assert: that: - result is failed - - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo', 'ansible.module_utils.zebra'])" + - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo4', 'ansible.module_utils.zebra'])" - name: Test that alias deprecation works test_alias_deprecation: diff --git a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 index 6170f046..9644df93 100644 --- a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 +++ b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 @@ -87,7 +87,7 @@ Function Assert-DictionaryEqual { } Function Exit-Module { - # Make sure Exit actually calls exit and not our overriden test behaviour + # Make sure Exit actually calls exit and not our overridden test behaviour [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc } Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99) $module.ExitJson() diff --git a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 index d18c42d7..5cb1a72d 100644 --- a/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 +++ b/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 @@ -328,5 +328,73 @@ finally { } Assert-Equal -actual ([Namespace12.Class12]::GetString()) -expected "b" +$unsafe_code_fail = @' +using System; + +namespace Namespace13 +{ + public class Class13 + { + + public static int GetNumber() + { + int num = 2; + int* numPtr = # + + DoubleNumber(numPtr); + + return num; + } + + private unsafe static void DoubleNumber(int* num) + { + *num = *num * 3; + } + } +} +'@ +$failed = $false +try { + Add-CSharpType -Reference $unsafe_code_fail +} +catch { + $failed = $true + $actual = $_.Exception.Message.Contains("error CS0227: Unsafe code may only appear if compiling with /unsafe") + Assert-Equal -actual $actual -expected $true +} +Assert-Equal -actual $failed -expected $true + +$unsafe_code = @' +using System; + +//AllowUnsafe + +namespace Namespace13 +{ + public class Class13 + { + public static int GetNumber() + { + int num = 2; + unsafe + { + int* numPtr = # + + DoubleNumber(numPtr); + } + + return num; + } + + private unsafe static void DoubleNumber(int* num) + { + *num = *num * 2; + } + } +} +'@ +Add-CSharpType -Reference $unsafe_code +Assert-Equal -actual ([Namespace13.Class13]::GetNumber()) -expected 4 + $result.res = "success" Exit-Json -obj $result diff --git a/test/integration/targets/no_log/runme.sh b/test/integration/targets/no_log/runme.sh index bb5c048f..bf764bf9 100755 --- a/test/integration/targets/no_log/runme.sh +++ b/test/integration/targets/no_log/runme.sh @@ -5,7 +5,7 @@ set -eux # This test expects 7 loggable vars and 0 non-loggable ones. # If either mismatches it fails, run the ansible-playbook command to debug. [ "$(ansible-playbook no_log_local.yml -i ../../inventory -vvvvv "$@" | awk \ -'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "26/0" ] +'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "27/0" ] # deal with corner cases with no log and loops # no log enabled, should produce 6 censored messages @@ -19,3 +19,8 @@ set -eux # test invalid data passed to a suboption [ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ] + +# test variations on ANSIBLE_NO_LOG +[ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] +[ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ] +[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ] diff --git a/test/integration/targets/old_style_cache_plugins/aliases b/test/integration/targets/old_style_cache_plugins/aliases index 37773831..163129e2 100644 --- a/test/integration/targets/old_style_cache_plugins/aliases +++ b/test/integration/targets/old_style_cache_plugins/aliases @@ -2,5 +2,4 @@ destructive needs/root shippable/posix/group5 context/controller -skip/osx skip/macos diff --git a/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py b/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py index 44b6cf93..23c7789b 100644 --- a/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py +++ b/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py @@ -44,7 +44,6 @@ DOCUMENTATION = ''' import time import json -from ansible import constants as C from ansible.errors import AnsibleError from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder from ansible.plugins.cache import BaseCacheModule diff --git a/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml b/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml index 8aad37a3..b7cd4831 100644 --- a/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml +++ b/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml @@ -20,8 +20,9 @@ - name: get the latest stable redis server release get_url: - url: http://download.redis.io/redis-stable.tar.gz + url: https://download.redis.io/redis-stable.tar.gz dest: ./ + timeout: 60 - name: unzip download unarchive: diff --git a/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py b/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py index d5c9a422..f554be04 100644 --- a/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py +++ b/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py @@ -2,7 +2,7 @@ from ansible.plugins.vars import BaseVarsPlugin class VarsModule(BaseVarsPlugin): - REQUIRES_WHITELIST = False + REQUIRES_WHITELIST = True def get_vars(self, loader, path, entities): return {} diff --git a/test/integration/targets/old_style_vars_plugins/runme.sh b/test/integration/targets/old_style_vars_plugins/runme.sh index 4cd19168..9f416235 100755 --- a/test/integration/targets/old_style_vars_plugins/runme.sh +++ b/test/integration/targets/old_style_vars_plugins/runme.sh @@ -12,9 +12,39 @@ export ANSIBLE_VARS_PLUGINS=./vars_plugins export ANSIBLE_VARS_ENABLED=require_enabled [ "$(ansible-inventory -i localhost, --list --yaml all "$@" | grep -c 'require_enabled')" = "1" ] -# Test the deprecated class attribute +# Test deprecated features export ANSIBLE_VARS_PLUGINS=./deprecation_warning -WARNING="The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. Use 'REQUIRES_ENABLED' instead." +WARNING_1="The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. Use 'REQUIRES_ENABLED' instead." +WARNING_2="The vars plugin v2_vars_plugin .* is relying on the deprecated entrypoints 'get_host_vars' and 'get_group_vars'" ANSIBLE_DEPRECATION_WARNINGS=True ANSIBLE_NOCOLOR=True ANSIBLE_FORCE_COLOR=False \ - ansible-inventory -i localhost, --list all 2> err.txt -ansible localhost -m debug -a "msg={{ lookup('file', 'err.txt') | regex_replace('\n', '') }}" | grep "$WARNING" + ansible-inventory -i localhost, --list all "$@" 2> err.txt +for WARNING in "$WARNING_1" "$WARNING_2"; do + ansible localhost -m debug -a "msg={{ lookup('file', 'err.txt') | regex_replace('\n', '') }}" | grep "$WARNING" +done + +# Test how many times vars plugins are loaded for a simple play containing a task +# host_group_vars is stateless, so we can load it once and reuse it, every other vars plugin should be instantiated before it runs +cat << EOF > "test_task_vars.yml" +--- +- hosts: localhost + connection: local + gather_facts: no + tasks: + - debug: +EOF + +# hide the debug noise by dumping to a file +trap 'rm -rf -- "out.txt"' EXIT + +ANSIBLE_DEBUG=True ansible-playbook test_task_vars.yml > out.txt +[ "$(grep -c "Loading VarsModule 'host_group_vars'" out.txt)" -eq 1 ] +[ "$(grep -c "Loading VarsModule 'require_enabled'" out.txt)" -gt 50 ] +[ "$(grep -c "Loading VarsModule 'auto_enabled'" out.txt)" -gt 50 ] + +export ANSIBLE_VARS_ENABLED=ansible.builtin.host_group_vars +ANSIBLE_DEBUG=True ansible-playbook test_task_vars.yml > out.txt +[ "$(grep -c "Loading VarsModule 'host_group_vars'" out.txt)" -eq 1 ] +[ "$(grep -c "Loading VarsModule 'require_enabled'" out.txt)" -lt 3 ] +[ "$(grep -c "Loading VarsModule 'auto_enabled'" out.txt)" -gt 50 ] + +ansible localhost -m include_role -a 'name=a' "$@" diff --git a/test/integration/targets/omit/75692.yml b/test/integration/targets/omit/75692.yml index b4000c97..5ba8a2df 100644 --- a/test/integration/targets/omit/75692.yml +++ b/test/integration/targets/omit/75692.yml @@ -2,10 +2,10 @@ hosts: testhost gather_facts: false become: yes + # become_user needed at play level for testing this behavior become_user: nobody roles: - name: setup_test_user - become: yes become_user: root tasks: - shell: whoami diff --git a/test/integration/targets/package/tasks/main.yml b/test/integration/targets/package/tasks/main.yml index c17525d8..37267aa6 100644 --- a/test/integration/targets/package/tasks/main.yml +++ b/test/integration/targets/package/tasks/main.yml @@ -239,4 +239,4 @@ that: - "result is changed" - when: ansible_distribution == "Fedora"
\ No newline at end of file + when: ansible_distribution == "Fedora" diff --git a/test/integration/targets/package_facts/aliases b/test/integration/targets/package_facts/aliases index 5a5e4646..f5edf4b1 100644 --- a/test/integration/targets/package_facts/aliases +++ b/test/integration/targets/package_facts/aliases @@ -1,3 +1,2 @@ shippable/posix/group2 -skip/osx skip/macos diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml index d225c0f9..25e91f28 100644 --- a/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml +++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml @@ -121,7 +121,10 @@ - result.cmd == "echo foo --arg=a --arg=b" - name: test includes with params - include: test_include.yml fact_name=include_params param="{{ test_input }}" + include_tasks: test_include.yml + vars: + fact_name: include_params + param: "{{ test_input }}" - name: assert the include set the correct fact for the param assert: @@ -129,7 +132,10 @@ - include_params == test_input - name: test includes with quoted params - include: test_include.yml fact_name=double_quoted_param param="this is a param with double quotes" + include_tasks: test_include.yml + vars: + fact_name: double_quoted_param + param: "this is a param with double quotes" - name: assert the include set the correct fact for the double quoted param assert: @@ -137,7 +143,10 @@ - double_quoted_param == "this is a param with double quotes" - name: test includes with single quoted params - include: test_include.yml fact_name=single_quoted_param param='this is a param with single quotes' + include_tasks: test_include.yml + vars: + fact_name: single_quoted_param + param: 'this is a param with single quotes' - name: assert the include set the correct fact for the single quoted param assert: @@ -145,7 +154,7 @@ - single_quoted_param == "this is a param with single quotes" - name: test includes with quoted params in complex args - include: test_include.yml + include_tasks: test_include.yml vars: fact_name: complex_param param: "this is a param in a complex arg with double quotes" @@ -165,7 +174,7 @@ - result.msg == "this should be debugged" - name: test conditional includes - include: test_include_conditional.yml + include_tasks: test_include_conditional.yml when: false - name: assert the nested include from test_include_conditional was not set diff --git a/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml index 070888da..a1d8b7ce 100644 --- a/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml +++ b/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml @@ -1 +1 @@ -- include: test_include_nested.yml +- include_tasks: test_include_nested.yml diff --git a/test/integration/targets/parsing/runme.sh b/test/integration/targets/parsing/runme.sh index 022ce4cf..2d550082 100755 --- a/test/integration/targets/parsing/runme.sh +++ b/test/integration/targets/parsing/runme.sh @@ -2,5 +2,5 @@ set -eux -ansible-playbook bad_parsing.yml -i ../../inventory -vvv "$@" --tags prepare,common,scenario5 -ansible-playbook good_parsing.yml -i ../../inventory -v "$@" +ansible-playbook parsing.yml -i ../../inventory "$@" -e "output_dir=${OUTPUT_DIR}" +ansible-playbook good_parsing.yml -i ../../inventory "$@" diff --git a/test/integration/targets/path_lookups/testplay.yml b/test/integration/targets/path_lookups/testplay.yml index 8bf45532..bc05c7e5 100644 --- a/test/integration/targets/path_lookups/testplay.yml +++ b/test/integration/targets/path_lookups/testplay.yml @@ -4,9 +4,11 @@ pre_tasks: - name: remove {{ remove }} file: path={{ playbook_dir }}/{{ remove }} state=absent - roles: - - showfile - post_tasks: + tasks: + - import_role: + name: showfile + tasks_from: notmain.yml + - name: from play set_fact: play_result="{{lookup('file', 'testfile')}}" diff --git a/test/integration/targets/pause/test-pause.py b/test/integration/targets/pause/test-pause.py index 3703470d..ab771fa0 100755 --- a/test/integration/targets/pause/test-pause.py +++ b/test/integration/targets/pause/test-pause.py @@ -168,7 +168,9 @@ pause_test = pexpect.spawn( pause_test.logfile = log_buffer pause_test.expect(r'Pausing for \d+ seconds') pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)") +pause_test.send('\n') # test newline does not stop the prompt - waiting for a timeout or ctrl+C pause_test.send('\x03') +pause_test.expect("Press 'C' to continue the play or 'A' to abort") pause_test.send('C') pause_test.expect('Task after pause') pause_test.expect(pexpect.EOF) @@ -187,6 +189,7 @@ pause_test.logfile = log_buffer pause_test.expect(r'Pausing for \d+ seconds') pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)") pause_test.send('\x03') +pause_test.expect("Press 'C' to continue the play or 'A' to abort") pause_test.send('A') pause_test.expect('user requested abort!') pause_test.expect(pexpect.EOF) @@ -225,6 +228,7 @@ pause_test.expect(r'Pausing for \d+ seconds') pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)") pause_test.expect(r"Waiting for two seconds:") pause_test.send('\x03') +pause_test.expect("Press 'C' to continue the play or 'A' to abort") pause_test.send('C') pause_test.expect('Task after pause') pause_test.expect(pexpect.EOF) @@ -244,6 +248,7 @@ pause_test.expect(r'Pausing for \d+ seconds') pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)") pause_test.expect(r"Waiting for two seconds:") pause_test.send('\x03') +pause_test.expect("Press 'C' to continue the play or 'A' to abort") pause_test.send('A') pause_test.expect('user requested abort!') pause_test.expect(pexpect.EOF) @@ -275,6 +280,24 @@ pause_test.send('\r') pause_test.expect(pexpect.EOF) pause_test.close() +# Test input is not returned if a timeout is given + +playbook = 'pause-6.yml' + +pause_test = pexpect.spawn( + 'ansible-playbook', + args=[playbook] + args, + timeout=10, + env=os.environ +) + +pause_test.logfile = log_buffer +pause_test.expect(r'Wait for three seconds:') +pause_test.send('ignored user input') +pause_test.expect('Task after pause') +pause_test.expect(pexpect.EOF) +pause_test.close() + # Test that enter presses may not continue the play when a timeout is set. diff --git a/test/integration/targets/pip/tasks/main.yml b/test/integration/targets/pip/tasks/main.yml index 66992fd0..a3770702 100644 --- a/test/integration/targets/pip/tasks/main.yml +++ b/test/integration/targets/pip/tasks/main.yml @@ -40,6 +40,9 @@ extra_args: "-c {{ remote_constraints }}" - include_tasks: pip.yml + + - include_tasks: no_setuptools.yml + when: ansible_python.version_info[:2] >= [3, 8] always: - name: platform specific cleanup include_tasks: "{{ cleanup_filename }}" diff --git a/test/integration/targets/pip/tasks/pip.yml b/test/integration/targets/pip/tasks/pip.yml index 39480614..9f1034d2 100644 --- a/test/integration/targets/pip/tasks/pip.yml +++ b/test/integration/targets/pip/tasks/pip.yml @@ -568,6 +568,28 @@ that: - "version13 is success" +- name: Test virtualenv command with venv formatting + when: ansible_python.version.major > 2 + block: + - name: Clean up the virtualenv + file: + state: absent + name: "{{ remote_tmp_dir }}/pipenv" + + # ref: https://github.com/ansible/ansible/issues/76372 + - name: install using different venv formatting + pip: + name: "{{ pip_test_package }}" + virtualenv: "{{ remote_tmp_dir }}/pipenv" + virtualenv_command: "{{ ansible_python_interpreter ~ ' -mvenv' }}" + state: present + register: version14 + + - name: ensure install using virtualenv_command with venv formatting + assert: + that: + - "version14 is changed" + ### test virtualenv_command end ### # https://github.com/ansible/ansible/issues/68592 diff --git a/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py b/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py index 9f1c5c0b..44412f22 100644 --- a/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py +++ b/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py @@ -11,7 +11,7 @@ __metaclass__ = type # noinspection PyUnresolvedReferences try: - from pkg_resources import Requirement + from pkg_resources import Requirement # pylint: disable=unused-import except ImportError: Requirement = None diff --git a/test/integration/targets/plugin_filtering/filter_lookup.yml b/test/integration/targets/plugin_filtering/filter_lookup.yml index 694ebfcb..5f183e9f 100644 --- a/test/integration/targets/plugin_filtering/filter_lookup.yml +++ b/test/integration/targets/plugin_filtering/filter_lookup.yml @@ -1,6 +1,6 @@ --- filter_version: 1.0 -module_blacklist: +module_rejectlist: # Specify the name of a lookup plugin here. This should have no effect as # this is only for filtering modules - list diff --git a/test/integration/targets/plugin_filtering/filter_modules.yml b/test/integration/targets/plugin_filtering/filter_modules.yml index 6cffa676..bef7d6d8 100644 --- a/test/integration/targets/plugin_filtering/filter_modules.yml +++ b/test/integration/targets/plugin_filtering/filter_modules.yml @@ -1,6 +1,6 @@ --- filter_version: 1.0 -module_blacklist: +module_rejectlist: # A pure action plugin - pause # A hybrid action plugin with module diff --git a/test/integration/targets/plugin_filtering/filter_ping.yml b/test/integration/targets/plugin_filtering/filter_ping.yml index 08e56f24..8604716e 100644 --- a/test/integration/targets/plugin_filtering/filter_ping.yml +++ b/test/integration/targets/plugin_filtering/filter_ping.yml @@ -1,5 +1,5 @@ --- filter_version: 1.0 -module_blacklist: +module_rejectlist: # Ping is special - ping diff --git a/test/integration/targets/plugin_filtering/filter_stat.yml b/test/integration/targets/plugin_filtering/filter_stat.yml index c1ce42ef..132bf03f 100644 --- a/test/integration/targets/plugin_filtering/filter_stat.yml +++ b/test/integration/targets/plugin_filtering/filter_stat.yml @@ -1,5 +1,5 @@ --- filter_version: 1.0 -module_blacklist: +module_rejectlist: # Stat is special - stat diff --git a/test/integration/targets/plugin_filtering/runme.sh b/test/integration/targets/plugin_filtering/runme.sh index aa0e2b0c..03d78abc 100755 --- a/test/integration/targets/plugin_filtering/runme.sh +++ b/test/integration/targets/plugin_filtering/runme.sh @@ -22,11 +22,11 @@ if test $? != 0 ; then fi # -# Check that if no modules are blacklisted then Ansible should not through traceback +# Check that if no modules are rejected then Ansible should not through traceback # -ANSIBLE_CONFIG=no_blacklist_module.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@" +ANSIBLE_CONFIG=no_rejectlist_module.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@" if test $? != 0 ; then - echo "### Failed to run tempfile with no modules blacklisted" + echo "### Failed to run tempfile with no modules rejected" exit 1 fi @@ -87,7 +87,7 @@ fi ANSIBLE_CONFIG=filter_lookup.ini ansible-playbook lookup.yml -i ../../inventory -vvv "$@" if test $? != 0 ; then - echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* blacklist" + echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* reject list" exit 1 fi @@ -107,10 +107,10 @@ ANSIBLE_CONFIG=filter_stat.ini export ANSIBLE_CONFIG CAPTURE=$(ansible-playbook copy.yml -i ../../inventory -vvv "$@" 2>&1) if test $? = 0 ; then - echo "### Copy ran even though stat is in the module blacklist" + echo "### Copy ran even though stat is in the module reject list" exit 1 else - echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.' + echo "$CAPTURE" | grep 'The stat module was specified in the module reject list file,.*, but Ansible will not function without the stat module. Please remove stat from the reject list.' if test $? != 0 ; then echo "### Stat did not give us our custom error message" exit 1 @@ -124,10 +124,10 @@ ANSIBLE_CONFIG=filter_stat.ini export ANSIBLE_CONFIG CAPTURE=$(ansible-playbook stat.yml -i ../../inventory -vvv "$@" 2>&1) if test $? = 0 ; then - echo "### Stat ran even though it is in the module blacklist" + echo "### Stat ran even though it is in the module reject list" exit 1 else - echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.' + echo "$CAPTURE" | grep 'The stat module was specified in the module reject list file,.*, but Ansible will not function without the stat module. Please remove stat from the reject list.' if test $? != 0 ; then echo "### Stat did not give us our custom error message" exit 1 diff --git a/test/integration/targets/plugin_loader/override/filters.yml b/test/integration/targets/plugin_loader/override/filters.yml index e51ab4e9..569a4479 100644 --- a/test/integration/targets/plugin_loader/override/filters.yml +++ b/test/integration/targets/plugin_loader/override/filters.yml @@ -1,7 +1,7 @@ - hosts: testhost gather_facts: false tasks: - - name: ensure local 'flag' filter works, 'flatten' is overriden and 'ternary' is still from core + - name: ensure local 'flag' filter works, 'flatten' is overridden and 'ternary' is still from core assert: that: - a|flag == 'flagged' diff --git a/test/integration/targets/plugin_loader/runme.sh b/test/integration/targets/plugin_loader/runme.sh index e30f6241..e68f06ad 100755 --- a/test/integration/targets/plugin_loader/runme.sh +++ b/test/integration/targets/plugin_loader/runme.sh @@ -34,3 +34,8 @@ done # test config loading ansible-playbook use_coll_name.yml -i ../../inventory -e 'ansible_connection=ansible.builtin.ssh' "$@" + +# test filter loading ignoring duplicate file basename +ansible-playbook file_collision/play.yml "$@" + +ANSIBLE_COLLECTIONS_PATH=$PWD/collections ansible-playbook unsafe_plugin_name.yml "$@" diff --git a/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py b/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py index e542913d..41a76d9b 100644 --- a/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py +++ b/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py @@ -64,7 +64,7 @@ from collections.abc import MutableMapping from ansible.errors import AnsibleError, AnsibleParserError from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.plugins.inventory import BaseFileInventoryPlugin NoneType = type(None) diff --git a/test/integration/targets/remote_tmp/playbook.yml b/test/integration/targets/remote_tmp/playbook.yml index 5adef626..2d0db4e8 100644 --- a/test/integration/targets/remote_tmp/playbook.yml +++ b/test/integration/targets/remote_tmp/playbook.yml @@ -30,30 +30,43 @@ - name: Test tempdir is removed hosts: testhost gather_facts: false + vars: + # These tests cannot be run with pipelining as it defeats the purpose of + # ensuring remote_tmp is cleaned up. Pipelining is enabled in the test + # inventory + ansible_pipelining: false + # Ensure that the remote_tmp_dir we create allows the unpriv connection user + # to create the remote_tmp + ansible_become: false tasks: - import_role: name: ../setup_remote_tmp_dir - - file: - state: touch - path: "{{ remote_tmp_dir }}/65393" + - vars: + # Isolate the remote_tmp used by these tests + ansible_remote_tmp: "{{ remote_tmp_dir }}/remote_tmp" + block: + - file: + state: touch + path: "{{ remote_tmp_dir }}/65393" - - copy: - src: "{{ remote_tmp_dir }}/65393" - dest: "{{ remote_tmp_dir }}/65393.2" - remote_src: true + - copy: + src: "{{ remote_tmp_dir }}/65393" + dest: "{{ remote_tmp_dir }}/65393.2" + remote_src: true - - find: - path: "~/.ansible/tmp" - use_regex: yes - patterns: 'AnsiballZ_.+\.py' - recurse: true - register: result + - find: + path: "{{ ansible_remote_tmp }}" + use_regex: yes + patterns: 'AnsiballZ_.+\.py' + recurse: true + register: result - debug: var: result - assert: that: - # Should find nothing since pipelining is used - - result.files|length == 0 + # Should only be AnsiballZ_find.py because find is actively running + - result.files|length == 1 + - result.files[0].path.endswith('/AnsiballZ_find.py') diff --git a/test/integration/targets/replace/tasks/main.yml b/test/integration/targets/replace/tasks/main.yml index d267b783..ca8b4ec1 100644 --- a/test/integration/targets/replace/tasks/main.yml +++ b/test/integration/targets/replace/tasks/main.yml @@ -263,3 +263,22 @@ - replace_cat8.stdout_lines[1] == "9.9.9.9" - replace_cat8.stdout_lines[7] == "0.0.0.0" - replace_cat8.stdout_lines[13] == "0.0.0.0" + +# For Python 3.6 or greater - https://github.com/ansible/ansible/issues/79364 +- name: Handle bad escape character in regular expression + replace: + path: /dev/null + after: ^ + before: $ + regexp: \. + replace: '\D' + ignore_errors: true + register: replace_test9 + when: ansible_python.version.major == 3 and ansible_python.version.minor > 6 + +- name: Validate the failure + assert: + that: + - replace_test9 is failure + - replace_test9.msg.startswith("Unable to process replace") + when: ansible_python.version.major == 3 and ansible_python.version.minor > 6 diff --git a/test/integration/targets/roles/runme.sh b/test/integration/targets/roles/runme.sh index bb98a932..bf3aaf58 100755 --- a/test/integration/targets/roles/runme.sh +++ b/test/integration/targets/roles/runme.sh @@ -3,26 +3,47 @@ set -eux # test no dupes when dependencies in b and c point to a in roles: -[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags inroles "$@" | grep -c '"msg": "A"')" = "1" ] -[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags acrossroles "$@" | grep -c '"msg": "A"')" = "1" ] -[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags intasks "$@" | grep -c '"msg": "A"')" = "1" ] +[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags inroles | grep -c '"msg": "A"')" = "1" ] +[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags acrossroles | grep -c '"msg": "A"')" = "1" ] +[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags intasks | grep -c '"msg": "A"')" = "1" ] # but still dupe across plays -[ "$(ansible-playbook no_dupes.yml -i ../../inventory "$@" | grep -c '"msg": "A"')" = "3" ] +[ "$(ansible-playbook no_dupes.yml -i ../../inventory | grep -c '"msg": "A"')" = "3" ] + +# and don't dedupe before the role successfully completes +[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags conditional_skipped | grep -c '"msg": "A"')" = "1" ] +[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags conditional_failed | grep -c '"msg": "failed_when task succeeded"')" = "1" ] +[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags unreachable -vvv | grep -c '"data": "reachable"')" = "1" ] +ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags unreachable | grep -e 'ignored=2' # include/import can execute another instance of role -[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags importrole "$@" | grep -c '"msg": "A"')" = "2" ] -[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags includerole "$@" | grep -c '"msg": "A"')" = "2" ] +[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags importrole | grep -c '"msg": "A"')" = "2" ] +[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags includerole | grep -c '"msg": "A"')" = "2" ] +[ "$(ansible-playbook dupe_inheritance.yml -i ../../inventory | grep -c '"msg": "abc"')" = "3" ] # ensure role data is merged correctly ansible-playbook data_integrity.yml -i ../../inventory "$@" # ensure role fails when trying to load 'non role' in _from -ansible-playbook no_outside.yml -i ../../inventory "$@" > role_outside_output.log 2>&1 || true +ansible-playbook no_outside.yml -i ../../inventory > role_outside_output.log 2>&1 || true if grep "as it is not inside the expected role path" role_outside_output.log >/dev/null; then echo "Test passed (playbook failed with expected output, output not shown)." else echo "Test failed, expected output from playbook failure is missing, output not shown)." exit 1 fi + +# ensure vars scope is correct +ansible-playbook vars_scope.yml -i ../../inventory "$@" + +# test nested includes get parent roles greater than a depth of 3 +[ "$(ansible-playbook 47023.yml -i ../../inventory | grep '\<\(Default\|Var\)\>' | grep -c 'is defined')" = "2" ] + +# ensure import_role called from include_role has the include_role in the dep chain +ansible-playbook role_dep_chain.yml -i ../../inventory "$@" + +# global role privacy setting test, set to private, set to not private, default +ANSIBLE_PRIVATE_ROLE_VARS=1 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@" +ANSIBLE_PRIVATE_ROLE_VARS=0 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@" +ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@" diff --git a/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml b/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml index 1a1ccbe4..10dce6d2 100644 --- a/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml +++ b/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml @@ -2,6 +2,15 @@ argument_specs: main: short_description: Main entry point for role C. options: + c_dict: + type: "dict" + required: true c_int: type: "int" required: true + c_list: + type: "list" + required: true + c_raw: + type: "raw" + required: true diff --git a/test/integration/targets/roles_arg_spec/test.yml b/test/integration/targets/roles_arg_spec/test.yml index 5eca7c73..b88d2e18 100644 --- a/test/integration/targets/roles_arg_spec/test.yml +++ b/test/integration/targets/roles_arg_spec/test.yml @@ -48,6 +48,7 @@ name: a vars: a_int: "{{ INT_VALUE }}" + a_str: "import_role" - name: "Call role entry point that is defined, but has no spec data" import_role: @@ -144,7 +145,10 @@ hosts: localhost gather_facts: false vars: + c_dict: {} c_int: 1 + c_list: [] + c_raw: ~ a_str: "some string" a_int: 42 tasks: @@ -156,6 +160,125 @@ include_role: name: c +- name: "New play to reset vars: Test nested role including/importing role fails with null required options" + hosts: localhost + gather_facts: false + vars: + a_main_spec: + a_str: + required: true + type: "str" + c_main_spec: + c_int: + required: true + type: "int" + c_list: + required: true + type: "list" + c_dict: + required: true + type: "dict" + c_raw: + required: true + type: "raw" + # role c calls a's main and alternate entrypoints + a_str: '' + c_dict: {} + c_int: 0 + c_list: [] + c_raw: ~ + tasks: + - name: test type coercion fails on None for required str + block: + - name: "Test import_role of role C (missing a_str)" + import_role: + name: c + vars: + a_str: ~ + - fail: + msg: "Should not get here" + rescue: + - debug: + var: ansible_failed_result + - name: "Validate import_role failure" + assert: + that: + # NOTE: a bug here that prevents us from getting ansible_failed_task + - ansible_failed_result.argument_errors == [error] + - ansible_failed_result.argument_spec_data == a_main_spec + vars: + error: >- + argument 'a_str' is of type <class 'NoneType'> and we were unable to convert to str: + 'None' is not a string and conversion is not allowed + + - name: test type coercion fails on None for required int + block: + - name: "Test import_role of role C (missing c_int)" + import_role: + name: c + vars: + c_int: ~ + - fail: + msg: "Should not get here" + rescue: + - debug: + var: ansible_failed_result + - name: "Validate import_role failure" + assert: + that: + # NOTE: a bug here that prevents us from getting ansible_failed_task + - ansible_failed_result.argument_errors == [error] + - ansible_failed_result.argument_spec_data == c_main_spec + vars: + error: >- + argument 'c_int' is of type <class 'NoneType'> and we were unable to convert to int: + <class 'NoneType'> cannot be converted to an int + + - name: test type coercion fails on None for required list + block: + - name: "Test import_role of role C (missing c_list)" + import_role: + name: c + vars: + c_list: ~ + - fail: + msg: "Should not get here" + rescue: + - debug: + var: ansible_failed_result + - name: "Validate import_role failure" + assert: + that: + # NOTE: a bug here that prevents us from getting ansible_failed_task + - ansible_failed_result.argument_errors == [error] + - ansible_failed_result.argument_spec_data == c_main_spec + vars: + error: >- + argument 'c_list' is of type <class 'NoneType'> and we were unable to convert to list: + <class 'NoneType'> cannot be converted to a list + + - name: test type coercion fails on None for required dict + block: + - name: "Test import_role of role C (missing c_dict)" + import_role: + name: c + vars: + c_dict: ~ + - fail: + msg: "Should not get here" + rescue: + - debug: + var: ansible_failed_result + - name: "Validate import_role failure" + assert: + that: + # NOTE: a bug here that prevents us from getting ansible_failed_task + - ansible_failed_result.argument_errors == [error] + - ansible_failed_result.argument_spec_data == c_main_spec + vars: + error: >- + argument 'c_dict' is of type <class 'NoneType'> and we were unable to convert to dict: + <class 'NoneType'> cannot be converted to a dict - name: "New play to reset vars: Test nested role including/importing role fails" hosts: localhost @@ -170,13 +293,15 @@ required: true type: "int" + c_int: 100 + c_list: [] + c_dict: {} + c_raw: ~ tasks: - block: - name: "Test import_role of role C (missing a_str)" import_role: name: c - vars: - c_int: 100 - fail: msg: "Should not get here" @@ -201,7 +326,6 @@ include_role: name: c vars: - c_int: 200 a_str: "some string" - fail: diff --git a/test/integration/targets/rpm_key/tasks/rpm_key.yaml b/test/integration/targets/rpm_key/tasks/rpm_key.yaml index 89ed2361..204b42ac 100644 --- a/test/integration/targets/rpm_key/tasks/rpm_key.yaml +++ b/test/integration/targets/rpm_key/tasks/rpm_key.yaml @@ -123,6 +123,32 @@ assert: that: "'rsa sha1 (md5) pgp md5 OK' in sl_check.stdout or 'digests signatures OK' in sl_check.stdout" +- name: get keyid + shell: "rpm -q gpg-pubkey | head -n 1 | xargs rpm -q --qf %{version}" + register: key_id + +- name: remove GPG key using keyid + rpm_key: + state: absent + key: "{{ key_id.stdout }}" + register: remove_keyid + failed_when: remove_keyid.changed == false + +- name: remove GPG key using keyid (idempotent) + rpm_key: + state: absent + key: "{{ key_id.stdout }}" + register: key_id_idempotence + +- name: verify idempotent (key_id) + assert: + that: "not key_id_idempotence.changed" + +- name: add very first key on system again + rpm_key: + state: present + key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7 + - name: Issue 20325 - Verify fingerprint of key, invalid fingerprint - EXPECTED FAILURE rpm_key: key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY.dag diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml index 74189f81..668ec48e 100644 --- a/test/integration/targets/script/tasks/main.yml +++ b/test/integration/targets/script/tasks/main.yml @@ -37,6 +37,17 @@ ## script ## +- name: Required one of free-form and cmd + script: + ignore_errors: yes + register: script_required + +- name: assert that the script fails if neither free-form nor cmd is given + assert: + that: + - script_required.failed + - "'one of the following' in script_required.msg" + - name: execute the test.sh script via command script: test.sh register: script_result0 diff --git a/test/integration/targets/service/aliases b/test/integration/targets/service/aliases index f2f9ac9d..f3703f85 100644 --- a/test/integration/targets/service/aliases +++ b/test/integration/targets/service/aliases @@ -1,4 +1,3 @@ destructive shippable/posix/group1 -skip/osx skip/macos diff --git a/test/integration/targets/service/files/ansible_test_service.py b/test/integration/targets/service/files/ansible_test_service.py index 522493fc..6292272e 100644 --- a/test/integration/targets/service/files/ansible_test_service.py +++ b/test/integration/targets/service/files/ansible_test_service.py @@ -9,7 +9,6 @@ __metaclass__ = type import os import resource import signal -import sys import time UMASK = 0 diff --git a/test/integration/targets/service_facts/aliases b/test/integration/targets/service_facts/aliases index 17d3eb75..32e10b03 100644 --- a/test/integration/targets/service_facts/aliases +++ b/test/integration/targets/service_facts/aliases @@ -1,4 +1,3 @@ shippable/posix/group2 skip/freebsd -skip/osx skip/macos diff --git a/test/integration/targets/setup_deb_repo/tasks/main.yml b/test/integration/targets/setup_deb_repo/tasks/main.yml index 471fb2a2..3e640f69 100644 --- a/test/integration/targets/setup_deb_repo/tasks/main.yml +++ b/test/integration/targets/setup_deb_repo/tasks/main.yml @@ -59,6 +59,7 @@ loop: - stable - testing + when: install_repo|default(True)|bool is true # Need to uncomment the deb-src for the universe component for build-dep state - name: Ensure deb-src for the universe component diff --git a/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml b/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml index f16d9b53..8c0b28bf 100644 --- a/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml +++ b/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml @@ -1,9 +1,2 @@ -- name: Setup remote constraints - include_tasks: setup-remote-constraints.yml - name: Install Paramiko for Python 3 on Alpine - pip: # no apk package manager in core, just use pip - name: paramiko - extra_args: "-c {{ remote_constraints }}" - environment: - # Not sure why this fixes the test, but it does. - SETUPTOOLS_USE_DISTUTILS: stdlib + command: apk add py3-paramiko diff --git a/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml b/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml index e9dcc62c..edb504ff 100644 --- a/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml +++ b/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml @@ -1,4 +1,2 @@ - name: Uninstall Paramiko for Python 3 on Alpine - pip: - name: paramiko - state: absent + command: apk del py3-paramiko diff --git a/test/integration/targets/setup_rpm_repo/tasks/main.yml b/test/integration/targets/setup_rpm_repo/tasks/main.yml index be20078f..bf5af101 100644 --- a/test/integration/targets/setup_rpm_repo/tasks/main.yml +++ b/test/integration/targets/setup_rpm_repo/tasks/main.yml @@ -24,9 +24,18 @@ args: name: "{{ rpm_repo_packages }}" - - name: Install rpmfluff via pip - pip: - name: rpmfluff + - name: Install rpmfluff via pip, ensure it is installed with default python as python3-rpm may not exist for other versions + block: + - action: "{{ ansible_facts.pkg_mgr }}" + args: + name: + - python3-pip + - python3 + state: latest + + - pip: + name: rpmfluff + executable: pip3 when: ansible_facts.os_family == 'RedHat' and ansible_distribution_major_version is version('9', '==') - set_fact: diff --git a/test/integration/targets/strategy_linear/runme.sh b/test/integration/targets/strategy_linear/runme.sh index cbb6aea3..a2734f97 100755 --- a/test/integration/targets/strategy_linear/runme.sh +++ b/test/integration/targets/strategy_linear/runme.sh @@ -5,3 +5,5 @@ set -eux ansible-playbook test_include_file_noop.yml -i inventory "$@" ansible-playbook task_action_templating.yml -i inventory "$@" + +ansible-playbook task_templated_run_once.yml -i inventory "$@" diff --git a/test/integration/targets/subversion/aliases b/test/integration/targets/subversion/aliases index 3cc41e4c..03b96434 100644 --- a/test/integration/targets/subversion/aliases +++ b/test/integration/targets/subversion/aliases @@ -1,6 +1,4 @@ shippable/posix/group2 -skip/osx skip/macos -skip/rhel/9.0b # svn checkout hangs destructive needs/root diff --git a/test/integration/targets/systemd/tasks/test_indirect_service.yml b/test/integration/targets/systemd/tasks/test_indirect_service.yml index fc11343e..0df60486 100644 --- a/test/integration/targets/systemd/tasks/test_indirect_service.yml +++ b/test/integration/targets/systemd/tasks/test_indirect_service.yml @@ -34,4 +34,4 @@ - assert: that: - systemd_enable_dummy_indirect_1 is changed - - systemd_enable_dummy_indirect_2 is not changed
\ No newline at end of file + - systemd_enable_dummy_indirect_2 is not changed diff --git a/test/integration/targets/systemd/vars/Debian.yml b/test/integration/targets/systemd/vars/Debian.yml index 613410f0..2dd0affb 100644 --- a/test/integration/targets/systemd/vars/Debian.yml +++ b/test/integration/targets/systemd/vars/Debian.yml @@ -1,3 +1,3 @@ ssh_service: ssh sleep_bin_path: /bin/sleep -indirect_service: dummy
\ No newline at end of file +indirect_service: dummy diff --git a/test/integration/targets/tags/runme.sh b/test/integration/targets/tags/runme.sh index 9da0b301..7dcb9985 100755 --- a/test/integration/targets/tags/runme.sh +++ b/test/integration/targets/tags/runme.sh @@ -73,3 +73,12 @@ ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=list --tags t ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged --tags untagged "$@" ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged_list --tags untagged,tag3 "$@" ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=tagged --tags tagged "$@" + +ansible-playbook test_template_parent_tags.yml "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_parent_tags.yml --tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt + +ansible-playbook test_template_parent_tags.yml --skip-tags tag1 "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'Tagged_task')" = "0" ]; rm out.txt diff --git a/test/integration/targets/tasks/playbook.yml b/test/integration/targets/tasks/playbook.yml index 80d9f8b1..10bd8591 100644 --- a/test/integration/targets/tasks/playbook.yml +++ b/test/integration/targets/tasks/playbook.yml @@ -6,6 +6,11 @@ debug: msg: Hello + # ensure we properly test for an action name, not a task name when cheking for a meta task + - name: "meta" + debug: + msg: Hello + - name: ensure malformed raw_params on arbitrary actions are not ignored debug: garbage {{"with a template"}} diff --git a/test/integration/targets/tasks/runme.sh b/test/integration/targets/tasks/runme.sh index 594447bd..57cbf28a 100755 --- a/test/integration/targets/tasks/runme.sh +++ b/test/integration/targets/tasks/runme.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -ansible-playbook playbook.yml "$@" +ansible-playbook playbook.yml
\ No newline at end of file diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh index 30163af7..d3913d97 100755 --- a/test/integration/targets/template/runme.sh +++ b/test/integration/targets/template/runme.sh @@ -8,7 +8,10 @@ ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@" ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef() }}" -e "vars2={{ vars1 }}" # Test for https://github.com/ansible/ansible/issues/27262 -ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@" +ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed.yml -i ../../inventory -v "$@" + +# Test for https://github.com/ansible/ansible/pull/79129 +ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed_79129.yml -i ../../inventory -v "$@" # Test for #42585 ANSIBLE_ROLES_PATH=../ ansible-playbook custom_template.yml -i ../../inventory -v "$@" @@ -39,7 +42,7 @@ ansible-playbook 72262.yml -v "$@" ansible-playbook unsafe.yml -v "$@" # ensure Jinja2 overrides from a template are used -ansible-playbook in_template_overrides.yml -v "$@" +ansible-playbook template_overrides.yml -v "$@" ansible-playbook lazy_eval.yml -i ../../inventory -v "$@" diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml index 3c91734b..34e88287 100644 --- a/test/integration/targets/template/tasks/main.yml +++ b/test/integration/targets/template/tasks/main.yml @@ -25,7 +25,7 @@ - name: show jinja2 version debug: - msg: "{{ lookup('pipe', '{{ ansible_python[\"executable\"] }} -c \"import jinja2; print(jinja2.__version__)\"') }}" + msg: "{{ lookup('pipe', ansible_python.executable ~ ' -c \"import jinja2; print(jinja2.__version__)\"') }}" - name: get default group shell: id -gn @@ -760,7 +760,7 @@ that: - test vars: - test: "{{ lookup('file', '{{ output_dir }}/empty_template.templated')|length == 0 }}" + test: "{{ lookup('file', output_dir ~ '/empty_template.templated')|length == 0 }}" - name: test jinja2 override without colon throws proper error block: diff --git a/test/integration/targets/template/unsafe.yml b/test/integration/targets/template/unsafe.yml index bef9a4b4..6f163881 100644 --- a/test/integration/targets/template/unsafe.yml +++ b/test/integration/targets/template/unsafe.yml @@ -3,6 +3,7 @@ vars: nottemplated: this should not be seen imunsafe: !unsafe '{{ nottemplated }}' + unsafe_set: !unsafe '{{ "test" }}' tasks: - set_fact: @@ -12,11 +13,15 @@ - set_fact: this_always_safe: '{{ imunsafe }}' + - set_fact: + this_unsafe_set: "{{ unsafe_set }}" + - name: ensure nothing was templated assert: that: - this_always_safe == imunsafe - imunsafe == this_was_unsafe.strip() + - unsafe_set == this_unsafe_set.strip() - hosts: localhost diff --git a/test/integration/targets/template_jinja2_non_native/macro_override.yml b/test/integration/targets/template_jinja2_non_native/macro_override.yml index 8a1cabd2..c3f9ab69 100644 --- a/test/integration/targets/template_jinja2_non_native/macro_override.yml +++ b/test/integration/targets/template_jinja2_non_native/macro_override.yml @@ -12,4 +12,4 @@ - "'foobar' not in data" - "'\"foo\" \"bar\"' in data" vars: - data: "{{ lookup('file', '{{ output_dir }}/macro_override.out') }}" + data: "{{ lookup('file', output_dir ~ '/macro_override.out') }}" diff --git a/test/integration/targets/templating/tasks/main.yml b/test/integration/targets/templating/tasks/main.yml index 312e171d..edbf012e 100644 --- a/test/integration/targets/templating/tasks/main.yml +++ b/test/integration/targets/templating/tasks/main.yml @@ -33,3 +33,14 @@ - result is failed - >- "TemplateSyntaxError: Could not load \"asdf \": 'invalid plugin name: ansible.builtin.asdf '" in result.msg + +- name: Make sure syntax errors originating from a template being compiled into Python code object result in a failure + debug: + msg: "{{ lookup('vars', 'v1', default='', default='') }}" + ignore_errors: true + register: r + +- assert: + that: + - r is failed + - "'keyword argument repeated' in r.msg" diff --git a/test/integration/targets/test_core/tasks/main.yml b/test/integration/targets/test_core/tasks/main.yml index 8c2decbd..ac06d67e 100644 --- a/test/integration/targets/test_core/tasks/main.yml +++ b/test/integration/targets/test_core/tasks/main.yml @@ -126,6 +126,16 @@ hello: world register: executed_task +- name: Skip me with multiple conditions + set_fact: + hello: world + when: + - True == True + - foo == 'bar' + vars: + foo: foo + register: skipped_task_multi_condition + - name: Try skipped test on non-dictionary set_fact: hello: "{{ 'nope' is skipped }}" @@ -136,8 +146,11 @@ assert: that: - skipped_task is skipped + - skipped_task.false_condition == False - executed_task is not skipped - misuse_of_skipped is failure + - skipped_task_multi_condition is skipped + - skipped_task_multi_condition.false_condition == "foo == 'bar'" - name: Not an async task set_fact: diff --git a/test/integration/targets/unarchive/tasks/main.yml b/test/integration/targets/unarchive/tasks/main.yml index 148e583f..b07c2fe7 100644 --- a/test/integration/targets/unarchive/tasks/main.yml +++ b/test/integration/targets/unarchive/tasks/main.yml @@ -20,3 +20,4 @@ - import_tasks: test_different_language_var.yml - import_tasks: test_invalid_options.yml - import_tasks: test_ownership_top_folder.yml +- import_tasks: test_relative_dest.yml diff --git a/test/integration/targets/unarchive/tasks/test_different_language_var.yml b/test/integration/targets/unarchive/tasks/test_different_language_var.yml index 9eec658e..32c84f4b 100644 --- a/test/integration/targets/unarchive/tasks/test_different_language_var.yml +++ b/test/integration/targets/unarchive/tasks/test_different_language_var.yml @@ -2,10 +2,10 @@ when: ansible_os_family == 'Debian' block: - name: install fr language pack - apt: + apt: name: language-pack-fr state: present - + - name: create our unarchive destination file: path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-くらとみ-tar-gz" diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml index 06fbc7b8..efd428eb 100644 --- a/test/integration/targets/unarchive/tasks/test_mode.yml +++ b/test/integration/targets/unarchive/tasks/test_mode.yml @@ -3,6 +3,29 @@ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz' state: directory +- name: test invalid modes + unarchive: + src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz" + dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz" + remote_src: yes + mode: "{{ item }}" + list_files: True + register: unarchive_mode_errors + ignore_errors: yes + loop: + - u=foo + - foo=r + - ufoo=r + - abc=r + - ao=r + - oa=r + +- assert: + that: + - item.failed + - "'bad symbolic permission for mode: ' + item.item == item.details" + loop: "{{ unarchive_mode_errors.results }}" + - name: unarchive and set mode to 0600, directories 0700 unarchive: src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz" diff --git a/test/integration/targets/unsafe_writes/aliases b/test/integration/targets/unsafe_writes/aliases index da1b554e..3560af2f 100644 --- a/test/integration/targets/unsafe_writes/aliases +++ b/test/integration/targets/unsafe_writes/aliases @@ -1,7 +1,6 @@ context/target needs/root skip/freebsd -skip/osx skip/macos shippable/posix/group2 needs/target/setup_remote_tmp_dir diff --git a/test/integration/targets/until/tasks/main.yml b/test/integration/targets/until/tasks/main.yml index 2b2ac94e..42ce9c8f 100644 --- a/test/integration/targets/until/tasks/main.yml +++ b/test/integration/targets/until/tasks/main.yml @@ -82,3 +82,37 @@ register: counter delay: 0.5 until: counter.rc == 0 + +- name: test retries without explicit until, defaults to "until task succeeds" + block: + - name: EXPECTED FAILURE + fail: + retries: 3 + delay: 0.1 + register: r + ignore_errors: true + + - assert: + that: + - r.attempts == 3 + + - vars: + test_file: "{{ lookup('env', 'OUTPUT_DIR') }}/until_success_test_file" + block: + - file: + name: "{{ test_file }}" + state: absent + + - name: fail on the first invocation, succeed on the second + shell: "[ -f {{ test_file }} ] || (touch {{ test_file }} && false)" + retries: 5 + delay: 0.1 + register: r + always: + - file: + name: "{{ test_file }}" + state: absent + + - assert: + that: + - r.attempts == 2 diff --git a/test/integration/targets/unvault/main.yml b/test/integration/targets/unvault/main.yml index a0f97b4b..8f0adc75 100644 --- a/test/integration/targets/unvault/main.yml +++ b/test/integration/targets/unvault/main.yml @@ -1,4 +1,5 @@ - hosts: localhost + gather_facts: false tasks: - set_fact: unvaulted: "{{ lookup('unvault', 'vault') }}" diff --git a/test/integration/targets/unvault/runme.sh b/test/integration/targets/unvault/runme.sh index df4585e3..054a14df 100755 --- a/test/integration/targets/unvault/runme.sh +++ b/test/integration/targets/unvault/runme.sh @@ -2,5 +2,5 @@ set -eux - +# simple run ansible-playbook --vault-password-file password main.yml diff --git a/test/integration/targets/uri/tasks/main.yml b/test/integration/targets/uri/tasks/main.yml index 9ba09ece..ddae83a0 100644 --- a/test/integration/targets/uri/tasks/main.yml +++ b/test/integration/targets/uri/tasks/main.yml @@ -132,7 +132,7 @@ - "result.changed == true" - name: "get ca certificate {{ self_signed_host }}" - get_url: + uri: url: "http://{{ httpbin_host }}/ca2cert.pem" dest: "{{ remote_tmp_dir }}/ca2cert.pem" @@ -638,9 +638,18 @@ - assert: that: - result['set_cookie'] == 'Foo=bar, Baz=qux' - # Python sorts cookies in order of most specific (ie. longest) path first + # Python 3.10 and earlier sorts cookies in order of most specific (ie. longest) path first # items with the same path are reversed from response order - result['cookies_string'] == 'Baz=qux; Foo=bar' + when: ansible_python_version is version('3.11', '<') + +- assert: + that: + - result['set_cookie'] == 'Foo=bar, Baz=qux' + # Python 3.11 no longer sorts cookies. + # See: https://github.com/python/cpython/issues/86232 + - result['cookies_string'] == 'Foo=bar; Baz=qux' + when: ansible_python_version is version('3.11', '>=') - name: Write out netrc template template: @@ -757,6 +766,30 @@ dest: "{{ remote_tmp_dir }}/output" state: absent +- name: Test download root to dir without content-disposition + uri: + url: "https://{{ httpbin_host }}/" + dest: "{{ remote_tmp_dir }}" + register: get_root_no_filename + +- name: Test downloading to dir without content-disposition + uri: + url: "https://{{ httpbin_host }}/response-headers" + dest: "{{ remote_tmp_dir }}" + register: get_dir_no_filename + +- name: Test downloading to dir with content-disposition + uri: + url: 'https://{{ httpbin_host }}/response-headers?Content-Disposition=attachment%3B%20filename%3D%22filename.json%22' + dest: "{{ remote_tmp_dir }}" + register: get_dir_filename + +- assert: + that: + - get_root_no_filename.path == remote_tmp_dir ~ "/index.html" + - get_dir_no_filename.path == remote_tmp_dir ~ "/response-headers" + - get_dir_filename.path == remote_tmp_dir ~ "/filename.json" + - name: Test follow_redirects=none import_tasks: redirect-none.yml diff --git a/test/integration/targets/uri/tasks/redirect-none.yml b/test/integration/targets/uri/tasks/redirect-none.yml index 0d1b2b34..060950d2 100644 --- a/test/integration/targets/uri/tasks/redirect-none.yml +++ b/test/integration/targets/uri/tasks/redirect-none.yml @@ -240,7 +240,7 @@ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything follow_redirects: none return_content: yes - method: GET + method: HEAD ignore_errors: yes register: http_308_head diff --git a/test/integration/targets/uri/tasks/redirect-urllib2.yml b/test/integration/targets/uri/tasks/redirect-urllib2.yml index 6cdafdb2..73e87960 100644 --- a/test/integration/targets/uri/tasks/redirect-urllib2.yml +++ b/test/integration/targets/uri/tasks/redirect-urllib2.yml @@ -237,7 +237,7 @@ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything follow_redirects: urllib2 return_content: yes - method: GET + method: HEAD ignore_errors: yes register: http_308_head @@ -250,6 +250,23 @@ - http_308_head.redirected == false - http_308_head.status == 308 - http_308_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything' + # Python 3.10 and earlier do not support HTTP 308 responses. + # See: https://github.com/python/cpython/issues/84501 + when: ansible_python_version is version('3.11', '<') + +# NOTE: The HTTP HEAD turns into an HTTP GET +- assert: + that: + - http_308_head is successful + - http_308_head.json.data == '' + - http_308_head.json.method == 'GET' + - http_308_head.json.url == 'https://{{ httpbin_host }}/anything' + - http_308_head.redirected == true + - http_308_head.status == 200 + - http_308_head.url == 'https://{{ httpbin_host }}/anything' + # Python 3.11 introduced support for HTTP 308 responses. + # See: https://github.com/python/cpython/issues/84501 + when: ansible_python_version is version('3.11', '>=') # FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809 - name: Test HTTP 308 using GET @@ -270,6 +287,22 @@ - http_308_get.redirected == false - http_308_get.status == 308 - http_308_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything' + # Python 3.10 and earlier do not support HTTP 308 responses. + # See: https://github.com/python/cpython/issues/84501 + when: ansible_python_version is version('3.11', '<') + +- assert: + that: + - http_308_get is successful + - http_308_get.json.data == '' + - http_308_get.json.method == 'GET' + - http_308_get.json.url == 'https://{{ httpbin_host }}/anything' + - http_308_get.redirected == true + - http_308_get.status == 200 + - http_308_get.url == 'https://{{ httpbin_host }}/anything' + # Python 3.11 introduced support for HTTP 308 responses. + # See: https://github.com/python/cpython/issues/84501 + when: ansible_python_version is version('3.11', '>=') # FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809 - name: Test HTTP 308 using POST diff --git a/test/integration/targets/uri/tasks/return-content.yml b/test/integration/targets/uri/tasks/return-content.yml index 5a9b97e6..cb8aeea2 100644 --- a/test/integration/targets/uri/tasks/return-content.yml +++ b/test/integration/targets/uri/tasks/return-content.yml @@ -46,4 +46,4 @@ assert: that: - result is failed - - "'content' not in result"
\ No newline at end of file + - "'content' not in result" diff --git a/test/integration/targets/uri/tasks/use_netrc.yml b/test/integration/targets/uri/tasks/use_netrc.yml index da745b89..521f8ebf 100644 --- a/test/integration/targets/uri/tasks/use_netrc.yml +++ b/test/integration/targets/uri/tasks/use_netrc.yml @@ -48,4 +48,4 @@ - name: Clean up file: dest: "{{ remote_tmp_dir }}/netrc" - state: absent
\ No newline at end of file + state: absent diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml index 9d36bfca..be4c4d6f 100644 --- a/test/integration/targets/user/tasks/main.yml +++ b/test/integration/targets/user/tasks/main.yml @@ -31,7 +31,9 @@ - import_tasks: test_expires.yml - import_tasks: test_expires_new_account.yml - import_tasks: test_expires_new_account_epoch_negative.yml +- import_tasks: test_expires_no_shadow.yml - import_tasks: test_expires_min_max.yml +- import_tasks: test_expires_warn.yml - import_tasks: test_shadow_backup.yml - import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_password_lock.yml diff --git a/test/integration/targets/user/tasks/test_create_user.yml b/test/integration/targets/user/tasks/test_create_user.yml index bced7905..644dbebb 100644 --- a/test/integration/targets/user/tasks/test_create_user.yml +++ b/test/integration/targets/user/tasks/test_create_user.yml @@ -65,3 +65,15 @@ - "user_test1.results[2]['state'] == 'present'" - "user_test1.results[3]['state'] == 'present'" - "user_test1.results[4]['state'] == 'present'" + +- name: register user informations + when: ansible_facts.system == 'Darwin' + command: dscl . -read /Users/ansibulluser + register: user_test2 + +- name: validate user defaults for MacOS + when: ansible_facts.system == 'Darwin' + assert: + that: + - "'RealName: ansibulluser' in user_test2.stdout_lines " + - "'PrimaryGroupID: 20' in user_test2.stdout_lines " diff --git a/test/integration/targets/user/tasks/test_create_user_home.yml b/test/integration/targets/user/tasks/test_create_user_home.yml index 1b529f76..5561a2f5 100644 --- a/test/integration/targets/user/tasks/test_create_user_home.yml +++ b/test/integration/targets/user/tasks/test_create_user_home.yml @@ -134,3 +134,21 @@ name: randomuser state: absent remove: yes + +- name: Create user home directory with /dev/null as skeleton, https://github.com/ansible/ansible/issues/75063 + # create_homedir is mostly used by linux, rest of OSs take care of it themselves via -k option (which fails this task) + when: ansible_system == 'Linux' + block: + - name: "Create user home directory with /dev/null as skeleton" + user: + name: withskeleton + state: present + skeleton: "/dev/null" + createhome: yes + register: create_user_with_skeleton_dev_null + always: + - name: "Remove test user" + user: + name: withskeleton + state: absent + remove: yes diff --git a/test/integration/targets/user/tasks/test_local.yml b/test/integration/targets/user/tasks/test_local.yml index 67c24a21..217d4769 100644 --- a/test/integration/targets/user/tasks/test_local.yml +++ b/test/integration/targets/user/tasks/test_local.yml @@ -86,9 +86,11 @@ - testgroup3 - testgroup4 - testgroup5 + - testgroup6 - local_ansibulluser tags: - user_test_local_mode + register: test_groups - name: Create local_ansibulluser with groups user: @@ -113,6 +115,18 @@ tags: - user_test_local_mode +- name: Append groups for local_ansibulluser (again) + user: + name: local_ansibulluser + state: present + local: yes + groups: ['testgroup3', 'testgroup4'] + append: yes + register: local_user_test_4_again + ignore_errors: yes + tags: + - user_test_local_mode + - name: Test append without groups for local_ansibulluser user: name: local_ansibulluser @@ -133,6 +147,28 @@ tags: - user_test_local_mode +- name: Append groups for local_ansibulluser using group id + user: + name: local_ansibulluser + state: present + append: yes + groups: "{{ test_groups.results[5]['gid'] }}" + register: local_user_test_7 + ignore_errors: yes + tags: + - user_test_local_mode + +- name: Append groups for local_ansibulluser using gid (again) + user: + name: local_ansibulluser + state: present + append: yes + groups: "{{ test_groups.results[5]['gid'] }}" + register: local_user_test_7_again + ignore_errors: yes + tags: + - user_test_local_mode + # If we don't re-assign, then "Set user expiration" will # fail. - name: Re-assign named group for local_ansibulluser @@ -164,6 +200,7 @@ - testgroup3 - testgroup4 - testgroup5 + - testgroup6 - local_ansibulluser tags: - user_test_local_mode @@ -175,7 +212,10 @@ - local_user_test_2 is not changed - local_user_test_3 is changed - local_user_test_4 is changed + - local_user_test_4_again is not changed - local_user_test_6 is changed + - local_user_test_7 is changed + - local_user_test_7_again is not changed - local_user_test_remove_1 is changed - local_user_test_remove_2 is not changed tags: diff --git a/test/integration/targets/user/vars/main.yml b/test/integration/targets/user/vars/main.yml index 4b328f71..2acd1e12 100644 --- a/test/integration/targets/user/vars/main.yml +++ b/test/integration/targets/user/vars/main.yml @@ -10,4 +10,4 @@ status_command: default_user_group: openSUSE Leap: users - MacOSX: admin + MacOSX: staff diff --git a/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml b/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml index f2b2e54a..ef2a06e1 100644 --- a/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml +++ b/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml @@ -1,4 +1,4 @@ -# test code +# test code # (c) 2014, Michael DeHaan <michael.dehaan@gmail.com> # This file is part of Ansible @@ -22,7 +22,7 @@ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}" - name: deploy a template that will use variables at various levels - template: src=foo.j2 dest={{output_dir}}/foo.templated + template: src=foo.j2 dest={{output_dir}}/foo.templated register: template_result - name: copy known good into place @@ -33,9 +33,9 @@ register: diff_result - name: verify templated file matches known good - assert: - that: - - 'diff_result.stdout == ""' + assert: + that: + - 'diff_result.stdout == ""' - name: check debug variable with same name as var content debug: var=same_value_as_var_name_var diff --git a/test/integration/targets/var_precedence/ansible-var-precedence-check.py b/test/integration/targets/var_precedence/ansible-var-precedence-check.py index fc31688b..b03c87b8 100755 --- a/test/integration/targets/var_precedence/ansible-var-precedence-check.py +++ b/test/integration/targets/var_precedence/ansible-var-precedence-check.py @@ -14,7 +14,6 @@ import stat import subprocess import tempfile import yaml -from pprint import pprint from optparse import OptionParser from jinja2 import Environment @@ -364,9 +363,9 @@ class VarTestMaker(object): block_wrapper = [debug_task, test_task] if 'include_params' in self.features: - self.tasks.append(dict(name='including tasks', include='included_tasks.yml', vars=dict(findme='include_params'))) + self.tasks.append(dict(name='including tasks', include_tasks='included_tasks.yml', vars=dict(findme='include_params'))) else: - self.tasks.append(dict(include='included_tasks.yml')) + self.tasks.append(dict(include_tasks='included_tasks.yml')) fname = os.path.join(TESTDIR, 'included_tasks.yml') with open(fname, 'w') as f: diff --git a/test/integration/targets/var_precedence/test_var_precedence.yml b/test/integration/targets/var_precedence/test_var_precedence.yml index 58584bfb..bba661db 100644 --- a/test/integration/targets/var_precedence/test_var_precedence.yml +++ b/test/integration/targets/var_precedence/test_var_precedence.yml @@ -1,14 +1,18 @@ --- - hosts: testhost vars: - - ansible_hostname: "BAD!" - - vars_var: "vars_var" - - param_var: "BAD!" - - vars_files_var: "BAD!" - - extra_var_override_once_removed: "{{ extra_var_override }}" - - from_inventory_once_removed: "{{ inven_var | default('BAD!') }}" + ansible_hostname: "BAD!" + vars_var: "vars_var" + param_var: "BAD!" + vars_files_var: "BAD!" + extra_var_override_once_removed: "{{ extra_var_override }}" + from_inventory_once_removed: "{{ inven_var | default('BAD!') }}" vars_files: - vars/test_var_precedence.yml + pre_tasks: + - name: param vars should also override set_fact + set_fact: + param_var: "BAD!" roles: - { role: test_var_precedence, param_var: "param_var" } tasks: diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml index f81fd0f2..74b8e9aa 100644 --- a/test/integration/targets/wait_for/tasks/main.yml +++ b/test/integration/targets/wait_for/tasks/main.yml @@ -91,7 +91,7 @@ wait_for: path: "{{remote_tmp_dir}}/wait_for_keyword" search_regex: completed (?P<foo>\w+) ([0-9]+) - timeout: 5 + timeout: 25 register: waitfor - name: verify test wait for keyword in file with match groups @@ -114,6 +114,15 @@ path: "{{remote_tmp_dir}}/utf16.txt" search_regex: completed +- name: test non mmapable file + wait_for: + path: "/sys/class/net/lo/carrier" + search_regex: "1" + timeout: 30 + when: + - ansible_facts['os_family'] not in ['FreeBSD', 'Darwin'] + - not (ansible_facts['os_family'] in ['RedHat', 'CentOS'] and ansible_facts['distribution_major_version'] is version('7', '<=')) + - name: test wait for port timeout wait_for: port: 12121 diff --git a/test/integration/targets/win_exec_wrapper/tasks/main.yml b/test/integration/targets/win_exec_wrapper/tasks/main.yml index 8fc54f7c..f1342c48 100644 --- a/test/integration/targets/win_exec_wrapper/tasks/main.yml +++ b/test/integration/targets/win_exec_wrapper/tasks/main.yml @@ -272,3 +272,12 @@ assert: that: - ps_log_count.stdout | int == 0 + +- name: test module that sets HadErrors with no error records + test_rc_1: + register: module_had_errors + +- name: assert test module that sets HadErrors with no error records + assert: + that: + - module_had_errors.rc == 0 diff --git a/test/integration/targets/win_fetch/tasks/main.yml b/test/integration/targets/win_fetch/tasks/main.yml index b5818352..16a28761 100644 --- a/test/integration/targets/win_fetch/tasks/main.yml +++ b/test/integration/targets/win_fetch/tasks/main.yml @@ -215,3 +215,17 @@ - fetch_special_file.checksum == '34d4150adc3347f1dd8ce19fdf65b74d971ab602' - fetch_special_file.dest == host_output_dir + "/abc$not var'quote‘" - fetch_special_file_actual.stdout == 'abc' + +- name: create file with wildcard characters + raw: Set-Content -LiteralPath '{{ remote_tmp_dir }}\abc[].txt' -Value 'abc' + +- name: fetch file with wildcard characters + fetch: + src: '{{ remote_tmp_dir }}\abc[].txt' + dest: '{{ host_output_dir }}/' + register: fetch_wildcard_file_nofail + +- name: assert fetch file with wildcard characters + assert: + that: + - "fetch_wildcard_file_nofail is not failed" diff --git a/test/integration/targets/win_script/files/test_script_with_args.ps1 b/test/integration/targets/win_script/files/test_script_with_args.ps1 index 01bb37f5..669c6410 100644 --- a/test/integration/targets/win_script/files/test_script_with_args.ps1 +++ b/test/integration/targets/win_script/files/test_script_with_args.ps1 @@ -2,5 +2,5 @@ # passed to the script. foreach ($i in $args) { - Write-Host $i; + Write-Host $i } diff --git a/test/integration/targets/win_script/files/test_script_with_errors.ps1 b/test/integration/targets/win_script/files/test_script_with_errors.ps1 index 56f97735..bdf7ee48 100644 --- a/test/integration/targets/win_script/files/test_script_with_errors.ps1 +++ b/test/integration/targets/win_script/files/test_script_with_errors.ps1 @@ -2,7 +2,7 @@ trap { Write-Error -ErrorRecord $_ - exit 1; + exit 1 } throw "Oh noes I has an error" diff --git a/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 b/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 index f1704964..d23bbc74 100644 --- a/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 +++ b/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 @@ -16,16 +16,16 @@ # POWERSHELL_COMMON -$params = Parse-Args $args $true; +$params = Parse-Args $args $true -$data = Get-Attr $params "data" "pong"; +$data = Get-Attr $params "data" "pong" $result = @{ changed = $false ping = "pong" -}; +} # Test that Set-Attr will replace an existing attribute. Set-Attr $result "ping" $data -Exit-Json $result; +Exit-Json $result diff --git a/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 b/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 index 508174af..09400d08 100644 --- a/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 +++ b/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 @@ -16,15 +16,15 @@ # POWERSHELL_COMMON -$params = Parse-Args $args $true; +$params = Parse-Args $args $true $params.thisPropertyDoesNotExist -$data = Get-Attr $params "data" "pong"; +$data = Get-Attr $params "data" "pong" $result = @{ changed = $false ping = $data -}; +} -Exit-Json $result; +Exit-Json $result diff --git a/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 b/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 index d4c9f07a..6932d538 100644 --- a/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 +++ b/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 @@ -18,13 +18,13 @@ $blah = 'I can't quote my strings correctly.' -$params = Parse-Args $args $true; +$params = Parse-Args $args $true -$data = Get-Attr $params "data" "pong"; +$data = Get-Attr $params "data" "pong" $result = @{ changed = $false ping = $data -}; +} -Exit-Json $result; +Exit-Json $result diff --git a/test/integration/targets/windows-minimal/library/win_ping_throw.ps1 b/test/integration/targets/windows-minimal/library/win_ping_throw.ps1 index 7306f4d2..2fba2092 100644 --- a/test/integration/targets/windows-minimal/library/win_ping_throw.ps1 +++ b/test/integration/targets/windows-minimal/library/win_ping_throw.ps1 @@ -18,13 +18,13 @@ throw -$params = Parse-Args $args $true; +$params = Parse-Args $args $true -$data = Get-Attr $params "data" "pong"; +$data = Get-Attr $params "data" "pong" $result = @{ changed = $false ping = $data -}; +} -Exit-Json $result; +Exit-Json $result diff --git a/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 b/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 index 09e3b7cb..62de8263 100644 --- a/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 +++ b/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 @@ -18,13 +18,13 @@ throw "no ping for you" -$params = Parse-Args $args $true; +$params = Parse-Args $args $true -$data = Get-Attr $params "data" "pong"; +$data = Get-Attr $params "data" "pong" $result = @{ changed = $false ping = $data -}; +} -Exit-Json $result; +Exit-Json $result diff --git a/test/integration/targets/yum/aliases b/test/integration/targets/yum/aliases index 1d491339..b12f3547 100644 --- a/test/integration/targets/yum/aliases +++ b/test/integration/targets/yum/aliases @@ -1,5 +1,4 @@ destructive shippable/posix/group1 skip/freebsd -skip/osx skip/macos diff --git a/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py b/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py index 27f38ce5..306ccd9a 100644 --- a/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py +++ b/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py @@ -1,8 +1,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.errors import AnsibleError, AnsibleFilterError - def filter_list_of_tuples_by_first_param(lst, search, startswith=False): out = [] diff --git a/test/lib/ansible_test/_data/completion/docker.txt b/test/lib/ansible_test/_data/completion/docker.txt index 9e1a9d5e..a863ecbf 100644 --- a/test/lib/ansible_test/_data/completion/docker.txt +++ b/test/lib/ansible_test/_data/completion/docker.txt @@ -1,9 +1,9 @@ -base image=quay.io/ansible/base-test-container:3.9.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 -default image=quay.io/ansible/default-test-container:6.13.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=collection -default image=quay.io/ansible/ansible-core-test-container:6.13.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=ansible-core -alpine3 image=quay.io/ansible/alpine3-test-container:4.8.0 python=3.10 cgroup=none audit=none -centos7 image=quay.io/ansible/centos7-test-container:4.8.0 python=2.7 cgroup=v1-only -fedora36 image=quay.io/ansible/fedora36-test-container:4.8.0 python=3.10 -opensuse15 image=quay.io/ansible/opensuse15-test-container:4.8.0 python=3.6 -ubuntu2004 image=quay.io/ansible/ubuntu2004-test-container:4.8.0 python=3.8 -ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:4.8.0 python=3.10 +base image=quay.io/ansible/base-test-container:5.10.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11 +default image=quay.io/ansible/default-test-container:8.12.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11 context=collection +default image=quay.io/ansible/ansible-core-test-container:8.12.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11 context=ansible-core +alpine3 image=quay.io/ansible/alpine3-test-container:6.3.0 python=3.11 cgroup=none audit=none +centos7 image=quay.io/ansible/centos7-test-container:6.3.0 python=2.7 cgroup=v1-only +fedora38 image=quay.io/ansible/fedora38-test-container:6.3.0 python=3.11 +opensuse15 image=quay.io/ansible/opensuse15-test-container:6.3.0 python=3.6 +ubuntu2004 image=quay.io/ansible/ubuntu2004-test-container:6.3.0 python=3.8 +ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:6.3.0 python=3.10 diff --git a/test/lib/ansible_test/_data/completion/remote.txt b/test/lib/ansible_test/_data/completion/remote.txt index 9cb8dee8..06d4b5ef 100644 --- a/test/lib/ansible_test/_data/completion/remote.txt +++ b/test/lib/ansible_test/_data/completion/remote.txt @@ -1,16 +1,14 @@ -alpine/3.16 python=3.10 become=doas_sudo provider=aws arch=x86_64 +alpine/3.18 python=3.11 become=doas_sudo provider=aws arch=x86_64 alpine become=doas_sudo provider=aws arch=x86_64 -fedora/36 python=3.10 become=sudo provider=aws arch=x86_64 +fedora/38 python=3.11 become=sudo provider=aws arch=x86_64 fedora become=sudo provider=aws arch=x86_64 -freebsd/12.4 python=3.9 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 -freebsd/13.2 python=3.8,3.7,3.9,3.10 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 +freebsd/13.2 python=3.9,3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 -macos/12.0 python=3.10 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 +macos/13.2 python=3.11 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 rhel/7.9 python=2.7 become=sudo provider=aws arch=x86_64 -rhel/8.6 python=3.6,3.8,3.9 become=sudo provider=aws arch=x86_64 -rhel/9.0 python=3.9 become=sudo provider=aws arch=x86_64 +rhel/8.8 python=3.6,3.11 become=sudo provider=aws arch=x86_64 +rhel/9.2 python=3.9,3.11 become=sudo provider=aws arch=x86_64 rhel become=sudo provider=aws arch=x86_64 -ubuntu/20.04 python=3.8,3.9 become=sudo provider=aws arch=x86_64 ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64 ubuntu become=sudo provider=aws arch=x86_64 diff --git a/test/lib/ansible_test/_data/completion/windows.txt b/test/lib/ansible_test/_data/completion/windows.txt index 92b0d086..860a2e32 100644 --- a/test/lib/ansible_test/_data/completion/windows.txt +++ b/test/lib/ansible_test/_data/completion/windows.txt @@ -1,5 +1,3 @@ -windows/2012 provider=azure arch=x86_64 -windows/2012-R2 provider=azure arch=x86_64 windows/2016 provider=aws arch=x86_64 windows/2019 provider=aws arch=x86_64 windows/2022 provider=aws arch=x86_64 diff --git a/test/lib/ansible_test/_data/requirements/ansible-test.txt b/test/lib/ansible_test/_data/requirements/ansible-test.txt index f7cb9c27..17662f07 100644 --- a/test/lib/ansible_test/_data/requirements/ansible-test.txt +++ b/test/lib/ansible_test/_data/requirements/ansible-test.txt @@ -1,4 +1,5 @@ # The test-constraints sanity test verifies this file, but changes must be made manually to keep it in up-to-date. virtualenv == 16.7.12 ; python_version < '3' -coverage == 6.5.0 ; python_version >= '3.7' and python_version <= '3.11' +coverage == 7.3.2 ; python_version >= '3.8' and python_version <= '3.12' +coverage == 6.5.0 ; python_version >= '3.7' and python_version <= '3.7' coverage == 4.5.4 ; python_version >= '2.6' and python_version <= '3.6' diff --git a/test/lib/ansible_test/_data/requirements/ansible.txt b/test/lib/ansible_test/_data/requirements/ansible.txt index 20562c3e..5eaf9f2c 100644 --- a/test/lib/ansible_test/_data/requirements/ansible.txt +++ b/test/lib/ansible_test/_data/requirements/ansible.txt @@ -12,4 +12,4 @@ packaging # NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69 # NOTE: When updating the upper bound, also update the latest version used # NOTE: in the ansible-galaxy-collection test suite. -resolvelib >= 0.5.3, < 0.9.0 # dependency resolver used by ansible-galaxy +resolvelib >= 0.5.3, < 1.1.0 # dependency resolver used by ansible-galaxy diff --git a/test/lib/ansible_test/_data/requirements/constraints.txt b/test/lib/ansible_test/_data/requirements/constraints.txt index 627f41df..dd837e3b 100644 --- a/test/lib/ansible_test/_data/requirements/constraints.txt +++ b/test/lib/ansible_test/_data/requirements/constraints.txt @@ -5,7 +5,6 @@ pywinrm >= 0.3.0 ; python_version < '3.11' # message encryption support pywinrm >= 0.4.3 ; python_version >= '3.11' # support for Python 3.11 pytest < 5.0.0, >= 4.5.0 ; python_version == '2.7' # pytest 5.0.0 and later will no longer support python 2.7 pytest >= 4.5.0 ; python_version > '2.7' # pytest 4.5.0 added support for --strict-markers -pytest-forked >= 1.0.2 # pytest-forked before 1.0.2 does not work with pytest 4.2.0+ ntlm-auth >= 1.3.0 # message encryption support using cryptography requests-ntlm >= 1.1.0 # message encryption support requests-credssp >= 0.1.0 # message encryption support @@ -13,5 +12,4 @@ pyparsing < 3.0.0 ; python_version < '3.5' # pyparsing 3 and later require pytho mock >= 2.0.0 # needed for features backported from Python 3.6 unittest.mock (assert_called, assert_called_once...) pytest-mock >= 1.4.0 # needed for mock_use_standalone_module pytest option setuptools < 45 ; python_version == '2.7' # setuptools 45 and later require python 3.5 or later -pyspnego >= 0.1.6 ; python_version >= '3.10' # bug in older releases breaks on Python 3.10 wheel < 0.38.0 ; python_version < '3.7' # wheel 0.38.0 and later require python 3.7 or later diff --git a/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt b/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt index 580f0641..66801459 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt @@ -1,8 +1,5 @@ # edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 Jinja2==3.1.2 -MarkupSafe==2.1.1 -packaging==21.3 -pyparsing==3.0.9 -PyYAML==6.0 +MarkupSafe==2.1.3 +packaging==23.2 +PyYAML==6.0.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.changelog.in b/test/lib/ansible_test/_data/requirements/sanity.changelog.in index 7f231827..81d65ff8 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.changelog.in +++ b/test/lib/ansible_test/_data/requirements/sanity.changelog.in @@ -1,3 +1,2 @@ -rstcheck < 4 # match version used in other sanity tests +rstcheck < 6 # newer versions have too many dependencies antsibull-changelog -docutils < 0.18 # match version required by sphinx in the docs-build sanity test diff --git a/test/lib/ansible_test/_data/requirements/sanity.changelog.txt b/test/lib/ansible_test/_data/requirements/sanity.changelog.txt index 1755a489..d763bad2 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.changelog.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.changelog.txt @@ -1,10 +1,9 @@ # edit "sanity.changelog.in" and generate with: hacking/update-sanity-requirements.py --test changelog -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -antsibull-changelog==0.16.0 -docutils==0.17.1 -packaging==21.3 -pyparsing==3.0.9 -PyYAML==6.0 -rstcheck==3.5.0 +antsibull-changelog==0.23.0 +docutils==0.18.1 +packaging==23.2 +PyYAML==6.0.1 +rstcheck==5.0.0 semantic-version==2.10.0 +types-docutils==0.18.3 +typing_extensions==4.8.0 diff --git a/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt b/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt index 93e147a5..56366b77 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt @@ -1,6 +1,4 @@ # edit "sanity.import.plugin.in" and generate with: hacking/update-sanity-requirements.py --test import.plugin -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 Jinja2==3.1.2 -MarkupSafe==2.1.1 -PyYAML==6.0 +MarkupSafe==2.1.3 +PyYAML==6.0.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.import.txt b/test/lib/ansible_test/_data/requirements/sanity.import.txt index 4fda120d..4d9d4f53 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.import.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.import.txt @@ -1,4 +1,2 @@ # edit "sanity.import.in" and generate with: hacking/update-sanity-requirements.py --test import -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -PyYAML==6.0 +PyYAML==6.0.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt b/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt index 51cc1ca3..17d60b6f 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt @@ -1,4 +1,2 @@ # edit "sanity.integration-aliases.in" and generate with: hacking/update-sanity-requirements.py --test integration-aliases -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -PyYAML==6.0 +PyYAML==6.0.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.mypy.in b/test/lib/ansible_test/_data/requirements/sanity.mypy.in index 98dead6c..f01ae948 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.mypy.in +++ b/test/lib/ansible_test/_data/requirements/sanity.mypy.in @@ -1,10 +1,10 @@ -mypy[python2] != 0.971 # regression in 0.971 (see https://github.com/python/mypy/pull/13223) +mypy +cryptography # type stubs not published separately +jinja2 # type stubs not published separately packaging # type stubs not published separately types-backports -types-jinja2 -types-paramiko < 2.8.14 # newer versions drop support for Python 2.7 -types-pyyaml < 6 # PyYAML 6+ stubs do not support Python 2.7 -types-cryptography < 3.3.16 # newer versions drop support for Python 2.7 +types-paramiko +types-pyyaml types-requests types-setuptools types-toml diff --git a/test/lib/ansible_test/_data/requirements/sanity.mypy.txt b/test/lib/ansible_test/_data/requirements/sanity.mypy.txt index 9dffc8fb..f6a47fb0 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.mypy.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.mypy.txt @@ -1,20 +1,18 @@ # edit "sanity.mypy.in" and generate with: hacking/update-sanity-requirements.py --test mypy -mypy==0.961 -mypy-extensions==0.4.3 -packaging==21.3 -pyparsing==3.0.9 +cffi==1.16.0 +cryptography==41.0.4 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +mypy==1.5.1 +mypy-extensions==1.0.0 +packaging==23.2 +pycparser==2.21 tomli==2.0.1 -typed-ast==1.5.4 types-backports==0.1.3 -types-cryptography==3.3.15 -types-enum34==1.1.8 -types-ipaddress==1.0.8 -types-Jinja2==2.11.9 -types-MarkupSafe==1.1.10 -types-paramiko==2.8.13 -types-PyYAML==5.4.12 -types-requests==2.28.10 -types-setuptools==65.3.0 -types-toml==0.10.8 -types-urllib3==1.26.24 -typing_extensions==4.3.0 +types-paramiko==3.3.0.0 +types-PyYAML==6.0.12.12 +types-requests==2.31.0.7 +types-setuptools==68.2.0.0 +types-toml==0.10.8.7 +typing_extensions==4.8.0 +urllib3==2.0.6 diff --git a/test/lib/ansible_test/_data/requirements/sanity.pep8.txt b/test/lib/ansible_test/_data/requirements/sanity.pep8.txt index 60d5784f..1a36d4da 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.pep8.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.pep8.txt @@ -1,2 +1,2 @@ # edit "sanity.pep8.in" and generate with: hacking/update-sanity-requirements.py --test pep8 -pycodestyle==2.9.1 +pycodestyle==2.11.0 diff --git a/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 b/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 index 68545c9e..df36d61a 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 +++ b/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 @@ -28,8 +28,10 @@ Function Install-PSModule { } } +# Versions changes should be made first in ansible-test which is then synced to +# the default-test-container over time Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -Install-PSModule -Name PSScriptAnalyzer -RequiredVersion 1.20.0 +Install-PSModule -Name PSScriptAnalyzer -RequiredVersion 1.21.0 if ($IsContainer) { # PSScriptAnalyzer contain lots of json files for the UseCompatibleCommands check. We don't use this rule so by diff --git a/test/lib/ansible_test/_data/requirements/sanity.pylint.in b/test/lib/ansible_test/_data/requirements/sanity.pylint.in index fde21f12..ae189587 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.pylint.in +++ b/test/lib/ansible_test/_data/requirements/sanity.pylint.in @@ -1,2 +1,2 @@ -pylint == 2.15.5 # currently vetted version +pylint pyyaml # needed for collection_detail.py diff --git a/test/lib/ansible_test/_data/requirements/sanity.pylint.txt b/test/lib/ansible_test/_data/requirements/sanity.pylint.txt index 44d8b88c..c3144fe5 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.pylint.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.pylint.txt @@ -1,15 +1,11 @@ # edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -astroid==2.12.12 -dill==0.3.6 -isort==5.10.1 -lazy-object-proxy==1.7.1 +astroid==3.0.0 +dill==0.3.7 +isort==5.12.0 mccabe==0.7.0 -platformdirs==2.5.2 -pylint==2.15.5 -PyYAML==6.0 +platformdirs==3.11.0 +pylint==3.0.1 +PyYAML==6.0.1 tomli==2.0.1 -tomlkit==0.11.5 -typing_extensions==4.3.0 -wrapt==1.14.1 +tomlkit==0.12.1 +typing_extensions==4.8.0 diff --git a/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt b/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt index b2b70567..4af9b95e 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt @@ -1,5 +1,3 @@ # edit "sanity.runtime-metadata.in" and generate with: hacking/update-sanity-requirements.py --test runtime-metadata -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -PyYAML==6.0 +PyYAML==6.0.1 voluptuous==0.13.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in b/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in index efe94004..78e116f5 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in +++ b/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in @@ -1,3 +1,4 @@ jinja2 # ansible-core requirement pyyaml # needed for collection_detail.py voluptuous +antsibull-docs-parser==1.0.0 diff --git a/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt b/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt index 8a877bba..4e24d64d 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt @@ -1,7 +1,6 @@ # edit "sanity.validate-modules.in" and generate with: hacking/update-sanity-requirements.py --test validate-modules -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 +antsibull-docs-parser==1.0.0 Jinja2==3.1.2 -MarkupSafe==2.1.1 -PyYAML==6.0 +MarkupSafe==2.1.3 +PyYAML==6.0.1 voluptuous==0.13.1 diff --git a/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt b/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt index dd401113..bafd30b6 100644 --- a/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt +++ b/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt @@ -1,6 +1,4 @@ # edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -pathspec==0.10.1 -PyYAML==6.0 -yamllint==1.28.0 +pathspec==0.11.2 +PyYAML==6.0.1 +yamllint==1.32.0 diff --git a/test/lib/ansible_test/_data/requirements/units.txt b/test/lib/ansible_test/_data/requirements/units.txt index d2f56d35..d723a65f 100644 --- a/test/lib/ansible_test/_data/requirements/units.txt +++ b/test/lib/ansible_test/_data/requirements/units.txt @@ -2,5 +2,4 @@ mock pytest pytest-mock pytest-xdist -pytest-forked pyyaml # required by the collection loader (only needed for collections) diff --git a/test/lib/ansible_test/_internal/ci/azp.py b/test/lib/ansible_test/_internal/ci/azp.py index 404f8056..ebf260b9 100644 --- a/test/lib/ansible_test/_internal/ci/azp.py +++ b/test/lib/ansible_test/_internal/ci/azp.py @@ -70,7 +70,7 @@ class AzurePipelines(CIProvider): os.environ['SYSTEM_JOBIDENTIFIER'], ) except KeyError as ex: - raise MissingEnvironmentVariable(name=ex.args[0]) + raise MissingEnvironmentVariable(name=ex.args[0]) from None return prefix @@ -121,7 +121,7 @@ class AzurePipelines(CIProvider): task_id=str(uuid.UUID(os.environ['SYSTEM_TASKINSTANCEID'])), ) except KeyError as ex: - raise MissingEnvironmentVariable(name=ex.args[0]) + raise MissingEnvironmentVariable(name=ex.args[0]) from None self.auth.sign_request(request) @@ -154,7 +154,7 @@ class AzurePipelinesAuthHelper(CryptographyAuthHelper): try: agent_temp_directory = os.environ['AGENT_TEMPDIRECTORY'] except KeyError as ex: - raise MissingEnvironmentVariable(name=ex.args[0]) + raise MissingEnvironmentVariable(name=ex.args[0]) from None # the temporary file cannot be deleted because we do not know when the agent has processed it # placing the file in the agent's temp directory allows it to be picked up when the job is running in a container @@ -181,7 +181,7 @@ class AzurePipelinesChanges: self.source_branch_name = os.environ['BUILD_SOURCEBRANCHNAME'] self.pr_branch_name = os.environ.get('SYSTEM_PULLREQUEST_TARGETBRANCH') except KeyError as ex: - raise MissingEnvironmentVariable(name=ex.args[0]) + raise MissingEnvironmentVariable(name=ex.args[0]) from None if self.source_branch.startswith('refs/tags/'): raise ChangeDetectionNotSupported('Change detection is not supported for tags.') diff --git a/test/lib/ansible_test/_internal/cli/environments.py b/test/lib/ansible_test/_internal/cli/environments.py index 94cafae3..7b1fd1c2 100644 --- a/test/lib/ansible_test/_internal/cli/environments.py +++ b/test/lib/ansible_test/_internal/cli/environments.py @@ -146,12 +146,6 @@ def add_global_options( help='install command requirements', ) - global_parser.add_argument( - '--no-pip-check', - action='store_true', - help=argparse.SUPPRESS, # deprecated, kept for now (with a warning) for backwards compatibility - ) - add_global_remote(global_parser, controller_mode) add_global_docker(global_parser, controller_mode) @@ -396,7 +390,6 @@ def add_global_docker( """Add global options for Docker.""" if controller_mode != ControllerMode.DELEGATED: parser.set_defaults( - docker_no_pull=False, docker_network=None, docker_terminate=None, prime_containers=False, @@ -407,12 +400,6 @@ def add_global_docker( return parser.add_argument( - '--docker-no-pull', - action='store_true', - help=argparse.SUPPRESS, # deprecated, kept for now (with a warning) for backwards compatibility - ) - - parser.add_argument( '--docker-network', metavar='NET', help='run using the specified network', diff --git a/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py b/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py index ad6cf86f..64bb13b0 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py +++ b/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py @@ -57,9 +57,9 @@ def load_report(report: dict[str, t.Any]) -> tuple[list[str], Arcs, Lines]: arc_data: dict[str, dict[str, int]] = report['arcs'] line_data: dict[str, dict[int, int]] = report['lines'] except KeyError as ex: - raise ApplicationError('Document is missing key "%s".' % ex.args) + raise ApplicationError('Document is missing key "%s".' % ex.args) from None except TypeError: - raise ApplicationError('Document is type "%s" instead of "dict".' % type(report).__name__) + raise ApplicationError('Document is type "%s" instead of "dict".' % type(report).__name__) from None arcs = dict((path, dict((parse_arc(arc), set(target_sets[index])) for arc, index in data.items())) for path, data in arc_data.items()) lines = dict((path, dict((int(line), set(target_sets[index])) for line, index in data.items())) for path, data in line_data.items()) @@ -72,12 +72,12 @@ def read_report(path: str) -> tuple[list[str], Arcs, Lines]: try: report = read_json_file(path) except Exception as ex: - raise ApplicationError('File "%s" is not valid JSON: %s' % (path, ex)) + raise ApplicationError('File "%s" is not valid JSON: %s' % (path, ex)) from None try: return load_report(report) except ApplicationError as ex: - raise ApplicationError('File "%s" is not an aggregated coverage data file. %s' % (path, ex)) + raise ApplicationError('File "%s" is not an aggregated coverage data file. %s' % (path, ex)) from None def write_report(args: CoverageAnalyzeTargetsConfig, report: dict[str, t.Any], path: str) -> None: diff --git a/test/lib/ansible_test/_internal/commands/coverage/combine.py b/test/lib/ansible_test/_internal/commands/coverage/combine.py index 12cb54e2..fdeac838 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/combine.py +++ b/test/lib/ansible_test/_internal/commands/coverage/combine.py @@ -121,7 +121,7 @@ def _command_coverage_combine_python(args: CoverageCombineConfig, host_state: Ho coverage_files = get_python_coverage_files() def _default_stub_value(source_paths: list[str]) -> dict[str, set[tuple[int, int]]]: - return {path: set() for path in source_paths} + return {path: {(0, 0)} for path in source_paths} counter = 0 sources = _get_coverage_targets(args, walk_compile_targets) diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py b/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py index e8020ca9..136c5331 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py @@ -8,7 +8,6 @@ from ....config import ( ) from ....containers import ( - CleanupMode, run_support_container, ) @@ -22,8 +21,6 @@ from . import ( class ACMEProvider(CloudProvider): """ACME plugin. Sets up cloud resources for tests.""" - DOCKER_SIMULATOR_NAME = 'acme-simulator' - def __init__(self, args: IntegrationConfig) -> None: super().__init__(args) @@ -51,17 +48,18 @@ class ACMEProvider(CloudProvider): 14000, # Pebble ACME CA ] - run_support_container( + descriptor = run_support_container( self.args, self.platform, self.image, - self.DOCKER_SIMULATOR_NAME, + 'acme-simulator', ports, - allow_existing=True, - cleanup=CleanupMode.YES, ) - self._set_cloud_config('acme_host', self.DOCKER_SIMULATOR_NAME) + if not descriptor: + return + + self._set_cloud_config('acme_host', descriptor.name) def _setup_static(self) -> None: raise NotImplementedError() diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py b/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py index 8588df7d..8060804a 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py @@ -21,7 +21,6 @@ from ....docker_util import ( ) from ....containers import ( - CleanupMode, run_support_container, wait_for_file, ) @@ -36,12 +35,10 @@ from . import ( class CsCloudProvider(CloudProvider): """CloudStack cloud provider plugin. Sets up cloud resources before delegation.""" - DOCKER_SIMULATOR_NAME = 'cloudstack-sim' - def __init__(self, args: IntegrationConfig) -> None: super().__init__(args) - self.image = os.environ.get('ANSIBLE_CLOUDSTACK_CONTAINER', 'quay.io/ansible/cloudstack-test-container:1.4.0') + self.image = os.environ.get('ANSIBLE_CLOUDSTACK_CONTAINER', 'quay.io/ansible/cloudstack-test-container:1.6.1') self.host = '' self.port = 0 @@ -96,10 +93,8 @@ class CsCloudProvider(CloudProvider): self.args, self.platform, self.image, - self.DOCKER_SIMULATOR_NAME, + 'cloudstack-sim', ports, - allow_existing=True, - cleanup=CleanupMode.YES, ) if not descriptor: @@ -107,7 +102,7 @@ class CsCloudProvider(CloudProvider): # apply work-around for OverlayFS issue # https://github.com/docker/for-linux/issues/72#issuecomment-319904698 - docker_exec(self.args, self.DOCKER_SIMULATOR_NAME, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';'], capture=True) + docker_exec(self.args, descriptor.name, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';'], capture=True) if self.args.explain: values = dict( @@ -115,10 +110,10 @@ class CsCloudProvider(CloudProvider): PORT=str(self.port), ) else: - credentials = self._get_credentials(self.DOCKER_SIMULATOR_NAME) + credentials = self._get_credentials(descriptor.name) values = dict( - HOST=self.DOCKER_SIMULATOR_NAME, + HOST=descriptor.name, PORT=str(self.port), KEY=credentials['apikey'], SECRET=credentials['secretkey'], diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py b/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py index 1391cd84..f7053c8b 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py @@ -10,12 +10,21 @@ from ....config import ( from ....docker_util import ( docker_cp_to, + docker_exec, ) from ....containers import ( run_support_container, ) +from ....encoding import ( + to_text, +) + +from ....util import ( + display, +) + from . import ( CloudEnvironment, CloudEnvironmentConfig, @@ -23,53 +32,59 @@ from . import ( ) -# We add BasicAuthentication, to make the tasks that deal with -# direct API access easier to deal with across galaxy_ng and pulp -SETTINGS = b''' -CONTENT_ORIGIN = 'http://ansible-ci-pulp:80' -ANSIBLE_API_HOSTNAME = 'http://ansible-ci-pulp:80' -ANSIBLE_CONTENT_HOSTNAME = 'http://ansible-ci-pulp:80/pulp/content' -TOKEN_AUTH_DISABLED = True -GALAXY_REQUIRE_CONTENT_APPROVAL = False -GALAXY_AUTHENTICATION_CLASSES = [ - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.BasicAuthentication", -] -''' - -SET_ADMIN_PASSWORD = b'''#!/usr/bin/execlineb -S0 -foreground { - redirfd -w 1 /dev/null - redirfd -w 2 /dev/null - export DJANGO_SETTINGS_MODULE pulpcore.app.settings - export PULP_CONTENT_ORIGIN localhost - s6-setuidgid postgres - if { /usr/local/bin/django-admin reset-admin-password --password password } - if { /usr/local/bin/pulpcore-manager create-group system:partner-engineers --users admin } -} -''' - -# There are 2 overrides here: -# 1. Change the gunicorn bind address from 127.0.0.1 to 0.0.0.0 now that Galaxy NG does not allow us to access the -# Pulp API through it. -# 2. Grant access allowing us to DELETE a namespace in Galaxy NG. This is as CI deletes and recreates repos and -# distributions in Pulp which now breaks the namespace in Galaxy NG. Recreating it is the "simple" fix to get it -# working again. -# These may not be needed in the future, especially if 1 becomes configurable by an env var but for now they must be -# done. -OVERRIDES = b'''#!/usr/bin/execlineb -S0 -foreground { - sed -i "0,/\\"127.0.0.1:24817\\"/s//\\"0.0.0.0:24817\\"/" /etc/services.d/pulpcore-api/run +GALAXY_HOST_NAME = 'galaxy-pulp' +SETTINGS = { + 'PULP_CONTENT_ORIGIN': f'http://{GALAXY_HOST_NAME}', + 'PULP_ANSIBLE_API_HOSTNAME': f'http://{GALAXY_HOST_NAME}', + 'PULP_GALAXY_API_PATH_PREFIX': '/api/galaxy/', + # These paths are unique to the container image which has an nginx location for /pulp/content to route + # requests to the content backend + 'PULP_ANSIBLE_CONTENT_HOSTNAME': f'http://{GALAXY_HOST_NAME}/pulp/content/api/galaxy/v3/artifacts/collections/', + 'PULP_CONTENT_PATH_PREFIX': '/pulp/content/api/galaxy/v3/artifacts/collections/', + 'PULP_GALAXY_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'django.contrib.auth.backends.ModelBackend', + ], + # This should probably be false see https://issues.redhat.com/browse/AAH-2328 + 'PULP_GALAXY_REQUIRE_CONTENT_APPROVAL': 'true', + 'PULP_GALAXY_DEPLOYMENT_MODE': 'standalone', + 'PULP_GALAXY_AUTO_SIGN_COLLECTIONS': 'false', + 'PULP_GALAXY_COLLECTION_SIGNING_SERVICE': 'ansible-default', + 'PULP_RH_ENTITLEMENT_REQUIRED': 'insights', + 'PULP_TOKEN_AUTH_DISABLED': 'false', + 'PULP_TOKEN_SERVER': f'http://{GALAXY_HOST_NAME}/token/', + 'PULP_TOKEN_SIGNATURE_ALGORITHM': 'ES256', + 'PULP_PUBLIC_KEY_PATH': '/src/galaxy_ng/dev/common/container_auth_public_key.pem', + 'PULP_PRIVATE_KEY_PATH': '/src/galaxy_ng/dev/common/container_auth_private_key.pem', + 'PULP_ANALYTICS': 'false', + 'PULP_GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_ACCESS': 'true', + 'PULP_GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_DOWNLOAD': 'true', + 'PULP_GALAXY_ENABLE_LEGACY_ROLES': 'true', + 'PULP_GALAXY_FEATURE_FLAGS__execution_environments': 'false', + 'PULP_SOCIAL_AUTH_LOGIN_REDIRECT_URL': '/', + 'PULP_GALAXY_FEATURE_FLAGS__ai_deny_index': 'true', + 'PULP_DEFAULT_ADMIN_PASSWORD': 'password' } -# This sed calls changes the first occurrence to "allow" which is conveniently the delete operation for a namespace. -# https://github.com/ansible/galaxy_ng/blob/master/galaxy_ng/app/access_control/statements/standalone.py#L9-L11. -backtick NG_PREFIX { python -c "import galaxy_ng; print(galaxy_ng.__path__[0], end='')" } -importas ng_prefix NG_PREFIX -foreground { - sed -i "0,/\\"effect\\": \\"deny\\"/s//\\"effect\\": \\"allow\\"/" ${ng_prefix}/app/access_control/statements/standalone.py -}''' + +GALAXY_IMPORTER = b''' +[galaxy-importer] +ansible_local_tmp=~/.ansible/tmp +ansible_test_local_image=false +check_required_tags=false +check_runtime_yaml=false +check_changelog=false +infra_osd=false +local_image_docker=false +log_level_main=INFO +require_v1_or_greater=false +run_ansible_doc=false +run_ansible_lint=false +run_ansible_test=false +run_flake8=false +'''.strip() class GalaxyProvider(CloudProvider): @@ -81,13 +96,9 @@ class GalaxyProvider(CloudProvider): def __init__(self, args: IntegrationConfig) -> None: super().__init__(args) - # Cannot use the latest container image as either galaxy_ng 4.2.0rc2 or pulp 0.5.0 has sporatic issues with - # dropping published collections in CI. Try running the tests multiple times when updating. Will also need to - # comment out the cache tests in 'test/integration/targets/ansible-galaxy-collection/tasks/install.yml' when - # the newer update is available. - self.pulp = os.environ.get( + self.image = os.environ.get( 'ANSIBLE_PULP_CONTAINER', - 'quay.io/ansible/pulp-galaxy-ng:b79a7be64eff' + 'quay.io/pulp/galaxy:4.7.1' ) self.uses_docker = True @@ -96,48 +107,46 @@ class GalaxyProvider(CloudProvider): """Setup cloud resource before delegation and reg cleanup callback.""" super().setup() - galaxy_port = 80 - pulp_host = 'ansible-ci-pulp' - pulp_port = 24817 - - ports = [ - galaxy_port, - pulp_port, - ] - - # Create the container, don't run it, we need to inject configs before it starts - descriptor = run_support_container( - self.args, - self.platform, - self.pulp, - pulp_host, - ports, - start=False, - allow_existing=True, - ) + with tempfile.NamedTemporaryFile(mode='w+') as env_fd: + settings = '\n'.join( + f'{key}={value}' for key, value in SETTINGS.items() + ) + env_fd.write(settings) + env_fd.flush() + display.info(f'>>> galaxy_ng Configuration\n{settings}', verbosity=3) + descriptor = run_support_container( + self.args, + self.platform, + self.image, + GALAXY_HOST_NAME, + [ + 80, + ], + aliases=[ + GALAXY_HOST_NAME, + ], + start=True, + options=[ + '--env-file', env_fd.name, + ], + ) if not descriptor: return - if not descriptor.running: - pulp_id = descriptor.container_id - - injected_files = { - '/etc/pulp/settings.py': SETTINGS, - '/etc/cont-init.d/111-postgres': SET_ADMIN_PASSWORD, - '/etc/cont-init.d/000-ansible-test-overrides': OVERRIDES, - } - for path, content in injected_files.items(): - with tempfile.NamedTemporaryFile() as temp_fd: - temp_fd.write(content) - temp_fd.flush() - docker_cp_to(self.args, pulp_id, temp_fd.name, path) - - descriptor.start(self.args) - - self._set_cloud_config('PULP_HOST', pulp_host) - self._set_cloud_config('PULP_PORT', str(pulp_port)) - self._set_cloud_config('GALAXY_PORT', str(galaxy_port)) + injected_files = [ + ('/etc/galaxy-importer/galaxy-importer.cfg', GALAXY_IMPORTER, 'galaxy-importer'), + ] + for path, content, friendly_name in injected_files: + with tempfile.NamedTemporaryFile() as temp_fd: + temp_fd.write(content) + temp_fd.flush() + display.info(f'>>> {friendly_name} Configuration\n{to_text(content)}', verbosity=3) + docker_exec(self.args, descriptor.container_id, ['mkdir', '-p', os.path.dirname(path)], True) + docker_cp_to(self.args, descriptor.container_id, temp_fd.name, path) + docker_exec(self.args, descriptor.container_id, ['chown', 'pulp:pulp', path], True) + + self._set_cloud_config('PULP_HOST', GALAXY_HOST_NAME) self._set_cloud_config('PULP_USER', 'admin') self._set_cloud_config('PULP_PASSWORD', 'password') @@ -150,21 +159,19 @@ class GalaxyEnvironment(CloudEnvironment): pulp_user = str(self._get_cloud_config('PULP_USER')) pulp_password = str(self._get_cloud_config('PULP_PASSWORD')) pulp_host = self._get_cloud_config('PULP_HOST') - galaxy_port = self._get_cloud_config('GALAXY_PORT') - pulp_port = self._get_cloud_config('PULP_PORT') return CloudEnvironmentConfig( ansible_vars=dict( pulp_user=pulp_user, pulp_password=pulp_password, - pulp_api='http://%s:%s' % (pulp_host, pulp_port), - pulp_server='http://%s:%s/pulp_ansible/galaxy/' % (pulp_host, pulp_port), - galaxy_ng_server='http://%s:%s/api/galaxy/' % (pulp_host, galaxy_port), + pulp_api=f'http://{pulp_host}', + pulp_server=f'http://{pulp_host}/pulp_ansible/galaxy/', + galaxy_ng_server=f'http://{pulp_host}/api/galaxy/', ), env_vars=dict( PULP_USER=pulp_user, PULP_PASSWORD=pulp_password, - PULP_SERVER='http://%s:%s/pulp_ansible/galaxy/api/' % (pulp_host, pulp_port), - GALAXY_NG_SERVER='http://%s:%s/api/galaxy/' % (pulp_host, galaxy_port), + PULP_SERVER=f'http://{pulp_host}/pulp_ansible/galaxy/api/', + GALAXY_NG_SERVER=f'http://{pulp_host}/api/galaxy/', ), ) diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py b/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py index 85065d6f..b3cf2d49 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py @@ -13,7 +13,6 @@ from ....config import ( ) from ....containers import ( - CleanupMode, run_support_container, ) @@ -62,8 +61,6 @@ class HttptesterProvider(CloudProvider): 'http-test-container', ports, aliases=aliases, - allow_existing=True, - cleanup=CleanupMode.YES, env={ KRB5_PASSWORD_ENV: generate_password(), }, diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py b/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py index 5bed8340..62dd1558 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py @@ -8,7 +8,6 @@ from ....config import ( ) from ....containers import ( - CleanupMode, run_support_container, ) @@ -22,8 +21,6 @@ from . import ( class NiosProvider(CloudProvider): """Nios plugin. Sets up NIOS mock server for tests.""" - DOCKER_SIMULATOR_NAME = 'nios-simulator' - # Default image to run the nios simulator. # # The simulator must be pinned to a specific version @@ -31,7 +28,7 @@ class NiosProvider(CloudProvider): # # It's source source itself resides at: # https://github.com/ansible/nios-test-container - DOCKER_IMAGE = 'quay.io/ansible/nios-test-container:1.4.0' + DOCKER_IMAGE = 'quay.io/ansible/nios-test-container:2.0.0' def __init__(self, args: IntegrationConfig) -> None: super().__init__(args) @@ -65,17 +62,18 @@ class NiosProvider(CloudProvider): nios_port, ] - run_support_container( + descriptor = run_support_container( self.args, self.platform, self.image, - self.DOCKER_SIMULATOR_NAME, + 'nios-simulator', ports, - allow_existing=True, - cleanup=CleanupMode.YES, ) - self._set_cloud_config('NIOS_HOST', self.DOCKER_SIMULATOR_NAME) + if not descriptor: + return + + self._set_cloud_config('NIOS_HOST', descriptor.name) def _setup_static(self) -> None: raise NotImplementedError() diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py b/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py index ddd434a8..6e8a5e4f 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py @@ -16,7 +16,6 @@ from ....config import ( ) from ....containers import ( - CleanupMode, run_support_container, wait_for_file, ) @@ -31,8 +30,6 @@ from . import ( class OpenShiftCloudProvider(CloudProvider): """OpenShift cloud provider plugin. Sets up cloud resources before delegation.""" - DOCKER_CONTAINER_NAME = 'openshift-origin' - def __init__(self, args: IntegrationConfig) -> None: super().__init__(args, config_extension='.kubeconfig') @@ -74,10 +71,8 @@ class OpenShiftCloudProvider(CloudProvider): self.args, self.platform, self.image, - self.DOCKER_CONTAINER_NAME, + 'openshift-origin', ports, - allow_existing=True, - cleanup=CleanupMode.YES, cmd=cmd, ) @@ -87,7 +82,7 @@ class OpenShiftCloudProvider(CloudProvider): if self.args.explain: config = '# Unknown' else: - config = self._get_config(self.DOCKER_CONTAINER_NAME, 'https://%s:%s/' % (self.DOCKER_CONTAINER_NAME, port)) + config = self._get_config(descriptor.name, 'https://%s:%s/' % (descriptor.name, port)) self._write_config(config) diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py b/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py index 242b0204..b0ff7fe3 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py @@ -2,7 +2,6 @@ from __future__ import annotations import configparser -import os from ....util import ( ApplicationError, @@ -13,11 +12,6 @@ from ....config import ( IntegrationConfig, ) -from ....containers import ( - CleanupMode, - run_support_container, -) - from . import ( CloudEnvironment, CloudEnvironmentConfig, @@ -28,66 +22,16 @@ from . import ( class VcenterProvider(CloudProvider): """VMware vcenter/esx plugin. Sets up cloud resources for tests.""" - DOCKER_SIMULATOR_NAME = 'vcenter-simulator' - def __init__(self, args: IntegrationConfig) -> None: super().__init__(args) - # The simulator must be pinned to a specific version to guarantee CI passes with the version used. - if os.environ.get('ANSIBLE_VCSIM_CONTAINER'): - self.image = os.environ.get('ANSIBLE_VCSIM_CONTAINER') - else: - self.image = 'quay.io/ansible/vcenter-test-container:1.7.0' - - # VMware tests can be run on govcsim or BYO with a static config file. - # The simulator is the default if no config is provided. - self.vmware_test_platform = os.environ.get('VMWARE_TEST_PLATFORM', 'govcsim') - - if self.vmware_test_platform == 'govcsim': - self.uses_docker = True - self.uses_config = False - elif self.vmware_test_platform == 'static': - self.uses_docker = False - self.uses_config = True + self.uses_config = True def setup(self) -> None: """Setup the cloud resource before delegation and register a cleanup callback.""" super().setup() - self._set_cloud_config('vmware_test_platform', self.vmware_test_platform) - - if self.vmware_test_platform == 'govcsim': - self._setup_dynamic_simulator() - self.managed = True - elif self.vmware_test_platform == 'static': - self._use_static_config() - self._setup_static() - else: - raise ApplicationError('Unknown vmware_test_platform: %s' % self.vmware_test_platform) - - def _setup_dynamic_simulator(self) -> None: - """Create a vcenter simulator using docker.""" - ports = [ - 443, - 8080, - 8989, - 5000, # control port for flask app in simulator - ] - - run_support_container( - self.args, - self.platform, - self.image, - self.DOCKER_SIMULATOR_NAME, - ports, - allow_existing=True, - cleanup=CleanupMode.YES, - ) - - self._set_cloud_config('vcenter_hostname', self.DOCKER_SIMULATOR_NAME) - - def _setup_static(self) -> None: - if not os.path.exists(self.config_static_path): + if not self._use_static_config(): raise ApplicationError('Configuration file does not exist: %s' % self.config_static_path) @@ -96,37 +40,21 @@ class VcenterEnvironment(CloudEnvironment): def get_environment_config(self) -> CloudEnvironmentConfig: """Return environment configuration for use in the test environment after delegation.""" - try: - # We may be in a container, so we cannot just reach VMWARE_TEST_PLATFORM, - # We do a try/except instead - parser = configparser.ConfigParser() - parser.read(self.config_path) # static - - env_vars = {} - ansible_vars = dict( - resource_prefix=self.resource_prefix, - ) - ansible_vars.update(dict(parser.items('DEFAULT', raw=True))) - except KeyError: # govcsim - env_vars = dict( - VCENTER_HOSTNAME=str(self._get_cloud_config('vcenter_hostname')), - VCENTER_USERNAME='user', - VCENTER_PASSWORD='pass', - ) - - ansible_vars = dict( - vcsim=str(self._get_cloud_config('vcenter_hostname')), - vcenter_hostname=str(self._get_cloud_config('vcenter_hostname')), - vcenter_username='user', - vcenter_password='pass', - ) + # We may be in a container, so we cannot just reach VMWARE_TEST_PLATFORM, + # We do a try/except instead + parser = configparser.ConfigParser() + parser.read(self.config_path) # static + + ansible_vars = dict( + resource_prefix=self.resource_prefix, + ) + ansible_vars.update(dict(parser.items('DEFAULT', raw=True))) for key, value in ansible_vars.items(): if key.endswith('_password'): display.sensitive.add(value) return CloudEnvironmentConfig( - env_vars=env_vars, ansible_vars=ansible_vars, module_defaults={ 'group/vmware': { diff --git a/test/lib/ansible_test/_internal/commands/sanity/__init__.py b/test/lib/ansible_test/_internal/commands/sanity/__init__.py index 0bc68a21..9b675e4a 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/__init__.py +++ b/test/lib/ansible_test/_internal/commands/sanity/__init__.py @@ -127,9 +127,13 @@ TARGET_SANITY_ROOT = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'sanity') # NOTE: must match ansible.constants.DOCUMENTABLE_PLUGINS, but with 'module' replaced by 'modules'! DOCUMENTABLE_PLUGINS = ( - 'become', 'cache', 'callback', 'cliconf', 'connection', 'httpapi', 'inventory', 'lookup', 'netconf', 'modules', 'shell', 'strategy', 'vars' + 'become', 'cache', 'callback', 'cliconf', 'connection', 'filter', 'httpapi', 'inventory', + 'lookup', 'netconf', 'modules', 'shell', 'strategy', 'test', 'vars', ) +# Plugin types that can have multiple plugins per file, and where filenames not always correspond to plugin names +MULTI_FILE_PLUGINS = ('filter', 'test', ) + created_venvs: list[str] = [] @@ -260,7 +264,7 @@ def command_sanity(args: SanityConfig) -> None: virtualenv_python = create_sanity_virtualenv(args, test_profile.python, test.name) if virtualenv_python: - virtualenv_yaml = check_sanity_virtualenv_yaml(virtualenv_python) + virtualenv_yaml = args.explain or check_sanity_virtualenv_yaml(virtualenv_python) if test.require_libyaml and not virtualenv_yaml: result = SanitySkipped(test.name) @@ -875,6 +879,7 @@ class SanityCodeSmellTest(SanitySingleVersion): self.__include_directories: bool = self.config.get('include_directories') self.__include_symlinks: bool = self.config.get('include_symlinks') self.__py2_compat: bool = self.config.get('py2_compat', False) + self.__error_code: str | None = self.config.get('error_code', None) else: self.output = None self.extensions = [] @@ -890,6 +895,7 @@ class SanityCodeSmellTest(SanitySingleVersion): self.__include_directories = False self.__include_symlinks = False self.__py2_compat = False + self.__error_code = None if self.no_targets: mutually_exclusive = ( @@ -909,6 +915,11 @@ class SanityCodeSmellTest(SanitySingleVersion): raise ApplicationError('Sanity test "%s" option "no_targets" is mutually exclusive with options: %s' % (self.name, ', '.join(problems))) @property + def error_code(self) -> t.Optional[str]: + """Error code for ansible-test matching the format used by the underlying test program, or None if the program does not use error codes.""" + return self.__error_code + + @property def all_targets(self) -> bool: """True if test targets will not be filtered using includes, excludes, requires or changes. Mutually exclusive with no_targets.""" return self.__all_targets @@ -992,6 +1003,8 @@ class SanityCodeSmellTest(SanitySingleVersion): pattern = '^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<message>.*)$' elif self.output == 'path-message': pattern = '^(?P<path>[^:]*): (?P<message>.*)$' + elif self.output == 'path-line-column-code-message': + pattern = '^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<code>[^:]*): (?P<message>.*)$' else: raise ApplicationError('Unsupported output type: %s' % self.output) @@ -1021,6 +1034,7 @@ class SanityCodeSmellTest(SanitySingleVersion): path=m['path'], line=int(m.get('line', 0)), column=int(m.get('column', 0)), + code=m.get('code'), ) for m in matches] messages = settings.process_errors(messages, paths) @@ -1166,20 +1180,23 @@ def create_sanity_virtualenv( run_pip(args, virtualenv_python, commands, None) # create_sanity_virtualenv() - write_text_file(meta_install, virtualenv_install) + if not args.explain: + write_text_file(meta_install, virtualenv_install) # false positive: pylint: disable=no-member if any(isinstance(command, PipInstall) and command.has_package('pyyaml') for command in commands): - virtualenv_yaml = yamlcheck(virtualenv_python) + virtualenv_yaml = yamlcheck(virtualenv_python, args.explain) else: virtualenv_yaml = None - write_json_file(meta_yaml, virtualenv_yaml) + if not args.explain: + write_json_file(meta_yaml, virtualenv_yaml) created_venvs.append(f'{label}-{python.version}') - # touch the marker to keep track of when the virtualenv was last used - pathlib.Path(virtualenv_marker).touch() + if not args.explain: + # touch the marker to keep track of when the virtualenv was last used + pathlib.Path(virtualenv_marker).touch() return virtualenv_python diff --git a/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py b/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py index 04080f60..ff035ef9 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py +++ b/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py @@ -2,11 +2,13 @@ from __future__ import annotations import collections +import json import os import re from . import ( DOCUMENTABLE_PLUGINS, + MULTI_FILE_PLUGINS, SanitySingleVersion, SanityFailure, SanitySuccess, @@ -85,6 +87,44 @@ class AnsibleDocTest(SanitySingleVersion): doc_targets[plugin_type].append(plugin_fqcn) env = ansible_environment(args, color=False) + + for doc_type in MULTI_FILE_PLUGINS: + if doc_targets.get(doc_type): + # List plugins + cmd = ['ansible-doc', '-l', '--json', '-t', doc_type] + prefix = data_context().content.prefix if data_context().content.collection else 'ansible.builtin.' + cmd.append(prefix[:-1]) + try: + stdout, stderr = intercept_python(args, python, cmd, env, capture=True) + status = 0 + except SubprocessError as ex: + stdout = ex.stdout + stderr = ex.stderr + status = ex.status + + if status: + summary = '%s' % SubprocessError(cmd=cmd, status=status, stderr=stderr) + return SanityFailure(self.name, summary=summary) + + if stdout: + display.info(stdout.strip(), verbosity=3) + + if stderr: + summary = 'Output on stderr from ansible-doc is considered an error.\n\n%s' % SubprocessError(cmd, stderr=stderr) + return SanityFailure(self.name, summary=summary) + + if args.explain: + continue + + plugin_list_json = json.loads(stdout) + doc_targets[doc_type] = [] + for plugin_name, plugin_value in sorted(plugin_list_json.items()): + if plugin_value != 'UNDOCUMENTED': + doc_targets[doc_type].append(plugin_name) + + if not doc_targets[doc_type]: + del doc_targets[doc_type] + error_messages: list[SanityMessage] = [] for doc_type in sorted(doc_targets): diff --git a/test/lib/ansible_test/_internal/commands/sanity/import.py b/test/lib/ansible_test/_internal/commands/sanity/import.py index b8083324..36f52415 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/import.py +++ b/test/lib/ansible_test/_internal/commands/sanity/import.py @@ -127,20 +127,26 @@ class ImportTest(SanityMultipleVersion): ('plugin', _get_module_test(False)), ): if import_type == 'plugin' and python.version in REMOTE_ONLY_PYTHON_VERSIONS: - continue + # Plugins are not supported on remote-only Python versions. + # However, the collection loader is used by the import sanity test and unit tests on remote-only Python versions. + # To support this, it is tested as a plugin, but using a venv which installs no requirements. + # Filtering of paths relevant to the Python version tested has already been performed by filter_remote_targets. + venv_type = 'empty' + else: + venv_type = import_type data = '\n'.join([path for path in paths if test(path)]) if not data and not args.prime_venvs: continue - virtualenv_python = create_sanity_virtualenv(args, python, f'{self.name}.{import_type}', coverage=args.coverage, minimize=True) + virtualenv_python = create_sanity_virtualenv(args, python, f'{self.name}.{venv_type}', coverage=args.coverage, minimize=True) if not virtualenv_python: display.warning(f'Skipping sanity test "{self.name}" on Python {python.version} due to missing virtual environment support.') return SanitySkipped(self.name, python.version) - virtualenv_yaml = check_sanity_virtualenv_yaml(virtualenv_python) + virtualenv_yaml = args.explain or check_sanity_virtualenv_yaml(virtualenv_python) if virtualenv_yaml is False: display.warning(f'Sanity test "{self.name}" ({import_type}) on Python {python.version} may be slow due to missing libyaml support in PyYAML.') diff --git a/test/lib/ansible_test/_internal/commands/sanity/mypy.py b/test/lib/ansible_test/_internal/commands/sanity/mypy.py index 57ce1277..c93474e8 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/mypy.py +++ b/test/lib/ansible_test/_internal/commands/sanity/mypy.py @@ -19,6 +19,7 @@ from . import ( from ...constants import ( CONTROLLER_PYTHON_VERSIONS, REMOTE_ONLY_PYTHON_VERSIONS, + SUPPORTED_PYTHON_VERSIONS, ) from ...test import ( @@ -36,6 +37,7 @@ from ...util import ( ANSIBLE_TEST_CONTROLLER_ROOT, ApplicationError, is_subdir, + str_to_version, ) from ...util_common import ( @@ -71,9 +73,19 @@ class MypyTest(SanityMultipleVersion): """Return the given list of test targets, filtered to include only those relevant for the test.""" return [target for target in targets if os.path.splitext(target.path)[1] == '.py' and target.path not in self.vendored_paths and ( target.path.startswith('lib/ansible/') or target.path.startswith('test/lib/ansible_test/_internal/') + or target.path.startswith('packaging/') or target.path.startswith('test/lib/ansible_test/_util/target/sanity/import/'))] @property + def supported_python_versions(self) -> t.Optional[tuple[str, ...]]: + """A tuple of supported Python versions or None if the test does not depend on specific Python versions.""" + # mypy 0.981 dropped support for Python 2 + # see: https://mypy-lang.blogspot.com/2022/09/mypy-0981-released.html + # cryptography dropped support for Python 3.5 in version 3.3 + # see: https://cryptography.io/en/latest/changelog/#v3-3 + return tuple(version for version in SUPPORTED_PYTHON_VERSIONS if str_to_version(version) >= (3, 6)) + + @property def error_code(self) -> t.Optional[str]: """Error code for ansible-test matching the format used by the underlying test program, or None if the program does not use error codes.""" return 'ansible-test' @@ -105,6 +117,7 @@ class MypyTest(SanityMultipleVersion): MyPyContext('ansible-test', ['test/lib/ansible_test/_internal/'], controller_python_versions), MyPyContext('ansible-core', ['lib/ansible/'], controller_python_versions), MyPyContext('modules', ['lib/ansible/modules/', 'lib/ansible/module_utils/'], remote_only_python_versions), + MyPyContext('packaging', ['packaging/'], controller_python_versions), ) unfiltered_messages: list[SanityMessage] = [] @@ -157,6 +170,9 @@ class MypyTest(SanityMultipleVersion): # However, it will also report issues on those files, which is not the desired behavior. messages = [message for message in messages if message.path in paths_set] + if args.explain: + return SanitySuccess(self.name, python_version=python.version) + results = settings.process_errors(messages, paths) if results: @@ -239,7 +255,7 @@ class MypyTest(SanityMultipleVersion): pattern = r'^(?P<path>[^:]*):(?P<line>[0-9]+):((?P<column>[0-9]+):)? (?P<level>[^:]+): (?P<message>.*)$' - parsed = parse_to_list_of_dict(pattern, stdout) + parsed = parse_to_list_of_dict(pattern, stdout or '') messages = [SanityMessage( level=r['level'], diff --git a/test/lib/ansible_test/_internal/commands/sanity/pylint.py b/test/lib/ansible_test/_internal/commands/sanity/pylint.py index c089f834..54b1952f 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/pylint.py +++ b/test/lib/ansible_test/_internal/commands/sanity/pylint.py @@ -18,6 +18,11 @@ from . import ( SANITY_ROOT, ) +from ...constants import ( + CONTROLLER_PYTHON_VERSIONS, + REMOTE_ONLY_PYTHON_VERSIONS, +) + from ...io import ( make_dirs, ) @@ -38,6 +43,7 @@ from ...util import ( from ...util_common import ( run_command, + process_scoped_temporary_file, ) from ...ansible_util import ( @@ -81,6 +87,8 @@ class PylintTest(SanitySingleVersion): return [target for target in targets if os.path.splitext(target.path)[1] == '.py' or is_subdir(target.path, 'bin')] def test(self, args: SanityConfig, targets: SanityTargets, python: PythonConfig) -> TestResult: + min_python_version_db_path = self.create_min_python_db(args, targets.targets) + plugin_dir = os.path.join(SANITY_ROOT, 'pylint', 'plugins') plugin_names = sorted(p[0] for p in [ os.path.splitext(p) for p in os.listdir(plugin_dir)] if p[1] == '.py' and p[0] != '__init__') @@ -163,7 +171,7 @@ class PylintTest(SanitySingleVersion): continue context_start = datetime.datetime.now(tz=datetime.timezone.utc) - messages += self.pylint(args, context, context_paths, plugin_dir, plugin_names, python, collection_detail) + messages += self.pylint(args, context, context_paths, plugin_dir, plugin_names, python, collection_detail, min_python_version_db_path) context_end = datetime.datetime.now(tz=datetime.timezone.utc) context_times.append('%s: %d (%s)' % (context, len(context_paths), context_end - context_start)) @@ -194,6 +202,22 @@ class PylintTest(SanitySingleVersion): return SanitySuccess(self.name) + def create_min_python_db(self, args: SanityConfig, targets: t.Iterable[TestTarget]) -> str: + """Create a database of target file paths and their minimum required Python version, returning the path to the database.""" + target_paths = set(target.path for target in self.filter_remote_targets(list(targets))) + controller_min_version = CONTROLLER_PYTHON_VERSIONS[0] + target_min_version = REMOTE_ONLY_PYTHON_VERSIONS[0] + min_python_versions = { + os.path.abspath(target.path): target_min_version if target.path in target_paths else controller_min_version for target in targets + } + + min_python_version_db_path = process_scoped_temporary_file(args) + + with open(min_python_version_db_path, 'w') as database_file: + json.dump(min_python_versions, database_file) + + return min_python_version_db_path + @staticmethod def pylint( args: SanityConfig, @@ -203,6 +227,7 @@ class PylintTest(SanitySingleVersion): plugin_names: list[str], python: PythonConfig, collection_detail: CollectionDetail, + min_python_version_db_path: str, ) -> list[dict[str, str]]: """Run pylint using the config specified by the context on the specified paths.""" rcfile = os.path.join(SANITY_ROOT, 'pylint', 'config', context.split('/')[0] + '.cfg') @@ -234,6 +259,7 @@ class PylintTest(SanitySingleVersion): '--rcfile', rcfile, '--output-format', 'json', '--load-plugins', ','.join(sorted(load_plugins)), + '--min-python-version-db', min_python_version_db_path, ] + paths # fmt: skip if data_context().content.collection: diff --git a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py index 3153bc99..e29b5dec 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py +++ b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py @@ -10,6 +10,7 @@ import typing as t from . import ( DOCUMENTABLE_PLUGINS, + MULTI_FILE_PLUGINS, SanitySingleVersion, SanityMessage, SanityFailure, @@ -128,6 +129,10 @@ class ValidateModulesTest(SanitySingleVersion): for target in targets.include: target_per_type[self.get_plugin_type(target)].append(target) + # Remove plugins that cannot be associated to a single file (test and filter plugins). + for plugin_type in MULTI_FILE_PLUGINS: + target_per_type.pop(plugin_type, None) + cmd = [ python.path, os.path.join(SANITY_ROOT, 'validate-modules', 'validate.py'), diff --git a/test/lib/ansible_test/_internal/commands/units/__init__.py b/test/lib/ansible_test/_internal/commands/units/__init__.py index 7d192e1b..71ce5c4d 100644 --- a/test/lib/ansible_test/_internal/commands/units/__init__.py +++ b/test/lib/ansible_test/_internal/commands/units/__init__.py @@ -253,7 +253,6 @@ def command_units(args: UnitsConfig) -> None: cmd = [ 'pytest', - '--forked', '-r', 'a', '-n', str(args.num_workers) if args.num_workers else 'auto', '--color', 'yes' if args.color else 'no', @@ -262,6 +261,7 @@ def command_units(args: UnitsConfig) -> None: '--junit-xml', os.path.join(ResultType.JUNIT.path, 'python%s-%s-units.xml' % (python.version, test_context)), '--strict-markers', # added in pytest 4.5.0 '--rootdir', data_context().content.root, + '--confcutdir', data_context().content.root, # avoid permission errors when running from an installed version and using pytest >= 8 ] # fmt:skip if not data_context().content.collection: @@ -275,6 +275,8 @@ def command_units(args: UnitsConfig) -> None: if data_context().content.collection: plugins.append('ansible_pytest_collections') + plugins.append('ansible_forked') + if plugins: env['PYTHONPATH'] += ':%s' % os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'pytest/plugins') env['PYTEST_PLUGINS'] = ','.join(plugins) diff --git a/test/lib/ansible_test/_internal/config.py b/test/lib/ansible_test/_internal/config.py index 4e697933..dbc137b5 100644 --- a/test/lib/ansible_test/_internal/config.py +++ b/test/lib/ansible_test/_internal/config.py @@ -8,7 +8,6 @@ import sys import typing as t from .util import ( - display, verify_sys_executable, version_to_str, type_guard, @@ -136,12 +135,6 @@ class EnvironmentConfig(CommonConfig): data_context().register_payload_callback(host_callback) - if args.docker_no_pull: - display.warning('The --docker-no-pull option is deprecated and has no effect. It will be removed in a future version of ansible-test.') - - if args.no_pip_check: - display.warning('The --no-pip-check option is deprecated and has no effect. It will be removed in a future version of ansible-test.') - @property def controller(self) -> ControllerHostConfig: """Host configuration for the controller.""" diff --git a/test/lib/ansible_test/_internal/containers.py b/test/lib/ansible_test/_internal/containers.py index 869f1fba..92a40a48 100644 --- a/test/lib/ansible_test/_internal/containers.py +++ b/test/lib/ansible_test/_internal/containers.py @@ -3,7 +3,6 @@ from __future__ import annotations import collections.abc as c import contextlib -import enum import json import random import time @@ -46,6 +45,7 @@ from .docker_util import ( get_docker_container_id, get_docker_host_ip, get_podman_host_ip, + get_session_container_name, require_docker, detect_host_properties, ) @@ -101,14 +101,6 @@ class HostType: managed = 'managed' -class CleanupMode(enum.Enum): - """How container cleanup should be handled.""" - - YES = enum.auto() - NO = enum.auto() - INFO = enum.auto() - - def run_support_container( args: EnvironmentConfig, context: str, @@ -117,8 +109,7 @@ def run_support_container( ports: list[int], aliases: t.Optional[list[str]] = None, start: bool = True, - allow_existing: bool = False, - cleanup: t.Optional[CleanupMode] = None, + cleanup: bool = True, cmd: t.Optional[list[str]] = None, env: t.Optional[dict[str, str]] = None, options: t.Optional[list[str]] = None, @@ -128,6 +119,8 @@ def run_support_container( Start a container used to support tests, but not run them. Containers created this way will be accessible from tests. """ + name = get_session_container_name(args, name) + if args.prime_containers: docker_pull(args, image) return None @@ -165,46 +158,13 @@ def run_support_container( options.extend(['--ulimit', 'nofile=%s' % max_open_files]) - support_container_id = None - - if allow_existing: - try: - container = docker_inspect(args, name) - except ContainerNotFoundError: - container = None - - if container: - support_container_id = container.id - - if not container.running: - display.info('Ignoring existing "%s" container which is not running.' % name, verbosity=1) - support_container_id = None - elif not container.image: - display.info('Ignoring existing "%s" container which has the wrong image.' % name, verbosity=1) - support_container_id = None - elif publish_ports and not all(port and len(port) == 1 for port in [container.get_tcp_port(port) for port in ports]): - display.info('Ignoring existing "%s" container which does not have the required published ports.' % name, verbosity=1) - support_container_id = None - - if not support_container_id: - docker_rm(args, name) - if args.dev_systemd_debug: options.extend(('--env', 'SYSTEMD_LOG_LEVEL=debug')) - if support_container_id: - display.info('Using existing "%s" container.' % name) - running = True - existing = True - else: - display.info('Starting new "%s" container.' % name) - docker_pull(args, image) - support_container_id = run_container(args, image, name, options, create_only=not start, cmd=cmd) - running = start - existing = False - - if cleanup is None: - cleanup = CleanupMode.INFO if existing else CleanupMode.YES + display.info('Starting new "%s" container.' % name) + docker_pull(args, image) + support_container_id = run_container(args, image, name, options, create_only=not start, cmd=cmd) + running = start descriptor = ContainerDescriptor( image, @@ -215,7 +175,6 @@ def run_support_container( aliases, publish_ports, running, - existing, cleanup, env, ) @@ -694,8 +653,7 @@ class ContainerDescriptor: aliases: list[str], publish_ports: bool, running: bool, - existing: bool, - cleanup: CleanupMode, + cleanup: bool, env: t.Optional[dict[str, str]], ) -> None: self.image = image @@ -706,7 +664,6 @@ class ContainerDescriptor: self.aliases = aliases self.publish_ports = publish_ports self.running = running - self.existing = existing self.cleanup = cleanup self.env = env self.details: t.Optional[SupportContainer] = None @@ -805,10 +762,8 @@ def wait_for_file( def cleanup_containers(args: EnvironmentConfig) -> None: """Clean up containers.""" for container in support_containers.values(): - if container.cleanup == CleanupMode.YES: - docker_rm(args, container.container_id) - elif container.cleanup == CleanupMode.INFO: - display.notice(f'Remember to run `{require_docker().command} rm -f {container.name}` when finished testing.') + if container.cleanup: + docker_rm(args, container.name) def create_hosts_entries(context: dict[str, ContainerAccess]) -> list[str]: diff --git a/test/lib/ansible_test/_internal/core_ci.py b/test/lib/ansible_test/_internal/core_ci.py index 6e44b3d9..77e6753f 100644 --- a/test/lib/ansible_test/_internal/core_ci.py +++ b/test/lib/ansible_test/_internal/core_ci.py @@ -28,7 +28,6 @@ from .io import ( from .util import ( ApplicationError, display, - ANSIBLE_TEST_TARGET_ROOT, mutex, ) @@ -292,18 +291,12 @@ class AnsibleCoreCI: """Start instance.""" display.info(f'Initializing new {self.label} instance using: {self._uri}', verbosity=1) - if self.platform == 'windows': - winrm_config = read_text_file(os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'ConfigureRemotingForAnsible.ps1')) - else: - winrm_config = None - data = dict( config=dict( platform=self.platform, version=self.version, architecture=self.arch, public_key=self.ssh_key.pub_contents, - winrm_config=winrm_config, ) ) diff --git a/test/lib/ansible_test/_internal/coverage_util.py b/test/lib/ansible_test/_internal/coverage_util.py index ae640249..30176236 100644 --- a/test/lib/ansible_test/_internal/coverage_util.py +++ b/test/lib/ansible_test/_internal/coverage_util.py @@ -69,7 +69,8 @@ class CoverageVersion: COVERAGE_VERSIONS = ( # IMPORTANT: Keep this in sync with the ansible-test.txt requirements file. - CoverageVersion('6.5.0', 7, (3, 7), (3, 11)), + CoverageVersion('7.3.2', 7, (3, 8), (3, 12)), + CoverageVersion('6.5.0', 7, (3, 7), (3, 7)), CoverageVersion('4.5.4', 0, (2, 6), (3, 6)), ) """ @@ -250,7 +251,9 @@ def generate_ansible_coverage_config() -> str: coverage_config = ''' [run] branch = True -concurrency = multiprocessing +concurrency = + multiprocessing + thread parallel = True omit = @@ -271,7 +274,9 @@ def generate_collection_coverage_config(args: TestConfig) -> str: coverage_config = ''' [run] branch = True -concurrency = multiprocessing +concurrency = + multiprocessing + thread parallel = True disable_warnings = no-data-collected diff --git a/test/lib/ansible_test/_internal/delegation.py b/test/lib/ansible_test/_internal/delegation.py index f9e54455..84896830 100644 --- a/test/lib/ansible_test/_internal/delegation.py +++ b/test/lib/ansible_test/_internal/delegation.py @@ -328,7 +328,6 @@ def filter_options( ) -> c.Iterable[str]: """Return an iterable that filters out unwanted CLI options and injects new ones as requested.""" replace: list[tuple[str, int, t.Optional[t.Union[bool, str, list[str]]]]] = [ - ('--docker-no-pull', 0, False), ('--truncate', 1, str(args.truncate)), ('--color', 1, 'yes' if args.color else 'no'), ('--redact', 0, False), diff --git a/test/lib/ansible_test/_internal/diff.py b/test/lib/ansible_test/_internal/diff.py index 2ddc2ff9..5a94aafc 100644 --- a/test/lib/ansible_test/_internal/diff.py +++ b/test/lib/ansible_test/_internal/diff.py @@ -143,7 +143,7 @@ class DiffParser: traceback.format_exc(), ) - raise ApplicationError(message.strip()) + raise ApplicationError(message.strip()) from None self.previous_line = self.line diff --git a/test/lib/ansible_test/_internal/docker_util.py b/test/lib/ansible_test/_internal/docker_util.py index 06f383b5..52b9691e 100644 --- a/test/lib/ansible_test/_internal/docker_util.py +++ b/test/lib/ansible_test/_internal/docker_util.py @@ -300,7 +300,7 @@ def detect_host_properties(args: CommonConfig) -> ContainerHostProperties: options = ['--volume', '/sys/fs/cgroup:/probe:ro'] cmd = ['sh', '-c', ' && echo "-" && '.join(multi_line_commands)] - stdout = run_utility_container(args, f'ansible-test-probe-{args.session_name}', cmd, options)[0] + stdout = run_utility_container(args, 'ansible-test-probe', cmd, options)[0] if args.explain: return ContainerHostProperties( @@ -336,7 +336,7 @@ def detect_host_properties(args: CommonConfig) -> ContainerHostProperties: cmd = ['sh', '-c', 'ulimit -Hn'] try: - stdout = run_utility_container(args, f'ansible-test-ulimit-{args.session_name}', cmd, options)[0] + stdout = run_utility_container(args, 'ansible-test-ulimit', cmd, options)[0] except SubprocessError as ex: display.warning(str(ex)) else: @@ -402,6 +402,11 @@ def detect_host_properties(args: CommonConfig) -> ContainerHostProperties: return properties +def get_session_container_name(args: CommonConfig, name: str) -> str: + """Return the given container name with the current test session name applied to it.""" + return f'{name}-{args.session_name}' + + def run_utility_container( args: CommonConfig, name: str, @@ -410,6 +415,8 @@ def run_utility_container( data: t.Optional[str] = None, ) -> tuple[t.Optional[str], t.Optional[str]]: """Run the specified command using the ansible-test utility container, returning stdout and stderr.""" + name = get_session_container_name(args, name) + options = options + [ '--name', name, '--rm', diff --git a/test/lib/ansible_test/_internal/host_profiles.py b/test/lib/ansible_test/_internal/host_profiles.py index a51eb693..09812456 100644 --- a/test/lib/ansible_test/_internal/host_profiles.py +++ b/test/lib/ansible_test/_internal/host_profiles.py @@ -99,7 +99,6 @@ from .ansible_util import ( ) from .containers import ( - CleanupMode, HostType, get_container_database, run_support_container, @@ -447,7 +446,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do @property def label(self) -> str: """Label to apply to resources related to this profile.""" - return f'{"controller" if self.controller else "target"}-{self.args.session_name}' + return f'{"controller" if self.controller else "target"}' def provision(self) -> None: """Provision the host before delegation.""" @@ -462,7 +461,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do ports=[22], publish_ports=not self.controller, # connections to the controller over SSH are not required options=init_config.options, - cleanup=CleanupMode.NO, + cleanup=False, cmd=self.build_init_command(init_config, init_probe), ) @@ -807,6 +806,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do - Avoid hanging indefinitely or for an unreasonably long time. NOTE: The container must have a POSIX-compliant default shell "sh" with a non-builtin "sleep" command. + The "sleep" command is invoked through "env" to avoid using a shell builtin "sleep" (if present). """ command = '' @@ -814,7 +814,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do command += f'{init_config.command} && ' if sleep or init_config.command_privileged: - command += 'sleep 60 ; ' + command += 'env sleep 60 ; ' if not command: return None @@ -838,7 +838,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do """Check the cgroup v1 systemd hierarchy to verify it is writeable for our container.""" probe_script = (read_text_file(os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'check_systemd_cgroup_v1.sh')) .replace('@MARKER@', self.MARKER) - .replace('@LABEL@', self.label)) + .replace('@LABEL@', f'{self.label}-{self.args.session_name}')) cmd = ['sh'] @@ -853,7 +853,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do def create_systemd_cgroup_v1(self) -> str: """Create a unique ansible-test cgroup in the v1 systemd hierarchy and return its path.""" - self.cgroup_path = f'/sys/fs/cgroup/systemd/ansible-test-{self.label}' + self.cgroup_path = f'/sys/fs/cgroup/systemd/ansible-test-{self.label}-{self.args.session_name}' # Privileged mode is required to create the cgroup directories on some hosts, such as Fedora 36 and RHEL 9.0. # The mkdir command will fail with "Permission denied" otherwise. diff --git a/test/lib/ansible_test/_internal/http.py b/test/lib/ansible_test/_internal/http.py index 8b4154bf..66afc60d 100644 --- a/test/lib/ansible_test/_internal/http.py +++ b/test/lib/ansible_test/_internal/http.py @@ -126,7 +126,7 @@ class HttpResponse: try: return json.loads(self.response) except ValueError: - raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response)) + raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response)) from None class HttpError(ApplicationError): diff --git a/test/lib/ansible_test/_internal/junit_xml.py b/test/lib/ansible_test/_internal/junit_xml.py index 76c8878b..8c4dba01 100644 --- a/test/lib/ansible_test/_internal/junit_xml.py +++ b/test/lib/ansible_test/_internal/junit_xml.py @@ -15,7 +15,7 @@ from xml.dom import minidom from xml.etree import ElementTree as ET -@dataclasses.dataclass # type: ignore[misc] # https://github.com/python/mypy/issues/5374 +@dataclasses.dataclass class TestResult(metaclass=abc.ABCMeta): """Base class for the result of a test case.""" diff --git a/test/lib/ansible_test/_internal/pypi_proxy.py b/test/lib/ansible_test/_internal/pypi_proxy.py index 5380dd9b..d119efa1 100644 --- a/test/lib/ansible_test/_internal/pypi_proxy.py +++ b/test/lib/ansible_test/_internal/pypi_proxy.py @@ -76,7 +76,7 @@ def run_pypi_proxy(args: EnvironmentConfig, targets_use_pypi: bool) -> None: args=args, context='__pypi_proxy__', image=image, - name=f'pypi-test-container-{args.session_name}', + name='pypi-test-container', ports=[port], ) diff --git a/test/lib/ansible_test/_internal/python_requirements.py b/test/lib/ansible_test/_internal/python_requirements.py index 506b802c..81006e41 100644 --- a/test/lib/ansible_test/_internal/python_requirements.py +++ b/test/lib/ansible_test/_internal/python_requirements.py @@ -297,7 +297,7 @@ def run_pip( connection.run([python.path], data=script, capture=True) except SubprocessError as ex: if 'pip is unavailable:' in ex.stdout + ex.stderr: - raise PipUnavailableError(python) + raise PipUnavailableError(python) from None raise @@ -441,8 +441,8 @@ def get_venv_packages(python: PythonConfig) -> dict[str, str]: # See: https://github.com/ansible/base-test-container/blob/main/files/installer.py default_packages = dict( - pip='21.3.1', - setuptools='60.8.2', + pip='23.1.2', + setuptools='67.7.2', wheel='0.37.1', ) @@ -452,11 +452,6 @@ def get_venv_packages(python: PythonConfig) -> dict[str, str]: setuptools='44.1.1', # 45.0.0 requires Python 3.5+ wheel=None, ), - '3.5': dict( - pip='20.3.4', # 21.0 requires Python 3.6+ - setuptools='50.3.2', # 51.0.0 requires Python 3.6+ - wheel=None, - ), '3.6': dict( pip='21.3.1', # 22.0 requires Python 3.7+ setuptools='59.6.0', # 59.7.0 requires Python 3.7+ diff --git a/test/lib/ansible_test/_internal/util.py b/test/lib/ansible_test/_internal/util.py index 1859be5b..394c2632 100644 --- a/test/lib/ansible_test/_internal/util.py +++ b/test/lib/ansible_test/_internal/util.py @@ -31,11 +31,6 @@ from termios import TIOCGWINSZ # CAUTION: Avoid third-party imports in this module whenever possible. # Any third-party imports occurring here will result in an error if they are vendored by ansible-core. -try: - from typing_extensions import TypeGuard # TypeGuard was added in Python 3.10 -except ImportError: - TypeGuard = None - from .locale_util import ( LOCALE_WARNING, CONFIGURED_LOCALE, @@ -436,7 +431,7 @@ def raw_command( display.info(f'{description}: {escaped_cmd}', verbosity=cmd_verbosity, truncate=True) display.info('Working directory: %s' % cwd, verbosity=2) - program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required='warning') + program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required=False) if program: display.info('Program found: %s' % program, verbosity=2) @@ -1155,7 +1150,7 @@ def verify_sys_executable(path: str) -> t.Optional[str]: return expected_executable -def type_guard(sequence: c.Sequence[t.Any], guard_type: t.Type[C]) -> TypeGuard[c.Sequence[C]]: +def type_guard(sequence: c.Sequence[t.Any], guard_type: t.Type[C]) -> t.TypeGuard[c.Sequence[C]]: """ Raises an exception if any item in the given sequence does not match the specified guard type. Use with assert so that type checkers are aware of the type guard. diff --git a/test/lib/ansible_test/_internal/util_common.py b/test/lib/ansible_test/_internal/util_common.py index 222366e4..77a6165c 100644 --- a/test/lib/ansible_test/_internal/util_common.py +++ b/test/lib/ansible_test/_internal/util_common.py @@ -88,7 +88,7 @@ class ExitHandler: try: func(*args, **kwargs) - except BaseException as ex: # pylint: disable=broad-except + except BaseException as ex: # pylint: disable=broad-exception-caught last_exception = ex display.fatal(f'Exit handler failed: {ex}') @@ -498,9 +498,14 @@ def run_command( ) -def yamlcheck(python: PythonConfig) -> t.Optional[bool]: +def yamlcheck(python: PythonConfig, explain: bool = False) -> t.Optional[bool]: """Return True if PyYAML has libyaml support, False if it does not and None if it was not found.""" - result = json.loads(raw_command([python.path, os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'yamlcheck.py')], capture=True)[0]) + stdout = raw_command([python.path, os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'yamlcheck.py')], capture=True, explain=explain)[0] + + if explain: + return None + + result = json.loads(stdout) if not result['yaml']: return None diff --git a/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json b/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json index 88858aeb..da4a0b10 100644 --- a/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json +++ b/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json @@ -2,6 +2,10 @@ "extensions": [ ".py" ], + "prefixes": [ + "lib/ansible/", + "plugins/" + ], "ignore_self": true, "output": "path-line-column-message" } diff --git a/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json b/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json index 88858aeb..da4a0b10 100644 --- a/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json +++ b/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json @@ -2,6 +2,10 @@ "extensions": [ ".py" ], + "prefixes": [ + "lib/ansible/", + "plugins/" + ], "ignore_self": true, "output": "path-line-column-message" } diff --git a/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py b/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py index 6cf27774..188d50fe 100644 --- a/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +++ b/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py @@ -16,9 +16,19 @@ from voluptuous.humanize import humanize_error from ansible.module_utils.compat.version import StrictVersion, LooseVersion from ansible.module_utils.six import string_types +from ansible.utils.collection_loader import AnsibleCollectionRef from ansible.utils.version import SemanticVersion +def fqcr(value): + """Validate a FQCR.""" + if not isinstance(value, string_types): + raise Invalid('Must be a string that is a FQCR') + if not AnsibleCollectionRef.is_valid_fqcr(value): + raise Invalid('Must be a FQCR') + return value + + def isodate(value, check_deprecation_date=False, is_tombstone=False): """Validate a datetime.date or ISO 8601 date string.""" # datetime.date objects come from YAML dates, these are ok @@ -126,12 +136,15 @@ def validate_metadata_file(path, is_ansible, check_deprecation_dates=False): with open(path, 'r', encoding='utf-8') as f_path: routing = yaml.safe_load(f_path) except yaml.error.MarkedYAMLError as ex: - print('%s:%d:%d: YAML load failed: %s' % (path, ex.context_mark.line + - 1, ex.context_mark.column + 1, re.sub(r'\s+', ' ', str(ex)))) + print('%s:%d:%d: YAML load failed: %s' % ( + path, + ex.context_mark.line + 1 if ex.context_mark else 0, + ex.context_mark.column + 1 if ex.context_mark else 0, + re.sub(r'\s+', ' ', str(ex)), + )) return except Exception as ex: # pylint: disable=broad-except - print('%s:%d:%d: YAML load failed: %s' % - (path, 0, 0, re.sub(r'\s+', ' ', str(ex)))) + print('%s:%d:%d: YAML load failed: %s' % (path, 0, 0, re.sub(r'\s+', ' ', str(ex)))) return if is_ansible: @@ -184,17 +197,37 @@ def validate_metadata_file(path, is_ansible, check_deprecation_dates=False): avoid_additional_data ) - plugin_routing_schema = Any( - Schema({ - ('deprecation'): Any(deprecation_schema), - ('tombstone'): Any(tombstoning_schema), - ('redirect'): Any(*string_types), - }, extra=PREVENT_EXTRA), + plugins_routing_common_schema = Schema({ + ('deprecation'): Any(deprecation_schema), + ('tombstone'): Any(tombstoning_schema), + ('redirect'): fqcr, + }, extra=PREVENT_EXTRA) + + plugin_routing_schema = Any(plugins_routing_common_schema) + + # Adjusted schema for modules only + plugin_routing_schema_modules = Any( + plugins_routing_common_schema.extend({ + ('action_plugin'): fqcr} + ) + ) + + # Adjusted schema for module_utils + plugin_routing_schema_mu = Any( + plugins_routing_common_schema.extend({ + ('redirect'): Any(*string_types)} + ), ) list_dict_plugin_routing_schema = [{str_type: plugin_routing_schema} for str_type in string_types] + list_dict_plugin_routing_schema_mu = [{str_type: plugin_routing_schema_mu} + for str_type in string_types] + + list_dict_plugin_routing_schema_modules = [{str_type: plugin_routing_schema_modules} + for str_type in string_types] + plugin_schema = Schema({ ('action'): Any(None, *list_dict_plugin_routing_schema), ('become'): Any(None, *list_dict_plugin_routing_schema), @@ -207,8 +240,8 @@ def validate_metadata_file(path, is_ansible, check_deprecation_dates=False): ('httpapi'): Any(None, *list_dict_plugin_routing_schema), ('inventory'): Any(None, *list_dict_plugin_routing_schema), ('lookup'): Any(None, *list_dict_plugin_routing_schema), - ('module_utils'): Any(None, *list_dict_plugin_routing_schema), - ('modules'): Any(None, *list_dict_plugin_routing_schema), + ('module_utils'): Any(None, *list_dict_plugin_routing_schema_mu), + ('modules'): Any(None, *list_dict_plugin_routing_schema_modules), ('netconf'): Any(None, *list_dict_plugin_routing_schema), ('shell'): Any(None, *list_dict_plugin_routing_schema), ('strategy'): Any(None, *list_dict_plugin_routing_schema), diff --git a/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json b/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json index 776590b7..ccee80a2 100644 --- a/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json +++ b/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json @@ -2,5 +2,9 @@ "extensions": [ ".py" ], + "prefixes": [ + "lib/ansible/", + "plugins/" + ], "output": "path-line-column-message" } diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini index 4d93f359..41d824b2 100644 --- a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini @@ -34,6 +34,9 @@ ignore_missing_imports = True [mypy-md5.*] ignore_missing_imports = True +[mypy-imp.*] +ignore_missing_imports = True + [mypy-scp.*] ignore_missing_imports = True diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini index 55738f87..6be35724 100644 --- a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini @@ -6,10 +6,10 @@ # There are ~350 errors reported in ansible-test when strict optional checking is enabled. # Until the number of occurrences are greatly reduced, it's better to disable strict checking. strict_optional = False -# There are ~25 errors reported in ansible-test under the 'misc' code. -# The majority of those errors are "Only concrete class can be given", which is due to a limitation of mypy. -# See: https://github.com/python/mypy/issues/5374 -disable_error_code = misc +# There are ~13 type-abstract errors reported in ansible-test. +# This is due to assumptions mypy makes about Type and abstract types. +# See: https://discuss.python.org/t/add-abstracttype-to-the-typing-module/21996/13 +disable_error_code = type-abstract [mypy-argcomplete] ignore_missing_imports = True diff --git a/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt b/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt index 659c7f59..4d1de692 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt +++ b/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt @@ -2,3 +2,8 @@ E402 W503 W504 E741 + +# The E203 rule is not PEP 8 compliant. +# Unfortunately this means it also conflicts with the output from `black`. +# See: https://github.com/PyCQA/pycodestyle/issues/373 +E203 diff --git a/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1 b/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1 index 2ae13b4c..7beb38c1 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1 +++ b/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1 @@ -4,6 +4,9 @@ Enable = $true MaximumLineLength = 160 } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } PSPlaceOpenBrace = @{ Enable = $true OnSameLine = $true diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg b/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg index aa347729..f8a0a8af 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg @@ -10,6 +10,7 @@ disable= raise-missing-from, # Python 2.x does not support raise from super-with-arguments, # Python 2.x does not support super without arguments redundant-u-string-prefix, # Python 2.x support still required + broad-exception-raised, # many exceptions with no need for a custom type too-few-public-methods, too-many-arguments, too-many-branches, @@ -19,6 +20,7 @@ disable= too-many-nested-blocks, too-many-return-statements, too-many-statements, + use-dict-literal, # ignoring as a common style issue useless-return, # complains about returning None when the return type is optional [BASIC] @@ -55,3 +57,5 @@ preferred-modules = # Listing them here makes it possible to enable the import-error check. ignored-modules = py, + pytest, + _pytest.runner, diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg b/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg index 1c03472c..5bec36fd 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg @@ -7,7 +7,7 @@ disable= deprecated-module, # results vary by Python version duplicate-code, # consistent results require running with --jobs 1 and testing all files import-outside-toplevel, # common pattern in ansible related code - raise-missing-from, # Python 2.x does not support raise from + broad-exception-raised, # many exceptions with no need for a custom type too-few-public-methods, too-many-public-methods, too-many-arguments, @@ -18,6 +18,7 @@ disable= too-many-nested-blocks, too-many-return-statements, too-many-statements, + use-dict-literal, # ignoring as a common style issue unspecified-encoding, # always run with UTF-8 encoding enforced useless-return, # complains about returning None when the return type is optional diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg b/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg index e3aa8eed..c30eb37a 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg @@ -17,6 +17,7 @@ disable= too-many-nested-blocks, too-many-return-statements, too-many-statements, + use-dict-literal, # ignoring as a common style issue unspecified-encoding, # always run with UTF-8 encoding enforced useless-return, # complains about returning None when the return type is optional diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg b/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg index 38b8d2d0..762d488d 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg @@ -9,7 +9,8 @@ disable= attribute-defined-outside-init, bad-indentation, bad-mcs-classmethod-argument, - broad-except, + broad-exception-caught, + broad-exception-raised, c-extension-no-member, cell-var-from-loop, chained-comparison, @@ -29,6 +30,7 @@ disable= consider-using-max-builtin, consider-using-min-builtin, cyclic-import, # consistent results require running with --jobs 1 and testing all files + deprecated-comment, # custom plugin only used by ansible-core, not collections deprecated-method, # results vary by Python version deprecated-module, # results vary by Python version duplicate-code, # consistent results require running with --jobs 1 and testing all files @@ -95,8 +97,6 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - trailing-comma-tuple, - trailing-comma-tuple, try-except-raise, unbalanced-tuple-unpacking, undefined-loop-variable, @@ -110,10 +110,9 @@ disable= unsupported-delete-operation, unsupported-membership-test, unused-argument, - unused-import, unused-variable, unspecified-encoding, # always run with UTF-8 encoding enforced - use-dict-literal, # many occurrences + use-dict-literal, # ignoring as a common style issue use-list-literal, # many occurrences use-implicit-booleaness-not-comparison, # many occurrences useless-object-inheritance, diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg b/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg index 6a242b8d..825e5df7 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg @@ -10,7 +10,8 @@ disable= attribute-defined-outside-init, bad-indentation, bad-mcs-classmethod-argument, - broad-except, + broad-exception-caught, + broad-exception-raised, c-extension-no-member, cell-var-from-loop, chained-comparison, @@ -61,8 +62,6 @@ disable= not-a-mapping, not-an-iterable, not-callable, - pointless-statement, - pointless-string-statement, possibly-unused-variable, protected-access, raise-missing-from, # Python 2.x does not support raise from @@ -91,8 +90,6 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - trailing-comma-tuple, - trailing-comma-tuple, try-except-raise, unbalanced-tuple-unpacking, undefined-loop-variable, @@ -105,10 +102,9 @@ disable= unsupported-delete-operation, unsupported-membership-test, unused-argument, - unused-import, unused-variable, unspecified-encoding, # always run with UTF-8 encoding enforced - use-dict-literal, # many occurrences + use-dict-literal, # ignoring as a common style issue use-list-literal, # many occurrences use-implicit-booleaness-not-comparison, # many occurrences useless-object-inheritance, diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py index 79b8bf15..f6c83373 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py @@ -5,14 +5,31 @@ from __future__ import annotations import datetime +import functools +import json import re +import shlex import typing as t +from tokenize import COMMENT, TokenInfo import astroid -from pylint.interfaces import IAstroidChecker -from pylint.checkers import BaseChecker -from pylint.checkers.utils import check_messages +# support pylint 2.x and 3.x -- remove when supporting only 3.x +try: + from pylint.interfaces import IAstroidChecker, ITokenChecker +except ImportError: + class IAstroidChecker: + """Backwards compatibility for 2.x / 3.x support.""" + + class ITokenChecker: + """Backwards compatibility for 2.x / 3.x support.""" + +try: + from pylint.checkers.utils import check_messages +except ImportError: + from pylint.checkers.utils import only_required_for_messages as check_messages + +from pylint.checkers import BaseChecker, BaseTokenChecker from ansible.module_utils.compat.version import LooseVersion from ansible.module_utils.six import string_types @@ -95,7 +112,7 @@ ANSIBLE_VERSION = LooseVersion('.'.join(ansible_version_raw.split('.')[:3])) def _get_expr_name(node): - """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr`` + """Function to get either ``attrname`` or ``name`` from ``node.func.expr`` Created specifically for the case of ``display.deprecated`` or ``self._display.deprecated`` """ @@ -106,6 +123,17 @@ def _get_expr_name(node): return node.func.expr.name +def _get_func_name(node): + """Function to get either ``attrname`` or ``name`` from ``node.func`` + + Created specifically for the case of ``from ansible.module_utils.common.warnings import deprecate`` + """ + try: + return node.func.attrname + except AttributeError: + return node.func.name + + def parse_isodate(value): """Parse an ISO 8601 date string.""" msg = 'Expected ISO 8601 date string (YYYY-MM-DD)' @@ -118,7 +146,7 @@ def parse_isodate(value): try: return datetime.datetime.strptime(value, '%Y-%m-%d').date() except ValueError: - raise ValueError(msg) + raise ValueError(msg) from None class AnsibleDeprecatedChecker(BaseChecker): @@ -160,6 +188,8 @@ class AnsibleDeprecatedChecker(BaseChecker): self.add_message('ansible-deprecated-date', node=node, args=(date,)) def _check_version(self, node, version, collection_name): + if collection_name is None: + collection_name = 'ansible.builtin' if not isinstance(version, (str, float)): if collection_name == 'ansible.builtin': symbol = 'ansible-invalid-deprecated-version' @@ -197,12 +227,17 @@ class AnsibleDeprecatedChecker(BaseChecker): @property def collection_name(self) -> t.Optional[str]: """Return the collection name, or None if ansible-core is being tested.""" - return self.config.collection_name + return self.linter.config.collection_name @property def collection_version(self) -> t.Optional[SemanticVersion]: """Return the collection version, or None if ansible-core is being tested.""" - return SemanticVersion(self.config.collection_version) if self.config.collection_version is not None else None + if self.linter.config.collection_version is None: + return None + sem_ver = SemanticVersion(self.linter.config.collection_version) + # Ignore pre-release for version comparison to catch issues before the final release is cut. + sem_ver.prerelease = () + return sem_ver @check_messages(*(MSGS.keys())) def visit_call(self, node): @@ -211,8 +246,9 @@ class AnsibleDeprecatedChecker(BaseChecker): date = None collection_name = None try: - if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or - node.func.attrname == 'deprecate' and _get_expr_name(node)): + funcname = _get_func_name(node) + if (funcname == 'deprecated' and 'display' in _get_expr_name(node) or + funcname == 'deprecate'): if node.keywords: for keyword in node.keywords: if len(node.keywords) == 1 and keyword.arg is None: @@ -258,6 +294,137 @@ class AnsibleDeprecatedChecker(BaseChecker): pass +class AnsibleDeprecatedCommentChecker(BaseTokenChecker): + """Checks for ``# deprecated:`` comments to ensure that the ``version`` + has not passed or met the time for removal + """ + + __implements__ = (ITokenChecker,) + + name = 'deprecated-comment' + msgs = { + 'E9601': ("Deprecated core version (%r) found: %s", + "ansible-deprecated-version-comment", + "Used when a '# deprecated:' comment specifies a version " + "less than or equal to the current version of Ansible", + {'minversion': (2, 6)}), + 'E9602': ("Deprecated comment contains invalid keys %r", + "ansible-deprecated-version-comment-invalid-key", + "Used when a '#deprecated:' comment specifies invalid data", + {'minversion': (2, 6)}), + 'E9603': ("Deprecated comment missing version", + "ansible-deprecated-version-comment-missing-version", + "Used when a '#deprecated:' comment specifies invalid data", + {'minversion': (2, 6)}), + 'E9604': ("Deprecated python version (%r) found: %s", + "ansible-deprecated-python-version-comment", + "Used when a '#deprecated:' comment specifies a python version " + "less than or equal to the minimum python version", + {'minversion': (2, 6)}), + 'E9605': ("Deprecated comment contains invalid version %r: %s", + "ansible-deprecated-version-comment-invalid-version", + "Used when a '#deprecated:' comment specifies an invalid version", + {'minversion': (2, 6)}), + } + + options = ( + ('min-python-version-db', { + 'default': None, + 'type': 'string', + 'metavar': '<path>', + 'help': 'The path to the DB mapping paths to minimum Python versions.', + }), + ) + + def process_tokens(self, tokens: list[TokenInfo]) -> None: + for token in tokens: + if token.type == COMMENT: + self._process_comment(token) + + def _deprecated_string_to_dict(self, token: TokenInfo, string: str) -> dict[str, str]: + valid_keys = {'description', 'core_version', 'python_version'} + data = dict.fromkeys(valid_keys) + for opt in shlex.split(string): + if '=' not in opt: + data[opt] = None + continue + key, _sep, value = opt.partition('=') + data[key] = value + if not any((data['core_version'], data['python_version'])): + self.add_message( + 'ansible-deprecated-version-comment-missing-version', + line=token.start[0], + col_offset=token.start[1], + ) + bad = set(data).difference(valid_keys) + if bad: + self.add_message( + 'ansible-deprecated-version-comment-invalid-key', + line=token.start[0], + col_offset=token.start[1], + args=(','.join(bad),) + ) + return data + + @functools.cached_property + def _min_python_version_db(self) -> dict[str, str]: + """A dictionary of absolute file paths and their minimum required Python version.""" + with open(self.linter.config.min_python_version_db) as db_file: + return json.load(db_file) + + def _process_python_version(self, token: TokenInfo, data: dict[str, str]) -> None: + current_file = self.linter.current_file + check_version = self._min_python_version_db[current_file] + + try: + if LooseVersion(data['python_version']) < LooseVersion(check_version): + self.add_message( + 'ansible-deprecated-python-version-comment', + line=token.start[0], + col_offset=token.start[1], + args=( + data['python_version'], + data['description'] or 'description not provided', + ), + ) + except (ValueError, TypeError) as exc: + self.add_message( + 'ansible-deprecated-version-comment-invalid-version', + line=token.start[0], + col_offset=token.start[1], + args=(data['python_version'], exc) + ) + + def _process_core_version(self, token: TokenInfo, data: dict[str, str]) -> None: + try: + if ANSIBLE_VERSION >= LooseVersion(data['core_version']): + self.add_message( + 'ansible-deprecated-version-comment', + line=token.start[0], + col_offset=token.start[1], + args=( + data['core_version'], + data['description'] or 'description not provided', + ) + ) + except (ValueError, TypeError) as exc: + self.add_message( + 'ansible-deprecated-version-comment-invalid-version', + line=token.start[0], + col_offset=token.start[1], + args=(data['core_version'], exc) + ) + + def _process_comment(self, token: TokenInfo) -> None: + if token.string.startswith('# deprecated:'): + data = self._deprecated_string_to_dict(token, token.string[13:].strip()) + if data['core_version']: + self._process_core_version(token, data) + if data['python_version']: + self._process_python_version(token, data) + + def register(linter): """required method to auto register this checker """ linter.register_checker(AnsibleDeprecatedChecker(linter)) + linter.register_checker(AnsibleDeprecatedCommentChecker(linter)) diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py index 934a9ae7..83c27734 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py @@ -5,23 +5,26 @@ from __future__ import annotations import astroid -from pylint.interfaces import IAstroidChecker -from pylint.checkers import BaseChecker -from pylint.checkers import utils -from pylint.checkers.utils import check_messages + +# support pylint 2.x and 3.x -- remove when supporting only 3.x +try: + from pylint.interfaces import IAstroidChecker +except ImportError: + class IAstroidChecker: + """Backwards compatibility for 2.x / 3.x support.""" + try: - from pylint.checkers.utils import parse_format_method_string + from pylint.checkers.utils import check_messages except ImportError: - # noinspection PyUnresolvedReferences - from pylint.checkers.strings import parse_format_method_string + from pylint.checkers.utils import only_required_for_messages as check_messages + +from pylint.checkers import BaseChecker +from pylint.checkers import utils MSGS = { - 'E9305': ("Format string contains automatic field numbering " - "specification", + 'E9305': ("disabled", # kept for backwards compatibility with inline ignores, remove after 2.14 is EOL "ansible-format-automatic-specification", - "Used when a PEP 3101 format string contains automatic " - "field numbering (e.g. '{}').", - {'minversion': (2, 6)}), + "disabled"), 'E9390': ("bytes object has no .format attribute", "ansible-no-format-on-bytestring", "Used when a bytestring was used as a PEP 3101 format string " @@ -64,20 +67,6 @@ class AnsibleStringFormatChecker(BaseChecker): if isinstance(strnode.value, bytes): self.add_message('ansible-no-format-on-bytestring', node=node) return - if not isinstance(strnode.value, str): - return - - if node.starargs or node.kwargs: - return - try: - num_args = parse_format_method_string(strnode.value)[1] - except utils.IncompleteFormatString: - return - - if num_args: - self.add_message('ansible-format-automatic-specification', - node=node) - return def register(linter): diff --git a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py index 1be42f51..f121ea58 100644 --- a/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +++ b/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py @@ -6,8 +6,14 @@ import typing as t import astroid +# support pylint 2.x and 3.x -- remove when supporting only 3.x +try: + from pylint.interfaces import IAstroidChecker +except ImportError: + class IAstroidChecker: + """Backwards compatibility for 2.x / 3.x support.""" + from pylint.checkers import BaseChecker -from pylint.interfaces import IAstroidChecker ANSIBLE_TEST_MODULES_PATH = os.environ['ANSIBLE_TEST_MODULES_PATH'] ANSIBLE_TEST_MODULE_UTILS_PATH = os.environ['ANSIBLE_TEST_MODULE_UTILS_PATH'] @@ -94,10 +100,7 @@ class AnsibleUnwantedChecker(BaseChecker): )), # see https://docs.python.org/3/library/collections.abc.html - collections=UnwantedEntry('ansible.module_utils.common._collections_compat', - ignore_paths=( - '/lib/ansible/module_utils/common/_collections_compat.py', - ), + collections=UnwantedEntry('ansible.module_utils.six.moves.collections_abc', names=( 'MappingView', 'ItemsView', diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py index 25c61798..2b92a56c 100644 --- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py @@ -33,6 +33,9 @@ from collections.abc import Mapping from contextlib import contextmanager from fnmatch import fnmatch +from antsibull_docs_parser import dom +from antsibull_docs_parser.parser import parse, Context + import yaml from voluptuous.humanize import humanize_error @@ -63,6 +66,7 @@ setup_collection_loader() from ansible import __version__ as ansible_version from ansible.executor.module_common import REPLACER_WINDOWS, NEW_STYLE_PYTHON_MODULE_RE +from ansible.module_utils.common.collections import is_iterable from ansible.module_utils.common.parameters import DEFAULT_TYPE_VALIDATORS from ansible.module_utils.compat.version import StrictVersion, LooseVersion from ansible.module_utils.basic import to_bytes @@ -74,9 +78,13 @@ from ansible.utils.version import SemanticVersion from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec -from .schema import ansible_module_kwargs_schema, doc_schema, return_schema +from .schema import ( + ansible_module_kwargs_schema, + doc_schema, + return_schema, +) -from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, is_empty, parse_yaml, parse_isodate +from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, parse_yaml, parse_isodate if PY3: @@ -297,8 +305,6 @@ class ModuleValidator(Validator): # win_dsc is a dynamic arg spec, the docs won't ever match PS_ARG_VALIDATE_REJECTLIST = frozenset(('win_dsc.ps1', )) - ACCEPTLIST_FUTURE_IMPORTS = frozenset(('absolute_import', 'division', 'print_function')) - def __init__(self, path, git_cache: GitCache, analyze_arg_spec=False, collection=None, collection_version=None, reporter=None, routing=None, plugin_type='module'): super(ModuleValidator, self).__init__(reporter=reporter or Reporter()) @@ -401,13 +407,10 @@ class ModuleValidator(Validator): if isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant) and isinstance(child.value.value, str): continue - # allowed from __future__ imports + # allow __future__ imports (the specific allowed imports are checked by other sanity tests) if isinstance(child, ast.ImportFrom) and child.module == '__future__': - for future_import in child.names: - if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS: - break - else: - continue + continue + return False return True except AttributeError: @@ -636,29 +639,21 @@ class ModuleValidator(Validator): ) def _ensure_imports_below_docs(self, doc_info, first_callable): - min_doc_line = min(doc_info[key]['lineno'] for key in doc_info) + doc_line_numbers = [lineno for lineno in (doc_info[key]['lineno'] for key in doc_info) if lineno > 0] + + min_doc_line = min(doc_line_numbers) if doc_line_numbers else None max_doc_line = max(doc_info[key]['end_lineno'] for key in doc_info) import_lines = [] for child in self.ast.body: if isinstance(child, (ast.Import, ast.ImportFrom)): + # allow __future__ imports (the specific allowed imports are checked by other sanity tests) if isinstance(child, ast.ImportFrom) and child.module == '__future__': - # allowed from __future__ imports - for future_import in child.names: - if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS: - self.reporter.error( - path=self.object_path, - code='illegal-future-imports', - msg=('Only the following from __future__ imports are allowed: %s' - % ', '.join(self.ACCEPTLIST_FUTURE_IMPORTS)), - line=child.lineno - ) - break - else: # for-else. If we didn't find a problem nad break out of the loop, then this is a legal import - continue + continue + import_lines.append(child.lineno) - if child.lineno < min_doc_line: + if min_doc_line and child.lineno < min_doc_line: self.reporter.error( path=self.object_path, code='import-before-documentation', @@ -675,7 +670,7 @@ class ModuleValidator(Validator): for grandchild in bodies: if isinstance(grandchild, (ast.Import, ast.ImportFrom)): import_lines.append(grandchild.lineno) - if grandchild.lineno < min_doc_line: + if min_doc_line and grandchild.lineno < min_doc_line: self.reporter.error( path=self.object_path, code='import-before-documentation', @@ -813,22 +808,22 @@ class ModuleValidator(Validator): continue if grandchild.id == 'DOCUMENTATION': - docs['DOCUMENTATION']['value'] = child.value.s + docs['DOCUMENTATION']['value'] = child.value.value docs['DOCUMENTATION']['lineno'] = child.lineno docs['DOCUMENTATION']['end_lineno'] = ( - child.lineno + len(child.value.s.splitlines()) + child.lineno + len(child.value.value.splitlines()) ) elif grandchild.id == 'EXAMPLES': - docs['EXAMPLES']['value'] = child.value.s + docs['EXAMPLES']['value'] = child.value.value docs['EXAMPLES']['lineno'] = child.lineno docs['EXAMPLES']['end_lineno'] = ( - child.lineno + len(child.value.s.splitlines()) + child.lineno + len(child.value.value.splitlines()) ) elif grandchild.id == 'RETURN': - docs['RETURN']['value'] = child.value.s + docs['RETURN']['value'] = child.value.value docs['RETURN']['lineno'] = child.lineno docs['RETURN']['end_lineno'] = ( - child.lineno + len(child.value.s.splitlines()) + child.lineno + len(child.value.value.splitlines()) ) return docs @@ -1041,6 +1036,8 @@ class ModuleValidator(Validator): 'invalid-documentation', ) + self._validate_all_semantic_markup(doc, returns) + if not self.collection: existing_doc = self._check_for_new_args(doc) self._check_version_added(doc, existing_doc) @@ -1166,6 +1163,113 @@ class ModuleValidator(Validator): return doc_info, doc + def _check_sem_option(self, part: dom.OptionNamePart, current_plugin: dom.PluginIdentifier) -> None: + if part.plugin is None or part.plugin != current_plugin: + return + if part.entrypoint is not None: + return + if tuple(part.link) not in self._all_options: + self.reporter.error( + path=self.object_path, + code='invalid-documentation-markup', + msg='Directive "%s" contains a non-existing option "%s"' % (part.source, part.name) + ) + + def _check_sem_return_value(self, part: dom.ReturnValuePart, current_plugin: dom.PluginIdentifier) -> None: + if part.plugin is None or part.plugin != current_plugin: + return + if part.entrypoint is not None: + return + if tuple(part.link) not in self._all_return_values: + self.reporter.error( + path=self.object_path, + code='invalid-documentation-markup', + msg='Directive "%s" contains a non-existing return value "%s"' % (part.source, part.name) + ) + + def _validate_semantic_markup(self, object) -> None: + # Make sure we operate on strings + if is_iterable(object): + for entry in object: + self._validate_semantic_markup(entry) + return + if not isinstance(object, string_types): + return + + if self.collection: + fqcn = f'{self.collection_name}.{self.name}' + else: + fqcn = f'ansible.builtin.{self.name}' + current_plugin = dom.PluginIdentifier(fqcn=fqcn, type=self.plugin_type) + for par in parse(object, Context(current_plugin=current_plugin), errors='message', add_source=True): + for part in par: + # Errors are already covered during schema validation, we only check for option and + # return value references + if part.type == dom.PartType.OPTION_NAME: + self._check_sem_option(part, current_plugin) + if part.type == dom.PartType.RETURN_VALUE: + self._check_sem_return_value(part, current_plugin) + + def _validate_semantic_markup_collect(self, destination, sub_key, data, all_paths): + if not isinstance(data, dict): + return + for key, value in data.items(): + if not isinstance(value, dict): + continue + keys = {key} + if is_iterable(value.get('aliases')): + keys.update(value['aliases']) + new_paths = [path + [key] for path in all_paths for key in keys] + destination.update([tuple(path) for path in new_paths]) + self._validate_semantic_markup_collect(destination, sub_key, value.get(sub_key), new_paths) + + def _validate_semantic_markup_options(self, options): + if not isinstance(options, dict): + return + for key, value in options.items(): + self._validate_semantic_markup(value.get('description')) + self._validate_semantic_markup_options(value.get('suboptions')) + + def _validate_semantic_markup_return_values(self, return_vars): + if not isinstance(return_vars, dict): + return + for key, value in return_vars.items(): + self._validate_semantic_markup(value.get('description')) + self._validate_semantic_markup(value.get('returned')) + self._validate_semantic_markup_return_values(value.get('contains')) + + def _validate_all_semantic_markup(self, docs, return_docs): + if not isinstance(docs, dict): + docs = {} + if not isinstance(return_docs, dict): + return_docs = {} + + self._all_options = set() + self._all_return_values = set() + self._validate_semantic_markup_collect(self._all_options, 'suboptions', docs.get('options'), [[]]) + self._validate_semantic_markup_collect(self._all_return_values, 'contains', return_docs, [[]]) + + for string_keys in ('short_description', 'description', 'notes', 'requirements', 'todo'): + self._validate_semantic_markup(docs.get(string_keys)) + + if is_iterable(docs.get('seealso')): + for entry in docs.get('seealso'): + if isinstance(entry, dict): + self._validate_semantic_markup(entry.get('description')) + + if isinstance(docs.get('attributes'), dict): + for entry in docs.get('attributes').values(): + if isinstance(entry, dict): + for key in ('description', 'details'): + self._validate_semantic_markup(entry.get(key)) + + if isinstance(docs.get('deprecated'), dict): + for key in ('why', 'alternative'): + self._validate_semantic_markup(docs.get('deprecated').get(key)) + + self._validate_semantic_markup_options(docs.get('options')) + self._validate_semantic_markup_return_values(return_docs) + def _check_version_added(self, doc, existing_doc): version_added_raw = doc.get('version_added') try: @@ -1233,6 +1337,31 @@ class ModuleValidator(Validator): self._validate_argument_spec(docs, spec, kwargs) + if isinstance(docs, Mapping) and isinstance(docs.get('attributes'), Mapping): + if isinstance(docs['attributes'].get('check_mode'), Mapping): + support_value = docs['attributes']['check_mode'].get('support') + if not kwargs.get('supports_check_mode', False): + if support_value != 'none': + self.reporter.error( + path=self.object_path, + code='attributes-check-mode', + msg="The module does not declare support for check mode, but the check_mode attribute's" + " support value is '%s' and not 'none'" % support_value + ) + else: + if support_value not in ('full', 'partial', 'N/A'): + self.reporter.error( + path=self.object_path, + code='attributes-check-mode', + msg="The module does declare support for check mode, but the check_mode attribute's support value is '%s'" % support_value + ) + if support_value in ('partial', 'N/A') and docs['attributes']['check_mode'].get('details') in (None, '', []): + self.reporter.error( + path=self.object_path, + code='attributes-check-mode-details', + msg="The module declares it does not fully support check mode, but has no details on what exactly that means" + ) + def _validate_list_of_module_args(self, name, terms, spec, context): if terms is None: return @@ -1748,7 +1877,7 @@ class ModuleValidator(Validator): ) arg_default = None - if 'default' in data and not is_empty(data['default']): + if 'default' in data and data['default'] is not None: try: with CaptureStd(): arg_default = _type_checker(data['default']) @@ -1789,7 +1918,7 @@ class ModuleValidator(Validator): try: doc_default = None - if 'default' in doc_options_arg and not is_empty(doc_options_arg['default']): + if 'default' in doc_options_arg and doc_options_arg['default'] is not None: with CaptureStd(): doc_default = _type_checker(doc_options_arg['default']) except (Exception, SystemExit): diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py index 03a14019..1b712171 100644 --- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py @@ -29,7 +29,7 @@ from contextlib import contextmanager from ansible.executor.powershell.module_manifest import PSModuleDepFinder from ansible.module_utils.basic import FILE_COMMON_ARGUMENTS, AnsibleModule from ansible.module_utils.six import reraise -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text from .utils import CaptureStd, find_executable, get_module_name_from_filename diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py index b2623ff7..a6068c60 100644 --- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py @@ -11,7 +11,8 @@ from ansible.module_utils.compat.version import StrictVersion from functools import partial from urllib.parse import urlparse -from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid, Exclusive +from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, MultipleInvalid, Required, Schema, Self, ValueInvalid, Exclusive +from ansible.constants import DOCUMENTABLE_PLUGINS from ansible.module_utils.six import string_types from ansible.module_utils.common.collections import is_iterable from ansible.module_utils.parsing.convert_bool import boolean @@ -19,6 +20,9 @@ from ansible.parsing.quoting import unquote from ansible.utils.version import SemanticVersion from ansible.release import __version__ +from antsibull_docs_parser import dom +from antsibull_docs_parser.parser import parse, Context + from .utils import parse_isodate list_string_types = list(string_types) @@ -80,26 +84,8 @@ def date(error_code=None): return Any(isodate, error_code=error_code) -_MODULE = re.compile(r"\bM\(([^)]+)\)") -_LINK = re.compile(r"\bL\(([^)]+)\)") -_URL = re.compile(r"\bU\(([^)]+)\)") -_REF = re.compile(r"\bR\(([^)]+)\)") - - -def _check_module_link(directive, content): - if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(content): - raise _add_ansible_error_code( - Invalid('Directive "%s" must contain a FQCN' % directive), 'invalid-documentation-markup') - - -def _check_link(directive, content): - if ',' not in content: - raise _add_ansible_error_code( - Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup') - idx = content.rindex(',') - title = content[:idx] - url = content[idx + 1:].lstrip(' ') - _check_url(directive, url) +# Roles can also be referenced by semantic markup +_VALID_PLUGIN_TYPES = set(DOCUMENTABLE_PLUGINS + ('role', )) def _check_url(directive, content): @@ -107,15 +93,10 @@ def _check_url(directive, content): parsed_url = urlparse(content) if parsed_url.scheme not in ('', 'http', 'https'): raise ValueError('Schema must be HTTP, HTTPS, or not specified') - except ValueError as exc: - raise _add_ansible_error_code( - Invalid('Directive "%s" must contain an URL' % directive), 'invalid-documentation-markup') - - -def _check_ref(directive, content): - if ',' not in content: - raise _add_ansible_error_code( - Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup') + return [] + except ValueError: + return [_add_ansible_error_code( + Invalid('Directive %s must contain a valid URL' % directive), 'invalid-documentation-markup')] def doc_string(v): @@ -123,25 +104,55 @@ def doc_string(v): if not isinstance(v, string_types): raise _add_ansible_error_code( Invalid('Must be a string'), 'invalid-documentation') - for m in _MODULE.finditer(v): - _check_module_link(m.group(0), m.group(1)) - for m in _LINK.finditer(v): - _check_link(m.group(0), m.group(1)) - for m in _URL.finditer(v): - _check_url(m.group(0), m.group(1)) - for m in _REF.finditer(v): - _check_ref(m.group(0), m.group(1)) + errors = [] + for par in parse(v, Context(), errors='message', strict=True, add_source=True): + for part in par: + if part.type == dom.PartType.ERROR: + errors.append(_add_ansible_error_code(Invalid(part.message), 'invalid-documentation-markup')) + if part.type == dom.PartType.URL: + errors.extend(_check_url('U()', part.url)) + if part.type == dom.PartType.LINK: + errors.extend(_check_url('L()', part.url)) + if part.type == dom.PartType.MODULE: + if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.fqcn): + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.fqcn)), + 'invalid-documentation-markup')) + if part.type == dom.PartType.PLUGIN: + if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn): + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)), + 'invalid-documentation-markup')) + if part.plugin.type not in _VALID_PLUGIN_TYPES: + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)), + 'invalid-documentation-markup')) + if part.type == dom.PartType.OPTION_NAME: + if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn): + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)), + 'invalid-documentation-markup')) + if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES: + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)), + 'invalid-documentation-markup')) + if part.type == dom.PartType.RETURN_VALUE: + if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn): + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)), + 'invalid-documentation-markup')) + if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES: + errors.append(_add_ansible_error_code(Invalid( + 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)), + 'invalid-documentation-markup')) + if len(errors) == 1: + raise errors[0] + if errors: + raise MultipleInvalid(errors) return v -def doc_string_or_strings(v): - """Match a documentation string, or list of strings.""" - if isinstance(v, string_types): - return doc_string(v) - if isinstance(v, (list, tuple)): - return [doc_string(vv) for vv in v] - raise _add_ansible_error_code( - Invalid('Must be a string or list of strings'), 'invalid-documentation') +doc_string_or_strings = Any(doc_string, [doc_string]) def is_callable(v): @@ -173,6 +184,11 @@ seealso_schema = Schema( 'description': doc_string, }, { + Required('plugin'): Any(*string_types), + Required('plugin_type'): Any(*DOCUMENTABLE_PLUGINS), + 'description': doc_string, + }, + { Required('ref'): Any(*string_types), Required('description'): doc_string, }, @@ -794,7 +810,7 @@ def author(value): def doc_schema(module_name, for_collection=False, deprecated_module=False, plugin_type='module'): - if module_name.startswith('_'): + if module_name.startswith('_') and not for_collection: module_name = module_name[1:] deprecated_module = True if for_collection is False and plugin_type == 'connection' and module_name == 'paramiko_ssh': @@ -864,9 +880,6 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False, plugi 'action_group': add_default_attributes({ Required('membership'): list_string_types, }), - 'forced_action_plugin': add_default_attributes({ - Required('action_plugin'): any_string_types, - }), 'platform': add_default_attributes({ Required('platforms'): Any(list_string_types, *string_types) }), diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py index 88d5b01a..15cb7037 100644 --- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py @@ -28,7 +28,7 @@ from io import BytesIO, TextIOWrapper import yaml import yaml.reader -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.yaml import SafeLoader from ansible.module_utils.six import string_types diff --git a/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py b/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py index d6de6117..ed1afcf3 100644 --- a/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py +++ b/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py @@ -181,15 +181,15 @@ class YamlChecker: if doc_types and target.id not in doc_types: continue - fmt_match = fmt_re.match(statement.value.s.lstrip()) + fmt_match = fmt_re.match(statement.value.value.lstrip()) fmt = 'yaml' if fmt_match: fmt = fmt_match.group(1) docs[target.id] = dict( - yaml=statement.value.s, + yaml=statement.value.value, lineno=statement.lineno, - end_lineno=statement.lineno + len(statement.value.s.splitlines()), + end_lineno=statement.lineno + len(statement.value.value.splitlines()), fmt=fmt.lower(), ) diff --git a/test/lib/ansible_test/_util/controller/tools/collection_detail.py b/test/lib/ansible_test/_util/controller/tools/collection_detail.py index 870ea59e..df52d099 100644 --- a/test/lib/ansible_test/_util/controller/tools/collection_detail.py +++ b/test/lib/ansible_test/_util/controller/tools/collection_detail.py @@ -50,7 +50,7 @@ def read_manifest_json(collection_path): ) validate_version(result['version']) except Exception as ex: # pylint: disable=broad-except - raise Exception('{0}: {1}'.format(os.path.basename(manifest_path), ex)) + raise Exception('{0}: {1}'.format(os.path.basename(manifest_path), ex)) from None return result @@ -71,7 +71,7 @@ def read_galaxy_yml(collection_path): ) validate_version(result['version']) except Exception as ex: # pylint: disable=broad-except - raise Exception('{0}: {1}'.format(os.path.basename(galaxy_path), ex)) + raise Exception('{0}: {1}'.format(os.path.basename(galaxy_path), ex)) from None return result diff --git a/test/lib/ansible_test/_util/target/common/constants.py b/test/lib/ansible_test/_util/target/common/constants.py index 9bddfaf4..36a5a2c4 100644 --- a/test/lib/ansible_test/_util/target/common/constants.py +++ b/test/lib/ansible_test/_util/target/common/constants.py @@ -7,14 +7,14 @@ __metaclass__ = type REMOTE_ONLY_PYTHON_VERSIONS = ( '2.7', - '3.5', '3.6', '3.7', '3.8', + '3.9', ) CONTROLLER_PYTHON_VERSIONS = ( - '3.9', '3.10', '3.11', + '3.12', ) diff --git a/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py b/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py index fefd6b0f..2f77c03b 100644 --- a/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py +++ b/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py @@ -32,6 +32,50 @@ def collection_pypkgpath(self): raise Exception('File "%s" not found in collection path "%s".' % (self.strpath, ANSIBLE_COLLECTIONS_PATH)) +def enable_assertion_rewriting_hook(): # type: () -> None + """ + Enable pytest's AssertionRewritingHook on Python 3.x. + This is necessary because the Ansible collection loader intercepts imports before the pytest provided loader ever sees them. + """ + import sys + + if sys.version_info[0] == 2: + return # Python 2.x is not supported + + hook_name = '_pytest.assertion.rewrite.AssertionRewritingHook' + hooks = [hook for hook in sys.meta_path if hook.__class__.__module__ + '.' + hook.__class__.__qualname__ == hook_name] + + if len(hooks) != 1: + raise Exception('Found {} instance(s) of "{}" in sys.meta_path.'.format(len(hooks), hook_name)) + + assertion_rewriting_hook = hooks[0] + + # This is based on `_AnsibleCollectionPkgLoaderBase.exec_module` from `ansible/utils/collection_loader/_collection_finder.py`. + def exec_module(self, module): + # short-circuit redirect; avoid reinitializing existing modules + if self._redirect_module: # pylint: disable=protected-access + return + + # execute the module's code in its namespace + code_obj = self.get_code(self._fullname) # pylint: disable=protected-access + + if code_obj is not None: # things like NS packages that can't have code on disk will return None + # This logic is loosely based on `AssertionRewritingHook._should_rewrite` from pytest. + # See: https://github.com/pytest-dev/pytest/blob/779a87aada33af444f14841a04344016a087669e/src/_pytest/assertion/rewrite.py#L209 + should_rewrite = self._package_to_load == 'conftest' or self._package_to_load.startswith('test_') # pylint: disable=protected-access + + if should_rewrite: + # noinspection PyUnresolvedReferences + assertion_rewriting_hook.exec_module(module) + else: + exec(code_obj, module.__dict__) # pylint: disable=exec-used + + # noinspection PyProtectedMember + from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionPkgLoaderBase + + _AnsibleCollectionPkgLoaderBase.exec_module = exec_module + + def pytest_configure(): """Configure this pytest plugin.""" try: @@ -40,6 +84,8 @@ def pytest_configure(): except AttributeError: pytest_configure.executed = True + enable_assertion_rewriting_hook() + # noinspection PyProtectedMember from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder diff --git a/test/lib/ansible_test/_util/target/sanity/import/importer.py b/test/lib/ansible_test/_util/target/sanity/import/importer.py index 44a5ddc9..38a73643 100644 --- a/test/lib/ansible_test/_util/target/sanity/import/importer.py +++ b/test/lib/ansible_test/_util/target/sanity/import/importer.py @@ -552,13 +552,11 @@ def main(): "Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography," " and will be removed in the next release.") - if sys.version_info[:2] == (3, 5): - warnings.filterwarnings( - "ignore", - "Python 3.5 support will be dropped in the next release ofcryptography. Please upgrade your Python.") - warnings.filterwarnings( - "ignore", - "Python 3.5 support will be dropped in the next release of cryptography. Please upgrade your Python.") + # ansible.utils.unsafe_proxy attempts patching sys.intern generating a warning if it was already patched + warnings.filterwarnings( + "ignore", + "skipped sys.intern patch; appears to have already been patched" + ) try: yield diff --git a/test/lib/ansible_test/_util/target/setup/bootstrap.sh b/test/lib/ansible_test/_util/target/setup/bootstrap.sh index ea17dad3..65673da5 100644 --- a/test/lib/ansible_test/_util/target/setup/bootstrap.sh +++ b/test/lib/ansible_test/_util/target/setup/bootstrap.sh @@ -53,7 +53,7 @@ install_pip() { pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-20.3.4.py" ;; *) - pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-21.3.1.py" + pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-23.1.2.py" ;; esac @@ -111,6 +111,15 @@ bootstrap_remote_alpine() echo "Failed to install packages. Sleeping before trying again..." sleep 10 done + + # Upgrade the `libexpat` package to ensure that an upgraded Python (`pyexpat`) continues to work. + while true; do + # shellcheck disable=SC2086 + apk upgrade -q libexpat \ + && break + echo "Failed to upgrade libexpat. Sleeping before trying again..." + sleep 10 + done } bootstrap_remote_fedora() @@ -163,8 +172,6 @@ bootstrap_remote_freebsd() # Declare platform/python version combinations which do not have supporting OS packages available. # For these combinations ansible-test will use pip to install the requirements instead. case "${platform_version}/${python_version}" in - "12.4/3.9") - ;; *) jinja2_pkg="" # not available cryptography_pkg="" # not available @@ -261,7 +268,7 @@ bootstrap_remote_rhel_8() if [ "${python_version}" = "3.6" ]; then py_pkg_prefix="python3" else - py_pkg_prefix="python${python_package_version}" + py_pkg_prefix="python${python_version}" fi packages=" @@ -269,6 +276,14 @@ bootstrap_remote_rhel_8() ${py_pkg_prefix}-devel " + # pip isn't included in the Python devel package under Python 3.11 + if [ "${python_version}" != "3.6" ]; then + packages=" + ${packages} + ${py_pkg_prefix}-pip + " + fi + # Jinja2 is not installed with an OS package since the provided version is too old. # Instead, ansible-test will install it using pip. if [ "${controller}" ]; then @@ -278,9 +293,19 @@ bootstrap_remote_rhel_8() " fi + # Python 3.11 isn't a module like the earlier versions + if [ "${python_version}" = "3.6" ]; then + while true; do + # shellcheck disable=SC2086 + yum module install -q -y "python${python_package_version}" \ + && break + echo "Failed to install packages. Sleeping before trying again..." + sleep 10 + done + fi + while true; do # shellcheck disable=SC2086 - yum module install -q -y "python${python_package_version}" && \ yum install -q -y ${packages} \ && break echo "Failed to install packages. Sleeping before trying again..." @@ -292,22 +317,34 @@ bootstrap_remote_rhel_8() bootstrap_remote_rhel_9() { - py_pkg_prefix="python3" + if [ "${python_version}" = "3.9" ]; then + py_pkg_prefix="python3" + else + py_pkg_prefix="python${python_version}" + fi packages=" gcc ${py_pkg_prefix}-devel " + # pip is not included in the Python devel package under Python 3.11 + if [ "${python_version}" != "3.9" ]; then + packages=" + ${packages} + ${py_pkg_prefix}-pip + " + fi + # Jinja2 is not installed with an OS package since the provided version is too old. # Instead, ansible-test will install it using pip. + # packaging and resolvelib are missing for Python 3.11 (and possible later) so we just + # skip them and let ansible-test install them from PyPI. if [ "${controller}" ]; then packages=" ${packages} ${py_pkg_prefix}-cryptography - ${py_pkg_prefix}-packaging ${py_pkg_prefix}-pyyaml - ${py_pkg_prefix}-resolvelib " fi @@ -387,14 +424,6 @@ bootstrap_remote_ubuntu() echo "Failed to install packages. Sleeping before trying again..." sleep 10 done - - if [ "${controller}" ]; then - if [ "${platform_version}/${python_version}" = "20.04/3.9" ]; then - # Install pyyaml using pip so libyaml support is available on Python 3.9. - # The OS package install (which is installed by default) only has a .so file for Python 3.8. - pip_install "--upgrade pyyaml" - fi - fi } bootstrap_docker() diff --git a/test/lib/ansible_test/_util/target/setup/quiet_pip.py b/test/lib/ansible_test/_util/target/setup/quiet_pip.py index 54f0f860..171ff8f3 100644 --- a/test/lib/ansible_test/_util/target/setup/quiet_pip.py +++ b/test/lib/ansible_test/_util/target/setup/quiet_pip.py @@ -27,10 +27,6 @@ WARNING_MESSAGE_FILTERS = ( # pip 21.0 will drop support for Python 2.7 in January 2021. # More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support 'DEPRECATION: Python 2.7 reached the end of its life ', - - # DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. - # pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality. - 'DEPRECATION: Python 3.5 reached the end of its life ', ) diff --git a/test/lib/ansible_test/config/cloud-config-aws.ini.template b/test/lib/ansible_test/config/cloud-config-aws.ini.template index 88b9fea6..503a14b3 100644 --- a/test/lib/ansible_test/config/cloud-config-aws.ini.template +++ b/test/lib/ansible_test/config/cloud-config-aws.ini.template @@ -6,7 +6,9 @@ # 2) Using the automatically provisioned AWS credentials in ansible-test. # # If you do not want to use the automatically provisioned temporary AWS credentials, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. +# If you need to omit optional fields like security_token, comment out that line. # This will cause ansible-test to use the given configuration instead of temporary credentials. # # NOTE: Automatic provisioning of AWS credentials requires an ansible-core-ci API key. diff --git a/test/lib/ansible_test/config/cloud-config-azure.ini.template b/test/lib/ansible_test/config/cloud-config-azure.ini.template index 766553d1..bf7cc022 100644 --- a/test/lib/ansible_test/config/cloud-config-azure.ini.template +++ b/test/lib/ansible_test/config/cloud-config-azure.ini.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned Azure credentials in ansible-test. # # If you do not want to use the automatically provisioned temporary Azure credentials, -# fill in the values below and save this file without the .template extension. +# fill in the values below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration instead of temporary credentials. # # NOTE: Automatic provisioning of Azure credentials requires an ansible-core-ci API key in ~/.ansible-core-ci.key diff --git a/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template b/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template index 1c99e9b8..8396e4c8 100644 --- a/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template +++ b/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template @@ -4,6 +4,8 @@ # # 1) Running integration tests without using ansible-test. # +# Fill in the value below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. [default] cloudscale_api_token = @API_TOKEN diff --git a/test/lib/ansible_test/config/cloud-config-cs.ini.template b/test/lib/ansible_test/config/cloud-config-cs.ini.template index f8d8a915..0589fd5f 100644 --- a/test/lib/ansible_test/config/cloud-config-cs.ini.template +++ b/test/lib/ansible_test/config/cloud-config-cs.ini.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned cloudstack-sim docker container in ansible-test. # # If you do not want to use the automatically provided CloudStack simulator, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration and not launch the simulator. # # It is recommended that you DO NOT use this template unless you cannot use the simulator. diff --git a/test/lib/ansible_test/config/cloud-config-gcp.ini.template b/test/lib/ansible_test/config/cloud-config-gcp.ini.template index 00a20971..626063da 100644 --- a/test/lib/ansible_test/config/cloud-config-gcp.ini.template +++ b/test/lib/ansible_test/config/cloud-config-gcp.ini.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned cloudstack-sim docker container in ansible-test. # # If you do not want to use the automatically provided GCP simulator, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration and not launch the simulator. # # It is recommended that you DO NOT use this template unless you cannot use the simulator. diff --git a/test/lib/ansible_test/config/cloud-config-hcloud.ini.template b/test/lib/ansible_test/config/cloud-config-hcloud.ini.template index 8db658db..8fc7fa77 100644 --- a/test/lib/ansible_test/config/cloud-config-hcloud.ini.template +++ b/test/lib/ansible_test/config/cloud-config-hcloud.ini.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned Hetzner Cloud credentials in ansible-test. # # If you do not want to use the automatically provisioned temporary Hetzner Cloud credentials, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration instead of temporary credentials. # # NOTE: Automatic provisioning of Hetzner Cloud credentials requires an ansible-core-ci API key. diff --git a/test/lib/ansible_test/config/cloud-config-opennebula.ini.template b/test/lib/ansible_test/config/cloud-config-opennebula.ini.template index 00c56db1..f155d987 100644 --- a/test/lib/ansible_test/config/cloud-config-opennebula.ini.template +++ b/test/lib/ansible_test/config/cloud-config-opennebula.ini.template @@ -6,7 +6,8 @@ # 2) Running integration tests against previously recorded XMLRPC fixtures # # If you want to test against a Live OpenNebula platform, -# fill in the values below and save this file without the .template extension. +# fill in the values below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration. # # If you run with @FIXTURES enabled (true) then you can decide if you want to @@ -17,4 +18,4 @@ opennebula_url: @URL opennebula_username: @USERNAME opennebula_password: @PASSWORD opennebula_test_fixture: @FIXTURES -opennebula_test_fixture_replay: @REPLAY
\ No newline at end of file +opennebula_test_fixture_replay: @REPLAY diff --git a/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template b/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template index 0a10f23b..5c022cde 100644 --- a/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template +++ b/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned openshift-origin docker container in ansible-test. # # If you do not want to use the automatically provided OpenShift container, -# place your kubeconfig file next to this file, with the same name, but without the .template extension. +# place your kubeconfig file next into the tests/integration directory of the collection you're testing, +# with the same name is this file, but without the .template extension. # This will cause ansible-test to use the given configuration and not launch the automatically provided container. # # It is recommended that you DO NOT use this template unless you cannot use the automatically provided container. diff --git a/test/lib/ansible_test/config/cloud-config-scaleway.ini.template b/test/lib/ansible_test/config/cloud-config-scaleway.ini.template index f10419e0..63e4e48f 100644 --- a/test/lib/ansible_test/config/cloud-config-scaleway.ini.template +++ b/test/lib/ansible_test/config/cloud-config-scaleway.ini.template @@ -5,7 +5,8 @@ # 1) Running integration tests without using ansible-test. # # If you want to test against the Vultr public API, -# fill in the values below and save this file without the .template extension. +# fill in the values below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration. [default] diff --git a/test/lib/ansible_test/config/cloud-config-vcenter.ini.template b/test/lib/ansible_test/config/cloud-config-vcenter.ini.template index eff8bf74..4e980137 100644 --- a/test/lib/ansible_test/config/cloud-config-vcenter.ini.template +++ b/test/lib/ansible_test/config/cloud-config-vcenter.ini.template @@ -6,7 +6,8 @@ # 2) Using the automatically provisioned VMware credentials in ansible-test. # # If you do not want to use the automatically provisioned temporary VMware credentials, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration instead of temporary credentials. # # NOTE: Automatic provisioning of VMware credentials requires an ansible-core-ci API key. diff --git a/test/lib/ansible_test/config/cloud-config-vultr.ini.template b/test/lib/ansible_test/config/cloud-config-vultr.ini.template index 48b82108..4530c326 100644 --- a/test/lib/ansible_test/config/cloud-config-vultr.ini.template +++ b/test/lib/ansible_test/config/cloud-config-vultr.ini.template @@ -5,7 +5,8 @@ # 1) Running integration tests without using ansible-test. # # If you want to test against the Vultr public API, -# fill in the values below and save this file without the .template extension. +# fill in the values below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # This will cause ansible-test to use the given configuration. [default] diff --git a/test/lib/ansible_test/config/inventory.networking.template b/test/lib/ansible_test/config/inventory.networking.template index a1545684..40a9f207 100644 --- a/test/lib/ansible_test/config/inventory.networking.template +++ b/test/lib/ansible_test/config/inventory.networking.template @@ -6,7 +6,8 @@ # 2) Using the `--platform` option to provision temporary network instances on EC2. # # If you do not want to use the automatically provisioned temporary network instances, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # # NOTE: Automatic provisioning of network instances on EC2 requires an ansible-core-ci API key. diff --git a/test/lib/ansible_test/config/inventory.winrm.template b/test/lib/ansible_test/config/inventory.winrm.template index 34bbee2d..3238b22e 100644 --- a/test/lib/ansible_test/config/inventory.winrm.template +++ b/test/lib/ansible_test/config/inventory.winrm.template @@ -6,7 +6,8 @@ # 1) Using the `--windows` option to provision temporary Windows instances on EC2. # # If you do not want to use the automatically provisioned temporary Windows instances, -# fill in the @VAR placeholders below and save this file without the .template extension. +# fill in the @VAR placeholders below and save this file without the .template extension, +# into the tests/integration directory of the collection you're testing. # # NOTE: Automatic provisioning of Windows instances on EC2 requires an ansible-core-ci API key. # diff --git a/test/sanity/code-smell/ansible-requirements.py b/test/sanity/code-smell/ansible-requirements.py index 4d1a652f..25d4ec88 100644 --- a/test/sanity/code-smell/ansible-requirements.py +++ b/test/sanity/code-smell/ansible-requirements.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -import sys def read_file(path): diff --git a/test/sanity/code-smell/deprecated-config.requirements.in b/test/sanity/code-smell/deprecated-config.requirements.in index 859c4ee7..4e859bb8 100644 --- a/test/sanity/code-smell/deprecated-config.requirements.in +++ b/test/sanity/code-smell/deprecated-config.requirements.in @@ -1,2 +1,2 @@ -jinja2 # ansible-core requirement +jinja2 pyyaml diff --git a/test/sanity/code-smell/deprecated-config.requirements.txt b/test/sanity/code-smell/deprecated-config.requirements.txt index 338e3f38..ae96cdf4 100644 --- a/test/sanity/code-smell/deprecated-config.requirements.txt +++ b/test/sanity/code-smell/deprecated-config.requirements.txt @@ -1,6 +1,4 @@ # edit "deprecated-config.requirements.in" and generate with: hacking/update-sanity-requirements.py --test deprecated-config -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 Jinja2==3.1.2 -MarkupSafe==2.1.1 -PyYAML==6.0 +MarkupSafe==2.1.3 +PyYAML==6.0.1 diff --git a/test/sanity/code-smell/obsolete-files.json b/test/sanity/code-smell/obsolete-files.json index 02d39204..3f69cdd6 100644 --- a/test/sanity/code-smell/obsolete-files.json +++ b/test/sanity/code-smell/obsolete-files.json @@ -1,6 +1,8 @@ { "include_symlinks": true, "prefixes": [ + "docs/", + "examples/", "test/runner/", "test/sanity/ansible-doc/", "test/sanity/compile/", diff --git a/test/sanity/code-smell/package-data.requirements.in b/test/sanity/code-smell/package-data.requirements.in index 3162feb6..81b58bcf 100644 --- a/test/sanity/code-smell/package-data.requirements.in +++ b/test/sanity/code-smell/package-data.requirements.in @@ -1,8 +1,8 @@ build # required to build sdist wheel # required to build wheel jinja2 -pyyaml # ansible-core requirement -resolvelib < 0.9.0 -rstcheck < 4 # match version used in other sanity tests +pyyaml +resolvelib < 1.1.0 +rstcheck < 6 # newer versions have too many dependencies antsibull-changelog -setuptools == 45.2.0 # minimum supported setuptools +setuptools == 66.1.0 # minimum supported setuptools diff --git a/test/sanity/code-smell/package-data.requirements.txt b/test/sanity/code-smell/package-data.requirements.txt index b66079d0..ce0fb9cf 100644 --- a/test/sanity/code-smell/package-data.requirements.txt +++ b/test/sanity/code-smell/package-data.requirements.txt @@ -1,18 +1,17 @@ # edit "package-data.requirements.in" and generate with: hacking/update-sanity-requirements.py --test package-data -# pre-build requirement: pyyaml == 6.0 -# pre-build constraint: Cython < 3.0 -antsibull-changelog==0.16.0 -build==0.10.0 -docutils==0.17.1 +antsibull-changelog==0.23.0 +build==1.0.3 +docutils==0.18.1 Jinja2==3.1.2 -MarkupSafe==2.1.1 -packaging==21.3 +MarkupSafe==2.1.3 +packaging==23.2 pyproject_hooks==1.0.0 -pyparsing==3.0.9 -PyYAML==6.0 -resolvelib==0.8.1 -rstcheck==3.5.0 +PyYAML==6.0.1 +resolvelib==1.0.1 +rstcheck==5.0.0 semantic-version==2.10.0 -setuptools==45.2.0 +setuptools==66.1.0 tomli==2.0.1 -wheel==0.41.0 +types-docutils==0.18.3 +typing_extensions==4.8.0 +wheel==0.41.2 diff --git a/test/sanity/code-smell/release-names.py b/test/sanity/code-smell/release-names.py index 81d90d81..cac3071d 100644 --- a/test/sanity/code-smell/release-names.py +++ b/test/sanity/code-smell/release-names.py @@ -22,7 +22,7 @@ Test that the release name is present in the list of used up release names from __future__ import annotations -from yaml import safe_load +import pathlib from ansible.release import __codename__ @@ -30,8 +30,7 @@ from ansible.release import __codename__ def main(): """Entrypoint to the script""" - with open('.github/RELEASE_NAMES.yml') as f: - releases = safe_load(f.read()) + releases = pathlib.Path('.github/RELEASE_NAMES.txt').read_text().splitlines() # Why this format? The file's sole purpose is to be read by a human when they need to know # which release names have already been used. So: @@ -41,7 +40,7 @@ def main(): if __codename__ == name: break else: - print('.github/RELEASE_NAMES.yml: Current codename was not present in the file') + print(f'.github/RELEASE_NAMES.txt: Current codename {__codename__!r} not present in the file') if __name__ == '__main__': diff --git a/test/sanity/code-smell/test-constraints.py b/test/sanity/code-smell/test-constraints.py index df30fe12..ac5bb4eb 100644 --- a/test/sanity/code-smell/test-constraints.py +++ b/test/sanity/code-smell/test-constraints.py @@ -65,12 +65,6 @@ def main(): # keeping constraints for tests other than sanity tests in one file helps avoid conflicts print('%s:%d:%d: put the constraint (%s%s) in `%s`' % (path, lineno, 1, name, raw_constraints, constraints_path)) - for name, requirements in frozen_sanity.items(): - if len(set(req[3].group('constraints').strip() for req in requirements)) != 1: - for req in requirements: - print('%s:%d:%d: sanity constraint (%s) does not match others for package `%s`' % ( - req[0], req[1], req[3].start('constraints') + 1, req[3].group('constraints'), name)) - def check_ansible_test(path: str, requirements: list[tuple[int, str, re.Match]]) -> None: sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent.joinpath('lib'))) diff --git a/test/sanity/code-smell/update-bundled.requirements.txt b/test/sanity/code-smell/update-bundled.requirements.txt index d9785e7b..53f1e434 100644 --- a/test/sanity/code-smell/update-bundled.requirements.txt +++ b/test/sanity/code-smell/update-bundled.requirements.txt @@ -1,3 +1,2 @@ # edit "update-bundled.requirements.in" and generate with: hacking/update-sanity-requirements.py --test update-bundled -packaging==21.3 -pyparsing==3.0.9 +packaging==23.2 diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 869522b1..c683fbe7 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -1,16 +1,21 @@ -.azure-pipelines/scripts/publish-codecov.py replace-urlopen lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang lib/ansible/config/base.yml no-unwanted-files -lib/ansible/executor/playbook_executor.py pylint:disallowed-name lib/ansible/executor/powershell/async_watchdog.ps1 pslint:PSCustomUseLiteralPath lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath -lib/ansible/executor/task_queue_manager.py pylint:disallowed-name +lib/ansible/galaxy/collection/__init__.py mypy-3.10:attr-defined # inline ignore has no effect +lib/ansible/galaxy/collection/__init__.py mypy-3.11:attr-defined # inline ignore has no effect +lib/ansible/galaxy/collection/__init__.py mypy-3.12:attr-defined # inline ignore has no effect +lib/ansible/galaxy/collection/gpg.py mypy-3.10:arg-type +lib/ansible/galaxy/collection/gpg.py mypy-3.11:arg-type +lib/ansible/galaxy/collection/gpg.py mypy-3.12:arg-type +lib/ansible/parsing/yaml/constructor.py mypy-3.10:type-var # too many occurrences to ignore inline +lib/ansible/parsing/yaml/constructor.py mypy-3.11:type-var # too many occurrences to ignore inline +lib/ansible/parsing/yaml/constructor.py mypy-3.12:type-var # too many occurrences to ignore inline lib/ansible/keyword_desc.yml no-unwanted-files lib/ansible/modules/apt.py validate-modules:parameter-invalid lib/ansible/modules/apt_repository.py validate-modules:parameter-invalid lib/ansible/modules/assemble.py validate-modules:nonexistent-parameter-documented -lib/ansible/modules/async_status.py use-argspec-type-path lib/ansible/modules/async_status.py validate-modules!skip lib/ansible/modules/async_wrapper.py ansible-doc!skip # not an actual module lib/ansible/modules/async_wrapper.py pylint:ansible-bad-function # ignore, required @@ -21,61 +26,48 @@ lib/ansible/modules/command.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/command.py validate-modules:doc-missing-type lib/ansible/modules/command.py validate-modules:nonexistent-parameter-documented lib/ansible/modules/command.py validate-modules:undocumented-parameter -lib/ansible/modules/copy.py pylint:disallowed-name lib/ansible/modules/copy.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/copy.py validate-modules:nonexistent-parameter-documented lib/ansible/modules/copy.py validate-modules:undocumented-parameter -lib/ansible/modules/dnf.py validate-modules:doc-required-mismatch lib/ansible/modules/dnf.py validate-modules:parameter-invalid +lib/ansible/modules/dnf5.py validate-modules:parameter-invalid lib/ansible/modules/file.py validate-modules:undocumented-parameter lib/ansible/modules/find.py use-argspec-type-path # fix needed -lib/ansible/modules/git.py pylint:disallowed-name lib/ansible/modules/git.py use-argspec-type-path -lib/ansible/modules/git.py validate-modules:doc-missing-type lib/ansible/modules/git.py validate-modules:doc-required-mismatch -lib/ansible/modules/iptables.py pylint:disallowed-name lib/ansible/modules/lineinfile.py validate-modules:doc-choices-do-not-match-spec lib/ansible/modules/lineinfile.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/lineinfile.py validate-modules:nonexistent-parameter-documented lib/ansible/modules/package_facts.py validate-modules:doc-choices-do-not-match-spec -lib/ansible/modules/pip.py pylint:disallowed-name lib/ansible/modules/replace.py validate-modules:nonexistent-parameter-documented +lib/ansible/modules/replace.py pylint:used-before-assignment # false positive detection by pylint lib/ansible/modules/service.py validate-modules:nonexistent-parameter-documented lib/ansible/modules/service.py validate-modules:use-run-command-not-popen -lib/ansible/modules/stat.py validate-modules:doc-default-does-not-match-spec # get_md5 is undocumented lib/ansible/modules/stat.py validate-modules:parameter-invalid -lib/ansible/modules/stat.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/stat.py validate-modules:undocumented-parameter lib/ansible/modules/systemd_service.py validate-modules:parameter-invalid -lib/ansible/modules/systemd_service.py validate-modules:return-syntax-error -lib/ansible/modules/sysvinit.py validate-modules:return-syntax-error lib/ansible/modules/uri.py validate-modules:doc-required-mismatch lib/ansible/modules/user.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/user.py validate-modules:use-run-command-not-popen -lib/ansible/modules/yum.py pylint:disallowed-name lib/ansible/modules/yum.py validate-modules:parameter-invalid -lib/ansible/modules/yum_repository.py validate-modules:doc-default-does-not-match-spec -lib/ansible/modules/yum_repository.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/yum_repository.py validate-modules:undocumented-parameter +lib/ansible/module_utils/basic.py pylint:unused-import # deferring resolution to allow enabling the rule now lib/ansible/module_utils/compat/_selectors2.py future-import-boilerplate # ignore bundled lib/ansible/module_utils/compat/_selectors2.py metaclass-boilerplate # ignore bundled -lib/ansible/module_utils/compat/_selectors2.py pylint:disallowed-name lib/ansible/module_utils/compat/selinux.py import-2.7!skip # pass/fail depends on presence of libselinux.so -lib/ansible/module_utils/compat/selinux.py import-3.5!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.6!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.7!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.8!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.9!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.10!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py import-3.11!skip # pass/fail depends on presence of libselinux.so +lib/ansible/module_utils/compat/selinux.py import-3.12!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/distro/_distro.py future-import-boilerplate # ignore bundled lib/ansible/module_utils/distro/_distro.py metaclass-boilerplate # ignore bundled lib/ansible/module_utils/distro/_distro.py no-assert -lib/ansible/module_utils/distro/_distro.py pylint:using-constant-test # bundled code we don't want to modify lib/ansible/module_utils/distro/_distro.py pep8!skip # bundled code we don't want to modify +lib/ansible/module_utils/distro/_distro.py pylint:undefined-variable # ignore bundled +lib/ansible/module_utils/distro/_distro.py pylint:using-constant-test # bundled code we don't want to modify lib/ansible/module_utils/distro/__init__.py empty-init # breaks namespacing, bundled, do not override lib/ansible/module_utils/facts/__init__.py empty-init # breaks namespacing, deprecate and eventually remove -lib/ansible/module_utils/facts/network/linux.py pylint:disallowed-name lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 pslint:PSUseApprovedVerbs lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSProvideCommentHelp # need to agree on best format for comment location lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSUseApprovedVerbs @@ -93,33 +85,23 @@ lib/ansible/module_utils/six/__init__.py no-dict-iteritems lib/ansible/module_utils/six/__init__.py no-dict-iterkeys lib/ansible/module_utils/six/__init__.py no-dict-itervalues lib/ansible/module_utils/six/__init__.py pylint:self-assigning-variable +lib/ansible/module_utils/six/__init__.py pylint:trailing-comma-tuple lib/ansible/module_utils/six/__init__.py replace-urlopen -lib/ansible/module_utils/urls.py pylint:arguments-renamed -lib/ansible/module_utils/urls.py pylint:disallowed-name lib/ansible/module_utils/urls.py replace-urlopen -lib/ansible/parsing/vault/__init__.py pylint:disallowed-name lib/ansible/parsing/yaml/objects.py pylint:arguments-renamed -lib/ansible/playbook/base.py pylint:disallowed-name lib/ansible/playbook/collectionsearch.py required-and-default-attributes # https://github.com/ansible/ansible/issues/61460 -lib/ansible/playbook/helpers.py pylint:disallowed-name -lib/ansible/playbook/playbook_include.py pylint:arguments-renamed lib/ansible/playbook/role/include.py pylint:arguments-renamed lib/ansible/plugins/action/normal.py action-plugin-docs # default action plugin for modules without a dedicated action plugin lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub for backwards compatibility lib/ansible/plugins/callback/__init__.py pylint:arguments-renamed lib/ansible/plugins/inventory/advanced_host_list.py pylint:arguments-renamed lib/ansible/plugins/inventory/host_list.py pylint:arguments-renamed -lib/ansible/plugins/lookup/random_choice.py pylint:arguments-renamed -lib/ansible/plugins/lookup/sequence.py pylint:disallowed-name -lib/ansible/plugins/shell/cmd.py pylint:arguments-renamed -lib/ansible/plugins/strategy/__init__.py pylint:disallowed-name -lib/ansible/plugins/strategy/linear.py pylint:disallowed-name lib/ansible/utils/collection_loader/_collection_finder.py pylint:deprecated-class lib/ansible/utils/collection_loader/_collection_meta.py pylint:deprecated-class -lib/ansible/vars/hostvars.py pylint:disallowed-name test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function # ignore, required for testing test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from # ignore, required for testing test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import # ignore, required for testing +test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/plugin_utils/check_pylint.py pylint:disallowed-name # ignore, required for testing test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level @@ -132,8 +114,10 @@ test/integration/targets/collections_relative_imports/collection_root/ansible_co test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py pylint:relative-beyond-top-level test/integration/targets/fork_safe_stdio/vendored_pty.py pep8!skip # vendored code test/integration/targets/gathering_facts/library/bogus_facts shebang +test/integration/targets/gathering_facts/library/dummy1 shebang test/integration/targets/gathering_facts/library/facts_one shebang test/integration/targets/gathering_facts/library/facts_two shebang +test/integration/targets/gathering_facts/library/slow shebang test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip test/integration/targets/json_cleanup/library/bad_json shebang test/integration/targets/lookup_csvfile/files/crlf.csv line-endings @@ -143,11 +127,6 @@ test/integration/targets/module_precedence/lib_with_extension/ping.ini shebang test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini shebang test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini shebang test/integration/targets/module_utils/library/test.py future-import-boilerplate # allow testing of Python 2.x implicit relative imports -test/integration/targets/module_utils/module_utils/bar0/foo.py pylint:disallowed-name -test/integration/targets/module_utils/module_utils/foo.py pylint:disallowed-name -test/integration/targets/module_utils/module_utils/sub/bar/bar.py pylint:disallowed-name -test/integration/targets/module_utils/module_utils/sub/bar/__init__.py pylint:disallowed-name -test/integration/targets/module_utils/module_utils/yak/zebra/foo.py pylint:disallowed-name test/integration/targets/old_style_modules_posix/library/helloworld.sh shebang test/integration/targets/template/files/encoding_1252_utf-8.expected no-smart-quotes test/integration/targets/template/files/encoding_1252_windows-1252.expected no-smart-quotes @@ -165,28 +144,9 @@ test/integration/targets/win_script/files/test_script_removes_file.ps1 pslint:PS test/integration/targets/win_script/files/test_script_with_args.ps1 pslint:PSAvoidUsingWriteHost # Keep test/integration/targets/win_script/files/test_script_with_splatting.ps1 pslint:PSAvoidUsingWriteHost # Keep test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 pslint:PSCustomUseLiteralPath # Uses wildcards on purpose -test/lib/ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1 pslint:PSCustomUseLiteralPath -test/lib/ansible_test/_util/target/setup/requirements.py replace-urlopen -test/support/integration/plugins/modules/timezone.py pylint:disallowed-name -test/support/integration/plugins/module_utils/compat/ipaddress.py future-import-boilerplate -test/support/integration/plugins/module_utils/compat/ipaddress.py metaclass-boilerplate -test/support/integration/plugins/module_utils/compat/ipaddress.py no-unicode-literals -test/support/integration/plugins/module_utils/network/common/utils.py future-import-boilerplate -test/support/integration/plugins/module_utils/network/common/utils.py metaclass-boilerplate -test/support/integration/plugins/module_utils/network/common/utils.py pylint:use-a-generator -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py pylint:used-before-assignment -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/filter/network.py pylint:consider-using-dict-comprehension test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py no-unicode-literals -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py pep8:E203 -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py pylint:unnecessary-comprehension -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py pylint:use-a-generator -test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py pylint:unnecessary-comprehension test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py pylint:arguments-renamed -test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py pep8:E501 test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py pylint:arguments-renamed -test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pep8:E231 -test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pylint:disallowed-name -test/support/windows-integration/plugins/action/win_copy.py pylint:used-before-assignment test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/module_utils/WebRequest.psm1 pslint!skip test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.ps1 pslint!skip test/support/windows-integration/plugins/modules/async_status.ps1 pslint!skip @@ -207,19 +167,11 @@ test/support/windows-integration/plugins/modules/win_user_right.ps1 pslint!skip test/support/windows-integration/plugins/modules/win_user.ps1 pslint!skip test/support/windows-integration/plugins/modules/win_wait_for.ps1 pslint!skip test/support/windows-integration/plugins/modules/win_whoami.ps1 pslint!skip -test/units/executor/test_play_iterator.py pylint:disallowed-name -test/units/modules/test_apt.py pylint:disallowed-name test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-no-version test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-version -test/units/module_utils/basic/test_run_command.py pylint:disallowed-name +test/units/module_utils/common/warnings/test_deprecate.py pylint:ansible-deprecated-no-version # testing Display.deprecated call without a version or date +test/units/module_utils/common/warnings/test_deprecate.py pylint:ansible-deprecated-version # testing Deprecated version found in call to Display.deprecated or AnsibleModule.deprecate test/units/module_utils/urls/fixtures/multipart.txt line-endings # Fixture for HTTP tests that use CRLF -test/units/module_utils/urls/test_fetch_url.py replace-urlopen -test/units/module_utils/urls/test_gzip.py replace-urlopen -test/units/module_utils/urls/test_Request.py replace-urlopen -test/units/parsing/vault/test_vault.py pylint:disallowed-name -test/units/playbook/role/test_role.py pylint:disallowed-name -test/units/plugins/test_plugins.py pylint:disallowed-name -test/units/template/test_templar.py pylint:disallowed-name test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py pylint:relative-beyond-top-level test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py empty-init # testing that collections don't need inits @@ -227,3 +179,26 @@ test/units/utils/collection_loader/fixtures/collections_masked/ansible_collectio test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/test_collection_loader.py pylint:undefined-variable # magic runtime local var splatting +.github/CONTRIBUTING.md pymarkdown:line-length +hacking/backport/README.md pymarkdown:no-bare-urls +hacking/ticket_stubs/bug_internal_api.md pymarkdown:no-bare-urls +hacking/ticket_stubs/bug_wrong_repo.md pymarkdown:no-bare-urls +hacking/ticket_stubs/collections.md pymarkdown:line-length +hacking/ticket_stubs/collections.md pymarkdown:no-bare-urls +hacking/ticket_stubs/guide_newbie_about_gh_and_contributing_to_ansible.md pymarkdown:no-bare-urls +hacking/ticket_stubs/no_thanks.md pymarkdown:line-length +hacking/ticket_stubs/no_thanks.md pymarkdown:no-bare-urls +hacking/ticket_stubs/pr_duplicate.md pymarkdown:no-bare-urls +hacking/ticket_stubs/pr_merged.md pymarkdown:no-bare-urls +hacking/ticket_stubs/proposal.md pymarkdown:no-bare-urls +hacking/ticket_stubs/question_not_bug.md pymarkdown:no-bare-urls +hacking/ticket_stubs/resolved.md pymarkdown:no-bare-urls +hacking/ticket_stubs/wider_discussion.md pymarkdown:no-bare-urls +lib/ansible/galaxy/data/apb/README.md pymarkdown:line-length +lib/ansible/galaxy/data/container/README.md pymarkdown:line-length +lib/ansible/galaxy/data/default/role/README.md pymarkdown:line-length +lib/ansible/galaxy/data/network/README.md pymarkdown:line-length +README.md pymarkdown:line-length +test/integration/targets/ansible-vault/invalid_format/README.md pymarkdown:no-bare-urls +test/support/README.md pymarkdown:no-bare-urls +test/units/cli/test_data/role_skeleton/README.md pymarkdown:line-length diff --git a/test/support/README.md b/test/support/README.md index 850bc921..d5244823 100644 --- a/test/support/README.md +++ b/test/support/README.md @@ -1,4 +1,4 @@ -# IMPORTANT! +# IMPORTANT Files under this directory are not actual plugins and modules used by Ansible and as such should **not be modified**. They are used for testing purposes diff --git a/test/support/integration/plugins/modules/sefcontext.py b/test/support/integration/plugins/modules/sefcontext.py index 5574abca..946ae880 100644 --- a/test/support/integration/plugins/modules/sefcontext.py +++ b/test/support/integration/plugins/modules/sefcontext.py @@ -105,13 +105,11 @@ RETURN = r''' # Default return values ''' -import os -import subprocess import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native SELINUX_IMP_ERR = None try: diff --git a/test/support/integration/plugins/modules/timezone.py b/test/support/integration/plugins/modules/timezone.py index b7439a12..dd374838 100644 --- a/test/support/integration/plugins/modules/timezone.py +++ b/test/support/integration/plugins/modules/timezone.py @@ -121,7 +121,7 @@ class Timezone(object): # running in the global zone where changing the timezone has no effect. zonename_cmd = module.get_bin_path('zonename') if zonename_cmd is not None: - (rc, stdout, _) = module.run_command(zonename_cmd) + (rc, stdout, stderr) = module.run_command(zonename_cmd) if rc == 0 and stdout.strip() == 'global': module.fail_json(msg='Adjusting timezone is not supported in Global Zone') @@ -731,7 +731,7 @@ class BSDTimezone(Timezone): # Strategy 3: # (If /etc/localtime is not symlinked) # Check all files in /usr/share/zoneinfo and return first non-link match. - for dname, _, fnames in sorted(os.walk(zoneinfo_dir)): + for dname, dirs, fnames in sorted(os.walk(zoneinfo_dir)): for fname in sorted(fnames): zoneinfo_file = os.path.join(dname, fname) if not os.path.islink(zoneinfo_file) and filecmp.cmp(zoneinfo_file, localtime_file): diff --git a/test/support/integration/plugins/modules/zypper.py b/test/support/integration/plugins/modules/zypper.py index bfb31819..cd67b605 100644 --- a/test/support/integration/plugins/modules/zypper.py +++ b/test/support/integration/plugins/modules/zypper.py @@ -41,7 +41,7 @@ options: - Package name C(name) or package specifier or a list of either. - Can include a version like C(name=1.0), C(name>3.4) or C(name<=2.7). If a version is given, C(oldpackage) is implied and zypper is allowed to update the package within the version range given. - - You can also pass a url or a local path to a rpm file. + - You can also pass a url or a local path to an rpm file. - When using state=latest, this can be '*', which updates all installed packages. required: true aliases: [ 'pkg' ] @@ -202,8 +202,7 @@ EXAMPLES = ''' import xml import re from xml.dom.minidom import parseString as parseXML -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native # import module snippets from ansible.module_utils.basic import AnsibleModule diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py index 40205a46..c6dbb2cf 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py @@ -24,7 +24,7 @@ import uuid import hashlib from ansible.errors import AnsibleError -from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.module_utils.connection import Connection, ConnectionError from ansible.plugins.action import ActionBase from ansible.module_utils.six.moves.urllib.parse import urlsplit diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py index 955329d4..6fa3b8d6 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py @@ -23,7 +23,7 @@ import uuid import hashlib from ansible.errors import AnsibleError -from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.module_utils.connection import Connection, ConnectionError from ansible.plugins.action import ActionBase from ansible.module_utils.six.moves.urllib.parse import urlsplit diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py index 5d05d338..fbcc9c13 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py @@ -25,7 +25,7 @@ import time import re from ansible.errors import AnsibleError -from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.module_utils.six.moves.urllib.parse import urlsplit from ansible.plugins.action.normal import ActionModule as _ActionModule from ansible.utils.display import Display diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py index fef40810..d0d977fa 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py @@ -302,7 +302,7 @@ from functools import wraps from io import BytesIO from ansible.errors import AnsibleConnectionFailure, AnsibleError -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.six import PY3 from ansible.module_utils.six.moves import cPickle @@ -1310,7 +1310,6 @@ class Connection(NetworkConnectionBase): remote host before triggering timeout exception :return: None """ - """Fetch file over scp/sftp from remote device""" ssh = self.ssh_type_conn._connect_uncached() if self.ssh_type == "libssh": self.ssh_type_conn.fetch_file(source, destination, proto=proto) diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py index b29b4872..c7379a63 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py @@ -29,7 +29,7 @@ options: """ from ansible.executor.task_executor import start_connection from ansible.plugins.connection import ConnectionBase -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.connection import Connection as SocketConnection from ansible.utils.display import Display diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py index bc458eb5..64150405 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py @@ -29,7 +29,7 @@ import re import hashlib from ansible.module_utils.six.moves import zip -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils.common.text.converters import to_bytes, to_native DEFAULT_COMMENT_TOKENS = ["#", "!", "/*", "*/", "echo"] diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py index 477d3184..2afa650e 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py @@ -79,7 +79,7 @@ class FactsBase(object): self._module.fail_json( msg="Subset must be one of [%s], got %s" % ( - ", ".join(sorted([item for item in valid_subsets])), + ", ".join(sorted(list(valid_subsets))), subset, ) ) diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py index 53a91e8c..1857f7df 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py @@ -27,7 +27,7 @@ # import sys -from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.module_utils.connection import Connection, ConnectionError try: diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py index 555fc713..149b4413 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py @@ -28,7 +28,7 @@ import traceback import json -from ansible.module_utils._text import to_text, to_native +from ansible.module_utils.common.text.converters import to_text, to_native from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.connection import Connection, ConnectionError diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py index 64eca157..4095f594 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py @@ -36,26 +36,12 @@ import json from itertools import chain -from ansible.module_utils._text import to_text, to_bytes -from ansible.module_utils.common._collections_compat import Mapping +from ansible.module_utils.common.text.converters import to_text, to_bytes +from ansible.module_utils.six.moves.collections_abc import Mapping from ansible.module_utils.six import iteritems, string_types from ansible.module_utils import basic from ansible.module_utils.parsing.convert_bool import boolean -# Backwards compatibility for 3rd party modules -# TODO(pabelanger): With move to ansible.netcommon, we should clean this code -# up and have modules import directly themself. -from ansible.module_utils.common.network import ( # noqa: F401 - to_bits, - is_netmask, - is_masklen, - to_netmask, - to_masklen, - to_subnet, - to_ipv6_network, - VALID_MASKS, -) - try: from jinja2 import Environment, StrictUndefined from jinja2.exceptions import UndefinedError @@ -607,7 +593,7 @@ def remove_empties(cfg_dict): elif ( isinstance(val, list) and val - and all([isinstance(x, dict) for x in val]) + and all(isinstance(x, dict) for x in val) ): child_val = [remove_empties(x) for x in val] if child_val: diff --git a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py index c1384c1d..9d07e856 100644 --- a/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py +++ b/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py @@ -206,7 +206,7 @@ import json from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import Connection -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text def validate_args(module, device_operations): diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py index feba971a..b9cb19d7 100644 --- a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py +++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py @@ -38,7 +38,7 @@ import json from collections.abc import Mapping from ansible.errors import AnsibleConnectionFailure -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( NetworkConfig, diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py index 6818a0ce..c16d84c6 100644 --- a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py +++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py @@ -27,7 +27,7 @@ # import json -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import env_fallback from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( to_list, diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py index ef383fcc..0b3be2a9 100644 --- a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py +++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py @@ -134,7 +134,7 @@ failed_conditions: """ import time -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import AnsibleModule from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( Conditional, diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py index beec5b8d..5048bbb5 100644 --- a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py +++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py @@ -34,7 +34,8 @@ extends_documentation_fragment: - cisco.ios.ios notes: - Tested against IOS 15.6 -- Abbreviated commands are NOT idempotent, see L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). +- Abbreviated commands are NOT idempotent, + see L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). options: lines: description: @@ -326,7 +327,7 @@ time: """ import json -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.connection import ConnectionError from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import ( run_commands, @@ -575,6 +576,7 @@ def main(): ) if running_config.sha1 != base_config.sha1: + before, after = "", "" if module.params["diff_against"] == "intended": before = running_config after = base_config diff --git a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py index 29f31b0e..97169529 100644 --- a/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py +++ b/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py @@ -24,7 +24,7 @@ import json import re from ansible.errors import AnsibleConnectionFailure -from ansible.module_utils._text import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.plugins.terminal import TerminalBase from ansible.utils.display import Display diff --git a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py index 3212615f..1f351dc5 100644 --- a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py +++ b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py @@ -37,7 +37,7 @@ import json from collections.abc import Mapping from ansible.errors import AnsibleConnectionFailure -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( NetworkConfig, ) diff --git a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py index 908395a6..7e8b2048 100644 --- a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py +++ b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py @@ -27,7 +27,7 @@ # import json -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import env_fallback from ansible.module_utils.connection import Connection, ConnectionError diff --git a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py index 18538491..7f7c30c2 100644 --- a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py +++ b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py @@ -133,7 +133,7 @@ warnings: """ import time -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import AnsibleModule from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( Conditional, @@ -192,7 +192,7 @@ def main(): interval = module.params["interval"] match = module.params["match"] - for _ in range(retries): + for dummy in range(retries): responses = run_commands(module, commands) for item in list(conditionals): @@ -213,7 +213,7 @@ def main(): module.fail_json(msg=msg, failed_conditions=failed_conditions) result.update( - {"stdout": responses, "stdout_lines": list(to_lines(responses)),} + {"stdout": responses, "stdout_lines": list(to_lines(responses)), } ) module.exit_json(**result) diff --git a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py index b899045a..e65f3ffd 100644 --- a/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py +++ b/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py @@ -178,7 +178,7 @@ time: """ import re -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import ConnectionError from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py index adb918be..79f72ef6 100644 --- a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py @@ -18,7 +18,7 @@ import zipfile from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.utils.hashing import checksum @@ -439,7 +439,7 @@ class ActionModule(ActionBase): source_full = self._loader.get_real_file(source, decrypt=decrypt) except AnsibleFileNotFound as e: result['failed'] = True - result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e)) + result['msg'] = "could not find src=%s, %s" % (source, to_text(e)) return result original_basename = os.path.basename(source) diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 index 071eb11c..9d29d6fc 100644 --- a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 @@ -95,7 +95,7 @@ If ($null -ne $info) { isreadonly = ($attributes -contains "ReadOnly") isreg = $false isshared = $false - nlink = 1 # Number of links to the file (hard links), overriden below if islnk + nlink = 1 # Number of links to the file (hard links), overridden below if islnk # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem hlnk_targets = @() diff --git a/test/support/windows-integration/plugins/action/win_copy.py b/test/support/windows-integration/plugins/action/win_copy.py index adb918be..79f72ef6 100644 --- a/test/support/windows-integration/plugins/action/win_copy.py +++ b/test/support/windows-integration/plugins/action/win_copy.py @@ -18,7 +18,7 @@ import zipfile from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.utils.hashing import checksum @@ -439,7 +439,7 @@ class ActionModule(ActionBase): source_full = self._loader.get_real_file(source, decrypt=decrypt) except AnsibleFileNotFound as e: result['failed'] = True - result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e)) + result['msg'] = "could not find src=%s, %s" % (source, to_text(e)) return result original_basename = os.path.basename(source) diff --git a/test/support/windows-integration/plugins/action/win_reboot.py b/test/support/windows-integration/plugins/action/win_reboot.py index c408f4f3..76f4a66b 100644 --- a/test/support/windows-integration/plugins/action/win_reboot.py +++ b/test/support/windows-integration/plugins/action/win_reboot.py @@ -4,10 +4,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from datetime import datetime +from datetime import datetime, timezone -from ansible.errors import AnsibleError -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.plugins.action import ActionBase from ansible.plugins.action.reboot import ActionModule as RebootActionModule from ansible.utils.display import Display @@ -65,7 +64,7 @@ class ActionModule(RebootActionModule, ActionBase): result = {} reboot_result = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE) - result['start'] = datetime.utcnow() + result['start'] = datetime.now(timezone.utc) # Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully stdout = reboot_result['stdout'] diff --git a/test/support/windows-integration/plugins/modules/win_stat.ps1 b/test/support/windows-integration/plugins/modules/win_stat.ps1 index 071eb11c..9d29d6fc 100644 --- a/test/support/windows-integration/plugins/modules/win_stat.ps1 +++ b/test/support/windows-integration/plugins/modules/win_stat.ps1 @@ -95,7 +95,7 @@ If ($null -ne $info) { isreadonly = ($attributes -contains "ReadOnly") isreg = $false isshared = $false - nlink = 1 # Number of links to the file (hard links), overriden below if islnk + nlink = 1 # Number of links to the file (hard links), overridden below if islnk # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem hlnk_targets = @() diff --git a/test/units/_vendor/test_vendor.py b/test/units/_vendor/test_vendor.py index 84b850e2..265f5b27 100644 --- a/test/units/_vendor/test_vendor.py +++ b/test/units/_vendor/test_vendor.py @@ -1,27 +1,22 @@ # (c) 2020 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - import os import pkgutil import pytest import sys -from unittest.mock import MagicMock, NonCallableMagicMock, patch +from unittest.mock import patch def reset_internal_vendor_package(): import ansible ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor') - if ansible_vendor_path in sys.path: - sys.path.remove(ansible_vendor_path) + list(map(sys.path.remove, [path for path in sys.path if path == ansible_vendor_path])) for pkg in ['ansible._vendor', 'ansible']: - if pkg in sys.modules: - del sys.modules[pkg] + sys.modules.pop(pkg, None) def test_package_path_masking(): @@ -50,16 +45,10 @@ def test_vendored(vendored_pkg_names=None): import ansible ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor') assert sys.path[0] == ansible_vendor_path - - if ansible_vendor_path in previous_path: - previous_path.remove(ansible_vendor_path) - assert sys.path[1:] == previous_path def test_vendored_conflict(): with pytest.warns(UserWarning) as w: - import pkgutil - import sys test_vendored(vendored_pkg_names=['sys', 'pkgutil']) # pass a real package we know is already loaded - assert any('pkgutil, sys' in str(msg.message) for msg in w) # ensure both conflicting modules are listed and sorted + assert any(list('pkgutil, sys' in str(msg.message) for msg in w)) # ensure both conflicting modules are listed and sorted diff --git a/test/units/cli/arguments/test_optparse_helpers.py b/test/units/cli/arguments/test_optparse_helpers.py index 082c9be4..ae8e8d73 100644 --- a/test/units/cli/arguments/test_optparse_helpers.py +++ b/test/units/cli/arguments/test_optparse_helpers.py @@ -14,10 +14,7 @@ from ansible.cli.arguments import option_helpers as opt_help from ansible import __path__ as ansible_path from ansible.release import __version__ as ansible_version -if C.DEFAULT_MODULE_PATH is None: - cpath = u'Default w/o overrides' -else: - cpath = C.DEFAULT_MODULE_PATH +cpath = C.DEFAULT_MODULE_PATH FAKE_PROG = u'ansible-cli-test' VERSION_OUTPUT = opt_help.version(prog=FAKE_PROG) diff --git a/test/units/cli/galaxy/test_execute_list_collection.py b/test/units/cli/galaxy/test_execute_list_collection.py index e8a834d9..5641cb86 100644 --- a/test/units/cli/galaxy/test_execute_list_collection.py +++ b/test/units/cli/galaxy/test_execute_list_collection.py @@ -5,37 +5,29 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +import pathlib + import pytest +from ansible import constants as C from ansible import context from ansible.cli.galaxy import GalaxyCLI from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.galaxy import collection from ansible.galaxy.dependency_resolution.dataclasses import Requirement -from ansible.module_utils._text import to_native - - -def path_exists(path): - if to_native(path) == '/root/.ansible/collections/ansible_collections/sandwiches/ham': - return False - elif to_native(path) == '/usr/share/ansible/collections/ansible_collections/sandwiches/reuben': - return False - elif to_native(path) == 'nope': - return False - else: - return True +from ansible.module_utils.common.text.converters import to_native +from ansible.plugins.loader import init_plugin_loader def isdir(path): if to_native(path) == 'nope': return False - else: - return True + return True def cliargs(collections_paths=None, collection_name=None): if collections_paths is None: - collections_paths = ['~/root/.ansible/collections', '/usr/share/ansible/collections'] + collections_paths = ['/root/.ansible/collections', '/usr/share/ansible/collections'] context.CLIARGS._store = { 'collections_path': collections_paths, @@ -46,95 +38,61 @@ def cliargs(collections_paths=None, collection_name=None): @pytest.fixture -def mock_collection_objects(mocker): - mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', '/usr/share/ansible/collections']) - mocker.patch('ansible.cli.galaxy.validate_collection_path', - side_effect=['/root/.ansible/collections/ansible_collections', '/usr/share/ansible/collections/ansible_collections']) - - collection_args_1 = ( - ( +def mock_from_path(mocker, monkeypatch): + collection_args = { + '/usr/share/ansible/collections/ansible_collections/sandwiches/pbj': ( 'sandwiches.pbj', - '1.5.0', - None, + '1.0.0', + '/usr/share/ansible/collections/ansible_collections/sandwiches/pbj', 'dir', None, ), - ( - 'sandwiches.reuben', - '2.5.0', - None, + '/usr/share/ansible/collections/ansible_collections/sandwiches/ham': ( + 'sandwiches.ham', + '1.0.0', + '/usr/share/ansible/collections/ansible_collections/sandwiches/ham', 'dir', None, ), - ) - - collection_args_2 = ( - ( + '/root/.ansible/collections/ansible_collections/sandwiches/pbj': ( 'sandwiches.pbj', - '1.0.0', - None, + '1.5.0', + '/root/.ansible/collections/ansible_collections/sandwiches/pbj', 'dir', None, ), - ( - 'sandwiches.ham', - '1.0.0', - None, + '/root/.ansible/collections/ansible_collections/sandwiches/reuben': ( + 'sandwiches.reuben', + '2.5.0', + '/root/.ansible/collections/ansible_collections/sandwiches/reuben', 'dir', None, ), - ) + } - collections_path_1 = [Requirement(*cargs) for cargs in collection_args_1] - collections_path_2 = [Requirement(*cargs) for cargs in collection_args_2] + def dispatch_requirement(path, am): + return Requirement(*collection_args[to_native(path)]) - mocker.patch('ansible.cli.galaxy.find_existing_collections', side_effect=[collections_path_1, collections_path_2]) + files_mock = mocker.MagicMock() + mocker.patch('ansible.galaxy.collection.files', return_value=files_mock) + files_mock.glob.return_value = [] + mocker.patch.object(pathlib.Path, 'is_dir', return_value=True) + for path, args in collection_args.items(): + files_mock.glob.return_value.append(pathlib.Path(args[2])) -@pytest.fixture -def mock_from_path(mocker): - def _from_path(collection_name='pbj'): - collection_args = { - 'sandwiches.pbj': ( - ( - 'sandwiches.pbj', - '1.5.0', - None, - 'dir', - None, - ), - ( - 'sandwiches.pbj', - '1.0.0', - None, - 'dir', - None, - ), - ), - 'sandwiches.ham': ( - ( - 'sandwiches.ham', - '1.0.0', - None, - 'dir', - None, - ), - ), - } - - from_path_objects = [Requirement(*args) for args in collection_args[collection_name]] - mocker.patch('ansible.cli.galaxy.Requirement.from_dir_path_as_unknown', side_effect=from_path_objects) - - return _from_path - - -def test_execute_list_collection_all(mocker, capsys, mock_collection_objects, tmp_path_factory): + mocker.patch('ansible.galaxy.collection.Candidate.from_dir_path_as_unknown', side_effect=dispatch_requirement) + + monkeypatch.setattr(C, 'COLLECTIONS_PATHS', ['/root/.ansible/collections', '/usr/share/ansible/collections']) + + +def test_execute_list_collection_all(mocker, capsys, mock_from_path, tmp_path_factory): """Test listing all collections from multiple paths""" cliargs() + init_plugin_loader() mocker.patch('os.path.exists', return_value=True) - mocker.patch('os.path.isdir', return_value=True) gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list']) tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections') concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False) @@ -152,21 +110,20 @@ def test_execute_list_collection_all(mocker, capsys, mock_collection_objects, tm assert out_lines[5] == 'sandwiches.reuben 2.5.0 ' assert out_lines[6] == '' assert out_lines[7] == '# /usr/share/ansible/collections/ansible_collections' - assert out_lines[8] == 'Collection Version' - assert out_lines[9] == '-------------- -------' - assert out_lines[10] == 'sandwiches.ham 1.0.0 ' - assert out_lines[11] == 'sandwiches.pbj 1.0.0 ' + assert out_lines[8] == 'Collection Version' + assert out_lines[9] == '----------------- -------' + assert out_lines[10] == 'sandwiches.ham 1.0.0 ' + assert out_lines[11] == 'sandwiches.pbj 1.0.0 ' -def test_execute_list_collection_specific(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory): +def test_execute_list_collection_specific(mocker, capsys, mock_from_path, tmp_path_factory): """Test listing a specific collection""" collection_name = 'sandwiches.ham' - mock_from_path(collection_name) cliargs(collection_name=collection_name) - mocker.patch('os.path.exists', path_exists) - mocker.patch('os.path.isdir', return_value=True) + init_plugin_loader() + mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name) mocker.patch('ansible.cli.galaxy._get_collection_widths', return_value=(14, 5)) @@ -186,15 +143,14 @@ def test_execute_list_collection_specific(mocker, capsys, mock_collection_object assert out_lines[4] == 'sandwiches.ham 1.0.0 ' -def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory): +def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_from_path, tmp_path_factory): """Test listing a specific collection that exists at multiple paths""" collection_name = 'sandwiches.pbj' - mock_from_path(collection_name) cliargs(collection_name=collection_name) - mocker.patch('os.path.exists', path_exists) - mocker.patch('os.path.isdir', return_value=True) + init_plugin_loader() + mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name) gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name]) @@ -221,6 +177,8 @@ def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_collect def test_execute_list_collection_specific_invalid_fqcn(mocker, tmp_path_factory): """Test an invalid fully qualified collection name (FQCN)""" + init_plugin_loader() + collection_name = 'no.good.name' cliargs(collection_name=collection_name) @@ -238,6 +196,7 @@ def test_execute_list_collection_no_valid_paths(mocker, capsys, tmp_path_factory """Test listing collections when no valid paths are given""" cliargs() + init_plugin_loader() mocker.patch('os.path.exists', return_value=True) mocker.patch('os.path.isdir', return_value=False) @@ -257,13 +216,14 @@ def test_execute_list_collection_no_valid_paths(mocker, capsys, tmp_path_factory assert 'exists, but it\nis not a directory.' in err -def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_collection_objects, tmp_path_factory): +def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_from_path, tmp_path_factory): """Test listing all collections when one invalid path is given""" - cliargs() + cliargs(collections_paths=['nope']) + init_plugin_loader() + mocker.patch('os.path.exists', return_value=True) mocker.patch('os.path.isdir', isdir) - mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', 'nope']) mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False) gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', '-p', 'nope']) diff --git a/test/units/cli/test_adhoc.py b/test/units/cli/test_adhoc.py index 18775f5d..7bcca471 100644 --- a/test/units/cli/test_adhoc.py +++ b/test/units/cli/test_adhoc.py @@ -93,19 +93,15 @@ def test_run_no_extra_vars(): assert exec_info.value.code == 2 -def test_ansible_version(capsys, mocker): +def test_ansible_version(capsys): adhoc_cli = AdHocCLI(args=['/bin/ansible', '--version']) with pytest.raises(SystemExit): adhoc_cli.run() version = capsys.readouterr() - try: - version_lines = version.out.splitlines() - except AttributeError: - # Python 2.6 does return a named tuple, so get the first item - version_lines = version[0].splitlines() + version_lines = version.out.splitlines() assert len(version_lines) == 9, 'Incorrect number of lines in "ansible --version" output' - assert re.match(r'ansible \[core [0-9.a-z]+\]$', version_lines[0]), 'Incorrect ansible version line in "ansible --version" output' + assert re.match(r'ansible \[core [0-9.a-z]+\]', version_lines[0]), 'Incorrect ansible version line in "ansible --version" output' assert re.match(' config file = .*$', version_lines[1]), 'Incorrect config file line in "ansible --version" output' assert re.match(' configured module search path = .*$', version_lines[2]), 'Incorrect module search path in "ansible --version" output' assert re.match(' ansible python module location = .*$', version_lines[3]), 'Incorrect python module location in "ansible --version" output' diff --git a/test/units/cli/test_data/collection_skeleton/README.md b/test/units/cli/test_data/collection_skeleton/README.md index 4cfd8afe..2e3e4ce5 100644 --- a/test/units/cli/test_data/collection_skeleton/README.md +++ b/test/units/cli/test_data/collection_skeleton/README.md @@ -1 +1 @@ -A readme
\ No newline at end of file +A readme diff --git a/test/units/cli/test_data/collection_skeleton/docs/My Collection.md b/test/units/cli/test_data/collection_skeleton/docs/My Collection.md index 6fa917f2..0d6781bc 100644 --- a/test/units/cli/test_data/collection_skeleton/docs/My Collection.md +++ b/test/units/cli/test_data/collection_skeleton/docs/My Collection.md @@ -1 +1 @@ -Welcome to my test collection doc for {{ namespace }}.
\ No newline at end of file +Welcome to my test collection doc for {{ namespace }}. diff --git a/test/units/cli/test_doc.py b/test/units/cli/test_doc.py index b10f0888..50b714eb 100644 --- a/test/units/cli/test_doc.py +++ b/test/units/cli/test_doc.py @@ -5,7 +5,7 @@ __metaclass__ = type import pytest from ansible.cli.doc import DocCLI, RoleMixin -from ansible.plugins.loader import module_loader +from ansible.plugins.loader import module_loader, init_plugin_loader TTY_IFY_DATA = { @@ -118,6 +118,7 @@ def test_builtin_modules_list(): args = ['ansible-doc', '-l', 'ansible.builtin', '-t', 'module'] obj = DocCLI(args=args) obj.parse() + init_plugin_loader() result = obj._list_plugins('module', module_loader) assert len(result) > 0 diff --git a/test/units/cli/test_galaxy.py b/test/units/cli/test_galaxy.py index 8ff56408..80a2dfae 100644 --- a/test/units/cli/test_galaxy.py +++ b/test/units/cli/test_galaxy.py @@ -20,6 +20,8 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import contextlib + import ansible from io import BytesIO import json @@ -37,7 +39,7 @@ from ansible.cli.galaxy import GalaxyCLI from ansible.galaxy import collection from ansible.galaxy.api import GalaxyAPI from ansible.errors import AnsibleError -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.utils import context_objects as co from ansible.utils.display import Display from units.compat import unittest @@ -60,8 +62,7 @@ class TestGalaxy(unittest.TestCase): cls.temp_dir = tempfile.mkdtemp(prefix='ansible-test_galaxy-') os.chdir(cls.temp_dir) - if os.path.exists("./delete_me"): - shutil.rmtree("./delete_me") + shutil.rmtree("./delete_me", ignore_errors=True) # creating framework for a role gc = GalaxyCLI(args=["ansible-galaxy", "init", "--offline", "delete_me"]) @@ -71,8 +72,7 @@ class TestGalaxy(unittest.TestCase): # making a temp dir for role installation cls.role_path = os.path.join(tempfile.mkdtemp(), "roles") - if not os.path.isdir(cls.role_path): - os.makedirs(cls.role_path) + os.makedirs(cls.role_path) # creating a tar file name for class data cls.role_tar = './delete_me.tar.gz' @@ -80,37 +80,29 @@ class TestGalaxy(unittest.TestCase): # creating a temp file with installation requirements cls.role_req = './delete_me_requirements.yml' - fd = open(cls.role_req, "w") - fd.write("- 'src': '%s'\n 'name': '%s'\n 'path': '%s'" % (cls.role_tar, cls.role_name, cls.role_path)) - fd.close() + with open(cls.role_req, "w") as fd: + fd.write("- 'src': '%s'\n 'name': '%s'\n 'path': '%s'" % (cls.role_tar, cls.role_name, cls.role_path)) @classmethod def makeTar(cls, output_file, source_dir): ''' used for making a tarfile from a role directory ''' # adding directory into a tar file - try: - tar = tarfile.open(output_file, "w:gz") + with tarfile.open(output_file, "w:gz") as tar: tar.add(source_dir, arcname=os.path.basename(source_dir)) - except AttributeError: # tarfile obj. has no attribute __exit__ prior to python 2. 7 - pass - finally: # ensuring closure of tarfile obj - tar.close() @classmethod def tearDownClass(cls): '''After tests are finished removes things created in setUpClass''' # deleting the temp role directory - if os.path.exists(cls.role_dir): - shutil.rmtree(cls.role_dir) - if os.path.exists(cls.role_req): + shutil.rmtree(cls.role_dir, ignore_errors=True) + with contextlib.suppress(FileNotFoundError): os.remove(cls.role_req) - if os.path.exists(cls.role_tar): + with contextlib.suppress(FileNotFoundError): os.remove(cls.role_tar) - if os.path.isdir(cls.role_path): - shutil.rmtree(cls.role_path) + shutil.rmtree(cls.role_path, ignore_errors=True) os.chdir('/') - shutil.rmtree(cls.temp_dir) + shutil.rmtree(cls.temp_dir, ignore_errors=True) def setUp(self): # Reset the stored command line args @@ -137,8 +129,7 @@ class TestGalaxy(unittest.TestCase): role_info = {'name': 'some_role_name', 'galaxy_info': galaxy_info} display_result = gc._display_role_info(role_info) - if display_result.find('\n\tgalaxy_info:') == -1: - self.fail('Expected galaxy_info to be indented once') + self.assertNotEqual(display_result.find('\n\tgalaxy_info:'), -1, 'Expected galaxy_info to be indented once') def test_run(self): ''' verifies that the GalaxyCLI object's api is created and that execute() is called. ''' @@ -176,7 +167,9 @@ class TestGalaxy(unittest.TestCase): with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display: # testing that error expected is raised self.assertRaises(AnsibleError, gc.run) - self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by ")) + assert mocked_display.call_count == 2 + assert mocked_display.mock_calls[0].args[0] == "Starting galaxy role install process" + assert "fake_role_name was NOT installed successfully" in mocked_display.mock_calls[1].args[0] def test_exit_without_ignore_with_flag(self): ''' tests that GalaxyCLI exits without the error specified if the --ignore-errors flag is used ''' @@ -184,7 +177,9 @@ class TestGalaxy(unittest.TestCase): gc = GalaxyCLI(args=["ansible-galaxy", "install", "--server=None", "fake_role_name", "--ignore-errors"]) with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display: gc.run() - self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by ")) + assert mocked_display.call_count == 2 + assert mocked_display.mock_calls[0].args[0] == "Starting galaxy role install process" + assert "fake_role_name was NOT installed successfully" in mocked_display.mock_calls[1].args[0] def test_parse_no_action(self): ''' testing the options parser when no action is given ''' @@ -277,8 +272,6 @@ class ValidRoleTests(object): # Make temp directory for testing cls.test_dir = tempfile.mkdtemp() - if not os.path.isdir(cls.test_dir): - os.makedirs(cls.test_dir) cls.role_dir = os.path.join(cls.test_dir, role_name) cls.role_name = role_name @@ -297,9 +290,8 @@ class ValidRoleTests(object): cls.role_skeleton_path = gc.galaxy.default_role_skeleton_path @classmethod - def tearDownClass(cls): - if os.path.isdir(cls.test_dir): - shutil.rmtree(cls.test_dir) + def tearDownRole(cls): + shutil.rmtree(cls.test_dir, ignore_errors=True) def test_metadata(self): with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf: @@ -349,6 +341,10 @@ class TestGalaxyInitDefault(unittest.TestCase, ValidRoleTests): def setUpClass(cls): cls.setUpRole(role_name='delete_me') + @classmethod + def tearDownClass(cls): + cls.tearDownRole() + def test_metadata_contents(self): with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf: metadata = yaml.safe_load(mf) @@ -361,6 +357,10 @@ class TestGalaxyInitAPB(unittest.TestCase, ValidRoleTests): def setUpClass(cls): cls.setUpRole('delete_me_apb', galaxy_args=['--type=apb']) + @classmethod + def tearDownClass(cls): + cls.tearDownRole() + def test_metadata_apb_tag(self): with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf: metadata = yaml.safe_load(mf) @@ -391,6 +391,10 @@ class TestGalaxyInitContainer(unittest.TestCase, ValidRoleTests): def setUpClass(cls): cls.setUpRole('delete_me_container', galaxy_args=['--type=container']) + @classmethod + def tearDownClass(cls): + cls.tearDownRole() + def test_metadata_container_tag(self): with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf: metadata = yaml.safe_load(mf) @@ -422,6 +426,10 @@ class TestGalaxyInitSkeleton(unittest.TestCase, ValidRoleTests): role_skeleton_path = os.path.join(os.path.split(__file__)[0], 'test_data', 'role_skeleton') cls.setUpRole('delete_me_skeleton', skeleton_path=role_skeleton_path, use_explicit_type=True) + @classmethod + def tearDownClass(cls): + cls.tearDownRole() + def test_empty_files_dir(self): files_dir = os.path.join(self.role_dir, 'files') self.assertTrue(os.path.isdir(files_dir)) @@ -763,6 +771,20 @@ def test_collection_install_with_names(collection_install): assert mock_install.call_args[0][6] is False # force_deps +def test_collection_install_with_invalid_requirements_format(collection_install): + output_dir = collection_install[2] + + requirements_file = os.path.join(output_dir, 'requirements.yml') + with open(requirements_file, 'wb') as req_obj: + req_obj.write(b'"invalid"') + + galaxy_args = ['ansible-galaxy', 'collection', 'install', '--requirements-file', requirements_file, + '--collections-path', output_dir] + + with pytest.raises(AnsibleError, match="Expecting requirements yaml to be a list or dictionary but got str"): + GalaxyCLI(args=galaxy_args).run() + + def test_collection_install_with_requirements_file(collection_install): mock_install, mock_warning, output_dir = collection_install @@ -1242,12 +1264,7 @@ def test_install_implicit_role_with_collections(requirements_file, monkeypatch): assert len(mock_role_install.call_args[0][0]) == 1 assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name' - found = False - for mock_call in mock_display.mock_calls: - if 'contains collections which will be ignored' in mock_call[1][0]: - found = True - break - assert not found + assert not any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls)) @pytest.mark.parametrize('requirements_file', [''' @@ -1274,12 +1291,7 @@ def test_install_explicit_role_with_collections(requirements_file, monkeypatch): assert len(mock_role_install.call_args[0][0]) == 1 assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name' - found = False - for mock_call in mock_display.mock_calls: - if 'contains collections which will be ignored' in mock_call[1][0]: - found = True - break - assert found + assert any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls)) @pytest.mark.parametrize('requirements_file', [''' @@ -1306,12 +1318,7 @@ def test_install_role_with_collections_and_path(requirements_file, monkeypatch): assert len(mock_role_install.call_args[0][0]) == 1 assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name' - found = False - for mock_call in mock_display.mock_calls: - if 'contains collections which will be ignored' in mock_call[1][0]: - found = True - break - assert found + assert any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls)) @pytest.mark.parametrize('requirements_file', [''' @@ -1338,9 +1345,4 @@ def test_install_collection_with_roles(requirements_file, monkeypatch): assert mock_role_install.call_count == 0 - found = False - for mock_call in mock_display.mock_calls: - if 'contains roles which will be ignored' in mock_call[1][0]: - found = True - break - assert found + assert any(list('contains roles which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls)) diff --git a/test/units/cli/test_vault.py b/test/units/cli/test_vault.py index 2304f4d5..f1399c3f 100644 --- a/test/units/cli/test_vault.py +++ b/test/units/cli/test_vault.py @@ -29,7 +29,7 @@ from units.mock.vault_helper import TextVaultSecret from ansible import context, errors from ansible.cli.vault import VaultCLI -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.utils import context_objects as co @@ -171,7 +171,28 @@ class TestVaultCli(unittest.TestCase): mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo']) cli.parse() + self.assertRaisesRegex(errors.AnsibleOptionsError, + "not a tty, editor cannot be opened", + cli.run) + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_create_skip_tty_check(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + cli = VaultCLI(args=['ansible-vault', 'create', '--skip-tty-check', '/dev/null/foo']) + cli.parse() + cli.run() + + @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') + @patch('ansible.cli.vault.VaultEditor') + def test_create_with_tty(self, mock_vault_editor, mock_setup_vault_secrets): + mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))] + self.tty_stdout_patcher = patch('ansible.cli.sys.stdout.isatty', return_value=True) + self.tty_stdout_patcher.start() + cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo']) + cli.parse() cli.run() + self.tty_stdout_patcher.stop() @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets') @patch('ansible.cli.vault.VaultEditor') diff --git a/test/units/compat/mock.py b/test/units/compat/mock.py index 58dc78e0..03154609 100644 --- a/test/units/compat/mock.py +++ b/test/units/compat/mock.py @@ -6,7 +6,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type try: - from unittest.mock import ( + from unittest.mock import ( # pylint: disable=unused-import call, patch, mock_open, diff --git a/test/units/config/manager/test_find_ini_config_file.py b/test/units/config/manager/test_find_ini_config_file.py index df411388..e67eecd9 100644 --- a/test/units/config/manager/test_find_ini_config_file.py +++ b/test/units/config/manager/test_find_ini_config_file.py @@ -13,7 +13,7 @@ import stat import pytest from ansible.config.manager import find_ini_config_file -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text real_exists = os.path.exists real_isdir = os.path.isdir @@ -28,22 +28,17 @@ cfg_in_homedir = os.path.expanduser('~/.ansible.cfg') @pytest.fixture -def setup_env(request): +def setup_env(request, monkeypatch): cur_config = os.environ.get('ANSIBLE_CONFIG', None) cfg_path = request.param[0] if cfg_path is None and cur_config: - del os.environ['ANSIBLE_CONFIG'] + monkeypatch.delenv('ANSIBLE_CONFIG') else: - os.environ['ANSIBLE_CONFIG'] = request.param[0] + monkeypatch.setenv('ANSIBLE_CONFIG', request.param[0]) yield - if cur_config is None and cfg_path: - del os.environ['ANSIBLE_CONFIG'] - else: - os.environ['ANSIBLE_CONFIG'] = cur_config - @pytest.fixture def setup_existing_files(request, monkeypatch): @@ -54,10 +49,8 @@ def setup_existing_files(request, monkeypatch): return False def _os_access(path, access): - if to_text(path) in (request.param[0]): - return True - else: - return False + assert to_text(path) in (request.param[0]) + return True # Enable user and system dirs so that we know cwd takes precedence monkeypatch.setattr("os.path.exists", _os_path_exists) @@ -162,13 +155,11 @@ class TestFindIniFile: real_stat = os.stat def _os_stat(path): - if path == working_dir: - from posix import stat_result - stat_info = list(real_stat(path)) - stat_info[stat.ST_MODE] |= stat.S_IWOTH - return stat_result(stat_info) - else: - return real_stat(path) + assert path == working_dir + from posix import stat_result + stat_info = list(real_stat(path)) + stat_info[stat.ST_MODE] |= stat.S_IWOTH + return stat_result(stat_info) monkeypatch.setattr('os.stat', _os_stat) @@ -187,13 +178,11 @@ class TestFindIniFile: real_stat = os.stat def _os_stat(path): - if path == working_dir: - from posix import stat_result - stat_info = list(real_stat(path)) - stat_info[stat.ST_MODE] |= stat.S_IWOTH - return stat_result(stat_info) - else: - return real_stat(path) + assert path == working_dir + from posix import stat_result + stat_info = list(real_stat(path)) + stat_info[stat.ST_MODE] |= stat.S_IWOTH + return stat_result(stat_info) monkeypatch.setattr('os.stat', _os_stat) @@ -215,14 +204,14 @@ class TestFindIniFile: real_stat = os.stat def _os_stat(path): - if path == working_dir: - from posix import stat_result - stat_info = list(real_stat(path)) - stat_info[stat.ST_MODE] |= stat.S_IWOTH - return stat_result(stat_info) - else: + if path != working_dir: return real_stat(path) + from posix import stat_result + stat_info = list(real_stat(path)) + stat_info[stat.ST_MODE] |= stat.S_IWOTH + return stat_result(stat_info) + monkeypatch.setattr('os.stat', _os_stat) warnings = set() @@ -240,13 +229,11 @@ class TestFindIniFile: real_stat = os.stat def _os_stat(path): - if path == working_dir: - from posix import stat_result - stat_info = list(real_stat(path)) - stat_info[stat.ST_MODE] |= stat.S_IWOTH - return stat_result(stat_info) - else: - return real_stat(path) + assert path == working_dir + from posix import stat_result + stat_info = list(real_stat(path)) + stat_info[stat.ST_MODE] |= stat.S_IWOTH + return stat_result(stat_info) monkeypatch.setattr('os.stat', _os_stat) diff --git a/test/units/config/test_manager.py b/test/units/config/test_manager.py index 8ef40437..0848276c 100644 --- a/test/units/config/test_manager.py +++ b/test/units/config/test_manager.py @@ -10,7 +10,7 @@ import os import os.path import pytest -from ansible.config.manager import ConfigManager, Setting, ensure_type, resolve_path, get_config_type +from ansible.config.manager import ConfigManager, ensure_type, resolve_path, get_config_type from ansible.errors import AnsibleOptionsError, AnsibleError from ansible.module_utils.six import integer_types, string_types from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode @@ -18,6 +18,7 @@ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode curdir = os.path.dirname(__file__) cfg_file = os.path.join(curdir, 'test.cfg') cfg_file2 = os.path.join(curdir, 'test2.cfg') +cfg_file3 = os.path.join(curdir, 'test3.cfg') ensure_test_data = [ ('a,b', 'list', list), @@ -65,6 +66,15 @@ ensure_test_data = [ ('None', 'none', type(None)) ] +ensure_unquoting_test_data = [ + ('"value"', '"value"', 'str', 'env'), + ('"value"', '"value"', 'str', 'yaml'), + ('"value"', 'value', 'str', 'ini'), + ('\'value\'', 'value', 'str', 'ini'), + ('\'\'value\'\'', '\'value\'', 'str', 'ini'), + ('""value""', '"value"', 'str', 'ini') +] + class TestConfigManager: @classmethod @@ -79,6 +89,11 @@ class TestConfigManager: def test_ensure_type(self, value, expected_type, python_type): assert isinstance(ensure_type(value, expected_type), python_type) + @pytest.mark.parametrize("value, expected_value, value_type, origin", ensure_unquoting_test_data) + def test_ensure_type_unquoting(self, value, expected_value, value_type, origin): + actual_value = ensure_type(value, value_type, origin) + assert actual_value == expected_value + def test_resolve_path(self): assert os.path.join(curdir, 'test.yml') == resolve_path('./test.yml', cfg_file) @@ -142,3 +157,16 @@ class TestConfigManager: actual_value = ensure_type(vault_var, value_type) assert actual_value == "vault text" + + +@pytest.mark.parametrize(("key", "expected_value"), ( + ("COLOR_UNREACHABLE", "bright red"), + ("COLOR_VERBOSE", "rgb013"), + ("COLOR_DEBUG", "gray10"))) +def test_256color_support(key, expected_value): + # GIVEN: a config file containing 256-color values with default definitions + manager = ConfigManager(cfg_file3) + # WHEN: get config values + actual_value = manager.get_config_value(key) + # THEN: no error + assert actual_value == expected_value diff --git a/test/units/executor/module_common/test_modify_module.py b/test/units/executor/module_common/test_modify_module.py index dceef763..89e4a163 100644 --- a/test/units/executor/module_common/test_modify_module.py +++ b/test/units/executor/module_common/test_modify_module.py @@ -8,9 +8,6 @@ __metaclass__ = type import pytest from ansible.executor.module_common import modify_module -from ansible.module_utils.six import PY2 - -from test_module_common import templar FAKE_OLD_MODULE = b'''#!/usr/bin/python @@ -22,10 +19,7 @@ print('{"result": "%s"}' % sys.executable) @pytest.fixture def fake_old_module_open(mocker): m = mocker.mock_open(read_data=FAKE_OLD_MODULE) - if PY2: - mocker.patch('__builtin__.open', m) - else: - mocker.patch('builtins.open', m) + mocker.patch('builtins.open', m) # this test no longer makes sense, since a Python module will always either have interpreter discovery run or # an explicit interpreter passed (so we'll never default to the module shebang) diff --git a/test/units/executor/module_common/test_module_common.py b/test/units/executor/module_common/test_module_common.py index fa6add8c..6e2a4956 100644 --- a/test/units/executor/module_common/test_module_common.py +++ b/test/units/executor/module_common/test_module_common.py @@ -27,7 +27,6 @@ import ansible.errors from ansible.executor import module_common as amc from ansible.executor.interpreter_discovery import InterpreterDiscoveryRequiredError -from ansible.module_utils.six import PY2 class TestStripComments: @@ -44,15 +43,16 @@ class TestStripComments: assert amc._strip_comments(all_comments) == u"" def test_all_whitespace(self): - # Note: Do not remove the spaces on the blank lines below. They're - # test data to show that the lines get removed despite having spaces - # on them - all_whitespace = u""" - - - -\t\t\r\n - """ # nopep8 + all_whitespace = ( + '\n' + ' \n' + '\n' + ' \n' + '\t\t\r\n' + '\n' + ' ' + ) + assert amc._strip_comments(all_whitespace) == u"" def test_somewhat_normal(self): @@ -80,31 +80,16 @@ class TestSlurp: def test_slurp_file(self, mocker): mocker.patch('os.path.exists', side_effect=lambda x: True) m = mocker.mock_open(read_data='This is a test') - if PY2: - mocker.patch('__builtin__.open', m) - else: - mocker.patch('builtins.open', m) + mocker.patch('builtins.open', m) assert amc._slurp('some_file') == 'This is a test' def test_slurp_file_with_newlines(self, mocker): mocker.patch('os.path.exists', side_effect=lambda x: True) m = mocker.mock_open(read_data='#!/usr/bin/python\ndef test(args):\nprint("hi")\n') - if PY2: - mocker.patch('__builtin__.open', m) - else: - mocker.patch('builtins.open', m) + mocker.patch('builtins.open', m) assert amc._slurp('some_file') == '#!/usr/bin/python\ndef test(args):\nprint("hi")\n' -@pytest.fixture -def templar(): - class FakeTemplar: - def template(self, template_string, *args, **kwargs): - return template_string - - return FakeTemplar() - - class TestGetShebang: """Note: We may want to change the API of this function in the future. It isn't a great API""" def test_no_interpreter_set(self, templar): diff --git a/test/units/executor/module_common/test_recursive_finder.py b/test/units/executor/module_common/test_recursive_finder.py index 8136a006..95b49d35 100644 --- a/test/units/executor/module_common/test_recursive_finder.py +++ b/test/units/executor/module_common/test_recursive_finder.py @@ -29,7 +29,7 @@ from io import BytesIO import ansible.errors from ansible.executor.module_common import recursive_finder - +from ansible.plugins.loader import init_plugin_loader # These are the modules that are brought in by module_utils/basic.py This may need to be updated # when basic.py gains new imports @@ -42,7 +42,6 @@ MODULE_UTILS_BASIC_FILES = frozenset(('ansible/__init__.py', 'ansible/module_utils/basic.py', 'ansible/module_utils/six/__init__.py', 'ansible/module_utils/_text.py', - 'ansible/module_utils/common/_collections_compat.py', 'ansible/module_utils/common/_json_compat.py', 'ansible/module_utils/common/collections.py', 'ansible/module_utils/common/parameters.py', @@ -79,6 +78,8 @@ ANSIBLE_LIB = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.pa @pytest.fixture def finder_containers(): + init_plugin_loader() + FinderContainers = namedtuple('FinderContainers', ['zf']) zipoutput = BytesIO() diff --git a/test/units/executor/test_interpreter_discovery.py b/test/units/executor/test_interpreter_discovery.py index 43db5950..10fc64be 100644 --- a/test/units/executor/test_interpreter_discovery.py +++ b/test/units/executor/test_interpreter_discovery.py @@ -9,7 +9,7 @@ __metaclass__ = type from unittest.mock import MagicMock from ansible.executor.interpreter_discovery import discover_interpreter -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text mock_ubuntu_platform_res = to_text( r'{"osrelease_content": "NAME=\"Ubuntu\"\nVERSION=\"16.04.5 LTS (Xenial Xerus)\"\nID=ubuntu\nID_LIKE=debian\n' @@ -20,7 +20,7 @@ mock_ubuntu_platform_res = to_text( def test_discovery_interpreter_linux_auto_legacy(): - res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND' + res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND' mock_action = MagicMock() mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}] @@ -35,7 +35,7 @@ def test_discovery_interpreter_linux_auto_legacy(): def test_discovery_interpreter_linux_auto_legacy_silent(): - res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND' + res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND' mock_action = MagicMock() mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}] @@ -47,7 +47,7 @@ def test_discovery_interpreter_linux_auto_legacy_silent(): def test_discovery_interpreter_linux_auto(): - res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND' + res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND' mock_action = MagicMock() mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}] diff --git a/test/units/executor/test_play_iterator.py b/test/units/executor/test_play_iterator.py index 6670888e..0fc59756 100644 --- a/test/units/executor/test_play_iterator.py +++ b/test/units/executor/test_play_iterator.py @@ -25,6 +25,7 @@ from unittest.mock import patch, MagicMock from ansible.executor.play_iterator import HostState, PlayIterator, IteratingStates, FailedStates from ansible.playbook import Playbook from ansible.playbook.play_context import PlayContext +from ansible.plugins.loader import init_plugin_loader from units.mock.loader import DictDataLoader from units.mock.path import mock_unfrackpath_noop @@ -85,7 +86,8 @@ class TestPlayIterator(unittest.TestCase): always: - name: role always task debug: msg="always task in block in role" - - include: foo.yml + - name: role include_tasks + include_tasks: foo.yml - name: role task after include debug: msg="after include in role" - block: @@ -170,12 +172,12 @@ class TestPlayIterator(unittest.TestCase): self.assertIsNotNone(task) self.assertEqual(task.name, "role always task") self.assertIsNotNone(task._role) - # role include task - # (host_state, task) = itr.get_next_task_for_host(hosts[0]) - # self.assertIsNotNone(task) - # self.assertEqual(task.action, 'debug') - # self.assertEqual(task.name, "role included task") - # self.assertIsNotNone(task._role) + # role include_tasks + (host_state, task) = itr.get_next_task_for_host(hosts[0]) + self.assertIsNotNone(task) + self.assertEqual(task.action, 'include_tasks') + self.assertEqual(task.name, "role include_tasks") + self.assertIsNotNone(task._role) # role task after include (host_state, task) = itr.get_next_task_for_host(hosts[0]) self.assertIsNotNone(task) @@ -286,6 +288,7 @@ class TestPlayIterator(unittest.TestCase): self.assertNotIn(hosts[0], failed_hosts) def test_play_iterator_nested_blocks(self): + init_plugin_loader() fake_loader = DictDataLoader({ "test_play.yml": """ - hosts: all @@ -427,12 +430,11 @@ class TestPlayIterator(unittest.TestCase): ) # iterate past first task - _, task = itr.get_next_task_for_host(hosts[0]) + dummy, task = itr.get_next_task_for_host(hosts[0]) while (task and task.action != 'debug'): - _, task = itr.get_next_task_for_host(hosts[0]) + dummy, task = itr.get_next_task_for_host(hosts[0]) - if task is None: - raise Exception("iterated past end of play while looking for place to insert tasks") + self.assertIsNotNone(task, 'iterated past end of play while looking for place to insert tasks') # get the current host state and copy it so we can mutate it s = itr.get_host_state(hosts[0]) diff --git a/test/units/executor/test_task_executor.py b/test/units/executor/test_task_executor.py index 315d26ae..66ab0036 100644 --- a/test/units/executor/test_task_executor.py +++ b/test/units/executor/test_task_executor.py @@ -25,7 +25,7 @@ from units.compat import unittest from unittest.mock import patch, MagicMock from ansible.errors import AnsibleError from ansible.executor.task_executor import TaskExecutor, remove_omit -from ansible.plugins.loader import action_loader, lookup_loader, module_loader +from ansible.plugins.loader import action_loader, lookup_loader from ansible.parsing.yaml.objects import AnsibleUnicode from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes from ansible.module_utils.six import text_type @@ -57,6 +57,7 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=mock_shared_loader, final_q=mock_queue, + variable_manager=MagicMock(), ) def test_task_executor_run(self): @@ -84,6 +85,7 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=mock_shared_loader, final_q=mock_queue, + variable_manager=MagicMock(), ) te._get_loop_items = MagicMock(return_value=None) @@ -102,7 +104,7 @@ class TestTaskExecutor(unittest.TestCase): self.assertIn("failed", res) def test_task_executor_run_clean_res(self): - te = TaskExecutor(None, MagicMock(), None, None, None, None, None, None) + te = TaskExecutor(None, MagicMock(), None, None, None, None, None, None, None) te._get_loop_items = MagicMock(return_value=[1]) te._run_loop = MagicMock( return_value=[ @@ -150,6 +152,7 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=mock_shared_loader, final_q=mock_queue, + variable_manager=MagicMock(), ) items = te._get_loop_items() @@ -186,6 +189,7 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=mock_shared_loader, final_q=mock_queue, + variable_manager=MagicMock(), ) def _execute(variables): @@ -206,6 +210,7 @@ class TestTaskExecutor(unittest.TestCase): loader=DictDataLoader({}), shared_loader_obj=MagicMock(), final_q=MagicMock(), + variable_manager=MagicMock(), ) context = MagicMock(resolved=False) @@ -214,20 +219,20 @@ class TestTaskExecutor(unittest.TestCase): action_loader.has_plugin.return_value = True action_loader.get.return_value = mock.sentinel.handler - mock_connection = MagicMock() mock_templar = MagicMock() action = 'namespace.prefix_suffix' te._task.action = action + te._connection = MagicMock() - handler = te._get_action_handler(mock_connection, mock_templar) + with patch('ansible.executor.task_executor.start_connection'): + handler = te._get_action_handler(mock_templar) self.assertIs(mock.sentinel.handler, handler) - action_loader.has_plugin.assert_called_once_with( - action, collection_list=te._task.collections) + action_loader.has_plugin.assert_called_once_with(action, collection_list=te._task.collections) - action_loader.get.assert_called_once_with( - te._task.action, task=te._task, connection=mock_connection, + action_loader.get.assert_called_with( + te._task.action, task=te._task, connection=te._connection, play_context=te._play_context, loader=te._loader, templar=mock_templar, shared_loader_obj=te._shared_loader_obj, collection_list=te._task.collections) @@ -242,6 +247,7 @@ class TestTaskExecutor(unittest.TestCase): loader=DictDataLoader({}), shared_loader_obj=MagicMock(), final_q=MagicMock(), + variable_manager=MagicMock(), ) context = MagicMock(resolved=False) @@ -251,20 +257,21 @@ class TestTaskExecutor(unittest.TestCase): action_loader.get.return_value = mock.sentinel.handler action_loader.__contains__.return_value = True - mock_connection = MagicMock() mock_templar = MagicMock() action = 'namespace.netconf_suffix' module_prefix = action.split('_', 1)[0] te._task.action = action + te._connection = MagicMock() - handler = te._get_action_handler(mock_connection, mock_templar) + with patch('ansible.executor.task_executor.start_connection'): + handler = te._get_action_handler(mock_templar) self.assertIs(mock.sentinel.handler, handler) action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections), # called twice mock.call(module_prefix, collection_list=te._task.collections)]) - action_loader.get.assert_called_once_with( - module_prefix, task=te._task, connection=mock_connection, + action_loader.get.assert_called_with( + module_prefix, task=te._task, connection=te._connection, play_context=te._play_context, loader=te._loader, templar=mock_templar, shared_loader_obj=te._shared_loader_obj, collection_list=te._task.collections) @@ -279,6 +286,7 @@ class TestTaskExecutor(unittest.TestCase): loader=DictDataLoader({}), shared_loader_obj=MagicMock(), final_q=MagicMock(), + variable_manager=MagicMock(), ) action_loader = te._shared_loader_obj.action_loader @@ -289,20 +297,22 @@ class TestTaskExecutor(unittest.TestCase): context = MagicMock(resolved=False) module_loader.find_plugin_with_context.return_value = context - mock_connection = MagicMock() mock_templar = MagicMock() action = 'namespace.prefix_suffix' module_prefix = action.split('_', 1)[0] te._task.action = action - handler = te._get_action_handler(mock_connection, mock_templar) + te._connection = MagicMock() + + with patch('ansible.executor.task_executor.start_connection'): + handler = te._get_action_handler(mock_templar) self.assertIs(mock.sentinel.handler, handler) action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections), mock.call(module_prefix, collection_list=te._task.collections)]) - action_loader.get.assert_called_once_with( - 'ansible.legacy.normal', task=te._task, connection=mock_connection, + action_loader.get.assert_called_with( + 'ansible.legacy.normal', task=te._task, connection=te._connection, play_context=te._play_context, loader=te._loader, templar=mock_templar, shared_loader_obj=te._shared_loader_obj, collection_list=None) @@ -318,6 +328,7 @@ class TestTaskExecutor(unittest.TestCase): mock_task.become = False mock_task.retries = 0 mock_task.delay = -1 + mock_task.delegate_to = None mock_task.register = 'foo' mock_task.until = None mock_task.changed_when = None @@ -329,6 +340,7 @@ class TestTaskExecutor(unittest.TestCase): # other reason is that if I specify 0 here, the test fails. ;) mock_task.async_val = 1 mock_task.poll = 0 + mock_task.evaluate_conditional_with_result.return_value = (True, None) mock_play_context = MagicMock() mock_play_context.post_validate.return_value = None @@ -343,6 +355,9 @@ class TestTaskExecutor(unittest.TestCase): mock_action = MagicMock() mock_queue = MagicMock() + mock_vm = MagicMock() + mock_vm.get_delegated_vars_and_hostname.return_value = {}, None + shared_loader = MagicMock() new_stdin = None job_vars = dict(omit="XXXXXXXXXXXXXXXXXXX") @@ -356,11 +371,14 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=shared_loader, final_q=mock_queue, + variable_manager=mock_vm, ) te._get_connection = MagicMock(return_value=mock_connection) context = MagicMock() - te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context)) + + with patch('ansible.executor.task_executor.start_connection'): + te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context)) mock_action.run.return_value = dict(ansible_facts=dict()) res = te._execute() @@ -392,8 +410,6 @@ class TestTaskExecutor(unittest.TestCase): mock_play_context = MagicMock() - mock_connection = MagicMock() - mock_action = MagicMock() mock_queue = MagicMock() @@ -412,6 +428,7 @@ class TestTaskExecutor(unittest.TestCase): loader=fake_loader, shared_loader_obj=shared_loader, final_q=mock_queue, + variable_manager=MagicMock(), ) te._connection = MagicMock() diff --git a/test/units/galaxy/test_api.py b/test/units/galaxy/test_api.py index 064aff29..b019f1aa 100644 --- a/test/units/galaxy/test_api.py +++ b/test/units/galaxy/test_api.py @@ -24,7 +24,7 @@ from ansible.errors import AnsibleError from ansible.galaxy import api as galaxy_api from ansible.galaxy.api import CollectionVersionMetadata, GalaxyAPI, GalaxyError from ansible.galaxy.token import BasicAuthToken, GalaxyToken, KeycloakToken -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.six.moves.urllib import error as urllib_error from ansible.utils import context_objects as co from ansible.utils.display import Display @@ -463,10 +463,9 @@ def test_publish_failure(api_version, collection_url, response, expected, collec def test_wait_import_task(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch): api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins) - if token_ins: - mock_token_get = MagicMock() - mock_token_get.return_value = 'my token' - monkeypatch.setattr(token_ins, 'get', mock_token_get) + mock_token_get = MagicMock() + mock_token_get.return_value = 'my token' + monkeypatch.setattr(token_ins, 'get', mock_token_get) mock_open = MagicMock() mock_open.return_value = StringIO(u'{"state":"success","finished_at":"time"}') @@ -496,10 +495,9 @@ def test_wait_import_task(server_url, api_version, token_type, token_ins, import def test_wait_import_task_multiple_requests(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch): api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins) - if token_ins: - mock_token_get = MagicMock() - mock_token_get.return_value = 'my token' - monkeypatch.setattr(token_ins, 'get', mock_token_get) + mock_token_get = MagicMock() + mock_token_get.return_value = 'my token' + monkeypatch.setattr(token_ins, 'get', mock_token_get) mock_open = MagicMock() mock_open.side_effect = [ @@ -543,10 +541,9 @@ def test_wait_import_task_multiple_requests(server_url, api_version, token_type, def test_wait_import_task_with_failure(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch): api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins) - if token_ins: - mock_token_get = MagicMock() - mock_token_get.return_value = 'my token' - monkeypatch.setattr(token_ins, 'get', mock_token_get) + mock_token_get = MagicMock() + mock_token_get.return_value = 'my token' + monkeypatch.setattr(token_ins, 'get', mock_token_get) mock_open = MagicMock() mock_open.side_effect = [ @@ -620,10 +617,9 @@ def test_wait_import_task_with_failure(server_url, api_version, token_type, toke def test_wait_import_task_with_failure_no_error(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch): api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins) - if token_ins: - mock_token_get = MagicMock() - mock_token_get.return_value = 'my token' - monkeypatch.setattr(token_ins, 'get', mock_token_get) + mock_token_get = MagicMock() + mock_token_get.return_value = 'my token' + monkeypatch.setattr(token_ins, 'get', mock_token_get) mock_open = MagicMock() mock_open.side_effect = [ @@ -693,10 +689,9 @@ def test_wait_import_task_with_failure_no_error(server_url, api_version, token_t def test_wait_import_task_timeout(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch): api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins) - if token_ins: - mock_token_get = MagicMock() - mock_token_get.return_value = 'my token' - monkeypatch.setattr(token_ins, 'get', mock_token_get) + mock_token_get = MagicMock() + mock_token_get.return_value = 'my token' + monkeypatch.setattr(token_ins, 'get', mock_token_get) def return_response(*args, **kwargs): return StringIO(u'{"state":"waiting"}') diff --git a/test/units/galaxy/test_collection.py b/test/units/galaxy/test_collection.py index 106251c5..991184ae 100644 --- a/test/units/galaxy/test_collection.py +++ b/test/units/galaxy/test_collection.py @@ -20,10 +20,11 @@ from unittest.mock import MagicMock, mock_open, patch import ansible.constants as C from ansible import context -from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF +from ansible.cli import galaxy +from ansible.cli.galaxy import GalaxyCLI from ansible.errors import AnsibleError from ansible.galaxy import api, collection, token -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.six.moves import builtins from ansible.utils import context_objects as co from ansible.utils.display import Display @@ -171,28 +172,6 @@ def manifest_info(manifest_template): @pytest.fixture() -def files_manifest_info(): - return { - "files": [ - { - "name": ".", - "ftype": "dir", - "chksum_type": None, - "chksum_sha256": None, - "format": 1 - }, - { - "name": "README.md", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "individual_file_checksum", - "format": 1 - } - ], - "format": 1} - - -@pytest.fixture() def manifest(manifest_info): b_data = to_bytes(json.dumps(manifest_info)) @@ -245,23 +224,19 @@ def test_cli_options(required_signature_count, valid, monkeypatch): { 'url': 'https://galaxy.ansible.com', 'validate_certs': 'False', - 'v3': 'False', }, # Expected server attributes { 'validate_certs': False, - '_available_api_versions': {}, }, ), ( { 'url': 'https://galaxy.ansible.com', 'validate_certs': 'True', - 'v3': 'True', }, { 'validate_certs': True, - '_available_api_versions': {'v3': '/v3'}, }, ), ], @@ -279,7 +254,6 @@ def test_bool_type_server_config_options(config, server, monkeypatch): "server_list=server1\n", "[galaxy_server.server1]", "url=%s" % config['url'], - "v3=%s" % config['v3'], "validate_certs=%s\n" % config['validate_certs'], ] @@ -299,7 +273,6 @@ def test_bool_type_server_config_options(config, server, monkeypatch): assert galaxy_cli.api_servers[0].name == 'server1' assert galaxy_cli.api_servers[0].validate_certs == server['validate_certs'] - assert galaxy_cli.api_servers[0]._available_api_versions == server['_available_api_versions'] @pytest.mark.parametrize('global_ignore_certs', [True, False]) @@ -411,6 +384,55 @@ def test_validate_certs_server_config(ignore_certs_cfg, ignore_certs_cli, expect assert galaxy_cli.api_servers[2].validate_certs is expected_server3_validate_certs +@pytest.mark.parametrize( + ["timeout_cli", "timeout_cfg", "timeout_fallback", "expected_timeout"], + [ + (None, None, None, 60), + (None, None, 10, 10), + (None, 20, 10, 20), + (30, 20, 10, 30), + ] +) +def test_timeout_server_config(timeout_cli, timeout_cfg, timeout_fallback, expected_timeout, monkeypatch): + cli_args = [ + 'ansible-galaxy', + 'collection', + 'install', + 'namespace.collection:1.0.0', + ] + if timeout_cli is not None: + cli_args.extend(["--timeout", f"{timeout_cli}"]) + + cfg_lines = ["[galaxy]", "server_list=server1"] + if timeout_fallback is not None: + cfg_lines.append(f"server_timeout={timeout_fallback}") + + # fix default in server config since C.GALAXY_SERVER_TIMEOUT was already evaluated + server_additional = galaxy.SERVER_ADDITIONAL.copy() + server_additional['timeout']['default'] = timeout_fallback + monkeypatch.setattr(galaxy, 'SERVER_ADDITIONAL', server_additional) + + cfg_lines.extend(["[galaxy_server.server1]", "url=https://galaxy.ansible.com/api/"]) + if timeout_cfg is not None: + cfg_lines.append(f"timeout={timeout_cfg}") + + monkeypatch.setattr(C, 'GALAXY_SERVER_LIST', ['server1']) + + with tempfile.NamedTemporaryFile(suffix='.cfg') as tmp_file: + tmp_file.write(to_bytes('\n'.join(cfg_lines), errors='surrogate_or_strict')) + tmp_file.flush() + + monkeypatch.setattr(C.config, '_config_file', tmp_file.name) + C.config._parse_config_file() + + galaxy_cli = GalaxyCLI(args=cli_args) + mock_execute_install = MagicMock() + monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install) + galaxy_cli.run() + + assert galaxy_cli.api_servers[0].timeout == expected_timeout + + def test_build_collection_no_galaxy_yaml(): fake_path = u'/fake/ÅÑŚÌβŁÈ/path' expected = to_native("The collection galaxy.yml path '%s/galaxy.yml' does not exist." % fake_path) @@ -479,19 +501,19 @@ def test_build_with_existing_files_and_manifest(collection_input): with tarfile.open(output_artifact, mode='r') as actual: members = actual.getmembers() - manifest_file = next(m for m in members if m.path == "MANIFEST.json") + manifest_file = [m for m in members if m.path == "MANIFEST.json"][0] manifest_file_obj = actual.extractfile(manifest_file.name) manifest_file_text = manifest_file_obj.read() manifest_file_obj.close() assert manifest_file_text != b'{"collection_info": {"version": "6.6.6"}, "version": 1}' - json_file = next(m for m in members if m.path == "MANIFEST.json") + json_file = [m for m in members if m.path == "MANIFEST.json"][0] json_file_obj = actual.extractfile(json_file.name) json_file_text = json_file_obj.read() json_file_obj.close() assert json_file_text != b'{"files": [], "format": 1}' - sub_manifest_file = next(m for m in members if m.path == "plugins/MANIFEST.json") + sub_manifest_file = [m for m in members if m.path == "plugins/MANIFEST.json"][0] sub_manifest_file_obj = actual.extractfile(sub_manifest_file.name) sub_manifest_file_text = sub_manifest_file_obj.read() sub_manifest_file_obj.close() @@ -618,7 +640,7 @@ def test_build_ignore_files_and_folders(collection_input, monkeypatch): tests_file.write('random') tests_file.flush() - actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel) + actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None) assert actual['format'] == 1 for manifest_entry in actual['files']: @@ -654,7 +676,7 @@ def test_build_ignore_older_release_in_root(collection_input, monkeypatch): file_obj.write('random') file_obj.flush() - actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel) + actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None) assert actual['format'] == 1 plugin_release_found = False @@ -682,7 +704,7 @@ def test_build_ignore_patterns(collection_input, monkeypatch): actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', ['*.md', 'plugins/action', 'playbooks/*.j2'], - Sentinel) + Sentinel, None) assert actual['format'] == 1 expected_missing = [ @@ -733,7 +755,7 @@ def test_build_ignore_symlink_target_outside_collection(collection_input, monkey link_path = os.path.join(input_dir, 'plugins', 'connection') os.symlink(outside_dir, link_path) - actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel) + actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None) for manifest_entry in actual['files']: assert manifest_entry['name'] != 'plugins/connection' @@ -757,7 +779,7 @@ def test_build_copy_symlink_target_inside_collection(collection_input): os.symlink(roles_target, roles_link) - actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel) + actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None) linked_entries = [e for e in actual['files'] if e['name'].startswith('playbooks/roles/linked')] assert len(linked_entries) == 1 @@ -790,11 +812,11 @@ def test_build_with_symlink_inside_collection(collection_input): with tarfile.open(output_artifact, mode='r') as actual: members = actual.getmembers() - linked_folder = next(m for m in members if m.path == 'playbooks/roles/linked') + linked_folder = [m for m in members if m.path == 'playbooks/roles/linked'][0] assert linked_folder.type == tarfile.SYMTYPE assert linked_folder.linkname == '../../roles/linked' - linked_file = next(m for m in members if m.path == 'docs/README.md') + linked_file = [m for m in members if m.path == 'docs/README.md'][0] assert linked_file.type == tarfile.SYMTYPE assert linked_file.linkname == '../README.md' @@ -802,7 +824,7 @@ def test_build_with_symlink_inside_collection(collection_input): actual_file = secure_hash_s(linked_file_obj.read()) linked_file_obj.close() - assert actual_file == '63444bfc766154e1bc7557ef6280de20d03fcd81' + assert actual_file == '08f24200b9fbe18903e7a50930c9d0df0b8d7da3' # shasum test/units/cli/test_data/collection_skeleton/README.md def test_publish_no_wait(galaxy_server, collection_artifact, monkeypatch): @@ -854,57 +876,6 @@ def test_publish_with_wait(galaxy_server, collection_artifact, monkeypatch): % galaxy_server.api_server -def test_find_existing_collections(tmp_path_factory, monkeypatch): - test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')) - concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) - collection1 = os.path.join(test_dir, 'namespace1', 'collection1') - collection2 = os.path.join(test_dir, 'namespace2', 'collection2') - fake_collection1 = os.path.join(test_dir, 'namespace3', 'collection3') - fake_collection2 = os.path.join(test_dir, 'namespace4') - os.makedirs(collection1) - os.makedirs(collection2) - os.makedirs(os.path.split(fake_collection1)[0]) - - open(fake_collection1, 'wb+').close() - open(fake_collection2, 'wb+').close() - - collection1_manifest = json.dumps({ - 'collection_info': { - 'namespace': 'namespace1', - 'name': 'collection1', - 'version': '1.2.3', - 'authors': ['Jordan Borean'], - 'readme': 'README.md', - 'dependencies': {}, - }, - 'format': 1, - }) - with open(os.path.join(collection1, 'MANIFEST.json'), 'wb') as manifest_obj: - manifest_obj.write(to_bytes(collection1_manifest)) - - mock_warning = MagicMock() - monkeypatch.setattr(Display, 'warning', mock_warning) - - actual = list(collection.find_existing_collections(test_dir, artifacts_manager=concrete_artifact_cm)) - - assert len(actual) == 2 - for actual_collection in actual: - if '%s.%s' % (actual_collection.namespace, actual_collection.name) == 'namespace1.collection1': - assert actual_collection.namespace == 'namespace1' - assert actual_collection.name == 'collection1' - assert actual_collection.ver == '1.2.3' - assert to_text(actual_collection.src) == collection1 - else: - assert actual_collection.namespace == 'namespace2' - assert actual_collection.name == 'collection2' - assert actual_collection.ver == '*' - assert to_text(actual_collection.src) == collection2 - - assert mock_warning.call_count == 1 - assert mock_warning.mock_calls[0][1][0] == "Collection at '%s' does not have a MANIFEST.json file, nor has it galaxy.yml: " \ - "cannot detect version." % to_text(collection2) - - def test_download_file(tmp_path_factory, monkeypatch): temp_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')) @@ -1111,7 +1082,7 @@ def test_verify_file_hash_deleted_file(manifest_info): with patch.object(collection.os.path, 'isfile', MagicMock(return_value=False)) as mock_isfile: collection._verify_file_hash(b'path/', 'file', digest, error_queue) - assert mock_isfile.called_once + mock_isfile.assert_called_once() assert len(error_queue) == 1 assert error_queue[0].installed is None @@ -1134,7 +1105,7 @@ def test_verify_file_hash_matching_hash(manifest_info): with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile: collection._verify_file_hash(b'path/', 'file', digest, error_queue) - assert mock_isfile.called_once + mock_isfile.assert_called_once() assert error_queue == [] @@ -1156,7 +1127,7 @@ def test_verify_file_hash_mismatching_hash(manifest_info): with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile: collection._verify_file_hash(b'path/', 'file', different_digest, error_queue) - assert mock_isfile.called_once + mock_isfile.assert_called_once() assert len(error_queue) == 1 assert error_queue[0].installed == digest diff --git a/test/units/galaxy/test_collection_install.py b/test/units/galaxy/test_collection_install.py index 2118f0ec..a61ae406 100644 --- a/test/units/galaxy/test_collection_install.py +++ b/test/units/galaxy/test_collection_install.py @@ -18,7 +18,6 @@ import yaml from io import BytesIO, StringIO from unittest.mock import MagicMock, patch -from unittest import mock import ansible.module_utils.six.moves.urllib.error as urllib_error @@ -27,7 +26,7 @@ from ansible.cli.galaxy import GalaxyCLI from ansible.errors import AnsibleError from ansible.galaxy import collection, api, dependency_resolution from ansible.galaxy.dependency_resolution.dataclasses import Candidate, Requirement -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.common.process import get_bin_path from ansible.utils import context_objects as co from ansible.utils.display import Display @@ -53,78 +52,6 @@ def call_galaxy_cli(args): co.GlobalCLIArgs._Singleton__instance = orig -def artifact_json(namespace, name, version, dependencies, server): - json_str = json.dumps({ - 'artifact': { - 'filename': '%s-%s-%s.tar.gz' % (namespace, name, version), - 'sha256': '2d76f3b8c4bab1072848107fb3914c345f71a12a1722f25c08f5d3f51f4ab5fd', - 'size': 1234, - }, - 'download_url': '%s/download/%s-%s-%s.tar.gz' % (server, namespace, name, version), - 'metadata': { - 'namespace': namespace, - 'name': name, - 'dependencies': dependencies, - }, - 'version': version - }) - return to_text(json_str) - - -def artifact_versions_json(namespace, name, versions, galaxy_api, available_api_versions=None): - results = [] - available_api_versions = available_api_versions or {} - api_version = 'v2' - if 'v3' in available_api_versions: - api_version = 'v3' - for version in versions: - results.append({ - 'href': '%s/api/%s/%s/%s/versions/%s/' % (galaxy_api.api_server, api_version, namespace, name, version), - 'version': version, - }) - - if api_version == 'v2': - json_str = json.dumps({ - 'count': len(versions), - 'next': None, - 'previous': None, - 'results': results - }) - - if api_version == 'v3': - response = {'meta': {'count': len(versions)}, - 'data': results, - 'links': {'first': None, - 'last': None, - 'next': None, - 'previous': None}, - } - json_str = json.dumps(response) - return to_text(json_str) - - -def error_json(galaxy_api, errors_to_return=None, available_api_versions=None): - errors_to_return = errors_to_return or [] - available_api_versions = available_api_versions or {} - - response = {} - - api_version = 'v2' - if 'v3' in available_api_versions: - api_version = 'v3' - - if api_version == 'v2': - assert len(errors_to_return) <= 1 - if errors_to_return: - response = errors_to_return[0] - - if api_version == 'v3': - response['errors'] = errors_to_return - - json_str = json.dumps(response) - return to_text(json_str) - - @pytest.fixture(autouse='function') def reset_cli_args(): co.GlobalCLIArgs._Singleton__instance = None @@ -371,6 +298,27 @@ def test_build_requirement_from_tar(collection_artifact): assert actual.ver == u'0.1.0' +def test_build_requirement_from_tar_url(tmp_path_factory): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + test_url = 'https://example.com/org/repo/sample.tar.gz' + expected = fr"^Failed to download collection tar from '{to_text(test_url)}'" + + with pytest.raises(AnsibleError, match=expected): + Requirement.from_requirement_dict({'name': test_url, 'type': 'url'}, concrete_artifact_cm) + + +def test_build_requirement_from_tar_url_wrong_type(tmp_path_factory): + test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) + concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False) + test_url = 'https://example.com/org/repo/sample.tar.gz' + expected = fr"^Unable to find collection artifact file at '{to_text(test_url)}'\.$" + + with pytest.raises(AnsibleError, match=expected): + # Specified wrong collection type for http URL + Requirement.from_requirement_dict({'name': test_url, 'type': 'file'}, concrete_artifact_cm) + + def test_build_requirement_from_tar_fail_not_tar(tmp_path_factory): test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input')) test_file = os.path.join(test_dir, b'fake.tar.gz') @@ -895,7 +843,8 @@ def test_install_collections_from_tar(collection_artifact, monkeypatch): concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] - collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False) + collection.install_collections( + requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set()) assert os.path.isdir(collection_path) @@ -919,57 +868,6 @@ def test_install_collections_from_tar(collection_artifact, monkeypatch): assert display_msgs[2] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" % to_text(collection_path) -def test_install_collections_existing_without_force(collection_artifact, monkeypatch): - collection_path, collection_tar = collection_artifact - temp_path = os.path.split(collection_tar)[0] - - mock_display = MagicMock() - monkeypatch.setattr(Display, 'display', mock_display) - - concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) - - assert os.path.isdir(collection_path) - - requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] - collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False) - - assert os.path.isdir(collection_path) - - actual_files = os.listdir(collection_path) - actual_files.sort() - assert actual_files == [b'README.md', b'docs', b'galaxy.yml', b'playbooks', b'plugins', b'roles', b'runme.sh'] - - # Filter out the progress cursor display calls. - display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] - assert len(display_msgs) == 1 - - assert display_msgs[0] == 'Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.' - - for msg in display_msgs: - assert 'WARNING' not in msg - - -def test_install_missing_metadata_warning(collection_artifact, monkeypatch): - collection_path, collection_tar = collection_artifact - temp_path = os.path.split(collection_tar)[0] - - mock_display = MagicMock() - monkeypatch.setattr(Display, 'display', mock_display) - - for file in [b'MANIFEST.json', b'galaxy.yml']: - b_path = os.path.join(collection_path, file) - if os.path.isfile(b_path): - os.unlink(b_path) - - concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) - requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] - collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False) - - display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1] - - assert 'WARNING' in display_msgs[0] - - # Makes sure we don't get stuck in some recursive loop @pytest.mark.parametrize('collection_artifact', [ {'ansible_namespace.collection': '>=0.0.1'}, @@ -984,7 +882,8 @@ def test_install_collection_with_circular_dependency(collection_artifact, monkey concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] - collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False) + collection.install_collections( + requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set()) assert os.path.isdir(collection_path) @@ -1021,7 +920,8 @@ def test_install_collection_with_no_dependency(collection_artifact, monkeypatch) concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False) requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)] - collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False) + collection.install_collections( + requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set()) assert os.path.isdir(collection_path) diff --git a/test/units/galaxy/test_role_install.py b/test/units/galaxy/test_role_install.py index 687fcac1..819ed186 100644 --- a/test/units/galaxy/test_role_install.py +++ b/test/units/galaxy/test_role_install.py @@ -7,6 +7,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import json import os import functools import pytest @@ -16,7 +17,7 @@ from io import StringIO from ansible import context from ansible.cli.galaxy import GalaxyCLI from ansible.galaxy import api, role, Galaxy -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.utils import context_objects as co @@ -24,7 +25,7 @@ def call_galaxy_cli(args): orig = co.GlobalCLIArgs._Singleton__instance co.GlobalCLIArgs._Singleton__instance = None try: - GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run() + return GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run() finally: co.GlobalCLIArgs._Singleton__instance = orig @@ -120,6 +121,22 @@ def test_role_download_github_no_download_url_for_version(init_mock_temp_file, m assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz' +@pytest.mark.parametrize( + 'state,rc', + [('SUCCESS', 0), ('FAILED', 1),] +) +def test_role_import(state, rc, mocker, galaxy_server, monkeypatch): + responses = [ + {"available_versions": {"v1": "v1/"}}, + {"results": [{'id': 12345, 'github_user': 'user', 'github_repo': 'role', 'github_reference': None, 'summary_fields': {'role': {'name': 'role'}}}]}, + {"results": [{'state': 'WAITING', 'id': 12345, 'summary_fields': {'task_messages': []}}]}, + {"results": [{'state': state, 'id': 12345, 'summary_fields': {'task_messages': []}}]}, + ] + mock_api = mocker.MagicMock(side_effect=[StringIO(json.dumps(rsp)) for rsp in responses]) + monkeypatch.setattr(api, 'open_url', mock_api) + assert call_galaxy_cli(['import', 'user', 'role']) == rc + + def test_role_download_url(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch): mock_api = mocker.MagicMock() mock_api.side_effect = [ diff --git a/test/units/galaxy/test_token.py b/test/units/galaxy/test_token.py index 24af3863..9fc12d46 100644 --- a/test/units/galaxy/test_token.py +++ b/test/units/galaxy/test_token.py @@ -13,7 +13,7 @@ from unittest.mock import MagicMock import ansible.constants as C from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF from ansible.galaxy.token import GalaxyToken, NoTokenSentinel -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text @pytest.fixture() diff --git a/test/units/inventory/test_host.py b/test/units/inventory/test_host.py index c8f47714..712ed302 100644 --- a/test/units/inventory/test_host.py +++ b/test/units/inventory/test_host.py @@ -69,10 +69,10 @@ class TestHost(unittest.TestCase): def test_equals_none(self): other = None - self.hostA == other - other == self.hostA - self.hostA != other - other != self.hostA + assert not (self.hostA == other) + assert not (other == self.hostA) + assert self.hostA != other + assert other != self.hostA self.assertNotEqual(self.hostA, other) def test_serialize(self): diff --git a/test/units/mock/loader.py b/test/units/mock/loader.py index f6ceb379..9dc32cae 100644 --- a/test/units/mock/loader.py +++ b/test/units/mock/loader.py @@ -21,16 +21,15 @@ __metaclass__ = type import os -from ansible.errors import AnsibleParserError from ansible.parsing.dataloader import DataLoader -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text class DictDataLoader(DataLoader): def __init__(self, file_mapping=None): file_mapping = {} if file_mapping is None else file_mapping - assert type(file_mapping) == dict + assert isinstance(file_mapping, dict) super(DictDataLoader, self).__init__() @@ -48,11 +47,7 @@ class DictDataLoader(DataLoader): # TODO: the real _get_file_contents returns a bytestring, so we actually convert the # unicode/text it's created with to utf-8 def _get_file_contents(self, file_name): - path = to_text(file_name) - if path in self._file_mapping: - return to_bytes(self._file_mapping[file_name]), False - else: - raise AnsibleParserError("file not found: %s" % file_name) + return to_bytes(self._file_mapping[file_name]), False def path_exists(self, path): path = to_text(path) @@ -91,25 +86,6 @@ class DictDataLoader(DataLoader): self._add_known_directory(dirname) dirname = os.path.dirname(dirname) - def push(self, path, content): - rebuild_dirs = False - if path not in self._file_mapping: - rebuild_dirs = True - - self._file_mapping[path] = content - - if rebuild_dirs: - self._build_known_directories() - - def pop(self, path): - if path in self._file_mapping: - del self._file_mapping[path] - self._build_known_directories() - - def clear(self): - self._file_mapping = dict() - self._known_directories = [] - def get_basedir(self): return os.getcwd() diff --git a/test/units/mock/procenv.py b/test/units/mock/procenv.py index 271a207e..1570c87e 100644 --- a/test/units/mock/procenv.py +++ b/test/units/mock/procenv.py @@ -27,7 +27,7 @@ from contextlib import contextmanager from io import BytesIO, StringIO from units.compat import unittest from ansible.module_utils.six import PY3 -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes @contextmanager @@ -54,30 +54,9 @@ def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): sys.argv = real_argv -@contextmanager -def swap_stdout(): - """ - context manager that temporarily replaces stdout for tests that need to verify output - """ - old_stdout = sys.stdout - - if PY3: - fake_stream = StringIO() - else: - fake_stream = BytesIO() - - try: - sys.stdout = fake_stream - - yield fake_stream - finally: - sys.stdout = old_stdout - - class ModuleTestCase(unittest.TestCase): - def setUp(self, module_args=None): - if module_args is None: - module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} + def setUp(self): + module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) diff --git a/test/units/mock/vault_helper.py b/test/units/mock/vault_helper.py index dcce9c78..5b2fdd2a 100644 --- a/test/units/mock/vault_helper.py +++ b/test/units/mock/vault_helper.py @@ -15,7 +15,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.parsing.vault import VaultSecret diff --git a/test/units/mock/yaml_helper.py b/test/units/mock/yaml_helper.py index 1ef17215..9f8b063b 100644 --- a/test/units/mock/yaml_helper.py +++ b/test/units/mock/yaml_helper.py @@ -4,8 +4,6 @@ __metaclass__ = type import io import yaml -from ansible.module_utils.six import PY3 -from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.dumper import AnsibleDumper @@ -15,21 +13,14 @@ class YamlTestUtils(object): """Vault related tests will want to override this. Vault cases should setup a AnsibleLoader that has the vault password.""" - return AnsibleLoader(stream) def _dump_stream(self, obj, stream, dumper=None): """Dump to a py2-unicode or py3-string stream.""" - if PY3: - return yaml.dump(obj, stream, Dumper=dumper) - else: - return yaml.dump(obj, stream, Dumper=dumper, encoding=None) + return yaml.dump(obj, stream, Dumper=dumper) def _dump_string(self, obj, dumper=None): """Dump to a py2-unicode or py3-string""" - if PY3: - return yaml.dump(obj, Dumper=dumper) - else: - return yaml.dump(obj, Dumper=dumper, encoding=None) + return yaml.dump(obj, Dumper=dumper) def _dump_load_cycle(self, obj): # Each pass though a dump or load revs the 'generation' @@ -62,63 +53,3 @@ class YamlTestUtils(object): # should be transitive, but... self.assertEqual(obj_2, obj_3) self.assertEqual(string_from_object_dump, string_from_object_dump_3) - - def _old_dump_load_cycle(self, obj): - '''Dump the passed in object to yaml, load it back up, dump again, compare.''' - stream = io.StringIO() - - yaml_string = self._dump_string(obj, dumper=AnsibleDumper) - self._dump_stream(obj, stream, dumper=AnsibleDumper) - - yaml_string_from_stream = stream.getvalue() - - # reset stream - stream.seek(0) - - loader = self._loader(stream) - # loader = AnsibleLoader(stream, vault_password=self.vault_password) - obj_from_stream = loader.get_data() - - stream_from_string = io.StringIO(yaml_string) - loader2 = self._loader(stream_from_string) - # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) - obj_from_string = loader2.get_data() - - stream_obj_from_stream = io.StringIO() - stream_obj_from_string = io.StringIO() - - if PY3: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) - else: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) - - yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() - yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() - - stream_obj_from_stream.seek(0) - stream_obj_from_string.seek(0) - - if PY3: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) - else: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) - - assert yaml_string == yaml_string_obj_from_stream - assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == - yaml_string_stream_obj_from_string) - assert obj == obj_from_stream - assert obj == obj_from_string - assert obj == yaml_string_obj_from_stream - assert obj == yaml_string_obj_from_string - assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - return {'obj': obj, - 'yaml_string': yaml_string, - 'yaml_string_from_stream': yaml_string_from_stream, - 'obj_from_stream': obj_from_stream, - 'obj_from_string': obj_from_string, - 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/test/units/module_utils/basic/test__symbolic_mode_to_octal.py b/test/units/module_utils/basic/test__symbolic_mode_to_octal.py index 7793b348..b3a73e5a 100644 --- a/test/units/module_utils/basic/test__symbolic_mode_to_octal.py +++ b/test/units/module_utils/basic/test__symbolic_mode_to_octal.py @@ -63,6 +63,14 @@ DATA = ( # Going from no permissions to setting all for user, group, and/or oth # Multiple permissions (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755), (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644), + (0o040000, u'ug=rx,o=', 0o0550), + (0o100000, u'ug=rx,o=', 0o0550), + (0o040000, u'u=rx,g=r', 0o0540), + (0o100000, u'u=rx,g=r', 0o0540), + (0o040777, u'ug=rx,o=', 0o0550), + (0o100777, u'ug=rx,o=', 0o0550), + (0o040777, u'u=rx,g=r', 0o0547), + (0o100777, u'u=rx,g=r', 0o0547), ) UMASK_DATA = ( diff --git a/test/units/module_utils/basic/test_argument_spec.py b/test/units/module_utils/basic/test_argument_spec.py index 211d65a2..5dbaf50c 100644 --- a/test/units/module_utils/basic/test_argument_spec.py +++ b/test/units/module_utils/basic/test_argument_spec.py @@ -453,7 +453,7 @@ class TestComplexOptions: 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}] ), # Check for elements in sub-options - ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3":['1.3', 1.3, 1]}]}, + ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3": ['1.3', 1.3, 1]}]}, [{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None, 'baz': None, 'bam': 'required_one_of', 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}] ), diff --git a/test/units/module_utils/basic/test_command_nonexisting.py b/test/units/module_utils/basic/test_command_nonexisting.py index 6ed7f91b..0dd3bd98 100644 --- a/test/units/module_utils/basic/test_command_nonexisting.py +++ b/test/units/module_utils/basic/test_command_nonexisting.py @@ -1,14 +1,11 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import sys -import pytest import json import sys import pytest import subprocess -import ansible.module_utils.basic -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils import basic diff --git a/test/units/module_utils/basic/test_filesystem.py b/test/units/module_utils/basic/test_filesystem.py index f09cecf4..50e674c4 100644 --- a/test/units/module_utils/basic/test_filesystem.py +++ b/test/units/module_utils/basic/test_filesystem.py @@ -143,6 +143,8 @@ class TestOtherFilesystem(ModuleTestCase): argument_spec=dict(), ) + am.selinux_enabled = lambda: False + file_args = { 'path': '/path/to/file', 'mode': None, diff --git a/test/units/module_utils/basic/test_run_command.py b/test/units/module_utils/basic/test_run_command.py index 04211e2d..259ac6c4 100644 --- a/test/units/module_utils/basic/test_run_command.py +++ b/test/units/module_utils/basic/test_run_command.py @@ -12,7 +12,7 @@ from io import BytesIO import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.six import PY2 from ansible.module_utils.compat import selectors @@ -109,7 +109,7 @@ def mock_subprocess(mocker): super(MockSelector, self).close() self._file_objs = [] - selectors.DefaultSelector = MockSelector + selectors.PollSelector = MockSelector subprocess = mocker.patch('ansible.module_utils.basic.subprocess') subprocess._output = {mocker.sentinel.stdout: SpecialBytesIO(b'', fh=mocker.sentinel.stdout), @@ -194,7 +194,7 @@ class TestRunCommandPrompt: @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) def test_prompt_no_match(self, mocker, rc_am): rc_am._os._cmd_out[mocker.sentinel.stdout] = BytesIO(b'hello') - (rc, _, _) = rc_am.run_command('foo', prompt_regex='[pP]assword:') + (rc, stdout, stderr) = rc_am.run_command('foo', prompt_regex='[pP]assword:') assert rc == 0 @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) @@ -204,7 +204,7 @@ class TestRunCommandPrompt: fh=mocker.sentinel.stdout), mocker.sentinel.stderr: SpecialBytesIO(b'', fh=mocker.sentinel.stderr)} - (rc, _, _) = rc_am.run_command('foo', prompt_regex=r'[pP]assword:', data=None) + (rc, stdout, stderr) = rc_am.run_command('foo', prompt_regex=r'[pP]assword:', data=None) assert rc == 257 @@ -212,7 +212,7 @@ class TestRunCommandRc: @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) def test_check_rc_false(self, rc_am): rc_am._subprocess.Popen.return_value.returncode = 1 - (rc, _, _) = rc_am.run_command('/bin/false', check_rc=False) + (rc, stdout, stderr) = rc_am.run_command('/bin/false', check_rc=False) assert rc == 1 @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) diff --git a/test/units/module_utils/basic/test_safe_eval.py b/test/units/module_utils/basic/test_safe_eval.py index e8538ca9..fdaab18a 100644 --- a/test/units/module_utils/basic/test_safe_eval.py +++ b/test/units/module_utils/basic/test_safe_eval.py @@ -67,4 +67,4 @@ def test_invalid_strings_with_exceptions(am, code, expected, exception): if exception is None: assert res[1] == exception else: - assert type(res[1]) == exception + assert isinstance(res[1], exception) diff --git a/test/units/module_utils/basic/test_sanitize_keys.py b/test/units/module_utils/basic/test_sanitize_keys.py index 180f8662..3edb216b 100644 --- a/test/units/module_utils/basic/test_sanitize_keys.py +++ b/test/units/module_utils/basic/test_sanitize_keys.py @@ -6,7 +6,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import pytest from ansible.module_utils.basic import sanitize_keys diff --git a/test/units/module_utils/basic/test_selinux.py b/test/units/module_utils/basic/test_selinux.py index d8557685..bdb6b9de 100644 --- a/test/units/module_utils/basic/test_selinux.py +++ b/test/units/module_utils/basic/test_selinux.py @@ -43,16 +43,21 @@ class TestSELinuxMU: with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_enabled() is False - # test selinux present/not-enabled - disabled_mod = no_args_module() - with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=0): - assert disabled_mod.selinux_enabled() is False + # test selinux present/not-enabled + disabled_mod = no_args_module() + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.is_selinux_enabled.return_value = 0 + assert disabled_mod.selinux_enabled() is False + # ensure value is cached (same answer after unpatching) assert disabled_mod.selinux_enabled() is False + # and present / enabled - enabled_mod = no_args_module() - with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=1): - assert enabled_mod.selinux_enabled() is True + with patch.object(basic, 'HAVE_SELINUX', True): + enabled_mod = no_args_module() + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.is_selinux_enabled.return_value = 1 + assert enabled_mod.selinux_enabled() is True # ensure value is cached (same answer after unpatching) assert enabled_mod.selinux_enabled() is True @@ -60,12 +65,16 @@ class TestSELinuxMU: # selinux unavailable, should return false with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_mls_enabled() is False - # selinux disabled, should return false - with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=0): - assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False - # selinux enabled, should pass through the value of is_selinux_mls_enabled - with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=1): - assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True + # selinux disabled, should return false + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.is_selinux_mls_enabled.return_value = 0 + assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False + + with patch.object(basic, 'HAVE_SELINUX', True): + # selinux enabled, should pass through the value of is_selinux_mls_enabled + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.is_selinux_mls_enabled.return_value = 1 + assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True def test_selinux_initial_context(self): # selinux missing/disabled/enabled sans MLS is 3-element None @@ -80,16 +89,19 @@ class TestSELinuxMU: assert no_args_module().selinux_default_context(path='/foo/bar') == [None, None, None] am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True) - # matchpathcon success - with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[0, 'unconfined_u:object_r:default_t:s0']): + with patch.object(basic, 'selinux', create=True) as selinux: + # matchpathcon success + selinux.matchpathcon.return_value = [0, 'unconfined_u:object_r:default_t:s0'] assert am.selinux_default_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0'] - # matchpathcon fail (return initial context value) - with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[-1, '']): + with patch.object(basic, 'selinux', create=True) as selinux: + # matchpathcon fail (return initial context value) + selinux.matchpathcon.return_value = [-1, ''] assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None] - # matchpathcon OSError - with patch('ansible.module_utils.compat.selinux.matchpathcon', side_effect=OSError): + with patch.object(basic, 'selinux', create=True) as selinux: + # matchpathcon OSError + selinux.matchpathcon.side_effect = OSError assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None] def test_selinux_context(self): @@ -99,19 +111,23 @@ class TestSELinuxMU: am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True) # lgetfilecon_raw passthru - with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[0, 'unconfined_u:object_r:default_t:s0']): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lgetfilecon_raw.return_value = [0, 'unconfined_u:object_r:default_t:s0'] assert am.selinux_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0'] # lgetfilecon_raw returned a failure - with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[-1, '']): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lgetfilecon_raw.return_value = [-1, ''] assert am.selinux_context(path='/foo/bar') == [None, None, None, None] # lgetfilecon_raw OSError (should bomb the module) - with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError(errno.ENOENT, 'NotFound')): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lgetfilecon_raw.side_effect = OSError(errno.ENOENT, 'NotFound') with pytest.raises(SystemExit): am.selinux_context(path='/foo/bar') - with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError()): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lgetfilecon_raw.side_effect = OSError() with pytest.raises(SystemExit): am.selinux_context(path='/foo/bar') @@ -166,25 +182,29 @@ class TestSELinuxMU: am.selinux_context = lambda path: ['bar_u', 'bar_r', None, None] am.is_special_selinux_path = lambda path: (False, None) - with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m: + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lsetfilecon.return_value = 0 assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True - m.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0') - m.reset_mock() + selinux.lsetfilecon.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0') + selinux.lsetfilecon.reset_mock() am.check_mode = True assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True - assert not m.called + assert not selinux.lsetfilecon.called am.check_mode = False - with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=1): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lsetfilecon.return_value = 1 with pytest.raises(SystemExit): am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) - with patch('ansible.module_utils.compat.selinux.lsetfilecon', side_effect=OSError): + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lsetfilecon.side_effect = OSError with pytest.raises(SystemExit): am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) am.is_special_selinux_path = lambda path: (True, ['sp_u', 'sp_r', 'sp_t', 's0']) - with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m: + with patch.object(basic, 'selinux', create=True) as selinux: + selinux.lsetfilecon.return_value = 0 assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True - m.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0') + selinux.lsetfilecon.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0') diff --git a/test/units/module_utils/basic/test_set_cwd.py b/test/units/module_utils/basic/test_set_cwd.py index 159236b7..c094c622 100644 --- a/test/units/module_utils/basic/test_set_cwd.py +++ b/test/units/module_utils/basic/test_set_cwd.py @@ -8,13 +8,10 @@ __metaclass__ = type import json import os -import shutil import tempfile -import pytest - -from units.compat.mock import patch, MagicMock -from ansible.module_utils._text import to_bytes +from units.compat.mock import patch +from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils import basic diff --git a/test/units/module_utils/basic/test_tmpdir.py b/test/units/module_utils/basic/test_tmpdir.py index 818cb9b1..ec12508b 100644 --- a/test/units/module_utils/basic/test_tmpdir.py +++ b/test/units/module_utils/basic/test_tmpdir.py @@ -14,7 +14,7 @@ import tempfile import pytest from units.compat.mock import patch, MagicMock -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils import basic diff --git a/test/units/module_utils/common/arg_spec/test_aliases.py b/test/units/module_utils/common/arg_spec/test_aliases.py index 7d30fb0f..7522c769 100644 --- a/test/units/module_utils/common/arg_spec/test_aliases.py +++ b/test/units/module_utils/common/arg_spec/test_aliases.py @@ -9,7 +9,6 @@ import pytest from ansible.module_utils.errors import AnsibleValidationError, AnsibleValidationErrorMultiple from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult -from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages # id, argument spec, parameters, expected parameters, deprecation, warning ALIAS_TEST_CASES = [ diff --git a/test/units/module_utils/common/parameters/test_handle_aliases.py b/test/units/module_utils/common/parameters/test_handle_aliases.py index e20a8882..6a8c2b2c 100644 --- a/test/units/module_utils/common/parameters/test_handle_aliases.py +++ b/test/units/module_utils/common/parameters/test_handle_aliases.py @@ -9,7 +9,7 @@ __metaclass__ = type import pytest from ansible.module_utils.common.parameters import _handle_aliases -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native def test_handle_aliases_no_aliases(): diff --git a/test/units/module_utils/common/parameters/test_list_deprecations.py b/test/units/module_utils/common/parameters/test_list_deprecations.py index 6f0bb71a..d667a2f0 100644 --- a/test/units/module_utils/common/parameters/test_list_deprecations.py +++ b/test/units/module_utils/common/parameters/test_list_deprecations.py @@ -5,21 +5,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import pytest from ansible.module_utils.common.parameters import _list_deprecations -@pytest.fixture -def params(): - return { - 'name': 'bob', - 'dest': '/etc/hosts', - 'state': 'present', - 'value': 5, - } - - def test_list_deprecations(): argument_spec = { 'old': {'type': 'str', 'removed_in_version': '2.5'}, diff --git a/test/units/module_utils/common/test_collections.py b/test/units/module_utils/common/test_collections.py index 95b2a402..8424502e 100644 --- a/test/units/module_utils/common/test_collections.py +++ b/test/units/module_utils/common/test_collections.py @@ -8,8 +8,7 @@ __metaclass__ = type import pytest -from ansible.module_utils.six import Iterator -from ansible.module_utils.common._collections_compat import Sequence +from ansible.module_utils.six.moves.collections_abc import Sequence from ansible.module_utils.common.collections import ImmutableDict, is_iterable, is_sequence @@ -25,16 +24,6 @@ class SeqStub: Sequence.register(SeqStub) -class IteratorStub(Iterator): - def __next__(self): - raise StopIteration - - -class IterableStub: - def __iter__(self): - return IteratorStub() - - class FakeAnsibleVaultEncryptedUnicode(Sequence): __ENCRYPTED__ = True @@ -42,10 +31,10 @@ class FakeAnsibleVaultEncryptedUnicode(Sequence): self.data = data def __getitem__(self, index): - return self.data[index] + raise NotImplementedError() # pragma: nocover def __len__(self): - return len(self.data) + raise NotImplementedError() # pragma: nocover TEST_STRINGS = u'he', u'Україна', u'Česká republika' @@ -93,14 +82,14 @@ def test_sequence_string_types_without_strings(string_input): @pytest.mark.parametrize( 'seq', - ([], (), {}, set(), frozenset(), IterableStub()), + ([], (), {}, set(), frozenset()), ) def test_iterable_positive(seq): assert is_iterable(seq) @pytest.mark.parametrize( - 'seq', (IteratorStub(), object(), 5, 9.) + 'seq', (object(), 5, 9.) ) def test_iterable_negative(seq): assert not is_iterable(seq) diff --git a/test/units/module_utils/common/text/converters/test_json_encode_fallback.py b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py index 022f38f4..808bf410 100644 --- a/test/units/module_utils/common/text/converters/test_json_encode_fallback.py +++ b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py @@ -20,12 +20,6 @@ class timezone(tzinfo): def utcoffset(self, dt): return self._offset - def dst(self, dt): - return timedelta(0) - - def tzname(self, dt): - return None - @pytest.mark.parametrize( 'test_input,expected', diff --git a/test/units/module_utils/common/validation/test_check_missing_parameters.py b/test/units/module_utils/common/validation/test_check_missing_parameters.py index 6cbcb8bf..364f9439 100644 --- a/test/units/module_utils/common/validation/test_check_missing_parameters.py +++ b/test/units/module_utils/common/validation/test_check_missing_parameters.py @@ -8,16 +8,10 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native -from ansible.module_utils.common.validation import check_required_one_of +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_missing_parameters -@pytest.fixture -def arguments_terms(): - return {"path": ""} - - def test_check_missing_parameters(): assert check_missing_parameters([], {}) == [] diff --git a/test/units/module_utils/common/validation/test_check_mutually_exclusive.py b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py index 7bf90760..acc67be8 100644 --- a/test/units/module_utils/common/validation/test_check_mutually_exclusive.py +++ b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_mutually_exclusive diff --git a/test/units/module_utils/common/validation/test_check_required_arguments.py b/test/units/module_utils/common/validation/test_check_required_arguments.py index 1dd54584..eb3d52e2 100644 --- a/test/units/module_utils/common/validation/test_check_required_arguments.py +++ b/test/units/module_utils/common/validation/test_check_required_arguments.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_required_arguments diff --git a/test/units/module_utils/common/validation/test_check_required_by.py b/test/units/module_utils/common/validation/test_check_required_by.py index 62cccff3..fcba0c14 100644 --- a/test/units/module_utils/common/validation/test_check_required_by.py +++ b/test/units/module_utils/common/validation/test_check_required_by.py @@ -8,7 +8,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_required_by diff --git a/test/units/module_utils/common/validation/test_check_required_if.py b/test/units/module_utils/common/validation/test_check_required_if.py index 4189164a..4590b05c 100644 --- a/test/units/module_utils/common/validation/test_check_required_if.py +++ b/test/units/module_utils/common/validation/test_check_required_if.py @@ -8,7 +8,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_required_if diff --git a/test/units/module_utils/common/validation/test_check_required_one_of.py b/test/units/module_utils/common/validation/test_check_required_one_of.py index b0818891..efdba537 100644 --- a/test/units/module_utils/common/validation/test_check_required_one_of.py +++ b/test/units/module_utils/common/validation/test_check_required_one_of.py @@ -8,7 +8,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_required_one_of diff --git a/test/units/module_utils/common/validation/test_check_required_together.py b/test/units/module_utils/common/validation/test_check_required_together.py index 8a2daab1..cf4626ab 100644 --- a/test/units/module_utils/common/validation/test_check_required_together.py +++ b/test/units/module_utils/common/validation/test_check_required_together.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_required_together diff --git a/test/units/module_utils/common/validation/test_check_type_bits.py b/test/units/module_utils/common/validation/test_check_type_bits.py index 7f6b11d3..aa91da94 100644 --- a/test/units/module_utils/common/validation/test_check_type_bits.py +++ b/test/units/module_utils/common/validation/test_check_type_bits.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_bits diff --git a/test/units/module_utils/common/validation/test_check_type_bool.py b/test/units/module_utils/common/validation/test_check_type_bool.py index bd867dc9..00b785f6 100644 --- a/test/units/module_utils/common/validation/test_check_type_bool.py +++ b/test/units/module_utils/common/validation/test_check_type_bool.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_bool diff --git a/test/units/module_utils/common/validation/test_check_type_bytes.py b/test/units/module_utils/common/validation/test_check_type_bytes.py index 6ff62dc2..c29e42f8 100644 --- a/test/units/module_utils/common/validation/test_check_type_bytes.py +++ b/test/units/module_utils/common/validation/test_check_type_bytes.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_bytes diff --git a/test/units/module_utils/common/validation/test_check_type_float.py b/test/units/module_utils/common/validation/test_check_type_float.py index 57837fae..a0218875 100644 --- a/test/units/module_utils/common/validation/test_check_type_float.py +++ b/test/units/module_utils/common/validation/test_check_type_float.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_float diff --git a/test/units/module_utils/common/validation/test_check_type_int.py b/test/units/module_utils/common/validation/test_check_type_int.py index 22cedf61..6f4dc6a2 100644 --- a/test/units/module_utils/common/validation/test_check_type_int.py +++ b/test/units/module_utils/common/validation/test_check_type_int.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_int diff --git a/test/units/module_utils/common/validation/test_check_type_jsonarg.py b/test/units/module_utils/common/validation/test_check_type_jsonarg.py index e78e54bb..d43bb035 100644 --- a/test/units/module_utils/common/validation/test_check_type_jsonarg.py +++ b/test/units/module_utils/common/validation/test_check_type_jsonarg.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_jsonarg diff --git a/test/units/module_utils/common/validation/test_check_type_str.py b/test/units/module_utils/common/validation/test_check_type_str.py index f10dad28..71af2a0b 100644 --- a/test/units/module_utils/common/validation/test_check_type_str.py +++ b/test/units/module_utils/common/validation/test_check_type_str.py @@ -7,7 +7,7 @@ __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.validation import check_type_str diff --git a/test/units/module_utils/conftest.py b/test/units/module_utils/conftest.py index 8bc13c4d..8e82bf2a 100644 --- a/test/units/module_utils/conftest.py +++ b/test/units/module_utils/conftest.py @@ -12,8 +12,8 @@ import pytest import ansible.module_utils.basic from ansible.module_utils.six import PY3, string_types -from ansible.module_utils._text import to_bytes -from ansible.module_utils.common._collections_compat import MutableMapping +from ansible.module_utils.common.text.converters import to_bytes +from ansible.module_utils.six.moves.collections_abc import MutableMapping @pytest.fixture diff --git a/test/units/module_utils/facts/base.py b/test/units/module_utils/facts/base.py index 33d3087b..3cada8f1 100644 --- a/test/units/module_utils/facts/base.py +++ b/test/units/module_utils/facts/base.py @@ -48,6 +48,9 @@ class BaseFactsTest(unittest.TestCase): @patch('platform.system', return_value='Linux') @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='systemd') def test_collect(self, mock_gfc, mock_ps): + self._test_collect() + + def _test_collect(self): module = self._mock_module() fact_collector = self.collector_class() facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts) @@ -62,4 +65,3 @@ class BaseFactsTest(unittest.TestCase): facts_dict = fact_collector.collect_with_namespace(module=module, collected_facts=self.collected_facts) self.assertIsInstance(facts_dict, dict) - return facts_dict diff --git a/test/units/module_utils/facts/hardware/linux_data.py b/test/units/module_utils/facts/hardware/linux_data.py index 3879188d..f92f14eb 100644 --- a/test/units/module_utils/facts/hardware/linux_data.py +++ b/test/units/module_utils/facts/hardware/linux_data.py @@ -18,6 +18,12 @@ __metaclass__ = type import os + +def read_lines(path): + with open(path) as file: + return file.readlines() + + LSBLK_OUTPUT = b""" /dev/sda /dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0 @@ -368,7 +374,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'armv61', 'nproc_out': 1, 'sched_getaffinity': set([0]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo')), 'expected_result': { 'processor': ['0', 'ARMv6-compatible processor rev 7 (v6l)'], 'processor_cores': 1, @@ -381,7 +387,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'armv71', 'nproc_out': 4, 'sched_getaffinity': set([0, 1, 2, 3]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'ARMv7 Processor rev 4 (v7l)', @@ -399,7 +405,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'aarch64', 'nproc_out': 4, 'sched_getaffinity': set([0, 1, 2, 3]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/aarch64-4cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/aarch64-4cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'AArch64 Processor rev 4 (aarch64)', @@ -417,7 +423,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'x86_64', 'nproc_out': 4, 'sched_getaffinity': set([0, 1, 2, 3]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-4cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-4cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216', @@ -435,7 +441,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'x86_64', 'nproc_out': 4, 'sched_getaffinity': set([0, 1, 2, 3]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-8cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-8cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz', @@ -457,7 +463,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'arm64', 'nproc_out': 4, 'sched_getaffinity': set([0, 1, 2, 3]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/arm64-4cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/arm64-4cpu-cpuinfo')), 'expected_result': { 'processor': ['0', '1', '2', '3'], 'processor_cores': 1, @@ -470,7 +476,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'armv71', 'nproc_out': 8, 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'ARMv7 Processor rev 3 (v7l)', @@ -492,7 +498,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'architecture': 'x86_64', 'nproc_out': 2, 'sched_getaffinity': set([0, 1]), - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-2cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-2cpu-cpuinfo')), 'expected_result': { 'processor': [ '0', 'GenuineIntel', 'Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz', @@ -505,7 +511,7 @@ CPU_INFO_TEST_SCENARIOS = [ 'processor_vcpus': 2}, }, { - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo')), 'architecture': 'ppc64', 'nproc_out': 8, 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]), @@ -528,7 +534,7 @@ CPU_INFO_TEST_SCENARIOS = [ }, }, { - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo')), 'architecture': 'ppc64le', 'nproc_out': 24, 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), @@ -567,7 +573,41 @@ CPU_INFO_TEST_SCENARIOS = [ }, }, { - 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu')).readlines(), + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/s390x-z13-2cpu-cpuinfo')), + 'architecture': 's390x', + 'nproc_out': 2, + 'sched_getaffinity': set([0, 1]), + 'expected_result': { + 'processor': [ + 'IBM/S390', + ], + 'processor_cores': 2, + 'processor_count': 1, + 'processor_nproc': 2, + 'processor_threads_per_core': 1, + 'processor_vcpus': 2 + }, + }, + { + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/s390x-z14-64cpu-cpuinfo')), + 'architecture': 's390x', + 'nproc_out': 64, + 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]), + 'expected_result': { + 'processor': [ + 'IBM/S390', + ], + 'processor_cores': 32, + 'processor_count': 1, + 'processor_nproc': 64, + 'processor_threads_per_core': 2, + 'processor_vcpus': 64 + }, + }, + { + 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu')), 'architecture': 'sparc64', 'nproc_out': 24, 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), diff --git a/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py b/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py index aea8694e..41674344 100644 --- a/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py +++ b/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py @@ -45,7 +45,7 @@ def test_get_cpu_info_missing_arch(mocker): module = mocker.Mock() inst = linux.LinuxHardware(module) - # ARM and Power will report incorrect processor count if architecture is not available + # ARM, Power, and zSystems will report incorrect processor count if architecture is not available mocker.patch('os.path.exists', return_value=False) mocker.patch('os.access', return_value=True) for test in CPU_INFO_TEST_SCENARIOS: @@ -56,7 +56,7 @@ def test_get_cpu_info_missing_arch(mocker): test_result = inst.get_cpu_facts() - if test['architecture'].startswith(('armv', 'aarch', 'ppc')): + if test['architecture'].startswith(('armv', 'aarch', 'ppc', 's390')): assert test['expected_result'] != test_result else: assert test['expected_result'] == test_result diff --git a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py index c0957566..6667ada7 100644 --- a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py +++ b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py @@ -21,7 +21,8 @@ def test_input(): def test_parse_distribution_file_clear_linux(mock_module, test_input): - test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files/ClearLinux')).read() + with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files/ClearLinux')) as file: + test_input['data'] = file.read() result = ( True, @@ -43,7 +44,8 @@ def test_parse_distribution_file_clear_linux_no_match(mock_module, distro_file, Test against data from Linux Mint and CoreOS to ensure we do not get a reported match from parse_distribution_file_ClearLinux() """ - test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read() + with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)) as file: + test_input['data'] = file.read() result = (False, {}) diff --git a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py index 53fd4ea1..efb937e0 100644 --- a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py +++ b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py @@ -19,9 +19,12 @@ from ansible.module_utils.facts.system.distribution import DistributionFiles ) ) def test_parse_distribution_file_slackware(mock_module, distro_file, expected_version): + with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)) as file: + data = file.read() + test_input = { 'name': 'Slackware', - 'data': open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read(), + 'data': data, 'path': '/etc/os-release', 'collected_facts': None, } diff --git a/test/units/module_utils/facts/test_collectors.py b/test/units/module_utils/facts/test_collectors.py index c4806025..984b5859 100644 --- a/test/units/module_utils/facts/test_collectors.py +++ b/test/units/module_utils/facts/test_collectors.py @@ -93,7 +93,7 @@ class TestApparmorFacts(BaseFactsTest): collector_class = ApparmorFactCollector def test_collect(self): - facts_dict = super(TestApparmorFacts, self).test_collect() + facts_dict = super(TestApparmorFacts, self)._test_collect() self.assertIn('status', facts_dict['apparmor']) @@ -191,7 +191,7 @@ class TestEnvFacts(BaseFactsTest): collector_class = EnvFactCollector def test_collect(self): - facts_dict = super(TestEnvFacts, self).test_collect() + facts_dict = super(TestEnvFacts, self)._test_collect() self.assertIn('HOME', facts_dict['env']) @@ -355,7 +355,6 @@ class TestSelinuxFacts(BaseFactsTest): facts_dict = fact_collector.collect(module=module) self.assertIsInstance(facts_dict, dict) self.assertEqual(facts_dict['selinux']['status'], 'Missing selinux Python library') - return facts_dict class TestServiceMgrFacts(BaseFactsTest): diff --git a/test/units/module_utils/facts/test_date_time.py b/test/units/module_utils/facts/test_date_time.py index 6abc36a7..6cc05f97 100644 --- a/test/units/module_utils/facts/test_date_time.py +++ b/test/units/module_utils/facts/test_date_time.py @@ -10,28 +10,27 @@ import datetime import string import time +from ansible.module_utils.compat.datetime import UTC from ansible.module_utils.facts.system import date_time EPOCH_TS = 1594449296.123456 DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356) -DT_UTC = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356) +UTC_DT = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356) @pytest.fixture def fake_now(monkeypatch): """ - Patch `datetime.datetime.fromtimestamp()`, `datetime.datetime.utcfromtimestamp()`, + Patch `datetime.datetime.fromtimestamp()`, and `time.time()` to return deterministic values. """ class FakeNow: @classmethod - def fromtimestamp(cls, timestamp): - return DT - - @classmethod - def utcfromtimestamp(cls, timestamp): - return DT_UTC + def fromtimestamp(cls, timestamp, tz=None): + if tz == UTC: + return UTC_DT.replace(tzinfo=tz) + return DT.replace(tzinfo=tz) def _time(): return EPOCH_TS diff --git a/test/units/module_utils/facts/test_sysctl.py b/test/units/module_utils/facts/test_sysctl.py index c369b610..0f1632bf 100644 --- a/test/units/module_utils/facts/test_sysctl.py +++ b/test/units/module_utils/facts/test_sysctl.py @@ -20,13 +20,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import os - -import pytest - # for testing from units.compat import unittest -from units.compat.mock import patch, MagicMock, mock_open, Mock +from units.compat.mock import MagicMock from ansible.module_utils.facts.sysctl import get_sysctl diff --git a/test/units/module_utils/facts/test_timeout.py b/test/units/module_utils/facts/test_timeout.py index 2adbc4a6..6ba7c397 100644 --- a/test/units/module_utils/facts/test_timeout.py +++ b/test/units/module_utils/facts/test_timeout.py @@ -139,7 +139,7 @@ def function_other_timeout(): @timeout.timeout(1) def function_raises(): - 1 / 0 + return 1 / 0 @timeout.timeout(1) diff --git a/test/units/module_utils/urls/test_Request.py b/test/units/module_utils/urls/test_Request.py index d2c4ea38..a8bc3a0b 100644 --- a/test/units/module_utils/urls/test_Request.py +++ b/test/units/module_utils/urls/test_Request.py @@ -33,6 +33,7 @@ def install_opener_mock(mocker): def test_Request_fallback(urlopen_mock, install_opener_mock, mocker): here = os.path.dirname(__file__) pem = os.path.join(here, 'fixtures/client.pem') + client_key = os.path.join(here, 'fixtures/client.key') cookies = cookiejar.CookieJar() request = Request( @@ -46,8 +47,8 @@ def test_Request_fallback(urlopen_mock, install_opener_mock, mocker): http_agent='ansible-tests', force_basic_auth=True, follow_redirects='all', - client_cert='/tmp/client.pem', - client_key='/tmp/client.key', + client_cert=pem, + client_key=client_key, cookies=cookies, unix_socket='/foo/bar/baz.sock', ca_path=pem, @@ -68,8 +69,8 @@ def test_Request_fallback(urlopen_mock, install_opener_mock, mocker): call(None, 'ansible-tests'), # http_agent call(None, True), # force_basic_auth call(None, 'all'), # follow_redirects - call(None, '/tmp/client.pem'), # client_cert - call(None, '/tmp/client.key'), # client_key + call(None, pem), # client_cert + call(None, client_key), # client_key call(None, cookies), # cookies call(None, '/foo/bar/baz.sock'), # unix_socket call(None, pem), # ca_path @@ -358,10 +359,7 @@ def test_Request_open_client_cert(urlopen_mock, install_opener_mock): assert ssl_handler.client_cert == client_cert assert ssl_handler.client_key == client_key - https_connection = ssl_handler._build_https_connection('ansible.com') - - assert https_connection.key_file == client_key - assert https_connection.cert_file == client_cert + ssl_handler._build_https_connection('ansible.com') def test_Request_open_cookies(urlopen_mock, install_opener_mock): diff --git a/test/units/module_utils/urls/test_fetch_file.py b/test/units/module_utils/urls/test_fetch_file.py index ed112270..ecb6b9f1 100644 --- a/test/units/module_utils/urls/test_fetch_file.py +++ b/test/units/module_utils/urls/test_fetch_file.py @@ -10,7 +10,6 @@ import os from ansible.module_utils.urls import fetch_file import pytest -from units.compat.mock import MagicMock class FakeTemporaryFile: diff --git a/test/units/module_utils/urls/test_prepare_multipart.py b/test/units/module_utils/urls/test_prepare_multipart.py index 226d9edd..ee320477 100644 --- a/test/units/module_utils/urls/test_prepare_multipart.py +++ b/test/units/module_utils/urls/test_prepare_multipart.py @@ -7,8 +7,6 @@ __metaclass__ = type import os -from io import StringIO - from email.message import Message import pytest diff --git a/test/units/module_utils/urls/test_urls.py b/test/units/module_utils/urls/test_urls.py index 69c1b824..f0e5e9ea 100644 --- a/test/units/module_utils/urls/test_urls.py +++ b/test/units/module_utils/urls/test_urls.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type from ansible.module_utils import urls -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native import pytest diff --git a/test/units/modules/conftest.py b/test/units/modules/conftest.py index a7d1e047..c60c586d 100644 --- a/test/units/modules/conftest.py +++ b/test/units/modules/conftest.py @@ -8,24 +8,15 @@ import json import pytest -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_bytes -from ansible.module_utils.common._collections_compat import MutableMapping +from ansible.module_utils.common.text.converters import to_bytes @pytest.fixture def patch_ansible_module(request, mocker): - if isinstance(request.param, string_types): - args = request.param - elif isinstance(request.param, MutableMapping): - if 'ANSIBLE_MODULE_ARGS' not in request.param: - request.param = {'ANSIBLE_MODULE_ARGS': request.param} - if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']: - request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp' - if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']: - request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False - args = json.dumps(request.param) - else: - raise Exception('Malformed data to the patch_ansible_module pytest fixture') + request.param = {'ANSIBLE_MODULE_ARGS': request.param} + request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp' + request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False + + args = json.dumps(request.param) mocker.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args)) diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py index 20e056ff..a5aa4a90 100644 --- a/test/units/modules/test_apt.py +++ b/test/units/modules/test_apt.py @@ -2,20 +2,13 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import collections -import sys from units.compat.mock import Mock from units.compat import unittest -try: - from ansible.modules.apt import ( - expand_pkgspec_from_fnmatches, - ) -except Exception: - # Need some more module_utils work (porting urls.py) before we can test - # modules. So don't error out in this case. - if sys.version_info[0] >= 3: - pass +from ansible.modules.apt import ( + expand_pkgspec_from_fnmatches, +) class AptExpandPkgspecTestCase(unittest.TestCase): @@ -29,25 +22,25 @@ class AptExpandPkgspecTestCase(unittest.TestCase): ] def test_trivial(self): - foo = ["apt"] + pkg = ["apt"] self.assertEqual( - expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo) + expand_pkgspec_from_fnmatches(None, pkg, self.fake_cache), pkg) def test_version_wildcard(self): - foo = ["apt=1.0*"] + pkg = ["apt=1.0*"] self.assertEqual( - expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo) + expand_pkgspec_from_fnmatches(None, pkg, self.fake_cache), pkg) def test_pkgname_wildcard_version_wildcard(self): - foo = ["apt*=1.0*"] + pkg = ["apt*=1.0*"] m_mock = Mock() self.assertEqual( - expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache), + expand_pkgspec_from_fnmatches(m_mock, pkg, self.fake_cache), ['apt', 'apt-utils']) def test_pkgname_expands(self): - foo = ["apt*"] + pkg = ["apt*"] m_mock = Mock() self.assertEqual( - expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache), + expand_pkgspec_from_fnmatches(m_mock, pkg, self.fake_cache), ["apt", "apt-utils"]) diff --git a/test/units/modules/test_async_wrapper.py b/test/units/modules/test_async_wrapper.py index 37b1fda3..dbaf6834 100644 --- a/test/units/modules/test_async_wrapper.py +++ b/test/units/modules/test_async_wrapper.py @@ -7,26 +7,21 @@ __metaclass__ = type import os import json import shutil +import sys import tempfile -import pytest - -from units.compat.mock import patch, MagicMock from ansible.modules import async_wrapper -from pprint import pprint - class TestAsyncWrapper: def test_run_module(self, monkeypatch): def mock_get_interpreter(module_path): - return ['/usr/bin/python'] + return [sys.executable] module_result = {'rc': 0} module_lines = [ - '#!/usr/bin/python', 'import sys', 'sys.stderr.write("stderr stuff")', "print('%s')" % json.dumps(module_result) diff --git a/test/units/modules/test_copy.py b/test/units/modules/test_copy.py index 20c309b6..beeef6d7 100644 --- a/test/units/modules/test_copy.py +++ b/test/units/modules/test_copy.py @@ -128,16 +128,19 @@ def test_split_pre_existing_dir_working_dir_exists(directory, expected, mocker): # # Info helpful for making new test cases: # -# base_mode = {'dir no perms': 0o040000, -# 'file no perms': 0o100000, -# 'dir all perms': 0o400000 | 0o777, -# 'file all perms': 0o100000, | 0o777} +# base_mode = { +# 'dir no perms': 0o040000, +# 'file no perms': 0o100000, +# 'dir all perms': 0o040000 | 0o777, +# 'file all perms': 0o100000 | 0o777} # -# perm_bits = {'x': 0b001, +# perm_bits = { +# 'x': 0b001, # 'w': 0b010, # 'r': 0b100} # -# role_shift = {'u': 6, +# role_shift = { +# 'u': 6, # 'g': 3, # 'o': 0} @@ -172,6 +175,10 @@ DATA = ( # Going from no permissions to setting all for user, group, and/or oth # chmod a-X statfile <== removes execute from statfile (0o100777, u'a-X', 0o0666), + # Verify X uses computed not original mode + (0o100777, u'a=,u=rX', 0o0400), + (0o040777, u'a=,u=rX', 0o0500), + # Multiple permissions (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755), (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644), @@ -185,6 +192,10 @@ UMASK_DATA = ( INVALID_DATA = ( (0o040000, u'a=foo', "bad symbolic permission for mode: a=foo"), (0o040000, u'f=rwx', "bad symbolic permission for mode: f=rwx"), + (0o100777, u'of=r', "bad symbolic permission for mode: of=r"), + + (0o100777, u'ao=r', "bad symbolic permission for mode: ao=r"), + (0o100777, u'oa=r', "bad symbolic permission for mode: oa=r"), ) diff --git a/test/units/modules/test_hostname.py b/test/units/modules/test_hostname.py index 9050fd04..1aa4a57a 100644 --- a/test/units/modules/test_hostname.py +++ b/test/units/modules/test_hostname.py @@ -6,7 +6,6 @@ import shutil import tempfile from units.compat.mock import patch, MagicMock, mock_open -from ansible.module_utils import basic from ansible.module_utils.common._utils import get_all_subclasses from ansible.modules import hostname from units.modules.utils import ModuleTestCase, set_module_args @@ -44,12 +43,9 @@ class TestHostname(ModuleTestCase): classname = "%sStrategy" % prefix cls = getattr(hostname, classname, None) - if cls is None: - self.assertFalse( - cls is None, "%s is None, should be a subclass" % classname - ) - else: - self.assertTrue(issubclass(cls, hostname.BaseStrategy)) + assert cls is not None + + self.assertTrue(issubclass(cls, hostname.BaseStrategy)) class TestRedhatStrategy(ModuleTestCase): diff --git a/test/units/modules/test_iptables.py b/test/units/modules/test_iptables.py index 265e770a..2459cf77 100644 --- a/test/units/modules/test_iptables.py +++ b/test/units/modules/test_iptables.py @@ -181,7 +181,7 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 2) + self.assertEqual(run_command.call_count, 1) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', @@ -208,7 +208,6 @@ class TestIptables(ModuleTestCase): commands_results = [ (1, '', ''), # check_rule_present - (0, '', ''), # check_chain_present (0, '', ''), ] @@ -218,7 +217,7 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 3) + self.assertEqual(run_command.call_count, 2) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', @@ -232,7 +231,7 @@ class TestIptables(ModuleTestCase): '-j', 'ACCEPT' ]) - self.assertEqual(run_command.call_args_list[2][0][0], [ + self.assertEqual(run_command.call_args_list[1][0][0], [ '/sbin/iptables', '-t', 'filter', @@ -272,7 +271,7 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 2) + self.assertEqual(run_command.call_count, 1) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', @@ -321,7 +320,7 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 3) + self.assertEqual(run_command.call_count, 2) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', @@ -343,7 +342,7 @@ class TestIptables(ModuleTestCase): '--to-ports', '8600' ]) - self.assertEqual(run_command.call_args_list[2][0][0], [ + self.assertEqual(run_command.call_args_list[1][0][0], [ '/sbin/iptables', '-t', 'nat', @@ -1019,10 +1018,8 @@ class TestIptables(ModuleTestCase): }) commands_results = [ - (1, '', ''), # check_rule_present (1, '', ''), # check_chain_present (0, '', ''), # create_chain - (0, '', ''), # append_rule ] with patch.object(basic.AnsibleModule, 'run_command') as run_command: @@ -1031,32 +1028,20 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 4) + self.assertEqual(run_command.call_count, 2) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', 'filter', - '-C', 'FOOBAR', - ]) - - self.assertEqual(run_command.call_args_list[1][0][0], [ - '/sbin/iptables', - '-t', 'filter', '-L', 'FOOBAR', ]) - self.assertEqual(run_command.call_args_list[2][0][0], [ + self.assertEqual(run_command.call_args_list[1][0][0], [ '/sbin/iptables', '-t', 'filter', '-N', 'FOOBAR', ]) - self.assertEqual(run_command.call_args_list[3][0][0], [ - '/sbin/iptables', - '-t', 'filter', - '-A', 'FOOBAR', - ]) - commands_results = [ (0, '', ''), # check_rule_present ] @@ -1078,7 +1063,6 @@ class TestIptables(ModuleTestCase): commands_results = [ (1, '', ''), # check_rule_present - (1, '', ''), # check_chain_present ] with patch.object(basic.AnsibleModule, 'run_command') as run_command: @@ -1087,17 +1071,11 @@ class TestIptables(ModuleTestCase): iptables.main() self.assertTrue(result.exception.args[0]['changed']) - self.assertEqual(run_command.call_count, 2) + self.assertEqual(run_command.call_count, 1) self.assertEqual(run_command.call_args_list[0][0][0], [ '/sbin/iptables', '-t', 'filter', - '-C', 'FOOBAR', - ]) - - self.assertEqual(run_command.call_args_list[1][0][0], [ - '/sbin/iptables', - '-t', 'filter', '-L', 'FOOBAR', ]) diff --git a/test/units/modules/test_known_hosts.py b/test/units/modules/test_known_hosts.py index 123dd75f..667f3e50 100644 --- a/test/units/modules/test_known_hosts.py +++ b/test/units/modules/test_known_hosts.py @@ -6,7 +6,7 @@ import tempfile from ansible.module_utils import basic from units.compat import unittest -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.basic import AnsibleModule from ansible.modules.known_hosts import compute_diff, sanity_check diff --git a/test/units/modules/test_unarchive.py b/test/units/modules/test_unarchive.py index 3e7a58c9..935231ba 100644 --- a/test/units/modules/test_unarchive.py +++ b/test/units/modules/test_unarchive.py @@ -8,20 +8,6 @@ import pytest from ansible.modules.unarchive import ZipArchive, TgzArchive -class AnsibleModuleExit(Exception): - def __init__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - - -class ExitJson(AnsibleModuleExit): - pass - - -class FailJson(AnsibleModuleExit): - pass - - @pytest.fixture def fake_ansible_module(): return FakeAnsibleModule() @@ -32,12 +18,6 @@ class FakeAnsibleModule: self.params = {} self.tmpdir = None - def exit_json(self, *args, **kwargs): - raise ExitJson(*args, **kwargs) - - def fail_json(self, *args, **kwargs): - raise FailJson(*args, **kwargs) - class TestCaseZipArchive: @pytest.mark.parametrize( diff --git a/test/units/modules/utils.py b/test/units/modules/utils.py index 6d169e36..b56229e8 100644 --- a/test/units/modules/utils.py +++ b/test/units/modules/utils.py @@ -6,14 +6,12 @@ import json from units.compat import unittest from units.compat.mock import patch from ansible.module_utils import basic -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes def set_module_args(args): - if '_ansible_remote_tmp' not in args: - args['_ansible_remote_tmp'] = '/tmp' - if '_ansible_keep_remote_files' not in args: - args['_ansible_keep_remote_files'] = False + args['_ansible_remote_tmp'] = '/tmp' + args['_ansible_keep_remote_files'] = False args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) basic._ANSIBLE_ARGS = to_bytes(args) @@ -28,8 +26,6 @@ class AnsibleFailJson(Exception): def exit_json(*args, **kwargs): - if 'changed' not in kwargs: - kwargs['changed'] = False raise AnsibleExitJson(kwargs) diff --git a/test/units/parsing/test_ajson.py b/test/units/parsing/test_ajson.py index 1b9a76b4..bb7bf1a7 100644 --- a/test/units/parsing/test_ajson.py +++ b/test/units/parsing/test_ajson.py @@ -109,7 +109,11 @@ class TestAnsibleJSONEncoder: def __len__(self): return len(self.__dict__) - return M(request.param) + mapping = M(request.param) + + assert isinstance(len(mapping), int) # ensure coverage of __len__ + + return mapping @pytest.fixture def ansible_json_encoder(self): diff --git a/test/units/parsing/test_dataloader.py b/test/units/parsing/test_dataloader.py index 9ec49a8d..a7f8b1d2 100644 --- a/test/units/parsing/test_dataloader.py +++ b/test/units/parsing/test_dataloader.py @@ -25,8 +25,7 @@ from units.compat import unittest from unittest.mock import patch, mock_open from ansible.errors import AnsibleParserError, yaml_strings, AnsibleFileNotFound from ansible.parsing.vault import AnsibleVaultError -from ansible.module_utils._text import to_text -from ansible.module_utils.six import PY3 +from ansible.module_utils.common.text.converters import to_text from units.mock.vault_helper import TextVaultSecret from ansible.parsing.dataloader import DataLoader @@ -92,11 +91,11 @@ class TestDataLoader(unittest.TestCase): - { role: 'testrole' } testrole/tasks/main.yml: - - include: "include1.yml" + - include_tasks: "include1.yml" static: no testrole/tasks/include1.yml: - - include: include2.yml + - include_tasks: include2.yml static: no testrole/tasks/include2.yml: @@ -229,11 +228,7 @@ class TestDataLoaderWithVault(unittest.TestCase): 3135306561356164310a343937653834643433343734653137383339323330626437313562306630 3035 """ - if PY3: - builtins_name = 'builtins' - else: - builtins_name = '__builtin__' - with patch(builtins_name + '.open', mock_open(read_data=vaulted_data.encode('utf-8'))): + with patch('builtins.open', mock_open(read_data=vaulted_data.encode('utf-8'))): output = self._loader.load_from_file('dummy_vault.txt') self.assertEqual(output, dict(foo='bar')) diff --git a/test/units/parsing/test_mod_args.py b/test/units/parsing/test_mod_args.py index 5d3f5d25..aeb74ad5 100644 --- a/test/units/parsing/test_mod_args.py +++ b/test/units/parsing/test_mod_args.py @@ -6,10 +6,10 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import pytest -import re from ansible.errors import AnsibleParserError from ansible.parsing.mod_args import ModuleArgsParser +from ansible.plugins.loader import init_plugin_loader from ansible.utils.sentinel import Sentinel @@ -119,19 +119,19 @@ class TestModArgsDwim: assert err.value.args[0] == msg def test_multiple_actions_ping_shell(self): + init_plugin_loader() args_dict = {'ping': 'data=hi', 'shell': 'echo hi'} m = ModuleArgsParser(args_dict) with pytest.raises(AnsibleParserError) as err: m.parse() - assert err.value.args[0].startswith("conflicting action statements: ") - actions = set(re.search(r'(\w+), (\w+)', err.value.args[0]).groups()) - assert actions == set(['ping', 'shell']) + assert err.value.args[0] == f'conflicting action statements: {", ".join(args_dict)}' def test_bogus_action(self): + init_plugin_loader() args_dict = {'bogusaction': {}} m = ModuleArgsParser(args_dict) with pytest.raises(AnsibleParserError) as err: m.parse() - assert err.value.args[0].startswith("couldn't resolve module/action 'bogusaction'") + assert err.value.args[0].startswith(f"couldn't resolve module/action '{next(iter(args_dict))}'") diff --git a/test/units/parsing/test_splitter.py b/test/units/parsing/test_splitter.py index a37de0f9..893f0473 100644 --- a/test/units/parsing/test_splitter.py +++ b/test/units/parsing/test_splitter.py @@ -21,10 +21,17 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.parsing.splitter import split_args, parse_kv +from ansible.errors import AnsibleParserError import pytest SPLIT_DATA = ( + (None, + [], + {}), + (u'', + [], + {}), (u'a', [u'a'], {u'_raw_params': u'a'}), @@ -46,6 +53,18 @@ SPLIT_DATA = ( (u'a="echo \\"hello world\\"" b=bar', [u'a="echo \\"hello world\\""', u'b=bar'], {u'a': u'echo "hello world"', u'b': u'bar'}), + (u'a="nest\'ed"', + [u'a="nest\'ed"'], + {u'a': u'nest\'ed'}), + (u' ', + [u' '], + {u'_raw_params': u' '}), + (u'\\ ', + [u' '], + {u'_raw_params': u' '}), + (u'a\\=escaped', + [u'a\\=escaped'], + {u'_raw_params': u'a=escaped'}), (u'a="multi\nline"', [u'a="multi\nline"'], {u'a': u'multi\nline'}), @@ -61,12 +80,27 @@ SPLIT_DATA = ( (u'a="multiline\nmessage1\\\n" b="multiline\nmessage2\\\n"', [u'a="multiline\nmessage1\\\n"', u'b="multiline\nmessage2\\\n"'], {u'a': 'multiline\nmessage1\\\n', u'b': u'multiline\nmessage2\\\n'}), + (u'line \\\ncontinuation', + [u'line', u'continuation'], + {u'_raw_params': u'line continuation'}), + (u'not jinja}}', + [u'not', u'jinja}}'], + {u'_raw_params': u'not jinja}}'}), + (u'a={{multiline\njinja}}', + [u'a={{multiline\njinja}}'], + {u'a': u'{{multiline\njinja}}'}), (u'a={{jinja}}', [u'a={{jinja}}'], {u'a': u'{{jinja}}'}), (u'a={{ jinja }}', [u'a={{ jinja }}'], {u'a': u'{{ jinja }}'}), + (u'a={% jinja %}', + [u'a={% jinja %}'], + {u'a': u'{% jinja %}'}), + (u'a={# jinja #}', + [u'a={# jinja #}'], + {u'a': u'{# jinja #}'}), (u'a="{{jinja}}"', [u'a="{{jinja}}"'], {u'a': u'{{jinja}}'}), @@ -94,17 +128,50 @@ SPLIT_DATA = ( (u'One\n Two\n Three\n', [u'One\n ', u'Two\n ', u'Three\n'], {u'_raw_params': u'One\n Two\n Three\n'}), + (u'\nOne\n Two\n Three\n', + [u'\n', u'One\n ', u'Two\n ', u'Three\n'], + {u'_raw_params': u'\nOne\n Two\n Three\n'}), ) -SPLIT_ARGS = ((test[0], test[1]) for test in SPLIT_DATA) -PARSE_KV = ((test[0], test[2]) for test in SPLIT_DATA) +PARSE_KV_CHECK_RAW = ( + (u'raw=yes', {u'_raw_params': u'raw=yes'}), + (u'creates=something', {u'creates': u'something'}), +) + +PARSER_ERROR = ( + '"', + "'", + '{{', + '{%', + '{#', +) +SPLIT_ARGS = tuple((test[0], test[1]) for test in SPLIT_DATA) +PARSE_KV = tuple((test[0], test[2]) for test in SPLIT_DATA) -@pytest.mark.parametrize("args, expected", SPLIT_ARGS) + +@pytest.mark.parametrize("args, expected", SPLIT_ARGS, ids=[str(arg[0]) for arg in SPLIT_ARGS]) def test_split_args(args, expected): assert split_args(args) == expected -@pytest.mark.parametrize("args, expected", PARSE_KV) +@pytest.mark.parametrize("args, expected", PARSE_KV, ids=[str(arg[0]) for arg in PARSE_KV]) def test_parse_kv(args, expected): assert parse_kv(args) == expected + + +@pytest.mark.parametrize("args, expected", PARSE_KV_CHECK_RAW, ids=[str(arg[0]) for arg in PARSE_KV_CHECK_RAW]) +def test_parse_kv_check_raw(args, expected): + assert parse_kv(args, check_raw=True) == expected + + +@pytest.mark.parametrize("args", PARSER_ERROR) +def test_split_args_error(args): + with pytest.raises(AnsibleParserError): + split_args(args) + + +@pytest.mark.parametrize("args", PARSER_ERROR) +def test_parse_kv_error(args): + with pytest.raises(AnsibleParserError): + parse_kv(args) diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py index 7afd3560..f94171a2 100644 --- a/test/units/parsing/vault/test_vault.py +++ b/test/units/parsing/vault/test_vault.py @@ -21,7 +21,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import binascii import io import os import tempfile @@ -34,7 +33,7 @@ from unittest.mock import patch, MagicMock from ansible import errors from ansible.module_utils import six -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.parsing import vault from units.mock.loader import DictDataLoader @@ -606,9 +605,6 @@ class TestVaultLib(unittest.TestCase): ('test_id', text_secret)] self.v = vault.VaultLib(self.vault_secrets) - def _vault_secrets(self, vault_id, secret): - return [(vault_id, secret)] - def _vault_secrets_from_password(self, vault_id, password): return [(vault_id, TextVaultSecret(password))] @@ -779,43 +775,6 @@ class TestVaultLib(unittest.TestCase): b_plaintext = self.v.decrypt(b_vaulttext) self.assertEqual(b_plaintext, b_orig_plaintext, msg="decryption failed") - # FIXME This test isn't working quite yet. - @pytest.mark.skip(reason='This test is not ready yet') - def test_encrypt_decrypt_aes256_bad_hmac(self): - - self.v.cipher_name = 'AES256' - # plaintext = "Setec Astronomy" - enc_data = '''$ANSIBLE_VAULT;1.1;AES256 -33363965326261303234626463623963633531343539616138316433353830356566396130353436 -3562643163366231316662386565383735653432386435610a306664636137376132643732393835 -63383038383730306639353234326630666539346233376330303938323639306661313032396437 -6233623062366136310a633866373936313238333730653739323461656662303864663666653563 -3138''' - b_data = to_bytes(enc_data, errors='strict', encoding='utf-8') - b_data = self.v._split_header(b_data) - foo = binascii.unhexlify(b_data) - lines = foo.splitlines() - # line 0 is salt, line 1 is hmac, line 2+ is ciphertext - b_salt = lines[0] - b_hmac = lines[1] - b_ciphertext_data = b'\n'.join(lines[2:]) - - b_ciphertext = binascii.unhexlify(b_ciphertext_data) - # b_orig_ciphertext = b_ciphertext[:] - - # now muck with the text - # b_munged_ciphertext = b_ciphertext[:10] + b'\x00' + b_ciphertext[11:] - # b_munged_ciphertext = b_ciphertext - # assert b_orig_ciphertext != b_munged_ciphertext - - b_ciphertext_data = binascii.hexlify(b_ciphertext) - b_payload = b'\n'.join([b_salt, b_hmac, b_ciphertext_data]) - # reformat - b_invalid_ciphertext = self.v._format_output(b_payload) - - # assert we throw an error - self.v.decrypt(b_invalid_ciphertext) - def test_decrypt_and_get_vault_id(self): b_expected_plaintext = to_bytes('foo bar\n') vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel diff --git a/test/units/parsing/vault/test_vault_editor.py b/test/units/parsing/vault/test_vault_editor.py index 77509f08..28561c6a 100644 --- a/test/units/parsing/vault/test_vault_editor.py +++ b/test/units/parsing/vault/test_vault_editor.py @@ -33,8 +33,7 @@ from ansible import errors from ansible.parsing import vault from ansible.parsing.vault import VaultLib, VaultEditor, match_encrypt_secret -from ansible.module_utils.six import PY3 -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text from units.mock.vault_helper import TextVaultSecret @@ -88,12 +87,10 @@ class TestVaultEditor(unittest.TestCase): suffix = '_ansible_unit_test_%s_' % (self.__class__.__name__) return tempfile.mkdtemp(suffix=suffix) - def _create_file(self, test_dir, name, content=None, symlink=False): + def _create_file(self, test_dir, name, content, symlink=False): file_path = os.path.join(test_dir, name) - opened_file = open(file_path, 'wb') - if content: + with open(file_path, 'wb') as opened_file: opened_file.write(content) - opened_file.close() return file_path def _vault_editor(self, vault_secrets=None): @@ -118,11 +115,8 @@ class TestVaultEditor(unittest.TestCase): def test_stdin_binary(self): stdin_data = '\0' - if PY3: - fake_stream = StringIO(stdin_data) - fake_stream.buffer = BytesIO(to_bytes(stdin_data)) - else: - fake_stream = BytesIO(to_bytes(stdin_data)) + fake_stream = StringIO(stdin_data) + fake_stream.buffer = BytesIO(to_bytes(stdin_data)) with patch('sys.stdin', fake_stream): ve = self._vault_editor() @@ -167,17 +161,15 @@ class TestVaultEditor(unittest.TestCase): self.assertNotEqual(src_file_contents, b_ciphertext, 'b_ciphertext should be encrypted and not equal to src_contents') - def _faux_editor(self, editor_args, new_src_contents=None): + def _faux_editor(self, editor_args, new_src_contents): if editor_args[0] == 'shred': return tmp_path = editor_args[-1] # simulate the tmp file being editted - tmp_file = open(tmp_path, 'wb') - if new_src_contents: + with open(tmp_path, 'wb') as tmp_file: tmp_file.write(new_src_contents) - tmp_file.close() def _faux_command(self, tmp_path): pass @@ -198,13 +190,13 @@ class TestVaultEditor(unittest.TestCase): ve._edit_file_helper(src_file_path, self.vault_secret, existing_data=src_file_contents) - new_target_file = open(src_file_path, 'rb') - new_target_file_contents = new_target_file.read() - self.assertEqual(src_file_contents, new_target_file_contents) + with open(src_file_path, 'rb') as new_target_file: + new_target_file_contents = new_target_file.read() + self.assertEqual(src_file_contents, new_target_file_contents) def _assert_file_is_encrypted(self, vault_editor, src_file_path, src_contents): - new_src_file = open(src_file_path, 'rb') - new_src_file_contents = new_src_file.read() + with open(src_file_path, 'rb') as new_src_file: + new_src_file_contents = new_src_file.read() # TODO: assert that it is encrypted self.assertTrue(vault.is_encrypted(new_src_file_contents)) @@ -339,8 +331,8 @@ class TestVaultEditor(unittest.TestCase): ve.encrypt_file(src_file_path, self.vault_secret) ve.edit_file(src_file_path) - new_src_file = open(src_file_path, 'rb') - new_src_file_contents = new_src_file.read() + with open(src_file_path, 'rb') as new_src_file: + new_src_file_contents = new_src_file.read() self.assertTrue(b'$ANSIBLE_VAULT;1.1;AES256' in new_src_file_contents) @@ -367,8 +359,8 @@ class TestVaultEditor(unittest.TestCase): vault_id='vault_secrets') ve.edit_file(src_file_path) - new_src_file = open(src_file_path, 'rb') - new_src_file_contents = new_src_file.read() + with open(src_file_path, 'rb') as new_src_file: + new_src_file_contents = new_src_file.read() self.assertTrue(b'$ANSIBLE_VAULT;1.2;AES256;vault_secrets' in new_src_file_contents) @@ -399,8 +391,8 @@ class TestVaultEditor(unittest.TestCase): ve.edit_file(src_file_link_path) - new_src_file = open(src_file_path, 'rb') - new_src_file_contents = new_src_file.read() + with open(src_file_path, 'rb') as new_src_file: + new_src_file_contents = new_src_file.read() src_file_plaintext = ve.vault.decrypt(new_src_file_contents) @@ -418,13 +410,6 @@ class TestVaultEditor(unittest.TestCase): src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents) - new_src_contents = to_bytes("The info is different now.") - - def faux_editor(editor_args): - self._faux_editor(editor_args, new_src_contents) - - mock_sp_call.side_effect = faux_editor - ve = self._vault_editor() self.assertRaisesRegex(errors.AnsibleError, 'input is not vault encrypted data', @@ -478,20 +463,14 @@ class TestVaultEditor(unittest.TestCase): ve = self._vault_editor(self._secrets("ansible")) # make sure the password functions for the cipher - error_hit = False - try: - ve.decrypt_file(v11_file.name) - except errors.AnsibleError: - error_hit = True + ve.decrypt_file(v11_file.name) # verify decrypted content - f = open(v11_file.name, "rb") - fdata = to_text(f.read()) - f.close() + with open(v11_file.name, "rb") as f: + fdata = to_text(f.read()) os.unlink(v11_file.name) - assert error_hit is False, "error decrypting 1.1 file" assert fdata.strip() == "foo", "incorrect decryption of 1.1 file: %s" % fdata.strip() def test_real_path_dash(self): @@ -501,21 +480,9 @@ class TestVaultEditor(unittest.TestCase): res = ve._real_path(filename) self.assertEqual(res, '-') - def test_real_path_dev_null(self): + def test_real_path_not_dash(self): filename = '/dev/null' ve = self._vault_editor() res = ve._real_path(filename) - self.assertEqual(res, '/dev/null') - - def test_real_path_symlink(self): - self._test_dir = os.path.realpath(self._create_test_dir()) - file_path = self._create_file(self._test_dir, 'test_file', content=b'this is a test file') - file_link_path = os.path.join(self._test_dir, 'a_link_to_test_file') - - os.symlink(file_path, file_link_path) - - ve = self._vault_editor() - - res = ve._real_path(file_link_path) - self.assertEqual(res, file_path) + self.assertNotEqual(res, '-') diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py index cbf5b456..8af1eeed 100644 --- a/test/units/parsing/yaml/test_dumper.py +++ b/test/units/parsing/yaml/test_dumper.py @@ -19,7 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import io -import yaml from jinja2.exceptions import UndefinedError @@ -27,7 +26,6 @@ from units.compat import unittest from ansible.parsing import vault from ansible.parsing.yaml import dumper, objects from ansible.parsing.yaml.loader import AnsibleLoader -from ansible.module_utils.six import PY2 from ansible.template import AnsibleUndefined from units.mock.yaml_helper import YamlTestUtils @@ -76,20 +74,6 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils): data_from_yaml = loader.get_single_data() result = b_text - if PY2: - # https://pyyaml.org/wiki/PyYAMLDocumentation#string-conversion-python-2-only - # pyyaml on Python 2 can return either unicode or bytes when given byte strings. - # We normalize that to always return unicode on Python2 as that's right most of the - # time. However, this means byte strings can round trip through yaml on Python3 but - # not on Python2. To make this code work the same on Python2 and Python3 (we want - # the Python3 behaviour) we need to change the methods in Ansible to: - # (1) Let byte strings pass through yaml without being converted on Python2 - # (2) Convert byte strings to text strings before being given to pyyaml (Without this, - # strings would end up as byte strings most of the time which would mostly be wrong) - # In practice, we mostly read bytes in from files and then pass that to pyyaml, for which - # the present behavior is correct. - # This is a workaround for the current behavior. - result = u'tr\xe9ma' self.assertEqual(result, data_from_yaml) @@ -105,10 +89,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils): self.assertEqual(u_text, data_from_yaml) def test_vars_with_sources(self): - try: - self._dump_string(VarsWithSources(), dumper=self.dumper) - except yaml.representer.RepresenterError: - self.fail("Dump VarsWithSources raised RepresenterError unexpectedly!") + self._dump_string(VarsWithSources(), dumper=self.dumper) def test_undefined(self): undefined_object = AnsibleUndefined() diff --git a/test/units/parsing/yaml/test_objects.py b/test/units/parsing/yaml/test_objects.py index f64b708f..f899915d 100644 --- a/test/units/parsing/yaml/test_objects.py +++ b/test/units/parsing/yaml/test_objects.py @@ -24,7 +24,7 @@ from units.compat import unittest from ansible.errors import AnsibleError -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.parsing import vault from ansible.parsing.yaml.loader import AnsibleLoader @@ -105,11 +105,6 @@ class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils): id_secret = vault.match_encrypt_secret(self.good_vault_secrets) return objects.AnsibleVaultEncryptedUnicode.from_plaintext(seq, vault=self.vault, secret=id_secret[1]) - def _from_ciphertext(self, ciphertext): - avu = objects.AnsibleVaultEncryptedUnicode(ciphertext) - avu.vault = self.vault - return avu - def test_empty_init(self): self.assertRaises(TypeError, objects.AnsibleVaultEncryptedUnicode) diff --git a/test/units/playbook/role/test_include_role.py b/test/units/playbook/role/test_include_role.py index 5e7625ba..aa97da15 100644 --- a/test/units/playbook/role/test_include_role.py +++ b/test/units/playbook/role/test_include_role.py @@ -108,8 +108,6 @@ class TestIncludeRole(unittest.TestCase): # skip meta: role_complete continue role = task._role - if not role: - continue yield (role.get_name(), self.var_manager.get_vars(play=play, task=task)) @@ -201,7 +199,7 @@ class TestIncludeRole(unittest.TestCase): self.assertEqual(task_vars.get('l3_variable'), 'l3-main') self.assertEqual(task_vars.get('test_variable'), 'l3-main') else: - self.fail() + self.fail() # pragma: nocover self.assertFalse(expected_roles) @patch('ansible.playbook.role.definition.unfrackpath', @@ -247,5 +245,5 @@ class TestIncludeRole(unittest.TestCase): self.assertEqual(task_vars.get('l3_variable'), 'l3-alt') self.assertEqual(task_vars.get('test_variable'), 'l3-alt') else: - self.fail() + self.fail() # pragma: nocover self.assertFalse(expected_roles) diff --git a/test/units/playbook/role/test_role.py b/test/units/playbook/role/test_role.py index 5d47631f..9d6b0edc 100644 --- a/test/units/playbook/role/test_role.py +++ b/test/units/playbook/role/test_role.py @@ -21,10 +21,12 @@ __metaclass__ = type from collections.abc import Container +import pytest + from units.compat import unittest from unittest.mock import patch, MagicMock -from ansible.errors import AnsibleError, AnsibleParserError +from ansible.errors import AnsibleParserError from ansible.playbook.block import Block from units.mock.loader import DictDataLoader @@ -42,12 +44,9 @@ class TestHashParams(unittest.TestCase): self._assert_set(res) self._assert_hashable(res) - def _assert_hashable(self, res): - a_dict = {} - try: - a_dict[res] = res - except TypeError as e: - self.fail('%s is not hashable: %s' % (res, e)) + @staticmethod + def _assert_hashable(res): + hash(res) def _assert_set(self, res): self.assertIsInstance(res, frozenset) @@ -87,36 +86,28 @@ class TestHashParams(unittest.TestCase): def test_generator(self): def my_generator(): - for i in ['a', 1, None, {}]: - yield i + yield params = my_generator() res = hash_params(params) self._assert_hashable(res) + assert list(params) def test_container_but_not_iterable(self): # This is a Container that is not iterable, which is unlikely but... class MyContainer(Container): - def __init__(self, some_thing): - self.data = [] - self.data.append(some_thing) + def __init__(self, _some_thing): + pass def __contains__(self, item): - return item in self.data - - def __hash__(self): - return hash(self.data) - - def __len__(self): - return len(self.data) + """Implementation omitted, since it will never be called.""" - def __call__(self): - return False + params = MyContainer('foo bar') - foo = MyContainer('foo bar') - params = foo + with pytest.raises(TypeError) as ex: + hash_params(params) - self.assertRaises(TypeError, hash_params, params) + assert ex.value.args == ("'MyContainer' object is not iterable",) def test_param_dict_dupe_values(self): params1 = {'foo': False} @@ -151,18 +142,18 @@ class TestHashParams(unittest.TestCase): self.assertNotEqual(hash(res1), hash(res2)) self.assertNotEqual(res1, res2) - foo = {} - foo[res1] = 'params1' - foo[res2] = 'params2' + params_dict = {} + params_dict[res1] = 'params1' + params_dict[res2] = 'params2' - self.assertEqual(len(foo), 2) + self.assertEqual(len(params_dict), 2) - del foo[res2] - self.assertEqual(len(foo), 1) + del params_dict[res2] + self.assertEqual(len(params_dict), 1) - for key in foo: - self.assertTrue(key in foo) - self.assertIn(key, foo) + for key in params_dict: + self.assertTrue(key in params_dict) + self.assertIn(key, params_dict) class TestRole(unittest.TestCase): @@ -177,7 +168,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -199,7 +190,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play, from_files=dict(tasks='custom_main')) @@ -217,7 +208,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_handlers', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -238,7 +229,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -259,7 +250,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -280,7 +271,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -303,7 +294,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -323,7 +314,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -370,7 +361,7 @@ class TestRole(unittest.TestCase): mock_play = MagicMock() mock_play.collections = None - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load('foo_metadata', play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) @@ -415,7 +406,7 @@ class TestRole(unittest.TestCase): }) mock_play = MagicMock() - mock_play.ROLE_CACHE = {} + mock_play.role_cache = {} i = RoleInclude.load(dict(role='foo_complex'), play=mock_play, loader=fake_loader) r = Role.load(i, play=mock_play) diff --git a/test/units/playbook/test_base.py b/test/units/playbook/test_base.py index d5810e73..bedd96a8 100644 --- a/test/units/playbook/test_base.py +++ b/test/units/playbook/test_base.py @@ -21,13 +21,12 @@ __metaclass__ = type from units.compat import unittest -from ansible.errors import AnsibleParserError +from ansible.errors import AnsibleParserError, AnsibleAssertionError from ansible.module_utils.six import string_types from ansible.playbook.attribute import FieldAttribute, NonInheritableFieldAttribute from ansible.template import Templar from ansible.playbook import base -from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes, AnsibleUnsafeText -from ansible.utils.sentinel import Sentinel +from ansible.utils.unsafe_proxy import AnsibleUnsafeText from units.mock.loader import DictDataLoader @@ -331,12 +330,6 @@ class ExampleSubClass(base.Base): def __init__(self): super(ExampleSubClass, self).__init__() - def get_dep_chain(self): - if self._parent: - return self._parent.get_dep_chain() - else: - return None - class BaseSubClass(base.Base): name = FieldAttribute(isa='string', default='', always_post_validate=True) @@ -588,10 +581,11 @@ class TestBaseSubClass(TestBase): bsc.post_validate, templar) def test_attr_unknown(self): - a_list = ['some string'] - ds = {'test_attr_unknown_isa': a_list} - bsc = self._base_validate(ds) - self.assertEqual(bsc.test_attr_unknown_isa, a_list) + self.assertRaises( + AnsibleAssertionError, + self._base_validate, + {'test_attr_unknown_isa': True} + ) def test_attr_method(self): ds = {'test_attr_method': 'value from the ds'} diff --git a/test/units/playbook/test_collectionsearch.py b/test/units/playbook/test_collectionsearch.py index be40d85e..d16541b7 100644 --- a/test/units/playbook/test_collectionsearch.py +++ b/test/units/playbook/test_collectionsearch.py @@ -22,7 +22,6 @@ from ansible.errors import AnsibleParserError from ansible.playbook.play import Play from ansible.playbook.task import Task from ansible.playbook.block import Block -from ansible.playbook.collectionsearch import CollectionSearch import pytest diff --git a/test/units/playbook/test_helpers.py b/test/units/playbook/test_helpers.py index a89730ca..23385c00 100644 --- a/test/units/playbook/test_helpers.py +++ b/test/units/playbook/test_helpers.py @@ -52,10 +52,6 @@ class MixinForMocks(object): self.mock_inventory = MagicMock(name='MockInventory') self.mock_inventory._hosts_cache = dict() - def _get_host(host_name): - return None - - self.mock_inventory.get_host.side_effect = _get_host # TODO: can we use a real VariableManager? self.mock_variable_manager = MagicMock(name='MockVariableManager') self.mock_variable_manager.get_vars.return_value = dict() @@ -69,11 +65,11 @@ class MixinForMocks(object): self._test_data_path = os.path.dirname(__file__) self.fake_include_loader = DictDataLoader({"/dev/null/includes/test_include.yml": """ - - include: other_test_include.yml + - include_tasks: other_test_include.yml - shell: echo 'hello world' """, "/dev/null/includes/static_test_include.yml": """ - - include: other_test_include.yml + - include_tasks: other_test_include.yml - shell: echo 'hello static world' """, "/dev/null/includes/other_test_include.yml": """ @@ -86,10 +82,6 @@ class TestLoadListOfTasks(unittest.TestCase, MixinForMocks): def setUp(self): self._setup() - def _assert_is_task_list(self, results): - for result in results: - self.assertIsInstance(result, Task) - def _assert_is_task_list_or_blocks(self, results): self.assertIsInstance(results, list) for result in results: @@ -168,57 +160,57 @@ class TestLoadListOfTasks(unittest.TestCase, MixinForMocks): ds, play=self.mock_play, use_handlers=True, variable_manager=self.mock_variable_manager, loader=self.fake_loader) - def test_one_bogus_include(self): - ds = [{'include': 'somefile.yml'}] + def test_one_bogus_include_tasks(self): + ds = [{'include_tasks': 'somefile.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_loader) self.assertIsInstance(res, list) - self.assertEqual(len(res), 0) + self.assertEqual(len(res), 1) + self.assertIsInstance(res[0], TaskInclude) - def test_one_bogus_include_use_handlers(self): - ds = [{'include': 'somefile.yml'}] + def test_one_bogus_include_tasks_use_handlers(self): + ds = [{'include_tasks': 'somefile.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, use_handlers=True, variable_manager=self.mock_variable_manager, loader=self.fake_loader) self.assertIsInstance(res, list) - self.assertEqual(len(res), 0) + self.assertEqual(len(res), 1) + self.assertIsInstance(res[0], TaskInclude) - def test_one_bogus_include_static(self): + def test_one_bogus_import_tasks(self): ds = [{'import_tasks': 'somefile.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_loader) self.assertIsInstance(res, list) self.assertEqual(len(res), 0) - def test_one_include(self): - ds = [{'include': '/dev/null/includes/other_test_include.yml'}] + def test_one_include_tasks(self): + ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) self.assertEqual(len(res), 1) self._assert_is_task_list_or_blocks(res) - def test_one_parent_include(self): - ds = [{'include': '/dev/null/includes/test_include.yml'}] + def test_one_parent_include_tasks(self): + ds = [{'include_tasks': '/dev/null/includes/test_include.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) self._assert_is_task_list_or_blocks(res) - self.assertIsInstance(res[0], Block) - self.assertIsInstance(res[0]._parent, TaskInclude) + self.assertIsInstance(res[0], TaskInclude) + self.assertIsNone(res[0]._parent) - # TODO/FIXME: do this non deprecated way - def test_one_include_tags(self): - ds = [{'include': '/dev/null/includes/other_test_include.yml', + def test_one_include_tasks_tags(self): + ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml', 'tags': ['test_one_include_tags_tag1', 'and_another_tagB'] }] res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) self._assert_is_task_list_or_blocks(res) - self.assertIsInstance(res[0], Block) + self.assertIsInstance(res[0], TaskInclude) self.assertIn('test_one_include_tags_tag1', res[0].tags) self.assertIn('and_another_tagB', res[0].tags) - # TODO/FIXME: do this non deprecated way - def test_one_parent_include_tags(self): - ds = [{'include': '/dev/null/includes/test_include.yml', + def test_one_parent_include_tasks_tags(self): + ds = [{'include_tasks': '/dev/null/includes/test_include.yml', # 'vars': {'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2']} 'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2'] } @@ -226,20 +218,20 @@ class TestLoadListOfTasks(unittest.TestCase, MixinForMocks): res = helpers.load_list_of_tasks(ds, play=self.mock_play, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) self._assert_is_task_list_or_blocks(res) - self.assertIsInstance(res[0], Block) + self.assertIsInstance(res[0], TaskInclude) self.assertIn('test_one_parent_include_tags_tag1', res[0].tags) self.assertIn('and_another_tag2', res[0].tags) - def test_one_include_use_handlers(self): - ds = [{'include': '/dev/null/includes/other_test_include.yml'}] + def test_one_include_tasks_use_handlers(self): + ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, use_handlers=True, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) self._assert_is_task_list_or_blocks(res) self.assertIsInstance(res[0], Handler) - def test_one_parent_include_use_handlers(self): - ds = [{'include': '/dev/null/includes/test_include.yml'}] + def test_one_parent_include_tasks_use_handlers(self): + ds = [{'include_tasks': '/dev/null/includes/test_include.yml'}] res = helpers.load_list_of_tasks(ds, play=self.mock_play, use_handlers=True, variable_manager=self.mock_variable_manager, loader=self.fake_include_loader) diff --git a/test/units/playbook/test_included_file.py b/test/units/playbook/test_included_file.py index 7341dffa..c7a66b06 100644 --- a/test/units/playbook/test_included_file.py +++ b/test/units/playbook/test_included_file.py @@ -105,7 +105,7 @@ def test_included_file_instantiation(): assert inc_file._task is None -def test_process_include_results(mock_iterator, mock_variable_manager): +def test_process_include_tasks_results(mock_iterator, mock_variable_manager): hostname = "testhost1" hostname2 = "testhost2" @@ -113,7 +113,7 @@ def test_process_include_results(mock_iterator, mock_variable_manager): parent_task = Task.load(parent_task_ds) parent_task._play = None - task_ds = {'include': 'include_test.yml'} + task_ds = {'include_tasks': 'include_test.yml'} loaded_task = TaskInclude.load(task_ds, task_include=parent_task) return_data = {'include': 'include_test.yml'} @@ -133,7 +133,7 @@ def test_process_include_results(mock_iterator, mock_variable_manager): assert res[0]._vars == {} -def test_process_include_diff_files(mock_iterator, mock_variable_manager): +def test_process_include_tasks_diff_files(mock_iterator, mock_variable_manager): hostname = "testhost1" hostname2 = "testhost2" @@ -141,11 +141,11 @@ def test_process_include_diff_files(mock_iterator, mock_variable_manager): parent_task = Task.load(parent_task_ds) parent_task._play = None - task_ds = {'include': 'include_test.yml'} + task_ds = {'include_tasks': 'include_test.yml'} loaded_task = TaskInclude.load(task_ds, task_include=parent_task) loaded_task._play = None - child_task_ds = {'include': 'other_include_test.yml'} + child_task_ds = {'include_tasks': 'other_include_test.yml'} loaded_child_task = TaskInclude.load(child_task_ds, task_include=loaded_task) loaded_child_task._play = None @@ -175,7 +175,7 @@ def test_process_include_diff_files(mock_iterator, mock_variable_manager): assert res[1]._vars == {} -def test_process_include_simulate_free(mock_iterator, mock_variable_manager): +def test_process_include_tasks_simulate_free(mock_iterator, mock_variable_manager): hostname = "testhost1" hostname2 = "testhost2" @@ -186,7 +186,7 @@ def test_process_include_simulate_free(mock_iterator, mock_variable_manager): parent_task1._play = None parent_task2._play = None - task_ds = {'include': 'include_test.yml'} + task_ds = {'include_tasks': 'include_test.yml'} loaded_task1 = TaskInclude.load(task_ds, task_include=parent_task1) loaded_task2 = TaskInclude.load(task_ds, task_include=parent_task2) diff --git a/test/units/playbook/test_play_context.py b/test/units/playbook/test_play_context.py index 7c24de51..7461b45f 100644 --- a/test/units/playbook/test_play_context.py +++ b/test/units/playbook/test_play_context.py @@ -12,10 +12,8 @@ import pytest from ansible import constants as C from ansible import context from ansible.cli.arguments import option_helpers as opt_help -from ansible.errors import AnsibleError from ansible.playbook.play_context import PlayContext from ansible.playbook.play import Play -from ansible.plugins.loader import become_loader from ansible.utils import context_objects as co diff --git a/test/units/playbook/test_taggable.py b/test/units/playbook/test_taggable.py index 3881e17d..c6ce35d3 100644 --- a/test/units/playbook/test_taggable.py +++ b/test/units/playbook/test_taggable.py @@ -29,6 +29,7 @@ class TaggableTestObj(Taggable): def __init__(self): self._loader = DictDataLoader({}) self.tags = [] + self._parent = None class TestTaggable(unittest.TestCase): diff --git a/test/units/playbook/test_task.py b/test/units/playbook/test_task.py index 070d7aa7..e28d2ecd 100644 --- a/test/units/playbook/test_task.py +++ b/test/units/playbook/test_task.py @@ -22,6 +22,7 @@ __metaclass__ = type from units.compat import unittest from unittest.mock import patch from ansible.playbook.task import Task +from ansible.plugins.loader import init_plugin_loader from ansible.parsing.yaml import objects from ansible import errors @@ -74,6 +75,7 @@ class TestTask(unittest.TestCase): @patch.object(errors.AnsibleError, '_get_error_lines_from_file') def test_load_task_kv_form_error_36848(self, mock_get_err_lines): + init_plugin_loader() ds = objects.AnsibleMapping(kv_bad_args_ds) ds.ansible_pos = ('test_task_faux_playbook.yml', 1, 1) mock_get_err_lines.return_value = (kv_bad_args_str, '') diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py index f2bbe194..33d09c42 100644 --- a/test/units/plugins/action/test_action.py +++ b/test/units/plugins/action/test_action.py @@ -22,6 +22,7 @@ __metaclass__ = type import os import re +from importlib import import_module from ansible import constants as C from units.compat import unittest @@ -30,9 +31,10 @@ from unittest.mock import patch, MagicMock, mock_open from ansible.errors import AnsibleError, AnsibleAuthenticationFailure from ansible.module_utils.six import text_type from ansible.module_utils.six.moves import shlex_quote, builtins -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.playbook.play_context import PlayContext from ansible.plugins.action import ActionBase +from ansible.plugins.loader import init_plugin_loader from ansible.template import Templar from ansible.vars.clean import clean_facts @@ -109,6 +111,11 @@ class TestActionBase(unittest.TestCase): self.assertEqual(results, {}) def test_action_base__configure_module(self): + init_plugin_loader() + # Pre-populate the ansible.builtin collection + # so reading the ansible_builtin_runtime.yml happens + # before the mock_open below + import_module('ansible_collections.ansible.builtin') fake_loader = DictDataLoader({ }) @@ -262,11 +269,8 @@ class TestActionBase(unittest.TestCase): def get_shell_opt(opt): - ret = None - if opt == 'admin_users': - ret = ['root', 'toor', 'Administrator'] - elif opt == 'remote_tmp': - ret = '~/.ansible/tmp' + assert opt == 'admin_users' + ret = ['root', 'toor', 'Administrator'] return ret @@ -662,17 +666,10 @@ class TestActionBase(unittest.TestCase): mock_task.no_log = False # create a mock connection, so we don't actually try and connect to things - def build_module_command(env_string, shebang, cmd, arg_path=None): - to_run = [env_string, cmd] - if arg_path: - to_run.append(arg_path) - return " ".join(to_run) - def get_option(option): return {'admin_users': ['root', 'toor']}.get(option) mock_connection = MagicMock() - mock_connection.build_module_command.side_effect = build_module_command mock_connection.socket_path = None mock_connection._shell.get_remote_filename.return_value = 'copy.py' mock_connection._shell.join_path.side_effect = os.path.join @@ -799,41 +796,7 @@ class TestActionBase(unittest.TestCase): class TestActionBaseCleanReturnedData(unittest.TestCase): def test(self): - - fake_loader = DictDataLoader({ - }) - mock_module_loader = MagicMock() - mock_shared_loader_obj = MagicMock() - mock_shared_loader_obj.module_loader = mock_module_loader - connection_loader_paths = ['/tmp/asdfadf', '/usr/lib64/whatever', - 'dfadfasf', - 'foo.py', - '.*', - # FIXME: a path with parans breaks the regex - # '(.*)', - '/path/to/ansible/lib/ansible/plugins/connection/custom_connection.py', - '/path/to/ansible/lib/ansible/plugins/connection/ssh.py'] - - def fake_all(path_only=None): - for path in connection_loader_paths: - yield path - - mock_connection_loader = MagicMock() - mock_connection_loader.all = fake_all - - mock_shared_loader_obj.connection_loader = mock_connection_loader - mock_connection = MagicMock() - # mock_connection._shell.env_prefix.side_effect = env_prefix - - # action_base = DerivedActionBase(mock_task, mock_connection, play_context, None, None, None) - action_base = DerivedActionBase(task=None, - connection=mock_connection, - play_context=None, - loader=fake_loader, - templar=None, - shared_loader_obj=mock_shared_loader_obj) data = {'ansible_playbook_python': '/usr/bin/python', - # 'ansible_rsync_path': '/usr/bin/rsync', 'ansible_python_interpreter': '/usr/bin/python', 'ansible_ssh_some_var': 'whatever', 'ansible_ssh_host_key_somehost': 'some key here', diff --git a/test/units/plugins/action/test_raw.py b/test/units/plugins/action/test_raw.py index 33480516..c50004a7 100644 --- a/test/units/plugins/action/test_raw.py +++ b/test/units/plugins/action/test_raw.py @@ -20,7 +20,6 @@ __metaclass__ = type import os -from ansible.errors import AnsibleActionFail from units.compat import unittest from unittest.mock import MagicMock, Mock from ansible.plugins.action.raw import ActionModule @@ -68,10 +67,7 @@ class TestCopyResultExclude(unittest.TestCase): task.args = {'_raw_params': 'Args1'} self.play_context.check_mode = True - try: - self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None) - except AnsibleActionFail: - pass + self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None) def test_raw_test_environment_is_None(self): diff --git a/test/units/plugins/cache/test_cache.py b/test/units/plugins/cache/test_cache.py index 25b84c06..b4ffe4e3 100644 --- a/test/units/plugins/cache/test_cache.py +++ b/test/units/plugins/cache/test_cache.py @@ -29,7 +29,7 @@ from units.compat import unittest from ansible.errors import AnsibleError from ansible.plugins.cache import CachePluginAdjudicator from ansible.plugins.cache.memory import CacheModule as MemoryCache -from ansible.plugins.loader import cache_loader +from ansible.plugins.loader import cache_loader, init_plugin_loader from ansible.vars.fact_cache import FactCache import pytest @@ -66,7 +66,7 @@ class TestCachePluginAdjudicator(unittest.TestCase): def test___getitem__(self): with pytest.raises(KeyError): - self.cache['foo'] + self.cache['foo'] # pylint: disable=pointless-statement def test_pop_with_default(self): assert self.cache.pop('foo', 'bar') == 'bar' @@ -183,6 +183,7 @@ class TestFactCache(unittest.TestCase): assert len(self.cache.keys()) == 0 def test_plugin_load_failure(self): + init_plugin_loader() # See https://github.com/ansible/ansible/issues/18751 # Note no fact_connection config set, so this will fail with mock.patch('ansible.constants.CACHE_PLUGIN', 'json'): diff --git a/test/units/plugins/connection/test_connection.py b/test/units/plugins/connection/test_connection.py index 38d66910..56095c60 100644 --- a/test/units/plugins/connection/test_connection.py +++ b/test/units/plugins/connection/test_connection.py @@ -27,6 +27,28 @@ from ansible.plugins.connection import ConnectionBase from ansible.plugins.loader import become_loader +class NoOpConnection(ConnectionBase): + + @property + def transport(self): + """This method is never called by unit tests.""" + + def _connect(self): + """This method is never called by unit tests.""" + + def exec_command(self): + """This method is never called by unit tests.""" + + def put_file(self): + """This method is never called by unit tests.""" + + def fetch_file(self): + """This method is never called by unit tests.""" + + def close(self): + """This method is never called by unit tests.""" + + class TestConnectionBaseClass(unittest.TestCase): def setUp(self): @@ -45,36 +67,8 @@ class TestConnectionBaseClass(unittest.TestCase): with self.assertRaises(TypeError): ConnectionModule1() # pylint: disable=abstract-class-instantiated - class ConnectionModule2(ConnectionBase): - def get(self, key): - super(ConnectionModule2, self).get(key) - - with self.assertRaises(TypeError): - ConnectionModule2() # pylint: disable=abstract-class-instantiated - def test_subclass_success(self): - class ConnectionModule3(ConnectionBase): - - @property - def transport(self): - pass - - def _connect(self): - pass - - def exec_command(self): - pass - - def put_file(self): - pass - - def fetch_file(self): - pass - - def close(self): - pass - - self.assertIsInstance(ConnectionModule3(self.play_context, self.in_stream), ConnectionModule3) + self.assertIsInstance(NoOpConnection(self.play_context, self.in_stream), NoOpConnection) def test_check_password_prompt(self): local = ( @@ -129,28 +123,7 @@ debug3: receive packet: type 98 debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo ''' - class ConnectionFoo(ConnectionBase): - - @property - def transport(self): - pass - - def _connect(self): - pass - - def exec_command(self): - pass - - def put_file(self): - pass - - def fetch_file(self): - pass - - def close(self): - pass - - c = ConnectionFoo(self.play_context, self.in_stream) + c = NoOpConnection(self.play_context, self.in_stream) c.set_become_plugin(become_loader.get('sudo')) c.become.prompt = '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: ' diff --git a/test/units/plugins/connection/test_local.py b/test/units/plugins/connection/test_local.py index e5525855..483a881b 100644 --- a/test/units/plugins/connection/test_local.py +++ b/test/units/plugins/connection/test_local.py @@ -21,7 +21,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from io import StringIO -import pytest from units.compat import unittest from ansible.plugins.connection import local diff --git a/test/units/plugins/connection/test_ssh.py b/test/units/plugins/connection/test_ssh.py index 662dff91..48ad3b73 100644 --- a/test/units/plugins/connection/test_ssh.py +++ b/test/units/plugins/connection/test_ssh.py @@ -24,14 +24,13 @@ from io import StringIO import pytest -from ansible import constants as C from ansible.errors import AnsibleAuthenticationFailure from units.compat import unittest from unittest.mock import patch, MagicMock, PropertyMock from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound from ansible.module_utils.compat.selectors import SelectorKey, EVENT_READ from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.playbook.play_context import PlayContext from ansible.plugins.connection import ssh from ansible.plugins.loader import connection_loader, become_loader @@ -142,9 +141,8 @@ class TestConnectionBaseClass(unittest.TestCase): conn.become.check_missing_password = MagicMock(side_effect=_check_missing_password) def get_option(option): - if option == 'become_pass': - return 'password' - return None + assert option == 'become_pass' + return 'password' conn.become.get_option = get_option output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nfoo\nline 3\nthis should be the remainder', False) @@ -351,7 +349,7 @@ class MockSelector(object): self.register = MagicMock(side_effect=self._register) self.unregister = MagicMock(side_effect=self._unregister) self.close = MagicMock() - self.get_map = MagicMock(side_effect=self._get_map) + self.get_map = MagicMock() self.select = MagicMock() def _register(self, *args, **kwargs): @@ -360,9 +358,6 @@ class MockSelector(object): def _unregister(self, *args, **kwargs): self.files_watched -= 1 - def _get_map(self, *args, **kwargs): - return self.files_watched - @pytest.fixture def mock_run_env(request, mocker): @@ -457,7 +452,8 @@ class TestSSHConnectionRun(object): def _password_with_prompt_examine_output(self, sourice, state, b_chunk, sudoable): if state == 'awaiting_prompt': self.conn._flags['become_prompt'] = True - elif state == 'awaiting_escalation': + else: + assert state == 'awaiting_escalation' self.conn._flags['become_success'] = True return (b'', b'') @@ -546,7 +542,6 @@ class TestSSHConnectionRetries(object): def test_incorrect_password(self, monkeypatch): self.conn.set_option('host_key_checking', False) self.conn.set_option('reconnection_retries', 5) - monkeypatch.setattr('time.sleep', lambda x: None) self.mock_popen_res.stdout.read.side_effect = [b''] self.mock_popen_res.stderr.read.side_effect = [b'Permission denied, please try again.\r\n'] @@ -669,7 +664,6 @@ class TestSSHConnectionRetries(object): self.conn.set_option('reconnection_retries', 3) monkeypatch.setattr('time.sleep', lambda x: None) - monkeypatch.setattr('ansible.plugins.connection.ssh.os.path.exists', lambda x: True) self.mock_popen_res.stdout.read.side_effect = [b"", b"my_stdout\n", b"second_line"] self.mock_popen_res.stderr.read.side_effect = [b"", b"my_stderr"] diff --git a/test/units/plugins/connection/test_winrm.py b/test/units/plugins/connection/test_winrm.py index cb52814b..c3060da5 100644 --- a/test/units/plugins/connection/test_winrm.py +++ b/test/units/plugins/connection/test_winrm.py @@ -13,8 +13,8 @@ import pytest from io import StringIO from unittest.mock import MagicMock -from ansible.errors import AnsibleConnectionFailure -from ansible.module_utils._text import to_bytes +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.module_utils.common.text.converters import to_bytes from ansible.playbook.play_context import PlayContext from ansible.plugins.loader import connection_loader from ansible.plugins.connection import winrm @@ -441,3 +441,103 @@ class TestWinRMKerbAuth(object): assert str(err.value) == \ "Kerberos auth failure for principal username with pexpect: " \ "Error with kinit\n<redacted>" + + def test_exec_command_with_timeout(self, monkeypatch): + requests_exc = pytest.importorskip("requests.exceptions") + + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + + mock_proto = MagicMock() + mock_proto.run_command.side_effect = requests_exc.Timeout("msg") + + conn._connected = True + conn._winrm_host = 'hostname' + + monkeypatch.setattr(conn, "_winrm_connect", lambda: mock_proto) + + with pytest.raises(AnsibleConnectionFailure) as e: + conn.exec_command('cmd', in_data=None, sudoable=True) + + assert str(e.value) == "winrm connection error: msg" + + def test_exec_command_get_output_timeout(self, monkeypatch): + requests_exc = pytest.importorskip("requests.exceptions") + + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + + mock_proto = MagicMock() + mock_proto.run_command.return_value = "command_id" + mock_proto.send_message.side_effect = requests_exc.Timeout("msg") + + conn._connected = True + conn._winrm_host = 'hostname' + + monkeypatch.setattr(conn, "_winrm_connect", lambda: mock_proto) + + with pytest.raises(AnsibleConnectionFailure) as e: + conn.exec_command('cmd', in_data=None, sudoable=True) + + assert str(e.value) == "winrm connection error: msg" + + def test_connect_failure_auth_401(self, monkeypatch): + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}}) + + mock_proto = MagicMock() + mock_proto.open_shell.side_effect = ValueError("Custom exc Code 401") + + mock_proto_init = MagicMock() + mock_proto_init.return_value = mock_proto + monkeypatch.setattr(winrm, "Protocol", mock_proto_init) + + with pytest.raises(AnsibleConnectionFailure, match="the specified credentials were rejected by the server"): + conn.exec_command('cmd', in_data=None, sudoable=True) + + def test_connect_failure_other_exception(self, monkeypatch): + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}}) + + mock_proto = MagicMock() + mock_proto.open_shell.side_effect = ValueError("Custom exc") + + mock_proto_init = MagicMock() + mock_proto_init.return_value = mock_proto + monkeypatch.setattr(winrm, "Protocol", mock_proto_init) + + with pytest.raises(AnsibleConnectionFailure, match="basic: Custom exc"): + conn.exec_command('cmd', in_data=None, sudoable=True) + + def test_connect_failure_operation_timed_out(self, monkeypatch): + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}}) + + mock_proto = MagicMock() + mock_proto.open_shell.side_effect = ValueError("Custom exc Operation timed out") + + mock_proto_init = MagicMock() + mock_proto_init.return_value = mock_proto + monkeypatch.setattr(winrm, "Protocol", mock_proto_init) + + with pytest.raises(AnsibleError, match="the connection attempt timed out"): + conn.exec_command('cmd', in_data=None, sudoable=True) + + def test_connect_no_transport(self): + pc = PlayContext() + new_stdin = StringIO() + conn = connection_loader.get('winrm', pc, new_stdin) + conn.set_options(var_options={"_extras": {}}) + conn._build_winrm_kwargs() + conn._winrm_transport = [] + + with pytest.raises(AnsibleError, match="No transport found for WinRM connection"): + conn._winrm_connect() diff --git a/test/units/plugins/filter/test_core.py b/test/units/plugins/filter/test_core.py index df4e4725..ab09ec43 100644 --- a/test/units/plugins/filter/test_core.py +++ b/test/units/plugins/filter/test_core.py @@ -3,13 +3,11 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function -from jinja2.runtime import Undefined -from jinja2.exceptions import UndefinedError __metaclass__ = type import pytest -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.plugins.filter.core import to_uuid from ansible.errors import AnsibleFilterError diff --git a/test/units/plugins/filter/test_mathstuff.py b/test/units/plugins/filter/test_mathstuff.py index f7938714..4ac5487f 100644 --- a/test/units/plugins/filter/test_mathstuff.py +++ b/test/units/plugins/filter/test_mathstuff.py @@ -1,9 +1,8 @@ # Copyright: (c) 2017, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type +from __future__ import annotations + import pytest from jinja2 import Environment @@ -12,54 +11,68 @@ import ansible.plugins.filter.mathstuff as ms from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError -UNIQUE_DATA = (([1, 3, 4, 2], [1, 3, 4, 2]), - ([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]), - (['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']), - (['a', 'a', 'd', 'b', 'a', 'd', 'c', 'b'], ['a', 'd', 'b', 'c']), - ) +UNIQUE_DATA = [ + ([], []), + ([1, 3, 4, 2], [1, 3, 4, 2]), + ([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]), + ([1, 2, 3, 4], [1, 2, 3, 4]), + ([1, 1, 4, 2, 1, 4, 3, 2], [1, 4, 2, 3]), +] + +TWO_SETS_DATA = [ + ([], [], ([], [], [])), + ([1, 2], [1, 2], ([1, 2], [], [])), + ([1, 2], [3, 4], ([], [1, 2], [1, 2, 3, 4])), + ([1, 2, 3], [5, 3, 4], ([3], [1, 2], [1, 2, 5, 4])), + ([1, 2, 3], [4, 3, 5], ([3], [1, 2], [1, 2, 4, 5])), +] + + +def dict_values(values: list[int]) -> list[dict[str, int]]: + """Return a list of non-hashable values derived from the given list.""" + return [dict(x=value) for value in values] + + +for _data, _expected in list(UNIQUE_DATA): + UNIQUE_DATA.append((dict_values(_data), dict_values(_expected))) + +for _dataset1, _dataset2, _expected in list(TWO_SETS_DATA): + TWO_SETS_DATA.append((dict_values(_dataset1), dict_values(_dataset2), tuple(dict_values(answer) for answer in _expected))) -TWO_SETS_DATA = (([1, 2], [3, 4], ([], sorted([1, 2]), sorted([1, 2, 3, 4]), sorted([1, 2, 3, 4]))), - ([1, 2, 3], [5, 3, 4], ([3], sorted([1, 2]), sorted([1, 2, 5, 4]), sorted([1, 2, 3, 4, 5]))), - (['a', 'b', 'c'], ['d', 'c', 'e'], (['c'], sorted(['a', 'b']), sorted(['a', 'b', 'd', 'e']), sorted(['a', 'b', 'c', 'e', 'd']))), - ) env = Environment() -@pytest.mark.parametrize('data, expected', UNIQUE_DATA) -class TestUnique: - def test_unhashable(self, data, expected): - assert ms.unique(env, list(data)) == expected +def assert_lists_contain_same_elements(a, b) -> None: + """Assert that the two values given are lists that contain the same elements, even when the elements cannot be sorted or hashed.""" + assert isinstance(a, list) + assert isinstance(b, list) - def test_hashable(self, data, expected): - assert ms.unique(env, tuple(data)) == expected + missing_from_a = [item for item in b if item not in a] + missing_from_b = [item for item in a if item not in b] + assert not missing_from_a, f'elements from `b` {missing_from_a} missing from `a` {a}' + assert not missing_from_b, f'elements from `a` {missing_from_b} missing from `b` {b}' -@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA) -class TestIntersect: - def test_unhashable(self, dataset1, dataset2, expected): - assert sorted(ms.intersect(env, list(dataset1), list(dataset2))) == expected[0] - def test_hashable(self, dataset1, dataset2, expected): - assert sorted(ms.intersect(env, tuple(dataset1), tuple(dataset2))) == expected[0] +@pytest.mark.parametrize('data, expected', UNIQUE_DATA, ids=str) +def test_unique(data, expected): + assert_lists_contain_same_elements(ms.unique(env, data), expected) -@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA) -class TestDifference: - def test_unhashable(self, dataset1, dataset2, expected): - assert sorted(ms.difference(env, list(dataset1), list(dataset2))) == expected[1] +@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str) +def test_intersect(dataset1, dataset2, expected): + assert_lists_contain_same_elements(ms.intersect(env, dataset1, dataset2), expected[0]) - def test_hashable(self, dataset1, dataset2, expected): - assert sorted(ms.difference(env, tuple(dataset1), tuple(dataset2))) == expected[1] +@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str) +def test_difference(dataset1, dataset2, expected): + assert_lists_contain_same_elements(ms.difference(env, dataset1, dataset2), expected[1]) -@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA) -class TestSymmetricDifference: - def test_unhashable(self, dataset1, dataset2, expected): - assert sorted(ms.symmetric_difference(env, list(dataset1), list(dataset2))) == expected[2] - def test_hashable(self, dataset1, dataset2, expected): - assert sorted(ms.symmetric_difference(env, tuple(dataset1), tuple(dataset2))) == expected[2] +@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str) +def test_symmetric_difference(dataset1, dataset2, expected): + assert_lists_contain_same_elements(ms.symmetric_difference(env, dataset1, dataset2), expected[2]) class TestLogarithm: diff --git a/test/units/plugins/inventory/test_constructed.py b/test/units/plugins/inventory/test_constructed.py index 581e0253..8ae78f1d 100644 --- a/test/units/plugins/inventory/test_constructed.py +++ b/test/units/plugins/inventory/test_constructed.py @@ -194,11 +194,11 @@ def test_parent_group_templating_error(inventory_module): 'parent_group': '{{ location.barn-yard }}' } ] - with pytest.raises(AnsibleParserError) as err_message: + with pytest.raises(AnsibleParserError) as ex: inventory_module._add_host_to_keyed_groups( keyed_groups, host.vars, host.name, strict=True ) - assert 'Could not generate parent group' in err_message + assert 'Could not generate parent group' in str(ex.value) # invalid parent group did not raise an exception with strict=False inventory_module._add_host_to_keyed_groups( keyed_groups, host.vars, host.name, strict=False @@ -213,17 +213,17 @@ def test_keyed_group_exclusive_argument(inventory_module): host = inventory_module.inventory.get_host('cow') keyed_groups = [ { - 'key': 'tag', + 'key': 'nickname', 'separator': '_', 'default_value': 'default_value_name', 'trailing_separator': True } ] - with pytest.raises(AnsibleParserError) as err_message: + with pytest.raises(AnsibleParserError) as ex: inventory_module._add_host_to_keyed_groups( keyed_groups, host.vars, host.name, strict=True ) - assert 'parameters are mutually exclusive' in err_message + assert 'parameters are mutually exclusive' in str(ex.value) def test_keyed_group_empty_value(inventory_module): diff --git a/test/units/plugins/inventory/test_inventory.py b/test/units/plugins/inventory/test_inventory.py index df246073..fb5342af 100644 --- a/test/units/plugins/inventory/test_inventory.py +++ b/test/units/plugins/inventory/test_inventory.py @@ -27,7 +27,7 @@ from unittest import mock from ansible import constants as C from units.compat import unittest from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from units.mock.path import mock_unfrackpath_noop from ansible.inventory.manager import InventoryManager, split_host_pattern diff --git a/test/units/plugins/inventory/test_script.py b/test/units/plugins/inventory/test_script.py index 9f75199f..89eb4f5b 100644 --- a/test/units/plugins/inventory/test_script.py +++ b/test/units/plugins/inventory/test_script.py @@ -28,7 +28,7 @@ from ansible import constants as C from ansible.errors import AnsibleError from ansible.plugins.loader import PluginLoader from units.compat import unittest -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils.common.text.converters import to_bytes, to_native class TestInventoryModule(unittest.TestCase): @@ -103,3 +103,11 @@ class TestInventoryModule(unittest.TestCase): self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py') assert e.value.message == to_native("failed to parse executable inventory script results from " "/foo/bar/foobar.py: needs to be a json dict\ndummyédata\n") + + def test_get_host_variables_subprocess_script_raises_error(self): + self.popen_result.returncode = 1 + self.popen_result.stderr = to_bytes("dummyéerror") + + with pytest.raises(AnsibleError) as e: + self.inventory_module.get_host_variables('/foo/bar/foobar.py', 'dummy host') + assert e.value.message == "Inventory script (/foo/bar/foobar.py) had an execution error: dummyéerror" diff --git a/test/units/plugins/lookup/test_password.py b/test/units/plugins/lookup/test_password.py index 318bc10b..685f2ce7 100644 --- a/test/units/plugins/lookup/test_password.py +++ b/test/units/plugins/lookup/test_password.py @@ -23,7 +23,7 @@ __metaclass__ = type try: import passlib from passlib.handlers import pbkdf2 -except ImportError: +except ImportError: # pragma: nocover passlib = None pbkdf2 = None @@ -36,7 +36,7 @@ from unittest.mock import mock_open, patch from ansible.errors import AnsibleError from ansible.module_utils.six import text_type from ansible.module_utils.six.moves import builtins -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.plugins.loader import PluginLoader, lookup_loader from ansible.plugins.lookup import password @@ -416,8 +416,6 @@ class BaseTestLookupModule(unittest.TestCase): password.os.open = lambda path, flag: None self.os_close = password.os.close password.os.close = lambda fd: None - self.os_remove = password.os.remove - password.os.remove = lambda path: None self.makedirs_safe = password.makedirs_safe password.makedirs_safe = lambda path, mode: None @@ -425,7 +423,6 @@ class BaseTestLookupModule(unittest.TestCase): password.os.path.exists = self.os_path_exists password.os.open = self.os_open password.os.close = self.os_close - password.os.remove = self.os_remove password.makedirs_safe = self.makedirs_safe @@ -467,23 +464,17 @@ class TestLookupModuleWithoutPasslib(BaseTestLookupModule): def test_lock_been_held(self, mock_sleep): # pretend the lock file is here password.os.path.exists = lambda x: True - try: + with pytest.raises(AnsibleError): with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m: # should timeout here - results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None) - self.fail("Lookup didn't timeout when lock already been held") - except AnsibleError: - pass + self.password_lookup.run([u'/path/to/somewhere chars=anything'], None) def test_lock_not_been_held(self): # pretend now there is password file but no lock password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere') - try: - with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m: - # should not timeout here - results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None) - except AnsibleError: - self.fail('Lookup timeouts when lock is free') + with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m: + # should not timeout here + results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None) for result in results: self.assertEqual(result, u'hunter42') @@ -531,10 +522,8 @@ class TestLookupModuleWithPasslib(BaseTestLookupModule): self.assertEqual(int(str_parts[2]), crypt_parts['rounds']) self.assertIsInstance(result, text_type) - @patch.object(PluginLoader, '_get_paths') @patch('ansible.plugins.lookup.password._write_password_file') - def test_password_already_created_encrypt(self, mock_get_paths, mock_write_file): - mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three'] + def test_password_already_created_encrypt(self, mock_write_file): password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere') with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m: @@ -542,6 +531,9 @@ class TestLookupModuleWithPasslib(BaseTestLookupModule): for result in results: self.assertEqual(result, u'$pbkdf2-sha256$20000$ODc2NTQzMjE$Uikde0cv0BKaRaAXMrUQB.zvG4GmnjClwjghwIRf2gU') + # Assert the password file is not rewritten + mock_write_file.assert_not_called() + @pytest.mark.skipif(passlib is None, reason='passlib must be installed to run these tests') class TestLookupModuleWithPasslibWrappedAlgo(BaseTestLookupModule): diff --git a/test/units/plugins/test_plugins.py b/test/units/plugins/test_plugins.py index be123b15..ba2ad2b6 100644 --- a/test/units/plugins/test_plugins.py +++ b/test/units/plugins/test_plugins.py @@ -46,14 +46,14 @@ class TestErrors(unittest.TestCase): # python library, and then uses the __file__ attribute of # the result for that to get the library path, so we mock # that here and patch the builtin to use our mocked result - foo = MagicMock() - bar = MagicMock() + foo_pkg = MagicMock() + bar_pkg = MagicMock() bam = MagicMock() bam.__file__ = '/path/to/my/foo/bar/bam/__init__.py' - bar.bam = bam - foo.return_value.bar = bar + bar_pkg.bam = bam + foo_pkg.return_value.bar = bar_pkg pl = PluginLoader('test', 'foo.bar.bam', 'test', 'test_plugin') - with patch('builtins.__import__', foo): + with patch('builtins.__import__', foo_pkg): self.assertEqual(pl._get_package_paths(), ['/path/to/my/foo/bar/bam']) def test_plugins__get_paths(self): diff --git a/test/units/requirements.txt b/test/units/requirements.txt index 1822adaa..c77c55cd 100644 --- a/test/units/requirements.txt +++ b/test/units/requirements.txt @@ -1,4 +1,4 @@ -bcrypt ; python_version >= '3.9' # controller only -passlib ; python_version >= '3.9' # controller only -pexpect ; python_version >= '3.9' # controller only -pywinrm ; python_version >= '3.9' # controller only +bcrypt ; python_version >= '3.10' # controller only +passlib ; python_version >= '3.10' # controller only +pexpect ; python_version >= '3.10' # controller only +pywinrm ; python_version >= '3.10' # controller only diff --git a/test/units/template/test_templar.py b/test/units/template/test_templar.py index 6747f768..02840e16 100644 --- a/test/units/template/test_templar.py +++ b/test/units/template/test_templar.py @@ -22,11 +22,10 @@ __metaclass__ = type from jinja2.runtime import Context from units.compat import unittest -from unittest.mock import patch from ansible import constants as C from ansible.errors import AnsibleError, AnsibleUndefinedVariable -from ansible.module_utils.six import string_types +from ansible.plugins.loader import init_plugin_loader from ansible.template import Templar, AnsibleContext, AnsibleEnvironment, AnsibleUndefined from ansible.utils.unsafe_proxy import AnsibleUnsafe, wrap_var from units.mock.loader import DictDataLoader @@ -34,6 +33,7 @@ from units.mock.loader import DictDataLoader class BaseTemplar(object): def setUp(self): + init_plugin_loader() self.test_vars = dict( foo="bar", bam="{{foo}}", @@ -62,14 +62,6 @@ class BaseTemplar(object): return self._ansible_context._is_unsafe(obj) -# class used for testing arbitrary objects passed to template -class SomeClass(object): - foo = 'bar' - - def __init__(self): - self.blip = 'blip' - - class SomeUnsafeClass(AnsibleUnsafe): def __init__(self): super(SomeUnsafeClass, self).__init__() @@ -266,8 +258,6 @@ class TestTemplarMisc(BaseTemplar, unittest.TestCase): templar.available_variables = "foo=bam" except AssertionError: pass - except Exception as e: - self.fail(e) def test_templar_escape_backslashes(self): # Rule of thumb: If escape backslashes is True you should end up with diff --git a/test/units/template/test_vars.py b/test/units/template/test_vars.py index 514104f2..f43cfac4 100644 --- a/test/units/template/test_vars.py +++ b/test/units/template/test_vars.py @@ -19,23 +19,16 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from units.compat import unittest -from unittest.mock import MagicMock - +from ansible.template import Templar from ansible.template.vars import AnsibleJ2Vars -class TestVars(unittest.TestCase): - def setUp(self): - self.mock_templar = MagicMock(name='mock_templar') +def test_globals_empty(): + assert isinstance(dict(AnsibleJ2Vars(Templar(None), {})), dict) - def test_globals_empty(self): - ajvars = AnsibleJ2Vars(self.mock_templar, {}) - res = dict(ajvars) - self.assertIsInstance(res, dict) - def test_globals(self): - res = dict(AnsibleJ2Vars(self.mock_templar, {'foo': 'bar', 'blip': [1, 2, 3]})) - self.assertIsInstance(res, dict) - self.assertIn('foo', res) - self.assertEqual(res['foo'], 'bar') +def test_globals(): + res = dict(AnsibleJ2Vars(Templar(None), {'foo': 'bar', 'blip': [1, 2, 3]})) + assert isinstance(res, dict) + assert 'foo' in res + assert res['foo'] == 'bar' diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py index 9d30580f..a85f422a 100644 --- a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py +++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ..module_utils.my_util import question +from ..module_utils.my_util import question # pylint: disable=unused-import def action_code(): diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py index 35e1381b..463b1334 100644 --- a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py +++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py @@ -1,4 +1,4 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from .my_util import question +from .my_util import question # pylint: disable=unused-import diff --git a/test/units/utils/collection_loader/test_collection_loader.py b/test/units/utils/collection_loader/test_collection_loader.py index f7050dcd..feaaf97a 100644 --- a/test/units/utils/collection_loader/test_collection_loader.py +++ b/test/units/utils/collection_loader/test_collection_loader.py @@ -13,7 +13,7 @@ from ansible.modules import ping as ping_module from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef from ansible.utils.collection_loader._collection_finder import ( _AnsibleCollectionFinder, _AnsibleCollectionLoader, _AnsibleCollectionNSPkgLoader, _AnsibleCollectionPkgLoader, - _AnsibleCollectionPkgLoaderBase, _AnsibleCollectionRootPkgLoader, _AnsiblePathHookFinder, + _AnsibleCollectionPkgLoaderBase, _AnsibleCollectionRootPkgLoader, _AnsibleNSTraversable, _AnsiblePathHookFinder, _get_collection_name_from_path, _get_collection_role_path, _get_collection_metadata, _iter_modules_impl ) from ansible.utils.collection_loader._collection_config import _EventSource @@ -29,8 +29,16 @@ def teardown(*args, **kwargs): # BEGIN STANDALONE TESTS - these exercise behaviors of the individual components without the import machinery -@pytest.mark.skipif(not PY3, reason='Testing Python 2 codepath (find_module) on Python 3') -def test_find_module_py3(): +@pytest.mark.filterwarnings( + 'ignore:' + r'find_module\(\) is deprecated and slated for removal in Python 3\.12; use find_spec\(\) instead' + ':DeprecationWarning', + 'ignore:' + r'FileFinder\.find_loader\(\) is deprecated and slated for removal in Python 3\.12; use find_spec\(\) instead' + ':DeprecationWarning', +) +@pytest.mark.skipif(not PY3 or sys.version_info >= (3, 12), reason='Testing Python 2 codepath (find_module) on Python 3, <= 3.11') +def test_find_module_py3_lt_312(): dir_to_a_file = os.path.dirname(ping_module.__file__) path_hook_finder = _AnsiblePathHookFinder(_AnsibleCollectionFinder(), dir_to_a_file) @@ -40,6 +48,16 @@ def test_find_module_py3(): assert path_hook_finder.find_module('missing') is None +@pytest.mark.skipif(sys.version_info < (3, 12), reason='Testing Python 2 codepath (find_module) on Python >= 3.12') +def test_find_module_py3_gt_311(): + dir_to_a_file = os.path.dirname(ping_module.__file__) + path_hook_finder = _AnsiblePathHookFinder(_AnsibleCollectionFinder(), dir_to_a_file) + + # setuptools may fall back to find_module on Python 3 if find_spec returns None + # see https://github.com/pypa/setuptools/pull/2918 + assert path_hook_finder.find_spec('missing') is None + + def test_finder_setup(): # ensure scalar path is listified f = _AnsibleCollectionFinder(paths='/bogus/bogus') @@ -828,6 +846,53 @@ def test_collectionref_components_invalid(name, subdirs, resource, ref_type, exp assert re.search(expected_error_expression, str(curerr.value)) +@pytest.mark.skipif(not PY3, reason='importlib.resources only supported for py3') +def test_importlib_resources(): + if sys.version_info < (3, 10): + from importlib_resources import files + else: + from importlib.resources import files + from pathlib import Path + + f = get_default_finder() + reset_collections_loader_state(f) + + ansible_collections_ns = files('ansible_collections') + ansible_ns = files('ansible_collections.ansible') + testns = files('ansible_collections.testns') + testcoll = files('ansible_collections.testns.testcoll') + testcoll2 = files('ansible_collections.testns.testcoll2') + module_utils = files('ansible_collections.testns.testcoll.plugins.module_utils') + + assert isinstance(ansible_collections_ns, _AnsibleNSTraversable) + assert isinstance(ansible_ns, _AnsibleNSTraversable) + assert isinstance(testcoll, Path) + assert isinstance(module_utils, Path) + + assert ansible_collections_ns.is_dir() + assert ansible_ns.is_dir() + assert testcoll.is_dir() + assert module_utils.is_dir() + + first_path = Path(default_test_collection_paths[0]) + second_path = Path(default_test_collection_paths[1]) + testns_paths = [] + ansible_ns_paths = [] + for path in default_test_collection_paths[:2]: + ansible_ns_paths.append(Path(path) / 'ansible_collections' / 'ansible') + testns_paths.append(Path(path) / 'ansible_collections' / 'testns') + + assert testns._paths == testns_paths + # NOTE: The next two asserts check for subsets to accommodate running the unit tests when externally installed collections are available. + assert set(ansible_ns_paths).issubset(ansible_ns._paths) + assert set(Path(p) / 'ansible_collections' for p in default_test_collection_paths[:2]).issubset(ansible_collections_ns._paths) + assert testcoll2 == second_path / 'ansible_collections' / 'testns' / 'testcoll2' + + assert {p.name for p in module_utils.glob('*.py')} == {'__init__.py', 'my_other_util.py', 'my_util.py'} + nestcoll_mu_init = first_path / 'ansible_collections' / 'testns' / 'testcoll' / 'plugins' / 'module_utils' / '__init__.py' + assert next(module_utils.glob('__init__.py')) == nestcoll_mu_init + + # BEGIN TEST SUPPORT default_test_collection_paths = [ diff --git a/test/units/utils/display/test_broken_cowsay.py b/test/units/utils/display/test_broken_cowsay.py index d888010a..96157e1a 100644 --- a/test/units/utils/display/test_broken_cowsay.py +++ b/test/units/utils/display/test_broken_cowsay.py @@ -12,16 +12,13 @@ from unittest.mock import MagicMock def test_display_with_fake_cowsay_binary(capsys, mocker): - mocker.patch("ansible.constants.ANSIBLE_COW_PATH", "./cowsay.sh") + display = Display() - def mock_communicate(input=None, timeout=None): - return b"", b"" + mocker.patch("ansible.constants.ANSIBLE_COW_PATH", "./cowsay.sh") mock_popen = MagicMock() - mock_popen.return_value.communicate = mock_communicate mock_popen.return_value.returncode = 1 mocker.patch("subprocess.Popen", mock_popen) - display = Display() assert not hasattr(display, "cows_available") assert display.b_cowsay is None diff --git a/test/units/utils/test_cleanup_tmp_file.py b/test/units/utils/test_cleanup_tmp_file.py index 2a44a55b..35374f4d 100644 --- a/test/units/utils/test_cleanup_tmp_file.py +++ b/test/units/utils/test_cleanup_tmp_file.py @@ -6,16 +6,11 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import os -import pytest import tempfile from ansible.utils.path import cleanup_tmp_file -def raise_error(): - raise OSError - - def test_cleanup_tmp_file_file(): tmp_fd, tmp = tempfile.mkstemp() cleanup_tmp_file(tmp) @@ -34,15 +29,21 @@ def test_cleanup_tmp_file_nonexistant(): assert None is cleanup_tmp_file('nope') -def test_cleanup_tmp_file_failure(mocker): +def test_cleanup_tmp_file_failure(mocker, capsys): tmp = tempfile.mkdtemp() - with pytest.raises(Exception): - mocker.patch('shutil.rmtree', side_effect=raise_error()) - cleanup_tmp_file(tmp) + rmtree = mocker.patch('shutil.rmtree', side_effect=OSError('test induced failure')) + cleanup_tmp_file(tmp) + out, err = capsys.readouterr() + assert out == '' + assert err == '' + rmtree.assert_called_once() def test_cleanup_tmp_file_failure_warning(mocker, capsys): tmp = tempfile.mkdtemp() - with pytest.raises(Exception): - mocker.patch('shutil.rmtree', side_effect=raise_error()) - cleanup_tmp_file(tmp, warn=True) + rmtree = mocker.patch('shutil.rmtree', side_effect=OSError('test induced failure')) + cleanup_tmp_file(tmp, warn=True) + out, err = capsys.readouterr() + assert out == 'Unable to remove temporary file test induced failure\n' + assert err == '' + rmtree.assert_called_once() diff --git a/test/units/utils/test_display.py b/test/units/utils/test_display.py index 6b1914bb..80b7a099 100644 --- a/test/units/utils/test_display.py +++ b/test/units/utils/test_display.py @@ -18,16 +18,14 @@ from ansible.utils.multiprocessing import context as multiprocessing_context @pytest.fixture def problematic_wcswidth_chars(): - problematic = [] - try: - locale.setlocale(locale.LC_ALL, 'C.UTF-8') - except Exception: - return problematic + locale.setlocale(locale.LC_ALL, 'C.UTF-8') candidates = set(chr(c) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == 'Cf') - for c in candidates: - if _LIBC.wcswidth(c, _MAX_INT) == -1: - problematic.append(c) + problematic = [candidate for candidate in candidates if _LIBC.wcswidth(candidate, _MAX_INT) == -1] + + if not problematic: + # Newer distributions (Ubuntu 22.04, Fedora 38) include a libc which does not report problematic characters. + pytest.skip("no problematic wcswidth chars found") # pragma: nocover return problematic @@ -54,9 +52,6 @@ def test_get_text_width(): def test_get_text_width_no_locale(problematic_wcswidth_chars): - if not problematic_wcswidth_chars: - pytest.skip("No problmatic wcswidth chars") - locale.setlocale(locale.LC_ALL, 'C.UTF-8') pytest.raises(EnvironmentError, get_text_width, problematic_wcswidth_chars[0]) @@ -108,9 +103,21 @@ def test_Display_display_fork(): display = Display() display.set_queue(queue) display.display('foo') - queue.send_display.assert_called_once_with( - 'foo', color=None, stderr=False, screen_only=False, log_only=False, newline=True - ) + queue.send_display.assert_called_once_with('display', 'foo') + + p = multiprocessing_context.Process(target=test) + p.start() + p.join() + assert p.exitcode == 0 + + +def test_Display_display_warn_fork(): + def test(): + queue = MagicMock() + display = Display() + display.set_queue(queue) + display.warning('foo') + queue.send_display.assert_called_once_with('warning', 'foo') p = multiprocessing_context.Process(target=test) p.start() diff --git a/test/units/utils/test_encrypt.py b/test/units/utils/test_encrypt.py index 72fe3b07..be325790 100644 --- a/test/units/utils/test_encrypt.py +++ b/test/units/utils/test_encrypt.py @@ -27,17 +27,26 @@ class passlib_off(object): def assert_hash(expected, secret, algorithm, **settings): + assert encrypt.do_encrypt(secret, algorithm, **settings) == expected if encrypt.PASSLIB_AVAILABLE: - assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected else: - assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected with pytest.raises(AnsibleError) as excinfo: encrypt.PasslibHash(algorithm).hash(secret, **settings) assert excinfo.value.args[0] == "passlib must be installed and usable to hash with '%s'" % algorithm @pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib') +def test_passlib_or_crypt(): + with passlib_off(): + expected = "$5$rounds=5000$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7" + assert encrypt.passlib_or_crypt("123", "sha256_crypt", salt="12345678", rounds=5000) == expected + + expected = "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7" + assert encrypt.passlib_or_crypt("123", "sha256_crypt", salt="12345678", rounds=5000) == expected + + +@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib') def test_encrypt_with_rounds_no_passlib(): with passlib_off(): assert_hash("$5$rounds=5000$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7", diff --git a/test/units/utils/test_unsafe_proxy.py b/test/units/utils/test_unsafe_proxy.py index ea653cfe..55f1b6dd 100644 --- a/test/units/utils/test_unsafe_proxy.py +++ b/test/units/utils/test_unsafe_proxy.py @@ -5,7 +5,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.six import PY3 +import pathlib +import sys + from ansible.utils.unsafe_proxy import AnsibleUnsafe, AnsibleUnsafeBytes, AnsibleUnsafeText, wrap_var from ansible.module_utils.common.text.converters import to_text, to_bytes @@ -19,10 +21,7 @@ def test_wrap_var_bytes(): def test_wrap_var_string(): - if PY3: - assert isinstance(wrap_var('foo'), AnsibleUnsafeText) - else: - assert isinstance(wrap_var('foo'), AnsibleUnsafeBytes) + assert isinstance(wrap_var('foo'), AnsibleUnsafeText) def test_wrap_var_dict(): @@ -95,12 +94,12 @@ def test_wrap_var_no_ref(): 'text': 'text', } wrapped_thing = wrap_var(thing) - thing is not wrapped_thing - thing['foo'] is not wrapped_thing['foo'] - thing['bar'][0] is not wrapped_thing['bar'][0] - thing['baz'][0] is not wrapped_thing['baz'][0] - thing['none'] is not wrapped_thing['none'] - thing['text'] is not wrapped_thing['text'] + assert thing is not wrapped_thing + assert thing['foo'] is not wrapped_thing['foo'] + assert thing['bar'][0] is not wrapped_thing['bar'][0] + assert thing['baz'][0] is not wrapped_thing['baz'][0] + assert thing['none'] is wrapped_thing['none'] + assert thing['text'] is not wrapped_thing['text'] def test_AnsibleUnsafeText(): @@ -119,3 +118,10 @@ def test_to_text_unsafe(): def test_to_bytes_unsafe(): assert isinstance(to_bytes(AnsibleUnsafeText(u'foo')), AnsibleUnsafeBytes) assert to_bytes(AnsibleUnsafeText(u'foo')) == AnsibleUnsafeBytes(b'foo') + + +def test_unsafe_with_sys_intern(): + # Specifically this is actually about sys.intern, test of pathlib + # because that is a specific affected use + assert sys.intern(AnsibleUnsafeText('foo')) == 'foo' + assert pathlib.Path(AnsibleUnsafeText('/tmp')) == pathlib.Path('/tmp') diff --git a/test/units/vars/test_module_response_deepcopy.py b/test/units/vars/test_module_response_deepcopy.py index 78f9de0e..3313dea1 100644 --- a/test/units/vars/test_module_response_deepcopy.py +++ b/test/units/vars/test_module_response_deepcopy.py @@ -7,8 +7,6 @@ __metaclass__ = type from ansible.vars.clean import module_response_deepcopy -import pytest - def test_module_response_deepcopy_basic(): x = 42 @@ -37,15 +35,6 @@ def test_module_response_deepcopy_empty_tuple(): assert x is y -@pytest.mark.skip(reason='No current support for this situation') -def test_module_response_deepcopy_tuple(): - x = ([1, 2], 3) - y = module_response_deepcopy(x) - assert y == x - assert x is not y - assert x[0] is not y[0] - - def test_module_response_deepcopy_tuple_of_immutables(): x = ((1, 2), 3) y = module_response_deepcopy(x) diff --git a/test/units/vars/test_variable_manager.py b/test/units/vars/test_variable_manager.py index 67ec120b..ee6de817 100644 --- a/test/units/vars/test_variable_manager.py +++ b/test/units/vars/test_variable_manager.py @@ -141,10 +141,8 @@ class TestVariableManager(unittest.TestCase): return # pylint: disable=unreachable - ''' - Tests complex variations and combinations of get_vars() with different - objects to modify the context under which variables are merged. - ''' + # Tests complex variations and combinations of get_vars() with different + # objects to modify the context under which variables are merged. # FIXME: BCS makethiswork # return True |