CMake 秘籍(三)(3)

简介: CMake 秘籍(三)

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,如果测试超时,需要终止测试,如下所示:

  1. 我们定义项目名称,启用测试,并定义测试:
# 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)
  1. 此外,我们为测试指定了一个TIMEOUT,并将其设置为 10 秒:
set_tests_properties(example PROPERTIES TIMEOUT 10)
  1. 我们知道如何配置和构建,我们期望测试通过:
$ 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
  1. 现在,为了验证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

如何操作

对于这个食谱,我们需要声明一个测试列表,如下所示:

  1. 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)
  1. 我们可以使用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
  1. 现在,如果我们碰巧有四个核心可用,我们可以在不到五秒的时间内将测试集运行在四个核心上:
$ 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, he同时开始。并行运行时总测试时间的减少可能是显著的。查看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-afeature-bfeature-c。我们还有三个较长的测试,名称分别为feature-dbenchmark-abenchmark-b。在本例中,我们可以使用 Python 脚本来表示这些测试,其中我们可以调整睡眠时间:

import sys
import time
# wait for 0.1 seconds
time.sleep(0.1)
# finally report success
sys.exit(0)

如何操作

以下是对我们的CMakeLists.txt内容的详细分解:

  1. 我们从一个相对紧凑的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
  )
  1. 此外,我们将较短的测试标记为"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"
  )
  1. 我们现在准备运行测试集,如下所示:
$ 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.pyfeature-a.pyfeature-b.pycleanup.py

如何操作

我们从熟悉的CMakeLists.txt结构开始,并添加了一些额外的步骤,如下所示:

  1. 我们准备好了熟悉的基础设施:
# 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()
  1. 然后,我们定义了四个测试步骤并将它们与一个固定装置绑定:
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
  )
  1. 运行整个集合并不会带来任何惊喜,正如以下输出所示:
$ 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
  1. 然而,有趣的部分在于当我们尝试单独运行测试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-afeature-b都需要设置和清理步骤才能运行。将这些绑定在一起,确保我们始终以明确定义的状态进入和退出步骤。

还有更多内容

如需了解更多背景信息以及使用此技术进行固定装置的出色动机,请参阅crascit.com/2016/10/18/test-fixtures-with-cmake-ctest/

CMake 秘籍(三)(4)https://developer.aliyun.com/article/1524626

相关文章
|
5月前
|
编译器 Shell 开发工具
CMake 秘籍(八)(5)
CMake 秘籍(八)
32 2
|
5月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
32 1
|
5月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
32 1
|
5月前
|
Linux C++ iOS开发
CMake 秘籍(三)(4)
CMake 秘籍(三)
31 1
|
5月前
|
Linux C++ iOS开发
CMake 秘籍(四)(1)
CMake 秘籍(四)
23 0
|
5月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(3)
CMake 秘籍(二)
26 0
|
5月前
|
编译器 测试技术 开发工具
CMake 秘籍(八)(4)
CMake 秘籍(八)
25 0
|
5月前
|
并行计算 Unix 编译器
CMake 秘籍(七)(5)
CMake 秘籍(七)
57 0
|
5月前
|
编译器 开发工具
CMake 秘籍(八)(2)
CMake 秘籍(八)
29 0
|
5月前
|
编译器 Linux C++
CMake 秘籍(六)(2)
CMake 秘籍(六)
64 0