第二十六章:测试

Posted by lili on

将项目构建的自然延续是测试它所创建的工件。CMake软件套件包括CTest工具,可用于自动化测试阶段,甚至整个配置、构建、测试甚至提交结果到仪表板的过程。本章首先涵盖了如何使用CMake定义测试并使用ctest命令行工具执行测试的简单情况。自动化整个配置-构建-测试过程使用了大部分相同的知识,并且在本章后面进行了讨论。

26.1. 定义和执行简单测试

在CMake项目中定义测试的第一步是在顶层CMakeLists.txt文件中的某个地方调用enable_testing()。这通常会在第一个project()调用之后很早就完成。这个函数的效果是指示CMake在CMAKE_CURRENT_BINARY_DIR中编写一个CTest输入文件,其中包含项目中定义的所有测试的详细信息(更准确地说,是在当前目录范围及其下定义的那些测试)。enable_testing()可以在子目录中调用而不会出错,但是如果在顶层没有调用enable_testing(),则不会在构建树的顶部创建CTest输入文件,而这是通常所期望的位置。

使用add_test()命令来定义单独的测试:

add_test(NAME testName
	COMMAND command [arg...]
	[CONFIGURATIONS config1 [config2...]]
	[WORKING_DIRECTORY dir]
	[COMMAND_EXPAND_LISTS] # CMake 3.16 or later
)

默认情况下,如果命令返回退出码0,则测试将被视为通过,但是支持更灵活的通过/失败处理,并在下一节中进行讨论。在CMake 3.19之前,testName不应包含任何空格、引号或其他特殊字符。在CMake 3.19或更高版本中,当策略CMP0110设置为NEW时,这些约束被移除。CMake 3.18.0最初引入了相同的功能,但在发现破坏了一些项目后,在3.18.1中将其撤销了。这个功能在3.19.0中再次引入,这次还附带了一个策略,以确保对依赖于旧行为的项目的向后兼容性。

该命令可以是可执行文件的完整路径,也可以是项目中定义的可执行文件目标的名称。当使用目标名称时,CMake会自动替换成真实的可执行文件路径。当使用多配置生成器(如Xcode、Visual Studio或Ninja Multi-Config)时,这尤其有用,因为可执行文件的位置将取决于配置。

cmake_minimum_required(VERSION 3.0)
project(CTestExample)
enable_testing()

add_executable(TestApp testapp.cpp)
add_test(NAME noArgs COMMAND TestApp)

目标自动替换为其真实位置的功能不适用于命令参数,只有命令本身支持这种替换。如果需要将目标位置作为命令行参数给出,可以使用生成器表达式。例如:

add_executable(App1 ...)
add_executable(App2 ...)
add_test(NAME WithArgs COMMAND App1 $<TARGET_FILE:App2>)

在运行测试时,用户可以指定应该测试哪个配置。当项目使用单一配置生成器时,配置不必与构建类型匹配。特别是,如果没有提供配置,则假定为空配置。如果没有使用可选的CONFIGURATIONS关键字,则将针对所有配置运行测试,而不管构建类型或用户请求的配置是什么。如果提供了CONFIGURATIONS关键字,则只会对列出的那些配置运行测试。注意,空配置仍然被视为有效,因此要在该场景下运行测试,必须将空字符串之一列入CONFIGURATIONS中。

例如,要添加一个只应在具有调试信息的配置中执行的测试,则可以列出Debug和RelWithDebInfo配置。同时添加空字符串也会使得在运行测试时未指定任何配置时运行该测试:

add_test(NAME DebugOnly
         COMMAND TestApp
         CONFIGURATIONS Debug RelWithDebInfo ""
)

在大多数情况下,不需要使用CONFIGURATIONS关键字,测试将对所有配置(包括空配置)进行执行。

默认情况下,测试将在CMAKE_CURRENT_BINARY_DIR目录中运行,但是可以使用WORKING_DIRECTORY选项将测试运行在其他位置。这种情况下可以发挥作用的一个示例是在不同目录中运行相同的可执行文件,以便获取不同的输入文件集,而无需将它们作为命令行参数进行指定。

add_test(NAME Foo
         COMMAND TestApp
         WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Foo)
add_test(NAME Bar
         COMMAND TestApp
         WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Bar)

如果要指定工作目录,请始终使用绝对路径。如果给出相对路径,它将被解释为相对于启动ctest本身的目录,但这可能不是构建树的顶部。为了确保工作目录可预测,项目应避免使用相对的WORKING_DIRECTORY。

如果在运行测试时指定的工作目录不存在,CMake版本3.11及更早版本将不会发出错误消息,并仍会运行测试,尽管它无法更改工作目录。CMake 3.12及更高版本将捕获错误并将测试视为失败。无论使用的CMake版本是什么,项目都有责任确保工作目录存在并具有适当的权限。

CMake 3.16增加了对COMMAND_EXPAND_LISTS关键字的支持,该关键字与add_custom_command()和add_custom_target()命令的同名选项具有相同的效果。当存在这个关键字时,任何作为命令名称或参数给出的列表都会被展开。这个列表展开发生在任何生成器表达式评估之后。这个特性的主要动机之一是避免在生成器表达式展开为空时将不需要的空字符串传递为命令参数。例如:

add_test(NAME Expander
    COMMAND someCommand $<$<CONFIG:Debug>:-g>
    COMMAND_EXPAND_LISTS
)

如果上述测试用于非调试配置,则COMMAND_EXPAND_LISTS确保在生成器表达式展开为空后,不会将空参数添加到命令行。

出于向后兼容性的原因,也支持add_test()命令的简化形式:

add_test(testName command [args...])

这种形式不应该在新项目中使用,因为它缺少完整的NAME和COMMAND形式的一些功能。主要的区别在于不支持生成器表达式,如果命令是目标的名称,CMake将不会自动替换其二进制文件的位置。

要运行测试,使用ctest命令行工具。通常应该从构建目录的顶部运行它,尽管在CMake 3.20或更高版本中,可以通过--test-dir命令行选项指定应该运行测试的目录。默认情况下,ctest将逐个执行所有定义的测试,并在每个测试开始和完成时记录状态消息,但隐藏所有测试输出。在最后将打印测试的总体摘要。典型的输出看起来可能是这样的:

Test project /path/to/build/dir
  Start 1: FooWithBar
1/2 Test #1: FooWithBar..............Passed 0.00 sec
  Start 2: FooWithoutBar
2/2 Test #2: FooWithoutBar...........Passed 0.00 sec

100% tests passed, 0 tests failed out of 2

Total Test time (real) = 0.02 sec

如果使用像Xcode、Visual Studio或Ninja Multi-Config这样的多配置生成器,需要告诉ctest要测试哪个配置。这可以通过包含-C configType选项来实现,其中configType将是支持的构建类型之一(如Debug、Release等)。对于单配置生成器,-C选项不是强制性的,因为构建只能产生一个配置,所以不会产生在哪里找到要执行的二进制文件的歧义。尽管如此,指定一个配置仍然很有用,以避免排除只在特定配置下运行的测试的不直观行为,并且空字符串不在列出的配置之中。

可以使用-V选项来指示ctest显示所有测试输出和关于运行的各种其他详细信息。-VV显示了更高级别的详细信息,但这通常只有在开发人员工作于ctest本身时才需要。即使是-V级别的详细信息通常也比用户想要看到的要多。更有可能的是,只需要显示失败测试的输出,这可以通过传递--output-on-failure选项来实现。另外,开发人员可以设置CTEST_OUTPUT_ON_FAILURE环境变量(值不会被使用,ctest只是检查是否设置了CTEST_OUTPUT_ON_FAILURE)。从CMake 3.18或更高版本开始,还可以通过使用--stop-on-failure选项来立即在遇到第一个错误时结束测试运行。

为了主要方便IDE应用程序,当启用测试时,CMake定义了一个自定义的构建目标,该目标调用ctest并传递了一组默认的参数。对于Xcode和Visual Studio生成器,这个目标将被称为RUN_TESTS,并且它将当前选择的构建类型作为配置传递给ctest。对于其他生成器,该目标简单地称为test,如果它是单配置生成器,那么在调用ctest时该目标不会指定任何配置。在CMake 3.16或更早版本中,没有设施来指定将执行哪些测试或传递给运行测试或测试构建目标的任何其他自定义选项。CMake 3.17引入了CMAKE_CTEST_ARGUMENTS变量,可以用于在该构建目标的ctest命令行前添加任意选项。

26.2. 测试环境

默认情况下,每个测试将使用与ctest命令相同的环境运行。如果一个测试需要对其环境进行更改,则可以通过ENVIRONMENT测试属性来实现。该属性预期是一个NAME=VALUE项目的列表,定义了在运行测试之前设置的环境变量。这些更改仅适用于该测试,不影响其他测试。

set_tests_properties(SomeTest PROPERTIES
    ENVIRONMENT "FOO=bar;HAVE_BAZ=1"
)

ENVIRONMENT测试属性工作方式的一个主要弱点是列表值变量存在问题。测试属性被写入另一个文件,后续由ctest读取。ctest将列表值中的分号解释为分隔符,用于分隔要设置的不同环境变量,而不是值的一部分。为了防止这种拆分,必须添加额外的转义级别。请注意以下示例中定义FOO值时的反斜杠: 如果在传递给set_tests_properties()之前在一个或多个变量中构建环境字符串,则可能需要进一步的转义。CMake的转义规则很复杂,这使得处理这些情况非常脆弱且容易出错。因此,尽量避免对具有列表值的变量使用ENVIRONMENT。

set_tests_properties(SomeTest PROPERTIES
    ENVIRONMENT "FOO=one\;two;HAVE_BAZ=1"
)

从CMake 3.22或更高版本开始,ENVIRONMENT_MODIFICATION测试属性提供了一种更可靠的修改环境变量的方法。与在配置时替换环境变量为一个确定值不同,ENVIRONMENT_MODIFICATION可以根据变量在ctest运行时的值应用一系列更改。更改以varName=operation:value的形式指定。支持多种操作,但其中一些更有用的操作包括string_append、string_prepend、path_list_append、path_list_prepend、cmake_list_append和cmake_list_prepend。string_…操作的行为符合预期,将新值追加和前置到变量的现有值中。cmake_list_…操作也是如此,除了它们还会在新值和现有值之间添加分号分隔符。path_list_…操作也是如此,不过分隔符将是主机平台用于类似PATH的环境变量的分隔符(即在Windows上是分号,在其他地方是冒号)。

# In this example, Algo is assumed to be a shared library defined elsewhere in
# the project and whose binary will be in a different directory to test_Algo
add_executable(test_Algo ...)
target_link_libraries(test_Algo PRIVATE Algo)
    add_test(NAME CheckAlgo COMMAND test_Algo)
    set_property(TEST CheckAlgo PROPERTY
    ENVIRONMENT_MODIFICATION
    SOME_VAR=string_append:ExtraPart
    QT_LOGGING_RULES=cmake_list_append:*.debug=true
)

if(WIN32)
    # Ensure the required DLLs can be found at runtime
    set(algoDir "$<SHELL_PATH:$<TARGET_FILE_DIR:Algo>>")
    set(otherDllDir "C:\\path\\to\\another\\dll")
    set_property(TEST CheckAlgo APPEND PROPERTY
        ENVIRONMENT_MODIFICATION
        PATH=path_list_prepend:${algoDir}
        PATH=path_list_prepend:${otherDllDir}
    )
endif()

通过ENVIRONMENT_MODIFICATION传递包含分号的值的相同脆弱性。然而,通过逐步应用值的变化一次性,通常可以避免分号,就像上面示例中的WIN32块中对PATH的修改一样。

对于需要修改而不是替换现有值的环境变量的情况,在CMake 3.21或更早版本中并不那么简单。如果环境应该基于运行CMake的环境而不是ctest命令的环境,则可以使用$ENV{SOMEVAR}形式获取现有值。再次强调,对可能包含分号的环境变量值需要额外小心。例如:

if(WIN32)
    set(algoDirset "$<SHELL_PATH:$<TARGET_FILE_DIR:Algo>>")
    set(execPath "PATH=${algoDir};$ENV{PATH}")
    
    # Add one level of escaping for any semicolons to prevent ctest from
    # treating them as separators between environment variable names instead
    # of as part of the PATH value.
    string(REPLACE ";" "\;" execPath "${execPath}")
        set_tests_properties(CheckAlgoENVIRONMENT "${execPath}"
    )
endif()

基于实际用于调用ctest的环境修改环境,而不是基于CMake的环境,会更加复杂。可以通过使用cmake -E env调用脚本的组合,并将CMake提供的位置作为变量传递给cmake -E env部分来实现。然后脚本使用这些值增强运行时环境,并调用测试可执行文件。这样的安排很复杂,可能很脆弱,除非确实需要支持CMake 3.21或更早版本的这种用例,否则应该避免使用。

26.3. 通过/失败标准和其他结果类型

仅基于测试命令的退出代码来确定测试结果可能会非常限制性。另一种支持的替代方案是指定用于匹配测试输出的正则表达式。

