Skip to content

Commit 8b8b524

Browse files
hoe-jocastler
authored andcommitted
[rules score] add coverage report
1 parent 3e73bfe commit 8b8b524

7 files changed

Lines changed: 255 additions & 2 deletions

File tree

.bazelrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,13 @@ build:info --//bazel/rules/rules_score:verbosity=info
2323
# debug build: complete output including debug/trace from all tools
2424
build:debug --//bazel/rules/rules_score:verbosity=debug
2525

26+
# Standard combined coverage (Rust + Python, no Ferrocene required)
27+
# Usage: bazel coverage --config=coverage <targets>
28+
# Then run: bazel run //coverage:combined_report
29+
coverage:coverage --combined_report=lcov
30+
coverage:coverage --instrumentation_filter=//plantuml,//validation,//manual_analysis,-//plantuml/parser/integration_test,-//validation/core/integration_test
31+
coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code
32+
coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1
33+
2634
# Import AI checker custom configuration
2735
try-import %workspace%/.bazelrc.ai_checker

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ external
99
.clwb/
1010

1111
__pycache__
12+
.ruff_cache/
13+
coverage-html/

MODULE.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ bazel_dep(name = "score_rust_policies", version = "0.0.2")
3333
bazel_dep(name = "bazel_skylib", version = "1.7.1")
3434
bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2")
3535
bazel_dep(name = "flatbuffers", version = "25.9.23")
36+
bazel_dep(name = "download_utils", version = "1.2.2")
3637

3738
# flatbuffers depends on this transitively, but older grpc-java version
3839
# The main problem is that there the command `bazel mod deps` is broken, which
@@ -137,6 +138,7 @@ PYTHON_VERSION = "3.12"
137138

138139
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
139140
python.toolchain(
141+
configure_coverage_tool = True,
140142
is_default = True,
141143
python_version = PYTHON_VERSION,
142144
)
@@ -211,6 +213,17 @@ multitool.hub(
211213
)
212214
use_repo(multitool, "yamlfmt_hub")
213215

216+
###############################################################################
217+
# lcov deb package (provides genhtml + lcov for combined coverage reports)
218+
###############################################################################
219+
deb = use_repo_rule("@download_utils//download/deb:defs.bzl", "download_deb")
220+
221+
deb(
222+
name = "lcov_deb",
223+
integrity = "sha256-Ip14IkKavqBtkQ7mh6AXzr/6YyHpvSAZ0veMmw1+N80=",
224+
urls = ["https://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_2.0-4ubuntu2_all.deb"],
225+
)
226+
214227
register_toolchains(
215228
"//bazel/rules/rules_score:sphinx_default_toolchain",
216229
)

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,19 @@ See the individual README files for detailed usage instructions and configuratio
3030
| **python_basics** | Python development utilities and testing | [README](python_basics/README.md) |
3131
| **starpls** | Starlark language server support | [README](starpls/README.md) |
3232
| **tools** | Formatters & Linters | [README](tools/README.md) |
33-
| **coverage** | Ferrocene Rust coverage workflow | [README](coverage/README.md) |
33+
| **coverage** | Rust + Python coverage reports | [README](coverage/README.md) |
34+
35+
## Coverage
36+
37+
Generate a combined Rust + Python HTML coverage report for `plantuml`, `validation`,
38+
and `manual_analysis`:
39+
40+
```bash
41+
bazel run //coverage:combined_report
42+
```
43+
44+
See [coverage/README.md](coverage/README.md) for full details, options, and the
45+
Ferrocene Rust coverage workflow.
3446

3547
## Usage Examples
3648

coverage/BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ sh_binary(
3636
srcs = ["llvm_profile_wrapper.sh"],
3737
visibility = ["//visibility:public"],
3838
)
39+
40+
sh_binary(
41+
name = "combined_report",
42+
srcs = ["run_combined_coverage.sh"],
43+
data = ["@lcov_deb//:srcs"],
44+
)

coverage/README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,76 @@ Copyright (c) 2026 Contributors to the Eclipse Foundation
44
SPDX-License-Identifier: Apache-2.0
55
-->
66

