From c595397ab22d2c74e5dea7d2e1c1788e11330255 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 19 Sep 2026 16:28:40 +0800 Subject: [PATCH] =?UTF-8?q?chore(tools):=20=E5=86=92=E7=83=9F=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E9=94=99=E8=AF=AF=E9=97=A8=E7=A6=81=E6=89=B9=EF=BC=88?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=2084=EF=BC=8C2026-09-19=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=B4=A8=E8=AF=A2=E3=80=8C=E5=8F=91=E7=8E=B0=20bug=20=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E5=B7=B2=E4=BF=AE=E5=A4=8D=E3=80=8D=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E8=A1=A5=E4=BF=AE=EF=BC=89=E2=80=94=E2=80=94=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20tools/run=5Fsmoke.py=20=E4=B8=89=E9=97=B8=E5=8C=85=E8=A3=85?= =?UTF-8?q?=E5=99=A8=EF=BC=9A=E2=91=A0=E8=BF=9B=E7=A8=8B=E9=80=80=E5=87=BA?= =?UTF-8?q?=E7=A0=81=20=E2=91=A1=E6=91=98=E8=A6=81=E8=A1=8C=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=95=B0=3D0=20=E2=91=A2SCRIPT=20ERROR/Parse=20Error?= =?UTF-8?q?=20=E8=BE=93=E5=87=BA=E6=89=AB=E6=8F=8F=EF=BC=88GDScript=20?= =?UTF-8?q?=E6=97=A0=E8=84=9A=E6=9C=AC=E9=94=99=E8=AF=AF=E9=99=B7=E9=98=B1?= =?UTF-8?q?=20API=EF=BC=8C=E8=84=9A=E6=9C=AC=E9=94=99=E8=AF=AF=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E5=90=9E=E6=96=AD=E8=A8=80=E7=9A=84=E7=9B=B2=E5=8C=BA?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E9=97=B8=E4=BD=8F=EF=BC=89=EF=BC=9B=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E9=97=B8=E9=BB=98=E8=AE=A4=20180s=E3=80=82CLAUDE.md?= =?UTF-8?q?=E3=80=8C=E8=BF=90=E8=A1=8C=E4=B8=8E=E9=AA=8C=E8=AF=81=E3=80=8D?= =?UTF-8?q?=E5=86=92=E7=83=9F=E5=91=BD=E4=BB=A4=E8=A1=8C=E6=94=B9=E6=8C=87?= =?UTF-8?q?=E5=8C=85=E8=A3=85=E5=99=A8=EF=BC=88=E5=BA=95=E5=B1=82=20headle?= =?UTF-8?q?ss=20=E5=91=BD=E4=BB=A4=E4=BF=9D=E7=95=99=E6=B3=A8=E8=AE=B0?= =?UTF-8?q?=EF=BC=89=E3=80=82=E9=AA=8C=E8=AF=81=EF=BC=9A=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E5=86=92=E7=83=9F=20317=20=E9=A1=B9=200=20=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=20PASS=EF=BC=8B=E6=95=85=E6=84=8F=E8=A7=A6=E5=8F=91=20Parse=20?= =?UTF-8?q?Error=20=E6=8E=A2=E9=92=88=20FAIL=EF=BC=88=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=90=8E=E5=8D=B3=E5=88=A0=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 +- tools/run_smoke.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tools/run_smoke.py diff --git a/CLAUDE.md b/CLAUDE.md index 8ee2cbb9..993b63e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ 工作目录 = 切片目录本身(含 `project.godot` 的那一层): -- 冒烟(全量 headless 断言):`…console.exe --headless --path . --script res://smoke/smoke_all.gd` +- 冒烟(全量 headless 断言):`python tools/run_smoke.py`(包装器三闸:退出码+摘要失败数+SCRIPT ERROR 扫描——脚本错误不计冒烟失败数且静默中断所在函数剩余断言,2026-09-19 台账 84 立闸)。底层命令 = `…console.exe --headless --path . --script res://smoke/smoke_all.gd` - 场景级探针(呈现层回归):`…console.exe --headless --path . res://smoke/ui_probe.tscn`;另有 `ui_tune_probe.tscn`(数值调整板)、`ui_click_probe.tscn`(合成鼠标事件行为探针) - 调参期批量工具:`--script res://smoke/level_monte_carlo.gd`(关卡期望扫描)· `--script res://smoke/reward_expectation.gd`(奖励获取期望) - 交付前验收 = smoke + 全部场景探针全绿(测试细则见 `docs/code-conventions.md`) diff --git a/tools/run_smoke.py b/tools/run_smoke.py new file mode 100644 index 00000000..662fe89b --- /dev/null +++ b/tools/run_smoke.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +r"""run_smoke.py — 冒烟运行器(脚本错误外部门禁) + +用法: + python tools/run_smoke.py # 默认跑切片全量冒烟(本机 Godot 固定路径) + python tools/run_smoke.py --script res://smoke/smoke_all.gd + python tools/run_smoke.py --godot <引擎路径> --path <切片目录> --timeout 180 + +背景(2026-09-19 台账 83 实证): GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、 +不计入冒烟失败数,且会静默中断所在冒烟函数的剩余断言——首跑「309 项 0 失败」实为断言未跑全。 +进程内无脚本错误陷阱 API(GDScript 无 try/catch),故闸设在外部:本包装除退出码与摘要行外, +另扫输出中的 SCRIPT ERROR / Parse Error,命中即判失败。 + +三闸: ① 进程退出码 = 0;② 摘要行「冒烟完成:N 项检查,0 项失败」在案且失败数为 0; + ③ 输出无「SCRIPT ERROR」「Parse Error」标记行。 +""" +import argparse +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" +DEFAULT_SLICE = ROOT / "prototypes" / "dungeon-sweeper-vertical-slice" +DEFAULT_SCRIPT = "res://smoke/smoke_all.gd" +SUMMARY_RE = re.compile(r"冒烟完成:(\d+) 项检查,(\d+) 项失败") +ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error") + + +def main(): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + ap = argparse.ArgumentParser(description="冒烟运行器(脚本错误外部门禁)") + ap.add_argument("--godot", default=DEFAULT_GODOT) + ap.add_argument("--path", default=str(DEFAULT_SLICE), help="切片项目目录(含 project.godot)") + ap.add_argument("--script", default=DEFAULT_SCRIPT) + ap.add_argument("--timeout", type=int, default=180, help="外部超时秒数(ADR-0004 硬性要求)") + args = ap.parse_args() + + cmd = [args.godot, "--headless", "--path", args.path, "--script", args.script] + try: + proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", + errors="replace", timeout=args.timeout) + except subprocess.TimeoutExpired: + print(f"[run_smoke] FAIL:外部超时(>{args.timeout}s——ADR-0004 硬性要求)") + return 1 + out = (proc.stdout or "") + (proc.stderr or "") + sys.stdout.write(out) + if out and not out.endswith("\n"): + print() + + gates = [] + if proc.returncode != 0: + gates.append(f"退出码 = {proc.returncode}(≠0)") + m = SUMMARY_RE.search(out) + if m is None: + gates.append("摘要行缺失(未检出「冒烟完成:N 项检查,M 项失败」——入口跑完前中断?)") + elif int(m.group(2)) != 0: + gates.append(f"冒烟失败 {m.group(2)} 项 / 共 {m.group(1)} 项") + hits = [ln.strip() for ln in out.splitlines() if any(k in ln for k in ERROR_MARKERS)] + if hits: + gates.append(f"脚本错误标记命中 {len(hits)} 行(首行: {hits[0][:140]}——脚本错误不计冒烟失败数," + "且中断所在函数的剩余断言,必须人工排查)") + + if m is not None: + print(f"[run_smoke] 摘要: {m.group(0)}") + if gates: + print("[run_smoke] FAIL——") + for g in gates: + print(f" - {g}") + return 1 + print("[run_smoke] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())