use std::fs::File;
use std::io::Write;
use std::num::NonZeroU64;
use tempfile::tempdir;
use super::*;
fn perform_checkpoint(
wal: &mut WalManager,
reason: CheckpointReason,
seal_current_segment: bool,
last_checkpointed_version: CheckpointState,
) -> Result<CheckpointState, WalError> {
let target = wal.compute_checkpoint_target(reason, last_checkpointed_version);
let Some(version) = target else {
return Ok(None);
};
if seal_current_segment && let Some(writer) = wal.active_writer.take() {
writer.seal()?;
}
wal.commit_checkpoint(version, last_checkpointed_version)?;
Ok(Some(version))
}
use crate::paths::DbPaths;
use crate::serialization::serialize_wal_op_raw;
use crate::types::{BlobHash, CheckpointReason, WalOpRaw};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TestKey(pub u64);
impl KeyBytes for TestKey {
type Bytes = [u8; 8];
fn to_key_bytes(&self) -> Self::Bytes {
self.0.to_le_bytes()
}
fn from_key_bytes(bytes: &[u8]) -> Option<Self> {
<[u8; 8]>::try_from(bytes).map(u64::from_le_bytes).map(Self).ok()
}
}
fn setup_wal_manager(num_ops_per_wal: u64) -> (WalManager, tempfile::TempDir) {
let dir = tempdir().unwrap();
let paths = DbPaths::new(dir.path().to_path_buf());
let wal_manager = WalManager::new(paths, NonZeroU64::new(num_ops_per_wal).unwrap()).unwrap();
(wal_manager, dir)
}
fn append_ops(wal_manager: &mut WalManager, count: u64) {
for i in 0..count {
let op_raw = WalOpRaw::Put {
key_bytes: i.to_le_bytes().to_vec(),
hash: BlobHash::from_bytes([0; 32]),
size: 1,
};
let op_data = serialize_wal_op_raw(&op_raw).unwrap();
wal_manager.append_op(&op_data).unwrap();
}
}
#[test]
fn wal_manager_new_fails_on_uncreatable_directory() {
#[cfg(unix)]
{
let invalid_path = std::path::PathBuf::from("/proc/test_wal_invalid");
let paths = DbPaths::new(invalid_path);
let result = WalManager::new(paths, NonZeroU64::new(100).unwrap());
assert!(matches!(result, Err(WalError::Io { .. })));
}
}
#[test]
fn append_op_fails_when_segment_rollover_cannot_create_file() {
let (mut wal_manager, dir) = setup_wal_manager(2);
append_ops(&mut wal_manager, 2);
assert_eq!(wal_manager.get_next_op_version().get(), 3);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
perms.set_mode(0o555); std::fs::set_permissions(dir.path(), perms).unwrap();
let result = wal_manager.append_op(b"op_data");
assert!(
matches!(result, Err(WalError::Io { operation: WalIoOperation::OpenSegmentWrite, .. })),
"want OpenSegmentWrite, got {result:?}",
);
let mut restore_perms = std::fs::metadata(dir.path()).unwrap().permissions();
restore_perms.set_mode(0o755);
std::fs::set_permissions(dir.path(), restore_perms).unwrap();
}
}
#[test]
fn checkpoint_succeeds_and_prunes_old_segments() {
let (mut wal_manager, _dir) = setup_wal_manager(2);
append_ops(&mut wal_manager, 5);
let checkpoint_segment_id = wal_manager.get_segment_id_for_previous_op();
assert_eq!(checkpoint_segment_id, 2);
let result = perform_checkpoint(&mut wal_manager, CheckpointReason::Explicit, false, None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), NonZeroU64::new(5));
let segments = wal_manager.storage.discover_segments().unwrap();
let segment_ids: Vec<_> = segments.iter().map(|s| s.id).collect();
assert_eq!(segment_ids, vec![2]);
}
#[test]
fn wal_manager_drop_is_safe_with_active_writer() {
{
let (mut wal_manager, _dir) = setup_wal_manager(5);
wal_manager.append_op(b"some data").unwrap();
} }
#[test]
fn replay_fails_on_key_decode_error() {
let (mut wal_manager, _dir) = setup_wal_manager(10);
let op_raw = WalOpRaw::Put {
key_bytes: vec![1],
hash: crate::types::BlobHash::from_bytes([0; 32]),
size: 1,
};
let op_data = serialize_wal_op_raw(&op_raw).unwrap();
wal_manager.append_op(&op_data).unwrap();
wal_manager.active_writer.take().unwrap().close().unwrap();
let result = wal_manager.replay_and_prepare::<TestKey>(None, |_| {});
assert!(matches!(result, Err(WalError::ReplayConvertWalOp { .. })));
}
#[test]
fn replay_fails_on_corrupted_op_entry() {
let (mut wal_manager, _dir) = setup_wal_manager(10);
let segment_path = wal_manager.storage.paths.wal_path_for_segment(0);
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&segment_path)
.unwrap();
let op_data = b"short data";
let incorrect_len = (op_data.len() + 10) as u32;
file.write_all(&1u64.to_le_bytes()).unwrap();
file.write_all(&[0u8; 32]).unwrap();
file.write_all(&incorrect_len.to_le_bytes()).unwrap();
file.write_all(op_data).unwrap();
file.sync_all().unwrap();
drop(file);
let result = wal_manager.replay_and_prepare::<TestKey>(None, |_| {});
assert!(
matches!(result, Err(WalError::ReplayIo { step: WalReplayIoStep::ReadOpData, .. })),
"want ReadOpData, got {result:?}",
);
}
#[test]
fn replay_should_ignore_segments_before_checkpoint() {
let (mut wal_manager, dir) = setup_wal_manager(2);
append_ops(&mut wal_manager, 4);
let checkpoint_version =
perform_checkpoint(&mut wal_manager, CheckpointReason::Explicit, false, None).unwrap();
assert_eq!(checkpoint_version, NonZeroU64::new(4));
append_ops(&mut wal_manager, 2);
let paths = DbPaths::new(dir.path().to_path_buf());
let mut new_wal_manager = WalManager::new(paths, NonZeroU64::new(2).unwrap()).unwrap();
let mut replayed_ops_count = 0;
let result = new_wal_manager.replay_and_prepare::<TestKey>(NonZeroU64::new(4), |_op| {
replayed_ops_count += 1;
tracing::debug!("Replaying op #{}", replayed_ops_count);
});
assert!(result.is_ok());
assert_eq!(replayed_ops_count, 2, "replayed wrong count");
assert_eq!(new_wal_manager.get_next_op_version().get(), 7);
}
#[test]
fn replay_on_completely_empty_directory() {
let (mut wal_manager, _dir) = setup_wal_manager(5);
let mut replayed_ops = Vec::new();
let result = wal_manager.replay_and_prepare::<TestKey>(None, |op| {
replayed_ops.push(op);
});
assert!(result.is_ok());
assert_eq!(replayed_ops.len(), 0);
assert_eq!(wal_manager.get_next_op_version().get(), 1);
}
#[test]
fn segment_reader_next_fails_on_checksum_mismatch() {
let dir = tempdir().unwrap();
let paths = DbPaths::new(dir.path().to_path_buf());
let segment_path = paths.wal_path_for_segment(0);
let mut file = std::fs::File::create(&segment_path).unwrap();
let op_data = b"some data";
file.write_all(&1u64.to_le_bytes()).unwrap(); file.write_all(&[0u8; 32]).unwrap(); file.write_all(&(op_data.len() as u32).to_le_bytes()).unwrap(); file.write_all(op_data).unwrap(); file.sync_all().unwrap();
drop(file);
let storage = SegmentStorage::new(paths);
let mut reader = storage.open_reader(0).unwrap();
let result = reader.next();
assert!(matches!(result, Some(Err(WalError::ReplayChecksumMismatch { .. }))));
}
#[test]
fn discover_segments_ignores_malformed_filenames() {
let dir = tempdir().unwrap();
let paths = DbPaths::new(dir.path().to_path_buf());
File::create(paths.wal_path_for_segment(0)).unwrap(); File::create(paths.wal_path_for_segment(2)).unwrap(); File::create(dir.path().join("abc_index.wal")).unwrap(); File::create(dir.path().join("1_index.wal.bak")).unwrap(); File::create(dir.path().join("checkpoint.meta")).unwrap();
let storage = SegmentStorage::new(paths);
let segments = storage.discover_segments().unwrap();
let [first, second] = segments.as_slice() else {
panic!("Expected two discovered segments");
};
assert_eq!(first.id, 0);
assert_eq!(second.id, 2);
}
#[test]
fn segment_id_calculation_is_correct() {
let dir = tempdir().unwrap();
let paths = DbPaths::new(dir.path().to_path_buf());
let wal = WalManager::new(paths, NonZeroU64::new(10).unwrap()).unwrap();
assert_eq!(wal.segment_id_for_op_version(1), 0);
assert_eq!(wal.segment_id_for_op_version(5), 0);
assert_eq!(wal.segment_id_for_op_version(10), 0);
assert_eq!(wal.segment_id_for_op_version(11), 1);
assert_eq!(wal.segment_id_for_op_version(20), 1);
assert_eq!(wal.segment_id_for_op_version(21), 2);
}
#[test]
#[should_panic]
fn zero_panics() {
let dir = tempdir().unwrap();
let paths = DbPaths::new(dir.path().to_path_buf());
let wal = WalManager::new(paths, NonZeroU64::new(10).unwrap()).unwrap();
wal.segment_id_for_op_version(0);
}
#[test]
fn segment_reader_stops_at_explicit_sentinel() {
let (mut wal_manager, _dir) = setup_wal_manager(10);
append_ops(&mut wal_manager, 3);
let writer = wal_manager.active_writer.take().unwrap();
let segment_id = writer.segment_id();
assert_eq!(segment_id, 0);
writer.seal().unwrap();
let mut reader = wal_manager.storage.open_reader(segment_id).unwrap();
assert!(reader.next().is_some()); assert!(reader.next().is_some()); assert!(reader.next().is_some());
assert!(reader.next().is_none(), "didn't stop at sentinel");
}