CMake: как провести модульное тестирование собственных макросов / функций сценария CMake?

Я написал несколько удобных оболочек для стандартных команд CMake и хочу протестировать этот код сценария CMake, чтобы убедиться в его функциональности.

Я добился некоторого прогресса, но есть две вещи, в которых я надеюсь получить помощь:

  1. Есть ли какой-нибудь «официальный» способ модульного тестирования вашего собственного кода сценария CMake? Что-то вроде специального режима для запуска CMake? Моя цель - «тестирование белого ящика» (насколько это возможно).
  2. Как мне справиться с проблемами глобальных переменных и областей видимости переменных? Внедрить глобальные переменные в тест, загрузив кеш проекта, настроив тестовый файл CMake или отправив его с помощью параметра командной строки -D? Моделирование / тестирование переменных областей (кешированные или некэшированные, макросы / функции / включает, параметры, передаваемые по ссылкам)?

Для начала я изучил исходный код CMake (я использую CMake версии 2.8.10) в разделе / ​​Tests и особенно в разделе Tests / CMakeTests. Существует огромное количество разновидностей, и похоже, что многие из них специализируются на одном тестовом примере.

Поэтому я также просмотрел некоторые доступные библиотеки сценариев CMake, такие как CMake ++, чтобы увидеть их решение, но те - когда они модульные тесты - сильно зависят от собственных библиотечных функций.


person Florian    schedule 23.04.2015    source источник


Ответы (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"
)
person Florian    schedule 24.11.2015

Как мне справиться с проблемами глобальных переменных и областей видимости переменных? Внедрить глобальные переменные в тест, загрузив кеш проекта, настроив тестовый файл 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:\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

В обязательном порядке список cmake должен быть инициализирован с помощью макроса configure_environment(...): https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/CMakeLists.txt

Прочтите файл readme, чтобы получить подробную информацию о проекте 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-скрипта runner, так и из *.include.cmake файла:

Где TestLib.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

Где TestModule.cmake автоматически включается во все *.test.cmake модули, в которые вы должны поместить свой тестовый код.

После этого вы просто используете tkl_test_assert_true внутри модуля *.test.cmake, чтобы пометить тест как успешный или неудачный.

Кроме того, вы можете использовать параметры фильтра в скриптах runner в подкаталоге _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. Пример run all в оболочке unix можно найти здесь: https://sourceforge.net/p/tacklelib/tacklelib/HEAD/tree/trunk/cmake_tests/_scripts/test_all.sh

В настоящее время весь проект является экспериментальным.

person Andry    schedule 06.05.2019
comment
Мне эта библиотека кажется действительно интересной. Можете ли вы уделить немного времени и точно описать, для чего он нужен и для чего он нужен (ридми мало что объясняет)? Или присоединяйтесь ко мне в chat.stackexchange.com/rooms/95293/tacklelib - person Adam Ryczkowski; 23.06.2019