Добавлю шпаргалку по CUnit+CMake.
Недавно попадалась мне статья по теме: "CUnit: Автоматическое тестирование с динамической загрузкой тестов". Из статьи было почерпнуто достаточно полезный код по подгрузке тестов, а также сборка таких тестов посредством CMake.
Но в статье описано только структура папки tests, а наличие полного примера проекта пролило бы больше света на возможности разработанного автором метода подключения тестов. Ниже, я приведу структуру проекта, где такая система тестирования добавлена как опция.
Пример проекта: https://github.com/bayrepo/arrayallocator
Последовательность сборки довольно проста:
- yum install CUnit CUnit-devel cmake gcc git
- git clone https://github.com/bayrepo/arrayallocator.git
- cd arrayallocator/build
- cmake ..
- make
- make unit-test
- make install
Юнит тестирование вынесено отдельной командой: 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 папку.