第11章 建立测试体系与GitLab持续集成

截至v0.5,项目已经积累了多类检查,但它们分散在各章记录中。本章建立持续集成(Continuous Integration,CI)。CI把可重复的检查纳入项目工具和GitLab流水线,为合并请求产生质量证据。

需求和风险
→ 测试用例
→ 本地统一命令
→ GitLab流水线
→ 合并请求检查
→ 发布候选版本

持续集成不能代替真实硬件和用户场景。它适合自动发现确定性错误;板卡、网络、功耗和现场效果仍需要明确的人工或硬件在环验证。

人工检查表适合低频发布,但频繁合并会使重复检查容易遗漏。CI把确定性检查交给统一环境执行,从而较早发现回归。它需要维护运行器和依赖,也可能受到不稳定测试影响,因此不能代替工程判断。

完成本章后,应当能够:

11.1 从需求和风险形成测试

11.1.1 每项测试都要有对象

测试不是为了增加文件数量,而是验证一个已经声明的行为或风险:

PR-02 事件送达
→ FR-004 事件上报
→ API-001 接收接口
→ IT-004 合法事件被接受
→ IT-005 重复事件不重复写入
→ ST-002 真实设备端云闭环

在docs/verification/test-matrix.md维护:

| 测试ID | 来源 | 层级 | 输入/条件 | 预期 | 执行方式 | 证据 | 状态 |
|---|---|---|---|---|---|---|---|
| CT-001 | EVENT-001 | 契约 | 合法示例 | Schema通过 | 自动 | CI日志 | Active |
| IT-005 | FR-004 | 集成 | 重复event_id | 单条记录 | 自动 | 测试报告 | Active |
| HT-003 | NFR-003 | 实机 | model-0.3.0 | RAM在预算内 | 人工/设备 | 资源报告 | Active |

测试状态可以是Planned、Active、Blocked或Retired。删除或停用测试必须说明对应需求是否也已改变。

11.1.2 测试层级

层级 主要对象 是否适合每次合并请求自动运行
静态检查 文件、格式、配置、秘密模式 是
单元测试 纯函数、状态机、缓冲逻辑 是
契约测试 JSON模式、构建物清单、接口样例 是
主机集成测试 服务器、存储、模型包、回放 是
固件构建 多配置编译、资源上限 是
硬件在环 开发板、传感器、真实推理 视课程设备条件
场景测试 用户、安装和现场网络 否,按版本执行

越接近底层的测试通常越快、越稳定;越接近真实场景的测试覆盖越完整但成本越高。项目需要组合,而不是只选择一种。

11.2 为AI变更先建立验收测试

11.2.1 用失败示例限制AI范围

假设议题要求“缺少model_version的事件必须被拒绝”。在让AI改服务器前,先增加一个失败测试:

给定:有效事件样例
当:删除model_version
则:Schema检查失败,服务器不写入

提交可以分为:

test(contract): reject event without model version
fix(server): enforce model version in event ingestion

第一提交描述缺陷的可执行表现,第二提交修复实现。AI更难通过增加无关功能来绕开问题。

11.2.2 测试本身也需要评审

检查:

AI可以生成测试框架,但学生必须故意破坏实现一次,确认测试能够捕获错误。

11.3 统一本地验证入口

tools/project.py增加:

python tools/project.py validate
python tools/project.py test-unit
python tools/project.py test-contract
python tools/project.py test-server
python tools/project.py test-model-package
python tools/project.py test-replay
python tools/project.py build-firmware
python tools/project.py release-check

release-check按固定顺序调用发布所需的非硬件检查,并在任一步失败时返回非零退出码。持续集成依据退出码判断作业成功或失败。

11.3.1 命令必须从干净环境可运行

验证:

  1. 新建或使用干净工作目录;
  2. 克隆指定提交;
  3. 按锁定文件安装依赖;
  4. 取得经过核对的模型构建物;
  5. 执行统一命令;
  6. 检查没有依赖个人全局配置。

如果命令只在开发者电脑上成功,应先修复环境说明或依赖锁定,再接入持续集成。

11.3.2 固定工具版本

项目保存:

requirements-ci.txt或等价锁定文件
固件工具链版本
板卡支持包版本
模型运行时版本
课程CI镜像版本

“安装未固定版本”会使同一提交在不同日期产生不同结果。依赖升级建立独立议题和合并请求,并重新执行相关测试。

11.4 认识GitLab流水线

早期软件项目常在开发结束后集中测试。问题会在较多变更叠加后才被发现,因而难以定位。持续集成把测试提前到每次合并附近,使失败能够关联到较小的差异。

