工程(合回闸关联档+闸机编排快速失败): 收口跑批按改动关联面选测+红树快速失败——①tools/run_all_gates.sh 关联档选闸(推广 2026-10-28 影响面选闸):00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降(测量面批不再免 00);03/04 探针与 05 渲染闸维持影响面口径;02 单元与集成改「选套件跑」——套件集=需求卡片申报∪diff 推导并集(申报=本批 diff 触及卡片∪roadmap 进行中行登记卡片的对照表「验收测试」列测试文件名;推导=脚本内套件族映射表按 game/ 代码面文件归族,映射不到一律横切),横切(公共件前缀 config_db*/rng_service*/save_manager*/meta_progression*/main*、project.godot autoload 注册面、game/data/tables、PARITY 触面表)→ 02 回落全部套件;PARITY 触面表上移单点定义(gate 11 与横切判定共用同一清单);回执加 scope(full|related)与 scope_terms 字段,worktree-guard 校验逻辑不动——②game/tests/run_all.gd 快速失败:连续 3 个套件超时(可调常量 CONSECUTIVE_TIMEOUT_ABORT 初值 3)即中止整轮、汇总已见失败与超时清单、退出码非 0,不连续超时照旧逐个回收;_run_suite 改返 {fail, timed_out};新增 --only-source=gates 来源标记(关联档选跑横幅,区别于开发期选跑免责句),无选跑参数时行为与现状完全一致——③规则同批同步(禁只改闸不改规则):AGENTS.md「交付前验收」句与硬性红线改按关联档选测口径(所选域绿即可合回,全量回归由用户自发、合入后无自动兜底,2026-10-29 用户裁)+规范导航行同步;process-conventions §3 第 8 条合回闸细则同口径;test-conventions §4 改「提交前验证」、code-conventions §3 主题行随改;game/tests/README.md 工具速查 run_all.gd 行(快速失败+闸机关联档选跑+manifest 实数 65)与挂死防护注、门禁/本地闸回执字段句同步——④待办清单「合回闸关联档」「闸机编排快速失败」两条目完成整条删除。孤立验证:推导沙箱七场景(行为面归族/触面横切/测量面批/main* 横切/project.godot 横切/申报并集/横切优先)+run_all 闸源选跑 9/65 绿+2s 截止连续超时第 3 个中止(exit 1 带汇总)
This commit is contained in:
1 parent
6abaf67dd5
commit
9f94c9e100
8 files changed
+231
-77
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
**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/` 已删除,不建不留不参考);闸清单与域划分只在本脚本定义,文档不另列
|
||||
**门禁**: 本地闸——`tools/run_all_gates.sh`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量;代码域 02 按关联档选套件,算法与套件族映射表只在脚本内定义)。本仓库**无远端、无 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` 亦在此,迁移归断言迁移批)
|
||||
|
||||
@@ -197,7 +197,7 @@ tests/
|
||||
|---|---|---|---|
|
||||
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
|
||||
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
|
||||
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
|
||||
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(65 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖);连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮、汇总已见失败与超时清单后非 0 退出(阈值=可调常量,不连续的超时照旧逐个回收) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;此形态仅供开发期最窄确认,不产闸回执、不作验收凭据);闸机关联档选跑——同一过滤机制加 `-- --only-source=gates`,由 `tools/run_all_gates.sh` 在 02 闸传入(回执 scope=related,是验收跑批本体) |
|
||||
| `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 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
|
||||
@@ -225,7 +225,9 @@ godot --headless --path . res://probe/<probe>.tscn
|
||||
|
||||
# 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间——
|
||||
# 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收
|
||||
#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸
|
||||
#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸;
|
||||
# 连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮(汇总已见失败与超时清单、退出码非 0),
|
||||
# 不连续的超时照旧逐个回收;阈值是 run_all.gd 内可调常量
|
||||
```
|
||||
|
||||
> 套件 manifest 现况:`_suites()` 登记 **65 条**(62 单元+3 集成,2026-10-29 实数;口径=manifest 条目数,
|
||||
@@ -297,9 +299,9 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam
|
||||
|
||||
## 本地闸(唯一验收门禁)
|
||||
|
||||
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量),
|
||||
跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。
|
||||
闸清单与域划分的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
|
||||
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related),
|
||||
跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。
|
||||
闸清单与域划分(含选闸算法与套件族映射表)的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
|
||||
|
||||
**判定三硬闸**(各闸一律适用,缺一即判定有洞):
|
||||
①**退出码为唯一权威判据**——日志里的 PASS 串只是日志,不作判据;
|
||||
|
||||
+61
-18
@@ -1,8 +1,9 @@
|
||||
# tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b)
|
||||
# 用法:godot --headless --path game --script res://tests/run_all.gd
|
||||
# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件;
|
||||
# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据;
|
||||
# 批末全量闸不带参数不经选跑分支。
|
||||
# 选跑态:追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件;
|
||||
# 零命中/空值=用法错 quit(2)。两个来源分开定性——开发期直跑(默认)仅供最窄确认,不产闸回执、
|
||||
# 不作验收凭据;闸机关联档选跑(--only-source=gates,由 tools/run_all_gates.sh 在 02 闸传入)
|
||||
# 是收口验收跑批本体,回执 scope=related。无任何选跑参数=全量态,行为与全量口径完全一致。
|
||||
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
|
||||
# 执行形态:逐套件起独立 Godot 子进程实跑
|
||||
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
|
||||
@@ -11,6 +12,9 @@
|
||||
# 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收
|
||||
#(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动)
|
||||
# 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。
|
||||
# 快速失败:连续 N 个套件超时=红树正在整批挂死(如公共件解析错让每个套件都起不来),
|
||||
# 继续逐个回收只剩烧钟——第 N 个超时即中止整轮,汇总已见失败与超时清单后非 0 退出;
|
||||
# 不连续的超时仍逐个回收照旧。
|
||||
# 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。
|
||||
extends SceneTree
|
||||
|
||||
@@ -23,21 +27,27 @@ const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"]
|
||||
const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC"
|
||||
const SUITE_TIMEOUT_DEFAULT_SEC := 120
|
||||
|
||||
# 快速失败阈值(可调常量,初值 3):连续 N 个套件超时即中止整轮。
|
||||
const CONSECUTIVE_TIMEOUT_ABORT := 3
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。
|
||||
# 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿);选跑态仅供开发期最窄确认,
|
||||
# 不产闸回执、不作验收凭据;批末全量闸不带参数,不走此分支。
|
||||
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配;
|
||||
# --only-source=gates=来源标记:由闸脚本传入的关联档选跑(验收跑批本体),缺省=开发期直跑。
|
||||
# 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿)。
|
||||
var only_terms := PackedStringArray()
|
||||
var has_only := false
|
||||
var only_from_gates := 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)
|
||||
elif a == "--only-source=gates":
|
||||
only_from_gates = true
|
||||
if has_only and only_terms.is_empty():
|
||||
push_error("[run_all] --only= 空值——用法错(选跑态禁把零套件当全量)")
|
||||
push_error("[run_all] --only= 空值——用法错(选跑禁把零套件当全量)")
|
||||
quit(2)
|
||||
return
|
||||
var manifest := _suites()
|
||||
@@ -50,15 +60,37 @@ func _init() -> void:
|
||||
quit(2)
|
||||
return
|
||||
filtered = true
|
||||
print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)])
|
||||
if only_from_gates:
|
||||
print("[run_all] GATE FILTERED RUN — 闸机关联档选跑 %d/%d 套件(--only=%s)" % [suites.size(), manifest.size(), ",".join(only_terms)])
|
||||
else:
|
||||
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
|
||||
var consecutive_timeouts := 0
|
||||
var failed_paths := PackedStringArray()
|
||||
var timed_out_paths := PackedStringArray()
|
||||
for suite in suites:
|
||||
failures += _run_suite(suite, ran, filtered)
|
||||
var outcome := _run_suite(suite, ran, filtered)
|
||||
var suite_fail: int = outcome["fail"]
|
||||
var suite_timed_out: bool = outcome["timed_out"]
|
||||
failures += suite_fail
|
||||
ran += 1
|
||||
var spath: String = suite.get("path", "")
|
||||
if suite_timed_out:
|
||||
consecutive_timeouts += 1
|
||||
timed_out_paths.append(spath)
|
||||
if consecutive_timeouts >= CONSECUTIVE_TIMEOUT_ABORT:
|
||||
push_error("[run_all] ABORT — 连续 %d 个套件超时(阈值 %d),中止整轮" % [consecutive_timeouts, CONSECUTIVE_TIMEOUT_ABORT])
|
||||
_report_abort(failed_paths, timed_out_paths, ran, suites.size())
|
||||
quit(1)
|
||||
return
|
||||
else:
|
||||
consecutive_timeouts = 0
|
||||
if suite_fail > 0:
|
||||
failed_paths.append(spath)
|
||||
# 实跑套件数校验:清单有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。
|
||||
# 选跑态对过滤后清单校验;全量态与原 manifest 条目数同一。
|
||||
if ran != suites.size():
|
||||
@@ -151,17 +183,19 @@ func _suites() -> Array:
|
||||
]
|
||||
|
||||
|
||||
func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
|
||||
# 返回 {"fail": int(该套件失败计 1/过 0), "timed_out": bool(是否超时挂死)}——
|
||||
# timed_out 供编排层做连续超时快速失败判定,fail 照旧累计。
|
||||
func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictionary:
|
||||
var path: String = suite.get("path", "")
|
||||
var expect: int = int(suite.get("expect", 0))
|
||||
if path.is_empty():
|
||||
push_error("[run_all] suite missing path")
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
var bin := _engine_bin()
|
||||
if bin == "":
|
||||
push_error("[run_all] 未找到 console 版引擎二进制(设 GODOT_BIN 指向 *_console.exe)——"
|
||||
+ "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地")
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
# 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空
|
||||
#(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道——
|
||||
# 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程
|
||||
@@ -180,7 +214,7 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
|
||||
var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd]))
|
||||
if pid < 0:
|
||||
push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid])
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
# 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。
|
||||
var timed_out := false
|
||||
var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000
|
||||
@@ -213,7 +247,7 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
|
||||
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid])
|
||||
else:
|
||||
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid])
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": true}
|
||||
# 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行——
|
||||
# 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。
|
||||
if negative:
|
||||
@@ -223,17 +257,26 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
|
||||
var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else ""
|
||||
if code_txt == "" or not code_txt.is_valid_int():
|
||||
push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile])
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
var code: int = int(code_txt)
|
||||
DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log)
|
||||
if not negative and _has_script_error(text):
|
||||
push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)])
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
if code != expect:
|
||||
push_error("[run_all] FAIL %s — 退出码 %d ≠ 期望 %d" % [path, code, expect])
|
||||
return 1
|
||||
return {"fail": 1, "timed_out": false}
|
||||
print("[run_all] PASS %s (exit=%d)" % [path, code])
|
||||
return 0
|
||||
return {"fail": 0, "timed_out": false}
|
||||
|
||||
|
||||
# 快速失败中止时的汇总:已见失败与超时清单+中止时进度——红树几分钟出结论,不磨完整轮。
|
||||
func _report_abort(failed: PackedStringArray, timed_out: PackedStringArray, ran: int, total: int) -> void:
|
||||
var timed_out_txt := "、".join(timed_out) if timed_out.size() > 0 else "(无)"
|
||||
var failed_txt := "、".join(failed) if failed.size() > 0 else "(无)"
|
||||
push_error("[run_all] 已见超时 %d 件:%s" % [timed_out.size(), timed_out_txt])
|
||||
push_error("[run_all] 已见失败(非超时)%d 件:%s" % [failed.size(), failed_txt])
|
||||
push_error("[run_all] 中止时进度:%d/%d 套件(余下未跑)" % [ran, total])
|
||||
|
||||
|
||||
# 选跑过滤:对套件 res:// 路径做子串匹配,任一词命中即选入,词序无关。
|
||||
|
||||
Reference in new issue
Block a user