PASS_REGULAR_EXPRESSION测试属性可用于指定一组正则表达式,其中至少有一个必须与测试输出匹配才能使测试通过。这些正则表达式通常跨越多行。类似地,FAIL_REGULAR_EXPRESSION测试属性可以设置为一组正则表达式。如果其中任何一个与测试输出匹配,则测试失败,即使输出也与PASS_REGULAR_EXPRESSION匹配或退出代码为0。一个测试可以同时设置PASS_REGULAR_EXPRESSION和FAIL_REGULAR_EXPRESSION,两者之一或两者都不设置。如果设置了PASS_REGULAR_EXPRESSION并且不为空,则在确定测试通过或失败时不考虑退出代码。

# Ignore exit code, check output to determine the pass/fail status
set_tests_properties(test_Foo PROPERTIES
    FAIL_REGULAR_EXPRESSION "warning|Warning|WARNING"
    PASS_REGULAR_EXPRESSION [[
Checking some condition for test_Foo: passed
+.*
All checks passed]]
)

有时,一个测试可能需要跳过,可能是由于只有测试本身才能确定的原因。SKIP_RETURN_CODE测试属性可以设置为一个值,该值表示测试跳过而不是失败。退出时带有SKIP_RETURN_CODE的测试将覆盖任何其他通过/失败标准。

CMakeLists.txt

add_executable(test_Foo test_Foo.cpp ...)
add_test(NAME Foo COMMAND test_Foo)

set_tests_properties(Foo PROPERTIES
    SKIP_RETURN_CODE 2
)

test_Foo.cpp

int main(int argc, char* argv[])
{
    if (shouldSkip())
        return 2; // Skipped
    if (runTest())
        return 0; // Passed
}

上述测试的输出可能类似于以下内容:

Test project /path/to/build/dir
    Start 1: Foo
1/1 Test #1: Foo ....................***Skipped 0.00 sec

100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.01 sec
The following tests did not run:
    1 - Foo (Skipped)

从CMake 3.16开始,还可以指定SKIP_REGULAR_EXPRESSION。它与PASS_REGULAR_EXPRESSION和FAIL_REGULAR_EXPRESSION的工作方式完全相同,如果输出与任何跳过表达式匹配,则测试将被跳过。跳过正则表达式也优先于任何通过或失败标准。

当至少有一个测试失败或由于某种原因未运行时,将在最后打印所有这些测试及其状态的摘要。通过其返回代码指示应该跳过的测试仍然计入总测试数。在CMake 3.9或更高版本中,这些跳过的测试不被视为失败,但在CMake 3.8及更早版本中被视为失败。无论CMake版本如何,一个测试也可能因其他原因而被跳过,这些原因可能被视为失败,例如测试依赖项未满足(在下面的“测试依赖项”第26.7节中讨论)。

从CMake 3.9或更高版本开始,还支持DISABLED测试属性。这可用于将测试标记为临时禁用,允许定义该测试,但不执行或计入总测试数。它不会被视为测试失败,但会显示适当的状态消息在测试结果中显示。请注意,这些测试通常不应长时间保持禁用状态。该功能旨在作为暂时禁用有问题或不完整测试的方法,直到修复为止。以下示例演示了该行为:

add_test(NAME FooWithBar ...)
add_test(NAME FooWithoutBar ...)
set_tests_properties(FooWithoutBar PROPERTIES DISABLED YES)

上述测试的ctest输出可能如下所示:

Test project
 /path/to/build/dir
    Start 1: FooWithBar
1/2 Test #1: FooWithBar .............. Passed 0.00 sec
    Start 2: FooWithoutBar
2/2 Test #2: FooWithoutBar ...........***Not Run (Disabled) 0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) = 0.01 sec

The following tests did not run:
    2 - FooWithoutBar (Disabled)

在某些情况下,一个测试可能预期失败。与禁用测试相比,将测试标记为期望失败可能更合适,以便继续执行。WILL_FAIL测试属性可以设置为true来指示此情况,然后将反转通过/失败结果。这有一个额外的优点,即如果测试意外地开始通过,ctest将认为这是一个失败,并且开发人员立即意识到行为的变化。

测试通过/失败状态的另一个方面是完成测试所需的时间。如果设置了TIMEOUT测试属性,则指定了测试允许运行的秒数,超过该时间将被终止并标记为失败。ctest命令行还接受一个--timeout选项,对于没有设置TIMEOUT属性的任何测试,该选项具有相同效果(即作为默认超时时间)。此外,也可以通过将--stop-time选项指定给ctest,将整个测试集的时间限制应用到整个测试集上。--stop-time后面的参数必须是一天中的真实时间,而不是秒数,如果未给出时区,则假定为本地时间。

add_test(NAME t1 COMMAND ...)
add_test(NAME t2 COMMAND ...)
set_tests_properties(t2 PROPERTIES TIMEOUT 10)
ctest --timeout 30 --stop-time 13:00

在上面的示例中,通过ctest命令行设置了每个测试的默认超时时间为30秒。由于t1没有设置TIMEOUT属性,因此它将具有30秒的超时时间,而t2的TIMEOUT属性设置为10,这将覆盖ctest命令行上设置的默认值。测试将有直到当地时间下午1点的时间来完成。

在某些情况下,测试可能需要等待特定条件发生才能开始真正的测试。可能希望仅对在满足该条件并开始真正测试后的部分应用超时。从CMake 3.6或更高版本开始,可以使用TIMEOUT_AFTER_MATCH测试属性来支持此行为。它预期一个包含两个项目的列表,第一个项目是在满足条件后应用的超时秒数,第二个项目是要与测试输出进行匹配的正则表达式。当找到正则表达式时,测试的超时倒计时和开始时间将被重置,并且超时值将设置为列表的第一个项目。

例如,以下示例将对测试应用总体超时时间为30秒,但一旦测试输出中出现字符串“Condition met”,则测试将有10秒的时间从那时开始完成,原始的30秒超时条件将不再适用:

set_tests_properties(t2 PROPERTIES
    TIMEOUT 30
    TIMEOUT_AFTER_MATCH "10;Condition met"
)

如果测试花费25秒来满足条件,那么测试的总时间可能长达35秒,但由于测试的开始时间也被重置,ctest将报告0到10秒之间的时间(即不计算满足条件所需的时间)。另一方面,如果在30秒内未满足条件,则测试将显示约30秒的总测试时间。

上述情况的报告时间可能有些令人困惑。因此,在可能的情况下,通常应避免使用TIMEOUT_AFTER_MATCH,而应选择其他处理前提条件的方法。下面的“测试依赖项”第26.7节和“并行执行”第26.5节将进一步讨论更好的替代方法。

26.4. 测试分组和选择

在较大的项目中,经常希望仅运行所有已定义测试的子集。开发人员可能专注于特定失败的测试,并且在解决该问题时可能对其他所有测试都不感兴趣。CMake提供了几种不同的方法来缩小要运行的测试集。

26.4.1. 正则表达式

执行特定子集测试的一种方法是向ctest提供 -R 和 -E 选项。这些选项分别指定要与测试名称进行匹配的正则表达式。 -R 选项选择要包含在测试集中的测试,而 -E 排除测试。可以同时指定这两个选项以结合它们的效果。

add_test(NAME FooOnly COMMAND ...)
add_test(NAME BarOnly COMMAND ...)
add_test(NAME FooWithBar COMMAND ...)
add_test(NAME FooSpecial COMMAND ...)
add_test(NAME Other_Foo COMMAND ...)
ctest -R Only # Run just FooOnly and BarOnly
ctest -E Bar  # Run all but FooWithBar
ctest -R '^Foo' -E FooSpecial # Run all tests starting with Foo except FooSpecial
ctest -R 'FooSpecial|Other_Foo' # Run only FooSpecial and Other_Foo

有时,很难编写一个正则表达式来捕获所需的测试,或者开发人员可能只想查看已定义的所有测试而不运行它们。 -N 选项指示 ctest 仅打印测试而不运行它们,这可以是检查正则表达式是否产生所需测试集的有效方式。

ctest -N
Test project /path/to/build/dir
    Test #1: FooOnly
    Test #2: BarOnly
    Test #3: FooWithBar
    Test #4: FooSpecial
    Test #5: Other_Foo
    
Total Tests: 5
ctest -N -R 'FooSpecial|Other_Foo'
Test project /path/to/build/dir
    Test #4: FooSpecial
    Test #5: Other_Foo
    
Total Tests: 2

26.4.2. 测试编号

每个测试被添加时都会被赋予一个测试编号。除非在项目中添加或删除另一个测试,否则该编号将在运行之间保持不变。 ctest 输出在测试旁边显示该编号。使用 -N 选项时,测试按照项目定义的顺序列出,但测试可能不一定按照该顺序执行。可以使用 -I 选项按测试编号而不是名称选择要运行的测试。这种方法相当脆弱,因为添加或删除单个测试可以更改分配给任意数量其他测试的编号。甚至通过 -C 选项向 ctest 传递不同的配置也可能导致测试编号发生变化。在大多数情况下,按名称匹配将更可取。

测试编号可能有用的一种情况是两个测试具有完全相同的名称。除非在同一目录中定义,否则两个测试都会被接受而不会发出任何警告。尽管通常应避免重复的测试名称,在涉及外部提供的测试的分层项目中,这可能并不总是可行。

-I 选项需要一个参数,其形式有些复杂。最直接的形式涉及在命令行上指定测试编号,用逗号分隔,不留空格:

ctest -I [start[,end[,stride[,testNum[,testNum...]]]]]

要仅指定单个测试编号,可以将起始、结束和步长留空,如下所示:

ctest -I ,,,3,2 # Selects tests 2 and 3 only

可以通过将文件名指定为 -I 选项的参数,而不是在命令行上指定这些参数,来从文件中读取相同的详细信息。如果经常运行相同复杂的测试集,并且没有添加或删除测试,则这可能很有用:

testNumbers.txt

,,,3,2
ctest -I testNumbers.txt

26.4.3. 标签

如果需要执行大量相关测试的大型测试集,则通过名称或编号逐个选择测试可能变得繁琐。可以使用 LABELS 测试属性为测试分配任意的标签列表,然后可以按这些标签选择测试。 -L 和 -LE 选项类似于 -R 和 -E 选项,但它们操作的是测试标签而不是测试名称。继续使用前面示例中定义的相同测试:

set_tests_properties(FooOnly PROPERTIES LABELS "Foo")
set_tests_properties(BarOnly PROPERTIES LABELS "Bar")
set_tests_properties(FooWithBar PROPERTIES LABELS "Foo;Bar;Multi")
set_tests_properties(FooSpecial PROPERTIES LABELS "Foo")
set_tests_properties(Other_Foo PROPERTIES LABELS "Foo")
ctest -L Bar

Test project /path/to/build/dir
    Start 2: BarOnly
1/2 Test #2: BarOnly ................. Passed 1.52 sec
    Start 3: FooWithBar
2/2 Test #3: FooWithBar .............. Passed 1.02 sec


100% tests passed, 0 tests failed out of 2

Label Time Summary:
Bar = 2.53 sec*proc (2 tests)
Foo = 1.02 sec*proc (1 test)
Multi = 1.02 sec*proc (1 test)

Total Test time (real) = 2.54 sec

标签不仅为测试执行提供方便的分组,还为基本执行时间统计提供分组。如上面示例输出所示,当执行的测试集中的任何测试设置了其 LABELS 属性时, ctest 命令会打印一个标签摘要。这使开发人员可以了解每个标签组如何为整体测试时间做出贡献。 sec*proc 单位中的 proc 部分指的是分配给测试的处理器数量(在下面的“并行执行”第26.5节中描述)。运行时间为3秒并且需要4个处理器的测试将报告一个值为12的结果。可以使用 --no-label-summary 选项来抑制标签时间摘要。

从CMake 3.22或更高版本开始,还可以在测试运行时动态添加标签。有关如何执行此操作的讨论,请参阅“测试测量和结果”第26.10.4节。

26.4.4. 重复测试

另一个常见的需求是仅重新运行上次 ctest 运行时失败的那些测试。这可以是在进行小修复后重新检查相关测试的便捷方式,或者重新运行由于某些临时环境条件导致失败的测试。 ctest 命令支持一个 --rerun-failed 选项,它提供了这种行为,而无需提供任何测试名称、编号或标签。

有时特定的测试或一组测试只是偶尔失败,因此可能需要多次运行测试来尝试复现失败。与反复运行 ctest 不同,可以使用 --repeat-until-fail 选项设置每个测试可以重复运行的最大次数。如果测试失败,它将不会在该 ctest 调用中再次运行。

ctest -L Bar --repeat-until-fail 3

Test project /path/to/build/dir
    Start 2: BarOnly
    Test #2: BarOnly ................. Passed
    Start 2: BarOnly
    Test #2: BarOnly .................***Failed
    Start 3: FooWithBar
    Test #3: FooWithBar .............. Passed
    Start 3: FooWithBar
    Test #3: FooWithBar .............. Passed
    Start 3: FooWithBar
2/2 Test #3: FooWithBar .............. Passed

50% tests passed, 1 tests failed out of 2

