23、CPack与CTest集成:在打包前自动运行测试

说实话,我见过太多这样的场景:开发团队辛辛苦苦打了包,结果用户一安装就报错。排查半天,发现是某个单元测试没跑过,代码里有明显的逻辑错误。嗯,这种尴尬我经历过不止一次。

所以这一章,我们来聊聊怎么让CPack在打包之前,自动把CTest跑一遍。说白了,就是给打包流程加一道「安检门」。

为什么要在打包前跑测试?

你想想看,打包的目的是什么?是交付。交付的东西如果连基本测试都过不了,那跟废品有什么区别?

我个人习惯的做法是:测试不通过,坚决不打。这个原则听起来简单,但真正落地需要工具链的支持。CPack和CTest的集成,就是帮你把这个原则变成自动化流程。

核心思路: 在CPack生成安装包之前,先调用CTest执行测试套件。如果测试失败,打包过程直接中止。

方案一:用CMake脚本控制流程

最直接的方式,是在你的打包脚本里显式调用CTest。我一般会在CMakeLists.txt里这样写:

# 启用测试
enable_testing()

# 添加测试用例
add_test(NAME test_core COMMAND test_core)
add_test(NAME test_io COMMAND test_io)

# 自定义打包目标,先测试再打包
add_custom_target(package_with_test
    COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
    COMMAND ${CMAKE_CPACK_COMMAND}
    DEPENDS ${PROJECT_NAME}
    COMMENT "Running tests before packaging..."
)

然后你运行make package_with_test,它就会先跑测试,再打包。测试失败了,打包就不会执行。

小技巧: 加上--output-on-failure参数,测试失败时能看到详细输出。我在调试复杂项目时,这个参数救过我很多次。

方案二:利用CPack预安装钩子

如果你用的是CPack的预安装机制,可以在CPackConfig.cmake里加一段逻辑:

# 在安装前运行测试
if(CPACK_INSTALL_CMAKE_PROJECTS)
    # 遍历所有要安装的项目
    foreach(proj ${CPACK_INSTALL_CMAKE_PROJECTS})
        # 提取项目路径
        list(GET proj 0 proj_path)
        # 在该路径下运行测试
        execute_process(
            COMMAND ${CMAKE_CTEST_COMMAND}
            WORKING_DIRECTORY ${proj_path}
            RESULT_VARIABLE test_result
        )
        if(NOT test_result EQUAL 0)
            message(FATAL_ERROR "Tests failed for ${proj_path}, aborting package generation.")
        endif()
    endforeach()
endif()

这段代码会在CPack执行安装步骤之前,遍历所有要安装的子项目,逐个跑测试。任何一个子项目测试失败,整个打包过程就停掉。

注意: 这种方案要求你的测试已经在构建目录里编译好了。如果测试还没编译,CTest会直接报错。我建议在构建阶段就把测试目标加进ALL依赖里。

CPACK_INSTALL_CMAKE_PROJECTS 的使用场景

这个变量,说白了就是告诉CPack:「我要安装哪些项目,以及它们分别在哪」。它的典型结构是这样的:

set(CPACK_INSTALL_CMAKE_PROJECTS
    "/path/to/build1;project1;ALL;/"
    "/path/to/build2;project2;runtime;/usr/local"
)

每一行包含四个字段:

字段 含义 示例
构建目录 项目编译的路径 /path/to/build1
项目名称 CMake项目名 project1
安装组件 ALL 或具体组件名 ALL
安装前缀 目标安装路径 / 或 /usr/local

我在实际项目中遇到过这样的场景:一个产品包含核心库、插件和工具集三个子项目。每个子项目独立构建,但最终要打包成一个安装包。这时候CPACK_INSTALL_CMAKE_PROJECTS就派上用场了:

set(CPACK_INSTALL_CMAKE_PROJECTS
    "${CMAKE_BINARY_DIR}/core;CoreLib;ALL;/opt/myapp"
    "${CMAKE_BINARY_DIR}/plugins;Plugins;ALL;/opt/myapp/plugins"
    "${CMAKE_BINARY_DIR}/tools;Tools;runtime;/opt/myapp/bin"
)

这样CPack就会依次安装这三个项目,最终生成一个完整的安装包。

集成测试与打包的完整流程

下面这张图展示了整个流程的逻辑:

CPack + CTest 集成流程 1. CMake 构建 编译所有目标 2. CTest 测试 运行测试套件 ? 测试失败 打包中止,输出错误 3. CPack 打包 生成安装包 安装包就绪 .deb / .rpm / .dmg 关键点:测试失败 → 打包中止,保证交付质量 CPACK_INSTALL_CMAKE_PROJECTS 可指定多个子项目,每个子项目独立测试后再统一打包

避坑指南

我曾经在一个大型项目中踩过一个坑:测试依赖外部数据库,但打包环境里没有数据库服务。结果CTest跑了一堆连接超时,打包流程卡了半小时。

后来我学乖了,在测试脚本里加了环境检测:

if(NOT DEFINED ENV{DB_HOST})
    message(WARNING "Database not available, skipping integration tests")
    return()
endif()

另外还有一个常见问题:测试超时。默认CTest没有超时限制,如果某个测试卡住了,整个打包流程就死在那了。我建议给每个测试加个超时:

add_test(NAME test_performance COMMAND test_performance)
set_tests_properties(test_performance PROPERTIES TIMEOUT 30)

30秒跑不完?那这个测试大概率有问题,直接标记失败,别让它拖累打包。

总结一下

CPack和CTest的集成,本质上就是给打包流程加了一道质量门。我个人觉得,这是CI/CD流水线里最值得投入的一环。你想想看,与其让用户发现bug,不如在打包阶段就把它拦住。

至于CPACK_INSTALL_CMAKE_PROJECTS,它解决的是多项目协同打包的问题。当你需要把多个独立构建的项目塞进一个安装包时,这个变量就是你的最佳拍档。

一句话记住: 先测试,后打包。测试不过,包就不打。

公众号:蓝海资料掘金营,微信deep3321