summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMatt Clay <matt@mystile.com>2022-08-03 10:11:22 -0700
committerGitHub <noreply@github.com>2022-08-03 10:11:22 -0700
commit426e4899a3dab37b88f128807616502b99dab5ae (patch)
treedc8f22734321611c47ed155cb7fe7511e0d63bac /test
parentcbec27fefc543258ecf3f71746551247d7f20d3a (diff)
downloadansible-426e4899a3dab37b88f128807616502b99dab5ae.tar.gz
[stable-2.13] ansible-test - Parse content config only once. (#78418) (#78426)
(cherry picked from commit f2abfc4b3d03a2baa078477d0ad2241263a00668)
Diffstat (limited to 'test')
-rw-r--r--test/integration/targets/ansible-test-config-invalid/aliases4
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml1
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases1
-rwxr-xr-xtest/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh1
-rw-r--r--test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py2
-rwxr-xr-xtest/integration/targets/ansible-test-config-invalid/runme.sh12
-rw-r--r--test/integration/targets/ansible-test-config/aliases4
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py14
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml2
-rw-r--r--test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py5
-rwxr-xr-xtest/integration/targets/ansible-test-config/runme.sh15
-rw-r--r--test/lib/ansible_test/_internal/commands/sanity/__init__.py18
-rw-r--r--test/lib/ansible_test/_internal/commands/units/__init__.py2
-rw-r--r--test/lib/ansible_test/_internal/config.py23
-rw-r--r--test/lib/ansible_test/_internal/content_config.py102
-rw-r--r--test/lib/ansible_test/_internal/delegation.py5
16 files changed, 165 insertions, 46 deletions
diff --git a/test/integration/targets/ansible-test-config-invalid/aliases b/test/integration/targets/ansible-test-config-invalid/aliases
new file mode 100644
index 0000000000..193276cc9e
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group1 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml
new file mode 100644
index 0000000000..9977a2836c
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml
@@ -0,0 +1 @@
+invalid
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases
new file mode 100644
index 0000000000..1af1cf90b6
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases
@@ -0,0 +1 @@
+context/controller
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh
new file mode 100755
index 0000000000..f1f641af19
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
new file mode 100644
index 0000000000..06e7782e57
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
@@ -0,0 +1,2 @@
+def test_me():
+ pass
diff --git a/test/integration/targets/ansible-test-config-invalid/runme.sh b/test/integration/targets/ansible-test-config-invalid/runme.sh
new file mode 100755
index 0000000000..6ff2d4067b
--- /dev/null
+++ b/test/integration/targets/ansible-test-config-invalid/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Make sure that ansible-test continues to work when content config is invalid.
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+ansible-test sanity --test import --python "${ANSIBLE_TEST_PYTHON_VERSION}" --color --venv -v
+ansible-test units --python "${ANSIBLE_TEST_PYTHON_VERSION}" --color --venv -v
+ansible-test integration --color --venv -v
diff --git a/test/integration/targets/ansible-test-config/aliases b/test/integration/targets/ansible-test-config/aliases
new file mode 100644
index 0000000000..193276cc9e
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/aliases
@@ -0,0 +1,4 @@
+shippable/posix/group1 # runs in the distro test containers
+shippable/generic/group1 # runs in the default test container
+context/controller
+needs/target/collection
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py
new file mode 100644
index 0000000000..962dba2b49
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py
@@ -0,0 +1,14 @@
+import sys
+import os
+
+
+def version_to_str(value):
+ return '.'.join(str(v) for v in value)
+
+
+controller_min_python_version = tuple(int(v) for v in os.environ['ANSIBLE_CONTROLLER_MIN_PYTHON_VERSION'].split('.'))
+current_python_version = sys.version_info[:2]
+
+if current_python_version < controller_min_python_version:
+ raise Exception('Current Python version %s is lower than the minimum controller Python version of %s. '
+ 'Did the collection config get ignored?' % (version_to_str(current_python_version), version_to_str(controller_min_python_version)))
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml
new file mode 100644
index 0000000000..7772d7d202
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml
@@ -0,0 +1,2 @@
+modules:
+ python_requires: controller # allow tests to pass when run against a Python version not supported by the controller
diff --git a/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
new file mode 100644
index 0000000000..b320a15aa7
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py
@@ -0,0 +1,5 @@
+from ansible_collections.ns.col.plugins.module_utils import test
+
+
+def test_me():
+ assert test
diff --git a/test/integration/targets/ansible-test-config/runme.sh b/test/integration/targets/ansible-test-config/runme.sh
new file mode 100755
index 0000000000..9636d04daa
--- /dev/null
+++ b/test/integration/targets/ansible-test-config/runme.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Make sure that ansible-test is able to parse collection config when using a venv.
+
+set -eu
+
+source ../collection/setup.sh
+
+set -x
+
+# On systems with a Python version below the minimum controller Python version, such as the default container, this test
+# will verify that the content config is working properly after delegation. Otherwise it will only verify that no errors
+# occur while trying to access content config (such as missing requirements).
+
+ansible-test sanity --test import --color --venv -v
+ansible-test units --color --venv -v
diff --git a/test/lib/ansible_test/_internal/commands/sanity/__init__.py b/test/lib/ansible_test/_internal/commands/sanity/__init__.py
index ff3a61699d..3069511091 100644
--- a/test/lib/ansible_test/_internal/commands/sanity/__init__.py
+++ b/test/lib/ansible_test/_internal/commands/sanity/__init__.py
@@ -162,6 +162,8 @@ def command_sanity(args): # type: (SanityConfig) -> None
targets_use_pypi = any(isinstance(test, SanityMultipleVersion) and test.needs_pypi for test in tests) and not args.list_tests
host_state = prepare_profiles(args, targets_use_pypi=targets_use_pypi) # sanity
+ get_content_config(args) # make sure content config has been parsed prior to delegation
+
if args.delegate:
raise Delegate(host_state=host_state, require=changes, exclude=args.exclude)
@@ -220,7 +222,7 @@ def command_sanity(args): # type: (SanityConfig) -> None
all_targets = SanityTargets.filter_and_inject_targets(test, all_targets)
usable_targets = SanityTargets.filter_and_inject_targets(test, usable_targets)
- usable_targets = sorted(test.filter_targets_by_version(list(usable_targets), version))
+ usable_targets = sorted(test.filter_targets_by_version(args, list(usable_targets), version))
usable_targets = settings.filter_skipped_targets(usable_targets)
sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets))
@@ -362,12 +364,12 @@ class SanityIgnoreParser:
for python_version in test.supported_python_versions:
test_name = '%s-%s' % (test.name, python_version)
- paths_by_test[test_name] = set(target.path for target in test.filter_targets_by_version(test_targets, python_version))
+ paths_by_test[test_name] = set(target.path for target in test.filter_targets_by_version(args, test_targets, python_version))
tests_by_name[test_name] = test
else:
unversioned_test_names.update(dict(('%s-%s' % (test.name, python_version), test.name) for python_version in SUPPORTED_PYTHON_VERSIONS))
- paths_by_test[test.name] = set(target.path for target in test.filter_targets_by_version(test_targets, ''))
+ paths_by_test[test.name] = set(target.path for target in test.filter_targets_by_version(args, test_targets, ''))
tests_by_name[test.name] = test
for line_no, line in enumerate(lines, start=1):
@@ -761,7 +763,7 @@ class SanityTest(metaclass=abc.ABCMeta):
raise NotImplementedError('Sanity test "%s" must implement "filter_targets" or set "no_targets" to True.' % self.name)
- def filter_targets_by_version(self, targets, python_version): # type: (t.List[TestTarget], str) -> t.List[TestTarget]
+ def filter_targets_by_version(self, args, targets, python_version): # type: (SanityConfig, t.List[TestTarget], str) -> t.List[TestTarget]
"""Return the given list of test targets, filtered to include only those relevant for the test, taking into account the Python version."""
del python_version # python_version is not used here, but derived classes may make use of it
@@ -769,7 +771,7 @@ class SanityTest(metaclass=abc.ABCMeta):
if self.py2_compat:
# This sanity test is a Python 2.x compatibility test.
- content_config = get_content_config()
+ content_config = get_content_config(args)
if content_config.py2_support:
# This collection supports Python 2.x.
@@ -1056,15 +1058,15 @@ class SanityMultipleVersion(SanityTest, metaclass=abc.ABCMeta):
"""A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
return SUPPORTED_PYTHON_VERSIONS
- def filter_targets_by_version(self, targets, python_version): # type: (t.List[TestTarget], str) -> t.List[TestTarget]
+ def filter_targets_by_version(self, args, targets, python_version): # type: (SanityConfig, t.List[TestTarget], str) -> t.List[TestTarget]
"""Return the given list of test targets, filtered to include only those relevant for the test, taking into account the Python version."""
if not python_version:
raise Exception('python_version is required to filter multi-version tests')
- targets = super().filter_targets_by_version(targets, python_version)
+ targets = super().filter_targets_by_version(args, targets, python_version)
if python_version in REMOTE_ONLY_PYTHON_VERSIONS:
- content_config = get_content_config()
+ content_config = get_content_config(args)
if python_version not in content_config.modules.python_versions:
# when a remote-only python version is not supported there are no paths to test
diff --git a/test/lib/ansible_test/_internal/commands/units/__init__.py b/test/lib/ansible_test/_internal/commands/units/__init__.py
index f20e96fd2f..42330a3bd4 100644
--- a/test/lib/ansible_test/_internal/commands/units/__init__.py
+++ b/test/lib/ansible_test/_internal/commands/units/__init__.py
@@ -103,7 +103,7 @@ def command_units(args): # type: (UnitsConfig) -> None
paths = [target.path for target in include]
- content_config = get_content_config()
+ content_config = get_content_config(args)
supported_remote_python_versions = content_config.modules.python_versions
if content_config.modules.controller_only:
diff --git a/test/lib/ansible_test/_internal/config.py b/test/lib/ansible_test/_internal/config.py
index 27395678c7..64e504cbd9 100644
--- a/test/lib/ansible_test/_internal/config.py
+++ b/test/lib/ansible_test/_internal/config.py
@@ -1,6 +1,7 @@
"""Configuration classes."""
from __future__ import annotations
+import dataclasses
import enum
import os
import sys
@@ -48,6 +49,22 @@ class TerminateMode(enum.Enum):
return self.name.lower()
+@dataclasses.dataclass(frozen=True)
+class ModulesConfig:
+ """Configuration for modules."""
+ python_requires: str
+ python_versions: tuple[str, ...]
+ controller_only: bool
+
+
+@dataclasses.dataclass(frozen=True)
+class ContentConfig:
+ """Configuration for all content."""
+ modules: ModulesConfig
+ python_versions: tuple[str, ...]
+ py2_support: bool
+
+
class EnvironmentConfig(CommonConfig):
"""Configuration common to all commands which execute in an environment."""
def __init__(self, args, command): # type: (t.Any, str) -> None
@@ -59,6 +76,10 @@ class EnvironmentConfig(CommonConfig):
self.pypi_proxy = args.pypi_proxy # type: bool
self.pypi_endpoint = args.pypi_endpoint # type: t.Optional[str]
+ # Populated by content_config.get_content_config on the origin.
+ # Serialized and passed to delegated instances to avoid parsing a second time.
+ self.content_config = None # type: t.Optional[ContentConfig]
+
# Set by check_controller_python once HostState has been created by prepare_profiles.
# This is here for convenience, to avoid needing to pass HostState to some functions which already have access to EnvironmentConfig.
self.controller_python = None # type: t.Optional[PythonConfig]
@@ -97,9 +118,11 @@ class EnvironmentConfig(CommonConfig):
if config.host_path:
settings_path = os.path.join(config.host_path, 'settings.dat')
state_path = os.path.join(config.host_path, 'state.dat')
+ config_path = os.path.join(config.host_path, 'config.dat')
files.append((os.path.abspath(settings_path), settings_path))
files.append((os.path.abspath(state_path), state_path))
+ files.append((os.path.abspath(config_path), config_path))
data_context().register_payload_callback(host_callback)
diff --git a/test/lib/ansible_test/_internal/content_config.py b/test/lib/ansible_test/_internal/content_config.py
index 10574cc0b6..39a8d4125c 100644
--- a/test/lib/ansible_test/_internal/content_config.py
+++ b/test/lib/ansible_test/_internal/content_config.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
+import pickle
import typing as t
from .constants import (
@@ -21,6 +22,7 @@ from .compat.yaml import (
)
from .io import (
+ open_binary_file,
read_text_file,
)
@@ -28,54 +30,59 @@ from .util import (
ApplicationError,
display,
str_to_version,
- cache,
)
from .data import (
data_context,
)
+from .config import (
+ EnvironmentConfig,
+ ContentConfig,
+ ModulesConfig,
+)
MISSING = object()
-class BaseConfig:
- """Base class for content configuration."""
- def __init__(self, data): # type: (t.Any) -> None
- if not isinstance(data, dict):
- raise Exception('config must be type `dict` not `%s`' % type(data))
-
+def parse_modules_config(data: t.Any) -> ModulesConfig:
+ """Parse the given dictionary as module config and return it."""
+ if not isinstance(data, dict):
+ raise Exception('config must be type `dict` not `%s`' % type(data))
-class ModulesConfig(BaseConfig):
- """Configuration for modules."""
- def __init__(self, data): # type: (t.Any) -> None
- super().__init__(data)
+ python_requires = data.get('python_requires', MISSING)
- python_requires = data.get('python_requires', MISSING)
+ if python_requires == MISSING:
+ raise KeyError('python_requires is required')
- if python_requires == MISSING:
- raise KeyError('python_requires is required')
+ return ModulesConfig(
+ python_requires=python_requires,
+ python_versions=parse_python_requires(python_requires),
+ controller_only=python_requires == 'controller',
+ )
- self.python_requires = python_requires
- self.python_versions = parse_python_requires(python_requires)
- self.controller_only = python_requires == 'controller'
+def parse_content_config(data: t.Any) -> ContentConfig:
+ """Parse the given dictionary as content config and return it."""
+ if not isinstance(data, dict):
+ raise Exception('config must be type `dict` not `%s`' % type(data))
-class ContentConfig(BaseConfig):
- """Configuration for all content."""
- def __init__(self, data): # type: (t.Any) -> None
- super().__init__(data)
+ # Configuration specific to modules/module_utils.
+ modules = parse_modules_config(data.get('modules', {}))
- # Configuration specific to modules/module_utils.
- self.modules = ModulesConfig(data.get('modules', {}))
+ # Python versions supported by the controller, combined with Python versions supported by modules/module_utils.
+ # Mainly used for display purposes and to limit the Python versions used for sanity tests.
+ python_versions = tuple(version for version in SUPPORTED_PYTHON_VERSIONS
+ if version in CONTROLLER_PYTHON_VERSIONS or version in modules.python_versions)
- # Python versions supported by the controller, combined with Python versions supported by modules/module_utils.
- # Mainly used for display purposes and to limit the Python versions used for sanity tests.
- self.python_versions = [version for version in SUPPORTED_PYTHON_VERSIONS
- if version in CONTROLLER_PYTHON_VERSIONS or version in self.modules.python_versions]
+ # True if Python 2.x is supported.
+ py2_support = any(version for version in python_versions if str_to_version(version)[0] == 2)
- # True if Python 2.x is supported.
- self.py2_support = any(version for version in self.python_versions if str_to_version(version)[0] == 2)
+ return ContentConfig(
+ modules=modules,
+ python_versions=python_versions,
+ py2_support=py2_support,
+ )
def load_config(path): # type: (str) -> t.Optional[ContentConfig]
@@ -95,7 +102,7 @@ def load_config(path): # type: (str) -> t.Optional[ContentConfig]
return None
try:
- config = ContentConfig(yaml_value)
+ config = parse_content_config(yaml_value)
except Exception as ex: # pylint: disable=broad-except
display.warning('Ignoring config "%s" due a config parsing error: %s' % (path, ex))
return None
@@ -105,13 +112,18 @@ def load_config(path): # type: (str) -> t.Optional[ContentConfig]
return config
-@cache
-def get_content_config(): # type: () -> ContentConfig
+def get_content_config(args): # type: (EnvironmentConfig) -> ContentConfig
"""
Parse and return the content configuration (if any) for the current collection.
For ansible-core, a default configuration is used.
Results are cached.
"""
+ if args.host_path:
+ args.content_config = deserialize_content_config(os.path.join(args.host_path, 'config.dat'))
+
+ if args.content_config:
+ return args.content_config
+
collection_config_path = 'tests/config.yml'
config = None
@@ -120,7 +132,7 @@ def get_content_config(): # type: () -> ContentConfig
config = load_config(collection_config_path)
if not config:
- config = ContentConfig(dict(
+ config = parse_content_config(dict(
modules=dict(
python_requires='default',
),
@@ -132,20 +144,36 @@ def get_content_config(): # type: () -> ContentConfig
'This collection provides the Python requirement: %s' % (
', '.join(SUPPORTED_PYTHON_VERSIONS), config.modules.python_requires))
+ args.content_config = config
+
return config
-def parse_python_requires(value): # type: (t.Any) -> t.List[str]
+def parse_python_requires(value): # type: (t.Any) -> tuple[str, ...]
"""Parse the given 'python_requires' version specifier and return the matching Python versions."""
if not isinstance(value, str):
raise ValueError('python_requires must must be of type `str` not type `%s`' % type(value))
+ versions: tuple[str, ...]
+
if value == 'default':
- versions = list(SUPPORTED_PYTHON_VERSIONS)
+ versions = SUPPORTED_PYTHON_VERSIONS
elif value == 'controller':
- versions = list(CONTROLLER_PYTHON_VERSIONS)
+ versions = CONTROLLER_PYTHON_VERSIONS
else:
specifier_set = SpecifierSet(value)
- versions = [version for version in SUPPORTED_PYTHON_VERSIONS if specifier_set.contains(Version(version))]
+ versions = tuple(version for version in SUPPORTED_PYTHON_VERSIONS if specifier_set.contains(Version(version)))
return versions
+
+
+def serialize_content_config(args: EnvironmentConfig, path: str) -> None:
+ """Serialize the content config to the given path. If the config has not been loaded, an empty config will be serialized."""
+ with open_binary_file(path, 'wb') as config_file:
+ pickle.dump(args.content_config, config_file)
+
+
+def deserialize_content_config(path: str) -> ContentConfig:
+ """Deserialize content config from the path."""
+ with open_binary_file(path) as config_file:
+ return pickle.load(config_file)
diff --git a/test/lib/ansible_test/_internal/delegation.py b/test/lib/ansible_test/_internal/delegation.py
index 975b6fc7e5..247ae3535e 100644
--- a/test/lib/ansible_test/_internal/delegation.py
+++ b/test/lib/ansible_test/_internal/delegation.py
@@ -68,6 +68,10 @@ from .provisioning import (
HostState,
)
+from .content_config import (
+ serialize_content_config,
+)
+
@contextlib.contextmanager
def delegation_context(args, host_state): # type: (EnvironmentConfig, HostState) -> t.Iterator[None]
@@ -81,6 +85,7 @@ def delegation_context(args, host_state): # type: (EnvironmentConfig, HostState
with tempfile.TemporaryDirectory(prefix='host-', dir=ResultType.TMP.path) as host_dir:
args.host_settings.serialize(os.path.join(host_dir, 'settings.dat'))
host_state.serialize(os.path.join(host_dir, 'state.dat'))
+ serialize_content_config(args, os.path.join(host_dir, 'config.dat'))
args.host_path = os.path.join(ResultType.TMP.relative_path, os.path.basename(host_dir))