Prepare ansible-test for supporting collections. (#58886)

This is a small but incomplete set of the initial changes for supporting testing of collections with ansible-test.
pull/58895/head
Matt Clay 5 years ago committed by GitHub
parent 73a7a0877d
commit 1e1463401d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,6 +15,7 @@ from lib.util import (
find_python, find_python,
run_command, run_command,
ApplicationError, ApplicationError,
INSTALL_ROOT,
) )
from lib.config import ( from lib.config import (
@ -36,7 +37,7 @@ def ansible_environment(args, color=True, ansible_config=None):
env = common_environment() env = common_environment()
path = env['PATH'] path = env['PATH']
ansible_path = os.path.join(os.getcwd(), 'bin') ansible_path = os.path.join(INSTALL_ROOT, 'bin')
if not path.startswith(ansible_path + os.path.pathsep): if not path.startswith(ansible_path + os.path.pathsep):
path = ansible_path + os.path.pathsep + path path = ansible_path + os.path.pathsep + path
@ -44,9 +45,9 @@ def ansible_environment(args, color=True, ansible_config=None):
if ansible_config: if ansible_config:
pass pass
elif isinstance(args, IntegrationConfig): elif isinstance(args, IntegrationConfig):
ansible_config = 'test/integration/%s.cfg' % args.command ansible_config = os.path.join(INSTALL_ROOT, 'test/integration/%s.cfg' % args.command)
else: else:
ansible_config = 'test/%s/ansible.cfg' % args.command ansible_config = os.path.join(INSTALL_ROOT, 'test/%s/ansible.cfg' % args.command)
if not args.explain and not os.path.exists(ansible_config): if not args.explain and not os.path.exists(ansible_config):
raise ApplicationError('Configuration not found: %s' % ansible_config) raise ApplicationError('Configuration not found: %s' % ansible_config)
@ -59,7 +60,7 @@ def ansible_environment(args, color=True, ansible_config=None):
ANSIBLE_RETRY_FILES_ENABLED='false', ANSIBLE_RETRY_FILES_ENABLED='false',
ANSIBLE_CONFIG=os.path.abspath(ansible_config), ANSIBLE_CONFIG=os.path.abspath(ansible_config),
ANSIBLE_LIBRARY='/dev/null', ANSIBLE_LIBRARY='/dev/null',
PYTHONPATH=os.path.abspath('lib'), PYTHONPATH=os.path.join(INSTALL_ROOT, 'lib'),
PAGER='/bin/cat', PAGER='/bin/cat',
PATH=path, PATH=path,
) )
@ -84,7 +85,7 @@ def check_pyyaml(args, version):
return return
python = find_python(version) python = find_python(version)
stdout, _dummy = run_command(args, [python, 'test/runner/yamlcheck.py'], capture=True) stdout, _dummy = run_command(args, [python, os.path.join(INSTALL_ROOT, 'test/runner/yamlcheck.py')], capture=True)
if args.explain: if args.explain:
return return

