Files
STM32F4-Base/docs/测试说明/流程与规范/测试开发标准流程.md
T
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

44 KiB
Raw Blame History

测试开发标准流程(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 三条铁律

  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_* 分发到套件运行函数:

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 硬件层 15 / NET 层 611 两层互斥(#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 刻意不 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 用标准注释头(与文档用例一一对应):

/*
 * 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 轮 (改宏 → 构建 → 烧录 → 采集 → 判读)。本脚本编排成一条命令:

& '.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):

  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);
  4. 挂 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 三条铁律

  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. 迭代节奏(完整闭环)

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 §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 §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 配置引擎解析/契约(主机侧纯逻辑单测)