use super::*;
use crate::chunk::ChunkLayout;
fn layout() -> ChunkLayout<f64> {
ChunkLayout::new(4, 1.0, [0.0; 3]).expect("a valid layout")
}
fn config(load: f64, unload: f64) -> StreamConfig<f64> {
StreamConfig::new(load, unload).expect("valid radii")
}
#[test]
fn the_band_must_have_width() {
assert!(StreamConfig::new(4.0f64, 4.0).is_err());
assert!(StreamConfig::new(4.0f64, 3.0).is_err());
assert!(StreamConfig::new(0.0f64, 4.0).is_err());
assert!(StreamConfig::new(f64::NAN, 4.0).is_err());
assert!(StreamConfig::new(4.0f64, f64::INFINITY).is_err());
assert!(StreamConfig::new(4.0f64, 4.001).is_ok());
}
#[allow(clippy::float_cmp, reason = "these are definitions, not measurements")]
#[test]
fn distance_is_to_the_box_and_is_zero_inside_it() {
let layout = layout();
let origin = ChunkId::new([0, 0, 0]);
assert_eq!(distance_to_chunk(&layout, origin, [2.0, 2.0, 2.0]), 0.0);
assert_eq!(distance_to_chunk(&layout, origin, [0.0, 0.0, 0.0]), 0.0);
assert_eq!(distance_to_chunk(&layout, origin, [7.0, 2.0, 2.0]), 3.0);
assert_eq!(distance_to_chunk(&layout, origin, [7.0, 8.0, 2.0]), 5.0);
let centre_distance = {
let o = layout.sample_origin(origin);
let half = 2.0;
let d = [o[0] + half - 0.1, o[1] + half - 0.1, o[2] + half - 0.1];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
};
assert!(centre_distance > 3.0, "the two measures differ materially");
}
#[test]
fn the_camera_s_own_chunk_is_always_resident() {
let layout = layout();
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
stream
.update(&layout, [2.0, 2.0, 2.0], &config(0.5, 1.0), &mut update)
.expect("update");
assert!(stream.resident().contains(&ChunkId::new([0, 0, 0])));
assert_eq!(update.unloaded, []);
}
#[test]
fn a_chunk_parked_in_the_band_never_thrashes() {
let layout = layout();
let config = config(9.0, 13.0);
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
stream
.update(&layout, [2.0, 2.0, 2.0], &config, &mut update)
.expect("update");
let banded: Vec<ChunkId> = stream
.resident()
.iter()
.copied()
.filter(|id| {
let d = distance_to_chunk(&layout, *id, [2.0, 2.0, 2.0]);
d > config.load() && d <= config.unload()
})
.collect();
assert!(
banded.is_empty(),
"a chunk entered without coming within the load radius: {banded:?}"
);
let mut churn = 0usize;
for step in 0..12 {
let x = if step % 2 == 0 { 2.0 } else { 5.5 };
stream
.update(&layout, [x, 2.0, 2.0], &config, &mut update)
.expect("update");
if step >= 2 {
churn += update.unloaded.len();
}
}
assert_eq!(
churn, 0,
"hysteresis did not hold: {churn} chunks unloaded while oscillating"
);
}
#[test]
fn without_hysteresis_the_same_motion_does_thrash() {
let layout = layout();
let narrow = config(9.0, 9.0 + 1e-9);
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
let mut churn = 0usize;
for step in 0..12 {
let x = if step % 2 == 0 { 2.0 } else { 5.5 };
stream
.update(&layout, [x, 2.0, 2.0], &narrow, &mut update)
.expect("update");
if step >= 2 {
churn += update.unloaded.len();
}
}
assert!(
churn > 0,
"a band of width 1e-9 absorbed the same motion, so the test above is \
not measuring hysteresis"
);
}
#[test]
fn a_chunk_leaves_only_past_the_unload_radius() {
let layout = layout();
let config = config(6.0, 14.0);
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
stream
.update(&layout, [2.0, 2.0, 2.0], &config, &mut update)
.expect("update");
let watched = ChunkId::new([0, 0, 0]);
assert!(stream.resident().contains(&watched));
let mut left_at = None;
for step in 1..40 {
let x = 2.0 + f64::from(step) * 2.0;
stream
.update(&layout, [x, 2.0, 2.0], &config, &mut update)
.expect("update");
if update.unloaded.contains(&watched) {
left_at = Some(distance_to_chunk(&layout, watched, [x, 2.0, 2.0]));
break;
}
}
let left_at = left_at.expect("the chunk should eventually unload");
assert!(
left_at > config.unload(),
"unloaded at {left_at}, which is inside the unload radius {}",
config.unload()
);
}
#[test]
fn the_result_is_a_pure_function_of_camera_and_config() {
let layout = layout();
let config = config(7.0, 11.0);
let mut update = StreamUpdate::new();
let mut direct = ChunkStream::new();
direct
.update(&layout, [30.0, 2.0, 2.0], &config, &mut update)
.expect("update");
let mut wandered = ChunkStream::new();
for x in [2.0, 40.0, 60.0, 30.0] {
wandered
.update(&layout, [x, 2.0, 2.0], &config, &mut update)
.expect("update");
}
assert_eq!(direct.resident(), wandered.resident());
}
#[test]
fn the_lists_are_sorted_and_disjoint_and_account_for_the_whole_change() {
let layout = layout();
let config = config(7.0, 11.0);
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
let mut previous: Vec<ChunkId> = Vec::new();
for step in 0..8 {
let x = 2.0 + f64::from(step) * 3.0;
stream
.update(&layout, [x, 2.0, 2.0], &config, &mut update)
.expect("update");
assert!(
update.loaded.windows(2).all(|w| w[0] < w[1]),
"loaded order"
);
assert!(
update.unloaded.windows(2).all(|w| w[0] < w[1]),
"unloaded order"
);
assert!(
update.loaded.iter().all(|id| !update.unloaded.contains(id)),
"a chunk both loaded and unloaded in one update"
);
let mut rebuilt = previous.clone();
rebuilt.retain(|id| !update.unloaded.contains(id));
rebuilt.extend(update.loaded.iter().copied());
rebuilt.sort_unstable();
assert_eq!(rebuilt, stream.resident(), "step {step}");
assert!(stream.resident().windows(2).all(|w| w[0] < w[1]));
previous = stream.resident().to_vec();
}
assert!(!previous.is_empty(), "the sweep never loaded anything");
}
#[test]
fn clearing_reports_everything_as_unloaded() {
let layout = layout();
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
stream
.update(&layout, [2.0, 2.0, 2.0], &config(7.0, 11.0), &mut update)
.expect("update");
let resident = stream.resident().to_vec();
assert!(!resident.is_empty());
stream.clear(&mut update);
assert!(stream.is_empty());
assert_eq!(update.unloaded, resident);
assert!(update.loaded.is_empty());
}
#[test]
fn an_absurd_radius_is_refused_rather_than_allocated() {
let layout = layout();
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
let config = config(1.0e7, 4.0e7);
assert!(
stream
.update(&layout, [0.0; 3], &config, &mut update)
.is_err()
);
}
#[test]
fn residency_reuses_its_buffers() {
let layout = layout();
let config = config(7.0, 11.0);
let mut stream = ChunkStream::new();
let mut update = StreamUpdate::new();
stream
.update(&layout, [2.0, 2.0, 2.0], &config, &mut update)
.expect("update");
update.reset();
assert!(update.is_empty());
stream
.update(&layout, [2.0, 2.0, 2.0], &config, &mut update)
.expect("update");
assert!(
update.is_empty(),
"a stationary camera produced {} loads and {} unloads",
update.loaded.len(),
update.unloaded.len()
);
}