Label Time Summary:
Bar = 1.02 sec*proc (2 tests)
Foo = 1.02 sec*proc (1 test)
Multi = 1.02 sec*proc (1 test)

Total Test time (real) = 4.59 sec

The following tests FAILED:
    2 - BarOnly (Failed)

Errors while running CTest

标签摘要不会累积重复测试的总时间,它只使用测试的最后一次执行时间。然而,总测试时间确实计算了所有重复测试的时间。

CMake 3.17 扩展了重复运行测试的能力,以覆盖更多情况。新增了一个 ctest 选项 --repeat mode:n,其中 n 是测试将运行的最大次数,mode 是以下模式之一:

  • until-fail:这对应于 --repeat-until-fail 选项,并提供了一致性。
  • until-pass:此模式会重复运行测试直到测试通过。通常情况下,测试应始终通过,但偶尔这个选项在开发过程中或者用于调查问题时可能会有用。但不应依赖它作为更永久的安排。
  • after-timeout:某些类型的测试可能偶尔由于外部环境因素而失败。例如,测试用例可能需要执行网络操作,有时可能需要的时间比预期长,甚至会无限期地阻塞。与互联网上的网站交互的测试尤其容易出现超时。这个选项可以用来允许在超时时重试此类测试。通常应该使用较低的 n 值,以避免重复超时显著延长总测试时间。

26.5 并行执行

对于大型项目或测试需要较长时间完成的情况,最大化测试吞吐量可能是一个重要的考虑因素。在ctest中运行测试的并行能力是一个关键特性,可以使用与标准make工具非常相似的命令行选项来启用。-j选项可以用于指定同时运行的测试的上限。与大多数make实现不同的是,必须提供一个值,否则该选项将不起作用。作为一种替代方案,可以使用CTEST_PARALLEL_LEVEL环境变量来指定作业数量,但是如果两者都使用,则命令行选项优先。这种安排对于持续集成构建特别有用,因为可以将CTEST_PARALLEL_LEVEL设置为每台机器上的CPU核心数,使每个项目无需计算最佳作业数量。对于需要限制并行作业数量的项目,仍然可以使用-j命令行选项覆盖CTEST_PARALLEL_LEVEL。

相关选项是-l,用于指定CPU负载的理想上限。ctest将尝试避免启动新测试,如果可能会导致负载超过此限制。不幸的是,此选项的缺点在测试开始时立即显现出来。通常情况下,ctest最初会启动与-j或CTEST_PARALLEL_LEVEL设置允许的作业限制一样多的测试,超过-l指定的任何限制。通常测量的CPU负载会有滞后,这使得ctest在负载增加之前会一次性启动太多的测试。为了防止这种情况发生,由-j或CTEST_PARALLEL_LEVEL指定的并行作业数量应设置为不超过-l所施加的限制。如果既未设置-j也未设置CTEST_PARALLEL_LEVEL,则-l选项将不起作用。

尽管存在这些限制,-l选项仍然可以在帮助减少共享系统上的CPU超载方面发挥作用,因为其他进程可能也在竞争CPU资源。

默认情况下,ctest将假定每个测试消耗一个CPU。对于使用多个CPU的测试用例,可以将它们的PROCESSORS测试属性设置为指示它们预计将使用多少个CPU。然后,ctest在确定是否有足够的CPU资源空闲以启动测试时将使用该值。如果PROCESSORS设置为比作业限制高的值,ctest在确定是否可以启动测试时将表现得像作业限制一样。

这些选项的效果可以在以下示例中看到,这些示例使用与之前定义的相同的测试集(为简洁起见,省略了测试摘要)。

ctest -j 5
Test project /path/to/build/dir
Start 5: Other_Foo
Start 2: BarOnly
Start 3: FooWithBar
Start 1: FooOnly
Start 4: FooSpecial
1/5 Test #4:
 FooSpecial .............. Passed 0.12 sec
2/5 Test #1:
 FooOnly ................. Passed 0.52 sec
3/5 Test #3:
 FooWithBar .............. Passed 1.01 sec
4/5 Test #2:
 BarOnly ................. Passed 1.52 sec
5/5 Test #5:
 Other_Foo ............... Passed 2.02 sec

五个测试被定义,并且作业限制在命令行上设置为5,因此ctest能够立即启动所有测试。每个测试完成时都会记录其结果,而不是按照它们启动的顺序。如果将作业限制减少到2,则输出可能更像以下内容:

ctest -j 2
Test project /path/to/build/dir
Start 5: Other_Foo
Start 2: BarOnly
1/5 Test #2: BarOnly ................. Passed 1.52 sec
Start 3: FooWithBar
2/5 Test #5: Other_Foo ............... Passed 2.01 sec
Start 1: FooOnly
3/5 Test #1: FooOnly ................. Passed 0.52 sec
Start 4: FooSpecial
4/5 Test #3: FooWithBar .............. Passed 1.02 sec
5/5 Test #4: FooSpecial .............. Passed 0.12 sec

对于大量测试和高作业限制,记录每个单独测试的开始和完成可能会难以跟踪。因此,运行结束时的整体测试摘要变得更加重要,其中列出了未通过的每个测试及其结果。在CMake 3.13中添加的--progress选项也可以帮助减少输出并专注于重要细节。它将开始和完成进度消息折叠成单行,类似于Ninja构建工具的输出。

26.6 测试资源管理

测试有时需要确保没有其他测试与它们并行运行。它们可能正在执行对计算机上其他活动敏感的操作,或者可能会创建干扰其他测试的条件。为了强制执行这种约束,可以将测试的RUN_SERIAL属性设置为true。

RUN_SERIAL是一种严格的约束,可能会对测试吞吐量产生较大影响。RESOURCE_LOCK测试属性通常是一个更好的替代方案。它提供了测试需要独占访问的资源列表。这些资源是任意字符串,ctest不会以任何方式解释这些字符串,除了确保没有其他测试的RESOURCE_LOCK属性中列出这些资源的测试会同时运行。这是一种很好的方法,可以将需要独占访问某些资源(例如数据库、共享内存)的测试进行序列化,而不会阻塞不使用该资源的测试。

set_tests_properties(FooOnly FooSpecial Other_Foo PROPERTIES
    RESOURCE_LOCK Foo
)
set_tests_properties(BarOnly PROPERTIES
    RESOURCE_LOCK Bar
)
set_tests_properties(FooWithBar PROPERTIES
    RESOURCE_LOCK "Foo;Bar"
)

以下是样本输出(测试总结被省略),显示尽管作业限制为5可以同时执行所有测试,ctest还是延迟启动一些测试,直到它们需要的资源可用为止。

ctest -j 5
Test project /path/to/build/dir
Start 5: Other_Foo
Start 2: BarOnly
1/5 Test #2: BarOnly ................. Passed 1.52 sec
2/5 Test #5: Other_Foo ............... Passed 2.02 sec
Start 3: FooWithBar
3/5 Test #3: FooWithBar .............. Passed 1.01 sec
Start 1: FooOnly
4/5 Test #1: FooOnly ................. Passed 0.52 sec
Start 4: FooSpecial
5/5 Test #4: FooSpecial .............. Passed 0.12 sec

RESOURCE_LOCK测试属性非常适合当测试需要对某些资源进行独占访问时,但是当需要对测试资源进行更细粒度的控制时,CMake 3.16或更高版本提供的RESOURCE_GROUPS测试属性可能更合适。资源组使项目能够定义测试所需的资源以及每种资源需要多少量。这样可以对资源进行受控共享,并且对于各种有趣的情景都很有用:

  • 需要大量内存的测试可以指定它们需要多少内存。ctest调度器将跟踪它分配给当前运行的所有测试的内存资源,并确保不超过可用的内存资源,延迟测试执行直到满足该测试的内存需求为止。
  • 多个测试可能需要访问GPU,但只要它们共享给定GPU的数量较少,它们可能会与其他作业共享GPU。最终用户的计算机可能只有一个GPU,也可能安装了多个GPU。项目只需要定义每个测试需要GPU的数量,并且ctest调度器会在运行时将测试分配到可用的GPU上。
  • 可能有许多测试都需要与特定服务通信。为了防止该服务被过度访问,可以对与其通信的测试数量进行限制。对于每个这样的测试,项目指定该测试需要代表该服务的资源。运行测试套件的用户控制与服务同时通信的测试数量的上限。

与RESOURCE_LOCK相比,设置资源组需要进行更多的工作,需要进行一些单独的步骤:

  • 定义测试运行所需的资源。这必须由项目进行。
  • 定义系统上可用的资源。可以由运行测试的用户或通过调用CDash运行的脚本(在第26.10节“CDash集成”中讨论)来完成。
  • 编写测试以利用ctest通过环境变量传递给它的资源详细信息。

26.6.1 定义测试资源需求

在测试中,其所需资源被定义为资源组的列表。每个组由一个或多个名称:值对组成,多个对之间用逗号分隔。对于每对,名称部分被称为资源类型。在以下示例中,该组指定了对16个mem_gb单位和4个cpu的需求:

mem_gb:16,cpus:4

组前面还可以加上需要整个组的数量的计数。计数由整数后跟逗号,然后是上述定义的组定义。当计数值被省略时,如上面的示例中所示,默认为1。以下演示了如何指定测试需要3组资源,每组需要2个gpu和4个工作者:

3,gpus:2,workers:4

一个组还可以多次列出特定的资源类型。考虑以下示例:

gpus:2,gpus:4

上述组需要总共6个gpu,但它们可以分配给两个独立的gpu资源类型实例(实例将在下一节讨论)。两个gpu可以来自一个实例,另外四个来自另一个实例。如果一个实例有足够的插槽,它们也可以全部来自一个实例,总共6个gpu。

一个测试可能需要多个资源组集合,其中组集合并不完全相同。RESOURCE_GROUPS属性接受一个列表,用于确切此目的。例如:

set_property(TEST ParallelCoordinator PROPERTY
    RESOURCE_GROUPS
        producers:1,consumers:1
        producers:1,consumers:4
        producers:4,consumers:1
        4,producers:1,consumers:1
)

上述规范使得ParallelCoordinator测试需要总共七个资源组。除非有足够的资源一次性满足所有七个组的需求,否则测试将不会被执行。

能够将总体资源需求分配到多个组中是支持某些系统资源配置的关键。接下来的部分将讨论利用这种能力的方案。

26.6.2 指定系统可用的资源

系统资源被指定在一个JSON文件中,该文件通过以下方式之一传递给ctest(按优先顺序列出):

  • 在CDash脚本中的ctest_test()调用中使用RESOURCE_SPEC_FILE关键字(参见第26.10节“CDash集成”)。
  • 在CDash脚本中或者在运行CDash脚本时作为ctest -D命令行选项设置CTEST_RESOURCE_SPEC_FILE变量。该变量作为调用ctest_test()时RESOURCE_SPEC_FILE关键字的默认值,但只支持CMake 3.18或更高版本。
  • 提供--resource-spec-file命令行选项给ctest。
  • 将CTEST_RESOURCE_SPEC_FILE设置为CMake变量(只支持CMake 3.18或更高版本)。为了确保用户始终具有控制权,这个变量应该只在使用cmake -D命令行选项设置,而不是直接在项目中硬编码。

这个JSON文件的格式可以理解为如下(在CMake文档的ctest命令中可以找到更正式的描述):

{
    "version": { "major": 1, "minor": 0 },
    "local": [
        {
            "resource1": [ ... ],
            "resource2": [ ... ],
            ...
        }
    ]
}

version对象必须存在于顶层,它必须包含主要和次要两个元素。

CMake 3.16中资源分配功能的第一个版本要求主要版本为1,次要版本为0。未来的版本可能支持其他版本组合。

其他顶级JSON元素必须命名为local,并且它必须是一个包含一个元素的数组(未来的CMake版本可能允许更多)。该数组元素是一个JSON对象,定义了测试将在其上运行的系统提供的每个资源类型。每个资源类型的名称必须全部小写,可以包含数字和下划线,但不能以数字开头。每个资源类型被指定为以下格式的数组项:

{ "id": "name", "slots": numericValue }

其中id是用于识别此特定资源实例的名称。该名称必须在此资源类型的所有实例中唯一,并且只能包含小写字母、数字或下划线。名称不需要以字母或下划线开头,如果需要,也可以只包含数字。slots指定此实例提供的资源量,并且必须以整数形式给出。

示例资源规范文件:

{
    "version": { "major": 1, "minor": 0 },
    "local": [
        {
            "mem_gb": [
                    { "id": "pool_0", "slots": 64 }
                    ],
            "gpus": [
                    { "id": "0", "slots": 2 },
                    { "id": "1", "slots": 2 }
                    ],
            "workers": [
                    { "id": "0", "slots": 8 },
                    { "id": "1", "slots": 4 }
                    ]
        }
    ]
}

上面的示例演示了id只需要在同一资源类型中是唯一的。gpus和workers都有id为0和1,但这没问题,因为它们是不同的资源类型。

