前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
15 使用 CMake 和 CTest 进行测试
测试是生产和维护健壮、有效的软件的关键工具。本章将研究 CMake 中支持软件测试的工具。我们将从简单讨论测试方法开始,然后讨论如何使用 CMake 向软件项目添加测试。
软件包的测试可以采用多种形式。在最基本的级别上有冒烟测试,例如一个简单地验证软件编译的测试。虽然这看起来像是一个简单的测试,但由于有各种各样的平台和配置可用,冒烟测试比任何其他类型的测试捕获的问题都要多。冒烟测试的另一种形式是验证测试运行时不会崩溃。当开发人员不想花时间创建更复杂的测试,但愿意运行一些简单的测试时,这是非常方便的。大多数时候,这些简单的测试可以是小的示例程序。运行它们不仅验证构建是否成功,而且验证任何所需的共享库是否可以加载(对于使用它们的项目),并且至少可以执行一些代码而不会崩溃。
超越基本的冒烟测试将导致更具体的测试,如回归测试、黑盒测试和白盒测试。每一种方法都有其优点。回归测试验证测试的结果不会随着时间或平台的变化而改变。当频繁执行时,这是非常有用的,因为它提供了一个快速检查软件的行为和结果并未改变的方法。当回归测试失败时,快速查看最近的代码更改通常可以识别出罪魁祸首。不幸的是,创建回归测试通常比其他测试需要更多的工作。
白盒测试和黑盒测试分别是指,在了解和不了解这些单元是如何分别实现的情况下,为测试代码单元(在不同的集成级别上)而编写的测试。白盒测试旨在强调代码中潜在的故障点,知道代码是如何编写的,因此知道它的弱点。与回归测试一样,创建好的测试需要花费大量的精力。黑盒测试通常除了软件的公共 API 外,对软件的实现了解甚少,甚至一无所知。黑盒测试可以提供大量的代码覆盖率,而不需要花费太多精力开发测试。这对于 API 定义良好的面向对象的软件库尤其如此。可以编写黑盒测试来遍历和调用软件中所有类上的各种典型方法。
我们将要讨论的最后一种测试类型是软件标准遵从性测试。当我们讨论的其他测试类型集中于确定代码是否正常工作时,遵从性测试试图确定代码是否遵循软件项目的编码标准。这可以是一个检查,以验证所有类都实现了某个关键方法,或者所有函数都有一个公共前缀。这类测试的选项是无限的,有许多方法可以执行这类测试。可以使用软件分析工具,或者编写专门的测试程序(可能是 Python 脚本等)。要认识到的关键点是,测试不一定要涉及到运行软件的某些部分。测试可能会在源代码本身上运行一些其他工具。
将测试支持集成到构建过程中是有帮助的,原因有很多。首先,复杂的软件项目可能有许多与配置或平台相关的选项。构建系统知道可以启用哪些选项,然后可以为这些选项启用适当的测试。例如,可视化工具包( VTK )包括对名为 MPI 的并行处理库的支持。如果构建 VTK 时使用了 MPI 支持,那么将启用使用 MPI 的附加测试,并验证 VTK 中特定于 MPI 的代码是否按预期工作。其次,构建系统知道可执行程序将放在哪里,并且它有工具来查找其他所需的可执行程序(如 perl 、 python 等)。第三个原因是,对于 UNIX Makefile ,通常在 Makefile 中有一个测试目标,以便开发人员可以输入 make test 并运行测试。为了使其工作,构建系统必须对测试过程有一定的了解。
15.1 CMake 如何推动测试 ?
CMake 通过特殊的测试命令和 CTest 可执行文件推动对软件的测试。首先,我们将讨论 CMake 中的关键测试命令。要向基于 CMake 的项目添加测试,只需简单地 include(CTest) ,并使用 add_test 命令。 add_test 命令的语法如下所示:
add_test(NAME TestName COMMAND ExecutableToRun arg1 arg2 ...)
第一个参数只是测试的字符串名称,这是测试程序将显示的名称。第二个参数是要运行的可执行文件。可执行文件可以作为项目的一部分构建,也可以是独立的可执行文件,如 python 、 perl 等。其余的参数将被传递给正在运行的可执行文件。使用 add_test 命令的典型测试示例如下:
add_executable(TestInstantiator TestInstantiator.cxx)
target_link_libraries(TestInstantiator vtkCommon)
add_test(NAME TestInstantiator
COMMAND TestInstantiator)
add_test 命令通常放在包含测试的目录的 CMakeLists 文件中。对于大型项目,可能会有多个包含 add_test 命令的 CMakeLists 文件。一旦 add_test 命令出现在项目中,用户就可以通过调用Makefile的“ test ”目标,或者 Visual Studio 或 Xcode 的 RUN_TESTS 目标来运行测试。在 Linux 上使用 Makefile 生成器在 CMake 测试上运行测试的一个示例是:
$ make test
Running tests...
Test project
Start 2: kwsys.testEncode
1/20 Test #2: kwsys.testEncode .......... Passed 0.02 sec
Start 3: kwsys.testTerminal
2/20 Test #3: kwsys.testTerminal ........ Passed 0.02 sec
Start 4: kwsys.testAutoPtr
3/20 Test #4: kwsys.testAutoPtr ......... Passed 0.02 sec
15.2 其他测试属性
默认情况下,如果以下所有条件都为真,测试通过:
- 找到了测试可执行文件。
- 测试毫无例外地运行。
- 测试退出,返回代码为 0 。
也就是说,这些行为可以使用 set_property 命令进行修改:
set_property(TEST test_name
PROPERTY prop1 value1 value2 ...)
此命令将为指定的测试设置附加属性。示例属性有:
- 指定应为运行测试而定义的环境变量。如果以 MYVAR=value 形式设置一组环境变量和值,那么将在测试运行时定义这些环境变量。测试完成后,环境将恢复到之前的状态。
- 指定与测试关联的文本标签列表。这些标签可用于根据测试内容将测试分组。例如,你可以向所有执行 MPI 代码的测试添加 MPI 标签。
- 如果将此选项设置为 true ,那么如果返回代码不为 0 ,则测试将通过,如果返回代码为 0 则失败。这与通过要求的第三个条件相反。
- 如果指定了此选项,则根据提供的正则表达式检查测试的输出(还可以传入一组正则表达式)。如果没有一个正则表达式匹配成功,那么测试将失败。如果其中至少有一个匹配成功,则测试将通过。
- 如果指定了此选项,则根据提供的正则表达式检查测试的输出(还可以传入一组正则表达式)。如果没有一个正则表达式匹配成功,那么测试将通过。如果至少有一个匹配成功,那么测试将失败。
如果同时指定了 PASS_REGULAR_EXPRESSION 和 FAIL_REGULAR_EXPRESSION ,则 FAIL_REGULAR_EXPRESSION 优先。下面的例子说明了如何使用 PASS_REGULAR_EXPRESSION 和 FAIL_REGULAR_EXPRESSION :
add_test (NAME outputTest COMMAND outputTest)
set (passRegex "^Test passed" "^All ok")
set (failRegex "Error" "Fail")
set_property (TEST outputTest
PROPERTY PASS_REGULAR_EXPRESSION "${passRegex}")
set_property (TEST outputTest
PROPERTY FAIL_REGULAR_EXPRESSION "${failRegex}")
15.3 使用 CTest 进行测试
当你从构建环境运行测试时,实际发生的情况是构建环境会运行 CTest 。 CTest 是 CMake 附带的可执行文件,它负责运行项目的测试。虽然 CTest 可以很好地与 CMake 一起工作,但你不必为了使用 CTest 而使用 CMake 。 CTest 的主输入文件称为 CTestTestfile.cmake 。 CMake 在处理的每个目录(通常是带有 CMakelists 文件的每个目录)时将创建该文件。 CTestTestfile.cmake 的语法类似于常规的 CMake 语法,有一个可用的命令子集。如果使用 CMake 生成测试文件,它们将列出需要处理的所有子目录以及所有 add_test 调用。这些子目录是通过 add_subdirectory 命令添加的。然后 CTest 可以解析这些文件,以确定要运行哪些测试。这样一个文件的示例如下所示:
# CMake generated Testfile for
# Source directory: C:/CMake
# Build directory: C:/CMakeBin
#
# This file includes the relevant testing commands required
# for testing this directory and lists subdirectories to
# be tested as well.
add_test (SystemInformationNew ...)
add_subdirectory (Source/kwsys)
add_subdirectory (Utilities/cmzlib)
...
当 CTest 解析 CTestTestfile.cmake 文件时,它将从中提取测试列表。这些测试将被运行,对于每个测试, CTest 将显示测试的名称及其状态。请看以下示例输出:
$ ctest
Test project C:/CMake-build26
Start 1: SystemInformationNew
1/21 Test #1: SystemInformationNew ...... Passed 5.78 sec
Start 2: kwsys.testEncode
2/21 Test #2: kwsys.testEncode .......... Passed 0.02 sec
Start 3: kwsys.testTerminal
3/21 Test #3: kwsys.testTerminal ........ Passed 0.00 sec
Start 4: kwsys.testAutoPtr
4/21 Test #4: kwsys.testAutoPtr ......... Passed 0.02 sec
Start 5: kwsys.testHashSTL
5/21 Test #5: kwsys.testHashSTL ......... Passed 0.02 sec
...
100% tests passed, 0 tests failed out of 21
Total Test time (real) = 59.22 sec
CTest 是从构建树中运行的。它将运行当前目录中的所有测试,以及 CTestTestfile.cmake 中列出的任何子目录。对于运行的每个测试, CTest 将报告测试是否通过以及运行测试用了多长时间。
CTest 可执行文件包含一些方便的命令行选项,可以使测试更容易一些。我们将从查看通常从命令行使用的选项开始。
-R <regex> Run tests matching regular expression
-E <regex> Exclude tests matching regular expression
-L <regex> Run tests with labels matching the regex
-LE <regex> Run tests with labels not matching regexp
-C <config> Choose the configuration to test
-V,--verbose Enable verbose output from tests.
-N,--show-only Disable actual execution of tests.
-I [Start,End,Stride,test#,test#|Test file]
Run specific tests by range and number.
-H Display a help message
-R 选项可能是最常用的。它允许你指定一个正则表达式,只运行名称与正则表达式匹配的测试。在测试的名称(或名称的一部分)中使用 -R 选项是运行单个测试的快速方法。 -E 选项与此类似,只是它排除所有匹配正则表达式的测试。 -L 和 -LE 选项类似于 -R 和 -E ,不同的是它们应用于使用先前中描述的 set_property 命令设置的测试标签。 -C 选项主要用于 IDE 构建,其中你可能有多个配置,例如在同一树中有 Release 和 Debug 。 -C 后面的参数决定要测试哪个配置。当你试图确定测试失败的原因时, -V 参数很有用。使用 -V , CTest 将打印出用于运行测试的命令行,以及来自测试本身的任何输出。 -V 选项可以与 CTest 的任何调用一起使用,以提供更详细的输出。如果你想查看 CTest 在不实际运行它们的情况下会运行哪些测试, -N 选项很有用。
在对软件提交任何更改之前,运行测试并确保它们都通过,这是提高软件质量和开发过程的可靠方法。不幸的是,对于大型项目,运行它们所需的测试数量和时间可能令人望而却步。在这些情况下,可以使用 CTest 的 -I 选项。 -I 选项允许你灵活地指定要运行的测试的子集。例如,下面的 CTest 调用将会把每第七个测试运行一次。
ctest -I ,,7
虽然这不如运行每一个测试,但总比不运行好,而且对于许多开发人员来说,这可能是一个更实际的解决方案。注意,如果没有指定 start 和 end 参数(如本例所示),则它们将默认为第一个和最后一个测试。在另一个示例中,假设你总是希望运行一些测试和其他测试的一个子集。这种情况下,你可以显式地将这些测试添加到 -I 参数的末尾。例如:
ctest -I ,,5,1,2,3,10
这将运行测试 1 、2 、 3 和 10 ,以及每隔 5 个的测试。你可以在步距参数之后传递任意多的测试编号。
15.4 使用 CTest 驱动复杂测试
有时为了正确地测试一个项目,你需要在测试阶段实际编译代码。这有几个原因。首先,如果测试程序是作为主项目的一部分编译的,那么它们最终会占用大量的构建时间。另外,如果一个测试构建失败,主构建也不应该失败。最后, IDE 项目可能很快变得太大而无法加载和使用。 CTest 命令支持一组命令行选项,允许将其用作测试可执行文件来运行。当用作测试可执行文件时, CTest 可以运行 CMake ,运行编译步骤,最后运行编译后的测试。现在我们来看看 CTest 的支持构建和运行测试的命令行选项。
--build-and-test src_directory build_directory
Run cmake on the given source directory using the specified build directory.
--test-command Name of the program to run.
--build-target Specify a specific target to build.
--build-nocmake Run the build without running cmake first.
--build-run-dir Specify directory to run programs from.
--build-two-config Run cmake twice before the build.
--build-exe-dir Specify the directory for the executable.
--build-generator Specify the generator to use.
--build-project Specify the name of the project to build.
--build-makeprogram Specify the make program to use.
--build-noclean Skip the make clean step.
--build-options Add extra options to the build step.
例如,考虑以下 add_test 命令,该命令取自 CMake 本身的 CMakeLists.txt 文件。它展示了如何使用 CTest 编译和运行测试。
add_test(simple ${CMAKE_CTEST_COMMAND}
--build-and-test "${CMAKE_SOURCE_DIR}/Tests/Simple"
"${CMAKE_BINARY_DIR}/Tests/Simple"
--build-generator ${CMAKE_GENERATOR}
--build-makeprogram ${CMAKE_MAKE_PROGRAM}
--build-project Simple
--test-command simple)
在本例中,首先向 add_test 命令传递测试的名称“ simple ”。在测试名后面指定要运行的命令。在本例中,要运行的测试命令是 CTest 。 CTest 命令通过 CMAKE_CTEST_COMMAND 变量引用。这个变量总是被 CMake 设置为 CTest 命令,该命令来自用于构建项目的 CMake 安装。接下来,指定源目录和二进制目录。 CTest 的下一个选项是 --build-generator 和 --build-makeprogram 选项。这些是使用 CMake 变量 CMAKE_MAKE_PROGRAM 和 CMAKE_GENERATOR 指定的。 CMAKE_MAKE_PROGRAM 和 CMAKE_GENERATOR 都由 CMake 定义。这是一个重要的步骤,因为它确保用于构建测试的生成器与用于构建项目本身的生成器是相同的。 --build-project 选项被传入了 Simple ,它对应于 Simple 测试中使用的 project 命令。最后一个参数是 --test-command ,它告诉 CTest 一旦成功构建就会运行的命令,这应该是将被测试所编译的可执行文件的名称。
15.5 处理大量测试
当单个项目中存在大量测试时,为每个测试提供单独的可执行程序是非常麻烦的。也就是说,不应该要求项目开发人员创建具有复杂参数解析的测试。这就是为什么 CMake 提供了一个方便的命令来创建测试驱动程序。该命令称为 create_test_sourcelist 。测试驱动程序是将许多小型测试链接到一个可执行文件中的程序。这在使用大型库构建静态可执行程序以缩小所需的总大小时非常有用。 create_test_sourcelist 的签名如下:
create_test_sourcelist (SourceListName
DriverName
test1 test2 test3
EXTRA_INCLUDE include.h
FUNCTION function
)
第一个参数是一个变量,它将包含必须编译的源文件列表,以创建用于测试的可执行程序。 DriverName 是测试驱动程序的名称(例如,生成的可执行文件的名称)。其余的参数是由测试源文件的列表组成。每个测试源文件中都应该有一个函数,该函数的名称与没有扩展名的文件相同( foo.cxx 应该有 int foo(argc, argv); )。生成的可执行文件将能够在命令行上按名称调用每个测试。 EXTRA_INCLUDE 和 FUNCTION 参数支持测试驱动程序的额外定制。考虑下面的 CMakeLists 文件片段,看看如何使用这个命令:
# 创建测试文件和测试列表
set (TestToRun
ObjectFactory.cxx
otherArrays.cxx
otherEmptyCell.cxx
TestSmartPointer.cxx
SystemInformation.cxx
...
)
create_test_sourcelist (Tests CommonCxxTests.cxx ${TestToRun})
# 添加可执行程序
add_executable (CommonCxxTests ${Tests})
# 为每个测试添加所有的 ADD_TEST
foreach (test ${TestsToRun})
get_filename_component (TName ${test} NAME_WE)
add_test (NAME ${TName} COMMAND CommonCxxTests ${TName})
endforeach ()
------------下段英文逻辑有些混乱,与上述代码不相符,因此进行了整理------------
在本例中, TestToRun 变量中存储了测试源文件的列表。然后调用 create_test_sourcelist 命令创建测试驱动程序所需的文件 CommonCxxTests.cxx 到项目的二进制树中,并将创建的文件和 TestToRun 列表合并 ,存放于 Tests 变量中。接下来,使用 add_executable 命令来构建测试驱动程序。然后,使用 foreach 命令遍历测试源文件列表中的源文件,提取其不带文件扩展名的名称并将其放入变量 TName 中,然后添加一个名为 TName 新的测试。最终结果是,对于 TestToRun 中的每个源文件,使用测试名称调用一个 add_test 命令。随着更多的测试被添加到 TestToRun 中, foreach 循环将自动为每个测试调用 add_test 。
15.6 管理测试数据
除了处理大量的测试, CMake 还包含一个管理测试数据的系统。它被封装在 CMake 的 ExternalData 模块中,根据需要下载大量数据,保留版本信息,并允许分布式存储。
ExternalData 的设计遵循分布式版本控制系统的设计,使用基于 Hash 的文件标识符和对象存储,但它也利用了基于依赖的构建系统的存在。下图说明了这种方法。源码树包含轻量级的“内容链接”,通过其内容的 Hash 引用远程存储中的数据。 ExternalData 模块生成构建规则以将数据下载到本地存储,并通过符号链接( Windows 上的副本)从构建树中引用它们。

内容链接是一个小的纯文本文件,包含真实数据的 Hash 。它的名字和它的数据文件一样,有一个额外的扩展名来标识 Hash 算法,例如 img.png.md5 。无论实际数据大小如何,内容链接在源码树中总是占用相同(少量)的空间。 CMake 配置文件在调用 ExternalData 模块 API 时使用 DATA{} 语法来引用数据。例如, DATA{img.png} 告诉 ExternalData 模块使 img.png 在构建树中可用,即使在源码树中只是出现一个 img.png.Md5 的内容链接。
ExternalData 模块实现了一个灵活的系统,以防止内容获取和存储的重复。对象从 ExternalData CMake 配置中指定的(可能冗余的)本地和远程位置列表中来搜索,这个位置列表被称为“ URL 模板”列表。远程存储系统的唯一要求是能够通过指定 Hash 算法和 Hash 值从用于定位内容的 URL 中获取内容。例如,本地或网络文件系统、 Apache FTP 服务器或 Midas 服务器都具有此功能。每个 URL 模板都有 %(algo) 和 %(hash) 占位符, ExternalData 会将其替换为内容链接中的值。
通过设置 ExternalData_OBJECT_STORES 这个 CMake 构建配置变量,持久的本地对象存储可以缓存下载的内容,以便在构建树之间共享。这有助于为多个构建树去除重复内容。它还解决了回归测试环境中一个重要的实用问题:当许多机器同时启动夜间指示板构建时,它们可以使用本地对象存储,而不会导致数据服务器过载和网络流量泛滥。
检索与基于依赖的构建系统集成在一起,因此只在需要时获取资源。例如,如果系统用于检索测试数据,并且 BUILD_TESTING 为 OFF ,则不会检索不必要的数据。当源码树更新并且内容链接更改时,构建系统将根据需要获取新数据。
由于离开源码树的所有引用都要经过 Hash ,因此它们不依赖于任何外部状态。可以重新定位远程和本地对象存储,而不会使旧版本源代码中的内容链接失效。可以在不修改对象存储的情况下重新定位或重命名源码树中的内容链接。重复的内容链接可以存在于源代码树中,但是下载只会发生一次。在项目历史记录中具有相同源码树文件名的数据的多个版本在对象存储区中是唯一标识的。
基于 Hash 的系统允许对远程资源使用不受信任的连接,因为下载的内容会在检索后进行验证。 URL 模板列表的配置通过允许多个冗余远程存储资源来提高健壮性。存储资源也可以根据需要随时间变化。如果项目的远程存储随着时间的推移而移动,则始终可以通过调整为构建树配置的 URL 模板或手动填充本地对象存储来构建较旧的源代码版本。
ExternalData 模块的一个简单应用程序如下:
include(ExternalData)
set(midas "http://midas.kitware.com/MyProject")
# 将标准远程对象存储添加到用户的配置中。
list(APPEND ExternalData_URL_TEMPLATES
"${midas}?algorithm=%(algo)&hash=%(hash)"
"ftp://myproject.org/files/%(algo)/%(hash)"
)
# 添加一个测试引用的数据。
ExternalData_Add_Test(MyProjectData
NAME SmoothingTest
COMMAND SmoothingExe DATA{Input/Image.png}
SmoothedImage.png
)
# 添加一个构建目标来填充实际数据。
ExternalData_Add_Target(MyProjectData)
ExternalData_Add_Test 函数是 CMake 的 add_test 命令的包装器。在源码树被探查到有一个包含了(所需)数据的 MD5 Hash 的 Input/Image.png.md5 内容链接。在检查本地对象存储之后,使用数据的 Hash 按顺序向 ExternalData_URL_TEMPLATES 列表中的每个 URL 发出请求。找到符号链接后,将在构建树中创建符号链接。 DATA{Input/Image.png} 路径将在测试命令行中展开为构建树路径。数据将在构建 MyProjectData 目标时被检索。