20、测试交叉编译结果:使用CMake的enable_testing()与add_test()在目标机上运行测试

交叉编译搞完了,二进制也生成好了。然后呢?

很多人觉得,能编译通过就万事大吉了。我刚开始做嵌入式那会儿也这么想。直到有一次,代码在x86上跑得飞起,放到ARM板子上直接段错误——查了两天才发现是字节对齐的问题。嗯,从那儿以后,我养成了一个习惯:交叉编译完,必须跑测试

这一章,我们就聊聊怎么用CMake自带的测试框架,在目标机上验证你的交叉编译结果。

为什么要在目标机上跑测试?

你想想看,模拟器再牛,也模拟不出真实硬件的全部行为。外设地址、中断响应、内存布局……这些差异,只有真机才能暴露出来。

我在项目中遇到过最典型的一个坑:浮点运算在QEMU模拟器上完全正常,烧到真机上就出乱码。后来发现是硬浮点ABI没选对。这种问题,单元测试一跑就现原形。

CMake测试框架的核心:enable_testing()与add_test()

CMake自带的测试能力其实很轻量。就两个关键命令:

  • enable_testing() —— 开启测试支持
  • add_test() —— 注册一个测试用例

说白了,add_test()就是告诉CMake:嘿,这个可执行文件是个测试,你帮我记着。然后ctest命令会去执行它。

重要提醒:enable_testing()必须在顶级CMakeLists.txt中调用,而且要在add_test()之前。否则CMake会报错。

一个完整的交叉编译测试例子

假设我们有一个简单的加法函数,要交叉编译到ARM平台,并在板子上验证。

项目结构:

cross_test/
├── CMakeLists.txt
├── toolchain-arm.cmake
├── src/
│   ├── add.c
│   └── add.h
└── test/
    └── test_add.c

顶级CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(CrossTest C)

# 开启测试
enable_testing()

# 主库
add_library(add STATIC src/add.c)

# 测试可执行文件
add_executable(test_add test/test_add.c)
target_link_libraries(test_add PRIVATE add)

# 注册测试
add_test(NAME test_add COMMAND test_add)

工具链文件 toolchain-arm.cmake:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

测试代码 test/test_add.c:

#include <stdio.h>
#include "add.h"

int main() {
    int result = add(2, 3);
    if (result == 5) {
        printf("PASS: add(2,3) = %d\n", result);
        return 0;
    } else {
        printf("FAIL: add(2,3) = %d, expected 5\n", result);
        return 1;
    }
}

构建与测试流程

在开发机上交叉编译:

mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake
make

这会生成一个test_add可执行文件。注意,它是ARM格式的,不能在x86开发机上直接运行。

接下来,把test_add拷贝到目标板上:

scp test_add root@192.168.1.100:/tmp/
ssh root@192.168.1.100 /tmp/test_add

如果输出PASS: add(2,3) = 5,说明交叉编译结果正确。

我的习惯:我会在目标板上创建一个/opt/tests/目录,专门存放测试二进制。然后用一个简单的shell脚本批量执行所有测试。这样每次更新固件后,跑一遍脚本就能快速验证。

如何让ctest在目标机上工作?

ctest默认是在本地执行测试的。要在目标机上用ctest,需要一点小技巧。

方法一:手动拷贝+远程执行

这是最直接的方式。把整个build目录下的测试二进制打包,传到目标板上,然后手动执行ctest:

# 在目标机上
cd /path/to/build
ctest --output-on-failure

前提是目标机上安装了ctest。如果没有,可以用一个简单的shell脚本替代:

#!/bin/sh
# run_tests.sh
for test in ./test_*; do
    if [ -x "$test" ]; then
        echo "Running $test..."
        $test
        if [ $? -eq 0 ]; then
            echo "PASS"
        else
            echo "FAIL"
        fi
    fi
done

方法二:使用CMake的CROSSCOMPILING_EMULATOR

如果你有QEMU用户态模拟器,可以设置这个变量,让ctest在开发机上直接运行ARM测试:

set(CMAKE_CROSSCOMPILING_EMULATOR qemu-arm-static)

然后在CMakeLists.txt中加上:

add_test(NAME test_add COMMAND test_add)

ctest会自动调用qemu-arm-static test_add来执行。不过要注意,QEMU模拟的只是指令集,外设和硬件特性是模拟不了的。

我曾经踩过的坑:用QEMU跑测试时一切正常,以为交叉编译没问题了。结果烧到真机上,一个依赖硬件定时器的测试直接挂掉。所以我的建议是:QEMU测试只能作为快速验证,最终一定要在真机上跑一遍

测试的组织与标签

当测试多了以后,你需要给测试打标签,方便分类执行。CMake支持用add_testCONFIGURATIONS参数来分组:

add_test(NAME test_add COMMAND test_add CONFIGURATIONS unit)
add_test(NAME test_i2c COMMAND test_i2c CONFIGURATIONS hardware)

然后你可以只跑某一类测试:

ctest -C unit    # 只跑单元测试
ctest -C hardware # 只跑硬件相关测试

我个人习惯把测试分为三类:

类别 标签 说明
单元测试 unit 纯算法逻辑,不依赖硬件
硬件测试 hardware 依赖外设、GPIO、I2C等
集成测试 integration 多模块联调

这样在开发机上可以快速跑unit测试,只有到了真机上才跑hardware测试。效率高很多。

测试超时与资源限制

嵌入式设备资源有限。一个测试如果卡死了,可能会拖垮整个测试流程。CMake提供了超时机制:

set_tests_properties(test_add PROPERTIES TIMEOUT 10)

如果test_add在10秒内没跑完,ctest会强制终止它,并标记为失败。

我在项目中遇到过一个问题:某个测试依赖外部中断,但中断信号没来,测试就永远等下去了。加上超时后,至少能保证测试套件不会卡死。

测试结果的收集与报告

ctest支持输出多种格式的测试报告。我最常用的是JUnit格式,可以集成到CI/CD流水线中:

ctest --output-junit test_results.xml

然后在Jenkins或GitLab CI中解析这个XML,就能看到每个测试的通过/失败情况。

如果你在目标板上手动跑测试,可以用重定向把结果保存下来:

./run_tests.sh > test_log.txt 2>&1
scp root@192.168.1.100:/tmp/test_log.txt .

本章核心逻辑图

下面这张图展示了交叉编译测试的完整流程:

交叉编译测试流程 开发机 (x86) 1. 编写CMakeLists.txt enable_testing() + add_test() 2. 交叉编译 cmake -DCMAKE_TOOLCHAIN_FILE=... && make 3. 生成ARM测试二进制 test_add (ARM ELF格式) scp / rsync 目标机 (ARM) 4. 拷贝测试二进制到目标机 /tmp/test_add 5. 在目标机上执行测试 ./test_add 或 ctest 6. 收集测试结果 PASS / FAIL 日志 ✅ 验证交叉编译结果正确 scp 日志回传 📋 分析测试报告

总结

交叉编译的测试,说白了就三步:

  1. 用enable_testing()和add_test()注册测试 —— 这是CMake的标准做法
  2. 交叉编译生成ARM测试二进制 —— 注意工具链要匹配
  3. 在目标机上执行并验证结果 —— 真机测试不可替代

我个人建议,从一开始就把测试框架搭好。哪怕只是一个简单的加法函数,也值得跑一下。因为测试框架本身也需要调试——你总不想在项目交付前夜才发现ctest配置有问题吧?

嗯,这一章就到这里。记住:交叉编译完,先跑测试,再谈其他