持续集成依赖稳定环境和确定性测试。它也会占用计算资源,并增加流水线维护成本。因此,项目应把可重复检查自动化,把现场判断保留给实机和用户验证。

GitLab持续集成由项目根目录.gitlab-ci.yml定义:

流水线 Pipeline
├─ validate阶段
│  ├─ 文件与契约作业
│  └─ 敏感信息作业
├─ test阶段
│  ├─ 单元测试作业
│  ├─ 服务器测试作业
│  └─ 模型包测试作业
├─ build阶段
│  └─ 固件构建作业
└─ package阶段
   └─ 发布候选构建物作业

流水线运行的是一个确定Git提交。查看失败时先记录CI_COMMIT_SHA,不要只写“最新代码失败”。

11.5 编写第一份.gitlab-ci.yml

课程环境卡提供经过批准的持续集成镜像和运行器。下面结构需要把镜像变量替换为校内部署的实际值:

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_COMMIT_TAG'

stages:
  - validate
  - test
  - build
  - package

default:
  image: "$COURSE_CI_IMAGE"
  before_script:
    - python --version
    - python tools/project.py ci-info

validate:
  stage: validate
  script:
    - python tools/project.py validate
    - python tools/project.py test-contract
    - python tools/project.py test-model-package

secret-check:
  stage: validate
  script:
    - python tools/secret_check.py

unit-test:
  stage: test
  script:
    - python tools/project.py test-unit
  artifacts:
    when: always
    reports:
      junit: build/reports/unit-tests.xml

server-test:
  stage: test
  script:
    - python tools/project.py test-server
  artifacts:
    when: always
    reports:
      junit: build/reports/server-tests.xml

replay-test:
  stage: test
  script:
    - python tools/project.py test-replay
  artifacts:
    when: always
    paths:
      - build/reports/replay-summary.json

firmware-build:
  stage: build
  script:
    - python tools/project.py fetch-model model-0.3.0
    - python tools/project.py build-firmware --release
    - python tools/project.py firmware-budget-check
  artifacts:
    paths:
      - build/firmware/
      - build/reports/firmware-size.json

release-package:
  stage: package
  script:
    - python tools/project.py package-candidate
  artifacts:
    paths:
      - build/release/
  rules:
    - if: '$CI_COMMIT_TAG'

该示例表达流水线结构,不保证直接适用于所有校内GitLab版本。提交前使用校内持续集成语法检查,并按实际报告路径调整。

11.6 控制流水线触发范围

11.6.1 合并请求和主分支都要运行

workflow.rules规定:

合并请求流水线验证源分支内容。若校内GitLab启用“合并结果流水线”,还可以验证源分支与目标分支合并后的结果;是否启用由课程管理员根据版本和运行器资源配置。

11.6.2 不要为节省时间跳过关键检查

可以使用changes只在相关文件变化时运行昂贵作业,但要考虑间接影响。例如:

若依赖关系难以准确表达,初期宁可运行完整基础流水线。

11.7 管理持续集成变量和秘密

11.7.1 不把秘密写进YAML

.gitlab-ci.yml只引用变量:

script:
  - python tools/project.py integration-test
variables:
  EVENT_SERVER_URL: "$CI_TEST_EVENT_SERVER_URL"

真实令牌在GitLab项目设置中保存为受保护、掩码或文件类型变量,具体能力取决于校内版本。

11.7.2 合并请求流水线默认不使用生产秘密

来自普通任务分支或分叉仓库的代码尚未通过评审,不应自动取得高权限变量。基础合并请求流水线使用:

需要真实设备或受控服务的作业,应设置为受保护分支、受控运行器或人工批准,并限制执行者。

11.7.3 敏感信息检查

tools/secret_check.py至少检查:

扫描命中后由人工判断。不能为消除失败而把真实秘密加入允许列表;应先吊销、删除并评估历史影响。

11.8 产生可审查的测试报告和构建物

11.8.1 日志应帮助定位

每个作业输出:

Git提交
工具版本
模型版本和散列
契约版本
执行命令
测试数量
失败摘要

日志不输出完整令牌、原始个人数据或无限长度的调试内容。

11.8.2 构建物不是永久发布

流水线构建物可以保存:

它们有保留期限,也可能受访问控制。正式发布还要形成带版本、散列值和说明的发布记录。某次流水线的下载链接不能代替发布记录。

11.8.3 发布构建物绑定提交

固件构建信息至少嵌入:

产品版本
Git提交
构建时间或可复现构建标识
模型版本
策略版本
硬件目标

用户反馈问题时,设备日志能够指出实际运行组合。

