use chisel::{Chisel, Options};
use std::collections::HashMap;
#[test]
fn repeated_update_of_same_handle_reaches_bounded_page_count() {
let mut db = Chisel::open_in_memory_with_options(Options::default()).unwrap();
db.begin().unwrap();
let h = db.allocate(b"a-small-constant-value").unwrap();
db.commit().unwrap();
let pages_before = db.stats().unwrap().total_pages;
const N: u64 = 2000;
for _ in 0..N {
db.begin().unwrap();
db.update(h, b"a-small-constant-value").unwrap();
db.commit().unwrap();
}
let pages_after = db.stats().unwrap().total_pages;
let growth = pages_after - pages_before;
assert!(
growth < 50,
"file grew by {growth} pages over {N} constant-size updates \
(pages_before={pages_before}, pages_after={pages_after}); \
expected a bounded steady state, not a per-op leak"
);
assert_eq!(db.read(h).unwrap(), b"a-small-constant-value");
}
#[test]
fn repeated_set_client_byte_reaches_bounded_page_count() {
let mut db = Chisel::open_in_memory_with_options(Options::default()).unwrap();
db.begin().unwrap();
let h = db.allocate(b"v").unwrap();
db.commit().unwrap();
let pages_before = db.stats().unwrap().total_pages;
const N: u64 = 2000;
for i in 0..N {
db.begin().unwrap();
db.set_client_byte(h, (i % 256) as u8).unwrap();
db.commit().unwrap();
}
let pages_after = db.stats().unwrap().total_pages;
let growth = pages_after - pages_before;
assert!(
growth < 50,
"file grew by {growth} pages over {N} set_client_byte calls \
(pages_before={pages_before}, pages_after={pages_after}); \
expected a bounded steady state, not a per-op leak"
);
assert_eq!(db.client_byte(h).unwrap(), ((N - 1) % 256) as u8);
}
#[test]
fn tagged_allocate_delete_churn_reaches_bounded_page_count() {
let mut db = Chisel::open_in_memory_with_options(Options::default()).unwrap();
db.begin().unwrap();
let keeper = db
.allocate_tagged(b"keeper", chisel::Tag::new(7).unwrap())
.unwrap();
db.commit().unwrap();
let pages_before = db.stats().unwrap().total_pages;
const N: u64 = 1000;
for _ in 0..N {
db.begin().unwrap();
let h = db
.allocate_tagged(b"ephemeral", chisel::Tag::new(7).unwrap())
.unwrap();
db.commit().unwrap();
db.begin().unwrap();
db.delete(h).unwrap();
db.commit().unwrap();
}
let pages_after = db.stats().unwrap().total_pages;
let growth = pages_after - pages_before;
assert!(
growth < 200,
"file grew by {growth} pages over {N} tagged alloc/delete cycles \
(pages_before={pages_before}, pages_after={pages_after}); \
expected sub-linear growth, not a per-op COW leak"
);
assert_eq!(db.tag(keeper).unwrap().unwrap(), 7);
assert_eq!(
db.handles_with_tag(chisel::Tag::new(7).unwrap()).unwrap(),
vec![keeper]
);
}
#[test]
fn heavy_churn_with_reclamation_survives_reopen_file_backed() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("churn.db");
let mut model: HashMap<chisel::Handle, (Vec<u8>, u32, u8)> = HashMap::new();
{
let mut db = Chisel::open(&path, Options::default()).unwrap();
db.begin().unwrap();
for i in 0u64..40 {
let tag = if i % 3 == 0 { (i % 5) as u32 + 1 } else { 0 };
let val = vec![i as u8; 100 + (i as usize % 7) * 50];
let h = if tag == 0 {
db.allocate(&val).unwrap()
} else {
db.allocate_tagged(&val, chisel::Tag::new(tag).unwrap())
.unwrap()
};
model.insert(h, (val, tag, 0));
}
db.commit().unwrap();
for round in 0u64..60 {
db.begin().unwrap();
let handles: Vec<chisel::Handle> = model.keys().copied().collect();
for (idx, h) in handles.iter().enumerate() {
match (round as usize + idx) % 4 {
0 => {
let new = vec![(round ^ h.get()) as u8; model[h].0.len()];
db.update(*h, &new).unwrap();
model.get_mut(h).unwrap().0 = new;
}
1 => {
let new = vec![(round as u8).wrapping_add(idx as u8); 9000];
db.update(*h, &new).unwrap();
model.get_mut(h).unwrap().0 = new;
}
2 => {
let b = (round as u8).wrapping_add(7);
db.set_client_byte(*h, b).unwrap();
model.get_mut(h).unwrap().2 = b;
}
_ => {
let (_, tag, _) = model.remove(h).unwrap();
if tag == 0 {
db.delete(*h).unwrap();
} else {
db.delete_tagged(*h, chisel::Tag::new(tag).unwrap())
.unwrap();
}
let val = vec![round as u8; 120];
let nh = if tag == 0 {
db.allocate(&val).unwrap()
} else {
db.allocate_tagged(&val, chisel::Tag::new(tag).unwrap())
.unwrap()
};
model.insert(nh, (val, tag, 0));
}
}
}
db.commit().unwrap();
}
db.close().unwrap();
}
let db = Chisel::open(&path, Options::default()).unwrap();
let live: std::collections::HashSet<chisel::Handle> =
db.handles().unwrap().into_iter().collect();
let expected: std::collections::HashSet<chisel::Handle> = model.keys().copied().collect();
assert_eq!(live, expected, "live handle set must survive reopen");
for (h, (val, tag, cb)) in &model {
assert_eq!(
&db.read(*h).unwrap(),
val,
"value for handle {h} after reopen"
);
assert_eq!(
db.tag(*h).unwrap().map_or(0, |t| t.get()),
*tag,
"tag for handle {h} after reopen"
);
assert_eq!(
db.client_byte(*h).unwrap(),
*cb,
"client byte for handle {h} after reopen"
);
}
let mut expected_by_tag: HashMap<u32, Vec<chisel::Handle>> = HashMap::new();
for (h, (_, tag, _)) in &model {
if *tag != 0 {
expected_by_tag.entry(*tag).or_default().push(*h);
}
}
for (tag, mut handles) in expected_by_tag {
let mut got = db.handles_with_tag(chisel::Tag::new(tag).unwrap()).unwrap();
got.sort_unstable();
handles.sort_unstable();
assert_eq!(got, handles, "handles_with_tag({tag}) after reopen");
}
}