def _get_coverage_report( output_dir: PurePath, report_type: CoverageReportType, coverage_insufficient: bool, result_stdout: bytes, result_snapshot: Snapshot, ) -> CoverageReport: if report_type == CoverageReportType.CONSOLE: return ConsoleCoverageReport(coverage_insufficient, result_stdout.decode()) report_file: PurePath | None if report_type == CoverageReportType.HTML: report_file = output_dir / "htmlcov" / "index.html" elif report_type == CoverageReportType.XML: report_file = output_dir / "coverage.xml" elif report_type == CoverageReportType.JSON: report_file = output_dir / "coverage.json" else: raise ValueError(f"Invalid coverage report type: {report_type}") return FilesystemCoverageReport( coverage_insufficient=coverage_insufficient, report_type=report_type.value, result_snapshot=result_snapshot, directory_to_materialize_to=output_dir, report_file=report_file, )
def _get_coverage_reports( output_dir: PurePath, report_types: Sequence[CoverageReportType], results: Tuple[ProcessResult, ...], ) -> List[CoverageReport]: coverage_reports: List[CoverageReport] = [] for result, report_type in zip(results, report_types): if report_type == CoverageReportType.CONSOLE: coverage_reports.append( ConsoleCoverageReport(result.stdout.decode())) continue report_file: Optional[PurePath] = None if report_type == CoverageReportType.HTML: report_file = output_dir / "htmlcov" / "index.html" elif report_type == CoverageReportType.XML: report_file = output_dir / "coverage.xml" elif report_type == CoverageReportType.JSON: report_file = output_dir / "coverage.json" else: raise ValueError(f"Invalid coverage report type: {report_type}") coverage_reports.append( FilesystemCoverageReport( report_type=report_type, result_digest=result.output_digest, directory_to_materialize_to=output_dir, report_file=report_file, )) return coverage_reports
async def generate_coverage_reports( merged_coverage_data: MergedCoverageData, coverage_setup: CoverageSetup, coverage_config: CoverageConfig, coverage_subsystem: CoverageSubsystem, transitive_targets: TransitiveTargets, ) -> CoverageReports: """Takes all Python test results and generates a single coverage report.""" sources = await Get( PythonSourceFiles, PythonSourceFilesRequest(transitive_targets.closure, include_resources=False), ) input_digest = await Get( Digest, MergeDigests(( merged_coverage_data.coverage_data, coverage_config.digest, coverage_setup.pex.digest, sources.source_files.snapshot.digest, )), ) pex_processes = [] report_types = [] coverage_reports: List[CoverageReport] = [] for report_type in coverage_subsystem.reports: if report_type == CoverageReportType.RAW: coverage_reports.append( FilesystemCoverageReport( report_type=CoverageReportType.RAW, result_digest=merged_coverage_data.coverage_data, directory_to_materialize_to=coverage_subsystem.output_dir, report_file=coverage_subsystem.output_dir / ".coverage", )) continue report_types.append(report_type) output_file = (f"coverage.{report_type.value}" if report_type in { CoverageReportType.XML, CoverageReportType.JSON } else None) pex_processes.append( PexProcess( coverage_setup.pex, # We pass `--ignore-errors` because Pants dynamically injects missing `__init__.py` # files and this will cause Coverage to fail. argv=(report_type.report_name, "--ignore-errors"), input_digest=input_digest, output_directories=("htmlcov", ) if report_type == CoverageReportType.HTML else None, output_files=(output_file, ) if output_file else None, description= f"Generate Pytest {report_type.report_name} coverage report.", level=LogLevel.DEBUG, )) results = await MultiGet( Get(ProcessResult, PexProcess, process) for process in pex_processes) coverage_reports.extend( _get_coverage_reports(coverage_subsystem.output_dir, report_types, results)) return CoverageReports(tuple(coverage_reports))
async def generate_coverage_reports( merged_coverage_data: MergedCoverageData, coverage_setup: CoverageSetup, coverage_config: CoverageConfig, coverage_subsystem: CoverageSubsystem, global_options: GlobalOptions, ) -> CoverageReports: """Takes all Python test results and generates a single coverage report.""" transitive_targets = await Get( TransitiveTargets, TransitiveTargetsRequest(merged_coverage_data.addresses)) sources = await Get( PythonSourceFiles, # Coverage sometimes includes non-Python files in its `.coverage` data. We need to # ensure that they're present when generating the report. We include all the files included # by `pytest_runner.py`. PythonSourceFilesRequest(transitive_targets.closure, include_files=True, include_resources=True), ) input_digest = await Get( Digest, MergeDigests(( merged_coverage_data.coverage_data, coverage_config.digest, sources.source_files.snapshot.digest, )), ) pex_processes = [] report_types = [] result_snapshot = await Get(Snapshot, Digest, merged_coverage_data.coverage_data) coverage_reports: list[CoverageReport] = [] for report_type in coverage_subsystem.reports: if report_type == CoverageReportType.RAW: coverage_reports.append( FilesystemCoverageReport( # We don't know yet if the coverage is sufficient, so we let some other report # trigger the failure if necessary. coverage_insufficient=False, report_type=CoverageReportType.RAW.value, result_snapshot=result_snapshot, directory_to_materialize_to=coverage_subsystem.output_dir, report_file=coverage_subsystem.output_dir / ".coverage", )) continue report_types.append(report_type) output_file = (f"coverage.{report_type.value}" if report_type in { CoverageReportType.XML, CoverageReportType.JSON } else None) args = [report_type.report_name, f"--rcfile={coverage_config.path}"] if coverage_subsystem.fail_under is not None: args.append(f"--fail-under={coverage_subsystem.fail_under}") pex_processes.append( VenvPexProcess( coverage_setup.pex, argv=tuple(args), input_digest=input_digest, output_directories=("htmlcov", ) if report_type == CoverageReportType.HTML else None, output_files=(output_file, ) if output_file else None, description= f"Generate Pytest {report_type.report_name} coverage report.", level=LogLevel.DEBUG, )) results = await MultiGet( Get(FallibleProcessResult, VenvPexProcess, process) for process in pex_processes) for proc, res in zip(pex_processes, results): if res.exit_code not in {0, 2}: # coverage.py uses exit code 2 if --fail-under triggers, in which case the # reports are still generated. raise ProcessExecutionFailure( res.exit_code, res.stdout, res.stderr, proc.description, local_cleanup=global_options.options. process_execution_local_cleanup, ) # In practice if one result triggers --fail-under, they all will, but no need to rely on that. result_exit_codes = tuple(res.exit_code for res in results) result_stdouts = tuple(res.stdout for res in results) result_snapshots = await MultiGet( Get(Snapshot, Digest, res.output_digest) for res in results) coverage_reports.extend( _get_coverage_report(coverage_subsystem.output_dir, report_type, exit_code != 0, stdout, snapshot) for (report_type, exit_code, stdout, snapshot) in zip( report_types, result_exit_codes, result_stdouts, result_snapshots)) return CoverageReports(tuple(coverage_reports))
async def generate_coverage_report( merged_coverage_data: MergedCoverageData, coverage_setup: CoverageSetup, coverage_subsystem: PytestCoverage, transitive_targets: TransitiveTargets, python_setup: PythonSetup, subprocess_encoding_environment: SubprocessEncodingEnvironment, ) -> CoverageReport: """Takes all Python test results and generates a single coverage report.""" requirements_pex = coverage_setup.requirements_pex python_targets = [tgt for tgt in transitive_targets.closure if tgt.has_field(PythonSources)] coverage_config = await Get[CoverageConfig]( CoverageConfigRequest(Targets(python_targets), is_test_time=False) ) sources = await Get[SourceFiles]( AllSourceFilesRequest( (tgt.get(Sources) for tgt in transitive_targets.closure), strip_source_roots=False ) ) sources_with_inits_snapshot = await Get[InitInjectedSnapshot]( InjectInitRequest(sources.snapshot) ) merged_input_files: Digest = await Get( Digest, DirectoriesToMerge( directories=( merged_coverage_data.coverage_data, coverage_config.digest, requirements_pex.directory_digest, sources_with_inits_snapshot.snapshot.directory_digest, ) ), ) report_type = coverage_subsystem.options.report coverage_args = [report_type.report_name] process = requirements_pex.create_process( pex_path=f"./{coverage_setup.requirements_pex.output_filename}", pex_args=coverage_args, input_files=merged_input_files, output_directories=("htmlcov",), output_files=("coverage.xml",), description=f"Generate Pytest coverage report.", python_setup=python_setup, subprocess_encoding_environment=subprocess_encoding_environment, ) result = await Get[ProcessResult](Process, process) if report_type == ReportType.CONSOLE: return ConsoleCoverageReport(result.stdout.decode()) report_dir = PurePath(coverage_subsystem.options.report_output_path) report_file: Optional[PurePath] = None if coverage_subsystem.options.report == ReportType.HTML: report_file = report_dir / "htmlcov" / "index.html" elif coverage_subsystem.options.report == ReportType.XML: report_file = report_dir / "coverage.xml" return FilesystemCoverageReport( result_digest=result.output_directory_digest, directory_to_materialize_to=report_dir, report_file=report_file, )
async def generate_coverage_reports( merged_coverage_data: MergedCoverageData, coverage_setup: CoverageSetup, coverage_config: CoverageConfig, coverage_subsystem: CoverageSubsystem, all_used_addresses: Addresses, ) -> CoverageReports: """Takes all Python test results and generates a single coverage report.""" transitive_targets = await Get( TransitiveTargets, TransitiveTargetsRequest(all_used_addresses)) sources = await Get( PythonSourceFiles, # Coverage sometimes includes non-Python files in its `.coverage` data. We need to # ensure that they're present when generating the report. We include all the files included # by `pytest_runner.py`. PythonSourceFilesRequest(transitive_targets.closure, include_files=True, include_resources=True), ) input_digest = await Get( Digest, MergeDigests(( merged_coverage_data.coverage_data, coverage_config.digest, sources.source_files.snapshot.digest, )), ) pex_processes = [] report_types = [] result_snapshot = await Get(Snapshot, Digest, merged_coverage_data.coverage_data) coverage_reports: List[CoverageReport] = [] for report_type in coverage_subsystem.reports: if report_type == CoverageReportType.RAW: coverage_reports.append( FilesystemCoverageReport( report_type=CoverageReportType.RAW.value, result_snapshot=result_snapshot, directory_to_materialize_to=coverage_subsystem.output_dir, report_file=coverage_subsystem.output_dir / ".coverage", )) continue report_types.append(report_type) output_file = (f"coverage.{report_type.value}" if report_type in { CoverageReportType.XML, CoverageReportType.JSON } else None) pex_processes.append( VenvPexProcess( coverage_setup.pex, argv=(report_type.report_name, f"--rcfile={coverage_config.path}"), input_digest=input_digest, output_directories=("htmlcov", ) if report_type == CoverageReportType.HTML else None, output_files=(output_file, ) if output_file else None, description= f"Generate Pytest {report_type.report_name} coverage report.", level=LogLevel.DEBUG, )) results = await MultiGet( Get(ProcessResult, VenvPexProcess, process) for process in pex_processes) result_stdouts = tuple(res.stdout for res in results) result_snapshots = await MultiGet( Get(Snapshot, Digest, res.output_digest) for res in results) coverage_reports.extend( _get_coverage_report(coverage_subsystem.output_dir, report_type, stdout, snapshot) for (report_type, stdout, snapshot) in zip(report_types, result_stdouts, result_snapshots)) return CoverageReports(tuple(coverage_reports))
async def generate_coverage_report( merged_coverage_data: MergedCoverageData, coverage_setup: CoverageSetup, coverage_subsystem: PytestCoverage, transitive_targets: TransitiveTargets, python_setup: PythonSetup, subprocess_encoding_environment: SubprocessEncodingEnvironment, ) -> CoverageReports: """Takes all Python test results and generates a single coverage report.""" requirements_pex = coverage_setup.requirements_pex coverage_config = await Get[CoverageConfig](CoverageConfigRequest( Targets(transitive_targets.closure), is_test_time=False)) sources = await Get[SourceFiles](AllSourceFilesRequest( (tgt.get(Sources) for tgt in transitive_targets.closure), strip_source_roots=False)) sources_with_inits_snapshot = await Get[InitInjectedSnapshot]( InjectInitRequest(sources.snapshot)) input_digest: Digest = await Get( Digest, MergeDigests(( merged_coverage_data.coverage_data, coverage_config.digest, requirements_pex.digest, sources_with_inits_snapshot.snapshot.digest, )), ) report_type = coverage_subsystem.options.report # The -i flag causes coverage to ignore files it doesn't have sources for. # Specifically, it will ignore the injected __init__.py files, which is what we want since # those are empty and don't correspond to a real source file the user is aware of. coverage_args = [report_type.report_name, "-i"] process = requirements_pex.create_process( pex_path=f"./{coverage_setup.requirements_pex.output_filename}", pex_args=coverage_args, input_digest=input_digest, output_directories=("htmlcov", ), output_files=("coverage.xml", ), description="Generate Pytest coverage report.", python_setup=python_setup, subprocess_encoding_environment=subprocess_encoding_environment, ) result = await Get[ProcessResult](Process, process) if report_type == CoverageReportType.CONSOLE: return CoverageReports( reports=(ConsoleCoverageReport(result.stdout.decode()), )) report_dir = PurePath(coverage_subsystem.options.report_output_path) report_file: Optional[PurePath] = None if report_type == CoverageReportType.HTML: report_file = report_dir / "htmlcov" / "index.html" elif report_type == CoverageReportType.XML: report_file = report_dir / "coverage.xml" fs_report = FilesystemCoverageReport( result_digest=result.output_digest, directory_to_materialize_to=report_dir, report_file=report_file, ) return CoverageReports(reports=(fs_report, ))
def run_test_rule( self, *, config: Type[TestConfiguration], targets: List[TargetWithOrigin], debug: bool = False, include_sources: bool = True, ) -> Tuple[int, str]: console = MockConsole(use_colors=False) options = create_goal_subsystem(TestOptions, debug=debug, run_coverage=False) interactive_runner = InteractiveRunner(self.scheduler) workspace = Workspace(self.scheduler) union_membership = UnionMembership( {TestConfiguration: OrderedSet([config])}) def mock_coordinator_of_tests( wrapped_config: WrappedTestConfiguration, ) -> AddressAndTestResult: config = wrapped_config.config return AddressAndTestResult( address=config.address, test_result=config.test_result, # type: ignore[attr-defined] ) result: Test = run_rule( run_tests, rule_args=[ console, options, interactive_runner, TargetsWithOrigins(targets), workspace, union_membership, RegisteredTargetTypes.create([MockTarget]), ], mock_gets=[ MockGet( product_type=AddressAndTestResult, subject_type=WrappedTestConfiguration, mock=lambda wrapped_config: mock_coordinator_of_tests( wrapped_config), ), MockGet( product_type=TestDebugRequest, subject_type=TestConfiguration, mock=lambda _: TestDebugRequest(self.make_ipr()), ), MockGet( product_type=ConfigurationsWithSources, subject_type=ConfigurationsWithSourcesRequest, mock=lambda configs: ConfigurationsWithSources( configs if include_sources else ()), ), MockGet( product_type=CoverageReport, subject_type=CoverageDataCollection, mock=lambda _: FilesystemCoverageReport( result_digest=EMPTY_DIRECTORY_DIGEST, directory_to_materialize_to=PurePath("mockety/mock"), report_file=None, ), ), ], union_membership=union_membership, ) return result.exit_code, console.stdout.getvalue()