Skip to content

Commit b66f303

Browse files
authored
Merge pull request #549 from castler/js_use_llvm_cov_for_coverage_and_justification
use llvm cov for coverage and justification
2 parents 69e7e8c + 0b875d2 commit b66f303

19 files changed

Lines changed: 2410 additions & 132 deletions

.bazelrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ common --check_direct_dependencies=error # Issue an error if a direct dependency
113113
build --test_env="GTEST_COLOR=1"
114114

115115
# Configuration options required for quality assurance
116-
import %workspace%/quality/coverage.bazelrc
116+
import %workspace%/quality/coverage/coverage.bazelrc
117117
import %workspace%/quality/sanitizer/sanitizer.bazelrc
118118
import %workspace%/quality/static_analysis/static_analysis.bazelrc
119119

@@ -128,3 +128,6 @@ build --tool_java_runtime_version=remotejdk_21
128128

129129
# Import AI checker custom configuration
130130
try-import %workspace%/.bazelrc.ai_checker
131+
132+
# Enable user-defined configs
133+
try-import %workspace%/user.bazelrc

.github/workflows/coverage_report.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ jobs:
6060
- name: Allow linux-sandbox
6161
uses: ./actions/unblock_user_namespace_for_linux_sandbox
6262

63-
- name: Install Perl dependencies for genhtml
64-
run: sudo apt-get install -y --no-install-recommends lcov
65-
6663
- name: Run Unit Test with Coverage for C++
6764
id: run-coverage
6865
run: |

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ rust-project.json
2020

2121
# docs-as-code
2222
docs/ubproject.toml
23+
cpp_coverage/
24+
__pycache__/

BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
1717
load("@score_tooling//:defs.bzl", "copyright_checker")
1818
load("//tools/lint:linters.bzl", "use_clang_tidy_targets")
1919

