#[cfg(test)]
mod tests {
use crate::wal::tests::helpers::*;
use crate::wal::{Wal, WalError};
use std::fs::{self, OpenOptions};
use tempfile::TempDir;
fn write_records(path: &std::path::Path, count: usize) -> u64 {
let wal: Wal<MemTableRecord> = Wal::open(path, None).unwrap();
for i in 0..count {
wal.append(&MemTableRecord {
key: format!("key_{i:04}").into_bytes(),
value: Some(format!("val_{i:04}").into_bytes()),
timestamp: i as u64,
deleted: false,
})
.unwrap();
}
drop(wal);
fs::metadata(path).unwrap().len()
}
fn truncate_file(path: &std::path::Path, size: u64) {
let f = OpenOptions::new().write(true).open(path).unwrap();
f.set_len(size).unwrap();
f.sync_all().unwrap();
}
fn replay_results(path: &std::path::Path) -> (Vec<MemTableRecord>, Option<WalError>) {
let wal: Wal<MemTableRecord> = Wal::open(path, None).unwrap();
let iter = wal.replay_iter().unwrap();
let mut ok_records = Vec::new();
let mut first_err = None;
for item in iter {
match item {
Ok(rec) => ok_records.push(rec),
Err(e) => {
first_err = Some(e);
break;
}
}
}
(ok_records, first_err)
}
#[test]
fn truncated_to_header_only_yields_zero_records() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
write_records(&path, 3);
let header_end = (WAL_HDR_SIZE + WAL_CRC32_SIZE) as u64;
truncate_file(&path, header_end);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 0);
assert!(err.is_none(), "Expected clean EOF, got: {err:?}");
}
#[test]
fn truncated_mid_length_field() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
write_records(&path, 3);
let header_end = (WAL_HDR_SIZE + WAL_CRC32_SIZE) as u64;
truncate_file(&path, header_end + 2);
let (records, _err) = replay_results(&path);
assert_eq!(records.len(), 0);
}
#[test]
fn truncated_mid_payload() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
write_records(&path, 3);
let header_end = (WAL_HDR_SIZE + WAL_CRC32_SIZE) as u64;
truncate_file(&path, header_end + 4 + 3);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 0);
assert!(err.is_some(), "Expected UnexpectedEof error");
assert!(
matches!(err.unwrap(), WalError::UnexpectedEof),
"Expected WalError::UnexpectedEof"
);
}
#[test]
fn truncated_missing_checksum_on_last_record() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
let full_size = write_records(&path, 3);
truncate_file(&path, full_size - 4);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 2, "First two records should be recovered");
assert!(err.is_some(), "Third record should yield an error");
assert!(
matches!(err.unwrap(), WalError::UnexpectedEof),
"Expected UnexpectedEof for missing checksum"
);
}
#[test]
fn truncated_partial_checksum_on_last_record() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
let full_size = write_records(&path, 3);
truncate_file(&path, full_size - 2);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 2, "First two records should be recovered");
assert!(err.is_some(), "Third record should yield an error");
assert!(
matches!(err.unwrap(), WalError::UnexpectedEof),
"Expected UnexpectedEof for partial checksum"
);
}
#[test]
fn truncated_second_record_first_survives() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
let size_after_1 = write_records(&path, 1);
{
let wal: Wal<MemTableRecord> = Wal::open(&path, None).unwrap();
for i in 1..3 {
wal.append(&MemTableRecord {
key: format!("key_{i:04}").into_bytes(),
value: Some(format!("val_{i:04}").into_bytes()),
timestamp: i as u64,
deleted: false,
})
.unwrap();
}
}
truncate_file(&path, size_after_1 + 4 + 5);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 1, "Only the first record should survive");
assert_eq!(records[0].key, b"key_0000");
assert!(err.is_some());
assert!(matches!(err.unwrap(), WalError::UnexpectedEof));
}
#[test]
fn zero_length_file_opens_as_fresh_wal() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
{
let _ = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.unwrap();
}
let wal: Wal<MemTableRecord> = Wal::open(&path, None).unwrap();
let records: Vec<_> = wal
.replay_iter()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn truncated_header_fails_to_open() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
write_records(&path, 1);
truncate_file(&path, 5);
let result = Wal::<MemTableRecord>::open(&path, None);
assert!(result.is_err(), "Partial header should fail to open");
}
#[test]
fn append_after_truncation_recovers_prior_records() {
init_tracing();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("000000.log");
let full_size = write_records(&path, 3);
truncate_file(&path, full_size - 4);
let wal: Wal<MemTableRecord> = Wal::open(&path, None).unwrap();
wal.append(&MemTableRecord {
key: b"new_key".to_vec(),
value: Some(b"new_val".to_vec()),
timestamp: 999,
deleted: false,
})
.unwrap();
drop(wal);
let (records, err) = replay_results(&path);
assert_eq!(records.len(), 2, "Only first two intact records survive");
assert!(err.is_some(), "Truncated 3rd record should error");
}
}