test(server): cover async reconcile convergence and eviction race
This commit is contained in:
parent
9001cc6f98
commit
19fdec44d1
|
|
@ -170,8 +170,77 @@ pub fn cylinder_chunks<S: std::hash::BuildHasher>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::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]
|
#[test]
|
||||||
fn cylinder_is_symmetric_and_bounded() {
|
fn cylinder_is_symmetric_and_bounded() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue