27. 7.3 代码覆盖率

引言:质检员的”巡检路线图”

在上一节(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/.*"
)

执行覆盖率测试

配置完成后,执行流程如下:

  1. 先以 Debug 模式配置和构建:
    cmake -B build -DCMAKE_BUILD_TYPE=Debug
    cmake --build build
  2. 运行测试并收集覆盖率:
    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% 覆盖率的垃圾代码依然是垃圾。覆盖率是一个必要的参考指标,但绝不是唯一的质量评判标准。

课后思考

  1. 为什么覆盖率标志不应该在 Release 模式下开启?除了性能损耗,是否还存在其他潜在风险?
  2. 如果你的项目使用了第三方头文件库(header-only library),GCov 会统计到这些头文件里的代码吗?你应该如何在 LCov 过滤规则中处理它们?
  3. 尝试为上一节的测试项目添加 coverage 自定义目标,成功在本地生成 HTML 报告,并找出当前测试覆盖率最低的那个源文件。
请登录后发表评论

    没有回复内容

正在唤醒异次元光景……