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

15 KiB
Raw Permalink Blame History

启动日志原子性复验流程(台架)

这篇是什么:验证"控制台整行独占 / 崩溃遗言原子性"这类行级完整性判据的固定动作与口径。 它补的是 ../../子系统设计/控制台单写者日志设计说明.md 的验收侧:那边讲怎么修,这边讲怎么证明修好了、 以及为什么"标记全中"不等于"日志没坏"。 来源:2026-10-26 台架实测(缺陷立项 task-23、收口复验 task-30、工具与流程吸收 task-36)。

1. 为什么子串标记照不出交错

.dsh/skills/serial-boot-test/scripts/verify_log.py 的 --required 是大小写敏感子串匹配。 交错缺陷的真实产物是这样的两行(原始捕获,未改写):

[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 '[' 即可,无需单独跑本脚本:

& '.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