#!/usr/bin/env python3 r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁) 用法: python tools/run_system_test.py # 跑系统测试(过渡期宿主 res://smoke/smoke_all.gd) python tools/run_system_test.py --script res://tests/system_test.gd python tools/run_system_test.py --godot <引擎路径> --path <切片目录> --timeout 300 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。 宿主过渡态:断言现全部住在 res://smoke/smoke_all.gd,按「只剪切禁改写」纪律迁往 res://tests/system_test.gd(宿主薄壳已在位)——迁移完成后本件缺省 --script 改指新宿主, 闸 01 系统测试随迁。 背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、不计入失败数, 且会静默中断所在断言函数的剩余断言——摘要零失败也可能断言未跑全。 进程内无脚本错误陷阱 API(GDScript 无 try/catch),故闸设在外部:本包装除退出码与摘要行外, 另扫输出中的 SCRIPT ERROR / Parse Error,命中即判失败。 三闸: ① 进程退出码 = 0;② 摘要行(「冒烟完成:N 项检查,0 项失败」或 「[system_test] 摘要:N 项检查,0 项失败」)在案且失败数为 0; ③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单经 tools/error_scan.py 复用,禁另写一份)。 """ 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 / "game" DEFAULT_SCRIPT = "res://smoke/smoke_all.gd" # 过渡期宿主;断言迁移批后改 res://tests/system_test.gd SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\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, help="系统测试宿主脚本") ap.add_argument("--timeout", type=int, default=300, help="外部超时秒数(硬性要求)") 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_system_test] FAIL:外部超时(>{args.timeout}s——硬性要求)") 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_system_test] 摘要: {m.group(0)}") if gates: print("[run_system_test] FAIL——") for g in gates: print(f" - {g}") return 1 print("[run_system_test] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)") return 0 if __name__ == "__main__": sys.exit(main())