导言
在上一节中,我们学习了如何通过 add_test 向项目中添加测试用例,以及如何设置超时、成本、标签和依赖关系等属性。如果把测试用例比作士兵,那么上一节我们完成的是“招兵买马”和“排兵布阵”;而在本节中,我们要学习的是如何“发号施令”——也就是通过 ctest 命令行工具来高效地运行这些测试,并对结果进行精准的分析与展示。
本节内容完全聚焦于测试的执行阶段。你将掌握从本地调试单个失败用例,到并行跑完上千个测试,再到将结果自动提交到 CDash 可视化平台的全套实战技巧。
一、ctest 命令行详解
ctest 是 CMake 内置的测试驱动程序。它不仅仅是一个简单的“运行所有测试”的工具,其命令行选项足以应对从本地开发到 CI 流水线的各种复杂场景。
1.1 基本语法与常用选项速查
在构建目录下(即运行过 cmake --build 的目录),直接输入 ctest 即可执行所有已注册的测试。但更多时候,我们需要精细控制其行为:
# 进入构建目录
cd build
# 最基本:运行所有测试
ctest
# 指定构建类型(对多配置生成器如 Visual Studio、Xcode 尤其重要)
ctest -C Debug
# 只列出测试而不执行(dry-run)
ctest -N
# 显示详细输出(每个测试的标准输出都会打印)
ctest -V
# 显示更详细的输出(包括测试配置信息)
ctest -VV
# 测试失败时立即打印该测试的输出(调试神器)
ctest --output-on-failure
# 只运行指定的测试编号(从 CTestTestfile.cmake 中的序号)
ctest -I 3,5,7
# 运行第 3 到第 10 个测试
ctest -I 3,10
对于日常开发,最推荐的本地调试组合是:
ctest -C Debug --output-on-failure -R "TestName"
这组命令的含义是:在 Debug 配置下,只运行名字匹配 TestName 的测试,如果失败则立即在终端显示该测试的 stdout/stderr,省去我们手动翻日志文件的麻烦。
1.2 退出码与 CI 集成
ctest 的退出码(Exit Code)遵循标准规范:0 表示所有测试通过,非 0 表示有测试失败。这使其可以无缝接入 CI/CD 流水线:
# .github/workflows/ci.yml 片段
- name: Run Tests
run: ctest --output-on-failure -C Release
二、并行测试执行
当项目中有数百甚至数千个测试用例时,串行执行会成为瓶颈。ctest 内置了强大的并行调度能力,其底层原理与 make -j 或 ninja 类似。
2.1 使用 -j 参数
通过 -j 参数指定并行作业数:
# 使用 4 个并行进程运行测试
ctest -j 4
# 自动检测 CPU 核心数并全部用上(慎用,可能耗尽系统资源)
ctest -j$(nproc) # Linux
ctest -j$(sysctl -n hw.ncpu) # macOS
需要注意的是,默认情况下 ctest 认为所有测试都可以安全并行。如果某些测试存在资源竞争(例如同时操作同一个数据库、抢占同一个网络端口),你就需要通过属性告诉 ctest 进行协调。
2.2 资源管理:RESOURCE_LOCK 与 PROCESSORS
在 7.1 节中我们提到过 set_tests_properties,这里我们用它来管理并行资源:
场景 A:互斥锁(RESOURCE_LOCK)
假设 test_db_write 和 test_db_migrate 都会修改同一个本地 SQLite 文件,它们不能同时运行,但各自都可以与其他不碰数据库的测试并行:
add_test(NAME test_db_write COMMAND test_db_write)
add_test(NAME test_db_migrate COMMAND test_db_migrate)
add_test(NAME test_math COMMAND test_math)
set_tests_properties(
test_db_write test_db_migrate
PROPERTIES RESOURCE_LOCK "sqlite_db_file"
)
这样,ctest -j 会确保这两个测试不会同时执行,但 test_math 可以与它们中的任一个并行。
场景 B:CPU 核心配额(PROCESSORS)
如果你的某个测试是多线程的,本身就占用了大量 CPU,可以声明它需要的处理器数量:
add_test(NAME heavy_simulation COMMAND heavy_sim)
set_tests_properties(heavy_simulation PROPERTIES PROCESSORS 4)
当运行 ctest -j 8 时,ctest 的调度器会把这个测试当作占用 4 个槽位(slot)的任务,从而最多再并行调度一个占用 4 槽位(或四个占用 1 槽位)的测试,避免系统过载。
2.3 COST 与负载均衡
结合 7.1 节提到的 COST 属性,ctest 会优先启动耗时长的测试,从而最大化并行效率:
set_tests_properties(
heavy_simulation
PROPERTIES COST 120 # 预计耗时 120 秒
)
set_tests_properties(
quick_unit_test
PROPERTIES COST 1.5 # 预计耗时 1.5 秒
)
在并行模式下,heavy_simulation 会被优先调度,避免它因为启动太晚而成为最后拖慢总耗时的长尾任务。
三、测试筛选
大型项目中,我们通常只想运行特定模块的测试,或排除某些耗时的集成测试。ctest 提供了丰富的筛选机制。
3.1 按名称正则匹配(-R 与 -E)
# 只运行名字包含 "network" 的测试
ctest -R network
# 运行以 "math_" 开头的测试
ctest -R "^math_"
# 排除名字包含 "slow" 的测试(E 即 Exclude)
ctest -E slow
# 组合使用:跑网络测试,但排除慢的
ctest -R network -E slow
3.2 按标签筛选(-L 与 -LE)
如果我们在 CMakeLists.txt 中给测试打好了标签:
add_test(NAME test_serialization COMMAND test_ser)
set_tests_properties(test_serialization PROPERTIES LABELS "io;fast")
add_test(NAME test_tcp_server COMMAND test_tcp)
set_tests_properties(test_tcp_server PROPERTIES LABELS "network;slow")
那么命令行筛选就非常直观:
# 只跑标签为 fast 的测试
ctest -L fast
# 排除标签为 slow 的测试(日常开发常用)
ctest -LE slow
# 多个标签是“或”关系:跑带有 fast 或 critical 标签的测试
ctest -L "fast|critical"
3.3 组合筛选策略
-R 和 -L 可以组合使用,它们之间是“与”关系:
# 名字包含 "test_io" 并且标签为 "fast" 的测试
ctest -R test_io -L fast
四、输出日志
测试失败时,快速定位问题核心在于查看日志。ctest 提供了多个维度的日志控制。
4.1 终端实时输出
# 失败时立即在终端显示该测试的输出(强烈推荐)
ctest --output-on-failure
# 显示所有测试的全部输出(很吵,但调试时有用)
ctest -V
# 超详细模式(包含 ctest 自身的调试信息)
ctest -VV
4.2 日志重定向到文件
默认情况下,ctest 会在构建目录下创建 Testing/Temporary/LastTest.log 保存上次运行的详细结果。但你也可以自定义输出:
# 将 ctest 自身的摘要输出保存
ctest --output-on-failure > ctest_summary.txt 2>&1
# 使用 -O 选项同时输出到终端和文件
ctest -O TestLog.xml --output-on-failure
4.3 测试输出目录结构
在构建目录的 Testing 文件夹下,ctest 会维护一个完整的结果数据库:
Testing/Temporary/LastTest.log:最近一次测试的完整日志Testing/Temporary/CTestCostData.txt:测试成本的历史记录(用于优化下次并行调度)Testing//:每次运行的独立目录,包含Test.xml等文件
4.4 控制单个测试的输出截断
某些测试(如压力测试)可能会输出海量日志。CMake 3.29+ 支持截断:
set_tests_properties(
spammy_test
PROPERTIES OUTPUT_SIZE 65536 # 只保留最后 64KB 输出
)
五、失败重试
在实际工程中,有些测试存在“不稳定性”(Flaky)——它们可能因网络抖动、磁盘 IO 竞争等原因偶发失败。ctest 提供了多种重试策略。
5.1 repeat 模式详解
# 模式 1:重复运行直到失败(用于复现偶现 Bug)
ctest -R test_network --repeat until-fail:100
# 含义:最多重复 100 次,一旦失败就停止。如果 100 次都通过,ctest 返回成功。
# 模式 2:重复运行直到通过(对 Flaky 测试的宽容策略)
ctest -R test_network --repeat until-pass:5
# 含义:如果测试失败,最多重试 5 次,只要有一次通过即算整体通过。
# 模式 3:超时后重试
ctest -R test_network --repeat after-timeout:3
# 含义:只有测试因 TIMEOUT 失败时才重试,最多 3 次。
注意: until-pass 虽然能绿 CI,但过度使用会掩盖真正的 Bug。建议仅对经过严格审查确认是环境问题的测试使用,并配合日志记录每次失败的原因。
5.2 在 CMakeLists.txt 中声明重试(CMake 3.17+)
你也可以在测试定义时就指定重试次数,无需在命令行记忆:
add_test(NAME flaky_network_test COMMAND flaky_network_test)
set_tests_properties(flaky_network_test PROPERTIES WILL_FAIL FALSE)
set_property(TEST flaky_network_test PROPERTY REPEAT_UNTIL_PASS 3)
这样每次运行 ctest 时,该测试会自动获得 3 次重试机会。
六、CDash 集成
在个人开发和 CI 环境中,测试结果往往分散在各处。CDash 是 Kitware 官方提供的开源测试仪表盘,可以将多平台、多配置的测试结果汇总到 Web 页面,进行趋势分析和可视化展示。
6.1 配置 CTestConfig.cmake
在项目根目录(与顶层 CMakeLists.txt 同级)创建 CTestConfig.cmake:
set(CTEST_PROJECT_NAME "MyAwesomeProject")
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=MyAwesomeProject")
set(CTEST_DROP_SITE_CDASH TRUE)
其中 my.cdash.org 可以替换为你团队部署的私有 CDash 服务器地址。
6.2 测试与提交的完整工作流
CDash 通过 ctest -T 或 ctest -D 来驱动。一个典型的“实验性”提交流程如下:
cd build
# 方式 1:分步执行(适合理解原理)
ctest -T Start # 通知 CDash:我要开始一轮新测试
ctest -T Configure # 提交 CMake 配置信息
ctest -T Build # 提交构建结果
ctest -T Test # 运行测试并收集结果
ctest -T Submit # 将所有结果打包上传到 CDash
# 方式 2:一键执行(日常最常用)
ctest -D Experimental
-D Experimental 等价于依次执行 Start、Update(空操作)、Configure、Build、Test、Submit 六个步骤。
6.3 自定义提交模型
CDash 支持多种预定义模型,用于区分测试的用途:
- Experimental:开发者本地随意提交,不期望代码是稳定的。
- Nightly:夜间定时构建,通常拉取主线最新代码完整跑一遍。
- Continuous:持续集成,代码提交后自动触发。
# Nightly 模式
ctest -D Nightly
# Continuous 模式
ctest -D Continuous
6.4 在 CI 中集成 CDash
以 GitHub Actions 为例,你可以在构建完成后增加提交步骤:
- name: Build and Test with CDash
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
cd build
ctest -D Experimental --output-on-failure
env:
CTEST_TOKEN: ${{ secrets.CDASH_TOKEN }}
如果 CDash 服务器需要认证,可以在 CTestConfig.cmake 中配置 CTEST_DROP_SITE_USER 和 CTEST_DROP_SITE_PASSWORD,或者通过环境变量注入 Token。
6.5 查看结果
提交成功后,打开 CDash 项目页面,你可以看到:
- 每次提交的通过/失败概览
- 单个测试的历史趋势(最近 7 天是否稳定)
- 失败的测试输出和标准错误
- 代码覆盖率随时间变化曲线(如果集成了 GCov)
小结
本节我们完成了从“写测试”到“跑测试”再到“看测试”的闭环:
- 掌握了
ctest命令行的核心选项,尤其是--output-on-failure和配置选择-C; - 学会了通过
-j加速测试,并利用RESOURCE_LOCK和PROCESSORS避免资源冲突; - 能够灵活运用
-R、-L及其排除参数对海量测试进行精准筛选; - 理解了 ctest 的日志机制和输出控制;
- 了解了针对不稳定测试的
--repeat重试策略; - 初步掌握了将测试结果提交到 CDash 进行团队级可视化分析的方法。
在下一节中,我们将继续深入质量保障领域,探讨如何利用 CMake 集成代码覆盖率(Code Coverage)工具,让你的测试不仅能“跑通”,还能“盖全”。


没有回复内容