test(server): cover async reconcile convergence and eviction race

This commit is contained in:
Serkyo 2026-07-07 22:27:00 +02:00
parent 9001cc6f98
commit 19fdec44d1

View file

@ -170,8 +170,77 @@ pub fn cylinder_chunks<S: std::hash::BuildHasher>(
#[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<ChunkPos>) -> 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() {