feat(测试代码): 批 A 新冒烟测试与系统测试宿主——smoke_boot.tscn/.gd 极小启动场景(项目运行目标拉起=真实开机路径,17 项存活断言零逻辑判定,摘要+退出码收口);run_smoke.py 重写为冒烟场景运行器(三闸判定,ERROR_MARKERS 仍供 error_scan 复用);run_system_test.py 系统测试运行器(过渡期宿主 smoke_all.gd);system_test.gd SceneTree+构造注入宿主薄壳(零断言,迁移归批 B)——两件均孤立验证 PASS(run_smoke 17/0、system_test 0/0)
This commit is contained in:
1 parent
3b8c6bc3e7
commit
c1f4f80a4f
7 files changed
+215
-16
No files matched your search
@@ -0,0 +1,77 @@
|
||||
# tests/smoke_boot.gd — 冒烟测试:极小启动场景(这版活着吗——零逻辑判定)
|
||||
# 以项目运行目标拉起=真实开机路径:autoload 四件注册在位、配置表全载(fail-fast 通道)、
|
||||
# 全局字体与主题可加载、主屏场景实例化、启动态菜单可见。
|
||||
# 断言只查存在性与可见性,不做任何玩法逻辑判定;断言完打印摘要并以退出码收口,
|
||||
# 判定三闸(退出码+摘要失败数+脚本错误扫描)由外部运行器 tools/run_smoke.py 承担。
|
||||
# 运行:python tools/run_smoke.py(本地闸 00 冒烟测试)
|
||||
extends Node
|
||||
|
||||
const MAIN_SCENE_PATH: String = "res://presentation/main.tscn"
|
||||
const FONT_PATH: String = "res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf"
|
||||
const THEME_PATH: String = "res://assets/theme/main_theme.tres"
|
||||
const BOOT_FRAMES: int = 4 # 主屏装配是逐帧推进的挂树过程,等满再断言(探针族同惯例)
|
||||
|
||||
var _failures: int = 0
|
||||
var _checks: int = 0
|
||||
|
||||
|
||||
func check(cond: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" [PASS] " + label)
|
||||
else:
|
||||
_failures += 1
|
||||
print(" [FAIL] " + label)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_check_autoloads()
|
||||
var main: Node = await _boot_main_scene()
|
||||
_check_main_scene(main)
|
||||
print("[smoke] 摘要:%d 项检查,%d 项失败" % [_checks, _failures])
|
||||
get_tree().quit(1 if _failures > 0 else 0)
|
||||
|
||||
|
||||
# 开机前置面:autoload 注册与资源可加载(挂主屏之前先查,失败时主屏断言整段跳过)
|
||||
func _check_autoloads() -> void:
|
||||
for autoload_name in ["ConfigDB", "RngService", "SaveManager", "MetaProgression"]:
|
||||
var node: Node = get_node_or_null(NodePath("/root/" + autoload_name))
|
||||
check(node != null, "autoload %s 在位" % autoload_name)
|
||||
var db: Node = get_node_or_null(NodePath("/root/ConfigDB"))
|
||||
check(db != null and bool(db.get("is_ready")) and db.get("hero") != null,
|
||||
"ConfigDB 就绪且配置表全载(is_ready+hero 表在座,校验失败会致命中止)")
|
||||
check(load(MAIN_SCENE_PATH) != null, "主屏场景资源可加载(%s)" % MAIN_SCENE_PATH)
|
||||
check(load(FONT_PATH) != null, "全局字体资源可加载(%s)" % FONT_PATH)
|
||||
check(load(THEME_PATH) != null, "全局主题资源可加载(%s)" % THEME_PATH)
|
||||
|
||||
|
||||
# 以项目运行目标同款方式实例化主屏(真实开机路径——区别于 --script 模式的无 autoload 裸跑)
|
||||
func _boot_main_scene() -> Node:
|
||||
await get_tree().process_frame
|
||||
var packed: PackedScene = load(MAIN_SCENE_PATH) as PackedScene
|
||||
if packed == null:
|
||||
return null
|
||||
var main: Node = packed.instantiate()
|
||||
if main == null:
|
||||
return null
|
||||
add_child(main)
|
||||
for i in range(BOOT_FRAMES):
|
||||
await get_tree().process_frame
|
||||
return main
|
||||
|
||||
|
||||
# 主屏存活面:组合根装配句柄在位(main.X 一级句柄契约,探针挂载假设的启动侧证明)
|
||||
func _check_main_scene(main: Node) -> void:
|
||||
check(main != null and main.is_inside_tree(), "主屏场景已实例化并在树")
|
||||
if main == null:
|
||||
return
|
||||
var run_scope: Node = main.get("run_scope")
|
||||
check(run_scope != null, "闲时会话 run_scope 非空(永非 null 契约)")
|
||||
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
|
||||
var start_overlay: Control = main.get("_start_overlay")
|
||||
check(start_overlay != null, "开始菜单层已装配(_start_overlay)")
|
||||
check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)")
|
||||
check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)")
|
||||
check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)")
|
||||
check(main.get("legend_overlay") != null, "图例覆盖层已装配(legend_overlay)")
|
||||
check(main.get("status_panel") != null, "局内状态面板已装配(status_panel)")
|
||||
@@ -0,0 +1 @@
|
||||
uid://m7p3g1baece4
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://csmokeboot01"]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/smoke_boot.gd" id="1"]
|
||||
|
||||
[node name="SmokeBoot" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,27 @@
|
||||
# tests/system_test.gd — 系统测试宿主薄壳(SceneTree+构造注入)
|
||||
# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一,闸 01)。
|
||||
# 本件现宿主零断言——smoke_all.gd 的断言按「只剪切禁改写」纪律迁入(断言迁移批执行,
|
||||
# 本批只落宿主薄壳,跑通即绿)。迁移完成后 tools/run_system_test.py 缺省 --script 改指本件。
|
||||
# 协议照单元侧惯例:extends SceneTree 获得 quit(code);--script 模式不加载 autoload,
|
||||
# 断言夹具显式实例化 ConfigDB/RngService 构造注入;摘要行由 tools/run_system_test.py 三闸判定
|
||||
# (退出码+摘要失败数+脚本错误扫描)。
|
||||
# 运行:python tools/run_system_test.py --script res://tests/system_test.gd
|
||||
extends SceneTree
|
||||
|
||||
var _failures: int = 0
|
||||
var _checks: int = 0
|
||||
|
||||
|
||||
func check(cond: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" [PASS] " + label)
|
||||
else:
|
||||
_failures += 1
|
||||
print(" [FAIL] " + label)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
print("=== Dungeon Sweeper 系统测试(宿主薄壳——断言面待迁移批自 smoke_all.gd 迁入) ===")
|
||||
print("[system_test] 摘要:%d 项检查,%d 项失败" % [_checks, _failures])
|
||||
quit(1 if _failures > 0 else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtoupwbm65jtw
|
||||
+19
-16
@@ -1,18 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""run_smoke.py — 冒烟运行器(脚本错误外部门禁)
|
||||
r"""run_smoke.py — 冒烟测试运行器(脚本错误外部门禁)
|
||||
|
||||
用法:
|
||||
python tools/run_smoke.py # 默认跑切片全量冒烟(本机 Godot 固定路径)
|
||||
python tools/run_smoke.py --script res://smoke/smoke_all.gd
|
||||
python tools/run_smoke.py --godot <引擎路径> --path <切片目录> --timeout 180
|
||||
python tools/run_smoke.py # 跑冒烟测试(game/tests/smoke_boot.tscn,本机 Godot 固定路径)
|
||||
python tools/run_smoke.py --scene res://tests/smoke_boot.tscn
|
||||
python tools/run_smoke.py --godot <引擎路径> --path <切片目录> --timeout 120
|
||||
|
||||
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、
|
||||
不计入冒烟失败数,且会静默中断所在冒烟函数的剩余断言——摘要零失败也可能断言未跑全。
|
||||
冒烟测试 = 极小启动场景(game/tests/smoke_boot.tscn/.gd):以项目运行目标拉起=真实开机路径
|
||||
(autoload 注册在位、配置表加载、主屏场景实例化、启动态菜单可见),断言只查存活,零逻辑判定。
|
||||
|
||||
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、不计入失败数,
|
||||
且会静默中断所在断言函数的剩余断言——摘要零失败也可能断言未跑全。
|
||||
进程内无脚本错误陷阱 API(GDScript 无 try/catch),故闸设在外部:本包装除退出码与摘要行外,
|
||||
另扫输出中的 SCRIPT ERROR / Parse Error,命中即判失败。
|
||||
|
||||
三闸: ① 进程退出码 = 0;② 摘要行「冒烟完成:N 项检查,0 项失败」在案且失败数为 0;
|
||||
③ 输出无「SCRIPT ERROR」「Parse Error」标记行。
|
||||
三闸: ① 进程退出码 = 0;② 摘要行「[smoke] 摘要:N 项检查,0 项失败」在案且失败数为 0;
|
||||
③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单供 tools/error_scan.py 复用,禁另写一份)。
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
@@ -23,22 +26,22 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
|
||||
DEFAULT_SLICE = ROOT / "game"
|
||||
DEFAULT_SCRIPT = "res://smoke/smoke_all.gd"
|
||||
SUMMARY_RE = re.compile(r"冒烟完成:(\d+) 项检查,(\d+) 项失败")
|
||||
DEFAULT_SCENE = "res://tests/smoke_boot.tscn"
|
||||
SUMMARY_RE = re.compile(r"\[smoke\] 摘要:(\d+) 项检查,(\d+) 项失败")
|
||||
ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")
|
||||
|
||||
|
||||
def main():
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="冒烟运行器(脚本错误外部门禁)")
|
||||
ap = argparse.ArgumentParser(description="冒烟测试运行器(脚本错误外部门禁)")
|
||||
ap.add_argument("--godot", default=DEFAULT_GODOT)
|
||||
ap.add_argument("--path", default=str(DEFAULT_SLICE), help="切片项目目录(含 project.godot)")
|
||||
ap.add_argument("--script", default=DEFAULT_SCRIPT)
|
||||
ap.add_argument("--timeout", type=int, default=180, help="外部超时秒数(硬性要求)")
|
||||
ap.add_argument("--scene", default=DEFAULT_SCENE, help="冒烟场景(项目运行目标)")
|
||||
ap.add_argument("--timeout", type=int, default=120, help="外部超时秒数(硬性要求)")
|
||||
args = ap.parse_args()
|
||||
|
||||
cmd = [args.godot, "--headless", "--path", args.path, "--script", args.script]
|
||||
cmd = [args.godot, "--headless", "--path", args.path, args.scene]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", timeout=args.timeout)
|
||||
@@ -55,12 +58,12 @@ def main():
|
||||
gates.append(f"退出码 = {proc.returncode}(≠0)")
|
||||
m = SUMMARY_RE.search(out)
|
||||
if m is None:
|
||||
gates.append("摘要行缺失(未检出「冒烟完成:N 项检查,M 项失败」——入口跑完前中断?)")
|
||||
gates.append("摘要行缺失(未检出「[smoke] 摘要:N 项检查,M 项失败」——场景跑完前中断?)")
|
||||
elif int(m.group(2)) != 0:
|
||||
gates.append(f"冒烟失败 {m.group(2)} 项 / 共 {m.group(1)} 项")
|
||||
hits = [ln.strip() for ln in out.splitlines() if any(k in ln for k in ERROR_MARKERS)]
|
||||
if hits:
|
||||
gates.append(f"脚本错误标记命中 {len(hits)} 行(首行: {hits[0][:140]}——脚本错误不计冒烟失败数,"
|
||||
gates.append(f"脚本错误标记命中 {len(hits)} 行(首行: {hits[0][:140]}——脚本错误不计失败数,"
|
||||
"且中断所在函数的剩余断言,必须人工排查)")
|
||||
|
||||
if m is not None:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁)
|
||||
|
||||
用法:
|
||||
python tools/run_system_test.py # 跑系统测试(过渡期宿主 res://smoke/smoke_all.gd)
|
||||
python tools/run_system_test.py --script res://tests/system_test.gd
|
||||
python tools/run_system_test.py --godot <引擎路径> --path <切片目录> --timeout 300
|
||||
|
||||
系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。
|
||||
宿主过渡态:断言现全部住在 res://smoke/smoke_all.gd,按「只剪切禁改写」纪律迁往
|
||||
res://tests/system_test.gd(宿主薄壳已在位)——迁移完成后本件缺省 --script 改指新宿主,
|
||||
闸 01 系统测试随迁。
|
||||
|
||||
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、不计入失败数,
|
||||
且会静默中断所在断言函数的剩余断言——摘要零失败也可能断言未跑全。
|
||||
进程内无脚本错误陷阱 API(GDScript 无 try/catch),故闸设在外部:本包装除退出码与摘要行外,
|
||||
另扫输出中的 SCRIPT ERROR / Parse Error,命中即判失败。
|
||||
|
||||
三闸: ① 进程退出码 = 0;② 摘要行(「冒烟完成:N 项检查,0 项失败」或
|
||||
「[system_test] 摘要:N 项检查,0 项失败」)在案且失败数为 0;
|
||||
③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单经 tools/error_scan.py 复用,禁另写一份)。
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
|
||||
DEFAULT_SLICE = ROOT / "game"
|
||||
DEFAULT_SCRIPT = "res://smoke/smoke_all.gd" # 过渡期宿主;断言迁移批后改 res://tests/system_test.gd
|
||||
SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\d+) 项检查,(\d+) 项失败")
|
||||
ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")
|
||||
|
||||
|
||||
def main():
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="系统测试运行器(脚本错误外部门禁)")
|
||||
ap.add_argument("--godot", default=DEFAULT_GODOT)
|
||||
ap.add_argument("--path", default=str(DEFAULT_SLICE), help="切片项目目录(含 project.godot)")
|
||||
ap.add_argument("--script", default=DEFAULT_SCRIPT, help="系统测试宿主脚本")
|
||||
ap.add_argument("--timeout", type=int, default=300, help="外部超时秒数(硬性要求)")
|
||||
args = ap.parse_args()
|
||||
|
||||
cmd = [args.godot, "--headless", "--path", args.path, "--script", args.script]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", timeout=args.timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"[run_system_test] FAIL:外部超时(>{args.timeout}s——硬性要求)")
|
||||
return 1
|
||||
out = (proc.stdout or "") + (proc.stderr or "")
|
||||
sys.stdout.write(out)
|
||||
if out and not out.endswith("\n"):
|
||||
print()
|
||||
|
||||
gates = []
|
||||
if proc.returncode != 0:
|
||||
gates.append(f"退出码 = {proc.returncode}(≠0)")
|
||||
m = SUMMARY_RE.search(out)
|
||||
if m is None:
|
||||
gates.append("摘要行缺失(未检出「冒烟完成:N 项检查,M 项失败」——宿主跑完前中断?)")
|
||||
elif int(m.group(2)) != 0:
|
||||
gates.append(f"系统测试失败 {m.group(2)} 项 / 共 {m.group(1)} 项")
|
||||
hits = [ln.strip() for ln in out.splitlines() if any(k in ln for k in ERROR_MARKERS)]
|
||||
if hits:
|
||||
gates.append(f"脚本错误标记命中 {len(hits)} 行(首行: {hits[0][:140]}——脚本错误不计失败数,"
|
||||
"且中断所在函数的剩余断言,必须人工排查)")
|
||||
|
||||
if m is not None:
|
||||
print(f"[run_system_test] 摘要: {m.group(0)}")
|
||||
if gates:
|
||||
print("[run_system_test] FAIL——")
|
||||
for g in gates:
|
||||
print(f" - {g}")
|
||||
return 1
|
||||
print("[run_system_test] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user