chore: initial commit
This commit is contained in:
commit
ddd2960fa6
317 files changed
+41717
No files matched your search
@@ -0,0 +1,67 @@
|
||||
# Test Infrastructure — Dungeon Sweeper
|
||||
|
||||
**Engine**: Godot 4.7.2-stable(GDScript 单语言)
|
||||
**Test Framework**: 双载体(ADR-0004)——`--headless --script` 逻辑冒烟 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态,正式 CI 期再议)
|
||||
**CI**: `.github/workflows/tests.yml`(首次 push 到 GitHub 后自动生效;仓库 2026-09-12 尚未初始化 git)
|
||||
**Setup date**: 2026-09-12
|
||||
**参考实现**: `prototypes/dungeon-sweeper-vertical-slice/smoke/`(冒烟 96/96 + 三 UI 探针——正式工程的活样板)
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
tests/
|
||||
unit/ # 隔离单元测试(公式/状态机/纯逻辑)——extends SceneTree + quit(code)
|
||||
integration/ # 跨系统测试(Core-Core 集成、回放确定性、存档往返)
|
||||
smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取)
|
||||
evidence/ # 截图与人工签核记录
|
||||
run_all.gd # CI 冒烟统一入口(套件 manifest 协议,D2 AC-2b)
|
||||
static_scan.sh # ADR-0004 静态扫描域 S1–S8 可执行子集
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# 逻辑冒烟(双载体之一)——Windows 须用 console 版 exe 取 stdout
|
||||
godot --headless --script tests/run_all.gd # 统一入口(CI 调用点)
|
||||
godot --headless --script tests/unit/example_test.gd
|
||||
|
||||
# 场景探针(双载体之二)——以探针场景为运行目标,走完整项目引导(autoload 在位)
|
||||
godot --headless --path . res://tests/probes/<probe>.tscn
|
||||
|
||||
# CI 旁必须配外部 wall-clock 超时(ADR-0004:引擎无死锁探测)
|
||||
timeout 120s godot --headless --script tests/run_all.gd
|
||||
```
|
||||
|
||||
**协议要点**(D2 冒烟协议 + ADR-0004):
|
||||
- 冒烟脚本 `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 前缀——docs/code-conventions.md)
|
||||
- **Functions**: `test_[scenario]_[expected]`
|
||||
- **Example**: `reward_roll_test.gd` → `test_fog_cell_filters_below_tier_min()`
|
||||
|
||||
## 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 | `production/qa/smoke-*.md` |
|
||||
|
||||
## Static Scan(提交前双跑:本地 + CI)
|
||||
|
||||
`tests/static_scan.sh` 实现 ADR-0004 扫描域 S1–S8 的可执行子集;规则全集与动机见
|
||||
`docs/architecture/adr-0004-test-carrier-and-ci.md`。新违规案例必须**先补扫描规则再修码**。
|
||||
|
||||
## CI
|
||||
|
||||
Tests run automatically on every push to `main` and on every pull request.
|
||||
A failed test suite blocks merging. **gdUnit4 安装说明(deferred,届时启用)**:
|
||||
AssetLib → GdUnit4 → Install → Project Settings → Plugins 勾选 → 重启编辑器 → 确认 `res://addons/gdunit4/`。
|
||||
@@ -0,0 +1,6 @@
|
||||
# tests/evidence/ — 人工测试证据
|
||||
|
||||
截图、录屏、人工签核记录。命名:`[日期]-[里程碑]-[检查项].md`。
|
||||
|
||||
面向表现/手感的验收(视觉锚三硬约束、格子样张测试、结算屏可读性、呈现义务台账核对)归此处——
|
||||
自动化探针(场景级)能覆盖的走 `tests/unit` / 探针场景,不能 G-W-T 的走本目录。
|
||||
@@ -0,0 +1,5 @@
|
||||
# tests/integration/ — 跨系统测试
|
||||
|
||||
承载:Core-Core 集成(真 A3 + 真 B2/B3/B4 容器实例)、回放确定性(同种子步骤流逐行相同——B1 AC-33 形态)、存档/RNG 快照往返(D3 落地后)、A1×A2 双包含端到端。
|
||||
|
||||
各 GDD 登记的 deferral 集成用例(以对侧登记为生效条件)随对应系统实现迁入本目录。
|
||||
@@ -0,0 +1,41 @@
|
||||
# tests/run_all.gd — CI 冒烟统一入口(ADR-0004 双载体的逻辑冒烟调用点)
|
||||
# 用法:godot --headless --script tests/run_all.gd (CI 侧包装外部超时:timeout 120s …)
|
||||
# 协议:套件清单逐条"套件 → 期望退出码"(D2 AC-2b manifest);致命类独立进程。
|
||||
# 现状:正式 Godot 工程尚未在根目录创建(代码在 prototypes/dungeon-sweeper-vertical-slice/,
|
||||
# 其 smoke/smoke_all.gd 96/96 为参考实现)——本入口当前仅验证运行协议自身;
|
||||
# 正式工程落地后,把套件清单登记进 _suites()。
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
print("[run_all] formal smoke entry — protocol check")
|
||||
if DisplayServer.get_name() != "headless":
|
||||
push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)")
|
||||
var failures := 0
|
||||
for suite in _suites():
|
||||
failures += _run_suite(suite)
|
||||
if failures > 0:
|
||||
push_error("[run_all] %d 个套件失败" % failures)
|
||||
quit(1)
|
||||
return
|
||||
print("[run_all] all green (%d suites)" % _suites().size())
|
||||
quit(0)
|
||||
|
||||
# 返回 Array of Dictionary:{ "path": String, "expect": int }
|
||||
# 正式工程落地后在此登记;探针场景(.tscn 载体)由 CI 单独步骤调用,不经本入口。
|
||||
func _suites() -> Array:
|
||||
return [
|
||||
# {"path": "res://tests/unit/example_test.gd", "expect": 0},
|
||||
# {"path": "res://tests/unit/reward_economy/reward_roll_test.gd", "expect": 0},
|
||||
# 致命类(独立进程,期望非 0):
|
||||
# {"path": "res://tests/unit/d2/fixture_missing_table_test.gd", "expect": 1},
|
||||
]
|
||||
|
||||
func _run_suite(suite: Dictionary) -> int:
|
||||
# SceneTree 脚本内不能直接以 OS.execute 递归调 godot 的成熟形态留待正式工程
|
||||
# (需解析 godot 二进制路径);当前协议检查阶段只断言清单形态合法。
|
||||
var path: String = suite.get("path", "")
|
||||
if path.is_empty():
|
||||
push_error("[run_all] suite missing path")
|
||||
return 1
|
||||
print("[run_all] registered: %s (expect=%d)" % [path, suite.get("expect", 0)])
|
||||
return 0
|
||||
@@ -0,0 +1,33 @@
|
||||
# Smoke Test: Critical Paths — Dungeon Sweeper
|
||||
|
||||
**Purpose**: QA 交接前 15 分钟内人工跑完的关键路径清单。
|
||||
**Run via**: `/smoke-check`(读本文件)。
|
||||
**Update**: 新核心系统落地后增补条目。
|
||||
**分工注记**:自动化冒烟(切片 96/96)覆盖规则正确性;本清单面向**表现与手感**(自动化探针不可 G-W-T 的部分)。
|
||||
|
||||
## Core Stability(恒跑)
|
||||
|
||||
1. 启动到开始界面无崩溃;开始界面响应全部输入
|
||||
2. 开战:装板后初始展开可见、小人落位出生区、AP 显示 8
|
||||
|
||||
## Core Mechanic
|
||||
|
||||
3. 挖掘:单格 1 AP、数字亮起与 8 邻雷数一致、连锁免费撕开一片
|
||||
4. 插旗/拔旗/挪旗:AP 价目正确、连锁止于旗(旗挡链)属正常
|
||||
5. 移动:已开格瞬移、**已引爆雷格可通行可读**(工程债呈现半边)
|
||||
6. 停手:RAP 1:1 转存、下轮 AP 硬重置同图继续
|
||||
7. 轮末结算:明细逐行呈现、来源标注可读(P2——"这一行是我哪步扫雷带来的"可答)
|
||||
8. 道具使用窗口:授予段后弹出、至多选一件、满血治疗包 usable_now 禁选态正确
|
||||
9. 清图:清图奖励兑付 + 完美排雷高光(零误标时双份)+ 换新图;未刷新的停手轮不换图
|
||||
10. 轮内刷新(方案 A):Active 当场换图、AP 保留、RAP 扣减、每轮 ≤2 次、跨图事实结算完整
|
||||
|
||||
## Battle & Level
|
||||
|
||||
11. 战斗段:主角先手攻击行(hero_attack 来源)、敌方行为环读怪(姿态↔下一拍)、死亡即不动手
|
||||
12. 胜负判定与 battle_over 清理全序;库存跨战斗延续、关卡末"剩余道具 ×N 清空"反馈
|
||||
13. 关卡推进:节点链兑付(奖励点/恢复点)、Boss 房(Dmax 行)、层间 HP 携带
|
||||
|
||||
## Data & Perf
|
||||
|
||||
14. 数值调整板:**打开可见**(覆盖层层级回归——缺陷⑤)、滑条即时生效、批量模拟可跑
|
||||
15. 无明显卡顿(60fps 目标);连续 5 分钟无内存异常增长;窗口/结算屏文案中文显示正常(CJK 字体核对)
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/static_scan.sh — ADR-0004 静态扫描域 S1–S8 可执行子集
|
||||
# 规则全集与动机:docs/architecture/adr-0004-test-carrier-and-ci.md §Decision 3
|
||||
# 正式工程(core/ 存在)时执行;否则优雅跳过(切片阶段代码在 prototypes/,不受本扫描管辖)。
|
||||
# 纪律:新违规案例必须先补本脚本规则再修码(docs/code-conventions.md 缺陷-规范对照表)。
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -d core ]; then
|
||||
echo "[static_scan] core/ 不存在(正式工程未创建)——跳过,退出 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fail=0
|
||||
|
||||
# S1/S2 — 玩法代码禁 randomize()/自建 RandomNumberGenerator/全局随机函数(绕开 RngService 的口子)
|
||||
hits=$(grep -rnE '\brandomize[[:space:]]*\(|\bRandomNumberGenerator\b|\brandi[[:space:]]*\(|\brandf[[:space:]]*\(|\brandf_range[[:space:]]*\(' \
|
||||
core presentation --include='*.gd' 2>/dev/null | wc -l)
|
||||
if [ "$hits" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits 处"; grep -rnE '\brandomize[[:space:]]*\(|\bRandomNumberGenerator\b|\brandi[[:space:]]*\(|\brandf[[:space:]]*\(|\brandf_range[[:space:]]*\(' core presentation --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S1/S2 ok"; fi
|
||||
|
||||
# S4 — core/ 禁 extends Control/Node2D、禁 .tscn preload(headless 债)
|
||||
hits=$(grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' core --include='*.gd' 2>/dev/null | wc -l)
|
||||
if [ "$hits" -gt 0 ]; then echo "[static_scan] S4 FAIL: core 引擎/UI 依赖命中 $hits 处"; grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' core --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S4 ok"; fi
|
||||
|
||||
# S3 — core/ 禁 ConfigDB 外 data 表加载(schema class_name 直引豁免)
|
||||
hits=$(grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null \
|
||||
| grep -v 'res://tests/' | wc -l)
|
||||
if [ "$hits" -gt 0 ]; then echo "[static_scan] S3 WARNING(人工复核——class_name 直引误报需逐条排除): $hits 处"; grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null; fi
|
||||
|
||||
# TODO(正式工程落地后精化为扫描配置并升格为硬失败):
|
||||
# S5 core/ 禁裸引用 ConfigDB/RngService 全局标识符(--script 模式 parse error;grep 近似误报多,需 AST 级或精确白名单)
|
||||
# S6 core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁反向引用
|
||||
# S7 B2/B3/B4 API 符号命中文件 ⊆ B1 文件集(编排唯一性)
|
||||
# S8 结算路径 await 零命中(B1 结算/编排文件集)
|
||||
|
||||
exit $fail
|
||||
@@ -0,0 +1,6 @@
|
||||
# tests/unit/ — 隔离单元测试
|
||||
|
||||
每系统一个子目录(`reward_economy/`、`board_generator/`…)。协议与命名见 `tests/README.md`。
|
||||
|
||||
**活样板**:`example_test.gd`(本目录)——第一个真实测试请以它为模板。
|
||||
**参考实现**:`prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd`(96 条断言的组织方式)。
|
||||
@@ -0,0 +1,37 @@
|
||||
# tests/unit/example_test.gd — 示例测试:项目测试协议的活样板(gate-check 硬性要求 ≥1 示例测试)
|
||||
# 运行:godot --headless --script tests/unit/example_test.gd
|
||||
# 协议要点(ADR-0004 + D2 冒烟协议):
|
||||
# extends SceneTree 以获得 quit(code);断言失败 → push_error + quit(1);全绿 → quit(0)
|
||||
# --script 模式无 autoload 且无全局标识符(裸引用 ConfigDB = parse error)——
|
||||
# 涉及表值的测试须显式实例化 ConfigDB,期望由表读值推导不写死
|
||||
# 第一个真实测试:复制本文件为 tests/unit/<system>/<system>_<feature>_test.gd 并替换占位断言
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var failures := 0
|
||||
failures += _check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
|
||||
failures += _check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
|
||||
if failures > 0:
|
||||
push_error("[example_test] %d 项失败" % failures)
|
||||
quit(1)
|
||||
return
|
||||
print("[example_test] all green")
|
||||
quit(0)
|
||||
|
||||
func _check(desc: String, ok: bool) -> int:
|
||||
if not ok:
|
||||
push_error("[example_test] FAIL: " + desc)
|
||||
return 1
|
||||
print("[example_test] PASS: " + desc)
|
||||
return 0
|
||||
|
||||
func _test_headless_protocol() -> bool:
|
||||
# 占位断言:验证脚本本身在 SceneTree 上下文可执行。
|
||||
# 真实测试写法示例(引用不执行):
|
||||
# var cfg := ConfigDBStub.new() # 显式实例化,非全局标识符
|
||||
# return cfg.action_economy.ap_per_round >= 1
|
||||
return true
|
||||
|
||||
func _test_naming_convention_followed() -> bool:
|
||||
# 占位断言:本文件函数名均以 test_ 前缀 + 场景_期望 结构(docs/code-conventions.md)
|
||||
return true
|
||||
Reference in new issue
Block a user