# 测试开发标准流程(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`](../单元测试/看门狗单元测试说明.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 三条铁律
1. **先写判定标准,再写测试代码** —— 文档里先写"什么算过 / 什么算失败"(精确到返回值 / 日志字符串 / 像素参考),再实现,避免"测了但不知道算不算过"。
2. **判定标准必须可机器判定** —— 必须能落到:串口日志标记(→ `run_boot_test.py --required`)、返回值 / 寄存器值(→ `TEST_CHECK`)、像素参考(→ 模拟器回归)、池水位(→ 基线常量)之一。无法机器判定的标准只能放 人工目检人工层。
3. **每条设计原则必须有对应用例** —— 覆盖表中每个 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_*` 分发到套件运行函数:
```mermaid
flowchart TD
T["testTask: start_test_task()(test/board/test_config.c)"] -->|TEST_SUITE_CH395F| A["ch395f_test_run()
硬件层 1~5 / NET 层 6~11"]
T -->|TEST_SUITE_STORAGE| B["storage_test_run()
FTL + FatFS"]
T -->|TEST_SUITE_GD5F| C["gd5f_test_run()
10 阶段"]
T -->|TEST_SUITE_RTC| D["sd2506_test_run()
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~5 / NET 层 6~11 | 两层互斥(`#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 套件的宏此前逐字重复,改一处要改四处:
```c
/* 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` 独立):
```c
/* 套件头文件内(如 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` 刻意不 include `dbg_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 用标准注释头(与文档用例一一对应):
```c
/*
* 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 修复后):
1. `netTask` = **基础线程**:定义于 `.ioc` → `Src/freertos.c` 的 **CubeMX 生成区无条件创建**(全构建唯一实例)。产品 / NET 层测试模式下正常运行:等存储就绪(≤10s)→ `net_stack_bring_up()`(CH395F 初始化)→ 置 `SYS_EVT_NET_READY` → `net_poll()` 轮询循环。
2. ~~USER CODE 区在 `ENABLE_NET_LAYER_TESTS` 下另建一个 netTask(双 netTask 并存)~~ → **2026-09-15 已删除**:NET 层测试构建不再重复创建。
3. **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
1. `test/` 下新建 `xxx_test.h` / `xxx_test_task.c`(按 §3.2 模式);
2. `test/board/test_config.h`:补 `TEST_SUITE_XXX` 注释开关 + 互斥 `#error` 加项 + 分发分支(include + `TEST_SUITE_NAME`);
3. `test/board/test_config.c`:`start_test_task()` 加分发分支;
4. **双端注册核查**(`new-source-file` skill):新 `.c` 必须加入 `MDK-ARM/STM32F407-Demo.uvprojx`;test/ 代码仅 MCU 端编译,**不**登记模拟器 CMakeLists(UI / App 共用代码才需双端);
5. `/keil-build` → 0 错 0 警;
6. 立 `docs/测试说明/<模块>_Test_Guide.md`(§2 模板);
7. `/serial-boot-test` 以套件标题行为 `-Required` 标记跑通回归。
### 3.6 多套件回归编排(`test/run_all_suites.py`,2026-10-01 加)
套件互斥(编译期 `#error`)→ 跑 4 个套件的回归本来要手工循环 4 轮
(改宏 → 构建 → 烧录 → 采集 → 判读)。本脚本编排成一条命令:
```powershell
& '.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 个配置实例)。
```powershell
& '.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`](../单元测试/看门狗单元测试说明.md)):
1. 模块的**硬件访问**收敛到薄平台层(`__weak` 钩子或后端函数)——参照
`App/cfg/cfg_file_store.h` 的"共享核心 + 平台后端";
2. 测试单元 `#include` **生产源文件**(保证测的是真代码,非副本),用桩替换平台层;
3. 写模块侧运行器:**只声明** `TestBuild`(源码 / 桩目录 / 汇总行标记)+ 调
`test/host/common/unit_test_harness.py`(工具链探测与编译运行细节在 harness 内,
见 [`../../../test/host/common/README.md`](../../../test/host/common/README.md));
4. 挂 pre-commit 钩子,被改动时自动跑(**只 commit 不烧录**,秒级)。
**环境要求**(详见 [`../../环境与部署/项目部署环境要求.md`](../../环境与部署/项目部署环境要求.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` |
**使用方式**:新增/改动上述模块前,按表判断是否需补 主机侧纯逻辑单测;`⬜ 候选` 表示"适格但未做",
在性价比允许时应排入(判据:边界密集、零硬件、单次真机回归 30s~2min vs 单测 2~5s)。
> ⭐ **两条实证**:`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 五步门禁(顺序执行,全过)
```powershell
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 流程
```mermaid
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`。
```powershell
# 测试套件构建回归(要求套件跑到汇总行,且断言数 > 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 三条铁律
1. **采集必须先于烧录武装** —— 开串口 + clear 在烧录前完成(daemon 常驻全量日志兜底,不会漏字节;`run_boot_test.py` 已保证顺序);
2. **端口以 `.serialtest/config.json` 为准** —— 每次连接前先读它;
3. **静默判据覆盖时间窗** —— `-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. 迭代节奏(完整闭环)
```mermaid
flowchart LR
A["文档:规划 + 判定标准
(Test_Guide)"] --> B["代码:套件 / 工具实现
+ 双端注册核查"]
B --> C["G1 /keil-build
0 错 0 警"]
C --> D["G2 /serial-boot-test
采集→烧录→校验"]
D -->|PASS| E["G3 ui-verify
(UI 改动时)"]
E --> F["人工目检上机目检"]
F --> G["提交
G0 门禁 + 逻辑提交"]
G --> H["沉淀:陷阱记录 /
文档同步 / 正文现状化"]
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` 闭环 | ⛔ | 烧录 + 串口采集 | 同上 |
| 人工目检 | ⛔ | 上机屏幕 | 人的判断 |
**明确不做的两件事(都是刻意为之)**:
1. **不跑 `/keil-build`(Keil UV4)**——需本机 Keil 授权,runner 上不具备。静态门禁的 cppcheck + 代码规范
已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(`AGENTS.md`)。
2. **`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`](../../CI/远程CI完整指南.md) §0.0。
> 🔴 **`gitlint` 属 `commit-msg` stage,`--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`](../../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`](../../CI/远程CI完整指南.md) §5.5 / §5.2 / §5.3 / §5.4(依次为:两个 job 的步骤要点 / 触发条件 / 内网 action 拉取 / 产物与汇总)。
### 9.3 本地等价:`scripts/ci_local.py`
CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runner 还没配好时**,用同一套判定在本地跑:
```powershell
& '.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`](../../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` | 配置引擎解析/契约(主机侧纯逻辑单测) |