Rust Test Coverage with tarpaulin and cargo-llvm-cov

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-tarpaulin

Basic 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 70

The 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 = 120

Or use .cargo/config.toml for project-wide settings:

[target.x86_64-unknown-linux-gnu]
# Allow tarpaulin to track coverage

What 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 Doctests

cargo-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-preview

Basic 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 --text

Configuring 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: on

Workspace 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-crate

nextest Integration

If you use cargo-nextest (see cargo test deep dive):

cargo llvm-cov nextest --html

Comparing 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 --branch

Tarpaulin 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.info

Codecov Badge

Add a coverage badge to your README:

[![Coverage](https://codecov.io/gh/your-org/your-repo/branch/main/graph/badge.svg)](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 70

Set 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:

  1. Generate lcov: cargo llvm-cov --lcov --output-path lcov.info
  2. 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 --html

Test 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-cov is the recommended choice for new projects — cross-platform, accurate, supports branch coverage and nextest
  • cargo-tarpaulin works 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

Read more

Start now free