Files
gplan-2/tools/render_gate.sh
DKChan a2ec3e8b94 工程(渲染闸脚本错误扫描): 三段探针日志逐段过 tools/error_scan.py 补三硬闸之二——shot(shot_checker 之后)/fx/board 各一次,命中或日志不可读即红,标记与 run_smoke 同源;头注补⑤与「新增日志段必须同步加扫描」纪律
验证=合成日志三分支(clean 0/命中 1/不可读 2)+真跑 render_gate exit=0 全绿(三段零命中、残留 Godot=0、golden changed 0.00%)+文档域闸五件 PASS。
文档同步=architecture 测试域两处「SCRIPT ERROR 扫描缺失」换代、game/tests/README.md 工具行补判定面。
清单=「测试域遗留」整条删除(第 1 项被能力清单模型取代/第 3 项与 test-conventions §6 存量不清扫口径冲突且多进程分片后前提消失/第 4 项行覆盖率用户裁无意义/第 2 项即本批);基线条目扩为 02/05/09 三键(05 主区实测绿,baseline 红键属代码面,随下一代码批在 worktree 内重生成)
2026-10-30 05:39:33 +08:00

180 lines
11 KiB
Bash
Raw Permalink 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
# 渲染闸正主——窗口化自动闸。
# 范围(渲染五闸的机器面:shot_probe/golden_compare/board_shot 三件入闸;
# fx_probe 逻辑层归 probe_gate --full;1920 档布局机检面待补)。
# 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿
# Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。
# 段:
# ① shot_probe 七屏窗口化截图(menu/settings/career/camp/level/battle/status)
# → tools/shot_checker.py A-D 语义断言自动判定(A 屏级骨架全屏/B 文字对比度/C 明度三档带/
# D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记待办清单。
# ② 取证段(Q7:超时/零字节/文件缺失任一即 FAIL——「渲染真实发生」必须成立,不做像素判定):
# fx_shot_probe 三时刻抓帧(行到达/结算排播/轮末连帧)+ board_shot_probe L1-L3 棋盘抓帧。
# ③ 棋盘 golden 逐像素:board 整屏帧按 board_meta.json 取样坐标裁剪棋盘区
# (tools/golden_board_crop.py——禁硬编码像素)→ tools/golden_compare.py 对
# game/tests/golden/ 三张基线(容差 provisional:差 ≤16/changed ≤2%,S2 停机点口径)。
# 素材缺位=判红:缺盘时这一段根本不对比,若 SKIP 则闸末照样打
# 「全绿」exit 0,等于空跑判绿。基线生成期素材缺位=L3 停机,运行期同样无权出验收结论。
# ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比
# 1.35×+左右黑边各 96px,构图与基准档一致(分辨率案甲)——分辨率维度不重复建闸。
# (1920 布局机检面现无承载,待补。)
# ⑤ 脚本错误扫描(三硬闸之二):shot/fx/board 三段 Godot 日志逐段过 tools/error_scan.py,
# 命中 SCRIPT ERROR/Parse Error 即红(标记与 run_smoke.py 同源);python 工具自身输出不扫。
# 此类错误不改退出码、不计失败数,且会中断所在函数的剩余断言——只能由外部扫日志。
# 新增日志段必须同步加扫描调用,漏加=该段静默错误漏网。
# 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git;
# 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。
# 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸
# (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。
# 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell + python(PIL);Godot 路径可用环境变量 GODOT_BIN 覆盖。
set -u
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
GAME="${1:-$REPO/game}"
GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式
TMP="$(mktemp -d /tmp/rendergate_XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
_rc=0
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算)
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1"
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r'
}
kill_orphans() {
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
}
scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红
local name="$1" log="$2"
if ! python "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then
echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):"
sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /'
_rc=1
fi
}
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
timeout 300 "$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 "[render_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi
echo "======== shot_probe 七屏截图 → shot_checker A-D ========"
SHOT="$TMP/shots"
timeout 180 "$G" --path "$GAME_WIN" res://probe/shot_probe.tscn -- "$SHOT" >"$TMP/shot.log" 2>&1
st=$?; kill_orphans
grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ] || ! grep -q "问题=0" "$TMP/shot.log"; then
echo "[render_gate] FAIL — shot_probe 未干净完成(exit=$st,缺失屏/空文件/路径断裂见上)"; _rc=1
fi
if [ "$_rc" -eq 0 ]; then
python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /'
_rc="${PIPESTATUS[0]}"
[ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)"
fi
scan_log "shot_probe" "$TMP/shot.log"
echo "======== fx_shot_probe 三时刻取证(Q7) ========"
FX="$TMP/fxshots"
timeout 180 "$G" --path "$GAME_WIN" res://probe/fx_shot_probe.tscn -- "$FX" >"$TMP/fx.log" 2>&1
st=$?; kill_orphans
grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — fx_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "fx_shot_probe" "$TMP/fx.log"
fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7)
local pat="$1" min="$2" n=0 f
for f in "$FX"/$pat; do
[ -f "$f" ] || continue
[ -s "$f" ] || { echo "[render_gate] FAIL — 取证零字节: $f"; _rc=1; return; }
n=$((n+1))
done
if [ "$n" -lt "$min" ]; then echo "[render_gate] FAIL — 取证文件缺失: $pat 仅 $n/<$min"; _rc=1; fi
}
fx_evidence "fx_dig*.png" 1
fx_evidence "fx_settle*.png" 1
fx_evidence "fx_round*.png" 6
fx_evidence "fx_final.png" 1
echo "======== board_shot_probe L1-L3 取证(Q7) ========"
BD="$TMP/boardshots"
timeout 180 "$G" --path "$GAME_WIN" res://probe/board_shot_probe.tscn -- "$BD" >"$TMP/bd.log" 2>&1
st=$?; kill_orphans
grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — board_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "board_shot_probe" "$TMP/bd.log"
for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do
if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi
done
echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========"
if [ ! -d "$REPO/game/tests/golden" ]; then
echo "[render_gate] FAIL — game/tests/golden/ 基线目录缺失(基线在库=验收断言②)"; _rc=1
elif [ ! -s "$BD/board_meta.json" ]; then
echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判"
else
# 素材缺位=判红:缺盘态下这一段一个像素都不比,而闸末照样打印
# 「全绿」退出码 0——那是空跑判绿,比误红危险得多。基线锚在素材齐盘态(Q5),缺盘态本就无权出验收结论。
_asset_miss=0
for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/seal_mark.png \
assets/art/character/character_hero_idle.png \
assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \
assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \
assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do
[ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: $a(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; }
done
if [ "$_asset_miss" -eq 0 ]; then
CROP="$TMP/crop"
python tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /'
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "[render_gate] FAIL — 棋盘区裁剪失败"; _rc=1
else
python tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \
board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png | sed 's/^/[render_gate] /'
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "[render_gate] FAIL — golden 逐像素超容差(S2 停机点:字体/平台误红呈报后定容差)"; _rc=1
fi
fi
fi
fi
echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========"
if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then
echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1
elif [ ! -s "$SHOT/shot_menu.png" ]; then
echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判"
elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then
echo "[render_gate] FAIL — 菜单素材缺位(字体/按钮石纹):基线锚素材齐盘态,缺盘不得出验收结论(2026-10-17 由 SKIP 改判红)"; _rc=1
else
MENUG="$TMP/menugolden"
mkdir -p "$MENUG"
cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png"
python tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /'
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1
fi
fi
echo "======== 残留检查(批末闸) ========"
left="$(orphans | wc -l | tr -d ' ')"
total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)"
echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)"
if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi
exit "$_rc"