godot-cold-parse only ever sees scripts on the STARTUP path: autoloads and
the main scene chain. That is the correct scope for the job it was built for
(Sprint 36's `Could not find base class "MetaScreen"`, a registration-ORDER
bug), but it is far narrower than the name suggests, and most of the codebase
is invisible to it. Verified by deliberately breaking a non-startup UI script
and a test file in turn: cold-parse reported "clean", exit 0, for both.
That is the second half of today's false green. A parse error in
test_step_canvas_annotation_layer.gd survived cold-parse AND survived
gdUnit4, which reports the suites that DID load as a clean pass. Two gates,
one blind spot: neither verified that a file it never opened was openable.
godot-parse-sweep opens every .gd in the project (226 today, addons and
.godot excluded) and fails on any that will not parse.
The split between the two halves is forced, not stylistic. No Godot API
reports GDScript parse failure reliably:
- ResourceLoader.load(path, "GDScript", CACHE_MODE_IGNORE) SEGFAULTS the
engine on a script that fails to parse — it dies on exactly the input the
tool exists to find.
- GDScript.new() + source_code + reload() returns a clean error code but
detaches the script from its resource_path, so class_name, preload() and
relative extends stop resolving: it reported 150 of 226 healthy scripts
as broken.
- Plain ResourceLoader.load() neither crashes nor false-positives, but
returns a NON-null object for a broken script, so its return value is
useless.
The engine's own stderr is the only honest signal. So the GDScript half just
opens files and makes no verdict; the wrapper scrapes the diagnosis. The
wrapper also refuses to pass unless the sweep reported completion, so a
future break in the walk cannot itself become a false green.
Unlike cold-parse, "Cannot infer the type" is NOT filtered. That filter is
precisely why cold-parse stayed silent about the file below.
First run found a real one: client/tests/util/scene_helper.gd has not parsed
since 2026-02-25 — five months — because `func(a := null, ...)` cannot infer
a type from null. Fixed with explicit `: Variant` params. Blast radius is
zero (the helper has no importers, so nothing else was taken out with it),
but it went unseen by two gates for five months, which is the point.
Full suite green at 3660.
Pair session with Jeroen, 2026-07-27.
Co-Authored-By: Claude <noreply@anthropic.com>
105 lines
3.9 KiB
GDScript
105 lines
3.9 KiB
GDScript
## Scene testing utilities for gdUnit4 tests.
|
|
##
|
|
## Loads a scene, instantiates it into the test suite's node tree,
|
|
## and provides helpers for node existence, signal, and node-path queries.
|
|
##
|
|
## Usage (from a GdUnitTestSuite subclass):
|
|
## var helper := SceneHelper.create(self, "res://scenes/main.tscn")
|
|
## helper.assert_node_exists("World")
|
|
## var world := helper.get_node_at("World")
|
|
## helper.monitor_signal(world, "ready")
|
|
## # ... trigger something ...
|
|
## helper.assert_signal_emitted(world, "ready")
|
|
##
|
|
## Design constraints (D-030): server-free, no running autoload dependencies.
|
|
class_name SceneHelper
|
|
extends RefCounted
|
|
|
|
var _suite # GdUnitTestSuite — untyped to avoid load-order dependency
|
|
var _scene: Node
|
|
# signal_key -> int. Key is "<node_instance_id>:<signal_name>" for uniqueness.
|
|
var _signal_hits: Dictionary = {}
|
|
|
|
|
|
## Load, instantiate, and attach a scene to the test suite's node tree.
|
|
## The scene node is registered for auto-free by gdUnit4.
|
|
## Returns a helper instance; fails the test if the scene cannot be loaded.
|
|
static func create(suite: GdUnitTestSuite, scene_path: String) -> SceneHelper:
|
|
var helper := SceneHelper.new()
|
|
helper._suite = suite
|
|
|
|
var packed: PackedScene = load(scene_path)
|
|
if packed == null:
|
|
suite.assert_that(packed).override_failure_message(
|
|
"SceneHelper: could not load scene at '%s'" % scene_path
|
|
).is_not_null()
|
|
return helper
|
|
|
|
helper._scene = packed.instantiate()
|
|
suite.auto_free(helper._scene)
|
|
suite.add_child(helper._scene)
|
|
return helper
|
|
|
|
|
|
## Returns the scene root node.
|
|
func scene() -> Node:
|
|
return _scene
|
|
|
|
|
|
## Assert that a node at node_path exists under the scene root.
|
|
## Fails the current test if the node is absent.
|
|
func assert_node_exists(node_path: String) -> void:
|
|
var node := _scene.get_node_or_null(NodePath(node_path))
|
|
_suite.assert_that(node).override_failure_message(
|
|
"SceneHelper: expected node at path '%s' — not found" % node_path
|
|
).is_not_null()
|
|
|
|
|
|
## Return the node at node_path under the scene root, or null if absent.
|
|
func get_node_at(node_path: String) -> Node:
|
|
return _scene.get_node_or_null(NodePath(node_path))
|
|
|
|
|
|
## Begin tracking emissions of signal_name on node.
|
|
## Must be called before the action that triggers the signal.
|
|
## Fails the test if node does not have the named signal.
|
|
func monitor_signal(node: Node, signal_name: String) -> void:
|
|
if not node.has_signal(signal_name):
|
|
_suite.assert_that(false).override_failure_message(
|
|
"SceneHelper: node '%s' has no signal '%s'" % [node.name, signal_name]
|
|
).is_true()
|
|
return
|
|
var key := _signal_key(node, signal_name)
|
|
_signal_hits[key] = 0
|
|
# Lambda accepts up to 4 positional args to tolerate signals with up to 4 params.
|
|
# GDScript default-param lambdas handle being called with fewer args correctly.
|
|
# The params MUST be explicitly `: Variant` — `a := null` cannot infer a type
|
|
# from null and is a hard parse error, which silently took this whole file
|
|
# (and every suite importing it) out of the run until the parse sweep found it.
|
|
node.connect(
|
|
signal_name,
|
|
func(a: Variant = null, b: Variant = null, c: Variant = null, d: Variant = null):
|
|
_signal_hits[key] = _signal_hits.get(key, 0) + 1
|
|
)
|
|
|
|
|
|
## Assert that signal_name was emitted at least once since monitor_signal().
|
|
## Fails the test if monitor_signal() was not called first, or if count is zero.
|
|
func assert_signal_emitted(node: Node, signal_name: String) -> void:
|
|
var key := _signal_key(node, signal_name)
|
|
if not _signal_hits.has(key):
|
|
_suite.assert_that(false).override_failure_message(
|
|
"SceneHelper: '%s' was not monitored — call monitor_signal() first" % signal_name
|
|
).is_true()
|
|
return
|
|
var count: int = _signal_hits[key]
|
|
_suite.assert_int(count).override_failure_message(
|
|
"SceneHelper: signal '%s' on '%s' was not emitted (count=%d)" % [
|
|
signal_name, node.name, count
|
|
]
|
|
).is_greater(0)
|
|
|
|
|
|
static func _signal_key(node: Node, signal_name: String) -> String:
|
|
return "%d:%s" % [node.get_instance_id(), signal_name]
|