CMake之创建和运行测试


我们承担ROS,FastDDS等通信中间件,C++,cmake等技术的项目开发和专业指导和培训,有10年+相关工作经验,质量有保证,如有需要请私信联系。

本文主要记录CTest的用法,以及常用的内存/线程/CPU检测工具在CMake中的集成和使用。通过ctest --help可以查询到详细命令信息

CTest

CTest是CMake的测试工具,通过两个命令 enable_testing()add_test() 创建构建树来进行测试,运行ctest后会跑用例并报告结果。
CTest遵循的标准约定是返回零意味着成功,非零为失败。可以返回零或非零的脚本,都可以做测试用例。
流程:

  1. 编译生成测试程序
  2. cmake中include(CTest)之后,使用命令enable_test() 开启测试
  3. cmake中使用add_test 添加测试
  4. 编译成功后命令行调用ctest执行测试

命令

  • enable_testing() 测试这个目录和所有子文件夹;这条命令会自动将CTest模块加载进来,除非BUILD_TESTING 选项被设置成off;
  • add_test: 定义一个新的测试,并设置测试名称和运行命令,该测试将被CTest运行。
add_test(
         NAME <name>  # 测试名
         COMMAND <command> [<arg>...]  # 指定测试命令行,以及这个命令的参数
         [CONFIGURATIONS <config>...] 
         [WORKING_DIRECTORY <dir>] # 要执行的测试文件的路径,如果没有指定就按在当前目录下运行
         [COMMAND_EXPAND_LISTS]
         )

示例代码

# 编译的可执行测试程序
add_test(
  NAME cpp_test
  COMMAND $<TARGET_FILE:cpp_test>
)
# 通过sh脚本调用测试程序,生成器表达式为test.sh的脚本参数
add_test(
    NAME bash_test
    COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.sh $<TARGET_FILE:sum_up>
)

${BASH_EXECUTABLE}是通过find_program查找到的bash程序。同理,可以通过python调用(test.py后面的均为脚本参数):

add_test(
    NAME python_test_long
    COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py --executable $<TARGET_FILE:sum_up>
)
  • 为测试设置属性
set_tests_properties(test1 [test2...] PROPERTIES prop1 value1 prop2 value2)

如果没有找到,ctest会报错。测试属性有这些,常用属性有以下:

  1. TIMEOUT:设置测试超时时间
# test.py
import sys
import time
time.sleep(12)
sys.exit(0)
find_package(PythonInterp REQUIRED)
enable_testing()
add_test(example ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py)
set_tests_properties(example PROPERTIES TIMEOUT 10)

超时导致测试fail

  1. WILL_FAIL:转换成功与失败
# test.py
import sys
sys.exit(1)
# CMakeLists.txt
enable_testing()
add_test(example ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py)
set_tests_properties(example PROPERTIES WILL_FAIL true)

将fail转换为pass

  1. COST:设置测试时间,示例参考并行测试
  2. LABELS:贴标签,可以在测试程序运行阶段按标签测试,避免不需要运行的测试:ctest -L <your labels>
  3. 设置测试固件: FIXTURES_SETUP, FIXTURES_REQUIRED, FIXTURES_CLEANUP ,示例参考测试固件

运行测试

  1. 在build目录(构建目录)下运行:ctest
  2. 使用CLI命令:cmake --build . --target test

并行测试

CTest可以支持并行测试

  • 命令行 ctest --parallel N 在N个内核上运行测试集,还可以使用环境变量 CTEST_PARALLEL_LEVEL 将其设置为所需的级别。
    测试python脚本如下:
import sys
import time
time.sleep(0.5) # 消耗时间分别为:a、b、c、d为0.5,e、f、g为1.5,h为2.5,i为3.5,j为4.5
sys.exit(0)
enable_testing()
add_test(a ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py)
...
add_test(j ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/j.py)

