测试闭环补固件身份戳、烧录回读校验与启动锚点计数,并修cmd引号解析与flash-cmd预检缺陷
This commit is contained in:
@@ -16,11 +16,11 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
## 构建与验证
|
||||
|
||||
- `@build` = `& "MDK-ARM\build.bat"`(仓库根目录用**相对路径**);退出码 0 = 0 错误 0 警告,1 = 有警告或错误(查 `MDK-ARM/build_log.txt`),2 = `DTU_KEIL_UV4` 未设/无效(换机必设,见 `docs/环境与部署/本地工具路径环境变量方案.md`);UV4 路径经 `DTU_KEIL_UV4` 环境变量解析(**不再硬编码**;显式设置但无效会立即报错、不静默回退)
|
||||
- `@test` / `test/host/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' test/host/run_boot_test.py`):开串口+清日志 → 烧录(`MDK-ARM/download.bat`,下载后 Reset&Run)→ wait-idle(等 RX 静默 8s,超时 30s)→ `test/host/verify_log.py` 标记校验;退出码 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法/预检错误
|
||||
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK`;Forbidden `FAIL` / `ERROR` / `HardFault` / `[ERR]`(**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 一律判 FAIL**,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
|
||||
- `@test` / `test/host/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' test/host/run_boot_test.py`):开串口+清日志 → 烧录(`MDK-ARM/download.bat`,下载后 Reset&Run;快路径 `--flash-cmd "MDK-ARM\reset.bat flash"`)→ **固件身份戳**(从刚烧进去的 hex 里抠 `fw build: <DATE> <TIME>` 追加成 Required 标记)→ wait-idle(等 RX 静默 8s,超时 30s,首字节 12s 未到判 `no_data`)→ 提取日志 → **烧录回读校验**(`reset.bat cmp` = `-CmpFile`;不一致 = 本次运行作废 exit 2)→ `test/host/verify_log.py` 标记校验(含**启动锚点计数**:`reset cause:` 出现 0 次或 >1 次 = FAIL,`--max-boot-count` 调);退出码 0=PASS / 1=FAIL / 2=采集或烧录错误(含回读不一致)/ 3=用法/预检错误
|
||||
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK` + **自动追加的固件身份戳 `fw build: <__DATE__> <__TIME__>`**(`App/app/app_main.c` 开机打印,`run_boot_test.py` 从待烧 hex 里抠出该串当 Required——此前所有标记都是固定串,**旧镜像/错镜像照样全中**,"反复重下载、芯片一直是旧镜像"在自动闭环里不可见,2026-10-10 补;`--no-fw-check` 关闭);Forbidden `FAIL` / `ERROR` / `HardFault` / `[ERR]`(**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 一律判 FAIL**,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
|
||||
- ⚠️ 旧绝对路径 `D:\Code\DTU 程序\...` 出现即失效(仓库已搬迁),一律改用仓库相对路径
|
||||
- **下载/烧录铁律(2026-09-20 实测)**:走 `& "MDK-ARM\reset.bat" flash [文件]`(= `ST-LINK_CLI -c SWD -P <文件> -Rst -Run`),烧后 `-Rst -Run` **不可省略**——裸 `-P` 会报 `Programming Complete`,但目标核处于死循环/异常状态时写入并不落地(芯片内容不变;这就是 09/19~09/20 "反复重下载、芯片一直是旧镜像、二分法失效" 的根因);`.hex`/`.bin` 均可直烧(1MB ≈ 6.5s,快于 UV4 路径);只重启不烧录用 `reset.bat` 无参(= `-c SWD -Rst -Run`,本机 3 线接线唯一复位手段:nRST 未接、`-HardRst`/`RM=` 不可用)
|
||||
- **烧录后必须回读校验**:`ST-LINK_CLI -c SWD -CmpFile <文件> 0x08000000`(全片逐字节比对,`No difference found` 才算真正写入);CLI 经 `DTU_STLINK_CLI` 环境变量解析(未设时回退 PATH;本机已入 PATH,未设也能工作)(v3.5.0.0,探针 SN `0B250D12A218303030303032`)→ 完整用法/复位语义/故障排查 `docs/测试说明/ST-LINK_CLI_复位使用说明.md`
|
||||
- **烧录后必须回读校验**:`& "MDK-ARM\reset.bat" cmp [文件]`(= `ST-LINK_CLI -c SWD -CmpFile <文件> 0x08000000`,全片逐字节比对,`No difference found` 才算真正写入);`run_boot_test.py` 已默认自动跑(`--no-verify-flash` 关闭,不一致 → exit 2 本次运行作废)。⚠ **`-CmpFile` 连接时会复位目标**(2026-10-10 实测:比对瞬间板子重打一遍开机日志)→ 在自动闭环里必须排在**日志采集之后**,否则开机日志被截断且启动锚点计数会误判成"中途复位";CLI 经 `DTU_STLINK_CLI` 环境变量解析(未设时回退 PATH;本机已入 PATH,未设也能工作)(v3.5.0.0,探针 SN `0B250D12A218303030303032`)→ 完整用法/复位语义/故障排查 `docs/测试说明/ST-LINK_CLI_复位使用说明.md`
|
||||
|
||||
## Python 环境
|
||||
|
||||
@@ -86,6 +86,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
| GD5F2GQ5UE NAND | SPI1 | PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD |
|
||||
| TPAFE5160 ADC | 并行16位 | PG0-PG15 数据, PC8 RD, PC9 CONVST, PC7 BUSY(V0.1;FRSTDATA=PC6,OS0/OS1/OS2=PA12/PA11/PA8) |
|
||||
| ADC 通道语义(V0.1,17 路 = 8×TPAFE + 9×STM32) | 见映射表 | **网名 `AINn` ≠ 板级 `ADCn` ≠ 转换器通道号**(13/17 路置换);语义 UA/UB/UC/UN=`AIN1~4`、IA/IB/IC/IN=`AIN8~11`、V_ZL2=温度、其余 8 路备用——取数**必须查** `docs/硬件平台/ADC通道映射说明.md`(TPAFE `buf[2]=IA`/`buf[5]=UN`/`buf[6]=UA`;网名 ADC17=PC4 用 `ADC_CHANNEL_14`,**不是** `ADC_CHANNEL_12`) |
|
||||
| ADC 通道归属(V0.1 **硬件缺陷**) | 8 路电气量被分片 | **UA/UN/IA 走 TPAFE5160、UB/UC/IB/IC/IN 走 STM32 ADC 是缺陷,不是有意分配**(2026-10-10 更正;跨转换器无共同采样触发 → 无同一时刻性/相位不确定,16 位 ±5 V 双极性 vs 12 位 0~3.3 V 单极性)→ V0.1 上主板侧**禁止**跨转换器做相位/功率/功率因数/矢量运算;**下一版硬件(V0.2)须把 8 路电气量全部接 TPAFE**(登记簿 N-18,理由与改版方案见 `docs/硬件平台/ADC通道映射说明.md` §1) |
|
||||
| USART1 调试串口 | UART | PA9 TX, PA10 RX (115200) |
|
||||
| RS-485 | UART4 | PC10 TX, PC11 RX, PA15 DE(半双工) |
|
||||
| 6x LED | GPIO | PC5, PB0, PB1, PB2, PF11, PF12(低电平点亮) |
|
||||
|
||||
@@ -106,6 +106,14 @@ int app_main_init(void) {
|
||||
* 故某次启动卡死时,最后一行日志即可指向卡住的步骤。 */
|
||||
DBG_INFO("boot: syslog init OK");
|
||||
|
||||
/* 固件身份戳(2026-10-10):__DATE__/__TIME__ 由编译器展开,与前面的字面量做
|
||||
* C 字符串拼接 → .rodata 里是一段连续 ASCII,可直接在 .hex 里原样搜到。
|
||||
* 用途:@test 把"芯片里正在跑的镜像"和"刚编出来的镜像"对上号。此前开机标记
|
||||
* 全是固定串(App init OK / Network Init / ...),旧镜像也照样全中,于是
|
||||
* "反复重下载、芯片一直是旧镜像"这类事故在自动闭环里完全不可见
|
||||
* (AGENTS.md 烧录铁律记的正是它)。代价:1 条 DBG ≈ 90 B Flash + 30 B 常量。 */
|
||||
DBG_INFO("fw build: " __DATE__ " " __TIME__);
|
||||
|
||||
/* 0) 复位原因上报(锁存值由 Src/main.c 在 HAL_Init() 后立即采得)。
|
||||
* 放在日志引擎之后、业务初始化之前:任何一次非上电复位都值得在
|
||||
* 开机第一时间看到——尤其 IWDG,它意味着上一轮运行有任务卡死。
|
||||
|
||||
@@ -40,6 +40,7 @@ if /i "%~1"=="halt" goto :halt
|
||||
if /i "%~1"=="run" goto :run
|
||||
if /i "%~1"=="list" goto :list
|
||||
if /i "%~1"=="flash" goto :flash
|
||||
if /i "%~1"=="cmp" goto :cmp
|
||||
goto :help
|
||||
|
||||
:reset_run
|
||||
@@ -80,6 +81,18 @@ echo FLASH: program "%FW%" then reset and run
|
||||
"%STLINK%" -c SWD -P "%FW%" -Rst -Run -Q
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:cmp
|
||||
if "%~2"=="" set "FW=%~dp0STM32F407-Demo\STM32F407-Demo.hex"
|
||||
if not "%~2"=="" set "FW=%~2"
|
||||
if not exist "%FW%" (
|
||||
echo CMP_FAIL: firmware not found "%FW%"
|
||||
exit /b 2
|
||||
)
|
||||
echo CMP: readback compare "%FW%" vs device flash
|
||||
echo CMP_NOTE: connecting resets the target - run this AFTER log capture, not before
|
||||
"%STLINK%" -c SWD -CmpFile "%FW%" 0x08000000
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:help
|
||||
echo Usage: reset.bat [command]
|
||||
echo.
|
||||
@@ -89,6 +102,8 @@ echo halt System reset and halt core (in-session read; auto-resumes after
|
||||
echo run Resume a halted core
|
||||
echo list List connected ST-LINK probes
|
||||
echo flash [F] Program F (default: latest build hex) then reset and run
|
||||
echo cmp [F] Readback compare F vs device flash; "No difference found" = write landed
|
||||
echo (connecting resets the target: run it after log capture, not before)
|
||||
echo.
|
||||
echo Exit codes: 0=OK 1=probe or target error 2=usage or tool missing
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 267,740 B (261.46 KB) | 1,048,576 B (1024 KB) | 25.5% |
|
||||
| ER_IROM1 | 267,820 B (261.54 KB) | 1,048,576 B (1024 KB) | 25.5% |
|
||||
| RW_IRAM1 | 85,032 B (83.04 KB) | 131,072 B (128 KB) | 64.9% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||||
| **Flash (ROM 合计)** | **268,480 B (262.2 KB)** | 1,048,576 B (1024 KB) | **25.6%** |
|
||||
| **Flash (ROM 合计)** | **268,560 B (262.3 KB)** | 1,048,576 B (1024 KB) | **25.6%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -151,7 +151,7 @@ ST-LINK_CLI -c SWD HOTPLUG -Dump 20000000 1000000 dump.bin :: 转储 SRAM
|
||||
| `-r8/-r16/-r32 <地址> <数量>` | 读内存(地址、数量均为**十六进制**) |
|
||||
| `-w8/-w32/-w64 <地址> <数据>` | 写内存 |
|
||||
| `-Dump <地址> <大小> <文件>` | 内存转储到文件 |
|
||||
| `-CmpFile <文件> [地址]` | 全片 Flash 与文件逐字节比对(2026-09-20 实测:`No difference found` = 真写入;v3.5.0.0 无 `-Cksum` 命令) |
|
||||
| `-CmpFile <文件> [地址]` | 全片 Flash 与文件逐字节比对(2026-09-20 实测:`No difference found` = 真写入;v3.5.0.0 无 `-Cksum` 命令)。⚠ **连接即复位目标**(2026-10-10 实测:比对瞬间板子重打一遍开机日志)→ 自动化里必须排在日志采集之后 |
|
||||
| `-P <文件> [地址] [ske] [skpv]` | 编程(ske=跳擦除 skpv=跳校验) |
|
||||
| `-V [after_programming]` | 校验 |
|
||||
| `-ME` / `-SE <扇区>` | 全片擦除 / 扇区擦除(⚠️ RDP≠0 时 `-ME` 会被拒) |
|
||||
@@ -177,11 +177,13 @@ Get-Content tools\serial_tool\state\serial.log -Tail 100
|
||||
ⓞ 实测补充三条:
|
||||
|
||||
1. **改过 `serial_config.json` 端口后必须重启 daemon**:daemon 在启动时缓存配置,改端口后 `close` + 杀旧进程再 `open`,否则一直按旧端口报 `port not found`;
|
||||
2. **复位刚发生的几秒内 `wait-idle` 可能报 `daemon unresponsive after auto-start: timed out`**——daemon 正在排空开机日志突发,数据其实已落盘;
|
||||
3. **日志校验直接读文件最可靠**:`tools/serial_tool/state/serial.log` 用 Read/Grep/Select-String 直接查(实测 `log --grep` 子命令对确定存在的标记返回 0 命中,语义存疑,勿作为判定依据)。
|
||||
2. **(已修,2026-10-10)旧版 `wait-idle` 在复位突发期会报 `daemon unresponsive after auto-start: timed out`**:CLI 把**超时**误判成"daemon 不可达",顺手拉起一个绑不上 HTTP 口的实例再把命令撞一次超时(数据其实已落盘)。现 CLI 超时按 `--max-ms` 自动推导、超时只快速失败不再误拉起实例;
|
||||
3. **日志校验**:`tools/serial_tool/state/serial.log` 用 Read/Grep 直接查最直观;`serial_tool.py log --grep <标记>` 现已可作判定依据(2026-10-10 修:旧实现先 `--tail`(默认 50)后 grep,开机标记必然查不到——这就是当年"实测 0 命中、语义存疑"的真根因)。
|
||||
|
||||
**B. 完整烧写 + 运行**:`MDK-ARM\reset.bat flash [xxx.hex]`(= `-c SWD -P <hex> -Rst -Run`;09/19 实测 1MB ≈ 6.5s,09/20 复测 6.47s + 全片 `-CmpFile` 逐字节一致)。正式流程仍以 `download.bat`(`UV4 -f`)为准,CLI 路径作为脚本化/快速路径。
|
||||
|
||||
**B2. 回读校验**:`MDK-ARM\reset.bat cmp [xxx.hex]`(= `-c SWD -CmpFile <hex> 0x08000000`,2026-10-10 加;默认对最新构建 hex)。判定看输出行首的 `No difference found`(`This file is already downloaded` 是同义提示)。⚠ **连接即复位目标**:实测比对瞬间板子重打一遍开机日志 → 在自动化闭环里回读必须排在**日志采集之后**(`run_boot_test.py` 已按此顺序内置)。
|
||||
|
||||
**C. 救援**(板子起不来 / 疑似 Flash 问题)——本机 3 线接线的可行子集:
|
||||
|
||||
```bat
|
||||
|
||||
+12
-4
@@ -9,8 +9,8 @@
|
||||
│
|
||||
├─ Step 0 预检 .venv + serial_tool(selftest/ports 确认 COM)
|
||||
├─ Step 1 构建验证 MDK-ARM/build.bat (0 错误 0 警告才继续)
|
||||
├─ Step 2 烧录 + 采集(一条命令编排:开串口 + 清日志 → 烧录 → wait-idle → 提取)
|
||||
│ test/host/run_boot_test.py (内置 serial_tool + download.bat + verify_log.py)
|
||||
├─ Step 2 烧录 + 采集(一条命令编排:开串口 + 清日志 → 烧录 → 固件身份戳 → wait-idle → 提取 → 回读校验)
|
||||
│ test/host/run_boot_test.py (内置 serial_tool + download.bat + reset.bat cmp + verify_log.py)
|
||||
├─ Step 3 判定:
|
||||
│ PASS → 报告,结束
|
||||
│ FAIL → 依据 [MISS]/[HIT] + 日志尾部,修改代码后回到 Step 1
|
||||
@@ -23,9 +23,10 @@
|
||||
| `@build` | `.opencode/command/build.md` | 仅编译验证(0 错误 0 警告) |
|
||||
| `@download` | `.opencode/command/download.md` | 编译并烧录(ST-Link) |
|
||||
| `@test` | `.opencode/command/test.md` | 完整闭环:build → capture+download → verify → iterate |
|
||||
| `test/host/run_boot_test.py` | `test/host/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → wait-idle(静默 8s/超时 30s)→ 提取 `[RX]` → verify,退出码 0=PASS/1=FAIL/2=采集或烧录错误/3=用法错误 |
|
||||
| `test/host/run_boot_test.py` | `test/host/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → 固件身份戳(从 hex 抠 `fw build:`)→ wait-idle(静默 8s/超时 30s/首字节 12s)→ 提取 `[RX]` → 回读校验(`reset.bat cmp`)→ verify,退出码 0=PASS/1=FAIL/2=采集·烧录·回读错误/3=用法错误 |
|
||||
| `MDK-ARM/reset.bat` | `MDK-ARM/reset.bat` | ST-LINK CLI 封装:`flash [hex]`(烧 + `-Rst -Run`)/ **`cmp [hex]`(回读比对,2026-10-10 加)**/ 无参复位 / `halt`/`run`/`list`/`hard` |
|
||||
| `tools/serial_tool/serial_tool.py` | `tools/serial_tool/` | Python 串口助手(常驻 daemon + 薄 CLI):ports/open/send/status/log/clear/wait-idle/selftest;全量日志 `state/serial.log`(20MB 自动截头) |
|
||||
| `test/host/verify_log.py` | `test/host/verify_log.py` | 对日志做标记校验,输出 PASS/FAIL 并返回退出码 |
|
||||
| `test/host/verify_log.py` | `test/host/verify_log.py` | 对日志做标记校验(含**启动锚点计数** `--max-boot-count`、复位原因白名单 `--expect-reset-cause`),输出 PASS/FAIL 并返回退出码 |
|
||||
|
||||
## 判定标准
|
||||
|
||||
@@ -35,6 +36,9 @@
|
||||
- `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-10 加)**:采集结束后自动跑 `reset.bat 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)。
|
||||
|
||||
### 自检结果判定(S7,`--require-selftest-pass`)
|
||||
|
||||
@@ -69,6 +73,9 @@
|
||||
| daemon 无响应 / 自动拉起失败 | 查 `tools/serial_tool/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 未重编译 | `reset.bat flash <hex>` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻) |
|
||||
| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标) | 按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §10 排查;`reset.bat flash` 重烧后闭环会自动复核 |
|
||||
| `boot banner x2 > allowed 1` | 采集窗口内板子自己复位过(IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启) | 看判定里逐条打印的 `reset cause:` 定位;跑**故意复位**的套件用 `--max-boot-count N` 放宽 |
|
||||
| 端口被占用(`port busy`) | 先 `serial_tool.py close` 或 `restart`;检查其他程序占用;Keil ST-Link 不占 COM |
|
||||
| 日志中无 UI 相关输出 | 见下节「UI 自动化」 |
|
||||
| 日志出现 `[ERR]` 行 | 固件错误级日志(`DBG_ERROR`),默认 Forbidden 会判 FAIL:按 TAG 查对应模块的 `docs/问题记录/*_Trap_Records.md`;若属测试套件的**预期**错误(守卫负路径、非法名校验等),用 `--forbidden` 放宽 |
|
||||
@@ -103,3 +110,4 @@
|
||||
| 2026-09-15 | 采集机制整体切换:serial-mcp-server(采集窗口/first-byte/EOF 重试)→ 仓库内置 Python 串口助手(daemon 常驻全量日志 + `wait-idle` 静默判据 `-SettleMs`/`-MaxWaitMs`);`serial_capture.ps1` 删除,流程/脚本清单/失败模式表同步 |
|
||||
| 2026-09-21 | 默认 Forbidden 增补 `[ERR]`:固件 `DBG_ERROR` 前缀是 `[ERR]`(≠ `ERROR` 子串),此前驱动/模块错误日志不会被自动闭环拦截;`verify_log.py` / `run_boot_test.py` 默认值同步,测试套件运行需显式放宽 |
|
||||
| 2026-10-10 | 采集链可靠性修复(serial_tool 第一批):`wait-idle` 的 `no_data` 改按本次增量判(旧实现常驻 daemon 下永不触发,"烧录后无数据"这一失败模式实际测不出来)+ 首字节门 `--first-timeout-ms`(`run_boot_test.py` 新增 `--first-wait-ms`,默认 12000);`log --grep` 改先过滤后截尾(旧顺序导致 marker 必然查不到)并加 `-i`/`--regex`;`wait-idle` CLI 超时自动推导,不再需手工 `--timeout`;HTTP 口独占绑定修正(旧选项名/数值均无效,实际未生效) |
|
||||
| 2026-10-10 | **第二批:闭环补上"烧进去的是哪份镜像"与"跑的过程中有没有偷偷复位"**:① 固件身份戳——`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`,`run_boot_test.py` 从待烧 hex 里抠出该串追加成 Required 标记(旧镜像/错镜像此前所有固定标记照样全中);② 烧录回读校验进闭环——`reset.bat` 新增 `cmp` 子命令(`-CmpFile` 全片比对),采集后自动跑,不一致 exit 2;实测 `-CmpFile` 连接时会复位目标,故必须排在采集之后;③ 启动锚点计数——`verify_log.py` 新增 `--max-boot-count`/`--boot-marker`/`--expect-reset-cause`,`reset cause:` 出现 0 次或 >1 次判 FAIL;④ 修 `--flash-cmd` 带参数时预检必 missing、以及 `cmd /c` 对含空格路径(本仓库目录名 `DTU Code`)的引号解析把回读假判成"芯片不一致"(改为直接 CreateProcess 跑 .bat)。真机验证:完整烧录闭环 PASS(含戳匹配 + `No difference found` + 锚点 x1);伪造戳 → 其余标记全中仍 FAIL |
|
||||
|
||||
+140
-8
@@ -4,10 +4,16 @@
|
||||
调用仓库内置串口助手(tools/serial_tool,常驻 daemon,子进程 CLI + JSON):
|
||||
1. open 串口 + clear 日志(本次运行独立代际)
|
||||
2. 烧录(默认 MDK-ARM\\download.bat:下载 + Reset & Run)
|
||||
2.5 固件身份戳:从刚烧进去的 .hex 里抠出 `fw build: <DATE> <TIME>`,追加成 Required
|
||||
标记——芯片里跑的必须就是这份构建(--no-fw-check 关闭)
|
||||
3. wait-idle:RX 静默 >= settle-ms 收尾;首字节超过 --first-wait-ms 未到 → no_data
|
||||
(快速失败);整体 max-wait-ms 超时
|
||||
4. 从日志提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀)
|
||||
5. verify_log.py 标记校验
|
||||
4.5 烧录回读校验:MDK-ARM\\reset.bat cmp(= ST-LINK_CLI -CmpFile 全片逐字节比对,
|
||||
`No difference found` 才算真写进去;--no-verify-flash 关闭)。
|
||||
**必须排在采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机日志)
|
||||
5. verify_log.py 标记校验(含启动锚点计数:`reset cause:` 出现 0 次或 >N 次 = FAIL,
|
||||
N 由 --max-boot-count 给,默认 1;--expect-reset-cause 可对第一个锚点做原因白名单)
|
||||
6. (仅当 --require-selftest-pass)自检记录联合判定(S7):
|
||||
--selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log
|
||||
(lftpd :21 匿名);判定:文件缺失 / 0 字节 = 无故障 → PASS,有 64B 记录 = FAIL
|
||||
@@ -32,19 +38,23 @@
|
||||
[--require-suite-summary "Storage Tests"]
|
||||
[--require-selftest-pass] [--selftest-log <本地副本>]
|
||||
[--device-ip 192.168.1.100] [--out-dir <目录>] [--show-tail-lines 15]
|
||||
[--fw-hex <构建产物 hex>] [--no-fw-check]
|
||||
[--no-verify-flash] [--readback-timeout 120]
|
||||
[--max-boot-count 1] [--expect-reset-cause 'software;power-on']
|
||||
|
||||
--port 缺省取 .opencode/serial_config.json 的 portName(单一配置源)
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(采集成功 + Required 全中 + Forbidden 零命中)
|
||||
1 = FAIL(标记缺失/命中)
|
||||
2 = 采集或烧录错误
|
||||
2 = 采集或烧录错误(含烧录回读不一致 = 芯片内容与刚编的镜像不同,本次运行作废)
|
||||
3 = 用法/预检错误
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ftplib
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -59,10 +69,70 @@ REPO_ROOT = SCRIPT_DIR.parent.parent
|
||||
SERIAL_TOOL = REPO_ROOT / "tools" / "serial_tool" / "serial_tool.py"
|
||||
CFG_FILE = REPO_ROOT / ".opencode" / "serial_config.json"
|
||||
DEFAULT_FLASH = REPO_ROOT / "MDK-ARM" / "download.bat"
|
||||
RESET_BAT = REPO_ROOT / "MDK-ARM" / "reset.bat"
|
||||
DEFAULT_HEX = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.hex"
|
||||
|
||||
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
|
||||
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;[ERR]"
|
||||
|
||||
# 固件身份戳(App/app/app_main.c 的 `DBG_INFO("fw build: " __DATE__ " " __TIME__)`)。
|
||||
# 编译期展开成一段连续 ASCII,故能直接从 .hex 里抠出来当 Required 标记用:
|
||||
# 芯片里跑的镜像 == 刚编出来的镜像,这才叫"烧进去了"。此前所有标记都是固定串,
|
||||
# 旧镜像照样全中——AGENTS.md 烧录铁律记的"反复重下载、芯片一直是旧镜像"在
|
||||
# 自动闭环里完全不可见(2026-10-10 加)。
|
||||
FW_MARKER = "fw build: "
|
||||
|
||||
|
||||
def _split_cmd(cmd_str):
|
||||
r"""把 `--flash-cmd` 这类命令串拆成 [exe, arg, ...](2026-10-10 实测坑)。
|
||||
|
||||
直接交给 subprocess、**不经 `cmd /c`**:CreateProcess 能直接跑 .bat,且 Python
|
||||
会给含空格的路径正确加引号。旧写法 `['cmd','/c', 串]` 在本仓库(目录名
|
||||
`DTU Code` 含空格)会被 cmd 的 /c 引号规则玩坏——实测报
|
||||
`'D:\Code\DTU' is not recognized`,回读校验被假判成"芯片内容不一致"。
|
||||
路径本身含空格时把该段用引号包住(`"D:\some dir\reset.bat" flash`)。
|
||||
posix=False 是必须的:posix=True 会吃掉 Windows 路径里的反斜杠。
|
||||
"""
|
||||
return [p.strip('"') for p in shlex.split(cmd_str, posix=False)]
|
||||
|
||||
|
||||
def _run_cmd(argv, timeout):
|
||||
"""跑一条外部命令,按字节取回输出并显式 UTF-8 解码(不受控制台代码页影响)。"""
|
||||
try:
|
||||
proc = subprocess.run(argv, capture_output=True, timeout=timeout)
|
||||
rc = proc.returncode
|
||||
raw = (proc.stdout or b"") + (proc.stderr or b"")
|
||||
except subprocess.TimeoutExpired as e:
|
||||
rc = -1
|
||||
raw = (e.stdout or b"") + (e.stderr or b"")
|
||||
return rc, raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def extract_fw_stamp(hex_path):
|
||||
"""从 Intel HEX 里抠固件身份戳(只扫数据记录,不建整片镜像,内存 O(镜像体积))。
|
||||
|
||||
返回戳串(如 'Oct 10 2026 02:38:12');找不到(旧固件/未重编译)返回 None。
|
||||
"""
|
||||
data = bytearray()
|
||||
for line in Path(hex_path).read_text(errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith(":"):
|
||||
continue
|
||||
try:
|
||||
rec = bytes.fromhex(line[1:])
|
||||
except ValueError:
|
||||
continue
|
||||
if len(rec) > 4 and rec[3] == 0:
|
||||
data += rec[4:4 + rec[0]]
|
||||
i = data.find(FW_MARKER.encode("ascii"))
|
||||
if i < 0:
|
||||
return None
|
||||
j = i + len(FW_MARKER)
|
||||
end = j
|
||||
while end < len(data) and 0x20 <= data[end] < 0x7F:
|
||||
end += 1
|
||||
return data[j:end].decode("ascii", errors="replace")
|
||||
|
||||
|
||||
def _cli(cmd_args, timeout):
|
||||
"""调用串口助手 CLI,返回 (rc, stdout_text, parsed_json_or_None)。
|
||||
@@ -173,6 +243,21 @@ def main(argv=None):
|
||||
help="device IP for fetching /log/selftest.log via FTP (lftpd anonymous)")
|
||||
ap.add_argument("--out-dir", default=str(Path(tempfile.gettempdir()) / "opencode"))
|
||||
ap.add_argument("--show-tail-lines", type=int, default=15)
|
||||
ap.add_argument("--fw-hex", default=str(DEFAULT_HEX),
|
||||
help="built image for the firmware-identity stamp and the readback "
|
||||
"compare (default: latest build hex)")
|
||||
ap.add_argument("--no-fw-check", action="store_true",
|
||||
help="do not require the 'fw build: <DATE> <TIME>' stamp in the boot log")
|
||||
ap.add_argument("--no-verify-flash", action="store_true",
|
||||
help="skip the ST-LINK -CmpFile readback (AGENTS.md makes it mandatory)")
|
||||
ap.add_argument("--readback-timeout", type=int, default=120,
|
||||
help="timeout for the readback compare, seconds")
|
||||
ap.add_argument("--max-boot-count", type=int, default=1,
|
||||
help="fail if the boot banner ('reset cause:') appears 0 times or more "
|
||||
"than N times (0 = check off)")
|
||||
ap.add_argument("--expect-reset-cause", default="",
|
||||
help="';'-separated substrings the FIRST reset cause must match "
|
||||
"(case-insensitive); empty = report only, no verdict")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# 串口助手 CLI 用当前解释器(AGENTS.md 统一入口 = 仓库 .venv python)
|
||||
@@ -191,7 +276,10 @@ def main(argv=None):
|
||||
print("RUN_TEST_ERROR: no --port given and no portName in {}".format(CFG_FILE))
|
||||
return 3
|
||||
|
||||
flash_cmd = Path(args.flash_cmd) if args.flash_cmd else DEFAULT_FLASH
|
||||
# --flash-cmd 允许带参数(如 "MDK-ARM\reset.bat flash"):拆成 argv,
|
||||
# 存在性只查 exe 段(整串当文件名必然 missing,2026-10-10 实测)
|
||||
flash_argv = _split_cmd(args.flash_cmd) if args.flash_cmd else [str(DEFAULT_FLASH)]
|
||||
flash_cmd = Path(flash_argv[0])
|
||||
|
||||
# 预检(对应旧版 missing-file 检查)
|
||||
for p in (SERIAL_TOOL, SCRIPT_DIR / "verify_log.py", flash_cmd):
|
||||
@@ -226,10 +314,10 @@ def main(argv=None):
|
||||
|
||||
# 3. flash(下载 + Reset & Run)。烧录失败不中断:板子可能已复位,
|
||||
# 由采集 + 标记判定结果(与旧版一致)
|
||||
print("FLASH_START: {}".format(flash_cmd))
|
||||
print("FLASH_START: {}".format(" ".join(flash_argv)))
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
proc = subprocess.run(["cmd", "/c", str(flash_cmd)],
|
||||
proc = subprocess.run(flash_argv,
|
||||
capture_output=True, timeout=args.flash_timeout)
|
||||
flash_rc = proc.returncode
|
||||
flash_bytes = (proc.stdout or b"") + (proc.stderr or b"")
|
||||
@@ -243,6 +331,25 @@ def main(argv=None):
|
||||
if flash_rc != 0:
|
||||
print("RUN_TEST_WARN: flash rc={} (continuing: board may still have reset)".format(flash_rc))
|
||||
|
||||
# 3.5 固件身份戳:从**刚烧进去的那份 .hex** 里抠出 `fw build: <DATE> <TIME>`,
|
||||
# 追加成 Required 标记 → 芯片里跑的必须就是这份构建。旧 hex 里没有该串
|
||||
# (未重编译)时只 WARN 跳过,不假判 FAIL。
|
||||
required = args.required
|
||||
fw_hex = Path(args.fw_hex)
|
||||
if args.no_fw_check:
|
||||
print("FW ID: skipped (--no-fw-check)")
|
||||
elif not fw_hex.is_file():
|
||||
print("RUN_TEST_WARN: fw hex not found {} -> identity + readback skipped".format(fw_hex))
|
||||
else:
|
||||
fw_stamp = extract_fw_stamp(fw_hex)
|
||||
if fw_stamp is None:
|
||||
print("RUN_TEST_WARN: no '{}' stamp in {} -> identity check skipped "
|
||||
"(old build: run @build first)".format(FW_MARKER, fw_hex.name))
|
||||
else:
|
||||
marker = FW_MARKER + fw_stamp
|
||||
required = (args.required + ";" + marker) if args.required else marker
|
||||
print("FW ID: expected stamp '{}' (from {})".format(marker, fw_hex.name))
|
||||
|
||||
# 4. wait-idle(RX 静默收尾或整体超时)
|
||||
# --first-timeout-ms:首字节门——烧录后板子全程不吐字节时按 no_data 快速失败
|
||||
# (旧版 daemon 常驻累计 rx_bytes>0,no_data 永不触发,只能靠后面"日志里没有
|
||||
@@ -280,17 +387,42 @@ def main(argv=None):
|
||||
boot_txt.write_bytes(("\r\n".join(rx) + "\r\n").encode("utf-8"))
|
||||
print("Boot log: {} (lines={} gen={})".format(boot_txt, len(rx), data.get("gen")))
|
||||
|
||||
# 5.6 烧录回读校验(AGENTS.md 铁律:`No difference found` 才算真写进去)。
|
||||
# 必须排在**采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机
|
||||
# 日志),排在采集前会截断开机日志并让启动锚点计数误判成"中途复位"。
|
||||
if args.no_verify_flash:
|
||||
print("READBACK: skipped (--no-verify-flash)")
|
||||
elif not fw_hex.is_file():
|
||||
print("READBACK: skipped (no fw hex: {})".format(fw_hex))
|
||||
else:
|
||||
rb_rc, rb_text = _run_cmd(_split_cmd('{} cmp "{}"'.format(RESET_BAT, fw_hex)),
|
||||
args.readback_timeout)
|
||||
# 判定按**行首**匹配:进度条和 reset.bat 的帮助文本里都含 "No difference found"
|
||||
# 字样,只有 ST-LINK 自己那行才独占行首(实测 `...100%\r\nNo difference found.`)。
|
||||
rb_ok = any(ln.strip().lower().startswith("no difference found")
|
||||
for ln in rb_text.splitlines())
|
||||
if rb_ok:
|
||||
print("READBACK: OK (No difference found) rc={}".format(rb_rc))
|
||||
else:
|
||||
print("READBACK: FAIL -- device flash does NOT match {}".format(fw_hex.name))
|
||||
for line in rb_text.strip().splitlines()[-10:]:
|
||||
print(" | {}".format(line))
|
||||
print("RUN_TEST_ERROR: flash readback mismatch (chip content != built image)")
|
||||
return 2
|
||||
|
||||
# 5.5 自检记录抓取(S7):启用且未提供本地副本时才走 FTP
|
||||
selftest_local = args.selftest_log
|
||||
if args.require_selftest_pass and not selftest_local:
|
||||
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp)
|
||||
|
||||
# 6. 标记校验(含可选的自检记录联合判定)
|
||||
# 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定)
|
||||
from verify_log import verify
|
||||
rc = verify(str(boot_txt), args.required, args.forbidden, args.show_tail_lines,
|
||||
rc = verify(str(boot_txt), required, args.forbidden, args.show_tail_lines,
|
||||
suite_summary=args.require_suite_summary,
|
||||
selftest_pass=args.require_selftest_pass,
|
||||
selftest_log=selftest_local)
|
||||
selftest_log=selftest_local,
|
||||
max_boot_count=args.max_boot_count,
|
||||
expect_reset_cause=args.expect_reset_cause)
|
||||
print("RUN_TEST_RC={}".format(rc))
|
||||
return rc
|
||||
|
||||
|
||||
+75
-3
@@ -11,6 +11,8 @@
|
||||
[--require-suite-summary 'Storage Tests;GD5F2GQ5UE Tests']
|
||||
[--require-selftest-pass] [--selftest-log <selftest.log 本地副本>]
|
||||
[--show-tail-lines 40]
|
||||
[--max-boot-count 1] [--boot-marker 'reset cause:']
|
||||
[--expect-reset-cause 'software;power-on']
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(所有 Required 标记找到,Forbidden 零命中,汇总行要求满足)
|
||||
@@ -42,6 +44,11 @@
|
||||
通过项):**文件缺失 / 0 字节 = 无故障 → PASS**;存在任何一条记录 = 有故障 → FAIL
|
||||
(退出码 1),并逐条打印时间戳 + 描述。抓取失败(设备不可达 / FTP 错误等)由调用方
|
||||
(run_boot_test.py)先打 WARN,本地文件按缺失处理 → 按上述规则不判整体 FAIL。
|
||||
- 启动锚点计数(2026-10-10):`--max-boot-count N`(run_boot_test.py 默认传 1)统计
|
||||
`reset cause:` 锚点出现次数——**0 次** = 采集没从复位开始(标记全中也可能只是上一轮
|
||||
的残留日志);**>N 次** = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
`NVIC_SystemReset` / 掉电重启),此前这类运行只要不命中 Forbidden 就判 PASS。
|
||||
`--expect-reset-cause` 对第一个锚点做原因白名单(大小写不敏感子串),空 = 只报告不判。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -53,6 +60,12 @@ from pathlib import Path
|
||||
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
|
||||
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;assert;[ERR]"
|
||||
|
||||
# 每轮启动的锚点行(App/app/app_main.c:`reset cause: <原因> (CSR=0x…)`)。
|
||||
# 一次采集里出现 >1 次 = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
# NVIC_SystemReset / 掉电重启),此前只有 Forbidden 命中时才被发现,
|
||||
# "跑完 PASS 但中途重启过"完全不可见(2026-10-10 加)。
|
||||
BOOT_MARKER_DEFAULT = "reset cause:"
|
||||
|
||||
# 套件汇总行:`=== Storage Tests: 3/3 PASSED (failed=0) ===`
|
||||
# 名称可含空格;N/M 为十进制;failed 允许前导 0。宽松容忍前后空白与
|
||||
# 日志时间戳前缀(`[2026-10-01 12:00:00.000]`)——故用 search 而非 match。
|
||||
@@ -77,9 +90,21 @@ def _read_lines(path):
|
||||
return raw.decode("utf-8", errors="replace").splitlines()
|
||||
|
||||
|
||||
def _reset_cause_of(line, marker):
|
||||
"""从 `reset cause: software (NVIC_SystemReset) (CSR=0x14000000)` 取原因文本。"""
|
||||
i = line.find(marker)
|
||||
if i < 0:
|
||||
return ""
|
||||
tail = line[i + len(marker):].strip()
|
||||
j = tail.find(" (CSR=")
|
||||
return tail[:j] if j > 0 else tail
|
||||
|
||||
|
||||
def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
show_tail=40, out=print, suite_summary="",
|
||||
selftest_pass=False, selftest_log=""):
|
||||
selftest_pass=False, selftest_log="",
|
||||
boot_marker=BOOT_MARKER_DEFAULT, max_boot_count=0,
|
||||
expect_reset_cause=""):
|
||||
"""标记校验核心(可被 run_boot_test.py 复用)。返回退出码 0/1/2。
|
||||
|
||||
suite_summary: ';' 分隔的套件名列表。非空时,要求日志中每个套件都出现
|
||||
@@ -87,6 +112,11 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
selftest_pass: True 时对 selftest_log(/log/selftest.log 本地副本)联合判定:
|
||||
文件缺失 / 0 字节 = 无故障(PASS);存在任何 64B 记录 = FAIL(S7)。
|
||||
selftest_log: 自检记录文件本地路径(空串 = 按文件缺失处理)。
|
||||
boot_marker / max_boot_count(2026-10-10):统计启动锚点出现次数。
|
||||
max_boot_count>0 时启用:0 次 = 采集没从复位开始;>N 次 = 板子
|
||||
在采集窗口内自己复位过(IWDG / Fault 后 NVIC_SystemReset)。
|
||||
expect_reset_cause: ';' 分隔的原因子串白名单,只对**第一个**锚点生效(大小写不敏感)。
|
||||
空 = 只报告不判定。
|
||||
"""
|
||||
p = Path(log_path)
|
||||
if not p.is_file():
|
||||
@@ -124,6 +154,37 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
else:
|
||||
out("[OK] forbidden pattern absent: {}".format(f))
|
||||
|
||||
# 启动锚点计数(2026-10-10):抓"跑完但中途复位过"和"采集没从复位开始"
|
||||
boot_issue = False
|
||||
if max_boot_count > 0:
|
||||
banner_lines = [ln for ln in lines if boot_marker in ln]
|
||||
n_boot = len(banner_lines)
|
||||
causes = [_reset_cause_of(ln, boot_marker) for ln in banner_lines]
|
||||
if n_boot == 0:
|
||||
out("[FAIL] no boot banner (marker '{}'): capture did not start at a reset".format(
|
||||
boot_marker))
|
||||
boot_issue = True
|
||||
elif n_boot > max_boot_count:
|
||||
out("[FAIL] boot banner x{} > allowed {} -> board reset itself during capture".format(
|
||||
n_boot, max_boot_count))
|
||||
for ln in banner_lines[:6]:
|
||||
out(" {}".format(ln.strip()))
|
||||
boot_issue = True
|
||||
else:
|
||||
out("[OK] boot banner x{} (marker '{}')".format(n_boot, boot_marker))
|
||||
if causes:
|
||||
out("[INFO] reset cause(s): {}".format("; ".join(causes)))
|
||||
exp = _split_list(expect_reset_cause)
|
||||
if exp:
|
||||
got = causes[0].lower()
|
||||
if any(e.lower() in got for e in exp):
|
||||
out("[OK] reset cause '{}' matches expected {}".format(
|
||||
causes[0], expect_reset_cause))
|
||||
else:
|
||||
out("[FAIL] reset cause '{}' not in expected: {}".format(
|
||||
causes[0], expect_reset_cause))
|
||||
boot_issue = True
|
||||
|
||||
# 套件汇总行解析(T-2):抓"没跑到汇总行 / 断言数为 0"的盲区
|
||||
bad_summary = []
|
||||
if summary_list:
|
||||
@@ -189,7 +250,7 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
out(ln)
|
||||
|
||||
out("")
|
||||
if missing or hits or bad_summary or selftest_fail:
|
||||
if missing or hits or bad_summary or selftest_fail or boot_issue:
|
||||
out("VERDICT: FAIL")
|
||||
return 1
|
||||
out("VERDICT: PASS")
|
||||
@@ -220,12 +281,23 @@ def main(argv=None):
|
||||
"missing/empty = PASS, any 64B record = FAIL")
|
||||
ap.add_argument("--selftest-log", default="",
|
||||
help="local copy path of /log/selftest.log (with --require-selftest-pass)")
|
||||
ap.add_argument("--boot-marker", default=BOOT_MARKER_DEFAULT,
|
||||
help="per-boot banner substring (default '{}')".format(BOOT_MARKER_DEFAULT))
|
||||
ap.add_argument("--max-boot-count", type=int, default=0,
|
||||
help="fail if the boot banner appears 0 times or more than N times "
|
||||
"(0 = check off). run_boot_test.py passes 1 by default")
|
||||
ap.add_argument("--expect-reset-cause", default="",
|
||||
help="';'-separated substrings the FIRST reset cause must match "
|
||||
"(case-insensitive). Empty = report only, no verdict")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
return verify(args.log_path, args.required, args.forbidden,
|
||||
args.show_tail_lines, suite_summary=args.require_suite_summary,
|
||||
selftest_pass=args.require_selftest_pass,
|
||||
selftest_log=args.selftest_log)
|
||||
selftest_log=args.selftest_log,
|
||||
boot_marker=args.boot_marker,
|
||||
max_boot_count=args.max_boot_count,
|
||||
expect_reset_cause=args.expect_reset_cause)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user