CMake:如何对您自己的 CMake 脚本进行单元测试 Macros/Functions?

CMake: How to Unit-Test your own CMake Script Macros/Functions?

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

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

  1. 是否有一些 "official" 单元测试您自己的 CMake 脚本代码的方法? 运行 CMake 的特殊模式之类的东西?我的目标是"white-box testing"(尽可能多)。
  2. 如何处理全局变量和变量作用域问题?通过加载项目的缓存将全局变量注入测试,配置测试 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:\lib1LIB1: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 上编写解析器脚本是一项艰巨的任务,至少阅读这些问题作为开始:

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

整个项目目前处于实验阶段。