Files
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

139 lines
16 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.
# 自动化测试流程(AI 迭代闭环)
> 目标:AI 改完代码后,可自动完成「构建 → 烧录 → 抓取串口调试信息 → 校验 → 迭代」,无需人工开终端/串口助手。
## 总体流程
```
/serial-boot-test 触发
│
├─ Step 0 预检 .venv + serial_tool(selftest/ports 确认 COM)
├─ Step 1 构建验证 keil-build skill(keil_build.py) (0 错误 0 警告才继续)
├─ Step 2 烧录 + 采集(一条命令编排:开串口 + 清日志 → 烧录 → 固件身份戳 → wait-idle → 提取 → 回读校验)
│ .dsh/skills/serial-boot-test/scripts/run_boot_test.py (内置 serial_tool + stlink-flash skill 的 flash/cmp + verify_log.py)
├─ Step 3 判定:
│ PASS → 报告,结束
│ FAIL → 依据 [MISS]/[HIT] + 日志尾部,修改代码后回到 Step 1
```
## 命令与脚本清单
| 名称 | 位置 | 作用 |
|---|---|---|
| `/keil-build` | `keil-build` skill(`.dsh/skills/keil-build/`,自带 `keil_build.py`) | 仅编译验证(0 错误 0 警告) |
| `@download` | `stlink-flash` skill(`.dsh/skills/stlink-flash/`,自带 `stlink_flash.py`) | 编译并烧录(ST-Link) |
| `/serial-boot-test` | `serial-boot-test` skill(`.dsh/skills/serial-boot-test/`) | 完整闭环:build → capture+download → verify → iterate |
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → 固件身份戳(从 hex 抠 `fw build:`)→ wait-idle(静默 8s/超时 30s/首字节 12s)→ 提取 `[RX]`(每行加 `[+Nms]` 时基)→ 回读校验(`stlink_flash.py cmp`)→ verify;`--keep-out` 清旧产物;退出码 0=PASS/1=FAIL/2=采集·烧录·回读错误/3=用法错误 |
| `.dsh/skills/stlink-flash/scripts/stlink_flash.py` | `stlink-flash` skill | ST-LINK CLI 封装:`flash [hex]`(烧 + `-Rst -Run`)/ **`cmp [hex]`(回读比对,2026-10-10 加)**/ `reset`(只复位)/ `hard`(nRST 脉冲,CLI 拒 `RM=` 自动降级)/ `halt`/`run`/`list`;原 `MDK-ARM/reset.bat` 已于 2026-10-10 删除并全并入本 skill |
| `.dsh/skills/serial-assistant/scripts/serial_tool.py` | `.dsh/skills/serial-assistant/` | Python 串口助手(常驻 daemon + 薄 CLI):ports/open/send/status/log/clear/wait-idle/selftest;全量日志 `state/serial.log`(20MB 自动截头) |
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对日志做标记校验:Required/Forbidden(**命中逐行打印行号+内容**)、启动锚点计数 `--max-boot-count`、原因白名单 `--expect-reset-cause`、标记顺序 `--require-order`、开机耗时 `--done-marker`/`--max-boot-ms`、`--json` 结构化输出、**整行锚定 `--exact` / 跨复位配对 `--pair` / 崩溃腿 `--allow-residual`+`--head-char` / 判别自检 `--selftest`(22 条,含三计数分类器)**;退出码 **0=PASS / 1=设备侧真有坏消息 / 2=测量没成立**(读不到日志,或开了锚点计数却零锚点——采集断了不算设备故障)。子串 Required 照不出"行首被吃掉"那一类行级损坏 → 行级完整性一律用 `--exact`,口径与固定动作见 `启动日志原子性复验流程.md` |
## 判定标准
- **构建**:`/keil-build` 退出码 0(0 错误 0 警告),否则不进入硬件环节。
- **烧录**:`@download` 退出码 0(`run_boot_test.py` 内部调用)。
- **日志**:`verify_log.py` 退出码
- `0` = PASS:所有 `-Required` 标记均出现,且无 `-Forbidden` 模式命中
- `1` = FAIL:存在缺失标记(`[MISS]`)或违规模式(`[HIT]`)
- `2` = 日志文件不可读
- **固件身份戳(2026-10-10 加)**:`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`(编译期展开成一段**连续 ASCII**,`.hex` 里可原样搜到)。`run_boot_test.py` 从**待烧的 hex** 里抠出该串并追加成 Required 标记 → 芯片里跑的必须就是这份构建。反例实测:把 hex 里的戳改成 `Oct 9 2026 01:00:00` 再跑,其余标记全中但 `[MISS] fw build: Oct 9 2026 01:00:00` → FAIL——这正是过去"反复重下载、芯片一直是旧镜像"在闭环里**完全隐形**的场景。`--no-fw-check` 关闭;hex 里没有该串(旧构建)只 WARN 跳过、不假判。
- **戳覆盖构建配置(2026-10-11 加)**:`__DATE__`/`__TIME__` 只在**该 TU 被重编**时才刷新——增量构建下只切 `test/board/test_config.h` 的套件宏**不会**重编 `app_main.c`,于是产品镜像与测试镜像顶着同一个戳(实测:切宏后的测试构建与产品构建戳同为 `Oct 11 2026 14:44:11`,只有回读校验才发现镜像不同)。两条防线:① `app_main.c` `#include "test_config.h"` 建立头依赖 → 改宏必重编本 TU、戳必刷新;② 戳串追加构建变体 ` suite=<TEST_SUITE_NAME> dstr=<on|off>`(`FW_BUILD_VARIANT`)→ 产品 = `suite=NONE dstr=off`、CH395F 测试 = `suite=CH395F dstr=on`,**即便戳时间相同也能一眼分辨**。实测:切宏前后 `Oct 11 2026 20:36:11 suite=NONE dstr=off` → `Oct 11 2026 20:36:40 suite=CH395F dstr=on`;`/serial-boot-test` 闭环对新格式匹配通过(Required 行就是完整戳串,脚本按"标记后连续可打印 ASCII"抠取,无需改脚本)。
- **烧录回读(2026-10-10 加)**:采集结束后自动跑 `stlink_flash.py cmp`(= `-CmpFile` 全片逐字节比对),判定按**行首** `No difference found`(进度条与帮助文本里也含该短语,故不能全文匹配);不一致 → `RUN_TEST_ERROR: flash readback mismatch` + 退出码 2(本次运行作废:日志是"某个未知镜像"跑出来的)。⚠ **顺序不能颠倒**——`-CmpFile` 连接时会复位目标(实测板子当场重打一遍开机日志)。`--no-verify-flash` 关闭。
- **启动锚点计数(2026-10-10 加)**:`reset cause:` 在一次采集里出现 **0 次**(采集没从复位开始,标记全中也可能只是上一轮残留)或 **>1 次**(板子中途自己复位:IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)→ FAIL;`--max-boot-count N` 放宽(跑故意复位的套件时用),`--expect-reset-cause 'software;power-on'` 对第一个锚点做原因白名单(SWD 软复位实测为 `software (NVIC_SystemReset)`,CSR=0x14000000)。
- **判定可读性 + 时序(2026-10-10 第三批)**:
- **Forbidden 命中行**:命中时逐行打印 `L<行号>: <内容>`(最多 6 行,剩余报数)。旧实现只说 `forbidden pattern: [ERR] (3 line(s))`,还得人再 grep 一遍才知道是哪几行。
- **`[+Nms]` 相对时基**:`run_boot_test.py` 给 boot_log 每行加行首 `[+Nms]`(daemon 毫秒时基,本次第一行 = +0ms;`--raw-log` 出纯载荷)。MCU 侧时间戳只有秒级、开机头几行还没时钟(`----/--/--`),量不了耗时。
- **开机耗时**:`--done-marker 'net init: OK'`(默认)+ `--max-boot-ms N`。实测基线 **+537 ms**(2026-10-10 台架,`stlink_flash.py flash` 后);默认 `--max-boot-ms 0` = 只报告不判,避免网络重试抖动造成假 FAIL。
- **标记顺序**:`--require-order 'App init OK>Network Init>net init: OK'` 按首次出现的**行序**校验;标记全中但顺序颠倒(屏障失序 / 重试补跑)此前完全不可见。
- **`--json`**:同一套判定以 JSON 输出(`required_missing` / `forbidden_hits[].lines` / `boot` / `timing` / `tail` / `verdict` / `exit`),退出码语义不变,供 CI 与上层脚本消费。
- **产物保留**:`--keep-out N`(默认 20)自动清理 `--out-dir` 里的旧 `boot_log_*.txt` / `selftest_log_*.log`(本机曾堆到 31+7 份)。
### 自检结果判定(S7,`--require-selftest-pass`)
`run_boot_test.py` / `verify_log.py` 支持 `--require-selftest-pass`:除串口标记外,还判定设备侧自检结论
(v1 只记故障、不记通过)。判定对象(20 处自检描述串)见 `实机测试实施指南.md` §3.2。
**判据默认只看「本次启动新增」**(`--selftest-scope this-run`,2026-10-22 改)。原因:
`/log/selftest.log` 是 **512 条环形累计历史**,而旧判据是"文件非空即 FAIL"——用它判一次启动,
在任何做过故障注入或长跑的设备上**永远不可能 PASS**(本仓台架实测:抓到的 29 条记录时间戳
全是 2026-10-10 ~ 10-14 留下的,没有一条来自本次)。新流程 = 烧录**前**先经 FTP 取一次基线、
启动后再取一次,只把"基线最后一条之后"的记录交给判定:
| 情形 | 含义 | 判定 |
|---|---|---|
| 设备侧无此文件(450/550) | 台账干净("仅故障才写"语义下本就可能不存在) | 无新增 → **PASS** |
| 抓到文件、本次无新增 | 本次启动没产生故障 | **PASS** |
| 抓到文件、本次有新增 | 本次启动有故障 | **FAIL**,只打印新增条目(落 `selftest_log_<时刻>_new.log`) |
| 连不上 / 登录被拒 / FTP 故障 | 结果**未知** | **FAIL**——旧行为是 WARN 后当"无记录"放过,那就是假绿 |
| 既没有 `device_ip` 也没有 `--selftest-log` | 要求判定却没有数据源 | **FAIL**(同上,不再 WARN 放行) |
| 基线最后一条在台账里找不到 | 512 条环已覆盖基线(更新量 ≥ 环容),无法证明 | **FAIL**(不拿"看不到"当"没有") |
- ⚠ **采集窗口内仍不要跑 FTP**:每条 FTP 命令都会打到串口上(日志变长、开机耗时指标被污染——
实测抓基线排在采集武装之后时,`net init: OK` 从 612 ms 被拉到 7997 ms)。基线抓取因此固定在
`open/clear` **之前**。(原先更要命的是客户端退出会打 `[ERR] [FTP] error reading next command`
→ 直接命中 Forbidden 里的 `[ERR]`;该措辞与级别已在 2026-10-22 归入"对端行为"级,见
`../../架构设计/STM32F4-Base存储架构说明.md` §10.3。注意:RST 硬拆仍会留一条 `[ERR][NET] sockN`
`TIMEOUT`——那是网络自愈架构要读的套接字超时锚点,不在降级范围内。
- `--selftest-scope all` = 保留旧的累计口径(任一记录即 FAIL),用于**审计**或第一次建基线。
- 给了 `--selftest-log`(本地副本)就没有基线可比 → 自动降级为累计口径并打印一行说明。
- 抓基线同样要 FTP 凭据:`--ftp-user` / `--ftp-pass`(config `ftp_user` / `ftp_pass`,默认出厂
`admin`/`admin`);设备**没有匿名通道** → `FTP_Test_Guide.md` §1。
### 默认开机标记(来自固件)
| 标记 | 来源 |
|---|---|
| `App init OK` | `App/app/app_main.c` |
| `RS485 init: OK` | `App/app/app_main.c` |
| `NAND init:` | `App/app/app_main.c` |
| `ADC init:` | `App/app/app_main.c` |
| `SD2506 ID:` / `time:` / `temp:` / `battery:` | `App/app/app_main.c` |
| `Network Init` / `net init: OK` / `MAC:` / `PHY:` | `App/task/net_task.c` |
| `phasor(first)` / `phasor(hb)` | `App/task/adc_task.c`(全周傅氏相量,2026-10-11 起常开;`hb` 每 10 s 一次,采集窗口需 ≥10 s——默认 `max_wait 30 s` 覆盖) |
**默认 Forbidden 模式**:`FAIL;ERROR;HardFault;[ERR]`
> ⚠ 固件错误级日志前缀是 `[ERR]`(`DBG_ERROR` → `[时间][ERR][TAG] 消息`),与 `ERROR`
> 子串**并不匹配**。2026-09-21 起默认列表显式加入 `[ERR]`——此前驱动/模块的错误行
> 不会被自动闭环拦截(历史日志中的 `[ERR]` 全是漏检)。产品构建正常开机应为零 `[ERR]`。
### 失败模式应对
| 现象 | 处理 |
|---|---|
| `selftest` FAIL / `ModuleNotFoundError: pyserial` | venv 未建或依赖缺失:按 AGENTS.md「Python 环境」重建 `.venv` + `pip install -r requirements.txt` |
| daemon 无响应 / 自动拉起失败 | 查 `.dsh/skills/serial-assistant/scripts/state/daemon.log`;`serial_tool.py restart` 重建 daemon;检查 COM 是否被其他程序占用 |
| 抓到日志缺 `Network Init` | `-SettleMs` 太小(boot 任务间有 ~3-4s 自然静默间隙),用默认 8000ms;2026-10-10 起首字节到达前不启动静默计时(`--first-wait-ms` 默认 12000),首行晚到不再被提前收尾 |
| 烧录后无数据(`no_data`) | 2026-10-10 起 `no_data` = **本次等待期间零字节**(旧实现比 daemon 累计字节,常驻 daemon 下永不触发,只能靠"日志里没有 [RX] 行"兜底);开串口 + clear 必须在烧录**前**完成(`run_boot_test.py` 已保证);核对 COM 号、板子供电,看 `state/serial.log` 有无 SYS 异常行 |
| `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`stlink_flash.py flash <hex>` 重烧并看 `READBACK: OK`;确认 `/keil-build` 已重跑(戳取自编译时刻)。⚠ 若戳**命中**但里面的 `suite=` 与本次要跑的套件不符(想跑 CH395F 却显示 `suite=NONE dstr=off`)→ 烧的是产品镜像,`/keil-build` 不会替你切宏(→ `board-test-suite` skill) |
| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/测试工具/ST-LINK_CLI_复位使用说明.md` §7 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 |
| `boot banner x2 > allowed 1` | 采集窗口内板子自己复位过(IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)。处置:看判定里逐条打印的 `reset cause:` 定位;跑**故意复位**的套件用 `--max-boot-count N` 放宽 |
| `marker order violated` | 标记全中但**先后顺序颠倒** = 某步在另一步之后补做(就绪屏障失序 / 失败重试补跑)。处置:看判定打印的各标记首次行号定位;`--require-order` 只在需要强约束时启用 |
| `boot time N ms > limit` | 能开机但慢(mount 重试 / PHY 重协商 / 复位重试)。实测基线:`net init: OK` 在 **+537ms**(2026-10-10 台架);默认只报告不判(`--max-boot-ms 0`),要设门禁再给预算 |
| 端口被占用(`port busy`) | 先 `serial_tool.py close` 或 `restart`;检查其他程序占用;Keil ST-Link 不占 COM |
| 配置里的 COM 号失效(换口/换机) | `serial_tool.py ports` 现回 `configured_present` + `hint`(系统里只有一个候选时直接给出口名),`open` 的报错里也带 `[hint: ...]`;确认后只改 `.serialtest/config.json` |
| 日志中无 UI 相关输出 | 见下节「UI 自动化」 |
| 日志出现 `[ERR]` 行 | 固件错误级日志(`DBG_ERROR`),默认 Forbidden 会判 FAIL:按 TAG 查对应模块的 `docs/问题记录/*_Trap_Records.md`;若属测试套件的**预期**错误(守卫负路径、非法名校验等),用 `--forbidden` 放宽 |
## UI 屏幕自动化
- **初始化回归**:`App/task/ui_task.c` 已有 `UI port init: OK` / `UI menu init: OK` / `UI net sink: started`
打印(见 `run_boot_test.py` 命令示例);将其加入 `--required` 即可经串口自动回归 UI 初始化。
- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 66 张黄金参考 + 池水位门禁,
见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(见 `测试开发标准流程.md` §1)。
## 扩展用法
- **单模块回归**:`run_boot_test.py` 后接 `--required` 定制,例如网络回归用
`-Required "App init OK;Network Init;net init: OK;PHY: 100M FULL"`。
- **测试套件构建**:`test/board/test_config.h` 开启某个 `TEST_SUITE_*` 后,把套件打印的
`=== ... Test:` 标题作为 Required 标记,配合 `test/` 下各用例输出判断。
**破坏性用例默认不跑**:`test_config.h` 的 `ENABLE_DESTRUCTIVE_TESTS`(默认关闭)控制写/擦 NAND、
格式化、坏块注入与 FatFS 写类用例;关闭时套件为只读模式(开机打印 `mode: READ-ONLY`),
可随时在在用设备上执行且不改动卷内容。首次上电/产线验证时才打开(打印 `mode: DESTRUCTIVE`)。
**注意**:套件内**预期**的错误日志会打印 `[ERR]`(GD5F 的 `TC-GD5F-704/705` DMA 守卫
负路径与 FTL 运行期坏块、FatFs 非法名校验等),与默认 Forbidden 冲突,须显式放宽:
`--forbidden "FAIL;HardFault"`(`[FAIL]` 仍会拦截真实断言失败);
例:GD5F 只读 `--required "=== GD5F2GQ5UE Tests: 16/16 PASSED"`,破坏性 `68/68`。
- **长时间采集**:daemon 全量持续写入 `state/serial.log`(20MB 上限自动截头保尾 10MB),直接读文件即可;或用 `log --grep <标记>` 过滤(2026-10-10 起先过滤后截尾,不必再配 `--tail 0`)。