CMake:如何对自己的CMake脚本宏/函数进行单元测试



我已经围绕标准CMake命令编写了一些方便的包装器,并希望对该CMake脚本代码进行单元测试,以确保其功能。

我已经取得了一些进展,但有两件事我希望得到帮助:

  1. 是否有一些"官方"的方式来单元测试您自己的CMake脚本代码?像是运行CMake的特殊模式吗?我的目标是"白盒测试"(尽可能多)
  2. 如何处理全局变量和变量范围问题?通过加载项目的缓存、配置测试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:lib1LIB1: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/18946
  • Not 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

整个项目目前都是实验性的

相关内容

  • 没有找到相关文章

最新更新