You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py

113 lines
4.9 KiB
Python

"""Enable unit testing of Ansible collections. PYTEST_DONT_REWRITE"""
from __future__ import annotations
import os
# set by ansible-test to a single directory, rather than a list of directories as supported by Ansible itself
ANSIBLE_COLLECTIONS_PATH = os.path.join(os.environ['ANSIBLE_COLLECTIONS_PATH'], 'ansible_collections')
# set by ansible-test to the minimum python version supported on the controller
ANSIBLE_CONTROLLER_MIN_PYTHON_VERSION = tuple(int(x) for x in os.environ['ANSIBLE_CONTROLLER_MIN_PYTHON_VERSION'].split('.'))
# this monkeypatch to _pytest.pathlib.resolve_package_path fixes PEP420 resolution for collections in pytest >= 6.0.0
# NB: this code should never run under py2
def collection_resolve_package_path(path):
"""Configure the Python package path so that pytest can find our collections."""
for parent in path.parents:
if str(parent) == ANSIBLE_COLLECTIONS_PATH:
return parent
raise Exception('File "%s" not found in collection path "%s".' % (path, ANSIBLE_COLLECTIONS_PATH))
# this monkeypatch to py.path.local.LocalPath.pypkgpath fixes PEP420 resolution for collections in pytest < 6.0.0
def collection_pypkgpath(self):
"""Configure the Python package path so that pytest can find our collections."""
for parent in self.parts(reverse=True):
if str(parent) == ANSIBLE_COLLECTIONS_PATH:
return parent
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
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:
if pytest_configure.executed:
return
except AttributeError:
pytest_configure.executed = True
enable_assertion_rewriting_hook()
# noinspection PyProtectedMember
from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder
# allow unit tests to import code from collections
# noinspection PyProtectedMember
_AnsibleCollectionFinder(paths=[os.path.dirname(ANSIBLE_COLLECTIONS_PATH)])._install() # pylint: disable=protected-access
try:
# noinspection PyProtectedMember
from _pytest import pathlib as _pytest_pathlib
except ImportError:
_pytest_pathlib = None
if hasattr(_pytest_pathlib, 'resolve_package_path'):
_pytest_pathlib.resolve_package_path = collection_resolve_package_path
else:
# looks like pytest <= 6.0.0, use the old hack against py.path
# noinspection PyProtectedMember
import py._path.local
# force collections unit tests to be loaded with the ansible_collections namespace
# original idea from https://stackoverflow.com/questions/50174130/how-do-i-pytest-a-project-using-pep-420-namespace-packages/50175552#50175552
# noinspection PyProtectedMember
py._path.local.LocalPath.pypkgpath = collection_pypkgpath # pylint: disable=protected-access
pytest_configure()