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
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ jobs:
run: |
cd cr_checker/tests
bazel test //...
- name: Run coverage module tests
run: |
bazel test //coverage/tests:all
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

module(
name = "score_tooling",
version = "1.0.5",
version = "1.1.0",
compatibility_level = 1,
)

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ See the individual README files for detailed usage instructions and configuratio
| **python_basics** | Python development utilities and testing | [README](python_basics/README.md) |
| **starpls** | Starlark language server support | [README](starpls/README.md) |
| **tools** | Formatters & Linters | [README](tools/README.md) |
| **coverage** | Ferrocene Rust coverage workflow | [README](coverage/README.md) |

## Usage Examples

Expand All @@ -32,6 +33,27 @@ Load tools in your `BUILD` files:
```starlark
load("@score_tooling//:defs.bzl", "score_py_pytest")
load("@score_tooling//:defs.bzl", "cli_tool")
load("@score_tooling//coverage:coverage.bzl", "rust_coverage_report")
```

Create a repo-local coverage target:

```starlark
rust_coverage_report(
name = "rust_coverage",
bazel_configs = [
"ferrocene-x86_64-linux",
"ferrocene-coverage",
],
query = 'kind("rust_test", //...)',
min_line_coverage = "80",
)
```

Then run:

```bash
bazel run //:rust_coverage -- --min-line-coverage 80
```

## Upgrading from separate MODULES
Expand All @@ -53,6 +75,7 @@ The available import targets are:
- cli_helper
- use_format_targets
- setup_starpls
- rust_coverage_report

## Format the tooling repository
```bash
Expand Down
38 changes: 38 additions & 0 deletions coverage/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# *******************************************************************************
# Copyright (c) 2025 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
# *******************************************************************************

load("@rules_shell//shell:sh_binary.bzl", "sh_binary")

package(default_visibility = ["//visibility:public"])

exports_files([
"ferrocene_report_wrapper.sh.tpl",
"scripts/normalize_symbol_report.py",
"scripts/parse_line_coverage.py",
])

sh_binary(
name = "ferrocene_report",
srcs = ["ferrocene_report.sh"],
data = [
"scripts/normalize_symbol_report.py",
"scripts/parse_line_coverage.py",
],
visibility = ["//visibility:public"],
)

sh_binary(
name = "llvm_profile_wrapper",
srcs = ["llvm_profile_wrapper.sh"],
visibility = ["//visibility:public"],
)
163 changes: 163 additions & 0 deletions coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Ferrocene Rust Coverage

This directory provides the Ferrocene Rust coverage workflow for Bazel-based
projects. It uses Ferrocene's `symbol-report` and `blanket` tools to generate
HTML coverage reports from `.profraw` files produced by Rust tests.

The workflow is intentionally split:
- Tests produce `.profraw` files (can run on host or target hardware).
- Reports are generated later on a host machine.

This makes it easy to collect coverage from cross-compiled tests or from
hardware-in-the-loop runs.

## Quick Start (Developers)

1) Run tests with coverage enabled:

```bash
bazel test --config=ferrocene-x86_64-linux --config=ferrocene-coverage \
--nocache_test_results \
//path/to:rust_tests
```

2) Generate coverage reports:

```bash
bazel run //:rust_coverage -- --min-line-coverage 80
```

The default report directory is:

```
$(bazel info bazel-bin)/coverage/rust-tests/<target>/blanket/index.html
```

The script prints per-target line coverage plus an overall summary line.

## Integrator Setup

### 1) MODULE.bazel

Add `score_tooling` and `score_toolchains_rust` as dependencies:

```starlark
bazel_dep(name = "score_tooling", version = "1.0.0")
bazel_dep(name = "score_toolchains_rust", version = "0.4.0")
```

### 2) .bazelrc

Add a Ferrocene coverage config. Names are examples; choose names that fit
your repo:

```
# Ferrocene toolchain for host execution
build:ferrocene-x86_64-linux --host_platform=@score_bazel_platforms//:x86_64-linux
build:ferrocene-x86_64-linux --platforms=@score_bazel_platforms//:x86_64-linux
build:ferrocene-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu

# Coverage flags for rustc
build:ferrocene-coverage --@rules_rust//rust/settings:extra_rustc_flag=-Cinstrument-coverage
build:ferrocene-coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code
build:ferrocene-coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1
build:ferrocene-coverage --@rules_rust//rust/settings:extra_rustc_flag=-Cdebuginfo=2
build:ferrocene-coverage --@rules_rust//rust/settings:extra_exec_rustc_flag=-Cinstrument-coverage
build:ferrocene-coverage --@rules_rust//rust/settings:extra_exec_rustc_flag=-Clink-dead-code
build:ferrocene-coverage --@rules_rust//rust/settings:extra_exec_rustc_flag=-Ccodegen-units=1
build:ferrocene-coverage --@rules_rust//rust/settings:extra_exec_rustc_flag=-Cdebuginfo=2
test:ferrocene-coverage --run_under=@score_tooling//coverage:llvm_profile_wrapper
```

### 3) Add a repo-local wrapper target

In a root `BUILD` file:

```starlark
load("@score_tooling//coverage:coverage.bzl", "rust_coverage_report")

