use plugmem_arena::{BlobHeap, BlobHeapBuilder, BlobHeapCfg, BlobId, Error};
use proptest::prelude::*;
#[test]
fn empty_heap() {
let heap = BlobHeap::new(BlobHeapCfg::new());
assert_eq!(heap.len(), 0);
assert!(heap.is_empty());
assert_eq!(heap.pool_bytes(), 0);
assert_eq!(heap.iter().count(), 0);
}
#[test]
fn push_get_roundtrip_ids_are_dense() {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
let a = heap.push(b"alpha").unwrap();
let b = heap.push(b"be").unwrap();
let c = heap.push(b"gamma-gamma").unwrap();
assert_eq!((a, b, c), (BlobId(0), BlobId(1), BlobId(2)));
assert_eq!(heap.get(a), b"alpha");
assert_eq!(heap.get(b), b"be");
assert_eq!(heap.get(c), b"gamma-gamma");
assert_eq!(heap.len(), 3);
assert!(!heap.is_empty());
assert_eq!(heap.pool_bytes(), 5 + 2 + 11);
}
#[test]
fn zero_length_blob_is_valid() {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
let empty = heap.push(b"").unwrap();
let after = heap.push(b"x").unwrap();
assert_eq!(heap.get(empty), b"");
assert_eq!(heap.get(after), b"x");
assert_eq!(heap.len(), 2);
assert_eq!(heap.pool_bytes(), 1);
}
#[test]
fn max_blob_boundary() {
let mut heap = BlobHeap::new(BlobHeapCfg::new().with_max_blob(4));
assert!(heap.push(&[7u8; 4]).is_ok());
assert_eq!(
heap.push(&[7u8; 5]),
Err(Error::BlobTooLarge {
len: 5,
max_blob: 4
})
);
assert_eq!(heap.len(), 1);
assert_eq!(heap.pool_bytes(), 4);
}
#[test]
fn max_bytes_boundary() {
let mut heap = BlobHeap::new(BlobHeapCfg::new().with_max_bytes(10));
assert!(heap.push(&[1u8; 6]).is_ok());
assert!(heap.push(&[2u8; 4]).is_ok()); assert_eq!(
heap.push(&[3u8; 1]),
Err(Error::CapacityExceeded { max_bytes: 10 })
);
assert_eq!(heap.len(), 2);
assert!(heap.push(b"").is_ok());
}
#[test]
#[should_panic]
fn get_with_dangling_id_panics() {
let heap = BlobHeap::new(BlobHeapCfg::new());
let _ = heap.get(BlobId(0));
}
#[test]
fn iter_yields_ids_in_order_with_contents() {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
let blobs: [&[u8]; 3] = [b"one", b"", b"three"];
for blob in blobs {
heap.push(blob).unwrap();
}
let got: Vec<(BlobId, &[u8])> = heap.iter().collect();
assert_eq!(
got,
vec![
(BlobId(0), b"one".as_slice()),
(BlobId(1), b"".as_slice()),
(BlobId(2), b"three".as_slice()),
]
);
}
#[test]
fn clone_and_eq_compare_contents() {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
heap.push(b"data").unwrap();
let copy = heap.clone();
assert_eq!(heap, copy);
let mut other = BlobHeap::new(BlobHeapCfg::new());
other.push(b"ohter").unwrap();
assert_ne!(heap, other);
}
#[test]
fn debug_is_a_summary() {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
heap.push(b"secret-content").unwrap();
let dump = format!("{heap:?}");
assert!(dump.contains("blobs: 1"));
assert!(dump.contains("pool_bytes: 14"));
assert!(!dump.contains("secret"));
}
#[test]
fn cfg_default_and_builders() {
assert_eq!(BlobHeapCfg::default(), BlobHeapCfg::new());
let cfg = BlobHeapCfg::new().with_max_bytes(100).with_max_blob(10);
assert_eq!(cfg.max_bytes, 100);
assert_eq!(cfg.max_blob, 10);
}
#[test]
fn builder_index_matches_a_heap_byte_for_byte() {
let blobs: [&[u8]; 4] = [b"alpha", b"", b"a longer blob here", b"z"];
let mut heap = BlobHeap::new(BlobHeapCfg::new());
let mut builder = BlobHeapBuilder::new(BlobHeapCfg::new());
for b in blobs {
assert_eq!(heap.push(b).unwrap(), builder.push_len(b.len()).unwrap());
}
assert_eq!(builder.len(), heap.len());
assert_eq!(builder.pool_bytes(), heap.pool_bytes());
let mut heap_index = Vec::new();
heap.dump_index(&mut heap_index);
let mut builder_index = Vec::new();
builder.dump_index(&mut builder_index);
assert_eq!(builder_index, heap_index);
let mut pool = Vec::new();
heap.dump_pool(&mut pool);
let reloaded = BlobHeap::load_borrowed(BlobHeapCfg::new(), &builder_index, &pool).unwrap();
assert_eq!(reloaded, heap);
}
#[test]
fn builder_validates_identically_to_push() {
let cfg = BlobHeapCfg::new().with_max_blob(4);
assert!(matches!(
BlobHeapBuilder::new(cfg).push_len(5),
Err(Error::BlobTooLarge { .. })
));
assert!(matches!(
BlobHeap::new(cfg).push(b"12345"),
Err(Error::BlobTooLarge { .. })
));
let cfg = BlobHeapCfg::new().with_max_bytes(6);
let mut builder = BlobHeapBuilder::new(cfg);
builder.push_len(4).unwrap();
assert!(matches!(
builder.push_len(3),
Err(Error::CapacityExceeded { .. })
));
let mut heap = BlobHeap::new(cfg);
heap.push(b"1234").unwrap();
assert!(matches!(
heap.push(b"567"),
Err(Error::CapacityExceeded { .. })
));
}
fn dump(heap: &BlobHeap) -> (Vec<u8>, Vec<u8>) {
let (mut index, mut pool) = (Vec::new(), Vec::new());
heap.dump_index(&mut index);
heap.dump_pool(&mut pool);
(index, pool)
}
#[test]
fn overlay_open_appends_to_tail_without_touching_base() {
let mut owned = BlobHeap::new(BlobHeapCfg::new());
owned.push(b"alpha").unwrap();
owned.push(b"beta").unwrap();
let (index, pool) = dump(&owned);
let base_snapshot = pool.clone();
let mut heap = BlobHeap::load_borrowed(BlobHeapCfg::new(), &index, &pool).unwrap();
assert_eq!(heap.len(), 2);
let c = heap.push(b"gamma").unwrap();
assert_eq!(c, BlobId(2));
assert_eq!(heap.get(BlobId(0)), b"alpha"); assert_eq!(heap.get(BlobId(1)), b"beta"); assert_eq!(heap.get(BlobId(2)), b"gamma"); assert_eq!(heap.len(), 3);
assert_eq!(heap.pool_bytes(), 5 + 4 + 5);
assert_eq!(pool, base_snapshot);
}
#[test]
fn overlay_dump_is_byte_identical_to_owned_and_compares_equal() {
let mut owned = BlobHeap::new(BlobHeapCfg::new());
for b in [b"a".as_slice(), b"bb", b"ccc"] {
owned.push(b).unwrap();
}
let mut seed = BlobHeap::new(BlobHeapCfg::new());
seed.push(b"a").unwrap();
let (index, pool) = dump(&seed);
let mut overlay = BlobHeap::load_overlay(BlobHeapCfg::new(), &index, &pool).unwrap();
overlay.push(b"bb").unwrap();
overlay.push(b"ccc").unwrap();
assert_eq!(dump(&owned), dump(&overlay));
assert_eq!(owned, overlay);
}
#[test]
fn overlay_iter_spans_base_and_tail() {
let mut seed = BlobHeap::new(BlobHeapCfg::new());
seed.push(b"x").unwrap();
let (index, pool) = dump(&seed);
let mut heap = BlobHeap::load_overlay(BlobHeapCfg::new(), &index, &pool).unwrap();
heap.push(b"yy").unwrap();
let got: Vec<Vec<u8>> = heap.iter().map(|(_, b)| b.to_vec()).collect();
assert_eq!(got, vec![b"x".to_vec(), b"yy".to_vec()]);
}
#[test]
#[cfg(target_pointer_width = "64")]
fn pool_offset_exceeds_4gib_on_64bit() {
let mut builder = BlobHeapBuilder::new(BlobHeapCfg::new());
let gib = 1usize << 30;
for _ in 0..5 {
builder.push_len(gib).unwrap(); }
assert_eq!(builder.pool_bytes(), 5 * gib);
assert!(
builder.pool_bytes() > u32::MAX as usize,
"the pool grew past the old 4 GiB u32 ceiling"
);
}
proptest! {
#[test]
#[cfg_attr(miri, ignore)]
fn behaves_like_vec_of_vecs(blobs in proptest::collection::vec(
proptest::collection::vec(any::<u8>(), 0..200),
1..64,
)) {
let mut heap = BlobHeap::new(BlobHeapCfg::new());
let mut model: Vec<Vec<u8>> = Vec::new();
for blob in &blobs {
let id = heap.push(blob).unwrap();
prop_assert_eq!(id.0 as usize, model.len());
model.push(blob.clone());
}
prop_assert_eq!(heap.len(), model.len());
for (i, blob) in model.iter().enumerate() {
prop_assert_eq!(heap.get(BlobId(i as u32)), blob.as_slice());
}
let iterated: Vec<Vec<u8>> = heap.iter().map(|(_, b)| b.to_vec()).collect();
prop_assert_eq!(iterated, model);
}
}