本机首次具备引擎自验证能力后的首跑修复。四条命令 + 主场景 180 帧全绿: - --import exit=0(漂移门 git diff data/build 空) - --validate-only exit=0 RESULT: PASS - test_runner exit=0 PASS(584 检查,8/8 用例) - sim_main exit=0(报告落 tests/sim_reports/report-2026-10-20.json) - 主场景 --quit-after 180 exit=0 零 ERROR/WARNING(F5 可玩) 预定位错误(team-lead 清单): - R1 economy_state.gd:596 enthone_ready -> enthrone_ready(单点故障连锁) - R2 hud_root.gd:142,160 HorizontalAlignment.LEFT -> HORIZONTAL_ALIGNMENT_LEFT - R3 boss_arena.gd 补 var _pending_hall_index := -1 - R4 hall_band_world.gd:95 删 "_ = ext"(非法语句;--check-only 定位) - R5 sheet_importer._type_ok 补 object->Dictionary / array->Array(9 条假违规) - R6 game_state.gd econ==null 防护(防 autoload 崩每帧刷错打满管道挂死) 引擎新暴露错误: - R7 prestige_model floor() Variant 推断(4.7.2 按错误)连锁编译失败 - R8 gates.gd var pass 保留字;R9 %g/%.*f 格式符 ×6 处 - R10 greedy.gd Dictionary.get() 推断;R11 probe_p1 条件表达式推断 - R12 测试用例 data 参数统一标 BalanceData(无类型参数成员调用返 Variant) - R13 boss_battle 用例 Dictionary 下标运算显式 float 化 - R14 band_world_root.compute_ranges 返回 Array[Vector2](类型化数组不隐式转换) 测试侧数值修正(根因已核实,实现符合设计): - gen3 可购断言 merit 1000->2000(800 文书 + 1200 首购) - 开场伤害断言改"循环至 ACTIVE 即停"(INTRO 结束 tick 不计军列伤) - 容量断言预算 30->34s(战斗真实给 intro 3s + active 30s) 工程加固: - EconomyState 红线守卫首次越线才报(sim 长跑日志 6.5MB->1KB) - stage_world.gd stretch=false(SubViewport 手动尺寸驱动生效) 知识缺口登记:docs/engineering/2026-10-20-f-engine-first-run-notes.md sim 首轮数据:主口径 11/14 过(#2/#3/#12/#13/#14 FAIL 待文策渊裁定)
86 lines
3.0 KiB
GDScript
86 lines
3.0 KiB
GDScript
extends SceneTree
|
||
|
||
# ============================================================================
|
||
# test_runner.gd —— core 层无头单测入口(GUT 未装前的自包含方案;W3 换 GUT)
|
||
#
|
||
# 运行:godot --headless --path . -s res://tests/unit/test_runner.gd
|
||
# 退出码:0 = 全过;1 = 有失败;2 = 数据/环境错误。
|
||
# 每个用例文件 = tests/unit/cases/*.gd,契约:func run(a) -> void(a = TestAssert)。
|
||
# ============================================================================
|
||
|
||
class TestAssert extends RefCounted:
|
||
var failures: Array[String] = []
|
||
var checks := 0
|
||
|
||
func check(cond: bool, msg: String) -> void:
|
||
checks += 1
|
||
if not cond:
|
||
failures.append(msg)
|
||
|
||
func eq(got: Variant, want: Variant, msg: String) -> void:
|
||
checks += 1
|
||
if got != want:
|
||
failures.append("%s:got %s want %s" % [msg, str(got), str(want)])
|
||
|
||
func near(got: float, want: float, tol: float, msg: String) -> void:
|
||
checks += 1
|
||
# %g 为 C 风格格式符,Godot String % 不支持(首跑实证)→ str()
|
||
if absf(float(got) - float(want)) > tol:
|
||
failures.append("%s:got %s want %s(tol %s)" % [msg, str(got), str(want), str(tol)])
|
||
|
||
func true_(cond: bool, msg: String) -> void:
|
||
check(cond, msg)
|
||
|
||
func false_(cond: bool, msg: String) -> void:
|
||
check(not cond, msg)
|
||
|
||
|
||
const CASE_FILES: Array[String] = [
|
||
"res://tests/unit/cases/test_number_format.gd",
|
||
"res://tests/unit/cases/test_economy_formulas.gd",
|
||
"res://tests/unit/cases/test_balance_data.gd",
|
||
"res://tests/unit/cases/test_economy_state.gd",
|
||
# W3 新增:相机 rig 数据行与纯数学 / 事件系统运行时 / 殿王战状态机
|
||
"res://tests/unit/cases/test_camera_data.gd",
|
||
"res://tests/unit/cases/test_event_director.gd",
|
||
"res://tests/unit/cases/test_boss_battle.gd",
|
||
# W4 新增:离线契约 v2 + 门 #9 三条子检查(P-8 裁定落地)
|
||
"res://tests/unit/cases/test_offline_contract.gd",
|
||
]
|
||
|
||
|
||
func _initialize() -> void:
|
||
var data := BalanceData.load_from_dir("res://data/build/")
|
||
if not data.load_errors.is_empty():
|
||
for e in data.load_errors:
|
||
printerr("test_runner:数据错误:%s" % e)
|
||
quit(2)
|
||
return
|
||
var total_checks := 0
|
||
var total_fail: Array[String] = []
|
||
for path in CASE_FILES:
|
||
var script: Variant = load(path)
|
||
if script == null:
|
||
printerr("test_runner:用例缺失 %s" % path)
|
||
quit(2)
|
||
return
|
||
var case_obj: Variant = script.new()
|
||
if not (case_obj is RefCounted) or not case_obj.has_method("run"):
|
||
printerr("test_runner:用例契约不符 %s" % path)
|
||
quit(2)
|
||
return
|
||
var a := TestAssert.new()
|
||
case_obj.run(a, data)
|
||
total_checks += a.checks
|
||
for f in a.failures:
|
||
total_fail.append("%s → %s" % [path.get_file(), f])
|
||
print("test_runner:%s:%d 检查,%d 失败" % [path.get_file(), a.checks, a.failures.size()])
|
||
if total_fail.is_empty():
|
||
print("test_runner:PASS(%d 检查全过)" % total_checks)
|
||
quit(0)
|
||
return
|
||
print("test_runner:FAIL(%d/%d)" % [total_fail.size(), total_checks])
|
||
for f in total_fail:
|
||
printerr(" ✗ %s" % f)
|
||
quit(1)
|