use proptest::prelude::*;
use super::HeightIndex;
struct Model {
keys: Vec<u64>,
heights: Vec<f32>,
}
impl Model {
fn offset_of(&self, index: usize) -> f32 {
self.heights[..index.min(self.heights.len())]
.iter()
.map(|height| f64::from(*height))
.sum::<f64>() as f32
}
fn total(&self) -> f32 {
self.offset_of(self.heights.len())
}
fn rebuilt(&self) -> HeightIndex<u64> {
let at: std::collections::HashMap<u64, usize> = self
.keys
.iter()
.enumerate()
.map(|(at, key)| (*key, at))
.collect();
HeightIndex::from_heights(self.keys.iter().copied(), |key| self.heights[at[key]])
}
}
#[derive(Debug, Clone)]
enum Edit {
Push(f32),
Insert(usize, f32),
Remove(usize),
Set(usize, f32),
}
fn height_strategy() -> impl Strategy<Value = f32> {
0.5f32..100.0
}
fn edit_strategy() -> impl Strategy<Value = Edit> {
prop_oneof![
3 => height_strategy().prop_map(Edit::Push),
1 => (0usize..64usize, height_strategy()).prop_map(|(at, height)| Edit::Insert(at, height)),
1 => (0usize..64usize).prop_map(Edit::Remove),
2 => (0usize..64usize, height_strategy()).prop_map(|(at, height)| Edit::Set(at, height)),
]
}
fn apply(index: &mut HeightIndex<u64>, model: &mut Model, edit: &Edit, next_key: &mut u64) {
match *edit {
Edit::Push(height) => {
index.push(*next_key, height);
model.keys.push(*next_key);
model.heights.push(height);
*next_key += 1;
}
Edit::Insert(at, height) => {
let at = at.min(model.keys.len());
model.keys.insert(at, *next_key);
model.heights.insert(at, height);
*next_key += 1;
*index = model.rebuilt();
}
Edit::Remove(at) => {
if model.keys.is_empty() {
return;
}
let at = at % model.keys.len();
model.keys.remove(at);
model.heights.remove(at);
*index = model.rebuilt();
}
Edit::Set(at, height) => {
if model.keys.is_empty() {
return;
}
let at = at % model.keys.len();
index.set_height(at, height);
model.heights[at] = height;
}
}
}
fn assert_agrees(index: &HeightIndex<u64>, model: &Model) {
assert_eq!(index.len(), model.keys.len());
assert_eq!(index.is_empty(), model.keys.is_empty());
for (at, key) in model.keys.iter().enumerate() {
assert_eq!(index.key(at), key, "key at {at}");
assert_eq!(index.position(key), Some(at), "position of key {key}");
}
for at in 0..model.keys.len() {
let expected = model.offset_of(at);
assert_eq!(index.offset_of(at), expected, "offset of row {at}");
assert_eq!(index.height(at), model.heights[at], "height of row {at}");
if model.heights[at] > 0.0 {
let mid = expected + model.heights[at] * 0.5;
assert_eq!(
index.index_at(mid),
at,
"row holding the middle of row {at}"
);
} else {
let named = index.index_at(expected);
assert!(named >= at, "the top of zero-height row {at} names {named}");
assert_eq!(
index.offset_of(named),
expected,
"row {named} shares the top"
);
assert!(
model.heights[named] > 0.0 || named == model.keys.len() - 1,
"the top of zero-height row {at} names zero-height row {named}"
);
}
}
assert_eq!(index.total(), model.total(), "total");
assert_eq!(
index.offset_of(model.keys.len()),
model.total(),
"offset past the end"
);
}
#[test]
fn an_empty_index_names_no_rows() {
let index = HeightIndex::<u64>::new();
assert!(index.is_empty());
assert_eq!(index.len(), 0);
assert_eq!(index.total(), 0.0);
assert_eq!(index.index_at(0.0), 0);
assert_eq!(index.index_at(-5.0), 0);
assert_eq!(index.position(&7), None);
}
#[test]
fn pushes_build_offsets_in_order() {
let mut index = HeightIndex::new();
for (key, height) in [30.0, 10.0, 24.0].into_iter().enumerate() {
index.push(key as u64, height);
}
assert_eq!(index.offset_of(0), 0.0);
assert_eq!(index.offset_of(1), 30.0);
assert_eq!(index.offset_of(2), 40.0);
assert_eq!(index.offset_of(3), 64.0);
assert_eq!(index.total(), 64.0);
assert_eq!(index.index_at(0.0), 0);
assert_eq!(index.index_at(29.9), 0);
assert_eq!(index.index_at(30.0), 1);
assert_eq!(index.index_at(39.9), 1);
assert_eq!(index.index_at(40.0), 2);
assert_eq!(index.index_at(1_000.0), 2);
}
#[test]
fn set_height_moves_the_rows_after_it() {
let mut index = HeightIndex::from_heights(0..4u64, |key| 10.0 + *key as f32);
index.set_height(1, 40.0);
assert_eq!(index.offset_of(1), 10.0);
assert_eq!(index.offset_of(2), 50.0);
assert_eq!(index.offset_of(3), 62.0);
assert_eq!(index.total(), 75.0);
assert_eq!(index.index_at(45.0), 1);
assert_eq!(index.index_at(50.0), 2);
}
#[test]
fn offsets_past_the_end_name_the_total_and_the_last_row() {
let index = HeightIndex::from_heights(0..3u64, |_| 10.0);
assert_eq!(index.offset_of(7), 30.0);
assert_eq!(index.index_at(29.0), 2);
assert_eq!(index.index_at(-1.0), 0);
}
#[test]
fn a_rebuilt_index_matches_one_built_in_order() {
let mut pushed = HeightIndex::new();
for key in 0..8u64 {
pushed.push(key, (key as f32 + 1.0) * 5.0);
}
let built = HeightIndex::from_heights(0..8u64, |key| (*key as f32 + 1.0) * 5.0);
for at in 0..built.len() {
assert_eq!(pushed.key(at), built.key(at));
assert_eq!(pushed.offset_of(at), built.offset_of(at));
}
assert_eq!(pushed.total(), built.total());
}
fn height_or_zero_strategy() -> impl Strategy<Value = f32> {
prop_oneof![1 => Just(0.0f32), 3 => height_strategy()]
}
fn edit_or_zero_strategy() -> impl Strategy<Value = Edit> {
prop_oneof![
3 => height_or_zero_strategy().prop_map(Edit::Push),
1 => (0usize..64usize, height_or_zero_strategy())
.prop_map(|(at, height)| Edit::Insert(at, height)),
1 => (0usize..64usize).prop_map(Edit::Remove),
2 => (0usize..64usize, height_or_zero_strategy())
.prop_map(|(at, height)| Edit::Set(at, height)),
]
}
#[test]
fn a_zero_height_run_shares_its_top_with_the_row_after_it() {
let index = HeightIndex::from_heights(0..5u64, |key| match *key {
1..=3 => 0.0,
_ => 10.0,
});
assert_eq!(index.offset_of(1), 10.0);
assert_eq!(index.offset_of(4), 10.0);
assert_eq!(
index.index_at(10.0),
4,
"the run's top names the row after it"
);
assert_eq!(index.index_at(9.9), 0);
assert_eq!(index.total(), 20.0);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "duplicate key")]
fn a_duplicate_pushed_key_trips_a_debug_assertion() {
let mut index = HeightIndex::new();
index.push(1u64, 10.0);
index.push(1u64, 10.0);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "duplicate key")]
fn a_duplicate_built_key_trips_a_debug_assertion() {
HeightIndex::from_heights([1u64, 2, 1], |_| 10.0);
}
proptest! {
#[test]
fn edits_with_zero_heights_match_a_naive_model(
edits in prop::collection::vec(edit_or_zero_strategy(), 1..=64),
) {
let mut index = HeightIndex::new();
let mut model = Model { keys: Vec::new(), heights: Vec::new() };
let mut next_key = 0u64;
for edit in &edits {
apply(&mut index, &mut model, edit, &mut next_key);
assert_agrees(&index, &model);
}
}
#[test]
fn edits_match_a_naive_model(edits in prop::collection::vec(edit_strategy(), 1..=64)) {
let mut index = HeightIndex::new();
let mut model = Model { keys: Vec::new(), heights: Vec::new() };
let mut next_key = 0u64;
for edit in &edits {
apply(&mut index, &mut model, edit, &mut next_key);
assert_agrees(&index, &model);
}
}
}
#[test]
fn many_height_changes_on_a_long_list_do_not_drift() {
let rows = 100_000u64;
let mut index = HeightIndex::from_heights(0..rows, |_| 110.0);
let mut first = 110.0f32;
for step in 0..20_000u32 {
first = 100.0 + (step % 7) as f32 * 0.37;
index.set_height(0, first);
}
let last = (rows - 1) as usize;
let expected_offset = f64::from(first) + 110.0 * (rows - 2) as f64;
let offset = f64::from(index.offset_of(last));
assert!(
(offset - expected_offset).abs() <= 2.0,
"the last row's offset drifted to {offset} from {expected_offset}"
);
let expected_total = expected_offset + 110.0;
let total = f64::from(index.total());
assert!(
(total - expected_total).abs() <= 2.0,
"the total drifted to {total} from {expected_total}"
);
}