diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 20b4ee0c..908b4c44 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,5 +1,8 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) # 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …) +# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; +# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据; +# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。 # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, @@ -13,24 +16,51 @@ const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"] func _init() -> void: - print("[run_all] unit/contract suite entry — %d suites" % _suites().size()) + # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。 + # 空值/零命中=用法错 quit(2)(fail-closed:禁把零套件当绿);选跑态仅供开发期最窄确认, + # 不产闸回执、不作验收凭据;批末全量闸不带参数,不走此分支。 + var only_terms := PackedStringArray() + var has_only := 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) + if has_only and only_terms.is_empty(): + push_error("[run_all] --only= 空值——用法错(选跑态禁把零套件当全量)") + quit(2) + return + var manifest := _suites() + var suites := manifest + var filtered := false + if has_only: + suites = _filter_suites(manifest, only_terms) + if suites.is_empty(): + push_error("[run_all] FILTERED RUN — --only=「%s」零命中(manifest 共 %d 套件)——用法错" % [",".join(only_terms), manifest.size()]) + quit(2) + return + filtered = true + 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 - for suite in _suites(): - failures += _run_suite(suite, ran) + for suite in suites: + failures += _run_suite(suite, ran, filtered) ran += 1 - # 实跑套件数校验:manifest 有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。 - if ran != _suites().size(): - push_error("[run_all] 实跑套件数 %d ≠ manifest 条目数 %d——有套件未跑" % [ran, _suites().size()]) + # 实跑套件数校验:清单有条目却被静默跳过(早退/continue)必须判红,不能只凭 failures。 + # 选跑态对过滤后清单校验;全量态与原 manifest 条目数同一。 + if ran != suites.size(): + push_error("[run_all] 实跑套件数 %d ≠ 清单条目数 %d——有套件未跑" % [ran, suites.size()]) quit(1) return if failures > 0: push_error("[run_all] %d 个套件失败" % failures) quit(1) return - print("[run_all] all green (%d suites)" % _suites().size()) + print("[run_all] all green (%d suites%s)" % [suites.size(), ",filtered" if filtered else ""]) quit(0) @@ -111,7 +141,7 @@ func _suites() -> Array: ] -func _run_suite(suite: Dictionary, idx: int) -> int: +func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: var path: String = suite.get("path", "") var expect: int = int(suite.get("expect", 0)) if path.is_empty(): @@ -125,7 +155,7 @@ func _run_suite(suite: Dictionary, idx: int) -> int: # 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空 #(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。 var proj := ProjectSettings.globalize_path("res://") - var log := proj + "/tests/.gates/run_all_%02d.log" % idx + var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx] DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates") var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log] var out := PackedStringArray() @@ -148,6 +178,19 @@ func _run_suite(suite: Dictionary, idx: int) -> int: return 0 +# 选跑过滤:对套件 res:// 路径做子串匹配,任一词命中即选入,词序无关。 +# 只在选跑态调用——全量态走原 manifest,条目与顺序零变化。 +func _filter_suites(manifest: Array, terms: PackedStringArray) -> Array: + var out: Array = [] + for suite in manifest: + var path: String = suite.get("path", "") + for t in terms: + if path.contains(t): + out.append(suite) + break + return out + + # 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe #(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢, # 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。