agentleFS
MindsMasterMindsMaster/godot-claude-code-skills/skills/godot-headless-testing/SKILL.md

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

The file

Files
SKILL.md

Godot 无头测试与命令行验证

用 Godot 命令行验证改动:参数、失败判定、冒烟脚本、输入模拟、截图、CI。示例在 4.7.2 编辑器版实测,$GODOT_BIN 指可执行文件。

范围

不含去哪
单个节点或 API 的用法对应领域 skill
GDScript 警告的含义与修法godot-gdscript
C# 构建、源码生成器、命名godot-csharp
编辑器插件、EditorScriptgodot-editor-tooling
性能测量与 Profilergodot-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 用 opengl3headless 下截图
录一段画面--write-movie out/f.png 或 .avi,同样要 xvfb循环截图
查类和方法是否存在ClassDB.class_has_method、类参考 XML凭记忆写
导出验证--export-pack,不需要模板手动打 zip
日志留档--log-file,目录须已存在tee 之外再造日志类
单元测试库GUT 或 gdUnit4自写断言框架

做法

验证顺序

前一步不过不进下一步。

  1. 改了 class_name、资源、自动加载、输入映射:--import
  2. 每个改动的脚本 --check-only
  3. 加载全部场景与脚本,看输出里的 ERROR
  4. 冒烟脚本,覆盖改到的系统
  5. 工程里有 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
解析错误1SCRIPT ERROR: Parse Error: ... 加 ERROR: Failed to load script
运行时错误后没走到 quit137SCRIPT ERROR: Cannot call method ...,随后被 KILL
push_error1ERROR: ...
导入失败1ERROR: 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.gd
  • gdrun 定义、复制、写脚本、运行放在同一条 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 会让终端静默两边都有输出

参考

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.