Expand delegation options for coverage commands.

This commit is contained in:
Matt Clay 2021-05-14 17:19:40 -07:00
parent e875e91363
commit ce04056797
8 changed files with 101 additions and 29 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- ansible-test - The ``ansible-test coverage`` commands ``combine``, ``report``, ``html`` and ``xml`` now support delegation.

View file

@ -108,14 +108,17 @@ from .util_common import (
from .commands.coverage.combine import ( from .commands.coverage.combine import (
command_coverage_combine, command_coverage_combine,
CoverageCombineConfig,
) )
from .commands.coverage.erase import ( from .commands.coverage.erase import (
command_coverage_erase, command_coverage_erase,
CoverageEraseConfig,
) )
from .commands.coverage.html import ( from .commands.coverage.html import (
command_coverage_html, command_coverage_html,
CoverageHtmlConfig,
) )
from .commands.coverage.report import ( from .commands.coverage.report import (
@ -125,6 +128,7 @@ from .commands.coverage.report import (
from .commands.coverage.xml import ( from .commands.coverage.xml import (
command_coverage_xml, command_coverage_xml,
CoverageXmlConfig,
) )
from .commands.coverage.analyze.targets.generate import ( from .commands.coverage.analyze.targets.generate import (
@ -154,7 +158,6 @@ from .commands.coverage.analyze.targets.missing import (
from .commands.coverage import ( from .commands.coverage import (
COVERAGE_GROUPS, COVERAGE_GROUPS,
CoverageConfig,
) )
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@ -579,6 +582,10 @@ def parse_args():
add_environments(coverage_common, argparse, isolated_delegation=False) 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', coverage = subparsers.add_parser('coverage',
help='code coverage management and reporting') help='code coverage management and reporting')
@ -588,11 +595,11 @@ def parse_args():
add_coverage_analyze(coverage_subparsers, coverage_common) add_coverage_analyze(coverage_subparsers, coverage_common)
coverage_combine = coverage_subparsers.add_parser('combine', coverage_combine = coverage_subparsers.add_parser('combine',
parents=[coverage_common], parents=[coverage_common_isolated_delegation],
help='combine coverage data and rewrite remote paths') help='combine coverage data and rewrite remote paths')
coverage_combine.set_defaults(func=command_coverage_combine, coverage_combine.set_defaults(func=command_coverage_combine,
config=CoverageConfig) config=CoverageCombineConfig)
coverage_combine.add_argument('--export', coverage_combine.add_argument('--export',
help='directory to export combined coverage files to') help='directory to export combined coverage files to')
@ -604,10 +611,10 @@ def parse_args():
help='erase coverage data files') help='erase coverage data files')
coverage_erase.set_defaults(func=command_coverage_erase, coverage_erase.set_defaults(func=command_coverage_erase,
config=CoverageConfig) config=CoverageEraseConfig)
coverage_report = coverage_subparsers.add_parser('report', coverage_report = coverage_subparsers.add_parser('report',
parents=[coverage_common], parents=[coverage_common_isolated_delegation],
help='generate console coverage report') help='generate console coverage report')
coverage_report.set_defaults(func=command_coverage_report, coverage_report.set_defaults(func=command_coverage_report,
@ -629,20 +636,20 @@ def parse_args():
add_extra_coverage_options(coverage_report) add_extra_coverage_options(coverage_report)
coverage_html = coverage_subparsers.add_parser('html', coverage_html = coverage_subparsers.add_parser('html',
parents=[coverage_common], parents=[coverage_common_isolated_delegation],
help='generate html coverage report') help='generate html coverage report')
coverage_html.set_defaults(func=command_coverage_html, coverage_html.set_defaults(func=command_coverage_html,
config=CoverageConfig) config=CoverageHtmlConfig)
add_extra_coverage_options(coverage_html) add_extra_coverage_options(coverage_html)
coverage_xml = coverage_subparsers.add_parser('xml', coverage_xml = coverage_subparsers.add_parser('xml',
parents=[coverage_common], parents=[coverage_common_isolated_delegation],
help='generate xml coverage report') help='generate xml coverage report')
coverage_xml.set_defaults(func=command_coverage_xml, coverage_xml.set_defaults(func=command_coverage_xml,
config=CoverageConfig) config=CoverageXmlConfig)
add_extra_coverage_options(coverage_xml) add_extra_coverage_options(coverage_xml)

View file

@ -59,12 +59,6 @@ class CoverageConfig(EnvironmentConfig):
def __init__(self, args): # type: (t.Any) -> None def __init__(self, args): # type: (t.Any) -> None
super(CoverageConfig, self).__init__(args, 'coverage') 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 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.""" """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) 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] def get_python_coverage_files(path=None): # type: (t.Optional[str]) -> t.List[str]
"""Return the list of Python coverage file paths.""" """Return the list of Python coverage file paths."""
return get_coverage_files('python', path) return get_coverage_files('python', path)

View file

@ -5,6 +5,8 @@ __metaclass__ = type
import os import os
import json import json
from ... import types as t
from ...target import ( from ...target import (
walk_compile_targets, walk_compile_targets,
walk_powershell_targets, walk_powershell_targets,
@ -17,7 +19,7 @@ from ...io import (
from ...util import ( from ...util import (
ANSIBLE_TEST_DATA_ROOT, ANSIBLE_TEST_DATA_ROOT,
display, display,
find_executable, ApplicationError,
) )
from ...util_common import ( from ...util_common import (
@ -27,10 +29,19 @@ from ...util_common import (
write_json_test_results, write_json_test_results,
) )
from ...executor import (
Delegate,
)
from ...data import (
data_context,
)
from . import ( from . import (
enumerate_python_arcs, enumerate_python_arcs,
enumerate_powershell_lines, enumerate_powershell_lines,
get_collection_path_regexes, get_collection_path_regexes,
get_all_coverage_files,
get_python_coverage_files, get_python_coverage_files,
get_python_modules, get_python_modules,
get_powershell_coverage_files, get_powershell_coverage_files,
@ -44,9 +55,28 @@ from . import (
def command_coverage_combine(args): def command_coverage_combine(args):
"""Patch paths in coverage files and merge into a single file. """Patch paths in coverage files and merge into a single file.
:type args: CoverageConfig :type args: CoverageCombineConfig
:rtype: list[str] :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) paths = _command_coverage_combine_powershell(args) + _command_coverage_combine_python(args)
for path in paths: for path in paths:
@ -55,9 +85,18 @@ def command_coverage_combine(args):
return paths 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): def _command_coverage_combine_python(args):
""" """
:type args: CoverageConfig :type args: CoverageCombineConfig
:rtype: list[str] :rtype: list[str]
""" """
coverage = initialize_coverage(args) coverage = initialize_coverage(args)
@ -136,7 +175,7 @@ def _command_coverage_combine_python(args):
def _command_coverage_combine_powershell(args): def _command_coverage_combine_powershell(args):
""" """
:type args: CoverageConfig :type args: CoverageCombineConfig
:rtype: list[str] :rtype: list[str]
""" """
coverage_files = get_powershell_coverage_files() coverage_files = get_powershell_coverage_files()
@ -217,7 +256,7 @@ def _command_coverage_combine_powershell(args):
def _get_coverage_targets(args, walk_func): def _get_coverage_targets(args, walk_func):
""" """
:type args: CoverageConfig :type args: CoverageCombineConfig
:type walk_func: Func :type walk_func: Func
:rtype: list[tuple[str, int]] :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): def _build_stub_groups(args, sources, default_stub_value):
""" """
:type args: CoverageConfig :type args: CoverageCombineConfig
:type sources: List[tuple[str, int]] :type sources: List[tuple[str, int]]
:type default_stub_value: Func[List[str]] :type default_stub_value: Func[List[str]]
:rtype: dict :rtype: dict
@ -273,7 +312,7 @@ def _build_stub_groups(args, sources, default_stub_value):
def get_coverage_group(args, coverage_file): def get_coverage_group(args, coverage_file):
""" """
:type args: CoverageConfig :type args: CoverageCombineConfig
:type coverage_file: str :type coverage_file: str
:rtype: str :rtype: str
""" """
@ -306,3 +345,16 @@ def get_coverage_group(args, coverage_file):
group = group.lstrip('=') group = group.lstrip('=')
return group 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

View file

@ -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.""" """Erase code coverage data files collected during test runs."""
coverage_dir = ResultType.COVERAGE.path coverage_dir = ResultType.COVERAGE.path
@ -25,3 +25,7 @@ def command_coverage_erase(args): # type: (CoverageConfig) -> None
if not args.explain: if not args.explain:
os.remove(path) os.remove(path)
class CoverageEraseConfig(CoverageConfig):
"""Configuration for the coverage erase command."""

View file

@ -18,17 +18,17 @@ from ...util_common import (
from .combine import ( from .combine import (
command_coverage_combine, command_coverage_combine,
CoverageCombineConfig,
) )
from . import ( from . import (
run_coverage, run_coverage,
CoverageConfig,
) )
def command_coverage_html(args): def command_coverage_html(args):
""" """
:type args: CoverageConfig :type args: CoverageHtmlConfig
""" """
output_files = command_coverage_combine(args) 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]) run_coverage(args, output_file, 'html', ['-i', '-d', dir_name])
display.info('HTML report generated: file:///%s' % os.path.join(dir_name, 'index.html')) display.info('HTML report generated: file:///%s' % os.path.join(dir_name, 'index.html'))
class CoverageHtmlConfig(CoverageCombineConfig):
"""Configuration for the coverage html command."""

View file

@ -18,11 +18,11 @@ from ...data import (
from .combine import ( from .combine import (
command_coverage_combine, command_coverage_combine,
CoverageCombineConfig,
) )
from . import ( from . import (
run_coverage, run_coverage,
CoverageConfig,
) )
@ -143,7 +143,7 @@ def _generate_powershell_output_report(args, coverage_file):
return report return report
class CoverageReportConfig(CoverageConfig): class CoverageReportConfig(CoverageCombineConfig):
"""Configuration for the coverage report command.""" """Configuration for the coverage report command."""
def __init__(self, args): def __init__(self, args):
""" """

View file

@ -36,17 +36,17 @@ from ...data import (
from .combine import ( from .combine import (
command_coverage_combine, command_coverage_combine,
CoverageCombineConfig,
) )
from . import ( from . import (
run_coverage, run_coverage,
CoverageConfig,
) )
def command_coverage_xml(args): def command_coverage_xml(args):
""" """
:type args: CoverageConfig :type args: CoverageXmlConfig
""" """
output_files = command_coverage_combine(args) 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 return total_lines_hit, total_line_count
class CoverageXmlConfig(CoverageCombineConfig):
"""Configuration for the coverage xml command."""