Добавлю шпаргалку по CUnit+CMake.

Недавно попадалась мне статья по теме: "CUnit: Автоматическое тестирование с динамической загрузкой тестов". Из статьи было почерпнуто достаточно полезный код по подгрузке тестов, а также сборка таких тестов посредством CMake.

Но в статье описано только структура папки tests, а наличие полного примера проекта пролило бы больше света на возможности разработанного автором метода подключения тестов. Ниже, я приведу структуру проекта, где такая система тестирования добавлена как опция.

Пример проекта: https://github.com/bayrepo/arrayallocator

Последовательность сборки довольно проста:

Юнит тестирование вынесено отдельной командой: make unit-test и вот что она выдает:

[@localhost build]$ make unit-test
[100%] Built target runtests
Scanning dependencies of target unit-test

     CUnit - A unit testing framework for C - Version 2.1-2
     http://cunit.sourceforge.net/

Suite: mallocsbrk
  Test: brp_malloc_init_1 ...passed
  Test: brp_malloc_init_small_1 ...passed
  Test: brp_malloc_1 ...passed
  Test: brp_malloc_reinit_1 ...passed
  Test: brp_free_1 ...passed
  Test: brp_malloc_size_1 ...passed
  Test: brp_malloc_malloc_free_large_1 ...passed
  Test: brp_realloc_1 ...passed
  Test: brp_realloc_big_small_1 ...passed
  Test: brp_calloc_1 ...passed
  Test: brp_free_null_1 ...passed
  Test: brp_realloc_null_1 ...passed
  Test: brp_get_next_region_info_1 ...passed
  Test: brp_get_next_elem_1 ...passed
  Test: brp_get_free_1 ...passed
  Test: brp_get_inuse_1 ...passed
  Test: brp_return_allocation_picture_1 ...passed
  Test: brp_check_cannot_allocate_1 ...passed
  Test: brp_check_concat_1 ...FAILED
    1. /home/alexey/cmoka/bayrepo/tests/suites/malloctest.c:359  - CU_ASSERT_STRING_EQUAL_FATAL(alloc_buffer,"UUU")
Suite: pointers table
  Test: brp_make_pointers_table_1 ...passed
  Test: brp_pointers_1 ...passed
  Test: brp_recreate_pointers_table_1 ...passed
Suite: strdup
  Test: brp_strdup ...passed
  Test: brp_strndup ...passed
Suite: utstring
  Test: brp_utsting ...passed
  Test: brp_utsting_free ...passed
Suite: utringbuffer
  Test: brp_utringbuffer ...passed
  Test: brp_utringbuffer_clear_int ...passed
  Test: brp_utringbuffer_clear_str ...passed
  Test: brp_utringbuffer_free ...passed
Suite: uthash
  Test: brp_uthash ...passed
  Test: brp_uthash_free ...passed
Suite: utarray
  Test: brp_utarray ...passed
  Test: brp_utarray_clear ...passed
  Test: brp_utarray_free ...passed

Run Summary:    Type  Total    Ran Passed Failed Inactive
              suites      7      7    n/a      0        0
               tests     35     35     34      1        0
             asserts    210    210    209      1      n/a

Elapsed time =    0.000 seconds
[100%] Built target unit-test

Достаточно информативно выдано сообщение об ошибке и выдан отчет.

А теперь про структуру проекта:

project+
       |--build+
       |--examples+
                  +--CMakeLists.txt
       |--includes+
                  +--*.h, *.hpp, etc...
       |--external+
                  +--any external sources with own CMakeLists.txt
       |--src+
             |--CMakeLists.txt
             +--any sources
       |--tests+
               |--suites+
                        |--CMakeLists.txt
                        +--*.c
               |--CMakeLists.txt
               |--main.c
               |--utils.c
               +--utils.h
       +--CMakeLists.txt
       +--LICENSE

Очень удобный метод сборки проекта вне корня исходников. В проекте есть или создается пустая папка build и в ней делается команда cmake ..?, таким образом все объектные файлы и временные файлы и бинарные файлы размещаются в папке build, котоая легко чистится в случае необходимости полной очистки конфигурации проекта.

Здесь размещаются примеры по использованию функций (в случае если проект - библиотека). Для каждого примера в CMakeList.txt описываются цели и свойства сборки. Пример такого файла - ниже:

CMAKE_MINIMUM_REQUIRED (VERSION 2.6)
SET(CMAKE_VERBOSE_MAKEFILE ON)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../includes  ${CMAKE_CURRENT_SOURCE_DIR}/../external/uthash)
add_executable(bayrepo_example1 hash_in_shmem.c)
add_executable(bayrepo_example2 list_in_array.c)

