ZeroErr
载入中...
搜索中...
未找到
逐测试 Clang 覆盖率

可选的插桩构建用于统计每个顶层 TEST_CASE 执行了哪些业务代码。普通构建不依赖 LLVM 覆盖率运行时。

推荐:像 fuzz 一样一条命令运行

已有 TEST_CASE 不用改写。首次使用 Clang 和 -DZEROERR_ENABLE_COVERAGE=ON 配置构建后:

./my_tests --coverage # 也可以用 -c
./my_tests -c --testcase='parser.*' # 保留已有筛选方式

这一条命令会运行测试、采集 profile、统计并打印覆盖比例、最慢的十个测试和覆盖相同的候选组。 每次在工作目录下创建独立的 zeroerr-coverage/run-<id>,保存 report.json、 report.summary.txt 和原始 profile,无需手动调用 LLVM 或 Python。 测试失败仍生成报告并返回失败状态;采集或分析错误返回 2。--list-test-cases 只列出测试。

CMake 在配置阶段查找 Python 3.9+ 和配套 LLVM 工具。默认统计接入项目源码根目录, 排除已执行测试所在源码文件、zeroerr 框架源码和头文件、third_party/_deps。 业务代码与测试在同一文件时,需明确选择该文件。显式 include 会取代自动排除规则; 用户指定的 exclude 始终生效:

./my_tests -c --coverage-include='/src/' --coverage-exclude='/generated/'
./my_tests -c --coverage-output=results/run-001 # 必须是新目录

项目可以添加方便复用的 CMake 目标:

add_subdirectory(path/to/zeroerr)
add_library(my_library STATIC src/library.cpp)
add_executable(my_tests tests/library_test.cpp)
target_link_libraries(my_tests PRIVATE my_library zeroerr)
if(ZEROERR_ENABLE_COVERAGE)
zeroerr_instrument_coverage(my_library)
zeroerr_add_coverage(my_tests) # 生成 my_tests-coverage
endif()

之后运行 cmake --build build --target my_tests-coverage,即可完成构建、运行与统计。 该函数还支持 INCLUDE "正则"、EXCLUDE "正则" ...、ARGS "--testcase=parser.*" ...。 zeroerr 自身的目标为 unittest-coverage,显式选择 zeroerr 自身作为被测业务代码。

仓库内提供了可以直接运行的独立示例:

cmake -S examples/coverage -B build-example -G Ninja \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake --build build-example --target coverage_example-coverage

示例为独立业务库插桩,覆盖 sign 函数的所有分支结果,并识别两个正数输入测试的相同覆盖。

手动编译或单头文件模式下,把 scripts/coverage-report.py 放到可执行文件旁并命名为 zeroerr-coverage.py,或通过环境变量 ZEROERR_COVERAGE_SCRIPT 指定位置。 Python 3 和配套的 llvm-cov/llvm-profdata 需要在 PATH 中;Python 名称不同时设置 ZEROERR_COVERAGE_PYTHON。此模式默认源码根目录为当前目录。 也可直接运行启动器: python coverage-report.py --run ./my_tests --source-root . -- --testcase='parser.*'。 启动器支持用 --llvm-cov/--llvm-profdata 指定带版本号的工具。 该辅助脚本是外部依赖,移动二进制时需一起部署;原 checkout 可用时优先使用 CMake 配置的脚本位置。

高级用法:分开采集和分析

cmake -S . -B build-coverage -G Ninja \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DBUILD_TEST=ON -DZEROERR_ENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build-coverage
./build-coverage/test/unittest --coverage-output=build-coverage/run-001
python scripts/coverage-report.py build-coverage/run-001 \
--binary build-coverage/test/unittest \
--include '/zeroerr/(src|include)/' \
--output build-coverage/report.json

使用与编译器配套的 llvm-profdata 和 llvm-cov,可通过 --llvm-profdata /path/to/llvm-profdata-21、--llvm-cov /path/to/llvm-cov-21 指定路径。统计脚本需要 Python 3.9+,支持带分支信息的 LLVM JSON 导出格式 2 和 3。 当前已在 Linux 的 Clang/LLVM 21 上验证,其他平台尚需验证。

