From 889092315e02a68b6339134e7655dc78c4e4e19e Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:48:36 +0300 Subject: [PATCH 1/2] Include virtual-includes and vendored headers in the coverage scope eclipse-score/baselibs#558: a workspace target whose hdrs are vendored from an external repository behind strip_include_prefix (//score/flatbuffers:flatbufferscpp) lost all its headers from the report. Two causes: the scope aspect dropped every file under external/, and the compiler does not even record that path. With strip_include_prefix / include_prefix Bazel compiles against a generated _virtual_includes/ symlink tree and the coverage mapping names that file, under a configuration-specific prefix (bazel-out/k8-fastbuild/bin/... in the test binaries, bazel-out/k8-opt-exec-*/bin/... in the scope's baseline archive). Aspect: for workspace targets the post-processing identity of the public headers (CcInfo.compilation_context.direct_public_headers) is listed as well: the _virtual_includes/ path, and headers a workspace target vendors from an external repository (external//...). Other generated headers stay out; external targets stay out. Reporter: get_covered_files returns raw -> normalized names and strip_config_prefix drops bazel-out//bin/ so covmap paths compare equal to the allowlist's short_path form; the same normalization applies to LCOV SF records and HTML titles. redundant_baseline_variants excludes the baseline archive's variant of a header that a test binary already covers, so such a header appears once instead of twice (once at 0 %). Verification: new analysis test (strip_include_prefix fixture), 5 unit tests for the normalization and the variant exclusion, and a header-only library behind strip_include_prefix in the integration workspace whose hand-derived record (3 of 6 lines, one call) joins expected_lcov.dat; all 15 integration checks pass. Validated on baselibs //score/flatbuffers/...: the 13 vendored headers appear in LCOV and HTML, each exactly once. The approach follows the patch candidates of baselibs#577 but needs no extra_deps attribute: the scope aspect handles it for every workspace target. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> (cherry picked from commit 40b17463234d2716d087a9960ee6f77776a35f64) --- docs/manual/known_problems.rst | 6 ++ docs/release/release_notes.rst | 6 ++ docs/requirements/tool_requirements.rst | 12 ++- docs/verification/verification_report.rst | 18 ++-- integration_tests/expected_lcov.dat | 20 +++++ integration_tests/src/BUILD | 15 +++- integration_tests/src/coverable_test.cpp | 4 + .../vendored/include/vendored/inline_math.h | 28 +++++++ integration_tests/tools/coverage/BUILD | 1 + score_coverage/coverage_scope.bzl | 30 ++++++- score_coverage/reporter.py | 67 +++++++++++---- score_coverage/tests/reporter_test.py | 84 ++++++++++++++++++- .../tests/starlark/coverage_scope_tests.bzl | 17 ++++ score_coverage/tests/starlark/fixtures/BUILD | 9 ++ .../fixtures/vendor/include/vendored/api.h | 14 ++++ 15 files changed, 304 insertions(+), 27 deletions(-) create mode 100644 integration_tests/src/vendored/include/vendored/inline_math.h create mode 100644 score_coverage/tests/starlark/fixtures/vendor/include/vendored/api.h diff --git a/docs/manual/known_problems.rst b/docs/manual/known_problems.rst index c418afc..daa3d0e 100644 --- a/docs/manual/known_problems.rst +++ b/docs/manual/known_problems.rst @@ -62,6 +62,12 @@ stay listed with their upstream references. - ``no coverage data found`` on a Rust archive. - Handled since the pipeline expands rlibs into their object members; if seen, the installed version predates the fix. + * - **Vendored headers appear under their virtual-includes path.** A header + compiled through ``strip_include_prefix`` is reported as + ``/_virtual_includes//``, not under the label it was + declared with, because that is the identity the compiler records. + - Report rows named ``_virtual_includes``. + - Expected; justifications for such lines must use the reported path. * - **Instrumentation filter appears ignored.** - ``--instrumentation_filter`` has no visible effect. - Expected: ``--experimental_use_llvm_covmap`` instruments everything; diff --git a/docs/release/release_notes.rst b/docs/release/release_notes.rst index 6fb298d..d276354 100644 --- a/docs/release/release_notes.rst +++ b/docs/release/release_notes.rst @@ -43,5 +43,11 @@ Changes relative to the pipeline in score_tooling 2.2.x: ``--lcov``; ``summary.txt`` is written for gcovr reports too. - The repository-bound ``combined_report`` and ``llvm_profile_wrapper`` helpers are not part of the module. +- Headers reached through ``strip_include_prefix`` / ``include_prefix`` (Bazel's + ``_virtual_includes/`` tree) and headers a workspace target vendors from an + external repository are now part of the coverage scope; the reporter + normalises the configuration-specific ``bazel-out//bin/`` prefix and + suppresses the duplicate baseline entry of such headers + (eclipse-score/baselibs#558). Known problems: see :doc:`../manual/known_problems`. diff --git a/docs/requirements/tool_requirements.rst b/docs/requirements/tool_requirements.rst index c034027..220be31 100644 --- a/docs/requirements/tool_requirements.rst +++ b/docs/requirements/tool_requirements.rst @@ -42,6 +42,11 @@ Scope and ``rust_library`` targets (``srcs``, ``hdrs``) and the ``CrateInfo`` sources of ``rust_binary`` targets, and shall write them sorted and deduplicated, one workspace-relative path per line, to the allowlist file. + For workspace targets it shall additionally list the post-processing + identity of the public headers: the generated ``_virtual_includes/`` path a + header gets through ``strip_include_prefix`` or ``include_prefix`` (the path + the coverage mapping records), and headers a workspace target vendors from + an external repository. .. tool_req:: External and generated sources are excluded from the scope :id: tool_req__coverage_scope_excludes @@ -186,8 +191,11 @@ Report The reporter shall rewrite the absolute workspace root and the compiler's ``/proc/self/cwd/`` prefix in LCOV ``SF:`` records and in HTML page titles to - workspace-relative paths, so that the archived report is portable and file - identity does not depend on the machine. + workspace-relative paths, and shall drop the configuration-specific + ``bazel-out//bin/`` prefix of generated headers, so that the archived + report is portable and file identity depends neither on the machine nor on + the build configuration. A generated header covered by a test binary shall + appear once, not additionally as a 0 % entry from the baseline archive. .. tool_req:: Report contents :id: tool_req__coverage_report_outputs diff --git a/docs/verification/verification_report.rst b/docs/verification/verification_report.rst index 5f8edb8..1d3b250 100644 --- a/docs/verification/verification_report.rst +++ b/docs/verification/verification_report.rst @@ -48,9 +48,10 @@ Test inventory - 20 - merge_profraw, merge_no_data, merge_tool_error * - ``//score_coverage/tests:reporter_test`` - - 34 - - report_merged_profile, report_allowlist, report_rlib_expansion, - report_missing_baseline, report_relative_paths, report_outputs + - 39 + - report_merged_profile, report_allowlist, report_baseline_zero, + report_rlib_expansion, report_missing_baseline, report_relative_paths, + report_outputs, scope_transitive * - ``//score_coverage/tests:justify_test`` - 41 - just_yaml, just_markers, just_unknown_id, just_platform, @@ -66,8 +67,8 @@ Test inventory * - ``//score_coverage/tests:coverage_summary_test`` - 17 - summary_first - * - ``//score_coverage/tests/starlark:coverage_scope_tests`` (8 analysis tests) - - 8 + * - ``//score_coverage/tests/starlark:coverage_scope_tests`` (9 analysis tests) + - 9 - scope_transitive, scope_excludes, scope_baseline_objects * - ``integration_tests/run_integration_test.sh`` (15 end-to-end checks) - 15 @@ -94,7 +95,7 @@ generated link: - :need:`tool_req__coverage_scope_transitive`, :need:`tool_req__coverage_scope_excludes` and - :need:`tool_req__coverage_scope_baseline_objects` are verified by the eight + :need:`tool_req__coverage_scope_baseline_objects` are verified by the nine Starlark analysis tests in ``score_coverage/tests/starlark`` (rules_testing produces no test properties). - :need:`tool_req__coverage_validation_ground_truth` is verified by the @@ -157,8 +158,9 @@ End-to-end validation --------------------- ``integration_tests/run_integration_test.sh`` builds a consumer workspace with a -tested and an untested C++ library, a tested Rust library and an untested Rust -binary, one justified line, and asserts: +tested and an untested C++ library, a header-only library reached through +``strip_include_prefix``, a tested Rust library and an untested Rust binary, one +justified line, and asserts: 1. the gate fails at 100 % and passes at 10 % (effective and raw mode); 2. the HTML, the summary and the archive tree are produced, the summary also diff --git a/integration_tests/expected_lcov.dat b/integration_tests/expected_lcov.dat index 18fbe5c..4766a68 100644 --- a/integration_tests/expected_lcov.dat +++ b/integration_tests/expected_lcov.dat @@ -27,6 +27,14 @@ # baseline; no branches. # src/uncovered.cpp no test links against it => all lines 0, both directions # of the branch on line 18 never executed ('-'). +# src/_virtual_includes/vendored_math/vendored/inline_math.h +# header-only library behind strip_include_prefix, reported +# under the generated virtual-includes path the compiler +# records (baselibs#558). twice() (lines 18-20) is called once +# by coverable_test; never_inlined() (22-24) is never called +# and clang still emits its mapping => 3 of 6 lines, no +# branches. Exactly one record: the baseline variant of the +# same header is suppressed. SF:rust/lib.rs DA:16,2 DA:17,2 @@ -95,3 +103,15 @@ BRH:0 LF:6 LH:0 end_of_record +SF:src/_virtual_includes/vendored_math/vendored/inline_math.h +DA:18,1 +DA:19,1 +DA:20,1 +DA:22,0 +DA:23,0 +DA:24,0 +BRF:0 +BRH:0 +LF:6 +LH:3 +end_of_record diff --git a/integration_tests/src/BUILD b/integration_tests/src/BUILD index 983b6b1..71e2aee 100644 --- a/integration_tests/src/BUILD +++ b/integration_tests/src/BUILD @@ -30,8 +30,21 @@ cc_library( hdrs = ["uncovered.h"], ) +# Header-only library whose header is reached through strip_include_prefix: +# the compiler sees it via Bazel's generated _virtual_includes/ tree, the same +# mechanism vendored third-party headers use (eclipse-score/baselibs#558). +# One inline function is called by the test, the other never. +cc_library( + name = "vendored_math", + hdrs = ["vendored/include/vendored/inline_math.h"], + strip_include_prefix = "vendored/include", +) + cc_test( name = "coverable_test", srcs = ["coverable_test.cpp"], - deps = [":coverable"], + deps = [ + ":coverable", + ":vendored_math", + ], ) diff --git a/integration_tests/src/coverable_test.cpp b/integration_tests/src/coverable_test.cpp index eb22a5e..7dbe368 100644 --- a/integration_tests/src/coverable_test.cpp +++ b/integration_tests/src/coverable_test.cpp @@ -13,6 +13,7 @@ #include #include "src/coverable.h" +#include "vendored/inline_math.h" // Deliberately exercises only the negative and zero branches; the positive // branch stays uncovered (and justified via the COV_JUSTIFIED marker). @@ -24,5 +25,8 @@ int main() { if (std::strcmp(classify(0), "zero") != 0) { return 1; } + if (coverage_integration::twice(21) != 42) { + return 1; + } return 0; } diff --git a/integration_tests/src/vendored/include/vendored/inline_math.h b/integration_tests/src/vendored/include/vendored/inline_math.h new file mode 100644 index 0000000..32b4d66 --- /dev/null +++ b/integration_tests/src/vendored/include/vendored/inline_math.h @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef COVERAGE_INTEGRATION_TESTS_SRC_VENDORED_INLINE_MATH_H +#define COVERAGE_INTEGRATION_TESTS_SRC_VENDORED_INLINE_MATH_H + +namespace coverage_integration { + +inline int twice(int value) { + return value * 2; +} + +inline int never_inlined(int value) { + return value - 1; +} + +} // namespace coverage_integration + +#endif // COVERAGE_INTEGRATION_TESTS_SRC_VENDORED_INLINE_MATH_H diff --git a/integration_tests/tools/coverage/BUILD b/integration_tests/tools/coverage/BUILD index e86d935..1e828b7 100644 --- a/integration_tests/tools/coverage/BUILD +++ b/integration_tests/tools/coverage/BUILD @@ -25,6 +25,7 @@ score_coverage_scope( "//rust:untested_tool", "//src:coverable", "//src:uncovered", + "//src:vendored_math", ], ) diff --git a/score_coverage/coverage_scope.bzl b/score_coverage/coverage_scope.bzl index acb722b..11a52ba 100644 --- a/score_coverage/coverage_scope.bzl +++ b/score_coverage/coverage_scope.bzl @@ -48,6 +48,21 @@ _CoverageScopeInfo = provider( # Aspect: traverses library/binary deps to collect files # ============================================================================= +def _is_workspace_target(target): + """True for targets of the main repository (not of an external module).""" + label = str(target.label) + return not label.startswith("@@") or label.startswith("@@//") + +def _workspace_relative(f): + """short_path in the form the reporter compares against covmap paths. + + Main-repository files: "/". External files have a short_path + of "..//"; the compiler records them as "external//". + """ + if f.short_path.startswith("../"): + return "external/" + f.short_path[3:] + return f.short_path + def _coverage_scope_aspect_impl(target, ctx): """Collects source file paths and archive files from the build graph.""" direct_files = [] @@ -58,6 +73,7 @@ def _coverage_scope_aspect_impl(target, ctx): # At cc_library / rust_library targets (rust_library provides CcInfo with # its rlib exposed as a .a symlink): collect srcs, hdrs, and static archive if CcInfo in target: + in_workspace = _is_workspace_target(target) for attr_name in ["srcs", "hdrs"]: if hasattr(ctx.rule.attr, attr_name): for src in getattr(ctx.rule.attr, attr_name): @@ -65,8 +81,20 @@ def _coverage_scope_aspect_impl(target, ctx): if not f.path.startswith("external/") and f.is_source: direct_files.append(f.short_path) + if in_workspace: + # Post-processing identity of the public headers. With + # strip_include_prefix / include_prefix, Bazel compiles against a + # generated _virtual_includes/ symlink tree and the coverage mapping + # records THAT path, never the declared header label. Headers a + # workspace target vendors from an external repository are part of + # that target and therefore in scope as well + # (eclipse-score/baselibs#558). Other generated headers stay out. + for f in target[CcInfo].compilation_context.direct_public_headers: + if "/_virtual_includes/" in f.short_path or f.is_source: + direct_files.append(_workspace_relative(f)) + # Only collect workspace-internal labels and archives - if not str(target.label).startswith("@@") or str(target.label).startswith("@@//"): + if in_workspace: # Collect .a archive files for baseline coverage. for linker_input in target[CcInfo].linking_context.linker_inputs.to_list(): for lib in linker_input.libraries: diff --git a/score_coverage/reporter.py b/score_coverage/reporter.py index 7e9eb29..d3e0b14 100644 --- a/score_coverage/reporter.py +++ b/score_coverage/reporter.py @@ -115,8 +115,9 @@ def main(argv: list[str] | None = None) -> None: print(f"INFO: Using coverage allowlist with {len(allowlist_files)} source files.", file=sys.stderr) allowlist_set = set(allowlist_files) - # Get files covered by test binaries. - test_covered_files = get_covered_files(llvm_bin_path, sorted_objects, str(merged_profdata), workspace_root) + # Get files covered by test binaries (raw covmap path -> normalized name). + test_covered = get_covered_files(llvm_bin_path, sorted_objects, str(merged_profdata), workspace_root) + test_covered_files = set(test_covered.values()) print(f"INFO: Test binaries cover {len(test_covered_files)} files.", file=sys.stderr) # Get files from baseline archives via a SEPARATE llvm-cov run. @@ -124,9 +125,11 @@ def main(argv: list[str] | None = None) -> None: # causes some files to vanish (suspected llvm-cov deduplication issue). # Some archives may have oversized coverage mappings ("malformed coverage # data"), so we iteratively remove bad ones. + baseline_covered: dict[str, str] = {} baseline_files = set() if baseline_objects: - baseline_files = get_covered_files(llvm_bin_path, baseline_objects, None, workspace_root) + baseline_covered = get_covered_files(llvm_bin_path, baseline_objects, None, workspace_root) + baseline_files = set(baseline_covered.values()) print(f"INFO: Baseline archives contain {len(baseline_files)} files.", file=sys.stderr) # Files only in baseline archives (not in any test binary). @@ -142,9 +145,14 @@ def main(argv: list[str] | None = None) -> None: # The _filter_lcov function will filter to only baseline-only files. baseline_only_archives = list(baseline_objects) - # Union of test + baseline for exclude-set calculation. + # Union of test + baseline for exclude-set calculation. A generated + # header (virtual includes) covered by a test binary also appears in + # the baseline archive under another configuration prefix; that raw + # variant would show up as a second, 0% row and is excluded here. all_covered_files = test_covered_files | baseline_files - files_to_exclude = all_covered_files - allowlist_set + files_to_exclude = (all_covered_files - allowlist_set) | redundant_baseline_variants( + test_covered, baseline_covered + ) filter_regexes = [re.escape(f) + "$" for f in sorted(files_to_exclude)] print(f"INFO: Excluding {len(filter_regexes)} files not in allowlist.", file=sys.stderr) else: @@ -227,18 +235,44 @@ def show_html(objects: list[str]) -> None: print(f"INFO: Coverage reporter completed. Output: {args.output_file}", file=sys.stderr) +# Configuration-specific root of a generated file's exec path, e.g. +# "bazel-out/k8-fastbuild/bin/" or "bazel-out/k8-opt-exec-ST-/bin/". Headers +# behind strip_include_prefix are compiled from such a _virtual_includes/ tree +# and the coverage mapping records that path; the scope allowlist carries the +# configuration-agnostic short_path, so both sides are normalized to it. +_BAZEL_OUT_CONFIG_RE = re.compile(r"^bazel-out/[^/]+/bin/") + + +def strip_config_prefix(path: str) -> str: + """Drop a leading ``bazel-out//bin/`` from a workspace-relative path.""" + return _BAZEL_OUT_CONFIG_RE.sub("", path, count=1) + + +def redundant_baseline_variants(test_covered: dict[str, str], baseline_covered: dict[str, str]) -> set[str]: + """Raw baseline paths whose normalized file is already covered by a test binary. + + Both dicts map raw covmap paths to normalized names. A plain source file + has the same raw path in both, so nothing is returned for it; a generated + header differs only in the configuration prefix, and its baseline variant + would otherwise appear as a second, spurious 0 % entry. + """ + covered_names = set(test_covered.values()) + return {raw for raw, name in baseline_covered.items() if name in covered_names and raw not in test_covered} + + def _make_lcov_paths_relative(lcov_content: str, workspace_root: str) -> str: """Rewrite absolute SF: paths under workspace_root to workspace-relative ones. - Paths outside the workspace (external deps that survived filtering) are - left unchanged. + A configuration-specific ``bazel-out//bin/`` prefix (generated + virtual-includes headers) is dropped as well. Paths outside the workspace + (external deps that survived filtering) are left unchanged. """ prefix = workspace_root if workspace_root.endswith("/") else workspace_root + "/" sf_prefix = "SF:" + prefix lines = [] for line in lcov_content.splitlines(keepends=True): if line.startswith(sf_prefix): - lines.append("SF:" + line[len(sf_prefix) :]) + lines.append("SF:" + strip_config_prefix(line[len(sf_prefix) :])) else: lines.append(line) return "".join(lines) @@ -261,7 +295,7 @@ def _make_html_paths_relative(html_dir: Path, workspace_root: str) -> None: def _repl(match: "re.Match") -> str: title = match.group(2) if title.startswith(prefix): - title = title[len(prefix) :] + title = strip_config_prefix(title[len(prefix) :]) return match.group(1) + title + match.group(3) for page in html_dir.rglob("*.html"): @@ -303,10 +337,15 @@ def get_covered_files( objects: list[str], instr_profile: str | None, workspace_root: str, -) -> set: +) -> dict[str, str]: """Run a quick llvm-cov report to discover all files with coverage data. - Returns a set of workspace-relative file paths. + Returns a dict mapping each raw file path as llvm-cov displays it (after + stripping the workspace root or ``/proc/self/cwd/``) to its normalized, + configuration-agnostic form. The raw form is what ``--ignore-filename-regex`` + must match to suppress one specific compiled variant of a generated file; + the normalized form is what the allowlist and the test/baseline set + arithmetic compare against. """ cmd = [ str(llvm_bin_path), @@ -323,9 +362,9 @@ def get_covered_files( result = run_command(cmd) if result.returncode != 0: - return set() + return {} - files = set() + files: dict[str, str] = {} in_files = False for line in result.stdout.splitlines(): if line.startswith("---"): @@ -347,7 +386,7 @@ def get_covered_files( if filename.startswith(prefix): filename = filename[len(prefix) :] break - files.add(filename) + files[filename] = strip_config_prefix(filename) return files diff --git a/score_coverage/tests/reporter_test.py b/score_coverage/tests/reporter_test.py index ab21d9f..c3ff025 100644 --- a/score_coverage/tests/reporter_test.py +++ b/score_coverage/tests/reporter_test.py @@ -211,6 +211,7 @@ def _write_tool(path: Path, body: str) -> Path: /proc/self/cwd/src/a.cpp 4 1 75.00% 1 0 100.00% 10 2 80.00% /ws/src/b.cpp 2 2 0.00% 1 1 0.00% 5 5 0.00% rust/lib.rs 3 0 100.00% 2 0 100.00% 8 0 100.00% +bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/api.h 2 0 100.00% 1 0 100.00% 3 0 100.00% ------------------------------------------------------------------------------------------------------- TOTAL 9 3 66.67% 4 1 75.00% 23 7 69.57% """ @@ -435,7 +436,16 @@ def _last(self): def test_get_covered_files_normalises_paths(self): with redirect_stderr(io.StringIO()): files = reporter.get_covered_files(self.cov, ["/o/a.a", "/o/b.a"], None, "/ws/") - self.assertEqual(files, {"src/a.cpp", "src/b.cpp", "rust/lib.rs"}) + # raw (workspace-root / /proc/self/cwd/ stripped) -> normalized (config prefix stripped) + self.assertEqual( + files, + { + "src/a.cpp": "src/a.cpp", + "src/b.cpp": "src/b.cpp", + "rust/lib.rs": "rust/lib.rs", + "bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/api.h": "src/_virtual_includes/v/api.h", + }, + ) argv = self._last() self.assertEqual(argv[:3], ["report", "--path-equivalence=/proc/self/cwd/,/ws/", "--empty-profile"]) self.assertEqual(argv[3:], ["/o/a.a", "--object", "/o/b.a"]) @@ -596,5 +606,77 @@ def test_empty_allowlist_is_an_error(self): reporter.main(self._argv()) +@verifies("tool_req__coverage_scope_transitive", "tool_req__coverage_report_relative_paths") +class ConfigPrefixTest(unittest.TestCase): + """Generated virtual-includes headers carry a configuration-specific bazel-out prefix.""" + + def test_strip_config_prefix(self): + self.assertEqual( + reporter.strip_config_prefix("bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/x.h"), + "src/_virtual_includes/v/x.h", + ) + self.assertEqual( + reporter.strip_config_prefix("bazel-out/k8-opt-exec-ST-db392155ee03/bin/src/_virtual_includes/v/x.h"), + "src/_virtual_includes/v/x.h", + ) + self.assertEqual(reporter.strip_config_prefix("src/a.cpp"), "src/a.cpp") + self.assertEqual( + reporter.strip_config_prefix("external/flatbuffers+/include/flatbuffers/base.h"), + "external/flatbuffers+/include/flatbuffers/base.h", + ) + # only a leading prefix is stripped, once + unchanged = "x/bazel-out/k8-fastbuild/bin/y.h" + self.assertEqual(reporter.strip_config_prefix(unchanged), unchanged) + + def test_lcov_and_html_paths_drop_the_config_prefix(self): + lcov = "SF:/ws/bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/x.h\nDA:1,1\nend_of_record\n" + self.assertEqual( + reporter._make_lcov_paths_relative(lcov, "/ws"), + "SF:src/_virtual_includes/v/x.h\nDA:1,1\nend_of_record\n", + ) + with tempfile.TemporaryDirectory() as tmp: + page = Path(tmp) / "p.html" + page.write_text( + "
/ws/bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/x.h"
+                "
", + encoding="utf-8", + ) + reporter._make_html_paths_relative(Path(tmp), "/ws") + self.assertIn("
src/_virtual_includes/v/x.h
", page.read_text(encoding="utf-8")) + + +@verifies("tool_req__coverage_report_baseline_zero", "tool_req__coverage_report_allowlist") +class RedundantBaselineVariantsTest(unittest.TestCase): + """A header covered by a test must not get a second 0 % row from the baseline archive.""" + + def test_generated_header_variant_from_baseline_is_redundant(self): + test_covered = { + "bazel-out/k8-fastbuild/bin/src/_virtual_includes/v/x.h": "src/_virtual_includes/v/x.h", + "src/a.cpp": "src/a.cpp", + } + baseline = { + "bazel-out/k8-opt-exec-ST-1/bin/src/_virtual_includes/v/x.h": "src/_virtual_includes/v/x.h", + "src/a.cpp": "src/a.cpp", + "src/uncovered.cpp": "src/uncovered.cpp", + } + self.assertEqual( + reporter.redundant_baseline_variants(test_covered, baseline), + {"bazel-out/k8-opt-exec-ST-1/bin/src/_virtual_includes/v/x.h"}, + ) + + def test_plain_sources_and_untested_files_are_kept(self): + test_covered = {"src/a.cpp": "src/a.cpp"} + baseline = { + "src/a.cpp": "src/a.cpp", + "bazel-out/k8-opt-exec-ST-1/bin/src/_virtual_includes/u/y.h": "src/_virtual_includes/u/y.h", + } + # a.cpp: identical raw path -> not redundant; y.h: not covered by any test -> kept as baseline + self.assertEqual(reporter.redundant_baseline_variants(test_covered, baseline), set()) + + def test_empty_inputs(self): + self.assertEqual(reporter.redundant_baseline_variants({}, {}), set()) + self.assertEqual(reporter.redundant_baseline_variants({"a": "a"}, {}), set()) + + if __name__ == "__main__": unittest.main() diff --git a/score_coverage/tests/starlark/coverage_scope_tests.bzl b/score_coverage/tests/starlark/coverage_scope_tests.bzl index 1e3be2d..8761ebe 100644 --- a/score_coverage/tests/starlark/coverage_scope_tests.bzl +++ b/score_coverage/tests/starlark/coverage_scope_tests.bzl @@ -118,6 +118,22 @@ def _test_generated_sources_are_excluded_impl(env, target): # The archive of the library with the generated source is still a baseline object. _objects(env, target).contains("libwith_generated.a") +# --- strip_include_prefix: virtual-includes identity ---------------------- + +def _test_virtual_include_headers_are_in_scope(name): + coverage_scope(name = name + "_subject", testonly = True, deps = [_FIX + ":vendored"]) + analysis_test(name = name, impl = _test_virtual_include_headers_are_in_scope_impl, target = name + "_subject") + +def _test_virtual_include_headers_are_in_scope_impl(env, target): + # Both the declared header and the generated path the compiler records + # (eclipse-score/baselibs#558) are listed. + _allowlist(env, target).equals( + "\n".join([ + _PKG + "/fixtures/_virtual_includes/vendored/vendored/api.h", + _PKG + "/fixtures/vendor/include/vendored/api.h", + ]) + "\n", + ) + # --- Rust: rust_library (CcInfo) and rust_binary (CrateInfo only) ----------- def _test_rust_library_sources_and_archive(name): @@ -170,6 +186,7 @@ def coverage_scope_test_suite(name): _test_shared_dependency_listed_once, _test_header_only_library_has_no_archive, _test_generated_sources_are_excluded, + _test_virtual_include_headers_are_in_scope, _test_rust_library_sources_and_archive, _test_rust_binary_collects_crate_sources_and_executable, _test_output_groups, diff --git a/score_coverage/tests/starlark/fixtures/BUILD b/score_coverage/tests/starlark/fixtures/BUILD index 1ba42de..4e1d44a 100644 --- a/score_coverage/tests/starlark/fixtures/BUILD +++ b/score_coverage/tests/starlark/fixtures/BUILD @@ -78,3 +78,12 @@ rust_binary( tags = MANUAL, deps = [":rlib"], ) + +# Header behind strip_include_prefix: the compiler (and the coverage mapping) +# see it through the generated _virtual_includes/ tree. +cc_library( + name = "vendored", + hdrs = ["vendor/include/vendored/api.h"], + strip_include_prefix = "vendor/include", + tags = MANUAL, +) diff --git a/score_coverage/tests/starlark/fixtures/vendor/include/vendored/api.h b/score_coverage/tests/starlark/fixtures/vendor/include/vendored/api.h new file mode 100644 index 0000000..b4d0052 --- /dev/null +++ b/score_coverage/tests/starlark/fixtures/vendor/include/vendored/api.h @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +// Fixture for the coverage_scope analysis tests; only analyzed. +inline int api() { return 4; } From 5d426f086677fba32bfe8bd1b119e6f480fe3e6d Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:00:05 +0300 Subject: [PATCH 2/2] Address review: external fixture, exclusion wording, license target - Analysis tests: a local module (score_coverage/tests/starlark/ external_fixture, wired via local_path_override) provides a header that two workspace fixture targets vendor, once behind strip_include_prefix and once directly. The tests assert both identities in the allowlist: external/coverage_external_fixture+/include/ext/ext.h and the _virtual_includes/ path. This exercises the ../ -> external/ conversion that baselibs#558 depends on. - tool_req__coverage_scope_excludes no longer contradicts tool_req__coverage_scope_transitive: external targets are not traversed and generated files are not listed, except the two header identities the transitive requirement names. - //:license-check: alias to a dash_license_checker in //tools over the Python requirements lock, so the cicd-workflows license check has its target. The root package still loads only runtime rules. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- .bazelignore | 2 ++ BUILD | 7 +++++ MODULE.bazel | 9 +++++++ docs/requirements/tool_requirements.rst | 7 +++-- docs/verification/verification_report.rst | 6 ++--- .../tests/starlark/coverage_scope_tests.bzl | 27 +++++++++++++++++++ .../tests/starlark/external_fixture/BUILD | 14 ++++++++++ .../starlark/external_fixture/MODULE.bazel | 18 +++++++++++++ .../external_fixture/include/ext/ext.h | 14 ++++++++++ score_coverage/tests/starlark/fixtures/BUILD | 16 +++++++++++ tools/BUILD | 10 ++++++- 11 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 score_coverage/tests/starlark/external_fixture/BUILD create mode 100644 score_coverage/tests/starlark/external_fixture/MODULE.bazel create mode 100644 score_coverage/tests/starlark/external_fixture/include/ext/ext.h diff --git a/.bazelignore b/.bazelignore index c6bb27f..819d1f1 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,3 +1,5 @@ # Nested consumer workspace with its own MODULE.bazel; built by # integration_tests/run_integration_test.sh, not as part of //... integration_tests +# Local module used as an external-repository fixture by the analysis tests. +score_coverage/tests/starlark/external_fixture diff --git a/BUILD b/BUILD index 9ddfc0c..1d800cd 100644 --- a/BUILD +++ b/BUILD @@ -83,3 +83,10 @@ cc_feature( args = [":runtime_relocation_args"], feature_name = "enable_llvm_coverage_for_death_tests", ) + +# Entry point expected by the cicd-workflows license check. An alias only: the +# implementation (dev dependency) lives in //tools. +alias( + name = "license-check", + actual = "//tools:license-check", +) diff --git a/MODULE.bazel b/MODULE.bazel index f40ce2a..e1cfd46 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -68,6 +68,15 @@ bazel_dep(name = "buildifier_prebuilt", version = "8.5.1", dev_dependency = True # Starlark analysis tests for the coverage_scope rule/aspect, incl. Rust fixtures # built with the standard Ferrocene toolchain (registered in .bazelrc). bazel_dep(name = "rules_testing", version = "0.9.0", dev_dependency = True) + +# External-repository header fixture for the analysis tests (a workspace target +# vendoring a header from another module, eclipse-score/baselibs#558). +bazel_dep(name = "coverage_external_fixture", version = "0.0.0", dev_dependency = True) +local_path_override( + module_name = "coverage_external_fixture", + path = "score_coverage/tests/starlark/external_fixture", +) + bazel_dep(name = "score_toolchains_rust", version = "0.10.0", dev_dependency = True) ############################################################################### diff --git a/docs/requirements/tool_requirements.rst b/docs/requirements/tool_requirements.rst index 220be31..f7e830e 100644 --- a/docs/requirements/tool_requirements.rst +++ b/docs/requirements/tool_requirements.rst @@ -56,8 +56,11 @@ Scope :safety: QM :satisfies: stkh_req__coverage__uc_scope_completeness - ``score_coverage_scope`` shall not list files from external repositories or - generated files in the allowlist. + ``score_coverage_scope`` shall not traverse external targets and shall not + list their files, and shall not list generated files, with one exception + each: headers a workspace target declares from an external repository, and + the ``_virtual_includes/`` identities of a workspace target's public headers + (see :need:`tool_req__coverage_scope_transitive`). .. tool_req:: Baseline objects accompany the scope :id: tool_req__coverage_scope_baseline_objects diff --git a/docs/verification/verification_report.rst b/docs/verification/verification_report.rst index 1d3b250..54ea9c1 100644 --- a/docs/verification/verification_report.rst +++ b/docs/verification/verification_report.rst @@ -67,8 +67,8 @@ Test inventory * - ``//score_coverage/tests:coverage_summary_test`` - 17 - summary_first - * - ``//score_coverage/tests/starlark:coverage_scope_tests`` (9 analysis tests) - - 9 + * - ``//score_coverage/tests/starlark:coverage_scope_tests`` (11 analysis tests) + - 11 - scope_transitive, scope_excludes, scope_baseline_objects * - ``integration_tests/run_integration_test.sh`` (15 end-to-end checks) - 15 @@ -95,7 +95,7 @@ generated link: - :need:`tool_req__coverage_scope_transitive`, :need:`tool_req__coverage_scope_excludes` and - :need:`tool_req__coverage_scope_baseline_objects` are verified by the nine + :need:`tool_req__coverage_scope_baseline_objects` are verified by the eleven Starlark analysis tests in ``score_coverage/tests/starlark`` (rules_testing produces no test properties). - :need:`tool_req__coverage_validation_ground_truth` is verified by the diff --git a/score_coverage/tests/starlark/coverage_scope_tests.bzl b/score_coverage/tests/starlark/coverage_scope_tests.bzl index 8761ebe..2adf03f 100644 --- a/score_coverage/tests/starlark/coverage_scope_tests.bzl +++ b/score_coverage/tests/starlark/coverage_scope_tests.bzl @@ -134,6 +134,31 @@ def _test_virtual_include_headers_are_in_scope_impl(env, target): ]) + "\n", ) +# --- headers vendored from an external repository --------------------------- + +def _test_external_vendored_header_behind_strip_prefix(name): + coverage_scope(name = name + "_subject", testonly = True, deps = [_FIX + ":vendored_external"]) + analysis_test(name = name, impl = _test_external_vendored_header_behind_strip_prefix_impl, target = name + "_subject") + +def _test_external_vendored_header_behind_strip_prefix_impl(env, target): + # The declared header is an external source file ("..//..." short_path, + # listed as "external//..."), and the compiler records the generated + # virtual-includes path; both identities are in scope. External TARGETS are + # still not traversed (the fixture module has none in deps). + _allowlist(env, target).equals( + "\n".join([ + "external/coverage_external_fixture+/include/ext/ext.h", + _PKG + "/fixtures/_virtual_includes/vendored_external/ext/ext.h", + ]) + "\n", + ) + +def _test_external_vendored_header_plain(name): + coverage_scope(name = name + "_subject", testonly = True, deps = [_FIX + ":vendored_external_plain"]) + analysis_test(name = name, impl = _test_external_vendored_header_plain_impl, target = name + "_subject") + +def _test_external_vendored_header_plain_impl(env, target): + _allowlist(env, target).equals("external/coverage_external_fixture+/include/ext/ext.h\n") + # --- Rust: rust_library (CcInfo) and rust_binary (CrateInfo only) ----------- def _test_rust_library_sources_and_archive(name): @@ -187,6 +212,8 @@ def coverage_scope_test_suite(name): _test_header_only_library_has_no_archive, _test_generated_sources_are_excluded, _test_virtual_include_headers_are_in_scope, + _test_external_vendored_header_behind_strip_prefix, + _test_external_vendored_header_plain, _test_rust_library_sources_and_archive, _test_rust_binary_collects_crate_sources_and_executable, _test_output_groups, diff --git a/score_coverage/tests/starlark/external_fixture/BUILD b/score_coverage/tests/starlark/external_fixture/BUILD new file mode 100644 index 0000000..8fe436f --- /dev/null +++ b/score_coverage/tests/starlark/external_fixture/BUILD @@ -0,0 +1,14 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +exports_files(["include/ext/ext.h"]) diff --git a/score_coverage/tests/starlark/external_fixture/MODULE.bazel b/score_coverage/tests/starlark/external_fixture/MODULE.bazel new file mode 100644 index 0000000..0ad478b --- /dev/null +++ b/score_coverage/tests/starlark/external_fixture/MODULE.bazel @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# A minimal external module for the coverage_scope analysis tests: a workspace +# target vendors its header (eclipse-score/baselibs#558). Wired in through +# local_path_override in the root MODULE.bazel; excluded from //... via +# .bazelignore. +module(name = "coverage_external_fixture") diff --git a/score_coverage/tests/starlark/external_fixture/include/ext/ext.h b/score_coverage/tests/starlark/external_fixture/include/ext/ext.h new file mode 100644 index 0000000..fac3cd9 --- /dev/null +++ b/score_coverage/tests/starlark/external_fixture/include/ext/ext.h @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +// Vendored header fixture for the coverage_scope analysis tests; only analyzed. +inline int ext() { return 5; } diff --git a/score_coverage/tests/starlark/fixtures/BUILD b/score_coverage/tests/starlark/fixtures/BUILD index 4e1d44a..aff50de 100644 --- a/score_coverage/tests/starlark/fixtures/BUILD +++ b/score_coverage/tests/starlark/fixtures/BUILD @@ -87,3 +87,19 @@ cc_library( strip_include_prefix = "vendor/include", tags = MANUAL, ) + +# A workspace target whose headers are vendored from an external module, +# once behind strip_include_prefix (as //score/flatbuffers:flatbufferscpp in +# baselibs) and once declared directly. +cc_library( + name = "vendored_external", + hdrs = ["@coverage_external_fixture//:include/ext/ext.h"], + strip_include_prefix = "/include", + tags = MANUAL, +) + +cc_library( + name = "vendored_external_plain", + hdrs = ["@coverage_external_fixture//:include/ext/ext.h"], + tags = MANUAL, +) diff --git a/tools/BUILD b/tools/BUILD index d7ce692..0f17114 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -12,7 +12,7 @@ # ******************************************************************************* load("@rules_python//python:defs.bzl", "py_binary", "py_test") -load("@score_tooling//:defs.bzl", "copyright_checker") +load("@score_tooling//:defs.bzl", "copyright_checker", "dash_license_checker") load("@score_tooling//third_party/format:macros.bzl", "use_format_targets") # Repository hygiene (copyright headers, formatting). Kept out of the root @@ -64,3 +64,11 @@ py_test( imports = ["."], main = "self_coverage_gate_test.py", ) + +# Eclipse Dash license check over the Python dependencies (cicd-workflows +# license-check runs `bazel run //:license-check`; the root BUILD aliases here). +dash_license_checker( + src = "//score_coverage:requirements_3_12.txt", + file_type = "requirements", + visibility = ["//visibility:public"], +)