Architecture#
score_coverage architecture
|
status: draft
security: NO
safety: ASIL_B
|
||||
The pipeline hooks into Bazel’s two coverage extension points,
--coverage_output_generator and --coverage_report_generator, and adds a
justification and gating layer on top. It has two phases and, in phase 1, two
backends that produce the same report zip:
llvm: Clang and rustc coverage mapping, read with
llvm-cov; Linux host tests, C++ and Rust.gcov: GCC and QNX QCC
.gcdacounters, read with the toolchain’sgcov; C++ only. On QNX the tests run inside QEMU throughscore_qnx_unit_tests, which carries the counters back to the host, and Bazel’s own per-test collector turns them into LCOV.
Phase 1: collection#
The llvm_cov bazelrc config, copied by the consumer from the integration
workspace, does four things:
Swaps the compilers. C++ is compiled with a hermetic Clang/LLVM toolchain instead of GCC; Rust with the Ferrocene toolchain of
score_toolchains_rust, which has LLVM coverage tools attached. Both emit the same covmap format.Turns on instrumentation.
--experimental_use_llvm_covmapplus thecoveragefeature for C++;rules_rustadds-Cinstrument-coverageto rustc once the toolchain declares coverage tools. Runtime counter relocation (-mllvm -runtime-counter-relocationvia thecc_feature,-Cllvm-args=-runtime-counter-relocationfor Rust) enables continuous mode so coverage survives abnormal termination. Rust branch regions need-Zcoverage-options=branchon a rolling Ferrocene.Installs the per-test tool.
merger.pymerges the test’sprofrawfiles withllvm-profdata, records the instrumented objects, and zips both as the test’scoverage.dat.Installs the final tool. The consumer’s
score_coverage_reportertarget wrapsreporter.pywith the scope, workspace root and LLVM tool labels. The reporter merges all per-test profiles and runsllvm-covthree times:show(HTML),export(LCOV),report(text).
Scope. Covmap instruments everything. Filtering happens at report time
through the allowlist written by score_coverage_scope: an aspect walks the
dependency graph from the listed production targets and collects every source
file a workspace target declares (including headers it vendors from an
external repository). Everything else, test sources, googletest, external
dependencies, headers a wrapper rule only forwards, is excluded. For headers
exposed through strip_include_prefix / include_prefix the aspect also
writes a path map from the generated _virtual_includes/ path the compiler
records to the declared header, and it exports the source files themselves.
Sources. The reporter does not read sources through the workspace directory: generated headers and external repositories are not there at report time. It links every in-scope file from its runfiles into a staging directory laid out like the coverage mapping expects, points llvm-cov at that directory, and afterwards files the HTML pages under canonical paths so the archive is machine-independent and every index link resolves.
Baseline. A file that no test executes produces no profile data. The scope
aspect therefore also collects the compiled archives and executables, and the
reporter runs llvm-cov --empty-profile over them, so untested files show up
with exact 0 % entries whose denominators come from the compiler’s coverage
map. Rust rlibs are expanded into their object members first because their
leading lib.rmeta member makes llvm-cov reject the archive.
Phase 1, gcov backend#
The gcov backend exists for toolchains that cannot emit LLVM coverage mapping: GCC on Linux and, the reason it was built, QCC on QNX. It changes the collection, not the report.
Compilers. The
coveragefeature of the S-CORE GCC and QCC toolchains adds-fprofile-arcs -ftest-coverage; the compiler writes a.gcnonotes file per translation unit and the instrumented binary writes.gcdacounters when it exits. rustc cannot produce either, so Rust sources in scope are reported as not instrumentable on this backend.Execution and transport. On Linux the test writes its
.gcdaunder Bazel’sCOVERAGE_DIR. On QNX the test runs inside a QEMU micro-VM (score_qnx_unit_tests,--run_under);GCOV_PREFIXpoints the counters at a guest directory, the guest tars them at exit, and the host runner extracts the archive intoCOVERAGE_DIR. From there both are the same.Per-test collection stays Bazel’s. Bazel’s
collect_coverage.shruns the toolchain’sgcovover the counters and itslcov_mergerwrites one LCOV file per test. Two properties of that collector shape the backend: it maps generated_virtual_includes/paths back to the declared header itself, and it keeps only files of its instrumented-files manifest, which never lists sources of external repositories, so a header vendored from an external repository has no data on this backend even when a test executes it. Tests are instrumented as well (--instrument_test_targets) so header-only code that only a test translation unit instantiates is measured; the scope allowlist still drops the test sources.Final report.
gcov_reporter.pysums the per-test LCOV records per file (the same semantics as merging profiles on the LLVM side), applies the scope through the shared selection logic, and adds the zero-coverage baseline:gcov --json-formatover the.gcnoof every in-scope translation unit without its.gcdayields every line and branch at zero. The.gcnofiles come fromInstrumentedFilesInfothrough the scope aspect, listed in<name>_gcno.txtand carried in the reporter’s runfiles. gcovr renders the HTML from the merged data (one page per file, under the canonical path) and the text summary; LCOV andunmapped_files.txtare written as on the LLVM side.
Line semantics differ between the backends and are recorded in the
integration ground truth: gcov counts only lines the compiler emitted code
for (no closing braces, no unused inline functions), while LLVM’s mapping
keeps unused functions at 0 %. The justification and gating layer is
backend-agnostic; effective_coverage.py recognises gcovr’s HTML layout.
Phase 2: report generation and gate#
generate_coverage_html.py unpacks the HTML from the zip and, when a
justification YAML is given, calls justify.py (YAML plus in-code markers to
a manifest of justified lines) and effective_coverage.py (recolours justified
lines, computes raw and effective figures, flags stale justifications, writes
report.json and summary.txt). The markdown summary is written next, then
the gate compares the unrounded gated percentage against COVERAGE_THRESHOLD.
Optionally the HTML, LCOV, justification report and JUnit XMLs are assembled
into an artifacts tree.
Exit code 2 is reserved for runs without a verdict, so a broken report, a bad threshold or a tool failure can never look like a pass.
Module and consumer split#
Lives in |
Role |
|---|---|
|
per-test profraw to profdata; C++ |
|
final merge, llvm-cov show/export/report, allowlist filtering,
|
|
gcov backend: per-test LCOV merge, |
|
the scope aspect and rule (CcInfo and CrateInfo) |
|
the consumer-facing |
|
justification, summary and gating layer |
|
|
Lives in the consumer repository |
Why it cannot move |
|---|---|
|
names the repository’s production targets |
|
carries the repository’s LLVM tool labels (or, with
|
|
reviewed, repository-specific engineering arguments |
MODULE.bazel toolchain blocks |
LLVM and Ferrocene pins are per-repository decisions |
the |
bazelrc cannot be imported across modules |
Two wiring details make the external hosting work: every path in the generated
reporter launcher uses rlocation form because it mixes files from the consumer
(_main), score_coverage and toolchain repositories, and the baseline
manifest is resolved against _main explicitly because its entries are
consumer files.
Design decisions#
Report-time filtering on top of full instrumentation. The scope allowlist decides what the report shows;
--instrumentation_filteris set to the module’s root package (^//score[/:]) so that every target is compiled with counters. Bazel’s guessed default covers only the packages of the test targets: a target outside it is compiled without counters unless a direct dep is instrumented (both backends), and on the gcov backend Bazel’s collector additionally drops the counters of every target outside the filter. The reporters warn when files without test data sit in a directory that is tested from atest/ortests/subdirectory (ERR-13).Fail loud, never fail green. Every input problem ends in exit 2. The gate compares unrounded values and floors displayed percentages.
Gate on the LCOV, not on llvm-cov’s text summary. The text summary omits baseline-only files; the LCOV includes them.
In-process tool calls.
generate_coverage_htmlimports the justification tools instead of nestingbazel run; this keeps one process, one exit code and testable seams.One report format for both backends. The gcov backend replaces only the final report step and produces the same zip layout, so phase 2, the archive, the job summary and the qualification evidence are shared. Bazel’s per-test gcov collector is kept as it is: it is where the QNX transport hands over, and re-implementing it would move the
.gcdahandling into the qualified tool for no gain.