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
11 changes: 11 additions & 0 deletions docs/manual/user_manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ configuration where that platform's toolchains are not registered and fail
toolchain resolution. The coverage run names the reporter, and through it
the scope, explicitly, so the tag does not affect it.

Binaries in the scope
~~~~~~~~~~~~~~~~~~~~~

A ``cc_binary`` or ``rust_binary`` root is handled like a library: its
sources and those of its in-workspace dependencies are in scope. Its own
translation units are linked into the executable rather than into an
archive, so the coverage-built executable is their zero-coverage baseline;
an untested ``main.cpp`` therefore appears in the report at 0 % and not in
``unmapped_files.txt`` (0.4.2).

Step 4: import the bazelrc config
---------------------------------

Expand Down Expand Up @@ -324,6 +334,7 @@ collector and points the final step at that reporter (copy and adapt the
coverage:qnx --test_lang_filters=cc
coverage:qnx --instrument_test_targets
coverage:qnx --instrumentation_filter=^//score[/:]
coverage:qnx --host_cxxopt=-O0 # baseline .gcno of untested files are built with the HOST options
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
Expand Down
38 changes: 38 additions & 0 deletions docs/release/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,44 @@ Release notes
:security: NO
:realizes: wp__module_sw_release_note

0.4.2 (unreleased)
------------------

In plain words
~~~~~~~~~~~~~~

**The sources of a binary show up at 0 % like everything else.** A
``cc_binary`` links its own files, typically ``main.cpp``, straight into the
executable instead of into a library archive. The reporter took its
zero-coverage baseline from archives only, so an untested binary's own files
were listed in ``unmapped_files.txt`` as ``no-data`` and were absent from the
report (lifecycle's ``launch_manager``, coverage_tool#40). The coverage-built
executable now serves as their baseline object, as it already did for
``rust_binary``.

Details for integrators
~~~~~~~~~~~~~~~~~~~~~~~

- Scope aspect: for a ``cc_binary`` in or below the scope roots, the
executable joins the baseline objects; libraries are unchanged. Nothing to
configure.
- Analysis test for the executable in the objects manifest; the end-to-end
workspace gains an untested ``cc_binary`` whose ``main.cpp`` is checked at
exact 0 % on both backends.
- HTML: baseline-only files are rendered in their own ``--empty-profile``
run and spliced into the index, the way the LCOV path already worked. In
the single combined run an executable's ``main`` collided with the test
binaries' ``main``: with the same structural hash llvm-cov attributed a
test's counts to it (100 % for an untested file), with a different hash it
dropped the file. The index totals, which the effective-coverage gate
reads, are recomputed over all rows; end-to-end checks of the index rows.
- gcov backend: the ``.gcno`` notes of untested files are built in the exec
configuration, which takes the **host** options. Add
``--host_cxxopt=-O0`` next to ``--cxxopt=-O0`` in the gcov config (user
manual step 4b), or GCC optimises those units and their line tables differ
from the ``-O0`` test builds; a folded ``return 0`` then lacks its line.
The LLVM backend is unaffected (front-end mapping).

0.4.1 (2026-10-13)
------------------

Expand Down
10 changes: 5 additions & 5 deletions docs/verification/verification_report.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Test inventory
- 20
- merge_profraw, merge_no_data, merge_tool_error
* - ``//tests/unit/score_coverage:reporter_test``
- 71
- 74
- report_merged_profile, report_allowlist, report_baseline_zero,
report_rlib_expansion, report_missing_baseline, report_relative_paths,
report_outputs, report_unmapped, scope_transitive,
Expand All @@ -77,15 +77,15 @@ Test inventory
* - ``//tests/unit/score_coverage:coverage_summary_test``
- 19
- summary_first
* - ``//tests/unit/starlark:coverage_scope_tests`` (17 analysis tests)
- 17
* - ``//tests/unit/starlark:coverage_scope_tests`` (18 analysis tests)
- 18
- scope_transitive, scope_excludes, scope_baseline_objects, scope_gcno,
scope_platform
* - ``//tests/end_to_end/testcases:blackbox_test``
- 4
- gate_no_verdict, gate_exit_codes, summary_first
* - ``tests/end_to_end/run_end_to_end_test.sh`` (24 end-to-end checks)
- 24
* - ``tests/end_to_end/run_end_to_end_test.sh`` (28 end-to-end checks)
- 28
- validation_ground_truth, instrumentation_hint, scope_platform, report_baseline_zero, report_relative_paths,
report_allowlist, report_unmapped, gcov_merge, gcov_baseline, gcov_html,
gate_exit_codes, gate_no_verdict, just_unknown_id, artifacts,
Expand Down
12 changes: 12 additions & 0 deletions score_coverage/coverage_scope.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ transitive deps. At each node it collects the actual source files.
(zero-coverage) reporting.
- rust_binary (no CcInfo): source files come from CrateInfo.srcs; the
coverage-built executable itself serves as the baseline object.
- cc_binary: srcs and hdrs as for cc_library; its own objects are in no
archive, so the coverage-built executable is the baseline object too.

The rule writes three text files the reporter consumes:

Expand Down Expand Up @@ -168,6 +170,16 @@ def _coverage_scope_aspect_impl(target, ctx):
if archive and "/external/" not in archive.path and not archive.path.startswith("external/"):
direct_archives.append(archive)
break

# A cc_binary links its own translation units (main.cpp) straight
# into the executable: they sit in no archive. The coverage-built
# executable is the only object carrying their mapping, so it is
# the baseline object for them, as for rust_binary
# (eclipse-score/coverage_tool#40).
if ctx.rule.kind == "cc_binary":
exe = target[DefaultInfo].files_to_run.executable
if exe and "/external/" not in exe.path and not exe.path.startswith("external/"):
direct_archives.append(exe)
elif CrateInfo in target:
# rust_binary: no CcInfo, collect .rs sources from CrateInfo and use the
# coverage-built executable as the baseline object.
Expand Down
117 changes: 105 additions & 12 deletions score_coverage/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,25 +125,41 @@ def show_html(objects: list[str]) -> None:
cxxfilt=cxxfilt,
)

# Generate HTML report including baseline-only files when valid archives are available.
# HTML from the test binaries and the merged profile. Baseline-only files
# are rendered in a run of their own below: an executable among the
# baseline objects carries a ``main`` like every test binary, and against
# the merged profile llvm-cov either attributes a test's counts to it (same
# structural hash) or drops it as mismatched (coverage_tool#40).
html_report_dir = Path.cwd() / "html_report"
show_html(sorted_objects)

# File the pages under canonical, machine-independent paths so the index
# links resolve wherever the archive is unpacked, and name the sources
# the same way in the page titles.
relocate_html_pages(html_report_dir, root, path_map)

if baseline_only_archives:
baseline_html_dir = Path.cwd() / "html_baseline"
try:
show_html(sorted_objects + baseline_only_archives)
run_llvm_cov_show(
llvm_bin_path,
baseline_only_archives,
None,
regexes,
root,
output_format="html",
html_report_dir=baseline_html_dir,
cxxfilt=cxxfilt,
)
relocate_html_pages(baseline_html_dir, root, path_map)
spliced = merge_baseline_html(html_report_dir, baseline_html_dir, selection.baseline_only)
print(f"INFO: Merged baseline HTML for {spliced} files.", file=sys.stderr)
except SystemExit:
# Some baseline archives caused llvm-cov show to fail; retry with test binaries only.
print(
"WARNING: HTML generation with baseline archives failed; falling back to test-only HTML.",
"WARNING: HTML generation for the baseline-only files failed; their pages are missing.",
file=sys.stderr,
)
show_html(sorted_objects)
else:
show_html(sorted_objects)

# File the pages under canonical, machine-independent paths so the index
# links resolve wherever the archive is unpacked, and name the sources
# the same way in the page titles.
relocate_html_pages(html_report_dir, root, path_map)
shutil.rmtree(baseline_html_dir, ignore_errors=True)
_make_html_paths_relative(html_report_dir, root, path_map)

# Generate LCOV report from test binaries.
Expand Down Expand Up @@ -555,6 +571,83 @@ def relocate_html_pages(html_dir: Path, source_root: str, path_map: dict[str, st
return moves


_INDEX_FILE_ROW_RE = re.compile(r"<tr class='light-row'>.*?</tr>", re.S)
_INDEX_TOTALS_RE = re.compile(r"<tr class='light-row-bold'><td><pre>Totals</pre></td>.*?</tr>", re.S)
_INDEX_CELL_RE = re.compile(r"<td class='column-entry-\w+'><pre>\s*(?:[\d.]+%|-) \((\d+)/(\d+)\)</pre></td>")
_INDEX_LINK_RE = re.compile(r"<a href='coverage/([^']+)\.html'>")
_INDEX_LABEL_RE = re.compile(r"<td><pre>(?:<a[^>]*>)?([^<]*)(?:</a>)?</pre></td>")


def _index_rows(text: str) -> dict[str, str]:
"""{canonical file name: row html} for the file rows of an llvm-cov index."""
rows: dict[str, str] = {}
for row in _INDEX_FILE_ROW_RE.findall(text):
link = _INDEX_LINK_RE.search(row)
label = _INDEX_LABEL_RE.search(row)
name = link.group(1) if link else (label.group(1).strip() if label else None)
if name:
rows[name] = row
return rows


def _index_cell(covered: int, total: int, high: int = 100, low: int = 50) -> str:
"""One index cell the way llvm-cov renders it (``--coverage-watermark=<high>,<low>``)."""
if total == 0:
return "<td class='column-entry-gray'><pre>- (0/0)</pre></td>"
pct = 100.0 * covered / total
colour = "green" if pct >= high else ("yellow" if pct >= low else "red")
return f"<td class='column-entry-{colour}'><pre>{pct:7.2f}% ({covered}/{total})</pre></td>"


def _totals_row(rows: list[str]) -> str:
sums = [[0, 0], [0, 0], [0, 0]]
for row in rows:
for column, (covered, total) in enumerate(_INDEX_CELL_RE.findall(row)[:3]):
sums[column][0] += int(covered)
sums[column][1] += int(total)
cells = "".join(_index_cell(covered, total) for covered, total in sums)
return f"<tr class='light-row-bold'><td><pre>Totals</pre></td>{cells}</tr>"


def merge_baseline_html(html_dir: Path, baseline_dir: Path, baseline_only: set[str]) -> int:
"""Add the pages and index rows of the baseline-only files to the test-binary HTML.

Both directories must already be relocated (pages under
``coverage/<canonical>.html``). Only names in ``baseline_only`` that the
main index lacks are taken, so a file the tests cover is never
duplicated. The Totals row, which the effective-coverage gate reads, is
recomputed over all rows. Returns the number of files spliced in.
"""
index = html_dir / "index.html"
baseline_index = baseline_dir / "index.html"
if not index.is_file() or not baseline_index.is_file():
return 0
text = index.read_text(encoding="utf-8", errors="replace")
main_rows = _index_rows(text)
baseline_rows = _index_rows(baseline_index.read_text(encoding="utf-8", errors="replace"))
added: list[str] = []
for name in sorted(baseline_only):
row = baseline_rows.get(name)
if row is None or name in main_rows:
continue
page = baseline_dir / "coverage" / (name + ".html")
if page.is_file():
target = html_dir / "coverage" / (name + ".html")
target.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(page), str(target))
added.append(row)
if not added:
return 0
totals = _INDEX_TOTALS_RE.search(text)
new_totals = _totals_row(list(main_rows.values()) + added)
if totals:
text = text[: totals.start()] + "".join(added) + new_totals + text[totals.end() :]
else:
text = text.replace("</table>", "".join(added) + new_totals + "</table>", 1)
index.write_text(text, encoding="utf-8")
return len(added)


def _canonicalize_summary(text: str, source_root: str, path_map: dict[str, str] | None = None) -> str:
"""Name the files of an llvm-cov text report the way LCOV and HTML do."""
prefixes = (source_root.rstrip("/") + "/", "/proc/self/cwd/")
Expand Down
4 changes: 4 additions & 0 deletions tests/end_to_end/consumer/.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ coverage:gcov --nocache_test_results
# compiled with instrumentation through an instrumented dependency.
coverage:gcov --instrumentation_filter=^//
coverage:gcov --cxxopt=-O0
# The scope's baseline objects (the .gcno notes of untested files) are built
# in the exec configuration, which takes the host options: without this, GCC
# optimises them and their line tables no longer match the -O0 test builds.
coverage:gcov --host_cxxopt=-O0
coverage:gcov --combined_report=lcov
coverage:gcov --dynamic_mode=off
coverage:gcov --host_platform=@score_bazel_platforms//:x86_64-linux-gcc_12.2.0-posix
Expand Down
8 changes: 8 additions & 0 deletions tests/end_to_end/consumer/src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_cc//cc:cc_test.bzl", "cc_test")

Expand Down Expand Up @@ -95,6 +96,13 @@ cc_library(
srcs = ["target_root.cpp"],
)

# Shipped binary without a test: its main.cpp is linked straight into the
# executable, not into any archive, and must still show at 0 % (coverage_tool#40).
cc_binary(
name = "tool",
srcs = ["main.cpp"],
)

cc_test(
name = "coverable_test",
srcs = ["coverable_test.cpp"],
Expand Down
19 changes: 19 additions & 0 deletions tests/end_to_end/consumer/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/********************************************************************************
* 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
********************************************************************************/
// Entry point of a shipped binary. Nothing tests it; the report must still
// list it at 0 % (coverage_tool#40). Linked straight into the executable, so
// its mapping lives in no archive: the executable is its baseline object.
int main()
{
return 0;
}
3 changes: 3 additions & 0 deletions tests/end_to_end/consumer/tools/coverage/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ SCOPE_DEPS = [
# Platform-dependent library (coverage_tool#23): the scope must list
# the variant of the platform the coverage run targets.
"//src:platform_dep",
# Untested cc_binary: own sources have no archive, the executable is the
# baseline object (coverage_tool#40).
"//src:tool",
] + select({
# One root per platform, the host root incompatible with the gcov
# platform: this select() is resolved for the scope's platform.
Expand Down
11 changes: 11 additions & 0 deletions tests/end_to_end/expected/expected_lcov.dat
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,14 @@ BRH:0
LF:6
LH:3
end_of_record
# src/main.cpp: own source of the untested cc_binary //src:tool (coverage_tool#40).
# Function body { ... } on lines 17-19 after the 12-line header, 1 blank, 3 comment lines.
SF:src/main.cpp
DA:17,0
DA:18,0
DA:19,0
BRF:0
BRH:0
LF:3
LH:0
end_of_record
7 changes: 7 additions & 0 deletions tests/end_to_end/expected/expected_lcov_gcov.dat
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,10 @@ DA:19,1
LF:2
LH:2
end_of_record
# src/main.cpp: gcov counts the signature line (function entry) and the return.
SF:src/main.cpp
DA:16,0
DA:18,0
LF:2
LH:0
end_of_record
20 changes: 20 additions & 0 deletions tests/end_to_end/run_end_to_end_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,28 @@ check_zero_coverage() {
}

check_zero_coverage "src/uncovered.cpp"
check_zero_coverage "src/main.cpp" # own source of a cc_binary (coverage_tool#40)
check_zero_coverage "rust/main.rs"

echo "=== Untested files must appear at 0% in the HTML index too (coverage_tool#40) ==="
unzip -p coverage_artifacts.zip artifacts/coverage_linux/index.html > index.html
check_zero_index_row() {
local file="$1" lines="$2" row
row=$(tr -d '\n' < index.html | grep -o "<tr class='light-row'><td><pre><a href='coverage/${file}.html'>${file}</a></pre></td>.*" | sed 's#</tr>.*##' || true)
if [[ -z "${row}" ]]; then
echo "ERROR: ${file} has no row in the HTML index" >&2
exit 1
fi
if ! grep -q "0.00% (0/${lines})" <<<"${row}"; then
echo "ERROR: ${file} index row is not at 0% of ${lines} lines: ${row}" >&2
exit 1
fi
echo "OK: ${file} in the HTML index at 0% (0/${lines})"
}
check_zero_index_row "src/uncovered.cpp" 6
check_zero_index_row "src/main.cpp" 3 # own source of a cc_binary: baseline-only, rendered in its own run
check_zero_index_row "rust/main.rs" 3

echo "=== LCOV must match the hand-verified ground truth exactly ==="
# Normalise: drop function records, keep one record per file sorted by SF, so
# the comparison is independent of record order and of symbol names.
Expand Down
3 changes: 2 additions & 1 deletion tests/end_to_end/testcases/test_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def test_markdown_summary_contains_coverage_and_source_categories(
"| Lines |",
"Raw vs effective",
"Coverage by directory",
"Files at exact 0% (3)",
"Files at exact 0% (4)", # uncovered.cpp, host_root.cpp, main.cpp, rust/main.rs
"- `src/main.cpp`",
"| In-scope files without coverage data | 2 |",
"In-scope files without coverage data (2)",
"- `src/unused_api.h`",
Expand Down
Loading
Loading