ajhahn.de
← Theria
GDScript 86 lines
extends GutTest
## The N5 network-condition simulator, exercised without any networking.
##
## NetSim shapes the client's incoming snapshot stream — holding each snapshot a
## base latency plus random jitter, dropping a loss fraction — so the smoothing can
## be exercised under a worse link than the local machine provides. These tests pin
## its contract: identity pass-through, the latency hold, the seeded loss rate,
## the jitter band, release ordering, and that a not-yet-due snapshot stays queued.
## It conditions opaque payloads and takes plain millisecond times, so the whole
## round trip is checked headlessly like the protocol, sim, and interpolation cores.


func test_identity_releases_immediately() -> void:
	# No latency, jitter, or loss: a snapshot is due the instant it arrives.
	var sim := NetSim.new(0.0, 0.0, 0.0, 1)
	assert_true(sim.receive("a", 100.0), "an unconditioned snapshot is always accepted")
	var due := sim.drain(100.0)
	assert_eq(due.size(), 1, "the snapshot is released at its arrival time")
	assert_eq(due[0]["data"], "a")
	assert_eq(due[0]["release"], 100.0)
	assert_false(sim.has_pending(), "nothing is left queued")


func test_latency_holds_until_due() -> void:
	var sim := NetSim.new(50.0, 0.0, 0.0, 1)
	sim.receive("a", 100.0)
	assert_eq(sim.drain(149.0).size(), 0, "before arrival + latency the snapshot is held")
	assert_true(sim.has_pending(), "and stays queued")
	var due := sim.drain(150.0)
	assert_eq(due.size(), 1, "at arrival + latency it is released")
	assert_eq(due[0]["release"], 150.0)


func test_loss_drops_at_its_rate_and_drops_stay_gone() -> void:
	# Seeded, so the drop pattern is fixed: at 50% loss over 1000 arrivals the
	# delivered count sits near 500, and a dropped snapshot is never released later.
	var sim := NetSim.new(0.0, 0.0, 0.5, 12345)
	var accepted := 0
	for i in range(1000):
		if sim.receive(i, float(i)):
			accepted += 1
	assert_between(accepted, 440, 560, "roughly half the snapshots survive the loss roll")
	assert_eq(sim.drain(2000.0).size(), accepted, "only the accepted snapshots are ever released")


func test_jitter_spreads_release_within_the_band() -> void:
	# Every snapshot shares one arrival time, so their spread comes purely from jitter:
	# each is held the base latency plus a random [0, jitter), never outside that band.
	var sim := NetSim.new(100.0, 40.0, 0.0, 7)
	for i in range(50):
		sim.receive(i, 0.0)
	var due := sim.drain(1000.0)
	assert_eq(due.size(), 50, "no loss, so all are released once due")
	var lowest := INF
	var highest := -INF
	for packet in due:
		lowest = minf(lowest, packet["release"])
		highest = maxf(highest, packet["release"])
	assert_gte(lowest, 100.0, "never released before the base latency")
	assert_lt(highest, 140.0, "never held beyond latency + jitter")
	assert_gt(highest - lowest, 0.0, "jitter actually spreads the release times")


func test_drain_returns_due_in_release_order() -> void:
	# Released oldest-first regardless of the order they were offered, so the
	# downstream buffer sees them in arrival order.
	var sim := NetSim.new(0.0, 0.0, 0.0, 1)
	sim.receive("c", 30.0)
	sim.receive("a", 10.0)
	sim.receive("b", 20.0)
	var order: Array = []
	for packet in sim.drain(100.0):
		order.append(packet["data"])
	assert_eq(order, ["a", "b", "c"], "drained in ascending release time, not offer order")


func test_a_not_yet_due_snapshot_waits_for_a_later_drain() -> void:
	var sim := NetSim.new(0.0, 0.0, 0.0, 1)
	sim.receive("a", 10.0)
	sim.receive("b", 30.0)
	assert_eq(sim.drain(20.0).size(), 1, "only the snapshot already due is released")
	assert_eq(sim.drain(20.0).size(), 0, "the released one is not handed out twice")
	var later := sim.drain(40.0)
	assert_eq(later.size(), 1, "the held snapshot is released once its time comes")
	assert_eq(later[0]["data"], "b")