From ac9ca33f0f9e38d7f0eb7c208f84eb9acace26b3 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 27 Oct 2026 00:13:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8F=B0=E6=9E=B6=E8=84=9A=E6=9C=AC):=20?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E5=99=A8=E5=8A=A0=E4=B8=A4=E6=9D=A1=E7=A1=AC?= =?UTF-8?q?=E9=97=B8=E2=80=94=E2=80=94=E6=89=A7=E8=A1=8C=E7=B1=BB=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=BB=98=E8=AE=A4=E4=B8=8D=E8=BF=9B=E3=80=81=E6=9C=AA?= =?UTF-8?q?=E7=9F=A5=E5=B1=8F=20ENTER=20=E6=9C=80=E5=A4=9A=E4=B8=80?= =?UTF-8?q?=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench-runner 在给 UI 窗口做准备时报出一条**三段叠加就会真执行破坏性动作**的路径, 而我就是其中一段的作者:496b4dc5 把 e 放进 UNKNOWN 屏的回退键序(当时的动机是真的 ——本机待机态按 s/l 无效,冷启动落在待机态时闭环导航直接开不了工)。叠加面在这里: · 导航器每进一个叶子就发一次 ENTER,且会替「查看密码」自动确认; · 装置维护的 强制复归 / 清除记录 已进菜单树; · 这两页是**两段式确认**——第一次 ENTER 只把该行标成「确认?」,第二次 ENTER 就真的 执行(复归全部保护动作位并写 SOE / 清 SOE·运行·操作记录)。 单个 ENTER 安全,但**同一屏上第二次 ENTER 不安全**;而退避键序是循环消费的,第一次 按 e、第四次又按 e。更要命的是:新字库缺字、白框、状态行变化恰恰最容易让页面被判成 UNKNOWN——那正是我们最想在 UI 窗口里抓的东西。**越是显示异常,越容易被脚本按执行**。 两条硬闸(不改任何固件、只动这一只台架脚本): ① to_root() 内 ENTER **最多按 BACKOFF_MAX_E 次**(默认 1);还需要第二次就当场报错 停机、并把 dump 的帧留在盘上,而不是把第二个 ENTER 投出去。报错文案直接点名执行类 风险与放行方式(改用无 ENTER 的键序 --backoff-keys sl)。 ② 执行类页面 EXEC_PAGE_IDS = clear / reset **默认拒绝下钻**;确有真机需要才显式加 --allow-exec-pages,且运行时打印警告。要看这两页的显示正确性,正路是模拟器黄金参考—— 同一份 UI 源码、零真机副作用。 通用口径一并写进文件头:**导航类自动化脚本默认零 ENTER**,ENTER 只允许出现在明确 指定的、非执行类的用例步骤里。「靠按 ENTER 从未知状态脱身」这个前提本身就与执行类 页面冲突,不该由脚本替你承担后果。 行为验证(假设备,绝不连真机、绝不注键):clear / reset 均被拒且理由含「真执行」; clock 不受该闸影响(走到设备层才失败);持续 UNKNOWN 的循环实际按键序列是 e-s-l 后 在第 2 次 ENTER 前停机,实测 ENTER 计数 = 1 = BACKOFF_MAX_E,报错理由点名执行类风险。 5/5 通过;py_compile 通过;--help 可跑。临时脚本没留在仓库里,__pycache__ 已清。 另两处纯计数更正(与文档道 task-39 同批真值:黄金参考 40 页):本文件 selftest_refs 的 docstring 27 → 40;.pre-commit-config.yaml 里 ui-golden 的显示名 27 → 40(这是 「声明 / 文档 / 实跑」三数同源的最后一处不同码)。 --- .pre-commit-config.yaml | 2 +- scripts/board_ui_nav.py | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4297cc7b..746cc55a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -641,7 +641,7 @@ repos: # 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看, # 连续两次失败才当真(不许直接 --no-verify 绕过)。 - id: ui-golden - name: UI 像素回归(菜单 8 状态 + 内容页 27 页 黄金参考) + name: UI 像素回归(菜单 8 状态 + 内容页 40 页 黄金参考) entry: python scripts/check_ui_golden.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$) diff --git a/scripts/board_ui_nav.py b/scripts/board_ui_nav.py index 7f852386..caf60e13 100644 --- a/scripts/board_ui_nav.py +++ b/scripts/board_ui_nav.py @@ -64,6 +64,20 @@ FAIL_DIR = None # 的 l 条件永不可达(classify 只在 MENU 态填 cols,UNKNOWN 恒 cols=None),等于静默连按 s: # 冷启动恰好落在待机态 = 闭环导航直接开不了工。--backoff-keys 可覆盖(如旧行为 "sl")。 BACKOFF_KEYS = "esl" +# ⚠ 但 e 在**内容页**上不是中性的:装置维护的 强制复归 / 清除记录 都是两段式确认——第一次 +# ENTER 只把该行标成「确认?」,**第二次 ENTER 就真的执行**(复归全部保护动作位并写 SOE / +# 清 SOE·运行·操作记录)。而这两页恰恰最容易被判成 UNKNOWN(新字库缺字、白框、状态行变化 +# ——正是我们想在 UI 窗口里抓的东西)。bench-runner 2026-10-26 报出这条叠险路径后加两条硬闸: +# ① 每次 to_root() 里 ENTER **最多按 BACKOFF_MAX_E 次**,还需要第二次就当场报错停机, +# 而不是把第二个 ENTER 投出去; +# ② 执行类页面默认**不进**(EXEC_PAGE_IDS);要看显示正确性去模拟器黄金参考,那里没有 +# 真机副作用。通用口径:**导航类自动化脚本默认零 ENTER**,ENTER 只允许出现在明确 +# 指定的、非执行类的用例步骤里。 +BACKOFF_MAX_E = 1 +# 执行类页面:进入即可能产生不可逆动作,导航器默认拒绝下钻(--allow-exec-pages 显式放行)。 +EXEC_PAGE_IDS = frozenset(("clear", "reset")) +# 默认 False:执行类页面拒绝下钻。显式 --allow-exec-pages 才放行(放行 = 接受后果)。 +ALLOW_EXEC_PAGES = False W = H = 160 STRIDE = W // 8 @@ -628,6 +642,7 @@ class Navigator(object): 的 ssssssss 回根法,正是深页错位的来源之一。 """ unk = 0 + unk_e = 0 # 本次 to_root() 已投出去的 ENTER 次数(见 BACKOFF_MAX_E) for _ in range(limit): fr, st = self.snap() if st["kind"] == "MENU" and self.n_cols(st) == 1: @@ -656,6 +671,17 @@ class Navigator(object): keys = BACKOFF_KEYS or "esl" ch = keys[unk % len(keys)] unk += 1 + if ch == "e": + # 第二条硬闸:未知屏上的 ENTER 可能就是执行类页面的第二段确认。 + # 宁可停机让人看图,也不拿 ENTER 去试未知状态。 + if unk_e >= BACKOFF_MAX_E: + raise RuntimeError( + "UNKNOWN 退避需要第 " + str(unk_e + 1) + " 次 ENTER,已拒绝(" + + "BACKOFF_MAX_E=" + str(BACKOFF_MAX_E) + ")。继续按 e 有真执行风险:" + + "装置维护的 强制复归/清除记录 是两段式确认,第二次 ENTER 即执行。" + + "请先看上面 dump 的帧判断这是哪一屏;确认不是执行类页面后,才用" + + " --backoff-keys 改用无 ENTER 的键序(如 sl)重来。") + unk_e += 1 why = ("UNKNOWN 屏第 %d 次:按 '%s'(退避键序 %r,可 --backoff-keys 覆盖)" % (unk, ch, keys)) print("[未知屏] %s" % why) @@ -740,6 +766,12 @@ class Navigator(object): path = page_path(self.clf, pid) if not path: raise RuntimeError("菜单树里没有页面 " + pid) + if pid in EXEC_PAGE_IDS and not ALLOW_EXEC_PAGES: + # 硬闸 ②:执行类页面默认不进(理由见文件头 BACKOFF_MAX_E 处那段说明)。 + # 要看这两页的显示正确性,请走模拟器黄金参考——同一份 UI 源码、零真机副作用。 + raise RuntimeError("刻意跳过执行类页面 " + pid + ":进入后第二次 ENTER 即真执行" + "(强制复归 = 复归保护位并写 SOE;清除记录 = 清 SOE/运行/操作记录)。" + "显示正确性请走模拟器黄金参考;确需真机下钻才加 --allow-exec-pages。") last_err = None for attempt in range(0, tries): self.to_root() @@ -831,7 +863,7 @@ def selftest_board_frames(clf): def selftest_refs(clf): - """仓库黄金参考(27 页)逐帧分类自检:每张图的期望值 = 它的文件名。 + """仓库黄金参考(40 页)逐帧分类自检:每张图的期望值 = 它的文件名。 这一组是**语义正确的基线**(模拟器与板端同一份 UI 源码),用来把"分类器错了"与 "留档帧本来就是走歪的"分开——两类失败含义完全不同,混在一起就只能靠猜。 @@ -941,7 +973,11 @@ def _dump_trace(log): def main(argv): - global TRACE, FAIL_DIR, BACKOFF_KEYS + global TRACE, FAIL_DIR, BACKOFF_KEYS, ALLOW_EXEC_PAGES + if "--allow-exec-pages" in argv: + ALLOW_EXEC_PAGES = True + print("[警告] --allow-exec-pages 已开:导航器会下钻执行类页面 " + + str(sorted(EXEC_PAGE_IDS)) + "。第二次 ENTER 即真执行,后果自负。") if "--trace" in argv: TRACE = argv[argv.index("--trace") + 1] if "--dump-fail" in argv: