Skip to content

Commit a8a99d5

Browse files
feat(coverage-gate): add target-specific policies (#101)
* feat(coverage-gate): add target-specific policies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: defer cargo-anvil adoption until release Keep the target-aware coverage-gate implementation publishable on its own. Cargo Anvil can consume version 0.4.0 after the crate has been released to crates.io. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(coverage-gate): cover target policy paths Exercise rustc target discovery failures and every target policy shape so coverage and mutation gates validate the new behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(coverage-gate): skip process tests under Miri The rustc discovery tests require filesystem and process access, both forbidden by Miri isolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(coverage-gate): reuse thresholds for target opt-outs Remove the redundant target enabled flag. Target policies now use the existing min-lines-percent and expect-no-coverable-lines keys exclusively. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(coverage-gate): document configuration capabilities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(coverage-gate): resolve effective target policies lazily Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(coverage-gate): preserve cross-package coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(coverage-gate): tighten target policy contracts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(coverage-gate): cover nested target selectors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve manifest dependency ordering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(coverage-gate): generate 0.4.0 changelog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(coverage-gate): clarify target policy resolution Separate pending target selectors from resolved workspace members, and align the public contract and diagnostics around base and effective policies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ebe9b38 commit a8a99d5

14 files changed

Lines changed: 1283 additions & 133 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ cargo-gamma-process = { path = "crates/cargo-gamma-process", default-features =
4141
cargo-gamma-rt = { path = "crates/cargo-gamma-rt", default-features = false, version = "0.1.0" }
4242
cargo-gamma-unsafe = { path = "crates/cargo-gamma-unsafe", default-features = false, version = "0.1.0" }
4343
cargo-heather = { path = "crates/cargo-heather", default-features = false, version = "0.2.1" }
44+
# external dependencies
45+
cargo-platform = { version = "0.3.3", default-features = false }
4446
cargo_metadata = { version = "0.23.1", default-features = false }
4547
cel = { version = "0.14.4", default-features = false }
4648
chrono = { version = "0.4.45", default-features = false }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,24 @@
11
# Changelog
2+
## [0.4.0] - 2026-08-28
3+
4+
- ✨ Features
5+
6+
- add target-specific policies
7+
- add cargo-aprz and cargo-ensure-no-default-features ([#76](https://github.com/microsoft/ox-tools/pull/76))
8+
- run a command per workspace member with cargo-style selection ([#61](https://github.com/microsoft/ox-tools/pull/61))
9+
- add expect-no-coverable-lines assertion ([#51](https://github.com/microsoft/ox-tools/pull/51))
10+
11+
- 🐛 Bug Fixes
12+
13+
- preserve cross-package coverage
14+
- resolve effective target policies lazily
15+
16+
- 📚 Documentation
17+
18+
- document configuration capabilities
19+
20+
- ♻️ Code Refactoring
21+
22+
- tighten target policy contracts
23+
- reuse thresholds for target opt-outs
24+

crates/cargo-coverage-gate/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "cargo-coverage-gate"
66
description = "A cargo subcommand that gates pull requests on per-package line coverage measured by cargo-llvm-cov"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
readme = "README.md"
99
keywords = ["oxidizer", "cargo", "subcommand", "coverage", "ci"]
1010
categories = ["command-line-utilities", "development-tools::cargo-plugins"]
@@ -31,6 +31,7 @@ name = "cargo-coverage-gate"
3131
path = "src/bin/cargo-coverage-gate/main.rs"
3232

3333
[dependencies]
34+
cargo-platform = { workspace = true }
3435
cargo_metadata = { workspace = true }
3536
clap = { workspace = true, features = ["derive", "std", "help", "usage", "error-context"] }
3637
lcov = { workspace = true }

crates/cargo-coverage-gate/README.md

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
A pull-request-time gate that compares per-package line coverage produced
1919
by [`cargo-llvm-cov`][__link0] against per-package thresholds carried in
2020
`Cargo.toml`. The accompanying `cargo-coverage-gate` binary reads the
21-
coverage lcov tracefile, resolves each package’s threshold from a small
22-
three-layer lookup, and emits a verdict table to stdout (and,
21+
coverage lcov tracefile, resolves each package’s base policy from a small
22+
three-layer lookup, applies any matching package target policy, and emits a
23+
verdict table to stdout (and,
2324
optionally, to a Markdown summary file for CI step summaries). A failing
2425
verdict includes actionable details without relying on a later
2526
coverage-service upload. A coverable line is a distinct LCOV `DA:` record.
@@ -28,9 +29,27 @@ Numeric failures show exact covered/coverable counts and uncovered ranges;
2829
and `NO DATA` explains that no records were attributed. Location lists are
2930
bounded, with an exact count of omitted locations.
3031

31-
### Threshold resolution
32+
### Configuration
3233

33-
For each workspace member, the effective threshold is the first match
34+
#### Numeric thresholds
35+
36+
A workspace can define the default line-coverage threshold:
37+
38+
```toml
39+
# Illustrative workspace policy.
40+
[workspace.metadata.coverage-gate]
41+
min-lines-percent = 80
42+
```
43+
44+
Individual packages can override it:
45+
46+
```toml
47+
# Illustrative package policy, intentionally stricter than the workspace.
48+
[package.metadata.coverage-gate]
49+
min-lines-percent = 95
50+
```
51+
52+
For each workspace member, the base threshold is the first match
3453
among:
3554

3655
1. `[package.metadata.coverage-gate] min-lines-percent = N` in the package’s
@@ -40,13 +59,67 @@ among:
4059
1. The built-in default of `100.0` — full coverage required.
4160

4261
Setting `min-lines-percent = 0.0` explicitly opts a package out of
43-
gating (it always passes, regardless of attributed data). A package
44-
that legitimately contains no coverable lines (pure re-exports, type
45-
definitions, a thin binary shim) instead declares
46-
`expect-no-coverable-lines = true`: the gate passes only while that
47-
holds and fails — as a regression — if coverable lines later appear.
48-
The two keys are mutually exclusive, and `expect-no-coverable-lines`
49-
is package-scoped only.
62+
gating: it always passes, regardless of attributed data. Thresholds must
63+
be in the inclusive range `0.0..=100.0`.
64+
65+
#### Packages with no coverable lines
66+
67+
A package that legitimately contains no coverable lines (pure re-exports,
68+
type definitions, or a thin binary shim) can make that invariant explicit:
69+
70+
```toml
71+
[package.metadata.coverage-gate]
72+
expect-no-coverable-lines = true
73+
```
74+
75+
The gate passes only while the package has no attributed coverable lines
76+
and fails as a regression if coverable code later appears. This differs
77+
from `min-lines-percent = 0`, which keeps passing if the package grows
78+
coverable code. The two keys are mutually exclusive, and
79+
`expect-no-coverable-lines` is package-scoped only.
80+
81+
#### Target-specific policies
82+
83+
A package can replace its base policy for a Cargo-style target selector:
84+
85+
```toml
86+
[package.metadata.coverage-gate]
87+
min-lines-percent = 100
88+
89+
[package.metadata.coverage-gate.target.'cfg(not(windows))']
90+
expect-no-coverable-lines = true
91+
92+
[package.metadata.coverage-gate.target.x86_64-unknown-linux-gnu]
93+
min-lines-percent = 100
94+
```
95+
96+
A target-specific no-coverable-lines assertion uses the same nesting:
97+
98+
```toml
99+
[package.metadata.coverage-gate.target.thumbv7em-none-eabihf]
100+
expect-no-coverable-lines = true
101+
```
102+
103+
Target tables are package-scoped; they are invalid in workspace metadata.
104+
Their keys accept exact Rust target triples or quoted `cfg(...)` expressions
105+
using the target-derived subset of Cargo’s target grammar. Target
106+
configuration options such as `windows`, `unix`, `target_os`, and
107+
`target_arch` are supported. Build-context options such as `feature`, `test`,
108+
`debug_assertions`, and `proc_macro` are rejected because a standalone target
109+
query cannot evaluate them. A selected target table sets either
110+
`min-lines-percent` or `expect-no-coverable-lines = true`, completely
111+
replacing the package’s base policy to produce its effective policy. Exact
112+
triples take precedence over matching `cfg(...)` expressions. Multiple
113+
matching cfg policies are a configuration error rather than depending on
114+
declaration order.
115+
116+
A zero target-specific threshold disables gating on the matching target,
117+
but does not disable test execution or instrumentation. Those test binaries
118+
remain instrumented because they may contribute coverage to other packages.
119+
If cargo-llvm-cov reports that an instrumented run produced no coverage
120+
data, automation can supply an empty lcov tracefile: zero-threshold and
121+
`expect-no-coverable-lines` packages pass, while positively gated packages
122+
report `NO DATA`.
50123

51124
### Why lcov, not the JSON?
52125

@@ -66,6 +139,7 @@ Codecov / ADO numbers confusing.
66139

67140
```text
68141
cargo coverage-gate [--lcov <path>]... [-p|--package <spec>]...
142+
[--target <triple>]
69143
[--summary-file <path>] [--quiet]
70144
```
71145

@@ -97,25 +171,29 @@ let code = report.verdict().as_exit_code();
97171

98172
### Public API
99173

100-
The library exposes [`evaluate`][__link1], which returns an
101-
[`EvaluatedReport`][__link2]. The report can be rendered as plain text via
102-
[`EvaluatedReport::render_text`][__link3] or as GitHub-flavored Markdown
103-
via [`EvaluatedReport::render_markdown`][__link4], and reduced to a single
104-
[`Verdict`][__link5] via [`EvaluatedReport::verdict`][__link6]. The accompanying
105-
binary loads the lcov tracefile from disk and orchestrates rendering
106-
plus the appropriate exit code.
174+
[`evaluate`][__link1] gates one lcov tracefile for the rustc host target, while
175+
[`evaluate_many`][__link2] merges multiple tracefiles at line level.
176+
[`evaluate_many_for_target`][__link3] evaluates a selected Rust target, which may be
177+
supplied explicitly or omitted to select the rustc host target.
178+
Evaluation returns an [`EvaluatedReport`][__link4], which renders as plain
179+
text via [`EvaluatedReport::render_text`][__link5] or GitHub-flavored Markdown via
180+
[`EvaluatedReport::render_markdown`][__link6] and reduces to a [`Verdict`][__link7] via
181+
[`EvaluatedReport::verdict`][__link8]. The accompanying binary loads tracefiles from
182+
disk and orchestrates rendering plus the appropriate exit code.
107183

108184

109185
<hr/>
110186
<sub>
111187
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/ox-tools/tree/main/crates/cargo-coverage-gate">source code</a>.
112188
</sub>
113189

114-
[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbSLvuLqYvzncbvkJE0ZCfihkbvLj44R1A234bYHxJKfzepgZhZIGDc2NhcmdvLWNvdmVyYWdlLWdhdGVlMC4zLjBzY2FyZ29fY292ZXJhZ2VfZ2F0ZQ
190+
[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQb3wjNoVxGaCAbpkmpjr98NCcbw-HRsqJQXfkb8-afvWiSredhZIGDc2NhcmdvLWNvdmVyYWdlLWdhdGVlMC40LjBzY2FyZ29fY292ZXJhZ2VfZ2F0ZQ
115191
[__link0]: https://github.com/taiki-e/cargo-llvm-cov
116-
[__link1]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/fn.evaluate.html
117-
[__link2]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/struct.EvaluatedReport.html
118-
[__link3]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/?search=EvaluatedReport::render_text
119-
[__link4]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/?search=EvaluatedReport::render_markdown
120-
[__link5]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/enum.Verdict.html
121-
[__link6]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/?search=EvaluatedReport::verdict
192+
[__link1]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/fn.evaluate.html
193+
[__link2]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/fn.evaluate_many.html
194+
[__link3]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/fn.evaluate_many_for_target.html
195+
[__link4]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/struct.EvaluatedReport.html
196+
[__link5]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/?search=EvaluatedReport::render_text
197+
[__link6]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/?search=EvaluatedReport::render_markdown
198+
[__link7]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/enum.Verdict.html
199+
[__link8]: https://docs.rs/cargo-coverage-gate/0.4.0/cargo_coverage_gate/?search=EvaluatedReport::verdict

0 commit comments

Comments
 (0)