目录
本文主要记录CTest的用法,以及常用的内存/线程/CPU检测工具在CMake中的集成和使用。通过ctest --help
可以查询到详细命令信息
CTest
CTest是CMake的测试工具,通过两个命令 enable_testing()
和 add_test()
创建构建树来进行测试,运行ctest后会跑用例并报告结果。
CTest遵循的标准约定是返回零意味着成功,非零为失败。可以返回零或非零的脚本,都可以做测试用例。
流程:
- 编译生成测试程序
- cmake中
include(CTest)
之后,使用命令enable_test()
开启测试 - cmake中使用
add_test
添加测试 - 编译成功后命令行调用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会报错。测试属性有这些,常用属性有以下:
- 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
- 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
- COST:设置测试时间,示例参考并行测试
- LABELS:贴标签,可以在测试程序运行阶段按标签测试,避免不需要运行的测试:
ctest -L <your labels>
- 设置测试固件:
FIXTURES_SETUP, FIXTURES_REQUIRED, FIXTURES_CLEANUP
,示例参考测试固件
运行测试
- 在build目录(构建目录)下运行:
ctest
- 使用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_tests
。gtest_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_PREFIX
和TEST_SUFFIX
用于指定测试名称的前缀和后缀。TEST_LIST
用于返回一个包含所有测试的变量。NO_PRETTY_TYPES
和NO_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()