Code Coverage
Automated code coverage measurement and tracking using cargo-llvm-cov, with optional Codecov reporting.
Reference
Section titled “Reference”| Field | Value |
|---|---|
| Workflow | .github/workflows/ci-coverage.yml |
| Tool | cargo-llvm-cov |
| Integration | Codecov (optional) |
| Triggers | Via pipeline.yml on push/PR/tag, plus manual (workflow_dispatch) |
| Target | ≥90% coverage |
CI pipeline stages
Section titled “CI pipeline stages”The workflow automatically:
- Instrument — compile with coverage instrumentation.
- Execute — run all tests (unit, integration, doc).
- Collect — gather coverage data.
- Report — generate HTML, LCOV, and JSON reports.
- Upload — send to Codecov (if a token is configured) and upload the
coverage-reportartifact. - Enforce — fail CI if total coverage is below the 90% threshold.
Reports are available via Actions → Workflow Run → Artifacts → coverage-report (30-day retention).
Summary output
Section titled “Summary output”Filename Regions Missed Regions Coverage---------------------------------------------------------crates/lib.rs 45 3 93.33%crates/parser.rs 78 12 84.62%crates/utils.rs 23 0 100.00%---------------------------------------------------------TOTAL 146 15 89.73%- Regions: code regions (branches, statements).
- Missed Regions: not executed during tests.
- Coverage: percent of regions executed.
HTML report
Section titled “HTML report”Interactive report at target/llvm-cov/html/index.html:
- Green: covered lines.
- Red: uncovered lines.
- Yellow: partially covered branches.
Coverage types
Section titled “Coverage types”- Line coverage: percent of lines executed.
- Branch coverage: percent of conditional branches taken.
- Function coverage: percent of functions called.
Coverage goals
Section titled “Coverage goals”| Coverage | Quality | Action |
|---|---|---|
< 50% | Poor ❌ | Critical gaps |
50-70% | Fair ⚠️ | Needs improvement |
70-85% | Good ✅ | Acceptable |
> 85% | Excellent 🌟 | High quality |
Project target: ≥90%
What coverage does not measure
Section titled “What coverage does not measure”Coverage shows execution, not:
- Correctness: executed code may still be wrong.
- Edge cases: may miss unusual inputs.
- Logic errors: all branches covered ≠ all cases tested.
- Race conditions: concurrency issues are invisible.
Use coverage alongside mutation testing, property-based testing, and manual review.
How-to
Section titled “How-to”Install and generate coverage locally
Section titled “Install and generate coverage locally”# Install cargo-llvm-covcargo install cargo-llvm-cov
# Install the llvm-tools componentrustup component add llvm-tools-preview
# Generate coverage for all testscargo llvm-cov
# Generate HTML reportcargo llvm-cov --html --open
# Generate LCOV format (for Codecov)cargo llvm-cov --lcov --output-path lcov.info
# Generate JSON reportcargo llvm-cov --json --output-path coverage.jsonVerify: cargo llvm-cov prints a TOTAL coverage line.
Find and close coverage gaps
Section titled “Find and close coverage gaps”-
Show uncovered lines:
Terminal window cargo llvm-cov --show-missing-linescargo llvm-cov --ignore-filename-regex tests/ -
Add tests for the uncovered branch. For example, this error path is not covered:
pub fn divide(a: i32, b: i32) -> Result<i32, Error> {if b == 0 {return Err(Error::DivideByZero); // ❌ Not covered}Ok(a / b) // ✅ Covered}Add the missing error-case test:
#[test]fn test_divide_by_zero() {assert!(divide(10, 0).is_err());} -
Cover the common gap categories:
// Error paths#[test]fn test_errors() {assert!(parse("").is_err());assert!(parse("invalid").is_err());assert!(parse("too_long_".repeat(1000).as_str()).is_err());}// Edge cases / boundaries#[test]fn test_boundaries() {assert_eq!(clamp(0, 0, 10), 0); // minassert_eq!(clamp(10, 0, 10), 10); // maxassert_eq!(clamp(-1, 0, 10), 0); // below minassert_eq!(clamp(11, 0, 10), 10); // above max}// Conditional branches — exercise both sides#[test]fn test_both_branches() {assert!(process(b"valid", true).is_ok());assert!(process(b"data", false).is_ok());}For
matchexpressions, ensure every arm is exercised by a test.
Verify: re-run cargo llvm-cov and confirm the coverage percentage increased.
Configure coverage
Section titled “Configure coverage”# Exclude test filescargo llvm-cov --ignore-filename-regex tests/
# Exclude generated codecargo llvm-cov --ignore-filename-regex generated/
# Coverage with all featurescargo llvm-cov --all-features
# Coverage with specific featurescargo llvm-cov --features feature1,feature2Enforce a local threshold:
coverage=$(cargo llvm-cov --summary-only | grep -oP 'TOTAL.*\K\d+\.\d+')if (( $(echo "$coverage < 90" | bc -l) )); then echo "Coverage ${coverage}% below threshold 90%" exit 1fiVerify: the script exits non-zero when coverage drops below the threshold.
Set up Codecov
Section titled “Set up Codecov”-
Sign up at https://codecov.io/.
-
Add the repository (GitHub integration).
-
Get the token: Settings → Repository Upload Token.
-
Add the secret: GitHub repo → Settings → Secrets →
CODECOV_TOKEN. -
Upload (CI does this automatically; manual command below):
Terminal window cargo llvm-cov --lcov --output-path lcov.infobash <(curl -s https://codecov.io/bash) -f lcov.info
Codecov then provides PR coverage diffs, trend tracking, a README badge, and a sunburst map. Add the badge to README.md:
[](https://codecov.io/gh/USER/REPO)Verify: the Codecov dashboard shows the uploaded report.
Advanced usage
Section titled “Advanced usage”# Coverage for changed files only (in a PR)git diff --name-only main | grep '\.rs$' | xargs cargo llvm-cov --include-ffi
# Generate profdata for analysiscargo llvm-cov --no-report --profdata-output rust_template.profdatallvm-profdata show rust_template.profdata
# Include documentation testscargo llvm-cov --doc
# Coverage across a workspacecargo llvm-cov --workspacecargo llvm-cov --workspace --exclude member1Exclude code from coverage
Section titled “Exclude code from coverage”// Ignore unreachable safety invariants#[cfg(not(tarpaulin_include))]fn internal_safety_check() { unreachable!("Safety invariant violated");}
// Ignore debug-only code#[cfg(debug_assertions)]fn debug_only_function() { // Not covered in release builds}Troubleshooting
Section titled “Troubleshooting”Zero coverage:
rustup component add llvm-tools-previewcargo cleancargo llvm-covIncomplete coverage:
cargo llvm-cov --all-targetscargo llvm-cov --docSlow coverage:
cargo llvm-cov -- --test-threads=4cargo llvm-cov -- --skip slow_testCodecov upload fails:
echo $CODECOV_TOKENbash <(curl -s https://codecov.io/bash) -f lcov.info -vBest practices
Section titled “Best practices”- Aim for ≥90% — a good balance of quality and effort.
- Test error paths — don’t just test the happy path.
- Exclude test code — focus on production code.
- Use integration tests — cover real usage patterns.
- Track trends — coverage should improve over time.
- Don’t game metrics — meaningful tests beat a coverage number.
Why this matters
Section titled “Why this matters”Coverage answers exactly one question: which lines ran during the tests. That is necessary but not sufficient — code that never executes is certainly untested, but executed code with a weak assertion is also effectively untested. That is why the target sits at ≥90% rather than 100%: the last few percent are usually unreachable error branches or defensive code where the cost of contrived tests outweighs the value, and chasing the number invites assertion-free tests that inflate coverage without catching bugs. The honest use of coverage is as a gap-finder feeding real tests, paired with mutation and property testing to check whether those tests actually assert anything.