Overhaul ansible-test code coverage and injector. (#53510)

This commit is contained in:
Matt Clay 2019-03-13 07:14:12 -07:00 committed by GitHub
parent 3bdbe24861
commit a8e328f474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 253 additions and 370 deletions

View file

@ -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 }}"

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +1 @@
injector.py python.py

View file

@ -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()

View file

@ -1 +1 @@
injector.py python.py

View file

@ -1 +0,0 @@
injector.py

63
test/runner/injector/python.py Executable file
View 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()

View file

@ -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):

View file

@ -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]

View file

@ -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)

View file

@ -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'):
""" """