漏改即成第二份互相矛盾的清单(本批正是要消灭这种漂移): - roadmap.md 迭代循环骨架第 5 步「重闸=全量档全绿」→ 按改动域选档;第 8 步收口五步③ 「里程碑移动更新本文件」加注:实现批禁写 roadmap,该步归文档批,实现批在 reports/ 反哺点 列明该改哪一行。 - game/tests/README.md 门禁行与「本地闸」节的「(全量)唯一入口」→ 按域选档 + 回执含 domain + 合回闸按 diff 推域校验。 连带(仓库外本地配置,同批已改):~/.zcode/skills/req-card/SKILL.md 六处——description 版本注、 §5 关联文档同步面(实现批禁写 design/、产出只落 reports/、07 入册转反哺点)、§6 末卡改「产出物 落位卡」、§7 闸口径改按域并禁卡内写死闸件名单、目检两分法落点、变异验证记录表落点。
143 lines
11 KiB
Markdown
143 lines
11 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`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量;`--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/` |
|
||
|
||
## game/smoke/ 游离脚本台账(迭代 29 T7 逐个定性)
|
||
|
||
`game/smoke/` 有 29 个 `.gd`,其中 14 个在闸(探针有 `.tscn` 宿主、被 `probe_gate.sh`/`run_smoke.py` 调用),
|
||
其余 15 个**不带 `.tscn`、不入任何闸**——它们是手动入口的测量/诊断件。逐个定性如下,
|
||
判据 = 头注自陈用途 + 有无外部引用(工具脚本/产品码/文档)。**存疑一律按在役登记并标注**(不退役)。
|
||
|
||
| 脚本 | 定性 | 用途与触发方式 | 结果由谁判 |
|
||
|---|---|---|---|
|
||
| `adventure_monte_carlo.gd` | **在役**(`CLAUDE.md` 在册入口) | 全冒险期望 MC;`tools/parity_anchor.py` 的对拍载体 | 工具脚本逐位比对 |
|
||
| `verifier_batch.gd` | **在役**(`CLAUDE.md` 在册入口) | `tools/balance_verifier.py` 的采集端(数值验证) | 报告落 `design/qa/` |
|
||
| `reward_expectation.gd` | **在役**(`CLAUDE.md` 在册入口) | 奖励期望测量(非封锁格分布/资源类型频次) | 人工读 stdout |
|
||
| `monte_carlo.gd` | 在役 | 战斗期望 MC(胜轮数/承伤/清图/开格分布) | 人工读 stdout |
|
||
| `auto_player.gd` | 在役(库件) | 自动玩家策略单一实现——被 `battle_driver` preload | 宿主脚本 |
|
||
| `battle_driver.gd` | 在役(库件) | 战斗驱动;被 `damage_probe` preload | 宿主脚本 |
|
||
| `level_autoplay.gd` | 在役(库件) | 被 `presentation/tune_sim_controller.gd` load | 宿主脚本 |
|
||
| `board_stat_probe.gd` | 在役 | `tools/clear_dominance_audit.py` 的取数端 | 工具脚本 |
|
||
| `calibrate_boards.gd` | 在役 | 生成器真值分母;`sim-data/analyze.py` 引用 | 工具脚本 |
|
||
| `damage_probe.gd` | 在役(诊断件) | 零承伤根因分割探针(头注自陈:非验收件·不入闸) | 人工读 stdout |
|
||
| `defense_scan.gd` | 在役(诊断件) | AC 叠层减伤读数分割(高 AC 轮 vs 基线轮) | 人工读 stdout |
|
||
| `skill_rhythm_probe.gd` | 在役(分析工具) | 技能池节奏定位(表侧池快照 + 摇号份额) | 人工读 stdout |
|
||
| `rhythm_batch.gd` | 在役(批量采集) | 技能池节奏批量读数(06 在册项的机器半边) | 人工/工具 |
|
||
| `dump_battle.gd` | **在役·一次性** | 打印一场完整战斗的步骤流与棋盘摘要(肉眼验收归因质量) | 人工(用完即止) |
|
||
| `experiment_density.gd` | **在役·一次性** | 不同 mine_density 下的推理率/残余挖掘量取数(历史调参用) | 人工(用完即止) |
|
||
|
||
> **纪律**:本表是这些件的唯一在册处。新增手动测量脚本必须同批登记(用途 + 触发方式 + 结果由谁判);
|
||
> 退役走 `design/dev/doc-conventions.md` 第 10 节退役规则,禁止「没看见=死件」式清理。
|
||
|
||
## Static Scan(提交前本地跑)
|
||
|
||
`tests/static_scan.sh` 实现静态扫描域 S1–S8 的可执行子集;规则全集与动机见
|
||
`design/dev/architecture/architecture.md`「测试与质量域」节。新违规案例必须**先补扫描规则再修码**。
|
||
|
||
## 本地闸(唯一验收门禁)
|
||
|
||
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量),
|
||
跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回 master 由 worktree-guard 按 diff 推域校验回执。
|
||
闸清单与域划分的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
|
||
|
||
**判定三硬闸**(各闸一律适用,缺一即判定有洞):
|
||
①**退出码为唯一权威判据**——日志里的 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/` 之名同理,里面既有存活面也有
|
||
> 系统级断言与场景探针。判定「是不是冒烟」看语义,不看文件名。
|