引言:质检员的”巡检路线图”
在上一节(7.2)中,我们的 CMake “施工队长”已经成功指挥质检小队完成了各项检测,并且学会了如何高效地调度、筛选和分析测试结果。但不知道你有没有想过这样一个问题:质检员说他检查了大楼,但他真的走进了每一个房间吗?有没有哪个储藏室因为门被锁着就被跳过了?
在 C++ 项目中,代码覆盖率(Code Coverage)就是这份”巡检路线图”。它不会直接告诉你墙砌得直不直(这由测试断言负责),但它能精确地告诉你:质检员的脚步是否覆盖了每一行代码、每一个分支、每一个函数入口。没有覆盖率的测试报告,就像没有签名的质检单——你不知道该信多少。
这一节,我们就来学习如何让 CMake 这位队长自动生成这份”路线图”:从编译器的探针植入,到 CTest 的一键收集,再到漂亮的 HTML 可视化报告,最后上传到 SonarQube 等集团级质量管理平台。
GCov/LCov 集成:给代码装上”定位器”
要让系统知道哪些代码被执行过,我们需要在编译阶段植入一些”探针”。GCC 和 Clang 都内置了这套机制,核心工具叫做 GCov。
什么是 GCov 和 LCov?
- GCov:GCC 自带的覆盖率分析工具。当你开启特定编译选项后,它会伴随编译过程产生两种文件:
.gcno(记录程序结构的笔记文件)和运行后产生的.gcda(记录实际执行路径的数据文件)。 - LCov:GCov 的图形化前端,能将散落的
.gcda数据汇总、过滤,并生成带有颜色标注的 HTML 报告。
配置编译器和链接器标志
在 CMake 中,我们需要为目标添加特殊标志:--coverage(它等价于 -fprofile-arcs -ftest-coverage)。这相当于给每块砖都装上了定位芯片,程序跑过的每一步都会被记录。
关键点在于:覆盖率标志只应该在 Debug 模式下开启。Release 模式下开启会严重影响性能,而且完全没有必要。
利用我们在 3.3 节学过的生成器表达式,可以精准控制,避免污染其他构建类型:
target_compile_options(my_app PRIVATE
$<$:--coverage>
)
target_link_options(my_app PRIVATE
$<$:--coverage>
)
如果你希望兼容更多编译器,或者项目需要同时支持 GCC 和 MSVC(MSVC 使用不同的覆盖率工具),可以加上编译器判断:
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(my_app PRIVATE
$<$:--coverage>
)
target_link_options(my_app PRIVATE
$<$:--coverage>
)
endif()
配置完成后,每次运行测试程序,编译器都会在构建目录中生成对应的 .gcda 文件。这些就是原始的”巡检记录”。
CTest 内置覆盖率支持:一键收集数据
手动收集散落在各处的 .gcda 文件非常麻烦,就像质检员把记录纸丢得满工地都是。幸运的是,CTest 内置了对覆盖率的支持,相当于给队长配了一个自动归档员。
启用 CTest 覆盖率收集
CTest 需要知道使用什么命令来分析覆盖率数据。通常默认就是 gcov,但显式声明更稳妥。在你的 CMakeLists.txt 中:
set(CTEST_COVERAGE_COMMAND "gcov")
include(CTest)
或者,你也可以在项目根目录创建 CTestConfig.cmake 文件进行配置:
set(CTEST_PROJECT_NAME "MyAwesomeProject")
set(CTEST_COVERAGE_COMMAND "gcov")
set(CTEST_CUSTOM_COVERAGE_EXCLUDE
".*third_party/.*"
".*tests/.*"
)
执行覆盖率测试
配置完成后,执行流程如下:
- 先以 Debug 模式配置和构建:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build - 运行测试并收集覆盖率:
ctest --test-dir build -T Coverage
这里的 -T Coverage 是灵魂参数。它会让 CTest 在完成测试后,自动扫描构建目录中的 .gcda 文件,并调用 GCov 生成每个源文件的覆盖率统计。
你会在终端看到类似这样的输出:
Performing coverage
Processing coverage (each . represents one file):
....................
Accumulating results (each . represents one file):
....................
Covered LOC: 1200
Not covered LOC: 300
Total LOC: 1500
Coverage: 80.00%
虽然 CTest 能快速给出百分比汇总,但它的原始输出并不够直观。要生成按文件、按行分析的漂亮报告,我们需要请出 LCov。
生成 HTML 报告:让数据开口说话
原始数据是工程师的语言,但人类更喜欢看颜色:绿色代表已覆盖,红色代表未覆盖,黄色代表部分覆盖。LCov + genhtml 就是绘制这幅”施工热力图”的画师。
命令行手动生成
在构建目录中,依次执行以下命令:
# 1. 收集数据,生成 info 文件
lcov --capture --directory . --output-file coverage.info
# 2. 过滤系统头文件、第三方库和测试代码本身(避免数据被污染)
lcov --remove coverage.info '/usr/*' '*/third_party/*' '*/tests/*' --output-file coverage.info
# 3. 生成 HTML 网页报告
genhtml coverage.info --output-directory coverage_report
# 4. 打开报告查看
# Linux:
xdg-open coverage_report/index.html
# macOS:
open coverage_report/index.html
打开 index.html,你会看到项目总览:函数覆盖率、行覆盖率、分支覆盖率。点击任意文件名,还能逐行查看源码——已被覆盖的代码呈现绿色背景,未被覆盖的代码呈现醒目的红色背景。
在 CMake 中添加自定义目标
每次都手动敲 LCov 命令太繁琐,而且容易漏掉过滤步骤。我们可以给 CMake 添加一个自定义目标,让队长帮我们一键搞定:
find_program(LCOV_PATH lcov REQUIRED)
find_program(GENHTML_PATH genhtml REQUIRED)
add_custom_target(coverage
# 清零之前的统计数据,保证本次结果纯净
COMMAND ${LCOV_PATH} --zerocounters --directory ${CMAKE_BINARY_DIR}
# 运行测试(确保生成新的 .gcda 文件)
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
# 收集本次构建目录下的覆盖率数据
COMMAND ${LCOV_PATH} --capture
--directory ${CMAKE_BINARY_DIR}
--output-file ${CMAKE_BINARY_DIR}/coverage.info
# 过滤系统路径、第三方库和测试目录
COMMAND ${LCOV_PATH} --remove ${CMAKE_BINARY_DIR}/coverage.info
'/usr/*'
'${CMAKE_SOURCE_DIR}/third_party/*'
'${CMAKE_SOURCE_DIR}/tests/*'
--output-file ${CMAKE_BINARY_DIR}/coverage.info
# 生成 HTML 报告到 coverage_report 目录
COMMAND ${GENHTML_PATH} ${CMAKE_BINARY_DIR}/coverage.info
--output-directory ${CMAKE_BINARY_DIR}/coverage_report
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating code coverage report..."
VERBATIM
)
配置完成后,只需运行:
cmake --build build --target coverage
CMake 就会自动完成:清零旧数据 → 跑测试 → 收集数据 → 过滤杂质 → 生成网页报告。整个过程一气呵成,最终报告躺在 build/coverage_report/index.html 里等你查阅。
与 SonarQube 等平台集成:把报告汇入”集团质量中心”
在大型企业中,单个工地的巡检记录通常要汇总到统一的质量管理平台,比如 SonarQube。这就像是把工地的日报,同步上传到集团的工程监理系统,供所有项目经理审阅。
SonarQube 的覆盖率格式
SonarQube 本身不直接读取 GCov 的原始数据,它通常期望特定格式的 XML 报告。对于 C++ 项目,最通用的做法是使用 gcovr 工具,将 LCov 的数据或直接解析 GCov 输出,转换为 SonarQube Generic Coverage 格式:
# 安装 gcovr: pip install gcovr
gcovr --sonarqube coverage.xml --root ${CMAKE_SOURCE_DIR}
然后在项目根目录的 sonar-project.properties 中指定路径:
sonar.projectKey=my_project
sonar.sources=src
sonar.tests=tests
sonar.coverageReportPaths=build/coverage.xml
如果你使用的是 SonarQube 的 C/C++ 社区插件(sonar-cxx),它可能支持直接读取 .gcov 文件,此时配置方式会略有不同,需要参考对应插件文档。
与 CI/CD 流水线集成
在 GitHub Actions、GitLab CI 或 Jenkins 中,覆盖率通常是流水线的一个独立阶段。以 GitLab CI 为例:
coverage:
stage: test
script:
- cmake -B build -DCMAKE_BUILD_TYPE=Debug
- cmake --build build
- cmake --build build --target coverage
- gcovr --sonarqube build/coverage.xml --root .
coverage: '/All filess+|s+([0-9.]+)/'
artifacts:
paths:
- build/coverage_report/
reports:
coverage_report:
coverage_format: cobertura
path: build/coverage.xml
这样配置后,每次提交代码,GitLab 都会在你的 Merge Request 中直接展示覆盖率的变化趋势;如果集成了 SonarQube,未覆盖的新代码会在审查界面被高亮标注,甚至可以通过质量门禁(Quality Gate)阻止低覆盖率的代码合入主干。
小结:从”测了没”到”测全了”
这一节,我们补全了自动化测试体系的最后一块重要拼图:
- GCov/LCov:通过
--coverage标志在 Debug 模式下植入探针,收集代码执行轨迹。 - CTest 内置支持:使用
ctest -T Coverage一键触发覆盖率统计,无需手动翻找.gcda文件。 - HTML 可视化报告:利用
add_custom_target(coverage)封装 LCov 工具链,将枯燥的数据转化为直观的网页,红绿黄三色标注让遗漏点无处藏身。 - 质量平台集成:通过
gcovr等工具转换格式,将结果汇入 SonarQube 或 CI/CD 系统,实现团队级的质量监控。
不过要牢记一个原则:高覆盖率不等于高质量。覆盖率只能证明代码”被运行过”,不能证明代码”运行正确”。100% 覆盖率的垃圾代码依然是垃圾。覆盖率是一个必要的参考指标,但绝不是唯一的质量评判标准。
课后思考
- 为什么覆盖率标志不应该在 Release 模式下开启?除了性能损耗,是否还存在其他潜在风险?
- 如果你的项目使用了第三方头文件库(header-only library),GCov 会统计到这些头文件里的代码吗?你应该如何在 LCov 过滤规则中处理它们?
- 尝试为上一节的测试项目添加
coverage自定义目标,成功在本地生成 HTML 报告,并找出当前测试覆盖率最低的那个源文件。


没有回复内容