基于插桩的代码覆盖率
简介
本文档描述了如何通过 -C instrument-coverage
编译器标志启用和使用基于 LLVM 插桩的覆盖率。
工作原理
当启用 -C instrument-coverage
时,Rust 编译器会通过以下方式增强基于 Rust 的库和二进制文件:
- 在已编译代码中的函数和分支处,自动注入对 LLVM 内置函数(
llvm.instrprof.increment
)的调用,以在执行代码的条件部分时递增计数器。 - 在每个库和二进制文件的数据部分中嵌入额外的信息(如果使用 LLVM 12 编译,则使用 LLVM 代码覆盖率映射格式 *Version 5*,如果使用 LLVM 13 或更高版本编译,则使用 *Version 6*),以定义被计数的代码区域(源代码中的起始和结束位置)。
当运行覆盖率插桩的程序时,计数器值会在程序终止时写入 profraw
文件。LLVM 捆绑了一些工具,可以读取计数器结果,将这些结果与覆盖率映射(嵌入在程序二进制文件中)相结合,并生成多种格式的覆盖率报告。
注意:
-C instrument-coverage
还会自动启用-C symbol-mangling-version=v0
(跟踪问题 #60705)。强烈建议使用v0
符号修饰器。可以通过显式添加-Z unstable-options -C symbol-mangling-version=legacy
来覆盖v0
解修饰器。
在 Rust 编译器中启用覆盖率分析
Rust 的基于源代码的代码覆盖率需要 Rust “分析器运行时”。如果没有它,使用 -C instrument-coverage
进行编译会生成一个错误,表明分析器运行时缺失。
默认情况下,Rust nightly
发行通道包含分析器运行时。
重要提示:如果您从源代码发行版构建 Rust 编译器,则默认的
config.example.toml
中未启用分析器运行时。编辑您的config.toml
文件,并确保将profiler
功能设置为true
(在[build]
部分下,或在单个[target.<triple>]
的设置下)。# Build the profiler runtime (required when compiling with options that depend # on this runtime, such as `-C profile-generate` or `-C instrument-coverage`). profiler = true
构建解修饰器
LLVM 覆盖率报告工具生成的结果可能包含函数名称和其他符号引用,并且原始覆盖率结果使用编译器符号名称的“修饰”版本报告符号,这可能难以解释。为了解决这个问题,LLVM 覆盖率工具还支持用户指定的符号名称解修饰器。
Rust 解修饰器的一个选项是 rustfilt
,可以使用以下命令安装:
cargo install rustfilt
启用覆盖率编译
设置 -C instrument-coverage
编译器标志以启用 LLVM 基于源代码的代码覆盖率分析。
默认选项会为所有函数生成覆盖率,包括未使用的(从未调用的)函数和泛型。编译器标志支持一个可选值来定制此行为。(请参见下文中的 -C instrument-coverage=<options>
。)
使用 cargo
,您可以同时插桩您的程序二进制文件和依赖项。
例如(如果您的项目的 Cargo.toml 默认构建一个二进制文件)
$ cd your-project
$ cargo clean
$ RUSTFLAGS="-C instrument-coverage" cargo build
如果 cargo
未配置为使用您启用 profiler
的 rustc
版本,请通过 RUSTC
环境变量显式设置路径。这是另一个示例,使用 rustc
的 stage1
构建来编译 example
二进制文件(来自 json5format
crate)
$ RUSTC=$HOME/rust/build/x86_64-unknown-linux-gnu/stage1/bin/rustc \
RUSTFLAGS="-C instrument-coverage" \
cargo build --example formatjson5
注意:一些编译器选项与
-C instrument-coverage
结合使用时,可能会生成与 LLVM 覆盖率映射不兼容的 LLVM IR 和/或链接的二进制文件。例如,覆盖率需要在 LLVM IR 中引用实际函数。如果任何被覆盖的函数被优化掉,覆盖率工具可能无法处理覆盖率结果。如果需要传递其他选项(启用覆盖率),请尽早测试它们,以确认您将获得期望的覆盖率结果。
运行插桩的二进制文件以生成原始覆盖率分析数据
在前面的示例中,cargo
生成了覆盖率插桩的二进制文件 formatjson5
$ echo "{some: 'thing'}" | target/debug/examples/formatjson5 -
{
some: "thing",
}
运行此程序后,当前工作目录中应该有一个类似于 default_11699812450447639123_0_20944
的新文件。每次运行程序时都会生成一个新的唯一文件名,以避免覆盖以前的数据。
$ echo "{some: 'thing'}" | target/debug/examples/formatjson5 -
...
$ ls default_*.profraw
default_11699812450447639123_0_20944.profraw
您还可以使用环境变量 LLVM_PROFILE_FILE
为生成的 .profraw
文件设置特定的文件名或路径
$ echo "{some: 'thing'}" \
| LLVM_PROFILE_FILE="formatjson5.profraw" target/debug/examples/formatjson5 -
...
$ ls formatjson5.profraw
formatjson5.profraw
如果 LLVM_PROFILE_FILE
包含一个不存在的目录的路径,则会创建缺失的目录结构。此外,以下特殊模式字符串会被重写
%p
- 进程 ID。%h
- 运行该程序的机器的主机名。%t
- TMPDIR 环境变量的值。%Nm
- 插桩二进制文件的签名:运行时创建一个 N 个原始分析文件的池,用于在线分析文件合并。运行时负责从池中选择一个原始分析文件,锁定它,并在程序退出前更新它。N
必须介于1
和9
之间,如果省略(仅使用%m
),则默认为1
。%c
- 不会向文件名添加任何内容,但会启用一种模式(在某些平台上,包括 Darwin),在该模式下,分析器计数器更新会持续同步到文件。这意味着,如果插桩的程序崩溃或被信号杀死,仍然可以恢复完整的覆盖率信息。
在上面的第一个示例中,生成的文件名中的值 11699812450447639123_0
是插桩二进制文件的签名,它替换了 %m
模式,值 20944
是正在执行的二进制文件的进程 ID。
安装 LLVM 覆盖率工具
LLVM 提供了两个工具——llvm-profdata
和 llvm-cov
——用于处理覆盖率数据并生成报告。有几种方法可以查找和/或安装这些工具,但请注意,Rust 编译器生成的覆盖率映射数据需要 LLVM 12 或更高版本,并且处理原始数据可能需要与编译器使用的 LLVM 版本完全相同的版本。(llvm-cov --version
通常会显示该工具的 LLVM 版本号,而 rustc --verbose --version
会显示 Rust 编译器使用的 LLVM 版本。)
- 您可以通过
rustup
组件llvm-tools-preview
安装这些工具的兼容版本。此组件是推荐的路径,尽管当前可用的特定工具及其接口不受 Rust 通常的稳定性保证约束。在这种情况下,您可能还会发现cargo-binutils
作为这些工具的包装器很有用。 - 您可以从您的操作系统发行版或您的 LLVM 发行版中安装兼容版本的 LLVM 工具。
- 如果您是从源代码构建 Rust 编译器,您可以选择使用从源代码构建的捆绑 LLVM 工具。这些工具二进制文件通常可以在您的构建平台目录中找到,例如:
rust/build/x86_64-unknown-linux-gnu/llvm/bin/llvm-*
。
本文档中的示例展示了如何直接使用 llvm 工具。
创建覆盖率报告
原始分析文件必须先被索引,然后才能用于生成覆盖率报告。这是使用 llvm-profdata merge
完成的,它可以合并多个原始分析文件并同时索引它们
$ llvm-profdata merge -sparse formatjson5.profraw -o formatjson5.profdata
最后,.profdata
文件与覆盖率映射(来自程序二进制文件)结合使用,以使用 llvm-cov report
生成覆盖率摘要;并使用 llvm-cov show
来查看覆盖在原始源代码上的行和区域(字符范围)的详细覆盖率。
这些命令有几个显示和过滤选项。例如
$ llvm-cov show -Xdemangler=rustfilt target/debug/examples/formatjson5 \
-instr-profile=formatjson5.profdata \
-show-line-counts-or-regions \
-show-instantiations \
-name=add_quoted_string

此示例中一些更值得注意的选项包括
--Xdemangler=rustfilt
- 用于解修饰 Rust 符号的命令名称或路径(在本例中为rustfilt
)target/debug/examples/formatjson5
- 插桩的二进制文件(从中提取覆盖率映射)--instr-profile=<path-to-file>.profdata
- 由llvm-profdata merge
创建的.profdata
文件的位置(来自插桩二进制文件生成的.profraw
文件)--name=<exact-function-name>
- 显示特定函数的覆盖率(或者,可以考虑使用其他筛选选项,例如--name-regex=<pattern>
)
注意:还可以通过使用 [
coverage(off)
属性] 注释函数来禁用单个函数的覆盖率(这需要功能标志#![feature(coverage)]
)。
解释报告
覆盖率摘要中跟踪了四个统计数据
- 函数覆盖率是至少执行一次的函数百分比。如果执行了函数的任何实例化,则认为该函数已执行。
- 实例化覆盖率是至少执行一次的函数实例化百分比。泛型函数和从宏生成的函数是可能具有多个实例化的两种函数。
- 行覆盖率是至少执行一次的代码行百分比。仅考虑函数体内的可执行行作为代码行。
- 区域覆盖率是至少执行一次的代码区域百分比。一个代码区域可能跨越多行:例如,在一个没有控制流的大型函数体中。在其他情况下,一行可能包含多个代码区域:
return x || (y && z)
对于x
(如果x
为true
,则可能解析表达式)、|| (y && z)
(仅当x
为false
时执行)和return
(在任何情况下都执行)具有可计数的代码区域。
在这四个统计数据中,函数覆盖率通常是最粗粒度的,而区域覆盖率是最细粒度的。每个统计数据的项目总计在摘要中列出。
测试覆盖率
覆盖率分析的典型用例是测试覆盖率。Rust 基于源代码的覆盖率工具既可以衡量测试的代码覆盖率百分比,也可以精确定位未测试的函数和分支。
以下示例(使用 json5format
crate,仅用于演示目的)展示了如何为 crate 中的所有测试生成和分析覆盖率结果。
由于 cargo test
既构建又运行测试,我们设置了额外的 RUSTFLAGS
,以添加 -C instrument-coverage
标志。
$ RUSTFLAGS="-C instrument-coverage" \
cargo test --tests
注意:
LLVM_PROFILE_FILE
的默认值是default_%m_%p.profraw
。1.65 之前的版本默认值为default.profraw
,因此如果使用较早的版本,建议显式设置LLVM_PROFILE_FILE="default_%m_%p.profraw"
,以避免多个测试覆盖.profraw
文件。
记下测试输出中“Running
”一词后显示的测试二进制文件路径
...
Compiling json5format v0.1.3 ($HOME/json5format)
Finished test [unoptimized + debuginfo] target(s) in 14.60s
Running target/debug/deps/json5format-fececd4653271682
running 25 tests
...
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/lib-30768f9c53506dc5
running 31 tests
...
test result: ok. 31 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
现在您应该有一个或多个 .profraw
文件,每个测试二进制文件一个。运行 profdata
工具来合并它们
$ llvm-profdata merge -sparse default_*.profraw -o json5format.profdata
然后,使用 profdata
文件和所有测试二进制文件运行 cov
工具
$ llvm-cov report \
--use-color --ignore-filename-regex='/.cargo/registry' \
--instr-profile=json5format.profdata \
--object target/debug/deps/lib-30768f9c53506dc5 \
--object target/debug/deps/json5format-fececd4653271682
$ llvm-cov show \
--use-color --ignore-filename-regex='/.cargo/registry' \
--instr-profile=json5format.profdata \
--object target/debug/deps/lib-30768f9c53506dc5 \
--object target/debug/deps/json5format-fececd4653271682 \
--show-instantiations --show-line-counts-or-regions \
--Xdemangler=rustfilt | less -R
注意:如果通过
LLVM_PROFILE_FILE
环境变量覆盖默认的profraw
文件名,强烈建议使用%m
和%p
特殊模式字符串,以便在执行多个测试二进制文件的情况下生成唯一的文件名。
注意:命令行选项
--ignore-filename-regex=/.cargo/registry
,它将依赖项的源代码从覆盖率结果中排除。
自动列出二进制文件的技巧
对于 bash
用户,一种建议的方法是使用如下命令自动完成带有二进制文件列表的 cov
命令:
$ llvm-cov report \
$( \
for file in \
$( \
RUSTFLAGS="-C instrument-coverage" \
cargo test --tests --no-run --message-format=json \
| jq -r "select(.profile.test == true) | .filenames[]" \
| grep -v dSYM - \
); \
do \
printf "%s %s " -object $file; \
done \
) \
--instr-profile=json5format.profdata --summary-only # and/or other options
将 --no-run --message-format=json
添加到用于运行测试的相同 cargo test
命令(包括相同的环境变量和标志)会生成 JSON 格式的输出,jq
可以轻松查询。
printf
命令会获取此列表,并为每个列出的测试二进制文件生成 --object <binary>
参数。
包括文档测试
前面的示例使用 --tests
运行 cargo test
,这会排除文档测试。1
要在覆盖率结果中包含文档测试,请删除 --tests
标志,并在 RUSTDOCFLAGS
环境变量中应用 -C instrument-coverage
标志和一些特定于文档测试的选项。(llvm-profdata
命令不会更改。)
$ RUSTFLAGS="-C instrument-coverage" \
RUSTDOCFLAGS="-C instrument-coverage -Z unstable-options --persist-doctests target/debug/doctestbins" \
cargo test
$ llvm-profdata merge -sparse default_*.profraw -o json5format.profdata
需要 -Z unstable-options --persist-doctests
标志,以保存 llvm-cov
的测试二进制文件(及其覆盖率映射)。
$ llvm-cov report \
$( \
for file in \
$( \
RUSTFLAGS="-C instrument-coverage" \
RUSTDOCFLAGS="-C instrument-coverage -Z unstable-options --persist-doctests target/debug/doctestbins" \
cargo test --no-run --message-format=json \
| jq -r "select(.profile.test == true) | .filenames[]" \
| grep -v dSYM - \
) \
target/debug/doctestbins/*/rust_out; \
do \
[[ -x $file ]] && printf "%s %s " -object $file; \
done \
) \
--instr-profile=json5format.profdata --summary-only # and/or other options
注意:此
llvm-cov
调用与不包含文档测试的版本相比,差异包括
cargo test ... --no-run
命令使用用于构建测试的相同的环境变量和标志进行更新,包括 文档测试。- 文件 glob 模式
target/debug/doctestbins/*/rust_out
添加为文档测试生成的rust_out
二进制文件(但是请注意,某些rust_out
文件可能不是可执行的二进制文件)。 [[ -x $file ]] &&
过滤传递给printf
的文件,仅包括可执行的二进制文件。
目前正在努力解决一个已知问题 (#79417),该问题导致文档测试覆盖率在 llvm-cov show
结果中生成不正确的源代码行号。
-C instrument-coverage=<options>
-C instrument-coverage=no
(或n
/off
/false
):不启用覆盖率检测。不会为覆盖率检测任何函数。- 这与完全不使用
-C instrument-coverage
标志相同。
- 这与完全不使用
-C instrument-coverage=yes
(或y
/on
/true
):启用具有默认行为的覆盖率检测。当前,这会检测所有函数,包括未使用的函数和未使用的泛型。- 这与不带值的
-C instrument-coverage
相同。
- 这与不带值的
其他值
-C instrument-coverage=all
:当前是yes
的别名,但如果添加更细粒度的覆盖率选项,将来可能会表现不同。目前不建议使用此值。
-Z coverage-options=<options>
其他参考资料
Rust 基于源代码的代码覆盖率的实现和工作流程基于用于实现 Clang 中基于源代码的代码覆盖率的相同库和工具。(本文档部分基于 Clang 指南。)