use super::*;
use crate::{bitmap::Prunable, hex};
#[test]
fn test_dirty_lifecycle_and_operations() {
let bitmap: BitMap<4> = BitMap::new();
assert_eq!(bitmap.len(), 0);
assert!(bitmap.is_empty());
assert_eq!(bitmap.commits().count(), 0);
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push(true).push(false).push(true);
})
.unwrap();
assert_eq!(bitmap.len(), 3);
assert!(bitmap.get_bit(0));
assert!(!bitmap.get_bit(1));
assert!(bitmap.get_bit(2));
assert_eq!(bitmap.commits().count(), 1);
let mut dirty = bitmap.into_dirty();
dirty.push(true).push(true);
let bitmap = dirty.abort();
assert_eq!(bitmap.len(), 3);
let mut dirty = bitmap.into_dirty();
assert!(dirty.get_bit(0)); dirty.set_bit(1, true); assert!(dirty.get_bit(1)); dirty.push(false); assert!(!dirty.get_bit(3)); let bitmap = dirty.commit(2).unwrap();
assert_eq!(bitmap.len(), 4);
assert!(bitmap.get_bit(1));
assert!(!bitmap.get_bit(3));
let bitmap = bitmap.apply_batch(3, |_dirty| {}).unwrap();
assert_eq!(bitmap.len(), 4);
assert!(bitmap.commit_exists(3));
let bitmap = bitmap
.apply_batch(4, |dirty| {
dirty.set_bit(0, false).push_byte(0xAA);
})
.unwrap();
assert_eq!(bitmap.len(), 12); assert!(!bitmap.get_bit(0)); }
#[test]
fn test_dirty_operations_push_pop_prune() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push(false).push(false).push(false);
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.set_bit(0, true); dirty.set_bit(1, true); dirty.push(true); dirty.push(true); })
.unwrap();
assert_eq!(bitmap.len(), 5);
assert!(bitmap.get_bit(0));
assert!(bitmap.get_bit(1));
assert!(!bitmap.get_bit(2));
let _bitmap = bitmap
.apply_batch(3, |dirty| {
dirty.push(false); let popped = dirty.pop(); assert!(!popped);
assert_eq!(dirty.len(), 5); })
.unwrap();
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push_chunk(&hex!("0x00000000"));
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.push_chunk(&hex!("0xAABBCCDD")); dirty.push_byte(0xFF); })
.unwrap();
assert_eq!(bitmap.len(), 72); let chunk = bitmap.get_chunk_containing(32); assert_eq!(chunk, &hex!("0xAABBCCDD"));
for i in 64..72 {
assert!(bitmap.get_bit(i)); }
let mut dirty = bitmap.into_dirty();
dirty.set_bit(32, true); dirty.set_bit(39, true); let chunk = dirty.get_chunk(32);
assert_eq!(chunk[0] & 0x01, 0x01); assert_eq!(chunk[0] & 0x80, 0x80); let bitmap = dirty.commit(3).unwrap();
let bitmap = bitmap
.apply_batch(4, |dirty| {
dirty.prune_to_bit(32);
})
.unwrap();
assert_eq!(bitmap.len(), 72); assert_eq!(bitmap.pruned_chunks(), 1); }
#[test]
fn test_commit_history_management() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(5, |dirty| {
dirty.push(true);
})
.unwrap();
let err = bitmap
.clone()
.apply_batch(5, |dirty| {
dirty.push(false);
})
.unwrap_err();
match err {
Error::NonMonotonicCommit {
previous,
attempted,
} => {
assert_eq!(previous, 5);
assert_eq!(attempted, 5);
}
_ => panic!("Expected NonMonotonicCommit error"),
}
let err = bitmap
.clone()
.apply_batch(3, |dirty| {
dirty.push(false);
})
.unwrap_err();
match err {
Error::NonMonotonicCommit {
previous,
attempted,
} => {
assert_eq!(previous, 5);
assert_eq!(attempted, 3);
}
_ => panic!("Expected NonMonotonicCommit error"),
}
let _bitmap = bitmap
.apply_batch(10, |dirty| {
dirty.push(false);
})
.unwrap();
let bitmap: BitMap<4> = BitMap::new();
assert!(bitmap.earliest_commit().is_none());
assert!(bitmap.latest_commit().is_none());
let mut bitmap = bitmap;
for i in 1..=5 {
bitmap = bitmap
.apply_batch(i * 10, |dirty| {
dirty.push(true);
})
.unwrap();
}
assert_eq!(bitmap.earliest_commit(), Some(10));
assert_eq!(bitmap.latest_commit(), Some(50));
assert!(bitmap.commit_exists(30));
assert!(!bitmap.commit_exists(25));
let commits: Vec<u64> = bitmap.commits().collect();
assert_eq!(commits, vec![10, 20, 30, 40, 50]);
let removed = bitmap.prune_commits_before(30);
assert_eq!(removed, 2);
assert_eq!(bitmap.commits().count(), 3);
bitmap.clear_history();
assert_eq!(bitmap.commits().count(), 0);
assert!(bitmap.earliest_commit().is_none());
assert!(bitmap.latest_commit().is_none());
assert_eq!(bitmap.len(), 5); }
#[test]
fn test_historical_reconstruction_with_modifications() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push(true).push(false).push(true);
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.set_bit(0, false);
dirty.push(false);
})
.unwrap();
let state_at_1 = bitmap.get_at_commit(1).unwrap();
assert_eq!(state_at_1.len(), 3);
assert!(state_at_1.get_bit(0)); assert!(!state_at_1.get_bit(1));
let state_at_2 = bitmap.get_at_commit(2).unwrap();
assert_eq!(state_at_2.len(), 4);
assert!(!state_at_2.get_bit(0)); assert!(!state_at_2.get_bit(3));
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push_chunk(&hex!("0xFF00FF00"));
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.set_bit(0, false);
dirty.set_bit(8, true);
})
.unwrap();
let bitmap = bitmap
.apply_batch(3, |dirty| {
dirty.set_bit(16, false);
dirty.set_bit(24, true);
})
.unwrap();
let state_at_1 = bitmap.get_at_commit(1).unwrap();
assert!(state_at_1.get_bit(0));
assert!(!state_at_1.get_bit(8));
let state_at_2 = bitmap.get_at_commit(2).unwrap();
assert!(!state_at_2.get_bit(0)); assert!(state_at_2.get_bit(8)); assert!(state_at_2.get_bit(16));
let state_at_3 = bitmap.get_at_commit(3).unwrap();
assert!(!state_at_3.get_bit(16)); assert!(state_at_3.get_bit(24));
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..4 {
dirty.push(true);
}
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.set_bit(0, false).set_bit(2, false);
dirty.push(false).push(false);
})
.unwrap();
let bitmap = bitmap
.apply_batch(3, |dirty| {
dirty.set_bit(1, false).set_bit(3, false);
dirty.push(true).push(true);
})
.unwrap();
let state_at_1 = bitmap.get_at_commit(1).unwrap();
assert_eq!(state_at_1.len(), 4);
for i in 0..4 {
assert!(state_at_1.get_bit(i)); }
let state_at_2 = bitmap.get_at_commit(2).unwrap();
assert_eq!(state_at_2.len(), 6);
assert!(!state_at_2.get_bit(0)); assert!(state_at_2.get_bit(1)); assert!(!state_at_2.get_bit(4));
let state_at_3 = bitmap.get_at_commit(3).unwrap();
assert_eq!(state_at_3.len(), 8);
assert!(!state_at_3.get_bit(1)); assert!(state_at_3.get_bit(6)); }
#[test]
fn test_historical_reconstruction_with_length_changes() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push(true).push(false);
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.push(true).push(true);
})
.unwrap();
let bitmap = bitmap
.apply_batch(3, |dirty| {
dirty.push(false).push(false);
})
.unwrap();
assert_eq!(bitmap.get_at_commit(1).unwrap().len(), 2);
assert_eq!(bitmap.get_at_commit(2).unwrap().len(), 4);
assert_eq!(bitmap.get_at_commit(3).unwrap().len(), 6);
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for i in 0..5 {
dirty.push(i % 2 == 0);
}
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.pop();
dirty.pop();
})
.unwrap();
let bitmap = bitmap
.apply_batch(3, |dirty| {
dirty.push(true).push(true).push(true);
})
.unwrap();
let state_1 = bitmap.get_at_commit(1).unwrap();
assert_eq!(state_1.len(), 5);
assert!(state_1.get_bit(0)); assert!(!state_1.get_bit(1)); assert!(state_1.get_bit(4));
let state_2 = bitmap.get_at_commit(2).unwrap();
assert_eq!(state_2.len(), 3);
let state_3 = bitmap.get_at_commit(3).unwrap();
assert_eq!(state_3.len(), 6);
assert!(state_3.get_bit(3));
assert!(state_3.get_bit(5));
}
#[test]
fn test_historical_reconstruction_with_pruning() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push_chunk(&hex!("0xAABBCCDD"));
dirty.push_chunk(&hex!("0x11223344"));
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.prune_to_bit(32);
})
.unwrap();
assert_eq!(bitmap.pruned_chunks(), 1);
let state_at_1 = bitmap.get_at_commit(1).unwrap();
assert_eq!(state_at_1.len(), 64);
assert_eq!(state_at_1.pruned_chunks(), 0); assert_eq!(state_at_1.get_chunk_containing(0), &hex!("0xAABBCCDD")); assert_eq!(state_at_1.get_chunk_containing(32), &hex!("0x11223344"));
let state_at_2 = bitmap.get_at_commit(2).unwrap();
assert_eq!(state_at_2.len(), 64);
assert_eq!(state_at_2.pruned_chunks(), 1); assert_eq!(state_at_2.get_chunk_containing(32), &hex!("0x11223344"));
}
#[test]
fn test_historical_reconstruction_edge_cases() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(10, |dirty| {
dirty.push(true);
})
.unwrap();
assert!(bitmap.get_at_commit(5).is_none());
assert!(bitmap.get_at_commit(15).is_none());
assert!(bitmap.get_at_commit(10).is_some());
let bitmap: BitMap<4> = BitMap::new();
let mut bitmap = bitmap;
for i in 1..=5 {
bitmap = bitmap
.apply_batch(i, |dirty| {
for _ in 0..i {
dirty.push(true);
}
})
.unwrap();
}
bitmap.prune_commits_before(3);
assert!(bitmap.get_at_commit(1).is_none());
assert!(bitmap.get_at_commit(2).is_none());
assert!(bitmap.get_at_commit(3).is_some());
assert!(bitmap.get_at_commit(4).is_some());
assert_eq!(bitmap.get_at_commit(3).unwrap().len(), 6); }
#[test]
fn test_dirty_modifications_on_appended_bits() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push(true); dirty.set_bit(0, false); })
.unwrap();
assert_eq!(bitmap.len(), 1);
assert!(!bitmap.get_bit(0));
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.push(true); dirty.set_bit(1, false); dirty.pop(); })
.unwrap();
assert_eq!(bitmap.len(), 1); }
#[test]
fn test_pop_behavior_with_modifications() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..10 {
dirty.push(true);
}
})
.unwrap();
let mut popped_value = true;
let _bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.set_bit(9, false); popped_value = dirty.pop(); })
.unwrap();
assert!(
!popped_value,
"pop() should return modified value, not original"
);
}
#[test]
#[should_panic(expected = "out of bounds")]
fn test_read_popped_bit_panics() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..10 {
dirty.push(true);
}
})
.unwrap();
let mut dirty = bitmap.into_dirty();
dirty.pop();
dirty.pop();
dirty.get_bit(8); }
#[test]
#[should_panic(expected = "beyond projected length")]
fn test_prune_beyond_length_panics() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..10 {
dirty.push(true);
}
})
.unwrap();
let mut dirty = bitmap.into_dirty();
dirty.pop(); dirty.prune_to_bit(100); }
#[test]
fn test_get_chunk_on_appended_only_chunk() {
let bitmap: BitMap<4> = BitMap::new();
let mut dirty = bitmap.into_dirty();
for i in 0..32 {
dirty.push(i % 2 == 0); }
let chunk = dirty.get_chunk(0);
assert_ne!(chunk[0] & 0x01, 0, "bit 0 should be true");
assert_eq!(chunk[0] & 0x02, 0, "bit 1 should be false");
assert_ne!(chunk[0] & 0x04, 0, "bit 2 should be true");
assert_eq!(chunk[0] & 0x08, 0, "bit 3 should be false");
assert_eq!(chunk[0], 0x55, "byte 0 should be 0x55");
}
#[test]
fn test_pop_zeros_chunk_tail() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..33 {
dirty.push(true);
}
})
.unwrap();
let mut dirty = bitmap.into_dirty();
dirty.pop(); dirty.pop();
let chunk = dirty.get_chunk(0);
let byte_31 = chunk[31 / 8]; let bit_31_in_byte = 31 % 8; let bit_31_set = (byte_31 >> bit_31_in_byte) & 1 == 1;
assert!(!bit_31_set);
}
#[test]
fn test_prune_freshly_appended_chunk() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..10 {
dirty.push(true);
}
})
.unwrap();
assert_eq!(bitmap.current().chunks_len(), 1);
let mut dirty = bitmap.into_dirty();
for _ in 0..54 {
dirty.push(true);
}
assert_eq!(dirty.len(), 64);
dirty.prune_to_bit(64);
let _bitmap = dirty.commit(2).unwrap();
}
#[test]
fn test_read_appended_bits_after_pops() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for _ in 0..10 {
dirty.push(true);
}
})
.unwrap();
let mut dirty = bitmap.into_dirty();
dirty.pop(); dirty.pop(); dirty.pop(); dirty.push(false); dirty.push(false);
assert!(!dirty.get_bit(7));
assert!(!dirty.get_bit(8));
let chunk = dirty.get_chunk(0); assert_eq!(chunk[0] & 0x80, 0, "bit 7 should be false in chunk");
assert_eq!(chunk[1] & 0x01, 0, "bit 8 should be false in chunk");
dirty.set_bit(7, true);
assert!(dirty.get_bit(7));
let bitmap = dirty.commit(2).unwrap();
assert_eq!(bitmap.len(), 9);
assert!(bitmap.get_bit(7));
assert!(!bitmap.get_bit(8));
}
#[test]
fn test_reconstruct_less_pruned_from_more_pruned() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for i in 0..64 {
dirty.push(i < 32); }
})
.unwrap();
assert_eq!(bitmap.len(), 64);
assert_eq!(bitmap.pruned_chunks(), 0);
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.prune_to_bit(32); })
.unwrap();
assert_eq!(bitmap.len(), 64);
assert_eq!(bitmap.pruned_chunks(), 1);
let reconstructed = bitmap
.get_at_commit(1)
.expect("should be able to reconstruct less-pruned state");
assert_eq!(reconstructed.len(), 64);
assert_eq!(reconstructed.pruned_chunks(), 0, "commit 1 had no pruning");
for i in 0..32 {
assert!(reconstructed.get_bit(i));
}
for i in 32..64 {
assert!(!reconstructed.get_bit(i));
}
}
#[test]
fn test_reconstruct_fully_pruned_commit() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
for i in 0..32 {
dirty.push(i % 2 == 0); }
})
.unwrap();
assert_eq!(bitmap.len(), 32);
assert_eq!(bitmap.pruned_chunks(), 0);
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.prune_to_bit(32); })
.unwrap();
assert_eq!(bitmap.len(), 32);
assert_eq!(bitmap.pruned_chunks(), 1);
let reconstructed = bitmap
.get_at_commit(2)
.expect("should be able to reconstruct fully pruned commit");
assert_eq!(reconstructed.len(), 32, "length should match");
assert_eq!(
reconstructed.pruned_chunks(),
1,
"should have 1 pruned chunk"
);
let before_prune = bitmap
.get_at_commit(1)
.expect("should be able to reconstruct pre-prune state");
assert_eq!(before_prune.len(), 32);
assert_eq!(before_prune.pruned_chunks(), 0);
for i in 0..32 {
assert_eq!(before_prune.get_bit(i), i % 2 == 0);
}
}
fn test_randomized_helper<R: rand::Rng>(rng: &mut R) {
const NUM_COMMITS: u64 = 20;
const OPERATIONS_PER_COMMIT: usize = 32;
const CHUNK_SIZE_BITS: u64 = Prunable::<4>::CHUNK_SIZE_BITS;
const PROB_PUSH: u64 = 55; const PROB_MODIFY: u64 = 75; const PROB_POP: u64 = 90; const PROB_PRUNE: u64 = 100;
let mut bitmap: BitMap<4> = BitMap::new();
let mut ground_truth = Prunable::<4>::new();
let mut checkpoints: Vec<(u64, Prunable<4>)> = Vec::new();
for commit_num in 0..NUM_COMMITS {
let initial_len = ground_truth.len();
let initial_pruned = ground_truth.pruned_chunks();
let mut current_len = initial_len;
let mut current_pruned = initial_pruned;
let mut dirty = bitmap.into_dirty();
for _ in 0..OPERATIONS_PER_COMMIT {
let op_choice = rng.gen_range(0..100);
if current_len == 0 {
let bit_value = rng.gen_bool(0.5);
dirty.push(bit_value);
ground_truth.push(bit_value);
current_len += 1;
continue;
}
if op_choice < PROB_PUSH {
let bit_value = rng.gen_bool(0.5);
dirty.push(bit_value);
ground_truth.push(bit_value);
current_len += 1;
}
else if op_choice < PROB_MODIFY {
let bit = rng.gen_range(0..current_len);
let new_value = rng.gen_bool(0.5);
let chunk_idx = Prunable::<4>::to_chunk_index(bit);
if chunk_idx >= current_pruned {
dirty.set_bit(bit, new_value);
ground_truth.set_bit(bit, new_value);
}
}
else if op_choice < PROB_POP {
dirty.pop();
ground_truth.pop();
current_len -= 1;
}
else if op_choice < PROB_PRUNE {
let total_chunks = (current_len / CHUNK_SIZE_BITS) as usize;
let max_prune_chunk = total_chunks.saturating_sub(1);
if max_prune_chunk > current_pruned {
let prune_chunk = rng.gen_range((current_pruned + 1)..=max_prune_chunk);
let prune_to = (prune_chunk as u64) * CHUNK_SIZE_BITS;
dirty.prune_to_bit(prune_to);
ground_truth.prune_to_bit(prune_to);
current_pruned = prune_chunk;
}
}
}
bitmap = dirty.commit(commit_num).unwrap();
checkpoints.push((commit_num, ground_truth.clone()));
}
for (commit_num, checkpoint) in &checkpoints {
let reconstructed = bitmap.get_at_commit(*commit_num).unwrap();
assert_eq!(
reconstructed.len(),
checkpoint.len(),
"Length mismatch at commit {commit_num}"
);
assert_eq!(
reconstructed.pruned_chunks(),
checkpoint.pruned_chunks(),
"Pruned chunks mismatch at commit {commit_num}"
);
let start_bit = reconstructed.pruned_chunks() as u64 * Prunable::<4>::CHUNK_SIZE_BITS;
for i in start_bit..checkpoint.len() {
let expected = checkpoint.get_bit(i);
let actual = reconstructed.get_bit(i);
assert_eq!(
actual, expected,
"Bit {i} mismatch at commit {commit_num} (expected {expected}, got {actual})"
);
}
}
}
#[test]
fn test_randomized_with_multiple_seeds() {
use rand::{rngs::StdRng, SeedableRng};
for seed in 0..=100 {
let mut rng = StdRng::seed_from_u64(seed);
test_randomized_helper(&mut rng);
}
}
#[test]
#[should_panic(expected = "bit pruned: 31")]
fn test_pop_into_pruned_region_panics() {
let bitmap: BitMap<4> = BitMap::new();
let bitmap = bitmap
.apply_batch(1, |dirty| {
dirty.push_chunk(&[0xFF; 4]);
dirty.push_chunk(&[0xFF; 4]);
})
.unwrap();
let bitmap = bitmap
.apply_batch(2, |dirty| {
dirty.prune_to_bit(32);
})
.unwrap();
assert_eq!(bitmap.len(), 64);
assert_eq!(bitmap.pruned_chunks(), 1);
let mut dirty = bitmap.into_dirty();
for _ in 0..33 {
dirty.pop();
}
}
#[test]
fn test_commit_u64_max_is_reserved() {
let bitmap: BitMap<4> = BitMap::new();
let result = bitmap.apply_batch(u64::MAX, |dirty| {
dirty.push(true);
});
assert!(matches!(result, Err(Error::ReservedCommitNumber)));
let bitmap: BitMap<4> = BitMap::new();
let result = bitmap.apply_batch(u64::MAX - 1, |dirty| {
dirty.push(true);
});
assert!(result.is_ok());
let bitmap = result.unwrap();
assert_eq!(bitmap.len(), 1);
assert!(bitmap.get_at_commit(u64::MAX).is_none());
assert!(bitmap.get_at_commit(u64::MAX - 1).is_some());
}