summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Clay <matt@mystile.com>2017-05-11 13:25:02 +0800
committerMatt Clay <matt@mystile.com>2017-05-12 14:55:48 +0800
commitdfd19a812f6f418025f8968a80772054abf28cb7 (patch)
treeef0c5c77365f9c4728aec68da57eabf4ed681def
parent548cacdf6a28a708142c4a2e4ed1ecba8102b60a (diff)
downloadansible-dfd19a812f6f418025f8968a80772054abf28cb7.tar.gz
Miscellaneous bug fixes for ansible-test.
- Overhauled coverage injector to fix issues with non-local tests. - Updated integration tests to work with the new coverage injector. - Fix concurrency issue by using random temp files for delegation. - Fix handling of coverage files from root user. - Fix handling of coverage files without arcs. - Make sure temp copy of injector is world readable and executable.
-rw-r--r--.gitignore1
-rwxr-xr-xtest/integration/targets/ansible/runme.sh12
l---------test/runner/injector/cover1
l---------test/runner/injector/cover21
l---------test/runner/injector/cover2.41
l---------test/runner/injector/cover2.61
l---------test/runner/injector/cover2.71
l---------test/runner/injector/cover31
l---------test/runner/injector/cover3.51
l---------test/runner/injector/cover3.61
-rwxr-xr-xtest/runner/injector/injector.py175
l---------test/runner/injector/runner1
l---------test/runner/injector/runner21
l---------test/runner/injector/runner2.41
l---------test/runner/injector/runner2.61
l---------test/runner/injector/runner2.71
l---------test/runner/injector/runner31
l---------test/runner/injector/runner3.51
l---------test/runner/injector/runner3.61
-rw-r--r--test/runner/lib/cover.py19
-rw-r--r--test/runner/lib/delegation.py127
-rw-r--r--test/runner/lib/executor.py35
-rw-r--r--test/runner/lib/manage_ci.py14
-rw-r--r--test/runner/lib/sanity.py2
-rw-r--r--test/runner/lib/test.py1
-rwxr-xr-xtest/runner/test.py4
26 files changed, 255 insertions, 151 deletions
diff --git a/.gitignore b/.gitignore
index c09b5cfaab..ece590c4d5 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 f38193d6bc..537351ffb3 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 57241c9557..31bc0cbc37 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 1f9d09cbf2..0000000000
--- 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 6d110a662f..a3747f706b 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<module>.*).py$', '\\g<module>', 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 40721d70cf..1bdad28422 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 e033eef1f2..92f476ec77 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 7f0910a1d3..72b7645fe2 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 a8294dcbbf..ee619cc16a 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 e07e813b4c..e3b8921765 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 873c505404..8371976801 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)