Repository navigation
Integrate coverage_tool reports into documentation #425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ _build | |
| ubproject.toml | ||
| /docs/_collections | ||
| /tests-report | ||
| /coverage-report | ||
|
|
||
| # Workspace files | ||
| /score_*/ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,17 @@ | |
| REF_INT = Path(__file__).resolve().parents[1] | ||
| UNIFIED_RC = REF_INT / "bazel_common" / "unified.bazelrc" | ||
| UNIFIED_TOOLCHAINS = REF_INT / "bazel_common" / "unified_toolchains.MODULE.bazel" | ||
| # Only enable the coverage_tool flow for releases already integrated in known_good.json. | ||
| COVERAGE_MODULES = { | ||
| "score_baselibs", | ||
| "score_persistency", | ||
| "score_logging", | ||
| # "score_kyron", # Enable once ref_int integrates a release using coverage_tool. | ||
| # "score_communication", # Enable once ref_int integrates a release using coverage_tool. | ||
| # "score_lifecycle", # Enable once ref_int integrates a release using coverage_tool. | ||
| # "score_time", # Enable once ref_int integrates a release using coverage_tool. | ||
| # "score_config_management", # Enable once ref_int integrates a release using coverage_tool. | ||
| } | ||
| # Bazel's per-target result lines, e.g. "//score/foo:bar_test FAILED in 2.1s". | ||
| STATUS_LINE = re.compile(r"^(//\S+|@\S+)\s+(FAILED TO BUILD|FAILED|TIMEOUT|NO STATUS|INCOMPLETE|FLAKY)\b", re.M) | ||
|
|
||
|
|
@@ -108,18 +119,46 @@ def override_rc(paths: dict[str, Path], extra_test_config: list[str] = ()) -> st | |
| return rc + "".join(f"build --{flag}\n" for flag in extra_test_config) | ||
|
|
||
|
|
||
| def coverage_args(test_args: list[str], langs: list[str], excludes: list[str] = ()) -> list[str]: | ||
| def coverage_args( | ||
| test_args: list[str], langs: list[str], excludes: list[str] = (), *, enabled: bool = True, module: str = "" | ||
| ) -> list[str]: | ||
| """The module's own test args with ref_int's unified config added before any ``--``. | ||
|
|
||
| Modules awaiting coverage_tool integration run their unit tests without coverage. | ||
| ``excludes`` (the module's ``exclude_test_targets``) become negative patterns after ``--``. | ||
| """ | ||
| ours = ["--config=unified", "--combined_report=lcov"] | ||
| ours = ["--config=unified"] | ||
| if enabled: | ||
| ours.extend(["--build_tests_only", "--config=llvm_cov"]) | ||
| if "rust" in langs: | ||
| ours.append("--config=unified-rust") | ||
| split = test_args.index("--") if "--" in test_args else len(test_args) | ||
| targets = [*test_args[split + 1 :], *(f"-{target}" for target in excludes)] | ||
| flags = test_args[:split] | ||
| targets = list(test_args[split + 1 :]) | ||
| if enabled and module == "score_baselibs": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hardcoded sounds not a good idea to me. Can we have a list of modules in config
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah true. I also have a full coverage config in coverage_tool tests. But currently no idea how to reuse it. As I dont want to encode it into the actual tool. Ideally we would not need it, but we are not there yet. |
||
| # Let llvm_cov select Clang without the module's GCC configuration. | ||
| flags = [arg for arg in flags if arg != "--config=score-linux-x86_64"] | ||
| targets.extend( | ||
| [ | ||
| # This library interposes __cxa_allocate_exception; ld.lld rejects the | ||
| # duplicate definition from the statically linked C++ runtime. | ||
| "-//score/language/safecpp/aborts_upon_exception/...", | ||
| # These tests require GCC FP semantics or fail gMock's leak check | ||
| # with static linking. Keep the exclusions specific to coverage. | ||
| "-//score/language/safecpp/safe_math/details:floating_point_environment_test", | ||
| "-//score/os/linux/utils/test:network_interface_test", | ||
| ] | ||
| ) | ||
| targets.extend(f"-{target}" for target in excludes) | ||
| separator = ["--"] if targets else [] | ||
| return ["coverage", "--lockfile_mode=update", *test_args[:split], *ours, *separator, *targets] | ||
| return [ | ||
| "coverage" if enabled else "test", | ||
| "--lockfile_mode=update", | ||
| *flags, | ||
| *ours, | ||
| *separator, | ||
| *targets, | ||
| ] | ||
|
|
||
|
|
||
| def _status(case: ET.Element) -> str: | ||
|
|
@@ -173,11 +212,64 @@ def _append_unified_toolchain(workspace: Path) -> None: | |
| f.write(f'\ninclude("//:{UNIFIED_TOOLCHAINS.name}")\n') | ||
|
|
||
|
|
||
| def _collect_coverage(workspace: Path, out: Path, env: dict) -> None: | ||
| (out / "coverage").mkdir() | ||
| lcov = Path(bazel(["info", "output_path"], workspace, env).stdout.strip()) / "_coverage" / "_coverage_report.dat" | ||
| if lcov.is_file(): | ||
| shutil.copy(lcov, out / "coverage" / "lcov.dat") | ||
| def normalize_lcov(text: str, module: str) -> str: | ||
| """Rebase coverage_tool's canonical paths into ref_int's external module namespace.""" | ||
| lines = [] | ||
| for line in text.splitlines(): | ||
| if line.startswith("SF:"): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks a bit fuzzy here. I would say lets use pythons path tooling and refactor
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately I dont have an idea how to improve that |
||
| path = line[3:] | ||
| if path.startswith("/"): | ||
| raise ValueError(f"{module}: coverage source must be repository-relative: {path}") | ||
| if path.startswith("../"): | ||
| path = "external/" + path[3:] | ||
| if not path.startswith("external/"): | ||
| path = f"external/{module}+/{path}" | ||
| parts = path.split("/") | ||
| if len(parts) < 3 or any(part in ("", ".", "..", "_virtual_includes") for part in parts): | ||
| raise ValueError(f"{module}: coverage source is not canonical: {path}") | ||
| if parts[1] == "_main": | ||
| parts[1] = f"{module}+" | ||
| line = "SF:" + "/".join(parts) | ||
| lines.append(line) | ||
| return "\n".join(lines) + "\n" if lines else "" | ||
|
|
||
|
|
||
| def combine_reports(reports: Path, modules: list[str], output: Path) -> None: | ||
| """Combine available module reports, leaving absent measurements unavailable.""" | ||
| chunks = [] | ||
| for module in sorted(modules): | ||
| report = reports / module / "coverage" / "coverage_report.dat" | ||
| if report.is_file(): | ||
| chunks.append(report.read_text(encoding="utf-8")) | ||
| else: | ||
| print(f"::warning::{module}: no documentation coverage report available") | ||
| output.parent.mkdir(parents=True, exist_ok=True) | ||
| output.write_text("".join(chunk.rstrip("\n") + "\n" for chunk in chunks), encoding="utf-8") | ||
|
|
||
|
|
||
| def _collect_coverage(workspace: Path, out: Path, env: dict) -> int: | ||
| # Export through coverage_tool's public CLI. It owns the backend's report packaging; | ||
| # ref_int only translates the exported source identities into its workspace context. | ||
| # Threshold zero collects evidence without adding a new percentage gate to module tests. | ||
| result = bazel( | ||
| [ | ||
| "run", | ||
| "--lockfile_mode=update", | ||
| "@score_coverage//:generate_coverage_html", | ||
| "--", | ||
| "--testlogs-subdir=", | ||
| "--archive-dir", | ||
| str(out / "coverage"), | ||
| ], | ||
| workspace, | ||
| {**env, "COVERAGE_THRESHOLD": "0"}, | ||
| ) | ||
| print(result.stdout) | ||
| if result.returncode: | ||
| return result.returncode | ||
| lcov = out / "coverage" / "coverage_report.dat" | ||
| lcov.write_text(normalize_lcov(lcov.read_text(encoding="utf-8"), out.name), encoding="utf-8") | ||
| return 0 | ||
|
|
||
|
|
||
| def _write_summary( | ||
|
|
@@ -241,7 +333,14 @@ def run(args: argparse.Namespace) -> int: | |
| unpinned = sorted(shared - set(pinned)) | ||
| shutil.copy(rc, out / "overrides.bazelrc") | ||
|
|
||
| command = coverage_args(module.metadata.test_args, module.metadata.langs, module.metadata.exclude_test_targets) | ||
| coverage_enabled = module.name in COVERAGE_MODULES | ||
| command = coverage_args( | ||
| module.metadata.test_args, | ||
| module.metadata.langs, | ||
| module.metadata.exclude_test_targets, | ||
| enabled=coverage_enabled, | ||
| module=module.name, | ||
| ) | ||
| exit_code, log = streamed(["bazel", *command], workspace, env) | ||
|
|
||
| results = list((workspace / "bazel-testlogs").glob("**/test.xml")) | ||
|
|
@@ -252,7 +351,9 @@ def run(args: argparse.Namespace) -> int: | |
| # Bazel exits 0 when every test is skipped (e.g. all incompatible with the platform). | ||
| no_tests = not results | ||
| exit_code = exit_code or int(no_tests) | ||
| _collect_coverage(workspace, out, env) | ||
| if coverage_enabled: | ||
| coverage_exit = _collect_coverage(workspace, out, env) | ||
| exit_code = exit_code or coverage_exit | ||
| (out / "result.json").write_text(json.dumps({"exit_code": exit_code}) + "\n") | ||
| _write_summary(module.name, out, exit_code, log, no_tests=no_tests, pinned=pinned, unpinned=unpinned) | ||
| return exit_code | ||
|
|
@@ -270,10 +371,10 @@ def report(args: argparse.Namespace) -> int: | |
| result_file = reports / "result.json" | ||
| result = json.loads(result_file.read_text()) if result_file.is_file() else {"exit_code": 1} | ||
| unit_tests[module.name] = {**count_testcases(reports / "tests"), "exit_code": result["exit_code"]} | ||
| lcov = reports / "coverage" / "lcov.dat" | ||
| if "cpp" in module.metadata.langs: | ||
| lcov = reports / "coverage" / "coverage_report.dat" | ||
| if module.name in COVERAGE_MODULES: | ||
| numbers = lcov_percentages(lcov) if lcov.is_file() else {} | ||
| coverage[f"{module.name}_cpp"] = {**numbers, "exit_code": 0 if numbers.get("lines") else 1} | ||
| coverage[module.name] = {**numbers, "exit_code": 0 if numbers.get("lines") else 1} | ||
|
|
||
| args.out_dir.mkdir(parents=True, exist_ok=True) | ||
| unit_tests_md = generate_markdown_report( | ||
|
|
@@ -289,6 +390,11 @@ def report(args: argparse.Namespace) -> int: | |
| output_path=args.out_dir / "coverage_summary.md", | ||
| ) | ||
| append_to_step_summary(unit_tests_md, coverage_md) | ||
| combine_reports( | ||
| args.reports_dir, | ||
| sorted(COVERAGE_MODULES & known.modules["target_sw"].keys()), | ||
| REF_INT / "coverage-report" / "coverage_report.dat", | ||
| ) | ||
| return 0 | ||
|
|
||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.