rust_coverage_report(
name = "rust_coverage",
bazel_configs = [
"ferrocene-x86_64-linux",
"ferrocene-coverage",
],
query = 'kind("rust_test", //...)',
min_line_coverage = "80",
)
```

Run it with:

```bash
bazel run //:rust_coverage
```

### 4) Optional: exclude known-problematic targets

```starlark
query = 'kind("rust_test", //...) except //path/to:tests',
```

## Cross/Target Execution

If tests run on target hardware, copy the `.profraw` files back to the host
and point the report generator to the directory:

```bash
bazel run //:rust_coverage -- --profraw-dir /path/to/profraw
```

## Coverage Gate Behavior

`--min-line-coverage` applies per target. If any target is below the minimum,
the script exits non-zero so CI can fail the job. An overall summary is printed
for visibility but does not change gating behavior.

## Common Pitfalls

- **"running 0 tests"**: The Rust test harness found no `#[test]` functions,
so coverage is 0%. Add tests or exclude the target from the query.
- **"couldn't find source file"** warnings: Usually path remapping or crate
mapping issues. Check that `crate` attributes in `rust_test` targets point to
the library crate (or exclude the target).
- **Cached test results**: Use `--nocache_test_results` if you need to re-run
tests and regenerate `.profraw` files.

## Troubleshooting

### Coverage is 0% but tests ran
- Verify the target contains real `#[test]` functions. A rust_test target with
no tests will run but report 0% coverage.
- Ensure you ran tests with `--config=ferrocene-coverage` so `.profraw` files
exist.
- If the test binary is cached, use `--nocache_test_results`.

### "couldn't find source file" warnings
- Check `crate` mapping on `rust_test` targets. If `crate = "name"` is used,
ensure it refers to the library crate in the same package.
- Confirm the reported paths exist in the workspace. Path remapping is required
so `blanket` can resolve files under `--ferrocene-src`.

### No `.profraw` files found
- Ensure `test:ferrocene-coverage` sets `--run_under=@score_tooling//coverage:llvm_profile_wrapper`.
- Re-run tests with `--nocache_test_results`.
- If tests ran on target hardware, copy the `.profraw` files back and pass
`--profraw-dir`.

### Coverage gate fails in CI
- The gate is per-target. A single target below the threshold fails the job.
- Use a stricter query (exclude known-zero targets) or add tests.

## CI Integration (Suggested Pattern)

Keep coverage generation separate from docs:

1) Coverage workflow:
- run `bazel run //:rust_coverage`
- upload `bazel-bin/coverage/rust-tests` as an artifact

2) Docs workflow:
- download the artifact
- copy into the docs output (e.g. `docs/_static/coverage/`)
- publish Sphinx docs to GitHub Pages
84 changes: 84 additions & 0 deletions coverage/coverage.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# *******************************************************************************
# Copyright (c) 2025 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
# *******************************************************************************

"""Bazel helpers for Ferrocene Rust coverage workflows."""

def _shell_quote(value):
if value == "":
return "''"
return "'" + value.replace("'", "'\"'\"'") + "'"
Comment on lines +16 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't quiet understand this.


def _rust_coverage_report_impl(ctx):
script = ctx.actions.declare_file(ctx.label.name + ".sh")

args = []
for cfg in ctx.attr.bazel_configs:
if cfg:
args.extend(["--bazel-config", cfg])
if ctx.attr.query:
args.extend(["--query", ctx.attr.query])
if ctx.attr.min_line_coverage:
args.extend(["--min-line-coverage", ctx.attr.min_line_coverage])

args_parts = [_shell_quote(a) for a in args]

# The wrapper script forwards preconfigured args and any extra CLI args.
exec_line = "exec \"${ferrocene_report}\""
if args_parts:
exec_line += " \\\n " + " \\\n ".join(args_parts)
exec_line += " \\\n \"$@\""

# Resolve the report script via runfiles for remote/CI compatibility.
runfile_path = ctx.executable._ferrocene_report.short_path
ctx.actions.expand_template(
template = ctx.file._wrapper_template,
output = script,
substitutions = {
"@@RUNFILE@@": _shell_quote(runfile_path),
"@@EXEC_LINE@@": exec_line,
},
is_executable = True,
)

report_runfiles = ctx.attr._ferrocene_report[DefaultInfo].default_runfiles
runfiles = ctx.runfiles(files = [ctx.executable._ferrocene_report]).merge(report_runfiles)
return [DefaultInfo(executable = script, runfiles = runfiles)]

rust_coverage_report = rule(
implementation = _rust_coverage_report_impl,
executable = True,
attrs = {
"bazel_configs": attr.string_list(
default = ["ferrocene-coverage"],
doc = "Bazel configs passed to ferrocene_report.",
),
"query": attr.string(
default = 'kind("rust_test", //...)',
doc = "Bazel query used to discover rust_test targets.",
),
"min_line_coverage": attr.string(
default = "",
doc = "Optional minimum line coverage percentage.",
),
"_ferrocene_report": attr.label(
default = Label("//coverage:ferrocene_report"),
executable = True,
cfg = "exec",
),
"_wrapper_template": attr.label(
default = Label("//coverage:ferrocene_report_wrapper.sh.tpl"),
allow_single_file = True,
),
},
doc = "Creates a repo-local wrapper for Ferrocene Rust coverage reports.",
)
Loading