#[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 leftover_tmp_from_interrupted_checkpoint() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let m = open_manifest(&temp);
m.set_active_wal(5).unwrap();
m.add_sstable(sst_entry(1)).unwrap();
}
let tmp_path = temp.path().join("MANIFEST-000001.tmp");
fs::write(&tmp_path, b"corrupted partial snapshot data").unwrap();
let m2 = open_manifest(&temp);
assert_eq!(m2.get_active_wal().unwrap(), 5);
assert_eq!(m2.get_sstables().unwrap().len(), 1);
assert_eq!(m2.get_sstables().unwrap()[0].id, 1);
}
#[test]
fn multiple_rapid_checkpoints() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
for i in 1..=5u64 {
m.add_sstable(sst_entry(i)).unwrap();
m.checkpoint().unwrap();
}
}
let m2 = open_manifest(&temp);
let ssts = m2.get_sstables().unwrap();
assert_eq!(ssts.len(), 5);
for i in 1..=5u64 {
assert!(ssts.iter().any(|e| e.id == i), "SST {} must exist", i);
}
}
#[test]
fn allocate_sst_id_monotonic_after_checkpoint() {
init_tracing();
let temp = TempDir::new().unwrap();
let (id_a, id_b);
{
let mut m = open_manifest(&temp);
id_a = m.allocate_sst_id().unwrap();
id_b = m.allocate_sst_id().unwrap();
assert!(id_b > id_a, "IDs should be monotonically increasing");
m.checkpoint().unwrap();
}
let m2 = open_manifest(&temp);
let id_c = m2.allocate_sst_id().unwrap();
assert!(
id_c > id_b,
"ID after reopen ({id_c}) must be > last pre-checkpoint ID ({id_b})"
);
let id_d = m2.allocate_sst_id().unwrap();
assert!(id_d > id_c);
}
#[test]
fn allocate_sst_id_monotonic_after_wal_recovery() {
init_tracing();
let temp = TempDir::new().unwrap();
let last_id;
{
let m = open_manifest(&temp);
for _ in 0..10 {
m.allocate_sst_id().unwrap();
}
last_id = m.peek_next_sst_id().unwrap() - 1;
}
let m2 = open_manifest(&temp);
let new_id = m2.allocate_sst_id().unwrap();
assert!(
new_id > last_id,
"Post-reopen ID ({new_id}) must exceed pre-crash last ({last_id})"
);
}
#[test]
fn large_state_checkpoint_round_trip() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
for i in 1..=100u64 {
m.add_sstable(sst_entry(i)).unwrap();
}
for w in 1..=50u64 {
m.add_frozen_wal(w).unwrap();
}
m.update_lsn(12345).unwrap();
m.set_active_wal(999).unwrap();
m.checkpoint().unwrap();
}
let m2 = open_manifest(&temp);
assert_eq!(m2.get_sstables().unwrap().len(), 100);
assert_eq!(m2.get_frozen_wals().unwrap().len(), 50);
assert_eq!(m2.get_last_lsn().unwrap(), 12345);
assert_eq!(m2.get_active_wal().unwrap(), 999);
}
#[test]
fn mutations_after_checkpoint_replayed() {
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.checkpoint().unwrap();
m.add_sstable(sst_entry(2)).unwrap();
m.add_frozen_wal(10).unwrap();
m.set_active_wal(11).unwrap();
m.update_lsn(42).unwrap();
}
let m2 = open_manifest(&temp);
let ssts = m2.get_sstables().unwrap();
assert_eq!(ssts.len(), 2, "Both SSTs (pre+post checkpoint) must exist");
assert_eq!(m2.get_active_wal().unwrap(), 11);
assert_eq!(m2.get_last_lsn().unwrap(), 42);
assert!(m2.get_frozen_wals().unwrap().contains(&10));
}
#[test]
fn corrupt_snapshot_falls_back_to_wal() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
m.add_sstable(sst_entry(1)).unwrap();
m.update_lsn(100).unwrap();
m.checkpoint().unwrap();
}
let snap_path = temp.path().join("MANIFEST-000001");
let mut data = fs::read(&snap_path).unwrap();
assert!(data.len() > 10, "Snapshot should be non-trivial");
let mid = data.len() / 2;
data[mid] ^= 0xFF;
fs::write(&snap_path, &data).unwrap();
let m2 = Manifest::open(temp.path()).unwrap();
assert_eq!(m2.get_last_lsn().unwrap(), 0);
assert!(m2.get_sstables().unwrap().is_empty());
}
#[test]
fn compaction_event_survives_checkpoint() {
init_tracing();
let temp = TempDir::new().unwrap();
{
let mut m = open_manifest(&temp);
m.add_sstable(sst_entry(1)).unwrap();
m.add_sstable(sst_entry(2)).unwrap();
m.add_sstable(sst_entry(3)).unwrap();
m.apply_compaction(vec![sst_entry(4)], vec![1, 2]).unwrap();
m.checkpoint().unwrap();
}
let m2 = open_manifest(&temp);
let ssts = m2.get_sstables().unwrap();
let ids: Vec<u64> = ssts.iter().map(|e| e.id).collect();
assert!(!ids.contains(&1), "SST 1 should have been removed");
assert!(!ids.contains(&2), "SST 2 should have been removed");
assert!(ids.contains(&3), "SST 3 should remain");
assert!(ids.contains(&4), "SST 4 should have been added");
}
}