由于在slots中给定的值不能指定单位,因此在资源类型的标签中包含单位可能是明智的选择,其中值的含义需要某种单位。在上面的示例中,mem_gb资源类型的名称清楚地表明slots应该被解释为千兆字节。对于gpus和workers,不需要单位,因为已经清楚slots值是这些资源的数量。对于不需要单位的资源类型名称,约定是名称通常应该是复数形式的。

当ctest尝试从可用资源池中满足测试的资源需求时,它不会合并所有测试的资源组。相反,它会逐个迭代资源组,尝试单独满足每个组的需求。对于每个组,每个名称:值对都会被评估。ctest将查找资源类型的系统分配情况,并尝试找到资源类型数组中有足够未分配插槽的项目,以满足该资源需求。重要的是,ctest不会合并多个数组元素的插槽,试图满足一个名称:值对的资源需求。

一个例子有助于展示分配逻辑。考虑上面给出的资源规范文件和一个定义为gpus:4的资源组。系统中gpus资源类型有4个插槽,但它们分布在两个具有id 0和1的单独项目中。因为ctest只能从资源类型数组的单个元素中满足一个名称:值对的资源需求,所以无法满足这个要求,测试将无法运行。相反,如果一个测试有一个资源组定义为4,gpus:1,它需要4个单独的组,每个组需要一个gpus插槽。这可以被满足,甚至两个组甚至可以共享一个数组项(例如,两个组可以共享id 0的资源,另外两个组可以共享资源id 1)。一个定义为gpus:1,gpus:1的组可以以三种不同的方式被满足。它可以从id 0接收两个插槽,也可以从id 1接收两个插槽,或者每个插槽从每个资源中接收一个插槽。

26.6.3. 使用为测试分配的资源

测试通过一些环境变量接收分配给它的资源信息。其中最基本的是CTEST_RESOURCE_GROUP_COUNT,它保存了测试指定的资源组总数。如果未定义此环境变量,则意味着在调用ctest时未提供资源规范文件。此时由测试决定在这种情况下该做什么。如果测试不能在没有提供资源分配的情况下运行,则测试应该失败或者指示其已被跳过(例如,通过与SKIP_RETURN_CODE测试属性匹配的返回代码或与SKIP_REGULAR_EXPRESSION匹配的输出)。 对于每个资源组,将有一组环境变量遵循CTEST_RESOURCE_GROUP_<num>模式,其中包含为该组分配的资源类型列表。为了确切地查看分配了给定资源类型的哪些资源,必须查看另一组环境变量的形式为CTEST_RESOURCE_GROUP_<num>_<resourceType>。其中<resourceType>将是资源类型的大写名称。这些变量的内容将是一个包含一项或多项id:X,slots:Y形式的列表,可以读作”来自id X的Y个插槽”。

为了说明,前一节的一个示例指定了一个资源组集合为4,gpus:1。这可能会导致测试接收到以下类似的环境变量集合:

CTEST_RESOURCE_GROUP_COUNT=4
CTEST_RESOURCE_GROUP_0=gpus
CTEST_RESOURCE_GROUP_1=gpus
CTEST_RESOURCE_GROUP_2=gpus
CTEST_RESOURCE_GROUP_3=gpus
CTEST_RESOURCE_GROUP_0_GPUS=id:0,slots:1
CTEST_RESOURCE_GROUP_1_GPUS=id:0,slots:1
CTEST_RESOURCE_GROUP_2_GPUS=id:1,slots:1
CTEST_RESOURCE_GROUP_3_GPUS=id:1,slots:1

另一个例子是,考虑一个测试有一个资源组的情况,其中该组定义为gpus:2,gpus:2,workers:4。它可能收到的一组环境变量如下所示:

CTEST_RESOURCE_GROUP_COUNT=1
CTEST_RESOURCE_GROUP_0=gpus,workers
CTEST_RESOURCE_GROUP_0_GPUS=id:0,slots:2;id:1,slots:2
CTEST_RESOURCE_GROUP_0_WORKERS=id:0,slots:4

注意,在CTEST_RESOURCE_GROUP_0_GPUS中返回了两个列表项,因为资源组两次列出了gpus:2。

测试如何使用通过环境变量提供的信息取决于测试本身,但至少应该始终确认CTEST_RESOURCE_GROUP_COUNT是否已定义。

26.7. 测试依赖关系

测试可以用来执行更多的任务,而不仅仅是验证特定条件,它们还可以用来强制执行这些条件。例如,一个测试可能需要连接到服务器以便验证客户端实现。与其依赖开发者确保这样的服务器可用性,不如创建另一个测试用例来确保服务器正在运行。然后客户端测试需要在某种程度上依赖服务器测试,以确保它们按正确的顺序运行。

DEPENDS测试属性允许表达这种约束的一种形式,通过维护一个其他测试的列表,这些测试必须在该测试运行之前完成。上述客户端/服务器示例可以松散地表达如下:

set_tests_properties(ClientTest1 ClientTest2
    PROPERTIES DEPENDS StartServer
)
set_tests_properties(StopServer
    PROPERTIES DEPENDS "ClientTest1;ClientTest2"
)

DEPENDS test属性的一个弱点在于,虽然它定义了测试的顺序,但并未考虑先决条件测试是否通过或失败。在上面的示例中,如果StartServer测试用例失败,则ClientTest1、ClientTest2和StopServer测试仍然会运行。这些测试然后很可能会失败,并且测试输出将显示所有四个测试均为失败,实际上只有StartServer测试失败,其他测试应该被跳过。

CMake 3.7添加了对测试固件(fixtures)的支持,这是一个允许测试之间依赖关系更加严格表达的概念。一个测试可以通过在其FIXTURES_REQUIRED测试属性中列出该固件名称来指示它需要特定的固件。任何具有相同固件名称的其他测试在其FIXTURES_SETUP测试属性中列出时必须成功完成,依赖测试将在之后启动。如果任何一个固件的设置测试失败,那么需要该固件的所有测试将被标记为跳过。类似地,一个测试可以在其FIXTURES_CLEANUP测试属性中列出一个固件,以表明它必须在任何具有相同固件列在其FIXTURES_SETUP或FIXTURES_REQUIRED属性的其他测试之后运行。这些清理测试不需要设置或依赖测试通过,因为即使早期测试失败,也可能需要清理。

这三个与固件相关的测试属性都接受一个固件名称列表。这些名称是任意的,并且不必与测试名称、它们使用的资源或任何其他属性相关。固件名称应该清楚地告诉开发人员它们表示什么,因此,虽然没有必要,但它们通常与用于RESOURCE_LOCK属性的值相同。

考虑之前的客户端/服务器示例。这可以使用固件属性严格表达如下:

set_tests_properties(StartServer
    PROPERTIES FIXTURES_SETUP Server
)
set_tests_properties(ClientTest1 ClientTest2
    PROPERTIES FIXTURES_REQUIRED Server
)
set_tests_properties(StopServer
    PROPERTIES FIXTURES_CLEANUP Server
)

在上述示例中,Server是固件的名称,只有在StartServer通过时,ClientTest1和ClientTest2才会运行,而StopServer将无论其他三个测试的结果如何都会在最后运行。如果启用了并行执行,则StartServer将首先运行,两个客户端测试将同时运行,并且只有在两个客户端测试完成或跳过后,StopServer才会运行。

固件的另一个好处可以在开发人员仅运行测试子集时看到。考虑到开发人员正在开发ClientTest2,并且不打算运行ClientTest1的情况。当使用DEPENDS表达测试之间的依赖关系时,开发人员需要负责确保他们还包括测试集中所需的测试,这意味着他们需要理解所有相关的依赖关系。这将导致ctest命令行:

ctest -R "StartServer|ClientTest2|StopServer"

当使用固件时,ctest会自动将任何设置或清理测试添加到要执行的测试集中,以满足固件的要求。这意味着开发人员只需指定他们想要关注的测试,并将依赖关系留给ctest:

ctest -R ClientTest2

当使用--rerun-failed选项时,这种相同的机制确保设置和清理测试被自动添加到测试集中,以满足先前失败测试的固件依赖关系。

一个固件可以有零个或多个设置测试和零个或多个清理测试。固件可以定义没有清理测试的设置测试,反之亦然。虽然没有特别的用处,但是一个固件可以完全没有设置或清理测试,这种情况下,该固件对要执行的测试或测试运行时间没有影响。同样,一个固件可以有与之相关的设置和/或清理测试,但没有需要它的测试。这些情况在开发过程中定义或暂时禁用测试时可能会出现。对于固件没有任何需要它的测试的情况,CMake 3.7中的一个bug允许固件的清理测试在设置测试之前运行,但该bug在3.8.0版本中修复了。

一个更复杂的例子演示了固件如何用于表达更复杂的测试依赖关系。扩展前面的例子,假设一个客户端测试仅需要一个服务器,而另一个客户端测试需要服务器和数据库都可用。这可以通过定义两个固件来简洁地表达:Server和Database。对于后者,只需检查是否有数据库可用并在没有时失败即可,因此Database固件不需要清理测试。Server和Database固件之间没有关联,因此它们之间不需要依赖关系。这些约束可以表达如下:

# Setup/cleanup
set_tests_properties(StartServer
    PROPERTIES FIXTURES_SETUP Server
)

set_tests_properties(StopServer
    PROPERTIES FIXTURES_CLEANUP Server
)

set_tests_properties(EnsureDbAvailable
    PROPERTIES FIXTURES_SETUP Database
)

# Client tests
set_tests_properties(ClientNoDb
    PROPERTIES FIXTURES_REQUIRED Server
)

set_tests_properties(ClientWithDb
    PROPERTIES FIXTURES_REQUIRED "Server;Database"
)

虽然使ctest自动将固件依赖关系添加到测试执行集中是一个有用的功能,但也有时可能是不希望的。继续上面的例子,开发人员可能希望保持服务器运行,并仅多次执行一个客户端测试。他们可能正在进行更改、重新编译代码,并在每次更改后检查客户端测试是否通过。为了支持这种程度的控制,CMake 3.9引入了-FS、-FC和-FA选项到ctest。每个选项需要一个正则表达式,将与固件名称匹配。-FS选项禁用为匹配提供的正则表达式的固件设置依赖关系。-FC对清理测试执行相同操作,-FA组合两者,禁用匹配的设置和清理测试。一个常见情况是完全禁用任何设置/清理依赖关系,这可以通过给出一个单独的句点(.)的正则表达式来实现。以下示例演示了各种固件控制选项及其效果:

26.8. 交叉编译与模拟器

当项目定义的可执行目标被用作add_test()命令的参数时,CMake会自动替换已构建可执行文件的位置。对于交叉编译的情况,这通常不起作用,因为主机通常不能直接运行为不同平台构建的二进制文件。

为了解决这个问题,CMake提供了CROSSCOMPILING_EMULATOR目标属性,可以设置为用于启动目标的脚本或可执行文件。CMake将这个属性添加到目标二进制文件之前,形成运行命令,因此真正的目标二进制文件将成为模拟器脚本或可执行文件的第一个参数。这样即使在交叉编译时也可以运行测试。在CMake 3.15或更高版本中,CROSSCOMPILING_EMULATOR可以是一个列表,允许在目标二进制文件之前包含参数。

CROSSCOMPILING_EMULATOR并不一定要是实际的模拟器,它只需要是一个可以在主机上运行的命令,用于启动目标可执行文件。虽然为目标平台专门设计的模拟器是明显的用例,但也可以将其设置为将可执行文件复制到目标机器并远程运行的脚本(例如通过SSH连接)。无论采用哪种方法,开发人员应该注意模拟器的启动时间或准备运行二进制文件的时间可能是非常大的,并可能会影响测试的时间测量。这可能意味着需要重新调整测试超时设置。

CROSSCOMPILING_EMULATOR目标属性的默认值来自CMAKE_CROSSCOMPILING_EMULATOR变量,这是指定模拟器详细信息的常用方式,而不是逐个设置每个目标的属性。该变量通常在工具链文件中设置,因为它影响类似于上述测试和自定义命令中描述的try_run()命令的内容。有关变量影响方面的更多讨论,请参阅第23.5节“编译器检查”。

即使不进行交叉编译,CMake仍然会遵循非空CROSSCOMPILING_EMULATOR目标属性,并将其添加到测试和执行该目标的自定义命令的命令行中。这可能非常有用,允许将该属性临时设置为启动脚本,以帮助调试或进行数据收集。不建议将此技术用作项目构建的永久特性,但在某些开发情况下可能会有用。

26.9. 构建和测试模式

ctest不仅可以执行一组测试,还可以驱动整个配置、构建和测试流程。

有两种主要的方法来实现这一点;一种是更基本的、独立的方式,另一种是与仪表板报告工具密切相关的更强大的方法。更基本的方法是使用带有--build-and-test命令行选项调用ctest工具,其形式如下:

ctest --build-and-test sourceDir buildDir
    --build-generator generator
    [options...]
    [--test-command testCommand [args...]]

在没有任何选项的情况下,上述命令将使用指定的sourceDir和binaryDir运行CMake,并使用指定的生成器。这三者都必须指定。如果CMake运行成功,ctest将构建clean目标,然后构建默认的all目标。要在构建步骤之后运行测试,命令行中的最后一个选项必须是--test-command,其关联的testCommand以及可选的一些参数。这可以是另一个调用ctest运行所有测试的示例,如下面的示例所示。

