feat(台架): V5 复跑判读脚本 v5_log_gap_report——静默段自动判类 + stall/MISS 双零计数
道F 批 2 支援 task-19(纯主机侧工具,零固件接触;合窗台架由 lead 执行, 本工具承担判读面)。 ① scripts/v5_log_gap_report.py:输入 serial-assistant 落盘日志,自动列 >=10s 静默段并逐段判类 NO-SOURCE / PROBE-WINDOW / BOOT-WINDOW / TRUE-STALL-SUSPECT——贴边证据 = 控制台两本账(CONSOLE dropped/tx fault) + syslog 三类 drop 的留痕行;口径唯一来源 = docs/子系统设计/ 控制台单写者日志设计说明.md 文末附录(2026-10-25 合并评估)。 task stall 计数 + MISS start 开机良性豁免内建(实机指南 §9 第 8 条)。 双钟源:采集相对钟 [+NNNms] 优先,缺行首钟退化设备绝对钟;RTC 前跳按 会话边界处理不产假静默、回拨钳制;会话切分 = 相对钟回退或 reset cause 锚点。--json 机读 / --report-only 只出报告 / --selftest 免台架自检。 退出码 0=双零达标 / 1=违规 / 2=日志不可用 / 3=用法(沿用 scripts 惯例)。 ② 用法十行落 docs/测试说明/实机测试实施指南.md §7.2(判据互链)。 【验证】py_compile 绿;--selftest 15/15 断言(双钟源、四类判定、良性豁免、 时钟边界、FAIL 判分路径全覆盖,断言即口径防漂移);真实样例三份: B6 PROTECT_MISS = 2 条良性 MISS + 0 静默 + PASS;B9 prod = PASS; probe_stale_gate = 124 时点/2 会话/PASS;合成负样例(stall 行 + 34s/28s 静默段)RC=1 且 28s 段判 PROBE-WINDOW——与 V5 时代 ~28s 静默签名吻合。 【门禁自查】doc-links 首跑拦下一处自引用错号(开机良性豁免条实在 §9 第 8 条 而非 §4.3——§4.3 属看门狗设计说明),指南与脚本头两处一并改齐后绿。 【越界】无——写域 = 看板声明两件(脚本 + 指南 §7.2 互链行)。 【门禁】脚本类无 -unit-test 钩子义务(判定面由 --selftest 承担)。
This commit is contained in:
@@ -278,6 +278,15 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
|
||||
再现静默先按三本账(`dbg_console_dropped/_tx_fault/_tx_timeout`)+ syslog 丢弃
|
||||
账对照定性,再决定是否开卡;销项条件 = N-17 修复版 V5 复跑(e02_probe + FTP
|
||||
≥30 min)运行期 stall 与非开机 `MISS start` 双零。
|
||||
- **判读工具(2026-10-25 起随单交付)**:
|
||||
`'.venv/Scripts/python.exe' scripts/v5_log_gap_report.py <serial-assistant 日志路径>`
|
||||
——自动列 `>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 条——改口径
|
||||
先改文档再改脚本(自检断言会拦漂移)。
|
||||
|
||||
**执行步骤**:长跑(§4.2 V5)期间周期性打点 + 对照 `CH395F` 无关的 SD2506 侧日志判别卡点。
|
||||
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
长跑复跑(V5 类)串口日志静默分析与看门狗计数工具(主机侧,纯标准库,不碰固件)。
|
||||
|
||||
判据口径出处(两处;改口径先改文档、再同步本脚本,自检断言会拦漂移):
|
||||
- 「无源 vs 真停滞」判别:docs/子系统设计/控制台单写者日志设计说明.md 文末附录
|
||||
(N-2 × S8 合并评估,2026-10-25)——静默段贴边(前一行 / 结束后一窗)命中
|
||||
「丢弃账 / 故障账」留痕行 = 真停滞嫌疑;零命中 = 无源(系统安静,正常形态)。
|
||||
- 开机良性豁免:docs/测试说明/实机测试实施指南.md §9 第 8 条——开机屏障期
|
||||
(reset cause / armed / netTask: started 等锚点后一窗)偶见的单抽样 MISS start
|
||||
属固有交错,不计运行期违规;连续 task stall 行才是真停摆。
|
||||
|
||||
输入契约(serial-assistant 落盘日志 = 纯文本完整行;两种时钟源自动择一):
|
||||
- 首选采集相对钟:行首 [+NNNms](@test/采集管线相对时戳;相对钟回退 >5s、或出现
|
||||
reset cause 锚点行 = 新会话边界——烧录/复位/clear 都会造成不连续);
|
||||
- 无相对钟时退化为设备绝对钟:行内 [YYYY-MM-DD HH:MM:SS](墙钟未就绪的
|
||||
[----/--/-- --:--:--] 不算时戳;RTC 同步类向前跳变按会话边界处理、不计静默;
|
||||
回拨钳制不产生假静默);
|
||||
- 无钟行并入前一个活动时点(裸 UART 取证行本身即活动,其时间取相邻活动时点)。
|
||||
|
||||
用法(十行):
|
||||
'.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。
|
||||
|
||||
创建日期:2026-10-25(道F 批 2 支援 task-19:探针窗心跳缺口修复后的长跑复跑
|
||||
判读工具;台架执行归 lead,本工具只做主机侧判读)。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# —— 行形态与固件/采集管线实际留痕逐字核对(改文案先改固件侧再同步这里)——
|
||||
RE_REL = re.compile(r"^\[\+(\d+)ms\]")
|
||||
RE_ABS = re.compile(r"(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})")
|
||||
RE_MISS = re.compile(r"MISS start snap=0x([0-9A-Fa-f]+) req=0x([0-9A-Fa-f]+)"
|
||||
r"(?: t=\d+)?")
|
||||
|
||||
S_STALL = "task stall:" # DBG_ERROR("[WD] task stall: mask=...")
|
||||
S_RESET = "reset cause:" # 会话边界锚(复位/烧录都会带)
|
||||
S_BOOT_ANCHORS = (S_RESET, "armed: timeout", "netTask: started")
|
||||
# 真停滞证据 = 两本「没落地」账的留痕行(附录口径的账目清单)
|
||||
S_EVIDENCE = (
|
||||
"CONSOLE dropped", # 控制台环丢弃(写者恢复后补记,贴边出现)
|
||||
"CONSOLE tx fault", # 僵尸通道自愈(伴随丢块)
|
||||
"queue full, drop", # syslog 队列满
|
||||
"preboot full, drop", # syslog 内核前缓冲满
|
||||
"hold-over drop", # syslog 积压让位丢
|
||||
)
|
||||
S_PROBE_OPEN = "no TIME packet" # time_sync 30s 预算耗尽(探针窗起点)
|
||||
S_PROBE_CLOSE = "auto-relisten ok" # 监听重建完成(探针窗终点)
|
||||
ENV_ERR = 2
|
||||
|
||||
|
||||
class Line:
|
||||
__slots__ = ("no", "rel_ms", "dt", "text")
|
||||
|
||||
def __init__(self, no, rel_ms, dt, text):
|
||||
self.no = no
|
||||
self.rel_ms = rel_ms
|
||||
self.dt = dt
|
||||
self.text = text
|
||||
|
||||
|
||||
def _clip(s, n=110):
|
||||
s = s.strip()
|
||||
return s if len(s) <= n else s[: n - 1] + "~"
|
||||
|
||||
|
||||
def parse_stream(txt):
|
||||
lines = []
|
||||
for i, s in enumerate(txt.splitlines(), 1):
|
||||
if not s.strip():
|
||||
continue
|
||||
m = RE_REL.match(s)
|
||||
rel = int(m.group(1)) if m else None
|
||||
ma = RE_ABS.search(s)
|
||||
dt = None
|
||||
if ma:
|
||||
try:
|
||||
dt = datetime(*[int(x) for x in ma.groups()])
|
||||
except ValueError:
|
||||
dt = None
|
||||
lines.append(Line(i, rel, dt, s))
|
||||
mode = "rel" if any(x.rel_ms is not None for x in lines) else "abs"
|
||||
return lines, mode
|
||||
|
||||
|
||||
def load_lines(path):
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
return parse_stream(raw.decode("utf-8", errors="replace"))
|
||||
|
||||
|
||||
def build_points(lines, mode, jump_cap_s):
|
||||
"""归并成活动时点 [(ts, [Line...])] + 会话切分 + 边界计数(见模块头契约)。"""
|
||||
points = []
|
||||
splits = [0]
|
||||
meta = {"jumps": 0, "clock_backs": 0, "no_ts_head": 0}
|
||||
last_ts = None
|
||||
for ln in lines:
|
||||
boundary = False
|
||||
if mode == "rel":
|
||||
if ln.rel_ms is None:
|
||||
if not points:
|
||||
meta["no_ts_head"] += 1
|
||||
continue
|
||||
ts = points[-1][0]
|
||||
else:
|
||||
ts = ln.rel_ms / 1000.0
|
||||
if last_ts is not None and ts < last_ts - 5.0:
|
||||
boundary = True
|
||||
else:
|
||||
if ln.dt is None:
|
||||
if not points:
|
||||
meta["no_ts_head"] += 1
|
||||
continue
|
||||
ts = points[-1][0]
|
||||
else:
|
||||
ts = ln.dt.timestamp()
|
||||
if last_ts is not None:
|
||||
d = ts - last_ts
|
||||
if d < 0:
|
||||
meta["clock_backs"] += 1
|
||||
ts = last_ts
|
||||
elif d > jump_cap_s:
|
||||
meta["jumps"] += 1
|
||||
boundary = True
|
||||
if S_RESET in ln.text and points and ts != points[-1][0]:
|
||||
boundary = True
|
||||
if boundary:
|
||||
splits.append(len(points))
|
||||
if points and points[-1][0] == ts and not boundary:
|
||||
points[-1][1].append(ln)
|
||||
else:
|
||||
points.append((ts, [ln]))
|
||||
if ln.rel_ms is not None or ln.dt is not None:
|
||||
last_ts = ts
|
||||
ends = splits[1:] + [len(points)]
|
||||
sessions = [(a, b) for a, b in zip(splits, ends) if b > a]
|
||||
return points, sessions, meta
|
||||
|
||||
|
||||
def _markers(txt):
|
||||
return any(e in txt for e in S_EVIDENCE)
|
||||
|
||||
|
||||
def _probe_windows(sp):
|
||||
wins = []
|
||||
open_ts = None
|
||||
for ts, ls in sp:
|
||||
for l in ls:
|
||||
if open_ts is None and S_PROBE_OPEN in l.text:
|
||||
open_ts = ts
|
||||
elif open_ts is not None and S_PROBE_CLOSE in l.text:
|
||||
wins.append((open_ts, ts))
|
||||
open_ts = None
|
||||
if open_ts is not None and sp:
|
||||
wins.append((open_ts, sp[-1][0]))
|
||||
return wins
|
||||
|
||||
|
||||
def analyze(lines, mode, args):
|
||||
points, sessions, meta = build_points(lines, mode, args.jump_cap_s)
|
||||
if not points:
|
||||
return None
|
||||
cs = {"stall": 0, "miss": 0, "miss_benign": 0, "miss_runtime": 0,
|
||||
"evidence": 0}
|
||||
gaps = []
|
||||
be = args.boot_exempt_s
|
||||
pw = args.post_window_s
|
||||
for si, (a, b) in enumerate(sessions):
|
||||
sp = points[a:b]
|
||||
anchors = [ts for ts, ls in sp
|
||||
for l in ls if any(k in l.text for k in S_BOOT_ANCHORS)]
|
||||
pwins = _probe_windows(sp)
|
||||
ev = [(ts, l) for ts, ls in sp for l in ls if _markers(l.text)]
|
||||
cs["evidence"] += len(ev)
|
||||
for ts, ls in sp:
|
||||
for l in ls:
|
||||
if S_STALL in l.text:
|
||||
cs["stall"] += 1
|
||||
if RE_MISS.search(l.text):
|
||||
cs["miss"] += 1
|
||||
if any(0 <= ts - an <= be for an in anchors):
|
||||
cs["miss_benign"] += 1
|
||||
else:
|
||||
cs["miss_runtime"] += 1
|
||||
for i in range(1, len(sp)):
|
||||
t0 = sp[i - 1][0]
|
||||
t1 = sp[i][0]
|
||||
if t1 - t0 < args.gap_threshold_s:
|
||||
continue
|
||||
pre_hit = [l.text for l in sp[i - 1][1] if _markers(l.text)]
|
||||
post_hit = [(t, l) for (t, l) in ev if t1 <= t <= t1 + pw]
|
||||
full_boot = any(t0 >= an - 0.001 and t1 <= an + be
|
||||
for an in anchors)
|
||||
in_probe = any(op - 2.0 <= t0 and t1 <= cl + 2.0
|
||||
for op, cl in pwins)
|
||||
if pre_hit or post_hit:
|
||||
verdict = "TRUE-STALL-SUSPECT"
|
||||
elif full_boot:
|
||||
verdict = "BOOT-WINDOW"
|
||||
elif in_probe:
|
||||
verdict = "PROBE-WINDOW(no-source)"
|
||||
else:
|
||||
verdict = "NO-SOURCE"
|
||||
evl = [_clip(x[1].text, 140) for x in post_hit[:5]]
|
||||
evl += [_clip(x, 140) for x in pre_hit[:2]]
|
||||
gaps.append({"session": si + 1,
|
||||
"start_line": sp[i - 1][1][-1].no,
|
||||
"end_line": sp[i][1][0].no,
|
||||
"t_start": round(t0, 3), "t_end": round(t1, 3),
|
||||
"dur_s": round(t1 - t0, 1), "verdict": verdict,
|
||||
"before": _clip(sp[i - 1][1][-1].text),
|
||||
"after": _clip(sp[i][1][0].text),
|
||||
"evidence_lines": evl})
|
||||
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} 条")
|
||||
nsus = sum(1 for g in gaps if g["verdict"] == "TRUE-STALL-SUSPECT")
|
||||
if nsus:
|
||||
violations.append(f"真停滞嫌疑静默段 {nsus} 处(贴边命中丢弃/故障账)")
|
||||
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}
|
||||
|
||||
|
||||
def render(rep, args):
|
||||
if rep is None:
|
||||
return "[ERR] 日志中无有效时戳行(既无行首相对钟,也无设备绝对钟)"
|
||||
out = ["=== V5 串口日志静默/看门狗计数报告 ==="]
|
||||
md, npt, nss = rep["mode"], rep["points"], rep["sessions"]
|
||||
thr, be, pw = rep["threshold_s"], args.boot_exempt_s, args.post_window_s
|
||||
out.append(f"时钟源={md} 活动时点={npt} 会话={nss} 静默阈值={thr:.0f}s "
|
||||
f"开机豁免={be:.0f}s 贴边回看={pw:.0f}s")
|
||||
m = rep["meta"]
|
||||
if m["jumps"] or m["clock_backs"] or m["no_ts_head"]:
|
||||
out.append(f"边界注记:向前时钟跳变(同步边界、不计静默)={m['jumps']} "
|
||||
f"回拨钳制={m['clock_backs']} 无钟头行={m['no_ts_head']}")
|
||||
c = rep["counters"]
|
||||
out.append("")
|
||||
out.append(f"[计数] task stall={c['stall']} | MISS start 总={c['miss']}"
|
||||
f"(开机良性={c['miss_benign']} / 运行期={c['miss_runtime']})"
|
||||
f"| 账目留痕行={c['evidence']}")
|
||||
gaps = rep["gaps_over_threshold"]
|
||||
out.append(f"[静默] >= {thr:.0f}s 共 {len(gaps)} 段:")
|
||||
for k, g in enumerate(gaps[: args.top], 1):
|
||||
out.append(f" #{k} 会话{g['session']} [{g['t_start']} -> "
|
||||
f"{g['t_end']}]s {g['dur_s']}s {g['verdict']}")
|
||||
out.append(f" 前 L{g['start_line']}: {g['before']}")
|
||||
out.append(f" 后 L{g['end_line']}: {g['after']}")
|
||||
for e in g["evidence_lines"]:
|
||||
out.append(f" 证据: {e}")
|
||||
if len(gaps) > args.top:
|
||||
out.append(f" ...其余 {len(gaps) - args.top} 段(--top 放大)")
|
||||
out.append("")
|
||||
if rep["violations"]:
|
||||
out.append("结论: FAIL")
|
||||
for v in rep["violations"]:
|
||||
out.append(" - " + v)
|
||||
else:
|
||||
out.append("结论: PASS(零 stall / 零运行期 MISS / 零真停滞嫌疑段;"
|
||||
"NO-SOURCE 段属正常空闲,不构成违规)")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def selftest():
|
||||
fails = []
|
||||
|
||||
def check(cond, name):
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
class A:
|
||||
gap_threshold_s = 10.0
|
||||
boot_exempt_s = 10.0
|
||||
post_window_s = 8.0
|
||||
jump_cap_s = 3600.0
|
||||
top = 50
|
||||
|
||||
rel = "\n".join([
|
||||
"[+50ms] reset cause: software (NVIC_SystemReset)",
|
||||
"[+600ms] MISS start snap=0x000001E7 req=0x000001F7 t=710",
|
||||
"[+2800ms] MISS start snap=0x000001E7 req=0x000001F7 t=2900",
|
||||
"[+4000ms] [2026-01-01 00:00:04][APP] storage ready",
|
||||
"[+9000ms] [2026-01-01 00:00:09][NET] link up",
|
||||
"[+25000ms] [2026-01-01 00:00:25][UI] page refresh",
|
||||
"[+26000ms] [2026-01-01 00:00:26][FTP] transfer done",
|
||||
"[+30000ms] [2026-01-01 00:00:30][FTP] stor start",
|
||||
"[+42000ms] [ERR][WD] CONSOLE dropped 3 lines",
|
||||
"[+43000ms] [2026-01-01 00:00:43][UI] page refresh",
|
||||
"[+50000ms] [ERR] [WD] task stall: mask=0x00000001 req=0x00000037 miss=8",
|
||||
"[+55000ms] [2026-01-01 00:00:55][TIME_SYNC] no TIME packet in 30s",
|
||||
"[+56000ms] [2026-01-01 00:00:56][NET] ftp session end",
|
||||
"[+70000ms] [2026-01-01 00:01:10][NET] link check",
|
||||
"[+75000ms] [INFO][LIS] sock1 auto-relisten ok (drop)",
|
||||
"[+100ms] reset cause: POR",
|
||||
"[+3000ms] [2026-01-01 00:02:03][APP] boot",
|
||||
"[+16000ms] [2026-01-01 00:02:16][UI] idle",
|
||||
]) + "\n"
|
||||
lines, mode = parse_stream(rel)
|
||||
check(mode == "rel", "rel 模式识别")
|
||||
rep = analyze(lines, mode, A())
|
||||
check(rep["sessions"] == 2, f"rel 回退+reset 切会话=2(得 {rep['sessions']})")
|
||||
c = rep["counters"]
|
||||
check(c["stall"] == 1, f"stall=1(得 {c['stall']})")
|
||||
check(c["miss"] == 2 and c["miss_benign"] == 2 and c["miss_runtime"] == 0,
|
||||
f"MISS 2/2/0(得 {c['miss']}/{c['miss_benign']}/{c['miss_runtime']})")
|
||||
check(c["evidence"] == 1, f"账目留痕行=1(得 {c['evidence']})")
|
||||
by = {}
|
||||
for x in rep["gaps_over_threshold"]:
|
||||
by[x["verdict"]] = by.get(x["verdict"], 0) + 1
|
||||
check(len(rep["gaps_over_threshold"]) == 4,
|
||||
f"静默段=4(得 {len(rep['gaps_over_threshold'])})")
|
||||
check(by.get("TRUE-STALL-SUSPECT") == 1, f"嫌疑段=1(得 {by})")
|
||||
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")
|
||||
abslog = "\n".join([
|
||||
"[2026-01-01 00:00:00] boot banner",
|
||||
"[----/--/-- --:--:--][APP] reset cause: POR",
|
||||
"[2026-01-01 00:00:05] pre-boot line",
|
||||
"[2026-01-01 00:00:20] after 15s gap",
|
||||
"[2026-10-25 08:00:00] rtc synced",
|
||||
"[2026-10-25 08:00:15] after gap 2",
|
||||
]) + "\n"
|
||||
lines2, mode2 = parse_stream(abslog)
|
||||
check(mode2 == "abs", "abs 模式识别")
|
||||
rep2 = analyze(lines2, mode2, A())
|
||||
check(rep2["meta"]["jumps"] >= 1, "RTC 前跳识别为边界(不计静默)")
|
||||
check(len(rep2["gaps_over_threshold"]) == 2,
|
||||
f"abs 静默段=2(得 {len(rep2['gaps_over_threshold'])})")
|
||||
check(rep2["verdict_pass"] is True, "abs 样例应判 PASS")
|
||||
if fails:
|
||||
for f in fails:
|
||||
print("SELFTEST FAIL: " + f)
|
||||
return 1
|
||||
print("SELFTEST PASS 15/15(rel+abs 双钟源、会话切分、四类判定、"
|
||||
"良性豁免、时钟跳变边界)")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
p = argparse.ArgumentParser(
|
||||
prog="v5_log_gap_report.py",
|
||||
description="V5 串口日志静默分析 + 看门狗计数(判据 = 控制台单写者日志"
|
||||
"设计说明文末附录 + 实机测试实施指南 §9 第 8 条)")
|
||||
p.add_argument("log", nargs="?", help="serial-assistant 落盘日志路径")
|
||||
p.add_argument("--gap-threshold-s", type=float, default=10.0)
|
||||
p.add_argument("--boot-exempt-s", type=float, default=10.0)
|
||||
p.add_argument("--post-window-s", type=float, default=8.0)
|
||||
p.add_argument("--jump-cap-s", type=float, default=3600.0)
|
||||
p.add_argument("--top", type=int, default=20)
|
||||
p.add_argument("--json", action="store_true", help="输出机读 JSON")
|
||||
p.add_argument("--report-only", action="store_true",
|
||||
help="只出报告:分析成功即退 0,不按判据定成败")
|
||||
p.add_argument("--selftest", action="store_true", help="跑内嵌合成样例")
|
||||
a = p.parse_args(argv)
|
||||
if a.selftest:
|
||||
return selftest()
|
||||
if not a.log:
|
||||
p.error("需要日志路径(或使用 --selftest)")
|
||||
try:
|
||||
lines, mode = load_lines(a.log)
|
||||
except OSError as e:
|
||||
print(f"[ERR] 读取失败: {e}")
|
||||
return ENV_ERR
|
||||
rep = analyze(lines, mode, a)
|
||||
if rep is None:
|
||||
print(render(None, a))
|
||||
return ENV_ERR
|
||||
if a.json:
|
||||
print(json.dumps(rep, ensure_ascii=False, indent=1))
|
||||
else:
|
||||
print(render(rep, a))
|
||||
if a.report_only:
|
||||
return 0
|
||||
return 0 if rep["verdict_pass"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user