7-
# Ferrocene Rust Coverage
7+
# Coverage
8+
9+
## Combined Rust + Python Coverage
10+
11+
The `//coverage:combined_report` target generates a single HTML coverage report
12+
for all Rust and Python tools in the repository using Bazel's built-in
13+
coverage support (`bazel coverage`) and `genhtml`.
14+
15+
### Usage
16+
17+
```bash
18+
bazel run //coverage:combined_report
19+
```
20+
21+
This runs `bazel coverage --config=coverage` for `//plantuml/...`,
22+
`//validation/...` and `//manual_analysis/...`, merges all LCOV data, and
23+
renders the report to `<workspace>/coverage-html/index.html`.
24+
25+
Custom output directory:
26+
27+
```bash
28+
bazel run //coverage:combined_report -- --out-dir /tmp/my-coverage
29+
```
30+
31+
Custom target set:
32+
33+
```bash
34+
bazel run //coverage:combined_report -- --targets "//plantuml/... //validation/core/..."
35+
```
36+
37+
### How it works
38+
39+
1. `bazel coverage --config=coverage` compiles Rust with `-Cinstrument-coverage`
40+
and wraps Python tests with `coverage.py` (via `rules_python`'s built-in
41+
`configure_coverage_tool`).
42+
2. Bazel merges all per-test LCOV files into one `_coverage_report.dat`
43+
(controlled by `--combined_report=lcov`).
44+
3. `--instrumentation_filter` limits instrumentation to the three tool
45+
packages, excluding external dependencies and generated code.
46+
4. Test infrastructure files (`integration_test/`, `tests/`) are excluded from
47+
instrumentation via `--instrumentation_filter`; external Python files are
48+
removed via `lcov --remove`.
49+
5. The HTML report uses a high-coverage threshold of **95 %** (green) and the
50+
default medium threshold of 75 % (yellow).
51+
6. `genhtml` and `lcov` are downloaded hermetically via the `download_utils`
52+
Bazel module (`@lcov_deb`) — no system installation of `lcov` is required.
53+
54+
### .bazelrc config
55+
56+
The `coverage:coverage` config in `.bazelrc` provides the required flags:
57+
58+
```
59+
coverage:coverage --combined_report=lcov
60+
coverage:coverage --instrumentation_filter=//plantuml,//validation,//manual_analysis,-//plantuml/parser/integration_test,-//validation/core/integration_test
61+
coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code
62+
coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1
63+
```
64+
65+
You can also run `bazel coverage` directly without the script (requires `genhtml`
66+
from the system `lcov` package):
67+
68+
```bash
69+
bazel coverage --config=coverage //plantuml/... //validation/... //manual_analysis/...
70+
genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat" \
71+
--output-directory coverage-html/
72+
```
73+
74+
---
75+
76+
## Ferrocene Rust Coverage
877

978
This directory provides the Ferrocene Rust coverage workflow for Bazel-based
1079
projects. It uses Ferrocene's `symbol-report` and `blanket` tools to generate

