Replace task/batch shorthand IDs and residual layer labels in human-facing docs with semantic phrases. Add the new cleanup policy to docs/编号规范.md and update the entry docs. Machine keys (TC/CODE-RULE/MENU-STATE/TRAP/FR), hardware designators/pins/MAC, protocol/version numbers and frozen historical evidence are intentionally kept. Docs only; no code behavior change. Verified with check_doc_links.py --all and git diff --check.
44 KiB
测试开发标准流程(STM32F4-Base)
定位:本项目测试开发的总流程,覆盖"规划 → 板端套件 → 主机侧工具 → 验证门禁 → 提交沉淀"全链路,并给出未来测试设计的约束。 各环节的具体操作见下表对应文档,本文只讲流程、门禁与约定,不重复其内容。
关联文档
| 文档 | 内容 |
|---|---|
docs/测试说明/流程与规范/自动化测试流程.md |
/serial-boot-test 闭环细节(命令 / 标记 / 失败模式) |
docs/测试说明/流程与规范/测试文档模板规范.md |
测试文档模板、命名规则(TM / TC / DP) |
docs/测试说明/测试工具/串口助手设计说明.md |
串口调试 / 采集工具与陷阱(Python 串口助手) |
docs/测试说明/测试工具/串口助手使用指南.md |
串口助手命令用法 / 工作流 / 排障 |
docs/UI设计/UI_自动化测试流程.md |
UI 模拟器回归(ui-verify)细节 |
docs/代码规范/pre-commit使用说明.md |
pre-commit 门禁配置与范围 |
docs/环境与部署/项目部署环境要求.md |
构建 / 测试工具链环境 |
docs/CI/README.md |
远程 CI(Gitea Actions)唯一入口:服务端 / runner 部署与保活 / workflow / 排障(仅本机一台 runner,日常开发不必读) |
docs/测试说明/<模块>_Test_Guide.md |
各模块测试规范(CH395F / GD5F / SD2506 / FTP) |
1. 测试分层与门禁总览
测试体系分四层,门禁由便宜到昂贵依次布防,改动须过对应门禁方可提交:
| 名称 | 载体 | 抓住什么 |
|---|---|---|
| 静态门禁 | pre-commit 钩子(cppcheck / 代码规范 / 乱码 / 空白 / gitlint)+ /keil-build |
编码规范、静态缺陷、任何编译警告 |
| 主机侧纯逻辑单测 | test/host/<模块>/run_unit_test.py + MSVC 离线编译真实源码(#include 纳入)+ 平台桩 |
纯逻辑模块的边界穷举(节流窗口、计数阈值、整数回绕)——板端无法覆盖、无需硬件 |
| 板端测试套件 | testTask + test/board/test_config.h 套件 |
驱动 / 协议功能(串口 [PASS]/[FAIL] + === ... PASSED 汇总) |
| 主机侧回归 | /serial-boot-test 串口日志标记 + LVGL 模拟器像素 / 池水位回归 |
启动序列、UI 渲染、LVGL 内存池水位 |
| 人工目检 | 上机屏幕、PC 端交互工具 | 视觉内容、自动化无法覆盖的场景 |
主机侧纯逻辑单测与主机侧回归的区别:主机侧回归的模拟器回归依赖 SDL / LVGL 与图形输出;主机侧纯逻辑单测是纯计算单测 (无窗口、无硬件、秒级),专测"判定逻辑"。目前仅看门狗模块使用,见
看门狗单元测试说明.md。适格对象:算法 / 状态机 / 协议编解码 / 参数校验等无副作用的模块。CI:静态门禁 + 主机侧纯逻辑单测已上 CI(Gitea Actions,
.gitea/workflows/ci.yml,2026-10-01); 板端测试套件/主机侧回归/人工目检需硬件,只在本地。本地等价命令scripts/ci_local.py。详见 §9。
1.1 门禁决策表(改动类型 → 必跑门禁)
| 改动类型 | 必跑门禁 |
|---|---|
| 任何源码提交 | G0 pre-commit(提交时自动触发)+ G1 /keil-build |
| App / Drivers 功能代码(板端行为可观测) | G0 + G1 + G2 /serial-boot-test(模块标记) |
| 有主机侧纯逻辑单测的模块(如看门狗) | G0(含该模块单测钩子,自动)+ G1 /keil-build(+ 台架项另按需 G2) |
| UI(菜单 / 内容页 / 字体 / 样式 / 几何) | G0 + G1 + G2 + G3 ui-verify 五步 |
| UI 新增中文文案 | 另需 font-regen(字体重生成 + 回归),先于提交 |
test/ 套件或 PC 端工具代码 |
G1 + G2(套件标题标记 / 工具自测) |
| 仅文档 | G0 |
G0~G3 命令详见 §6 / §7;ui-verify 完整命令见 UI_自动化测试流程.md。
2. 测试规划(文档先行)
2.1 何时必须写测试文档
新驱动 / 新模块 / 新功能 / 新协议 / 新接口:动手写代码前先立 docs/测试说明/<模块>_Test_Guide.md,结构遵循 测试文档模板规范.md(概述含设计原则覆盖表 → 阶段元信息表 → 测试用例表 → 脚本参考 → 已知陷阱 → 附录矩阵)。
2.2 命名(与模板规范一致)
| 对象 | 格式 | 现用示例 |
|---|---|---|
| 阶段 | 直接写中文「阶段 N」(不再发号) | 阶段 6 |
| 测试用例 | TC-{MOD}-{NNN} |
TC-NET-601(CH395F)、TC-GD5F-205、TC-STORAGE-03、TC-RTC-002 |
| 设计原则 | 写语义短语(不再发号) | 单连接 socket |
模块缩写 {MOD} 在新模块测试文档立文档时确定(2~4 个大写字母);现有:NET=CH395F 网络、GD5F、STO=存储、RTC、ADC、RS485。
2.3 三条铁律
- 先写判定标准,再写测试代码 —— 文档里先写"什么算过 / 什么算失败"(精确到返回值 / 日志字符串 / 像素参考),再实现,避免"测了但不知道算不算过"。
- 判定标准必须可机器判定 —— 必须能落到:串口日志标记(→
run_boot_test.py --required)、返回值 / 寄存器值(→TEST_CHECK)、像素参考(→ 模拟器回归)、池水位(→ 基线常量)之一。无法机器判定的标准只能放 人工目检人工层。 - 每条设计原则必须有对应用例 —— 覆盖表中每个 DP-xx 至少一个 TC;新功能设计时先定 DP,再设计 TC。
3. 板端测试套件(test/)
3.1 统一入口与套件互斥
所有测试套件经唯一入口调度:testTask(test/board/test_config.c 的 start_test_task()),按 test/board/test_config.h 选中的 TEST_SUITE_* 分发到套件运行函数:
flowchart TD
T["testTask: start_test_task()(test/board/test_config.c)"] -->|TEST_SUITE_CH395F| A["ch395f_test_run()<br/>硬件层 1~5 / NET 层 6~11"]
T -->|TEST_SUITE_STORAGE| B["storage_test_run()<br/>FTL + FatFS"]
T -->|TEST_SUITE_GD5F| C["gd5f_test_run()<br/>10 阶段"]
T -->|TEST_SUITE_RTC| D["sd2506_test_run()<br/>2 个阶段组"]
T -->|全部注释=产品构建| E["空闲(仅打印 suite=NONE)"]
- 套件互斥:一次构建最多启用一个
TEST_SUITE_*,否则#error(编译期门禁);全部注释 = 正常产品构建。 - 测试固件不得静默提交:
TEST_SUITE_*开关受 pre-commit 钩子test-suite-switch门禁(默认要求全部注释)。跑完套件回归务必改回产品构建再提交;确需提交测试构建时用ALLOW_TEST_BUILD=1显式放行(会打[WARN]横幅)。见 §7.1。 - main.c / freertos.c 不含任何测试逻辑 —— 测试代码只能出现在
test/。
| 套件 | 宏 | 文件 | 阶段组织 | 备注 |
|---|---|---|---|---|
| CH395F | TEST_SUITE_CH395F |
ch395f_test.h/.c + net_test_task.c |
硬件层 1 |
两层互斥(#error);阶段 2/3/4/5 与 6/7/8/10/11 需 PC 端配合 |
| STORAGE | TEST_SUITE_STORAGE |
storage_test.h + storage_test_task.c |
FTL + FatFS 集成(TC-STORAGE-01~04) | 恢复测试(重建 BBT)默认关闭 |
| GD5F | TEST_SUITE_GD5F |
gd5f_test.h + gd5f_test_task.c |
10 阶段(选中时自动全开) | NAND 驱动全覆盖 |
| RTC | TEST_SUITE_RTC |
sd2506_test.h + sd2506_test_task.c |
阶段 1 基本 / 阶段 2 API(默认全开) | SD2506 RTC |
3.2 套件文件模式
每个套件两个文件,模式一致(参照 ch395f_test.h / storage_test.h):
xxx_test.h—— 阶段使能开关(取消注释启用)+ 本套件独立统计结构体 +XXX_TEST_CHECK/XXX_TEST_REPORT宏 +xxx_test_run()声明;xxx_test_task.c——xxx_test_run()入口 + 每阶段一个static phaseN_run()。
断言宏(语义统一,统计变量按套件独立,防跨套件串数):
内核集中在 test_common.h(2026-10-01 抽取)——4 套件的宏此前逐字重复,改一处要改四处:
/* test/board/test_common.h */
typedef struct { uint16_t total, passed, failed; } test_stats_t;
#define TEST_CHECK_IMPL(stats, cond, fmt, ...) do { \
(stats).total++; \
if (cond) { (stats).passed++; DBG_INFO("[PASS] " fmt, ##__VA_ARGS__); } \
else { (stats).failed++; DBG_ERROR("[FAIL] " fmt, ##__VA_ARGS__); } \
} while (0)
#define TEST_REPORT_IMPL(stats, name) do { \
DBG_INFO("=== %s: %u/%u PASSED (failed=%u) ===", name, ...); \
} while (0)
各套件用一行宏把它绑定到自己的统计变量(保持 g_xxx_stats 独立):
/* 套件头文件内(如 gd5f_test.h) */
#include "test_common.h"
typedef test_stats_t gd5f_test_stats_t;
extern gd5f_test_stats_t g_gd5f_test_stats;
#define GD5F_TEST_CHECK(cond, fmt, ...) \
TEST_CHECK_IMPL(g_gd5f_test_stats, cond, fmt, ##__VA_ARGS__)
#define GD5F_TEST_REPORT(name) TEST_REPORT_IMPL(g_gd5f_test_stats, name)
⚠️
test_common.h刻意不 includedbg_log.h:dbg_log.h在未定义DBG_TAG时给出 默认值"",而本头文件被套件头在很靠前的位置包含——若在此处拉入,会把空DBG_TAG冻结,导致.c随后#define DBG_TAG "[TEST_TASK]"触发 ARMCC#47-D: incompatible redefinition(实测 8 条警告)。宏只在使用点展开(使用点在已按约定先定义DBG_TAG的.c内),故无需在此引入。
每 Phase 用标准注释头(与文档用例一一对应):
/*
* Phase N — 功能说明
* 测试目的 / 前置条件 / 通过 / 失败(四行,与 Test_Guide 文档一致)
*/
static void phaseN_run(void) {
DBG_INFO("=== XXX Phase N Tests ===");
/* ---- TC-XXX-NNN: 用例标题 ---- */
{
// 原理说明;通过:精确条件;失败:精确条件
ret = some_api();
DBG_INFO(" expect: ...");
DBG_INFO(" actual: ...");
XXX_TEST_CHECK(ret == EXPECTED, "description");
}
XXX_TEST_REPORT("Phase N");
}
套件运行结束行的 === <名称> Tests: N/N PASSED (failed=0) === 即该套件 /serial-boot-test 回归的默认标记(各套件确切字符串见对应模块 Test_Guide,如 === Storage Tests: / === GD5F2GQ5UE Tests:)。
3.3 编码约定
| # | 约定 | 说明 |
|---|---|---|
| 1 | 判定点三行输出 | expect: / actual: / [PASS]/[FAIL],一眼看出"期望什么、拿了什么、过没过" |
| 2 | 硬编码值加注释 | 协议字节 / 寄存器 / 阈值必须注释含义与来源(手册页 / 协议条款) |
| 3 | 日志字符串纯 ASCII | ARMCC v5 不识别 UTF-8 多字节字符(#870-D 警告);注释可中文,DBG_* 字符串不可 |
| 4 | 大缓冲用 static | 大缓冲(64KB 级)static uint8_t s_rx_buf[N],不放任务栈 |
| 5 | 条件编译包裹变量 | 仅特定阶段使用的变量 / 函数用 #if 包裹,消除 #177-D 未使用变量警告 |
| 6 | 就绪等待用就绪屏障 | 任务间时序依赖 SYS_EVT_*(osEventFlagsWait),禁止魔法延时 |
注意:
test/在 pre-commit 检查范围之外(工具代码豁免)——上表约定属自律要求,但 0 错 0 警的构建红线对测试代码同样生效。
3.4 测试构建下的 netTask(2026-09-15 已修复,未来测试设计必读)
代码事实(2026-09-15 修复后):
netTask= 基础线程:定义于.ioc→Src/freertos.c的 CubeMX 生成区无条件创建(全构建唯一实例)。产品 / NET 层测试模式下正常运行:等存储就绪(≤10s)→net_stack_bring_up()(CH395F 初始化)→ 置SYS_EVT_NET_READY→net_poll()轮询循环。USER CODE 区在→ 2026-09-15 已删除:NET 层测试构建不再重复创建。ENABLE_NET_LAYER_TESTS下另建一个 netTask(双 netTask 并存)- CH395F 硬件层模式下
net_task_func入口自挂起(2026-09-15 守卫,ENABLE_HW_LAYER_TESTS):不 bring_up、不轮询、不广播 NET_READY;testTask 自行net_stack_bring_up()后独占 CH395F 跑各阶段。任务仍被创建(基础线程策略:恒存在、该模式"不运行"),占 4KB 栈——彻底方案(.ioc移除 + 条件创建)经评估未采用:网络线程是基础线程,仅测试模式"不运行"。
由此产生的设计约束:
- 硬件层模式:netTask 已挂起,无人消费/清除芯片全局中断标志——"中断状态寄存器全程 0x0000"类断言在该模式下安全(测试入口的预读清除仍须保留,用于清除
net_stack_bring_up自身强制 PHY 重协商锁存的PHY_CHANGE残留)。 - NET 层模式 / 产品构建:netTask 恒存在且在轮询(全局中断由它消费清除)——新增测试用例不得断言"无人动芯片/中断"。
- 硬件层模式下挂起的 netTask 仍占 4KB 栈,为基础线程策略的接受成本(彻底方案可回收,暂无计划)。
3.5 新测试文件 / 新套件 checklist
test/下新建xxx_test.h/xxx_test_task.c(按 §3.2 模式);test/board/test_config.h:补TEST_SUITE_XXX注释开关 + 互斥#error加项 + 分发分支(include +TEST_SUITE_NAME);test/board/test_config.c:start_test_task()加分发分支;- 双端注册核查(
new-source-fileskill):新.c必须加入MDK-ARM/STM32F407-Demo.uvprojx;test/ 代码仅 MCU 端编译,不登记模拟器 CMakeLists(UI / App 共用代码才需双端); /keil-build→ 0 错 0 警;- 立
docs/测试说明/<模块>_Test_Guide.md(§2 模板); /serial-boot-test以套件标题行为-Required标记跑通回归。
3.6 多套件回归编排(test/run_all_suites.py,2026-10-01 加)
套件互斥(编译期 #error)→ 跑 4 个套件的回归本来要手工循环 4 轮
(改宏 → 构建 → 烧录 → 采集 → 判读)。本脚本编排成一条命令:
& '.venv\Scripts\python.exe' test/run_all_suites.py # 全部套件
& '.venv\Scripts\python.exe' test/run_all_suites.py --suites STORAGE,GD5F
& '.venv\Scripts\python.exe' test/run_all_suites.py --dry-run # 只看计划
& '.venv\Scripts\python.exe' test/run_all_suites.py --check-build # 只编译不烧录
核心保障:try/finally 无条件还原 test_config.h(含异常 / KeyboardInterrupt
路径)——跑完忘还原 = 测试固件进库。这与提交期门禁 test-suite-switch(§7.1)
是两条独立防线:前者是流程自动化、后者是门禁兜底。
⚠️ 完整回归不挂 pre-commit:每轮真机烧录 + 采集(8s 静默 + boot),4 套件约数分钟起, 且破坏性套件会改设备持久状态。仅回归窗口手工使用。
3.6.1 --check-build:只编译不烧录(2026-10-02 加)
解决的问题:测试套件的构建可编译性没有独立门禁——产品构建 /keil-build 天天跑,
但测试构建可能长期"编不过"而无人知,直到某次要用才发现(2026-08~09 CH395F 套件
7 个配置实例)。
& '.venv\Scripts\python.exe' test/run_all_suites.py --check-build
| 项 | 说明 |
|---|---|
| 做什么 | 逐套件:改 test_config.h 宏 → /keil-build → 记 PASS/FAIL;结束后无条件还原 |
| 不需要 | MCU / 串口 / ST-LINK(纯离机,可在 CI 或任何有 Keil 的机器上跑) |
| 判定 | /keil-build 退出码 0 = PASS;非 0(含 L6406E 链接失败)= FAIL |
| RAM 溢出 | "编译过但链接失败"这类问题会自然暴露为 L6406E——这正是想要的 |
| 退出码 | 同完整模式:0=全 PASS / 1=有套件编不过 / 2=用法缺文件 |
2026-10-02 实测:4 套件(CH395F / STORAGE / GD5F / RTC)全部
BUILD_OK: 0 errors, 0 warnings——即前述"CH395F 套件不可编译"问题已修复,现已纳入可自检范围。
⚠️ 隐藏陷阱:
.map必须复位为产品态(2026-10-02 实测踩到)。脚本跑完最后一轮留下的STM32F407-Demo.map是测试套件的体积(RTC 套件下 ER_IROM1 达 266 KB), 会被mem-numbers钩子判为"文档快照与.map不一致"→ 下次提交直接 FAIL。 故脚本在还原test_config.h之后追加一次产品构建([..] 复位构建产物为产品态)—— 仅还原源码不够,必须再编一次。同理适用于完整回归模式。
建议用法:改动 test/board/ 下任何测试代码、或改 test_config.h 后,跑一次
--check-build 再提交;比逐个手工改宏构建省事,且不会忘记还原(含 .map 复位)。
4. 主机侧测试工具
4.1 工具清单
| 工具 | 位置 | 作用 |
|---|---|---|
| Python 串口助手(serial_tool) | .dsh/skills/serial-assistant/scripts/serial_tool.py(.venv + pyserial) |
常驻 daemon + 薄 CLI:列端口 / 开关 / 收发 / 全量日志 / wait-idle |
.dsh/skills/serial-boot-test/scripts/run_boot_test.py |
test/host/ |
一键闭环:采集 → 烧录 → 提取日志 → 校验 |
test/run_all_suites.py |
test/ |
多套件回归编排 + --check-build 只编译不烧录(纯离机,验证 4 套件可编译性);见 §3.6 |
.dsh/skills/serial-boot-test/scripts/verify_log.py |
test/host/ |
日志标记校验(Required / Forbidden → PASS / FAIL)+ 可选套件汇总行校验 |
test/host/common/unit_test_harness.py |
test/host/common/ |
主机侧纯逻辑单测通用单测 harness:工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码契约。各模块运行器只做声明式配置 |
test/host/watchdog/run_unit_test.py |
test/host/watchdog/ |
主机侧纯逻辑单测:声明 TestBuild 后调 harness,穷举看门狗判定状态机(零硬件、秒级)。需本机装有 VS(cl.exe),见 §4.2 环境要求 |
test/host/sys_clock/run_unit_test.py |
test/host/sys_clock/ |
主机侧纯逻辑单测:穷举 sys_clock 时间换算 / 星期 / 惰性再基准化 / RTC 故障降级(100 断言,首批主机侧单测) |
test/host/cfg_file/run_unit_test.py |
test/host/cfg_file/ |
主机侧纯逻辑单测:INI 解析 / 值编解码 / 段注册 / load-save-poll 契约,内存虚拟 FS 驱动(139 断言,首批主机侧单测) |
test/host/nand_port/run_unit_test.py |
test/host/nand_port/ |
主机侧纯逻辑单测:离线编译真实 Drivers/BSP/GD5F2GQ5UE/nand_ftl.c + 真实 dhara,以 RAM 假芯片驱动,149 断言钉死 is_free 整页扫描与六个入口的越界守卫 |
scripts/ci_local.py |
scripts/ |
本地跑与 CI 等价的门禁(静态门禁 + 主机侧纯逻辑单测):自动选用装了 pre-commit 的解释器;.map 自适应跳过 mem-numbers;退出码 0/1/2 与 CI 对齐。见 §9.3 |
.serialtest/config.json |
仓库 | 串口端口单一配置源(当前 COM6 / 115200);换端口只改这个文件 |
test/host/ftp_*.py(4 个)/ ch395f_socket_test.py / time_sync_server.py |
test/host/ |
PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) |
simulator/tests/*.py(4 个) |
simulator/tests/ |
LVGL 模拟器回归脚本(见 §5) |
4.2 主机侧纯逻辑单测(纯逻辑,无硬件)
适用对象:无副作用的纯逻辑模块——算法、状态机、协议编解码、参数校验等 (例:看门狗的位图判定 / 掩码 / 计数阈值 / 整数回绕)。
做法(以看门狗为范式,见 看门狗单元测试说明.md):
- 模块的硬件访问收敛到薄平台层(
__weak钩子或后端函数)——参照App/cfg/cfg_file_store.h的"共享核心 + 平台后端"; - 测试单元
#include生产源文件(保证测的是真代码,非副本),用桩替换平台层; - 写模块侧运行器:只声明
TestBuild(源码 / 桩目录 / 汇总行标记)+ 调test/host/common/unit_test_harness.py(工具链探测与编译运行细节在 harness 内, 见../../../test/host/common/README.md); - 挂 pre-commit 钩子,被改动时自动跑(只 commit 不烧录,秒级)。
环境要求(详见 ../../环境与部署/项目部署环境要求.md §2.3 / §2.7):
| 项 | 要求 | 说明 |
|---|---|---|
MSVC cl.exe |
装了 VS(2019/2022/2026)即有 | 与 LVGL 模拟器共用同一套 VS,无需另装;脚本按本机 VS/SDK 目录自动拼 INCLUDE/LIB 后直接调 cl.exe |
| Windows SDK | 随 VS 勾选"使用 C++ 的桌面开发"+ SDK | cl.exe 编 stdio.h 等标准头从这里取 |
| Python | 仓库 .venv 或任意 python3 |
运行器纯标准库,无需第三方依赖 |
| 硬件 | 无 | 不需要开发板 / 串口 / ST-LINK |
| 缺失时 | 运行器报环境错误(退出码 2)→ pre-commit 钩子拦截提交 | 未装 VS 的机器改动被测文件时会被拦,须先装 VS |
⚠ 不调
vcvars64.bat:它内部调用reg.exe探测环境,部分受限机器(安全策略禁reg.exe) 会失败 → 无INCLUDE→fatal error C1083: 无法打开包括文件 "stdio.h"。 运行器因此直接拼INCLUDE/LIB环境变量 + 绝对路径调cl.exe,无副作用、可重复。
约束:
- 平台层的抽取必须零行为变化、零额外开销(MCU 端默认实现即原逻辑;
/keil-build须 0/0); - 测试专用钩子(如静态态复位)用
#ifdef <模块>_UNIT_TEST守卫,MCU 构建不编入(零符号); - 单测不能替代板端验证:真机寄存器 / 复位原因 / 时序仍须走板端测试套件 / 主机侧回归台架闭环。
4.2.1 主机侧纯逻辑单测:适格模块台账(2026-10-02 加)
问题:主机侧纯逻辑单测起步阶段只有"看门狗"一个先例,下一个该测谁靠临时判断,易遗漏高价值模块。
判据(是否适格 主机侧纯逻辑单测):模块的核心逻辑是否无副作用且与硬件解耦—— 满足"纯逻辑 + 已抽象平台层(或依赖全是外部符号/宏)"即可主机侧穷举。
| 模块 | 规模 | 状态 | 平台层形态 | 备注 |
|---|---|---|---|---|
App/util/task_watchdog.c |
— | ✅ 已覆盖(39 断言) | 内联 __weak 钩子 |
范式来源;#ifdef TASK_WATCHDOG_UNIT_TEST 复位钩子 |
App/util/sys_clock.c |
479 行 | ✅ 已覆盖(100 断言,2026-10-02 首批) | 外部符号(HAL_GetTick/sd2506_*)+ 宏 |
零侵入(无需 #ifdef);首测即抓出 Zeller 星期缺陷 |
App/cfg/cfg_file.c |
1250 行 | ✅ 已覆盖(139 断言,2026-10-02 首批) | 后端抽象(cfg_file_store.h) |
内存虚拟 FS 桩;首测即抓出外科手术保存死码 P0(见 §5) |
App/log/sys_log_mcu.c |
— | ⬜ 候选 | 记录编解码(定长 64B) | 编解码纯逻辑;写入端依赖 FatFS + 队列,测编解码即可 |
App/util/time_sync.c |
— | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 |
Drivers/BSP/NETCFG/net_cfg_legacy.c |
— | ⛔ 已删除(2026-10-10) | — | 旧档 NCFG 迁移整体移除(旧档 /cfg/netcfg.bin 不读、不删、不迁移),测试候选随之撤销 |
App/protocol/iec104/*(104 协议栈) |
— | ✅ 已覆盖(82 断言,2026-10-12 IEC 104 上行) | 数据源 vtable + tick 注入 + 发送回调注入 | APCI 重组/总召分片/COV/k-w-t1~t3/两段式全离线穷举;iec104-unit-test 钩子 |
Drivers/BSP/GD5F2GQ5UE/nand_ftl.c(dhara 移植层) |
573 行 | ✅ 已覆盖(149 断言,2026-10-15 起离线钉死 dhara 移植层两条硬约束) | 11 个 SPI 原语(gd5f2gq5ue.h)→ RAM 假芯片 |
移植层是纯地址算术 + 边界判定,换掉芯片即可离线跑;钉死 is_free 整页扫描与六入口越界守卫,并内联旧算法做对照证明断言有牙 |
App/util/rs485.c |
— | ⏸ 待功能实现 | — | 上游 RS-485 采集链路;当前为空桩 |
Drivers/BSP/TPAFE5160/*.c |
— | ❌ 不适格 | — | 寄存器序列为主,需真机时序 |
| 其余驱动(CH395F/GD5F2GQ5UE 芯片层/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线。⚠ 例外:GD5F 之上的 nand_ftl.c 移植层适格(见上一行)——"驱动不适格"不等于"驱动之上的胶水层不适格" |
App/util/crc.c · ringbuf.c |
120/154 行 | ⛔ 已删除(2026-10-02) | — | 孤儿文件,零引用,已 git rm |
使用方式:新增/改动上述模块前,按表判断是否需补 主机侧纯逻辑单测;⬜ 候选 表示"适格但未做",
在性价比允许时应排入(判据:边界密集、零硬件、单次真机回归 30s2min vs 单测 25s)。
⭐ 两条实证:
sys_clock与cfg_file首次落地即各抓出一处 生产缺陷(Zeller 星期公式错误 / 外科手术保存死码)。这印证了本台账的性价比判断—— 高价值纯逻辑模块的 主机侧纯逻辑单测覆盖,是投入最小、产出最大的测试改进方向。
4.3 新工具约定
- test/ 自动化脚本为纯 Python 标准库(2026-09-20 由 PowerShell 迁移:绕开控制台代码页/执行策略陷阱,全部 I/O 显式 UTF-8/字节级处理;
requirements.txt仍须纯 ASCII——pip 按 GBK 代码页解码); - Python 统一用仓库
.venv;第三方依赖必须进requirements.txt(纯 ASCII); - 落位:板端交互工具 →
test/;模拟器回归 →simulator/tests/; - 新工具引入新的"通过标记"时,必须登记到 §6.2 标记表,供
/serial-boot-test闭环复用。
5. UI 测试门禁(ui-verify)
触发条件:任何 Drivers/BSP/UI/ 下改动、字体(ui_font_cn_12.c / ui_cn_chars.txt)、菜单 / 内容页。
5.1 五步门禁(顺序执行,全过)
cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警告)
.venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 1) 菜单 8 状态像素回归 + 池基线
.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 2) 内容页黄金参考 + 往返池水位
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py # 3) Keil 构建(UI 源码双端共用,改一端必过另一端)
.venv\Scripts\python.exe scripts/check_coding_standard.py # 4) 代码规范机械检查
远显推流通道另按需跑 simulator/tests/check_remo_sink.py。
5.2 三层断言(两个回归脚本内置)
| 层 | 载体 | 抓住什么 |
|---|---|---|
| 文本层 | check_content_pages.py 字形模板匹配(310 条、覆盖当时 37 页,解析字体 cmap / glyph_dsc / bitmap) |
缺字显示空白 / 标签写错等静默失败(不依赖 ref/,先于像素比对运行、失败即中止防误刷参考) |
| 像素层 | 内容页黄金参考 66 张(64 页)+ 菜单 8 状态,与黄金参考逐字节比对 | 几何 / 样式 / 光标反显 / 整体外观 |
| 池水位层 | MENU_POOL_BASELINE=5368 B / POOL_AFTER_ROUNDTRIP=5448 B |
LVGL 内存池泄漏 / 结构性漂移(量的是 used,与 LV_MEM_SIZE 总大小无关) |
5.3 黄金参考与字体纪律
simulator/tests/ref/*.bin(像素黄金参考)入库(2026-10-22 起,新克隆即可跑回归;跨独立构建目录逐字节可复现已实测);派生的.png目检图仍不入库;--update有误刷保护(先跑文本层断言,失败即中止)——未有意改视觉而 FAIL = bug,修代码,禁止刷参考掩盖;基线入库后刷参考会在git diff里显形,必须逐页过目并在提交正文说明;- 刷新参考后必须重跑
regen_ref_png.py重生成目检 PNG(否则旧 PNG 看似乱码); - 门禁常量漂移:先确认新值合理(残留分解见
菜单页面设计.md§9.5)→ 更新常量 + 在UI_自动化测试流程.md正文写清当前常量值与它的实测来源; - UI 新增中文文案提交前必须先跑 font-regen(extract_cn → make_font → diff 检查):字符集
ui_cn_chars.txt缺字则上机显示空白——2026-10-15 起该约定已入门禁(font-charset钩子:源码在用字符 ∉ txt 或 txt ∉ 字体 cmap → 提交 FAIL,详见docs/门禁说明/font-charset.md)。
6. 自动化闭环(/serial-boot-test)
6.1 流程
flowchart TD
A[代码改完] --> B["G1 /keil-build(0 错 0 警,否则修复重编)"]
B --> C["run_boot_test.py:烧录前武装采集(daemon 全量日志兜底)"]
C --> D["@download 烧录(Reset & Run 触发重启)"]
D --> E[提取日志 + verify_log 标记校验]
E -->|exit 0| F["PASS → G3(UI 改动)→ 人工目检上机目检 → 提交"]
E -->|exit 1 FAIL| G["[MISS]/[HIT] + 日志尾部定位 → 修代码"]
G --> B
E -->|exit 2/3 环境错误| H["按 自动化测试流程.md 失败模式表处理 → 重试"]
6.2 判定标记
默认:-Required "App init OK;Network Init;net init: OK" / -Forbidden "FAIL;ERROR;HardFault;assert"。
| 回归场景 | -Required 定制 |
|---|---|
| 网络 | App init OK;Network Init;net init: OK;PHY: 100M FULL |
| UI 初始化 | UI port init: OK;UI menu init: OK;UI net sink: started |
| 开机全量(存储 / RTC / RS485 / ADC) | 默认 + RS485 init: OK;NAND init:;ADC init:;SD2506 ID: |
| 测试套件构建 | 套件结尾汇总行(=== Storage Tests: / === GD5F2GQ5UE Tests: 等,确切字符串见各模块 Test_Guide) |
6.2.1 套件汇总行校验(2026-10-01 加)
⚠️ 纯 Required 子串匹配有盲区:套件启动横幅命中即判 PASS——套件是否跑到汇总行、 断言数是否为 0 完全不可见。套件中途挂死/复位、或 Phase 宏全关(没编入任何断言)时, 会被误判为通过。测试套件构建的回归必须加
--require-suite-summary。
# 测试套件构建回归(要求套件跑到汇总行,且断言数 > 0)
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py `
--required "=== Storage Tests:" --forbidden "ERROR;HardFault;[ERR]" `
--require-suite-summary "Storage Tests"
校验语义(verify_log.py):
| 情形 | 判定 |
|---|---|
汇总行存在、N>0、failed=0、N==M |
PASS(打印 [OK] suite summary ok) |
| 汇总行缺失(套件没跑到/挂死) | FAIL [MISS] |
total == 0(没编入任何断言) |
FAIL [FAIL] total is 0 |
failed != 0 |
FAIL [FAIL] reports failures |
passed != total |
FAIL [FAIL] inconsistent |
未传
--require-suite-summary时行为与旧版逐字一致(向后兼容,产品构建回归不受影响)。
6.3 三条铁律
- 采集必须先于烧录武装 —— 开串口 + clear 在烧录前完成(daemon 常驻全量日志兜底,不会漏字节;
run_boot_test.py已保证顺序); - 端口以
.serialtest/config.json为准 —— 每次连接前先读它; - 静默判据覆盖时间窗 ——
-SettleMs=8000(覆盖 boot ~3-4s 自然静默间隙)、-MaxWaitMs=30000(整体超时兜底);
7. 提交门禁与沉淀
7.1 提交门禁(G0,pre-commit)
提交被钩子门禁,任一失败即拦截:
| 钩子 | 内容 |
|---|---|
| trailing-whitespace / end-of-file-fixer / check-added-large-files / check-merge-conflict / check-yaml | 官方空白 / 文件尾 / 大文件 / 冲突 / YAML 检查 |
| gitlint | commit message 标题 ≤100 字符 |
| cppcheck | 静态分析(warning / performance / portability / style,含 suppress 清单) |
| no-mojibake | 禁止 U+FFFD 乱码字节(保护中文注释) |
| coding-standard | 代码规范 V1.0 机械检查(条款号 CODE-RULE-nn,规则表在 scripts/check_coding_standard.py) |
| test-suite-switch | 测试套件开关=产品构建(test/board/test_config.h 的 TEST_SUITE_* 须全注释;跑套件回归确需提交测试构建时用 --allow-test-build 显式放行) |
范围:仅项目自有代码受检;第三方与生成物(Lib/、Middlewares/、CMSIS/HAL、lftpd、dhara、MDK-ARM/、test/、CubeMX 生成的 Src/ Inc/ 等)被排除(详见 pre-commit使用说明.md)。
7.2 提交纪律
- 逻辑提交:一笔提交只做一件事(代码 / 文档 / 工具分开);
- 编码铁律(最高优先级):含中文文件只用 Edit 工具精确替换修改;禁止 PowerShell / cmd 整文件重写(U+FFFD 乱码不可逆);BOM 状态按原样保持(UI 相关源 /
lv_conf.h带 BOM,其余无 BOM); - 内存预算:新增任务 / 缓冲 / 栈前必须先对照 RAM 红线(当前 SRAM 占用 95.4%、仅余 ~5.8KB,见
docs/内存管理/)。
7.3 沉淀(必须)
| 事件 | 动作 |
|---|---|
| 测试发现陷阱(硬件 / 协议 / 工具链 / 时序) | 写入 docs/问题记录/<模块>_Trap_Records.md:现象 / 根因 / 修复 / 规避 |
| 代码变更影响判定标准 | 立即把模块 Test_Guide 正文改到与代码一致(现状说了算,不再记流水) |
| 有意改视觉 | 刷黄金参考 + 重生成目检 PNG + 把新口径写进 UI_自动化测试流程.md 正文对应小节 |
| 新增测试工具 / 新标记 | 自动化测试流程.md 命令清单登记(§4.2) |
| 栈 / 堆预算相关改动或基线重采 | 把 /serial-boot-test 的 boot_log 快照(%TEMP%\serial-boot-test\boot_log_*.txt)存入 docs/测试说明/测试数据/boot_log_data/ 并在其 §3 清单登记(防临时目录被清理 / 长跑日志截头丢 [STACK] 行) |
文档与代码一体两面:代码改了文档没同步 = 未完成。
8. 迭代节奏(完整闭环)
flowchart LR
A["文档:规划 + 判定标准<br/>(Test_Guide)"] --> B["代码:套件 / 工具实现<br/>+ 双端注册核查"]
B --> C["G1 /keil-build<br/>0 错 0 警"]
C --> D["G2 /serial-boot-test<br/>采集→烧录→校验"]
D -->|PASS| E["G3 ui-verify<br/>(UI 改动时)"]
E --> F["人工目检上机目检"]
F --> G["提交<br/>G0 门禁 + 逻辑提交"]
G --> H["沉淀:陷阱记录 /<br/>文档同步 / 正文现状化"]
D -->|FAIL| I["[MISS]/[HIT] 定位"]
I --> B
各模块测试迭代都走这个闭环:文档先行、门禁验收、沉淀回灌。
9. CI 持续集成(Gitea Actions,2026-10-01 加)
远程仓库为 Gitea 1.25.4(gitea.bitnasdaq.vip/wandering1/STM32F4-Base.git),支持 Gitea Actions
(兼容 GitHub Actions 语法,workflow 置于 .gitea/workflows/)。落地文件 .gitea/workflows/ci.yml。
9.1 覆盖范围:只做"不依赖硬件"的层
| 层 | 上 CI? | 载体 | 理由 |
|---|---|---|---|
| 静态门禁 | ✅ | pre-commit run --all-files(提交期 47 根快钩子)+「手动阶段门禁」步逐根 --hook-stage manual 跑 cppcheck / test-cppcheck / test-coverage |
纯 Python + cppcheck,零硬件 |
| 主机侧纯逻辑单测 | ✅ | test/host/**/run_unit_test.py(MSVC 离线编译) |
纯标准库 + cl.exe,零硬件 |
| 板端测试套件 | ⛔ | test/board/ |
需真机 + 串口 + ST-LINK |
主机侧回归 /serial-boot-test 闭环 |
⛔ | 烧录 + 串口采集 | 同上 |
| 人工目检 | ⛔ | 上机屏幕 | 人的判断 |
明确不做的两件事(都是刻意为之):
- 不跑
/keil-build(Keil UV4)——需本机 Keil 授权,runner 上不具备。静态门禁的 cppcheck + 代码规范 已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(AGENTS.md)。 mem-numbers钩子按.map有无自适应跳过——它的事实源是MDK-ARM/**/*.map(Keil 构建产物,已.gitignore),无/keil-build即无.map。故 workflow 在检测不到.map时自动跳过该钩子(SKIP=mem-numbers,钩子显示Skipped而非Passed—— 是"不适用",不是"通过");有.map的 runner(如自建带 Keil 的机器)则照常跑。
🔴 门禁盲区(2026-10-02 记录):跳过
mem-numbers意味着 CI 不守 Flash/RAM 体积门禁—— 新增任务/栈/缓冲/代码导致的体积增长不会在 CI 被拦。故该跳过以::warning::明示 (不是弱化的 notice)并写入 step-summary。兜底必须靠本地:改代码后/keil-build(0/0) →python scripts/check_mem_numbers.py --scan。详见../../CI/远程CI完整指南.md§0.0。
🔴
gitlint属commit-msgstage,--all-files永远跑不到它(2026-10-11 审计):.pre-commit-config.yaml当时定义 18 个钩子,而 CI 日志只报 17 个(passed=16 skipped=1)—— 缺的正是 gitlint。即"单行中文标题 ≤100 字符"这条门禁本地有、CI 无,且表现为全绿 而非失败,所以长期无人发现。现由 CI 侧scripts/ci_gitlint_range.ps1(PR 用base..head、 push 用event.before..sha、跳过 merge 提交)与本地ci_local.py的 提交信息门禁 段分别逐条校验。 ⚠️ 机制根因(stages: [commit-msg]写在 gitlint 上游 的.pre-commit-hooks.yaml里, 项目配置中 grep 不到)与完整排障口径见../../CI/远程CI完整指南.md§7"钩子条数对不上"行。
9.2 两个 job
| job | 覆盖层 | 一句话职责 |
|---|---|---|
static-gates |
静态门禁 | pre-commit run --all-files(编码规范 / 文档一致性 / 主机侧单测快归因钩子等 47 根)+「手动阶段门禁」步逐根 --hook-stage manual 跑 cppcheck / test-cppcheck / test-coverage + 提交信息门禁(gitlint,见上方 🔴);无 .map 时自适应跳过 mem-numbers |
host-unit-tests |
主机侧纯逻辑单测 | 探测 MSVC 后遍历 test/host/**/run_unit_test.py 逐个跑并聚合断言数;未装 MSVC 时降级为 WARNING 跳过(不硬失败,但等于该层裸奔) |
触发:push(paths-ignore: docs/** / **/*.md / .workbuddy-ai/** → 纯文档改动不会触发 CI)
与 pull_request,另留 workflow_dispatch 手动触发;同分支 cancel-in-progress: true。
两个 job 都写 step-summary + ::notice:: 汇总行,日志落 ci-artifacts/*.log。
📖 步骤级细节不在本文(探测链顺序、
GITHUB_OUTPUT跳过决策、Tee落盘、产物存档、 内网拉不到 action 的处置):见../../CI/远程CI完整指南.md§5.5 / §5.2 / §5.3 / §5.4(依次为:两个 job 的步骤要点 / 触发条件 / 内网 action 拉取 / 产物与汇总)。
9.3 本地等价:scripts/ci_local.py
CI 只在 push / PR 时跑,且需 runner 就绪。提交前自查、或 runner 还没配好时,用同一套判定在本地跑:
& '.venv\Scripts\python.exe' scripts/ci_local.py # 静态门禁 + 提交信息门禁 + 主机侧纯逻辑单测(与 CI 等价)
& '.venv\Scripts\python.exe' scripts/ci_local.py --static-only # 只跑静态门禁(含提交信息门禁)
& '.venv\Scripts\python.exe' scripts/ci_local.py --host-unit-only # 只跑主机侧纯逻辑单测
& '.venv\Scripts\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验最近 20 条
- 默认段 = 静态段(47 根)+ 提交信息段 + manual 段 + 体积门禁 + 主机侧纯逻辑单测:manual 段逐根
--hook-stage manual跑cppcheck/test-cppcheck/test-coverage(三根带stages: [manual],git commit阶段不跑;一次只能给一个 id,多 id 实测报unrecognized arguments);--coverage旗标已并入 manual 段,保留只为兼容旧命令。 - 自动选用装了 pre-commit 的解释器(优先级:
DTU_PRECOMMIT_PYTHON显式指定 → 当前解释器(重建.venv已自带 pre-commit)→ 仓库.venv→ PATH 的python→ 常见机器级安装位置;显式值无效 → 硬报错并指明变量名,不静默回退); 仍不可用时退出码 2 并列出全部已尝试路径。 ⚠️ 该候选表与 CI 侧scripts/ci_find_python.ps1必须同序同步——2026-10-11 的 静态门禁全灭就是 workflow 单方面把候选收窄成"PATH python + 仓库.venv"(runner 上两者都是死路)导致的。 - 提交信息门禁:与 CI 的
ci_gitlint_range.ps1同语义(--all-files跑不到 commit-msg stage)。 .map自适应与 CI 完全一致:本地有.map就照常跑mem-numbers,没有就跳过。- 退出码:
0通过 /1门禁或单测失败 /2环境错误。
⚠️ CI 不能替代本地门禁:
.map类体积门禁、/keil-build、板端测试套件/主机侧回归台架项都只存在于本地。 提交前跑一遍ci_local.py仍是强制动作,见 §7.1(pre-commit run --all-files只覆盖提交期 47 根, 走不到commit-msg与 manual 阶段)。
9.4 部署前置(需人工确认)
CI 能不能跑起来,取决于 runner 机器上四件事是否就位:服务端开关 / runner 注册与 labels /
看门狗常驻 / 机器级 DTU_* 工具变量。清单与逐项部署步骤都在
../../CI/远程CI完整指南.md:§0.2(前置清单)、§2(注册)、§2.4(看门狗)、§4.1.3(runner 以 SYSTEM 跑,必须写 MACHINE 级变量)。
注册细节:--instance 用回环、labels 必须精确匹配 runs-on;看门狗缺了它 Gitea 一重启
CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。
⚠️ 对测试流程只需记住一条:runner 不在线时,
ci_local.py就是 静态门禁 / 主机侧纯逻辑单测的唯一保障。 "CI 显示绿"与"本地门禁通过"是两回事,见 §9.3 与 §7.1。
附录 A:test/ 文件清单(现状)
| 文件 | 作用 |
|---|---|
test_config.h / test_config.c |
套件选择(互斥 #error)+ 统一调度入口 start_test_task() |
test_common.h |
板端测试公共断言内核(test_stats_t + TEST_CHECK_IMPL/TEST_REPORT_IMPL) |
ch395f_test.h / ch395f_test_task.c |
CH395F 套件:阶段开关、断言宏、Phase 1~5 实现 |
net_test_task.h / net_test_task.c |
CH395F 套件 NET 层(6~11):TCP 监听循环 / Client 主动连接测试 |
storage_test.h / storage_test_task.c |
STORAGE 套件:FTL + FatFS 集成(TC-STORAGE-01~04)+ 恢复测试 |
gd5f_test.h / gd5f_test_task.c |
GD5F 套件:NAND 驱动 10 阶段 |
sd2506_test.h / sd2506_test_task.c |
RTC 套件:SD2506 RTC 2 阶段组 |
run_boot_test.py / verify_log.py + serial_tool |
自动化闭环三件套(纯 Python 标准库 / 仓库内置) |
ftp_test.py / ftp_capture.py / ftp_up_dl_test.py / ftp_speed_test.py |
FTP PC 端测试工具 |
ch395f_socket_test.py |
CH395F PC 端 socket 回显 / 大文件传输工具 |
time_sync_server.py |
对时推送 PC 端服务器 |
附录 B:模块测试文档
| 文档 | 模块 / 套件 |
|---|---|
docs/测试说明/板级测试/CH395F_Test_Guide.md |
CH395F(11 阶段,TC-NET-xxx) |
docs/测试说明/板级测试/GD5F2GQ5UE_Test_Guide.md |
GD5F + 存储集成(TC-GD5F / TC-STO) |
docs/测试说明/板级测试/SD2506_Test_Guide.md |
RTC(TC-RTC-xxx) |
docs/测试说明/板级测试/FTP_Test_Guide.md |
FTP 服务(手动 + 阶段 8 自动化) |
docs/测试说明/测试工具/串口助手设计说明.md |
串口调试工具(Python 串口助手) |
docs/测试说明/流程与规范/自动化测试流程.md |
/serial-boot-test 闭环细节 |
docs/测试说明/流程与规范/测试文档模板规范.md |
测试文档模板与命名 |
docs/测试说明/单元测试/看门狗单元测试说明.md |
看门狗判定状态机(主机侧纯逻辑单测) |
docs/测试说明/单元测试/系统时钟单元测试说明.md |
系统时钟换算(主机侧纯逻辑单测) |
docs/测试说明/单元测试/配置文件单元测试说明.md |
配置引擎解析/契约(主机侧纯逻辑单测) |