ctest:
在这里插入图片描述
ctest --parallel 4
在这里插入图片描述
从验证结果可以得出:
耗时最长的几个测试同时进行并最开始执行,最后运行最短的测试,总测试时间会显著减少。

  • 通过属性指定测试耗时时间
add_test(a ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py)
...
add_test(d ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/d.py)
set_tests_properties(a b c d PROPERTIES COST 0.5)
...
add_test(j ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/j.py)
set_tests_properties(j PROPERTIES COST 4.5)
  • 但这种方式也是序列化执行,和第一种方式一样,耗时时间也很长,实测比第一种方式时间更长一点

运行子集

import sys
import time
time.sleep(0.1) # sleep时间根据情况改动
sys.exit(0)
enable_testing()
add_test(
  NAME feature-a
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py
  )
add_test(
  NAME feature-b
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/b.py
  )
add_test(
  NAME feature-c
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/c.py
  )
add_test(
  NAME benchmark-d
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/d.py
  )
add_test(
  NAME benchmark-e
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/e.py
  )
add_test(
  NAME benchmark-f
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/f.py
  )
set_tests_properties(
    feature-a feature-b feature-c PROPERTIES LABELS "quick"
)
set_tests_properties(
    benchmark-d benchmark-e benchmark-f PROPERTIES LABELS "long"
)

通过属性LABELS 指定子集,可以通过ctest -L指定要运行的子集,这样其他部分不需要执行,如执行 ctest -L long则只运行benchmark-d、benchmark-e、benchmark-f这三个测试。
另外,还可以通过ctest -R指定名称运行测试,如ctest -R feature只运行feature*相关的几个测试。

测试固件

enable_testing()
add_test(
  NAME setup
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/a.py
)
set_tests_properties(
  setup PROPERTIES FIXTURES_SETUP my-fixture
)
# 以上 为setup属性设置为FIXTURES_SETUP 
add_test(
  NAME feature-b
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/b.py
)
add_test(
  NAME feature-c
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/c.py
)
set_tests_properties(
  feature-b feature-c PROPERTIES FIXTURES_REQUIRED my-fixture
)
# 将feature-b和feature-c设置为FIXTURES_REQUIRED 
add_test(
  NAME cleanup
  COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/d.py
)
set_tests_properties(
  cleanup PROPERTIES FIXTURES_CLEANUP my-fixture
)
# 为cleanup设置为cleanup

使用测试固件后不管是运行 ctest (测试全部)还是运行 ctest -R feature-a(运行部分测试)都会先后执行预设的部分,类似gtest中的Setup和TearDown

使用GoogleTest

构建

本地gtest构建

这种方式是先在本地构建GoogleTest,然后在target_include_directories和target_link_libraries等命令中指定头文件和库的路径,使用举例如下:

target_include_directories(
                     ${TARGET}
                     PUBLIC
                     ${GTEST_DIR}/googletest-1.13.0/googletest/include
                     ${GTEST_DIR}/googletest-1.13.0/googlemock/include
                     # ...
                     )

target_link_libraries(
                      ${TARGET}
                      PUBLIC 
                      ${GTEST_DIR}/googletest-1.13.0/build/lib/libgtest.a
                      )  # 这一行必须要指定到.a才有效

这样在编译目标可执行文件时就可以本地查看和链接googltest相关文件了

构建时下载

使用FetchContent模块在项目构建时下载。CMakeLists.txt中写法如下:
首先,包含FetchContent模块,并使用FetchContent模块命令下载指定的googletest:

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG origin/main
)

FetchContent_MakeAvailable(googletest)

然后,在链接时直接使用gtest_main,gtest, gmock_main, gmock库,不需要在target_include_directories中指定头文件的目录

add_executable(${TARGET_NAME})
target_link_libraries(
    ${TARGET_NAME}
    PUBLIC
    gtest_main
)

测试

使用ctest

继续使用ctest测试gtest的ut用例,cmake写法如下:

include(CTest)
enable_testing()
add_test(
    NAME ${TEST_EXE}
    COMMAND $<TARGET_FILE:${TEST_NAME}>
)

