CMake 秘籍(三)(1)https://developer.aliyun.com/article/1524615
它是如何工作的
由于 Catch2 是一个单头文件框架,因此不需要定义和构建额外的目标。我们只需要确保 CMake 能够找到catch.hpp
来构建test.cpp
。为了方便,我们将其放置在与test.cpp
相同的目录中,但我们也可以选择不同的位置,并使用target_include_directories
指示该位置。另一种方法是将头文件包装成一个INTERFACE
库。这可以按照 Catch2 文档中的说明进行(https://github.com/catchorg/Catch2/blob/master/docs/build-systems.md#cmake
):
# Prepare "Catch" library for other executables set(CATCH_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/catch) add_library(Catch INTERFACE) target_include_directories(Catch INTERFACE ${CATCH_INCLUDE_DIR})
那么我们将按照以下方式链接库:
target_link_libraries(cpp_test Catch)
我们从第一章,从简单可执行文件到库中的食谱 3,构建和链接静态和共享库的讨论中回忆起,INTERFACE
库是 CMake 提供的伪目标,对于指定项目外部的目标使用要求非常有用。
还有更多
这是一个简单的例子,重点在于 CMake。当然,Catch2 提供了更多功能。要获取 Catch2 框架的完整文档,请访问github.com/catchorg/Catch2
。
另请参阅
Catch2 代码仓库包含一个由贡献的 CMake 函数,用于解析 Catch 测试并自动创建 CMake 测试,而无需显式键入add_test()
函数;请参阅github.com/catchorg/Catch2/blob/master/contrib/ParseAndAddCatchTests.cmake
。
定义单元测试并链接 Google Test
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-03
找到,并包含一个 C++示例。本食谱适用于 CMake 版本 3.11(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。代码仓库还包含一个与 CMake 3.5 兼容的示例。
在本食谱中,我们将演示如何使用 CMake 和 Google Test 框架实现单元测试。与之前的食谱不同,Google Test 框架不仅仅是一个头文件;它是一个包含多个需要构建和链接的文件的库。我们可以将这些文件与我们的代码项目放在一起,但为了让代码项目更轻量级,我们将在配置时下载 Google Test 源代码的明确定义版本,然后构建框架并与之链接。我们将使用相对较新的FetchContent
模块(自 CMake 版本 3.11 起可用)。我们将在第八章,超级构建模式中重新讨论FetchContent
,在那里我们将讨论模块在幕后是如何工作的,以及我们还将说明如何使用ExternalProject_Add
来模拟它。本食谱的灵感来自(并改编自)cmake.org/cmake/help/v3.11/module/FetchContent.html
的示例。
准备工作
我们将保持main.cpp
、sum_integers.cpp
和sum_integers.hpp
与之前的食谱不变,但将更新test.cpp
源代码,如下所示:
#include "sum_integers.hpp" #include "gtest/gtest.h" #include <vector> int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } TEST(example, sum_zero) { auto integers = {1, -1, 2, -2, 3, -3}; auto result = sum_integers(integers); ASSERT_EQ(result, 0); } TEST(example, sum_five) { auto integers = {1, 2, 3, 4, 5}; auto result = sum_integers(integers); ASSERT_EQ(result, 15); }
如前述代码所示,我们选择不在我们的代码项目仓库中显式放置gtest.h
或其他 Google Test 源文件,而是通过使用FetchContent
模块在配置时下载它们。
如何操作
以下步骤描述了如何逐步设置CMakeLists.txt
,以使用 GTest 编译可执行文件及其相应的测试:
CMakeLists.txt
的开头与前两个配方相比大部分未变,只是我们需要 CMake 3.11 以访问FetchContent
模块:
# set minimum cmake version cmake_minimum_required(VERSION 3.11 FATAL_ERROR) # project name and language project(recipe-03 LANGUAGES CXX) # require C++11 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # example library add_library(sum_integers sum_integers.cpp) # main code add_executable(sum_up main.cpp) target_link_libraries(sum_up sum_integers)
- 然后我们引入了一个 if 语句,检查
ENABLE_UNIT_TESTS
。默认情况下它是ON
,但我们希望有可能将其关闭,以防我们没有网络下载 Google Test 源码:
option(ENABLE_UNIT_TESTS "Enable unit tests" ON) message(STATUS "Enable testing: ${ENABLE_UNIT_TESTS}") if(ENABLE_UNIT_TESTS) # all the remaining CMake code will be placed here endif()
- 在 if 语句内部,我们首先包含
FetchContent
模块,声明一个新的要获取的内容,并查询其属性:
include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.8.0 ) FetchContent_GetProperties(googletest)
- 如果内容尚未填充(获取),我们获取并配置它。这将添加一些我们可以链接的目标。在本例中,我们对
gtest_main
感兴趣。该示例还包含一些使用 Visual Studio 编译的解决方法:
if(NOT googletest_POPULATED) FetchContent_Populate(googletest) # Prevent GoogleTest from overriding our compiler/linker options # when building with Visual Studio set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from using PThreads set(gtest_disable_pthreads ON CACHE BOOL "" FORCE) # adds the targers: gtest, gtest_main, gmock, gmock_main add_subdirectory( ${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} ) # Silence std::tr1 warning on MSVC if(MSVC) foreach(_tgt gtest gtest_main gmock gmock_main) target_compile_definitions(${_tgt} PRIVATE "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" ) endforeach() endif() endif()
- 然后我们定义了
cpp_test
可执行目标,并使用target_sources
命令指定其源文件,使用target_link_libraries
命令指定其链接库:
add_executable(cpp_test "") target_sources(cpp_test PRIVATE test.cpp ) target_link_libraries(cpp_test PRIVATE sum_integers gtest_main )
- 最后,我们使用熟悉的
enable_testing
和add_test
命令来定义单元测试:
enable_testing() add_test( NAME google_test COMMAND $<TARGET_FILE:cpp_test> )
- 现在,我们准备好配置、构建和测试项目了:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ctest Test project /home/user/cmake-cookbook/chapter-04/recipe-03/cxx-example/build Start 1: google_test 1/1 Test #1: google_test ...................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.00 sec
- 我们也可以尝试直接运行
cpp_test
,如下所示:
$ ./cpp_test [==========] Running 2 tests from 1 test case. [----------] Global test environment set-up. [----------] 2 tests from example [ RUN ] example.sum_zero [ OK ] example.sum_zero (0 ms) [ RUN ] example.sum_five [ OK ] example.sum_five (0 ms) [----------] 2 tests from example (0 ms total) [----------] Global test environment tear-down [==========] 2 tests from 1 test case ran. (0 ms total) [ PASSED ] 2 tests.
它是如何工作的
FetchContent
模块允许在配置时填充内容,通过任何ExternalProject
模块支持的方法,并且已成为 CMake 3.11 版本的标准部分。而ExternalProject_Add()
在构建时下载(如第八章,超级构建模式所示),FetchContent
模块使内容立即可用,以便主项目和获取的外部项目(在本例中为 Google Test)可以在 CMake 首次调用时处理,并且可以使用add_subdirectory
嵌套。
为了获取 Google Test 源码,我们首先声明了外部内容:
include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.8.0 )
在这种情况下,我们获取了一个带有特定标签(release-1.8.0
)的 Git 仓库,但我们也可以从 Subversion、Mercurial 或 HTTP(S)源获取外部项目。有关可用选项,请参阅cmake.org/cmake/help/v3.11/module/ExternalProject.html
上相应ExternalProject_Add
命令的选项。
我们在调用FetchContent_Populate()
之前使用FetchContent_GetProperties()
命令检查内容填充是否已经处理;否则,如果FetchContent_Populate()
被调用多次,它会抛出一个错误。
FetchContent_Populate(googletest)
命令填充源码并定义googletest_SOURCE_DIR
和googletest_BINARY_DIR
,我们可以使用它们来处理 Google Test 项目(使用add_subdirectory()
,因为它恰好也是一个 CMake 项目):
add_subdirectory( ${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} )
上述定义了以下目标:gtest
、gtest_main
、gmock
和gmock_main
。在本示例中,我们只对gtest_main
目标感兴趣,作为单元测试示例的库依赖项:
target_link_libraries(cpp_test PRIVATE sum_integers gtest_main )
在构建我们的代码时,我们可以看到它如何正确地触发了 Google Test 的配置和构建步骤。有一天,我们可能希望升级到更新的 Google Test 版本,我们可能需要更改的唯一一行是详细说明GIT_TAG
的那一行。
还有更多
我们已经初步了解了FetchContent
及其构建时的表亲ExternalProject_Add
,我们将在第八章,超级构建模式中重新审视这些命令。对于可用选项的详细讨论,请参考cmake.org/cmake/help/v3.11/module/FetchContent.html
。
在本示例中,我们在配置时获取了源代码,但我们也可以在系统环境中安装它们,并使用FindGTest
模块来检测库和头文件(cmake.org/cmake/help/v3.5/module/FindGTest.html
)。从版本 3.9 开始,CMake 还提供了一个GoogleTest
模块(cmake.org/cmake/help/v3.9/module/GoogleTest.html
),该模块提供了一个gtest_add_tests
函数。这个函数可以用来自动添加测试,通过扫描源代码中的 Google Test 宏。
另请参阅
显然,Google Test 有许多超出本示例范围的功能,如github.com/google/googletest
所列。
定义单元测试并链接到 Boost 测试
本示例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-04
找到,并包含一个 C++示例。本示例适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Boost 测试是 C++社区中另一个非常流行的单元测试框架,在本示例中,我们将演示如何使用 Boost 测试对我们的熟悉求和示例代码进行单元测试。
准备工作
我们将保持main.cpp
、sum_integers.cpp
和sum_integers.hpp
与之前的示例不变,但我们将更新test.cpp
作为使用 Boost 测试库的单元测试的简单示例:
#include "sum_integers.hpp" #include <vector> #define BOOST_TEST_MODULE example_test_suite #include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_CASE(add_example) { auto integers = {1, 2, 3, 4, 5}; auto result = sum_integers(integers); BOOST_REQUIRE(result == 15); }
如何操作
以下是使用 Boost 测试构建我们项目的步骤:
- 我们从熟悉的
CMakeLists.txt
结构开始:
# set minimum cmake version cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # project name and language project(recipe-04 LANGUAGES CXX) # require C++11 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) # example library add_library(sum_integers sum_integers.cpp) # main code add_executable(sum_up main.cpp) target_link_libraries(sum_up sum_integers)
- 我们检测 Boost 库并链接
cpp_test
:
find_package(Boost 1.54 REQUIRED COMPONENTS unit_test_framework) add_executable(cpp_test test.cpp) target_link_libraries(cpp_test PRIVATE sum_integers Boost::unit_test_framework ) # avoid undefined reference to "main" in test.cpp target_compile_definitions(cpp_test PRIVATE BOOST_TEST_DYN_LINK )
- 最后,我们定义单元测试:
enable_testing() add_test( NAME boost_test COMMAND $<TARGET_FILE:cpp_test> )
- 以下是我们需要配置、构建和测试代码的所有内容:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ctest Test project /home/user/cmake-recipes/chapter-04/recipe-04/cxx-example/build Start 1: boost_test 1/1 Test #1: boost_test ....................... Passed 0.01 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.01 sec $ ./cpp_test Running 1 test case... *** No errors detected
工作原理
我们使用了find_package
来检测 Boost 的unit_test_framework
组件(请参阅第三章,检测外部库和程序,第八部分,检测 Boost 库)。我们坚持认为这个组件是REQUIRED
,如果无法在系统环境中找到,配置将停止。cpp_test
目标需要知道在哪里找到 Boost 头文件,并需要链接到相应的库;这两者都由IMPORTED
库目标Boost::unit_test_framework
提供,该目标由成功的find_package
调用设置。我们从第一章,从简单可执行文件到库中的第三部分,构建和链接静态和共享库的讨论中回忆起,IMPORTED
库是 CMake 提供的伪目标,用于表示预先存在的依赖关系及其使用要求。
还有更多内容
在本节中,我们假设 Boost 已安装在系统上。或者,我们可以在编译时获取并构建 Boost 依赖项(请参阅第八章,超级构建模式,第二部分,使用超级构建管理依赖项:I. Boost 库)。然而,Boost 不是一个轻量级依赖项。在我们的示例代码中,我们仅使用了最基本的基础设施,但 Boost 提供了丰富的功能和选项,我们将引导感兴趣的读者访问www.boost.org/doc/libs/1_65_1/libs/test/doc/html/index.html
。
使用动态分析检测内存缺陷
本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-05
找到,并提供了一个 C++示例。本节适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
内存缺陷,例如越界写入或读取内存,或者内存泄漏(已分配但从未释放的内存),可能会产生难以追踪的讨厌错误,因此尽早检测它们是有用的。Valgrind(valgrind.org
)是一个流行且多功能的工具,用于检测内存缺陷和内存泄漏,在本节中,我们将使用 Valgrind 来提醒我们使用 CMake/CTest 运行测试时的内存问题(请参阅第十四章,测试仪表板,以讨论相关的AddressSanitizer
和ThreadSanitizer
)。
准备就绪
对于本节,我们需要三个文件。第一个是我们希望测试的实现(我们可以将文件称为leaky_implementation.cpp
):
#include "leaky_implementation.hpp" int do_some_work() { // we allocate an array double *my_array = new double[1000]; // do some work // ... // we forget to deallocate it // delete[] my_array; return 0; }
我们还需要相应的头文件(leaky_implementation.hpp
):
#pragma once int do_some_work();
我们需要测试文件(test.cpp
):
#include "leaky_implementation.hpp" int main() { int return_code = do_some_work(); return return_code; }
我们期望测试通过,因为return_code
被硬编码为0
。然而,我们也希望检测到内存泄漏,因为我们忘记了释放my_array
。
如何操作
以下是如何设置CMakeLists.txt
以执行代码的动态分析:
- 我们首先定义了最低 CMake 版本、项目名称、语言、目标和依赖项:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-05 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(example_library leaky_implementation.cpp)
add_executable(cpp_test test.cpp) target_link_libraries(cpp_test example_library)
- 然后,我们不仅定义了测试,还定义了
MEMORYCHECK_COMMAND
:
find_program(MEMORYCHECK_COMMAND NAMES valgrind) set(MEMORYCHECK_COMMAND_OPTIONS "--trace-children=yes --leak-check=full") # add memcheck test action include(CTest) enable_testing() add_test( NAME cpp_test COMMAND $<TARGET_FILE:cpp_test> )
- 运行测试集报告测试通过,如下所示:
$ ctest Test project /home/user/cmake-recipes/chapter-04/recipe-05/cxx-example/build Start 1: cpp_test 1/1 Test #1: cpp_test ......................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.00 sec
- 现在,我们希望检查内存缺陷,并可以观察到内存泄漏被检测到:
$ ctest -T memcheck Site: myhost Build name: Linux-c++ Create new tag: 20171127-1717 - Experimental Memory check project /home/user/cmake-recipes/chapter-04/recipe-05/cxx-example/build Start 1: cpp_test 1/1 MemCheck #1: cpp_test ......................... Passed 0.40 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.40 sec -- Processing memory checking output: 1/1 MemCheck: #1: cpp_test ......................... Defects: 1 MemCheck log files can be found here: ( * corresponds to test number) /home/user/cmake-recipes/chapter-04/recipe-05/cxx-example/build/Testing/Temporary/MemoryChecker.*.log Memory checking results: Memory Leak - 1
- 作为最后一步,你应该尝试修复内存泄漏,并验证
ctest -T memcheck
报告没有错误。
工作原理
我们使用find_program(MEMORYCHECK_COMMAND NAMES valgrind)
来查找 Valgrind 并将其完整路径设置为MEMORYCHECK_COMMAND
。我们还需要显式包含CTest
模块以启用memcheck
测试动作,我们可以通过使用ctest -T memcheck
来使用它。此外,请注意我们能够使用set(MEMORYCHECK_COMMAND_OPTIONS "--trace-children=yes --leak-check=full")
将选项传递给 Valgrind。内存检查步骤创建一个日志文件,可用于详细检查内存缺陷。
一些工具,如代码覆盖率和静态分析工具,可以类似地设置。然而,使用其中一些工具更为复杂,因为需要专门的构建和工具链。Sanitizers 就是一个例子。有关更多信息,请参阅github.com/arsenm/sanitizers-cmake
。此外,请查看第十四章,测试仪表板,以讨论AddressSanitizer
和ThreadSanitizer
。
还有更多
本食谱可用于向夜间测试仪表板报告内存缺陷,但我们在这里演示了此功能也可以独立于测试仪表板使用。我们将在第十四章,测试仪表板中重新讨论与 CDash 结合使用的情况。
另请参阅
有关 Valgrind 及其功能和选项的文档,请参阅valgrind.org
。
测试预期失败
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-06
找到。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
理想情况下,我们希望我们的所有测试在每个平台上都能始终通过。然而,我们可能想要测试在受控环境中是否会发生预期的失败或异常,在这种情况下,我们将预期的失败定义为成功的结果。我们相信,通常这应该是测试框架(如 Catch2 或 Google Test)的任务,它应该检查预期的失败并将成功报告给 CMake。但是,可能会有情况,你希望将测试的非零返回代码定义为成功;换句话说,你可能想要反转成功和失败的定义。在本节中,我们将展示这样的情况。
准备工作
本节的成分将是一个微小的 Python 脚本(test.py
),它总是返回1
,CMake 将其解释为失败:
import sys # simulate a failing test sys.exit(1)
如何操作
逐步地,这是如何编写CMakeLists.txt
来完成我们的任务:
- 在本节中,我们不需要 CMake 提供任何语言支持,但我们需要找到一个可用的 Python 解释器:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-06 LANGUAGES NONE) find_package(PythonInterp REQUIRED)
- 然后我们定义测试并告诉 CMake 我们期望它失败:
enable_testing() add_test(example ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py) set_tests_properties(example PROPERTIES WILL_FAIL true)
- 最后,我们验证它被报告为成功的测试,如下所示:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ctest Test project /home/user/cmake-recipes/chapter-04/recipe-06/example/build Start 1: example 1/1 Test #1: example .......................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.01 sec
它是如何工作的
使用set_tests_properties(example PROPERTIES WILL_FAIL true)
,我们将属性WILL_FAIL
设置为true
,这会反转成功/失败的状态。然而,这个功能不应该用来临时修复损坏的测试。
还有更多
如果你需要更多的灵活性,你可以结合使用测试属性PASS_REGULAR_EXPRESSION
和FAIL_REGULAR_EXPRESSION
与set_tests_properties
。如果设置了这些属性,测试输出将被检查与作为参数给出的正则表达式列表进行匹配,如果至少有一个正则表达式匹配,则测试分别通过或失败。还有许多其他属性可以设置在测试上。可以在cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#properties-on-tests
找到所有可用属性的完整列表。
CMake 秘籍(三)(3)https://developer.aliyun.com/article/1524621