target_link_libraries(bayrepo_example1 pthread bayrepos)
target_link_libraries(bayrepo_example2 pthread bayrepos)

ADD_CUSTOM_TARGET(examples1 COMMAND "bayrepo_example1")
ADD_CUSTOM_TARGET(examples2 COMMAND "bayrepo_example2")
  • include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../includes ${CMAKE_CURRENT_SOURCE_DIR}/../external/uthash) - подключает пути к заголовочным файлам проекта.
  • add_executable(bayrepo_example1 hash_in_shmem.c) - указывает какое будет имя собранного бинарного файла и из каких исходных кодов его собирать.
  • target_link_libraries(bayrepo_example1 pthread bayrepos) - добавляет список библиотек, которые будут слинкованы с бинарником, библиотеки могут быть как статические, так и динамические.
  • ADD_CUSTOM_TARGET(examples1 COMMAND "bayrepo_example1") - добавляет make examples1 команду и запускает указанный бинарник.

Содержит все заголовочные файлы проекта.

Содержит исходные коды других проектов с их CMakeList.txt. Важно, если для сборки необходимо компиллировать исходники отдельно, то это должно быть описано в глобальном CMakeList.txt. В моем случае такого не понадобилось, т.к в папке external находились лишь заголовочные файлы.

Пример подключения: include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../includes ${CMAKE_CURRENT_SOURCE_DIR}/../external/uthash)

Здесь уже располагаются исходные файлы проекта и CMakeList.txt, который должен описывать, как их собирать.

include(GNUInstallDirs)

IF(NOT DEFINED LIB_VERSION_MAJOR)
   set(LIB_VERSION_MAJOR 0)
ENDIF(NOT DEFINED LIB_VERSION_MAJOR)
IF(NOT DEFINED LIB_VERSION_MINOR)
   set(LIB_VERSION_MINOR 1)
ENDIF(NOT DEFINED LIB_VERSION_MINOR)

set(LIB_VERSION_STRING ${LIB_VERSION_MAJOR}.${LIB_VERSION_MINOR})

file(GLOB LIB_SRC "*.c")
add_definitions(-DMTHREAD)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../includes  ${CMAKE_CURRENT_SOURCE_DIR}/../external/uthash)
add_library(bayrepo SHARED ${LIB_SRC})
add_library(bayrepos STATIC ${LIB_SRC})

target_link_libraries(bayrepo pthread)
target_link_libraries(bayrepos pthread)

set_target_properties(bayrepo PROPERTIES VERSION ${LIB_VERSION_STRING})
set_target_properties(bayrepos PROPERTIES VERSION ${LIB_VERSION_STRING})

install(TARGETS bayrepo DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME})
install(TARGETS bayrepos DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME})

include(GNUInstallDirs) - директива подключает макросы, содержащие стандартные пути для размещения собранных файлов, например CMAKE_INSTALL_LIBDIR

Стоит отметить, что по команде make, собираются две версии библиотеки:

add_library(bayrepo SHARED ${LIB_SRC})
add_library(bayrepos STATIC ${LIB_SRC})

Для тестов необходимо использовать SHARED, а для примеров результирующие программы линкуются со STATIC библиотекой.

То, что тесты требуют наличие именно SHARED библиотеки в определенных проектах неудобно.

Основная структура этой папки описана в статье: "CUnit: Автоматическое тестирование с динамической загрузкой тестов". Я процитирую:

Итак, вначале определимся со структурой каталогов и файлов:

.tests
|-- suites
|   |-- CMakeLists.txt
|   |-- suite1.c
|   |-- suite2.c
|   |-- suite3.c
|-- main.c
|-- utils.c
|-- utils.h   
|-- CMakeLists.txt

Каждый тест-сьют будет расположен в отдельном файле в каталоге suites. Задача разработчика или тестировщика только написать тест-сьют и положить его в папку suites. Других телодвижений от разработчика/тестера не требуется, тест-сьют будет автоматически подхвачен системой сборки для компиляции, а потом собственно и исполняемой программой при запуске тестов.

После сборки на выходе мы должны получить runtests — исполняемая программа и модули с тест-сьютами.

Соглашение о наименовании функций

Договоримся, что тест-кейсы будут иметь префикс test_. То есть если мы тестируем библиотечную функцию foo(), то тест-кейс для функции должен называться test_foo().

В каждом динамическом модуле тест-сьюте должна быть экспортирована функция runSuite(), которая будет вызываться в исполняемой программе. В даной функции должен создаваться тест-сьют, средствами CUnit, с которым связываются тест-кейсы. Прототип функции:

