Files
gplan-2/game/tests/README.md
T
DKChan 57b0c3a59e docs(闸分域): 判据由路径前缀改文件性质——game/ 内 .md 归文档域
- 合回闸与 run_all_gates.sh 同口径:代码域=game/ 下非 .md 的行为载体;
  game/ 内 .md 与 game/ 外一切路径=文档域;两类都有仍跑全量档
- 回执对齐(covers_tip)与 dirty_game 同步改只看代码面
- 修 git diff 默认 quotepath 把中文路径写成转义串、后缀判 .md 必失效的缺陷
- 同步 AGENTS/process-conventions/architecture/roadmap/tests README 与四份技能
- 规则评估样例:c25daa30(旧判 full、新判 docs);本批自身(docs)
2026-10-26 01:38:25 +08:00

185 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Test Manual — Dungeon Sweeper(测试工具手册)
**Engine**: Godot 4.7.2-stable(GDScript 单语言)
**Test Framework**: 标准四级测试层(冒烟/单元/集成/系统)+ headless 场景探针与渲染 golden;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节)
**门禁**: 本地闸——`tools/run_all_gates.sh`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单与域划分只在本脚本定义,文档不另列
**Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/engineering/`、production 解散)
**参考实现**: `game/probe/`(场景探针族)· `game/sim/`(测量/诊断/模拟驱动件)(场景探针族——正式工程的活样板;系统测试断言面过渡宿主 `smoke_all.gd` 亦在此,迁移归断言迁移批)
本手册是测试面每件工具的介绍处:**它是什么、判什么、怎么单独跑、结果由谁判、什么时候会用它**。
分工三线——介绍归本文;**规则与时机**(什么时候必须跑哪些测试、测试怎么写、怎么算合格)归
`design/engineering/test-conventions.md`;**收口验收跑哪几道闸**(闸清单与改动域划分)只在
`tools/run_all_gates.sh` 脚本内定义一处,本手册不另列闸清单。
## Directory Layout
```
tests/
unit/ # 单元测试(公式/状态机/纯逻辑,隔离断言)——extends SceneTree + quit(code)
integration/ # 集成测试(跨系统闭环——与 unit/ 同一入口同一协议,见下「测试层级」节)
smoke_boot.tscn/.gd # 冒烟测试——极小启动场景(真实开机路径存活检查,闸 00)
system_test.gd # 系统测试宿主薄壳(SceneTree+构造注入;断言迁移批后承接 smoke_all.gd,闸 01)
smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取)
evidence/ # 截图与人工签核记录
run_all.gd # 单元与集成测试统一入口(套件 manifest 协议,D2 AC-2b)
static_scan.sh # 静态扫描域 S1–S12 可执行子集
```
## 测试层级(标准四级+UI 探针)
| 层 | 判什么 | 承载 | 入闸方式 |
|---|---|---|---|
| **冒烟测试** | 这版活着吗——能否启动、能否加载表、能否进主屏。零逻辑判定 | `tests/smoke_boot.tscn/.gd`(以项目运行目标拉起=真实开机路径) | `run_smoke.py`(闸 00) |
| **单元测试** | 最单元的逻辑代码对不对(公式/状态机/纯逻辑) | `tests/unit/` | `run_all.gd` manifest(闸 02) |
| **集成测试** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元测试同一入口、同一协议) |
| **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `game/probe/smoke_all.gd`(断言迁移批后=`tests/system_test.gd`) | `run_system_test.py`(闸 01) |
| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) |
> **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。
> 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里,
> 回放确定性已由 `tools/parity_anchor.py` 承担并收编为全量域闸 11 号条件闸件;
> Core-Core 与 A1×A2 端到端仍随属主批增量登记;各 GDD 登记的 deferral 集成用例以对侧登记为生效条件。
>
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = 单元测试;
> 需要两个以上真实系统协同并走落盘/落库 = 集成测试;需要完整项目引导与场景树 = 冒烟/系统/UI 探针。
## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正)
历史 README 写死「752+ 断言/十五套件」必然漂移,改为两种口径并注明语义:
- **运行时口径**:系统测试运行器(`tools/run_system_test.py`)打印的通过项数——循环内断言按执行次数计,随断言面增减浮动,以当次跑测打印为准。
- **静态口径**:断言面源码 `check(` 调用点计数——按源码位置计,一处在循环内即多次执行。
两口径数值不同且各自浮动,禁互相换算或写死。
## 工具速查(逐件:判什么 / 结果谁判 / 什么时候用)
| 工具 | 判什么 | 结果谁判 | 什么时候会用它 |
|---|---|---|---|
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(40 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑 |
| `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 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
| `tools/parity_anchor.py`(回放对拍) | 同种子下三变体逐位确定性 | 工具脚本逐位比对 | 本批触及数值/存档/RNG/结算面时;须用户批准发起 |
| 测量件(`game/sim/`,逐件见下「游离脚本台账」) | 期望/分布读数——只给读数,不判对错 | 人工读 stdout 或配套工具脚本 | 调参与归因;MC 类一律用户发起(档位表见 `sim-data/README.md`「MC 采集档位表」) |
> 单件直跑是调试期最窄确认手段,**不构成验收**——验收只有 `tools/run_all_gates.sh` 一个入口(判定三硬闸见下「本地闸」节)。
## Running Tests
```bash
# 冒烟测试(闸 00)——极小启动场景,秒级
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/unit/example_test.gd
# 系统测试(闸 01)——过渡期宿主 smoke_all.gd;分钟级
python tools/run_system_test.py
# 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://probe/<probe>.tscn
# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置)
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 统一流。
**协议要点**(D2 冒烟协议):
- `--script` 载体 `extends SceneTree` 以获得 `quit(code)`;成功 `quit(0)`、失败 `quit(1)`
- `--script` 模式**不加载 autoload 且不注册全局标识符**(裸引用 `ConfigDB` = parse error)——ConfigDB/RngService 由脚本显式实例化,core 一律构造注入
- 冒烟测试走项目运行目标(场景拉起),autoload 全在位——与 `--script` 载体互补
- 致命类用例独立进程,runner manifest 登记"调用 → 期望退出码"
- 每用例先 `reset_diagnostics()`(诊断收集器协议)
- 期望值由表读值推导,不写死玩法数值
## Test Naming
- **Files**: `[system]_[feature]_test.gd`(领域命名,禁 ID 前缀——design/engineering/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 test pass | 跑批回执 `game/tests/.gates/receipt.json`(不入库)+结论随批记 `reports/` |
## game/probe/ 游离脚本台账(迭代 29 T7 逐个定性)
`game/probe/` 里的脚本两分:**在闸件**(探针有 `.tscn` 宿主、被 `probe_gate.sh`/`render_gate.sh` 调用;
`smoke_all.gd` 被系统测试运行器调用——过渡期宿主)与**不入闸的手动测量/诊断件**(下表)。
判据 = 头注自陈用途 + 有无外部引用(工具脚本/产品码/文档)。**存疑一律按在役登记并标注**(不退役)。
| 脚本 | 定性 | 用途与触发方式 | 结果由谁判 |
|---|---|---|---|
| `adventure_monte_carlo.gd` | **在役**(`AGENTS.md` 在册入口) | 全冒险期望 MC;`tools/parity_anchor.py` 的比对载体 | 工具脚本逐位比对 |
| `verifier_batch.gd` | **在役**(`AGENTS.md` 在册入口) | `tools/balance_verifier.py` 的采集端(数值验证) | 报告与原始数据落 `sim-data/` |
| `reward_expectation.gd` | **在役**(`AGENTS.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 | 宿主脚本 |
| `probe_ui_util.gd` | 在役(库件) | 探针公共工具(存档隔离/句柄解析)——被各探针 preload | 宿主探针 |
| `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/engineering/doc-conventions.md` 第 10 节退役规则,禁止「没看见=死件」式清理。
> 测量类批量的发起规则(档位与批准)见 `sim-data/README.md`「MC 采集档位表」——无数值改动禁重跑。
## Static Scan(提交前本地跑)
`tests/static_scan.sh` 实现静态扫描域 S1–S12 的可执行子集;S1–S10 的规则全集与动机见
`design/engineering/architecture/architecture.md`「测试与质量域」节,**S11/S12 两段的规则说明自持于本文件与
`tests/static_scan.sh` 头注**——迭代 32 两卡是代码实现批,按流水线红线禁写 `design/`,故这两段未进架构文档。
新违规案例必须**先补扫描规则再修码**。
S11 代码注释禁用词:扫描 game/ 全域 .gd/.tres/.md/.json/.sh 的注释面;禁词全部由 `design/gdd/术语表.md`「退役词对照」第一列解析供给(脚本内不内联任何词面——单一事实源);解析结果再按「巢穴」「旗」「flag_*」三条整行排除,另设同形异义豁免「去重闸」(封印重判去重的机制判别器,与某闸档退役词后二字同形——行内含「去重闸」整行跳过);三层豁免(闸门自指/显式留档 4 处/非注释面天然不取);两道自检(扫描面文件数非空+词表解析非空)。
S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `game/data/tables/*.tres`,禁已删符号(`counters_export`/`counters_seed`/`_reset_tree_limited_counters` 等)与带引号的旧计数器 id 字面(引号锚定,不误伤 `adv_`/`round_` 前缀新名);扫描面文件数为零即判红。
## 本地闸(唯一验收门禁)
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量),
跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。
闸清单与域划分的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
**判定三硬闸**(各闸一律适用,缺一即判定有洞):
①**退出码为唯一权威判据**——日志里的 PASS 串只是日志,不作判据;
②**脚本错误扫描命中即红**——GDScript 的 `SCRIPT ERROR` / `Parse Error` 不改进程退出码、
不计失败数,且会静默中断所在函数的剩余断言,只能由外部扫输出(闸件 `tools/error_scan.py`,
错误标记与 `tools/run_smoke.py` 同源);
③**断言数为零即红**——`test_kit.gd` 的 `finish()` 在零断言时返回专用退出码 2(与「断言失败=1」
区分,避免致命类套件的 expect 协议把它当成预期结果放过)。
**gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有载体(`--script` 套件+场景运行目标)内解决。
## 术语(本项目内的唯一语义)
标准四级测试层(2026-10-22 用户裁,权威对照=design/gdd/术语表.md「退役词对照」):
| 词 | 语义 | 载体 |
|---|---|---|
| **冒烟测试** | 秒级存活检查——能否启动、能否加载表、能否进主屏。判断「这版还活着吗」,不判断「这版对不对」 | `tests/smoke_boot.tscn/.gd`、`tools/run_smoke.py` |
| **单元测试** | 隔离的公式/状态机/纯逻辑断言面,逐套件独立进程实跑 | `game/tests/unit/`、`run_all.gd` |
| **集成测试** | 跨系统闭环断言面(存档⇄RNG 往返、回放确定性等) | `game/tests/integration/`、`run_all.gd` |
| **系统测试** | 启动真实工程走真实玩法路径的断言面 | 过渡期 `game/probe/smoke_all.gd`+`tools/run_system_test.py`;迁移后 `tests/system_test.gd` |
| **场景探针** | 以探针场景为运行目标、走完整项目引导(autoload 在位)的 UI/场景断言 | `game/probe/*.tscn`、`probe_gate.sh` |
| **渲染 golden** | 窗口化截图与基线图的逐像素容差比对 | `render_gate.sh`、`tests/golden/` |