# 4.8 并行测试

**NOTE**:*此示例代码可以在* <https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-08> *中找到。该示例在CMake 3.5版(或更高版本)中是有效的，并且已经在GNU/Linux、macOS和Windows上进行过测试。*

大多数现代计算机都有4个或更多个CPU核芯。CTest有个非常棒的特性，能够并行运行测试，如果您有多个可用的核。这可以减少测试的总时间，而减少总测试时间才是真正重要的，从而开发人员频繁地进行测试。本示例中，我们将演示这个特性，并讨论如何优化测试以获得最大的性能。

其他测试可以进行相应地表示，我们把这些测试脚本放在`CMakeLists.txt`同目录下面的test目录中。

## 准备工作

我们假设测试集包含标记为a, b，…，j的测试用例，每一个都有特定的持续时间:

| 测试用例       | 该单元的耗时 |
| ---------- | ------ |
| a, b, c, d | 0.5    |
| e, f, g    | 1.5    |
| h          | 2.5    |
| i          | 3.5    |
| j          | 4.5    |

时间单位可以是分钟，但是为了保持简单和简短，我们将使用秒。为简单起见，我们可以用Python脚本表示`test a`，它消耗0.5个时间单位:

```python
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)
   ```
2. 我们可以配置项目，使用`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
   ```
3. 现在，如果机器有4个内核可用，我们可以在不到5秒的时间内在4个内核上运行测试集:

   ```
   $ 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测试在4个核上的叠加比较，从最长的开始，如下图所示:

```
--> 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
```

按照定义测试的顺序运行测试，总的来说需要更多的时间，因为这会让2个核大部分时间处于空闲状态(这里的核3和核4)。CMake知道每个测试的时间成本，是因为我们先顺序运行了测试，将每个测试的成本数据记录在`test/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
```

如果在配置项目之后立即开始并行测试，它将按照定义测试的顺序运行测试，在4个核上的总测试时间明显会更长。这意味着什么呢？这意味着，我们应该减少的时间成本来安排测试？这是一种决策，但事实证明还有另一种方法，我们可以自己表示每次测试的时间成本:

```
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)
```

成本参数可以是一个估计值，也可以从`test/Temporary/CTestCostData.txt`中提取。

## 更多信息

除了使用`ctest --parallel N`，还可以使用环境变量`CTEST_PARALLEL_LEVEL`将其设置为所需的级别。
