unit-testingcmakemockingctest

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


I've written some convenience wrappers around standard CMake commands and want to unit-test this CMake script code to ensure its functionality.

I've made some progress, but there are two things I hope to get help with:

  1. 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).
  2. 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? Simulation/Testing of variable scopes (cached vs. non-cached, macros/functions/includes, parameters passed by references)?

To start with I've looked into the CMake source code (I'm using CMake version 2.8.10) under /Tests and especially under Tests/CMakeTests. There is a huge number of varieties to be found and it looks like a lot of them are specialized on a single test case.

So I looked also into some available CMake script libraries like CMake++ to see their solution, but those - when they have unit tests - are heavily depending on their own library functions.


Solution

  • Here is my current solution for unit-testing my own CMake script code.

    By the assumption that using CMake Script processing mode is my best catch and that I have to mock the CMake commands that are not usable in script mode I - so far - came up with the following.

    The Helper Functions

    Utilizing my own global properties, I have written helper functions to store and compare function calls:

    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()
    

    The Mockups

    By adding mockups like:

    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()
    

    The Tests

    I can write a test like this:

    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)
    

    And include it into my CMake projects with:

    CMakeLists.txt

    add_test(
        NAME TestMyCMake 
        COMMAND ${CMAKE_COMMAND} -P "MyUnitTests.cmake"
    )