mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
4533da34e0
Lands tasks 1.0-1.8 of multiplayer-todo.md: the pure-function test runner, net_codec (wire format quantizers/pack-unpack), NetworkManager (ENet transport, manual polling, min-RTT clock sync), MatchNet (handshake, protocol/tick-rate gating, roster with team+ready state), lobby.tscn (team columns, switch team, ready toggle), server_boot.tscn (headless dedicated server with structured logging and an overrun watchdog), and main_menu.gd's Host/Join-by-IP UI (connecting overlay, cancel, bounded failure path). Followed by an adversarial review (Opus subagent) that found and fixed two real bugs - an unvalidated player_name broadcast that let one client's oversized name head-of-line-block the reliable channel for everyone, and a server-side roster leak across a host/re-host cycle - plus three gaps in the test suite itself where a claim of "verified" wasn't actually backed by what the test checked. All five two-process smoke tests plus the pure-function suite are green with the strengthened assertions in place.
82 lines
3.1 KiB
GDScript
82 lines
3.1 KiB
GDScript
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
|