如何测量测试覆盖率?

解读

在国内 Rust 岗位面试中,面试官问“如何测量测试覆盖率”并不是想听你背命令,而是考察三点:

  1. 是否真正在 CI/本地跑过覆盖率,而不是“听说过”;
  2. 能否把覆盖率工具链与 Cargo 生态打通,给出可落地的工程方案;
  3. 是否知道 Rust 编译器基于 LLVM 的覆盖率映射机制,并能解释 -C instrument-coveragegrcov/llvm-cov 的关系。
    回答时要体现“编译参数 → 数据收集 → 报告生成 → CI 集成 → 质量门禁”完整闭环,并指出国内常见坑:Windows 路径、源码映射、profraw 合并、CI 缓存过大等。

知识点

  • LLVM 覆盖率映射(Coverage Mapping):Rust 1.60+ 官方方案,编译期插入探针,开销 < 5%。
  • 编译开关-C instrument-coverage-C link-dead-code-C codegen-units=1 保证行号精确。
  • 数据格式.profrawllvm-profdata merge.profdatagrcov 可直接解析 .profraw 生成 lcov/json/cobertura。
  • 工具链
    grcov(Mozilla 开源,跨平台,支持 Cobertura,国内 GitLab/Azure DevOps 友好);
    cargo-llvm-cov(社区维护,一行命令出报告,支持 doctest、doc 覆盖);
    tarpaulin(仅 Linux x86_64,基于 ptrace,对 async/await 偶有漏报,国内镜像源拉取慢)。
  • 报告指标:行覆盖、区域覆盖(Region)、分支覆盖(MC/DC 国内轨交、车载代码强制要求)。
  • CI 集成:GitHub Actions / Gitee Go 里缓存 ~/.cargo/bintarget/debug/deps/*.profraw,加阈值门禁 grcov --threshold 80
  • 常见坑
    – 路径映射:Docker 内编译需加 --remap-path-prefix
    – 并行测试:多进程写同一 .profrawLLVM_PROFILE_FILE="foo-%p-%m.profraw"
    – 二进制改名:cargo nextest 需 --profile cov 保证测试二进制带探针。

答案

工程上我采用官方 LLVM 方案,三步落地:

  1. 编译:
    RUSTFLAGS="-C instrument-coverage -C link-dead-code -C codegen-units=1" cargo build --profile test
    这样所有测试二进制都带探针,且死代码也被链接,避免漏统计。
  2. 运行:
    LLVM_PROFILE_FILE="target/cov/raw-%p-%m.profraw" cargo test --profile test
    每个测试进程写独立 profraw,防止并发写冲突。
  3. 收集与报告:
    使用 grcov(国内源已做 crates.io 镜像,下载 30 秒以内):
    grcov target/cov --binary-path ./target/debug/deps -t lcov --branch --ignore-not-existing -o target/cov/lcov.info
    再把 lcov.info 推送到 GitLab CI,配合 coverage: '/lines\.*: (\d+\.\d+)%/' 即可在 Merge Request 里看到行覆盖 87.3%,分支覆盖 81.2%
    若客户要求 HTML 自托管,执行
    genhtml -o target/cov/html target/cov/lcov.info
    即可在 Nginx 静态目录浏览,红色行一目了然。
    对于doctest 覆盖,用 cargo llvm-cov --doctests --open,可把文档测试纳入统计,这是 tarpaulin 做不到的。
    最后在 ci.yml 里加门禁:
    - grcov --threshold 80 --fail-under-lines 80
    低于 80% 直接拒绝合并,保证交付质量。

拓展思考

  1. 国内车载软件要求MC/DC 100%,而 LLVM 区域覆盖只能近似分支覆盖,如何进一步插桩布尔短路表达式?可写 proc-macro 在 &&/|| 处插入 #[coverage(off/on)] 手动标记,再二次解析 .profdata 生成 MC/DC 报告。
  2. 对于no_std 嵌入式环境,没有文件系统,如何把探针数据吐出来?自定义 __llvm_profile_write_buffer 重定向到串口,上位机收集后拼接成 .profraw,再回主机用 llvm-profdata 合并,实现裸机覆盖率
  3. 超大单仓库(百万行)CI 耗时 30 min,主要卡在 grcov 解析。可改用增量覆盖:只统计 Merge Request diff 涉及的文件,用 git diff --name-only 过滤,再把增量报告上传到 CodeCov 的 carryforward flag,既满足门禁又节省 70% 时间。