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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user