Files
STM32F4-Base/docs/测试说明/自动化测试流程.md
T
wandering1 88601422c3 docs(文档规范): 清空壳变更记录节,例外清单改成可复跑口径
17 篇文档的变更记录(含修订记录、带编号与中文序号标题)节里只剩一句
「不再记流水账」的占位话,整节删掉,连带它前面的分隔线。
仍保留真台账的只有 5 篇(看门狗 / 配置文件设计 / RAM 预算台账 /
内容页架构 / 本地工具路径方案),外加 scripts/archive 归档区。

份数口径改成可复跑的普查:文档规范 §5.1 补自查命令,
stale-doc-count-sweep 的速查表与陷阱各加一条——只 grep 裸标题会漏
带编号与中文序号的两种写法,实测 5 篇只数得出 3 篇,差点据此把
全仓正确的份数改错。

门禁:逐篇与全量 check_doc_links 除 simulator/SDL2 联接假红外 0 问题;
skill-size rc=0;表格列数与字节体检相对 HEAD 零新增问题。
2026-10-29 21:21:59 +08:00

16 KiB
Raw Blame History

自动化测试流程(AI 迭代闭环)

目标:AI 改完代码后,可自动完成「构建 → 烧录 → 抓取串口调试信息 → 校验 → 迭代」,无需人工开终端/串口助手。

总体流程

@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

命令与脚本清单

名称 位置 作用
@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)
@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

判定标准

  • 构建:@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;@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;确认 @build 已重跑(戳取自编译时刻)。⚠ 若戳命中但里面的 suite= 与本次要跑的套件不符(想跑 CH395F 却显示 suite=NONE dstr=off)→ 烧的是产品镜像,@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 打印(见 @test 命令示例);将其加入 -Required 即可经串口自动回归 UI 初始化。
  • 屏幕内容回归:由 LVGL 模拟器像素回归覆盖(文本级断言 + 66 张黄金参考 + 池水位门禁, 见 docs/UI设计/UI_自动化测试流程.md);上机屏幕仍需人工目检(见 测试开发标准流程.md §1)。

扩展用法

  • 单模块回归:@test 后接 -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)。