Coverage & CI
Coverage measures which lines executed during tests. cargo-llvm-cov integrates with Rust's LLVM coverage and fits CI pipelines alongside fmt, clippy, and nextest.
Recipe
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --lcov --output-path lcov.info# CI excerpt
- run: cargo llvm-cov --workspace --fail-under-lines 70When to reach for this: Tracking test gaps on critical crates and preventing coverage regressions on main.
Working Example
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.97.0
- run: cargo fmt --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo nextest run --workspace
- run: cargo install cargo-llvm-cov
- run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- uses: codecov/codecov-action@v4
with:
files: lcov.infoWhat this demonstrates:
- Toolchain pinned to Rust 1.97.0
- Tests run before coverage collection
- LCOV export for Codecov or similar
- Optional
--fail-under-linesgate on PRs
Deep Dive
What Coverage Means in Rust
| Metric | Notes |
|---|---|
| Line coverage | Common CI metric |
| Branch coverage | Harder; llvm-cov focus on regions |
Uncovered match arms | High-value targets |
Coverage does not equal correctness. Pair with clippy, property tests, and review.
Local Workflow
cargo llvm-cov --open # HTML report
cargo llvm-cov --summary-only # quick terminal summaryExclude generated code and #[cfg(test)] only modules via config file .llvm-cov.toml if needed.
Gotchas
- Chasing 100% coverage - diminishing returns and brittle tests. Fix: gate critical crates (domain, payment) higher than glue code.
- Coverage without tests running - false confidence. Fix: ensure
cargo testornextestpasses first in CI job. - Doctests skew - run
cargo llvm-cov --docseparately if policy includes docs. Fix: document which targets count toward gate. - Flaky integration tests in coverage job - red CI unrelated to coverage. Fix: split fast unit+coverage job from slow E2E.
- Debug profile coverage - not comparable to release. Fix: coverage always on test profile (debug) consistently.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
tarpaulin | Linux-only quick setup | Cross-platform llvm consistency needed |
| No coverage gate | Early prototype | Production payment/auth paths |
| Mutation testing | Test quality audit | Default CI (expensive) |
FAQs
llvm-cov vs tarpaulin?
llvm-cov uses rustc's native instrumentation; generally preferred for accuracy and workspace support.
What fail-under threshold?
Start 60-70% lines on domain crates; ratchet upward; never block on generated/boilerplate modules.
Include integration tests?
Yes by default when running full workspace test; use -p to scope.
How exclude files?
.llvm-cov.toml ignore patterns for build.rs output and tests/fixtures.
Coverage on macOS CI?
llvm-cov works; ensure Xcode/clang toolchain available on runner.
Merge coverage from matrix?
Upload per-job lcov; Codecov merges reports from OS/feature matrix.
Does coverage include benches?
No unless you run benches under llvm-cov explicitly; benches are separate targets.
Feature matrix coverage?
Run --all-features job separately; coverage can differ per cfg.
PR comments?
Codecov or similar posts diff coverage on changed lines.
Workspace virtual manifest?
Run from root with --workspace; same as tests.
Related
- Testing Best Practices - CI policy
- Linting in CI - fmt/clippy gates
- CI Config for Rust - full pipeline
- Useful Cargo Subcommands - nextest
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.