Files
opendance-windows/tests/run_tests.gd
T

165 lines
4.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)