feat(win): T-WIN-004 239 字节关键点帧编解码 + 无头测试框架

This commit is contained in:
2026-09-14 20:04:54 +08:00
parent 85a2568266
commit ad62929f59
4 changed files with 590 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
extends SceneTree
## 无头测试入口。
##
## 用法:
## godot --headless --path . --script res://tests/run_tests.gd
##
## 约定:tests/test_*.gd 继承 TestCase,其中 test_ 开头的方法会被执行。
## 不依赖 autoload —— 测试直接 preload 被测类,确保可在 --script 模式下运行。
const TEST_DIR := "res://tests"
const TEST_PREFIX := "test_"
const TEST_SUFFIX := ".gd"
## 断言基类所在文件,不作为测试用例收集
const BASE_CASE_FILE := "test_case.gd"
var _passed := 0
var _failed := 0
var _skipped := 0
var _assertions := 0
var _failures: Array[String] = []
var _current := ""
var _started := false
## 注意:不能在 _initialize() 里跑测试。
## 那时 root Window 尚未真正进入场景树,HTTPRequest.request() 会返回
## ERR_UNCONFIGURED(3),导致所有网络相关测试假失败。改在首帧 _process 中启动。
func _initialize() -> void:
pass
func _process(_delta: float) -> bool:
# 必须始终返回 false:只要测试里有 await(例如真实 HTTP 请求),
# 协程就会跨帧挂起;此时若返回 true,主循环会立刻退出,
# 测试被拦腰截断且悄无声息(同步测试因为不跨帧,一直掩盖了这个缺陷)。
# 结束由 _run_all() 内的 quit() 负责。
if not _started:
_started = true
_run_all()
return false
func _run_all() -> void:
var files := _discover_tests()
if files.is_empty():
print("未发现任何测试文件")
quit(1)
return
print("")
print("== OpenDance Studio Windows 客户端 —— 单元测试 ==")
print("")
for path in files:
await _run_file(path)
print("")
var skip_note := (",%d 跳过" % _skipped) if _skipped > 0 else ""
print("== 结果:%d 通过 / %d 失败%s ==" % [_passed, _failed, skip_note])
if _failed > 0:
print("")
print("失败明细:")
for line in _failures:
print(" ✗ %s" % line)
print("")
quit(1)
else:
print("全部通过")
print("")
quit(0)
func _discover_tests() -> Array[String]:
var out: Array[String] = []
var dir := DirAccess.open(TEST_DIR)
if dir == null:
push_error("无法打开 %s" % TEST_DIR)
return out
dir.list_dir_begin()
var name := dir.get_next()
while name != "":
if not dir.current_is_dir() and name.begins_with(TEST_PREFIX) and name.ends_with(TEST_SUFFIX) and name != BASE_CASE_FILE:
out.append("%s/%s" % [TEST_DIR, name])
name = dir.get_next()
dir.list_dir_end()
out.sort()
return out
func _run_file(path: String) -> void:
var case_name := path.get_file().get_basename()
_current = case_name
print("[%s]" % case_name)
var script: GDScript = load(path)
# 脚本编译失败时 load() 仍返回对象,但 can_instantiate() 为 false。
# 不做这层检查的话,整个文件会静默贡献 0 个测试而不被察觉。
if script == null or not script.can_instantiate():
_record(false, case_name, "测试脚本无法编译/实例化(查看导入日志中的 Parse Error)")
return
var instance: Object = script.new()
if instance == null:
_record(false, case_name, "测试脚本实例化返回 null")
return
# 注入断言上下文
instance.set("_runner", self)
var methods: Array[String] = []
for m in instance.get_method_list():
var mname: String = m["name"]
if mname.begins_with("test_") and not methods.has(mname):
methods.append(mname)
methods.sort()
for mname in methods:
var before_failed := _failed
var before_skipped := _skipped
var before_assertions := _assertions
# call() 对协程返回 GDScriptFunctionState,await 会等到其真正结束;
# 对普通方法返回值则立即返回。因此同步与异步测试共用一条路径。
await instance.call(mname)
if _failed != before_failed or _skipped != before_skipped:
continue
# 关键防线:一个测试方法若一条断言都没执行,说明它静默失败了
# (脚本编译失败、preload 失败、方法内报错提前返回等),一律计为失败,
# 绝不允许「什么都没做」被记成通过。
if _assertions == before_assertions:
_record(false, case_name, "%s 未执行任何断言(疑似脚本编译/加载失败)" % mname)
else:
passed("%s.%s" % [case_name, mname], true)
func report_assert(ok: bool, label: String, detail: String) -> void:
_record(ok, _current, label, detail)
## 跳过当前测试(例如依赖外部服务而该服务不可达)
func report_skip(label: String, reason: String) -> void:
_skipped += 1
print(" ⊘ %s — %s" % [label, reason])
func _record(ok: bool, group: String, label: String, detail: String = "") -> void:
_assertions += 1
if ok:
_passed += 1
print(" ✓ %s" % label)
else:
_failed += 1
var line := "%s > %s" % [group, label]
if detail != "":
line += " — %s" % detail
_failures.append(line)
print(" ✗ %s%s" % [label, (" — " + detail) if detail != "" else ""])
func passed(label: String, silent: bool = false) -> void:
if not silent:
_passed += 1
print(" ✓ %s" % label)