godot-gdscript-headless-testing について
Godot 4.7のヘッドレスコマンドラインでGDScriptテストを実行するためのスキルガイドです。終了コードやCI設定、インポート手順、ハングの原因を扱います。
主な機能
- 01ヘッドレスでのGDScriptテスト実行
- 02SceneTreeテストランナーのパターン
- 03失敗数とプロセス終了コード
- 04新規チェックアウト時のインポート設定
- 05CIタイムアウトとトラブルシューティング
活用シーン
- 01ターミナルでロジックテストを実行
- 02テスト失敗時にCIビルドを失敗させる
- 03ヘッドレス実行のハングや異常を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gamedev-skills/ awesome-gamedev-agent-skills --skill godot-gdscript-headless-testingインストール後、Claude Code で/godot-gdscript-headless-testingを入力して読み込まれたか確認します。
詳細
Godot GDScript のヘッドレステスト(4.x)
エディター GUI を使わずにコマンドラインから GDScript テストを実行し、CI が判定できる実際のプロセス終了コードを取得します。対象は Godot 4.7 のヘッドレス CLI です。
使用する場面
- Godot プロジェクトにテスト用アドオンがインストールされておらず、ターミナルまたは CI パイプラインから GDScript のロジック(純粋関数、リソースの読み込み、オートロードの状態)を手早く検証したい場合に使用します。
.gdテストが失敗したときにビルドを失敗させる CI ジョブを設定する場合に使用します。godot --headlessの実行がハングする、ウィンドウが開く、またはアサーションが失敗しても終了コードが 0 になる原因を調べる場合に使用します。
使用しない場面: GDScript の構文や言語機能そのもの → godot-gdscript、エクスポート/ビルドのパイプラインやプラットフォームテンプレート → godot-export(独自の --headless のユースケースであり、テストの実行ではなくバイナリを生成します)。
ワークフロー
- バイナリがヘッドレスで起動することを確認します。 Godot 4.x には
--headlessが標準で組み込まれています(エクスポートテンプレートは不要です)。godot --headless --versionを実行し、GUI ウィンドウではなくバージョン文字列が出力されることを確認します。 - 新しいチェックアウトでは、テストの実行前にインポートします。 通常
.godot/はコミットされないため、クリーンなチェックアウトにはインポートキャッシュがありません。そのため、class_name型の解決に失敗し(Identifier "X" not declared in the current scope)、インポート済みアセットの読み込みにも失敗します(No loader found for resource: res://...)。CI とローカルの両方で、最初に一度godot --headless --path <project_dir> --importを実行します。 - ランナーは
Nodeシーンではなく、SceneTreeスクリプトとして作成します。SceneTreeスクリプトの_initialize()は最初のフレームより前に一度実行されるため、純粋なロジックのテストには十分で、起動用の.tscnも不要です。 - 成功数と失敗数は自分で記録し、明示的に
quit(<code>)を呼び出します。テストを失敗させるために、単独のassert()を使わないでください。push_error()だけでは Godot はプロセスの終了コードを非ゼロにしません。ランナー側で失敗数を数え、quit(1)を呼び出す必要があります。さらに問題なのは、_initialize()内のassert()が失敗すると(公式/デバッグビルド)、SCRIPT ERROR: Assertion failedが出力され、quit()が実行される前に処理が停止するため、プロセスが終了せず、CI がタイムアウトするまでハングすることです。失敗を記録し、処理を続けるassert_eq()形式のヘルパーを使用します。 - **
godot --headless --path <project_dir> --script res://<runner>.gdで実行し、**シェルまたは CI のステップから標準出力だけでなく、プロセス終了コードを確認します。--scriptにはres://からの相対パスと絶対ファイルシステムパス(プロジェクトフォルダー外にあるランナーなど)の両方を指定でき、どちらも動作します。 - ラッパーシェルから実行をスクリプト化する場合は、標準出力と標準エラー出力をファイルにリダイレクトします(PowerShell、一部の CI ランナー)。
push_error()の出力は標準エラー出力に送られるため、標準出力だけをライブで取得すると、欠落したり順序が入れ替わったりすることがあります。 - CI にステップのタイムアウトを設定します。
assert()の落とし穴を回避しても、解決されないawait(パターン #2)があるとランナーは永久にハングします。CI ステップのtimeout-minutesは CI 側の最後の防護策であり、すべてのawaitにタイムアウトノードを用意する代わりにはなりません。
パターン
1. 実際の終了コードを返す最小限の SceneTree テストランナー
# res://test_runner.gd — run with:
# godot --headless --path . --script res://test_runner.gd
extends SceneTree
var passed := 0
var failed := 0
func _initialize() -> void:
test_add()
print("Results: %d passed, %d failed" % [passed, failed])
quit(1 if failed > 0 else 0) # non-zero exit fails the CI step
func assert_eq(actual, expected, label: String) -> void:
if actual == expected:
passed += 1
else:
failed += 1
push_error("FAIL %s: expected %s, got %s" % [label, expected, actual])
func test_add() -> void:
assert_eq(2 + 2, 4, "test_add")
Godot 4.7.2 で検証済みです。godot --headless --path . --script res://test_runner.gd を実行すると、Results: N passed, M failed が標準出力に出力され、push_error の行は標準エラー出力に送られます。また、failed == 0 の場合はプロセス終了コード 0、それ以外の場合は 1 が返されます。
2. フレーム、タイマー、またはシグナルが必要なものをテストする
extends SceneTree
var passed := 0
var failed := 0
func _initialize() -> void:
await run_tests()
print("Results: %d passed, %d failed" % [passed, failed])
quit(1 if failed > 0 else 0) # track and report failures here too
func assert_eq(actual, expected, label: String) -> void:
if actual == expected:
passed += 1
else:
failed += 1
push_error("FAIL %s: expected %s, got %s" % [label, expected, actual])
func run_tests() -> void:
# `root` is not inside the tree yet during _initialize(): a Timer started now
# errors ("not inside the tree") and its `timeout` never fires. Wait one frame.
await process_frame
var timer_node := Timer.new()
timer_node.one_shot = true # default Timer restarts after timeout
root.add_child(timer_node)
timer_node.start(0.1)
await timer_node.timeout
# assertions here can rely on the node having been in the tree for a frame
assert_eq(timer_node.is_stopped(), true, "timer_fires_once")
timer_node.queue_free()
_initialize() は await できます。これにより、ノードが実際にツリーに入る必要がある場合、タイマーが発火する必要がある場合、またはシグナルを送出する必要がある場合にも、このパターンが機能します。いずれもエンジンが少なくとも 1 フレーム処理するまでは発生しません。パターン #1 と同じ passed/failed カウンターと assert_eq() ヘルパーを使用します。常に quit(0) を呼び出すこのパターンの実装では、ビルドを失敗させることができません。
3. 終了コードでゲートする CI ステップ(GitHub Actions)
- name: Import project (populates .godot/ on a fresh checkout)
run: godot --headless --path . --import
- name: Run GDScript tests
timeout-minutes: 5
run: godot --headless --path . --script res://test_runner.gd
クリーンなチェックアウトでは、インポートステップが必要です。これを行わないと、class_name 型とインポート済みリソースの解決に失敗します。テストステップ自体に追加のフラグは必要ありません。run: の終了コードが 0 以外の場合、ランナーはすでにジョブを失敗させます。徹底すべきなのは CI の設定ではなく、ランナースクリプト内で quit() を呼び出すことです。timeout-minutes は await がハングした場合の最後の保護策であり(「落とし穴」を参照)、すべての await にタイムアウトノードを用意する代わりにはなりません。
落とし穴
- スクリプトが「何もしない」、またはエディターウィンドウが開く →
--headlessが指定されていないか、スクリプトのパスが間違っています。--scriptは--path <project_dir>を基準に解決されるres://相対パスと、絶対ファイルシステムパスを受け付けます。どちらも機能します。 - 新しいチェックアウトでのみ、
Identifier "X" not declared in the current scopeが表示される、またはリソースの読み込みに失敗する →.godot/(インポートキャッシュ)は通常コミットされないため、class_name型とインポート済みアセットはまだ解決されていません。テストステップの前に、godot --headless --path <project_dir> --importを一度実行してください。 assert()が失敗しても、テストが失敗せずにハングする → 公式ビルドまたはデバッグビルドでは、_initialize()内のassert()が失敗するとSCRIPT ERROR: Assertion failedが出力され、quit()に到達する前にその関数が停止します。プロセスは終了せず、CI はタイムアウトまで待機します。テストランナーでは、単独のassert()ではなくassert_eq()カウンター(パターン #1)を使用してください。- アサーションが失敗しても終了コードが 0 のままになる → ランナーが
quit(1)を呼び出していないか、(パターン #2)失敗の有無にかかわらず常にquit(0)を呼び出しています。失敗を自分で追跡し、その結果を反映するコードを明示的に指定してquit()を呼び出してください。終了コードが変わることをassert()やpush_error()のみに頼らないでください。 - ノード、タイマー、シグナルがまだ存在しないうちに
_initialize()が実行される → フレームの処理を必要とするロジックでは、アサーションを行う前にシグナルまたはタイマーをawaitする必要があります。パターン #2 を参照してください。root自体がまだシーンツリー内にないため、そこでTimerを追加して開始するとエラーが発生し、timeoutは発火しません(ランナーがハングします)。まずawait process_frameを実行してください。 - ラッパーシェルから見ると、出力が空に見えたり順序が乱れたりする → 一部のシェル(特に PowerShell)は、ネイティブプロセスの stdout/stderr をリアルタイムに表示する際、順序を変えたり出力を取りこぼしたりすることがあります。リアルタイムのコンソール表示を信頼するのではなく、両方のストリームをファイルにリダイレクトし、プロセス終了後にファイルを読み込んでください。
- ランナーが終了しない →
SceneTreeスクリプトは、何かがquit()を呼び出すまで実行を続けます。発火しないシグナルをawaitするテストはジョブを永久にハングさせます。必ずawaitにフォールバック用のタイムアウトノードを用意し、最後の保護策として CI ステップにtimeout-minutesを設定してください。
関連スキル
godot-gdscript— このパターンのランナースクリプトで使用する言語構文とノードのライフサイクルです。godot-export— ヘッドレス CLI によるエクスポート/ビルドです。--headlessの別の使用例です。