引言:当”质检报告”出炉之后
在上一节(7.1)中,我们的 CMake “施工队长”已经成功招聘并培训了一支质检小队——我们用 add_test 定义了检测项目,用 set_tests_properties 设定了检测标准(超时、成本、依赖等)。但招聘培训只是第一步,真正考验管理能力的,是如何高效地指挥这支队伍干活。
想象你是一位工程总监,手里拿着一叠质检任务单。你会面临很多实际问题:能不能同时派多个质检员去不同楼层检查(并行执行)?如何只检查水电相关项目,不看油漆(筛选)?如果某面墙第一次敲有空鼓声、第二次又好了,要不要多敲几遍确认(失败重试)?最终,这些质检结果是记在小本子上,还是挂到公司公示栏供所有人查看(可视化集成)?
这一节,我们要让 CTest 从”立正稍息”的新兵,变成一支能打仗、会汇报的精锐部队。
一、ctest 命令行详解——质检员的”遥控器”
在构建目录下,CMake 会生成一个名为 ctest 的命令行工具(或者直接调用 cmake --build . --target test,但功能较弱)。ctest 才是我们与质检小队沟通的”对讲机”。
1.1 最基础的用法
进入构建目录,直接输入:
ctest
它会默认运行所有已定义的测试,并给出简洁的通过/失败摘要。如果全部通过,你会看到一句让人安心的:
100% tests passed, 0 tests failed out of 42
1.2 常用选项速查
作为新手,你不需要记住所有参数,但以下几个是”吃饭的家伙”:
-N, --show-only:只点名不干活。列出所有将要运行的测试及其编号,但不真正执行。适合检查测试是否被 CMake 正确识别。-V, --verbose:详细模式。输出每个测试的执行命令和结果。-VV, --extra-verbose:话痨模式。连测试内部的每一行标准输出都打印出来,调试时救命用。-C <Config>:指定构建类型。在多配置生成器(如 Visual Studio)中必须指定-C Debug或-C Release。--output-on-failure:失败时才啰嗦。平时保持安静,只有测试挂掉时才把该测试的 stdout/stderr 打印出来。这是 CI 环境中最受欢迎的选项。--timeout <seconds>:全局超时设置,覆盖所有未单独设置TIMEOUT属性的测试。-T <Action>:执行特殊动作,如-T MemCheck(内存检查)、-T Coverage(覆盖率)、-T Submit(提交到 CDash)。
一个典型的本地调试组合拳:
ctest -C Debug --output-on-failure -R "Math"
含义:在 Debug 模式下,只跑名字带 "Math" 的测试,如果失败了就把临终遗言打印出来。
二、并行测试执行——让质检员"多线程"干活
现代 CPU 都是多核心的,如果一个个顺序跑测试,就像让十个质检员排队检查同一面墙,其他九个人在喝咖啡。CTest 支持并行执行,命令极其简单:
ctest -j8
这表示同时启动 8 个测试进程。但等等!不是什么测试都能无脑并行的。
2.1 资源冲突:别让你的测试"抢车位"
假设两个测试都要读写同一个临时文件 /tmp/my_data.db,或者都要占用 8080 端口。如果并行执行,它们会互相干扰,导致"薛定谔的失败"——单独跑都过,一起跑就挂。
CMake 3.16+ 引入了资源管理机制,你可以在 set_tests_properties 中声明测试需要占用多少资源:
set_tests_properties(NetworkTest1 NetworkTest2 PROPERTIES
RESOURCE_GROUPS "network:1"
PROCESSORS 2
)
然后在运行时用 --resource-spec-file 指定资源池大小,ctest 会自动调度,避免"抢车位"。不过对于大多数中小项目,更简单的做法是:
- 让每个测试操作独立的临时目录(用
${CMAKE_CURRENT_BINARY_DIR}隔离)。 - 让占用固定端口的测试顺序执行(通过
RUN_SERIAL属性或DEPENDS串行化)。
2.2 RUN_SERIAL:这个测试必须"包场"
如果你知道某个测试绝对不能跟别人一起跑(比如它在测试全局单例、操作真实硬件),给它打上标签:
set_tests_properties(HardwareIntegrationTest PROPERTIES RUN_SERIAL TRUE)
即使你用 ctest -j16,这个测试也会享受 VIP 待遇——等前面的测试跑完了,它独占 CPU 慢慢跑。
三、测试筛选——精准"点名"
大型项目可能有成百上千个测试,全跑一遍耗时太久。ctest 提供了强大的筛选语法,让你指哪打哪。
3.1 按名字筛选:-R 与 -E
-R <regex>(Run):只跑匹配正则表达式的测试。-E <regex>(Exclude):排除匹配正则表达式的测试。
示例:
# 只跑以 Math 开头的测试
ctest -R "^Math"
# 跑所有测试,但跳过名字带 Slow 的
ctest -E "Slow"
# 组合使用:跑 Math 相关,但跳过慢速的
ctest -R "Math" -E "Slow"
3.2 按标签筛选:-L 与 -LE
还记得上节课我们用 LABELS 属性给测试分过类吗?现在派上用场了:
# 只跑打了 "unit" 标签的单元测试
ctest -L unit
# 跑除了 "integration" 之外的所有测试
ctest -LE integration
# 组合:单元测试中排除慢速的(假设慢速测试也打了 "slow" 标签)
ctest -L unit -LE slow
3.3 编号筛选
ctest 内部给每个测试分配了编号(从 1 开始)。如果你知道编号,也可以用 -I 指定范围:
# 只跑第 3 到第 10 个测试
ctest -I 3,10
# 只跑第 5、8、12 个测试
ctest -I 3,5,5,8,8,12
(格式为 start,end,stride1,test1,test2...,虽然有点古怪,但在调试特定失败时很有用。)
四、输出日志——看懂质检员的"工作笔记"
测试失败了,但只看到一句 Failed 是远远不够的。我们需要看测试死前说了什么。
4.1 三种输出模式对比
| 模式 | 命令 | 适用场景 |
|---|---|---|
| 静默模式 | ctest |
CI 流水线默认跑法,只看最终摘要 |
| 失败显式 | ctest --output-on-failure |
本地开发首选,失败才打印详情,成功时保持清爽 |
| 全程啰嗦 | ctest -V 或 -VV |
本地调试疑难杂症,连执行命令都打印 |
4.2 输出重定向到文件
在 CI 服务器上,终端输出可能被截断或混淆。你可以让 ctest 把完整报告写入文件:
ctest --output-log test_results.log --output-on-failure
这样即使终端滚动条飞过去了,你也能在 test_results.log 里找到完整的"犯罪现场记录"。
4.3 测试内部的输出捕获
CTest 默认会捕获测试程序的标准输出(stdout/stderr),只有在启用详细模式或失败时才会显示。如果你希望测试程序实时把日志吐到文件,可以在测试命令里自己重定向:
add_test(NAME MyTest COMMAND MyExe --log_file=${CMAKE_CURRENT_BINARY_DIR}/my_test.log)
五、失败重试——排查"偶发性故障"
最让开发者抓狂的,不是"总是失败"的测试,而是"十次里挂一次"的薛定谔测试(Flaky Test)。它可能是由 race condition、网络抖动或资源竞争引起的。
CTest 提供了优雅的重试机制,不需要你写 shell 循环:
5.1 一直跑到失败为止(稳定性验证)
你修复了一个 Bug,怀疑某个测试是不是真的稳了?让它跑 100 次,只要有一次失败就停:
ctest -R "RaceConditionTest" --repeat until-fail:100
如果 100 次全过,你可以比较有信心地说它真的被修好了。
5.2 一直跑到通过为止(宽容模式)
有些测试依赖外部网络服务,偶尔抽风。你不希望因为一次网络超时导致整个构建被标记为失败:
ctest -R "CloudSyncTest" --repeat until-pass:3 --timeout 30
这表示:最多试 3 次,只要有一次通过就算胜利。注意:不要在核心逻辑测试上使用这个,它只适用于外部依赖不稳定的场景,否则你会把真正的 Bug 掩盖掉。
5.3 超时后重试
ctest --repeat after-timeout:2
只对因为超时而失败的测试进行重试,不影响正常失败的测试。
5.4 策略建议
- 单元测试:绝不应该 Flaky。如果出现了,优先修复代码,而不是加 retry。
- 集成/系统测试:涉及数据库、网络、硬件时,可以用
until-pass做缓冲,但要限制重试次数。 - 稳定性验证:发布前对核心流程用
until-fail做压力测试。
六、CDash 集成——把质检报告挂上"公示栏"
一个人跑测试看结果,就像把质检报告锁在抽屉里。在团队协作中,我们需要一个公开的、可视化的、带历史记录的测试看板。CMake 官方提供的 CDash 就是干这个的。
6.1 CDash 是什么
CDash 是一个基于 Web 的测试仪表盘(Dashboard)。它可以接收 CTest 提交的测试结果、编译警告、覆盖率报告、内存检查报告等,并以漂亮的图表展示出来。你可以把它理解成质检部门的电子公示栏:今天谁过了、谁挂了、最近一周质量趋势是上升还是下降,一目了然。
6.2 配置提交地址
在项目源码根目录(或构建目录)创建 CTestConfig.cmake:
set(CTEST_PROJECT_NAME "MyAwesomeProject")
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")
set(CTEST_DROP_METHOD "https")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=MyAwesomeProject")
set(CTEST_DROP_SITE_CDASH TRUE)
6.3 提交测试结果
不需要手动上传文件,ctest 自带"快递服务":
# 方式一:先跑测试,再单独提交
ctest -T Test
ctest -T Submit
# 方式二:一站式完成(Experimental 模式)
ctest -D Experimental
# 方式三:Nightly 模式(用于定时构建,CDash 会按日期归档)
ctest -D Nightly
-D 参数是 CTest 的"剧本模式"(Dashboard),它会自动执行配置、构建、测试、覆盖率、内存检查、提交等一系列步骤。
6.4 在 CDash 上看什么
提交成功后,打开你的 CDash 网址,可以看到:
- Build Summary:编译通过了没?有多少警告?
- Test Summary:哪些测试挂了?失败的历史趋势如何?
- Coverage:代码覆盖率变化曲线(需要提前用
-T Coverage生成)。 - Dynamic Analysis:Valgrind、AddressSanitizer 发现的问题汇总。
6.5 没有自己的 CDash 服务器?
如果只是小团队,搭建 CDash 服务器可能太重。你可以:
- 使用 CMake 官方提供的 my.cdash.org 公共实例。
- 把
ctest --output-junit result.xml生成的 JUnit 格式报告,交给 Jenkins、GitLab CI、GitHub Actions 等现代 CI 系统展示。
ctest --output-junit test-results.xml
# 然后在 CI 配置里把 test-results.xml 作为产物上传
总结:从"会写测试"到"会跑测试"
这一节,我们给 CTest 质检小队配齐了现代化的装备:
- 我们拿到了遥控器(
ctest命令行选项),学会了控制各种运行参数; - 我们引入了并行作业(
-j),大幅提升检测效率,同时学会了用RUN_SERIAL和资源管理避免冲突; - 我们掌握了精准点名(
-R/-L),不再盲目全量跑测; - 我们学会了阅读工作笔记(输出日志),并能把日志保存归档;
- 我们制定了复检策略(
--repeat),对付偶发性故障; - 最后,我们把报告挂上了公示栏(CDash / JUnit),让整个团队的质量状况透明可见。
至此,你的 CMake 项目已经具备了从编译到测试的完整闭环。但仅仅"测了"还不够,下一节我们将探讨如何衡量测试的有效性——也就是代码覆盖率。毕竟,质检员检查了每一面墙,和你只检查了大门,出来的"通过率"都是 100%,但含金量完全不同。


没有回复内容