use std::io;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use wal_db::{MemStore, Wal, WalError, WalStore};
struct FaultyStore {
inner: MemStore,
writes: AtomicUsize,
fail_write_on: AtomicUsize,
fail_sync: AtomicBool,
}
impl FaultyStore {
fn new() -> Self {
FaultyStore {
inner: MemStore::new(),
writes: AtomicUsize::new(0),
fail_write_on: AtomicUsize::new(usize::MAX),
fail_sync: AtomicBool::new(false),
}
}
fn fail_write_number(self, n: usize) -> Self {
self.fail_write_on.store(n, Ordering::SeqCst);
self
}
fn failing_sync(self) -> Self {
self.fail_sync.store(true, Ordering::SeqCst);
self
}
}
impl WalStore for FaultyStore {
fn write_at(&self, offset: u64, bytes: &[u8]) -> wal_db::Result<()> {
let n = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
if n >= self.fail_write_on.load(Ordering::SeqCst) {
return Err(io::Error::other("injected: disk full").into());
}
self.inner.write_at(offset, bytes)
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> wal_db::Result<usize> {
self.inner.read_at(offset, buf)
}
fn truncate(&self, len: u64) -> wal_db::Result<()> {
self.inner.truncate(len)
}
fn sync(&self) -> wal_db::Result<()> {
if self.fail_sync.load(Ordering::SeqCst) {
return Err(io::Error::other("injected: fsync failed").into());
}
self.inner.sync()
}
fn len(&self) -> wal_db::Result<u64> {
self.inner.len()
}
}
#[test]
fn disk_full_on_append_returns_error_and_keeps_earlier_records() {
let store = FaultyStore::new().fail_write_number(4);
let wal = Wal::with_store(store).unwrap();
let _ = wal.append(b"one").unwrap();
let _ = wal.append(b"two").unwrap();
let _ = wal.append(b"three").unwrap();
let err = wal.append(b"four").unwrap_err();
assert!(
matches!(err, WalError::Io { .. }),
"disk-full should be an Io error, got {err:?}"
);
let got: Vec<Vec<u8>> = wal
.iter()
.unwrap()
.map(|e| e.unwrap().into_data())
.collect();
assert_eq!(
got,
vec![b"one".to_vec(), b"two".to_vec(), b"three".to_vec()]
);
}
#[test]
fn sync_after_a_failed_write_reports_the_truncation() {
let store = FaultyStore::new().fail_write_number(2);
let wal = Wal::with_store(store).unwrap();
let _ = wal.append(b"good").unwrap();
let _ = wal.append(b"doomed").unwrap_err();
let err = wal.sync().unwrap_err();
assert!(
matches!(err, WalError::Corruption { .. }),
"sync past a failed write should report corruption, got {err:?}"
);
assert_eq!(wal.iter().unwrap().count(), 1);
}
#[test]
fn fsync_failure_surfaces_as_an_error() {
let store = FaultyStore::new().failing_sync();
let wal = Wal::with_store(store).unwrap();
let _ = wal.append(b"record").unwrap();
let err = wal.sync().unwrap_err();
assert!(
matches!(err, WalError::Io { .. }),
"fsync failure should be an Io error, got {err:?}"
);
}
#[test]
fn append_and_sync_propagates_a_write_failure() {
let store = FaultyStore::new().fail_write_number(1); let wal = Wal::with_store(store).unwrap();
let err = wal.append_and_sync(b"never lands").unwrap_err();
assert!(matches!(err, WalError::Io { .. }), "got {err:?}");
assert_eq!(wal.iter().unwrap().count(), 0);
}