这种方式在运行ctest之后,不管有几条用例,输出结果只有一条:

aa@gg:~/data/ilearning/code/study/CMake/project_cmaketest/build$ ctest
Test project /home/aa/data/ilearning/code/study/CMake/project_cmaketest/build
    Start 1: ctest_test
1/1 Test #1: ctest_test..................   Passed    0.01 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

使用gtest

这种方式使用GoogleTest模块提供的gtest_discover_testsgtest_discover_tests是CMake中用于自动发现GoogleTest测试的函数,它会自动扫描目标二进制文件中的测试,并为每个测试生成一个CMake测试。完整签名如下:

gtest_discover_tests(<test_target>
                     [TEST_PREFIX <prefix>]
                     [TEST_SUFFIX <suffix>]
                     [TEST_LIST <list_var>]
                     [NO_PRETTY_TYPES]
                     [NO_PRETTY_VALUES]
                     [PROPERTIES <prop1> <value1>...]
                     [DISCOVERY_TIMEOUT <timeout>]
                     [XML_OUTPUT_DIR <xml_output_dir>]
                     [WORKING_DIRECTORY <dir>]
                     )

其中,

  • <test_target> 是目标测试二进制文件。
  • TEST_PREFIXTEST_SUFFIX 用于指定测试名称的前缀和后缀。
  • TEST_LIST 用于返回一个包含所有测试的变量。
  • NO_PRETTY_TYPESNO_PRETTY_VALUES 用于关闭测试名称的美化。
  • PROPERTIES 用于设置测试属性。
  • DISCOVERY_TIMEOUT 用于设置发现测试的超时时间。
  • XML_OUTPUT_DIR 用于设置XML测试报告的输出目录。
  • WORKING_DIRECTORY 用于设置测试的工作目录。

CMakeLists.txt中写法如下:

enable_testing()
include(GoogleTest)
gtest_discover_tests(${TEST_NAME})  # TEST_NAME即为测试用例代码编译的可执行文件

在运行ctest之后,会输出每条测试用例的结果,同单独运行测试程序结果一样,如下所示:

aa@gg:~/data/ilearning/code/study/CMake/project_cmaketest/build$ ctest
Test project /home/aa/data/ilearning/code/study/CMake/project_cmaketest/build
      Start  1: OneTest.funca_test
 1/36 Test  #1: OneTest.funca_test...................   Passed    0.01 sec
      Start  2: OneTest.funca_test_01
 2/36 Test  #2: OneTest.funca_test_01................   Passed    0.01 sec
      Start  3: OneTest.funca_test_02
 3/36 Test  #3: OneTest.funca_test_02................   Passed    0.01 sec
      Start  4: OneTest.funcb_test
 4/36 Test  #4: OneTest.funcb_test...................   Passed    0.01 sec
...

使用valgrind检测内存

一些工具如代码覆盖率和静态分析工具,可以进行类似地设置,一些使用更为复杂的工具需要专门的构建和工具链,如valgrind, Sanitizers
valgrind用法示例:

...
find_program(MEMORYCHECK_COMMAND NAMES valgrind)
set(MEMORYCHECK_COMMAND_OPTIONS "--trace-children=yes --leak-check=full")
include(CTest)
enable_testing()
add_test(
    NMAE cpp_test
    COMMAND $<TARGET_FILE:cpp_test>
)

在构建目录下命令行运行:ctest -T memcheck 检测内存问题

使用ASAN检测内存

cmake只需添加如下代码即可:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")  # 如果是C语言用CMAKE_C_FLAGS替代CMAKE_CXX_FLAGS

通常只在调试阶段启用它,而在发布版本中禁用它。可以使用CMake的CMAKE_BUILD_TYPE变量来区分这两种情况

if (CMAKE_BUILD_TYPE STREQUAL "Debug")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")  # 这一行不知道有什么用
endif()

检测内容可以参考这里

使用TSAN检测线程

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值