CMake 秘籍(三)(2)https://developer.aliyun.com/article/1524618
为长时间测试设置超时
本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-07
找到。本节适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
理想情况下,测试集应该只需要很短的时间,以激励开发者频繁运行测试集,并使得对每次提交(变更集)进行测试成为可能(或更容易)。然而,有些测试可能会耗时较长或卡住(例如,由于高文件 I/O 负载),我们可能需要实施超时机制来终止超时的测试,以免它们堆积起来延迟整个测试和部署流水线。在本节中,我们将展示一种实施超时的方法,可以为每个测试单独调整。
准备工作
本食谱的成分将是一个微小的 Python 脚本(test.py
),它总是返回0
。为了保持超级简单并专注于 CMake 方面,测试脚本除了等待两秒钟之外不做任何事情;但是,我们可以想象在现实生活中,这个测试脚本会执行更有意义的工作:
import sys import time # wait for 2 seconds time.sleep(2) # report success sys.exit(0)
如何操作
我们需要通知 CTest,如果测试超时,需要终止测试,如下所示:
- 我们定义项目名称,启用测试,并定义测试:
# set minimum cmake version cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name project(recipe-07 LANGUAGES NONE) # detect python find_package(PythonInterp REQUIRED) # define tests enable_testing() # we expect this test to run for 2 seconds add_test(example ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py)
- 此外,我们为测试指定了一个
TIMEOUT
,并将其设置为 10 秒:
set_tests_properties(example PROPERTIES TIMEOUT 10)
- 我们知道如何配置和构建,我们期望测试通过:
$ ctest Test project /home/user/cmake-recipes/chapter-04/recipe-07/example/build Start 1: example 1/1 Test #1: example .......................... Passed 2.01 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 2.01 sec
- 现在,为了验证
TIMEOUT
是否有效,我们将test.py
中的睡眠命令增加到 11 秒,并重新运行测试:
$ ctest Test project /home/user/cmake-recipes/chapter-04/recipe-07/example/build Start 1: example 1/1 Test #1: example ..........................***Timeout 10.01 sec 0% tests passed, 1 tests failed out of 1 Total Test time (real) = 10.01 sec The following tests FAILED: 1 - example (Timeout) Errors while running CTest
工作原理
TIMEOUT
是一个方便的属性,可用于通过使用set_tests_properties
为单个测试指定超时。如果测试超过该时间,无论出于何种原因(测试停滞或机器太慢),测试都会被终止并标记为失败。
并行运行测试
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-08
找到。该食谱适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
大多数现代计算机都有四个或更多的 CPU 核心。CTest 的一个很棒的功能是,如果你有多个核心可用,它可以并行运行测试。这可以显著减少总测试时间,减少总测试时间才是真正重要的,以激励开发者频繁测试。在这个食谱中,我们将演示这个功能,并讨论如何优化你的测试定义以获得最大性能。
准备就绪
让我们假设我们的测试集包含标记为a, b, …, j的测试,每个测试都有特定的持续时间:
测试 | 持续时间(以时间单位计) |
a, b, c, d | 0.5 |
e, f, g | 1.5 |
h | 2.5 |
i | 3.5 |
j | 4.5 |
时间单位可以是分钟,但为了保持简单和短,我们将使用秒。为了简单起见,我们可以用一个 Python 脚本来表示消耗 0.5 时间单位的测试a:
import sys import time # wait for 0.5 seconds time.sleep(0.5) # finally report success sys.exit(0)
其他测试可以相应地表示。我们将把这些脚本放在CMakeLists.txt
下面的一个目录中,目录名为test
。
如何操作
对于这个食谱,我们需要声明一个测试列表,如下所示:
CMakeLists.txt
非常简短:
# set minimum cmake version cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name project(recipe-08 LANGUAGES NONE) # detect python find_package(PythonInterp REQUIRED) # define tests enable_testing() add_test(a ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py) add_test(b ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/b.py) add_test(c ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/c.py) add_test(d ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/d.py) add_test(e ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/e.py) add_test(f ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/f.py) add_test(g ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/g.py) add_test(h ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/h.py) add_test(i ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/i.py) add_test(j ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/j.py)
- 我们可以使用
ctest
配置项目并运行测试,总共需要 17 秒:
$ mkdir -p build $ cd build $ cmake .. $ ctest Start 1: a 1/10 Test #1: a ................................ Passed 0.51 sec Start 2: b 2/10 Test #2: b ................................ Passed 0.51 sec Start 3: c 3/10 Test #3: c ................................ Passed 0.51 sec Start 4: d 4/10 Test #4: d ................................ Passed 0.51 sec Start 5: e 5/10 Test #5: e ................................ Passed 1.51 sec Start 6: f 6/10 Test #6: f ................................ Passed 1.51 sec Start 7: g 7/10 Test #7: g ................................ Passed 1.51 sec Start 8: h 8/10 Test #8: h ................................ Passed 2.51 sec Start 9: i 9/10 Test #9: i ................................ Passed 3.51 sec Start 10: j 10/10 Test #10: j ................................ Passed 4.51 sec 100% tests passed, 0 tests failed out of 10 Total Test time (real) = 17.11 sec
- 现在,如果我们碰巧有四个核心可用,我们可以在不到五秒的时间内将测试集运行在四个核心上:
$ ctest --parallel 4 Start 10: j Start 9: i Start 8: h Start 5: e 1/10 Test #5: e ................................ Passed 1.51 sec Start 7: g 2/10 Test #8: h ................................ Passed 2.51 sec Start 6: f 3/10 Test #7: g ................................ Passed 1.51 sec Start 3: c 4/10 Test #9: i ................................ Passed 3.63 sec 5/10 Test #3: c ................................ Passed 0.60 sec Start 2: b Start 4: d 6/10 Test #6: f ................................ Passed 1.51 sec 7/10 Test #4: d ................................ Passed 0.59 sec 8/10 Test #2: b ................................ Passed 0.59 sec Start 1: a 9/10 Test #10: j ................................ Passed 4.51 sec 10/10 Test #1: a ................................ Passed 0.51 sec 100% tests passed, 0 tests failed out of 10 Total Test time (real) = 4.74 sec
工作原理
我们可以看到,在并行情况下,测试j, i, h和e同时开始。并行运行时总测试时间的减少可能是显著的。查看ctest --parallel 4
的输出,我们可以看到并行测试运行从最长的测试开始,并在最后运行最短的测试。从最长的测试开始是一个非常好的策略。这就像打包搬家箱子:我们从较大的物品开始,然后用较小的物品填充空隙。比较在四个核心上从最长测试开始的a-j测试的堆叠,看起来如下:
--> time core 1: jjjjjjjjj core 2: iiiiiiibd core 3: hhhhhggg core 4: eeefffac
按照定义的顺序运行测试看起来如下:
--> time core 1: aeeeiiiiiii core 2: bfffjjjjjjjjj core 3: cggg core 4: dhhhhh
按照定义的顺序运行测试总体上需要更多时间,因为它让两个核心大部分时间处于空闲状态(这里,核心 3 和 4)。CMake 是如何知道哪些测试需要最长的时间?CMake 知道每个测试的时间成本,因为我们首先按顺序运行了测试,这记录了每个测试的成本数据在文件Testing/Temporary/CTestCostData.txt
中,看起来如下:
a 1 0.506776 b 1 0.507882 c 1 0.508175 d 1 0.504618 e 1 1.51006 f 1 1.50975 g 1 1.50648 h 1 2.51032 i 1 3.50475 j 1 4.51111
如果我们刚配置完项目就立即开始并行测试,它将按照定义的顺序运行测试,并且在四个核心上,总测试时间会明显更长。这对我们意味着什么?这是否意味着我们应该根据递减的时间成本来排序测试?这是一个选项,但事实证明还有另一种方法;我们可以自行指示每个测试的时间成本:
add_test(a ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py) add_test(b ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/b.py) add_test(c ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/c.py) add_test(d ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/d.py) set_tests_properties(a b c d PROPERTIES COST 0.5) add_test(e ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/e.py) add_test(f ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/f.py) add_test(g ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/g.py) set_tests_properties(e f g PROPERTIES COST 1.5) add_test(h ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/h.py) set_tests_properties(h PROPERTIES COST 2.5) add_test(i ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/i.py) set_tests_properties(i PROPERTIES COST 3.5) add_test(j ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/j.py) set_tests_properties(j PROPERTIES COST 4.5)
COST
参数可以是估计值或从Testing/Temporary/CTestCostData.txt
提取。
还有更多内容。
除了使用ctest --parallel N
,你还可以使用环境变量CTEST_PARALLEL_LEVEL
,并将其设置为所需的级别。
运行测试子集
本示例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-09
找到。本示例适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在前面的示例中,我们学习了如何借助 CMake 并行运行测试,并讨论了从最长的测试开始的优势。虽然这种策略可以最小化总测试时间,但在特定功能的代码开发或调试过程中,我们可能不希望运行整个测试集。我们可能更倾向于从最长的测试开始,特别是在调试由短测试执行的功能时。对于调试和代码开发,我们需要能够仅运行选定的测试子集。在本示例中,我们将介绍实现这一目标的策略。
准备工作
在本例中,我们假设总共有六个测试;前三个测试较短,名称分别为feature-a
、feature-b
和feature-c
。我们还有三个较长的测试,名称分别为feature-d
、benchmark-a
和benchmark-b
。在本例中,我们可以使用 Python 脚本来表示这些测试,其中我们可以调整睡眠时间:
import sys import time # wait for 0.1 seconds time.sleep(0.1) # finally report success sys.exit(0)
如何操作
以下是对我们的CMakeLists.txt
内容的详细分解:
- 我们从一个相对紧凑的
CMakeLists.txt
开始,定义了六个测试:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name project(recipe-09 LANGUAGES NONE) # detect python find_package(PythonInterp REQUIRED) # define tests enable_testing() add_test( NAME feature-a COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-a.py ) add_test( NAME feature-b COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-b.py ) add_test( NAME feature-c COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-c.py ) add_test( NAME feature-d COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-d.py ) add_test( NAME benchmark-a COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/benchmark-a.py )
add_test( NAME benchmark-b COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/benchmark-b.py )
- 此外,我们将较短的测试标记为
"quick"
,将较长的测试标记为"long"
:
set_tests_properties( feature-a feature-b feature-c PROPERTIES LABELS "quick" ) set_tests_properties( feature-d benchmark-a benchmark-b PROPERTIES LABELS "long" )
- 我们现在准备运行测试集,如下所示:
$ mkdir -p build $ cd build $ cmake .. $ ctest Start 1: feature-a 1/6 Test #1: feature-a ........................ Passed 0.11 sec Start 2: feature-b 2/6 Test #2: feature-b ........................ Passed 0.11 sec Start 3: feature-c 3/6 Test #3: feature-c ........................ Passed 0.11 sec Start 4: feature-d 4/6 Test #4: feature-d ........................ Passed 0.51 sec Start 5: benchmark-a 5/6 Test #5: benchmark-a ...................... Passed 0.51 sec Start 6: benchmark-b 6/6 Test #6: benchmark-b ...................... Passed 0.51 sec
100% tests passed, 0 tests failed out of 6 Label Time Summary: long = 1.54 sec*proc (3 tests) quick = 0.33 sec*proc (3 tests) Total Test time (real) = 1.87 sec
工作原理
现在每个测试都有一个名称和一个标签。在 CMake 中,所有测试都有编号,因此它们也具有唯一编号。定义了测试标签后,我们现在可以运行整个集合,也可以根据测试的名称(使用正则表达式)、标签或编号来运行测试。
通过名称运行测试(这里,我们运行所有名称匹配feature
的测试)如下所示:
$ ctest -R feature Start 1: feature-a 1/4 Test #1: feature-a ........................ Passed 0.11 sec Start 2: feature-b 2/4 Test #2: feature-b ........................ Passed 0.11 sec Start 3: feature-c 3/4 Test #3: feature-c ........................ Passed 0.11 sec Start 4: feature-d 4/4 Test #4: feature-d ........................ Passed 0.51 sec 100% tests passed, 0 tests failed out of 4
通过标签运行测试(这里,我们运行所有long
测试)产生:
$ ctest -L long Start 4: feature-d 1/3 Test #4: feature-d ........................ Passed 0.51 sec Start 5: benchmark-a 2/3 Test #5: benchmark-a ...................... Passed 0.51 sec Start 6: benchmark-b 3/3 Test #6: benchmark-b ...................... Passed 0.51 sec 100% tests passed, 0 tests failed out of 3
通过编号运行测试(这里,我们运行第 2 到第 4 个测试)得到:
$ ctest -I 2,4 Start 2: feature-b 1/3 Test #2: feature-b ........................ Passed 0.11 sec Start 3: feature-c 2/3 Test #3: feature-c ........................ Passed 0.11 sec Start 4: feature-d 3/3 Test #4: feature-d ........................ Passed 0.51 sec 100% tests passed, 0 tests failed out of 3
不仅如此
尝试使用**$ ctest --help**
,您将看到大量可供选择的选项来定制您的测试。
使用测试夹具
本例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-10
找到。本例适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
本例灵感来源于 Craig Scott 的工作,我们建议读者也参考相应的博客文章以获取更多背景信息,网址为crascit.com/2016/10/18/test-fixtures-with-cmake-ctest/
。本例的动机是展示如何使用测试夹具。对于需要测试前设置动作和测试后清理动作的更复杂的测试来说,这些夹具非常有用(例如创建示例数据库、设置连接、断开连接、清理测试数据库等)。我们希望确保运行需要设置或清理动作的测试时,这些步骤能以可预测和稳健的方式自动触发,而不会引入代码重复。这些设置和清理步骤可以委托给测试框架,如 Google Test 或 Catch2,但在这里,我们展示了如何在 CMake 级别实现测试夹具。
准备就绪
我们将准备四个小型 Python 脚本,并将它们放置在test
目录下:setup.py
、feature-a.py
、feature-b.py
和cleanup.py
。
如何操作
我们从熟悉的CMakeLists.txt
结构开始,并添加了一些额外的步骤,如下所示:
- 我们准备好了熟悉的基础设施:
# set minimum cmake version cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name project(recipe-10 LANGUAGES NONE) # detect python find_package(PythonInterp REQUIRED) # define tests enable_testing()
- 然后,我们定义了四个测试步骤并将它们与一个固定装置绑定:
add_test( NAME setup COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/setup.py ) set_tests_properties( setup PROPERTIES FIXTURES_SETUP my-fixture ) add_test( NAME feature-a COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-a.py ) add_test( NAME feature-b COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/feature-b.py ) set_tests_properties( feature-a feature-b PROPERTIES FIXTURES_REQUIRED my-fixture ) add_test( NAME cleanup COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/cleanup.py ) set_tests_properties( cleanup PROPERTIES FIXTURES_CLEANUP my-fixture )
- 运行整个集合并不会带来任何惊喜,正如以下输出所示:
$ mkdir -p build $ cd build $ cmake .. $ ctest Start 1: setup 1/4 Test #1: setup ............................ Passed 0.01 sec Start 2: feature-a 2/4 Test #2: feature-a ........................ Passed 0.01 sec Start 3: feature-b 3/4 Test #3: feature-b ........................ Passed 0.00 sec Start 4: cleanup 4/4 Test #4: cleanup .......................... Passed 0.01 sec 100% tests passed, 0 tests failed out of 4
- 然而,有趣的部分在于当我们尝试单独运行测试
feature-a
时。它正确地调用了setup
步骤和cleanup
步骤:
$ ctest -R feature-a Start 1: setup 1/3 Test #1: setup ............................ Passed 0.01 sec Start 2: feature-a
2/3 Test #2: feature-a ........................ Passed 0.00 sec Start 4: cleanup 3/3 Test #4: cleanup .......................... Passed 0.01 sec 100% tests passed, 0 tests failed out of 3
工作原理
在本例中,我们定义了一个文本固定装置并将其命名为my-fixture
。我们为设置测试赋予了FIXTURES_SETUP
属性,为清理测试赋予了FIXTURES_CLEANUP
属性,并且使用FIXTURES_REQUIRED
确保测试feature-a
和feature-b
都需要设置和清理步骤才能运行。将这些绑定在一起,确保我们始终以明确定义的状态进入和退出步骤。
还有更多内容
如需了解更多背景信息以及使用此技术进行固定装置的出色动机,请参阅crascit.com/2016/10/18/test-fixtures-with-cmake-ctest/
。
CMake 秘籍(三)(4)https://developer.aliyun.com/article/1524626