13、CTest超时与资源管理:TIMEOUT属性、RESOURCE_LOCK、并行测试

大家好,我是老张。今天我们来聊聊测试执行中两个非常实际的问题——超时和资源冲突。

你想想看,一个测试跑着跑着就卡死了,或者几个测试同时抢同一个硬件端口导致互相干扰……这些场景,我在项目中遇到过太多次了。CMake 的 CTest 其实提供了很成熟的机制来解决这些问题,只是很多人不知道,或者用错了。

这一节,我们就来彻底搞懂 TIMEOUTRESOURCE_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 配置文件中设置 CTEST_TEST_TIMEOUT 变量。但注意,这个默认值会被测试自身的 TIMEOUT 属性覆盖。

13.3 超时后的处理:测试失败还是跳过?

测试超时后,CTest 默认会把它标记为 失败(Failed)。但有时候,超时可能只是因为环境负载高,而不是代码有问题。这时候你可以用 ATTACHED_FILESFAIL_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_writetest_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 知识体系图

下面这张图展示了本章的核心逻辑:

CTest 超时与资源管理知识体系 TIMEOUT 超时控制 RESOURCE_LOCK 资源锁 并行测试 -j 参数 set_tests_properties TIMEOUT CTEST_TEST_TIMEOUT 默认值 超时 = 失败(推荐) 自定义锁名称(如 "database") 同名锁的测试串行执行 不同锁的测试可并行 ctest -j N 指定并行数 根据 CPU/内存调整 找到性能拐点 三者协同:超时 + 资源锁 + 并行 = 稳定高效的测试

13.8 实战建议

最后,我总结几条实战中的经验:

  1. 每个测试都要设超时。哪怕你觉得它不可能超时。这是最低成本的保险。
  2. 资源锁的名称要有意义。别用 "lock1""lock2" 这种名字。用 "database""gpu:0""serial_port" 这样一看就懂的。
  3. 并行数不要拍脑袋。先测一下单线程耗时,再逐步增加并行数。我一般从 -j 2 开始,然后 -j 4-j 8,直到发现耗时不再明显下降为止。
  4. 超时时间和资源锁要一起用。光有资源锁,测试卡死了还是会卡住。光有超时,资源冲突了还是会乱。两个一起用,才是完整的方案。

核心要点:

  • TIMEOUT 防止测试卡死,每个测试单独设置
  • RESOURCE_LOCK 解决资源冲突,同名锁串行,不同名锁并行
  • -j 参数控制并行度,需要根据机器配置调整
  • 三者配合使用,才能构建稳定高效的测试套件

好了,这一节的内容就到这里。超时和资源管理是测试工程化中非常基础但又容易被忽视的部分。希望你能在自己的项目里用起来。

公众号:蓝海资料掘金营,微信 deep3321