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:
Matt Clay 2017-05-11 13:25:02 +08:00
commit dfd19a812f
26 changed files with 259 additions and 155 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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