Files
gplan-2/game/tests/run_all.gd
T
DKChan d091c98163 feat(迭代29 T5): L1 覆盖补洞——4 个零指名 core 模块 + 呈现层两件直接单测
新增六套(run_all 27 → 33 套),全部进 manifest,全绿:
· core/board_generator_test(9 条):尺寸与 board_gen 表一致、每个安全格数字恒等于八邻雷数
  (全盘逐格复算)、同种子雷集/数字集逐位一致、可推理性度量与清图判定自洽。
· core/minesweeper_rules_test(12 条):装板带初始扩张揭示且未铺满安全格、安全格余量=总数−已揭示、
  挖已揭示格拒(target_revealed)、挖非相邻格拒(not_adjacent)、fx_reveal 推进揭示并递减余量、
  插旗落旗态与计数、目录外意图被拒。
· core/config_db_resolver_test(8 条):ceil 进度键偏难语义(请求 (1,1) 命中 (1,3) 而非 (1,0))、
  行集按表文件行序返回、ceil 行集、难度乘数 clamp 上下限、未登记坐标返回空修饰。
· core/config_db_cross_validator_test(4 条):正向全对齿零错(防误伤)、悬空 skill_id 必报、
  buff 挂主角侧状态必报(cast_side)。
· presentation/hud_presenter_test(5 条)+ battle_narrator_test(9 条):文案逐拒绝原因正确、
  兜底不吐空串、数值带入、胜负互异、关卡收束态恒定、Boss 名带入。
三屏壳的断言面由 T4 screen_flow_probe 承担(六步流程每步判屏级互斥),不另起第二载体。

变异验证:hud_presenter 的「AP 不足。」改漏句号 → hud_presenter_test 精确见红。
另有一批「写反的断言被实现抓出」的证据(首跑即红、逐条订正后转绿):装板并非零揭示而是带初始扩张、
挖受英雄邻接约束、插旗需 flag_range_free 或相邻且要先给足 AP、技能 type 是 basic 不是 attack、
状态表 id 键是 effect_id 不是 status_id、battle_end_text 在 level_end=true 时恒为「关卡通关!」。

顺带实证本批核心命题:config_db_resolver_test 首跑因诊断回调的 lambda 形参类型写错
(首参是表 id 字符串而非 int)触发 Callable 调用失败——脚本错误不改退出码却让整个解析函数静默
返回 null,套件仍会报「全绿」。正是脚本错误扫描存在的原因。
2026-10-16 15:55:33 +08:00

146 lines
10 KiB
GDScript
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.
# 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}, # 战斗叙述(数值带入/胜负互异/收束态恒定)
]
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 ""