diff --git a/docs/测试说明/实机测试实施指南.md b/docs/测试说明/实机测试实施指南.md index ba49e8fe..3ecb795b 100644 --- a/docs/测试说明/实机测试实施指南.md +++ b/docs/测试说明/实机测试实施指南.md @@ -277,16 +277,25 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L 真停滞」判别口径落 `../子系统设计/控制台单写者日志设计说明.md` 文末附录—— 再现静默先按三本账(`dbg_console_dropped/_tx_fault/_tx_timeout`)+ syslog 丢弃 账对照定性,再决定是否开卡;销项条件 = N-17 修复版 V5 复跑(e02_probe + FTP - ≥30 min)运行期 stall 与非开机 `MISS start` 双零。 -- **判读工具(2026-10-25 起随单交付)**: + ≥30 min)运行期 `task stall` 与真停滞嫌疑静默段双零(2026-10-25 v1.1 起 + `MISS start` 降级为诊断行不计销项——见下方判读工具与 + `../子系统设计/看门狗设计说明.md` §6.1)。 +- **判读工具 v1.1(2026-10-25 大窗判读 F1 起)**: `'.venv/Scripts/python.exe' scripts/v5_log_gap_report.py ` ——自动列 `>10s` 静默段(`--gap-threshold-s` 可调),每段带判定(NO-SOURCE / - PROBE-WINDOW / BOOT-WINDOW / TRUE-STALL-SUSPECT)与贴边证据行;内置 - `task stall` 计数与开机良性豁免(§9 第 8 条口径);退出码 0=双零达标 / - 1=违规 / 2=日志无有效时戳;`--json` 机读、`--report-only` 只出报告、 - `--selftest` 内嵌合成样例自检(无台架可跑)。口径唯一来源 = - `../子系统设计/控制台单写者日志设计说明.md` 文末附录 + §9 第 8 条——改口径 - 先改文档再改脚本(自检断言会拦漂移)。 + PROBE-WINDOW / BOOT-WINDOW / TRUE-STALL-SUSPECT)与贴边证据行。违规口径 = + `task stall` 行 >0 **或** 真停滞嫌疑段 >0;退出码 0=达标 / 1=违规 / + 2=日志无有效时戳;`--json` 机读、`--report-only` 只出报告、`--selftest` + 内嵌合成样例自检(无台架可跑)。**v1.1 变更(判读口径,先文档后脚本)**: + ① `MISS start` 降级为**诊断行**(看门狗 v1.5 按位分账后它只是复合缺席边沿, + 不代表停喂,`../子系统设计/看门狗设计说明.md` §6.1)——报告改出归因诊断: + 开机良性 / 探针窗内 / 窗外运行期 + 窗外每分钟速率 top;v1.0 把运行期 MISS + 计违规属收紧过头(2026-10-25 台架 70min 窗 1262 条经逐窗归因实为风暴复合边沿 + 与传输窗抖动),予以纠正;② `task stall` 行语义 = v1.5 起「同一任务 800ms + 单串真停摆」(行文本不变,`../子系统设计/看门狗设计说明.md` §5.3),每行都是 + 真猎物;开机豁免口径(§9 第 8 条)保留、仅作用于 MISS 诊断分类。口径唯一 + 来源 = `../子系统设计/控制台单写者日志设计说明.md` 文末附录 + §9 第 8 条 + + 看门狗设计说明——改口径先改文档再改脚本(自检断言会拦漂移)。 **执行步骤**:长跑(§4.2 V5)期间周期性打点 + 对照 `CH395F` 无关的 SD2506 侧日志判别卡点。 diff --git a/scripts/check_size_gate.py b/scripts/check_size_gate.py index 4f571fa9..8b1b3ecf 100644 --- a/scripts/check_size_gate.py +++ b/scripts/check_size_gate.py @@ -443,7 +443,11 @@ def main(argv=None): g = ap.add_mutually_exclusive_group() g.add_argument("--stamp", action="store_true", help="用新鲜 .map 盖同码戳(@build 后)") g.add_argument("--local", action="store_true", help="本地/ci_local 自查:工作树须与测量同码") - g.add_argument("--ci", action="store_true", help="CI 模式:同码性 + 已提交数字的 92% 红线") + # ⚠ help 字符串里 % 必须写成 %%(argparse 会对 help 做 %-插值;裸 % + 中文 + # 如"92% 红"在**无参/非法参的 print_help 路径**上才炸:ValueError + # unsupported format character(2026-10-25 道F task-22 顺手修,直调崩溃) + g.add_argument("--ci", action="store_true", + help="CI 模式:同码性 + 已提交数字的 92%% 红线") g.add_argument("--show", action="store_true", help="打印当前指纹与凭证状态") ap.add_argument("--map", default=str(ram_baseline.MAP_DEFAULT), help=".map 路径(--stamp 用)") diff --git a/scripts/v5_log_gap_report.py b/scripts/v5_log_gap_report.py index 7c9ba422..93c1569b 100644 --- a/scripts/v5_log_gap_report.py +++ b/scripts/v5_log_gap_report.py @@ -9,7 +9,9 @@ 「丢弃账 / 故障账」留痕行 = 真停滞嫌疑;零命中 = 无源(系统安静,正常形态)。 - 开机良性豁免:docs/测试说明/实机测试实施指南.md §9 第 8 条——开机屏障期 (reset cause / armed / netTask: started 等锚点后一窗)偶见的单抽样 MISS start - 属固有交错,不计运行期违规;连续 task stall 行才是真停摆。 + 属固有交错;且自 v1.1 起 MISS start 整体降级为**诊断行**(复合缺席边沿,不代表 + 停喂,v2-on-ui 看门狗 v1.5 按位分账语义)——违规判定只看 task stall 行与 + 真停滞嫌疑段,见下。 输入契约(serial-assistant 落盘日志 = 纯文本完整行;两种时钟源自动择一): - 首选采集相对钟:行首 [+NNNms](@test/采集管线相对时戳;相对钟回退 >5s、或出现 @@ -23,13 +25,21 @@ '.venv/Scripts/python.exe' scripts/v5_log_gap_report.py <日志路径> [--gap-threshold-s 10] [--boot-exempt-s 10] [--post-window-s 8] [--jump-cap-s 3600] [--top 20] [--json] [--report-only] [--selftest] - 输出 = 汇总(stall 计数 / 运行期 MISS / >=阈值 静默段清单:每段带判定与贴边 - 证据行);--json 出机读整包;--selftest 跑内嵌合成样例(无需台架日志)。 - 退出码:0=达标(零 stall、零运行期 MISS、零真停滞嫌疑段);1=有违规; - 2=文件不可读/无有效时戳行;3=用法错误;--report-only 分析成功即退 0。 + 输出 = 汇总(stall 计数 / MISS 归因诊断[探针窗内/窗外 + 每分钟速率] / >=阈值 + 静默段清单:每段带判定与贴边证据行);--json 出机读整包;--selftest 跑内嵌 + 合成样例(无需台架日志)。 + 退出码:0=达标(零 stall、零真停滞嫌疑段;MISS 为诊断行不参与判定,v1.1 起); + 1=有违规;2=文件不可读/无有效时戳行;3=用法错误;--report-only 分析成功即退 0。 创建日期:2026-10-25(道F 批 2 支援 task-19:探针窗心跳缺口修复后的长跑复跑 判读工具;台架执行归 lead,本工具只做主机侧判读)。 + +v1.1(2026-10-25,道F task-22 / 大窗判读 F1):违规口径 = stall>0 或真停滞嫌疑段 +(MISS start 降级为诊断行并出归因:探针窗内/窗外 + 每分钟速率;v1.0 曾把运行期 +MISS 计违规,经 2026-10-25 台架 70min 窗逐窗归因属收紧过头,予以纠正)。stall 行 +语义随看门狗 v1.5 按位分账变化(行文本不变):每行 = 同一任务 800ms 单串,见 +docs/子系统设计/看门狗设计说明.md §5.3/§6.1;使用文档 +docs/测试说明/实机测试实施指南.md §7.2。 """ import argparse @@ -174,6 +184,7 @@ def analyze(lines, mode, args): if not points: return None cs = {"stall": 0, "miss": 0, "miss_benign": 0, "miss_runtime": 0, + "miss_probe_win": 0, "miss_outside": 0, "miss_minutes": {}, "evidence": 0} gaps = [] be = args.boot_exempt_s @@ -195,6 +206,14 @@ def analyze(lines, mode, args): cs["miss_benign"] += 1 else: cs["miss_runtime"] += 1 + # v1.1 诊断归因:探针窗内 = 重建风暴的复合边沿(正常形态), + # 窗外 = 值得看一眼的缺席;两者都不参与违规判定 + if any(op - 2.0 <= ts <= cl + 2.0 for op, cl in pwins): + cs["miss_probe_win"] += 1 + else: + cs["miss_outside"] += 1 + mi = int(ts // 60) + cs["miss_minutes"][mi] = cs["miss_minutes"].get(mi, 0) + 1 for i in range(1, len(sp)): t0 = sp[i - 1][0] t1 = sp[i][0] @@ -224,20 +243,35 @@ def analyze(lines, mode, args): "before": _clip(sp[i - 1][1][-1].text), "after": _clip(sp[i][1][0].text), "evidence_lines": evl}) + # v1.1 违规口径(道F task-22,2026-10-25 大窗判读 F1): + # violation := stall>0(看门狗 v1.5 按位分账后每行 = 同一任务连续缺席 >=8 + # 窗的单任务真停摆,docs/子系统设计/看门狗设计说明.md §5.3) + # 或 真停滞嫌疑段 >0。MISS start 自 v1.1 起降级为**诊断行**(复合缺席边沿, + # 不代表停喂,docs/子系统设计/看门狗设计说明.md §6.1):v1.0 把运行期 MISS + # 计入违规属把判读口径收紧过头(2026-10-25 台架 70min 窗 1262 条运行期 MISS + # 经逐窗归因实为风暴复合边沿 + 传输窗抖动,非心跳丢失),改出下方归因诊断。 violations = [] if cs["stall"] > 0: n1 = cs["stall"] - violations.append(f"task stall 行 {n1} 条(判据 = 0;连续 stall 行 = 真停摆)") - if cs["miss_runtime"] > 0: - n2 = cs["miss_runtime"] - violations.append(f"运行期(非开机豁免窗)MISS start {n2} 条") + violations.append(f"task stall 行 {n1} 条(判据 = 0;v1.5 按位分账后 " + f"每行 = 同一任务 800ms 单串真停摆)") nsus = sum(1 for g in gaps if g["verdict"] == "TRUE-STALL-SUSPECT") if nsus: violations.append(f"真停滞嫌疑静默段 {nsus} 处(贴边命中丢弃/故障账)") + topmin = sorted(cs["miss_minutes"].items(), key=lambda kv: -kv[1])[:5] + mdiag = { + "total": cs["miss"], + "boot_benign": cs["miss_benign"], + "runtime": cs["miss_runtime"], + "in_probe_window": cs["miss_probe_win"], + "outside_windows": cs["miss_outside"], + "runtime_top_minutes": [{"minute": mi, "count": nc} for mi, nc in topmin], + "semantics": "v1.1 诊断行:MISS start = 复合缺席边沿,不代表停喂,不参与违规", + } return {"mode": mode, "points": len(points), "sessions": len(sessions), "meta": meta, "counters": cs, "threshold_s": args.gap_threshold_s, - "gaps_over_threshold": gaps, "violations": violations, - "verdict_pass": not violations} + "gaps_over_threshold": gaps, "miss_diag": mdiag, + "violations": violations, "verdict_pass": not violations} def render(rep, args): @@ -257,6 +291,17 @@ def render(rep, args): out.append(f"[计数] task stall={c['stall']} | MISS start 总={c['miss']}" f"(开机良性={c['miss_benign']} / 运行期={c['miss_runtime']})" f"| 账目留痕行={c['evidence']}") + mdg = rep["miss_diag"] + tot, bb = mdg["total"], mdg["boot_benign"] + ipw, outw = mdg["in_probe_window"], mdg["outside_windows"] + out.append(f"[MISS 诊断(非违规)] 总={tot} 开机良性={bb} 探针窗内={ipw}" + f" 窗外运行期={outw}") + if mdg["runtime_top_minutes"]: + parts = [] + for mm in mdg["runtime_top_minutes"]: + parts.append("第%d分x%d" % (mm["minute"], mm["count"])) + out.append(" 窗外 MISS 每分钟速率 top:" + " ".join(parts) + + "(分钟号=所选时钟源时基的第几分钟)") gaps = rep["gaps_over_threshold"] out.append(f"[静默] >= {thr:.0f}s 共 {len(gaps)} 段:") for k, g in enumerate(gaps[: args.top], 1): @@ -274,15 +319,17 @@ def render(rep, args): for v in rep["violations"]: out.append(" - " + v) else: - out.append("结论: PASS(零 stall / 零运行期 MISS / 零真停滞嫌疑段;" - "NO-SOURCE 段属正常空闲,不构成违规)") + out.append("结论: PASS(零 stall / 零真停滞嫌疑段;MISS 为诊断行、" + "NO-SOURCE 为正常空闲,均不构成违规)") return "\n".join(out) def selftest(): fails = [] + nchk = [0] def check(cond, name): + nchk[0] += 1 if not cond: fails.append(name) @@ -331,6 +378,29 @@ def selftest(): check(by.get("PROBE-WINDOW(no-source)") == 1, f"探针窗段=1(得 {by})") check(by.get("NO-SOURCE") == 2, f"无源段=2(得 {by})") check(rep["verdict_pass"] is False, "含 stall 的样例应判 FAIL") + # v1.1 回归钉:零 stall + 运行期 MISS 存在 = PASS(MISS 已降级为诊断行), + # 且归因正确:探针窗内 / 窗外各自计数、每分钟速率分桶 + rel2 = "\n".join([ + "[+100ms] reset cause: POR", + "[+5000ms] [2026-01-01 00:00:05][NET] link up", + "[+40000ms] [2026-01-01 00:00:40][TS] no TIME packet in 30s", + "[+45000ms] MISS start snap=0x000001F6 req=0x000001F7 t=45000", + "[+47000ms] [2026-01-01 00:00:47][NET] ftp session end", + "[+50000ms] [INFO][LIS] sock1 auto-relisten ok (drop)", + "[+70000ms] MISS start snap=0x000001E7 req=0x000001F7 t=70000", + "[+72000ms] [2026-01-01 00:01:12][UI] tick", + ]) + "\n" + lines3, mode3 = parse_stream(rel2) + rep3 = analyze(lines3, mode3, A()) + c3, d3 = rep3["counters"], rep3["miss_diag"] + check(rep3["verdict_pass"] is True, + f"零 stall + 运行期 MISS 应判 PASS(v1.1 降级口径)(得 {rep3['violations']})") + check(d3["runtime"] == 2 and d3["total"] == 2, + f"MISS 运行期=2(得 {d3['runtime']}/{d3['total']})") + check(d3["in_probe_window"] == 1 and d3["outside_windows"] == 1, + f"探针窗内/窗外 = 1/1(得 {d3['in_probe_window']}/{d3['outside_windows']})") + check(sum(x["count"] for x in d3["runtime_top_minutes"]) == 2, + "窗外+窗内 MISS 分钟桶总量=2") abslog = "\n".join([ "[2026-01-01 00:00:00] boot banner", "[----/--/-- --:--:--][APP] reset cause: POR", @@ -350,8 +420,8 @@ def selftest(): for f in fails: print("SELFTEST FAIL: " + f) return 1 - print("SELFTEST PASS 15/15(rel+abs 双钟源、会话切分、四类判定、" - "良性豁免、时钟跳变边界)") + print(f"SELFTEST PASS {nchk[0]}/{nchk[0]}(rel+abs 双钟源、会话切分、四类" + "判定、良性豁免、时钟跳变边界、v1.1 MISS 诊断降级与归因)") return 0