CMake:如何对您自己的 CMake 脚本进行单元测试 Macros/Functions?
CMake: How to Unit-Test your own CMake Script Macros/Functions?
我已经围绕标准 CMake 命令编写了一些方便的包装器,并希望对该 CMake 脚本代码进行单元测试以确保其功能。
我已经取得了一些进步,但有两件事我希望得到帮助:
- 是否有一些 "official" 单元测试您自己的 CMake 脚本代码的方法? 运行 CMake 的特殊模式之类的东西?我的目标是"white-box testing"(尽可能多)。
- 如何处理全局变量和变量作用域问题?通过加载项目的缓存将全局变量注入测试,配置测试 CMake 文件或通过 -D 命令行选项推送它? Simulation/Testing 个变量范围(缓存与非缓存,macros/functions/includes,通过引用传递的参数)?
首先,我查看了 /Tests 下的 CMake 源代码(我使用的是 CMake 版本 2.8.10),尤其是在 Tests/CMakeTests 下。可以找到大量的变体,看起来很多变体专门针对单个测试用例。
所以我也查看了一些可用的 CMake 脚本库,例如 CMake++ 以查看他们的解决方案,但是那些 - 当他们进行单元测试时 - 在很大程度上取决于他们自己的库函数。
这是我当前对自己的 CMake 脚本代码进行单元测试的解决方案。
假设使用 CMake 脚本处理模式是我的最佳选择,并且我必须模拟在脚本模式下不可用的 CMake 命令,我 - 到目前为止 - 想到了以下内容。
辅助函数
利用我自己的全局属性,我编写了辅助函数来存储和比较函数调用:
function(cmakemocks_clearlists _message_type)
_get_property(_list_names GLOBAL PROPERTY MockLists)
if (NOT "${_list_names}" STREQUAL "")
foreach(_name IN ITEMS ${_list_names})
_get_property(_list_values GLOBAL PROPERTY ${_name})
if (NOT "${_list_values}" STREQUAL "")
foreach(_value IN ITEMS ${_list_values})
_message(${_message_type} "cmakemocks_clearlists(${_name}): \"${_value}\"")
endforeach()
endif()
_set_property(GLOBAL PROPERTY ${_name} "")
endforeach()
endif()
endfunction()
function(cmakemocks_pushcall _name _str)
_message("cmakemocks_pushcall(${_name}): \"${_str}\"")
_set_property(GLOBAL APPEND PROPERTY MockLists "${_name}")
_set_property(GLOBAL APPEND PROPERTY ${_name} "${_str}")
endfunction()
function(cmakemocks_popcall _name _str)
_get_property(_list GLOBAL PROPERTY ${_name})
set(_idx -1)
list(FIND _list "${_str}" _idx)
if ((NOT "${_list}" STREQUAL "") AND (NOT ${_idx} EQUAL -1))
_message("cmakemocks_popcall(${_name}): \"${_str}\"")
list(REMOVE_AT _list ${_idx})
_set_property(GLOBAL PROPERTY ${_name} ${_list})
else()
_message(FATAL_ERROR "cmakemocks_popcall(${_name}): No \"${_str}\"")
endif()
endfunction()
function(cmakemocks_expectcall _name _str)
_message("cmakemocks_expectcall(${_name}): \"${_str}\" -> \"${ARGN}\"")
_set_property(GLOBAL APPEND PROPERTY MockLists "${_name}")
string(REPLACE ";" "|" _value_str "${ARGN}")
_set_property(GLOBAL APPEND PROPERTY ${_name} "${_str} <<<${_value_str}>>>")
endfunction()
function(cmakemocks_getexpect _name _str _ret)
if(NOT DEFINED ${_ret})
_message(SEND_ERROR "cmakemocks_getexpect: ${_ret} given as _ret parameter in not a defined variable. Please specify a proper variable name as parameter.")
endif()
_message("cmakemocks_getexpect(${_name}): \"${_str}\"")
_get_property(_list_values GLOBAL PROPERTY ${_name})
set(_value_str "")
foreach(_value IN ITEMS ${_list_values})
set(_idx -1)
string(FIND "${_value}" "${_str}" _idx)
if ((NOT "${_value}" STREQUAL "") AND (NOT ${_idx} EQUAL -1))
list(REMOVE_ITEM _list_values "${_value}")
_set_property(GLOBAL PROPERTY ${_name} ${_list_values})
string(FIND "${_value}" "<<<" _start)
string(FIND "${_value}" ">>>" _end)
math(EXPR _start "${_start} + 3")
math(EXPR _len "${_end} - ${_start}")
string(SUBSTRING "${_value}" ${_start} ${_len} _value_str)
string(REPLACE "|" ";" _value_list "${_value_str}")
set(${_ret} "${_value_list}" PARENT_SCOPE)
break()
endif()
endforeach()
endfunction()
模型
通过添加像这样的模型:
macro(add_library)
string(REPLACE ";" " " _str "${ARGN}")
cmakemocks_pushcall(MockLibraries "${_str}")
endmacro()
macro(get_target_property _var)
string(REPLACE ";" " " _str "${ARGN}")
set(${_var} "[NULL]")
cmakemocks_getexpect(MockGetTargetProperties "${_str}" ${_var})
endmacro()
测试
我可以这样写一个测试:
MyUnitTests.cmake
cmakemocks_expectcall(MockGetTargetProperties "MyLib TYPE" "STATIC_LIBRARY")
my_add_library(MyLib "src/Test1.cc")
cmakemocks_popcall(MockLibraries "MyLib src/Test1.cc")
...
cmakemocks_clearlists(STATUS)
并将其包含到我的 CMake 项目中:
CMakeLists.txt
add_test(
NAME TestMyCMake
COMMAND ${CMAKE_COMMAND} -P "MyUnitTests.cmake"
)
How do I handle the global variables and the variable scopes issues?
Inject Global variables into the test via loading the a project's
cache, configure the test CMake file or pushing it via -D command line
option?
一般来说,所有当前存在的方法(通过缓存、通过环境变量和通过 -D 命令行)在某种情况下都是一个糟糕的选择,因为涉及不可预测的行为。
这是我至少记得的问题列表:
- 哪个变量可以 intersect/overlap 另一个和什么时候?
- 无法在 cmake 检测阶段(例如,在 cmake 脚本模式下)应用变量加载或设置。
- 同一个唯一变量不能为不同的 OS/compilers/configurations/architectures 等保存不同的值。
- 变量不能附加到由
Find*
或 add_subdirectory
等系统函数表示的包(非作用域)术语。
我在 cmake 列表中使用了很长时间的变量,并决定编写我自己的解决方案以一次性将它们全部从 cmake 列表中删除。
我们的想法是通过 cmake 脚本编写一个独立的解析器,以从一个文件或一组文件中加载变量,并定义一组规则以启用以预定义或严格顺序设置的变量,并检查冲突和重叠。
这里列出了几个功能:
bool A=ON
等于bool A=TRUE
等于bool A=1
path B="c:\abc"
等于 Windows 到 path B="C:\ABC"
(显式 path
变量而不是默认的字符串)
B_ROOT="c:\abc"
等于 Windows 到 B_ROOT="C:\ABC"
(通过变量名结尾的变量类型检测)
LIB1:WIN="c:\lib1"
仅在 Windows 中设置,而 LIB1:UNIX="/lib/lib1"
仅在 Unix 中设置(变量专门化)。
LIB1:WIN=c:\lib1
、LIB1:WIN:MSVC:RELEASE=$/{LIB1}\msvc_release
- 通过扩展和专业化重用变量
这里不能说全,大家可以以tacklelib
库(https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/)为例,自行研究实现。
描述的配置文件示例存储在这里:https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/_config/
实施:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake/tacklelib/SetVarsFromFiles.cmake
必须通过 configure_environment(...)
宏初始化 cmake 列表:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/CMakeLists.txt
阅读自述文件以了解有关 tacklelib
项目的详细信息:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/README_EN.txt
整个项目目前处于实验阶段。
PS:
在 cmake 上编写解析器脚本是一项艰巨的任务,至少阅读这些问题作为开始:
;-escape list implicit unescaping
: https://gitlab.kitware.com/cmake/cmake/issues/18946
Not paired
]or
[ characters breaks "file(STRINGS"
: https://gitlab.kitware.com/cmake/cmake/issues/19156
Is there some "official" way of unit-testing your own CMake script code?
Something like a special mode to run CMake in? My goal is "white-box testing" (as much as possible).
我做了自己的“白盒”或测试自己脚本的方法。我已经编写了一组模块(它们本身依赖于库)以在单独的 cmake 进程中进行 运行 测试:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake/tacklelib/testlib/
我的测试基于它:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/
想法是在测试目录中放入包含测试的目录和文件的层次结构,运行ner 代码将按预定义的顺序搜索测试,以在单独的 cmake 进程中执行每个测试:
function(tkl_testlib_enter_dir test_dir)
# Algorithm:
# 1. Iterate not recursively over all `*.include.cmake` files and
# call to `tkl_testlib_include` function on each file, then
# if at least one is iterated then
# exit the algorithm.
# 2. Iterate non recursively over all subdirectories and
# call to the algorithm recursively on each subdirectory, then
# continue.
# 3. Iterate not recursively over all `*.test.cmake` files and
# call to `tkl_testlib_test` function on each file, then
# exit the algorithm.
#
,其中一组函数可以从 运行ner cmake 脚本或 *.include.cmake
文件中使用:
其中 TestLib.cmake
旨在 运行 使用测试模块循环创建外部 cmake 进程 - *.test.cmake
并且应从 运行ner 脚本调用这些函数或来自包含模块(其他包含模块组 - *.include.cmake
或测试模块 - *.test.cmake
):
tkl_testlib_enter_dir test_dir
tkl_testlib_include test_dir test_file_name
tkl_testlib_test test_dir test_file_name
其中 TestModule.cmake
自动包含在您必须放置测试代码的所有 *.test.cmake
模块中。
之后,您只需在 *.test.cmake
模块中使用 tkl_test_assert_true
即可将测试标记为成功或失败。
此外,您可以在 _scripts
子目录中的 运行ner 脚本中使用过滤器参数来过滤测试:
--path_match_filter <[+]regex_include_match_expression> | <-regex_exclude_match_expression>[;...]
--test_case_match_filter <[+]regex_include_match_expression> | <-regex_exclude_match_expression>[;...]
优点:
TestModule.cmake
确实通过预定义的规则遍历整个目录进行测试,您只需要确保正确的层次结构和命名即可对测试进行排序。
- 使用每个目录的基础单独包含文件
*.include.cmake
以独占包含或重新排序目录及其后代中的测试。
*.test.cmake
文件的存在是将测试默认设置为 运行ning 的唯一要求。要专门包含或排除测试,您可以开始使用命令行标志 --path_match_filter ...
和 --test_case_match_filter ...
.
缺点:
- 大部分测试函数都是通过
function
关键字实现的,这有点减少了几个函数的功能。例如,tkl_test_assert_true
只能标记测试成功或失败。要显式中断测试,您必须通过调用 tkl_return_if_failed
宏进行分支。
- 包含测试的目录中的所有文件都必须具有后缀,
.test.cmake
- 用于测试,.include.cmake
- 用于包含命令。所有内置搜索逻辑都依赖于它。
- 您已经编写了自己的 运行ner 来调用脚本
RunTestLib.cmake
。 Unix shell 上的 run all
示例可在此处找到:https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/_build/test_all.sh
整个项目目前处于实验阶段。
我已经围绕标准 CMake 命令编写了一些方便的包装器,并希望对该 CMake 脚本代码进行单元测试以确保其功能。
我已经取得了一些进步,但有两件事我希望得到帮助:
- 是否有一些 "official" 单元测试您自己的 CMake 脚本代码的方法? 运行 CMake 的特殊模式之类的东西?我的目标是"white-box testing"(尽可能多)。
- 如何处理全局变量和变量作用域问题?通过加载项目的缓存将全局变量注入测试,配置测试 CMake 文件或通过 -D 命令行选项推送它? Simulation/Testing 个变量范围(缓存与非缓存,macros/functions/includes,通过引用传递的参数)?
首先,我查看了 /Tests 下的 CMake 源代码(我使用的是 CMake 版本 2.8.10),尤其是在 Tests/CMakeTests 下。可以找到大量的变体,看起来很多变体专门针对单个测试用例。
所以我也查看了一些可用的 CMake 脚本库,例如 CMake++ 以查看他们的解决方案,但是那些 - 当他们进行单元测试时 - 在很大程度上取决于他们自己的库函数。
这是我当前对自己的 CMake 脚本代码进行单元测试的解决方案。
假设使用 CMake 脚本处理模式是我的最佳选择,并且我必须模拟在脚本模式下不可用的 CMake 命令,我 - 到目前为止 - 想到了以下内容。
辅助函数
利用我自己的全局属性,我编写了辅助函数来存储和比较函数调用:
function(cmakemocks_clearlists _message_type)
_get_property(_list_names GLOBAL PROPERTY MockLists)
if (NOT "${_list_names}" STREQUAL "")
foreach(_name IN ITEMS ${_list_names})
_get_property(_list_values GLOBAL PROPERTY ${_name})
if (NOT "${_list_values}" STREQUAL "")
foreach(_value IN ITEMS ${_list_values})
_message(${_message_type} "cmakemocks_clearlists(${_name}): \"${_value}\"")
endforeach()
endif()
_set_property(GLOBAL PROPERTY ${_name} "")
endforeach()
endif()
endfunction()
function(cmakemocks_pushcall _name _str)
_message("cmakemocks_pushcall(${_name}): \"${_str}\"")
_set_property(GLOBAL APPEND PROPERTY MockLists "${_name}")
_set_property(GLOBAL APPEND PROPERTY ${_name} "${_str}")
endfunction()
function(cmakemocks_popcall _name _str)
_get_property(_list GLOBAL PROPERTY ${_name})
set(_idx -1)
list(FIND _list "${_str}" _idx)
if ((NOT "${_list}" STREQUAL "") AND (NOT ${_idx} EQUAL -1))
_message("cmakemocks_popcall(${_name}): \"${_str}\"")
list(REMOVE_AT _list ${_idx})
_set_property(GLOBAL PROPERTY ${_name} ${_list})
else()
_message(FATAL_ERROR "cmakemocks_popcall(${_name}): No \"${_str}\"")
endif()
endfunction()
function(cmakemocks_expectcall _name _str)
_message("cmakemocks_expectcall(${_name}): \"${_str}\" -> \"${ARGN}\"")
_set_property(GLOBAL APPEND PROPERTY MockLists "${_name}")
string(REPLACE ";" "|" _value_str "${ARGN}")
_set_property(GLOBAL APPEND PROPERTY ${_name} "${_str} <<<${_value_str}>>>")
endfunction()
function(cmakemocks_getexpect _name _str _ret)
if(NOT DEFINED ${_ret})
_message(SEND_ERROR "cmakemocks_getexpect: ${_ret} given as _ret parameter in not a defined variable. Please specify a proper variable name as parameter.")
endif()
_message("cmakemocks_getexpect(${_name}): \"${_str}\"")
_get_property(_list_values GLOBAL PROPERTY ${_name})
set(_value_str "")
foreach(_value IN ITEMS ${_list_values})
set(_idx -1)
string(FIND "${_value}" "${_str}" _idx)
if ((NOT "${_value}" STREQUAL "") AND (NOT ${_idx} EQUAL -1))
list(REMOVE_ITEM _list_values "${_value}")
_set_property(GLOBAL PROPERTY ${_name} ${_list_values})
string(FIND "${_value}" "<<<" _start)
string(FIND "${_value}" ">>>" _end)
math(EXPR _start "${_start} + 3")
math(EXPR _len "${_end} - ${_start}")
string(SUBSTRING "${_value}" ${_start} ${_len} _value_str)
string(REPLACE "|" ";" _value_list "${_value_str}")
set(${_ret} "${_value_list}" PARENT_SCOPE)
break()
endif()
endforeach()
endfunction()
模型
通过添加像这样的模型:
macro(add_library)
string(REPLACE ";" " " _str "${ARGN}")
cmakemocks_pushcall(MockLibraries "${_str}")
endmacro()
macro(get_target_property _var)
string(REPLACE ";" " " _str "${ARGN}")
set(${_var} "[NULL]")
cmakemocks_getexpect(MockGetTargetProperties "${_str}" ${_var})
endmacro()
测试
我可以这样写一个测试:
MyUnitTests.cmake
cmakemocks_expectcall(MockGetTargetProperties "MyLib TYPE" "STATIC_LIBRARY")
my_add_library(MyLib "src/Test1.cc")
cmakemocks_popcall(MockLibraries "MyLib src/Test1.cc")
...
cmakemocks_clearlists(STATUS)
并将其包含到我的 CMake 项目中:
CMakeLists.txt
add_test(
NAME TestMyCMake
COMMAND ${CMAKE_COMMAND} -P "MyUnitTests.cmake"
)
How do I handle the global variables and the variable scopes issues? Inject Global variables into the test via loading the a project's cache, configure the test CMake file or pushing it via -D command line option?
一般来说,所有当前存在的方法(通过缓存、通过环境变量和通过 -D 命令行)在某种情况下都是一个糟糕的选择,因为涉及不可预测的行为。
这是我至少记得的问题列表:
- 哪个变量可以 intersect/overlap 另一个和什么时候?
- 无法在 cmake 检测阶段(例如,在 cmake 脚本模式下)应用变量加载或设置。
- 同一个唯一变量不能为不同的 OS/compilers/configurations/architectures 等保存不同的值。
- 变量不能附加到由
Find*
或add_subdirectory
等系统函数表示的包(非作用域)术语。
我在 cmake 列表中使用了很长时间的变量,并决定编写我自己的解决方案以一次性将它们全部从 cmake 列表中删除。
我们的想法是通过 cmake 脚本编写一个独立的解析器,以从一个文件或一组文件中加载变量,并定义一组规则以启用以预定义或严格顺序设置的变量,并检查冲突和重叠。
这里列出了几个功能:
bool A=ON
等于bool A=TRUE
等于bool A=1
path B="c:\abc"
等于 Windows 到path B="C:\ABC"
(显式path
变量而不是默认的字符串)B_ROOT="c:\abc"
等于 Windows 到B_ROOT="C:\ABC"
(通过变量名结尾的变量类型检测)LIB1:WIN="c:\lib1"
仅在 Windows 中设置,而LIB1:UNIX="/lib/lib1"
仅在 Unix 中设置(变量专门化)。LIB1:WIN=c:\lib1
、LIB1:WIN:MSVC:RELEASE=$/{LIB1}\msvc_release
- 通过扩展和专业化重用变量
这里不能说全,大家可以以tacklelib
库(https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/)为例,自行研究实现。
描述的配置文件示例存储在这里:https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/_config/
实施: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake/tacklelib/SetVarsFromFiles.cmake
必须通过 configure_environment(...)
宏初始化 cmake 列表:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/CMakeLists.txt
阅读自述文件以了解有关 tacklelib
项目的详细信息:
https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/README_EN.txt
整个项目目前处于实验阶段。
PS: 在 cmake 上编写解析器脚本是一项艰巨的任务,至少阅读这些问题作为开始:
;-escape list implicit unescaping
: https://gitlab.kitware.com/cmake/cmake/issues/18946Not paired
]or
[characters breaks "file(STRINGS"
: https://gitlab.kitware.com/cmake/cmake/issues/19156
Is there some "official" way of unit-testing your own CMake script code? Something like a special mode to run CMake in? My goal is "white-box testing" (as much as possible).
我做了自己的“白盒”或测试自己脚本的方法。我已经编写了一组模块(它们本身依赖于库)以在单独的 cmake 进程中进行 运行 测试: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake/tacklelib/testlib/
我的测试基于它: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/
想法是在测试目录中放入包含测试的目录和文件的层次结构,运行ner 代码将按预定义的顺序搜索测试,以在单独的 cmake 进程中执行每个测试:
function(tkl_testlib_enter_dir test_dir)
# Algorithm:
# 1. Iterate not recursively over all `*.include.cmake` files and
# call to `tkl_testlib_include` function on each file, then
# if at least one is iterated then
# exit the algorithm.
# 2. Iterate non recursively over all subdirectories and
# call to the algorithm recursively on each subdirectory, then
# continue.
# 3. Iterate not recursively over all `*.test.cmake` files and
# call to `tkl_testlib_test` function on each file, then
# exit the algorithm.
#
,其中一组函数可以从 运行ner cmake 脚本或 *.include.cmake
文件中使用:
其中 TestLib.cmake
旨在 运行 使用测试模块循环创建外部 cmake 进程 - *.test.cmake
并且应从 运行ner 脚本调用这些函数或来自包含模块(其他包含模块组 - *.include.cmake
或测试模块 - *.test.cmake
):
tkl_testlib_enter_dir test_dir
tkl_testlib_include test_dir test_file_name
tkl_testlib_test test_dir test_file_name
其中 TestModule.cmake
自动包含在您必须放置测试代码的所有 *.test.cmake
模块中。
之后,您只需在 *.test.cmake
模块中使用 tkl_test_assert_true
即可将测试标记为成功或失败。
此外,您可以在 _scripts
子目录中的 运行ner 脚本中使用过滤器参数来过滤测试:
--path_match_filter <[+]regex_include_match_expression> | <-regex_exclude_match_expression>[;...]
--test_case_match_filter <[+]regex_include_match_expression> | <-regex_exclude_match_expression>[;...]
优点:
TestModule.cmake
确实通过预定义的规则遍历整个目录进行测试,您只需要确保正确的层次结构和命名即可对测试进行排序。- 使用每个目录的基础单独包含文件
*.include.cmake
以独占包含或重新排序目录及其后代中的测试。 *.test.cmake
文件的存在是将测试默认设置为 运行ning 的唯一要求。要专门包含或排除测试,您可以开始使用命令行标志--path_match_filter ...
和--test_case_match_filter ...
.
缺点:
- 大部分测试函数都是通过
function
关键字实现的,这有点减少了几个函数的功能。例如,tkl_test_assert_true
只能标记测试成功或失败。要显式中断测试,您必须通过调用tkl_return_if_failed
宏进行分支。 - 包含测试的目录中的所有文件都必须具有后缀,
.test.cmake
- 用于测试,.include.cmake
- 用于包含命令。所有内置搜索逻辑都依赖于它。 - 您已经编写了自己的 运行ner 来调用脚本
RunTestLib.cmake
。 Unix shell 上的run all
示例可在此处找到:https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/_build/test_all.sh
整个项目目前处于实验阶段。