mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Overhaul ansible-test code coverage and injector. (#53510)
This commit is contained in:
parent
3bdbe24861
commit
a8e328f474
19 changed files with 253 additions and 370 deletions
|
@ -2,4 +2,5 @@
|
||||||
# For script based test targets (using runme.sh) put the inventory file in the test's directory instead.
|
# For script based test targets (using runme.sh) put the inventory file in the test's directory instead.
|
||||||
|
|
||||||
[testgroup]
|
[testgroup]
|
||||||
testhost ansible_connection=local
|
# ansible_python_interpreter must be set to avoid interpreter discovery
|
||||||
|
testhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1,245 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""Interpreter and code coverage injector for use with ansible-test.
|
|
||||||
|
|
||||||
The injector serves two main purposes:
|
|
||||||
|
|
||||||
1) Control the python interpreter used to run test tools and ansible code.
|
|
||||||
2) Provide optional code coverage analysis of ansible code.
|
|
||||||
|
|
||||||
The injector is executed one of two ways:
|
|
||||||
|
|
||||||
1) On the controller via a symbolic link such as ansible or pytest.
|
|
||||||
This is accomplished by prepending the injector directory to the PATH by ansible-test.
|
|
||||||
|
|
||||||
2) As the python interpreter when running ansible modules.
|
|
||||||
This is only supported when connecting to the local host.
|
|
||||||
Otherwise set the ANSIBLE_TEST_REMOTE_INTERPRETER environment variable.
|
|
||||||
It can be empty to auto-detect the python interpreter on the remote host.
|
|
||||||
If not empty it will be used to set ansible_python_interpreter.
|
|
||||||
|
|
||||||
NOTE: Running ansible-test with the --tox option or inside a virtual environment
|
|
||||||
may prevent the injector from working for tests which use connection
|
|
||||||
types other than local, or which use become, due to lack of permissions
|
|
||||||
to access the interpreter for the virtual environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import, print_function
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import pipes
|
|
||||||
import logging
|
|
||||||
import getpass
|
|
||||||
import resource
|
|
||||||
|
|
||||||
logger = logging.getLogger('injector') # pylint: disable=locally-disabled, invalid-name
|
|
||||||
# pylint: disable=locally-disabled, invalid-name
|
|
||||||
config = None # type: InjectorConfig
|
|
||||||
|
|
||||||
|
|
||||||
class InjectorConfig(object):
|
|
||||||
"""Mandatory configuration."""
|
|
||||||
def __init__(self, config_path):
|
|
||||||
"""Initialize config."""
|
|
||||||
with open(config_path) as config_fd:
|
|
||||||
_config = json.load(config_fd)
|
|
||||||
|
|
||||||
self.python_interpreter = _config['python_interpreter']
|
|
||||||
self.coverage_file = _config['coverage_file']
|
|
||||||
|
|
||||||
# Read from the environment instead of config since it needs to be changed by integration test scripts.
|
|
||||||
# It also does not need to flow from the controller to the remote. It is only used on the controller.
|
|
||||||
self.remote_interpreter = os.environ.get('ANSIBLE_TEST_REMOTE_INTERPRETER', None)
|
|
||||||
|
|
||||||
self.arguments = [to_text(c) for c in sys.argv]
|
|
||||||
|
|
||||||
|
|
||||||
def to_text(value):
|
|
||||||
"""
|
|
||||||
:type value: str | None
|
|
||||||
:rtype: str | None
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(value, bytes):
|
|
||||||
return value.decode('utf-8')
|
|
||||||
|
|
||||||
return u'%s' % value
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
global config # pylint: disable=locally-disabled, global-statement
|
|
||||||
|
|
||||||
formatter = logging.Formatter('%(asctime)s %(process)d %(levelname)s %(message)s')
|
|
||||||
log_name = 'ansible-test-coverage.%s.log' % getpass.getuser()
|
|
||||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
handler = logging.FileHandler(os.path.join('/tmp', log_name))
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
handler = logging.FileHandler(os.path.abspath(os.path.join(self_dir, '..', 'logs', log_name)))
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.debug('Self: %s', __file__)
|
|
||||||
|
|
||||||
# to achieve a consistent nofile ulimit, set to 16k here, this can affect performance in subprocess.Popen when
|
|
||||||
# being called with close_fds=True on Python (8x the time on some environments)
|
|
||||||
nofile_limit = 16 * 1024
|
|
||||||
current_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
||||||
new_limit = (nofile_limit, nofile_limit)
|
|
||||||
if current_limit > new_limit:
|
|
||||||
logger.debug('RLIMIT_NOFILE: %s -> %s', current_limit, new_limit)
|
|
||||||
resource.setrlimit(resource.RLIMIT_NOFILE, (nofile_limit, nofile_limit))
|
|
||||||
else:
|
|
||||||
logger.debug('RLIMIT_NOFILE: %s', current_limit)
|
|
||||||
|
|
||||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'injector.json')
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = InjectorConfig(config_path)
|
|
||||||
except IOError:
|
|
||||||
logger.exception('Error reading config: %s', config_path)
|
|
||||||
exit('No injector config found. Set ANSIBLE_TEST_REMOTE_INTERPRETER if the test is not connecting to the local host.')
|
|
||||||
|
|
||||||
logger.debug('Arguments: %s', ' '.join(pipes.quote(c) for c in config.arguments))
|
|
||||||
logger.debug('Python interpreter: %s', config.python_interpreter)
|
|
||||||
logger.debug('Remote interpreter: %s', config.remote_interpreter)
|
|
||||||
logger.debug('Coverage file: %s', config.coverage_file)
|
|
||||||
|
|
||||||
if os.path.basename(__file__) == 'injector.py':
|
|
||||||
args, env = runner() # code coverage collection is baked into the AnsiballZ wrapper when needed
|
|
||||||
elif os.path.basename(__file__) == 'python.py':
|
|
||||||
args, env = python() # run arbitrary python commands using the correct python and with optional code coverage
|
|
||||||
else:
|
|
||||||
args, env = injector()
|
|
||||||
|
|
||||||
logger.debug('Run command: %s', ' '.join(pipes.quote(c) for c in args))
|
|
||||||
|
|
||||||
for key in sorted(env.keys()):
|
|
||||||
logger.debug('%s=%s', key, env[key])
|
|
||||||
|
|
||||||
os.execvpe(args[0], args, env)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.fatal(ex)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def python():
|
|
||||||
"""
|
|
||||||
:rtype: list[str], dict[str, str]
|
|
||||||
"""
|
|
||||||
if config.coverage_file:
|
|
||||||
args, env = coverage_command()
|
|
||||||
else:
|
|
||||||
args, env = [config.python_interpreter], os.environ.copy()
|
|
||||||
|
|
||||||
args += config.arguments[1:]
|
|
||||||
|
|
||||||
return args, env
|
|
||||||
|
|
||||||
|
|
||||||
def injector():
|
|
||||||
"""
|
|
||||||
:rtype: list[str], dict[str, str]
|
|
||||||
"""
|
|
||||||
command = os.path.basename(__file__)
|
|
||||||
|
|
||||||
run_as_python_module = (
|
|
||||||
'pytest',
|
|
||||||
)
|
|
||||||
|
|
||||||
if command in run_as_python_module:
|
|
||||||
executable_args = ['-m', command]
|
|
||||||
else:
|
|
||||||
executable_args = [find_executable(command)]
|
|
||||||
|
|
||||||
if config.coverage_file:
|
|
||||||
args, env = coverage_command()
|
|
||||||
else:
|
|
||||||
args, env = [config.python_interpreter], os.environ.copy()
|
|
||||||
|
|
||||||
args += executable_args
|
|
||||||
|
|
||||||
if command in ('ansible', 'ansible-playbook', 'ansible-pull'):
|
|
||||||
if config.remote_interpreter is None:
|
|
||||||
interpreter = os.path.join(os.path.dirname(__file__), 'injector.py')
|
|
||||||
elif config.remote_interpreter == '':
|
|
||||||
interpreter = None
|
|
||||||
else:
|
|
||||||
interpreter = config.remote_interpreter
|
|
||||||
|
|
||||||
if interpreter:
|
|
||||||
args += ['--extra-vars', 'ansible_python_interpreter=' + interpreter]
|
|
||||||
|
|
||||||
args += config.arguments[1:]
|
|
||||||
|
|
||||||
return args, env
|
|
||||||
|
|
||||||
|
|
||||||
def runner():
|
|
||||||
"""
|
|
||||||
:rtype: list[str], dict[str, str]
|
|
||||||
"""
|
|
||||||
args, env = [config.python_interpreter], os.environ.copy()
|
|
||||||
|
|
||||||
args += config.arguments[1:]
|
|
||||||
|
|
||||||
return args, env
|
|
||||||
|
|
||||||
|
|
||||||
def coverage_command():
|
|
||||||
"""
|
|
||||||
:rtype: list[str], dict[str, str]
|
|
||||||
"""
|
|
||||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
args = [
|
|
||||||
config.python_interpreter,
|
|
||||||
'-m',
|
|
||||||
'coverage.__main__',
|
|
||||||
'run',
|
|
||||||
'--rcfile',
|
|
||||||
os.path.join(self_dir, '.coveragerc'),
|
|
||||||
]
|
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['COVERAGE_FILE'] = config.coverage_file
|
|
||||||
|
|
||||||
return args, env
|
|
||||||
|
|
||||||
|
|
||||||
def find_executable(executable):
|
|
||||||
"""
|
|
||||||
:type executable: str
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
self = os.path.abspath(__file__)
|
|
||||||
path = os.environ.get('PATH', os.path.defpath)
|
|
||||||
seen_dirs = set()
|
|
||||||
|
|
||||||
for path_dir in path.split(os.path.pathsep):
|
|
||||||
if path_dir in seen_dirs:
|
|
||||||
continue
|
|
||||||
|
|
||||||
seen_dirs.add(path_dir)
|
|
||||||
candidate = os.path.abspath(os.path.join(path_dir, executable))
|
|
||||||
|
|
||||||
if candidate == self:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if os.path.exists(candidate) and os.access(candidate, os.F_OK | os.X_OK):
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
raise Exception('Executable "%s" not found in path: %s' % (executable, path))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
|
@ -1 +1 @@
|
||||||
injector.py
|
python.py
|
|
@ -1 +0,0 @@
|
||||||
injector.py
|
|
63
test/runner/injector/python.py
Executable file
63
test/runner/injector/python.py
Executable file
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Provides an entry point for python scripts and python modules on the controller with the current python interpreter and optional code coverage collection."""
|
||||||
|
|
||||||
|
import imp
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
name = os.path.basename(__file__)
|
||||||
|
args = [sys.executable]
|
||||||
|
|
||||||
|
coverage_config = os.environ.get('_ANSIBLE_COVERAGE_CONFIG')
|
||||||
|
coverage_output = os.environ.get('_ANSIBLE_COVERAGE_OUTPUT')
|
||||||
|
|
||||||
|
if coverage_config:
|
||||||
|
if coverage_output:
|
||||||
|
args += ['-m', 'coverage.__main__', 'run', '--rcfile', coverage_config]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
imp.find_module('coverage')
|
||||||
|
except ImportError:
|
||||||
|
exit('ERROR: Could not find `coverage` module. Did you use a virtualenv created without --system-site-packages or with the wrong interpreter?')
|
||||||
|
|
||||||
|
if name == 'python.py':
|
||||||
|
if sys.argv[1] == '-c':
|
||||||
|
# prevent simple misuse of python.py with -c which does not work with coverage
|
||||||
|
sys.exit('ERROR: Use `python -c` instead of `python.py -c` to avoid errors when code coverage is collected.')
|
||||||
|
elif name == 'pytest':
|
||||||
|
args += ['-m', 'pytest']
|
||||||
|
else:
|
||||||
|
args += [find_executable(name)]
|
||||||
|
|
||||||
|
args += sys.argv[1:]
|
||||||
|
|
||||||
|
os.execv(args[0], args)
|
||||||
|
|
||||||
|
|
||||||
|
def find_executable(name):
|
||||||
|
"""
|
||||||
|
:type name: str
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
path = os.environ.get('PATH', os.path.defpath)
|
||||||
|
seen = set([os.path.abspath(__file__)])
|
||||||
|
|
||||||
|
for base in path.split(os.path.pathsep):
|
||||||
|
candidate = os.path.abspath(os.path.join(base, name))
|
||||||
|
|
||||||
|
if candidate in seen:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen.add(candidate)
|
||||||
|
|
||||||
|
if os.path.exists(candidate) and os.access(candidate, os.F_OK | os.X_OK):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
raise Exception('Executable "%s" not found in path: %s' % (name, path))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -15,6 +15,9 @@ import sys
|
||||||
import hashlib
|
import hashlib
|
||||||
import difflib
|
import difflib
|
||||||
import filecmp
|
import filecmp
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import shutil
|
||||||
|
|
||||||
import lib.pytar
|
import lib.pytar
|
||||||
import lib.thread
|
import lib.thread
|
||||||
|
@ -50,12 +53,13 @@ from lib.util import (
|
||||||
is_binary_file,
|
is_binary_file,
|
||||||
find_executable,
|
find_executable,
|
||||||
raw_command,
|
raw_command,
|
||||||
get_coverage_path,
|
get_python_path,
|
||||||
get_available_port,
|
get_available_port,
|
||||||
generate_pip_command,
|
generate_pip_command,
|
||||||
find_python,
|
find_python,
|
||||||
get_docker_completion,
|
get_docker_completion,
|
||||||
named_temporary_file,
|
named_temporary_file,
|
||||||
|
COVERAGE_OUTPUT_PATH,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lib.docker_util import (
|
from lib.docker_util import (
|
||||||
|
@ -112,6 +116,7 @@ from lib.metadata import (
|
||||||
from lib.integration import (
|
from lib.integration import (
|
||||||
integration_test_environment,
|
integration_test_environment,
|
||||||
integration_test_config_file,
|
integration_test_config_file,
|
||||||
|
setup_common_temp_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
SUPPORTED_PYTHON_VERSIONS = (
|
SUPPORTED_PYTHON_VERSIONS = (
|
||||||
|
@ -359,7 +364,7 @@ def command_network_integration(args):
|
||||||
instances = [] # type: list [lib.thread.WrappedThread]
|
instances = [] # type: list [lib.thread.WrappedThread]
|
||||||
|
|
||||||
if args.platform:
|
if args.platform:
|
||||||
get_coverage_path(args, args.python_executable) # initialize before starting threads
|
get_python_path(args, args.python_executable) # initialize before starting threads
|
||||||
|
|
||||||
configs = dict((config['platform_version'], config) for config in args.metadata.instance_config)
|
configs = dict((config['platform_version'], config) for config in args.metadata.instance_config)
|
||||||
|
|
||||||
|
@ -527,7 +532,7 @@ def command_windows_integration(args):
|
||||||
httptester_id = None
|
httptester_id = None
|
||||||
|
|
||||||
if args.windows:
|
if args.windows:
|
||||||
get_coverage_path(args, args.python_executable) # initialize before starting threads
|
get_python_path(args, args.python_executable) # initialize before starting threads
|
||||||
|
|
||||||
configs = dict((config['platform_version'], config) for config in args.metadata.instance_config)
|
configs = dict((config['platform_version'], config) for config in args.metadata.instance_config)
|
||||||
|
|
||||||
|
@ -833,6 +838,12 @@ def command_integration_filtered(args, targets, all_targets, inventory_path, pre
|
||||||
|
|
||||||
current_environment = None # type: EnvironmentDescription | None
|
current_environment = None # type: EnvironmentDescription | None
|
||||||
|
|
||||||
|
# common temporary directory path that will be valid on both the controller and the remote
|
||||||
|
# it must be common because it will be referenced in environment variables that are shared across multiple hosts
|
||||||
|
common_temp_path = '/tmp/ansible-test-%s' % ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
|
||||||
|
|
||||||
|
setup_common_temp_dir(args, common_temp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for target in targets_iter:
|
for target in targets_iter:
|
||||||
if args.start_at and not found:
|
if args.start_at and not found:
|
||||||
|
@ -863,11 +874,11 @@ def command_integration_filtered(args, targets, all_targets, inventory_path, pre
|
||||||
if cloud_environment:
|
if cloud_environment:
|
||||||
cloud_environment.setup_once()
|
cloud_environment.setup_once()
|
||||||
|
|
||||||
run_setup_targets(args, test_dir, target.setup_once, all_targets_dict, setup_targets_executed, inventory_path, False)
|
run_setup_targets(args, test_dir, target.setup_once, all_targets_dict, setup_targets_executed, inventory_path, common_temp_path, False)
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
run_setup_targets(args, test_dir, target.setup_always, all_targets_dict, setup_targets_executed, inventory_path, True)
|
run_setup_targets(args, test_dir, target.setup_always, all_targets_dict, setup_targets_executed, inventory_path, common_temp_path, True)
|
||||||
|
|
||||||
if not args.explain:
|
if not args.explain:
|
||||||
# create a fresh test directory for each test target
|
# create a fresh test directory for each test target
|
||||||
|
@ -879,9 +890,9 @@ def command_integration_filtered(args, targets, all_targets, inventory_path, pre
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if target.script_path:
|
if target.script_path:
|
||||||
command_integration_script(args, target, test_dir, inventory_path)
|
command_integration_script(args, target, test_dir, inventory_path, common_temp_path)
|
||||||
else:
|
else:
|
||||||
command_integration_role(args, target, start_at_task, test_dir, inventory_path)
|
command_integration_role(args, target, start_at_task, test_dir, inventory_path, common_temp_path)
|
||||||
start_at_task = None
|
start_at_task = None
|
||||||
finally:
|
finally:
|
||||||
if post_target:
|
if post_target:
|
||||||
|
@ -945,6 +956,15 @@ def command_integration_filtered(args, targets, all_targets, inventory_path, pre
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if not args.explain:
|
if not args.explain:
|
||||||
|
if args.coverage:
|
||||||
|
coverage_temp_path = os.path.join(common_temp_path, COVERAGE_OUTPUT_PATH)
|
||||||
|
coverage_save_path = 'test/results/coverage'
|
||||||
|
|
||||||
|
for filename in os.listdir(coverage_temp_path):
|
||||||
|
shutil.copy(os.path.join(coverage_temp_path, filename), os.path.join(coverage_save_path, filename))
|
||||||
|
|
||||||
|
remove_tree(common_temp_path)
|
||||||
|
|
||||||
results_path = 'test/results/data/%s-%s.json' % (args.command, re.sub(r'[^0-9]', '-', str(datetime.datetime.utcnow().replace(microsecond=0))))
|
results_path = 'test/results/data/%s-%s.json' % (args.command, re.sub(r'[^0-9]', '-', str(datetime.datetime.utcnow().replace(microsecond=0))))
|
||||||
|
|
||||||
data = dict(
|
data = dict(
|
||||||
|
@ -1086,7 +1106,7 @@ rdr pass inet proto tcp from any to any port 443 -> 127.0.0.1 port 8443
|
||||||
raise ApplicationError('No supported port forwarding mechanism detected.')
|
raise ApplicationError('No supported port forwarding mechanism detected.')
|
||||||
|
|
||||||
|
|
||||||
def run_setup_targets(args, test_dir, target_names, targets_dict, targets_executed, inventory_path, always):
|
def run_setup_targets(args, test_dir, target_names, targets_dict, targets_executed, inventory_path, temp_path, always):
|
||||||
"""
|
"""
|
||||||
:type args: IntegrationConfig
|
:type args: IntegrationConfig
|
||||||
:type test_dir: str
|
:type test_dir: str
|
||||||
|
@ -1094,6 +1114,7 @@ def run_setup_targets(args, test_dir, target_names, targets_dict, targets_execut
|
||||||
:type targets_dict: dict[str, IntegrationTarget]
|
:type targets_dict: dict[str, IntegrationTarget]
|
||||||
:type targets_executed: set[str]
|
:type targets_executed: set[str]
|
||||||
:type inventory_path: str
|
:type inventory_path: str
|
||||||
|
:type temp_path: str
|
||||||
:type always: bool
|
:type always: bool
|
||||||
"""
|
"""
|
||||||
for target_name in target_names:
|
for target_name in target_names:
|
||||||
|
@ -1108,9 +1129,9 @@ def run_setup_targets(args, test_dir, target_names, targets_dict, targets_execut
|
||||||
make_dirs(test_dir)
|
make_dirs(test_dir)
|
||||||
|
|
||||||
if target.script_path:
|
if target.script_path:
|
||||||
command_integration_script(args, target, test_dir, inventory_path)
|
command_integration_script(args, target, test_dir, inventory_path, temp_path)
|
||||||
else:
|
else:
|
||||||
command_integration_role(args, target, None, test_dir, inventory_path)
|
command_integration_role(args, target, None, test_dir, inventory_path, temp_path)
|
||||||
|
|
||||||
targets_executed.add(target_name)
|
targets_executed.add(target_name)
|
||||||
|
|
||||||
|
@ -1156,12 +1177,13 @@ def integration_environment(args, target, test_dir, inventory_path, ansible_conf
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def command_integration_script(args, target, test_dir, inventory_path):
|
def command_integration_script(args, target, test_dir, inventory_path, temp_path):
|
||||||
"""
|
"""
|
||||||
:type args: IntegrationConfig
|
:type args: IntegrationConfig
|
||||||
:type target: IntegrationTarget
|
:type target: IntegrationTarget
|
||||||
:type test_dir: str
|
:type test_dir: str
|
||||||
:type inventory_path: str
|
:type inventory_path: str
|
||||||
|
:type temp_path: str
|
||||||
"""
|
"""
|
||||||
display.info('Running %s integration test script' % target.name)
|
display.info('Running %s integration test script' % target.name)
|
||||||
|
|
||||||
|
@ -1190,16 +1212,17 @@ def command_integration_script(args, target, test_dir, inventory_path):
|
||||||
cmd += ['-e', '@%s' % config_path]
|
cmd += ['-e', '@%s' % config_path]
|
||||||
|
|
||||||
coverage = args.coverage and 'non_local/' not in target.aliases
|
coverage = args.coverage and 'non_local/' not in target.aliases
|
||||||
intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd, coverage=coverage)
|
intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd, temp_path=temp_path, coverage=coverage)
|
||||||
|
|
||||||
|
|
||||||
def command_integration_role(args, target, start_at_task, test_dir, inventory_path):
|
def command_integration_role(args, target, start_at_task, test_dir, inventory_path, temp_path):
|
||||||
"""
|
"""
|
||||||
:type args: IntegrationConfig
|
:type args: IntegrationConfig
|
||||||
:type target: IntegrationTarget
|
:type target: IntegrationTarget
|
||||||
:type start_at_task: str | None
|
:type start_at_task: str | None
|
||||||
:type test_dir: str
|
:type test_dir: str
|
||||||
:type inventory_path: str
|
:type inventory_path: str
|
||||||
|
:type temp_path: str
|
||||||
"""
|
"""
|
||||||
display.info('Running %s integration test role' % target.name)
|
display.info('Running %s integration test role' % target.name)
|
||||||
|
|
||||||
|
@ -1273,7 +1296,7 @@ def command_integration_role(args, target, start_at_task, test_dir, inventory_pa
|
||||||
env['ANSIBLE_ROLES_PATH'] = os.path.abspath(os.path.join(test_env.integration_dir, 'targets'))
|
env['ANSIBLE_ROLES_PATH'] = os.path.abspath(os.path.join(test_env.integration_dir, 'targets'))
|
||||||
|
|
||||||
coverage = args.coverage and 'non_local/' not in target.aliases
|
coverage = args.coverage and 'non_local/' not in target.aliases
|
||||||
intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd, coverage=coverage)
|
intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd, temp_path=temp_path, coverage=coverage)
|
||||||
|
|
||||||
|
|
||||||
def command_units(args):
|
def command_units(args):
|
||||||
|
|
|
@ -6,6 +6,7 @@ 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 (
|
||||||
|
@ -24,6 +25,11 @@ from lib.util import (
|
||||||
display,
|
display,
|
||||||
make_dirs,
|
make_dirs,
|
||||||
named_temporary_file,
|
named_temporary_file,
|
||||||
|
COVERAGE_CONFIG_PATH,
|
||||||
|
COVERAGE_OUTPUT_PATH,
|
||||||
|
MODE_DIRECTORY,
|
||||||
|
MODE_DIRECTORY_WRITE,
|
||||||
|
MODE_FILE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lib.cache import (
|
from lib.cache import (
|
||||||
|
@ -35,6 +41,28 @@ from lib.cloud import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_common_temp_dir(args, path):
|
||||||
|
"""
|
||||||
|
:type args: IntegrationConfig
|
||||||
|
:type path: str
|
||||||
|
"""
|
||||||
|
if args.explain:
|
||||||
|
return
|
||||||
|
|
||||||
|
os.mkdir(path)
|
||||||
|
os.chmod(path, MODE_DIRECTORY)
|
||||||
|
|
||||||
|
coverage_config_path = os.path.join(path, COVERAGE_CONFIG_PATH)
|
||||||
|
|
||||||
|
shutil.copy(COVERAGE_CONFIG_PATH, coverage_config_path)
|
||||||
|
os.chmod(coverage_config_path, MODE_FILE)
|
||||||
|
|
||||||
|
coverage_output_path = os.path.join(path, COVERAGE_OUTPUT_PATH)
|
||||||
|
|
||||||
|
os.mkdir(coverage_output_path)
|
||||||
|
os.chmod(coverage_output_path, MODE_DIRECTORY_WRITE)
|
||||||
|
|
||||||
|
|
||||||
def generate_dependency_map(integration_targets):
|
def generate_dependency_map(integration_targets):
|
||||||
"""
|
"""
|
||||||
:type integration_targets: list[IntegrationTarget]
|
:type integration_targets: list[IntegrationTarget]
|
||||||
|
|
|
@ -116,9 +116,10 @@ class ImportTest(SanityMultipleVersion):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
virtualenv_python = os.path.join(virtual_environment_bin, 'python')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stdout, stderr = intercept_command(args, cmd, data=data, target_name=self.name, env=env, capture=True, python_version=python_version,
|
stdout, stderr = intercept_command(args, cmd, self.name, env, capture=True, data=data, python_version=python_version, virtualenv=virtualenv_python)
|
||||||
path=env['PATH'])
|
|
||||||
|
|
||||||
if stdout or stderr:
|
if stdout or stderr:
|
||||||
raise SubprocessError(cmd, stdout=stdout, stderr=stderr)
|
raise SubprocessError(cmd, stdout=stdout, stderr=stderr)
|
||||||
|
|
|
@ -39,13 +39,30 @@ except ImportError:
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
DOCKER_COMPLETION = {}
|
DOCKER_COMPLETION = {}
|
||||||
COVERAGE_PATHS = {} # type: dict[str, str]
|
PYTHON_PATHS = {} # type: dict[str, str]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
MAXFD = subprocess.MAXFD
|
MAXFD = subprocess.MAXFD
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
MAXFD = -1
|
MAXFD = -1
|
||||||
|
|
||||||
|
COVERAGE_CONFIG_PATH = '.coveragerc'
|
||||||
|
COVERAGE_OUTPUT_PATH = 'coverage'
|
||||||
|
|
||||||
|
# Modes are set to allow all users the same level of access.
|
||||||
|
# This permits files to be used in tests that change users.
|
||||||
|
# The only exception is write access to directories for the user creating them.
|
||||||
|
# This avoids having to modify the directory permissions a second time.
|
||||||
|
|
||||||
|
MODE_READ = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
|
||||||
|
|
||||||
|
MODE_FILE = MODE_READ
|
||||||
|
MODE_FILE_EXECUTE = MODE_FILE | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||||
|
MODE_FILE_WRITE = MODE_FILE | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
|
||||||
|
|
||||||
|
MODE_DIRECTORY = MODE_READ | stat.S_IWUSR | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||||
|
MODE_DIRECTORY_WRITE = MODE_DIRECTORY | stat.S_IWGRP | stat.S_IWOTH
|
||||||
|
|
||||||
|
|
||||||
def get_docker_completion():
|
def get_docker_completion():
|
||||||
"""
|
"""
|
||||||
|
@ -107,6 +124,83 @@ def read_lines_without_comments(path, remove_blank_lines=False):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def get_python_path(args, interpreter):
|
||||||
|
"""
|
||||||
|
:type args: TestConfig
|
||||||
|
:type interpreter: str
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
python_path = PYTHON_PATHS.get(interpreter)
|
||||||
|
|
||||||
|
if python_path:
|
||||||
|
return python_path
|
||||||
|
|
||||||
|
prefix = 'python-'
|
||||||
|
suffix = '-ansible'
|
||||||
|
|
||||||
|
root_temp_dir = '/tmp'
|
||||||
|
|
||||||
|
if args.explain:
|
||||||
|
return os.path.join(root_temp_dir, ''.join((prefix, 'temp', suffix)))
|
||||||
|
|
||||||
|
python_path = tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=root_temp_dir)
|
||||||
|
|
||||||
|
os.chmod(python_path, MODE_DIRECTORY)
|
||||||
|
os.symlink(interpreter, os.path.join(python_path, 'python'))
|
||||||
|
|
||||||
|
if not PYTHON_PATHS:
|
||||||
|
atexit.register(cleanup_python_paths)
|
||||||
|
|
||||||
|
PYTHON_PATHS[interpreter] = python_path
|
||||||
|
|
||||||
|
return python_path
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_python_paths():
|
||||||
|
"""Clean up all temporary python directories."""
|
||||||
|
for path in sorted(PYTHON_PATHS.values()):
|
||||||
|
display.info('Cleaning up temporary python directory: %s' % path, verbosity=2)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_coverage_environment(args, target_name, version, temp_path):
|
||||||
|
"""
|
||||||
|
:type args: TestConfig
|
||||||
|
:type target_name: str
|
||||||
|
:type version: str
|
||||||
|
:type temp_path: str
|
||||||
|
:rtype: dict[str, str]
|
||||||
|
"""
|
||||||
|
if temp_path:
|
||||||
|
# integration tests (both localhost and the optional testhost)
|
||||||
|
# config and results are in a temporary directory
|
||||||
|
coverage_config_base_path = temp_path
|
||||||
|
coverage_output_base_path = temp_path
|
||||||
|
else:
|
||||||
|
# unit tests, sanity tests and other special cases (localhost only)
|
||||||
|
# config and results are in the source tree
|
||||||
|
coverage_config_base_path = os.getcwd()
|
||||||
|
coverage_output_base_path = os.path.abspath(os.path.join('test/results'))
|
||||||
|
|
||||||
|
config_file = os.path.join(coverage_config_base_path, COVERAGE_CONFIG_PATH)
|
||||||
|
coverage_file = os.path.join(coverage_output_base_path, COVERAGE_OUTPUT_PATH, '%s=%s=%s=%s=coverage' % (
|
||||||
|
args.command, target_name, args.coverage_label or 'local-%s' % version, 'python-%s' % version))
|
||||||
|
|
||||||
|
if args.coverage_check:
|
||||||
|
coverage_file = ''
|
||||||
|
|
||||||
|
env = dict(
|
||||||
|
# both AnsiballZ and the ansible-test coverage injector rely on this
|
||||||
|
_ANSIBLE_COVERAGE_CONFIG=config_file,
|
||||||
|
# used during AnsiballZ wrapper creation to set COVERAGE_FILE for the module
|
||||||
|
_ANSIBLE_COVERAGE_OUTPUT=coverage_file,
|
||||||
|
# handle cases not covered by the AnsiballZ wrapper creation above
|
||||||
|
COVERAGE_FILE=coverage_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
def find_executable(executable, cwd=None, path=None, required=True):
|
def find_executable(executable, cwd=None, path=None, required=True):
|
||||||
"""
|
"""
|
||||||
:type executable: str
|
:type executable: str
|
||||||
|
@ -183,18 +277,19 @@ def generate_pip_command(python):
|
||||||
return [python, '-m', 'pip.__main__']
|
return [python, '-m', 'pip.__main__']
|
||||||
|
|
||||||
|
|
||||||
def intercept_command(args, cmd, target_name, capture=False, env=None, data=None, cwd=None, python_version=None, path=None, coverage=None):
|
def intercept_command(args, cmd, target_name, env, capture=False, data=None, cwd=None, python_version=None, temp_path=None, coverage=None, virtualenv=None):
|
||||||
"""
|
"""
|
||||||
:type args: TestConfig
|
:type args: TestConfig
|
||||||
:type cmd: collections.Iterable[str]
|
:type cmd: collections.Iterable[str]
|
||||||
:type target_name: str
|
:type target_name: str
|
||||||
|
:type env: dict[str, str]
|
||||||
:type capture: bool
|
:type capture: bool
|
||||||
:type env: dict[str, str] | None
|
|
||||||
:type data: str | None
|
:type data: str | None
|
||||||
:type cwd: str | None
|
:type cwd: str | None
|
||||||
:type python_version: str | None
|
:type python_version: str | None
|
||||||
:type path: str | None
|
:type temp_path: str | None
|
||||||
:type coverage: bool | None
|
:type coverage: bool | None
|
||||||
|
:type virtualenv: str | None
|
||||||
:rtype: str | None, str | None
|
:rtype: str | None, str | None
|
||||||
"""
|
"""
|
||||||
if not env:
|
if not env:
|
||||||
|
@ -205,108 +300,26 @@ def intercept_command(args, cmd, target_name, capture=False, env=None, data=None
|
||||||
|
|
||||||
cmd = list(cmd)
|
cmd = list(cmd)
|
||||||
version = python_version or args.python_version
|
version = python_version or args.python_version
|
||||||
interpreter = find_python(version, path)
|
interpreter = virtualenv or find_python(version)
|
||||||
inject_path = get_coverage_path(args, interpreter)
|
inject_path = os.path.abspath('test/runner/injector')
|
||||||
config_path = os.path.join(inject_path, 'injector.json')
|
|
||||||
coverage_file = os.path.abspath(os.path.join(inject_path, '..', 'output', '%s=%s=%s=%s=coverage' % (
|
|
||||||
args.command, target_name, args.coverage_label or 'local-%s' % version, 'python-%s' % version)))
|
|
||||||
|
|
||||||
if args.coverage_check:
|
if not virtualenv:
|
||||||
coverage_file = ''
|
# injection of python into the path is required when not activating a virtualenv
|
||||||
|
# otherwise scripts may find the wrong interpreter or possibly no interpreter
|
||||||
|
python_path = get_python_path(args, interpreter)
|
||||||
|
inject_path = python_path + os.path.pathsep + inject_path
|
||||||
|
|
||||||
env['PATH'] = inject_path + os.path.pathsep + env['PATH']
|
env['PATH'] = inject_path + os.path.pathsep + env['PATH']
|
||||||
env['ANSIBLE_TEST_PYTHON_VERSION'] = version
|
env['ANSIBLE_TEST_PYTHON_VERSION'] = version
|
||||||
env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = interpreter
|
env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = interpreter
|
||||||
|
|
||||||
if coverage:
|
if coverage:
|
||||||
env['_ANSIBLE_COVERAGE_CONFIG'] = os.path.join(inject_path, '.coveragerc')
|
# add the necessary environment variables to enable code coverage collection
|
||||||
env['_ANSIBLE_COVERAGE_OUTPUT'] = coverage_file
|
env.update(get_coverage_environment(args, target_name, version, temp_path))
|
||||||
|
|
||||||
config = dict(
|
|
||||||
python_interpreter=interpreter,
|
|
||||||
coverage_file=coverage_file if coverage else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not args.explain:
|
|
||||||
with open(config_path, 'w') as config_fd:
|
|
||||||
json.dump(config, config_fd, indent=4, sort_keys=True)
|
|
||||||
|
|
||||||
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd)
|
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
def get_coverage_path(args, interpreter):
|
|
||||||
"""
|
|
||||||
:type args: TestConfig
|
|
||||||
:type interpreter: str
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
coverage_path = COVERAGE_PATHS.get(interpreter)
|
|
||||||
|
|
||||||
if coverage_path:
|
|
||||||
return os.path.join(coverage_path, 'coverage')
|
|
||||||
|
|
||||||
prefix = 'ansible-test-coverage-'
|
|
||||||
tmp_dir = '/tmp'
|
|
||||||
|
|
||||||
if args.explain:
|
|
||||||
return os.path.join(tmp_dir, '%stmp' % prefix, 'coverage')
|
|
||||||
|
|
||||||
src = os.path.abspath(os.path.join(os.getcwd(), 'test/runner/injector/'))
|
|
||||||
|
|
||||||
coverage_path = tempfile.mkdtemp('', prefix, dir=tmp_dir)
|
|
||||||
os.chmod(coverage_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
|
||||||
|
|
||||||
shutil.copytree(src, os.path.join(coverage_path, 'coverage'))
|
|
||||||
shutil.copy('.coveragerc', os.path.join(coverage_path, 'coverage', '.coveragerc'))
|
|
||||||
|
|
||||||
for root, dir_names, file_names in os.walk(coverage_path):
|
|
||||||
for name in dir_names + file_names:
|
|
||||||
os.chmod(os.path.join(root, name), stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
|
||||||
|
|
||||||
for directory in 'output', 'logs':
|
|
||||||
os.mkdir(os.path.join(coverage_path, directory))
|
|
||||||
os.chmod(os.path.join(coverage_path, directory), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
||||||
|
|
||||||
os.symlink(interpreter, os.path.join(coverage_path, 'coverage', 'python'))
|
|
||||||
|
|
||||||
if not COVERAGE_PATHS:
|
|
||||||
atexit.register(cleanup_coverage_dirs)
|
|
||||||
|
|
||||||
COVERAGE_PATHS[interpreter] = coverage_path
|
|
||||||
|
|
||||||
return os.path.join(coverage_path, 'coverage')
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_coverage_dirs():
|
|
||||||
"""Clean up all coverage directories."""
|
|
||||||
for path in COVERAGE_PATHS.values():
|
|
||||||
display.info('Cleaning up coverage directory: %s' % path, verbosity=2)
|
|
||||||
cleanup_coverage_dir(path)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_coverage_dir(coverage_path):
|
|
||||||
"""Copy over coverage data from temporary directory and purge temporary directory.
|
|
||||||
:type coverage_path: str
|
|
||||||
"""
|
|
||||||
output_dir = os.path.join(coverage_path, 'output')
|
|
||||||
|
|
||||||
for filename in os.listdir(output_dir):
|
|
||||||
src = os.path.join(output_dir, filename)
|
|
||||||
dst = os.path.join(os.getcwd(), 'test', 'results', 'coverage')
|
|
||||||
shutil.copy(src, dst)
|
|
||||||
|
|
||||||
logs_dir = os.path.join(coverage_path, 'logs')
|
|
||||||
|
|
||||||
for filename in os.listdir(logs_dir):
|
|
||||||
random_suffix = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
|
|
||||||
new_name = '%s.%s.log' % (os.path.splitext(os.path.basename(filename))[0], random_suffix)
|
|
||||||
src = os.path.join(logs_dir, filename)
|
|
||||||
dst = os.path.join(os.getcwd(), 'test', 'results', 'logs', new_name)
|
|
||||||
shutil.copy(src, dst)
|
|
||||||
|
|
||||||
shutil.rmtree(coverage_path)
|
|
||||||
|
|
||||||
|
|
||||||
def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None,
|
def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None,
|
||||||
cmd_verbosity=1, str_errors='strict'):
|
cmd_verbosity=1, str_errors='strict'):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue