26. 7.2 测试运行与结果分析

引言:当”质检报告”出炉之后

在上一节(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 质检小队配齐了现代化的装备:

  1. 我们拿到了遥控器ctest 命令行选项),学会了控制各种运行参数;
  2. 我们引入了并行作业-j),大幅提升检测效率,同时学会了用 RUN_SERIAL 和资源管理避免冲突;
  3. 我们掌握了精准点名-R / -L),不再盲目全量跑测;
  4. 我们学会了阅读工作笔记(输出日志),并能把日志保存归档;
  5. 我们制定了复检策略--repeat),对付偶发性故障;
  6. 最后,我们把报告挂上了公示栏(CDash / JUnit),让整个团队的质量状况透明可见。

至此,你的 CMake 项目已经具备了从编译到测试的完整闭环。但仅仅"测了"还不够,下一节我们将探讨如何衡量测试的有效性——也就是代码覆盖率。毕竟,质检员检查了每一面墙,和你只检查了大门,出来的"通过率"都是 100%,但含金量完全不同。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……