Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ alias(
actual = "//score_coverage:merger",
)

alias(
name = "gcov_reporter",
actual = "//score_coverage:gcov_reporter",
)

alias(
name = "generate_coverage_html",
actual = "//score_coverage:generate_coverage_html",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

# coverage_tool — Bazel module `score_coverage`

LLVM source-based code coverage pipeline for Eclipse S-CORE: one
Code coverage pipeline for Eclipse S-CORE, LLVM source-based on Linux and
gcov-based for QNX on-target tests: one
`bazel coverage` run gives one line and branch coverage report for C++ and
Rust, untested in-scope files at exact 0 %, reviewed justifications with an
effective-coverage metric, and a CI threshold gate.
Expand Down
35 changes: 30 additions & 5 deletions defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ and point Bazel at them from their coverage bazelrc config:
coverage:llvm_cov --coverage_output_generator=@score_coverage//:merger
coverage:llvm_cov --coverage_report_generator=//tools/coverage:reporter_wrapper
For gcov-based toolchains (GCC on Linux, QCC on QNX with the tests executed
on target through score_qnx_unit_tests) a second reporter with
backend = "gcov" and the toolchain's gcov binary is declared; the gcov
coverage config keeps Bazel's own per-test merger:
coverage:qnx --coverage_output_generator=@bazel_tools//tools/test:lcov_merger
coverage:qnx --coverage_report_generator=//tools/coverage:gcov_reporter_wrapper
See README.md for the complete adoption guide (toolchains, bazelrc,
justifications, CI) and COVERAGE_GUIDE.md for how the pipeline works.
"""
Expand All @@ -51,17 +59,19 @@ score_coverage_scope = _coverage_scope
def score_coverage_reporter(
name,
coverage_scope,
llvm_cov,
llvm_profdata,
llvm_cov = None,
llvm_profdata = None,
llvm_cxxfilt = None,
module_bazel = "//:MODULE.bazel",
backend = "llvm",
gcov = None,
**kwargs):
"""Declare the consumer-side coverage report generator.
The generated executable is passed to Bazel as
--coverage_report_generator=//<pkg>:<name>. It wires the consumer's
coverage scope, workspace root and LLVM tools into score_coverage's
reporter.
coverage scope, workspace root and coverage tools into score_coverage's
reporter for the chosen backend.
Args:
name: Target name, referenced by --coverage_report_generator.
Expand All @@ -70,21 +80,36 @@ def score_coverage_reporter(
llvm_cov: Label of the llvm-cov binary (the consumer's LLVM toolchain,
e.g. "@llvm_toolchain//:llvm-cov"). Must come from the same LLVM
major version that produced the coverage instrumentation.
llvm_profdata: Label of the llvm-profdata binary.
Required for backend = "llvm".
llvm_profdata: Label of the llvm-profdata binary. Required for
backend = "llvm".
llvm_cxxfilt: Optional label of llvm-cxxfilt for symbol demangling
(C++ Itanium and Rust v0/legacy). toolchains_llvm exposes it as
"@llvm_toolchain_llvm//:bin/llvm-cxxfilt".
module_bazel: The consumer's root MODULE.bazel, used at runtime to
locate the real workspace root. Requires
exports_files(["MODULE.bazel"]) in the consumer's root BUILD file.
backend: "llvm" (Clang / rustc coverage mapping, Linux host tests) or
"gcov" (GCC / QCC .gcda counters, e.g. QNX on-target tests run
through score_qnx_unit_tests). One reporter target per backend.
gcov: Label of the gcov binary matching the compiler that produced the
.gcno/.gcda files, e.g. "@score_qcc_x86_64_toolchain_pkg//:gcov" or
"@score_gcc_x86_64_toolchain_pkg//:gcov". Required for
backend = "gcov".
**kwargs: Common rule attributes (testonly, visibility, tags, ...).
"""
if backend == "llvm" and (not llvm_cov or not llvm_profdata):
fail("score_coverage_reporter(%s): backend \"llvm\" needs llvm_cov and llvm_profdata" % name)
if backend == "gcov" and not gcov:
fail("score_coverage_reporter(%s): backend \"gcov\" needs gcov" % name)
_reporter_wrapper(
name = name,
backend = backend,
coverage_scope = coverage_scope,
module_bazel = module_bazel,
llvm_cov = llvm_cov,
llvm_profdata = llvm_profdata,
llvm_cxxfilt = llvm_cxxfilt,
gcov = gcov,
**kwargs
)
86 changes: 80 additions & 6 deletions docs/architecture/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ Architecture
:security: NO
:realizes: wp__sw_implementation

The pipeline replaces Bazel's two coverage hooks, ``--coverage_output_generator``
and ``--coverage_report_generator``, with its own tools and adds a
justification and gating layer on top. It has two phases.
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 ``.gcda`` counters, read with the toolchain's
``gcov``; C++ only. On QNX the tests run inside QEMU through
``score_qnx_unit_tests``, which carries the counters back to the host, and
Bazel's own per-test collector turns them into LCOV.

.. uml::

Expand All @@ -41,6 +49,15 @@ justification and gating layer on top. It has two phases.
(allowlist.txt\npath_map.txt\nobjects.txt\nsource files) --> [reporter.py\n--coverage_report_generator]
[reporter.py\n--coverage_report_generator] --> (_coverage_report.dat zip\nhtml_report, lcov_report, text_report)
}
package "Phase 1, gcov backend: bazel coverage --config=qnx" {
[GCC / QCC\n-fprofile-arcs -ftest-coverage] --> [test binaries (QEMU on QNX)]
[test binaries (QEMU on QNX)] --> (.gcda per test)
(.gcda per test) --> [Bazel collector\ngcov + lcov_merger]
[Bazel collector\ngcov + lcov_merger] --> (coverage.dat LCOV)
(coverage.dat LCOV) --> [gcov_reporter.py\n--coverage_report_generator]
(allowlist.txt\npath_map.txt\ngcno.txt\nsource files) --> [gcov_reporter.py\n--coverage_report_generator]
[gcov_reporter.py\n--coverage_report_generator] --> (_coverage_report.dat zip\nhtml_report, lcov_report, text_report)
}
package "Phase 2: bazel run //:generate_coverage_html" {
(_coverage_report.dat zip\nhtml_report, lcov_report, text_report) --> [generate_coverage_html.py]
[generate_coverage_html.py] --> [justify.py]
Expand Down Expand Up @@ -101,6 +118,52 @@ 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.

1. **Compilers.** The ``coverage`` feature of the S-CORE GCC and QCC
toolchains adds ``-fprofile-arcs -ftest-coverage``; the compiler writes a
``.gcno`` notes file per translation unit and the instrumented binary
writes ``.gcda`` counters when it exits. rustc cannot produce either, so
Rust sources in scope are reported as *not instrumentable* on this backend.
2. **Execution and transport.** On Linux the test writes its ``.gcda`` under
Bazel's ``COVERAGE_DIR``. On QNX the test runs inside a QEMU micro-VM
(``score_qnx_unit_tests``, ``--run_under``); ``GCOV_PREFIX`` points the
counters at a guest directory, the guest tars them at exit, and the host
runner extracts the archive into ``COVERAGE_DIR``. From there both are the
same.
3. **Per-test collection stays Bazel's.** Bazel's ``collect_coverage.sh``
runs the toolchain's ``gcov`` over the counters and its ``lcov_merger``
writes 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.
4. **Final report.** ``gcov_reporter.py`` sums 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-format`` over the ``.gcno`` of every in-scope
translation unit without its ``.gcda`` yields every line and branch at
zero. The ``.gcno`` files come from ``InstrumentedFilesInfo`` through the
scope aspect, listed in ``<name>_gcno.txt`` and 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 and
``unmapped_files.txt`` are 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
-----------------------------------

Expand Down Expand Up @@ -130,7 +193,10 @@ Module and consumer split
manifest discovery
* - ``score_coverage/reporter.py``
- final merge, llvm-cov show/export/report, allowlist filtering,
``--empty-profile`` baselines, rlib expansion, path normalisation
``--empty-profile`` baselines, rlib expansion, path normalisation; the
selection, staging and path helpers shared with the gcov backend
* - ``score_coverage/gcov_reporter.py``
- gcov backend: per-test LCOV merge, ``.gcno`` baselines, gcovr HTML
* - ``score_coverage/coverage_scope.bzl``
- the scope aspect and rule (CcInfo and CrateInfo)
* - ``score_coverage/reporter_wrapper.bzl``, ``defs.bzl``
Expand All @@ -151,12 +217,14 @@ Module and consumer split
* - ``score_coverage_scope(deps = [...])``
- names the repository's production targets
* - ``score_coverage_reporter(...)``
- carries the repository's LLVM tool labels and workspace root
- carries the repository's LLVM tool labels (or, with
``backend = "gcov"``, the toolchain's gcov) and workspace root; one
target per backend
* - ``coverage_justifications.yaml``
- reviewed, repository-specific engineering arguments
* - MODULE.bazel toolchain blocks
- LLVM and Ferrocene pins are per-repository decisions
* - the ``coverage:llvm_cov`` bazelrc block
* - the ``coverage:llvm_cov`` and ``coverage:qnx`` bazelrc blocks
- bazelrc cannot be imported across modules

Two wiring details make the external hosting work: every path in the generated
Expand All @@ -178,3 +246,9 @@ Design decisions
- **In-process tool calls.** ``generate_coverage_html`` imports the justification
tools instead of nesting ``bazel 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 ``.gcda`` handling into
the qualified tool for no gain.
23 changes: 21 additions & 2 deletions docs/manual/constraints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ CSTR-01 Qualified environment
Use the tool only in the environment it was validated in: Linux x86_64 host,
Bazel 8.6, ``toolchains_llvm`` 1.8.0 with LLVM 22.1.7 for C++, the standard
Ferrocene toolchain of ``score_toolchains_rust`` 0.10.0 or newer (built by
``ferrocene_toolchain_builder`` 1.3.1 or newer) for Rust. QNX on-target coverage
is outside this environment. Mitigates ERR-08.
``ferrocene_toolchain_builder`` 1.3.1 or newer) for Rust; for the gcov
backend the S-CORE GCC 12.2.0 toolchain (``score_bazel_cpp_toolchains``) on
Linux and QCC of QNX SDP 8.0 with the tests executed through
``score_qnx_unit_tests`` (see :ref:`CSTR-11 <cstr_coverage_qnx_transport>`).
Mitigates ERR-08.

.. _cstr_coverage_scope_list:

Expand Down Expand Up @@ -116,3 +119,19 @@ CSTR-10 Treat exit code 2 as a failed run

Exit code 2 means the tool could not produce a verdict. CI must fail on it
exactly like on exit code 1. Never map it to a pass. Mitigates ERR-03.

.. _cstr_coverage_qnx_transport:

CSTR-11 QNX: run the tests through score_qnx_unit_tests and keep the collector
------------------------------------------------------------------------------

On QNX the counters are written inside the QEMU guest. Only the
``run_under_qnx`` runner of ``score_qnx_unit_tests`` (0.2.0 or newer) brings
them back: it sets ``GCOV_PREFIX`` in the guest, archives the counters at exit
and extracts them into Bazel's ``COVERAGE_DIR``. The QNX coverage config must
therefore use that runner, Bazel's own per-test collector
(``--coverage_output_generator=@bazel_tools//tools/test:lcov_merger``) and
``--instrument_test_targets``, exactly as in the ``coverage:gcov`` block of the
integration workspace. Check that the QNX report lists the same files as the
Linux report of the same tree (Rust files excepted): a missing file is a lost
transport, not a coverage result. Mitigates ERR-11.
26 changes: 19 additions & 7 deletions docs/manual/known_problems.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,25 @@ stay listed with their upstream references.
- Exit 127 in the test log; no profraw.
- Exclude containerised or system tests from the coverage run; they keep
running in the regular test jobs.
* - **QNX on-target coverage is not supported by this tool.** The
orchestrator accepts only the LLVM zip report. The gcovr-based HTML
post-processing exists but is reachable only through the consumer-side
flow of the ``communication`` repository (tooling issue #427).
- Exit 2 with ``is not the LLVM pipeline zip report`` on a gcov run.
- Use the Linux host pipeline; QNX centralisation is tracked in tooling
issue #427.
* - **QNX on-target coverage covers C++ only.** rustc emits no gcov
counters, and the LLVM profile transport from the QEMU guest is not
established yet (tooling issue #427, track 2).
- Rust sources listed as ``not-instrumented`` in ``unmapped_files.txt``
of a QNX report.
- Measure Rust with the Linux (LLVM) run of the same tree.
* - **Vendored external headers have no data on the gcov backend.** Bazel's
per-test collector keeps only files of its instrumented-files manifest,
which never lists sources from external repositories, although gcov
itself recorded them.
- Such a header is ``no-data`` in a QNX report and measured in the Linux
report of the same tree.
- Known limitation of Bazel's collector; use the Linux report for those
headers.
* - **gcov and LLVM count different lines.** gcov reports only lines the
compiler emitted code for: unused inline functions and closing braces
have no line, while LLVM's mapping keeps unused functions at 0 %.
- Different totals for the same file on QNX and Linux.
- Expected; compare the two reports per file, do not merge them.
* - **An archive is rejected by llvm-cov** because one member has no
coverage mapping: the ``lib.rmeta`` of a Rust rlib, or the object of an
empty translation unit.
Expand Down
72 changes: 71 additions & 1 deletion docs/manual/user_manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,75 @@ Do **not** combine ``--config=llvm_cov`` with configs that append other
``--extra_toolchains`` (for example a GCC host config): the last toolchain wins
resolution and a GCC toolchain produces no covmap data.

Step 4b (optional): QNX on-target coverage, the gcov backend
------------------------------------------------------------

QCC is GCC-based and cannot emit LLVM coverage mapping, so QNX coverage uses
gcov counters. The tests run inside QEMU through ``score_qnx_unit_tests``,
which brings the counters back; Bazel's own collector turns them into LCOV;
score_coverage's gcov reporter produces the same report zip as on Linux. C++
only: Rust sources in scope are listed as *not instrumentable* on this
backend and are measured by the Linux run.

Declare a second reporter next to the LLVM one, with the gcov binary of the
QCC package (add ``score_qcc_x86_64_toolchain_pkg`` to the ``use_repo`` of
the toolchain extension):

.. code-block:: starlark
score_coverage_reporter(
name = "gcov_reporter_wrapper",
testonly = True,
backend = "gcov",
coverage_scope = ":coverage_scope",
gcov = "@score_qcc_x86_64_toolchain_pkg//:gcov",
tags = ["manual"], # keeps `bazel build //...` on a Linux host from fetching the QNX SDP
)
The ``manual`` tag matters: the target depends on the QNX SDP package, and a
wildcard build on a host without QNX credentials would otherwise fail on the
download. ``--coverage_report_generator`` names the target explicitly and is
not affected.

and a coverage config that resets the LLVM settings, keeps Bazel's per-test
collector and points the final step at that reporter (copy and adapt the
``coverage:gcov`` block of ``integration_tests/.bazelrc``):

.. code-block:: text
coverage:qnx --config=<your QNX build config> # QCC, IFS toolchain, platforms
coverage:qnx --run_under=@score_qnx_unit_tests//src:run_under_qnx
coverage:qnx --test_lang_filters=cc
coverage:qnx --instrument_test_targets
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 --coverage_output_generator=@bazel_tools//tools/test:lcov_merger
coverage:qnx --coverage_report_generator=//tools/coverage:gcov_reporter_wrapper
The LLVM-only rustc flags (``-Zcoverage-options=branch`` and friends) stay
out of the way as long as they live in their own ``coverage:llvm_cov`` config,
as in Step 4. If your workspace puts them on the bare ``coverage`` command
instead, declare them with the list-typed
``--@rules_rust//rust/settings:extra_rustc_flags`` and add
``coverage:qnx --@rules_rust//rust/settings:extra_rustc_flags=`` to clear
them: an empty value resets that list, whereas the repeatable singular
``extra_rustc_flag`` accumulates and cannot be reset from a config.

Run and report as on Linux, with the platform filter for justifications:

.. code-block:: shell
bazel coverage --config=qnx //score/... --build_tests_only
bazel run @score_coverage//:generate_coverage_html -- --platform qnx \
--yaml tools/coverage/coverage_justifications.yaml --archive-dir coverage_qnx_artifacts
Known differences to the LLVM backend: gcov has no lines for unused inline
functions and for closing braces; headers a workspace target vendors from an
external repository are not measured (Bazel's collector filters them out) and
appear as ``no-data``; the report lists the toolchain's line semantics, not
LLVM's, so the two reports are compared per file, not merged.

Step 5 (optional): justifications
---------------------------------

Expand Down Expand Up @@ -259,7 +328,8 @@ Command reference
- Subtree of ``bazel-testlogs`` whose ``test.xml`` files are archived.
* - ``--platform linux|qnx``
- Platform filter for justifications and default output directory
``coverage_<platform>``.
``coverage_<platform>``. Use ``qnx`` for reports of the gcov backend
produced from QNX on-target runs.
* - ``--summary-md <path>``
- Write the markdown summary to ``<path>`` instead of the step summary.
* - ``output-dir``
Expand Down
Loading
Loading