应用项目链接 CMake 的 zeroerr target 后,测试目标会继承覆盖率编译、链接选项。 对于单独编译的业务库,还需在引入 zeroerr 子目录后调用 zeroerr_instrument_coverage(my_library)。已编译且未插桩的库无法采集。 覆盖率构建使用原子计数更新,避免工作线程丢失计数;所有工作线程必须在测试返回前结束。 当前后端面向单个可执行文件及静态链接代码,不采集具有独立插桩运行时的动态库。

单头文件方式需要在实现翻译单元定义 ZEROERR_ENABLE_COVERAGE,为被测代码添加 -fprofile-instr-generate -fcoverage-mapping -fprofile-update=atomic, 链接时添加 -fprofile-instr-generate。覆盖率模式需要 C++17 的 std::filesystem。

采集语义

每次 --coverage-output 必须指定**新目录**,已存在的目录会被拒绝且保持原样。 已有测试过滤选项继续生效。列出测试不会创建目录,跳过的测试不产生 profile。 失败测试和捕获的异常也会保存 profile 及结果;统计是独立步骤,可以在测试失败后执行。

执行器在每个顶层测试之前清零 LLVM 计数,测试结束后通过 LLVM profile buffer API 保存快照。嵌套 SUB_CASE、fixture 构造和析构归入父测试;测试前的全局初始化不计入。 边界内执行的框架代码也可能出现在 profile 中,应通过业务源码范围过滤。 目前不声称提供独立子用例归属。不要在同一进程内并发执行多个测试,也不要让后台工作跨越测试边界。 fork 子进程及外部子进程的覆盖率不在采集范围内。

目录包含 manifest.jsonl(名称、源码位置、耗时、结果、profile 文件名)和每个测试的 .profraw 文件。完成标记用于识别中断或崩溃的不完整采集。 采集 I/O 失败返回退出码 2;未启用覆盖率的构建收到采集选项也返回 2。 LLVM 可能在退出时另外生成普通整体 profile,本统计器不会使用它。

统计与解释

离线分析时 --include 为必填的源码路径正则,路径统一使用正斜杠。可重复传入 --exclude REGEX。 比较业务覆盖时,应排除测试实现、断言辅助函数、第三方代码。 没有匹配的插桩代码、采集未完成、LLVM 诊断或缺少分支数据都会报错。

JSON 报告通过共享源码特征字典和每测试特征 ID 集合,分别保存三个指标:

  • **函数**:保留修饰后的函数名,区分不同模板实例。
  • **源码区域**:LLVM 代码区域的起止行列;排除间隙、跳过区域和展开标记。 展开后的代码区域保留局部文件 ID 以区分归属。
  • **分支结果**:LLVM 导出的分支区域的真、假结果,包括实际求值的短路操作数。 这不等于 MC/DC 或路径覆盖。

每项指标提供覆盖数、总数,以及相对于本次采集集合的逐测试独有覆盖数。 总数来自可执行文件中匹配的覆盖映射,包括所有测试均未命中的映射;未进入二进制的代码不计入。 行、条件、MC/DC、路径明确标为未采集。耗时来自插桩构建中的测试执行,不包含 profile 序列化, 不能当作普通构建的性能基准。

identical_coverage_candidates 将三个指标的覆盖集合均相同的成功测试分组,空集合不参与。 独有覆盖计算包含所有已采集测试,包括失败测试。 覆盖相同不代表输入、断言或缺陷检测能力等价;不会自动删除或合并测试。 分组采用覆盖签名,避免所有测试两两比较。

必须使用**采集时的原始可执行文件**分析,并将 profile 与构建一起保存。 报告记录分析时二进制的 SHA-256,但如果二进制在分析前已被替换,无法据此证明它就是采集时的版本。 特征 ID 仅在单份报告内有效,不可跨构建直接比较。延后分析时建议同时归档二进制。 原始 profile 空间随测试数与插桩二进制 profile 大小的乘积增长,当前离线分析逐测试调用 LLVM 工具。 建议按需诊断采集,不在每次编辑后运行。

参考 Clang 源码覆盖率 和 llvm-cov export。