@ -5,6 +5,8 @@ from __future__ import absolute_import, print_function
import os import os
import sys import sys
import lib.types as t
from lib.util import ( from lib.util import (
CommonConfig, CommonConfig,
is_shippable, is_shippable,
@ -112,6 +114,7 @@ class TestConfig(EnvironmentConfig):
self.coverage = args.coverage # type: bool self.coverage = args.coverage # type: bool
self.coverage_label = args.coverage_label # type: str self.coverage_label = args.coverage_label # type: str
self.coverage_check = args.coverage_check # type: bool self.coverage_check = args.coverage_check # type: bool
self.coverage_config_base_path = None # type: t.Optional[str]
self.include = args.include or [] # type: list [str] self.include = args.include or [] # type: list [str]
self.exclude = args.exclude or [] # type: list [str] self.exclude = args.exclude or [] # type: list [str]
self.require = args.require or [] # type: list [str] self.require = args.require or [] # type: list [str]

@ -61,6 +61,7 @@ from lib.util import (
named_temporary_file, named_temporary_file,
COVERAGE_OUTPUT_PATH, COVERAGE_OUTPUT_PATH,
cmd_quote, cmd_quote,
INSTALL_ROOT,
) )
from lib.docker_util import ( from lib.docker_util import (
@ -273,10 +274,10 @@ def generate_egg_info(args):
""" """
:type args: EnvironmentConfig :type args: EnvironmentConfig
""" """
if os.path.isdir('lib/ansible.egg-info'): if os.path.isdir(os.path.join(INSTALL_ROOT, 'lib/ansible.egg-info')):
return return
run_command(args, [args.python_executable, 'setup.py', 'egg_info'], capture=args.verbosity < 3) run_command(args, [args.python_executable, 'setup.py', 'egg_info'], cwd=INSTALL_ROOT, capture=args.verbosity < 3)
def generate_pip_install(pip, command, packages=None): def generate_pip_install(pip, command, packages=None):
@ -1796,9 +1797,10 @@ class EnvironmentDescription(object):
versions += SUPPORTED_PYTHON_VERSIONS versions += SUPPORTED_PYTHON_VERSIONS
versions += list(set(v.split('.')[0] for v in SUPPORTED_PYTHON_VERSIONS)) versions += list(set(v.split('.')[0] for v in SUPPORTED_PYTHON_VERSIONS))
version_check = os.path.join(INSTALL_ROOT, 'test/runner/versions.py')
python_paths = dict((v, find_executable('python%s' % v, required=False)) for v in sorted(versions)) python_paths = dict((v, find_executable('python%s' % v, required=False)) for v in sorted(versions))
pip_paths = dict((v, find_executable('pip%s' % v, required=False)) for v in sorted(versions)) pip_paths = dict((v, find_executable('pip%s' % v, required=False)) for v in sorted(versions))
program_versions = dict((v, self.get_version([python_paths[v], 'test/runner/versions.py'], warnings)) for v in sorted(python_paths) if python_paths[v]) program_versions = dict((v, self.get_version([python_paths[v], version_check], warnings)) for v in sorted(python_paths) if python_paths[v])
pip_interpreters = dict((v, self.get_shebang(pip_paths[v])) for v in sorted(pip_paths) if pip_paths[v]) pip_interpreters = dict((v, self.get_shebang(pip_paths[v])) for v in sorted(pip_paths) if pip_paths[v])
known_hosts_hash = self.get_hash(os.path.expanduser('~/.ssh/known_hosts')) known_hosts_hash = self.get_hash(os.path.expanduser('~/.ssh/known_hosts'))

@ -2,13 +2,7 @@
from __future__ import absolute_import, print_function from __future__ import absolute_import, print_function
try: import lib.types as t
# noinspection PyUnresolvedReferences
from typing import (
Optional,
)
except ImportError:
pass
from lib.util import ( from lib.util import (
SubprocessError, SubprocessError,
@ -18,7 +12,7 @@ from lib.util import (
class Git(object): class Git(object):
"""Wrapper around git command-line tools.""" """Wrapper around git command-line tools."""
def __init__(self, root=None): # type: (Optional[str]) -> None def __init__(self, root=None): # type: (t.Optional[str]) -> None
self.git = 'git' self.git = 'git'
self.root = root self.root = root

@ -6,7 +6,6 @@ import contextlib
import json import json
import os import os
import shutil import shutil
import stat
import tempfile import tempfile
from lib.target import ( from lib.target import (
@ -30,6 +29,7 @@ from lib.util import (
MODE_DIRECTORY, MODE_DIRECTORY,
MODE_DIRECTORY_WRITE, MODE_DIRECTORY_WRITE,
MODE_FILE, MODE_FILE,
INSTALL_ROOT,
) )
from lib.cache import ( from lib.cache import (
@ -172,9 +172,9 @@ def integration_test_environment(args, target, inventory_path):
ansible_config = os.path.join(integration_dir, '%s.cfg' % args.command) ansible_config = os.path.join(integration_dir, '%s.cfg' % args.command)
file_copies = [ file_copies = [
('test/integration/%s.cfg' % args.command, ansible_config), (os.path.join(INSTALL_ROOT, 'test/integration/%s.cfg' % args.command), ansible_config),
('test/integration/integration_config.yml', os.path.join(integration_dir, vars_file)), (os.path.join(INSTALL_ROOT, 'test/integration/integration_config.yml'), os.path.join(integration_dir, vars_file)),
(inventory_path, os.path.join(integration_dir, inventory_name)), (os.path.join(INSTALL_ROOT, inventory_path), os.path.join(integration_dir, inventory_name)),
] ]
file_copies += [(path, os.path.join(temp_dir, path)) for path in files_needed] file_copies += [(path, os.path.join(temp_dir, path)) for path in files_needed]

@ -39,7 +39,7 @@ class AnsibleDocTest(SanityMultipleVersion):
:rtype: TestResult :rtype: TestResult
""" """
skip_file = 'test/sanity/ansible-doc/skip.txt' skip_file = 'test/sanity/ansible-doc/skip.txt'
skip_modules = set(read_lines_without_comments(skip_file, remove_blank_lines=True)) skip_modules = set(read_lines_without_comments(skip_file, remove_blank_lines=True, optional=True))
# This should use documentable plugins from constants instead # This should use documentable plugins from constants instead
plugin_type_blacklist = set([ plugin_type_blacklist = set([

@ -46,7 +46,8 @@ class ImportTest(SanityMultipleVersion):
:rtype: TestResult :rtype: TestResult
""" """
skip_file = 'test/sanity/import/skip.txt' skip_file = 'test/sanity/import/skip.txt'
skip_paths = read_lines_without_comments(skip_file, remove_blank_lines=True) skip_paths = read_lines_without_comments(skip_file, remove_blank_lines=True, optional=True)
skip_paths_set = set(skip_paths) skip_paths_set = set(skip_paths)
paths = sorted( paths = sorted(

@ -17,6 +17,7 @@ from lib.util import (
run_command, run_command,
read_lines_without_comments, read_lines_without_comments,
parse_to_list_of_dict, parse_to_list_of_dict,
INSTALL_ROOT,
) )
from lib.config import ( from lib.config import (
@ -39,13 +40,13 @@ class Pep8Test(SanitySingleVersion):
:type targets: SanityTargets :type targets: SanityTargets
:rtype: TestResult :rtype: TestResult
""" """
skip_paths = read_lines_without_comments(PEP8_SKIP_PATH) skip_paths = read_lines_without_comments(PEP8_SKIP_PATH, optional=True)
legacy_paths = read_lines_without_comments(PEP8_LEGACY_PATH) legacy_paths = read_lines_without_comments(PEP8_LEGACY_PATH, optional=True)
legacy_ignore_file = 'test/sanity/pep8/legacy-ignore.txt' legacy_ignore_file = os.path.join(INSTALL_ROOT, 'test/sanity/pep8/legacy-ignore.txt')
legacy_ignore = set(read_lines_without_comments(legacy_ignore_file, remove_blank_lines=True)) legacy_ignore = set(read_lines_without_comments(legacy_ignore_file, remove_blank_lines=True))
current_ignore_file = 'test/sanity/pep8/current-ignore.txt' current_ignore_file = os.path.join(INSTALL_ROOT, 'test/sanity/pep8/current-ignore.txt')
current_ignore = sorted(read_lines_without_comments(current_ignore_file, remove_blank_lines=True)) current_ignore = sorted(read_lines_without_comments(current_ignore_file, remove_blank_lines=True))
skip_paths_set = set(skip_paths) skip_paths_set = set(skip_paths)

@ -42,11 +42,11 @@ class PslintTest(SanitySingleVersion):
:type targets: SanityTargets :type targets: SanityTargets
:rtype: TestResult :rtype: TestResult
""" """
skip_paths = read_lines_without_comments(PSLINT_SKIP_PATH) skip_paths = read_lines_without_comments(PSLINT_SKIP_PATH, optional=True)
invalid_ignores = [] invalid_ignores = []
ignore_entries = read_lines_without_comments(PSLINT_IGNORE_PATH) ignore_entries = read_lines_without_comments(PSLINT_IGNORE_PATH, optional=True)
ignore = collections.defaultdict(dict) ignore = collections.defaultdict(dict)
line = 0 line = 0

@ -17,6 +17,7 @@ from lib.util import (
parse_to_list_of_dict, parse_to_list_of_dict,
display, display,
read_lines_without_comments, read_lines_without_comments,
INSTALL_ROOT,
) )
from lib.config import ( from lib.config import (
@ -40,7 +41,7 @@ class RstcheckTest(SanitySingleVersion):
display.warning('Skipping rstcheck on unsupported Python version %s.' % args.python_version) display.warning('Skipping rstcheck on unsupported Python version %s.' % args.python_version)
return SanitySkipped(self.name) return SanitySkipped(self.name)
ignore_file = 'test/sanity/rstcheck/ignore-substitutions.txt' ignore_file = os.path.join(INSTALL_ROOT, 'test/sanity/rstcheck/ignore-substitutions.txt')
ignore_substitutions = sorted(set(read_lines_without_comments(ignore_file, remove_blank_lines=True))) ignore_substitutions = sorted(set(read_lines_without_comments(ignore_file, remove_blank_lines=True)))
paths = sorted(i.path for i in targets.include if os.path.splitext(i.path)[1] in ('.rst',)) paths = sorted(i.path for i in targets.include if os.path.splitext(i.path)[1] in ('.rst',))

@ -36,10 +36,10 @@ class ShellcheckTest(SanitySingleVersion):
:rtype: TestResult :rtype: TestResult
""" """
skip_file = 'test/sanity/shellcheck/skip.txt' skip_file = 'test/sanity/shellcheck/skip.txt'
skip_paths = set(read_lines_without_comments(skip_file, remove_blank_lines=True)) skip_paths = set(read_lines_without_comments(skip_file, remove_blank_lines=True, optional=True))
exclude_file = 'test/sanity/shellcheck/exclude.txt' exclude_file = 'test/sanity/shellcheck/exclude.txt'
exclude = set(read_lines_without_comments(exclude_file, remove_blank_lines=True)) exclude = set(read_lines_without_comments(exclude_file, remove_blank_lines=True, optional=True))
paths = sorted(i.path for i in targets.include if os.path.splitext(i.path)[1] == '.sh' and i.path not in skip_paths) paths = sorted(i.path for i in targets.include if os.path.splitext(i.path)[1] == '.sh' and i.path not in skip_paths)

@ -16,6 +16,7 @@ from lib.util import (
SubprocessError, SubprocessError,
run_command, run_command,
display, display,
INSTALL_ROOT,
) )
from lib.config import ( from lib.config import (
@ -71,7 +72,7 @@ class YamllintTest(SanitySingleVersion):
""" """
cmd = [ cmd = [
args.python_executable, args.python_executable,
'test/sanity/yamllint/yamllinter.py', os.path.join(INSTALL_ROOT, 'test/sanity/yamllint/yamllinter.py'),
] ]
data = '\n'.join(paths) data = '\n'.join(paths)

@ -0,0 +1,19 @@
"""Import wrapper for type hints when available."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
try:
from typing import (
Any,
Dict,
FrozenSet,
Iterable,
List,
Optional,
Set,
Tuple,
Type,
TypeVar,
)
except ImportError:
pass

@ -7,7 +7,6 @@ import contextlib
import errno import errno
import fcntl import fcntl
import inspect import inspect
import json
import os import os
import pkgutil import pkgutil
import random import random
@ -43,6 +42,14 @@ try:
except ImportError: except ImportError:
from pipes import quote as cmd_quote from pipes import quote as cmd_quote
import lib.types as t
try:
C = t.TypeVar('C')
except AttributeError:
C = None
DOCKER_COMPLETION = {} # type: dict[str, dict[str, str]] DOCKER_COMPLETION = {} # type: dict[str, dict[str, str]]
REMOTE_COMPLETION = {} # type: dict[str, dict[str, str]] REMOTE_COMPLETION = {} # type: dict[str, dict[str, str]]
PYTHON_PATHS = {} # type: dict[str, str] PYTHON_PATHS = {} # type: dict[str, str]
@ -55,6 +62,8 @@ except AttributeError:
COVERAGE_CONFIG_PATH = '.coveragerc' COVERAGE_CONFIG_PATH = '.coveragerc'
COVERAGE_OUTPUT_PATH = 'coverage' COVERAGE_OUTPUT_PATH = 'coverage'
INSTALL_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# Modes are set to allow all users the same level of access. # Modes are set to allow all users the same level of access.
# This permits files to be used in tests that change users. # This permits files to be used in tests that change users.
# The only exception is write access to directories for the user creating them. # The only exception is write access to directories for the user creating them.
@ -91,7 +100,7 @@ def get_parameterized_completion(cache, name):
:rtype: dict[str, dict[str, str]] :rtype: dict[str, dict[str, str]]
""" """
if not cache: if not cache:
images = read_lines_without_comments('test/runner/completion/%s.txt' % name, remove_blank_lines=True) images = read_lines_without_comments(os.path.join(INSTALL_ROOT, 'test/runner/completion/%s.txt' % name), remove_blank_lines=True)
cache.update(dict(kvp for kvp in [parse_parameterized_completion(i) for i in images] if kvp)) cache.update(dict(kvp for kvp in [parse_parameterized_completion(i) for i in images] if kvp))
@ -129,12 +138,15 @@ def remove_file(path):
os.remove(path) os.remove(path)
def read_lines_without_comments(path, remove_blank_lines=False): def read_lines_without_comments(path, remove_blank_lines=False, optional=False): # type: (str, bool, bool) -> t.List[str]
""" """
:type path: str Returns lines from the specified text file with comments removed.
:type remove_blank_lines: bool Comments are any content from a hash symbol to the end of a line.
:rtype: list[str] Any spaces immediately before a comment are also removed.
""" """
if optional and not os.path.exists(path):
return []
with open(path, 'r') as path_fd: with open(path, 'r') as path_fd:
lines = path_fd.read().splitlines() lines = path_fd.read().splitlines()
@ -236,7 +248,7 @@ def get_coverage_environment(args, target_name, version, temp_path, module_cover
else: else:
# unit tests, sanity tests and other special cases (localhost only) # unit tests, sanity tests and other special cases (localhost only)
# config and results are in the source tree # config and results are in the source tree
coverage_config_base_path = os.getcwd() coverage_config_base_path = args.coverage_config_base_path or INSTALL_ROOT
coverage_output_base_path = os.path.abspath(os.path.join('test/results')) coverage_output_base_path = os.path.abspath(os.path.join('test/results'))
config_file = os.path.join(coverage_config_base_path, COVERAGE_CONFIG_PATH) config_file = os.path.join(coverage_config_base_path, COVERAGE_CONFIG_PATH)
@ -365,7 +377,7 @@ def intercept_command(args, cmd, target_name, env, capture=False, data=None, cwd
cmd = list(cmd) cmd = list(cmd)
version = python_version or args.python_version version = python_version or args.python_version
interpreter = virtualenv or find_python(version) interpreter = virtualenv or find_python(version)
inject_path = os.path.abspath('test/runner/injector') inject_path = os.path.join(INSTALL_ROOT, 'test/runner/injector')
if not virtualenv: if not virtualenv:
# injection of python into the path is required when not activating a virtualenv # injection of python into the path is required when not activating a virtualenv
@ -937,11 +949,8 @@ def get_available_port():
return socket_fd.getsockname()[1] return socket_fd.getsockname()[1]
def get_subclasses(class_type): def get_subclasses(class_type): # type: (t.Type[C]) -> t.Set[t.Type[C]]
""" """Returns the set of types that are concrete subclasses of the given type."""
:type class_type: type
:rtype: set[str]
"""
subclasses = set() subclasses = set()
queue = [class_type] queue = [class_type]
@ -957,26 +966,59 @@ def get_subclasses(class_type):
return subclasses return subclasses
def import_plugins(directory): def is_subdir(candidate_path, path): # type: (str, str) -> bool
"""Returns true if candidate_path is path or a subdirectory of path."""
if not path.endswith(os.sep):
path += os.sep
if not candidate_path.endswith(os.sep):
candidate_path += os.sep
return candidate_path.startswith(path)
def import_plugins(directory, root=None): # type: (str, t.Optional[str]) -> None
""" """
:type directory: str Import plugins from the given directory relative to the given root.
If the root is not provided, the 'lib' directory for the test runner will be used.
""" """
path = os.path.join(os.path.dirname(__file__), directory) if root is None:
prefix = 'lib.%s.' % directory root = os.path.dirname(__file__)
path = os.path.join(root, directory)
prefix = 'lib.%s.' % directory.replace(os.sep, '.')
for (_, name, _) in pkgutil.iter_modules([path], prefix=prefix): for (_, name, _) in pkgutil.iter_modules([path], prefix=prefix):
__import__(name) module_path = os.path.join(root, name[4:].replace('.', os.sep) + '.py')
load_module(module_path, name)
def load_plugins(base_type, database): def load_plugins(base_type, database): # type: (t.Type[C], t.Dict[str, t.Type[C]]) -> None
""" """
:type base_type: type Load plugins of the specified type and track them in the specified database.
:type database: dict[str, type] Only plugins which have already been imported will be loaded.
""" """
plugins = dict((sc.__module__.split('.')[2], sc) for sc in get_subclasses(base_type)) # type: dict [str, type] plugins = dict((sc.__module__.split('.')[2], sc) for sc in get_subclasses(base_type)) # type: t.Dict[str, t.Type[C]]
for plugin in plugins: for plugin in plugins:
database[plugin] = plugins[plugin] database[plugin] = plugins[plugin]
def load_module(path, name): # type: (str, str) -> None
"""Load a Python module using the given name and path."""
if sys.version_info >= (3, 4):
import importlib.util
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
sys.modules[name] = module
else:
import imp
with open(path, 'r') as module_file:
imp.load_module(name, module_file, path, ('.py', 'r', imp.PY_SOURCE))
display = Display() # pylint: disable=locally-disabled, invalid-name display = Display() # pylint: disable=locally-disabled, invalid-name

@ -74,6 +74,8 @@ def main():
is_module = False is_module = False
is_integration = False is_integration = False
dirname = os.path.dirname(path)
if path.startswith('lib/ansible/modules/'): if path.startswith('lib/ansible/modules/'):
is_module = True is_module = True
elif path.startswith('lib/') or path.startswith('test/runner/lib/'): elif path.startswith('lib/') or path.startswith('test/runner/lib/'):
@ -87,14 +89,14 @@ def main():
elif path.startswith('test/integration/targets/'): elif path.startswith('test/integration/targets/'):
is_integration = True is_integration = True
dirname = os.path.dirname(path)
if dirname.endswith('/library') or dirname.endswith('/plugins/modules') or dirname in ( if dirname.endswith('/library') or dirname.endswith('/plugins/modules') or dirname in (
# non-standard module library directories # non-standard module library directories
'test/integration/targets/module_precedence/lib_no_extension', 'test/integration/targets/module_precedence/lib_no_extension',
'test/integration/targets/module_precedence/lib_with_extension', 'test/integration/targets/module_precedence/lib_with_extension',
): ):
is_module = True is_module = True
elif dirname == 'plugins/modules':
is_module = True
if is_module: if is_module:
if executable: if executable:

@ -31,6 +31,8 @@ good-names=i,
k, k,
ex, ex,
Run, Run,
C,
__metaclass__,
method-rgx=[a-z_][a-z0-9_]{2,40}$ method-rgx=[a-z_][a-z0-9_]{2,40}$
function-rgx=[a-z_][a-z0-9_]{2,40}$ function-rgx=[a-z_][a-z0-9_]{2,40}$

@ -38,9 +38,11 @@ class YamlChecker(object):
""" """
:type paths: str :type paths: str
""" """
yaml_conf = YamlLintConfig(file='test/sanity/yamllint/config/default.yml') config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config')
module_conf = YamlLintConfig(file='test/sanity/yamllint/config/modules.yml')
plugin_conf = YamlLintConfig(file='test/sanity/yamllint/config/plugins.yml') yaml_conf = YamlLintConfig(file=os.path.join(config_path, 'default.yml'))
module_conf = YamlLintConfig(file=os.path.join(config_path, 'modules.yml'))
plugin_conf = YamlLintConfig(file=os.path.join(config_path, 'plugins.yml'))
for path in paths: for path in paths:
extension = os.path.splitext(path)[1] extension = os.path.splitext(path)[1]

Loading…
Cancel
Save