e7299b17e9
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
649 lines
20 KiB
GDScript
649 lines
20 KiB
GDScript
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())
|