Files
gplan-2/tools/probe_gate.sh
T

206 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 探针闸正主——headless 安全段固化,禁再手拼同款 Godot 命令。
# 三闸:①每段 timeout 守卫+超时整树回收(挂死=FAIL 不静默;taskkill /T 连引擎子进程一起杀,
# 不再只杀 wrapper 留孤儿)②段前预清+段后清本 game 目录孤儿 Godot
# (按命令行精确匹配本 game 路径,不误杀其他 worktree 会话的合法进程)③末尾残留清点=批末闸(必须 0)。
# 段(单一形态,十件恒跑):
# 默认段五件:ui_input_probe/ui_struct_probe(
# 全 UI 屏结构断言:按钮互压/等宽/出屏/零尺寸/标题屏双列)/ui_contrast_probe(
# UI 文字对比度机检 ≥4.5:1 无区域例外,测点表随改动维护)/screen_flow_probe(L3 屏壳切换)。
# 行为走查段:ui_click_probe(自退出+退出码闸;超时多半=探针未适配新停点层,需做探针适配)。
# layout_probe=默认段五件之一(分辨率几何断言,档位表是数据/断言是一份函数,
# 增删档位只改表内一行;用 content_scale_size 改逻辑视口,headless 下真生效——
# get_window().size 在 headless 下不改变视口,其「1920 档恒定」类断言是
# 永远为真的假断言,禁用)。
# 判定口径(三硬闸):每段走 verdict 函数——①退出码为唯一权威判据(PASS 串降级为日志)
# ②脚本错误扫描命中即红(tools/error_scan.py,标记与 run_smoke.py 同源)③PASS 串保留为附加判据。
# 扩展段(带断言探针四件):ui_probe/ui_tune_probe/t17_defer_probe/fx_probe。
# 渲染面归 render_gate.sh 正主(窗口化:shot_probe 七屏+fx_shot/board_shot 取证+棋盘 golden 逐像素)。
# 用法: tools/probe_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell(Windows 侧);macOS/Linux 走 tools/gate_platform.sh
# 共享跨平台件(Godot 路径两平台都可用 GODOT_BIN 环境变量覆盖)。
set -u
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀)
GAME=""
for a in "$@"; do
case "$a" in
*) GAME="$a" ;;
esac
done
GAME="${GAME:-$REPO/game}"
GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样)
TMP="$(mktemp -d /tmp/probegate_XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
_rc=0
# 错误扫描闸件用纯标准库 python(不依赖 PIL),优先带 PIL 的 gamedev venv 以保持与全量域闸同解释器。
PY=""
for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "python" "python3"; do
if "$cand" -c "import sys" >/dev/null 2>&1; then PY="$cand"; break; fi
done
if [ -z "$PY" ]; then
echo "[probe_gate] 警告:未找到 python,脚本错误扫描闸不生效(三硬闸缺一)"
fi
# 判定三硬闸——各探针段统一走本函数,禁再手拼判定:
# ①退出码为唯一权威判据(PASS 串降级为日志,不得再当判据)
# ②脚本错误扫描命中即红(SCRIPT ERROR/Parse Error 不改退出码,只能外部扫)
# ③签名标记保留为附加判据(退出码 0 却无该探针的签名行=静默退出,仍判红)。
# 标记默认 PASS;ui_click_probe 本就走退出码语义、日志不写 PASS,传它自己的摘要标记。
verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记]
local name="$1" log="$2" st="$3" mark="${4:-PASS}"
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
# macOS quit(0) 挂死受控例外:守卫回收后若 PASS 签名在位且错误扫描净=探针已完成判定,
# 挂死只发生在退出段(run26 实证:headless 探针同样挂)——按完成处理,签名缺失仍判红。
if [ -n "$PY" ] && "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1 && grep -q "$mark" "$log"; then
echo "[probe_gate] OK — $name 判定完成(exit=$st 为 macOS 退出段挂死回收,签名+扫描双证齐全)"
return 0
fi
echo "[probe_gate] FAIL — $name 超时挂死(exit=$st),孤儿已清"; return 1
fi
if [ "$st" -ne 0 ]; then
echo "[probe_gate] FAIL — $name 退出码=${st}(退出码为唯一权威判据,PASS 串不抵)"; return 1
fi
if [ -n "$PY" ] && ! "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1; then
echo "[probe_gate] FAIL — $name 脚本错误扫描命中:"
sed -n '1,6p' "$TMP/err.txt" | sed 's/^/ /'
return 1
fi
if ! grep -q "$mark" "$log"; then
echo "[probe_gate] FAIL — $name 未见签名标记「${mark}」(exit=${st}——退出码 0 但零输出亦判红)"; return 1
fi
return 0
}
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台)
gate_orphans "$GAME_WIN"
}
kill_orphans() {
gate_kill_orphans "$GAME_WIN"
}
# 每段跑批统一走本函数:后台启动记 MSYS PID→WINPID,守卫进程到点先 taskkill /T 整树回收
# (顺序要紧:先杀 wrapper 再清树会让 /T 走不到已死的父进程,引擎子进程照样泄漏——
# 2026-10-25 在册挂死根源),后 kill -9 收 MSYS 侧。正常退出不动进程树;段后 kill_orphans 照跑兜底。
run_suite() { # run_suite <timeout秒> <日志> <场景 res:// 路径>
local t="$1" log="$2" scene="$3"
if ! gate_is_win; then
# macOS/Linux:无 console wrapper(Godot 单进程),走共享带超时执行+孤儿清杀兜底。
local st
gate_run_timeout "$t" "$log" "$G" --headless --path "$GAME_WIN" "$scene"
st=$?
gate_kill_orphans "$GAME_WIN"
return "$st"
fi
local kf="$TMP/killed.$RANDOM"
rm -f "$kf"
"$G" --headless --path "$GAME_WIN" "$scene" >"$log" 2>&1 &
local pid=$!
local wpid=""
wpid="$(ps -W 2>/dev/null | awk -v p="$pid" '$1==p{print $4; exit}')"
( sleep "$t" 2>/dev/null
touch "$kf"
[ -n "$wpid" ] && taskkill //F //T //PID "$wpid" >/dev/null 2>&1
kill -9 "$pid" 2>/dev/null
true ) &
local guard=$!
wait "$pid"; local st=$?
kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null
# taskkill 强杀后 MSYS wait 可能报 0——超时判定以守卫落下的标志文件为准(与 124 同判)。
if [ "$st" -ge 128 ] || [ -f "$kf" ]; then
st=124
echo "[probe_gate] 套件超时(${t}s),已整树回收(PID=$pid/WINPID=${wpid:-未取到——靠孤儿清杀兜底})"
fi
rm -f "$kf"
kill_orphans
return "$st"
}
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
kill_orphans
fi
pre="$(orphans | wc -l | tr -d ' ')"
if [ "$pre" -gt 0 ]; then echo "[probe_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi
echo "======== ui_input_probe ========"
run_suite 120 "$TMP/uip.log" res://probe/ui_input_probe.tscn
st=$?
grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12
verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1
echo "======== level_map_viewport_probe(卷轴视口批 R1-R8) ========"
run_suite 120 "$TMP/lvp.log" res://probe/level_map_viewport_probe.tscn
st=$?
grep -E "PASS|FAIL" "$TMP/lvp.log" | head -12
verdict "level_map_viewport_probe" "$TMP/lvp.log" "$st" || _rc=1
echo "======== ui_struct_probe ========"
run_suite 120 "$TMP/stp.log" res://probe/ui_struct_probe.tscn
st=$?
grep -E "PASS|FAIL" "$TMP/stp.log" | head -12
verdict "ui_struct_probe" "$TMP/stp.log" "$st" || _rc=1
echo "======== layout_probe(分辨率几何·两档) ========"
run_suite 120 "$TMP/lay.log" res://probe/layout_probe.tscn
st=$?
grep -E "PASS|FAIL" "$TMP/lay.log" | head -12
verdict "layout_probe" "$TMP/lay.log" "$st" || _rc=1
echo "======== ui_contrast_probe ========"
run_suite 120 "$TMP/ctp.log" res://probe/ui_contrast_probe.tscn
st=$?
grep -E "PASS|FAIL" "$TMP/ctp.log" | head -12
verdict "ui_contrast_probe" "$TMP/ctp.log" "$st" || _rc=1
echo "======== screen_flow_probe(L3 屏壳切换·迭代 29 T4) ========"
run_suite 120 "$TMP/sfp.log" res://probe/screen_flow_probe.tscn
st=$?
grep -E "screen_flow_probe" "$TMP/sfp.log" | tail -3
verdict "screen_flow_probe" "$TMP/sfp.log" "$st" "screen_flow_probe" || _rc=1
echo "======== ui_click_probe 行为走查 ========"
run_suite 240 "$TMP/clk.log" res://probe/ui_click_probe.tscn
st=$?
grep -E "click_probe" "$TMP/clk.log" | tail -4
verdict "ui_click_probe" "$TMP/clk.log" "$st" "click_probe" || _rc=1
# ── 扩展段(四件带断言探针;实测时长 ui_probe 17.4s/
# ui_tune_probe 49.6s/t17_defer_probe 63.0s/fx_probe 21.9s,timeout 120=2-7×余量) ──
echo "======== ui_probe(扩展段) ========"
run_suite 120 "$TMP/flp.log" res://probe/ui_probe.tscn
st=$?
grep -E "\[ui_probe\]" "$TMP/flp.log" | tail -2
verdict "ui_probe" "$TMP/flp.log" "$st" || _rc=1
echo "======== ui_tune_probe(扩展段) ========"
run_suite 120 "$TMP/flt.log" res://probe/ui_tune_probe.tscn
st=$?
grep -E "\[tune_probe\]" "$TMP/flt.log" | tail -2
verdict "ui_tune_probe" "$TMP/flt.log" "$st" || _rc=1
echo "======== t17_defer_probe(扩展段) ========"
run_suite 120 "$TMP/fld.log" res://probe/t17_defer_probe.tscn
st=$?
grep -E "\[t17_defer\]" "$TMP/fld.log" | tail -2
verdict "t17_defer_probe" "$TMP/fld.log" "$st" || _rc=1
echo "======== fx_probe(扩展段) ========"
run_suite 120 "$TMP/flx.log" res://probe/fx_probe.tscn
st=$?
grep -E "\[fx_probe\]" "$TMP/flx.log" | tail -2
verdict "fx_probe" "$TMP/flx.log" "$st" || _rc=1
echo "======== 残留检查(批末闸) ========"
left="$(orphans | wc -l | tr -d ' ')"
total="$(gate_godot_count_all)"
echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)"
if [ "$left" -ne 0 ]; then echo "[probe_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
if [ "$_rc" -eq 0 ]; then echo "[probe_gate] 全绿"; else echo "[probe_gate] 存在 FAIL,见上"; fi
exit "$_rc"