use chisel::{Chisel, ChiselError, DrainInsertion, Options};
#[test]
fn large_transaction_with_spill_produces_identical_state() {
let cache_max_bytes = 16 * 8192; let opts_with_spillway = Options::default()
.cache_max_bytes(cache_max_bytes)
.spillway_max_bytes(1024 * cache_max_bytes)
.drain_insertion(DrainInsertion::LruTail);
let mut db_a = Chisel::open_in_memory_with_options(opts_with_spillway.clone()).unwrap();
db_a.begin().unwrap();
let mut handles_a = Vec::new();
for i in 0..200 {
let payload = vec![i as u8; 1024]; let h = db_a.allocate(&payload).unwrap();
handles_a.push((h, payload));
}
db_a.commit().unwrap();
let mut db_b = Chisel::open_in_memory_with_options(opts_with_spillway).unwrap();
let mut handles_b = Vec::new();
for chunk_start in (0..200).step_by(20) {
db_b.begin().unwrap();
for i in chunk_start..chunk_start + 20 {
let payload = vec![i as u8; 1024];
let h = db_b.allocate(&payload).unwrap();
handles_b.push((h, payload));
}
db_b.commit().unwrap();
}
assert_eq!(
handles_a.len(),
handles_b.len(),
"both runs must allocate the same number of chunks"
);
assert_eq!(handles_a.len(), 200, "sanity: the full workload ran");
for (i, (h_a, expected)) in handles_a.iter().enumerate() {
let (h_b, _) = handles_b[i];
assert_eq!(
*h_a, h_b,
"handle id diverged between the spill run and the control run at index {i}"
);
let a = db_a.read(*h_a).unwrap();
let b = db_b.read(h_b).unwrap();
assert_eq!(a, *expected, "spill-run handle {h_a} content corrupt");
assert_eq!(
a, b,
"spill run and control run disagree on handle {h_a} — \
the spill path produced a different mapping"
);
}
}
#[test]
fn rollback_with_spill_leaves_main_file_unchanged() {
let cache_max_bytes = 16 * 8192;
let opts = Options::default()
.cache_max_bytes(cache_max_bytes)
.spillway_max_bytes(1024 * cache_max_bytes)
.drain_insertion(DrainInsertion::LruTail);
let mut db = Chisel::open_in_memory_with_options(opts).unwrap();
db.begin().unwrap();
let baseline_h = db.allocate(b"baseline").unwrap();
db.commit().unwrap();
db.begin().unwrap();
let mut spilled_handles = Vec::new();
for i in 0..200 {
let h = db.allocate(&vec![i as u8; 1024]).unwrap();
spilled_handles.push(h);
}
db.rollback().unwrap();
let bytes = db.read(baseline_h).unwrap();
assert_eq!(bytes, b"baseline");
for h in spilled_handles {
assert!(
matches!(db.read(h), Err(ChiselError::InvalidHandle(_))),
"handle {h} survived rollback: expected InvalidHandle"
);
}
db.begin().unwrap();
let _h = db.allocate(b"post-rollback").unwrap();
db.commit().unwrap();
}
#[test]
fn no_spill_workload_preserves_two_fsync_commit() {
let mut db = Chisel::open_in_memory().unwrap();
db.begin().unwrap();
for i in 0..50u32 {
db.allocate(&i.to_le_bytes()).unwrap();
}
let pre_commit_counters = db.counters().unwrap();
db.commit().unwrap();
let post_commit_counters = db.counters().unwrap();
let fsync_delta = post_commit_counters.fsync_calls - pre_commit_counters.fsync_calls;
assert_eq!(
fsync_delta, 3,
"no-spill commit must issue exactly 3 fsyncs (I28 pre-drain + main + superblock); \
the spillway must add zero. Got {fsync_delta}"
);
}
#[test]
fn spillway_max_bytes_zero_disables_spillway_and_fires_cache_full() {
let cache_max_bytes = 4 * 8192; let opts = Options::default()
.cache_max_bytes(cache_max_bytes)
.spillway_max_bytes(0) .drain_insertion(DrainInsertion::LruTail);
let mut db = Chisel::open_in_memory_with_options(opts).unwrap();
db.begin().unwrap();
let mut hit_cache_full = false;
for _ in 0..50 {
match db.allocate(&[0u8; 4096]) {
Ok(_) => {}
Err(ChiselError::CacheFull { .. }) => {
hit_cache_full = true;
break;
}
Err(other) => panic!("unexpected error: {other:?}"),
}
}
assert!(
hit_cache_full,
"with spillway disabled, allocation must trip CacheFull"
);
db.rollback().unwrap();
}
#[test]
fn spillway_max_bytes_zero_creates_no_spillway_file() {
use std::path::PathBuf;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let db_path: PathBuf = dir.path().join("test.chisel");
let spillway_path: PathBuf = {
let mut p = db_path.as_os_str().to_owned();
p.push(".spillway");
PathBuf::from(p)
};
let opts = Options::default().spillway_max_bytes(0);
let mut db = Chisel::open(&db_path, opts).unwrap();
db.begin().unwrap();
let _h = db.allocate(b"x").unwrap();
db.commit().unwrap();
drop(db);
assert!(
!spillway_path.exists(),
"spillway file should not exist when spillway_max_bytes = 0"
);
}
#[test]
fn crash_mid_spill_recovers_to_last_committed_state() {
use std::path::PathBuf;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let db_path: PathBuf = dir.path().join("crash.chisel");
let spillway_path: PathBuf = {
let mut p = db_path.as_os_str().to_owned();
p.push(".spillway");
PathBuf::from(p)
};
{
let mut db = Chisel::open(&db_path, Options::default()).unwrap();
db.begin().unwrap();
let _h = db.allocate(b"baseline").unwrap();
db.commit().unwrap();
}
std::fs::write(&spillway_path, b"\xFF\xFF\xFF crashed mid-spill garbage").unwrap();
assert!(spillway_path.exists());
let pre_open_garbage_size = std::fs::metadata(&spillway_path).unwrap().len();
assert!(pre_open_garbage_size > 0);
let cache_max_bytes = 4 * 8192; let opts = Options::default()
.cache_max_bytes(cache_max_bytes)
.spillway_max_bytes(1024 * cache_max_bytes);
let mut db = Chisel::open(&db_path, opts).unwrap();
db.begin().unwrap();
for i in 0..20u8 {
db.allocate(&vec![i; 4096]).unwrap();
}
db.commit().unwrap();
drop(db);
let post_recovery_size = std::fs::metadata(&spillway_path)
.map(|m| m.len())
.unwrap_or(0);
assert_eq!(
post_recovery_size, 0,
"spillway must be truncated to zero after open + commit (garbage from crash must be gone)"
);
}
#[test]
fn stats_exposes_spillway_capacity_across_lifecycle() {
let cache_max_bytes: u64 = 16 * 8192; let spillway_cap = 1024u64 * cache_max_bytes;
let opts = Options::default()
.cache_max_bytes(cache_max_bytes)
.spillway_max_bytes(spillway_cap)
.drain_insertion(DrainInsertion::LruTail);
let mut db = Chisel::open_in_memory_with_options(opts).unwrap();
let s = db.stats().unwrap();
assert_eq!(
s.spillway_logical_bytes, None,
"fresh handle: spillway has never been opened"
);
assert_eq!(
s.spillway_max_bytes, None,
"fresh handle: spillway has never been opened"
);
db.begin().unwrap();
for i in 0..200 {
let payload = vec![i as u8; 1024];
db.allocate(&payload).unwrap();
}
let s = db.stats().unwrap();
assert!(
s.spillway_logical_bytes.is_some(),
"after overflow workload: spillway must have been opened"
);
assert!(
s.spillway_logical_bytes.unwrap() > 0,
"after overflow workload: spillway must hold at least one spilled page"
);
assert_eq!(
s.spillway_max_bytes,
Some(spillway_cap),
"max_bytes must report the configured cap"
);
db.commit().unwrap();
let s = db.stats().unwrap();
assert_eq!(
s.spillway_logical_bytes,
Some(0),
"post-commit: spillway exists but is drained"
);
assert_eq!(
s.spillway_max_bytes,
Some(spillway_cap),
"max_bytes is unchanged across commit"
);
}