feat(迭代29 T1): 本地闸三硬闸落地——退出码/脚本错误扫描/断言数下限
①退出码为唯一权威判据:probe_gate.sh 八件探针(轻闸 4 + full 4)判定统一走 verdict 函数, 退出码≠0 即红,PASS 串降级为附加判据(ui_click_probe 本就走退出码语义,签名标记传它自己的 click_probe 串,不强加 PASS)。 ②脚本错误扫描闸件 tools/error_scan.py(标记 import 自 run_smoke.py,禁第二份清单): probe_gate 逐探针段扫日志;run_all 侧因 --script 子进程的 stdout 管道恒空(OS.execute 在本机 Godot/Windows 组合下拿不到输出,且 GUI 子系统 exe 的 stdout/stderr 句柄无效),改为整条命令 交给系统命令解释器重定向落盘再读,退出码原样回传——引擎二进制取 *_console 版(GODOT_BIN 优先)。 ③断言数下限:Kit.finish 在 checks==0 时返回专用退出码 2(不是 1——致命类套件期望 1,返回 1 会被 expect 协议放过);run_all 汇总校验实跑套件数 == manifest 条目数。 ④example_test.gd 退出生产 manifest(协议活样板,文件保留;同类先例 fog_lantern 摘除)。 变异验证(三反例 T0 判绿、T1 后全部判红):零断言套件 exit=2 判红/脚本错误套件命中 SCRIPT ERROR 判红/探针打印 PASS 后 quit(1) 判红「PASS 串不抵」。 本卡附带裁决落地(均已用户裁准): · fx_grant_gp 补进行来源断言豁免表(grant_segment 的专长 GP 入账行无棋盘来源格,每局触发 7 次 断言失败却因脚本错误不改退出码长期漏过;迭代 21 同款修过 fx_reveal)。 · manifest 增 negative 字段,仅 reward_dangling_treasure 一件(坏表是其测试目标,错误扫描误伤)。 · config_db_test 的 rows 定型数组赋值改 rows.assign——原写法触发 Invalid assignment 中断整个函数, 敌人表行集三条纪律 10 条断言从未执行过(31 处 check 实跑 21 条);修复后 26 条全跑,其中两条正向 断言见红,根因是夹具缺 depth-0 锚行(validate 的 anchor 规则),补锚行后全绿。 · system_contracts_test 的 RngStub/UnitContainerStub 空桩换真实实例(定型成员赋值报 type 空)。
This commit is contained in:
1 parent
698d864dcc
commit
8c82d3bf56
8 files changed
+189
-78
No files matched your search
@@ -39,8 +39,10 @@ signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态
|
||||
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
|
||||
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
|
||||
# fx_reveal=专长揭示掷格(落空行无真实格〔迭代 21 存量红修复——a57489e9 落地件三处空来源
|
||||
# 触发行来源断言,51 行/冒烟,两轮确定性复现〕)
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal"]
|
||||
# 触发行来源断言,51 行/冒烟,两轮确定性复现〕);
|
||||
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指——与 fx_reveal 同款的漏网豁免,
|
||||
# 未豁免时每局触发行来源断言失败 7 次,因脚本错误不改退出码而被闸长期漏过)
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"]
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
|
||||
+63
-8
@@ -7,14 +7,25 @@
|
||||
# 场景探针(.tscn 载体)由 CI/轻闸单独步骤调用,不经本入口。
|
||||
extends SceneTree
|
||||
|
||||
# 脚本错误标记与 tools/run_smoke.py 的 ERROR_MARKERS 同源(GDScript 无脚本错误陷阱 API,
|
||||
# 错误不改进程退出码、不计失败数,只能由调用方扫输出——三硬闸之二)。
|
||||
const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"]
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
print("[run_all] unit/contract suite entry — %d suites" % _suites().size())
|
||||
if DisplayServer.get_name() != "headless":
|
||||
push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)")
|
||||
var failures := 0
|
||||
var ran := 0
|
||||
for suite in _suites():
|
||||
failures += _run_suite(suite)
|
||||
failures += _run_suite(suite, ran)
|
||||
ran += 1
|
||||
# 实跑套件数校验:manifest 有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。
|
||||
if ran != _suites().size():
|
||||
push_error("[run_all] 实跑套件数 %d ≠ manifest 条目数 %d——有套件未跑" % [ran, _suites().size()])
|
||||
quit(1)
|
||||
return
|
||||
if failures > 0:
|
||||
push_error("[run_all] %d 个套件失败" % failures)
|
||||
quit(1)
|
||||
@@ -23,15 +34,18 @@ func _init() -> void:
|
||||
quit(0)
|
||||
|
||||
|
||||
# 套件清单:{ "path": String, "expect": int };致命类(期望非 0)独立进程断言退出码。
|
||||
# 套件清单:{ "path": String, "expect": int, "negative"?: bool };致命类(期望非 0)独立进程断言退出码。
|
||||
func _suites() -> Array:
|
||||
return [
|
||||
{"path": "res://tests/unit/example_test.gd", "expect": 0},
|
||||
# example_test.gd 是协议活样板(tests/unit/README 明示),不进生产 manifest——
|
||||
# 样板件占位断言跑绿没有信息量,且占据「27 套件」计数。文件保留作模板(同类先例见下 fog_lantern 摘除行)。
|
||||
{"path": "res://tests/unit/data_config/config_db_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/data_config/forge_table_test.gd", "expect": 0}, # 迭代 23 锻造工坊批 T1——六路线 schema 正反(V1)+特效留空(V11)+几何价格梯(V2 半边)
|
||||
{"path": "res://tests/unit/data_config/config_db_fatal_oob_test.gd", "expect": 1},
|
||||
{"path": "res://tests/unit/data_config/treasure_target_violation_test.gd", "expect": 1}, # 迭代 7 T1——真视窄域护栏
|
||||
{"path": "res://tests/unit/data_config/reward_dangling_treasure_test.gd", "expect": 1}, # 迭代 7 T1——秘宝双表校验
|
||||
# negative=true=负面套件:坏表/违规输入是它的测试目标,输出里必然带 Parse Error,
|
||||
# 脚本错误扫描对它属误伤——豁免只放行这一件,且仅限「错误扫描」一闸(退出码与断言下限照判)。
|
||||
{"path": "res://tests/unit/data_config/reward_dangling_treasure_test.gd", "expect": 1, "negative": true}, # 迭代 7 T1——秘宝双表校验
|
||||
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 迭代 7 T7——秘宝结构冻结断言面
|
||||
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 迭代 8 卡①/T9——断点往返/续玩确定性/幂等/退出清断点
|
||||
@@ -57,19 +71,60 @@ func _suites() -> Array:
|
||||
]
|
||||
|
||||
|
||||
func _run_suite(suite: Dictionary) -> int:
|
||||
func _run_suite(suite: Dictionary, idx: int) -> int:
|
||||
var path: String = suite.get("path", "")
|
||||
var expect: int = int(suite.get("expect", 0))
|
||||
if path.is_empty():
|
||||
push_error("[run_all] suite missing path")
|
||||
return 1
|
||||
var args := PackedStringArray(["--headless", "--path", ProjectSettings.globalize_path("res://"), "--script", path])
|
||||
var bin := _engine_bin()
|
||||
if bin == "":
|
||||
push_error("[run_all] 未找到 console 版引擎二进制(设 GODOT_BIN 指向 *_console.exe)——"
|
||||
+ "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地")
|
||||
return 1
|
||||
# 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空
|
||||
#(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。
|
||||
var proj := ProjectSettings.globalize_path("res://")
|
||||
var log := proj + "/tests/.gates/run_all_%02d.log" % idx
|
||||
DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates")
|
||||
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log]
|
||||
var out := PackedStringArray()
|
||||
var code: int = OS.execute(OS.get_executable_path(), args, out, true, false)
|
||||
var text := "\n".join(out)
|
||||
var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false)
|
||||
var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else ""
|
||||
print(text)
|
||||
if not bool(suite.get("negative", false)) and _has_script_error(text):
|
||||
push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)])
|
||||
return 1
|
||||
if code != expect:
|
||||
push_error("[run_all] FAIL %s — 退出码 %d ≠ 期望 %d" % [path, code, expect])
|
||||
return 1
|
||||
print("[run_all] PASS %s (exit=%d)" % [path, code])
|
||||
return 0
|
||||
|
||||
|
||||
# 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe
|
||||
#(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢,
|
||||
# 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。
|
||||
func _engine_bin() -> String:
|
||||
var env := OS.get_environment("GODOT_BIN")
|
||||
if env != "" and FileAccess.file_exists(env):
|
||||
return env
|
||||
var derived := OS.get_executable_path().replace(".exe", "_console.exe")
|
||||
if FileAccess.file_exists(derived):
|
||||
return derived
|
||||
return ""
|
||||
|
||||
|
||||
func _has_script_error(text: String) -> bool:
|
||||
return _script_error_line(text) != ""
|
||||
|
||||
|
||||
func _script_error_line(text: String) -> String:
|
||||
for ln in text.split("\n"):
|
||||
var s := String(ln).strip_edges()
|
||||
if s.is_empty():
|
||||
continue
|
||||
for marker in ERROR_MARKERS:
|
||||
if s.contains(marker):
|
||||
return s.substr(0, mini(s.length(), 160))
|
||||
return ""
|
||||
@@ -11,6 +11,7 @@ extends SceneTree
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const StatusContainerScript := preload("res://core/status_container.gd")
|
||||
const UnitContainerScript := preload("res://core/unit_container.gd")
|
||||
const DamageControllerScript := preload("res://core/damage_controller.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
@@ -86,23 +87,13 @@ func _test_ticket_nine_columns() -> void:
|
||||
|
||||
func _bare_controller():
|
||||
var db = ConfigDBScript.new()
|
||||
# 只取 open_ticket 纯构造面——容器/依赖传桩(构造期不消费)
|
||||
var units = UnitContainerStub.new()
|
||||
var effects = StatusContainerStub.new()
|
||||
return DamageControllerScript.new(db, RngStub.new(), units, effects)
|
||||
|
||||
|
||||
# 桩容器:构造期零消费(DamageController._init 只 connect unit_died 信号——桩需同名信号)
|
||||
class UnitContainerStub:
|
||||
signal unit_died(u: Dictionary)
|
||||
|
||||
|
||||
class StatusContainerStub:
|
||||
pass
|
||||
|
||||
|
||||
class RngStub:
|
||||
pass
|
||||
# 只取 open_ticket 纯构造面——四个依赖一律传真实实例,不用空桩:
|
||||
# DamageController 的 db/rng/units/effects 全是定型标注,传无类型的空桩会触发
|
||||
# 「Trying to assign value of type '' to a variable of type '…'」脚本错误——该错误不改退出码,
|
||||
# 却让赋值静默失败(成员留 null),脚本错误扫描落地后必红。三者均可零消费构造。
|
||||
var units = UnitContainerScript.new(db)
|
||||
var effects = StatusContainerScript.new(db)
|
||||
return DamageControllerScript.new(db, RngServiceScript.new(), units, effects)
|
||||
|
||||
|
||||
# ---- B4→B3:增益授予布尔受理口径(受理/拒绝二元面 + 投影一致性) ----
|
||||
|
||||
@@ -193,23 +193,29 @@ func _test_manifest_dir_sync(db) -> void:
|
||||
# (rowset_rule——行序即契约,R6 加载期兜底)③行集实例总数 ≤4(骰后值运行面,spawn 守卫——冒烟侧验证)。
|
||||
|
||||
func _test_rowset_discipline() -> void:
|
||||
# 行集用 rows.assign([...]) 而非直接赋值:rows 是 Array[Dictionary] 定型属性,直接赋 untyped
|
||||
# 数组字面量会触发「Invalid assignment」脚本错误——该错误不改退出码,却会**中断本函数剩余断言**
|
||||
#(本函数五条纪律断言曾因此全部静默不跑,套件仍报 all green,是零覆盖假象)。
|
||||
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
|
||||
var mk_row := func(l: int, d: int, eid: String) -> Dictionary:
|
||||
return {"level_index": l, "node_depth": d, "enemy_id": eid, "display_name": eid + "名",
|
||||
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
# 正向:同坐标两行不同敌种(相邻)= 混编合法,validate 零错
|
||||
var t_mix = EnemyTableScript.new()
|
||||
t_mix.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "bugbear")]
|
||||
t_mix.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "bugbear"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(validate 的 anchor 规则:每 level 必须有)
|
||||
var errs: Array = t_mix.validate()
|
||||
Kit.check("混编行集加载通过(同坐标两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty())
|
||||
# 正向:Boss 坐标两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸)
|
||||
var t_boss = EnemyTableScript.new()
|
||||
t_boss.rows = [mk_row.call(2, 9, "drider"), mk_row.call(2, 9, "bugbear")]
|
||||
t_boss.rows.assign([mk_row.call(2, 9, "drider"), mk_row.call(2, 9, "bugbear"),
|
||||
mk_row.call(2, 0, "drider")]) # 末行=depth-0 锚行
|
||||
errs = t_boss.validate()
|
||||
Kit.check("Boss 坐标混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty())
|
||||
# 负向:同坐标行不相邻 = rowset_rule 且报错定位到坐标(R6 行序纪律 + R3 错误类别可定位)
|
||||
var t_gap = EnemyTableScript.new()
|
||||
t_gap.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 9, "hobgoblin"), mk_row.call(1, 5, "bugbear")]
|
||||
t_gap.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 9, "hobgoblin"), mk_row.call(1, 5, "bugbear"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(隔断关系不受影响)
|
||||
errs = t_gap.validate()
|
||||
var gap_hit := false
|
||||
for e in errs:
|
||||
@@ -218,7 +224,8 @@ func _test_rowset_discipline() -> void:
|
||||
Kit.check("行序相邻校验:同坐标行被隔断报 rowset_rule 且定位坐标(R6——行序即契约加载期兜底)", gap_hit)
|
||||
# 负向:同敌种第二行仍被 dup_key 拦(键含 enemy_id——纪律①既有通道不回归)
|
||||
var t_dup = EnemyTableScript.new()
|
||||
t_dup.rows = [mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "goblin")]
|
||||
t_dup.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "goblin"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行
|
||||
errs = t_dup.validate()
|
||||
var dup_hit := false
|
||||
for e in errs:
|
||||
@@ -227,7 +234,7 @@ func _test_rowset_discipline() -> void:
|
||||
Kit.check("行集内同敌种第二行仍被 dup_key 拦(纪律①——enemy_id 唯一)", dup_hit)
|
||||
# 负向:域校验不回归(单行 unit_count 越域仍拦——UNIT_COUNT_MAX 单种数语义下的同款域闸)
|
||||
var t_dom = EnemyTableScript.new()
|
||||
t_dom.rows = [mk_row.call(1, 5, "goblin")]
|
||||
t_dom.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行
|
||||
t_dom.rows[0]["unit_count"] = 4
|
||||
errs = t_dom.validate()
|
||||
var dom_hit := false
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
# Kit.check("断言描述", cond) # 计数 + 即时 PASS/FAIL 行
|
||||
# Kit.check_quiet(cond, "描述") # 计数 + 失败收集(finish 时统一汇总——原 _problems 风格,原参序)
|
||||
# quit(Kit.finish("suite_name")) # 汇总输出 + 返回退出码(0 全绿 / 1 有败)
|
||||
# quit(Kit.finish("suite_name")) # 汇总输出 + 返回退出码(0 全绿 / 1 有败 / 2 零断言)
|
||||
# 计数器为 static(套件 = 独立 Godot 子进程恰一份生命周期,见 run_all.gd 执行形态);
|
||||
# 单进程跑多套件须先 Kit.reset()。
|
||||
extends RefCounted
|
||||
@@ -39,6 +39,12 @@ static func check_quiet(ok: bool, desc: String) -> void:
|
||||
#(保留原套件 PASS 摘要文案;缺省 = "all green (N checks)" 计数口径)。
|
||||
# 失败路径:push_error 计数汇总 + problems 逐条 FAIL 行(print + push_error 双通道——原两风格并档)。
|
||||
static func finish(suite: String, pass_line: String = "") -> int:
|
||||
# 断言数下限(三硬闸之三):零断言的套件必须判红——「跑了一遍没崩」不等于测过。
|
||||
# 退出码用 2(而非 1)与「断言失败」区分:致命类套件 manifest 期望 1,
|
||||
# 零断言若也返回 1 会被 expect 协议当成预期结果放过。
|
||||
if checks == 0:
|
||||
push_error("[%s] 零断言——套件未执行任何断言(疑似脚本错误中断或误挂空壳),判红" % suite)
|
||||
return 2
|
||||
if failures > 0:
|
||||
push_error("[%s] %d/%d 项失败" % [suite, failures, checks])
|
||||
for p in problems:
|
||||
|
||||
@@ -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())
|
||||
+44
-45
@@ -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,32 @@ 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
|
||||
|
||||
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 +125,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 "======== 残留检查(批末闸) ========"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user