mirror of https://github.com/ansible/ansible.git
Add collection config support to ansible-test.
parent
bacede7a2b
commit
c4e76a7f80
@ -0,0 +1,9 @@
|
||||
minor_changes:
|
||||
- ansible-test - Add support for an ansible-test configuration file in collections under ``tests/config.yml``.
|
||||
- ansible-test - Collections can limit the Python versions used for testing their remote-only code (modules/module_utils and related tests).
|
||||
- ansible-test - Collections can declare their remote-only code (modules/module_utils and related tests) as controller-only.
|
||||
- ansible-test - Sanity test warnings relating to Python version support have been improved.
|
||||
major_changes:
|
||||
- ansible-test - The ``import`` and ``compile`` sanity tests limit remote-only Python version checks to remote-only code.
|
||||
- ansible-test - The ``future-import-boilerplate`` and ``metaclass-boilerplate`` sanity tests are limited to remote-only code.
|
||||
Additionally, they are skipped for collections which declare no support for Python 2.x.
|
@ -0,0 +1,12 @@
|
||||
"""Packaging compatibility."""
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
try:
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version
|
||||
PACKAGING_IMPORT_ERROR = None
|
||||
except ImportError as ex:
|
||||
SpecifierSet = None
|
||||
Version = None
|
||||
PACKAGING_IMPORT_ERROR = ex
|
@ -0,0 +1,21 @@
|
||||
"""PyYAML compatibility."""
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from functools import (
|
||||
partial,
|
||||
)
|
||||
|
||||
try:
|
||||
import yaml as _yaml
|
||||
YAML_IMPORT_ERROR = None
|
||||
except ImportError as ex:
|
||||
yaml_load = None # pylint: disable=invalid-name
|
||||
YAML_IMPORT_ERROR = ex
|
||||
else:
|
||||
try:
|
||||
_SafeLoader = _yaml.CSafeLoader
|
||||
except AttributeError:
|
||||
_SafeLoader = _yaml.SafeLoader
|
||||
|
||||
yaml_load = partial(_yaml.load, Loader=_SafeLoader)
|
@ -0,0 +1,155 @@
|
||||
"""Content configuration."""
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
|
||||
from . import types as t
|
||||
|
||||
from .compat.packaging import (
|
||||
PACKAGING_IMPORT_ERROR,
|
||||
SpecifierSet,
|
||||
Version,
|
||||
)
|
||||
|
||||
from .compat.yaml import (
|
||||
YAML_IMPORT_ERROR,
|
||||
yaml_load,
|
||||
)
|
||||
|
||||
from .io import (
|
||||
read_text_file,
|
||||
)
|
||||
|
||||
from .util import (
|
||||
ApplicationError,
|
||||
CONTROLLER_PYTHON_VERSIONS,
|
||||
SUPPORTED_PYTHON_VERSIONS,
|
||||
display,
|
||||
str_to_version,
|
||||
)
|
||||
|
||||
from .data import (
|
||||
data_context,
|
||||
)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
class ModulesConfig(BaseConfig):
|
||||
"""Configuration for modules."""
|
||||
def __init__(self, data): # type: (t.Any) -> None
|
||||
super(ModulesConfig, self).__init__(data)
|
||||
|
||||
python_requires = data.get('python_requires', MISSING)
|
||||
|
||||
if python_requires == MISSING:
|
||||
raise KeyError('python_requires is required')
|
||||
|
||||
self.python_requires = python_requires
|
||||
self.python_versions = parse_python_requires(python_requires)
|
||||
self.controller_only = python_requires == 'controller'
|
||||
|
||||
|
||||
class ContentConfig(BaseConfig):
|
||||
"""Configuration for all content."""
|
||||
def __init__(self, data): # type: (t.Any) -> None
|
||||
super(ContentConfig, self).__init__(data)
|
||||
|
||||
# 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.
|
||||
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.
|
||||
self.py2_support = any(version for version in self.python_versions if str_to_version(version)[0] == 2)
|
||||
|
||||
|
||||
def load_config(path): # type: (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 = ContentConfig(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(): # type: () -> ContentConfig
|
||||
"""
|
||||
Parse and return the content configuration (if any) for the current collection.
|
||||
For ansible-core, a default configuration is used.
|
||||
Results are cached.
|
||||
"""
|
||||
try:
|
||||
return get_content_config.config
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
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 = ContentConfig(dict(
|
||||
modules=dict(
|
||||
python_requires='default',
|
||||
),
|
||||
))
|
||||
|
||||
get_content_config.config = config
|
||||
|
||||
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))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def parse_python_requires(value): # type: (t.Any) -> t.List[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))
|
||||
|
||||
if value == 'default':
|
||||
versions = list(SUPPORTED_PYTHON_VERSIONS)
|
||||
elif value == 'controller':
|
||||
versions = list(CONTROLLER_PYTHON_VERSIONS)
|
||||
else:
|
||||
specifier_set = SpecifierSet(value)
|
||||
versions = [version for version in SUPPORTED_PYTHON_VERSIONS if specifier_set.contains(Version(version))]
|
||||
|
||||
return versions
|
@ -0,0 +1,41 @@
|
||||
# Sample ansible-test configuration file for collections.
|
||||
# Support for this feature was first added in ansible-core 2.12.
|
||||
# Use of this file is optional.
|
||||
# If used, this file must be placed in "tests/config.yml" relative to the base of the collection.
|
||||
|
||||
modules:
|
||||
# Configuration for modules/module_utils.
|
||||
# These settings do not apply to other content in the collection.
|
||||
|
||||
python_requires: default
|
||||
# Python versions supported by modules/module_utils.
|
||||
# This setting is required.
|
||||
#
|
||||
# Possible values:
|
||||
#
|
||||
# - 'default' - All Python versions supported by Ansible.
|
||||
# This is the default value if no configuration is provided.
|
||||
# - 'controller' - All Python versions supported by the Ansible controller.
|
||||
# This indicates the modules/module_utils can only run on the controller.
|
||||
# Intended for use only with modules/module_utils that depend on ansible-connection, which only runs on the controller.
|
||||
# Unit tests for modules/module_utils will be permitted to import any Ansible code, instead of only module_utils.
|
||||
# - SpecifierSet - A PEP 440 specifier set indicating the supported Python versions.
|
||||
# This is only needed when modules/module_utils do not support all Python versions supported by Ansible.
|
||||
# It is not necessary to exclude versions which Ansible does not support, as this will be done automatically.
|
||||
#
|
||||
# What does this affect?
|
||||
#
|
||||
# - Unit tests will be skipped on any unsupported Python version.
|
||||
# - Sanity tests that are Python version specific will be skipped on any unsupported Python version that is not supported by the controller.
|
||||
#
|
||||
# Sanity tests that are Python version specific will always be executed for Python versions supported by the controller, regardless of this setting.
|
||||
# Reasons for this restriction include, but are not limited to:
|
||||
#
|
||||
# - AnsiballZ must be able to AST parse modules/module_utils on the controller, even though they may execute on a managed node.
|
||||
# - ansible-doc must be able to AST parse modules/module_utils on the controller to display documentation.
|
||||
# - ansible-test must be able to AST parse modules/module_utils to perform static analysis on them.
|
||||
# - ansible-test must be able to execute portions of modules/module_utils to validate their argument specs.
|
||||
#
|
||||
# These settings only apply to modules/module_utils.
|
||||
# It is not possible to declare supported Python versions for controller-only code.
|
||||
# All Python versions supported by the controller must be supported by controller-only code.
|
Loading…
Reference in New Issue