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
|
# Release directory
|
||||||
packaging/release/ansible_release
|
packaging/release/ansible_release
|
||||||
/.cache/
|
/.cache/
|
||||||
|
/test/results/coverage/*=coverage.*
|
||||||
/test/results/coverage/coverage*
|
/test/results/coverage/coverage*
|
||||||
/test/results/reports/coverage.xml
|
/test/results/reports/coverage.xml
|
||||||
/test/results/reports/coverage/
|
/test/results/reports/coverage/
|
||||||
|
|
|
@ -2,12 +2,8 @@
|
||||||
|
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
env
|
|
||||||
|
|
||||||
which python
|
|
||||||
python --version
|
|
||||||
|
|
||||||
which ansible
|
|
||||||
ansible --version
|
ansible --version
|
||||||
ansible testhost -i ../../inventory -vvv -e "ansible_python_interpreter=$(which python)" -m ping
|
ansible --help
|
||||||
ansible testhost -i ../../inventory -vvv -e "ansible_python_interpreter=$(which python)" -m setup
|
|
||||||
|
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
|
#!/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
|
from __future__ import absolute_import, print_function
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import pipes
|
import pipes
|
||||||
|
@ -11,10 +34,45 @@ import logging
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
logger = logging.getLogger('injector') # pylint: disable=locally-disabled, invalid-name
|
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():
|
def main():
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
|
global config # pylint: disable=locally-disabled, global-statement
|
||||||
|
|
||||||
formatter = logging.Formatter('%(asctime)s %(process)d %(levelname)s %(message)s')
|
formatter = logging.Formatter('%(asctime)s %(process)d %(levelname)s %(message)s')
|
||||||
log_name = 'ansible-test-coverage.%s.log' % getpass.getuser()
|
log_name = 'ansible-test-coverage.%s.log' % getpass.getuser()
|
||||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
@ -31,25 +89,49 @@ def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug('Self: %s', __file__)
|
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'):
|
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'injector.json')
|
||||||
args, env = runner()
|
|
||||||
elif os.path.basename(__file__).startswith('cover'):
|
try:
|
||||||
args, env = cover()
|
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:
|
else:
|
||||||
args, env = injector()
|
args, env = injector()
|
||||||
|
|
||||||
logger.debug('Run command: %s', ' '.join(pipes.quote(c) for c in args))
|
logger.debug('Run command: %s', ' '.join(pipes.quote(c) for c in args))
|
||||||
|
|
||||||
|
altered_cwd = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
except OSError as ex:
|
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:
|
if ex.errno != errno.EACCES:
|
||||||
raise
|
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()):
|
for key in sorted(env.keys()):
|
||||||
logger.debug('%s=%s', key, env[key])
|
logger.debug('%s=%s', key, env[key])
|
||||||
|
@ -64,29 +146,28 @@ def injector():
|
||||||
"""
|
"""
|
||||||
:rtype: list[str], dict[str, str]
|
:rtype: list[str], dict[str, str]
|
||||||
"""
|
"""
|
||||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
command = os.path.basename(__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)
|
executable = find_executable(command)
|
||||||
|
|
||||||
if mode in ('coverage', 'version'):
|
if config.coverage_file:
|
||||||
if mode == 'coverage':
|
args, env = coverage_command()
|
||||||
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]
|
|
||||||
else:
|
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
|
return args, env
|
||||||
|
|
||||||
|
@ -95,61 +176,53 @@ def runner():
|
||||||
"""
|
"""
|
||||||
:rtype: list[str], dict[str, str]
|
:rtype: list[str], dict[str, str]
|
||||||
"""
|
"""
|
||||||
command = os.path.basename(__file__)
|
args, env = [config.python_interpreter], os.environ.copy()
|
||||||
version = command.replace('runner', '')
|
|
||||||
|
|
||||||
interpreter = find_executable('python' + version)
|
args += config.arguments[1:]
|
||||||
args, env = [interpreter], os.environ.copy()
|
|
||||||
|
|
||||||
args += sys.argv[1:]
|
|
||||||
|
|
||||||
return args, env
|
return args, env
|
||||||
|
|
||||||
|
|
||||||
def cover():
|
def cover():
|
||||||
"""
|
"""
|
||||||
:rtype: list[str], dict[str, str]
|
:rtype: list[str], dict[str, str], bool
|
||||||
"""
|
"""
|
||||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
if len(config.arguments) > 1:
|
||||||
command = os.path.basename(__file__)
|
executable = config.arguments[1]
|
||||||
version = command.replace('cover', '')
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
executable = sys.argv[1]
|
|
||||||
else:
|
else:
|
||||||
executable = ''
|
executable = ''
|
||||||
|
|
||||||
|
require_cwd = False
|
||||||
|
|
||||||
if os.path.basename(executable).startswith('ansible_module_'):
|
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:
|
else:
|
||||||
interpreter = find_executable('python' + version)
|
args, env = [config.python_interpreter], os.environ.copy()
|
||||||
args, env = [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]
|
:rtype: list[str], dict[str, str]
|
||||||
"""
|
"""
|
||||||
executable = 'coverage'
|
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
if version:
|
|
||||||
executable += '-%s' % version
|
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
find_executable(executable),
|
config.python_interpreter,
|
||||||
|
'-m',
|
||||||
|
'coverage.__main__',
|
||||||
'run',
|
'run',
|
||||||
'--rcfile',
|
'--rcfile',
|
||||||
os.path.join(self_dir, '.coveragerc'),
|
os.path.join(self_dir, '.coveragerc'),
|
||||||
]
|
]
|
||||||
|
|
||||||
env = os.environ.copy()
|
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
|
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()))
|
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)
|
coverage_files = [os.path.join(COVERAGE_DIR, f) for f in os.listdir(COVERAGE_DIR) if '=coverage.' in f]
|
||||||
if f.startswith('coverage') and f != 'coverage']
|
|
||||||
|
|
||||||
arc_data = {}
|
arc_data = {}
|
||||||
|
|
||||||
|
@ -60,7 +59,12 @@ def command_coverage_combine(args):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for filename in original.measured_files():
|
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:
|
if '/ansible_modlib.zip/ansible/' in filename:
|
||||||
new_name = re.sub('^.*/ansible_modlib.zip/ansible/', ansible_path, filename)
|
new_name = re.sub('^.*/ansible_modlib.zip/ansible/', ansible_path, filename)
|
||||||
|
@ -68,11 +72,14 @@ def command_coverage_combine(args):
|
||||||
filename = new_name
|
filename = new_name
|
||||||
elif '/ansible_module_' in filename:
|
elif '/ansible_module_' in filename:
|
||||||
module = re.sub('^.*/ansible_module_(?P<module>.*).py$', '\\g<module>', 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])
|
new_name = os.path.abspath(modules[module])
|
||||||
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
||||||
filename = new_name
|
filename = new_name
|
||||||
elif filename.startswith('/root/ansible/'):
|
elif re.search('^(/.*?)?/root/ansible/', filename):
|
||||||
new_name = re.sub('^/.*?/ansible/', root_path, filename)
|
new_name = re.sub('^(/.*?)?/root/ansible/', root_path, filename)
|
||||||
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
||||||
filename = new_name
|
filename = new_name
|
||||||
|
|
||||||
|
@ -125,7 +132,7 @@ def command_coverage_erase(args):
|
||||||
initialize_coverage(args)
|
initialize_coverage(args)
|
||||||
|
|
||||||
for name in os.listdir(COVERAGE_DIR):
|
for name in os.listdir(COVERAGE_DIR):
|
||||||
if not name.startswith('coverage'):
|
if not name.startswith('coverage') and '=coverage.' not in name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
path = os.path.join(COVERAGE_DIR, name)
|
path = os.path.join(COVERAGE_DIR, name)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import absolute_import, print_function
|
from __future__ import absolute_import, print_function
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
@ -124,6 +125,10 @@ def delegate_tox(args, exclude, require):
|
||||||
if not args.python:
|
if not args.python:
|
||||||
cmd += ['--python', version]
|
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)
|
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)
|
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 isinstance(args, IntegrationConfig):
|
||||||
if not args.allow_destructive:
|
if not args.allow_destructive:
|
||||||
cmd.append('--allow-destructive')
|
cmd.append('--allow-destructive')
|
||||||
|
@ -162,75 +173,77 @@ def delegate_docker(args, exclude, require):
|
||||||
if isinstance(args, ShellConfig):
|
if isinstance(args, ShellConfig):
|
||||||
cmd_options.append('-it')
|
cmd_options.append('-it')
|
||||||
|
|
||||||
if not args.explain:
|
with tempfile.NamedTemporaryFile(prefix='ansible-source-', suffix='.tgz') as local_source_fd:
|
||||||
lib.pytar.create_tarfile('/tmp/ansible.tgz', '.', lib.pytar.ignore)
|
try:
|
||||||
|
if not args.explain:
|
||||||
|
lib.pytar.create_tarfile(local_source_fd.name, '.', lib.pytar.ignore)
|
||||||
|
|
||||||
try:
|
if util_image:
|
||||||
if util_image:
|
util_options = [
|
||||||
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',
|
'--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:
|
if args.explain:
|
||||||
util_id = 'util_id'
|
test_id = 'test_id'
|
||||||
else:
|
else:
|
||||||
util_id = util_id.strip()
|
test_id = test_id.strip()
|
||||||
else:
|
|
||||||
util_id = None
|
|
||||||
|
|
||||||
test_options = [
|
# write temporary files to /root since /tmp isn't ready immediately on container start
|
||||||
'--detach',
|
docker_put(args, test_id, 'test/runner/setup/docker.sh', '/root/docker.sh')
|
||||||
'--volume', '/sys/fs/cgroup:/sys/fs/cgroup:ro',
|
docker_exec(args, test_id, ['/bin/bash', '/root/docker.sh'])
|
||||||
'--privileged=%s' % str(privileged).lower(),
|
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:
|
# docker images are only expected to have a single python version available
|
||||||
test_options += [
|
if isinstance(args, UnitsConfig) and not args.python:
|
||||||
'--link', '%s:ansible.http.tests' % util_id,
|
cmd += ['--python', 'default']
|
||||||
'--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):
|
try:
|
||||||
cloud_platforms = get_cloud_providers(args)
|
docker_exec(args, test_id, cmd, options=cmd_options)
|
||||||
|
finally:
|
||||||
for cloud_platform in cloud_platforms:
|
with tempfile.NamedTemporaryFile(prefix='ansible-result-', suffix='.tgz') as local_result_fd:
|
||||||
test_options += cloud_platform.get_docker_run_options()
|
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)
|
||||||
test_id, _ = docker_run(args, test_image, options=test_options)
|
run_command(args, ['tar', 'oxzf', local_result_fd.name, '-C', 'test'])
|
||||||
|
|
||||||
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)
|
|
||||||
finally:
|
finally:
|
||||||
docker_exec(args, test_id, ['tar', 'czf', '/root/results.tgz', '-C', '/root/ansible/test', 'results'])
|
if util_id:
|
||||||
docker_get(args, test_id, '/root/results.tgz', '/tmp/results.tgz')
|
docker_rm(args, util_id)
|
||||||
run_command(args, ['tar', 'oxzf', '/tmp/results.tgz', '-C', 'test'])
|
|
||||||
finally:
|
|
||||||
if util_id:
|
|
||||||
docker_rm(args, util_id)
|
|
||||||
|
|
||||||
if test_id:
|
if test_id:
|
||||||
docker_rm(args, test_id)
|
docker_rm(args, test_id)
|
||||||
|
|
||||||
|
|
||||||
def delegate_remote(args, exclude, require):
|
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)
|
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 isinstance(args, IntegrationConfig):
|
||||||
if not args.allow_destructive:
|
if not args.allow_destructive:
|
||||||
cmd.append('--allow-destructive')
|
cmd.append('--allow-destructive')
|
||||||
|
|
|
@ -12,7 +12,6 @@ import functools
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import random
|
import random
|
||||||
import pipes
|
|
||||||
import string
|
import string
|
||||||
import atexit
|
import atexit
|
||||||
|
|
||||||
|
@ -607,7 +606,7 @@ def command_integration_script(args, target):
|
||||||
env = integration_environment(args, target, cmd)
|
env = integration_environment(args, target, cmd)
|
||||||
cwd = target.path
|
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):
|
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')
|
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):
|
def command_units(args):
|
||||||
|
@ -723,7 +722,7 @@ def command_units(args):
|
||||||
display.info('Unit test with Python %s' % version)
|
display.info('Unit test with Python %s' % version)
|
||||||
|
|
||||||
try:
|
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:
|
except SubprocessError as ex:
|
||||||
# pytest exits with status code 5 when all tests are skipped, which isn't an error for our use case
|
# pytest exits with status code 5 when all tests are skipped, which isn't an error for our use case
|
||||||
if ex.status != 5:
|
if ex.status != 5:
|
||||||
|
@ -838,7 +837,7 @@ def compile_version(args, python_version, include, exclude):
|
||||||
return TestSuccess(command, test, python_version=python_version)
|
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 args: TestConfig
|
||||||
:type cmd: collections.Iterable[str]
|
: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()
|
env = common_environment()
|
||||||
|
|
||||||
cmd = list(cmd)
|
cmd = list(cmd)
|
||||||
escaped_cmd = ' '.join(pipes.quote(c) for c in cmd)
|
|
||||||
inject_path = get_coverage_path(args)
|
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['PATH'] = inject_path + os.pathsep + env['PATH']
|
||||||
env['ANSIBLE_TEST_COVERAGE'] = 'coverage' if args.coverage else 'version'
|
env['ANSIBLE_TEST_PYTHON_VERSION'] = version
|
||||||
env['ANSIBLE_TEST_PYTHON_VERSION'] = python_version or args.python_version
|
env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = interpreter
|
||||||
env['ANSIBLE_TEST_CMD'] = escaped_cmd
|
|
||||||
|
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)
|
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.copytree(src, os.path.join(coverage_path, 'coverage'))
|
||||||
shutil.copy('.coveragerc', os.path.join(coverage_path, 'coverage', '.coveragerc'))
|
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':
|
for directory in 'output', 'logs':
|
||||||
os.mkdir(os.path.join(coverage_path, directory))
|
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.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]
|
:type command: list[str]
|
||||||
:rtype: 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()
|
return (stdout or '').strip() + (stderr or '').strip()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
from __future__ import absolute_import, print_function
|
from __future__ import absolute_import, print_function
|
||||||
|
|
||||||
|
import os
|
||||||
import pipes
|
import pipes
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
@ -135,11 +137,15 @@ class ManagePosixCI(object):
|
||||||
|
|
||||||
def upload_source(self):
|
def upload_source(self):
|
||||||
"""Upload and extract source."""
|
"""Upload and extract source."""
|
||||||
if not self.core_ci.args.explain:
|
with tempfile.NamedTemporaryFile(prefix='ansible-source-', suffix='.tgz') as local_source_fd:
|
||||||
lib.pytar.create_tarfile('/tmp/ansible.tgz', '.', lib.pytar.ignore)
|
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')
|
if not self.core_ci.args.explain:
|
||||||
self.ssh('rm -rf ~/ansible && mkdir ~/ansible && cd ~/ansible && tar oxzf /tmp/ansible.tgz')
|
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):
|
def download(self, remote, local):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -644,7 +644,7 @@ def command_sanity_ansible_doc(args, targets, python_version):
|
||||||
cmd = ['ansible-doc'] + modules
|
cmd = ['ansible-doc'] + modules
|
||||||
|
|
||||||
try:
|
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
|
status = 0
|
||||||
except SubprocessError as ex:
|
except SubprocessError as ex:
|
||||||
stdout = ex.stdout
|
stdout = ex.stdout
|
||||||
|
|
|
@ -65,6 +65,7 @@ class TestConfig(EnvironmentConfig):
|
||||||
super(TestConfig, self).__init__(args, command)
|
super(TestConfig, self).__init__(args, command)
|
||||||
|
|
||||||
self.coverage = args.coverage # type: bool
|
self.coverage = args.coverage # type: bool
|
||||||
|
self.coverage_label = args.coverage_label # type: str
|
||||||
self.include = args.include # type: list [str]
|
self.include = args.include # type: list [str]
|
||||||
self.exclude = args.exclude # type: list [str]
|
self.exclude = args.exclude # type: list [str]
|
||||||
self.require = args.require # type: list [str]
|
self.require = args.require # type: list [str]
|
||||||
|
|
|
@ -168,6 +168,10 @@ def parse_args():
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='analyze code coverage when running tests')
|
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',
|
test.add_argument('--metadata',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue