Why Code Coverage is More Than Just a CI/CD Checkbox?
\n\nImagine: you wrote a Rust module for parsing complex configuration files. The compiler is happy, tests pass, but a month later production crashes with an error on a rare edge case. Sound familiar? This is where code coverage in Rust comes into play — a metric that shows what percentage of your code is actually executed during tests.
\n\nBut let's be honest: chasing 100% coverage is a trap. It's much more important to understand which lines remain untested and why. In this article, we'll break down three main tools for Rust: tarpaulin, grcov, and the built-in LLVM toolchain. You'll learn how to turn dry percentages into actionable insights for refactoring.
Why Should a Rust Developer Care About Coverage?
\nRust is famous for memory safety, but that doesn't eliminate logic errors. Code coverage helps:
\n- \n
- Detect "dead code" — functions or branches that are never called. \n
- Increase confidence during refactoring — knowing critical paths are covered by tests. \n
- Improve documentation — tests often serve as the best documentation if they cover all scenarios. \n
The main metric is line coverage, but we'll also look at branch coverage, which is especially important in Rust due to the abundance of match expressions.
\n\nTool #1: cargo-tarpaulin — Simplicity and Speed
\ntarpaulin is the most popular tool for code coverage in Rust. It works "out of the box" and requires no complex setup. Installation:
cargo install cargo-tarpaulin\nRunning a basic analysis:
\ncargo tarpaulin --out Html\nThis command generates an HTML report in the tarpaulin-report.html folder. Open it — and you'll see a color map of your project: green lines (covered by tests), red lines (uncovered), and gray lines (non-executable, e.g., comments).
Example: Testing a Simple Function
\nCreate a file src/lib.rs with a factorial function:
pub fn factorial(n: u32) -> u32 {\n match n {\n 0 | 1 => 1,\n _ => n * factorial(n - 1),\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_factorial() {\n assert_eq!(factorial(0), 1);\n assert_eq!(factorial(1), 1);\n assert_eq!(factorial(5), 120);\n }\n}\nRun tarpaulin:
\ncargo tarpaulin --ignore-tests --out Html\nThe --ignore-tests flag excludes the test code itself from the report. Result: 100% coverage for this function. But let's make it more complex — add error handling:
pub fn factorial_safe(n: i32) -> Result<u32, String> {\n if n < 0 {\n return Err("Negative number!".to_string());\n }\n Ok(factorial(n as u32))\n}\nNow tarpaulin will show that the Err branch is not covered by tests. Add a test:
#[test]\nfn test_negative() {\n assert!(factorial_safe(-5).is_err());\n}\nAfter running again, coverage returns to 100%.
\n\nAdvanced tarpaulin Settings
\nFor large projects, use:
\n- \n
--fail-under 80— exit with an error if coverage is below 80%. \n --skip-clean— don't rebuild the project if changes are minor. \n --engine llvm— use the LLVM toolchain (experimental but more accurate). \n
Tool #2: grcov — The King of Integrations
\ngrcov is a more flexible tool that works with the LLVM toolchain. It's especially useful if you use CI/CD (GitHub Actions, GitLab