Rust Test Coverage with tarpaulin and cargo-llvm-cov
Code coverage tells you which lines your tests exercise. It won't tell you if your tests are good, but it does tell you what you're definitely not testing. In Rust, you have two main options: cargo-tarpaulin and cargo-llvm-cov. This guide covers both, when to use each, and how to integrate coverage into CI.
What Coverage Tells You (and What It Doesn't)
A line is "covered" if any test executes it. That's it. Coverage doesn't measure:
- Whether your assertions are correct
- Whether edge cases are handled
- Whether the test scenario is realistic
80% coverage can coexist with a terrible test suite. Use coverage as a floor ("we test at least this much") not a ceiling or a quality proxy. That said, 20% coverage is a red flag — significant parts of your code are completely untested.
cargo-tarpaulin
Tarpaulin is the oldest and most widely used Rust coverage tool. It works by intercepting system calls to track which lines execute.
Limitation: tarpaulin is Linux-only. It uses ptrace under the hood, which isn't available on macOS or Windows.
Installation
cargo install cargo-tarpaulinBasic Usage
# Run coverage and print summary to console
cargo tarpaulin
# Generate HTML report
cargo tarpaulin --out Html
# Generate multiple output formats
cargo tarpaulin --out Html --out Lcov --out Xml
# Exclude test files from coverage
cargo tarpaulin --exclude-files "tests/*"
# Set a minimum coverage threshold (fails if below)
cargo tarpaulin --fail-under 70The HTML report opens as tarpaulin-report.html in the current directory. It shows line-by-line coverage with green (covered) and red (not covered) highlighting.
Configuring tarpaulin
Create tarpaulin.toml in your project root:
[report]
out = ["Html", "Lcov"]
output-dir = "target/coverage"
[run]
exclude-files = ["tests/*", "benches/*"]
fail-under = 70
timeout = 120Or use .cargo/config.toml for project-wide settings:
[target.x86_64-unknown-linux-gnu]
# Allow tarpaulin to track coverageWhat tarpaulin Covers
Tarpaulin instruments:
- Unit tests (
#[cfg(test)]modules) - Integration tests (in
tests/) - Doc tests (with
--run-types Doctests)
# Include doc tests in coverage
cargo tarpaulin --run-types Tests Doctestscargo-llvm-cov
cargo-llvm-cov uses LLVM's built-in instrumentation, the same mechanism used by clang --coverage. It works on Linux, macOS, and Windows, making it the better choice for cross-platform development.
Installation
cargo install cargo-llvm-cov
# Also install the llvm-tools component
rustup component add llvm-tools-previewBasic Usage
# Print coverage summary
cargo llvm-cov
# Generate HTML report (opens in browser with --open)
cargo llvm-cov --html --open
# Generate lcov for tools like VS Code Coverage Gutters
cargo llvm-cov --lcov --output-path lcov.info
# Generate Cobertura XML (for CI systems)
cargo llvm-cov --cobertura --output-path coverage.xml
# Show line-by-line coverage in terminal
cargo llvm-cov --textConfiguring Exclusions
Exclude files and regions you don't want to count:
# Exclude by file pattern
cargo llvm-cov --ignore-filename-regex "tests|benches|build"
# Exclude generated code
cargo llvm-cov --ignore-filename-regex "\.generated\.rs$"For inline exclusions, use the #[coverage(off)] attribute (nightly only) or special comments:
// coverage: off
fn debug_helper() {
// not part of production logic
}
// coverage: onWorkspace Coverage
For workspaces with multiple crates, measure coverage across all of them:
# Coverage for entire workspace
cargo llvm-cov --workspace
# Coverage for specific package
cargo llvm-cov --package my-cratenextest Integration
If you use cargo-nextest (see cargo test deep dive):
cargo llvm-cov nextest --htmlComparing tarpaulin vs llvm-cov
| Feature | tarpaulin | llvm-cov |
|---|---|---|
| Platform | Linux only | Linux, macOS, Windows |
| Accuracy | Good | Excellent |
| Speed | Moderate | Fast |
| Branch coverage | Limited | Yes |
| nextest support | No | Yes |
| Setup complexity | Low | Low |
| Maintained | Yes | Yes (by community) |
Use tarpaulin if: you're on Linux and already use it in CI.
Use llvm-cov if: you develop on macOS, use Windows, want branch coverage, or want nextest integration.
Branch Coverage
Line coverage tells you a line was executed. Branch coverage tells you whether all paths through a conditional were taken.
fn absolute(n: i64) -> i64 {
if n < 0 { -n } else { n } // Two branches
}
#[test]
fn test_absolute() {
assert_eq!(absolute(5), 5); // Only tests n >= 0 branch
// Missing: test with n < 0
}Line coverage says this is 100% covered. Branch coverage says one branch is untested.
# llvm-cov includes branch coverage by default in HTML
cargo llvm-cov --html
# Explicit branch coverage output
cargo llvm-cov --branchTarpaulin has limited branch coverage support. llvm-cov handles it more accurately.
CI Integration
GitHub Actions with llvm-cov
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}GitHub Actions with tarpaulin
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Generate coverage
run: cargo tarpaulin --out Lcov --output-dir target/coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: target/coverage/lcov.infoCodecov Badge
Add a coverage badge to your README:
[](https://codecov.io/gh/your-org/your-repo)Enforcing Minimum Coverage
Fail CI if coverage drops below a threshold:
# With llvm-cov
cargo llvm-cov --fail-under-lines 70
# With tarpaulin
cargo tarpaulin --fail-under 70Set realistic thresholds. 70% is reasonable for most projects. New code pulled in from external sources (generated code, vendored dependencies) can skew numbers — exclude it.
VS Code Integration
Install the Coverage Gutters extension for VS Code. It reads lcov.info and shows coverage inline:
- Generate lcov:
cargo llvm-cov --lcov --output-path lcov.info - Open VS Code → Cmd+Shift+P → "Coverage Gutters: Display Coverage"
Red/green gutters appear next to line numbers, showing what's covered without leaving your editor.
Interpreting Coverage Reports
The HTML report from either tool shows:
- File-level summary — which files have low coverage
- Function coverage — which functions are never called
- Line coverage — which lines within functions never execute
- Branch coverage (llvm-cov) — which conditional branches are never taken
Start with function coverage. A function that's never called in any test is the most severe gap — you have zero confidence in it. Then look for files with very low line coverage.
Don't chase 100%. Some code is intentionally hard to test:
- Error handling for truly unexpected conditions
- Platform-specific fallback paths
- Debug/logging infrastructure
Mark these with comments and accept imperfect numbers.
Common Pitfalls
Async code coverage: async functions can show misleading coverage because the generated state machine has many virtual branches. Ensure your tests await futures to completion.
Generic code: generic functions are instantiated per type. Coverage may vary across instantiations. Test with multiple types if the behavior differs.
cfg-gated code: code behind #[cfg(feature = "...")] won't show in coverage unless you enable that feature:
cargo llvm-cov --features postgres,redis --htmlTest helpers in mod tests: the testing helper code itself counts toward coverage. If coverage is suspiciously high, check that you're not accidentally counting test utilities.
Summary
cargo-llvm-covis the recommended choice for new projects — cross-platform, accurate, supports branch coverage and nextestcargo-tarpaulinworks well on Linux and has a long track record- HTML reports give line-by-line visibility; lcov integrates with editors and Codecov
- Branch coverage catches untested conditional paths that line coverage misses
- CI integration with Codecov or similar catches coverage regressions before merge
- Treat coverage as a floor, not a target — 70% coverage with good tests beats 100% with trivial assertions