From 23daa3d9ad6a631d15ee9deb32addbd8bcf480a1 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 22 Oct 2026 22:50:53 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E5=8F=B0=E6=9E=B6):=20=E5=88=A0=E9=9B=B6?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=9A=84=E4=B8=80=E6=AC=A1=E6=80=A7=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E3=80=81=E4=BF=AE=E5=8F=AF=E4=BF=A1=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=99=BB=E5=BD=95=E3=80=81=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E8=90=BD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两件事一起做:把"没人用又已经失效"的一次性探针清掉,把"台架现场脏"这件事的口径写进文档。 **删除(5 个文件,全库零引用)**:`MDK-ARM/ftp_retr_abort_probe.py`(其职能已被 `test/host/ftp_abort_probe.py` 取代,Trap 记录引用的也是后者)与整个 `MDK-ARM/bench_diag/` (4 个诊断脚本:abort_gap / pcref / recover / sink_recycle)。删除判据三条同时成立:① 全库 `git grep` 除自身外零引用;② 自 2026-10-11 设备启用 FTP 鉴权起,脚本里写死的匿名登录必然 530 → 已经跑不动 11 天;③ 都是"跑一次拿现场"的诊断件,价值在结论而结论已沉淀进 Trap 记录。 需要时从 git 历史取回(`test/host/README.md` 与 `MDK-ARM/README.md` 都写明了这一点)。 **刻意保留并修好**:`MDK-ARM/ftp_freeze_probe.py`——它不是一次性件: `CH395F_Trap_Records.md` 拿它当"177 轮 SHA-256 全匹配"的**可信复现配方**、 `BSD_Socket_API_使用指南.md` 把它列进排障判据、`Drivers/BSP/NET/lftpd/README.md` 也指它。 删掉它等于删掉文档指定的真值工具。改为 `--user/--password`(默认出厂 admin/admin,模块常量 `FTP_USER`/`FTP_PASS`,两处登录点都读它),实测一轮:`ROUND 1 size=4096 stor=OK retr=OK hash=MATCH`、`SUMMARY pass=1 fail=0`——修前它连 530 都过不去。 **台架设备清理(不动仓库内容)**:`/` 根目录实测残留 **27 个探针文件 / 262 KB**(`AB000x`、 `A000x`、`R000x`、`SZ2K~64K`、`E12A`、`DIAG_A`、`PP0~PP4`——正是刚删的那批一次性探针留下的)。 逐个 `DELE` 后剩 `LOG` + `CONFIG.INI` + `PROT_SET.BIN` 三条目。**判据:只删显式点名的文件, 绝不 glob**;保留清单必须含 `/config.ini`(网口 + FTP 鉴权)、`/PROT_SET.BIN`(保护定值 blob, `PROT_SET_BLOB_SZ` = 176 B,删了等于定值清零)、`/log` 目录。清完两步验收:`@test` PASS (630 ms,定值巡检与配置加载都在这条链上)+ `ftp_list_fill_test.py` PASS(29 行 / 1851 B / 完成回包 226,26 个新文件全列出)。口径写进 `docs/测试说明/存储维护与容量恢复.md` 新 §4.1 (明确它是"动作 D 清文件",与 A 重建 BBT / B 全片擦除 / C 容量恢复区分——那三个会丢卷内容)。 门禁:`py_compile`(freeze 探针)通过;`check_doc_links.py --all --sections-strict` 146 文档 0 问题(删除的 5 个文件确认无任何文档引用);`check_ids.py --strict` 增量拦截通过(可判定 82.82%);`layer-deps` 通过;4 个改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。 本轮无固件改动,未跑 `@build`(设备侧清理后的 `@test` 已跑,镜像仍是上一笔的构建)。未推送。 --- MDK-ARM/README.md | 5 +- MDK-ARM/bench_diag/bench_diag_abort_gap.py | 323 ------------------ MDK-ARM/bench_diag/bench_diag_pcref.py | 56 --- MDK-ARM/bench_diag/bench_diag_recover.py | 259 -------------- MDK-ARM/bench_diag/bench_diag_sink_recycle.py | 187 ---------- MDK-ARM/ftp_freeze_probe.py | 16 +- MDK-ARM/ftp_retr_abort_probe.py | 144 -------- docs/测试说明/存储维护与容量恢复.md | 18 + test/host/README.md | 8 +- 9 files changed, 39 insertions(+), 977 deletions(-) delete mode 100644 MDK-ARM/bench_diag/bench_diag_abort_gap.py delete mode 100644 MDK-ARM/bench_diag/bench_diag_pcref.py delete mode 100644 MDK-ARM/bench_diag/bench_diag_recover.py delete mode 100644 MDK-ARM/bench_diag/bench_diag_sink_recycle.py delete mode 100644 MDK-ARM/ftp_retr_abort_probe.py diff --git a/MDK-ARM/README.md b/MDK-ARM/README.md index b9af4c6c..0980c758 100644 --- a/MDK-ARM/README.md +++ b/MDK-ARM/README.md @@ -37,8 +37,7 @@ | 脚本 | 作用 | |---|---| | `bench_verdict.py` | 台架判读(把运行结果判为 PASS/FAIL) | -| `ftp_freeze_probe.py` | FTP 上传+回读哈希(真实路径冻结探针) | -| `ftp_retr_abort_probe.py` | FTP RETR 中止探针 | +| `ftp_freeze_probe.py` | FTP 上传+回读哈希(真实路径冻结探针)——**被 Trap 记录当作可信判据引用,保留**;2026-10-22 起用设备账号登录(设备无匿名通道) | | `tcp_reconnect_probe.py` | TCP 快速复连探针(TRAP-NET-17) | | `audit_runs.py` / `audit_tcpclient.py` | 运行记录审计 / TCP 客户端审计 | | `check_crecipe_content.py` | 检查创建配方内容 | @@ -50,7 +49,7 @@ | `RTE/` | Keil Run-Time Environment 配置 | | `DebugConfig/` | 调试会话配置 | | `STM32F407-Demo/` | 构建输出(`.o`/`.axf`/`.map` 等,**产物不提交**) | -| `bench_diag/` | 台架诊断数据 | +| ~~`bench_diag/`~~ | 2026-10-22 删除:4 个一次性台架诊断脚本,全库零引用、且自设备启用 FTP 鉴权起就登录不上(需要时从 git 历史取回) | | `retracted_bench_20260922/` | 已撤回的台架数据(存证) | ## ⚠️ 注意 diff --git a/MDK-ARM/bench_diag/bench_diag_abort_gap.py b/MDK-ARM/bench_diag/bench_diag_abort_gap.py deleted file mode 100644 index 2ced7bf7..00000000 --- a/MDK-ARM/bench_diag/bench_diag_abort_gap.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env python3 -# Reusable bench probe (TRAP-NET-18 boundary scan; lives in MDK-ARM/bench_diag/). -# Boundary probe for TRAP-NET-18 residual: abort an in-flight STOR, wait G ms, then run a -# fresh STOR(4096) and measure BOTH directions INDEPENDENTLY: -# ab_stored / fu_stored = device-side byte count, authoritative, parsed from the -# device log line "UPLOAD '' bytes OK" -# fu_retr = client-side received length + sha256 (fresh session) -# The abort->reopen gap G is swept so the temporal boundary can be read off directly. -# -# Round: -# 1. abort phase : login -> PASV -> STOR /aNNNN.bin (8192B burst) -> abort (RST or FIN) -# 2. gap : sleep G ms (also measured: abort -> next control/data connect) -# 3. follow-up : login -> PASV -> STOR /fNNNN.bin (4096B, graceful data close) -> 226 -# 4. verify : fresh session -> RETR /fNNNN.bin -> length + sha256 -> DELE -# 5. classify : UP_TRUNC (device stored != 4096) / DN_TRUNC (stored ok, client short) -# -# --control drops phase 1 entirely (no preceding abort) = baseline defect rate. -import argparse, hashlib, os, random, re, socket, struct, sys, time - -DEVLOG = os.path.join("tools", "serial_tool", "state", "serial.log") - - -def log(m): - sys.stdout.write(m + "\n") - sys.stdout.flush() - - -class Ctl: - """Minimal raw FTP control channel (no ftplib) so close semantics/timing are exact.""" - - def __init__(self, ip, port, timeout=10.0): - self.s = socket.create_connection((ip, port), timeout=timeout) - self.f = self.s.makefile("rb") - self.banner = self.resp() - - def resp(self): - line = self.f.readline() - if not line: - raise EOFError("control closed by peer") - txt = line.decode("latin1").rstrip("\r\n") - code = txt[:3] - if len(txt) > 3 and txt[3] == "-": - while True: - l2 = self.f.readline() - if not l2: - break - t2 = l2.decode("latin1").rstrip("\r\n") - txt += "\n" + t2 - if t2.startswith(code + " "): - break - return txt - - def cmd(self, line): - self.s.sendall((line + "\r\n").encode()) - return self.resp() - - def login(self): - self.cmd("USER anonymous") - self.cmd("PASS bench@local") - self.cmd("TYPE I") - - def pasv(self, timeout=10.0): - r = self.cmd("PASV") - m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r) - if not m: - raise RuntimeError("PASV failed: %r" % r[:80]) - host = ".".join(m.group(i) for i in (1, 2, 3, 4)) - port = (int(m.group(5)) << 8) | int(m.group(6)) - return socket.create_connection((host, port), timeout=timeout) - - def close(self): - """Close BOTH the makefile handle and the socket: leaving the makefile open keeps - the fd alive, no FIN is sent, and the device holds the stale connection until its - chip TCP timeout (~2.5s of :21 refusals) -- an artefact, not a device defect.""" - try: - self.f.close() - except Exception: - pass - try: - self.s.close() - except OSError: - pass - - def abort(self, fin): - """Tear the control channel down with the same semantic as the trigger.""" - if fin: - try: - self.s.shutdown(socket.SHUT_RDWR) - except OSError: - pass - else: - self.s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) - self.close() - - def quit(self): - try: - self.cmd("QUIT") - except Exception: - pass - self.close() - - -def abort_sock(s, fin, plain=False): - if plain: - # pure FIN: plain close, no shutdown(SHUT_RD) -- so the peer's FIN arriving later is - # NOT answered with an RST (which is what shutdown(SHUT_RDWR) causes on Windows) - try: - s.close() - except OSError: - pass - return - if fin: - try: - s.shutdown(socket.SHUT_RDWR) - except OSError: - pass - else: - s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) - try: - s.close() - except OSError: - pass - - -def connect_ctl(ip, port, trace=False, tries=4, spacing=0.15): - """Connect the control channel, tolerating a short post-abort refusal window. - - Returns (Ctl, seconds_since_first_attempt_or_None).""" - t0 = time.monotonic() - last = None - for k in range(tries): - try: - return Ctl(ip, port), (time.monotonic() - t0) - except Exception as e: - last = e - if trace: - log(" [trace] connect attempt %d failed: %s" % (k + 1, type(e).__name__)) - time.sleep(spacing) - raise last - - -def read_until_eof(s, deadline_s): - got = bytearray() - s.settimeout(deadline_s) - while True: - try: - b = s.recv(65536) - except socket.timeout: - break - except OSError: - break - if not b: - break - got.extend(b) - return bytes(got) - - -def devlog_offset(path): - try: - return os.path.getsize(path) - except OSError: - return 0 - - -def devlog_new(path, off): - try: - with open(path, "rb") as f: - f.seek(off) - return f.read().decode("utf-8", "replace") - except OSError: - return "" - - -def parse_uploads(text): - """name -> stored size, from device log 'UPLOAD '' bytes OK'.""" - out = {} - for m in re.finditer(r"UPLOAD '([^']*)'\s+(\d+) bytes OK", text): - out[os.path.basename(m.group(1))] = int(m.group(2)) - return out - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--ip", default="192.168.1.100") - ap.add_argument("--port", type=int, default=21) - ap.add_argument("--n", type=int, default=8) - ap.add_argument("--gap", type=int, default=0, help="abort -> reopen gap in ms") - ap.add_argument("--size", type=int, default=4096, help="follow-up STOR payload bytes") - ap.add_argument("--abort-send", type=int, default=8192, help="aborted STOR payload bytes") - ap.add_argument("--fin", action="store_true", help="graceful FIN abort instead of RST") - ap.add_argument("--ctl-abort", action="store_true", - help="also RST the control channel (aggressive, kills :21 for ~19s)") - ap.add_argument("--fast-teardown", action="store_true", - help="close the control channel ~30ms after the abort instead of 500ms") - ap.add_argument("--followups", type=int, default=1, - help="consecutive STORs after one abort (is the damage one-shot?)") - ap.add_argument("--plain-close", action="store_true", - help="close the data socket with a plain close (pure FIN, no SHUT_RD)") - ap.add_argument("--control", action="store_true", help="no abort phase (baseline)") - ap.add_argument("--trace", action="store_true", help="per-phase progress + traceback") - ap.add_argument("--devlog", default=DEVLOG) - a = ap.parse_args() - - off = devlog_offset(a.devlog) - payload = bytes(random.getrandbits(8) for _ in range(a.size)) - want = hashlib.sha256(payload).hexdigest() - - res = {"OK": 0, "UP_TRUNC": 0, "DN_TRUNC": 0, "ERR": 0} - log("== gap sweep: gap=%dms abort=%s control=%s n=%d size=%d" - % (a.gap, "FIN" if a.fin else "RST", a.control, a.n, a.size)) - for i in range(1, a.n + 1): - ab_name = "/a%04d.bin" % i - fu_name = "/f%04d.bin" % i - t_abort = None - dt_ctl = dt_data = -1.0 - try: - # ---- 1. abort phase ---- - if not a.control: - c = Ctl(a.ip, a.port) - if a.trace: - log(" [trace] abort-phase control connected: %r" % c.banner[:40]) - c.login() - d = c.pasv() - c.cmd("STOR " + ab_name) - d.sendall(bytes(random.getrandbits(8) for _ in range(a.abort_send))) - abort_sock(d, a.fin, a.plain_close) - t_abort = time.monotonic() - if a.ctl_abort: - # aggressive variant: RST the control channel too (kills the :21 - # listener for ~19s until the chip's own TCP timeout fires -- see - # device log 'sock1 TIMEOUT' / 'sock1 relisten quarantine') - c.abort(True) - else: - # faithful to ftp_abort_probe/real client: data channel aborted, - # control channel closed gracefully. --fast-teardown shrinks the - # settle sleep 500ms -> 30ms so the follow-up can land inside the - # post-abort window (this is the boundary variable under test). - time.sleep(0.03 if a.fast_teardown else 0.5) - try: - c.resp() - except Exception: - pass - c.quit() - # ---- 2. gap ---- - if a.gap > 0: - time.sleep(a.gap / 1000.0) - # ---- 3./4. follow-up STOR(s) + verify ---- - fu_names = [("/f%04d_%d.bin" % (i, k)) if a.followups > 1 else ("/f%04d.bin" % i) - for k in range(a.followups)] - for k, fu_name in enumerate(fu_names): - c2, t_accept = connect_ctl(a.ip, a.port, a.trace) - if a.trace: - log(" [trace] follow-up control connected: %r" % c2.banner[:40]) - dt_ctl = (time.monotonic() - t_abort) if t_abort else -1.0 - c2.login() - d2 = c2.pasv() - dt_data = (time.monotonic() - t_abort) if t_abort else -1.0 - c2.cmd("STOR " + fu_name) - d2.sendall(payload) - try: - d2.shutdown(socket.SHUT_WR) - except OSError: - pass - r = c2.resp() - d2.close() - c2.quit() - # verify: fresh session, RETR - c3, _ = connect_ctl(a.ip, a.port, a.trace) - if a.trace: - log(" [trace] verify control connected: %r" % c3.banner[:40]) - c3.login() - d3 = c3.pasv() - c3.cmd("RETR " + fu_name) - got = read_until_eof(d3, 15.0) - d3.close() - try: - c3.cmd("DELE " + fu_name) - if not a.control: - c3.cmd("DELE " + ab_name) - except Exception: - pass - c3.quit() - retr = len(got) - sha_ok = hashlib.sha256(got).hexdigest() == want - # stored sizes are resolved after the run from the device log; make a - # provisional classification from the client side only - cls = "OK" if (retr == a.size and sha_ok) else "DN_TRUNC" - log(" %2d.%d resp=%-4s retr=%5d/%d sha=%-4s t_accept=%6.0fms dt_data=%6.0fms provisional=%s" - % (i, k + 1, r[:3], retr, a.size, "OK" if sha_ok else "BAD", - (t_accept or 0.0) * 1e3, dt_data * 1e3, cls)) - res[cls] += 1 - except Exception as e: - res["ERR"] += 1 - log(" %2d EXC %s: %s" % (i, type(e).__name__, str(e)[:70])) - if a.trace: - import traceback - traceback.print_exc() - time.sleep(0.3) - - # ---- device-log truth: authoritative stored sizes ---- - up = parse_uploads(devlog_new(a.devlog, off)) - log("-- device log: %d UPLOAD records" % len(up)) - up_trunc = 0 - for i in range(1, a.n + 1): - for k in range(a.followups): - nm = ("f%04d_%d.bin" % (i, k)) if a.followups > 1 else ("f%04d.bin" % i) - sz = up.get(nm) - if sz is None: - log(" %s : (no UPLOAD record)" % nm) - elif sz != a.size: - up_trunc += 1 - log(" %s : stored=%d <== UPLOAD-SIDE TRUNCATION" % (nm, sz)) - ab = [(k, v) for k, v in up.items() if k.startswith("a")] - if ab: - log("-- aborted-phase stored sizes (expected partial): %s" - % ", ".join("%s=%d" % kv for kv in sorted(ab))) - log("SUMMARY gap=%dms abort=%s control=%s n=%d client_ok=%d client_dn_trunc=%d " - "device_up_trunc=%d err=%d" - % (a.gap, "FIN" if a.fin else "RST", a.control, a.n, res["OK"], - res["DN_TRUNC"], up_trunc, res["ERR"])) - - -main() diff --git a/MDK-ARM/bench_diag/bench_diag_pcref.py b/MDK-ARM/bench_diag/bench_diag_pcref.py deleted file mode 100644 index 82433bbb..00000000 --- a/MDK-ARM/bench_diag/bench_diag_pcref.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# PC-side control experiment (no device involved; lives in MDK-ARM/bench_diag/). -# Question: is "sendall(4096) then close()" against a SLOW receiver (small SO_RCVBUF, -# no prompt read) lossless on a normal TCP stack? If yes here but lossy against the -# device, the loss is device-side; if lossy here too, it is generic TCP/client behaviour. -import socket, sys, threading, time - -def log(m): - sys.stdout.write(m + "\n"); sys.stdout.flush() - -def run(rcvbuf, read_delay, send_size=4096): - srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - srv.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, rcvbuf) - srv.bind(("127.0.0.1", 0)) - srv.listen(1) - port = srv.getsockname()[1] - got = bytearray() - res = {} - - def server(): - c, _ = srv.accept() - if read_delay: - time.sleep(read_delay) # receiver does not read promptly - c.settimeout(3.0) - try: - while True: - b = c.recv(65536) - if not b: - break - got.extend(b) - except Exception as e: - res["err"] = repr(e) - res["n"] = len(got) - c.close() - - t = threading.Thread(target=server, daemon=True) - t.start() - time.sleep(0.15) - cli = socket.create_connection(("127.0.0.1", port), timeout=5.0) - cli.sendall(bytes([(i * 13 + 5) & 0xFF for i in range(send_size)])) - cli.close() # close immediately after sendall - t.join(timeout=8) - srv.close() - ok = res.get("n") == send_size - log(" PC-control rcvbuf=%-6d read_delay=%-4s sent=%d received=%s %s %s" - % (rcvbuf, read_delay, send_size, res.get("n"), - "OK" if ok else "LOSS", res.get("err", ""))) - return res.get("n") - -log("=== PC-side reference (loopback): does an immediate close() lose data when the") -log("=== receiver reads late / has a tiny buffer? Expected on a correct stack: no loss.") -run(2048, 0.0) -run(2048, 1.5) -run(65536, 0.0) -run(65536, 1.5) diff --git a/MDK-ARM/bench_diag/bench_diag_recover.py b/MDK-ARM/bench_diag/bench_diag_recover.py deleted file mode 100644 index 808aeefe..00000000 --- a/MDK-ARM/bench_diag/bench_diag_recover.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -# Reusable bench probe (post-abort recovery timeline; lives in MDK-ARM/bench_diag/). -# RECOVERY TIMELINE after an aborted transfer: how long is the device impaired, and is -# the impairment specific to the FTP application (lftpd) or common to the net/chip layer? -# -# Trigger (t=0): -# --target ftp : raw control login -> PASV -> data conn -> STOR -> 8192B burst -> RST the -# DATA channel; control channel teardown is either -# clean = 0.5s settle, read the reply, QUIT (what real clients do) -# fast = QUIT ~20ms after the abort (events coincide) -# --target sink: :7003 connect -> INIT -> LCDMEM -> N keepalive frames (RX in flight, -# unread) -> RST (NO lftpd code involved at all) -# -# Then every --step ms a full short session is attempted until it succeeds --first-ok -# marks the moment the device is usable again-- and every attempt's payload integrity is -# recorded (FTP: NOOP reply; sink: 3209B LCDMEM frame length + CRC, and the echo count of a -# keepalive burst = RX direction integrity). -import argparse, re, socket, struct, sys, time - -TAG_CLIENT = 0xAA -CMD_KEEP = 0 -CMD_INIT = 1 -CMD_LCDMEM = 3 -MEMSIZE = 3200 -LCDMEM_TOTAL = 3209 -ACK_LEN = 5 - - -def log(m): - sys.stdout.write(m + "\n") - sys.stdout.flush() - - -def xor_bytes(b): - c = 0 - for x in b: - c ^= x - return c - - -def req(cmd, data=b""): - body = bytes(data) - return (bytes([TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF]) + body - + bytes([xor_bytes(body)])) - - -def read_exact(s, want, deadline): - buf = bytearray() - while len(buf) < want: - left = deadline - time.monotonic() - if left <= 0: - break - s.settimeout(left) - try: - d = s.recv(want - len(buf)) - except (socket.timeout, OSError): - break - if not d: - break - buf.extend(d) - return bytes(buf) - - -def read_frame(s, deadline): - """Read exactly one RemoDispBus frame -> (got, total, crc_ok).""" - buf = bytearray() - total = None - while True: - want = (4 - len(buf)) if total is None else (total - len(buf)) - if want <= 0: - break - left = deadline - time.monotonic() - if left <= 0: - break - s.settimeout(left) - try: - d = s.recv(min(want, 4096)) - except (socket.timeout, OSError): - break - if not d: - break - buf.extend(d) - if total is None and len(buf) >= 4: - total = 4 + (((buf[2] << 8) | buf[3])) + 1 - if total is None or len(buf) < total: - return len(buf), (total or 0), False - body = bytes(buf[4:total - 1]) - return len(buf), total, (buf[total - 1] == xor_bytes(body)) - - -def rst(s): - try: - s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) - except OSError: - pass - try: - s.close() - except OSError: - pass - - -# ------------------------------- FTP target ------------------------------- - -def ftp_recv(s, timeout=2.0): - s.settimeout(timeout) - try: - return s.recv(4096).decode("latin1", "replace") - except (socket.timeout, OSError): - return "" - - -def ftp_trigger(ip, port, teardown, name, send=8192): - ctl = socket.create_connection((ip, port), timeout=5.0) - ftp_recv(ctl) - ctl.sendall(b"USER anonymous\r\n"); ftp_recv(ctl) - ctl.sendall(b"TYPE I\r\n"); ftp_recv(ctl) - ctl.sendall(b"PASV\r\n") - r = ftp_recv(ctl) - m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r) - if not m: - raise RuntimeError("PASV failed: %r" % r[:60]) - dhost = ".".join(m.group(i) for i in (1, 2, 3, 4)) - dport = (int(m.group(5)) << 8) | int(m.group(6)) - d = socket.create_connection((dhost, dport), timeout=5.0) - ctl.sendall(("STOR " + name + "\r\n").encode()) - ftp_recv(ctl, 1.0) - d.sendall(bytes((i * 7 + 3) & 0xFF for i in range(send))) - t = time.monotonic() - rst(d) # abort mid-upload - if teardown == "fast": - time.sleep(0.02) - ftp_recv(ctl, 0.2) - else: - time.sleep(0.5) - ftp_recv(ctl, 1.0) - try: - ctl.sendall(b"QUIT\r\n") - ftp_recv(ctl, 0.5) - except OSError: - pass - try: - ctl.close() - except OSError: - pass - return t - - -def ftp_probe(ip, port, budget=0.4): - """One short real session: connect -> NOOP reply integrity -> QUIT.""" - t = time.monotonic() - try: - s = socket.create_connection((ip, port), timeout=budget) - except Exception as e: - return False, "REFUSED" if isinstance(e, ConnectionRefusedError) else type(e).__name__ - banner = ftp_recv(s, budget) - if not banner.startswith("220"): - rst(s) - return False, "nobanner" - s.sendall(b"NOOP\r\n") - r = ftp_recv(s, budget) - ok = r.startswith("2") - try: - s.sendall(b"QUIT\r\n"); ftp_recv(s, 0.3) - except OSError: - pass - try: - s.close() - except OSError: - pass - return True, ("ok" if ok else "noop=%r" % r[:16]) - - -# ------------------------------- sink target ------------------------------- - -def sink_trigger(ip, port, burst): - s = socket.create_connection((ip, port), timeout=5.0) - s.sendall(req(CMD_INIT)) - read_frame(s, time.monotonic() + 5.0) - if burst: - s.sendall(req(CMD_KEEP) * burst) - s.sendall(req(CMD_LCDMEM, bytes(4))) - t = time.monotonic() - rst(s) - return t - - -def sink_probe(ip, port, burst=0, budget=2.0): - """One real session: INIT + keepalive burst echo count + LCDMEM frame integrity.""" - try: - s = socket.create_connection((ip, port), timeout=budget) - except Exception as e: - return False, "REFUSED" if isinstance(e, ConnectionRefusedError) else type(e).__name__ - dl = time.monotonic() + budget - s.sendall(req(CMD_INIT)) - ini_got, _, ini_crc = read_frame(s, dl) - ack = -1 - if burst: - s.sendall(req(CMD_KEEP) * burst) - ack = len(read_exact(s, burst * ACK_LEN, time.monotonic() + budget)) - s.sendall(req(CMD_LCDMEM, bytes(4))) - got, total, crc = read_frame(s, time.monotonic() + budget) - try: - s.sendall(b"") # no-op: close gracefully (FIN) so we never re-trigger - s.shutdown(socket.SHUT_RDWR) - except OSError: - pass - try: - s.close() - except OSError: - pass - ok = (ini_got == 13 and got == LCDMEM_TOTAL and crc and (not burst or ack == burst * ACK_LEN)) - return True, ("ok" if ok else "init=%d lcd=%d/%d crc=%s ack=%s" - % (ini_got, got, LCDMEM_TOTAL, "OK" if crc else "BAD", - ack if burst else "-")) - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--target", choices=("ftp", "sink"), default="ftp") - ap.add_argument("--ip", default="192.168.1.100") - ap.add_argument("--port", type=int, default=0, help="0 = 21 for ftp, 7003 for sink") - ap.add_argument("--rounds", type=int, default=3) - ap.add_argument("--span", type=int, default=4000, help="timeline length in ms") - ap.add_argument("--step", type=int, default=100) - ap.add_argument("--teardown", choices=("clean", "fast"), default="clean") - ap.add_argument("--burst", type=int, default=400, help="sink: keepalive frames in flight") - a = ap.parse_args() - port = a.port or (21 if a.target == "ftp" else 7003) - - log("== recovery timeline target=%s port=%d teardown=%s burst=%d step=%dms span=%dms" - % (a.target, port, a.teardown, a.burst, a.step, a.span)) - for r in range(1, a.rounds + 1): - if a.target == "ftp": - t0 = ftp_trigger(a.ip, port, a.teardown, "/r%04d.bin" % r) - else: - t0 = sink_trigger(a.ip, port, a.burst) - seq = [] - first_ok = None - t = 0 - while t < a.span: - time.sleep(a.step / 1000.0) - t = int((time.monotonic() - t0) * 1e3) - if a.target == "ftp": - up, det = ftp_probe(a.ip, port) - else: - up, det = sink_probe(a.ip, port, a.burst) - seq.append("%d:%s" % (t, det)) - if up: - if first_ok is None: - first_ok = t - if det == "ok" and first_ok is not None and t > first_ok + 400: - break - log(" round %d first_usable=%s ms" % (r, first_ok if first_ok is not None else ">%d" % a.span)) - log(" " + " | ".join(seq)) - time.sleep(6.0) # let the device fully settle before the next trigger - log("SUMMARY target=%s teardown=%s rounds=%d done" % (a.target, a.teardown, a.rounds)) - - -main() diff --git a/MDK-ARM/bench_diag/bench_diag_sink_recycle.py b/MDK-ARM/bench_diag/bench_diag_sink_recycle.py deleted file mode 100644 index 27fd22d1..00000000 --- a/MDK-ARM/bench_diag/bench_diag_sink_recycle.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -# Reusable bench probe (non-FTP :7003 close->reopen boundary; lives in MDK-ARM/bench_diag/). -# Non-FTP boundary probe: the same close->reopen recycle, but on the raw RemoDispBus -# service (:7003, Drivers/BSP/UI/ui_net_sink.c), which shares the net/chip layer with FTP -# and none of lftpd's code. If a rapid recycle damages data here too, the defect lives in -# the net/chip layer and FTP only makes it visible. -# -# Session A (the trigger): connect -> CMD_INIT -> CMD_LCDMEM (3209B, CRC) -> [--burst N -# keepalive frames queued unread = data still in flight at abort] -> abort (RST / --fin). -# Gap G ms. -# Session B (the victim): connect -> [--burst N keepalive frames -> count the echoed acks, -# this is the RX direction analogue of "upload truncated to 2048B"] -> CMD_INIT -> -# CMD_LCDMEM -> frame length + CRC (device->client analogue). -# -# Classification per round: -# RX_SHORT : session B echoed fewer acks than frames sent (client->device data cut short) -# TX_SHORT : session B LCDMEM frame short of 3209 (device->client data cut short) -# CORRUPT : full length but CRC mismatch / unexpected frame -# OK : everything intact -import argparse, socket, struct, sys, time - -TAG_CLIENT = 0xAA -TAG_DEVICE = 0xBB -CMD_KEEP = 0 -CMD_INIT = 1 -CMD_LCDMEM = 3 -MEMSIZE = 3200 -LCDMEM_TOTAL = 4 + 4 + MEMSIZE + 1 # 3209 -ACK_LEN = 5 # empty frame reply to a keepalive - - -def log(m): - sys.stdout.write(m + "\n") - sys.stdout.flush() - - -def xor_bytes(b): - c = 0 - for x in b: - c ^= x - return c - - -def req(cmd, data=b""): - body = bytes(data) - return (bytes([TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF]) + body - + bytes([xor_bytes(body)])) - - -def read_frame(s, deadline): - """Read exactly one device frame; returns total/got/crc_ok/short.""" - buf = bytearray() - total = None - while True: - want = (4 - len(buf)) if total is None else (total - len(buf)) - if want <= 0: - break - left = deadline - time.monotonic() - if left <= 0: - return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True} - s.settimeout(left) - try: - d = s.recv(min(want, 4096)) - except socket.timeout: - return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True} - except OSError: - d = b"" - if not d: - return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True} - buf.extend(d) - if total is None and len(buf) >= 4: - total = 4 + (((buf[2] << 8) | buf[3])) + 1 - body = bytes(buf[4:total - 1]) - return {"total": total, "got": len(buf), "crc_ok": (buf[total - 1] == xor_bytes(body)), - "short": (len(buf) != total)} - - -def read_n(s, want, deadline): - """Read up to `want` bytes; stops at deadline / EOF. Returns bytes.""" - buf = bytearray() - while len(buf) < want: - left = deadline - time.monotonic() - if left <= 0: - break - s.settimeout(left) - try: - d = s.recv(min(want - len(buf), 4096)) - except socket.timeout: - break - except OSError: - break - if not d: - break - buf.extend(d) - return bytes(buf) - - -def abort_sock(s, fin): - try: - if fin: - s.shutdown(socket.SHUT_RDWR) - else: - s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) - except OSError: - pass - try: - s.close() - except OSError: - pass - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--ip", default="192.168.1.100") - ap.add_argument("--port", type=int, default=7003) - ap.add_argument("--n", type=int, default=8) - ap.add_argument("--gap", type=int, default=0, help="abort -> reconnect gap in ms") - ap.add_argument("--fin", action="store_true") - ap.add_argument("--burst", type=int, default=400, - help="keepalive frames used to load the RX direction (0 = TCP-only test)") - ap.add_argument("--no-abort", action="store_true", help="no session A (baseline)") - a = ap.parse_args() - - res = {"OK": 0, "RX_SHORT": 0, "TX_SHORT": 0, "CORRUPT": 0, "ERR": 0} - log("== sink recycle: port=%d gap=%dms abort=%s burst=%d n=%d" - % (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"), - a.burst, a.n)) - for i in range(1, a.n + 1): - try: - # ---- session A: trigger ---- - if not a.no_abort: - s1 = socket.create_connection((a.ip, a.port), timeout=10.0) - s1.sendall(req(CMD_INIT)) - read_frame(s1, time.monotonic() + 10.0) - if a.burst: - s1.sendall(req(CMD_KEEP) * a.burst) - s1.sendall(req(CMD_LCDMEM, bytes(4))) - f1 = read_frame(s1, time.monotonic() + 10.0) - abort_sock(s1, a.fin) - t_ab = time.monotonic() - else: - f1 = {"got": -1, "crc_ok": True} - t_ab = time.monotonic() - # ---- gap ---- - if a.gap > 0: - time.sleep(a.gap / 1000.0) - # ---- session B: victim ---- - s2 = socket.create_connection((a.ip, a.port), timeout=10.0) - rx_bytes = 0 - if a.burst: - s2.sendall(req(CMD_KEEP) * a.burst) - got = read_n(s2, a.burst * ACK_LEN, time.monotonic() + 10.0) - rx_bytes = len(got) - s2.sendall(req(CMD_INIT)) - ini = read_frame(s2, time.monotonic() + 10.0) - s2.sendall(req(CMD_LCDMEM, bytes(4))) - f2 = read_frame(s2, time.monotonic() + 10.0) - dt = (time.monotonic() - t_ab) * 1e3 - abort_sock(s2, a.fin) - rx_want = a.burst * ACK_LEN - rx_short = a.burst and rx_bytes < rx_want - tx_short = (f2["got"] != LCDMEM_TOTAL) or (ini["got"] != 13) - if rx_short: - cls = "RX_SHORT" - elif tx_short: - cls = "TX_SHORT" - elif not (f2["crc_ok"] and ini["crc_ok"]): - cls = "CORRUPT" - else: - cls = "OK" - res[cls] += 1 - log(" %2d A lcd=%4d crc=%-4s | B ack=%5d/%5d init=%2dB lcd=%4d/%d crc=%-4s " - "dt=%6.0fms -> %s" - % (i, f1["got"], "OK" if f1["crc_ok"] else "BAD", rx_bytes, rx_want, - ini["got"], f2["got"], LCDMEM_TOTAL, - "OK" if f2["crc_ok"] else "BAD", dt, cls)) - except Exception as e: - res["ERR"] += 1 - log(" %2d EXC %s: %s" % (i, type(e).__name__, str(e)[:70])) - time.sleep(0.3) - log("SUMMARY port=%d gap=%dms abort=%s burst=%d n=%d ok=%d rx_short=%d tx_short=%d " - "corrupt=%d err=%d" - % (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"), a.burst, - a.n, res["OK"], res["RX_SHORT"], res["TX_SHORT"], res["CORRUPT"], res["ERR"])) - - -main() diff --git a/MDK-ARM/ftp_freeze_probe.py b/MDK-ARM/ftp_freeze_probe.py index 6c6895f9..cca29f89 100644 --- a/MDK-ARM/ftp_freeze_probe.py +++ b/MDK-ARM/ftp_freeze_probe.py @@ -22,6 +22,13 @@ import argparse, hashlib, io, os, random, socket, sys, time from ftplib import FTP, error_reply, error_temp, error_perm, all_errors +# Device-side FTP auth: lftpd validates against /config.ini [ftp] (user / pwd_hash). +# This device has NO anonymous channel (USER anonymous -> 530), so the probe must log +# in with the factory account. Override with --user/--password if it was changed. +FTP_USER = "admin" +FTP_PASS = "admin" + + def log(msg): sys.stdout.write(msg + "\n") sys.stdout.flush() @@ -31,7 +38,7 @@ def liveness(ip, port, timeout=3.0): try: f = FTP() f.connect(ip, port, timeout=timeout) - f.login("anonymous", "probe@local") + f.login(FTP_USER, FTP_PASS) f.voidcmd("NOOP") f.quit() return "alive" @@ -39,6 +46,7 @@ def liveness(ip, port, timeout=3.0): return "dead" def main(): + global FTP_USER, FTP_PASS # 两处登录点都读模块常量,命令行覆盖后即刻生效 ap = argparse.ArgumentParser() ap.add_argument("--ip", default="192.168.1.100") ap.add_argument("--port", type=int, default=21) @@ -47,7 +55,11 @@ def main(): ap.add_argument("--gap", type=float, default=3.0) ap.add_argument("--keep", action="store_true", help="do not DELE the uploaded file") ap.add_argument("--seed", type=int, default=604) + ap.add_argument("--user", default="admin", + help="FTP user (device auth; this device has no anonymous channel)") + ap.add_argument("--password", default="admin", help="FTP password (factory default)") a = ap.parse_args() + FTP_USER, FTP_PASS = a.user, a.password rng = random.Random(a.seed) npass = nfail = 0 @@ -63,7 +75,7 @@ def main(): ctrl = "na" try: f.connect(a.ip, a.port, timeout=15.0) - f.login("anonymous", "probe@local") + f.login(FTP_USER, FTP_PASS) t0 = time.time() try: f.storbinary("STOR " + name, io.BytesIO(data), blocksize=8192) diff --git a/MDK-ARM/ftp_retr_abort_probe.py b/MDK-ARM/ftp_retr_abort_probe.py deleted file mode 100644 index 42884c42..00000000 --- a/MDK-ARM/ftp_retr_abort_probe.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# RETR-side (device-as-sender) abort probe — validates the 2026-09-24 send -# fast-fail fix (commit 13fe184): client starts a RETR, reads PART of the data, -# then aborts the data connection (RST or FIN) while the device is still -# sending. The device's in-flight net_send must now fail immediately with -# CONNRESET (device log: "short send ... (errno=-8)") instead of stalling up -# to NET_SEND_TIMEOUT_MS = 1s and reporting TIMEDOUT (errno=-9, old behavior). -# -# The STOR-side abort probe (ftp_abort_probe.py) cannot validate this path: -# during STOR the device is the receiver (no in-flight device send exists). -# -# Usage: ftp_retr_abort_probe.py [--ip ..] [--port 21] [--rounds 10] -# [--file-size 262144] [--read 16384] [--gap 2] [--graceful] -# Output: TS / ABORT read= ctrl= alive= followup= -# SUMMARY rounds= pass= fail= -# Exit 0 all follow-ups pass, 1 otherwise. -import argparse, hashlib, io, random, re, socket, struct, sys, time -from ftplib import FTP, all_errors - -def log(m): - sys.stdout.write(m + "\n"); sys.stdout.flush() - -def rd(sock): - try: - return sock.recv(4096).decode("latin1").strip() - except Exception: - return "" - -def raw_login(ip, port, timeout=10.0): - s = socket.create_connection((ip, port), timeout=timeout) - rd(s) # 220 banner - s.sendall(b"USER anonymous\r\n"); rd(s) - s.sendall(b"TYPE I\r\n"); rd(s) - return s - -def pasv_data_socket(ctl): - ctl.sendall(b"PASV\r\n") - r = rd(ctl) - m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r) - if not m: - raise RuntimeError("PASV failed: " + r) - host = ".".join(m.group(i) for i in (1, 2, 3, 4)) - port = (int(m.group(5)) << 8) | int(m.group(6)) - return socket.create_connection((host, port), timeout=10.0) - -def followup_ok(ip, port, size=4096, tries=3): - """normal upload + download + sha256 -> True/False. - Retries the initial connect a few times: the post-abort relisten window - (close->reopen chip settle, see bench 20260924 finding F1) may refuse a - SYN for up to ~5s; a refused follow-up connect is an F1-class failure, - not a send fast-fail failure — retrying decouples the two.""" - data = bytes(random.getrandbits(8) for _ in range(size)) - want = hashlib.sha256(data).hexdigest() - for t in range(tries): - try: - f = FTP(); f.connect(ip, port, timeout=15.0); f.login("anonymous", "x") - f.storbinary("STOR /rb_ok.bin", io.BytesIO(data)) - buf = bytearray(); f.retrbinary("RETR /rb_ok.bin", buf.extend) - try: - f.delete("/rb_ok.bin") - except all_errors: - pass - f.quit() - return hashlib.sha256(bytes(buf)).hexdigest() == want - except all_errors: - time.sleep(1.0) - return False - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--ip", default="192.168.1.100") - ap.add_argument("--port", type=int, default=21) - ap.add_argument("--rounds", type=int, default=10) - ap.add_argument("--file-size", type=int, default=262144, - help="size of the file uploaded before the RETR aborts") - ap.add_argument("--read", type=int, default=16384, - help="bytes the client reads before aborting") - ap.add_argument("--gap", type=float, default=2.0) - ap.add_argument("--graceful", action="store_true", - help="close the data connection with FIN instead of RST") - a = ap.parse_args() - - # Upload the RETR source file once (8.3 name, FatFs constraint). - file_data = bytes(random.getrandbits(8) for _ in range(a.file_size)) - f = FTP(); f.connect(a.ip, a.port, timeout=15.0); f.login("anonymous", "setup") - f.storbinary("STOR /raba.bin", io.BytesIO(file_data)) - f.quit() - - npass = nfail = 0 - try: - for n in range(1, a.rounds + 1): - log("TS %d" % int(time.time())) - ctl_reply = "NA" - alive = "no" - got = 0 - try: - ctl = raw_login(a.ip, a.port) - ds = pasv_data_socket(ctl) - ctl.sendall(b"RETR /raba.bin\r\n") - ctl_reply = rd(ctl) # 150 - # read PART of the transfer, then abort while device is sending - try: - got = len(ds.recv(a.read)) - except Exception: - pass - if a.graceful: - ds.shutdown(socket.SHUT_RDWR) # graceful FIN - else: - ds.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, - struct.pack("ii", 1, 0)) # RST - ds.close() - time.sleep(0.5) - rd(ctl) # 450/226/221 reply - alive = "yes" - try: - ctl.sendall(b"NOOP\r\n"); rd(ctl) - except Exception: - alive = "no" - except Exception as e: - ctl_reply = "EXC:%s" % type(e).__name__ - finally: - try: - ctl.close() - except Exception: - pass - ok = followup_ok(a.ip, a.port) - npass += 1 if ok else 0 - nfail += 0 if ok else 1 - log("ABORT %d read=%d ctrl=%s alive=%s followup=%s" - % (n, got, ctl_reply, alive, "OK" if ok else "FAIL")) - time.sleep(a.gap) - f = FTP(); f.connect(a.ip, a.port, timeout=15.0); f.login("anonymous", "cleanup") - try: - f.delete("/raba.bin") - except all_errors: - pass - f.quit() - except Exception as e: - log("FATAL: %s" % e) - - log("SUMMARY rounds=%d pass=%d fail=%d" % (a.rounds, npass, nfail)) - sys.exit(0 if nfail == 0 else 1) - -main() diff --git a/docs/测试说明/存储维护与容量恢复.md b/docs/测试说明/存储维护与容量恢复.md index 66b1b7f6..177b6d63 100644 --- a/docs/测试说明/存储维护与容量恢复.md +++ b/docs/测试说明/存储维护与容量恢复.md @@ -64,8 +64,26 @@ - **别为了"省一次擦除"而手工擦单块**:容量是否恢复取决于 checkpoint 是否全部失效,只擦单块无法保证(`resume` 会锚定到更早的 checkpoint)。 - 恢复后建议跑一次只读套件确认(GD5F 只读 16/16 或存储只读 3/3)。 +### 4.1 根目录残留文件清理(2026-10-22 实测,动作 C 之外的日常维护) + +台架 `/` 根目录常年堆着 FTP 探针留下的文件——**不占坏块、不影响挂载,但会把"目录列表类" +问题的现场搞混**,也让容量读数失真。本次实测残留 **27 个文件 / 262 KB**(`AB000x`、`A000x`、 +`R000x`、`SZ2K~64K`、`E12A`、`DIAG_A`、`PP0~PP4`,全是历史一次性探针留下的,那些探针脚本本身 +已于同日删除,见 `MDK-ARM/README.md`)。 + +- **判据:只删显式点名的文件,绝不 glob**。清理脚本里写死 27 个文件名逐个 `DELE`;保留清单 + 必须含 **`/config.ini`**(网口与 FTP 鉴权配置)、**`/PROT_SET.BIN`**(保护定值 blob, + `PROT_SET_BLOB_SZ` = 176 B,删了等于把定值清零)、**`/log`** 目录(运行与自检记录)。 +- 三步验收:① 清完 `LIST` 只剩 `LOG` + `CONFIG.INI` + `PROT_SET.BIN`;② `@test` PASS(定值巡检 + 与配置加载都在这条链上);③ 跑 `test/host/ftp_list_fill_test.py` 确认 FTP 面正常。 +- 这属于**动作 D(清文件)**,与本文 §1 的 A(重建 BBT + 格式化)/ B(全片擦除)/ C(容量恢复) + 都不同:A/B 会丢卷内容,D 不动文件系统结构。 + ## 5. 变更记录 | 日期 | 变更 | |---|---| | 2026-09-21 | 新建:A/B/C 三个维护动作;容量恢复原理(dhara checkpoint 坏块计数不清零)与全片擦除恢复路径(实测 95200 → 96064) | +| 2026-10-22 | 加 §4.1「根目录残留文件清理」(动作 D,与 A/B/C 区分):台架实测 27 个探针残留文件 / + 262 KB 已清;判据 = 只删显式点名、保留 `/config.ini`、`/PROT_SET.BIN`、`/log`;清后 `@test` + PASS + LIST 完整性回归 PASS | diff --git a/test/host/README.md b/test/host/README.md index 2c0c45a3..a502c9fb 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -139,9 +139,11 @@ - 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免); 但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 16 个) - 台架探针族分工(2026-10-13 起):**自愈/Trap 常备回归腿在本目录**(`relisten_stress` / - `ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余 - 历史一次性探针(`ftp_freeze_probe` / `tcp_reconnect_probe` / `ftp_retr_abort_probe` / - `bench_verdict`) + `ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余被文档**当作 + 可信判据引用过**的三件(`ftp_freeze_probe` / `tcp_reconnect_probe` / `bench_verdict`)—— + 2026-10-22 清掉了一批既无人引用、又因设备侧启用鉴权而再也登录不上的历史一次性探针 + (`MDK-ARM/ftp_retr_abort_probe.py` 与整个 `MDK-ARM/bench_diag/`(4 个诊断脚本)),需要时从 + git 历史取回;`ftp_freeze_probe.py` 因被 Trap 记录当复现配方而保留,已改用设备账号登录。 - 全部主机侧单测(16 个模块目录,含本目录新增的 `nand_port/`)都需要本机装有 MSVC (VS2019/2022/2026 的 C++ 工具链);无则脚本报环境错误(退出码 2)