我已经围绕标准CMake命令编写了一些方便的包装器,并希望对该CMake脚本代码进行单元测试,以确保其功能。
我已经取得了一些进展,但有两件事我希望得到帮助:
- 是否有一些"官方"的方式来单元测试您自己的CMake脚本代码?像是运行CMake的特殊模式吗?我的目标是"白盒测试"(尽可能多)
- 如何处理全局变量和变量范围问题?通过加载项目的缓存、配置测试CMake文件或通过-D命令行选项将其推送,将全局变量注入测试中?变量作用域的模拟/测试(缓存与非缓存、宏/函数/包含、引用传递的参数)
首先,我已经在/Tests下查看了CMake源代码(我使用的是CMake 2.8.10版本),尤其是在Tests/CMakeTests下。有大量的变种可以找到,看起来其中很多都是专门针对单个测试用例的。
因此,我还研究了一些可用的CMake脚本库,如CMake++,以查看他们的解决方案,但当他们进行单元测试时,这些脚本库在很大程度上取决于他们自己的库函数。
这是我当前用于单元测试自己的CMake脚本代码的解决方案。
假设使用CMake脚本处理模式是我的最佳捕获,并且我必须模拟在脚本模式中不可用的CMake命令,到目前为止,我得出了以下结论。
Helper函数
利用我自己的全局属性,我编写了辅助函数来存储和比较函数调用:
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"
)
如何处理全局变量和变量范围问题?通过加载项目的缓存,配置测试CMake文件或通过-D命令行推送它选项
一般来说,所有当前存在的方法(通过缓存、环境变量和-D命令行)在一种或另一种情况下都是一个糟糕的选择,因为它们涉及不可预测的行为。
这是我能回忆起的最少的问题列表:
- 哪个变量可以与另一个变量相交/重叠,何时
- 加载或设置的变量不能在cmake检测阶段之外应用(例如,在cmake脚本模式中)
- 对于不同的操作系统/编译器/配置/体系结构等,同一个唯一变量不能包含不同的值
- 变量不能附加到由
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://github.com/andry81/tacklelib)自己研究实现。
所述配置文件的示例存储在此处:https://github.com/andry81/tacklelib/blob/master/_config
实施:https://github.com/andry81/tacklelib/blob/master/cmake/tacklelib/SetVarsFromFiles.cmake
必须通过configure_environment(...)
宏初始化cmake列表,这是强制性的:https://github.com/andry81/tacklelib/blob/master/CMakeLists.txt
有关tacklelib
项目的详细信息,请阅读自述文件:https://github.com/andry81/tacklelib/blob/master/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
是否存在一些";官方的";单元测试您自己的CMake脚本代码的方式?像是运行CMake的特殊模式吗?我的目标是;白盒测试";(尽可能多)。
我做了我自己的"白盒";或者一种测试我自己脚本的方法。我已经编写了一组模块(它本身依赖于库),以在单独的cmake过程中运行测试:https://github.com/andry81/tacklelib/blob/master/cmake/tacklelib/testlib
我的测试建立在它之上:https://github.com/andry81/tacklelib/blob/master/cmake_tests
其想法是将包含测试的目录和文件的层次结构放入测试目录中,运行程序代码只需按照预定义的顺序搜索测试,即可在单独的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.
#
,其中函数集可以从runner cmake脚本或*.include.cmake
文件中使用:
其中TestLib.cmake
被设计为使用测试模块*.test.cmake
运行循环创建外部cmake进程,并且这些函数应该从runner脚本或include模块(其他include模块组-*.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
子目录中的runner脚本中使用过滤参数来过滤测试:
--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
文件才能运行测试。要以独占方式包含或排除测试,可以开始使用命令行标志--path_match_filter ...
和--test_case_match_filter ...
缺点:
- 大多数测试函数都是通过
function
关键字实现的,这稍微减少了几个函数的功能。例如,tkl_test_assert_true
只能标记测试成功或失败。要显式中断测试,您必须通过调用tkl_return_if_failed
宏进行分支 - 包含测试的目录中的所有文件都必须具有后缀
.test.cmake
(对于测试)和.include.cmake
(对于包含命令)。所有内置的搜索逻辑都依赖于它 - 您已经编写了自己的runner来调用脚本
RunTestLib.cmake
。unix shell上的run all
示例如下:https://github.com/andry81/tacklelib/blob/master/cmake_tests/_build/test_all.sh
整个项目目前都是实验性的