mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
548cacdf6a
commit
dfd19a812f
26 changed files with 259 additions and 155 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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 "$@"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
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'
|
||||
else:
|
||||
interpreter = find_executable('python' + version)
|
||||
args, env = [interpreter, executable], os.environ.copy()
|
||||
tool = 'runner'
|
||||
|
||||
if command in ('ansible', 'ansible-playbook', 'ansible-pull'):
|
||||
interpreter = find_executable(tool + version)
|
||||
args += ['--extra-vars', 'ansible_python_interpreter=' + interpreter]
|
||||
if config.coverage_file:
|
||||
args, env = coverage_command()
|
||||
else:
|
||||
args, env = [executable], os.environ.copy()
|
||||
args, env = [config.python_interpreter], os.environ.copy()
|
||||
|
||||
args += sys.argv[1:]
|
||||
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 = config.remote_interpreter
|
||||
|
||||
if interpreter:
|
||||
args += ['--extra-vars', 'ansible_python_interpreter=' + interpreter]
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.py
|
|
@ -1 +0,0 @@
|
|||
injector.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)
|
||||
|
|
|
@ -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 = [
|
||||
if util_image:
|
||||
util_options = [
|
||||
'--detach',
|
||||
]
|
||||
|
||||
util_id, _ = docker_run(args, util_image, options=util_options)
|
||||
|
||||
if args.explain:
|
||||
util_id = 'util_id'
|
||||
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(),
|
||||
]
|
||||
|
||||
util_id, _ = docker_run(args, util_image, options=util_options)
|
||||
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',
|
||||
]
|
||||
|
||||
if isinstance(args, TestConfig):
|
||||
cloud_platforms = get_cloud_providers(args)
|
||||
|
||||
for cloud_platform in cloud_platforms:
|
||||
test_options += cloud_platform.get_docker_run_options()
|
||||
|
||||
test_id, _ = docker_run(args, test_image, options=test_options)
|
||||
|
||||
if args.explain:
|
||||
util_id = 'util_id'
|
||||
test_id = 'test_id'
|
||||
else:
|
||||
util_id = util_id.strip()
|
||||
else:
|
||||
util_id = None
|
||||
test_id = test_id.strip()
|
||||
|
||||
test_options = [
|
||||
'--detach',
|
||||
'--volume', '/sys/fs/cgroup:/sys/fs/cgroup:ro',
|
||||
'--privileged=%s' % str(privileged).lower(),
|
||||
]
|
||||
# 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'])
|
||||
|
||||
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',
|
||||
]
|
||||
# docker images are only expected to have a single python version available
|
||||
if isinstance(args, UnitsConfig) and not args.python:
|
||||
cmd += ['--python', 'default']
|
||||
|
||||
if isinstance(args, TestConfig):
|
||||
cloud_platforms = get_cloud_providers(args)
|
||||
|
||||
for cloud_platform in cloud_platforms:
|
||||
test_options += cloud_platform.get_docker_run_options()
|
||||
|
||||
test_id, _ = docker_run(args, test_image, options=test_options)
|
||||
|
||||
if args.explain:
|
||||
test_id = 'test_id'
|
||||
else:
|
||||
test_id = test_id.strip()
|
||||
|
||||
# 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'])
|
||||
|
||||
# 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)
|
||||
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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue