diff options
Diffstat (limited to 'test/lib/ansible_test/_internal/content_config.py')
-rw-r--r-- | test/lib/ansible_test/_internal/content_config.py | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/test/lib/ansible_test/_internal/content_config.py b/test/lib/ansible_test/_internal/content_config.py new file mode 100644 index 0000000..7ac1876 --- /dev/null +++ b/test/lib/ansible_test/_internal/content_config.py @@ -0,0 +1,179 @@ +"""Content configuration.""" +from __future__ import annotations + +import os +import pickle +import typing as t + +from .constants import ( + CONTROLLER_PYTHON_VERSIONS, + SUPPORTED_PYTHON_VERSIONS, +) + +from .compat.packaging import ( + PACKAGING_IMPORT_ERROR, + SpecifierSet, + Version, +) + +from .compat.yaml import ( + YAML_IMPORT_ERROR, + yaml_load, +) + +from .io import ( + open_binary_file, + read_text_file, +) + +from .util import ( + ApplicationError, + display, + str_to_version, +) + +from .data import ( + data_context, +) + +from .config import ( + EnvironmentConfig, + ContentConfig, + ModulesConfig, +) + +MISSING = object() + + +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)) + + python_requires = data.get('python_requires', MISSING) + + 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', + ) + + +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)) + + # Configuration specific to modules/module_utils. + modules = parse_modules_config(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) + + # True if Python 2.x is supported. + py2_support = any(version for version in 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: str) -> t.Optional[ContentConfig]: + """Load and parse the specified config file and return the result or None if loading/parsing failed.""" + if YAML_IMPORT_ERROR: + raise ApplicationError('The "PyYAML" module is required to parse config: %s' % YAML_IMPORT_ERROR) + + if PACKAGING_IMPORT_ERROR: + raise ApplicationError('The "packaging" module is required to parse config: %s' % PACKAGING_IMPORT_ERROR) + + value = read_text_file(path) + + try: + yaml_value = yaml_load(value) + except Exception as ex: # pylint: disable=broad-except + display.warning('Ignoring config "%s" due to a YAML parsing error: %s' % (path, ex)) + return None + + try: + 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 + + display.info('Loaded configuration: %s' % path, verbosity=1) + + return config + + +def get_content_config(args: 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 + + if data_context().content.collection and os.path.exists(collection_config_path): + config = load_config(collection_config_path) + + if not config: + config = parse_content_config(dict( + modules=dict( + python_requires='default', + ), + )) + + if not config.modules.python_versions: + raise ApplicationError('This collection does not declare support for modules/module_utils on any known Python version.\n' + 'Ansible supports modules/module_utils on Python versions: %s\n' + '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: 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 = SUPPORTED_PYTHON_VERSIONS + elif value == 'controller': + versions = CONTROLLER_PYTHON_VERSIONS + else: + specifier_set = SpecifierSet(value) + 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) |