20+
exports_files(["MODULE.bazel"])
21+
2022
compile_pip_requirements(
2123
name = "pip_requirements",
2224
src = "requirements.in",

MODULE.bazel

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -290,18 +290,6 @@ use_repo(
290290

291291
# We use here a pre-compiled fully static and hermetic clang_format binary
292292
# and not the one provided by llvm_toolchain, because the one from llvm_toolchain is not fully hermetic (and different version for now)
293-
###############################################################################
294-
# lcov deb package (provides genhtml + lcov for coverage HTML reports)
295-
###############################################################################
296-
deb = use_repo_rule("@download_utils//download/deb:defs.bzl", "download_deb")
297-
298-
deb(
299-
name = "lcov_deb",
300-
dev_dependency = True,
301-
integrity = "sha256-Ip14IkKavqBtkQ7mh6AXzr/6YyHpvSAZ0veMmw1+N80=",
302-
urls = ["https://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_2.0-4ubuntu2_all.deb"],
303-
)
304-
305293
download_file = use_repo_rule("@download_utils//download/file:defs.bzl", "download_file")
306294

307295
download_file(

quality/coverage.bazelrc

Lines changed: 0 additions & 30 deletions
This file was deleted.

quality/coverage/BUILD

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@ load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
1616
sh_binary(
1717
name = "generate_coverage_html",
1818
srcs = ["generate_coverage_html.sh"],
19-
data = ["@lcov_deb//:srcs"],
2019
visibility = ["//visibility:private"],
2120
)

quality/coverage/README.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Coverage Infrastructure
2+
3+
This directory contains the tooling to generate, post-process, and report C++ code coverage for the Score Communication project using LLVM's source-based coverage instrumentation (`llvm-cov`).
4+
5+
## Overview
6+
7+
```
8+
quality/coverage/
9+
├── README.md ← You are here
10+
├── BUILD ← Bazel target for generate_coverage_html
11+
├── coverage.bazelrc ← Bazel coverage configuration flags
12+
├── coverage_justifications.yaml ← Central justification database
13+
├── generate_coverage_html.sh ← Orchestrator script (entry point)
14+
└── llvm_cov/ ← Python tools for coverage processing
15+
├── README.md ← Detailed tool documentation
16+
├── BUILD ← Bazel targets for Python tools
17+
├── merger.py ← Per-test coverage output generator
18+
├── reporter.py ← Final combined report generator
19+
├── justify.py ← Justification resolver
20+
└── effective_coverage.py ← HTML post-processor & effective coverage calculator
21+
```
22+
23+
## Requirements
24+
25+
The coverage pipeline was built to satisfy the following requirements:
26+
27+
### REQ-COV-001: Native llvm-cov HTML Reports
28+
29+
Coverage reports **must** be generated directly by `llvm-cov show` using LLVM's source-based coverage (`--experimental_use_llvm_covmap`). No intermediate LCOV-to-HTML conversion (genhtml) is used. This provides accurate source-level coverage including branch and expansion views.
30+
31+
### REQ-COV-002: Instrumentation Filtering
32+
33+
Only project source code under `//score/message_passing` and `//score/mw/com` shall be instrumented and reported. Tests, benchmarks, and external/third-party code must be excluded from the report.
34+
35+
> **Note:** `--experimental_use_llvm_covmap` causes Bazel to instrument ALL targets regardless of `--instrumentation_filter`. Actual source filtering is enforced by `--ignore-filename-regex` in the merger and reporter. See `coverage.bazelrc` for details.
36+
37+
### REQ-COV-003: Coverage Justification Infrastructure
38+
39+
A YAML-based justification system must allow developers to "argue" non-covered lines and branches to achieve 100% effective coverage. Justified lines must:
40+
- Be tracked in a central YAML file with unique IDs, categories, and rationale
41+
- Optionally be referenced from code via `COV_JUSTIFIED` markers
42+
- Appear visually distinct (yellow/orange) in the HTML report
43+
- Be reflected in both per-file and total coverage percentages
44+
45+
### REQ-COV-004: Effective Coverage Calculation
46+
47+
The system must calculate and display:
48+
- **Raw coverage**: actual lines/branches hit ÷ total instrumented lines/branches
49+
- **Effective coverage**: (hit + justified) ÷ total
50+
51+
Both line and branch effective coverage must be shown in the summary, per-file index table, and totals row.
52+
53+
### REQ-COV-005: Stale Justification Detection
54+
55+
Justifications for lines/branches that are actually covered by tests must be detected and reported as stale warnings, enabling cleanup.
56+
57+
### REQ-COV-006: Template Instantiation Handling
58+
59+
For C++ templates with multiple instantiations, a line or branch is considered "covered" if ANY instantiation covers it (consistent with llvm-cov semantics). This prevents inflated totals from repeated template expansions.
60+
61+
### REQ-COV-007: Threshold Enforcement
62+
63+
The pipeline must support a configurable effective coverage threshold (default: 100%) and emit a warning when coverage falls below it.
64+
65+
## Quick Start
66+
67+
### 1. Run Coverage Collection
68+
69+
```bash
70+
# Full project
71+
bazel coverage //...
72+
73+
# Specific target
74+
bazel coverage //score/message_passing:client_connection_test_linux
75+
```
76+
77+
### 2. Generate the HTML Report
78+
79+
```bash
80+
bazel run //quality/coverage:generate_coverage_html
81+
```
82+
83+
This extracts the HTML report to `cpp_coverage/`, runs justification processing, and prints the coverage summary. Open the report:
84+
85+
```bash
86+
xdg-open cpp_coverage/index.html
87+
```
88+
89+
### 3. Create an Archive (CI)
90+
91+
```bash
92+
bazel run //quality/coverage:generate_coverage_html -- --archive coverage-report
93+
```
94+
95+
Creates `coverage-report.zip` containing the HTML report, LCOV data, and JUnit XML test results.
96+
97+
## Pipeline Architecture
98+
99+
The coverage pipeline has two phases:
100+
101+
### Phase 1: Bazel Coverage Collection
102+
103+
Configured by `coverage.bazelrc`, Bazel runs tests with coverage instrumentation enabled:
104+
105+
```
106+
bazel coverage //...
107+
108+
├── Per-test: merger.py (--coverage_output_generator)
109+
│ • Receives .profraw files from test execution
110+
│ • Merges into .profdata via llvm-profdata
111+
│ • Packages profdata + metadata into a zip
112+
113+
└── Final: reporter.py (--coverage_report_generator)
114+
• Merges all per-test profdata into one
115+
• Runs llvm-cov show → HTML report
116+
• Runs llvm-cov export → LCOV data
117+
• Runs llvm-cov report → text summary
118+
• Packages everything into _coverage_report.dat (zip)
119+
```
120+
121+
### Phase 2: Report Extraction & Justification
122+
123+
```
124+
bazel run //quality/coverage:generate_coverage_html
125+
126+
└── generate_coverage_html.sh
127+
├── Extract HTML from _coverage_report.dat → cpp_coverage/
128+
├── justify.py: YAML + code markers → manifest.json
129+
├── effective_coverage.py: Post-process HTML + calculate effective %
130+
└── Print summary + threshold check
131+
```
132+
133+
## Configuration
134+
135+
### coverage.bazelrc
136+
137+
Key settings:
138+
139+
| Flag | Purpose |
140+
|------|---------|
141+
| `--experimental_use_llvm_covmap` | Use LLVM source-based coverage (not gcov) |
142+
| `--instrumentation_filter` | Documents intended scope (not enforced by Bazel with covmap) |
143+
| `--coverage_output_generator` | Points to `merger.py` for per-test processing |
144+
| `--coverage_report_generator` | Points to `reporter.py` for final aggregation |
145+
| `--test_env=LLVM_PROFILE_CONTINUOUS_MODE=1` | Enables profiling of abnormal terminations |
146+
| `-mllvm -runtime-counter-relocation` | Required for continuous-mode profiling with LLVM |
147+
148+
### Environment Variables
149+
150+
| Variable | Default | Description |
151+
|----------|---------|-------------|
152+
| `COVERAGE_THRESHOLD` | `100` | Minimum effective line coverage % (warning if below) |
153+
154+
## Coverage Justifications
155+
156+
See [`coverage_justifications.yaml`](coverage_justifications.yaml) for the justification database and [`llvm_cov/README.md`](llvm_cov/README.md) for detailed documentation of the justification tools.
157+
158+
### Adding a Justification
159+
160+
1. **Via YAML** — add an entry to `coverage_justifications.yaml`:
161+
162+
```yaml
163+
justifications:
164+
- id: my-unique-id # kebab-case, must be unique
165+
category: defensive_programming # or: tool_false_positive, platform_specific, other
166+
reason: >
167+
Explanation of why these lines cannot be covered by tests.
168+
locations:
169+
- file: score/mw/com/impl/some_file.cpp
170+
line_start: 42
171+
line_end: 45
172+
```
173+
174+
2. **Via code markers** — reference the ID from source (no `locations` needed in YAML):
175+
176+
```cpp
177+
unreachable_code(); // COV_JUSTIFIED my-unique-id
178+
179+
// COV_JUSTIFIED_START my-unique-id
180+
defensive_block();
181+
more_defensive_code();
182+
// COV_JUSTIFIED_STOP
183+
```
184+
185+
Both methods can be combined. A justification covers both the line and any branches on that line.
186+
187+
We strongly suggest though to use the in-code marker where possible, as this better supports refactorings and avoids
188+
better that justifications get outdated.
189+
190+
### Justification Categories
191+
192+
| Category | Use Case |
193+
|----------|----------|
194+
| `defensive_programming` | Unreachable code kept as safety guard (e.g., default case in exhaustive switch) |
195+
| `tool_false_positive` | Coverage tool incorrectly marks line as uncovered |
196+
| `platform_specific` | Code path only reachable on platforms not under test |
197+
| `other` | Any other valid reason |
198+
199+
### Visual Indicators in HTML Report
200+
201+
| Color | Meaning |
202+
|-------|---------|
203+
| **Green** | Covered by tests |
204+
| **Red** | Not covered (needs tests or justification) |
205+
| **Yellow/Orange** | Justified — not covered but argued with rationale |
206+
207+
The index page shows a banner with overall effective coverage and updates per-file percentages in the table to reflect justifications.

quality/coverage/coverage.bazelrc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# NOTE: --experimental_use_llvm_covmap (required for llvm-cov) causes Bazel to instrument
15+
# ALL targets regardless of --instrumentation_filter. The actual source filtering happens
16+
# in the merger/reporter via --ignore-filename-regex. The instrumentation_filter is kept
17+
# for documentation purposes and in case this Bazel limitation is fixed in the future.
18+
coverage --nocache_test_results
19+
coverage --cxxopt=-O0
20+
coverage --instrumentation_filter="^//score/message_passing[/:],^//score/mw/com/(impl|gateway|dependability|design|example|mocking|doc)"
21+
coverage --experimental_generate_llvm_lcov
22+
coverage --experimental_use_llvm_covmap
23+
coverage --combined_report=lcov
24+
coverage --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux
25+
coverage --extra_toolchains=@ferrocene_x86_64_unknown_linux_gnu_llvm//:rust_ferrocene_toolchain
26+
27+
# Use llvm-cov directly for HTML report generation instead of genhtml/lcov.
28+
# The custom merger (per-test) receives profraw files and produces a zip with profdata + HTML.
29+
# The custom reporter (final) merges all profdata and generates the combined HTML report.
30+
coverage --coverage_output_generator=//quality/coverage/llvm_cov:merger
31+
coverage --coverage_report_generator=//quality/coverage/llvm_cov:reporter_wrapper
32+
coverage --experimental_fetch_all_coverage_outputs
33+
34+
# Override GENERATE_LLVM_LCOV to keep raw profraw files instead of converting to LCOV.
35+
# The custom merger handles profraw→profdata→HTML directly.
36+
coverage --test_env=GENERATE_LLVM_LCOV=0
37+
# Suppress the default gcov path since we use llvm-cov directly.
38+
coverage --test_env=COVERAGE_GCOV_PATH=/usr/bin/true
39+
40+
# These compile time options are required to cover abnormal termination cases. In GCC one can use `__gcc_dump()`, but this does not work with LLVM
41+
# LLVM provided these compile-time options in combination with a specific profile setting which is enabled in bazel via `LLVM_PROFILE_CONTINUOUS_MODE`
42+
coverage --test_env=LLVM_PROFILE_CONTINUOUS_MODE=1
43+
coverage --cxxopt -mllvm
44+
coverage --cxxopt -runtime-counter-relocation
45+
46+
# By default Bazel creates for each library a *.so for its tests. If there is a header that is used by multiple *.so files
47+
# and these *.so files have different instrumentation (production code has, test has not) a conflict arises which one to use when calculating coverage.
48+
# Then the first object is used, and it is pure luck if it contains the correct data or not. Even worse, small changes can change the order and then lead
49+
# to big coverage gaps. All these problems do not arise if no dynamic libs are used. Thus, we rather take bigger build times and binaries in advance for correct data.
50+
coverage --dynamic_mode=off

0 commit comments

Comments
 (0)