use common::database::block_offset_index::{BlockOffsetIndex, OffsetMarker};
use common::types::EntityId;
use proptest::prelude::*;
fn assert_marker_index_consistent(idx: &BlockOffsetIndex) {
for (i, (marker, _)) in idx.entries.iter().enumerate() {
let expected = Some((
idx.entries[i].1,
idx.entries
.get(i + 1)
.map(|(_, bs)| *bs)
.unwrap_or(idx.total_bytes()),
));
assert_eq!(
idx.range_of(*marker),
expected,
"range_of({:?}) inconsistent with entries[{}]",
marker,
i
);
}
}
#[test]
fn empty_index_returns_none_everywhere() {
let idx = BlockOffsetIndex::new();
assert_eq!(idx.range_of_block(1), None);
assert_eq!(idx.block_at_byte(0), None);
assert_eq!(idx.block_at_byte(100), None);
assert_eq!(idx.byte_to_block_byte(0), None);
assert!(idx.is_empty());
assert_eq!(idx.len(), 0);
}
#[test]
fn single_block_range_covers_full_rope() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(100);
idx.push_block(42, 0);
assert_eq!(idx.range_of_block(42), Some((0, 100)));
assert_eq!(idx.block_at_byte(0), Some(42));
assert_eq!(idx.block_at_byte(50), Some(42));
assert_eq!(idx.block_at_byte(100), Some(42)); assert_eq!(idx.block_at_byte(101), None);
assert_eq!(idx.byte_to_block_byte(30), Some((42, 30)));
}
#[test]
fn three_blocks_disjoint_ranges() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
assert_eq!(idx.range_of_block(1), Some((0, 10)));
assert_eq!(idx.range_of_block(2), Some((10, 20)));
assert_eq!(idx.range_of_block(3), Some((20, 30)));
assert_eq!(idx.range_of_block(99), None);
assert_eq!(idx.block_at_byte(0), Some(1));
assert_eq!(idx.block_at_byte(9), Some(1));
assert_eq!(idx.block_at_byte(10), Some(2)); assert_eq!(idx.block_at_byte(19), Some(2));
assert_eq!(idx.block_at_byte(20), Some(3));
assert_eq!(idx.block_at_byte(30), Some(3));
assert_eq!(idx.byte_to_block_byte(15), Some((2, 5)));
}
#[test]
fn shift_after_propagates_to_total_and_entries() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
idx.shift_after(12, 5);
assert_eq!(idx.range_of_block(1), Some((0, 10)));
assert_eq!(idx.range_of_block(2), Some((10, 25))); assert_eq!(idx.range_of_block(3), Some((25, 35))); assert_eq!(idx.total_bytes(), 35);
}
#[test]
fn shift_after_negative_when_threshold_above_first_block() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
idx.shift_after(12, -5);
assert_eq!(idx.range_of_block(1), Some((0, 10)));
assert_eq!(idx.range_of_block(2), Some((10, 15)));
assert_eq!(idx.range_of_block(3), Some((15, 25)));
assert_eq!(idx.total_bytes(), 25);
}
#[test]
fn remove_at_drops_entry_and_subsequent_blocks_extend() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
idx.remove_at(1);
assert_eq!(idx.len(), 2);
assert_eq!(idx.range_of_block(1), Some((0, 20))); assert_eq!(idx.range_of_block(2), None);
assert_eq!(idx.range_of_block(3), Some((20, 30)));
}
#[test]
fn clone_roundtrips() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(42);
idx.push_block(1, 0);
idx.push_block(2, 21);
let cloned = idx.clone();
assert_eq!(idx, cloned);
}
#[test]
fn insert_at_middle_keeps_marker_index_consistent() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(3, 20);
assert_marker_index_consistent(&idx);
idx.insert_at(1, OffsetMarker::Block(2), 10);
assert_eq!(idx.range_of_block(1), Some((0, 10)));
assert_eq!(idx.range_of_block(2), Some((10, 20)));
assert_eq!(idx.range_of_block(3), Some((20, 30)));
assert_marker_index_consistent(&idx);
}
#[test]
fn remove_at_keeps_marker_index_consistent() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(40);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
idx.push_block(4, 30);
idx.remove_at(1);
assert_eq!(idx.range_of_block(1), Some((0, 20)));
assert_eq!(idx.range_of_block(2), None);
assert_eq!(idx.range_of_block(3), Some((20, 30)));
assert_eq!(idx.range_of_block(4), Some((30, 40)));
assert_marker_index_consistent(&idx);
}
#[test]
fn clear_resets_state() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 15);
idx.clear();
assert!(idx.is_empty());
assert_eq!(idx.total_bytes(), 0);
assert_eq!(idx.range_of_block(1), None);
assert_marker_index_consistent(&idx);
}
#[test]
fn shift_after_does_not_touch_marker_index() {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(30);
idx.push_block(1, 0);
idx.push_block(2, 10);
idx.push_block(3, 20);
idx.shift_after(12, 5);
assert_marker_index_consistent(&idx);
}
fn arb_index() -> impl Strategy<Value = BlockOffsetIndex> {
proptest::collection::vec(1u32..50u32, 1..10).prop_map(|gaps| {
let mut idx = BlockOffsetIndex::new();
let mut cursor = 0u32;
for (i, gap) in gaps.iter().enumerate() {
idx.push_block((i as EntityId) + 1, cursor);
cursor = cursor.saturating_add(*gap);
}
idx.set_total_bytes(cursor);
idx
})
}
proptest! {
#[test]
fn prop_block_at_byte_returns_a_containing_block(
idx in arb_index(),
byte_frac in 0u32..1000u32,
) {
let total = idx.total_bytes();
prop_assume!(total > 0);
let byte = byte_frac % (total + 1);
let block_id = idx
.block_at_byte(byte)
.expect("block_at_byte must succeed for byte ≤ total_bytes");
let (start, end) = idx.range_of_block(block_id).expect("block must be indexed");
prop_assert!(
start <= byte && byte <= end,
"byte {} must fall inside [{}, {}] for block {}",
byte, start, end, block_id
);
}
#[test]
fn prop_byte_to_block_byte_consistent_with_range_of(
idx in arb_index(),
byte_frac in 0u32..1000u32,
) {
let total = idx.total_bytes();
prop_assume!(total > 0);
let byte = byte_frac % (total + 1);
let (block_id, byte_in) = idx
.byte_to_block_byte(byte)
.expect("byte must map to a block");
let (start, _end) = idx.range_of_block(block_id).unwrap();
prop_assert_eq!(byte_in, byte - start);
}
#[test]
fn prop_shift_zero_is_noop(idx in arb_index()) {
let mut moved = idx.clone();
moved.shift_after(0, 0);
prop_assert_eq!(idx, moved);
}
#[test]
fn prop_shift_is_reversible(
idx in arb_index(),
threshold_frac in 0u32..1000u32,
delta in 1i32..50i32,
) {
let threshold = idx
.total_bytes()
.checked_add(1)
.map(|t| threshold_frac % t)
.unwrap_or(0);
let mut moved = idx.clone();
moved.shift_after(threshold, delta);
moved.shift_after(threshold, -delta);
prop_assert_eq!(idx, moved);
}
#[test]
fn prop_shift_preserves_sort(
idx in arb_index(),
threshold_frac in 0u32..1000u32,
delta in 0i32..50i32,
) {
let threshold = idx
.total_bytes()
.checked_add(1)
.map(|t| threshold_frac % t)
.unwrap_or(0);
let mut moved = idx;
moved.shift_after(threshold, delta);
let starts: Vec<u32> = moved.entries.iter().map(|(_, bs)| *bs).collect();
for window in starts.windows(2) {
prop_assert!(
window[0] <= window[1],
"entries must stay sorted by byte_start"
);
}
}
#[test]
fn prop_marker_index_consistent_after_random_ops(
ops in proptest::collection::vec(
(0u32..30u32, 0u32..50u32, prop::bool::ANY),
0..20,
),
) {
let mut idx = BlockOffsetIndex::new();
idx.set_total_bytes(1000);
let mut next_id: EntityId = 1;
for (position_seed, byte_start, is_remove) in ops {
let len = idx.len();
if is_remove && len > 0 {
let p = (position_seed as usize) % len;
idx.remove_at(p);
} else {
let p = if len == 0 { 0 } else { (position_seed as usize) % (len + 1) };
idx.insert_at(p, OffsetMarker::Block(next_id), byte_start);
next_id += 1;
}
assert_marker_index_consistent(&idx);
}
}
#[test]
fn prop_range_of_matches_linear_scan(idx in arb_index()) {
for (marker, _) in idx.entries.iter() {
let cached = idx.range_of(*marker);
let scan_idx = idx.entries.iter().position(|(m, _)| *m == *marker);
let scan_range = scan_idx.map(|i| {
let start = idx.entries[i].1;
let end = idx
.entries
.get(i + 1)
.map(|(_, bs)| *bs)
.unwrap_or(idx.total_bytes());
(start, end)
});
prop_assert_eq!(cached, scan_range);
}
}
}