extends Node # Headless test runner (task 1.0). Discovers every *.gd under tests/cases/, # instances it, and calls every test_*() method by name — a test case is # picked up by dropping a file in that folder, not by registering it here. # Run with: godot --headless --path Game res://tests/test_runner.tscn const CASES_DIR := "res://tests/cases" const TestCase = preload("res://tests/test_case.gd") func _ready() -> void: var case_paths := _discover_case_paths() var total := 0 var failed := 0 var failure_messages: Array[String] = [] for path in case_paths: # Adversarial-review regression: a case file with a parse/compile # error used to hang the whole runner forever. load() on a broken # script does NOT return null here — it returns a non-null but # uninstantiable GDScript resource, so a plain null check doesn't # catch it; calling .new() on it throws "Invalid call: Nonexistent # function 'new'", severe enough to abort _ready() entirely without # ever reaching quit(). can_instantiate() is the real guard. var script: GDScript = load(path) if script == null or not script.can_instantiate(): failed += 1 failure_messages.append("%s: failed to load (parse/compile error — see SCRIPT ERROR above)" % path.get_file()) continue var instance = script.new() if instance == null: failed += 1 failure_messages.append("%s: script.new() returned null" % path.get_file()) continue for method in instance.get_method_list(): var method_name: String = method["name"] if not method_name.begins_with("test_"): continue total += 1 instance.failures.clear() instance.assertions_made = 0 instance.call(method_name) # Adversarial-review regression: GDScript has no exceptions, so # a runtime error partway through a test (before it reaches its # first assert_*) just logs a SCRIPT ERROR and returns — # `failures` stays empty exactly as if every assertion passed, # and this used to count as a PASS. A test that completes # having made zero assertions is itself a failure: it proved # nothing, whether because it crashed early or was just never # written to assert anything. if instance.assertions_made == 0: failed += 1 failure_messages.append("%s.%s: made no assertions (crashed before the first assert_*, or the test itself is incomplete)" % [path.get_file(), method_name]) elif not instance.failures.is_empty(): failed += 1 for f in instance.failures: failure_messages.append("%s.%s: %s" % [path.get_file(), method_name, f]) print("Ran %d tests from %d case file(s), %d failed" % [total, case_paths.size(), failed]) for message in failure_messages: print(" FAIL: " + message) get_tree().quit(1 if failed > 0 else 0) func _discover_case_paths() -> Array[String]: var paths: Array[String] = [] var dir := DirAccess.open(CASES_DIR) if dir == null: push_error("Cannot open " + CASES_DIR) return paths dir.list_dir_begin() var file_name := dir.get_next() while file_name != "": if file_name.ends_with(".gd") and not dir.current_is_dir(): paths.append(CASES_DIR + "/" + file_name) file_name = dir.get_next() dir.list_dir_end() paths.sort() return paths