Add the gcov backend for QNX on-target coverage - #10
Conversation
QCC is GCC-based and cannot emit LLVM coverage mapping, and QNX tests run inside QEMU. This adds a second backend that changes the collection, not the report (tooling issue #427, track 1): - score_qnx_unit_tests' run_under_qnx runner brings the .gcda counters back from the guest; Bazel's own per-test collector (gcov + lcov_merger) turns them into LCOV; the new gcov_reporter.py is the --coverage_report_generator: it sums per-test records per file, applies the scope through the selection shared with the LLVM reporter, adds a zero-coverage baseline by running gcov over the .gcno notes of every in-scope translation unit, renders HTML and text summary with gcovr 8.6 (new pip dependency), and writes the same zip layout, so phase 2 (justifications, summary, gate, archive) is unchanged. - score_coverage_scope collects the .gcno files through InstrumentedFilesInfo (<name>_gcno.txt, gcno/gcno_files output groups). score_coverage_reporter gained backend = "llvm"|"gcov" and gcov = <toolchain gcov label>; the LLVM tools are required only for the LLVM backend. Existing consumers need no change. - Rust sources are reported as not-instrumented on this backend (new unmapped category); headers vendored from external repositories have no data because Bazel's collector filters them out (documented). Two defects found on the way, both fixed: - effective_coverage's gcovr parser matched across table rows; real gcovr pages have bare rows for non-instrumented lines, so every line status was attributed to the previous instrumented line and no justification ever applied to a gcovr report. - the self-coverage gate double counted branches when two test targets import the same module: coverage.py's BRDA branch field encodes whether the arc was taken, Bazel's merger keeps both variants. Arcs are now keyed by (line, block). Validation: the integration workspace gained the S-CORE GCC 12.2.0 toolchain, a coverage:gcov config, a hand-derived expected_lcov_gcov.dat and gcov checks (gate, ground truth, every gcovr link opens, justification applied, categories); 20 new unit tests; one new analysis test. GCC on Linux takes exactly the collection path QCC takes on QNX; the QEMU transport is validated on a consumer. Docs: architecture (backend section and diagram), tool requirements (scope_gcno, backend_select, gcov_merge, gcov_baseline, gcov_html), potential errors ERR-11/ERR-12, constraint CSTR-11, user manual step 4b, known problems, release notes 0.3.0, verification report. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
License Check Results🚀 The license check job ran with the Bazel command: bazel run //:license-checkStatus: Click to expand output |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Canonical path variants can lose per-test coverage counts, and the documented Rust build-setting label is invalid.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds a gcov backend for GCC/QNX coverage while preserving the existing report format and gating pipeline.
Changes:
- Adds gcov collection, baseline generation, merging, and gcovr reporting.
- Extends Bazel scope/wrapper rules and integration coverage.
- Fixes gcovr justification parsing and self-coverage branch deduplication.
| File | Description |
|---|---|
tools/self_coverage_gate.py |
Deduplicates detailed LCOV records. |
tools/self_coverage_gate_test.py |
Tests LCOV deduplication. |
score_coverage/tests/starlark/coverage_scope_tests.bzl |
Tests gcno outputs. |
score_coverage/tests/gcov_reporter_test.py |
Tests the gcov reporter. |
score_coverage/tests/effective_coverage_test.py |
Adds gcovr markup regression coverage. |
score_coverage/tests/BUILD |
Registers gcov reporter tests. |
score_coverage/requirements.in |
Adds gcovr. |
score_coverage/requirements_3_12.txt |
Locks gcovr dependencies. |
score_coverage/reporter_wrapper.bzl |
Selects LLVM or gcov backend. |
score_coverage/generate_coverage_html.py |
Generalizes report errors. |
score_coverage/gcov_reporter.py |
Implements gcov report generation. |
score_coverage/effective_coverage.py |
Constrains gcovr row parsing. |
score_coverage/coverage_summary.py |
Adds not-instrumented category. |
score_coverage/coverage_scope.bzl |
Exports gcno baseline inputs. |
score_coverage/BUILD |
Defines gcov reporter targets. |
README.md |
Documents dual backends. |
integration_tests/tools/coverage/BUILD |
Adds gcov wrapper target. |
integration_tests/run_integration_test.sh |
Exercises gcov end to end. |
integration_tests/MODULE.bazel.lock |
Updates integration dependencies. |
integration_tests/MODULE.bazel |
Adds GCC toolchain. |
integration_tests/expected_lcov_gcov.dat |
Defines gcov ground truth. |
integration_tests/.bazelrc |
Adds gcov coverage configuration. |
docs/verification/verification_report.rst |
Records gcov verification. |
docs/requirements/tool_requirements.rst |
Specifies gcov requirements. |
docs/requirements/potential_errors.rst |
Adds gcov failure modes. |
docs/release/release_notes.rst |
Documents release changes. |
docs/manual/user_manual.rst |
Adds QNX adoption instructions. |
docs/manual/known_problems.rst |
Documents backend limitations. |
docs/manual/constraints.rst |
Adds QNX transport constraints. |
docs/architecture/index.rst |
Describes gcov architecture. |
defs.bzl |
Extends the public reporter macro. |
BUILD |
Exports the gcov reporter alias. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| merged: dict[str, FileRecord] = {} | ||
| for raw, name in selection.staged.items(): | ||
| if raw in tested: | ||
| merged.setdefault(name, FileRecord()).add(tested[raw]) | ||
| for raw, name in selection.staged.items(): | ||
| if raw in baseline and name not in merged: | ||
| merged.setdefault(name, FileRecord()).add(baseline[raw].zeroed()) | ||
| return merged |
There was a problem hiding this comment.
Valid, thanks. For the gcov backend the merge is ours (unlike llvm-cov, which needs one path per file), so dropping the duplicate raw variants lost the second test's counts. Fixed in 55bf112: test and baseline records are now summed per canonical name before the selection (merge_by_name), with a unit test for a header recorded under its declared path by one test and under a _virtual_includes path by another.
| coverage:qnx --noexperimental_use_llvm_covmap | ||
| coverage:qnx --noexperimental_generate_llvm_lcov | ||
| coverage:qnx --test_env=GENERATE_LLVM_LCOV --test_env=COVERAGE_GCOV_PATH --test_env=LLVM_PROFILE_CONTINUOUS_MODE | ||
| coverage:qnx --@rules_rust//rust/settings:extra_rustc_flags= |
There was a problem hiding this comment.
Both settings exist in rules_rust: the repeatable extra_rustc_flag (singular, used by the coverage:llvm_cov block) and the list-typed extra_rustc_flags (plural). The line was not a typo, but you have a point that it was misleading: an empty value resets only the list-typed plural flag, and the singular one accumulates and cannot be reset at all (verified with bazel aquery on the integration workspace). A workspace following Step 4 keeps the LLVM rustc flags in their own config, so the QNX block does not need to reset anything. Removed the line and documented the reset for workspaces that put the flags on the bare coverage command (that is communication's setup, which uses the plural flag).
There was a problem hiding this comment.
hallucination
Found on communication: the QNX gcov reporter depends on the QNX SDP package, and bazel build //... on a Linux host without credentials fails on that download. The manual tag keeps wildcards away from the target; --coverage_report_generator still names it explicitly. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
The coverage pipeline that lived under quality/coverage (merger, reporter, reporter_wrapper, coverage_scope aspect, justify.py, effective_coverage.py, lcov_to_html.py, generate_coverage_html.sh) is the origin of the qualified S-CORE coverage tool, score_coverage (https://eclipse-score.github.io/coverage_tool/main/). This switches the repository to that module and deletes the copy: - quality/coverage/BUILD keeps only what is repository-specific: the coverage scope (unchanged dependable elements) and two report generators, LLVM for Linux and, new, gcov for QNX on target. - coverage.bazelrc: the default coverage command is the LLVM backend as before; --config=qnx now keeps Bazel's per-test gcov collector and points the final step at score_coverage's gcov reporter instead of Bazel's stock LCOV report, so the QNX report has the same layout, scope, 0 % baselines, justifications and gate as the Linux one. - workflows: the Linux coverage report is produced by @score_coverage//:generate_coverage_html (same archive name and layout, cpp_coverage_linux/); the QNX workflow gains a coverage step after the tests and uploads coverage_report_qnx. - MODULE.bazel: score_coverage dev dependency; rules_rust 0.68.2-score as required by it; the death-test coverage feature now comes from the module. gcovr leaves requirements.in (pyyaml stays, used elsewhere). - docs and the visibility-guard golden updated. Validated locally on //score/message_passing/... (Linux, LLVM): the scope allowlist is identical (613 files) and all 347 LCOV records are identical in covered/uncovered status to the previous pipeline (the only differences are execution counts of logging/timing code). The QNX run is validated in CI. VALIDATION ONLY: MODULE.bazel carries a git_override to the coverage_tool branch of eclipse-score/coverage_tool#10 for the gcov backend; replace it by the 0.3.0 registry release once published. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
|
Real QNX validation: baselibs PR #626 ran the gcov backend on bl-x86_64-qnx in CI (278 tests in QEMU, https://github.com/eclipse-score/baselibs/actions/runs/36388596870): 615 files reported, every index link opens, 81.59 % raw line coverage, baseline for 160 untested files from the gcno notes, categories as designed (50 Rust |
The gcov reporter dropped every raw path variant of a canonical file except one before merging. When one test reports a header by its declared path and another test through a _virtual_includes path, the second test's counts were lost. Merge test and baseline records by canonical name before the selection so all variants are summed. Reported by the Copilot review on PR #10. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
AlexanderLanin
left a comment
There was a problem hiding this comment.
Not sure who is supposed to review this any deeper. LGTM
Only the list-typed extra_rustc_flags setting can be cleared with an empty value; the repeatable extra_rustc_flag accumulates. The QNX block no longer resets a flag that a workspace following Step 4 never sets, and the manual says what to do when the LLVM rustc flags sit on the bare coverage command. Count the new gcov reporter test. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
The coverage pipeline that lived under quality/coverage (merger, reporter, reporter_wrapper, coverage_scope aspect, justify.py, effective_coverage.py, lcov_to_html.py, generate_coverage_html.sh) is the origin of the qualified S-CORE coverage tool, score_coverage (https://eclipse-score.github.io/coverage_tool/main/). This switches the repository to that module and deletes the copy: - quality/coverage/BUILD keeps only what is repository-specific: the coverage scope (unchanged dependable elements) and two report generators, LLVM for Linux and, new, gcov for QNX on target. - coverage.bazelrc: the default coverage command is the LLVM backend as before; --config=qnx now keeps Bazel's per-test gcov collector and points the final step at score_coverage's gcov reporter instead of Bazel's stock LCOV report, so the QNX report has the same layout, scope, 0 % baselines, justifications and gate as the Linux one. - workflows: the Linux coverage report is produced by @score_coverage//:generate_coverage_html (same archive name and layout, cpp_coverage_linux/); the QNX workflow gains a coverage step after the tests and uploads coverage_report_qnx. - MODULE.bazel: score_coverage dev dependency; rules_rust 0.68.2-score as required by it; the death-test coverage feature now comes from the module. gcovr leaves requirements.in (pyyaml stays, used elsewhere). - docs and the visibility-guard golden updated. Validated locally on //score/message_passing/... (Linux, LLVM): the scope allowlist is identical (613 files) and all 347 LCOV records are identical in covered/uncovered status to the previous pipeline (the only differences are execution counts of logging/timing code). The QNX run is validated in CI. VALIDATION ONLY: MODULE.bazel carries a git_override to the coverage_tool branch of eclipse-score/coverage_tool#10 for the gcov backend; replace it by the 0.3.0 registry release once published. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
The coverage pipeline that lived under quality/coverage (merger, reporter, reporter_wrapper, coverage_scope aspect, justify.py, effective_coverage.py, lcov_to_html.py, generate_coverage_html.sh) is the origin of the qualified S-CORE coverage tool, score_coverage (https://eclipse-score.github.io/coverage_tool/main/). This switches the repository to that module and deletes the copy: - quality/coverage/BUILD keeps only what is repository-specific: the coverage scope (unchanged dependable elements) and two report generators, LLVM for Linux and, new, gcov for QNX on target. - coverage.bazelrc: the default coverage command is the LLVM backend as before; --config=qnx now keeps Bazel's per-test gcov collector and points the final step at score_coverage's gcov reporter instead of Bazel's stock LCOV report, so the QNX report has the same layout, scope, 0 % baselines, justifications and gate as the Linux one. - workflows: the Linux coverage report is produced by @score_coverage//:generate_coverage_html (same archive name and layout, cpp_coverage_linux/); the QNX workflow gains a coverage step after the tests and uploads coverage_report_qnx. - MODULE.bazel: score_coverage dev dependency; rules_rust 0.68.2-score as required by it; the death-test coverage feature now comes from the module. gcovr leaves requirements.in (pyyaml stays, used elsewhere). - docs and the visibility-guard golden updated. Validated locally on //score/message_passing/... (Linux, LLVM): the scope allowlist is identical (613 files) and all 347 LCOV records are identical in covered/uncovered status to the previous pipeline (the only differences are execution counts of logging/timing code). The QNX run is validated in CI. VALIDATION ONLY: MODULE.bazel carries a git_override to the coverage_tool branch of eclipse-score/coverage_tool#10 for the gcov backend; replace it by the 0.3.0 registry release once published. Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>


Adds QNX on-target coverage as a second backend of the tool (tooling#427, track 1). QCC is GCC-based and cannot emit LLVM coverage mapping; QNX tests run in QEMU. The backend changes the collection, not the report.
How it works
-fprofile-arcs -ftest-coverage(toolchaincoveragefeature)score_qnx_unit_tests(--run_under), which tars the.gcdacounters and extracts them into Bazel'sCOVERAGE_DIRmerger.py(profraw → profdata)collect_coverage.sh+lcov_merger(kept as is)reporter.py(llvm-cov)gcov_reporter.py: sums per-test LCOV per file, applies the scope, zero-coverage baseline viagcov --json-formatover the.gcnonotes of in-scope translation units, HTML + summary via gcovr 8.6Consumer side: one more
score_coverage_reporter(backend = "gcov", gcov = "@score_qcc_x86_64_toolchain_pkg//:gcov")target and acoverage:qnxbazelrc block (user manual, step 4b). Existing consumers need no change.Limits, documented: C++ only (Rust sources are reported
not-instrumented); headers vendored from external repositories have no data on this backend (Bazel's collector drops them); gcov and LLVM count lines differently, so the two reports are compared per file.Two defects found on the way
effective_coverage's gcovr parser matched across table rows. Real gcovr pages carry bare rows for non-instrumented lines, so every status shifted by one row and no justification ever applied to a gcovr report. The synthetic test HTML had a class on every row. Fixed, with a regression test using the real markup.(line, block).Verification
gcov_reporter_test), 1 new analysis testcoverage:gcov)expected_lcov_gcov.datmatches, every gcovr link opens, justification applied (effective 66.66 % > raw 58.33 %), categories incl.not-instrumentedgcov_reporter.py96.0 % / 90.9 %GCC on Linux takes exactly the collection path QCC takes on QNX; the QEMU transport itself is
communication's andbaselibs' existingrun_under_qnxflow. A real QNX run is validated on baselibs (follow-up PR with agit_overrideto this branch; the QNX credentials live in that repository's CI).Docs
Architecture (backend section and diagram), tool requirements
scope_gcno,backend_select,gcov_merge,gcov_baseline,gcov_html; potential errors ERR-11/ERR-12; constraint CSTR-11; user manual step 4b; known problems; release notes 0.3.0; verification report.