chore(tools): 冒烟脚本错误门禁批(台账 84,2026-09-19 用户质询「发现 bug 是否已修复」触发补修)——新增 tools/run_smoke.py 三闸包装器:①进程退出码 ②摘要行失败数=0 ③SCRIPT ERROR/Parse Error 输出扫描(GDScript 无脚本错误陷阱 API,脚本错误静默吞断言的盲区外部闸住);超时闸默认 180s。CLAUDE.md「运行与验证」冒烟命令行改指包装器(底层 headless 命令保留注记)。验证:真实冒烟 317 项 0 失败 PASS+故意触发 Parse Error 探针 FAIL(验证后即删)。

This commit is contained in:
DKChan committed 2026-09-19 16:28:40 +08:00
1 parent d236d9f8e1
commit c595397ab2
2 files changed
+79 -1

No files matched your search

+1 -1
View File
@@ -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`)
+78
View File
@@ -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())