ctest --build-and-test sourceDir buildDir \
      --build-generator Ninja \
      --test-command ctest -j 4

上面的内容执行了完整的配置-清理-构建-测试流程。提供了各种选项,可以用于修改运行流程的哪些部分以及如何运行它们。例如,--build-nocmake和--build-noclean分别禁用了配置和清理步骤。--build-two-config选项将调用CMake两次,处理某些需要第二次CMake pass才能完全配置项目的特殊情况。当使用像Visual Studio这样的生成器时,可能需要使用--build-generator-platform和--build-generator-toolset指定额外的生成器细节,这些细节将作为-A和-T选项传递给CMake进行配置步骤。一些生成器(例如Xcode)可能需要给出项目名称,以便它可以找到由配置阶段生成的项目文件,这可以使用--build-project选项完成。在构建步骤中要构建的目标可以使用--build-target选项设置,而构建工具可以通过传递--build-makeprogram和备用工具来覆盖。

正如上文所示,与--build-and-test模式相关的所有选项都以--build开头。虽然大多数选项都有直观的名称,但通用的--build前缀可能会导致一些令人遗憾的混淆。存在一个名为--build-options的选项,可能最初看起来与构建步骤有关,但实际上是用于将命令行选项传递给cmake命令。它还有一个额外的约束,即它必须在命令行中处于最后的位置,除非还提供了--test-command,此时--build-options必须位于--test-command之前。以下示例应该可以澄清这些约束。它向cmake调用添加了两个缓存变量定义,并在构建步骤后运行了完整的测试套件。

ctest --build-and-test sourceDir buildDir \
      --build-generator Ninja \
      --build-options -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON \
      --test-command ctest -j 4

还有一些其他--build-…选项,但上面涵盖了最有用的选项。另一个应该提到的剩余选项是--test-timeout,它在测试命令被强制终止之前设置了一个时间限制(以秒为单位)。

使用单个ctest命令控制整个流程是否比显式调用每个阶段所需的工具更好或更差,这取决于具体情况。上文的最后一个示例可以在Unix上用以下等价的命令序列轻松完成:

mkdir -p buildDir
cd buildDir
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON sourceDir
cmake --build . --target clean
cmake --build .
ctest -j 4

逐个调用工具允许它们使用全套选项运行,而ctest --build-and-test方法只能非常有限地控制构建阶段。

一个特别方便使用构建和测试模式的情况是,项目需要在一侧执行完整的配置-构建-测试循环,与主要构建分开。由于整个循环可以由单个ctest调用控制,因此它可以作为调用add_test()的一部分,将基本的CMake项目添加到主项目的测试套件中的过程相对简单。CMake本身在其自己的测试套件中广泛使用ctest构建和测试模式,就是以这种方式进行的。

下面的示例展示了如何使用单独的构建来测试主项目构建的库提供的API:

add_library(Decoder foo.c bar.c)

add_test(NAME Decoder.api
COMMAND ${CMAKE_CTEST_COMMAND}
        --build-and-test ${CMAKE_CURRENT_LIST_DIR}/test_api
                         ${CMAKE_CURRENT_BINARY_DIR}/test_api
        --build-generator ${CMAKE_GENERATOR}
        --build-options -DDECODER_LIB=$<TARGET_FILE:Decoder>
        --test-command ${CMAKE_CTEST_COMMAND}
)

test_api源目录将包含一个独立的CMakeLists.txt文件,其唯一目的是配置一个与Decoder库链接的构建,其绝对路径在DECODER_LIB变量中设置(这只是传递库位置给测试项目的几种方式之一)。

这种测试的一个有趣之处在于,它还可以用于验证特定的测试项目是否不会构建,或验证配置是否因特定的致命错误(例如,缺少符号)而失败。这些预期的致命构建错误无法在主项目中进行测试,因为这会导致主项目的构建失败。将测试构建与主构建分开意味着它可以失败而不影响主构建,并且测试可以使用FAIL_REGULAR_EXPRESSION或非零返回码来验证失败。

另一个这种测试有用的场景是测试主项目创建的代码生成器的输出。测试夹具可以用来设置一对测试,一个用于生成代码,另一个用于使用它进行测试构建。如果代码生成器创建了通常由cmake读取的文件,例如CMakeLists.txt文件,则这将非常有帮助。例如:

add_executable(CodeGen generator.cpp)

add_test(NAME GenerateCode COMMAND CodeGen)
add_test(NAME BuildGeneratedCode
        COMMAND ${CMAKE_CTEST_COMMAND}
        --build-and-test ${CMAKE_CURRENT_LIST_DIR}/test_gen
                         ${CMAKE_CURRENT_BINARY_DIR}/test_gen
        --build-generator ${CMAKE_GENERATOR}
        --test-command ${CMAKE_CTEST_COMMAND}
)

set_tests_properties(GenerateCode
    PROPERTIES FIXTURES_SETUP Generator
)
set_tests_properties(BuildGeneratedCode
    PROPERTIES FIXTURES_REQUIRED Generator
)

构建和测试模式也可以用于验证CMake实用脚本,方法是将它们包含在一个小的测试项目中,并根据需要调用其功能。实际上,这提供了一种相当便捷的方式来实现CMake脚本的单元测试,而无需将这些测试放入主项目的配置阶段。

虽然构建和测试模式在像上面提到的情况下肯定是有用的,但它缺乏完全脚本化运行的灵活性,其中每个单独的命令都有完整的选项集。下一节介绍了一种调用ctest的替代方式,它提供了对整个流程的更强大处理,包括一些有用的额外报告功能。

26.10 CDash集成

CTest有着与另一个名为CDash的产品密切相关的悠久历史和紧密联系,CDash也由CMake和CTest背后的同一公司开发。CDash是一个基于网络的仪表板,它收集软件构建和测试流程的结果,该流程由ctest驱动。它收集每个流程阶段的警告和错误,并显示每个阶段的摘要,可以点击查看每个单独的警告或错误。过去流程的历史记录允许随着时间观察趋势并比较运行结果。

CMake本身有一个相当广泛的仪表板,用于跟踪每晚的构建、与合并请求相关的构建等等。探索一个样本仪表板几分钟时间会有助于理解本节介绍的内容:

https://open.cdash.org/index.php?project=CMake

26.10.1. CDash的关键概念

有三个重要概念将CTest和CDash的流程执行和结果报告联系在一起:步骤(有时也称为操作)、模型(有时也称为模式)和组(在CMake 3.15及更早版本中曾称为轨道)。步骤是流程执行的一系列操作。通常情况下,按照它们正常调用的顺序定义的主要操作集合是:

  • 开始
  • 更新
  • 配置
  • 构建
  • 测试
  • 覆盖率
  • 内存检查
  • 提交

并非所有操作都必须执行,有些操作可能不被支持或者不需要运行。粗略地说,CDash仪表板中的每一行对应于单个流程,通常会显示每个操作的摘要(提交哈希、警告总数、错误总数、失败总数等)。

每个流程必须与一个模型关联,该模型用于定义某些行为,例如在特定步骤失败后是否继续进行后续步骤。模型还在未请求特定操作时提供默认的操作集合。支持的模型有:

Nightly

旨在每天执行一次,通常由自动化作业在执行机器较为空闲时执行。默认的操作集包括上面列出的所有步骤,除了内存检查。如果更新步骤失败,仍将执行后续步骤。

Continuous

与Nightly非常相似,只是旨在根据需要多次执行,通常是响应提交的更改。它定义了与Nightly相同的默认操作集合,但如果更新步骤失败,将不会执行后续步骤。

Experimental

顾名思义,该模型旨在由开发人员根据需要执行临时实验。其默认操作集包括所有步骤,除了更新和内存检查。如果指定的模型不是三个定义的模型之一,或者根本没有指定模型,它将被视为实验性。

组控制流程结果将在仪表板结果下显示的组。组名可以是项目或开发人员希望使用的任何内容,但如果未指定组,则将设置为与模型相同。这导致了一个常见的误解,即模型控制仪表板中的分组,但实际上是组在进行此操作。覆盖率和内存检查操作是一种特殊情况,它们实际上忽略了组,并且它们的仪表板结果显示在它们各自的专用组中(分别为覆盖率和动态分析)。从CMake 3.22或更高版本开始,除了填充专用的动态分析组外,内存检查操作还将以与测试操作相同的方式提交测试结果,将使用指定的组。

26.10.2. 执行流程和操作

为了具备必要的配置文件(在下一节中介绍)的项目,可以使用以下形式的ctest命令来调用整个流程或单个步骤:

ctest [-M Model] [-T Action] [--group Group] [otherOptions...]

如果使用的是CMake 3.16或更早版本,则必须使用--track Track而不是--group Group。

必须指定至少一个模型和/或操作。为方便起见,-M和-T选项可以组合成单个-D选项,如下所示:

ctest -D Model[Action] [--group Group] [otherOptions...]

-D的参数可以省略操作或将其附加到模型。有效参数示例包括Continuous、NightlyConfigure、ExperimentalBuild等。如果需要,可以多次指定-T和-D选项以在一个ctest调用中列出多个步骤。

注意,-D还用于定义ctest变量,ctest命令将任何未识别的模型或模型操作视为设置变量而不是执行操作。建议使用-M和-T选项而不是-D选项,以最大程度地减少命令行选项输错导致误解的机会。

使用默认步骤运行Nightly任务,并将其结果报告在默认组Nightly下,可以简单地调用如下:

ctest -M Nightly

对于相同的操作,但结果报告在名为Nightly Master的组下:

ctest -M Nightly --group "Nightly Master"

考虑一个自定义的实验性流程,只包括配置、构建和测试步骤,结果分组为Simple Tests。由于这与实验性模型定义的默认操作集不同(不执行Coverage步骤),因此需要显式指定步骤集。可以将其作为多个步骤的序列进行ctest调用,每个调用一个步骤,或者可以在单个命令行上使用多个-T选项将它们全部列出。两种形式均作为比较显示:

分开的命令:

ctest -T Start -M Experimental --group "Simple Tests"
ctest -T Configure
ctest -T Build
ctest -T Test
ctest -T Submit

单一命令:

ctest -M Experimental --group "Simple Tests" \
            -T Start -T Configure -T Build -T Test -T Submit

第一步应该是启动操作(Start action),用于初始化流程细节并记录后续步骤将使用的模型和组名。如果将每个操作拆分为独立的 ctest 调用,则不需要为后续步骤重复这些细节。假设目标是将最终结果提交到仪表板,则最后一步应该是提交操作(Submit action)。以上所有输出都会收集到 Testing 子目录下,该子目录位于调用 ctest 的目录下。启动操作会写入一个名为 TAG 的文件,其中至少包含两行,第一行是以 YYYYMMDD-hhmm 形式表示的运行开始的日期时间,第二行是组名。CMake 3.12 添加了第三行,其中包含模型名称。

在执行启动操作后的每个步骤中,它将在 Testing/YYYYMMDD-hhmm/.xml 处创建自己的输出文件,以及一个日志文件在 Testing/Temporary/Last_YYYYMMDD-hhmm.log 处。对于 MemCheck 步骤,这些文件名中的 部分将不是 MemCheck 而是 DynamicAnalysis(在 CMake 3.22 或更高版本中,还会创建一个 Testing/YYYYMMDD-hhmm/DynamicAnalysis-Test.xml 文件)。提交操作将收集 XML 输出文件和一些日志文件,并将它们提交到指定的仪表板。

要将构建注释附加到整个流程,请在提交步骤中使用 -A 或 --add-notes 选项指定要上传的文件名,如果要添加多个文件,则用分号分隔它们。这是记录有关特定流程的额外详细信息的有用方式,例如来自启动运行的持续集成系统的信息。

ctest -T Submit --add-note JobNote.txt

还支持 --extra-submit 选项,但它更多地用于 ctest 的内部使用。它不是一个通用的文件上传机制,但经常被错误地认为是这样。开发人员或项目不应直接使用它。

虽然上述功能主要用于与 CDash 集成,但也可以用于其他场景。例如,Jenkins CI 系统有一个插件,可以读取 Test 操作的 Test.xml 输出文件,并类似于 CDash 记录测试结果。它可以作为仪表板运行而不是以普通方式运行 ctest,只需要执行 Test 操作。Jenkins 插件只需要告诉它在哪里找到 Test.xml 文件,就可以读取测试结果。在这种用法中,甚至可以省略启动操作。如果没有任何先前的启动操作,ctest 将静默执行类似于使用实验模型的启动操作,执行其他步骤。项目可能希望在这样做之前清除 Testing 目录的任何先前内容,以确保 Jenkins 只会捡起当前运行的结果。

当将操作的 XML 输出文件传递给除 CDash 之外的工具时,可能需要指示 ctest 不压缩捕获的输出。默认情况下,操作的输出会被压缩并以 ASCII 编码形式写入 XML 文件,但可以通过向 ctest 传递 --no-compress-output 选项来阻止这种情况。只有在必要时才使用此选项,因为它会导致更大的输出文件。

