79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
#!/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
|
||
|
||
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、
|
||
不计入冒烟失败数,且会静默中断所在冒烟函数的剩余断言——摘要零失败也可能断言未跑全。
|
||
进程内无脚本错误陷阱 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 / "game"
|
||
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="外部超时秒数(硬性要求)")
|
||
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——硬性要求)")
|
||
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())
|