合并 master(21cd0f2e) 进 iter40: 接回挂死防护·probe_gate件批(run_all_gates/probe_gate/run_all.gd超时守护/undo_proof/reshape_cards工具+process-conventions/architecture/test-conventions规约面+待办清账)——iter40侧同改文件仅run_all.gd(manifest wr8登记行),与守护改动异段预期自动合并

This commit is contained in:
DKChan committed 2026-10-28 04:18:32 +08:00
commit 185bf81560
10 files changed
+516 -44

No files matched your search

+4 -3
View File
@@ -58,7 +58,7 @@ tests/
|---|---|---|---|
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(40 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 |
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
@@ -75,6 +75,7 @@ python tools/run_smoke.py
# 单元与集成测试(闸 02)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层)
godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点)
godot --headless --script tests/run_all.gd -- --only=battle_settlement # 选跑态——只跑路径含该词的套件(开发期最窄确认;零命中=用法错 exit 2,不产闸回执)
godot --headless --script tests/unit/example_test.gd
# 系统测试(闸 01)——过渡期宿主 smoke_all.gd;分钟级
@@ -87,8 +88,8 @@ godot --headless --path . res://probe/<probe>.tscn
timeout 120s godot --headless --script tests/run_all.gd
```
> 套件 manifest 现况:`_suites()` 登记 **40 条**(39 单元+1 集成,2026-10-22 实数;口径=manifest 条目数,
> 随套件增减浮动)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/存档槽位/结算五段等)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集/模态编排等);集成 1 条存 `tests/integration/`。致命类独立进程 `expect=1`;负面套件 `negative=true` 只豁免错误扫描一闸。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。
> 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数,
> 随套件增减浮动)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/存档槽位/结算五段等)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集/模态编排等);集成 3 条存 `tests/integration/`。致命类独立进程 `expect=1`;负面套件 `negative=true` 只豁免错误扫描一闸。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。
**协议要点**(D2 冒烟协议):
- `--script` 载体 `extends SceneTree` 以获得 `quit(code)`;成功 `quit(0)`、失败 `quit(1)`
+52 -9
View File
@@ -1,5 +1,8 @@
# tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b)
# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …)
# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件;
# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据;
# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
# 执行形态:逐套件起独立 Godot 子进程实跑
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
@@ -13,24 +16,51 @@ const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"]
func _init() -> void:
print("[run_all] unit/contract suite entry — %d suites" % _suites().size())
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。
# 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿);选跑态仅供开发期最窄确认,
# 不产闸回执、不作验收凭据;批末全量闸不带参数,不走此分支。
var only_terms := PackedStringArray()
var has_only := false
for a in OS.get_cmdline_user_args():
if a.begins_with("--only="):
has_only = true
var term := a.trim_prefix("--only=").strip_edges()
if term != "":
only_terms.append(term)
if has_only and only_terms.is_empty():
push_error("[run_all] --only= 空值——用法错(选跑态禁把零套件当全量)")
quit(2)
return
var manifest := _suites()
var suites := manifest
var filtered := false
if has_only:
suites = _filter_suites(manifest, only_terms)
if suites.is_empty():
push_error("[run_all] FILTERED RUN — --only=「%s」零命中(manifest 共 %d 套件)——用法错" % [",".join(only_terms), manifest.size()])
quit(2)
return
filtered = true
print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)])
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)
for suite in suites:
failures += _run_suite(suite, ran, filtered)
ran += 1
# 实跑套件数校验:manifest 有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。
if ran != _suites().size():
push_error("[run_all] 实跑套件数 %d ≠ manifest 条目数 %d——有套件未跑" % [ran, _suites().size()])
# 实跑套件数校验:清单有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。
# 选跑态对过滤后清单校验;全量态与原 manifest 条目数同一。
if ran != suites.size():
push_error("[run_all] 实跑套件数 %d ≠ 清单条目数 %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())
print("[run_all] all green (%d suites%s)" % [suites.size(), ",filtered" if filtered else ""])
quit(0)
@@ -112,7 +142,7 @@ func _suites() -> Array:
]
func _run_suite(suite: Dictionary, idx: int) -> int:
func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
var path: String = suite.get("path", "")
var expect: int = int(suite.get("expect", 0))
if path.is_empty():
@@ -126,7 +156,7 @@ func _run_suite(suite: Dictionary, idx: int) -> int:
# 子进程输出只能落盘再读: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
var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", 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()
@@ -149,6 +179,19 @@ func _run_suite(suite: Dictionary, idx: int) -> int:
return 0
# 选跑过滤:对套件 res:// 路径做子串匹配,任一词命中即选入,词序无关。
# 只在选跑态调用——全量态走原 manifest,条目与顺序零变化。
func _filter_suites(manifest: Array, terms: PackedStringArray) -> Array:
var out: Array = []
for suite in manifest:
var path: String = suite.get("path", "")
for t in terms:
if path.contains(t):
out.append(suite)
break
return out
# 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe
#(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢,
# 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。