另一个可以在没有 CDash 的情况下使用仪表板步骤的情况是利用对代码覆盖率或内存检查(Valgrind、Purify、各种 sanitizer 等)的支持。这些仪表板操作可以更轻松地调用相关工具并收集结果。详情请参阅下一节有关如何设置和使用这些工具的内容。

26.10.3 CTEST 配置

为了将项目准备好与 CDash 集成,主要是通过 CMake 提供的 CTest 模块来处理的。应该在顶层的 CMakeLists.txt 文件中的 project() 命令后不久包含这个模块。这很重要,因为该模块在被包含的时候会将各种文件写入当前构建目录,而开发人员通常希望能够从顶层构建目录运行 ctest。

cmake_minimum_required(VERSION 3.0)
project(CDashExample)

# ... set any variables to customize CTest behavior

include(CTest)

# ... Define targets and tests as usual

CTest 模块定义了一个 BUILD_TESTING 缓存变量,默认值为 true。它用于决定模块是否调用 enable_testing(),所以项目不必自己调用 enable_testing()。这个缓存变量也可以被项目用来在测试启用时执行某些处理。如果项目有许多构建时间很长的测试,这可以是一个有用的方法,可以避免在不需要时将它们添加到构建中。

cmake_minimum_required(VERSION 3.0)
project(CDashExample)

include(CTest)
# ... define regular targets

if(BUILD_TESTING)
    # ... define test targets and add tests
endif()

CTest 模块为每个模型(Model)和每个模型操作(ModelAction)组合定义了构建目标。这些目标执行 ctest,并将 -D 选项设置为目标名称,旨在方便地从 IDE 应用程序中执行整个流程或仅执行一个仪表板操作。如果从命令行工作,则这些目标与直接调用 ctest 没有实际优势。

CTest 模块执行的更重要的任务是在构建目录中写出一个名为 DartConfiguration.tcl 的配置文件。这个文件的名称是历史性的,Dart 是 CDash 项目的原始名称。该文件记录了基本信息,如源目录和构建目录的位置,正在进行构建的机器信息,使用的工具链,各种工具的位置和其他默认值。它还将包含 CDash 服务器的详细信息,但为了能够这样做,项目需要在源树的顶部提供一个名为 CTestConfig.cmake 的文件,并提供相关内容。可以从 CDash 本身获取一个合适的 CTestConfig.cmake 文件(需要管理员权限),但通常手动创建一个并不难。

一个适用于所有 CMake 版本的最小示例如下所示:

# Name used by CDash to refer to the project
set(CTEST_PROJECT_NAME "MyProject")

# Time to use for the start of each day. Used by CDash to group results by day,
# usually set to midnight in the local timezone of the CDash server.
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")

# Details of the CDash server to submit to
set(CTEST_DROP_METHOD "https")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=${CTEST_PROJECT_NAME}")
set(CTEST_DROP_SITE_CDASH YES)

# Optional, but recommended so that command lines can be seen in the CDash logs
set(CTEST_USE_LAUNCHERS YES)

从CMake 3.14开始,各种CTEST_DROP_…选项可以被一个单独的CTEST_SUBMIT_URL选项替代。这样更简单易读,所以如果项目的最低CMake版本至少是3.14,应该优先选择这种方式。上面例子的等价方式如下:

set(CTEST_SUBMIT_URL "https://my.cdash.org/submit.php?project=${CTEST_PROJECT_NAME}")

CTest模块写出的DartConfiguration.tcl文件包含了仪表板操作的选项。大多数选项默认设置为适当的值,但是Coverage和MemCheck步骤有一些可能感兴趣的选项。这些选项由CMake变量控制,开发人员可以在包含CTest模块之前在CMake缓存或CMakeLists.txt文件中操作这些变量。

Coverage步骤假定调用gcov,CTest模块将搜索该名称的命令。COVERAGE_COMMAND缓存变量保存了该搜索结果,但如果需要,开发人员可以修改它。第二个缓存变量COVERAGE_EXTRA_FLAGS用于保存应立即跟随COVERAGE_COMMAND的选项,因此开发人员可以控制使用的命令和传递给它的选项。

MemCheck步骤更有趣。支持多种不同的内存检查器,包括Valgrind、Purify、BoundsChecker、Dr Memory(CMake 3.17或更高版本)、Cuda Sanitizer(CMake 3.19或更高版本)和其他各种检查器。对于前五种检查器,可以通过将MEMORYCHECK_COMMAND设置为相关可执行文件的位置来选择它们。ctest将根据可执行文件的名称识别检查器。对于Valgrind,还可以通过设置VALGRIND_COMMAND_OPTIONS变量来覆盖传递给valgrind本身的选项。Dr Memory也具有类似的功能,可以使用DRMEMORY_COMMAND_OPTIONS变量。要使用其中一种检查器,将MEMORYCHECK_TYPE设置为以下字符串之一(然后MEMORYCHECK_COMMAND将被忽略):

  • AddressSanitizer
  • LeakSanitizer
  • MemorySanitizer
  • ThreadSanitizer
  • UndefinedBehaviorSanitizer

ctest会像平常一样启动测试可执行文件,但会设置相关的环境变量以启用请求的检查器。请注意,检查器需要使用相关的编译器和链接器标志进行构建(通常是 -fsanitize=XXX 和可能是 -fno-omit-frame-pointer)。有关相关标志的进一步详细信息以及各种检查器的功能,请查阅Clang或GCC文档。

上述细节足以执行各种仪表板操作并将结果提交到CDash服务器,但存在一个鸡生蛋还是蛋生鸡的问题。Update和Configure步骤需要先执行以获取DartConfiguration.tcl文件。因此,这两个步骤的细节无法被捕获,或者在Configure步骤的情况下,第一次运行cmake的输出会丢失,只能获取已配置的构建目录中重新运行CMake的输出。尽管如此,所有其他步骤的输出都将被捕获,对于某些情况可能已经足够。

例如,当使用Gitlab CI或Jenkins等持续集成系统时,源树的初始克隆或更新可以由CI系统本身处理。可以执行初始cmake运行,然后剩余的步骤可以作为仪表板操作运行。最终结果可以提交到CDash服务器,直接由CI系统读取,或者可能两者都可以。

要捕获完整的流水线,包括源树的初始克隆或更新和第一次配置步骤,必须编写一个自定义的ctest脚本来定义所需的设置详细信息并调用相关的ctest函数。这可能是一个复杂的过程,如果已经使用其他CI系统,通常是不必要的。如果不需要捕获克隆/更新步骤,则自定义脚本的复杂性会降低。在这种用法中,通过使用 -S 选项和要执行的脚本名称来调用ctest。以下是一个相当简单的示例:

ctest -S MyCustomCTestJob.cmake
# Re-use CDash server details we already have
include(${CTEST_SCRIPT_DIRECTORY}/CTestConfig.cmake)

# Basic information every run should set, values here are just examples
site_name(CTEST_SITE)
set(CTEST_BUILD_NAME ${CMAKE_HOST_SYSTEM_NAME})
set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}")
set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build")
set(CTEST_CMAKE_GENERATOR Ninja)
set(CTEST_CONFIGURATION_TYPE RelWithDebInfo)

# Dashboard actions to execute, always clearing the build directory first
ctest_empty_binary_directory(${CTEST_BINARY_DIRECTORY})
ctest_start(Experimental)
ctest_configure()
ctest_build()
ctest_test()
ctest_submit()

以下更有趣的示例展示了如何通过自定义脚本定义更灵活的行为:

include(${CTEST_SCRIPT_DIRECTORY}/CTestConfig.cmake)

site_name(CTEST_SITE)
set(CTEST_BUILD_NAME "${CMAKE_HOST_SYSTEM_NAME}-ASan")
set(CTEST_SOURCE_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}")
set(CTEST_BINARY_DIRECTORY "${CTEST_SCRIPT_DIRECTORY}/build")
set(CTEST_CMAKE_GENERATOR Ninja)
set(CTEST_CONFIGURATION_TYPE RelWithDebInfo)
set(CTEST_MEMORYCHECK_TYPE AddressSanitizer)
set(configureOpts
    "-DCMAKE_CXX_FLAGS_INIT=-fsanitize=address -fno-omit-frame-pointer"
    "-DCMAKE_EXE_LINKER_FLAGS_INIT=-fsanitize=address -fno-omit-frame-pointer"
)


ctest_empty_binary_directory(${CTEST_BINARY_DIRECTORY})
ctest_start(Experimental GROUP Sanitizers)
ctest_configure(OPTIONS "${configureOpts}")
ctest_submit(PARTS Start Configure)
ctest_build()
ctest_submit(PARTS Build)
ctest_memcheck()
ctest_submit(PARTS MemCheck)
ctest_upload(FILES
    ${CTEST_BINARY_DIRECTORY}/mytest.log
    ${CTEST_BINARY_DIRECTORY}/anotherFile.txt
)
ctest_submit(PARTS Upload Submit)

if(NOT CMAKE_VERSION VERSION_LESS "3.14")
    ctest_submit(PARTS Done)
endif()

在这个更为实用的示例中,不是在运行的最后才将结果提交到仪表板,而是在每个步骤之后逐步提交结果(如果某些步骤需要很长时间,则非常有用)。可执行文件使用地址检查器支持构建,并且运行地址检查器检查而不是常规测试。此外,还会在最后上传一些额外的文件。

Done部分仅在CMake 3.14中添加,并用于告知CDash任务已完成。近期版本的CDash会使用它来报告任务的更可靠总持续时间。较早的CDash版本将简单忽略它。当不逐部分提交时,Done部分将自动处理为Submit操作的一部分,或者调用没有指定部分的ctest_submit()。

每个各种ctest_…命令都在CMake文档中有详细说明,以及可用于自定义每个步骤或以不同方式影响处理的CTest和CMake变量。以上应该是一个良好的基础脚本,可用于尝试不同的参数和变量。

创建一个还处理克隆/更新项目的脚本会增加更多复杂性。项目通常有自己特殊的方法来做这件事,他们通常需要决定诸如夜间和连续构建应该如何安排等事项。支持诸如合并请求的自动化构建将严重依赖于托管项目的存储库的能力。对于有兴趣探索这条路径的人来说,一个推荐的入门方式是找到一个使用类似存储库托管安排的项目,并将其用作指南。一些项目在其存储库中包含自定义脚本以便于访问(Kitware的许多项目都这样做,并且脚本已经被合理地记录了)。

26.10.4. 测试测量和结果

上面的示例简要展示了如何将文件上传结合到自定义的CTest脚本中。ctest_upload()命令提供了一个基本机制,用于记录要与构建结果一起上传的文件。上传是作为后续调用ctest_submit()的一部分执行的。然而,有时候文件上传应该与特定的测试相关联,而不是整个脚本运行。为此,CMake提供了ATTACHED_FILES和ATTACHED_FILES_ON_FAIL测试属性。两者都保存要上传的文件列表,并与特定的测试相关联,唯一的区别在于后者包含仅在测试失败时上传的文件。这是一种非常有用的方式,用于记录关于失败的附加信息,以便进一步调查。

add_executable(CodeGen ...)
add_test(NAME GenerateFile COMMAND CodeGen)

set_tests_properties(GenerateFile PROPERTIES
    ATTACHED_FILES_ON_FAIL
    ${CMAKE_CURRENT_BINARY_DIR}/generated.c
    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
)

测试还可以记录单个测量值,该值将记录并在CDash中进行跟踪。一个测量通常具有key=value的形式,尽管=value部分可以省略以使用默认值1。测量被记录为测试属性,如下所示:

set_tests_properties(PerfRun PROPERTIES
    MEASUREMENT mySpeed=${someValue}
)

由于测量值必须在测试运行之前定义,因此其使用性有限。更有用的是一个长期受支持但只在CMake 3.21及更高版本中有文档记录的功能,即测量可以以类似HTML标签的形式嵌入到测试输出中。ctest会扫描测试输出以获取这些测量,提取相关数据并将其上传到CDash作为测试结果的一部分。然后,在测试详细信息页面的顶部附近会显示这些测量结果。最简单的测量类型由以下形式定义:

<DartMeasurement name="key" type="someType">value</DartMeasurement>

name属性将作为结果表中测量的标签,而type属性通常是类似text/string、text/link(用于URL)或numeric/double的内容。value是对于该测量有意义的文本或数值内容。对于数值数值,CDash提供了一个工具来绘制每个测量在最近测试运行中的历史,这对于发现随时间变化的行为变化非常有用。

另一种形式可以用于嵌入文件而不是特定值:

<DartMeasurementFile name="key" type="someType">filePath</DartMeasurementFile>

这种第二种形式对于上传图像最有用,其中type属性应该是类似image/png或image/jpeg的内容。filePath应该是要上传的文件的绝对路径。对于图像,CDash识别一些特殊的测量名称。这些名称可用于帮助比较预期和实际图像,CDash甚至提供了一个有用的交互式UI元素用于重叠比较。识别的name属性及其含义包括:

TestImage

