Files
STM32F4-Base/docs/测试说明/专项验证/启动日志原子性复验流程.md
T
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

163 lines
15 KiB
Markdown
Raw 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.
# 启动日志原子性复验流程(台架)
> **这篇是什么**:验证"控制台整行独占 / 崩溃遗言原子性"这类**行级完整性**判据的固定动作与口径。
> 它补的是 `../../子系统设计/控制台单写者日志设计说明.md` 的**验收侧**:那边讲怎么修,这边讲怎么证明修好了、
> 以及**为什么"标记全中"不等于"日志没坏"**。
> 来源:2026-10-26 台架实测(缺陷立项 task-23、收口复验 task-30、工具与流程吸收 task-36)。
## 1. 为什么子串标记照不出交错
`.dsh/skills/serial-boot-test/scripts/verify_log.py` 的 `--required` 是**大小写敏感子串**匹配。
交错缺陷的真实产物是这样的两行(原始捕获,未改写):
```text
[PROT] uncalibrated -> protection [blocked (UA/UN/IA VALI2D not all s0et)
26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL...
```
第二行**仍然包含**标记串 `Initializing NAND FTL` —— 于是旧口径把这份启动判成 PASS。
实测对照(同一份文件、同一台机器):
| 口径 | 命令要点 | 对缺陷捕获的判定 |
|---|---|---|
| 旧(子串) | `--required 'Initializing NAND FTL;uncalibrated -> protection'` | **PASS(假绿)** |
| 新(整行) | `--exact '\[...\]\[NAND_FTL\] Initializing NAND FTL\.\.\.'` | **FAIL**,并把缺行首的邻居原样打出来 |
**结论**:任何"这一行可能丢行首 / 可能被插字节"的标记,必须用 `--exact` 整行锚定,
不得只用 `--required`。
## 2. 校验器口径(`verify_log.py`)
| 开关 | 语义 | 为什么这样默认 |
|---|---|---|
| `--exact 'REGEX'` | 剥掉采集器前缀(`[RX] <ts>` / `[+Nms]`)后**整行** `^REGEX$` 必须命中;同时报告"含该式最长字面串但未整行命中"的邻居行 = 交错特征 | 单点插字节即失配;邻居不得静默 |
| `…@@LEN=<n>` 后缀 | **显式**钉该行字节长度 | 长度属于**某一份像**的属性;默认不钉,否则无辜改动就假红(实测同一份像 uiTask 峰值自己摆 72 B) |
| `--allow-residual N` | 允许行首前 N 个杂散字节(复位打断移位寄存器的已知残留),**记录但不判失败** | 崩溃注入腿允许 ≤1 残留;写成失败就会把已知现象当回归 |
| `--head-char C` | 命中行(去残留后)必须以 C 开头,如 `[` | 崩溃日志的行首形态本身就是判据 |
| `--pair FIRST THEN` | 恰好一条 FIRST,且其后**只有一条** THEN(跨复位配对) | 崩溃遗言必须与下一次启动的 `reset cause:` 成对;出现两条 = 有未记录/重复复位 |
| `--include-comments` | 不跳过 `#` 行 | 默认**跳过**:归档证据文件带 ASCII 溯源头,头里提到 `[ERR]` 不该触发 Forbidden(工具级假红防线) |
| `--selftest` | 跑内置 **22 条**判别用例后退出,无需日志与硬件(含下表三计数分类器) | "不可能失败的 PASS 就是空跑"——用例里含交错、缺行首、残留 1/2、配对错序,以及**"旧子串口径在同一条缺陷行上判 PASS"** 这一条盲区证明 |
用法示例(一次仅复位启动的原子性判定)。四个行级开关**也已接进 `run_boot_test.py`**,
所以 /serial-boot-test 闭环里直接加 `--exact ... --pair ... --allow-residual 1 --head-char '['` 即可,无需单独跑本脚本:
```powershell
& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\verify_log.py `
--log-path <boot_log.txt> `
--required 'App init OK;net init: OK' `
--exact '\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[PROT\] uncalibrated -> protection blocked \(UA/UN/IA VALID not all set\)' `
--exact '\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[NAND_FTL\] Initializing NAND FTL\.\.\.' `
--max-boot-count 1 --show-tail-lines 0
```
**退出码与三计数(task-32 定稿)——判据必须自己说清「没启动」与「没采到」**:
| 码 | 含义 | 触发条件 | 聚合口径 |
|---|---|---|---|
| 0 | PASS | 标记齐、Forbidden 零、锚点合法 | 一次好启动 |
| 1 | **设备侧真有坏消息** | 缺 Required / 命中 Forbidden / 顺序或耗时超限 / 整行锚定失配 | 记一次设备故障 |
| 2 | **测量没成立** | 日志读不到;或开了 `--max-boot-count` 却**零启动锚点**(采集器没开、日志被清、采集没从复位开始) | **不得**计入设备故障 |
三计数读法:`hits=0 + loose_neighbours=0 + 锚点=0` = 采集断了,先修测量,此时任何通过都无意义;
`hits=0 + neighbours≥1` = 行被啃(真缺陷,正是 `--exact` 要抓的);
`锚点≥1 + hits=0 + neighbours=0` 才允许当"功能缺失"看。
优先级铁律:**零锚点但同时有 Forbidden 命中或自检故障记录 → 仍判 1**,测量问题只在一边加 WARN
——绝不能把一次真崩溃降级成「没数据」(实测已按此口径验过:造一份"零锚点但含 `[ERR]` MEMFAULT"的日志,
工具仍判 FAIL 并另加一行 WARN,没有把崩溃改判成测量问题)。
`--selftest` 里钉了 6 条分类器用例;把分类器改成常量返回"不坏",自检立刻红(真跑过)。
## 3. 固定动作(顺序即判据,别调)
1. **环境**:继承完整环境,只补 `DTU_KEIL_UV4` / `DTU_STLINK_CLI`,并把 `TEMP`/`TMP` 指到真实目录。
⚠ 根因记账:本机注册表 **User 级 `TMP` 是未展开的字面量 `%USERPROFILE%\AppData\Local\Temp`** ——
残缺环境里 UV4 会在仓库根建出字面量目录 `%SystemDrive%/`,而 `task_worktree.py` 会因找不到
`git` 让 `keil_build.py` 报**假的"build 锁被占用"(rc=4)**。看到 rc=4 先 `lock status` 再判断,别默认是并发。
2. **出像**:像必须来自**已提交 commit + 干净工作树**。主树脏(别人在飞)时走仓外只读 detached worktree:
`git worktree add --detach <_wt>/bench-<ID>-<sha> <sha>` + `mklink /J <wt>\.venv <main>\.venv`;
建树后 `git -C <wt> status --short` 必须为空,全程只读、不 commit、不跑生成器。
3. **/keil-build**:0 错误 0 警告;随后**从 .hex 二进制里抠身份戳**(不是从 build_log 里读),与之后线上读到的
`fw build:` 逐字对上——这是"烧进去的就是这份像"的唯一硬证据。
4. **烧录启动**:`/serial-boot-test` 已保证 flash(`-Rst -Run`) → 采日志 → 回读(`-CmpFile`) 的次序(`-CmpFile` 会复位目标,
必须排最后)。
5. **N 次仅复位对照**(N≥3):每次 `clear`(记下 gen,证明窗口不重叠)→ `stlink_flash.py reset`(`-c SWD -Rst -Run`,
**零重烧**)→ `wait-idle` → 取日志逐块存档。"保存的值重启后还在"用重烧去验是**无意义**的。
6. **逐块校验**:`--exact` 锚定受害行 + `--max-boot-count 1` + Forbidden 默认含 `[ERR]`;
崩溃注入腿才加 `--pair\ --allow-residual 1\ --head-char '['`。
7. **非空跑对照**:每换一版校验器,至少跑一次 `--selftest`,并把**缺陷发生时的原始捕获**再判一遍——必须 FAIL。
8. **收尾**:最后一次硬件动作是 `cmp`(排在全批日志之后);台账与证据按 `../测试数据/boot_log_data/README.md` 的口径归档。
## 4. 三条容易踩的纪律
1. **故意复位的腿不得并入"连续仅复位不重烧"的计数**。崩溃注入腿(MemManage/栈溢出钩子)本身就要复位设备,
并进常规序列会被下一个人数锚点数判成异常。它必须单独跑、单独进台账、标题写明"本腿含故意复位"。
**同一条口径覆盖"前面带烧录的圈"**:烧录启动之后紧跟的那一次启动不属于"连续仅复位不重烧",
报数写成两句并列——「烧录启动 ×1(不进序列)+ 仅复位 ×N」;若那一圈连字节都没采到,则**两条独立理由**都排除它(没数据谈不上计数)。
2. **崩溃腿的判据是原子性,不是 `[ERR]` 为零**。内核故障钩子的输出**本身就以 `[ERR]` 开头**
(`[ERR] MEMFAULT …` / `[ERR] STACK OVERFLOW task=…`),在同一条路径上要求 `[ERR]=0` 等于要求它别工作;
而**产品构建的正常启动根本不走 IPSR 路径**(多段裸发只在 `Src/freertos.c` 的两个内核钩子里),
所以"跑 5 次正常启动、[ERR] 零命中"**证不到 IPSR**——那是定义上的必然,是空跑。
IPSR 主张交给主机侧套件(假 USART + IPSR + 临界区深度水位);要硬件证据就得由代码道**提交触发器**,
判据写"崩溃遗言一整行、恰好一次、`dropped=0`",并按 `../测试数据/boot_log_data/README.md` §2「故障注入日志」
运行时放宽 `/serial-boot-test`(`--forbidden` 去掉 `[ERR]`、`--max-boot-count 2`)——**不改仓库配置**。
## 5. 栈判据(task-32 定稿:三档分权,不再是"待定")
**一句话裁定:静态最坏链是唯一拦提交项(`scripts/stack_report.py` 每任务 verdict);运行时
水位只出告警(`scripts/stack_gate.py`,超容差 = WARN);采集健康门不过则整轮作废(VOID,
既不红也不绿、也不得写进台账)。**
| 档 | 判据 | 等级与权限 | 由谁跑 |
|---|---|---|---|
| 硬线 | `margin = cfg − max(静态最坏链, 运行时峰值) ≥ max(1024 B ISR, cfg×30%)` | **FAIL = 拦提交** | `scripts/stack_report.py`(看它退出码。**Lead 裁定 2026-10-27:提交时守这条的一直是 stack_report 本身;`stack_gate.py` 定为台架侧门禁、不接 pre-commit**——提交时既无设备也无当轮日志,挂上去只会「永远 SKIP」或「拿上一轮日志判这一轮的码」,后者是假凭证同族) |
| 趋势 | 同镜像 ≥3 圈取最大值 vs 基线轮 ≥3 圈最大值,`Δ > tol(task)` | **WARN = 记录 + 归因,永不单独拦提交** | `scripts/stack_gate.py` |
| 绝对底线 | 任一任务 `free_min < 25% × cfg` | **FAIL**(运行时唯一有 FAIL 权限的一条,不需要跨轮比较) | `scripts/stack_gate.py` |
| 采集健康门 | 锚点 `reset cause:` 恰 1 次 / 身份戳存在且**同轮唯一** / 每块 ≥20 行 / 每块都有 `[STACK]` 行 / ≥3 圈(基线轮同样要过门,否则本轮只跳趋势比较) | **VOID,exit 2** | `scripts/stack_gate.py` |
**容差是推导值,不是拍脑袋**:`tol(task) = 向上取整到 16 B 格的 max(128 B, 2 × 同镜像实测最大摆动)`。
实测来源:同一份像 4 圈仅复位里 uiTask 摆 72 B、adcTask 摆 80 B(task-30),2026-10-27 那份像 4 圈零摆动
(task-42)→ 最大摆动 80 B ⇒ **全任务统一 160 B**,登记在 `scripts/stack_tolerance.tsv`(含来源列)。
**谁能改这个数**:只有台架在「改栈配置 / 抬栈 / 新增 `DBG_*` 调用点」之后重测同镜像摆动才允许回填;
**禁止为了让某一轮变绿而调大容差**——被这把尺考核的人不得当场决定尺子的松紧(与 §7 里
"Forbidden 令牌不得落在英文单词内部"同源:判据的松紧与写法都要由测量侧一次性定死)。
**身份戳与归因**:跨轮比较本来就是两份镜像,正常;但 Δ 只有在**附上两次构建之间的源码 diff**
时才可归因到某笔改动。台账行必须写**两次的 `fw build` 时间戳(含 `suite=`/`dstr=`)**,
SHA 只当坐标(2026-10-26 那天 SHA 被 amend 挪了三轮)。同镜像比较里出现"零摆动"不代表判据失效,
只代表这次没噪声。
**为什么必须留 VOID 这一档**(设计输入=当天两次自纠):常驻 daemon 死掉后 `status` 会自动拉起
一个新 daemon 并"正常"应答,`rx_lines=0` 也照样返回成功 JSON;而 Forbidden 令牌 `Fault` 会命中
`de`+`fault` 制造假红。所以判据**不许依赖采集健康的自我感觉**:门不过 → exit 2 与 exit 1 分开,
按码聚合的人不会把"端口没开"记成"又崩了一次栈"。
**判据自己必须能红**:`& '.venv\Scripts\python.exe' scripts/stack_gate.py --selftest`(14 条,含
"72 B 实测摆动不算红"、"超容差只 WARN"、"单圈基线拒比"、"死采集器 VOID"、"同轮混两份身份戳 VOID"、
"绝对底线 FAIL"、"峰值取最坏一圈而非最后一圈")。**改完判据不跑这条,就等于没改。**
## 6. 锁与脚手架
- bench 锁是**会话级**,必须手工 acquire/release,且**用同一个名字**释放。
- 非登记任务名(如临时验收窗)走 `lock release bench --task <NAME>`(**不带** `--bench-pass`)即可释放;
`--bench-pass` 会先读 `_wt/.state/<NAME>.json` 台账,没有就报"任务不存在",`--force` 也救不了。
为记一个 PASS 去**造假台账**会白占并发工作树名额——PASS 写进任务卡描述即可。
- ⚠ **脚手架 `*.json` 一律不得放 `_wt/.state/`**:那是 task-worktree 的**台账目录**,脚本把每个 `*.json`
当台账读,实测 `task_worktree.py list` 直接崩在 `KeyError: 'id'`。放 `_wt/scratch/`。
## 7. 台架卫生清单(每一条都是实测踩过的,不是设想)
1. **每个台架窗口开头、以及每一圈复位之前**:`serial_tool.py open` + `status` 断言 `port.ready=true`,并**观察 `rx_lines` 在几秒内增长**。
实测:常驻 daemon 会死(今晚 pid 18020 跑了 156589 s 后没了),而 `status` 会**自动拉起一个新 daemon**,于是它一直"正常"地报 `ready=false / rx_lines=0` —— 端口状态**不会**跨小时存活,`/serial-boot-test` 开过也不算数。
2. **`serial_tool.py log / status / clear` 返回 JSON,不是纯文本**。按 `[RX]` 行首过滤纯文本会得到**恒 0 行**且看起来"跑成功了"。
3. **Forbidden 令牌不得落在英文单词内部**:裸 `Fault` 命中 `de`+`fault` → 打到 `[FTP] using factory default password` 与 `[STACK] default peak_used`(实测假红两次)。四个 Fault 的崩溃日志都以 `[ERR]` 开头,列 `[ERR]` 就够,要加就加 `MEMFAULT/BUSFAULT/USAGEFAULT/HardFault`。
4. **工具输出别混流**:非零退出时 stdout(结果)与 stderr(诊断)拼接后再 `JSON.parse` 必崩;解析只吃 stdout。
5. **给同事/lead 发消息时,正文里的引号用全角「」**:在 JS/TS 双引号字符串里放 ASCII `"` 会直接语法错,消息发不出去(本人一晚三次)。
6. **判据落点顺序**:先 grep 谁在消费这个码/字段(今晚的唯一消费者 `run_boot_test.py` 只对 `rc==0` 分支),再决定动不动语义。
## 8. 关联
- 修法与设计口径:`../../子系统设计/控制台单写者日志设计说明.md`(O1 两实例与字节账、§4.4.1 崩溃路径验证设计)
- 闭环与标记口径:`../流程与规范/自动化测试流程.md`
- 证据归档与命名:`../测试数据/boot_log_data/README.md`
- 复位语义与回读次序:`../测试工具/ST-LINK_CLI_复位使用说明.md`
- 烧录 / 采集 / 校验脚本:`../../../.dsh/skills/serial-boot-test/SKILL.md`、`../../../.dsh/skills/stlink-flash/SKILL.md`、
`../../../.dsh/skills/serial-assistant/SKILL.md`