extends Node # ═══════════════════════════════════════════════════════════════════════════ # Network Simulator — Unit & Integration Tests # # Tests the NetSim network condition simulator for correctness: # - Packet loss rate (statistical, within confidence) # - Latency simulation (timing) # - Jitter distribution # - Packet reordering # - Duplication rate # - Burst loss pattern # - Bandwidth throttling # - Silent passthrough with all conditions at 0 # - Profile presets # - Multi-peer isolation # # Run with: godot --headless --script tests/test_net_sim.gd # ═══════════════════════════════════════════════════════════════════════════ const SIM_SCRIPT = preload("res://server/scripts/net_sim.gd") const UTILS_SCRIPT = preload("res://tests/test_utils.gd") var sim # NetSim — loaded via preload var tu # TestUtils — loaded via preload # ─── Test Suites ────────────────────────────────────────────────────────── func _test_silent_passthrough() -> void: tu.describe("Silent Passthrough — zero conditions, no side effects") sim.packet_loss = 0.0 sim.latency_ms = 0 sim.jitter_ms = 0 sim.reorder_window = 0 sim.duplicate_rate = 0.0 sim.bandwidth_limit = 0 sim.burst_loss_count = 0 sim.burst_loss_interval = 0 var sent = 100 var received = 0 for i in range(sent): var pkt = SIM_SCRIPT.make_test_packet(i, "test") if sim.send_packet(pkt, SIM_SCRIPT.CHANNEL_UNRELIABLE): pass # All should be in inbound queue (no loss) received = sim._outbound_queue.size() tu.assert_eq(received, sent, "all packets queued with no conditions") # No dropped stats tu.assert_eq(sim._total_lost, 0, "zero loss") tu.assert_eq(sim._total_burst_dropped, 0, "zero burst drops") tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bw drops") # Drain queue sim._outbound_queue.clear() func _test_packet_loss_rate() -> void: tu.describe("Packet Loss — statistical rate verification") sim.packet_loss = 0.2 # 20% loss sim.latency_ms = 0 sim.jitter_ms = 0 sim.reorder_window = 0 sim.duplicate_rate = 0.0 sim.bandwidth_limit = 0 sim.burst_loss_count = 0 sim.burst_loss_interval = 0 sim.reset_stats() var total = 5000 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "payload_%d" % i) sim.send_packet(pkt) # With 20% loss on 5000 packets: expected ~1000 lost # Acceptable range: 800–1200 (within 4 sigma of binomial) var lost = sim._total_lost tu.assert_true(lost >= 700 and lost <= 1300, "loss count %d in acceptable range [700, 1300] for 20%% on %d" % [lost, total]) sim.reset_stats() sim.packet_loss = 0.0 func _test_latency_timing() -> void: tu.describe("Latency — timing accuracy") sim.packet_loss = 0.0 sim.latency_ms = 100 # 100ms one-way sim.jitter_ms = 0 sim.reorder_window = 0 sim.duplicate_rate = 0.0 sim.burst_loss_count = 0 sim.burst_loss_interval = 0 sim.reset_stats() var pkt = SIM_SCRIPT.make_test_packet(1, "latency_test") var before = Time.get_ticks_msec() sim.send_packet(pkt) # Packet should be in queue with delay tu.assert_eq(sim._outbound_queue.size(), 1, "one packet queued") var queued = sim._outbound_queue[0] var expected_delivery = before + 100 var actual_delivery = queued.time var diff = actual_delivery - before tu.assert_true(diff >= 95 and diff <= 105, "latency %dms within [95, 105]ms tolerance (got %dms)" % [100, diff]) sim.reset_stats() func _test_jitter_distribution() -> void: tu.describe("Jitter — distribution within range") sim.packet_loss = 0.0 sim.latency_ms = 50 sim.jitter_ms = 30 # ±30ms -> effective range: [20, 80] sim.reset_stats() var delays: Array[float] = [] var samples = 200 var before = Time.get_ticks_msec() for i in range(samples): var pkt = SIM_SCRIPT.make_test_packet(i, "jitter_test") sim.send_packet(pkt) var queued = sim._outbound_queue[i] delays.append(queued.time - before) before = Time.get_ticks_msec() # time moves forward naturally # Check bounds: each delay should be latency ± jitter # Min: max(0, 50-30) = 20, Max: 50+30 = 80 var within_bounds = true var min_seen = 999.0 var max_seen = 0.0 for d in delays: if d < 20.0 or d > 80.0: within_bounds = false min_seen = min(min_seen, d) max_seen = max(max_seen, d) tu.assert_true(within_bounds, "all jittered delays within [20, 80]ms (min=%.1f max=%.1f)" % [min_seen, max_seen]) sim.reset_stats() func _test_packet_duplication() -> void: tu.describe("Duplication — statistical rate verification") sim.packet_loss = 0.0 sim.latency_ms = 0 sim.duplicate_rate = 0.15 # 15% sim.reset_stats() var total = 2000 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "dupe_test") sim.send_packet(pkt) var dupe = sim._total_duplicated tu.assert_true(dupe >= 150 and dupe <= 450, "duplicate count %d in acceptable range [150, 450] for 15%% on %d" % [dupe, total]) sim.duplicate_rate = 0.0 sim.reset_stats() func _test_burst_loss() -> void: tu.describe("Burst Loss — pattern verification") sim.packet_loss = 0.0 sim.latency_ms = 0 sim.burst_loss_count = 5 # drop 5 consecutive packets sim.burst_loss_interval = 10 # every 10 packets sim.reset_stats() var total = 50 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "burst") sim.send_packet(pkt) # With burst_loss_interval=10 and burst_loss_count=5: # Packets 10-14 dropped, 20-24 dropped, 30-34 dropped, 40-44 dropped = 20 drops var burst_drops = sim._total_burst_dropped tu.assert_eq(burst_drops, 20, "expected 20 burst drops out of 50 (5/10 pattern = 20)") # The sum of inbound + burst_dropped should equal total var queued = sim._outbound_queue.size() tu.assert_eq(queued + burst_drops, total, "queued %d + burst_dropped %d = total %d" % [queued, burst_drops, total]) sim.burst_loss_count = 0 sim.burst_loss_interval = 0 sim.reset_stats() func _test_bandwidth_throttling() -> void: tu.describe("Bandwidth — throttling verification") sim.packet_loss = 0.0 sim.latency_ms = 0 var pkt_size = 100 # bytes per test packet sim.bandwidth_limit = 2000 # 2000 bytes/s sim.reset_stats() var total = 100 for i in range(total): var data = PackedByteArray() data.resize(pkt_size) data[0] = i sim.send_packet(data) # At 2000 bytes/s with 100-byte packets: max ~20 packets should pass var bw_drops = sim._total_bandwidth_dropped tu.assert_true(bw_drops >= 70, "at least 70 of 100 packets dropped by bandwidth limit (got %d)" % bw_drops) tu.assert_true(sim._outbound_queue.size() <= 30, "at most 30 packets pass bandwidth limit (got %d)" % sim._outbound_queue.size()) sim.bandwidth_limit = 0 sim.reset_stats() func _test_reorder_window() -> void: tu.describe("Packet Reordering — window flipping") sim.packet_loss = 0.0 sim.latency_ms = 1 sim.jitter_ms = 0 sim.reorder_window = 5 sim.reset_stats() # Send 20 packets for i in range(20): var pkt = SIM_SCRIPT.make_test_packet(i, "reorder") sim.send_packet(pkt) # Flush the reorder buffer sim._flush_reorder_buffer() # The reorder buffer shuffles groups of 5. # This doesn't guarantee reorder happened (shuffle may produce same order), # but it should have triggered the reorder path tu.assert_true(sim._total_reordered > 0, "reordering occurred (>0 packets shuffled)") sim.reorder_window = 0 sim.reset_stats() func _test_profile_presets() -> void: tu.describe("Profile Presets — quick-set correctness") tu.assert_true(sim.set_profile("lan"), "lan profile set") tu.assert_eq(sim.packet_loss, 0.0, "lan: no loss") tu.assert_eq(sim.latency_ms, 1, "lan: 1ms latency") tu.assert_true(sim.set_profile("cellular"), "cellular profile set") tu.assert_eq(sim.packet_loss, 0.02, "cellular: 2% loss") tu.assert_eq(sim.latency_ms, 60, "cellular: 60ms latency") tu.assert_true(sim.set_profile("satellite"), "satellite profile set") tu.assert_eq(sim.packet_loss, 0.01, "satellite: 1% loss") tu.assert_eq(sim.latency_ms, 600, "satellite: 600ms latency") tu.assert_true(sim.set_profile("warzone"), "warzone profile set") tu.assert_eq(sim.packet_loss, 0.10, "warzone: 10% loss") tu.assert_eq(sim.latency_ms, 200, "warzone: 200ms latency") tu.assert_eq(sim.burst_loss_count, 3, "warzone: burst loss 3") # Unknown profile should fail tu.assert_false(sim.set_profile("nonexistent"), "unknown profile fails") func _test_stats_report() -> void: tu.describe("Stats Report — format correctness") sim.packet_loss = 0.1 sim.reset_stats() for i in range(100): var pkt = SIM_SCRIPT.make_test_packet(i, "stats") sim.send_packet(pkt) var report = sim.get_stats_string() tu.assert_true(report.contains("Sent:"), "report contains Sent:") tu.assert_true(report.contains("Config:"), "report contains Config:") tu.assert_true(report.contains("Effective loss:"), "report contains Effective loss:") func _test_high_loss_extreme() -> void: tu.describe("Extreme Conditions — 100% packet loss") sim.packet_loss = 1.0 sim.latency_ms = 0 sim.reset_stats() for i in range(100): var pkt = SIM_SCRIPT.make_test_packet(i, "all_lost") sim.send_packet(pkt) tu.assert_eq(sim._total_lost, 100, "all 100 packets lost at 100% loss") tu.assert_eq(sim._outbound_queue.size(), 0, "no packets in queue at 100% loss") sim.packet_loss = 0.0 sim.reset_stats() func _test_zero_bandwidth() -> void: tu.describe("Edge: zero bandwidth limit") sim.packet_loss = 0.0 sim.latency_ms = 0 sim.bandwidth_limit = 0 # 0 = unlimited sim.reset_stats() for i in range(10): var pkt = SIM_SCRIPT.make_test_packet(i, "no_bw_limit") sim.send_packet(pkt) # Should not drop any — 0 means unlimited tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bandwidth limit treated as unlimited") tu.assert_eq(sim._outbound_queue.size(), 10, "all 10 packets queued with unlimited bw") sim.reset_stats() func _test_complex_profile() -> void: tu.describe("Complex Profile — combined conditions") # Simulate a realistic bad network: 3% loss, 80ms latency, 25ms jitter sim.packet_loss = 0.03 sim.latency_ms = 80 sim.jitter_ms = 25 sim.duplicate_rate = 0.01 sim.reorder_window = 0 sim.bandwidth_limit = 0 sim.burst_loss_count = 0 sim.reset_stats() var total = 1000 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "complex") sim.send_packet(pkt) var queued = sim._outbound_queue.size() var lost = sim._total_lost var duped = sim._total_duplicated var delayed = sim._total_delayed # At 3% loss: ~30 lost (range: 10-60) tu.assert_true(lost >= 5 and lost <= 80, "loss %d in range for 3%% on %d" % [lost, total]) # At 1% dupe: ~10 dupes tu.assert_true(duped >= 0 and duped <= 40, "dupes %d in range for 1%% on %d" % [duped, total]) # All non-lost packets should be delayed (latency > 0) tu.assert_true(delayed >= queued, "all queued packets marked as delayed (%d >= %d)" % [delayed, queued]) sim.reset_stats() # ═══════════════════════════════════════════════════════════════════════════ # NEW: Edge Case Tests (Phase 6 bug bash) # ═══════════════════════════════════════════════════════════════════════════ func _test_jitter_exceeds_latency() -> void: tu.describe("Edge: Jitter exceeds latency — negative delay clamping") # When jitter > latency, the per-packet delay can go negative. # max(0, delay) clamps to 0. Test that all packets still get some delay # and no crashes occur. sim.packet_loss = 0.0 sim.latency_ms = 30 sim.jitter_ms = 100 # ±100ms on 30ms base → effective range [-70, +130] → clamped to [0, 130] sim.reset_stats() var total = 500 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "neg_jitter") sim.send_packet(pkt) # All packets should be queued (no loss) var queued = sim._outbound_queue.size() tu.assert_eq(queued, total, "all %d packets queued despite negative jitter" % total) # All should be marked as delayed (some have 0ms due to clamping but # the _total_delayed counter counts packets that got any non-zero delay) tu.assert_true(sim._total_delayed > 0, "at least some packets received non-zero delay (got %d)" % sim._total_delayed) # Verify no negative delivery times for entry in sim._outbound_queue: tu.assert_true(entry.time >= 0, "all delivery times >= 0 (no wrap)") sim.reset_stats() func _test_combined_high_loss_latency() -> void: tu.describe("Edge: High loss (25%) + high latency (500ms) combined") # Simulate extreme network conditions: 25% loss, 500ms latency, 100ms jitter sim.packet_loss = 0.25 sim.latency_ms = 500 sim.jitter_ms = 100 sim.duplicate_rate = 0.0 sim.bandwidth_limit = 0 sim.burst_loss_count = 0 sim.reset_stats() var total = 2000 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "extreme") sim.send_packet(pkt) var lost = sim._total_lost var queued = sim._outbound_queue.size() # At 25% loss on 2000: expected ~500 lost (range: 350-650) tu.assert_true(lost >= 300 and lost <= 700, "loss %d in acceptable range [300, 700] for 25%% on %d" % [lost, total]) # Queued = total - lost tu.assert_eq(queued, total - lost, "queued %d + lost %d = total %d" % [queued, lost, total]) # All queued packets should have delay near 500ms var min_delay = 99999.0 var max_delay = 0.0 var now = Time.get_ticks_msec() for entry in sim._outbound_queue: var effective = entry.time - now min_delay = min(min_delay, effective) max_delay = max(max_delay, effective) # With 500ms latency + 100ms jitter: effective range [400, 600] tu.assert_true(min_delay >= 0, "minimum effective delay >= 0 (got %.1f)" % min_delay) tu.assert_true(max_delay <= 700, "maximum effective delay <= 700 (got %.1f)" % max_delay) sim.reset_stats() func _test_burst_with_reorder() -> void: tu.describe("Edge: Burst loss + reorder window combined") sim.packet_loss = 0.0 sim.latency_ms = 5 sim.jitter_ms = 0 sim.burst_loss_count = 3 sim.burst_loss_interval = 10 sim.reorder_window = 4 sim.reset_stats() var total = 60 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "burst_reorder") sim.send_packet(pkt) # Flush reorder buffer sim._flush_reorder_buffer() # With interval=10, count=3: ~6 bursts = 18 drops var burst_drops = sim._total_burst_dropped var queued = sim._outbound_queue.size() var triggered = sim._total_reordered tu.assert_eq(queued + burst_drops, total, "queued %d + burst_drops %d = total %d" % [queued, burst_drops, total]) tu.assert_true(triggered > 0, "reordering was triggered (%d packets reordered)" % triggered) sim.burst_loss_count = 0 sim.burst_loss_interval = 0 sim.reorder_window = 0 sim.reset_stats() func _test_rapid_sequence_10k() -> void: tu.describe("Performance: 10,000 rapid packets — timing precision") # Simulate rapid-fire packet generation at 128 tickrate sim.packet_loss = 0.02 sim.latency_ms = 50 sim.jitter_ms = 10 sim.duplicate_rate = 0.005 sim.reset_stats() var total = 10000 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "rapid_%d" % i) sim.send_packet(pkt) var lost = sim._total_lost var duped = sim._total_duplicated var queued = sim._outbound_queue.size() tu.assert_eq(queued + lost, total, "queued %d + lost %d = total %d (no phantom packets)" % [queued, lost, total]) # At 2% loss on 10000: ~200 lost (range: 100-400) tu.assert_true(lost >= 50 and lost <= 500, "loss %d in range for 2%% on %d" % [lost, total]) # At 0.5% dupe: ~50 dupes tu.assert_true(duped >= 5 and duped <= 200, "dupes %d in range for 0.5%% on %d" % [duped, total]) # Verify no negative delivery times for entry in sim._outbound_queue: tu.assert_true(entry.time >= 0, "all delivery times >= 0 (no integer overflow)") sim.reset_stats() func _test_bandwidth_with_loss() -> void: tu.describe("Edge: Bandwidth limit combined with packet loss") # Loss removes some packets before bandwidth check has a chance to drop them. # Effective throughput = min(bandwidth adjusted, surviving packets). sim.packet_loss = 0.1 # 10% loss sim.latency_ms = 0 sim.bandwidth_limit = 5000 # 5000 bytes/s sim.reset_stats() var pkt_size = 100 var total = 200 for i in range(total): var data = PackedByteArray() data.resize(pkt_size) data[0] = i sim.send_packet(data) var lost_loss = sim._total_lost var bw_drops = sim._total_bandwidth_dropped var queued = sim._outbound_queue.size() # 10% of 200 = ~20 lost to random loss (range: 10-30) tu.assert_true(lost_loss >= 5 and lost_loss <= 40, "loss %d in range for 10%% on %d" % [lost_loss, total]) # Remaining 180 packets compete for 5000/100 = 50 packet slots # But since bandwidth is checked after loss, at most ~50-60 should pass tu.assert_true(bw_drops >= 100, "at least 100 packets dropped by bandwidth limit (got %d)" % bw_drops) tu.assert_true(queued + bw_drops + lost_loss == total, "accounting: queued %d + bw %d + lost %d = total %d" % [queued, bw_drops, lost_loss, total]) sim.bandwidth_limit = 0 sim.reset_stats() func _test_zero_packet_loss_timing() -> void: tu.describe("Edge: Zero loss + zero latency — no side effects") sim.packet_loss = 0.0 sim.latency_ms = 0 sim.jitter_ms = 0 sim.reorder_window = 0 sim.duplicate_rate = 0.0 sim.bandwidth_limit = 0 sim.burst_loss_count = 0 sim.burst_loss_interval = 0 sim.reset_stats() var total = 500 var seq = 0 for i in range(total): var pkt = SIM_SCRIPT.make_test_packet(i, "zero") var ok = sim.send_packet(pkt) tu.assert_true(ok, "packet %d send returned true" % i) # All should queue with 0 delay (delivery at now + 0ms) var queued = sim._outbound_queue.size() tu.assert_eq(queued, total, "all %d packets queued" % total) tu.assert_eq(sim._total_lost, 0, "zero loss") tu.assert_eq(sim._total_duplicated, 0, "zero dupes") tu.assert_eq(sim._total_burst_dropped, 0, "zero burst drops") tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bw drops") # Delivery times should all be at or near current time var now = Time.get_ticks_msec() for entry in sim._outbound_queue: tu.assert_true(entry.time >= now - 5 and entry.time <= now + 5, "zero-delay packet delivery at current time (time=%d, now=%d)" % [entry.time, now]) sim.reset_stats() # ── Main Entry Point ────────────────────────────────────────────────────── func _ready() -> void: """Run all tests and exit.""" print("\n══════════════════════════════════════════════") print(" Network Simulator — Unit & Integration Tests") print("══════════════════════════════════════════════\n") sim = SIM_SCRIPT.new() add_child(sim) # Use deterministic seed for reproducibility sim.seed_value = 42 sim._rng.seed = 42 tu = UTILS_SCRIPT.new() add_child(tu) # Run all test suites _test_silent_passthrough() _test_packet_loss_rate() _test_latency_timing() _test_jitter_distribution() _test_packet_duplication() _test_burst_loss() _test_bandwidth_throttling() _test_reorder_window() _test_profile_presets() _test_stats_report() _test_high_loss_extreme() _test_zero_bandwidth() _test_complex_profile() # Phase 6 bug bash edge case tests _test_jitter_exceeds_latency() _test_combined_high_loss_latency() _test_burst_with_reorder() _test_rapid_sequence_10k() _test_bandwidth_with_loss() _test_zero_packet_loss_timing() # Summary print("══════════════════════════════════════════════") var total = tu._passed + tu._failed print(" Total: %d passed, %d failed out of %d" % [tu._passed, tu._failed, total]) print("══════════════════════════════════════════════\n") get_tree().quit(tu.exit_code())