From fe2732b91e538e0278104d71417ddfd0aae01eed Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 20 Feb 2023 17:54:20 -0800 Subject: [PATCH] ansible-test - Support pylint assertion rewriting (#80020) Add support for `pylint` assertion rewriting when running unit tests on Python 3.5 and later. --- ...nsible-test-pytest-assertion-rewriting.yml | 5 ++ .../ansible-test-units-assertions/aliases | 4 ++ .../unit/plugins/modules/test_assertion.py | 6 +++ .../ansible-test-units-assertions/runme.sh | 22 +++++++++ .../targets/ansible-test/venv-pythons.py | 10 ++++ .../plugins/ansible_pytest_collections.py | 46 +++++++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 changelogs/fragments/ansible-test-pytest-assertion-rewriting.yml create mode 100644 test/integration/targets/ansible-test-units-assertions/aliases create mode 100644 test/integration/targets/ansible-test-units-assertions/ansible_collections/ns/col/tests/unit/plugins/modules/test_assertion.py create mode 100755 test/integration/targets/ansible-test-units-assertions/runme.sh diff --git a/changelogs/fragments/ansible-test-pytest-assertion-rewriting.yml b/changelogs/fragments/ansible-test-pytest-assertion-rewriting.yml new file mode 100644 index 00000000000..df357c2de20 --- /dev/null +++ b/changelogs/fragments/ansible-test-pytest-assertion-rewriting.yml @@ -0,0 +1,5 @@ +bugfixes: + - ansible-test - Add support for ``pylint`` assertion rewriting when running unit tests on Python 3.5 and later. + Resolves issue https://github.com/ansible/ansible/issues/68032 +known_issues: + - ansible-test - Unit tests for collections do not support ``pylint`` assertion rewriting on Python 2.7. diff --git a/test/integration/targets/ansible-test-units-assertions/aliases b/test/integration/targets/ansible-test-units-assertions/aliases new file mode 100644 index 00000000000..f25bc6778e5 --- /dev/null +++ b/test/integration/targets/ansible-test-units-assertions/aliases @@ -0,0 +1,4 @@ +shippable/generic/group1 # runs in the default test container +context/controller +needs/target/collection +needs/target/ansible-test diff --git a/test/integration/targets/ansible-test-units-assertions/ansible_collections/ns/col/tests/unit/plugins/modules/test_assertion.py b/test/integration/targets/ansible-test-units-assertions/ansible_collections/ns/col/tests/unit/plugins/modules/test_assertion.py new file mode 100644 index 00000000000..e172200410f --- /dev/null +++ b/test/integration/targets/ansible-test-units-assertions/ansible_collections/ns/col/tests/unit/plugins/modules/test_assertion.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def test_assertion(): + assert dict(yes=True) == dict(no=False) diff --git a/test/integration/targets/ansible-test-units-assertions/runme.sh b/test/integration/targets/ansible-test-units-assertions/runme.sh new file mode 100755 index 00000000000..671fc129910 --- /dev/null +++ b/test/integration/targets/ansible-test-units-assertions/runme.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +source ../collection/setup.sh + +set -x + +options=$("${TEST_DIR}"/../ansible-test/venv-pythons.py --only-versions) +IFS=', ' read -r -a pythons <<< "${options}" + +for python in "${pythons[@]}"; do + if ansible-test units --color --truncate 0 --python "${python}" --requirements "${@}" 2>&1 | tee pylint.log; then + echo "Test did not fail as expected." + exit 1 + fi + + if [ "${python}" = "2.7" ]; then + grep "^E *AssertionError$" pylint.log + else + + grep "^E *AssertionError: assert {'yes': True} == {'no': False}$" pylint.log + fi +done diff --git a/test/integration/targets/ansible-test/venv-pythons.py b/test/integration/targets/ansible-test/venv-pythons.py index b380f147fca..97998bcd7c2 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/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 fefd6b0f8eb..8473d9b4c20 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 pylint's AssertionRewritingHook on Python 3.x. + This is necessary because the Ansible collection loader intercepts imports before the pylint 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