11.9 分析流水线失败

11.9.1 从第一条有效错误开始

处理顺序:

  1. 记录流水线和提交标识;
  2. 找到第一个失败作业;
  3. 找到该作业第一条实际错误;
  4. 判断是代码、测试、依赖、环境还是服务问题;
  5. 在相同提交上本地复现;
  6. 建立最小修复;
  7. 运行失败作业及受影响检查;
  8. 推送后观察新流水线。

后续大量错误可能由第一项失败连锁产生。把全部日志交给AI并要求“修好持续集成”,容易造成大范围修改。

11.9.2 给AI提供受限故障上下文

任务:分析unit-test作业的第一条失败。

提交:<实际SHA>
失败命令:python tools/project.py test-unit
第一条错误:<原文>

允许读取:
- 失败测试文件
- 对应实现文件
- 相关接口头文件

先回答:
1. 错误发生在哪一层;
2. 它是否由当前MR引入;
3. 最小修复候选;
4. 需要重新运行的测试。

不修改CI配置,不删除测试,不更改验收值,不安装新依赖。

人工确认原因后再允许实施。

11.9.3 不接受以下“修复”

这些变化让流水线变绿,却没有修复产品。

11.10 处理不稳定测试

同一提交在相同环境中时而成功、时而失败的测试称为不稳定测试。常见原因:

处理:

  1. 保存至少两次相反结果;
  2. 建立缺陷议题;
  3. 隔离共享状态;
  4. 固定输入、时间和随机种子;
  5. 把外部服务替换为受控测试服务;
  6. 修复后重复运行。

关键合并条件中的测试不能长期通过“重跑直到成功”处理。

11.11 接入硬件在环验证

若课程具备专用开发板和受控运行器,可以建立人工触发作业:

hardware-in-loop:
  stage: test
  script:
    - python tools/project.py hil-test --board "$COURSE_BOARD_ID"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
  tags:
    - course-hardware

硬件在环作业要处理:

没有专用设施时,继续使用规范化人工记录。不能用一条总是成功的“占位作业”代替实机检查。

11.12 设置合并条件

教师或维护者在GitLab中配置:

[ ] main禁止直接推送
[ ] 合并请求流水线必须成功
[ ] 所有阻塞讨论必须解决
[ ] 至少一名同伴评审
[ ] 模型、硬件或数据变化请求对应责任人评审
[ ] 禁止合并存在冲突的分支
[ ] 合并后主分支流水线再次运行

自动检查负责机械规则,评审者负责需求、架构、风险和证据判断。两者缺一不可。

11.13 形成发布候选版本

11.13.1 发布候选检查

运行:

python tools/project.py release-check

并完成:

[ ] 所有v1.0范围需求有实现和测试
[ ] 契约、数据、模型和硬件清单一致
[ ] 固件发布构建通过资源预算
[ ] 服务器和回放测试通过
[ ] 真实设备端云验证完成
[ ] 敏感信息检查通过
[ ] README.md和已知限制更新
[ ] 未关闭的阻塞议题为0

11.13.2 创建预发布标签

语义化版本中,v1.0.0-rc.1表示第一个1.0发布候选:

git switch main
git pull --ff-only
git tag -a v1.0.0-rc.1 -m "v1.0.0 release candidate 1"
git push origin v1.0.0-rc.1

试用中发现问题后,以新提交修复并创建rc.2,不移动rc.1标签。

11.14 本章小结

本章把分散的工程检查组织为分层测试。GitLab持续集成为合并请求、主分支和版本标签生成质量证据。流水线验证契约、代码、模型包和固件构建。硬件与场景验证继续由真实设备和规范记录完成。

AI可以生成测试、解释第一条实际错误和实施小范围修复,但不能删除检查、放宽验收或隐藏失败。v1.0.0-rc.1为下一章的小规模试用、开源审查和正式发布提供了固定基线。

11.15 综合实践

  1. 从本组一项需求和一项风险出发,建立测试追踪。分别设计自动测试、实机测试和场景验证,并说明三者边界。
  2. 为项目设计最小CI流水线。选择阶段、作业、构建物和合并条件,说明每项检查为什么适合自动执行。
  3. 有意引入一个安全的契约或构建错误。根据流水线第一条有效错误完成定位,并形成最小修复合并请求。
  4. 选择一项硬件在环检查,设计触发条件、设备身份、结果记录和失败处理。说明它为什么不能并入普通运行器。
  5. 评审当前合并门禁,提出一项能够提高质量且成本可接受的改进,并用一次实际流水线结果验证。

11.16 拓展阅读