标识符面:flag_*→seal_ 族(含 Godot 信号 flag_judged→seal_judged 与连接/回调同名、 FACTS 两键 flags_count→seals_count/dug_under_flag→dug_under_seal 全链、 .tres 键名与 schema .gd 同批同步);显式豁免零改动=size_flags_*(Godot 布局属性)+_fatal_flag。 中文面:金币→GP(金币袋→GP 袋)/插旗→封印/雷格→危险格/惩罚格→危险格/旗→封印·封印印记族, game/ 内 *.gd/.tres/.md/.json 四类禁用词与 flag 双双零命中(V1/V4)。 守卫字面量不动(被检查的禁字不是使用):smoke_all V21 零「雷/旗」检查面、 ui_font_asset_test 字形集攻/雷/旗/封 保持原断言逻辑。 代拍板:板面单字槽 glyph「旗」→「封」(禁字纪律+单字位宽,消费面四处同名随动)。 素材换代(2026-10-18 用户裁案甲):assets/art/board/flag_seal.png→seal_mark.png(素材不入 git, 盘上改名+导入重刷),代码两处 res:// 字符串+COL_FLAG/CURSOR_FLAG 常量同步, 闸件四处随批(run_all_gates.sh/render_gate.sh/golden_baseline_refresh.sh/path_check.py)。 anchor.json 键名手改,重锚与改前改后 diff 随本卡收口(同裁案甲)。 基线读数(改前,主区 ed2dfaf6):冒烟 1151 检查 0 失败/单元 34 套全绿/探针 --click 存量红仅 screen_flow_probe(user:// 断点污染在册已知项)。 改后同批读数(本 worktree):冒烟 1151 检查 0 失败/探针 --click 同一存量红、其余全绿。
147 lines
11 KiB
GDScript
147 lines
11 KiB
GDScript
# tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b)
|
||
# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …)
|
||
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
|
||
# 执行形态:逐套件起独立 Godot 子进程实跑
|
||
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
|
||
# 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。
|
||
# 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。
|
||
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, 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)
|
||
return
|
||
print("[run_all] all green (%d suites)" % _suites().size())
|
||
quit(0)
|
||
|
||
|
||
# 套件清单:{ "path": String, "expect": int, "negative"?: bool };致命类(期望非 0)独立进程断言退出码。
|
||
func _suites() -> Array:
|
||
return [
|
||
# 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——真视窄域护栏
|
||
# 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——断点往返/续玩确定性/幂等/退出清断点
|
||
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 2026-10-06——三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
|
||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # 迭代 8 卡⑦ AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 迭代 7 hotfix——奖励节点 UI 直验(盲区补漏)
|
||
{"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 迭代 23 锻造工坊批 T6——工作间两区六路线 UI 直验(V13 自动化前置面)
|
||
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
|
||
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
|
||
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # 迭代14④ T6——Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
|
||
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
|
||
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
|
||
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
|
||
# fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面)
|
||
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0},
|
||
{"path": "res://tests/unit/core/treasure_effects_test.gd", "expect": 0}, # 迭代 22 秘宝批 T3——新目录 12 件逐件行为(V7 附属表正常/边界/反例)
|
||
{"path": "res://tests/unit/core/tree_rules_v11_test.gd", "expect": 0}, # 迭代 21 天赋树批 V11——六键合成行闭集校验 # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
|
||
{"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 机制位批 2026-10-13——requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态)
|
||
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
|
||
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
|
||
{"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 迭代 27 T2——结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序)
|
||
{"path": "res://tests/unit/presentation/modal_flow_test.gd", "expect": 0}, # 迭代 27 T4——模态编排纯逻辑(V6:直接 new/互斥域谓词/三例外锁/演播后排窗)
|
||
# ---- L2 集成层(迭代 29 T3 建承载——跨系统用例,与 unit/ 的隔离断言面分工)----
|
||
{"path": "res://tests/integration/save_rng_roundtrip_test.gd", "expect": 0}, # 存档⇄RNG 快照往返(SaveManager 迁移链 × 断点消费 × RNG 续跑 × 停点态确定性)
|
||
# ---- 迭代 29 T5 覆盖补洞:此前零指名的 core 四件与呈现层两件 ----
|
||
{"path": "res://tests/unit/core/board_generator_test.gd", "expect": 0}, # 棋盘生成(尺寸合表/数字=八邻雷数/同种子逐位一致/可推理自洽)
|
||
{"path": "res://tests/unit/core/minesweeper_rules_test.gd", "expect": 0}, # 排雷规则(装板不变量/挖的 face 规则/揭示推进/封印/目录外拒)
|
||
{"path": "res://tests/unit/core/config_db_resolver_test.gd", "expect": 0}, # 配置解析(ceil 偏难语义/行集行序/难度 clamp)
|
||
{"path": "res://tests/unit/core/config_db_cross_validator_test.gd", "expect": 0}, # 跨表校验(正向零错/悬空 skill_id/side 反向)
|
||
{"path": "res://tests/unit/presentation/hud_presenter_test.gd", "expect": 0}, # HUD 投影文案(逐拒绝原因/兜底不空)
|
||
{"path": "res://tests/unit/presentation/battle_narrator_test.gd", "expect": 0}, # 战斗叙述(数值带入/胜负互异/收束态恒定)
|
||
{"path": "res://tests/unit/presentation/type_glow_test.gd", "expect": 0}, # 迭代 30 卡① T2-②——未开格类型微光数据面(九型归组封闭/清单外显式失败/冻结色相只准动明度/α170·带宽cell÷8锚定/合成空间阶梯单调+模型CR≥1.20/probe API 形状)
|
||
]
|
||
|
||
|
||
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 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("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false)
|
||
var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else ""
|
||
var negative := bool(suite.get("negative", false))
|
||
# 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行——
|
||
# 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。
|
||
if negative:
|
||
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
|
||
else:
|
||
print(text)
|
||
if not negative 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 ""
|