From 4a4cd2934213a610bf19838a6e67777fca0cc3e4 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 2 Oct 2026 15:06:36 +0800 Subject: [PATCH] =?UTF-8?q?S7=EF=BC=9A@test=E9=97=AD=E7=8E=AF=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=87=AA=E6=A3=80=E7=BB=93=E8=AE=BA=E8=81=94=E5=90=88?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=EF=BC=88selftest.log=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E6=88=96=E7=A9=BA=3DPASS=E3=80=81=E6=9C=8964B=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=3DFAIL=EF=BC=9BFTP=E5=8C=BF=E5=90=8D=E6=8A=93?= =?UTF-8?q?=E5=8F=96+=E6=9C=AC=E5=9C=B0=E5=89=AF=E6=9C=AC=E4=B8=A4?= =?UTF-8?q?=E7=A7=8D=E6=9D=A5=E6=BA=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/host/run_boot_test.py | 51 +++++++++++++++++++++++++++++++++++--- test/host/verify_log.py | 46 +++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/test/host/run_boot_test.py b/test/host/run_boot_test.py index dc7a5149..399d33a7 100644 --- a/test/host/run_boot_test.py +++ b/test/host/run_boot_test.py @@ -7,6 +7,9 @@ 3. wait-idle:RX 静默 >= settle-ms,或 max-wait-ms 超时 4. 从日志提取 [RX] 载荷行(剥掉 "[RX] " 前缀) 5. verify_log.py 标记校验 + 6. (仅当 --require-selftest-pass)自检记录联合判定(S7): + --selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log + (lftpd :21 匿名);判定:文件缺失 / 0 字节 = 无故障 → PASS,有 64B 记录 = FAIL 采集在烧录**之前**武装(open + clear 先于烧录),daemon 常驻全量日志兜底, 无采集窗口/首字节等待仪式(与旧 PowerShell 版行为一致)。 @@ -25,7 +28,8 @@ [--required "App init OK;Network Init;net init: OK"] [--forbidden "FAIL;ERROR;HardFault;[ERR]"] [--require-suite-summary "Storage Tests"] - [--out-dir <目录>] [--show-tail-lines 15] + [--require-selftest-pass] [--selftest-log <本地副本>] + [--device-ip 192.168.1.100] [--out-dir <目录>] [--show-tail-lines 15] --port 缺省取 .opencode/serial_config.json 的 portName(单一配置源) @@ -37,6 +41,7 @@ """ import argparse +import ftplib import json import subprocess import sys @@ -79,6 +84,32 @@ def _tool_cmd(py, cmd, *rest, timeout=15): return data, out +def _fetch_selftest_log(device_ip, out_dir, stamp): + """从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。 + + 成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 "" + (按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。 + """ + local = Path(out_dir) / "selftest_log_{}.log".format(stamp) + try: + ftp = ftplib.FTP() + ftp.connect(device_ip, 21, timeout=10) + ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS) + buf = bytearray() + ftp.retrbinary("RETR /log/selftest.log", buf.extend) + try: + ftp.quit() + except OSError: + pass + local.write_bytes(bytes(buf)) + print("SELFTEST_FETCH: {} -> {} ({} bytes)".format(device_ip, local, len(buf))) + return str(local) + except OSError as e: + print("RUN_TEST_WARN: selftest fetch failed: {} " + "(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e)) + return "" + + def main(argv=None): # 控制台输出固定 UTF-8:Windows 代码页只影响显示,不会让脚本崩溃 for stream in (sys.stdout, sys.stderr): @@ -106,6 +137,13 @@ def main(argv=None): ap.add_argument("--require-suite-summary", default="", help="suite names that MUST appear as '=== : N/M PASSED (failed=0) ===' " "with N>0, ';' separated (T-2). Empty = off") + ap.add_argument("--require-selftest-pass", action="store_true", + help="jointly judge /log/selftest.log (S7): missing/empty = PASS, " + "any 64B record = FAIL") + ap.add_argument("--selftest-log", default="", + help="local copy of /log/selftest.log (skips device-side fetch when given)") + ap.add_argument("--device-ip", default="192.168.1.100", + 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) args = ap.parse_args(argv) @@ -211,10 +249,17 @@ 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"))) - # 6. 标记校验 + # 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. 标记校验(含可选的自检记录联合判定) from verify_log import verify rc = verify(str(boot_txt), args.required, args.forbidden, args.show_tail_lines, - suite_summary=args.require_suite_summary) + suite_summary=args.require_suite_summary, + selftest_pass=args.require_selftest_pass, + selftest_log=selftest_local) print("RUN_TEST_RC={}".format(rc)) return rc diff --git a/test/host/verify_log.py b/test/host/verify_log.py index 6a57dea3..269742f1 100644 --- a/test/host/verify_log.py +++ b/test/host/verify_log.py @@ -9,6 +9,7 @@ [--required 'marker1;marker2'] [--forbidden 'FAIL;ERROR;HardFault;assert;[ERR]'] [--require-suite-summary 'Storage Tests;GD5F2GQ5UE Tests'] + [--require-selftest-pass] [--selftest-log ] [--show-tail-lines 40] 退出码(与旧版一致): @@ -36,6 +37,11 @@ 命中即判 PASS,**套件是否跑到汇总行、断言数是否为 0 完全不可见**(套件中途 挂死/复位、或 Phase 宏全关时会被误判为通过)。`--require-suite-summary` 要求日志中出现 `=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。 + - 自检记录联合判定(S7,2026-10 实机窗口):`--require-selftest-pass` 对 /log/selftest.log + 的本地副本(定长 64B 记录,格式见 App/log/sys_log.h)判定。自检仅故障写盘(v1 不记 + 通过项):**文件缺失 / 0 字节 = 无故障 → PASS**;存在任何一条记录 = 有故障 → FAIL + (退出码 1),并逐条打印时间戳 + 描述。抓取失败(设备不可达 / FTP 错误等)由调用方 + (run_boot_test.py)先打 WARN,本地文件按缺失处理 → 按上述规则不判整体 FAIL。 """ import argparse @@ -72,11 +78,15 @@ def _read_lines(path): def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT, - show_tail=40, out=print, suite_summary=""): + show_tail=40, out=print, suite_summary="", + selftest_pass=False, selftest_log=""): """标记校验核心(可被 run_boot_test.py 复用)。返回退出码 0/1/2。 suite_summary: ';' 分隔的套件名列表。非空时,要求日志中每个套件都出现 `=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。 + selftest_pass: True 时对 selftest_log(/log/selftest.log 本地副本)联合判定: + 文件缺失 / 0 字节 = 无故障(PASS);存在任何 64B 记录 = FAIL(S7)。 + selftest_log: 自检记录文件本地路径(空串 = 按文件缺失处理)。 """ p = Path(log_path) if not p.is_file(): @@ -149,6 +159,29 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT, if not ok: bad_summary.append(name) + # 自检记录联合判定(S7):/log/selftest.log 定长 64B 记录(格式见 App/log/sys_log.h), + # 仅故障写盘:文件缺失 / 0 字节 = 无故障 → PASS;存在任何一条记录 → FAIL + selftest_fail = False + if selftest_pass: + sp = Path(selftest_log) if selftest_log else None + if sp is None or not sp.is_file() or sp.stat().st_size == 0: + out("[OK] selftest: no fault record (file missing or empty)") + else: + raw = sp.read_bytes() + n = len(raw) // 64 + if len(raw) % 64: + out("[WARN] selftest log has {} trailing incomplete bytes (ignored)".format(len(raw) % 64)) + if n > 0: + selftest_fail = True + out("[FAIL] selftest: {} fault record(s) in {}".format(n, selftest_log)) + for i in range(n): + rec = raw[i * 64:(i + 1) * 64] + ts = rec[0:19].decode("ascii", errors="replace") + desc = rec[31:59].split(b"\0", 1)[0].decode("utf-8", errors="replace") + out(" #{}: {} {}".format(i, ts, desc)) + else: + out("[OK] selftest: no fault record (0 complete records)") + if show_tail > 0 and lines: out("") out("=== Last {} line(s) of log ===".format(show_tail)) @@ -156,7 +189,7 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT, out(ln) out("") - if missing or hits or bad_summary: + if missing or hits or bad_summary or selftest_fail: out("VERDICT: FAIL") return 1 out("VERDICT: PASS") @@ -182,10 +215,17 @@ def main(argv=None): "with N>0, ';' separated (T-2). Empty = off") ap.add_argument("--show-tail-lines", type=int, default=40, help="show last N log lines (0 = off)") + ap.add_argument("--require-selftest-pass", action="store_true", + help="judge /log/selftest.log local copy (S7): " + "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)") args = ap.parse_args(argv) return verify(args.log_path, args.required, args.forbidden, - args.show_tail_lines, suite_summary=args.require_suite_summary) + args.show_tail_lines, suite_summary=args.require_suite_summary, + selftest_pass=args.require_selftest_pass, + selftest_log=args.selftest_log) if __name__ == "__main__":