Overhaul ansible-test code coverage and injector. (#53510)

This commit is contained in:
Matt Clay 2019-03-13 07:14:12 -07:00 committed by GitHub
parent 3bdbe24861
commit a8e328f474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 253 additions and 370 deletions

View file

@ -39,13 +39,30 @@ except ImportError:
from configparser import ConfigParser
DOCKER_COMPLETION = {}
COVERAGE_PATHS = {} # type: dict[str, str]
PYTHON_PATHS = {} # type: dict[str, str]
try:
MAXFD = subprocess.MAXFD
except AttributeError:
MAXFD = -1
COVERAGE_CONFIG_PATH = '.coveragerc'
COVERAGE_OUTPUT_PATH = 'coverage'
# Modes are set to allow all users the same level of access.
# This permits files to be used in tests that change users.
# The only exception is write access to directories for the user creating them.
# This avoids having to modify the directory permissions a second time.
MODE_READ = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
MODE_FILE = MODE_READ
MODE_FILE_EXECUTE = MODE_FILE | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
MODE_FILE_WRITE = MODE_FILE | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
MODE_DIRECTORY = MODE_READ | stat.S_IWUSR | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
MODE_DIRECTORY_WRITE = MODE_DIRECTORY | stat.S_IWGRP | stat.S_IWOTH
def get_docker_completion():
"""
@ -107,6 +124,83 @@ def read_lines_without_comments(path, remove_blank_lines=False):
return lines
def get_python_path(args, interpreter):
"""
:type args: TestConfig
:type interpreter: str
:rtype: str
"""
python_path = PYTHON_PATHS.get(interpreter)
if python_path:
return python_path
prefix = 'python-'
suffix = '-ansible'
root_temp_dir = '/tmp'
if args.explain:
return os.path.join(root_temp_dir, ''.join((prefix, 'temp', suffix)))
python_path = tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=root_temp_dir)
os.chmod(python_path, MODE_DIRECTORY)
os.symlink(interpreter, os.path.join(python_path, 'python'))
if not PYTHON_PATHS:
atexit.register(cleanup_python_paths)
PYTHON_PATHS[interpreter] = python_path
return python_path
def cleanup_python_paths():
"""Clean up all temporary python directories."""
for path in sorted(PYTHON_PATHS.values()):
display.info('Cleaning up temporary python directory: %s' % path, verbosity=2)
shutil.rmtree(path)
def get_coverage_environment(args, target_name, version, temp_path):
"""
:type args: TestConfig
:type target_name: str
:type version: str
:type temp_path: str
:rtype: dict[str, str]
"""
if temp_path:
# integration tests (both localhost and the optional testhost)
# config and results are in a temporary directory
coverage_config_base_path = temp_path
coverage_output_base_path = temp_path
else:
# unit tests, sanity tests and other special cases (localhost only)
# config and results are in the source tree
coverage_config_base_path = os.getcwd()
coverage_output_base_path = os.path.abspath(os.path.join('test/results'))
config_file = os.path.join(coverage_config_base_path, COVERAGE_CONFIG_PATH)
coverage_file = os.path.join(coverage_output_base_path, COVERAGE_OUTPUT_PATH, '%s=%s=%s=%s=coverage' % (
args.command, target_name, args.coverage_label or 'local-%s' % version, 'python-%s' % version))
if args.coverage_check:
coverage_file = ''
env = dict(
# both AnsiballZ and the ansible-test coverage injector rely on this
_ANSIBLE_COVERAGE_CONFIG=config_file,
# used during AnsiballZ wrapper creation to set COVERAGE_FILE for the module
_ANSIBLE_COVERAGE_OUTPUT=coverage_file,
# handle cases not covered by the AnsiballZ wrapper creation above
COVERAGE_FILE=coverage_file,
)
return env
def find_executable(executable, cwd=None, path=None, required=True):
"""
:type executable: str
@ -183,18 +277,19 @@ def generate_pip_command(python):
return [python, '-m', 'pip.__main__']
def intercept_command(args, cmd, target_name, capture=False, env=None, data=None, cwd=None, python_version=None, path=None, coverage=None):
def intercept_command(args, cmd, target_name, env, capture=False, data=None, cwd=None, python_version=None, temp_path=None, coverage=None, virtualenv=None):
"""
:type args: TestConfig
:type cmd: collections.Iterable[str]
:type target_name: str
:type env: dict[str, str]
:type capture: bool
:type env: dict[str, str] | None
:type data: str | None
:type cwd: str | None
:type python_version: str | None
:type path: str | None
:type temp_path: str | None
:type coverage: bool | None
:type virtualenv: str | None
:rtype: str | None, str | None
"""
if not env:
@ -205,108 +300,26 @@ def intercept_command(args, cmd, target_name, capture=False, env=None, data=None
cmd = list(cmd)
version = python_version or args.python_version
interpreter = find_python(version, path)
inject_path = get_coverage_path(args, interpreter)
config_path = os.path.join(inject_path, 'injector.json')
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, 'python-%s' % version)))
interpreter = virtualenv or find_python(version)
inject_path = os.path.abspath('test/runner/injector')
if args.coverage_check:
coverage_file = ''
if not virtualenv:
# injection of python into the path is required when not activating a virtualenv
# otherwise scripts may find the wrong interpreter or possibly no interpreter
python_path = get_python_path(args, interpreter)
inject_path = python_path + os.path.pathsep + inject_path
env['PATH'] = inject_path + os.path.pathsep + env['PATH']
env['ANSIBLE_TEST_PYTHON_VERSION'] = version
env['ANSIBLE_TEST_PYTHON_INTERPRETER'] = interpreter
if coverage:
env['_ANSIBLE_COVERAGE_CONFIG'] = os.path.join(inject_path, '.coveragerc')
env['_ANSIBLE_COVERAGE_OUTPUT'] = coverage_file
config = dict(
python_interpreter=interpreter,
coverage_file=coverage_file if 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)
# add the necessary environment variables to enable code coverage collection
env.update(get_coverage_environment(args, target_name, version, temp_path))
return run_command(args, cmd, capture=capture, env=env, data=data, cwd=cwd)
def get_coverage_path(args, interpreter):
"""
:type args: TestConfig
:type interpreter: str
:rtype: str
"""
coverage_path = COVERAGE_PATHS.get(interpreter)
if coverage_path:
return os.path.join(coverage_path, 'coverage')
prefix = 'ansible-test-coverage-'
tmp_dir = '/tmp'
if args.explain:
return os.path.join(tmp_dir, '%stmp' % prefix, 'coverage')
src = os.path.abspath(os.path.join(os.getcwd(), 'test/runner/injector/'))
coverage_path = tempfile.mkdtemp('', prefix, dir=tmp_dir)
os.chmod(coverage_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
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)
os.symlink(interpreter, os.path.join(coverage_path, 'coverage', 'python'))
if not COVERAGE_PATHS:
atexit.register(cleanup_coverage_dirs)
COVERAGE_PATHS[interpreter] = coverage_path
return os.path.join(coverage_path, 'coverage')
def cleanup_coverage_dirs():
"""Clean up all coverage directories."""
for path in COVERAGE_PATHS.values():
display.info('Cleaning up coverage directory: %s' % path, verbosity=2)
cleanup_coverage_dir(path)
def cleanup_coverage_dir(coverage_path):
"""Copy over coverage data from temporary directory and purge temporary directory.
:type coverage_path: str
"""
output_dir = os.path.join(coverage_path, 'output')
for filename in os.listdir(output_dir):
src = os.path.join(output_dir, filename)
dst = os.path.join(os.getcwd(), 'test', 'results', 'coverage')
shutil.copy(src, dst)
logs_dir = os.path.join(coverage_path, 'logs')
for filename in os.listdir(logs_dir):
random_suffix = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
new_name = '%s.%s.log' % (os.path.splitext(os.path.basename(filename))[0], random_suffix)
src = os.path.join(logs_dir, filename)
dst = os.path.join(os.getcwd(), 'test', 'results', 'logs', new_name)
shutil.copy(src, dst)
shutil.rmtree(coverage_path)
def run_command(args, cmd, capture=False, env=None, data=None, cwd=None, always=False, stdin=None, stdout=None,
cmd_verbosity=1, str_errors='strict'):
"""