21、构建自动化:CI/CD概念、Jenkins/GitHub Actions配置、自动化测试集成
说实话,很多C语言开发者对CI/CD有误解。
他们觉得这是Java、Python那些“高级语言”才玩的东西。C语言嘛,make一下、gcc编译一把,能跑就行。我以前也这么想,直到有一次,一个同事改了头文件,没通知任何人。结果整个团队编译出来的固件,一半跑飞了。那天晚上,我们排查到凌晨三点。
嗯,从那以后,我下定决心:C语言项目,必须上CI/CD。
CI/CD到底是什么?
说白了,CI/CD就是给你的代码请了个“自动管家”。
- CI(持续集成):你每次提交代码,系统自动拉下来、编译、跑测试。有问题立刻告诉你。
- CD(持续交付/部署):通过测试的代码,自动打包、发布、甚至部署到目标设备。
你想想看,嵌入式C项目最怕什么?最怕“在我机器上能跑”。CI/CD就是专门治这个毛病的。
核心价值:把“人肉检查”变成“机器检查”。每次提交都跑一遍完整的构建+测试流程,确保代码库始终处于健康状态。
我建议的C语言CI/CD流水线结构
我在多个嵌入式项目中总结了一套标准流程。直接上图:
这个流程看起来复杂,但核心逻辑很简单:每一道关卡都在帮你拦截问题。静态检查抓代码风格和潜在bug,编译构建确保能通过,单元测试验证逻辑正确性,集成测试检查模块间配合。
GitHub Actions配置实战
我个人更偏爱GitHub Actions。为什么?因为对C语言项目来说,它足够轻量,配置简单,而且免费额度对大多数团队都够用。
下面是我在一个STM32项目里用的配置。直接看代码:
# .github/workflows/ci.yml
name: C Project CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 安装工具链
run: |
sudo apt-get update
sudo apt-get install -y gcc-arm-none-eabi cmake cppcheck
- name: 静态代码分析
run: |
cppcheck --enable=all --suppress=missingIncludeSystem \
--error-exitcode=1 src/ include/
- name: 编译固件
run: |
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-gcc.cmake
make -j$(nproc)
- name: 运行单元测试
run: |
cd build
ctest --output-on-failure
小技巧:在C语言项目中,cppcheck的--error-exitcode=1参数很关键。它让静态检查发现问题时返回非零值,从而让CI流程失败。我曾经见过有人忘了加这个,结果代码里一堆警告,CI还一直绿着……
Jenkins配置要点
如果你的项目在本地服务器上,或者需要更复杂的流水线控制,Jenkins是更好的选择。
我建议用Pipeline as Code的方式,把流水线定义写在Jenkinsfile里。这样版本控制也方便。
// Jenkinsfile
pipeline {
agent any
stages {
stage('检出代码') {
steps {
checkout scm
}
}
stage('静态分析') {
steps {
sh 'cppcheck --enable=all --suppress=missingIncludeSystem src/'
}
}
stage('编译') {
steps {
sh 'mkdir -p build && cd build && cmake .. && make'
}
post {
failure {
// 编译失败时,保存编译日志
archiveArtifacts artifacts: 'build/*.log'
}
}
}
stage('单元测试') {
steps {
sh 'cd build && ctest --output-on-failure'
}
post {
always {
// 无论成败,都保存测试报告
junit 'build/test-results/*.xml'
}
}
}
stage('固件打包') {
steps {
sh './scripts/package_firmware.sh'
}
}
}
post {
failure {
// 发送通知
emailext(
subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请检查构建结果: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
}
}
注意:Jenkins的post块非常有用。我曾经踩过一个坑:测试失败了,但没人知道,因为邮件通知没配。后来我养成了习惯,任何CI流程都必须配置失败通知。不然CI就失去了意义。
自动化测试集成
测试是CI/CD的灵魂。没有测试的CI,说白了就是个编译检查器。
对于C语言项目,我推荐以下测试框架:
| 框架 | 适用场景 | 特点 |
|---|---|---|
| CUnit | 桌面端C项目 | 成熟稳定,文档丰富 |
| Unity | 嵌入式C项目 | 轻量级,适合资源受限环境 |
| CMock | 需要mock的场景 | 与Unity配合,自动生成桩函数 |
| Check | Linux环境 | 支持fork模式,隔离性好 |
我个人习惯用Unity + CMock的组合。为什么?因为嵌入式项目里,硬件依赖太多。你想想看,测试一个传感器驱动,总不能每次都接真硬件吧?用CMock生成桩函数,模拟硬件行为,测试就能在CI环境里跑起来。
下面是一个Unity测试的示例:
// test/test_temperature_sensor.c
#include "unity.h"
#include "temperature_sensor.h"
// 模拟I2C读取函数
int mock_i2c_read(uint8_t addr, uint8_t reg, uint8_t* data) {
*data = 0x1A; // 模拟返回25°C
return 0;
}
void setUp(void) {
// 注册mock函数
i2c_read_ExpectAndReturn(0x48, 0x00, NULL, 0);
i2c_read_AddCallback(mock_i2c_read);
}
void tearDown(void) {
// 清理
}
void test_sensor_reads_temperature(void) {
float temp = temperature_sensor_read();
TEST_ASSERT_FLOAT_WITHIN(0.5, 25.0, temp);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_sensor_reads_temperature);
return UNITY_END();
}
关键点:测试代码要和产品代码分开管理。我建议在项目根目录下建test/文件夹,每个模块对应一个测试文件。CMake里用add_test()注册,这样ctest就能一键运行所有测试。
避坑指南
做CI/CD这几年,我踩过的坑不少。挑几个典型的说说:
- 我曾经把整个工具链都放在CI脚本里安装,结果每次构建都要等10分钟装依赖。后来改用Docker镜像,构建时间从10分钟降到了2分钟。
- 我曾经忽略了测试的隔离性,测试用例之间共享全局变量,结果一个测试失败导致后续全挂。后来每个测试用例都独立初始化,问题解决。
- 我曾经把CI跑在Windows上,结果路径分隔符、换行符各种问题。现在统一用Linux环境,省心多了。
我的建议:刚开始做CI/CD时,不要追求一步到位。先配一个最简单的流程:代码提交→编译→跑测试。跑通了,再慢慢加静态检查、打包、部署。小步快跑,比憋大招靠谱。
好了,关于CI/CD的内容就聊到这里。记住一句话:自动化不是目的,质量才是。CI/CD只是帮你把质量检查变成自动化的手段。工具选GitHub Actions还是Jenkins不重要,重要的是你真正用起来了,并且坚持下去了。
公众号:蓝海资料掘金营,微信deep3321