* ansible-test - Backport `InternalError` NOTE: This is a partial backport, including only one new class. (cherry picked from commitb960641759) * ansible-test - Fix subprocess management. (#77641) * Run code-smell sanity tests in UTF-8 Mode. * Update subprocess use in sanity test programs. * Use raw_command instead of run_command with always=True set. * Add more capture=True usage. * Don't expose stdin to subprocesses. * Capture more output. Warn on retry. * Add more captures. * Capture coverage cli output. * Capture windows and network host checks. * Be explicit about interactive usage. * Use a shell for non-captured, non-interactive subprocesses. * Add integration test to assert no TTY. * Add unit test to assert no TTY. * Require blocking stdin/stdout/stderr. * Use subprocess.run in ansible-core sanity tests. * Remove unused arg. * Be explicit with subprocess.run check=False. * Add changelog. * Use a Python subprocess instead of a shell. * Use InternalError instead of Exception. * Require capture argument. * Check for invalid raw_command arguments. * Removed pointless communicate=True usage. * Relocate stdout w/o capture check. * Use threads instead of a subprocess for IO. (cherry picked from commit5c2d830dea) * ansible-test - Add support for remote Ubuntu VMs. (cherry picked from commit6513453310) * ansible-test - Fix remote completion validation. (cherry picked from commite2200e8dfc) * ansible-test - Add multi-arch remote support. (cherry picked from commit2cc74b04c4) * ansible-test - Enhance the shell command. (#77734) * ansible-test - Add shell --export option. * ansible-test - Support cmd args for shell command. Also allow shell to be used without a valid layout if no delegation is required. * ansible-test - Improve stderr/stdout consistency. By default all output goes to stdout only, with the exception of a fatal error. When using any of the following, all output defaults to stderr instead: * sanity with the `--lint` option -- sanity messages to stdout * coverage analyze -- output to stdout if the output file is `/dev/stdout` * shell -- shell output to stdout This fixes issues two main issues: * Unpredictable output order when using both info and error/warning messages. * Mixing of lint/command/shell output with bootstrapping messages on stdout. * ansible-test - Add changelog fragment. (cherry picked from commitfe349a1ccd) * ansible-test - Fix remote args restriction. The platform-specific and global fallbacks were not working with the `--remote` option. This regression was introduced by https://github.com/ansible/ansible/pull/77711 (cherry picked from commit76ead1e768)
199 lines
5.5 KiB
Python
199 lines
5.5 KiB
Python
"""Utility code for facilitating collection of code coverage when running tests."""
|
|
from __future__ import annotations
|
|
|
|
import atexit
|
|
import os
|
|
import tempfile
|
|
import typing as t
|
|
|
|
from .config import (
|
|
IntegrationConfig,
|
|
SanityConfig,
|
|
TestConfig,
|
|
)
|
|
|
|
from .io import (
|
|
write_text_file,
|
|
make_dirs,
|
|
)
|
|
|
|
from .util import (
|
|
COVERAGE_CONFIG_NAME,
|
|
remove_tree,
|
|
sanitize_host_name,
|
|
)
|
|
|
|
from .data import (
|
|
data_context,
|
|
)
|
|
|
|
from .util_common import (
|
|
intercept_python,
|
|
ResultType,
|
|
)
|
|
|
|
from .host_configs import (
|
|
DockerConfig,
|
|
HostConfig,
|
|
OriginConfig,
|
|
PosixRemoteConfig,
|
|
PosixSshConfig,
|
|
PythonConfig,
|
|
)
|
|
|
|
|
|
def cover_python(
|
|
args, # type: TestConfig
|
|
python, # type: PythonConfig
|
|
cmd, # type: t.List[str]
|
|
target_name, # type: str
|
|
env, # type: t.Dict[str, str]
|
|
capture, # type: bool
|
|
data=None, # type: t.Optional[str]
|
|
cwd=None, # type: t.Optional[str]
|
|
): # type: (...) -> t.Tuple[t.Optional[str], t.Optional[str]]
|
|
"""Run a command while collecting Python code coverage."""
|
|
if args.coverage:
|
|
env.update(get_coverage_environment(args, target_name, python.version))
|
|
|
|
return intercept_python(args, python, cmd, env, capture, data, cwd)
|
|
|
|
|
|
def get_coverage_platform(config): # type: (HostConfig) -> str
|
|
"""Return the platform label for the given host config."""
|
|
if isinstance(config, PosixRemoteConfig):
|
|
platform = f'remote-{sanitize_host_name(config.name)}'
|
|
elif isinstance(config, DockerConfig):
|
|
platform = f'docker-{sanitize_host_name(config.name)}'
|
|
elif isinstance(config, PosixSshConfig):
|
|
platform = f'ssh-{sanitize_host_name(config.host)}'
|
|
elif isinstance(config, OriginConfig):
|
|
platform = 'origin' # previous versions of ansible-test used "local-{python_version}"
|
|
else:
|
|
raise NotImplementedError(f'Coverage platform label not defined for type: {type(config)}')
|
|
|
|
return platform
|
|
|
|
|
|
def get_coverage_environment(
|
|
args, # type: TestConfig
|
|
target_name, # type: str
|
|
version, # type: str
|
|
): # type: (...) -> t.Dict[str, str]
|
|
"""Return environment variables needed to collect code coverage."""
|
|
# unit tests, sanity tests and other special cases (localhost only)
|
|
# config is in a temporary directory
|
|
# results are in the source tree
|
|
config_file = get_coverage_config(args)
|
|
coverage_name = '='.join((args.command, target_name, get_coverage_platform(args.controller), f'python-{version}', 'coverage'))
|
|
coverage_dir = os.path.join(data_context().content.root, data_context().content.results_path, ResultType.COVERAGE.name)
|
|
coverage_file = os.path.join(coverage_dir, coverage_name)
|
|
|
|
make_dirs(coverage_dir)
|
|
|
|
if args.coverage_check:
|
|
# cause the 'coverage' module to be found, but not imported or enabled
|
|
coverage_file = ''
|
|
|
|
# Enable code coverage collection on local Python programs (this does not include Ansible modules).
|
|
# Used by the injectors to support code coverage.
|
|
# Used by the pytest unit test plugin to support code coverage.
|
|
# The COVERAGE_FILE variable is also used directly by the 'coverage' module.
|
|
env = dict(
|
|
COVERAGE_CONF=config_file,
|
|
COVERAGE_FILE=coverage_file,
|
|
)
|
|
|
|
return env
|
|
|
|
|
|
def get_coverage_config(args): # type: (TestConfig) -> str
|
|
"""Return the path to the coverage config, creating the config if it does not already exist."""
|
|
try:
|
|
return get_coverage_config.path # type: ignore[attr-defined]
|
|
except AttributeError:
|
|
pass
|
|
|
|
coverage_config = generate_coverage_config(args)
|
|
|
|
if args.explain:
|
|
temp_dir = '/tmp/coverage-temp-dir'
|
|
else:
|
|
temp_dir = tempfile.mkdtemp()
|
|
atexit.register(lambda: remove_tree(temp_dir))
|
|
|
|
path = os.path.join(temp_dir, COVERAGE_CONFIG_NAME)
|
|
|
|
if not args.explain:
|
|
write_text_file(path, coverage_config)
|
|
|
|
get_coverage_config.path = path # type: ignore[attr-defined]
|
|
|
|
return path
|
|
|
|
|
|
def generate_coverage_config(args): # type: (TestConfig) -> str
|
|
"""Generate code coverage configuration for tests."""
|
|
if data_context().content.collection:
|
|
coverage_config = generate_collection_coverage_config(args)
|
|
else:
|
|
coverage_config = generate_ansible_coverage_config()
|
|
|
|
return coverage_config
|
|
|
|
|
|
def generate_ansible_coverage_config(): # type: () -> str
|
|
"""Generate code coverage configuration for Ansible tests."""
|
|
coverage_config = '''
|
|
[run]
|
|
branch = True
|
|
concurrency = multiprocessing
|
|
parallel = True
|
|
|
|
omit =
|
|
*/python*/dist-packages/*
|
|
*/python*/site-packages/*
|
|
*/python*/distutils/*
|
|
*/pyshared/*
|
|
*/pytest
|
|
*/AnsiballZ_*.py
|
|
*/test/results/*
|
|
'''
|
|
|
|
return coverage_config
|
|
|
|
|
|
def generate_collection_coverage_config(args): # type: (TestConfig) -> str
|
|
"""Generate code coverage configuration for Ansible Collection tests."""
|
|
coverage_config = '''
|
|
[run]
|
|
branch = True
|
|
concurrency = multiprocessing
|
|
parallel = True
|
|
disable_warnings =
|
|
no-data-collected
|
|
'''
|
|
|
|
if isinstance(args, IntegrationConfig):
|
|
coverage_config += '''
|
|
include =
|
|
%s/*
|
|
*/%s/*
|
|
''' % (data_context().content.root, data_context().content.collection.directory)
|
|
elif isinstance(args, SanityConfig):
|
|
# temporary work-around for import sanity test
|
|
coverage_config += '''
|
|
include =
|
|
%s/*
|
|
|
|
omit =
|
|
%s/*
|
|
''' % (data_context().content.root, os.path.join(data_context().content.root, data_context().content.results_path))
|
|
else:
|
|
coverage_config += '''
|
|
include =
|
|
%s/*
|
|
''' % data_context().content.root
|
|
|
|
return coverage_config
|