diff --git a/.gitignore b/.gitignore index c09b5cfaab5..ece590c4d56 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ ansible.egg-info/ # Release directory packaging/release/ansible_release /.cache/ +/test/results/coverage/*=coverage.* /test/results/coverage/coverage* /test/results/reports/coverage.xml /test/results/reports/coverage/ diff --git a/test/integration/targets/ansible/runme.sh b/test/integration/targets/ansible/runme.sh index f38193d6bcb..537351ffb3f 100755 --- a/test/integration/targets/ansible/runme.sh +++ b/test/integration/targets/ansible/runme.sh @@ -2,12 +2,8 @@ set -eux -env - -which python -python --version - -which ansible ansible --version -ansible testhost -i ../../inventory -vvv -e "ansible_python_interpreter=$(which python)" -m ping -ansible testhost -i ../../inventory -vvv -e "ansible_python_interpreter=$(which python)" -m setup +ansible --help + +ansible testhost -i ../../inventory -m ping "$@" +ansible testhost -i ../../inventory -m setup "$@" diff --git a/test/runner/injector/cover b/test/runner/injector/cover deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover2 b/test/runner/injector/cover2 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover2 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover2.4 b/test/runner/injector/cover2.4 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover2.4 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover2.6 b/test/runner/injector/cover2.6 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover2.6 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover2.7 b/test/runner/injector/cover2.7 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover2.7 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover3 b/test/runner/injector/cover3 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover3 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover3.5 b/test/runner/injector/cover3.5 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover3.5 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/cover3.6 b/test/runner/injector/cover3.6 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/cover3.6 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/injector.py b/test/runner/injector/injector.py index 57241c9557c..31bc0cbc375 100755 --- a/test/runner/injector/injector.py +++ b/test/runner/injector/injector.py @@ -1,9 +1,32 @@ #!/usr/bin/env python -"""Code coverage wrapper.""" +"""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 errno +import json import os import sys import pipes @@ -11,10 +34,45 @@ import logging import getpass 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__)) @@ -31,25 +89,49 @@ def main(): try: logger.debug('Self: %s', __file__) - logger.debug('Arguments: %s', ' '.join(pipes.quote(c) for c in sys.argv)) - if os.path.basename(__file__).startswith('runner'): - args, env = runner() - elif os.path.basename(__file__).startswith('cover'): - args, env = cover() + 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) + + require_cwd = False + + if os.path.basename(__file__) == 'injector.py': + if config.coverage_file: + args, env, require_cwd = cover() + else: + args, env = runner() else: args, env = injector() logger.debug('Run command: %s', ' '.join(pipes.quote(c) for c in args)) + altered_cwd = False + try: cwd = os.getcwd() except OSError as ex: + # some platforms, such as OS X, may not allow querying the working directory when using become to drop privileges if ex.errno != errno.EACCES: raise - cwd = None + if require_cwd: + # make sure the program we execute can determine the working directory if it's required + cwd = '/' + os.chdir(cwd) + altered_cwd = True + else: + cwd = None - logger.debug('Working directory: %s', cwd or '?') + logger.debug('Working directory: %s%s', cwd or '?', ' (altered)' if altered_cwd else '') for key in sorted(env.keys()): logger.debug('%s=%s', key, env[key]) @@ -64,29 +146,28 @@ def injector(): """ :rtype: list[str], dict[str, str] """ - self_dir = os.path.dirname(os.path.abspath(__file__)) command = os.path.basename(__file__) - mode = os.environ.get('ANSIBLE_TEST_COVERAGE') - version = os.environ.get('ANSIBLE_TEST_PYTHON_VERSION', '') executable = find_executable(command) - if mode in ('coverage', 'version'): - if mode == 'coverage': - args, env = coverage_command(self_dir, version) - args += [executable] - tool = 'cover' + if config.coverage_file: + args, env = coverage_command() + else: + args, env = [config.python_interpreter], os.environ.copy() + + args += [executable] + + 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 = find_executable('python' + version) - args, env = [interpreter, executable], os.environ.copy() - tool = 'runner' + interpreter = config.remote_interpreter - if command in ('ansible', 'ansible-playbook', 'ansible-pull'): - interpreter = find_executable(tool + version) + if interpreter: args += ['--extra-vars', 'ansible_python_interpreter=' + interpreter] - else: - args, env = [executable], os.environ.copy() - args += sys.argv[1:] + args += config.arguments[1:] return args, env @@ -95,61 +176,53 @@ def runner(): """ :rtype: list[str], dict[str, str] """ - command = os.path.basename(__file__) - version = command.replace('runner', '') + args, env = [config.python_interpreter], os.environ.copy() - interpreter = find_executable('python' + version) - args, env = [interpreter], os.environ.copy() - - args += sys.argv[1:] + args += config.arguments[1:] return args, env def cover(): """ - :rtype: list[str], dict[str, str] + :rtype: list[str], dict[str, str], bool """ - self_dir = os.path.dirname(os.path.abspath(__file__)) - command = os.path.basename(__file__) - version = command.replace('cover', '') - - if len(sys.argv) > 1: - executable = sys.argv[1] + if len(config.arguments) > 1: + executable = config.arguments[1] else: executable = '' + require_cwd = False + if os.path.basename(executable).startswith('ansible_module_'): - args, env = coverage_command(self_dir, version) + args, env = coverage_command() + # coverage requires knowing the working directory + require_cwd = True else: - interpreter = find_executable('python' + version) - args, env = [interpreter], os.environ.copy() + args, env = [config.python_interpreter], os.environ.copy() - args += sys.argv[1:] + args += config.arguments[1:] - return args, env + return args, env, require_cwd -def coverage_command(self_dir, version): +def coverage_command(): """ - :type self_dir: str - :type version: str :rtype: list[str], dict[str, str] """ - executable = 'coverage' - - if version: - executable += '-%s' % version + self_dir = os.path.dirname(os.path.abspath(__file__)) args = [ - find_executable(executable), + config.python_interpreter, + '-m', + 'coverage.__main__', 'run', '--rcfile', os.path.join(self_dir, '.coveragerc'), ] env = os.environ.copy() - env['COVERAGE_FILE'] = os.path.abspath(os.path.join(self_dir, '..', 'output', 'coverage')) + env['COVERAGE_FILE'] = config.coverage_file return args, env diff --git a/test/runner/injector/runner b/test/runner/injector/runner deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner2 b/test/runner/injector/runner2 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner2 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner2.4 b/test/runner/injector/runner2.4 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner2.4 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner2.6 b/test/runner/injector/runner2.6 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner2.6 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner2.7 b/test/runner/injector/runner2.7 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner2.7 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner3 b/test/runner/injector/runner3 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner3 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner3.5 b/test/runner/injector/runner3.5 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner3.5 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/injector/runner3.6 b/test/runner/injector/runner3.6 deleted file mode 120000 index 1f9d09cbf2a..00000000000 --- a/test/runner/injector/runner3.6 +++ /dev/null @@ -1 +0,0 @@ -injector.py \ No newline at end of file diff --git a/test/runner/lib/cover.py b/test/runner/lib/cover.py index 6d110a662f6..a3747f706b1 100644 --- a/test/runner/lib/cover.py +++ b/test/runner/lib/cover.py @@ -33,8 +33,7 @@ def command_coverage_combine(args): modules = dict((t.module, t.path) for t in list(walk_module_targets())) - coverage_files = [os.path.join(COVERAGE_DIR, f) for f in os.listdir(COVERAGE_DIR) - if f.startswith('coverage') and f != 'coverage'] + coverage_files = [os.path.join(COVERAGE_DIR, f) for f in os.listdir(COVERAGE_DIR) if '=coverage.' in f] arc_data = {} @@ -60,7 +59,12 @@ def command_coverage_combine(args): continue for filename in original.measured_files(): - arcs = set(original.arcs(filename)) + arcs = set(original.arcs(filename) or []) + + if not arcs: + # This is most likely due to using an unsupported version of coverage. + display.warning('No arcs found for "%s" in coverage file: %s' % (filename, coverage_file)) + continue if '/ansible_modlib.zip/ansible/' in filename: new_name = re.sub('^.*/ansible_modlib.zip/ansible/', ansible_path, filename) @@ -68,11 +72,14 @@ def command_coverage_combine(args): filename = new_name elif '/ansible_module_' in filename: module = re.sub('^.*/ansible_module_(?P.*).py$', '\\g', filename) + if module not in modules: + display.warning('Skipping coverage of unknown module: %s' % module) + continue new_name = os.path.abspath(modules[module]) display.info('%s -> %s' % (filename, new_name), verbosity=3) filename = new_name - elif filename.startswith('/root/ansible/'): - new_name = re.sub('^/.*?/ansible/', root_path, filename) + elif re.search('^(/.*?)?/root/ansible/', filename): + new_name = re.sub('^(/.*?)?/root/ansible/', root_path, filename) display.info('%s -> %s' % (filename, new_name), verbosity=3) filename = new_name @@ -125,7 +132,7 @@ def command_coverage_erase(args): initialize_coverage(args) for name in os.listdir(COVERAGE_DIR): - if not name.startswith('coverage'): + if not name.startswith('coverage') and '=coverage.' not in name: continue path = os.path.join(COVERAGE_DIR, name) diff --git a/test/runner/lib/delegation.py b/test/runner/lib/delegation.py index 40721d70cf4..1bdad28422e 100644 --- a/test/runner/lib/delegation.py +++ b/test/runner/lib/delegation.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function import os +import re import sys import tempfile @@ -124,6 +125,10 @@ def delegate_tox(args, exclude, require): if not args.python: cmd += ['--python', version] + if isinstance(args, TestConfig): + if args.coverage and not args.coverage_label: + cmd += ['--coverage-label', 'tox-%s' % version] + run_command(args, tox + cmd) @@ -153,6 +158,12 @@ def delegate_docker(args, exclude, require): cmd = generate_command(args, '/root/ansible/test/runner/test.py', options, exclude, require) + if isinstance(args, TestConfig): + if args.coverage and not args.coverage_label: + image_label = re.sub('^ansible/ansible:', '', args.docker) + image_label = re.sub('[^a-zA-Z0-9]+', '-', image_label) + cmd += ['--coverage-label', 'docker-%s' % image_label] + if isinstance(args, IntegrationConfig): if not args.allow_destructive: cmd.append('--allow-destructive') @@ -162,75 +173,77 @@ def delegate_docker(args, exclude, require): if isinstance(args, ShellConfig): cmd_options.append('-it') - if not args.explain: - lib.pytar.create_tarfile('/tmp/ansible.tgz', '.', lib.pytar.ignore) + with tempfile.NamedTemporaryFile(prefix='ansible-source-', suffix='.tgz') as local_source_fd: + try: + if not args.explain: + lib.pytar.create_tarfile(local_source_fd.name, '.', lib.pytar.ignore) - try: - if util_image: - util_options = [ - '--detach', - ] + if util_image: + util_options = [ + '--detach', + ] - util_id, _ = docker_run(args, util_image, options=util_options) + util_id, _ = docker_run(args, util_image, options=util_options) - if args.explain: - util_id = 'util_id' + if args.explain: + util_id = 'util_id' + else: + util_id = util_id.strip() else: - util_id = util_id.strip() - else: - util_id = None - - test_options = [ - '--detach', - '--volume', '/sys/fs/cgroup:/sys/fs/cgroup:ro', - '--privileged=%s' % str(privileged).lower(), - ] - - if util_id: - test_options += [ - '--link', '%s:ansible.http.tests' % util_id, - '--link', '%s:sni1.ansible.http.tests' % util_id, - '--link', '%s:sni2.ansible.http.tests' % util_id, - '--link', '%s:fail.ansible.http.tests' % util_id, - '--env', 'HTTPTESTER=1', + util_id = None + + test_options = [ + '--detach', + '--volume', '/sys/fs/cgroup:/sys/fs/cgroup:ro', + '--privileged=%s' % str(privileged).lower(), ] - if isinstance(args, TestConfig): - cloud_platforms = get_cloud_providers(args) + if util_id: + test_options += [ + '--link', '%s:ansible.http.tests' % util_id, + '--link', '%s:sni1.ansible.http.tests' % util_id, + '--link', '%s:sni2.ansible.http.tests' % util_id, + '--link', '%s:fail.ansible.http.tests' % util_id, + '--env', 'HTTPTESTER=1', + ] - for cloud_platform in cloud_platforms: - test_options += cloud_platform.get_docker_run_options() + if isinstance(args, TestConfig): + cloud_platforms = get_cloud_providers(args) - test_id, _ = docker_run(args, test_image, options=test_options) + for cloud_platform in cloud_platforms: + test_options += cloud_platform.get_docker_run_options() - if args.explain: - test_id = 'test_id' - else: - test_id = test_id.strip() + test_id, _ = docker_run(args, test_image, options=test_options) - # write temporary files to /root since /tmp isn't ready immediately on container start - docker_put(args, test_id, 'test/runner/setup/docker.sh', '/root/docker.sh') - docker_exec(args, test_id, ['/bin/bash', '/root/docker.sh']) - docker_put(args, test_id, '/tmp/ansible.tgz', '/root/ansible.tgz') - docker_exec(args, test_id, ['mkdir', '/root/ansible']) - docker_exec(args, test_id, ['tar', 'oxzf', '/root/ansible.tgz', '-C', '/root/ansible']) + if args.explain: + test_id = 'test_id' + else: + test_id = test_id.strip() - # docker images are only expected to have a single python version available - if isinstance(args, UnitsConfig) and not args.python: - cmd += ['--python', 'default'] + # write temporary files to /root since /tmp isn't ready immediately on container start + docker_put(args, test_id, 'test/runner/setup/docker.sh', '/root/docker.sh') + docker_exec(args, test_id, ['/bin/bash', '/root/docker.sh']) + docker_put(args, test_id, local_source_fd.name, '/root/ansible.tgz') + docker_exec(args, test_id, ['mkdir', '/root/ansible']) + docker_exec(args, test_id, ['tar', 'oxzf', '/root/ansible.tgz', '-C', '/root/ansible']) - try: - docker_exec(args, test_id, cmd, options=cmd_options) + # docker images are only expected to have a single python version available + if isinstance(args, UnitsConfig) and not args.python: + cmd += ['--python', 'default'] + + try: + docker_exec(args, test_id, cmd, options=cmd_options) + finally: + with tempfile.NamedTemporaryFile(prefix='ansible-result-', suffix='.tgz') as local_result_fd: + docker_exec(args, test_id, ['tar', 'czf', '/root/results.tgz', '-C', '/root/ansible/test', 'results']) + docker_get(args, test_id, '/root/results.tgz', local_result_fd.name) + run_command(args, ['tar', 'oxzf', local_result_fd.name, '-C', 'test']) finally: - docker_exec(args, test_id, ['tar', 'czf', '/root/results.tgz', '-C', '/root/ansible/test', 'results']) - docker_get(args, test_id, '/root/results.tgz', '/tmp/results.tgz') - run_command(args, ['tar', 'oxzf', '/tmp/results.tgz', '-C', 'test']) - finally: - if util_id: - docker_rm(args, util_id) + if util_id: + docker_rm(args, util_id) - if test_id: - docker_rm(args, test_id) + if test_id: + docker_rm(args, test_id) def delegate_remote(args, exclude, require): @@ -257,6 +270,10 @@ def delegate_remote(args, exclude, require): cmd = generate_command(args, 'ansible/test/runner/test.py', options, exclude, require) + if isinstance(args, TestConfig): + if args.coverage and not args.coverage_label: + cmd += ['--coverage-label', 'remote-%s-%s' % (platform, version)] + if isinstance(args, IntegrationConfig): if not args.allow_destructive: cmd.append('--allow-destructive') diff --git a/test/runner/lib/executor.py b/test/runner/lib/executor.py index e033eef1f26..92f476ec77b 100644 --- a/test/runner/lib/executor.py +++ b/test/runner/lib/executor.py @@ -12,7 +12,6 @@ import functools import shutil import stat import random -import pipes import string import atexit @@ -607,7 +606,7 @@ def command_integration_script(args, target): env = integration_environment(args, target, cmd) cwd = target.path - intercept_command(args, cmd, env=env, cwd=cwd) + intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd) def command_integration_role(args, target, start_at_task): @@ -668,7 +667,7 @@ def command_integration_role(args, target, start_at_task): env['ANSIBLE_ROLES_PATH'] = os.path.abspath('test/integration/targets') - intercept_command(args, cmd, env=env, cwd=cwd) + intercept_command(args, cmd, target_name=target.name, env=env, cwd=cwd) def command_units(args): @@ -723,7 +722,7 @@ def command_units(args): display.info('Unit test with Python %s' % version) try: - intercept_command(args, command, env=env, python_version=version) + intercept_command(args, command, target_name='units', env=env, python_version=version) except SubprocessError as ex: # pytest exits with status code 5 when all tests are skipped, which isn't an error for our use case if ex.status != 5: @@ -838,7 +837,7 @@ def compile_version(args, python_version, include, exclude): return TestSuccess(command, test, python_version=python_version) -def intercept_command(args, cmd, capture=False, env=None, data=None, cwd=None, python_version=None): +def intercept_command(args, cmd, target_name, capture=False, env=None, data=None, cwd=None, python_version=None): """ :type args: TestConfig :type cmd: collections.Iterable[str] @@ -853,13 +852,25 @@ def intercept_command(args, cmd, capture=False, env=None, data=None, cwd=None, p env = common_environment() cmd = list(cmd) - escaped_cmd = ' '.join(pipes.quote(c) for c in cmd) inject_path = get_coverage_path(args) + config_path = os.path.join(inject_path, 'injector.json') + version = python_version or args.python_version + interpreter = find_executable('python%s' % version) + 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, version))) env['PATH'] = inject_path + os.pathsep + env['PATH'] - env['ANSIBLE_TEST_COVERAGE'] = 'coverage' if args.coverage else 'version' - env['ANSIBLE_TEST_PYTHON_VERSION'] = python_version or args.python_version - env['ANSIBLE_TEST_CMD'] = escaped_cmd + env['ANSIBLE_TEST_PYTHON_VERSION'] = version + env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = interpreter + + config = dict( + python_interpreter=interpreter, + coverage_file=coverage_file if args.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) @@ -888,6 +899,10 @@ def get_coverage_path(args): 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) @@ -1210,7 +1225,7 @@ class EnvironmentDescription(object): :type command: list[str] :rtype: str """ - stdout, stderr = raw_command(command, capture=True) + stdout, stderr = raw_command(command, capture=True, cmd_verbosity=2) return (stdout or '').strip() + (stderr or '').strip() @staticmethod diff --git a/test/runner/lib/manage_ci.py b/test/runner/lib/manage_ci.py index 7f0910a1d31..72b7645fe23 100644 --- a/test/runner/lib/manage_ci.py +++ b/test/runner/lib/manage_ci.py @@ -2,7 +2,9 @@ from __future__ import absolute_import, print_function +import os import pipes +import tempfile from time import sleep @@ -135,11 +137,15 @@ class ManagePosixCI(object): def upload_source(self): """Upload and extract source.""" - if not self.core_ci.args.explain: - lib.pytar.create_tarfile('/tmp/ansible.tgz', '.', lib.pytar.ignore) + with tempfile.NamedTemporaryFile(prefix='ansible-source-', suffix='.tgz') as local_source_fd: + remote_source_dir = '/tmp' + remote_source_path = os.path.join(remote_source_dir, os.path.basename(local_source_fd.name)) - self.upload('/tmp/ansible.tgz', '/tmp') - self.ssh('rm -rf ~/ansible && mkdir ~/ansible && cd ~/ansible && tar oxzf /tmp/ansible.tgz') + if not self.core_ci.args.explain: + lib.pytar.create_tarfile(local_source_fd.name, '.', lib.pytar.ignore) + + self.upload(local_source_fd.name, remote_source_dir) + self.ssh('rm -rf ~/ansible && mkdir ~/ansible && cd ~/ansible && tar oxzf %s' % remote_source_path) def download(self, remote, local): """ diff --git a/test/runner/lib/sanity.py b/test/runner/lib/sanity.py index a8294dcbbf8..ee619cc16ad 100644 --- a/test/runner/lib/sanity.py +++ b/test/runner/lib/sanity.py @@ -644,7 +644,7 @@ def command_sanity_ansible_doc(args, targets, python_version): cmd = ['ansible-doc'] + modules try: - stdout, stderr = intercept_command(args, cmd, env=env, capture=True, python_version=python_version) + stdout, stderr = intercept_command(args, cmd, target_name='ansible-doc', env=env, capture=True, python_version=python_version) status = 0 except SubprocessError as ex: stdout = ex.stdout diff --git a/test/runner/lib/test.py b/test/runner/lib/test.py index e07e813b4ca..e3b89217650 100644 --- a/test/runner/lib/test.py +++ b/test/runner/lib/test.py @@ -65,6 +65,7 @@ class TestConfig(EnvironmentConfig): super(TestConfig, self).__init__(args, command) self.coverage = args.coverage # type: bool + self.coverage_label = args.coverage_label # type: str self.include = args.include # type: list [str] self.exclude = args.exclude # type: list [str] self.require = args.require # type: list [str] diff --git a/test/runner/test.py b/test/runner/test.py index 873c5054047..8371976801c 100755 --- a/test/runner/test.py +++ b/test/runner/test.py @@ -168,6 +168,10 @@ def parse_args(): action='store_true', help='analyze code coverage when running tests') + test.add_argument('--coverage-label', + default='', + help='label to include in coverage output file names') + test.add_argument('--metadata', help=argparse.SUPPRESS)