13、CTest超时与资源管理:TIMEOUT属性、RESOURCE_LOCK、并行测试
大家好,我是老张。今天我们来聊聊测试执行中两个非常实际的问题——超时和资源冲突。
你想想看,一个测试跑着跑着就卡死了,或者几个测试同时抢同一个硬件端口导致互相干扰……这些场景,我在项目中遇到过太多次了。CMake 的 CTest 其实提供了很成熟的机制来解决这些问题,只是很多人不知道,或者用错了。
这一节,我们就来彻底搞懂 TIMEOUT、RESOURCE_LOCK 和并行测试这三个核心概念。
13.1 为什么需要超时控制?
先问一个问题:你的测试套件里,有没有那种“偶尔会挂住”的测试?
比如一个网络通信测试,服务器没启动,客户端就一直等。或者一个文件读写测试,磁盘 I/O 卡住了,测试进程就僵在那里。如果没有超时机制,整个测试流水线就会被这一个测试拖死。
我个人习惯是:每个测试都必须设置超时。哪怕你觉得它只跑 1 秒,也设个 10 秒的超时。这不是多余,这是保险。
13.2 TIMEOUT 属性:给测试上个闹钟
CMake 中设置测试超时非常简单,就是在 add_test 之后,用 set_tests_properties 设置 TIMEOUT 属性。单位是秒。
add_test(NAME test_network COMMAND test_network)
set_tests_properties(test_network PROPERTIES TIMEOUT 30)
add_test(NAME test_database COMMAND test_database)
set_tests_properties(test_database PROPERTIES TIMEOUT 60)
这里有个细节:TIMEOUT 是每个测试单独设置的。你可以给不同的测试设不同的超时时间。比如单元测试设 10 秒,集成测试设 60 秒,压力测试设 300 秒。
CTEST_TEST_TIMEOUT 变量。但注意,这个默认值会被测试自身的 TIMEOUT 属性覆盖。
13.3 超时后的处理:测试失败还是跳过?
测试超时后,CTest 默认会把它标记为 失败(Failed)。但有时候,超时可能只是因为环境负载高,而不是代码有问题。这时候你可以用 ATTACHED_FILES 或 FAIL_REGULAR_EXPRESSION 来做更精细的控制。
不过我个人建议:超时就当失败处理。因为一个测试如果经常超时,说明它本身就不稳定,要么是代码有 bug,要么是环境配置有问题。我曾经有一个测试,在 CI 上总是超时,本地跑却正常。后来发现是 CI 机器的磁盘 I/O 性能太差。我们优化了测试的数据量,问题就解决了。
13.4 RESOURCE_LOCK:解决资源冲突
并行测试是好事,但并行测试带来的资源冲突问题也很头疼。
比如你有两个测试,都需要操作同一个数据库。如果它们同时运行,一个在写数据,一个在读数据,结果就会乱套。或者两个测试都要占用同一个 GPU 显存,同时启动就会 OOM。
这时候就需要 RESOURCE_LOCK 了。
它的原理很简单:给测试分配一个“资源锁”,CTest 会保证持有同一把锁的测试不会同时运行。
add_test(NAME test_db_write COMMAND test_db_write)
set_tests_properties(test_db_write PROPERTIES RESOURCE_LOCK "database")
add_test(NAME test_db_read COMMAND test_db_read)
set_tests_properties(test_db_read PROPERTIES RESOURCE_LOCK "database")
add_test(NAME test_gpu_infer COMMAND test_gpu_infer)
set_tests_properties(test_gpu_infer PROPERTIES RESOURCE_LOCK "gpu:0")
你看,test_db_write 和 test_db_read 都锁了 "database",所以它们不会同时跑。而 test_gpu_infer 锁的是 "gpu:0",跟数据库测试没有冲突,可以并行。
RESOURCE_LOCK 的字符串是自定义的。你叫它 "database" 还是 "db" 都可以,只要两个测试用同一个字符串就行。但建议用有意义的名称,方便维护。
13.5 并行测试:CTest 的 -j 参数
CTest 支持并行执行测试,通过 -j 或 --parallel 参数指定并行数。
ctest -j 4
# 或者
ctest --parallel 4
这个参数告诉 CTest:最多同时启动 4 个测试进程。如果你的机器有 8 核 CPU,设 -j 8 通常能最大化利用资源。
但要注意:并行数不是越大越好。如果测试本身很轻量,比如每个测试只跑 0.1 秒,那并行 100 个也没问题。但如果测试很重,比如每个测试要占 2GB 内存,那并行 8 个可能就把机器搞崩了。
我建议的做法是:先跑一次单线程的测试,看看总耗时和资源占用。然后逐步增加并行数,找到性能拐点。
13.6 超时、资源锁与并行的协同
这三个机制不是孤立的,它们需要配合使用。
举个例子:
add_test(NAME test_gpu_training COMMAND test_gpu_training)
set_tests_properties(test_gpu_training PROPERTIES
TIMEOUT 300
RESOURCE_LOCK "gpu:0"
)
add_test(NAME test_gpu_inference COMMAND test_gpu_inference)
set_tests_properties(test_gpu_inference PROPERTIES
TIMEOUT 60
RESOURCE_LOCK "gpu:0"
)
这里两个测试都锁了 "gpu:0",所以它们不会同时跑。同时每个测试都有自己的超时时间,防止某个测试卡死。
当你用 ctest -j 4 运行时,CTest 会这样调度:
- 先启动
test_gpu_training(因为它排在前面) - 其他 3 个并行槽位可以跑其他不锁 GPU 的测试
- 等
test_gpu_training跑完,再启动test_gpu_inference
这样既保证了资源不冲突,又充分利用了并行能力。
13.7 知识体系图
下面这张图展示了本章的核心逻辑:
13.8 实战建议
最后,我总结几条实战中的经验:
- 每个测试都要设超时。哪怕你觉得它不可能超时。这是最低成本的保险。
- 资源锁的名称要有意义。别用
"lock1"、"lock2"这种名字。用"database"、"gpu:0"、"serial_port"这样一看就懂的。 - 并行数不要拍脑袋。先测一下单线程耗时,再逐步增加并行数。我一般从
-j 2开始,然后-j 4、-j 8,直到发现耗时不再明显下降为止。 - 超时时间和资源锁要一起用。光有资源锁,测试卡死了还是会卡住。光有超时,资源冲突了还是会乱。两个一起用,才是完整的方案。
核心要点:
- TIMEOUT 防止测试卡死,每个测试单独设置
- RESOURCE_LOCK 解决资源冲突,同名锁串行,不同名锁并行
- -j 参数控制并行度,需要根据机器配置调整
- 三者配合使用,才能构建稳定高效的测试套件
好了,这一节的内容就到这里。超时和资源管理是测试工程化中非常基础但又容易被忽视的部分。希望你能在自己的项目里用起来。