Files
STM32F4-Base/docs/测试说明/自动化测试流程.md
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +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)。

变更记录

(本文件不再记流水账:改了什么是正文说了算,改动过程查 git log。)