这被解释为测试生成的图像。可以将其视为测试输出,并且将显示在独立位置以及作为交互比较图像的一部分。

ValidImage

这相当于测试的预期图像。它通常应与TestImage具有相同的尺寸,但不一定需要与相同的图像格式相同。它将仅包含在交互式图像中。BaselineImage也可以用作名称,其含义与ValidImage相同。

DifferenceImage2

可以使用各种工具生成表示两个其他图像之间差异的图像。如果测试提供这样的图像文件,它可以使用此名称将其包含在上传到CDash的测试输出测量中。它将被合并到交互式比较图像中。

除了上述名称和类型以外,还可以上传其他名称和类型的内容,但是在CMake 3.20及更早版本中可能会出现问题,可能会导致不稳定性。为了获得最佳结果,如果可能的话,请使用3.21或更高版本。CMake 3.21还在类型设置为file时为非图像文件提供了更好的处理方式。文件将被上传为附件,就像ATTACHED_FILES测试属性一样。对于任何其他类型,它将被视为一个命名测量,并且可能在CDash中显示不恰当。

CMake 3.21还增加了测试输出覆盖CDash中测试详细信息字段的功能。默认内容通常说明测试是否完成以及测试结果是什么,但是测试可以使用此功能提供更具体的信息。不过,它仍然应该相对简短,最好不要超过一行。

<CTestDetails>Replacement test details go here</CTestDetails>

从CMake 3.22开始,测试还可以在运行时通过在输出中包含相关标签来动态添加标签,如下所示:

<CTestLabel>Some dynamic label</CTestLabel>
<CTestLabel>Another label</CTestLabel>

这些标签与测试结果一起上传到CDash,就像在配置时通过LABELS测试属性为测试分配常规标签一样(请参阅第26.4.3节,“标签”)。这些动态标签也包含在ctest运行结束时的结果摘要中。

由于动态标签仅在测试执行时分配,因此无法用于包含或排除要运行的测试。如果用户指定了ctest选项(例如-L或-LE)来控制要执行哪些测试,则只有通过LABELS测试属性静态分配的标签会被考虑。如果一个测试将一个标签动态添加到其输出中,并且相同的标签在另一个测试中静态设置,那么如果用户尝试基于该标签过滤测试集,则可能会对用户造成困惑。他们认为已经过滤掉的测试可能仍然会显示在输出中,或者他们认为包含的测试可能会缺失。考虑使用静态和动态情况下的不重叠标签集,以避免设置这种情况。

26.11 输出控制

CDash在收集和跟踪测试结果方面非常有效,但许多项目使用其他工具来完成这些目的。为了支持这些工作流程,CMake 3.21添加了以广泛支持的JUnit XML格式提供测试结果的功能。JUnit XML文件通常可以导入到CI工具和各种测试报告软件中。

对于非仪表板运行,可以通过在ctest命令行中添加一个选项来生成JUnit结果文件,并指定要将结果写入的文件名:

ctest --output-junit /path/to/resultFile.xml ...

对于仪表板运行,通过ctest_test()命令的一个额外参数可以实现类似的功能:

ctest_test(OUTPUT_JUNIT /path/to/resultFile.xml)

诸如命名测量和文件附件等功能在JUnit输出中不受支持。因此,它并不能完全替代CDash可以完成的任务,但是减少的功能集合可能对许多项目来说仍然足够。

测试有时可能会产生大量输出。仪表板运行将截断报告的输出,超出可配置的限制,无论是在默认的CDash结果中还是在JUnit XML文件输出中(如果启用)。默认情况下,任何通过的测试的输出将在1024字节处截断。对于失败的测试,输出将在307,200字节处截断(即300kB)。这些限制可以通过在仪表板脚本中设置变量来覆盖。CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE和CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE可用于指定要截断的字节数,而不是默认值。这些变量长期以来一直受到CMake的支持,但自CMake 3.4以来才正式记录在案。

使用CMake 3.24或更高版本,还可以使用CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION变量来控制输出截断的类型。默认设置通常是最合适的,但在某些情况下,以不同的方式进行截断可能会有用。有效值包括:

tail

保留输出的开头部分,截断结尾部分。这是默认行为。

head

保留输出的结尾部分,跳过开头部分。如果测试输出的结尾更可能包含有趣的信息,这可能会有用。请注意,通常第一个错误最重要,后面的错误往往是虚假的或者是前面错误的结果。使用头部截断可能会增加错过第一个错误的机会,并导致用户关注可能不是问题实际原因的后续错误。

middle

保留输出的开头和结尾部分,省略输出的中间部分。这可能不太有用,但如果重要的细节通常首先记录,并且错误可能会在结尾处捕获,这可能是合适的。

# Save more output, keeping the start and end
set(CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE 10000)
set(CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION middle)

ctest_test()

使用 --output-junit 选项的非仪表板运行可以通过相应的 ctest 命令行选项 --test-output-size-passed、--test-output-size-failed 和 --test-output-truncation 来类似地自定义输出。

ctest --output-junit some-file.xml \
      --test-output-size-passed 10000 \
      --test-output-truncation middle

从 CMake 3.21 或更高版本开始,单个测试可以通过在其输出中记录特殊字符串 CTEST_FULL_OUTPUT 来覆盖输出限制。然后,无论任何输出限制,该测试的整个输出都将包含在测试结果中。请谨慎使用此功能,因为它有可能显著增加所报告和上传的数据量。

26.12. GoogleTest

CMake/ctest提供了构建、执行和确定测试结果的支持。项目负责提供测试代码本身,这就是像GoogleTest这样的测试框架可以发挥作用的地方。这些框架可以补充CMake和ctest提供的功能,有助于编写清晰、结构良好的测试用例,与CMake和ctest的工作方式完美结合。

很长一段时间以来,CMake一直通过FindGTest模块支持GoogleTest。该模块搜索预构建的GoogleTest位置,并创建变量供项目使用,将GoogleTest整合到它们的构建中。从CMake 3.5开始,还提供了导入目标GTest::GTest和GTest::Main,强烈建议使用这些导入目标而不是变量。使用导入目标会更加稳健地处理使用要求和属性。不幸的是,这些目标名称与上游GoogleTest项目定义的目标名称略有不同,上游项目定义了稍有不同的导入目标名称GTest::gtest和GTest::gtest_main。因此,CMake 3.20在FindGTest模块中添加了这些新的导入目标名称,并弃用了旧名称。因此,建议项目将其最低版本设置为至少CMake 3.20,并使用这些更新的导入目标。以下是如何在CMake 3.20或更高版本中使用该模块的简单示例:

add_executable(MyGTestCases ...)
find_package(GTest REQUIRED)
target_link_libraries(MyGTestCases PRIVATE GTest::gtest)
add_test(NAME MyGTestCases COMMAND MyGTestCases)

导入目标会确保在构建MyGTestCases时使用相关的头文件搜索路径,并在需要时链接适当的线程库等。以上方法适用于所有平台,在不同平台和编译器上使用的不同名称、运行时、标志等相关的相当复杂的事项都被隐藏了。如果使用模块定义的变量而不是导入目标,这些事项大部分必须手动处理,这是一个相当脆弱的任务。

更加稳健的方法是直接将GoogleTest的源代码整合到构建中,而不是依赖于预先构建的二进制文件。这样可以确保GoogleTest使用与项目其余部分完全相同的编译器和链接器设置构建,从而避免了在使用预先构建的GoogleTest二进制文件时可能出现的许多微妙问题。项目可以通过多种方式实现这一点,每种方式都有其优点和缺点。将源代码和头文件嵌入到项目中是最简单的方法,但这将使得项目与将来对GoogleTest的改进断开连接。GoogleTest git仓库可以作为git子模块添加到项目中,但这也带来了其自身的稳健性问题。第三种选项是在配置步骤中下载GoogleTest源代码,详细讨论了这种方法,它不具有其他方法的缺点(在CMake 3.11及更高版本中添加的功能也使得这一操作非常简单)。

使用GoogleTest的测试可执行文件通常定义多个测试用例。一次运行可执行文件并假设它是单个测试用例的常规模式并不合适。理想情况下,每个GoogleTest测试用例都应该对ctest可见,以便可以单独运行和评估每个测试用例。FindGTest模块提供了一个gtest_add_test()函数,该函数扫描源代码以查找使用相关GoogleTest宏的地方,并将每个单独的测试用例提取为其自己的ctest测试。这个命令的形式传统上是以下形式:

gtest_add_tests(executable "extraArgs" sourceFiles..)

自CMake 3.1开始,可以用关键字AUTO替换要扫描的sourceFiles列表。然后,源代码会通过假设可执行文件是一个CMake目标,并使用其SOURCES目标属性来获取。

在CMake 3.9中,意识到项目可能希望使用由项目自身构建的GoogleTest来使用gtest_add_tests()函数。这意味着项目不需要一个Find模块,所以该函数被移到一个新的GoogleTest模块中,然后FindGTest包含它以保持向后兼容性。还作为那项工作的一部分添加了一种带有关键字参数的改进形式:

gtest_add_tests(
    TARGET target
    [SOURCES src1...]
    [EXTRA_ARGS arg1...]
    [WORKING_DIRECTORY dir]
    [TEST_PREFIX prefix]
    [TEST_SUFFIX suffix]
    [SKIP_DEPENDENCY]
    [TEST_LIST outVar]
)

旧形式仍然受支持,但项目应该尽可能使用新形式,因为它更灵活、更稳健。例如,可以将相同的目标给多个调用g测试_add_tests()的调用,每个调用有不同的TEST_PREFIX和/或TEST_SUFFIX来区分生成的测试集。当给出TEST_LIST选项时,新形式还提供了找到的测试集。通过可用的测试名称,项目可以根据需要修改测试的属性。以下示例演示了这些不同的功能:

# Assume GoogleTest is already part of the build, so we don't need
# FindGTest and can reference the GTest::gtest target directly
include(GoogleTest)

add_executable(TestDriver ...)
target_link_libraries(TestDriver PRIVATE GTest::gtest)

# Run the TestDriver twice with two different arguments
gtest_add_tests(
    TARGET TestDriver
    EXTRA_ARGS --algo=fast
    TEST_SUFFIX .Fast
    TEST_LIST fastTests
)
gtest_add_tests(
    TARGET TestDriver
    EXTRA_ARGS --algo=accurate
    TEST_SUFFIX .Accurate
    TEST_LIST accurateTests
)

set_tests_properties(${fastTests} PROPERTIES TIMEOUT 3)
set_tests_properties(${accurateTests} PROPERTIES TIMEOUT 20)
set(betaTests ${fastTests} ${accurateTests})

list(FILTER betaTests INCLUDE REGEX Beta)
set_tests_properties(${betaTests} PROPERTIES LABELS Beta)

上面的示例创建了两组测试,并为它们应用不同的超时限制。测试名称在每个组中都有不同的后缀。如果没有TEST_SUFFIX选项,对gtest_add_tests()的第二次调用将失败,因为它将尝试创建与第一次调用相同名称的测试。示例还将Beta标签设置为一些测试,无论它们属于哪个测试集。

gtest_add_tests()适用于简单情况,但它不处理参数化测试或通过自定义宏定义的测试。它还会强制CMake在下次构建时重新运行以重新扫描源文件,这可能会让人感到沮丧,特别是如果CMake步骤较慢。SKIP_DEPENDENCY选项可以阻止该行为,并依赖于开发人员手动重新运行CMake以更新测试集。这是在测试时的一个临时解决方案,不应该永久留在项目中。

CMake 3.10添加了一个新函数来解决gtest_add_tests()的不足之处。它在构建过程中或运行ctest时查询可执行文件的测试列表,而不是在配置阶段扫描源代码。CMake无需在测试源代码更改时重新运行,支持参数化测试,并且对测试定义的格式或方式没有限制。其中的一个权衡是在CMake运行期间无法获取测试列表。

gtest_discover_tests(target
    [EXTRA_ARGS arg1...]
    [WORKING_DIRECTORY dir]
    [TEST_PREFIX prefix]
    [TEST_SUFFIX suffix]
    [NO_PRETTY_TYPES]
    [NO_PRETTY_VALUES]
    [PROPERTIES name1 value1...]
    [TEST_LIST var]
    [TEST_FILTER filter] # CMake 3.22 or later
    [DISCOVERY_TIMEOUT seconds] # See notes below
    [DISCOVERY_MODE] # CMake 3.18 or later
    [XML_OUTPUT_DIR] # CMake 3.18 or later
)

对于CMake 3.22或更高版本,可以使用TEST_FILTER选项来限制可执行文件报告的测试集。当使用--gtest_list_tests参数要求可执行文件列出其测试时,指定的过滤器也会作为--gtest_filter=filter传递。这允许选择可用测试的子集。

默认情况下,生成参数化测试名称时,函数将尝试使用类型或值名称而不是数字索引。这通常会导致更可读且更有用的名称。对于不希望这样的情况,可以使用NO_PRETTY_TYPES和NO_PRETTY_VALUES选项来抑制替换,并只使用索引值。请注意,如果使用TEST_FILTER选项,过滤器将与由--gtest_list_tests报告的原始测试名称匹配,而不是由ctest使用的漂亮测试名称。

