#!/usr/bin/env python3 """Summarize strict-core LCOV lines and LLVM JSON branches by source location. Merge generic monomorphizations by source location. A location is covered if any instance executed it: * lines use LCOV `DA` file and line numbers * branch outcomes use their full source spans JSON segments mark instrumentation boundaries and omit some `DA` lines. Using them would undercount the line denominator. Reject macro expansions: invocation spans cannot distinguish expanded predicates. """ from __future__ import annotations import argparse import json from collections.abc import Iterable from pathlib import Path import sys class CoverageFormatError(ValueError): """Coverage records cannot be safely summarized by source location.""" def selected_files( report: dict[object, object], source_path: str ) -> Iterable[dict[object, object]]: """Yield selected file records after rejecting ambiguous macro expansion.""" exports = report.get("data") if not isinstance(exports, list): raise CoverageFormatError("LLVM JSON has no data array") for export in exports: if not isinstance(export, dict): raise CoverageFormatError("LLVM JSON data array contains a non-object") files = export.get("files") if not isinstance(files, list): raise CoverageFormatError("LLVM JSON export has no files array") for file_record in files: if not isinstance(file_record, dict): raise CoverageFormatError("LLVM JSON files array contains a non-object") filename = file_record.get("filename") if not isinstance(filename, str) or source_path not in filename: continue expansions = file_record.get("expansions", []) if not isinstance(expansions, list): raise CoverageFormatError(f"{filename}: expansions is not an array") if expansions: raise CoverageFormatError( f"{filename}: macro-expansion coverage is present; " "refusing to merge it by invocation source span" ) yield file_record def lcov_line_totals(report: str, source_path: str) -> tuple[int, int, set[str]]: """Return the union of selected LCOV `DA` lines and their source files.""" lines: dict[tuple[str, int], bool] = {} files: set[str] = set() filename: str | None = None for record in report.splitlines(): if record.startswith("SF:"): filename = record[3:] if source_path in filename: files.add(filename) continue if record == "end_of_record": filename = None continue if ( filename is None or source_path not in filename or not record.startswith("DA:") ): continue fields = record[3:].split(",", 2) if len(fields) < 2: raise CoverageFormatError(f"{filename}: malformed LCOV DA record") try: line = int(fields[0]) count = int(fields[1]) except ValueError as error: raise CoverageFormatError( f"{filename}: non-integer LCOV DA record" ) from error if line <= 0 or count < 0: raise CoverageFormatError(f"{filename}: invalid LCOV DA location or count") key = (filename, line) lines[key] = lines.get(key, False) or count > 0 if not files: raise CoverageFormatError(f"no LCOV source file contains {source_path!r}") if not lines: raise CoverageFormatError(f"no LCOV DA records found under {source_path!r}") return sum(lines.values()), len(lines), files def branch_totals( report: dict[object, object], source_path: str ) -> tuple[int, int, set[str]]: """Return the union of selected JSON branch outcomes and source files.""" branches: dict[tuple[str, int, int, int, int], list[bool]] = {} files: set[str] = set() for file_record in selected_files(report, source_path): filename = file_record["filename"] assert isinstance(filename, str) files.add(filename) raw_branches = file_record.get("branches") if not isinstance(raw_branches, list): raise CoverageFormatError(f"{filename}: branches is not an array") for branch in raw_branches: if ( not isinstance(branch, list) or len(branch) != 9 or not all(type(value) is int for value in branch) ): raise CoverageFormatError(f"{filename}: invalid branch record") ( start_line, start_column, end_line, end_column, true_count, false_count, file_id, expanded_file_id, _kind, ) = branch if ( start_line <= 0 or start_column <= 0 or end_line < start_line or end_column <= 0 or (end_line == start_line and end_column < start_column) or true_count < 0 or false_count < 0 or file_id < 0 or expanded_file_id < 0 ): raise CoverageFormatError( f"{filename}: invalid branch location or count" ) if file_id != expanded_file_id: raise CoverageFormatError( f"{filename}:{start_line}:{start_column}: macro-expanded branch is present; " "refusing to merge it by invocation source span" ) key = (filename, start_line, start_column, end_line, end_column) outcomes = branches.setdefault(key, [False, False]) outcomes[0] = outcomes[0] or true_count > 0 outcomes[1] = outcomes[1] or false_count > 0 if not files: raise CoverageFormatError(f"no LLVM JSON source file contains {source_path!r}") branch_hit = sum(sum(outcomes) for outcomes in branches.values()) return branch_hit, 2 * len(branches), files def coverage_totals( lcov_report: str, json_report: dict[object, object], source_path: str ) -> tuple[int, int, int, int]: """Return strict source-level line and branch coverage for one core crate.""" lines_hit, lines_found, lcov_files = lcov_line_totals(lcov_report, source_path) branch_hit, branches_found, json_files = branch_totals(json_report, source_path) if lcov_files != json_files: raise CoverageFormatError( "LCOV/JSON source-file mismatch: " f"LCOV={sorted(lcov_files)!r}, JSON={sorted(json_files)!r}" ) return lines_hit, lines_found, branch_hit, branches_found def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "lcov_report", type=Path, help="LCOV report from cargo llvm-cov" ) parser.add_argument( "json_report", type=Path, help="LLVM JSON report from cargo llvm-cov" ) parser.add_argument( "source_path", help="workspace-relative path fragment to include" ) arguments = parser.parse_args() try: lcov_report = arguments.lcov_report.read_text(encoding="utf-8") with arguments.json_report.open(encoding="utf-8") as report_file: json_report = json.load(report_file) if not isinstance(json_report, dict): raise CoverageFormatError("LLVM JSON root is not an object") print(*coverage_totals(lcov_report, json_report, arguments.source_path)) except (CoverageFormatError, OSError, json.JSONDecodeError) as error: print(f"coverage summary error: {error}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main())