【问题标题】:CMake: How to Unit-Test your own CMake Script Macros/Functions?CMake:如何对自己的 CMake 脚本宏/功能进行单元测试?
【发布时间】:2015-04-23 09:08:16
【问题描述】:

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

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

  1. 是否有一些“官方”方法可以对您自己的 CMake 脚本代码进行单元测试?类似于运行 CMake 的特殊模式?我的目标是“白盒测试”(尽可能)。
  2. 如何处理全局变量和变量范围问题?通过加载项目的缓存、配置测试 CMake 文件或通过 -D 命令行选项将其推送到测试中,将全局变量注入测试?变量范围的模拟/测试(缓存与非缓存、宏/函数/包含、引用传递的参数)?

首先,我查看了 /Tests 下,尤其是 Tests/CMakeTests 下的 CMake 源代码(我使用的是 CMake 版本 2.8.10)。有大量的品种可以找到,看起来其中很多都是专门针对单个测试用例的。

因此,我还查看了一些可用的 CMake 脚本库,例如 CMake++,以查看他们的解决方案,但那些 - 当他们有单元测试时 - 很大程度上取决于他们自己的库函数。

【问题讨论】:

    标签: unit-testing cmake mocking ctest


    【解决方案1】:

    这是我目前对我自己的 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"
    )
    

    【讨论】:

      【解决方案2】:

      如何处理全局变量和变量范围问题? 通过加载项目的注入全局变量到测试中 缓存,配置测试 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://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

      cmake 列表必须通过configure_environment(...) 宏进行初始化: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/CMakeLists.txt

      阅读自述文件以了解有关 tacklelib 项目的详细信息: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/README_EN.txt

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

      附言: 在 cmake 上编写解析器脚本是一项艰巨的任务,请至少阅读以下这些问题:

      是否有一些“官方”方法可以对您自己的 CMake 脚本代码进行单元测试? 类似于运行 CMake 的特殊模式?我的目标是“白盒测试”(尽可能)。

      我做了自己的“白盒”或测试自己的脚本的方法。我编写了一组模块(它本身依赖于库)在单独的 cmake 进程中运行测试: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake/tacklelib/testlib/

      我的测试建立在它之上: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/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.
        #
      

      ,其中一组函数可以从运行器 cmake 脚本或*.include.cmake 文件中使用:

      其中TestLib.cmake 旨在使用测试模块 - *.test.cmake 运行循环创建外部 cmake 进程,并且应从运行程序脚本或包含模块(组其他包含模块 - *.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 子目录中的运行器脚本中使用过滤器参数来过滤掉测试:

      --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 - 用于包含命令。所有内置搜索逻辑都依赖于它。
      • 您已经编写了自己的跑步者来调用脚本RunTestLib.cmake。可以在此处找到 unix shell 上的 run all 示例:https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/_build/test_all.sh

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

      【讨论】:

      猜你喜欢
      • 2019-07-06
      • 2017-08-30
      • 2016-01-17
      • 2014-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-01
      • 1970-01-01
      相关资源
      最近更新 更多