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_test的CONFIGURATIONS参数来分组:
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 .
本章核心逻辑图
下面这张图展示了交叉编译测试的完整流程:
总结
交叉编译的测试,说白了就三步:
- 用enable_testing()和add_test()注册测试 —— 这是CMake的标准做法
- 交叉编译生成ARM测试二进制 —— 注意工具链要匹配
- 在目标机上执行并验证结果 —— 真机测试不可替代
我个人建议,从一开始就把测试框架搭好。哪怕只是一个简单的加法函数,也值得跑一下。因为测试框架本身也需要调试——你总不想在项目交付前夜才发现ctest配置有问题吧?
嗯,这一章就到这里。记住:交叉编译完,先跑测试,再谈其他。