diff --git a/changelogs/fragments/ansible-test-coverage-delegation.yml b/changelogs/fragments/ansible-test-coverage-delegation.yml new file mode 100644 index 00000000000..106fb0d7143 --- /dev/null +++ b/changelogs/fragments/ansible-test-coverage-delegation.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-test - The ``ansible-test coverage`` commands ``combine``, ``report``, ``html`` and ``xml`` now support delegation. diff --git a/test/lib/ansible_test/_internal/cli.py b/test/lib/ansible_test/_internal/cli.py index 4d547ade58b..2cc78f52362 100644 --- a/test/lib/ansible_test/_internal/cli.py +++ b/test/lib/ansible_test/_internal/cli.py @@ -108,14 +108,17 @@ from .util_common import ( from .commands.coverage.combine import ( command_coverage_combine, + CoverageCombineConfig, ) from .commands.coverage.erase import ( command_coverage_erase, + CoverageEraseConfig, ) from .commands.coverage.html import ( command_coverage_html, + CoverageHtmlConfig, ) from .commands.coverage.report import ( @@ -125,6 +128,7 @@ from .commands.coverage.report import ( from .commands.coverage.xml import ( command_coverage_xml, + CoverageXmlConfig, ) from .commands.coverage.analyze.targets.generate import ( @@ -154,7 +158,6 @@ from .commands.coverage.analyze.targets.missing import ( from .commands.coverage import ( COVERAGE_GROUPS, - CoverageConfig, ) if t.TYPE_CHECKING: @@ -579,6 +582,10 @@ def parse_args(): add_environments(coverage_common, argparse, isolated_delegation=False) + coverage_common_isolated_delegation = argparse.ArgumentParser(add_help=False, parents=[common]) + + add_environments(coverage_common_isolated_delegation, argparse) + coverage = subparsers.add_parser('coverage', help='code coverage management and reporting') @@ -588,11 +595,11 @@ def parse_args(): add_coverage_analyze(coverage_subparsers, coverage_common) coverage_combine = coverage_subparsers.add_parser('combine', - parents=[coverage_common], + parents=[coverage_common_isolated_delegation], help='combine coverage data and rewrite remote paths') coverage_combine.set_defaults(func=command_coverage_combine, - config=CoverageConfig) + config=CoverageCombineConfig) coverage_combine.add_argument('--export', help='directory to export combined coverage files to') @@ -604,10 +611,10 @@ def parse_args(): help='erase coverage data files') coverage_erase.set_defaults(func=command_coverage_erase, - config=CoverageConfig) + config=CoverageEraseConfig) coverage_report = coverage_subparsers.add_parser('report', - parents=[coverage_common], + parents=[coverage_common_isolated_delegation], help='generate console coverage report') coverage_report.set_defaults(func=command_coverage_report, @@ -629,20 +636,20 @@ def parse_args(): add_extra_coverage_options(coverage_report) coverage_html = coverage_subparsers.add_parser('html', - parents=[coverage_common], + parents=[coverage_common_isolated_delegation], help='generate html coverage report') coverage_html.set_defaults(func=command_coverage_html, - config=CoverageConfig) + config=CoverageHtmlConfig) add_extra_coverage_options(coverage_html) coverage_xml = coverage_subparsers.add_parser('xml', - parents=[coverage_common], + parents=[coverage_common_isolated_delegation], help='generate xml coverage report') coverage_xml.set_defaults(func=command_coverage_xml, - config=CoverageConfig) + config=CoverageXmlConfig) add_extra_coverage_options(coverage_xml) diff --git a/test/lib/ansible_test/_internal/commands/coverage/__init__.py b/test/lib/ansible_test/_internal/commands/coverage/__init__.py index e21c302486e..940dd2e325d 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/__init__.py +++ b/test/lib/ansible_test/_internal/commands/coverage/__init__.py @@ -59,12 +59,6 @@ class CoverageConfig(EnvironmentConfig): def __init__(self, args): # type: (t.Any) -> None super(CoverageConfig, self).__init__(args, 'coverage') - self.group_by = frozenset(args.group_by) if 'group_by' in args and args.group_by else set() # type: t.FrozenSet[str] - self.all = args.all if 'all' in args else False # type: bool - self.stub = args.stub if 'stub' in args else False # type: bool - self.export = args.export if 'export' in args else None # type: str - self.coverage = False # temporary work-around to support intercept_command in cover.py - def initialize_coverage(args): # type: (CoverageConfig) -> coverage_module """Delegate execution if requested, install requirements, then import and return the coverage module. Raises an exception if coverage is not available.""" @@ -111,6 +105,11 @@ def run_coverage(args, output_file, command, cmd): # type: (CoverageConfig, str intercept_command(args, target_name='coverage', env=env, cmd=cmd, disable_coverage=True) +def get_all_coverage_files(): # type: () -> t.List[str] + """Return a list of all coverage file paths.""" + return get_python_coverage_files() + get_powershell_coverage_files() + + def get_python_coverage_files(path=None): # type: (t.Optional[str]) -> t.List[str] """Return the list of Python coverage file paths.""" return get_coverage_files('python', path) diff --git a/test/lib/ansible_test/_internal/commands/coverage/combine.py b/test/lib/ansible_test/_internal/commands/coverage/combine.py index f163dff76b6..0dd6f0fa873 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/combine.py +++ b/test/lib/ansible_test/_internal/commands/coverage/combine.py @@ -5,6 +5,8 @@ __metaclass__ = type import os import json +from ... import types as t + from ...target import ( walk_compile_targets, walk_powershell_targets, @@ -17,7 +19,7 @@ from ...io import ( from ...util import ( ANSIBLE_TEST_DATA_ROOT, display, - find_executable, + ApplicationError, ) from ...util_common import ( @@ -27,10 +29,19 @@ from ...util_common import ( write_json_test_results, ) +from ...executor import ( + Delegate, +) + +from ...data import ( + data_context, +) + from . import ( enumerate_python_arcs, enumerate_powershell_lines, get_collection_path_regexes, + get_all_coverage_files, get_python_coverage_files, get_python_modules, get_powershell_coverage_files, @@ -44,9 +55,28 @@ from . import ( def command_coverage_combine(args): """Patch paths in coverage files and merge into a single file. - :type args: CoverageConfig + :type args: CoverageCombineConfig :rtype: list[str] """ + if args.delegate: + if args.docker or args.remote: + paths = get_all_coverage_files() + exported_paths = [path for path in paths if path.endswith('=coverage.combined')] + + if not exported_paths: + raise ExportedCoverageDataNotFound() + + pairs = [(path, os.path.relpath(path, data_context().content.root)) for path in exported_paths] + + def coverage_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None + """Add the coverage files to the payload file list.""" + display.info('Including %d exported coverage file(s) in payload.' % len(pairs), verbosity=1) + files.extend(pairs) + + data_context().register_payload_callback(coverage_callback) + + raise Delegate() + paths = _command_coverage_combine_powershell(args) + _command_coverage_combine_python(args) for path in paths: @@ -55,9 +85,18 @@ def command_coverage_combine(args): return paths +class ExportedCoverageDataNotFound(ApplicationError): + """Exception when no combined coverage data is present yet is required.""" + def __init__(self): + super(ExportedCoverageDataNotFound, self).__init__( + 'Coverage data must be exported before processing with the `--docker` or `--remote` option.\n' + 'Export coverage with `ansible-test coverage combine` using the `--export` option.\n' + 'The exported files must be in the directory: %s/' % ResultType.COVERAGE.relative_path) + + def _command_coverage_combine_python(args): """ - :type args: CoverageConfig + :type args: CoverageCombineConfig :rtype: list[str] """ coverage = initialize_coverage(args) @@ -136,7 +175,7 @@ def _command_coverage_combine_python(args): def _command_coverage_combine_powershell(args): """ - :type args: CoverageConfig + :type args: CoverageCombineConfig :rtype: list[str] """ coverage_files = get_powershell_coverage_files() @@ -217,7 +256,7 @@ def _command_coverage_combine_powershell(args): def _get_coverage_targets(args, walk_func): """ - :type args: CoverageConfig + :type args: CoverageCombineConfig :type walk_func: Func :rtype: list[tuple[str, int]] """ @@ -240,7 +279,7 @@ def _get_coverage_targets(args, walk_func): def _build_stub_groups(args, sources, default_stub_value): """ - :type args: CoverageConfig + :type args: CoverageCombineConfig :type sources: List[tuple[str, int]] :type default_stub_value: Func[List[str]] :rtype: dict @@ -273,7 +312,7 @@ def _build_stub_groups(args, sources, default_stub_value): def get_coverage_group(args, coverage_file): """ - :type args: CoverageConfig + :type args: CoverageCombineConfig :type coverage_file: str :rtype: str """ @@ -306,3 +345,16 @@ def get_coverage_group(args, coverage_file): group = group.lstrip('=') return group + + +class CoverageCombineConfig(CoverageConfig): + """Configuration for the coverage combine command.""" + def __init__(self, args): # type: (t.Any) -> None + super(CoverageCombineConfig, self).__init__(args) + + self.group_by = frozenset(args.group_by) if args.group_by else frozenset() # type: t.FrozenSet[str] + self.all = args.all # type: bool + self.stub = args.stub # type: bool + + # only available to coverage combine + self.export = args.export if 'export' in args else False # type: str diff --git a/test/lib/ansible_test/_internal/commands/coverage/erase.py b/test/lib/ansible_test/_internal/commands/coverage/erase.py index 7ba9608dfbc..7a41f56f470 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/erase.py +++ b/test/lib/ansible_test/_internal/commands/coverage/erase.py @@ -13,7 +13,7 @@ from . import ( ) -def command_coverage_erase(args): # type: (CoverageConfig) -> None +def command_coverage_erase(args): # type: (CoverageEraseConfig) -> None """Erase code coverage data files collected during test runs.""" coverage_dir = ResultType.COVERAGE.path @@ -25,3 +25,7 @@ def command_coverage_erase(args): # type: (CoverageConfig) -> None if not args.explain: os.remove(path) + + +class CoverageEraseConfig(CoverageConfig): + """Configuration for the coverage erase command.""" diff --git a/test/lib/ansible_test/_internal/commands/coverage/html.py b/test/lib/ansible_test/_internal/commands/coverage/html.py index 717f237d0e6..b34e1ef4ecb 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/html.py +++ b/test/lib/ansible_test/_internal/commands/coverage/html.py @@ -18,17 +18,17 @@ from ...util_common import ( from .combine import ( command_coverage_combine, + CoverageCombineConfig, ) from . import ( run_coverage, - CoverageConfig, ) def command_coverage_html(args): """ - :type args: CoverageConfig + :type args: CoverageHtmlConfig """ output_files = command_coverage_combine(args) @@ -43,3 +43,7 @@ def command_coverage_html(args): run_coverage(args, output_file, 'html', ['-i', '-d', dir_name]) display.info('HTML report generated: file:///%s' % os.path.join(dir_name, 'index.html')) + + +class CoverageHtmlConfig(CoverageCombineConfig): + """Configuration for the coverage html command.""" diff --git a/test/lib/ansible_test/_internal/commands/coverage/report.py b/test/lib/ansible_test/_internal/commands/coverage/report.py index ae13b0db500..498d543403f 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/report.py +++ b/test/lib/ansible_test/_internal/commands/coverage/report.py @@ -18,11 +18,11 @@ from ...data import ( from .combine import ( command_coverage_combine, + CoverageCombineConfig, ) from . import ( run_coverage, - CoverageConfig, ) @@ -143,7 +143,7 @@ def _generate_powershell_output_report(args, coverage_file): return report -class CoverageReportConfig(CoverageConfig): +class CoverageReportConfig(CoverageCombineConfig): """Configuration for the coverage report command.""" def __init__(self, args): """ diff --git a/test/lib/ansible_test/_internal/commands/coverage/xml.py b/test/lib/ansible_test/_internal/commands/coverage/xml.py index b02520ec5b9..2296ef61c28 100644 --- a/test/lib/ansible_test/_internal/commands/coverage/xml.py +++ b/test/lib/ansible_test/_internal/commands/coverage/xml.py @@ -36,17 +36,17 @@ from ...data import ( from .combine import ( command_coverage_combine, + CoverageCombineConfig, ) from . import ( run_coverage, - CoverageConfig, ) def command_coverage_xml(args): """ - :type args: CoverageConfig + :type args: CoverageXmlConfig """ output_files = command_coverage_combine(args) @@ -189,3 +189,7 @@ def _add_cobertura_package(packages, package_name, package_data): }) return total_lines_hit, total_line_count + + +class CoverageXmlConfig(CoverageCombineConfig): + """Configuration for the coverage xml command."""