CMake 秘籍(三)(2)

简介: CMake 秘籍(三)

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.cppsum_integers.cppsum_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 编译可执行文件及其相应的测试:

  1. 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)
  1. 然后我们引入了一个 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()
  1. 在 if 语句内部,我们首先包含FetchContent模块,声明一个新的要获取的内容,并查询其属性:
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.8.0
)
FetchContent_GetProperties(googletest)
  1. 如果内容尚未填充(获取),我们获取并配置它。这将添加一些我们可以链接的目标。在本例中,我们对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()
  1. 然后我们定义了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
  )
  1. 最后,我们使用熟悉的enable_testingadd_test命令来定义单元测试:
enable_testing()
add_test(
  NAME google_test
  COMMAND $<TARGET_FILE:cpp_test>
  )
  1. 现在,我们准备好配置、构建和测试项目了:
$ 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
  1. 我们也可以尝试直接运行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_DIRgoogletest_BINARY_DIR,我们可以使用它们来处理 Google Test 项目(使用add_subdirectory(),因为它恰好也是一个 CMake 项目):

add_subdirectory(
  ${googletest_SOURCE_DIR}
  ${googletest_BINARY_DIR}
  )

上述定义了以下目标:gtestgtest_maingmockgmock_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.cppsum_integers.cppsum_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 测试构建我们项目的步骤:

  1. 我们从熟悉的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)
  1. 我们检测 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
  )
  1. 最后,我们定义单元测试:
enable_testing()
add_test(
  NAME boost_test
  COMMAND $<TARGET_FILE:cpp_test>
  )
  1. 以下是我们需要配置、构建和测试代码的所有内容:
$ 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 运行测试时的内存问题(请参阅第十四章,测试仪表板,以讨论相关的AddressSanitizerThreadSanitizer)。

准备就绪

对于本节,我们需要三个文件。第一个是我们希望测试的实现(我们可以将文件称为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以执行代码的动态分析:

  1. 我们首先定义了最低 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)
  1. 然后,我们不仅定义了测试,还定义了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>
  )
  1. 运行测试集报告测试通过,如下所示:
$ 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
  1. 现在,我们希望检查内存缺陷,并可以观察到内存泄漏被检测到:
$ 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
  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。此外,请查看第十四章,测试仪表板,以讨论AddressSanitizerThreadSanitizer

还有更多

本食谱可用于向夜间测试仪表板报告内存缺陷,但我们在这里演示了此功能也可以独立于测试仪表板使用。我们将在第十四章,测试仪表板中重新讨论与 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来完成我们的任务:

  1. 在本节中,我们不需要 CMake 提供任何语言支持,但我们需要找到一个可用的 Python 解释器:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-06 LANGUAGES NONE)
find_package(PythonInterp REQUIRED)
  1. 然后我们定义测试并告诉 CMake 我们期望它失败:
enable_testing()
add_test(example ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py)
set_tests_properties(example PROPERTIES WILL_FAIL true)
  1. 最后,我们验证它被报告为成功的测试,如下所示:
$ 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_EXPRESSIONFAIL_REGULAR_EXPRESSIONset_tests_properties。如果设置了这些属性,测试输出将被检查与作为参数给出的正则表达式列表进行匹配,如果至少有一个正则表达式匹配,则测试分别通过或失败。还有许多其他属性可以设置在测试上。可以在cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#properties-on-tests找到所有可用属性的完整列表。

CMake 秘籍(三)(3)https://developer.aliyun.com/article/1524621

相关文章
|
5月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
45 2
|
5月前
|
编译器 Shell 开发工具
CMake 秘籍(八)(5)
CMake 秘籍(八)
32 2
|
5月前
|
编译器 Shell
CMake 秘籍(八)(3)
CMake 秘籍(八)
35 2
|
5月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
32 1
|
5月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
32 1
|
5月前
|
Linux C++ iOS开发
CMake 秘籍(三)(4)
CMake 秘籍(三)
31 1
|
5月前
|
编译器 Linux 开发工具
CMake 秘籍(四)(2)
CMake 秘籍(四)
20 0
|
5月前
|
Linux C++ iOS开发
CMake 秘籍(四)(3)
CMake 秘籍(四)
17 0
|
5月前
|
编译器 开发工具
CMake 秘籍(八)(2)
CMake 秘籍(八)
29 0
|
5月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(4)
CMake 秘籍(二)
41 0