use std::fs;
use wal_db::{Wal, WalConfig};
fn build_log(records: &[&[u8]]) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hostile.wal");
let wal = Wal::open(&path).unwrap();
for record in records {
let _ = wal.append(record).unwrap();
}
wal.sync().unwrap();
(dir, path)
}
fn record_count(path: &std::path::Path) -> usize {
Wal::open(path).unwrap().iter().unwrap().count()
}
#[test]
fn garbage_prefix_recovers_nothing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("garbage.wal");
fs::write(&path, b"this is not a wal-db record at all, just text").unwrap();
let wal = Wal::open(&path).unwrap();
assert_eq!(wal.iter().unwrap().count(), 0);
assert_eq!(wal.len(), 0);
}
#[test]
fn implausible_length_is_rejected_without_allocating() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("biglen.wal");
fs::write(&path, [0u8, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF]).unwrap();
let config = WalConfig::new().with_max_record_size(1024);
let wal = Wal::open_with(&path, config).unwrap(); assert_eq!(wal.iter().unwrap().count(), 0);
assert_eq!(wal.len(), 0);
}
#[test]
fn all_zeros_recovers_nothing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("zeros.wal");
fs::write(&path, vec![0u8; 4096]).unwrap();
let wal = Wal::open(&path).unwrap();
assert_eq!(wal.iter().unwrap().count(), 0);
}
#[test]
fn valid_records_then_garbage_tail_keeps_the_valid_ones() {
let (_dir, path) = build_log(&[b"first", b"second", b"third"]);
let clean_len = fs::metadata(&path).unwrap().len();
let mut bytes = fs::read(&path).unwrap();
bytes.extend_from_slice(b"\xDE\xAD\xBE\xEF garbage tail");
fs::write(&path, &bytes).unwrap();
let wal = Wal::open(&path).unwrap();
let got: Vec<Vec<u8>> = wal
.iter()
.unwrap()
.map(|e| e.unwrap().into_data())
.collect();
assert_eq!(
got,
vec![b"first".to_vec(), b"second".to_vec(), b"third".to_vec()]
);
assert_eq!(wal.len(), clean_len);
}
#[test]
fn corrupt_middle_record_truncates_from_there() {
let (_dir, path) = build_log(&[b"alpha", b"bravo", b"charlie"]);
let mut bytes = fs::read(&path).unwrap();
bytes[21] ^= 0xFF;
fs::write(&path, &bytes).unwrap();
let wal = Wal::open(&path).unwrap();
let got: Vec<Vec<u8>> = wal
.iter()
.unwrap()
.map(|e| e.unwrap().into_data())
.collect();
assert_eq!(got, vec![b"alpha".to_vec()]);
assert_eq!(wal.len(), 13);
}
#[test]
fn truncated_mid_payload_drops_the_partial_record() {
let (_dir, path) = build_log(&[b"complete", b"incomplete"]);
let file = fs::OpenOptions::new().write(true).open(&path).unwrap();
file.set_len(28).unwrap();
drop(file);
assert_eq!(record_count(&path), 1);
}
#[test]
fn a_torn_head_marker_falls_back_to_full_recovery() {
let dir = tempfile::tempdir().unwrap();
{
let wal = Wal::open_segmented(dir.path(), 32).unwrap();
for i in 0..10u32 {
let _ = wal.append(format!("rec{i}").as_bytes()).unwrap();
}
wal.sync().unwrap();
}
fs::write(dir.path().join("head"), 3u64.to_le_bytes()).unwrap(); let wal = Wal::open_segmented(dir.path(), 32).unwrap();
assert_eq!(wal.iter().unwrap().count(), 10);
let mut corrupt = [0u8; 12];
corrupt[..8].copy_from_slice(&3u64.to_le_bytes()); fs::write(dir.path().join("head"), corrupt).unwrap();
let wal = Wal::open_segmented(dir.path(), 32).unwrap();
assert_eq!(wal.iter().unwrap().count(), 10);
}
#[test]
fn truncated_mid_header_drops_the_partial_record() {
let (_dir, path) = build_log(&[b"whole"]);
let mut bytes = fs::read(&path).unwrap();
bytes.extend_from_slice(&[0xAB; 4]);
fs::write(&path, &bytes).unwrap();
let wal = Wal::open(&path).unwrap();
assert_eq!(wal.iter().unwrap().count(), 1);
assert_eq!(wal.len(), 13); }