Code Coverage — Seeing What Tests Miss 🟢
代码覆盖率:看见测试遗漏的部分 🟢
What you’ll learn:
本章将学到什么:
- Source-based coverage with
cargo-llvm-cov(the most accurate Rust coverage tool)
如何使用源码级覆盖率工具cargo-llvm-cov,这是 Rust 里最准确的覆盖率方案- Quick coverage checks with
cargo-tarpaulinand Mozilla’sgrcov
如何用cargo-tarpaulin与 Mozilla 的grcov做快速覆盖率检查- Setting up coverage gates in CI with Codecov and Coveralls
如何在 CI 里结合 Codecov 和 Coveralls 建立覆盖率门槛- A coverage-guided testing strategy that prioritizes high-risk blind spots
如何基于覆盖率制定测试策略,优先填补高风险盲区Cross-references: Miri and Sanitizers — coverage finds untested code, Miri finds UB in tested code · Benchmarking — coverage shows what’s tested, benchmarks show what’s fast · CI/CD Pipeline — coverage gate in the pipeline
交叉阅读: Miri 与 Sanitizer 用来发现“已经被测试覆盖到的代码”里有没有未定义行为;覆盖率负责找出“根本没测到的代码”。基准测试 回答的是“哪里快”,覆盖率回答的是“哪里测到了”。CI/CD 流水线 则会把覆盖率门槛接进流水线。
Code coverage measures which lines, branches, or functions your tests actually execute. It doesn’t prove correctness (a covered line can still have bugs), but it reliably reveals blind spots — code paths that no test exercises at all.
代码覆盖率衡量的是:测试真实执行到了哪些代码行、哪些分支、哪些函数。它并不能证明程序正确,因为一行被执行过的代码照样可能有 bug;但它能非常稳定地揭露 盲区,也就是那些完全没有任何测试碰到的代码路径。
With 1,006 tests across many crates, the project has substantial test investment. Coverage analysis answers: “Is that investment reaching the code that matters?”
当前工程分布在多个 crate 上,已经有 1,006 个测试,投入其实不小。覆盖率分析要回答的问题就是:这些测试投入,到底有没有覆盖到真正重要的代码。
Source-Based Coverage with llvm-cov
使用 llvm-cov 做源码级覆盖率分析
Rust uses LLVM, which provides source-based coverage instrumentation — the most accurate coverage method available. The recommended tool is cargo-llvm-cov:
Rust 基于 LLVM,而 LLVM 自带源码级覆盖率插桩能力,这是当前最准确的覆盖率手段。推荐工具是 cargo-llvm-cov。
# Install
cargo install cargo-llvm-cov
# Or via rustup component (for the raw llvm tools)
rustup component add llvm-tools-preview
Basic usage:
基础用法:
# Run tests and show per-file coverage summary
cargo llvm-cov
# Generate HTML report (browsable, line-by-line highlighting)
cargo llvm-cov --html
# Output: target/llvm-cov/html/index.html
# Generate LCOV format (for CI integrations)
cargo llvm-cov --lcov --output-path lcov.info
# Workspace-wide coverage (all crates)
cargo llvm-cov --workspace
# Include only specific packages
cargo llvm-cov --package accel_diag --package topology_lib
# Coverage including doc tests
cargo llvm-cov --doctests
Reading the HTML report:
怎么看 HTML 报告:
target/llvm-cov/html/index.html
├── Filename │ Function │ Line │ Branch │ Region
├─ accel_diag/src/lib.rs │ 78.5% │ 82.3% │ 61.2% │ 74.1%
├─ sel_mgr/src/parse.rs│ 95.2% │ 96.8% │ 88.0% │ 93.5%
├─ topology_lib/src/.. │ 91.0% │ 93.4% │ 79.5% │ 89.2%
└─ ...
Green = covered Red = not covered Yellow = partially covered (branch)
Green = covered Red = not covered Yellow = partially covered (branch)
绿色表示已覆盖,红色表示未覆盖,黄色表示部分覆盖,通常意味着分支只走到了其中一部分。
Coverage types explained:
几种覆盖率指标分别代表什么:
| Type 类型 | What It Measures 衡量内容 | Significance 意义 |
|---|---|---|
| Line coverage 行覆盖率 | Which source lines were executed 哪些源码行被执行过 | Basic “was this code reached?” 最基础的“这段代码有没有被跑到” |
| Branch coverage 分支覆盖率 | Which if/match arms were taken哪些 if 或 match 分支被走到 | Catches untested conditions 更容易发现条件分支漏测 |
| Function coverage 函数覆盖率 | Which functions were called 哪些函数被调用过 | Finds dead code 适合发现死代码 |
| Region coverage 区域覆盖率 | Which code regions (sub-expressions) were hit 哪些更细粒度代码区域被命中 | Most granular 颗粒度最细 |
cargo-tarpaulin — The Quick Path
cargo-tarpaulin:快速上手路线
cargo-tarpaulin is a Linux-specific coverage tool that’s simpler to set up (no LLVM components needed):cargo-tarpaulin 是一个仅支持 Linux 的覆盖率工具,搭起来更省事,因为不需要额外折腾 LLVM 组件。
# Install
cargo install cargo-tarpaulin
# Basic coverage report
cargo tarpaulin
# HTML output
cargo tarpaulin --out Html
# With specific options
cargo tarpaulin \
--workspace \
--timeout 120 \
--out Xml Html \
--output-dir coverage/ \
--exclude-files "*/tests/*" "*/benches/*" \
--ignore-panics
# Skip certain crates
cargo tarpaulin --workspace --exclude diag_tool # exclude the binary crate
tarpaulin vs llvm-cov comparison:tarpaulin 和 llvm-cov 的对比:
| Feature 特性 | cargo-llvm-cov | cargo-tarpaulin |
|---|---|---|
| Accuracy 准确性 | Source-based (most accurate) 源码级,最准确 | Ptrace-based (occasional overcounting) 基于 ptrace,偶尔会高估 |
| Platform 平台 | Any (llvm-based) 跨平台,只要 LLVM 可用 | Linux only 仅 Linux |
| Branch coverage 分支覆盖率 | Yes 支持 | Limited 支持有限 |
| Doc tests 文档测试 | Yes 支持 | No 不支持 |
| Setup 准备成本 | Needs llvm-tools-preview需要 llvm-tools-preview | Self-contained 自身更完整 |
| Speed 速度 | Faster (compile-time instrumentation) 更快,编译期插桩 | Slower (ptrace overhead) 更慢,ptrace 有额外开销 |
| Stability 稳定性 | Very stable 很稳定 | Occasional false positives 偶尔会有误报 |
Recommendation: Use cargo-llvm-cov for accuracy. Use cargo-tarpaulin when you need a quick check without installing LLVM tools.
建议做法 很简单:重视准确性时用 cargo-llvm-cov;只想快速看一眼、又懒得装 LLVM 工具时,再考虑 cargo-tarpaulin。
grcov — Mozilla’s Coverage Tool
grcov:Mozilla 的覆盖率聚合工具
grcov is Mozilla’s coverage aggregator. It consumes raw LLVM profiling data and produces reports in multiple formats:grcov 是 Mozilla 出的覆盖率聚合工具。它吃的是原始 LLVM profiling 数据,然后吐出多种格式的覆盖率报告。
# Install
cargo install grcov
# Step 1: Build with coverage instrumentation
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests
# Step 2: Run tests (generates .profraw files)
cargo test
# Step 3: Aggregate with grcov
grcov target/coverage/ \
--binary-path target/debug/ \
--source-dir . \
--output-types html,lcov \
--output-path target/coverage/report \
--branch \
--ignore-not-existing \
--ignore "*/tests/*" \
--ignore "*/.cargo/*"
# Step 4: View report
open target/coverage/report/html/index.html
When to use grcov: It’s most useful when you need to merge coverage from multiple test runs (e.g., unit tests + integration tests + fuzz tests) into a single report.
什么时候该用 grcov:当覆盖率需要从多轮测试里合并时,它就很值钱。例如单元测试、集成测试、fuzz 测试各跑一遍,然后合成一份总报告。
Coverage in CI: Codecov and Coveralls
CI 里的覆盖率:Codecov 与 Coveralls
Upload coverage data to a tracking service for historical trends and PR annotations:
把覆盖率数据上传到托管服务以后,就能查看历史趋势,也能在 PR 上挂注释。
# .github/workflows/coverage.yml
name: Code Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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 --workspace --lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Optional: enforce minimum coverage
- name: Check coverage threshold
run: |
cargo llvm-cov --workspace --fail-under-lines 80
# Fails the build if line coverage drops below 80%
Coverage gates — enforce minimums per crate by reading the JSON output:
覆盖率门槛 还可以更细,借助 JSON 输出按 crate 单独卡最低值。
# Get per-crate coverage as JSON
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'
# Fail if below threshold
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60
Coverage-Guided Testing Strategy
基于覆盖率的测试策略
Coverage numbers alone are meaningless without a strategy. Here’s how to use coverage data effectively:
只有数字没有策略,覆盖率就只是个热闹。真正有用的是知道怎么拿这些数据指导测试。
Step 1: Triage by risk
第一步:按风险分层处理。
| Risk pattern 风险组合 | Action 处理建议 |
|---|---|
| High coverage, high risk 高覆盖,高风险 | ✅ Good — maintain it 状态不错,继续维持。 |
| High coverage, low risk 高覆盖,低风险 | 🔄 Possibly over-tested — skip if slow 可能已经测过头了,如果测试很慢,可以暂时停一停。 |
| Low coverage, high risk 低覆盖,高风险 | 🔴 Write tests NOW — this is where bugs hide 优先补测试,bug 最喜欢藏在这里。 |
| Low coverage, low risk 低覆盖,低风险 | 🟡 Track but don’t panic 持续记录,先别慌。 |
Step 2: Focus on branch coverage, not line coverage
第二步:别只盯着行覆盖率,更要盯分支覆盖率。
#![allow(unused)]
fn main() {
// 100% line coverage, 50% branch coverage — still risky!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
if temp_c > 105 { // ← tested with temp=110 → Critical
ThermalState::Critical
} else if temp_c > 85 { // ← tested with temp=90 → Warning
ThermalState::Warning
} else if temp_c < -10 { // ← NEVER TESTED → sensor error case missed
ThermalState::SensorError
} else {
ThermalState::Normal // ← tested with temp=25 → Normal
}
}
}
This example is a classic trap: line coverage may reach 100%, but the temp_c < -10 branch is never tested, so the sensor-error path quietly slips through.
这就是一个很典型的坑:行覆盖率看着像 100%,但 temp_c < -10 这个分支根本没人测,传感器异常场景就这样漏掉了。只盯着行覆盖率,很容易被表面数字骗过去;分支覆盖率更容易把这种问题拽出来。
Step 3: Exclude noise
第三步:把噪音剔出去。
# Exclude test code from coverage (it's always "covered")
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'
# Exclude generated code
cargo llvm-cov --workspace --ignore-filename-regex 'target/'
In code, mark untestable sections:
在代码层面,也可以把那些天然难测的区域单独标记出来:
#![allow(unused)]
fn main() {
// Coverage tools recognize this pattern
#[cfg(not(tarpaulin_include))] // tarpaulin
fn unreachable_hardware_path() {
// This path requires actual GPU hardware to trigger
}
// For llvm-cov, use a more targeted approach:
// Simply accept that some paths need integration/hardware tests,
// not unit tests. Track them in a coverage exceptions list.
}
Complementary Testing Tools
互补的测试工具
proptest — Property-Based Testing finds edge cases that hand-written tests miss:proptest:属性测试,专门擅长挖出手写样例测试漏掉的边界情况。
[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn parse_never_panics(input in "\\PC*") {
// proptest generates thousands of random strings
// If parse_gpu_csv panics on any input, the test fails
// and proptest minimizes the failing case for you.
let _ = parse_gpu_csv(&input);
}
#[test]
fn temperature_roundtrip(raw in 0u16..4096) {
let temp = Temperature::from_raw(raw);
let md = temp.millidegrees_c();
// Property: millidegrees should always be derivable from raw
assert_eq!(md, (raw as i32) * 625 / 10);
}
}
}
insta — Snapshot Testing for large structured outputs (JSON, text reports):insta:快照测试,很适合校验大段结构化输出,例如 JSON 或文本报告。
[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
let report = generate_der_report(&test_results);
// First run: creates a snapshot file. Subsequent runs: compares against it.
// Run `cargo insta review` to accept changes interactively.
insta::assert_json_snapshot!(report);
}
}
When to add proptest/insta: If your unit tests are all “happy path” examples, proptest will find the edge cases you missed. If you’re testing large output formats (JSON reports, DER records), insta snapshots are faster to write and maintain than hand-written assertions.
什么时候该加proptest和insta:如果单元测试几乎全是“顺利路径”的例子,那就该让proptest出手,去抠那些容易被忽略的边界条件。如果测的是大型输出格式,例如 JSON 报告、DER 记录,insta往往比手写一堆断言省力得多。
Application: 1,000+ Tests Coverage Map
应用场景:1000+ 测试的覆盖率地图
The project has 1,000+ tests but no coverage tracking. Adding it reveals the testing investment distribution. Uncovered paths are prime candidates for Miri and sanitizer verification:
当前工程测试数量已经过千,但还没有覆盖率跟踪。把覆盖率补上之后,测试投入究竟落在哪些模块、哪些路径,一下就能看清。那些仍旧没覆盖到的路径,就是继续交给 Miri 与 Sanitizer 深挖的重点对象。
Recommended coverage configuration:
建议的覆盖率配置:
# Quick workspace coverage (proposed CI command)
cargo llvm-cov --workspace \
--ignore-filename-regex 'tests?\.rs$' \
--fail-under-lines 75 \
--html
# Per-crate coverage for targeted improvement
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
echo "=== $crate ==="
cargo llvm-cov --package "$crate" --json 2>/dev/null | \
jq -r '.data[0].totals | "Lines: \(.lines.percent | round)% Branches: \(.branches.percent | round)%"'
done
Expected high-coverage crates (based on test density):
预期覆盖率较高的 crate,从测试密度看大概会是这些:
topology_lib— 922-line golden-file test suitetopology_lib:有一套长达 922 行的 golden file 测试。event_log— registry withcreate_test_record()helpersevent_log:带有create_test_record()这类测试辅助构造器。cable_diag—make_test_event()/make_test_context()patternscable_diag:已经形成了make_test_event()、make_test_context()这种测试模式。
Expected coverage gaps (based on code inspection):
预期覆盖率缺口,根据代码阅读大概率会落在这些位置:
- Error handling arms in IPMI communication paths
IPMI 通信路径里的错误处理分支。 - GPU hardware-specific branches (require actual GPU)
依赖真实 GPU 硬件才能触发的分支。 dmesgparsing edge cases (platform-dependent output)dmesg解析里的边界情况,尤其是平台相关输出差异。
The 80/20 rule of coverage: Getting from 0% to 80% coverage is straightforward. Getting from 80% to 95% requires increasingly contrived test scenarios. Getting from 95% to 100% requires
#[cfg(not(...))]exclusions and is rarely worth the effort. Target 80% line coverage and 70% branch coverage as a practical floor.
覆盖率的 80/20 规律 很真实:从 0% 做到 80% 通常比较顺手;从 80% 抬到 95% 就开始要拼各种拧巴场景;再从 95% 折腾到 100%,常常要靠#[cfg(not(...))]这种排除技巧硬抠,投入产出比就很难看了。一个更务实的目标,是把 行覆盖率做到 80%,分支覆盖率做到 70%。
Troubleshooting Coverage
覆盖率排障
| Symptom 现象 | Cause 原因 | Fix 处理方式 |
|---|---|---|
llvm-cov shows 0% for all filesllvm-cov 所有文件都显示 0% | Instrumentation not applied 没有真正插桩 | Ensure you run cargo llvm-cov, not cargo test + llvm-cov separately确认执行的是 cargo llvm-cov,别拆成 cargo test 加单独的 llvm-cov。 |
Coverage counts unreachable!() as uncoveredunreachable!() 被算成未覆盖 | Those branches exist in compiled code 这些分支在编译产物里确实存在 | Use #[cfg(not(tarpaulin_include))] or add to exclusion regex用 #[cfg(not(tarpaulin_include))] 或者在排除规则里单独处理。 |
| Test binary crashes under coverage 测试二进制在覆盖率模式下崩溃 | Instrumentation + sanitizer conflict 插桩和 sanitizer 发生冲突 | Don’t combine cargo llvm-cov with -Zsanitizer=address; run them separately别把 cargo llvm-cov 和 -Zsanitizer=address 混在同一次运行里。 |
Coverage differs between llvm-cov and tarpaulinllvm-cov 和 tarpaulin 结果差异很大 | Different instrumentation techniques 插桩机制不同 | Use llvm-cov as source of truth (compiler-native); file issues for large discrepancies优先以编译器原生的 llvm-cov 为准,差异太大时再单独排查。 |
error: profraw file is malformed出现 error: profraw file is malformed | Test binary crashed mid-execution 测试进程中途异常退出 | Fix the test failure first; profraw files are corrupt when the process exits abnormally 先修测试崩溃,因为进程异常退出时 .profraw 很容易损坏。 |
| Branch coverage seems impossibly low 分支覆盖率低得离谱 | Optimizer creates branches for match arms, unwrap, etc. 优化器会为 match 分支、unwrap 等生成额外分支 | Focus on line coverage for practical thresholds; branch coverage is inherently lower 门槛设置上优先看行覆盖率,分支覆盖率天然就会更低。 |
Try It Yourself
动手试一试
-
Measure coverage on your project: Run
cargo llvm-cov --workspace --htmland open the report. Find the three files with the lowest coverage. Are they untested, or inherently hard to test (hardware-dependent code)?
先量一遍覆盖率:执行cargo llvm-cov --workspace --html,打开报告,找出覆盖率最低的三个文件。它们究竟是完全没测,还是天然难测,例如依赖硬件。 -
Set a coverage gate: Add
cargo llvm-cov --workspace --fail-under-lines 60to your CI. Intentionally comment out a test and verify CI fails. Then raise the threshold to your project’s actual coverage level minus 2%.
再加一个覆盖率门槛:把cargo llvm-cov --workspace --fail-under-lines 60放进 CI,故意注释掉一个测试,确认 CI 会失败。随后把阈值提高到“当前实际覆盖率减 2%”附近。 -
Branch vs. line coverage: Write a function with a 3-arm
matchand test only 2 arms. Compare line coverage (may show 66%) vs. branch coverage (may show 50%). Which metric is more useful for your project?
最后对比分支覆盖率和行覆盖率:写一个有 3 个分支的match,只测试其中 2 个分支,比较行覆盖率和分支覆盖率。看一看对当前项目来说,哪个指标更有参考价值。
Coverage Tool Selection
覆盖率工具选择
flowchart TD
START["Need code coverage?<br/>需要代码覆盖率吗?"] --> ACCURACY{"Priority?<br/>优先级是什么?"}
ACCURACY -->|"Most accurate<br/>最准确"| LLVM["cargo-llvm-cov<br/>Source-based, compiler-native<br/>源码级,编译器原生"]
ACCURACY -->|"Quick check<br/>快速检查"| TARP["cargo-tarpaulin<br/>Linux only, fast<br/>仅 Linux,部署快"]
ACCURACY -->|"Multi-run aggregate<br/>多轮结果聚合"| GRCOV["grcov<br/>Mozilla, combines profiles<br/>Mozilla 出品,可合并多轮 profiling"]
LLVM --> CI_GATE["CI coverage gate<br/>--fail-under-lines 80<br/>CI 覆盖率门槛"]
TARP --> CI_GATE
CI_GATE --> UPLOAD{"Upload to?<br/>上传到哪里?"}
UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
style LLVM fill:#91e5a3,color:#000
style TARP fill:#e3f2fd,color:#000
style GRCOV fill:#e3f2fd,color:#000
style CI_GATE fill:#ffd43b,color:#000
🏋️ Exercises
🏋️ 练习
🟢 Exercise 1: First Coverage Report
🟢 练习 1:第一份覆盖率报告
Install cargo-llvm-cov, run it on any Rust project, and open the HTML report. Find the three files with the lowest line coverage.
安装 cargo-llvm-cov,对任意 Rust 项目跑一遍,再打开 HTML 报告,找出行覆盖率最低的三个文件。
Solution 参考答案
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# The report sorts files by coverage — lowest at the bottom
# Look for files under 50% — those are your blind spots
🟡 Exercise 2: CI Coverage Gate
🟡 练习 2:CI 覆盖率门槛
Add a coverage gate to a GitHub Actions workflow that fails if line coverage drops below 60%. Verify it works by commenting out a test.
在 GitHub Actions 工作流里加入覆盖率门槛,只要行覆盖率跌破 60% 就让任务失败。可以通过临时注释掉一个测试来验证这件事。
Solution 参考答案
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- run: cargo install cargo-llvm-cov
- run: cargo llvm-cov --workspace --fail-under-lines 60
Comment out a test, push, and watch the workflow fail.
注释掉一个测试,推送一次,就能看到工作流如预期失败。
Key Takeaways
本章要点
cargo-llvm-covis the most accurate coverage tool for Rust — it uses the compiler’s own instrumentationcargo-llvm-cov是当前最准确的 Rust 覆盖率工具,因为它使用的是编译器原生插桩。- Coverage doesn’t prove correctness, but zero coverage proves zero testing — use it to find blind spots
覆盖率证明不了正确性,但 零覆盖率就等于零测试,这已经足够说明问题了。 - Set a coverage gate in CI (e.g.,
--fail-under-lines 80) to prevent regressions
把覆盖率门槛放进 CI,可以防止测试质量一轮轮往下掉。 - Don’t chase 100% coverage — focus on high-risk code paths (error handling, unsafe, parsing)
别死抠 100%,重点盯高风险路径,例如错误处理、unsafe、解析逻辑。 - Never combine coverage instrumentation with sanitizers in the same run
覆盖率插桩和 sanitizer 不要放在同一轮执行里,一起上很容易互相掐架。