From 19fdec44d1a6ea96eb3119368f14e8919f596c64 Mon Sep 17 00:00:00 2001 From: Serkyo Date: Tue, 7 Jul 2026 22:27:00 +0200 Subject: [PATCH] test(server): cover async reconcile convergence and eviction race --- crates/server/src/world_server.rs | 71 ++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/server/src/world_server.rs b/crates/server/src/world_server.rs index bed997b..bd278b7 100644 --- a/crates/server/src/world_server.rs +++ b/crates/server/src/world_server.rs @@ -170,8 +170,77 @@ pub fn cylinder_chunks( #[cfg(test)] mod tests { - use super::{ChunkPos, cylinder_chunks}; + use super::{ChunkPos, ServerWorld, StreamStats, cylinder_chunks}; + use shared::generator::{VoxelGenerator, WorldGenConfig}; + use shared::world::BlockId; use std::collections::HashSet; + use std::time::{Duration, Instant}; + + /// Builds a server world backed by a real worker pool for streaming tests. + fn test_world() -> ServerWorld { + let config = WorldGenConfig { + base_height: 8, + noise_scale: 0.05, + surface_block: BlockId(1), + subsurface_block: BlockId(2), + stone_block: BlockId(3), + }; + ServerWorld::new(VoxelGenerator::new(config, 42)) + } + + /// Repeatedly reconciles against `desired` until the worker pool reports no outstanding work, returning the final pass's stats. Fails the test if the pool does not drain within a fixed timeout. + fn drain_to_idle(world: &mut ServerWorld, desired: &HashSet) -> StreamStats { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let stats = world.reconcile(desired); + if stats.in_flight == 0 { + return stats; + } + assert!( + Instant::now() < deadline, + "worker pool did not drain in time" + ); + std::thread::sleep(Duration::from_millis(1)); + } + } + + #[test] + fn reconcile_converges_over_multiple_passes() { + let mut world = test_world(); + let mut desired = HashSet::new(); + cylinder_chunks(ChunkPos::new(0, 0, 0), 2, &mut desired); + + // The first pass only dispatches work; because generation is off-thread, nothing is resident yet and every position is in flight. + let first = world.reconcile(&desired); + assert_eq!(first.loaded, 0); + assert_eq!(first.resident, 0); + assert!(first.in_flight > 0); + + // Later passes drain finished chunks until the pool is idle, at which point every desired position must be resident. + let final_stats = drain_to_idle(&mut world, &desired); + assert_eq!(final_stats.in_flight, 0); + assert_eq!(final_stats.resident, desired.len()); + } + + #[test] + fn evicted_chunk_is_not_repopulated_on_arrival() { + let mut world = test_world(); + let target = ChunkPos::new(0, 0, 0); + let mut desired = HashSet::new(); + desired.insert(target); + + // Dispatch the chunk, then immediately stop wanting it. + world.reconcile(&desired); + + // Every subsequent pass reconciles against an empty desired set, so whenever the worker returns the chunk the drain guard discards it rather than resurrecting an unwanted chunk. + let empty = HashSet::new(); + let final_stats = drain_to_idle(&mut world, &empty); + assert_eq!(final_stats.in_flight, 0); + assert_eq!( + final_stats.resident, 0, + "a chunk that is no longer wanted must not become resident when it arrives" + ); + } #[test] fn cylinder_is_symmetric_and_bounded() {