#[cfg(test)]
mod tests {
use crate::manifest::{Manifest, ManifestSstEntry};
use std::fs;
use tempfile::TempDir;
use tracing_subscriber::EnvFilter;
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.try_init();
}
fn open_manifest(temp: &TempDir) -> Manifest {
Manifest::open(temp.path()).expect("Manifest open failed")
}
fn sst_entry(id: u64) -> ManifestSstEntry {
ManifestSstEntry {
id,
path: format!("sst_{:06}.sst", id).into(),
}
}
#[test]
fn dirty_flag_lifecycle() {
init_tracing();
let temp = TempDir::new().unwrap();
let mut m = open_manifest(&temp);
assert!(!m.is_dirty().unwrap(), "fresh manifest should be clean");
m.set_active_wal(1).unwrap();
assert!(m.is_dirty().unwrap(), "mutation should set dirty");
m.checkpoint().unwrap();
assert!(!m.is_dirty().unwrap(), "checkpoint should clear dirty");
m.update_lsn(42).unwrap();
assert!(
m.is_dirty().unwrap(),
"post-checkpoint mutation should set dirty"
);
}
#[test]
fn peek_next_sst_id_does_not_allocate() {
init_tracing();
let temp = TempDir::new().unwrap();
let m = open_manifest(&temp);
let peek1 = m.peek_next_sst_id().unwrap();
let peek2 = m.peek_next_sst_id().unwrap();
assert_eq!(peek1, peek2, "consecutive peeks should be equal");
let allocated = m.allocate_sst_id().unwrap();
assert_eq!(allocated, peek1, "allocate should return the peeked value");
let peek3 = m.peek_next_sst_id().unwrap();
assert_eq!(peek3, allocated + 1, "peek after allocate should be +1");
}
#[test]
fn mutation_replay_sets_dirty() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let m = open_manifest(&temp);
m.set_active_wal(7).unwrap();
m.update_lsn(10).unwrap();
}
let m2 = open_manifest(&temp);
assert!(m2.is_dirty().unwrap(), "WAL replay should set dirty flag");
assert_eq!(m2.get_active_wal().unwrap(), 7);
}
#[test]
fn corrupt_snapshot_recovers_post_checkpoint_wal() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
m.add_sstable(sst_entry(1)).unwrap();
m.set_active_wal(10).unwrap();
m.update_lsn(100).unwrap();
m.checkpoint().unwrap();
m.add_sstable(sst_entry(2)).unwrap();
m.update_lsn(200).unwrap();
}
let snap_path = temp.path().join("MANIFEST-000001");
let mut raw = fs::read(&snap_path).unwrap();
let mid = raw.len() / 2;
raw[mid] ^= 0xFF;
fs::write(&snap_path, &raw).unwrap();
let m2 = open_manifest(&temp);
let ssts = m2.get_sstables().unwrap();
assert!(
!ssts.iter().any(|e| e.id == 1),
"SST 1 was in corrupt snapshot and should be lost"
);
assert!(
ssts.iter().any(|e| e.id == 2),
"SST 2 was in post-checkpoint WAL and should be recovered"
);
assert_eq!(m2.get_last_lsn().unwrap(), 200);
}
#[test]
fn allocate_sst_id_strict_monotonicity() {
init_tracing();
let temp = TempDir::new().unwrap();
let m = open_manifest(&temp);
let mut prev = 0u64;
for _ in 0..100 {
let id = m.allocate_sst_id().unwrap();
assert!(id > prev, "ID {} should be > previous {}", id, prev);
prev = id;
}
}
#[test]
fn clean_after_checkpoint_reopen() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
m.set_active_wal(5).unwrap();
m.checkpoint().unwrap();
}
let m2 = open_manifest(&temp);
assert!(
!m2.is_dirty().unwrap(),
"manifest should be clean after checkpoint + reopen with empty WAL"
);
}
#[test]
fn allocate_sst_id_with_intermixed_ops() {
init_tracing();
let temp = TempDir::new().unwrap();
let id3;
{
let mut m = open_manifest(&temp);
let id1 = m.allocate_sst_id().unwrap();
m.add_sstable(sst_entry(id1)).unwrap();
m.set_active_wal(1).unwrap();
let id2 = m.allocate_sst_id().unwrap();
assert!(id2 > id1);
m.add_sstable(sst_entry(id2)).unwrap();
m.checkpoint().unwrap();
id3 = m.allocate_sst_id().unwrap();
assert!(id3 > id2);
}
let m2 = open_manifest(&temp);
let id4 = m2.allocate_sst_id().unwrap();
assert!(
id4 > id3,
"ID after reopen ({id4}) must exceed pre-close ID ({id3})"
);
}
}