建 game/tests/integration/(此前只有章程句无实体):首个集成用例 save_rng_roundtrip_test.gd =存档 ⇄ RNG 快照往返,17 条断言,跨 SaveManager(落盘+版本迁移链)× AdventureRunner(断点消费) × RngService(续跑零跳号)× 停点态确定性。与 unit/core/breakpoint_resume_test 的分工:那件是单测 只验首战开板一致,本件验跨系统闭环——含「旧版本档经迁移链后断点仍可续玩」这一真实玩家路径。 并入 run_all manifest(27 套,零新机制)。 变异验证:把断点 rng_counter 改坏 → 精确命中「续玩取数号位 = 断点号位」断言见红。 tools/run_all_gates.sh 新增 11 号「回放确定性对拍」**条件闸件**:每次跑批都对 merge-base 做一遍 触面判定(data/tables/rng_service/save_manager/meta_progression/settlement/damage_math/ damage_controller/battle_orchestrator/adventure_monte_carlo),未触面 SKIP;触面也**不自动跑**—— 对拍调的是 MC 脚本,属「跑批发起前须用户批准」红线,闸只负责把触面判定做一遍并显式要求批准 (批准后 PARITY_APPROVED=1 重跑),杜绝「该跑却没人记得」。指纹逻辑把 SKIP/NEED-APPROVAL 同记 skip, 两种未跑状态互变不算闸况变化。baseline.json 登记 11 闸条目(新增闸件否则会判「无基线」而红)。 README:目录布局增 integration/;新增「测试三层与承载目录」节(L1/L2/L3 各自的判什么/承载/入闸方式 与分层判据);integration 章程句由「将来落地」改为「已建承载」。
116 lines
8.4 KiB
Markdown
116 lines
8.4 KiB
Markdown
# Test Infrastructure — Dungeon Sweeper
|
||
|
||
**Engine**: Godot 4.7.2-stable(GDScript 单语言)
|
||
**Test Framework**: 双载体——`--headless --script` 单元套件 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节)
|
||
**门禁**: 本地闸——`tools/run_all_gates.sh`(全量,收口唯一验收入口)/`--quick`(中段自检)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单只在本脚本定义,文档不另列
|
||
**Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/dev/`、production 解散)
|
||
**参考实现**: `game/smoke/`(冒烟断言 + 场景探针族——正式工程的活样板;断言计数双口径见下节)
|
||
|
||
## Directory Layout
|
||
|
||
```
|
||
tests/
|
||
unit/ # 隔离单元测试(公式/状态机/纯逻辑)——extends SceneTree + quit(code)
|
||
integration/ # L2 集成用例(跨系统闭环——与 unit/ 的隔离断言面分工,见下「分层」节)
|
||
smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取)
|
||
evidence/ # 截图与人工签核记录
|
||
run_all.gd # 单元套件统一入口(套件 manifest 协议,D2 AC-2b)
|
||
static_scan.sh # 静态扫描域 S1–S8 可执行子集
|
||
```
|
||
|
||
## 测试三层与承载目录
|
||
|
||
| 层 | 判什么 | 承载 | 入闸方式 |
|
||
|---|---|---|---|
|
||
| **L1 单元** | 最单元的逻辑代码对不对(公式/状态机/纯逻辑) | `tests/unit/` | `run_all.gd` manifest(闸 02) |
|
||
| **L2 集成** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元套件同一入口、同一协议) |
|
||
| **L3 系统/场景** | 跑起来之后 UI 与场景切换对不对 | `game/smoke/*.tscn` 探针 | `probe_gate.sh`(闸 03/04) |
|
||
|
||
> **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。
|
||
> 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里,
|
||
> 回放确定性已由 `tools/parity_anchor.py` 承担并收编为重闸 11 号条件闸件;
|
||
> Core-Core 与 A1×A2 端到端仍随属主批增量登记;各 GDD 登记的 deferral 集成用例以对侧登记为生效条件。
|
||
>
|
||
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = L1;
|
||
> 需要两个以上真实系统协同并走落盘/落库 = L2;需要完整项目引导与场景树 = L3。
|
||
|
||
## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正)
|
||
|
||
历史 README 写死「752+ 断言/十五套件」必然漂移,改为两种口径并注明语义:
|
||
|
||
- **运行时口径**:`python tools/run_smoke.py` 打印的通过项数(迭代 19 卡①分支基点实测 **840 项 0 失败**,2026-10-08;master 合并卡②④后为 846——随断言面增减浮动,以当次跑测打印为准)——循环内断言按执行次数计。
|
||
- **静态口径**:`game/smoke/` 源码 `check(` 调用点计数(2026-10-08 本分支实测 **764 处**)——按源码位置计,一处在循环内即多次执行,故两口径数值不同且各自浮动,禁互相换算或写死。
|
||
|
||
## Running Tests
|
||
|
||
```bash
|
||
# 单元套件(双载体之一)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层)
|
||
godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点)
|
||
godot --headless --script tests/unit/example_test.gd
|
||
|
||
# 场景探针(双载体之二)——以探针场景为运行目标,走完整项目引导(autoload 在位)
|
||
godot --headless --path . res://smoke/<probe>.tscn
|
||
|
||
# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置)
|
||
timeout 120s godot --headless --script tests/run_all.gd
|
||
```
|
||
|
||
> 单元/契约套件状态(单元测试与契约层批 2026-09-28 立基,后随迭代 7/8 增补):`_suites()` manifest 已登记实跑 **26 套件**(2026-10-16 迭代 29 T1:样板件 `example_test.gd` 退出生产 manifest,27 → 26;`fog_lantern_wiring_test` 随提灯载体退役摘除在前)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/提灯装配链)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集);致命类独立进程 `expect=1`。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。
|
||
|
||
**协议要点**(D2 冒烟协议):
|
||
- 冒烟脚本 `extends SceneTree` 以获得 `quit(code)`;成功 `quit(0)`、失败 `quit(1)`
|
||
- `--script` 模式**不加载 autoload 且不注册全局标识符**(裸引用 `ConfigDB` = parse error)——ConfigDB/RngService 由脚本显式实例化,core 一律构造注入
|
||
- 致命类用例独立进程,runner manifest 登记"调用 → 期望退出码"
|
||
- 每用例先 `reset_diagnostics()`(诊断收集器协议)
|
||
- 期望值由表读值推导,不写死玩法数值
|
||
|
||
## Test Naming
|
||
|
||
- **Files**: `[system]_[feature]_test.gd`(领域命名,禁 ID 前缀——design/dev/code-conventions.md)
|
||
- **Functions**: `test_[scenario]_[expected]`
|
||
- **Example**: `reward_roll_test.gd` → `test_power_interval_rolls_within_bounds()`
|
||
|
||
## Story Type → Test Evidence
|
||
|
||
| Story Type | Required Evidence | Location |
|
||
|---|---|---|
|
||
| Logic | Automated unit test — must pass | `tests/unit/[system]/` |
|
||
| Integration | Integration test OR playtest doc | 落地时随批建 `tests/integration/[system]/`(目录已删空壳,章程见上) |
|
||
| Visual/Feel | Screenshot + lead sign-off | `tests/evidence/` |
|
||
| UI | Manual walkthrough OR interaction test(场景探针为首选) | `tests/evidence/` |
|
||
| Config/Data | Smoke check pass | `design/qa/` |
|
||
|
||
## Static Scan(提交前本地跑)
|
||
|
||
`tests/static_scan.sh` 实现静态扫描域 S1–S8 的可执行子集;规则全集与动机见
|
||
`design/dev/architecture/architecture.md`「测试与质量域」节。新违规案例必须**先补扫描规则再修码**。
|
||
|
||
## 本地闸(唯一验收门禁)
|
||
|
||
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh`(全量)是收口唯一入口,
|
||
跑完写回执 `game/tests/.gates/receipt.json`,合回 master 由 worktree-guard 校验回执。
|
||
闸清单的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
|
||
|
||
**判定三硬闸**(各闸一律适用,缺一即判定有洞):
|
||
①**退出码为唯一权威判据**——日志里的 PASS 串只是日志,不作判据;
|
||
②**脚本错误扫描命中即红**——GDScript 的 `SCRIPT ERROR` / `Parse Error` 不改进程退出码、
|
||
不计失败数,且会静默中断所在函数的剩余断言,只能由外部扫输出(闸件 `tools/error_scan.py`,
|
||
错误标记与 `tools/run_smoke.py` 同源);
|
||
③**断言数为零即红**——`test_kit.gd` 的 `finish()` 在零断言时返回专用退出码 2(与「断言失败=1」
|
||
区分,避免致命类套件的 expect 协议把它当成预期结果放过)。
|
||
|
||
**gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有双载体(`--script` 套件 + 探针场景)内解决。
|
||
|
||
## 术语(本项目内的唯一语义)
|
||
|
||
| 词 | 语义 | 载体 |
|
||
|---|---|---|
|
||
| **冒烟(smoke)** | **秒级存活检查**——能否启动、能否加载表、能否进主屏。判断「这版还活着吗」,不判断「这版对不对」 | `game/smoke/` 的存活面 |
|
||
| **单元套件** | 隔离的公式/状态机/纯逻辑断言面,逐套件独立进程实跑 | `game/tests/unit/`、`run_all.gd` |
|
||
| **场景探针** | 以探针场景为运行目标、走完整项目引导(autoload 在位)的 UI/场景断言 | `game/smoke/*.tscn`、`probe_gate.sh` |
|
||
| **集成用例** | 跨系统测试(Core-Core/回放确定性/存档⇄RNG 往返/端到端) | `game/tests/integration/` |
|
||
|
||
> ⚠ **两处历史遗留命名,读代码时别被字面骗了**:`tools/run_smoke.py` 之名来自冒烟,但**它承载的是
|
||
> 系统级断言面**(1127 项检查,远不止存活检查);`game/smoke/` 之名同理,里面既有存活面也有
|
||
> 系统级断言与场景探针。判定「是不是冒烟」看语义,不看文件名。
|