#![cfg(not(target_arch = "wasm32"))]
#![allow(dead_code)]
#![allow(unused_imports)]
pub mod artifacts;
pub mod assertions;
pub mod consistency;
#[cfg(feature = "test-hooks")]
pub mod fault_injection;
pub mod fixtures;
pub mod harness;
pub mod lane;
pub mod metrics;
pub mod replay;
pub mod tracing_setup;
pub mod v06_baseline;
pub mod watchdog;
pub mod workload;
pub use artifacts::*;
pub use assertions::*;
pub use consistency::*;
#[cfg(feature = "test-hooks")]
pub use fault_injection::*;
pub use fixtures::*;
pub use harness::*;
pub use lane::*;
pub use metrics::*;
pub use replay::*;
pub use tracing_setup::*;
pub use v06_baseline::*;
pub use watchdog::*;
pub use workload::*;
use alopex_core::error::{Error, Result};
use alopex_core::kv::{KVStore, KVTransaction};
use alopex_core::lsm::wal::{WalSectionHeader, WAL_SECTION_HEADER_SIZE, WAL_SEGMENT_HEADER_SIZE};
use alopex_core::lsm::{LsmKV, LsmKVConfig, RecoveryResult};
use alopex_core::types::TxnMode;
use std::fs::{self, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub struct CrashSimulator {
path: PathBuf,
config: LsmKVConfig,
}
impl CrashSimulator {
pub fn new(path: impl AsRef<Path>) -> Self {
let path = path.as_ref().to_path_buf();
let _ = fs::create_dir_all(&path);
Self {
path,
config: LsmKVConfig::default(),
}
}
pub fn config_mut(&mut self) -> &mut LsmKVConfig {
&mut self.config
}
pub fn open_store(&self) -> Result<(LsmKV, RecoveryResult)> {
LsmKV::open_with_config(&self.path, self.config.clone())
}
pub fn crash_after_writes(&self, writes: usize) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
let (store, _) = self.open_store()?;
let mut out = Vec::with_capacity(writes);
for i in 0..writes {
let key = format!("key-{i}").into_bytes();
let value = format!("value-{i}").into_bytes();
let mut tx = store.begin(TxnMode::ReadWrite)?;
tx.put(key.clone(), value.clone())?;
tx.commit_self()?;
out.push((key, value));
}
drop(store);
Ok(out)
}
pub fn truncate_wal(&self, bytes: u64) -> Result<()> {
let wal_path = self.wal_path();
let mut file = OpenOptions::new().read(true).write(true).open(&wal_path)?;
let mut header_bytes = [0u8; WAL_SECTION_HEADER_SIZE];
file.read_exact(&mut header_bytes)?;
let section = WalSectionHeader::from_bytes(&header_bytes)?;
let max_segments = self.config.wal.max_segments as u64;
if max_segments == 0 {
return Err(Error::InvalidFormat("max_segments must be >= 1".into()));
}
let segment_size = self.config.wal.segment_size as u64;
let segment_header = WAL_SEGMENT_HEADER_SIZE as u64;
if segment_size <= segment_header {
return Err(Error::InvalidFormat("segment size too small".into()));
}
let segment_data_len = segment_size - segment_header;
let ring_len = segment_data_len
.checked_mul(max_segments)
.ok_or_else(|| Error::InvalidFormat("ring length overflow".into()))?;
let mut remaining = bytes.min(ring_len);
let mut logical = if section.end_offset >= remaining {
section.end_offset - remaining
} else {
ring_len - (remaining - section.end_offset)
};
while remaining > 0 {
let segment_index = logical / segment_data_len;
let offset_in_segment = logical % segment_data_len;
let remaining_in_segment = segment_data_len - offset_in_segment;
let chunk = remaining.min(remaining_in_segment);
let phys = (WAL_SECTION_HEADER_SIZE as u64)
+ (segment_index * segment_size)
+ segment_header
+ offset_in_segment;
file.seek(SeekFrom::Start(phys))?;
let zeros = vec![0u8; chunk as usize];
file.write_all(&zeros)?;
logical = (logical + chunk) % ring_len;
remaining -= chunk;
}
file.sync_data()?;
Ok(())
}
pub fn corrupt_wal_byte(&self, offset: u64) -> Result<()> {
let wal_path = self.wal_path();
let mut file = OpenOptions::new().read(true).write(true).open(&wal_path)?;
file.seek(SeekFrom::Start(offset))?;
let mut buf = [0u8; 1];
file.read_exact(&mut buf)?;
buf[0] ^= 0xFF;
file.seek(SeekFrom::Start(offset))?;
file.write_all(&buf)?;
file.sync_data()?;
Ok(())
}
pub fn recover_and_verify<F>(&self, verifier: F) -> Result<RecoveryResult>
where
F: FnOnce(&LsmKV, &RecoveryResult) -> Result<()>,
{
let (store, recovery) = self.open_store()?;
verifier(&store, &recovery)?;
Ok(recovery)
}
fn wal_path(&self) -> PathBuf {
self.path.join("lsm.wal")
}
}