5

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

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

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

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

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

4

2 回答 2

2

这是我当前对我自己的 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"
)
于 2015-11-24T20:15:10.700 回答
0

如何处理全局变量和变量范围问题?通过加载项目的缓存、配置测试 CMake 文件或通过 -D 命令行选项将其推送到测试中,将全局变量注入测试?

一般来说,所有当前存在的方法(通过缓存、通过环境变量和通过 -D 命令行)在一种或另一种情况下都是一个糟糕的选择,因为它涉及不可预测的行为。

这是我能回忆起的最少问题清单:

  • 哪一个变量可以在何时与另一个变量相交/重叠?
  • 变量 load 或 set 不能应用到 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://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 上编写解析器脚本是一项艰巨的任务,至少阅读这些问题开始:

是否有一些“官方”方式对您自己的 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文件中使用:

WhereTestLib.cmake旨在使用测试模块在创建外部 cmake 进程时运行循环 -*.test.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

whereTestModule.cmake自动包含在*.test.cmake您必须放置测试代码的所有模块中。

之后,您只需在模块tkl_test_assert_true内部使用*.test.cmake将测试标记为成功或失败。

_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.cmakerun all可以在此处找到 unix shell 上的示例: https ://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/_build/test_all.sh

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

于 2019-05-06T16:29:22.973 回答