注意
本文最后更新于 2024-02-01,文中内容可能已过时。
一、导言
导言
任何编程语言中,函数允许我们抽象(隐藏)细节并避免代码重复,****CMake
也不例外。我们将以宏和函数为例进行讨论,并介绍一个宏,以便方便地定义测试和设置测试的顺序。我们的目标是定义一个宏,能够替换add_test
和set_tests_properties
,用于定义每组和设置每个测试的预期开销。
二、项目结构
1
2
3
4
5
6
7
8
9
10
11
| .
├── CMakeLists.txt
├── src
│ ├── CMakeLists.txt
│ ├── main.cpp
│ ├── sum_integers.cpp
│ └── sum_integers.hpp
└── tests
├── catch.hpp
├── CMakeLists.txt
└── test.cpp
|
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter7/01
CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
add_subdirectory(src)
enable_testing()
add_subdirectory(tests)
|
tips
1
2
3
4
5
6
7
| include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
|
根据GNU
标准定义binary
和library
路径。
tips
1
2
3
| add_subdirectory(src)
enable_testing()
add_subdirectory(tests)
|
使用add_subdirectory
调用src/CMakeLists.txt
和tests/CMakeLists.txt
。
src/CMakeLists.txt
1
2
3
4
5
6
| set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)
add_library(sum_integers sum_integers.cpp)
add_executable(sum_up main.cpp)
target_link_libraries(sum_up sum_integers)
|
tips
1
| set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)
|
这个命令会将当前目录,添加到CMakeLists.txt
中定义的所有目标的interface_include_directory
属性中。换句话说,我们不需要使用target_include_directory
来添加cpp_test
所需头文件的位置。
src/sun_integers.hpp
1
2
3
4
5
| #ifndef SUM_INTEGERS_H
#define SUM_INTEGERS_H
#include <vector>
int sum_integers(const std::vector<int> &integers);
#endif // ! SUM_INTEGERS_H
|
src/sun_integers.cpp
1
2
3
4
5
6
7
8
9
| #include "sum_integers.hpp"
int sum_integers(const std::vector<int>& integers) {
auto sum = 0;
for (auto i : integers) {
sum += i;
}
return sum;
}
|
src/main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #include <iostream>
#include <string>
#include "sum_integers.hpp"
int main(int argc, char *argv[]) {
std::vector<int> integers;
for (auto i = 1; i < argc; i++) {
integers.push_back(std::stoi(argv[i]));
}
auto sum = sum_integers(integers);
std::cout << sum << std::endl;
return 0;
}
|
tests/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test sum_integers)
macro(add_catch_test _name _cost)
math(EXPR num_macro_calls "${num_macro_calls} + 1")
message(STATUS "add_catch_test called with ${ARGC} arguments: ${ARGV}")
set(_argn "${ARGN}")
if(_argn)
message(STATUS "oops - macro received argument(s) we did not expect: ${ARGN}")
endif()
add_test(
NAME
${_name}
COMMAND
$<TARGET_FILE:cpp_test>
[${_name}] --success --out
${PROJECT_BINARY_DIR}/tests/${_name}.log --durations yes
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
)
set_tests_properties(
${_name}
PROPERTIES
COST ${_cost}
)
endmacro()
set(num_macro_calls 0)
add_catch_test(short 1.5)
add_catch_test(long1 2.5)
add_catch_test(long2 3.0 extra_argument)
message(STATUS "in total there were ${num_macro_calls} calls to add_catch_test")
|
tips
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| macro(add_catch_test _name _cost)
math(EXPR num_macro_calls "${num_macro_calls} + 1")
message(STATUS "add_catch_test called with ${ARGC} arguments: ${ARGV}")
set(_argn "${ARGN}")
if(_argn)
message(STATUS "oops - macro received argument(s) we did not expect: ${ARGN}")
endif()
add_test(
NAME
${_name}
COMMAND
$<TARGET_FILE:cpp_test>
[${_name}] --success --out
${PROJECT_BINARY_DIR}/tests/${_name}.log --durations yes
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
)
set_tests_properties(
${_name}
PROPERTIES
COST ${_cost}
)
endmacro()
|
这个配置中新添加了add_catch_test
宏。这个宏需要两个参数_name
和_cost
,可以在宏中使用这些参数来调用add_test
和set_tests_properties
。参数前面的下划线,是为了表明这些参数只能在宏中访问。另外,宏自动填充了${ARGC}
(参数数量)和${ARGV}
(参数列表),我们可以在输出中验证了这一点:
1
2
| -- add_catch_test called with 2 arguments: short;1.5
-- add_catch_test called with 3 arguments: long;2.5;extra_argument
|
宏还定义了${ARGN}
,用于保存最后一个参数之后的参数列表。此外,我们还可以使用${ARGV0}
、${ARGV1}
等来处理参数。我们演示一下,如何捕捉到调用中的额外参数(extra_argument
):
1
| add_catch_test(long 2.5 extra_argument)
|
使用了以下方法:
1
2
3
4
| set(_argn "${ARGN}")
if(_argn)
message(STATUS "oops - macro received argument(s) we did not expect: ${ARGN}")
endif()
|
这个if
语句中,我们引入一个新变量,但不能直接查询ARGN
,因为它不是通常意义上的CMake
变量。使用这个宏,我们可以通过它们的名称和命令来定义测试,还可以指示预期的开销,这会让耗时长的测试在耗时短测试之前启动,这要归功于COST
属性。
为了演示作用域,我们在定义宏之后编写了以下调用:
1
2
3
4
| set(num_macro_calls 0)
add_catch_test(short 1.5)
add_catch_test(long 2.5 extra_argument)
message(STATUS "in total there were ${num_macro_calls} calls to add_catch_test")
|
在宏内部,将num_macro_calls
加1
:
1
| math(EXPR num_macro_calls "${num_macro_calls} + 1")
|
产生的输出:
1
| -- in total there were 2 calls to add_catch_test
|
tests/test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <vector>
#include "sum_integers.hpp"
// this tells catch to provide a main()
// only do this in one cpp file
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
TEST_CASE("Sum of integers for a short vector", "[shirt]") {
auto integers = {1, 2, 3, 4, 5};
REQUIRE(sum_integers(integers) == 15);
}
TEST_CASE("Sum of integers for a longer vector", "[long]") {
std::vector<int> integers;
for (int i = 1; i < 1001; ++i) {
integers.push_back(i);
}
REQUIRE(sum_integers(integers) == 500500);
}
|
三、结果展示
1
2
3
4
5
6
7
8
9
| $ mkdir -p build
$ cd build
$ cmake ..
-- ...
-- add_catch_test called with 2 arguments: short;1.5
-- add_catch_test called with 3 arguments: long;2.5;extra_argument
-- oops - macro received argument(s) we did not expect: extra_argument
-- in total there were 2 calls to add_catch_test
-- ...
|
构建并运行测试
1
2
| $ cmake --build .
$ ctest
|
测试结果展示
四、补充内容
上述内容中的使用宏定义的方法替换add_test
、add_tests_properties
的方法可以使用一个函数来实现:
1
2
3
| function(add_catch_test _name _cost)
...
endfunction()
|
宏和函数之间的区别在于它们的变量范围。宏在调用者的范围内执行,而函数有自己的变量范围。换句话说,如果我们使用宏,需要设置或修改对调用者可用的变量。如果不去设置或修改输出变量,最好使用函数。我们注意到,可以在函数中修改父作用域变量,但这必须使用PARENT_SCOPE
显式表示:
1
| set(variable_visible_outside "some value" PARENT_SCOPE)
|
如果我们将宏更改为函数,测试仍然可以工作,但是num_macro_calls
在父范围内的所有调用中始终为0。将CMake
宏想象成类似函数是很有用的,这些函数被直接替换到它们被调用的地方(在C
语言中内联)。将CMake
函数想象成黑盒函数很有必要。黑盒中,除非显式地将其定义为PARENT_SCOPE
,否则不会返回任何内容。CMake
中的函数没有返回值。