godot-headless-testing
Skill3 starsChanged 19 days agoMITView on GitHub
在终端里验证 Godot 4 改动时用,看不到编辑器也能自己跑通。修完脚本报错要确认整个项目没有脚本错误和 GDScript 警告,改完 GDScript 场景 C# 想自己验证,写冒烟测试 自动化测试,配 CI。--headless --import --check-only --script、退出码、从输出判 SCRIPT ERROR、模拟输入、截图、GUT gdUnit4。headless smoke test.
- Deletes or force-pushes
In this repository18 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- godot-2d-in-3dSkill
- godot-2dSkill
- godot-3dSkill
- godot-animationSkill
- godot-audioSkill
- godot-builtin-firstSkill
- godot-csharpSkill
- godot-editor-toolingSkill
- godot-gdscriptSkill
- godot-inputSkill
- godot-multiplayerSkill
- godot-performanceSkill
- godot-resources-dataSkill
- godot-scene-architectureSkill
- godot-scene-authoringSkill
- godot-shaders-vfxSkill
- godot-uiSkill
The file
Files
Godot 无头测试与命令行验证
用 Godot 命令行验证改动:参数、失败判定、冒烟脚本、输入模拟、截图、CI。示例在 4.7.2 编辑器版实测,$GODOT_BIN 指可执行文件。
范围
| 不含 | 去哪 |
| 单个节点或 API 的用法 | 对应领域 skill |
| GDScript 警告的含义与修法 | godot-gdscript |
| C# 构建、源码生成器、命名 | godot-csharp |
编辑器插件、EditorScript | godot-editor-tooling |
| 性能测量与 Profiler | godot-performance |
先查内置
| 需求 | 用这个 | 别自己写 |
| 查单个脚本语法 | --headless --check-only -s x.gd,退出码 0 或 1 | 正则或自写解析器 |
| 新增 class_name 或资源后 | --headless --import | 手改 .godot/global_script_class_cache.cfg |
| 跑主场景 N 帧后退出 | --quit-after N,配 --fixed-fps 60 可复现 | sleep 加 kill |
| 加载场景、断言、按结果退出 | SceneTree 脚本加 quit(code) | 自己拼 main 循环 |
| 引擎报错也算失败 | OS.add_logger 的 Logger 子类计数 4.5+ | 只靠 grep 或只靠退出码 |
| 传参给脚本 | -- 之后的参数,OS.get_cmdline_user_args() 读 | 环境变量或写临时文件 |
| 按键、动作输入 | Input.action_press、Input.parse_input_event | 直接改节点状态冒充输入 |
| 看画面 | xvfb-run,Forward+ 用 --rendering-driver vulkan,Compatibility 用 opengl3 | headless 下截图 |
| 录一段画面 | --write-movie out/f.png 或 .avi,同样要 xvfb | 循环截图 |
| 查类和方法是否存在 | ClassDB.class_has_method、类参考 XML | 凭记忆写 |
| 导出验证 | --export-pack,不需要模板 | 手动打 zip |
| 日志留档 | --log-file,目录须已存在 | tee 之外再造日志类 |
| 单元测试库 | GUT 或 gdUnit4 | 自写断言框架 |
做法
验证顺序
前一步不过不进下一步。
- 改了 class_name、资源、自动加载、输入映射:
--import - 每个改动的脚本
--check-only - 加载全部场景与脚本,看输出里的 ERROR
- 冒烟脚本,覆盖改到的系统
- 工程里有 GUT 或 gdUnit4 测试时跑它们
所有命令包进 gdrun:
| 处理 | 防什么 |
外层 timeout -s KILL | 卡死 |
</dev/null | 调试器提示符等输入 |
| 去 ANSI 颜色 | grep 漏匹配 |
| 有失败特征行时退出码改 1 | 运行时错误不改退出码 |
gdrun() {
local log rc
log=$(mktemp)
timeout -s KILL "${GD_TIMEOUT:-60}" "${GODOT_BIN:-godot}" --headless --no-header --path "${GD_PROJECT:-.}" "$@" </dev/null >"$log" 2>&1
rc=$?
sed -i 's/\x1b\[[0-9;]*m//g' "$log"
cat "$log"
if [ "$rc" -eq 0 ] && grep -E 'SCRIPT ERROR|Parse Error|^ERROR:|Error importing|Debugger Break|handle_crash|^FAIL:|^LOAD_FAIL|^ENGINE_ERRORS' "$log" | grep -vqE 'resources still in use|RID allocations|singleton'; then
rc=1
fi
rm -f "$log"
return "$rc"
}
gdrun --import
gdrun --check-only -s scripts/player.gd
gdrun -s tests/load_all.gd
GD_TIMEOUT=120 gdrun -s tests/smoke_level.gd| 情况 | gdrun 退出码 | 特征输出 |
| 正常 | 0 | 脚本自己的 print |
| 解析错误 | 1 | SCRIPT ERROR: Parse Error: ... 加 ERROR: Failed to load script |
| 运行时错误后没走到 quit | 137 | SCRIPT ERROR: Cannot call method ...,随后被 KILL |
push_error | 1 | ERROR: ... |
| 导入失败 | 1 | ERROR: Error importing 'res://bad.png'. |
- 排除的
resources still in use与RID allocations来自测试脚本没释放的节点 - 测试脚本自己
free()创建的节点,这两行就不出现
GDScript 警告
命令行运行默认不打印警告。下面的脚本生成 override.cfg,把 Warn 级警告改成 Error。
<!-- check: gdonly -->extends SceneTree
func _initialize() -> void:
var cfg := ConfigFile.new()
for p: Dictionary in ProjectSettings.get_property_list():
var key: String = p["name"]
if not key.begins_with("debug/gdscript/warnings/"):
continue
if typeof(ProjectSettings.get_setting(key)) == TYPE_INT and ProjectSettings.get_setting(key) == 1:
cfg.set_value("debug", key.trim_prefix("debug/"), 2)
cfg.save("res://override.cfg")
quit()gdrun -s tests/warn_as_error.gd
gdrun --quit-after 5
gdrun --check-only -s enemies/slime.gd
rm override.cfg- 只有被加载的脚本才报,没挂进场景的脚本逐个
--check-only - 默认关闭的项如
untyped_declaration不在其中 - 要查就在
override.cfg的[debug]下写gdscript/warnings/名字=2 override.cfg对之后所有运行生效,用完删掉- 不用
--debug看警告:解析错误时崩溃 134,运行时错误停在debug>
沙箱与只读目录
HOME 不可写时 Godot 报 Could not create directory,把三个 XDG 目录指到临时目录。项目目录也不可写时复制一份再跑。
T=$(mktemp -d)
cp -r . "$T/p"
export XDG_DATA_HOME="$T/d" XDG_CONFIG_HOME="$T/c" XDG_CACHE_HOME="$T/k" GD_PROJECT="$T/p"
mkdir -p "$T/p/tests"
cat > "$T/p/tests/warn_as_error.gd" <<'GD'
# 上一节的脚本全文
GD
gdrun --import
gdrun -s tests/warn_as_error.gd
gdrun --quit-after 5
gdrun --check-only -s enemies/slime.gdgdrun定义、复制、写脚本、运行放在同一条 Bash 命令里- 每次
mktemp -d建新副本,不rm -rf ./*清旧副本 - 修改写回原项目,再重新复制验证
- 副本里的
override.cfg不用删
冒烟脚本
继承 SceneTree,从 _initialize 开始,失败自己计数,最后 quit(code)。
<!-- check: gdonly -->extends SceneTree
var _failures: int = 0
func _check(ok: bool, msg: String) -> void:
if not ok:
_failures += 1
printerr("FAIL: ", msg)
func _initialize() -> void:
create_timer(30.0).timeout.connect(quit.bind(2))
_run()
func _run() -> void:
var level := (load("res://level.tscn") as PackedScene).instantiate()
root.add_child(level)
await physics_frame
for i: int in 10:
await physics_frame
_check(level.is_inside_tree(), "level in tree")
level.queue_free()
await process_frame
quit(1 if _failures > 0 else 0)- 自动加载在
_initialize里已是root的子节点,在_init里还不是 _initialize返回前root不在树内,_ready没跑- 用
global_position等全局变换前先await process_frame - 场景用
root.add_child挂上,进树后_ready才跑 - 物理相关断言等够
physics_frame再查 - 不用
assert,失败计数后quit(1) - 必须有看门狗:协程里报错后面不执行,进程不退出
- 结尾
queue_free创建的场景再等一帧,退出时无泄漏警告
模拟输入
<!-- check: gdonly -->extends SceneTree
func _initialize() -> void:
Input.action_press("jump")
print(Input.is_action_pressed("jump"))
Input.action_release("jump")
var ev := InputEventAction.new()
ev.action = "ui_accept"
ev.pressed = true
Input.parse_input_event(ev)
await process_frame
quit()Input.action_press当场生效,parse_input_event要过一帧- 动作名不在 InputMap 里时报错,状态恒为 false
- 无头下根窗口第一帧后只有 64x64,窗口外的点击点不到控件
await process_frame后设root.size = Vector2i(1152, 648)才能点DisplayServer.window_set_size无效- 点 UI 也可用焦点加
ui_accept,或直接pressed.emit() - 按下之后发松开
截图
headless 不渲染,用 xvfb 起真窗口。
xvfb-run -a -s "-screen 0 1280x720x24" "$GODOT_BIN" --path . --audio-driver Dummy --rendering-driver vulkan -s tests/shot.gd- 脚本里
await RenderingServer.frame_post_draw后save_png,再用 Read 看图 - 装了
mesa-vulkan-drivers时vulkan跑 Forward+,opengl3跑 Compatibility - 没有 Vulkan 驱动时
vulkan打 ERROR 后回退 OpenGL 3,会被判失败 - CI 上要么装驱动,要么改
opengl3
查 API
先看类参考 XML 与 docs.godotengine.org/en/stable/classes/class_<小写类名>.html。
<!-- check: gdonly -->extends SceneTree
func _initialize() -> void:
print(ClassDB.class_has_method("Node", "queue_free"))
print(ClassDB.class_get_signal_list("Timer", true).size())
quit()--doctool导出的 XML 说明为空,只能确认签名--gdscript-docs从脚本的##注释生成 XML
C# 工程
- 测试脚本仍用 GDScript,mono 版同时支持两种语言
- 运行前先
dotnet build,没构建就跑会卡住 - 构建与运行的实测输出见 references/csharp.md
陷阱
| 现象 | 做法 |
| 运行时 SCRIPT ERROR 后进程卡住,退出码不变 | 套 timeout -s KILL,脚本里放看门狗 |
--quit-after N 前报过错,退出码仍是 0 | 靠 grep 或 Logger 计数判失败 |
assert 失败只中断函数,走不到 quit() | 测试里不用 assert,release 导出里它被忽略 |
-d 遇错停在 debug>,EOF 后退出码 0 或崩溃 | 不带 -d,输入重定向到 /dev/null |
缺 ext_resource 的 .tscn 仍 load 出非 null | 同时查输出里的 ERROR |
--import 有导入或语法错误仍退出 0 | 看输出;错误只在首次导入打印 |
报 Identifier "X" not declared,X 是 class_name | 先 --import 建全局缓存,不删类型标注 |
--check-only 报 autoload 名 Identifier not found | 误报;SceneTree 脚本用 root.get_node("名字") |
--quit-after 与物理帧对不齐 | 加 --fixed-fps 60 |
headless 下截图 null,frame_post_draw 不触发 | 用 xvfb |
--write-movie 或 --log-file 失败 | 输出目录先建好 |
标准版跑 .cs 报 No loader found | 用 mono 版并先 dotnet build |
grep ERROR: 命中退出泄漏、ALSA、singleton is null | 具体排除这几行,不整类忽略 |
WARNING: N ObjectDB instances were leaked | 测试脚本没 free 创建的节点,--verbose 看明细 |
以为 --log-file 会让终端静默 | 两边都有输出 |
参考
- references/csharp.md:C# 项目先读,mono 版构建、运行、失败输出
- references/cli-flags.md:每个参数的作用与实测输出、退出码表、渲染驱动实测
- references/smoke-scripts.md:冒烟脚本全文、加载全部场景、check-only 循环、点按钮、截图、运行时查 API
- references/ci-and-frameworks.md:下载 Godot 的 URL、导出、GUT 与 gdUnit4 命令行
- https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html
- https://docs.godotengine.org/en/stable/classes/class_scenetree.html
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

