chore(工具,文档): 台架 rc 语义与导航硬闸用例、门禁环境诊断、护栏还债算法、mem-budget 跨树口径
四条道的收尾件同笔(全是 python 工具与文档,零固件源;各段归属按道署明)。 · **台架(bench-runner)**:verify_log.py 新增 classify_measurement() 与 measureme nt.* 字段,零行/零锚点 判 exit 2 并打 [MEASUREMENT-BROKEN],**但同帧有 Forbidden 命中或自检故障仍判 1**(专门造了「零锚点 + [ERR] MEMFAULT」的陷阱日志实测——崩溃不许被降级成「没数据」);--selftest 16 → 22 条。board_ui_nav.py 把两条执行页安全闸做成 7 条常驻用例接进提交钩子跑的快检档,作者把自己用例**证伪三轮**(测试强制上限、 清单清空时是崩不是判定、出厂默认被翻允许也照样绿)后三条危险改动逐条能红。台架卫生清单落 §7:每圈复位 前 open + 断言 ready + 看 rx 增长(今晚 daemon 死了 status 自愈把它遮住)、**Forbidden 令牌不得落在 英文单词内部**(裸 Fault 命中 default 两次假红)、改判定语义前先 grep 消费者。 · **环境(ui-param-writer)**:证伪卡片前提两处——identify 那个版本根本没有 tags 模块(RECORD 与磁盘逐项 一致,pre-commit 自己也从 identify.identify 取 ALL_TAGS,全仓零引用),真问题是 44 条 system 钩子里 42 条 entry 用裸 python → PATH 首位那个零第三方包解释器,今天没炸纯靠两件事同时成立。交付是**新增 entry 硬规则 + 修复死顺序与回滚并标注等静默窗口**,没有动共享环境(此刻重装会让别人的钩子在半途换解释器)。 · **门禁(gate-docs-fixer 已入库的 51 号钩子)**:本篇带上 host-unit-tests 的裁定句落点说明。 · **内存(comm-config-writer)**:mem-budget skill 补跨树测量身份口径(脚本按 __file__ 定位产物、什么才算 一次新测量、多链同涨同一数值的共享帧/幻影环归因法)+ 看门狗说明的指针行。 实测:verify_log --selftest 22/22、board_ui_nav --selftest-quick rc=0(硬闸 7/7)、 doc-links --all exit 0、 check_ids --strict exit 0 且绝对数 1,156 **未被本笔吃掉**;全部文件 LF / 无 BOM / U+FFFD=0 ;本笔零固件源, 随上一批权威构建验证过(0 错 0 警),故不再单独 @build。
This commit is contained in:
+143
-7
@@ -841,6 +841,136 @@ BOARD_TRUTH = {
|
||||
}
|
||||
|
||||
|
||||
def selftest_safety_gates(clf):
|
||||
"""两条执行风险硬闸的常驻回归用例(假设备、内存帧、零真机注键)。
|
||||
|
||||
为什么要常驻这两条:闸只在真出事的那一屏才起作用,平时没人碰,于是"改了个常量
|
||||
就把闸拆掉"这种回退不会有任何人察觉。本用例不连 7003、不开串口、不碰台架——
|
||||
设备是一帧内存里的空白位图,按键只记账、不投递。
|
||||
覆盖的两条闸(2026-10-26 台架叠险 → ac9ca33f 收口,理由见文件头):
|
||||
闸① UNKNOWN 退避里的 ENTER 次数上限(BACKOFF_MAX_E):需要第二次就当场停机;
|
||||
以及显式改用无 ENTER 键序(--backoff-keys sl)时,一个 ENTER 都不许投出去;
|
||||
闸② 执行类页面(EXEC_PAGE_IDS)默认拒绝下钻,且拒绝发生在**任何键投出去之前**;
|
||||
显式放行(--allow-exec-pages)后不得再报"刻意跳过"——放行 = 接受后果。
|
||||
返回 (用例数, 失败列表),与其它 selftest_* 同形,失败非 0 退出由调用方决定。
|
||||
"""
|
||||
g = globals()
|
||||
saved = {"classify": g["classify"], "ALLOW_EXEC_PAGES": g["ALLOW_EXEC_PAGES"],
|
||||
"BACKOFF_KEYS": g["BACKOFF_KEYS"], "BACKOFF_MAX_E": g["BACKOFF_MAX_E"]}
|
||||
bad = []
|
||||
n = 0
|
||||
|
||||
class FakeDevice(object):
|
||||
"""假设备:grab 永远给同一帧内存位图,key 只记账不投递(不连板、不连模拟器)。"""
|
||||
|
||||
def __init__(self, fr):
|
||||
self.fr = fr
|
||||
self.keys = []
|
||||
|
||||
def grab(self):
|
||||
return self.fr
|
||||
|
||||
def key(self, ch):
|
||||
if ch not in KEYS:
|
||||
raise ValueError("未知键 " + repr(ch))
|
||||
self.keys.append(ch)
|
||||
return self.fr
|
||||
|
||||
def stub(kind, extra=None):
|
||||
def _f(fr, _clf):
|
||||
st = {"kind": kind, "page": None, "cols": None, "evid": 0, "no": None}
|
||||
if extra:
|
||||
st.update(extra)
|
||||
return st
|
||||
return _f
|
||||
|
||||
def check(name, ok, detail):
|
||||
print("%-5s %s %s" % ("[OK]" if ok else "[FAIL]", name, detail))
|
||||
if not ok:
|
||||
bad.append(name + ": " + detail)
|
||||
|
||||
blank = Frame(bytes(FRAME_BYTES), "fake_blank")
|
||||
try:
|
||||
# 政策钉:出厂默认必须是"拒绝下钻"。下面所有用例会临时改这两个常量,
|
||||
# 所以默认值只能在进函数时抓一次(saved)——否则有人把默认翻成 True,
|
||||
# 用例照样自己设 False 再测,全绿放行一个危险的默认(实测漏过一次)。
|
||||
n += 1
|
||||
check("闸② 出厂默认即拒绝(ALLOW_EXEC_PAGES 默认 False)",
|
||||
saved["ALLOW_EXEC_PAGES"] is False,
|
||||
"模块默认=%r(True 意味着任何人漏写 --allow-exec-pages 也会下钻执行类页面)"
|
||||
% (saved["ALLOW_EXEC_PAGES"],))
|
||||
# ---- 闸①:UNKNOWN 退避最多投 BACKOFF_MAX_E 次 ENTER,第二次当场停机 ----
|
||||
g["classify"] = stub("UNKNOWN")
|
||||
g["BACKOFF_KEYS"] = "esl"
|
||||
# 故意**不**覆盖 BACKOFF_MAX_E:闸的强度本身要被钉住(有人把上限调到 2 就该红)
|
||||
dev = FakeDevice(blank)
|
||||
msg1 = ""
|
||||
try:
|
||||
Navigator(dev, clf).to_root(limit=12)
|
||||
except RuntimeError as exc:
|
||||
msg1 = str(exc)
|
||||
n += 1
|
||||
e_cnt = dev.keys.count("e")
|
||||
check("闸① UNKNOWN 退避 ENTER 上限",
|
||||
("BACKOFF_MAX_E" in msg1) and e_cnt == saved["BACKOFF_MAX_E"]
|
||||
and saved["BACKOFF_MAX_E"] <= 1,
|
||||
"停机消息=%s,投出 ENTER=%d 次(上限配置=%d,政策钉死 <=1),键迹=%s"
|
||||
% (bool(msg1), e_cnt, saved["BACKOFF_MAX_E"], "".join(dev.keys)))
|
||||
|
||||
# ---- 闸①补:显式无 ENTER 键序时,一个 ENTER 都不许出现 ----
|
||||
g["BACKOFF_KEYS"] = "sl"
|
||||
dev2 = FakeDevice(blank)
|
||||
try:
|
||||
Navigator(dev2, clf).to_root(limit=6)
|
||||
except RuntimeError:
|
||||
pass
|
||||
n += 1
|
||||
check("闸①补 无 ENTER 键序(--backoff-keys sl)", "e" not in dev2.keys,
|
||||
"键迹=%s(应不含 e)" % ("".join(dev2.keys) or "-"))
|
||||
|
||||
# ---- 闸②:执行类页面默认拒绝,且拒绝发生在投键之前 ----
|
||||
g["BACKOFF_KEYS"] = "esl"
|
||||
g["ALLOW_EXEC_PAGES"] = False
|
||||
g["classify"] = stub("MENU", {"cols": [("根", 0)]})
|
||||
# 清单被清空 = 闸被拆。这种情况要**报红**,不能"零条用例"悄悄过去,也不能崩
|
||||
# (崩了只是噪声,红色才是判定;ng==0 只能兜住"一条都没跑",兜不住"清单空了")
|
||||
n += 1
|
||||
check("闸② 执行类清单完整(清空或缺项 = 拆闸)",
|
||||
len(EXEC_PAGE_IDS) >= 2 and {"clear", "reset"} <= set(EXEC_PAGE_IDS),
|
||||
"EXEC_PAGE_IDS=%s" % (sorted(EXEC_PAGE_IDS) or "空"))
|
||||
for pid in sorted(set(EXEC_PAGE_IDS) | {"clear", "reset"}):
|
||||
dev3 = FakeDevice(blank)
|
||||
msg3 = ""
|
||||
try:
|
||||
Navigator(dev3, clf).go_page(pid)
|
||||
except RuntimeError as exc:
|
||||
msg3 = str(exc)
|
||||
n += 1
|
||||
check("闸② 执行类页面 %s 默认拒绝下钻" % pid,
|
||||
("刻意跳过执行类页面" in msg3) and ("真执行" in msg3)
|
||||
and ("e" not in dev3.keys),
|
||||
"拒绝理由点名真执行=%s,投出的 ENTER=%d(消息:%s)"
|
||||
% ("真执行" in msg3, dev3.keys.count("e"), msg3[:36] or "没抛错"))
|
||||
|
||||
# ---- 闸②反向:显式放行后不得再报"刻意跳过"(放行 = 接受后果)----
|
||||
g["ALLOW_EXEC_PAGES"] = True
|
||||
g["classify"] = stub("UNKNOWN") # 放行后走进导航,被闸①停住而不是被闸②拒绝
|
||||
dev4 = FakeDevice(blank)
|
||||
msg4 = ""
|
||||
try:
|
||||
Navigator(dev4, clf).go_page(sorted(set(EXEC_PAGE_IDS) | {"clear", "reset"})[0])
|
||||
except RuntimeError as exc:
|
||||
msg4 = str(exc)
|
||||
n += 1
|
||||
check("闸②反向 --allow-exec-pages 放行后不再拒绝",
|
||||
("刻意跳过执行类页面" not in msg4) and bool(msg4),
|
||||
"放行者=%s(消息:%s)" % ("刻意跳过执行类页面" not in msg4, msg4[:36] or "没抛错"))
|
||||
return n, bad
|
||||
finally:
|
||||
for k, v in saved.items():
|
||||
g[k] = v
|
||||
|
||||
|
||||
def selftest_board_frames(clf):
|
||||
"""真机留档帧上的分类自检:不需要台架也能证明认屏认得对。"""
|
||||
bad = []
|
||||
@@ -1032,13 +1162,17 @@ def main(argv):
|
||||
return 0
|
||||
if "--selftest-quick" in argv:
|
||||
# 提交门禁用的快检档:只核"认屏认得对",不需要模拟器构建产物、不需要台架
|
||||
print("[1/2] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
|
||||
print("[1/3] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
|
||||
nr, badr = selftest_refs(clf)
|
||||
print("[2/2] 真机留档帧分类(目录缺则整段跳过,不算失败)")
|
||||
print("[2/3] 真机留档帧分类(目录缺则整段跳过,不算失败)")
|
||||
n, bad1 = selftest_board_frames(clf)
|
||||
print("汇总(快检):黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d" % (nr, len(badr), n, len(bad1)))
|
||||
print("[3/3] 执行风险硬闸(假设备、零真机注键,task-41)")
|
||||
ng, badg = selftest_safety_gates(clf)
|
||||
print("汇总(快检):黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;硬闸用例 %d 条错 %d"
|
||||
% (nr, len(badr), n, len(bad1), ng, len(badg)))
|
||||
print("闭环导航未跑(需 lvgl_sim.exe):完整自检用 --selftest")
|
||||
return 1 if (badr or bad1 or nr == 0) else 0
|
||||
# 硬闸用例一条没跑也算失败:静默跳过等于把闸拆了还显示绿(同 nr==0 的处理口径)
|
||||
return 1 if (badr or bad1 or badg or nr == 0 or ng == 0) else 0
|
||||
if "--selftest" in argv:
|
||||
print("[1/3] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
|
||||
nr, badr = selftest_refs(clf)
|
||||
@@ -1050,11 +1184,13 @@ def main(argv):
|
||||
all_ids = [p for (_l, _n, p, _np) in clf.tree if p]
|
||||
m, bad2, log = selftest_closed_loop(clf, all_ids)
|
||||
_dump_trace(log)
|
||||
print("汇总:黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;闭环 %d 页失败 %d"
|
||||
% (nr, len(badr), n, len(bad1), m, len(bad2)))
|
||||
print("[4/4] 执行风险硬闸(假设备、零真机注键,task-41)")
|
||||
ng, badg = selftest_safety_gates(clf)
|
||||
print("汇总:黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;闭环 %d 页失败 %d;硬闸用例 %d 条错 %d"
|
||||
% (nr, len(badr), n, len(bad1), m, len(bad2), ng, len(badg)))
|
||||
# 留档帧目录(ci-artifacts/uiwalk,不受版本控制)可以不存在 → 那一段不算失败;
|
||||
# 但黄金参考与闭环必须真跑过:任一为 0 说明这次"自检"其实什么都没检查
|
||||
return 1 if (badr or bad1 or bad2 or nr == 0 or m == 0) else 0
|
||||
return 1 if (badr or bad1 or bad2 or badg or nr == 0 or m == 0 or ng == 0) else 0
|
||||
if "--walk" in argv:
|
||||
rc = bench_gate(argv, "--walk")
|
||||
if rc:
|
||||
|
||||
@@ -71,6 +71,11 @@ def _global_python_candidates():
|
||||
L0 全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
|
||||
都是死路),而真正装了 pre-commit 的机器级解释器(如
|
||||
C:\\ProgramData\\anaconda3\\python.exe)不再被探测。
|
||||
|
||||
本机实测矩阵(哪个解释器装了哪些包、钩子实际跑在谁身上)见
|
||||
docs/环境与部署/本地工具路径环境变量方案.md §5:提交期钩子由 .git/hooks/pre-commit
|
||||
里的 INSTALL_PYTHON 钉死在仓库 .venv,而 language: system 的 entry 用裸 python 时
|
||||
**不继承**它——所以依赖第三方包的脚本必须像本函数这样自己解析解释器。
|
||||
"""
|
||||
out = []
|
||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
|
||||
Reference in New Issue
Block a user