merge: 迭代29测试分层与门禁语义修正批入库(十提交, worktree g-plan2-iter29-testlayers)

This commit is contained in:
DKChan committed 2026-10-17 00:13:57 +08:00
commit fff4f120d3
28 files changed
+1436 -123

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""error_scan.py — 脚本错误扫描闸件(判定层通用,供本地闸各段复用)
用法: python tools/error_scan.py <日志文件> [...]
背景: GDScript 运行时脚本错误(SCRIPT ERROR / Parse Error)不改进程退出码、不计失败数,
故闸只能设在外部——本件把 run_smoke.py 的同一套判定抽出来给探针段与套件段共用。
错误标记与 tools/run_smoke.py 的 ERROR_MARKERS 同源(直接 import,禁复制第二份清单)。
退出码:0=无命中;1=命中脚本错误标记;2=用法错或日志不可读。
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from run_smoke import ERROR_MARKERS # noqa: E402 单一事实源,禁在本件另写一份
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
args = sys.argv[1:]
if not args:
print("[error_scan] 用法: python tools/error_scan.py <日志文件> [...]")
return 2
hits = []
for a in args:
p = Path(a)
if not p.is_file():
print("[error_scan] FAIL — 日志不可读: %s" % a)
return 2
for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
s = ln.strip()
if s and any(k in s for k in ERROR_MARKERS):
hits.append((p.name, s))
if hits:
print("[error_scan] FAIL — 脚本错误标记命中 %d 行(此类错误不改退出码,"
"且会中断所在函数的剩余断言)" % len(hits))
for name, s in hits[:5]:
print(" - [%s] %s" % (name, s[:160]))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+50 -45
View File
@@ -10,6 +10,8 @@
# 增删档位只改表内一行;用 content_scale_size 改逻辑视口,headless 下真生效——
# 旧 ui_layout_probe 用 get_window().size 在 headless 下不改变视口,其「1920 档恒定」
# 断言是永远为真的假断言,随该探针退役)。
# 判定口径(迭代 29 T1 三硬闸):每段走 verdict 函数——①退出码为唯一权威判据(PASS 串降级为日志)
# ②脚本错误扫描命中即红(tools/error_scan.py,标记与 run_smoke.py 同源)③PASS 串保留为附加判据。
# --full 段(迭代 19 卡① T3 收编 2026-10-08——重闸件):ui_probe/ui_tune_probe/t17_defer_probe/fx_probe
# 四件带断言探针收进(此前游离在一切闸外只能手敲);轻闸默认段断言面零删减零改动(--full 只增不改)。
# 渲染面归 render_gate.sh 正主(窗口化:shot_probe 七屏+fx_shot/board_shot 取证+棋盘 golden 逐像素)——
@@ -33,6 +35,39 @@ 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
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(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'
@@ -55,52 +90,38 @@ echo "======== ui_input_probe ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_input_probe.tscn >"$TMP/uip.log" 2>&1
st=$?; kill_orphans
grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_input_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/uip.log"; then
echo "[probe_gate] FAIL — ui_input_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1
echo "======== ui_struct_probe ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_struct_probe.tscn >"$TMP/stp.log" 2>&1
st=$?; kill_orphans
grep -E "PASS|FAIL" "$TMP/stp.log" | head -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_struct_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/stp.log"; then
echo "[probe_gate] FAIL — ui_struct_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "ui_struct_probe" "$TMP/stp.log" "$st" || _rc=1
echo "======== layout_probe(分辨率几何·两档) ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/layout_probe.tscn >"$TMP/lay.log" 2>&1
st=$?; kill_orphans
grep -E "PASS|FAIL" "$TMP/lay.log" | head -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — layout_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/lay.log"; then
echo "[probe_gate] FAIL — layout_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "layout_probe" "$TMP/lay.log" "$st" || _rc=1
echo "======== ui_contrast_probe ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_contrast_probe.tscn >"$TMP/ctp.log" 2>&1
st=$?; kill_orphans
grep -E "PASS|FAIL" "$TMP/ctp.log" | head -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_contrast_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/ctp.log"; then
echo "[probe_gate] FAIL — ui_contrast_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "ui_contrast_probe" "$TMP/ctp.log" "$st" || _rc=1
echo "======== screen_flow_probe(L3 屏壳切换·迭代 29 T4) ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/screen_flow_probe.tscn >"$TMP/sfp.log" 2>&1
st=$?; kill_orphans
grep -E "screen_flow_probe" "$TMP/sfp.log" | tail -3
verdict "screen_flow_probe" "$TMP/sfp.log" "$st" "screen_flow_probe" || _rc=1
if [ "$WITH_CLICK" -eq 1 ]; then
echo "======== ui_click_probe 行为走查 ========"
timeout 240 "$G" --headless --path "$GAME_WIN" res://smoke/ui_click_probe.tscn >"$TMP/clk.log" 2>&1
st=$?; kill_orphans
grep -E "click_probe" "$TMP/clk.log" | tail -4
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_click_probe 超时(exit=$st):挂死或探针未适配新停点层(归 probe适配批),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ]; then
echo "[probe_gate] FAIL — ui_click_probe 退出码=$st(done=false 或 问题>0)"; _rc=1
fi
verdict "ui_click_probe" "$TMP/clk.log" "$st" "click_probe" || _rc=1
fi
# ── --full 收编段(迭代 19 卡① T3 2026-10-08——四件带断言探针入闸;实测时长 ui_probe 17.4s/
@@ -110,41 +131,25 @@ if [ "$WITH_FULL" -eq 1 ]; then
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_probe.tscn >"$TMP/flp.log" 2>&1
st=$?; kill_orphans
grep -E "\[ui_probe\]" "$TMP/flp.log" | tail -2
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/flp.log"; then
echo "[probe_gate] FAIL — ui_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "ui_probe" "$TMP/flp.log" "$st" || _rc=1
echo "======== ui_tune_probe(--full 收编) ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_tune_probe.tscn >"$TMP/flt.log" 2>&1
st=$?; kill_orphans
grep -E "\[tune_probe\]" "$TMP/flt.log" | tail -2
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — ui_tune_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/flt.log"; then
echo "[probe_gate] FAIL — ui_tune_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "ui_tune_probe" "$TMP/flt.log" "$st" || _rc=1
echo "======== t17_defer_probe(--full 收编) ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/t17_defer_probe.tscn >"$TMP/fld.log" 2>&1
st=$?; kill_orphans
grep -E "\[t17_defer\]" "$TMP/fld.log" | tail -2
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — t17_defer_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/fld.log"; then
echo "[probe_gate] FAIL — t17_defer_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "t17_defer_probe" "$TMP/fld.log" "$st" || _rc=1
echo "======== fx_probe(--full 收编) ========"
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/fx_probe.tscn >"$TMP/flx.log" 2>&1
st=$?; kill_orphans
grep -E "\[fx_probe\]" "$TMP/flx.log" | tail -2
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[probe_gate] FAIL — fx_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif ! grep -q "PASS" "$TMP/flx.log"; then
echo "[probe_gate] FAIL — fx_probe 未见 PASS(exit=$st)"; _rc=1
fi
verdict "fx_probe" "$TMP/flx.log" "$st" || _rc=1
fi
echo "======== 残留检查(批末闸) ========"
+48 -1
View File
@@ -87,6 +87,12 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd ) \
>"$STAMP_DIR/unit.log" 2>&1
local rc=$?
# 三硬闸之二(脚本错误扫描):子套件的 SCRIPT ERROR/Parse Error 只写 stderr,入口内收不到,
# 只能在编排层扫本闸日志。命中即红——此类错误不改退出码、且会中断所在函数的剩余断言。
if [ "$rc" -eq 0 ] && ! "$PY" "$REPO/tools/error_scan.py" "$STAMP_DIR/unit.log" >>"$STAMP_DIR/unit.log" 2>&1; then
rc=1
tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /'
fi
[ $rc -ne 0 ] && tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /'
NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("unit.log")
return 0
@@ -113,6 +119,45 @@ gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict
gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict
# ── 6b. 状态文件体量闸(active.md 只记现在:条目数/快照行数/编号/死指针四条硬上限)──
gate "08b active.md 体量" "active_state.log" "$PY" "$REPO/tools/audit_active_state.py"
# ── 6c. 回放确定性对拍(条件闸件——迭代 29 T3 收编)──
# 触面判定:本批 diff 相对 master 分叉点,命中下列路径面之一即「触面」。
# 触面也**不自动跑**——对拍调的是 MC 脚本,属「MC/统计类跑批发起前须用户批准」红线管辖;
# 闸只负责每次跑批都把触面判定做一遍并显式要求批准,杜绝「该跑却没人记得」。
# 用户批准后设 PARITY_APPROVED=1 重跑本脚本即实跑三变体对拍。
PARITY_TOUCH_FACES="
game/data/tables/
game/core/rng_service.gd
game/core/save_manager.gd
game/core/meta_progression.gd
game/core/settlement/
game/core/damage_math.gd
game/core/damage_controller.gd
game/core/battle_orchestrator.gd
game/smoke/adventure_monte_carlo.gd
"
_base="$(git -C "$REPO" merge-base master HEAD 2>/dev/null || true)"
_changed_files="$(git -C "$REPO" diff --name-only "${_base:-HEAD}" HEAD 2>/dev/null || true)"
_touch_hit=0
for _f in $_changed_files; do
for _pat in $PARITY_TOUCH_FACES; do
case "$_f" in
"$_pat"*) _touch_hit=1 ;;
esac
done
done
_parity_name="11 回放确定性对拍"
if [ "$_touch_hit" -eq 0 ]; then
NAMES+=("$_parity_name"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] $_parity_name → SKIP(本批 diff 未触数值/存档/RNG/结算面)"
elif [ "${PARITY_APPROVED:-0}" = "1" ]; then
gate "$_parity_name" "parity.log" "$PY" "$REPO/tools/parity_anchor.py" check
else
NAMES+=("$_parity_name"); EXITS+=("NEED-APPROVAL"); LOGS+=("")
echo ""
echo "[gates] $_parity_name → 本批 diff 触及数值/存档/RNG/结算面,对拍需用户批准发起"
echo " (批准后设 PARITY_APPROVED=1 重跑本脚本即实跑;不得借机重锚)"
fi
# ── 7. 静态扫描域 + 熵增闸 ──
if [ $QUICK -eq 0 ]; then
gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh"
@@ -148,7 +193,9 @@ def head():
def fingerprint(exit_code, log):
"""红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声。
绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。"""
if exit_code in ("0", "SKIP"):
# SKIP / NEED-APPROVAL 都属「本闸未实跑」状态,指纹一律记 skip——
# 两者互变不是闸况变化(同是没跑),只有「未跑 → 跑出结果」或「绿 → 红」才算变化。
if exit_code in ("0", "SKIP", "NEED-APPROVAL"):
return "green" if exit_code == "0" else "skip"
if not log:
return "red-nolog"