void runSuite(vod);

Шаблон динамического модуля — тест-сьют

suite1.c:
/* Первый тест-кейс */
static void test_foo(void) {
/* Код тест-кейса */
}
/* Второй тест-кейс */
static void test_foo2(void) {
/* Код тест-кейса */
}
void runSuite(void) {
/* Код тест-сьюта */
}

Как это должно работать

В момент запуска исполняемой программы runtests она загружает все динамические модули — тест-сьюты из каталога suites, если не задана переменная окружения TEST_MODULES_DIR, и выполняет функцию runSuite() каждого модуля. Если указана переменная среды окружения TEST_MODULES_DIR, то модули будут загружаться из каталога на который указывает эта переменная Вспомогательные функции и макросы окружения

Для того чтобы постоянно вручную не писать префикс у тест-кейсов или, чего хуже, если в последующем префикс будет изменен, не переименовывать все тест-кейсы напишем вспомогательный макрос TEST_FUNCT:

#define TEST_FUNCT(name) \
static void test_##name() 

Теперь вместо того чтобы писать:

static void test_foo() {
/* Some code */
}

пишем:

TEST_FUNCT(foo) {
/* Some code */
}

Добавим еще один макрос ADD_SUITE_TEST для добавления тест-кейсов к тест-сьюту:

#define ADD_SUITE_TEST(suite, name) \
if ((NULL == CU_add_test(suite, #name, (CU_TestFunc)test_##name))) {\
CU_cleanup_registry();\
return;\
}\

Ну и последние, что нам нужно — это вспомогательная функция для создания тест-сьюта CUnitCreateSuite()

В файл tests/suites/CMakeLists.txt добавляются папки с заголовочными файлами, а также линкеру указывается с какой библиотекой необходимо линковать тесты.

Вот прабочие примеры для tests/CMakeLists.txt:

CMAKE_MINIMUM_REQUIRED (VERSION 2.6)
SET(CMAKE_VERBOSE_MAKEFILE ON)

PROJECT("runtests")

ADD_SUBDIRECTORY(suites)

SET(CMAKE_C_FLAGS " -std=c99 -O0 -Wall -Wextra -Wimplicit")

INCLUDE_DIRECTORIES ( "/usr/include" "${CMAKE_SOURCE_DIR}/../includes" )
ADD_EXECUTABLE(runtests main.c utils.c)
TARGET_LINK_LIBRARIES(runtests cunit dl)

ADD_CUSTOM_TARGET(unit-test COMMAND ${PROJECT_NAME})

И для tests/suites/CMakeLists.txt:

MACRO(ADD_MODULE file)
    ADD_LIBRARY( ${file} MODULE ${file}.c ../utils.c )
    TARGET_LINK_LIBRARIES( ${file} cunit bayrepo)
    SET_TARGET_PROPERTIES( ${file} PROPERTIES
            PREFIX ""
            LIBRARY_OUTPUT_DIRECTORY "."
    )
ENDMACRO(ADD_MODULE file)

FILE(GLOB C_FILES RELATIVE "${CMAKE_SOURCE_DIR}/tests/suites" "${CMAKE_SOURCE_DIR}/tests/suites/*.c")

include_directories("${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/../../includes" 
"${CMAKE_CURRENT_SOURCE_DIR}/../" ${CMAKE_CURRENT_SOURCE_DIR}/../../external/uthash)

FOREACH ( module ${C_FILES} )
    STRING( REGEX REPLACE ".c$" "" module "${module}" )
    MESSAGE(STATUS "Found test suite: ${module}")
    ADD_MODULE(${module})
ENDFOREACH ( module ${MODULES} )

В нем описаны все подключаемые подпапки add_subdirectory

cmake_minimum_required(VERSION 2.6)

project(bayrepo)

IF(NOT DEFINED CMAKE_BUILD_TYPE)
   SET(CMAKE_BUILD_TYPE Release)
ENDIF(NOT DEFINED CMAKE_BUILD_TYPE)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/includes)

add_subdirectory (tests) 
add_subdirectory (src)
add_subdirectory (examples)

file(GLOB HEADERS includes/bayrepomalloc.h includes/bayrepostrings.h external/uthash/*.h)
install(FILES ${HEADERS} DESTINATION include/${PROJECT_NAME})

А так же доописаны правила установки заголовочных файлов библиотеки, хотя возможно правильнее это вынести отдельным CMakeLists.txt в includes папку.

Добавить комментарий

Blog Comments powered by Disqus.

Предыдущая запись