Files
super_agent_bot 72b4b0943c docs: clear task and layer shorthand IDs
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.
2026-11-02 03:33:51 +08:00

628 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试开发标准流程(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()<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~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["文档:规划 + 判定标准<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` 闭环 | ⛔ | 烧录 + 串口采集 | 同上 |
| 人工目检 | ⛔ | 上机屏幕 | 人的判断 |
**明确不做的两件事(都是刻意为之)**:
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` | 配置引擎解析/契约(主机侧纯逻辑单测) |