coverage/run_combined_coverage.sh

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env bash
2+
# *******************************************************************************
3+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
4+
#
5+
# See the NOTICE file(s) distributed with this work for additional
6+
# information regarding copyright ownership.
7+
#
8+
# This program and the accompanying materials are made available under the
9+
# terms of the Apache License Version 2.0 which is available at
10+
# https://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# SPDX-License-Identifier: Apache-2.0
13+
# *******************************************************************************
14+
set -euo pipefail
15+
16+
usage() {
17+
cat <<'USAGE'
18+
Generate a combined Rust + Python coverage HTML report.
19+
20+
Runs 'bazel coverage' for plantuml, validation and manual_analysis, then
21+
renders a single HTML report via genhtml.
22+
23+
Usage:
24+
bazel run //coverage:combined_report -- [options]
25+
26+
Options:
27+
--out-dir <path> Output directory for the HTML report.
28+
Default: <workspace>/coverage-html
29+
--targets <labels> Space-separated list of Bazel target patterns.
30+
Default: //plantuml/... //validation/... //manual_analysis/...
31+
--help Show this help.
32+
33+
Requirements:
34+
genhtml and lcov must be available either via the Bazel-managed @lcov_deb
35+
runfiles (automatic when run via 'bazel run //coverage:combined_report') or
36+
installed system-wide (apt install lcov).
37+
USAGE
38+
}
39+
40+
OUT_DIR=""
41+
TARGETS=""
42+
43+
while [[ $# -gt 0 ]]; do
44+
case "$1" in
45+
--out-dir)
46+
OUT_DIR="$2"; shift 2 ;;
47+
--targets)
48+
TARGETS="$2"; shift 2 ;;
49+
-h|--help)
50+
usage; exit 0 ;;
51+
*)
52+
echo "Unknown option: $1" >&2
53+
usage >&2
54+
exit 1 ;;
55+
esac
56+
done
57+
58+
# When invoked via 'bazel run', BUILD_WORKSPACE_DIRECTORY is set to the workspace
59+
# root. We must cd into it before calling nested bazel commands, because Bazel
60+
# refuses to be invoked from inside its own output tree.
61+
WORKSPACE_DIR="${BUILD_WORKSPACE_DIRECTORY:-$(bazel info workspace 2>/dev/null)}"
62+
cd "$WORKSPACE_DIR"
63+
64+
if [[ -z "$OUT_DIR" ]]; then
65+
OUT_DIR="${WORKSPACE_DIR}/coverage-html"
66+
fi
67+
68+
if [[ -z "$TARGETS" ]]; then
69+
TARGETS="//plantuml/... //validation/... //manual_analysis/..."
70+
fi
71+
72+
# ---------------------------------------------------------------------------
73+
# Resolve lcov tools: prefer Bazel-managed binaries from @lcov_deb runfiles
74+
# so that no system lcov installation is required. Fall back to PATH.
75+
# ---------------------------------------------------------------------------
76+
_tool_path() {
77+
local name="$1"
78+
local found=""
79+
# Search runfiles for the Bazel-managed binary (works regardless of the
80+
# canonical repo name Bazel assigns under bzlmod, e.g. +_repo_rules+lcov_deb)
81+
if [[ -n "${RUNFILES_DIR:-}" ]]; then
82+
found=$(find "${RUNFILES_DIR}" -path "*/lcov_deb/usr/bin/${name}" -type f 2>/dev/null | head -1)
83+
fi
84+
# Fall back to system PATH
85+
if [[ -z "${found}" ]]; then
86+
found=$(command -v "${name}" 2>/dev/null || true)
87+
fi
88+
echo "${found}"
89+
}
90+
91+
GENHTML="$(_tool_path genhtml)"
92+
LCOV="$(_tool_path lcov)"
93+
94+
if [[ -z "$GENHTML" ]]; then
95+
echo "ERROR: 'genhtml' not found. Run via 'bazel run //coverage:combined_report' or install 'lcov'." >&2
96+
exit 1
97+
fi
98+
if [[ -z "$LCOV" ]]; then
99+
echo "ERROR: 'lcov' not found. Run via 'bazel run //coverage:combined_report' or install 'lcov'." >&2
100+
exit 1
101+
fi
102+
103+
# When using the Bazel-managed tools, set PERL5LIB so Perl finds lcovutil.pm.
104+
if [[ -n "${RUNFILES_DIR:-}" ]]; then
105+
lcov_lib=$(find "${RUNFILES_DIR}" -path "*/lcov_deb/usr/lib/lcov" -type d 2>/dev/null | head -1)
106+
if [[ -n "${lcov_lib}" ]]; then
107+
export PERL5LIB="${lcov_lib}${PERL5LIB:+:${PERL5LIB}}"
108+
fi
109+
fi
110+
111+
echo "==> Running bazel coverage --config=coverage ${TARGETS}"
112+
# shellcheck disable=SC2086 # word-splitting of TARGETS is intentional
113+
bazel coverage --config=coverage $TARGETS
114+
115+
DAT_FILE="$(bazel info output_path)/_coverage/_coverage_report.dat"
116+
117+
if [[ ! -f "$DAT_FILE" ]]; then
118+
echo "ERROR: Coverage data not found at ${DAT_FILE}" >&2
119+
echo " Make sure at least one test ran successfully." >&2
120+
exit 1
121+
fi
122+
123+
echo "==> Generating HTML report in ${OUT_DIR}"
124+
mkdir -p "$OUT_DIR"
125+
126+
# Remove files that should not count towards coverage:
127+
# - external/ : Python files from rules_python internals captured by coverage.py
128+
# lcov --ignore-errors unused prevents a failure when none of the patterns match.
129+
FILTERED_DAT="${OUT_DIR}/filtered_coverage.dat"
130+
"$LCOV" --remove "$DAT_FILE" \
131+
"external/*" \
132+
--output-file "$FILTERED_DAT" \
133+
--ignore-errors unused
134+
135+
"$GENHTML" "$FILTERED_DAT" \
136+
--output-directory "$OUT_DIR" \
137+
--legend \
138+
--title "Combined Rust + Python Coverage" \
139+
--rc genhtml_hi_limit=95 \
140+
--ignore-errors source
141+
142+
echo ""
143+
echo "Coverage report: ${OUT_DIR}/index.html"

0 commit comments

Comments
 (0)