Введение
Основной цикл статей про тестирование и DevOps-цикл (статьи 449, 451) разбирали тестирование и упаковку как отдельные задачи — CMake предоставляет встроенную инфраструктуру для обеих этих задач, интегрированную непосредственно с системой сборки: CTest для запуска тестов и CPack для создания дистрибутивных пакетов (инсталляторов, архивов), разбираемые в этой статье.
Концепция
CTest регистрирует тесты через команду add_test(), связывающую конкретную команду (обычно — запуск скомпилированного тестового исполняемого файла, статья 449 DevOps-цикла) с понятным, именованным тестовым случаем, после чего команда ctest запускает все зарегистрированные тесты, агрегируя результаты в единый, удобный для просмотра и интеграции с CI отчёт. CPack использует уже настроенные команды install() (статья 582) текущего проекта для автоматической генерации дистрибутивных пакетов в различных, специфичных для целевой платформы форматах (ZIP/TGZ архивы кроссплатформенно, NSIS/MSI инсталляторы для Windows, DEB/RPM пакеты для Linux, DMG для macOS) без необходимости вручную писать отдельную, специфичную для каждого формата логику упаковки.
Пример кода: CTest и CPack
# Регистрация тестов через CTest
enable_testing() # ОБЯЗАТЕЛЬНАЯ команда перед использованием add_test
add_executable(MathTests test_math.cpp)
target_link_libraries(MathTests PRIVATE MathUtils gtest gtest_main) # статья 579 — FetchContent для gtest
add_test(NAME MathTests COMMAND MathTests) # связь "имя теста" -> "реальная команда для его запуска"
# Использование GoogleTest-специфичной интеграции для автоматической регистрации ОТДЕЛЬНЫХ тестовых случаев
include(GoogleTest)
gtest_discover_tests(MathTests) # каждый отдельный TEST() внутри MathTests становится ОТДЕЛЬНЫМ ctest-тестом
# Запуск тестов через CTest
ctest # запуск всех зарегистрированных тестов
ctest --output-on-failure # вывод подробностей ТОЛЬКО для упавших тестов — не загромождает вывод
ctest -R "Math.*" # запуск только тестов, чьё имя соответствует регулярному выражению
ctest -j4 # параллельный запуск — до 4 тестов одновременно
# CPack — конфигурация для генерации дистрибутивных пакетов
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_VENDOR "MyCompany")
if(WIN32)
set(CPACK_GENERATOR "NSIS") # инсталлятор для Windows
elseif(APPLE)
set(CPACK_GENERATOR "DragNDrop") # DMG-образ для macOS
else()
set(CPACK_GENERATOR "DEB;RPM") # СРАЗУ оба формата для разных Linux-дистрибутивов
endif()
include(CPack) # ОБЯЗАТЕЛЬНО последняя строка — подключает реальную функциональность CPack
Пояснения к коду
enable_testing() перед add_test() — обязательная, легко забываемая команда, без которой последующие вызовы add_test просто не имеют эффекта. gtest_discover_tests(MathTests) показывает удобную, специфичную для GoogleTest интеграцию — вместо единственного, «грубого» add_test, регистрирующего весь тестовый executable как один большой тест (успех/неудача которого определяется по общему коду возврата всего процесса), эта команда автоматически обнаруживает и регистрирует КАЖДЫЙ отдельный тестовый случай (TEST()/TEST_F()) внутри исполняемого файла как самостоятельный, отдельно отображаемый и запускаемый CTest-тест, что даёт значительно более детальную, полезную картину результатов. ctest -R "Math.*" показывает практичную возможность выборочного запуска только подмножества тестов по паттерну имени, что особенно полезно во время активной разработки конкретной части кода, когда полный, повторяющийся запуск ВСЕХ тестов проекта может быть избыточно медленным. Конфигурация CPack с условным выбором генератора в зависимости от платформы (NSIS для Windows, DragNDrop для macOS, DEB;RPM для Linux) показывает, как один и тот же, общий CMakeLists.txt способен генерировать платформенно-уместный формат дистрибутива без отдельной, специфичной для каждой платформы логики упаковки, написанной с нуля разработчиком.
Подводные камни
- Забытый
enable_testing()— без этой командыadd_test()молчаливо не регистрирует тесты, и попытка запуститьctestпосле такой ошибки выдаст сообщение об отсутствии тестов для выполнения, что может ввести в заблуждение разработчика, ожидающего реального запуска тестов, написанных и, казалось бы, корректно зарегистрированных черезadd_test. - Использование единственного, «грубого»
add_test()для целого тестового executable вместо детальной интеграции черезgtest_discover_tests(или аналогичные команды для других тестовых фреймворков) — это лишает CTest возможности показать, какие именно конкретные тестовые случаи внутри большого тестового исполняемого файла прошли или не прошли, агрегируя результат всех тестов в единый успех/неудачу всего процесса, что существенно усложняет диагностику конкретной причины сбоя при наличии множества разных тестовых случаев в одном исполняемом файле. - Игнорирование необходимости установки зависимостей (статья 582) для корректной работы сгенерированного CPack-пакета на машине конечного пользователя — простая, неполная конфигурация
install()(например, без копирования необходимых динамических библиотек, статья 451 DevOps-цикла проwindeployqt/аналогичные инструменты для Qt-зависимостей) приведёт к тому, что сгенерированный пакет/инсталлятор технически собирается без ошибок, но устанавливаемое приложение не работает корректно на чистой машине пользователя без вручную, отдельно установленных зависимостей. - Недостаточное тестирование сгенерированных через CPack пакетов на действительно «чистой» целевой системе, аналогично общему подводному камню статей 451/581 про тестирование на чистом окружении — успешная генерация пакета через CPack сама по себе не гарантирует, что результат корректно устанавливается и работает на машине, отличающейся от машины разработчика (другая версия ОС, отсутствие предустановленных runtime-зависимостей), и реальная, финальная проверка на представительной для конечных пользователей машине остаётся необходимым шагом перед реальным распространением сгенерированного пакета.