DISCOVERY_TIMEOUT选项指的是运行可执行文件以获取测试列表所花费的时间。默认值为5秒对于除了具有大量测试或其他导致其返回测试列表时间长的行为的可执行文件来说应该是足够的。该选项最初是在CMake 3.10.1中添加的,使用的关键字名称为TIMEOUT,但发现会导致与TIMEOUT测试属性发生名称冲突,从而导致意外但合法的行为。关键字在CMake 3.10.3中更改为DISCOVERY_TIMEOUT以避免这些情况。

由于测试列表不会返回给调用方,因此无法调用set_tests_properties()或set_property()来修改发现的测试的属性。相反,gtest_discover_tests()允许在调用中指定属性及其值,然后将其写入到ctest输入文件中,在运行ctest时应用。虽然不能提供在CMake中遍历并单独处理发现的测试集的所有灵活性,但设置发现的测试整体属性通常是所需的,并且通常不是一个重要的限制。主要例外情况是无法设置与gtest_discover_tests()命令中的关键字对应的测试属性的名称,或者属性需要值为列表的情况。必须使用自定义ctest脚本来处理这些情况,以下给出了一个示例。

TEST_LIST选项在gtest_discover_tests()和gtest_add_tests()中的工作方式不同。在这种情况下,给定的变量名称与此选项一起在由CMake编写的ctest输入文件中使用,而不是直接提供给CMake。只有当项目向生成的ctest输入文件添加一些自定义逻辑并且想要引用生成的测试列表时,才需要TEST_LIST选项。即使如此,只有在多次调用gtest_discover_tests()中使用相同的目标时才需要这样做。如果没有通过TEST_LIST选项设置,默认的变量名称为_TESTS。

可以通过将文件名附加到TEST_INCLUDE_FILES目录属性中保存的文件列表来添加自定义代码。项目不得覆盖此目录属性,只能追加到其中,因为gtest_discover_tests()使用该属性来构建要由ctest读取的文件集。

以下示例展示了如何使用自定义文件来操作发现的测试的属性,并实现与先前示例中gtest_add_tests()的相同等效逻辑,包括对TIMEOUT名称冲突边缘情况的解决方案。

gtest_discover_tests(
    TestDriver
    EXTRA_ARGS --algo=fast
    TEST_SUFFIX .Fast
    TEST_LIST fastTests
)
gtest_discover_tests(
    TestDriver
    EXTRA_ARGS --algo=accurate
    TEST_SUFFIX .Accurate
    TEST_LIST accurateTests
)

set_property(DIRECTORY APPEND PROPERTY 
        TEST_INCLUDE_FILES ${CMAKE_CURRENT_LIST_DIR}/customTestManip.cmake
)

customTestManip.cmake

# Set here to work around the TIMEOUT keyword clash for the
# gtest_discover_tests() call, works for all CMake versions
set_tests_properties(${fastTests} PROPERTIES TIMEOUT 3)
set_tests_properties(${accurateTests} PROPERTIES TIMEOUT 20)

set(betaTests ${fastTests} ${accurateTests})
list(FILTER betaTests INCLUDE REGEX Beta)
set_tests_properties(${betaTests} PROPERTIES LABELS Beta)

使用自定义ctest脚本会给项目增加一些复杂性,但它允许完全控制测试属性。不必担心与gtest_discover_tests()发生名称冲突,且具有列表值的属性可以安全处理。

在CMake 3.17及之前的版本中,在构建过程中将可执行文件查询为测试列表的POST_BUILD步骤。在CMake 3.18或更高版本中,可以将测试列表的查询推迟到ctest运行时。这至少具有以下优势:

  • 不一定总能在构建阶段运行测试可执行文件。另一方面,在测试时测试可执行文件必须可运行,无论是本地还是通过仿真器。
  • 因为查询发生在测试阶段而不是构建阶段,所以执行查询的成本只在测试时支付。这提高了开发人员专注于使测试代码编译的转换时间。在交叉编译的情况下,仿真器启动时间可能不可忽视,因此这种延迟的潜在好处可能很大。
  • 在Windows上,在构建期间执行测试列表查询将需要设置PATH环境变量,以便可以找到所有测试可执行文件的DLL。这不仅不方便,而且有改变构建方式的潜力。将查询推迟到测试阶段意味着可执行文件不必在构建时运行,从而避免了这些问题。

DISCOVERY_MODE选项控制测试列表查询的执行时间。它从CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE变量获取默认值。如果未设置该变量,则模式默认为POST_BUILD以保持向后兼容性。可以将模式设置为PRE_TEST,该模式将查询推迟到测试阶段。在实践中,PRE_TEST模式通常更可取。通常更方便的做法是通过变量在整个项目范围内设置发现模式,而不是在每次调用gtest_discover_tests()时显式设置它。

CMake 3.18中添加的另一个功能是XML_OUTPUT_DIR关键字。当存在此关键字时,测试将其输出保存到关键字后面给定的目录中的XML文件中。文件名将基于测试名称,包括其前缀和后缀(如果设置)。这确保每个测试都有其自己独特的输出文件,因此测试可以安全并行运行,而不必担心任何输出文件损坏。这些XML输出文件可以由一些持续集成系统和测试报告工具处理,使它们成为在合并请求和其他类似用例中提供结果的可能便捷方式。

推荐做法

目标是使每个测试名称简短,但足够具体以反映测试的性质。这样可以通过向ctest提供的-R和-E选项使用正则表达式轻松缩小测试集合。避免在名称中包含test,因为它会在测试输出中增加额外内容而没有好处。

假设项目有一天可能会并入一个更大的项目层次结构,其中可能有许多其他测试。目标是使用足够具体的测试名称,以减少名称冲突的可能性。更重要的是,最好让父项目控制是否添加测试,并定义默认行为仅在没有父项目时添加测试。使用非缓存变量来实现此控制,以便父项目可以选择是否将其公开在缓存中。一个合适的变量名应该是TEST_XXX,其中XXX是大写的项目名称。以下示例演示了一个顶层项目FooBar的这种安排:

为了进一步改进与父项目的集成,考虑使用LABELS测试属性为每个测试包含一个项目特定的标签。这些项目特定的标签应该允许通过传递给ctest的-L和-LE选项的正则表达式轻松地包含或排除测试。测试可以有多个标签,因此这不限制标签的其他用途,但可能难以确保项目特定的标签在项目的所有测试上都严格设置。

标签的另一个良好用途是识别预计需要长时间运行的测试。开发人员和持续集成系统可能希望较少频繁地运行这些测试,因此可以根据测试标签排除它们可能非常方便。考虑为运行时间较长且不需要经常运行的测试添加一个标签。如果没有其他现有的约定,那么LongRunning标签是一个不错的选择。

除了对测试名称和标签进行正则表达式匹配之外,还可以将测试集合缩小到特定目录及其子目录。而不是从构建树顶部运行ctest,可以从其下面的子目录运行ctest。只有来自该目录关联的源目录及其以下级别定义的测试才会被ctest识别。为了充分利用这一点,不应将所有测试收集到一个地方并且没有目录结构。保持测试与它们测试的源代码密切相关可能很有用,这样源代码的自然目录结构也可以用于为测试提供结构。如果将源代码移动,这种方法还使得与之相关的测试移动更加容易。

编写测试并简单地打开大量日志记录,然后使用通过/失败正则表达式来确定成功可能是诱人的。这可能是一种相当脆弱的方法,因为开发人员经常更改记录的输出,假设它只是用于信息目的。将时间戳添加到记录的输出中进一步复杂化了该方法。如果可能,最好让测试代码本身通过显式测试预期的前后条件、中间值等来确定成功或失败状态,而不是依赖于匹配记录的输出。测试框架如GoogleTest使编写和维护这些测试变得更加简单,强烈推荐使用(至少使用某种合适的框架比哪种框架不太重要)。

如果使用GoogleTest框架,请考虑使用GoogleTest模块提供的gtest_add_tests()和gtest_discover_tests()函数。如果测试代码简单到gtest_add_tests()可以找到所有测试,那么它提供了最简单和最灵活的方式来操作单个测试的属性,但在编写测试代码本身时可能不太方便,因为它可能需要频繁重新运行CMake。如果项目可以要求最低使用CMake 3.10.3或更高版本,那么gtest_discover_tests()可能更合适。这个函数的主要缺点是将测试属性设置为列表值需要更多的工作,这在考虑到上述关于使用测试标签的建议时尤其相关。如果需要支持3.9版本之前的CMake版本,则只能使用gtest_add_tests(),并且只能使用命令的简单形式。项目还将需要使用FindGTest模块而不是GoogleTest模块,如果GoogleTest作为项目本身的一部分构建,则会增加进一步的复杂性。因此,强烈建议项目使用GoogleTest时移至CMake 3.9或更高版本,最好是3.10.3或更高版本。此外,最好将CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE变量设置为PRE_TEST。这在使用CMake 3.18或更高版本时为gtest_discover_tests()提供更稳健和更方便的行为。

如果测试的环境变量需要修改,请优先使用ENVIRONMENT_MODIFICATION测试属性。它比旧的ENVIRONMENT属性更灵活、更稳健。避免将列表值传递给任何一个测试属性,这样就不需要对分号进行脆弱的转义。

对于可能进行不同目标平台的交叉编译的项目,请考虑测试是否可以编写为在仿真器下运行或通过脚本或等效机制在远程系统上复制和执行。可以使用CMake的CMAKE_CROSSCOMPILING_EMULATOR变量和相关的CROSSCOMPILING_EMULATOR目标属性来实现这两种策略之一。最好将CMAKE_CROSSCOMPILING_EMULATOR设置在用于交叉编译的工具链文件中。

充分利用ctest中的并行测试执行支持。对于已知使用多个CPU的测试,设置这些测试的PROCESSORS属性以为ctest提供更好的调度指导。如果测试需要对共享资源进行独占访问,请使用RESOURCE_LOCK属性控制对该资源的访问,并避免使用RUN_SERIAL测试属性,除非没有其他选择。RUN_SERIAL对并行测试性能可能有很大的负面影响,并且除了快速、临时的开发者实验外,很少有正当理由使用它。如果运行ctest的机器可能有其他进程导致CPU负载,考虑使用-l选项来帮助限制CPU过度承诺。这在开发者机器上特别有用,因为开发者可能会同时为多个项目构建和运行测试。

如果最低CMake版本可以设置为3.7或更高版本,请优先使用测试装置来定义测试之间的依赖关系。定义测试用例来设置和清理其他测试所需的资源,启动和停止服务等。当通过正则表达式匹配或选项如--rerun-failed导致测试集合减少时,ctest会自动将所需的装置测试添加到测试集合中。与仅仅控制测试顺序而不强制成功要求的DEPENDS测试属性不同,装置还确保了测试依赖项失败时跳过测试。为了对哪些测试将自动添加到测试集合以满足装置依赖关系进行细粒度控制,使用CMake 3.9或更高版本以用于在该版本中添加的-FS,-FC和-FA选项。项目仍然可以要求至少CMake 3.7作为最低版本。此外,由于清晰的依赖关系和时间控制,最好使用装置而不是TIMEOUT_AFTER_MATCH测试属性。

ctest构建和测试模式可以是将小的测试构建侧边作为主项目测试套件中的测试用例的一种有用方式。当其中一些测试构建需要验证某些情况导致配置或构建错误时,这些测试用例尤其有效。由于测试用例可以定义为预计会失败,因此它们可以在不使主项目构建失败的情况下验证这些条件。考虑将ctest构建和测试模式用作调用add_test()的COMMAND部分,以定义这样的测试用例。

对于运行主项目的完整配置、构建和测试流水线,请考虑使用CDash集成功能而不是使用ctest构建和测试模式。这些功能更好地捕获整个流水线的输出,并提供定制每个步骤行为的机制。它还具有额外功能,有助于使用代码覆盖和动态分析工具,例如内存检查器、消毒剂等。这些功能可以用于将结果提交到CDash服务器或不提交到CDash服务器。实际上,驱动整个CDash流水线的自定义ctest脚本功能可以在没有CDash的情况下使用,使其成为一个潜在便捷的跨平台方式来为其他持续集成系统编写整个构建和测试流水线的脚本。CMake 3.21或更高版本提供的JUnit输出支持在这种情况下尤其有用。CDash服务器也可以与其他CI系统配合使用,以提供更丰富的功能,记录和比较构建历史、测试失败趋势等。

使用CMake 3.18或更高版本,可以使用ctest --stop-on-failure选项在遇到第一个错误时立即结束测试运行。这可以节省时间,在开发过程中,任何失败都可能与开发者当时正在处理的区域有关。它还可以快速结束持续集成运行,以便尽早报告错误。这会以只提供关于可能有很多错误之一的反馈为代价,因此通常只有在运行所有测试所需的时间较长时才应考虑。与ctest_test()命令的STOP_ON_FAILURE选项具有类似效果,但将此行为硬编码到仪表板脚本中可能不如使用--stop-on-failure命令行选项灵活。