extends Node # Manual two-process smoke test for NetSim (task 2.8 acceptance: # "--net-sim-latency 80 measurably raises observed RTT"). Deliberately not # part of the pure-function suite — needs two real processes and real wall # time to observe a delayed pong. # # NetSim reads its own --net-sim-* flags directly from OS.get_cmdline_user_args() # (see net_sim.gd) — this driver only needs --role= and passes any # --net-sim-* flags straight through untouched. The host is where the # _pong reply gets delayed, so --net-sim-latency=/--net-sim-loss= belong on # the HOST invocation; the client just observes NetworkManager.rtt_ms. # # Usage: # godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=host --net-sim-latency=80 # godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=client --min-rtt=70 # # For the loss scenario: # godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=host --net-sim-loss=1.0 # godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=client-loss const DEFAULT_PORT := 7810 const TIMEOUT_SECONDS := 12.0 const HOST_LIFETIME_SECONDS := 8.0 # Loose ceiling, not a tight bound: real localhost jitter plus one full # PING_INTERVAL_SEC of scheduling slack is possible before the first sample # lands, so this only needs to catch a badly broken (e.g. no-op) NetSim. const MAX_RTT_SLACK_MS := 400.0 var _role := "" var _port := DEFAULT_PORT var _min_rtt_ms := 0.0 var _finished := false func _ready() -> void: for arg: String in OS.get_cmdline_user_args(): if arg.begins_with("--role="): _role = arg.substr("--role=".length()) elif arg.begins_with("--port="): _port = int(arg.substr("--port=".length())) elif arg.begins_with("--min-rtt="): _min_rtt_ms = arg.substr("--min-rtt=".length()).to_float() if _role == "host": var err := NetworkManager.host(_port) if err != OK: _finish(false, "host() failed: %s" % error_string(err)) return print("SMOKE: hosting on port %d (net-sim latency=%.1fms jitter=%.1fms loss=%.2f)" % [ _port, NetSim.latency_ms, NetSim.jitter_ms, NetSim.loss_fraction ]) get_tree().create_timer(HOST_LIFETIME_SECONDS).timeout.connect(func() -> void: _finish(true, "host ran for %.1fs" % HOST_LIFETIME_SECONDS)) elif _role == "client": NetworkManager.clock_updated.connect(_on_clock_updated) var err := NetworkManager.join("127.0.0.1", _port) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return print("SMOKE: joining 127.0.0.1:%d, expecting rtt >= %.1fms ..." % [_port, _min_rtt_ms]) elif _role == "client-loss": NetworkManager.clock_updated.connect(_on_unexpected_clock_updated) var err := NetworkManager.join("127.0.0.1", _port) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return print("SMOKE: joining 127.0.0.1:%d, expecting NO rtt sample (100%% loss) ..." % _port) get_tree().create_timer(HOST_LIFETIME_SECONDS - 1.0).timeout.connect(func() -> void: _finish(NetworkManager.rtt_ms < 0.0, "rtt_ms=%.1f after %.1fs (expected -1, no pong ever arrived)" % [ NetworkManager.rtt_ms, HOST_LIFETIME_SECONDS - 1.0 ])) else: _finish(false, "missing or unrecognised --role= (expected host|client|client-loss)") return get_tree().create_timer(TIMEOUT_SECONDS).timeout.connect(_on_timeout) func _process(_delta: float) -> void: NetworkManager.poll() func _physics_process(_delta: float) -> void: NetworkManager.poll() func _on_clock_updated(rtt_ms: float, _offset_ms: float) -> void: var ceiling := _min_rtt_ms * 4.0 + MAX_RTT_SLACK_MS var ok := rtt_ms >= _min_rtt_ms and rtt_ms <= ceiling print("SMOKE INFO: observed rtt_ms=%.2f (want >= %.1f, <= %.1f)" % [rtt_ms, _min_rtt_ms, ceiling]) _finish(ok, "client observed rtt_ms=%.2f against min=%.1f" % [rtt_ms, _min_rtt_ms]) func _on_unexpected_clock_updated(rtt_ms: float, _offset_ms: float) -> void: _finish(false, "client received a pong (rtt_ms=%.2f) despite --net-sim-loss=1.0 on the host" % rtt_ms) func _on_timeout() -> void: if not _finished: _finish(false, "timed out waiting for a clock sample") func _finish(success: bool, message: String) -> void: if _finished: return _finished = true print("SMOKE %s: %s" % ["PASS" if success else "FAIL", message]) await get_tree().create_timer(0.3).timeout NetworkManager.shutdown() get_tree().quit(0 if success else 1)