#![expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "test code")]
use super::*;
use crate::fs::StdFs;
use test_log::test;
#[test]
fn write_then_read_roundtrips_the_bound() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
write(&StdFs, &sst, None, 7, b"k00130", SyncMode::Normal).unwrap();
match read(&StdFs, &sst, None).unwrap() {
SidecarRead::Present(id, bound) => {
assert_eq!(id, 7);
assert_eq!(bound, b"k00130");
}
_ => panic!("expected Present"),
}
}
#[test]
fn write_then_read_roundtrips_a_foreign_table_id() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
let foreign_id = 0xDEAD_BEEF_u64;
write(&StdFs, &sst, None, foreign_id, b"k00130", SyncMode::Normal).unwrap();
match read(&StdFs, &sst, None).unwrap() {
SidecarRead::Present(id, bound) => {
assert_eq!(
id, foreign_id,
"the stored id round-trips, not the file name"
);
assert_eq!(bound, b"k00130");
}
_ => panic!("expected Present"),
}
}
#[test]
fn absent_sidecar_reads_missing() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Missing
));
}
#[test]
fn corrupted_payload_reads_corrupt() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
write(&StdFs, &sst, None, 7, b"k00130", SyncMode::Normal).unwrap();
let path = sidecar_path(&sst);
let mut bytes = std::fs::read(&path).unwrap();
let mid = bytes.len() / 2;
bytes[mid] ^= 0xFF;
std::fs::write(&path, &bytes).unwrap();
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Corrupt
));
}
#[test]
fn truncated_sidecar_reads_corrupt() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
write(&StdFs, &sst, None, 7, b"k00130", SyncMode::Normal).unwrap();
let path = sidecar_path(&sst);
std::fs::write(&path, b"short").unwrap();
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Corrupt
));
}
#[test]
fn oversized_sidecar_reads_corrupt() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
write(&StdFs, &sst, None, 7, b"k00130", SyncMode::Normal).unwrap();
let over = HEADER_LEN + u16::MAX as usize + CHECKSUM_LEN + 1;
std::fs::write(sidecar_path(&sst), vec![0u8; over]).unwrap();
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Corrupt
));
}
#[test]
fn remove_deletes_the_sidecar() {
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
write(&StdFs, &sst, None, 7, b"k00130", SyncMode::Normal).unwrap();
assert!(exists(&StdFs, &sst).unwrap());
remove(&StdFs, &sst, SyncMode::Normal);
assert!(!exists(&StdFs, &sst).unwrap());
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Missing
));
}
#[test]
fn failed_publish_cleans_up_the_temp() {
use crate::fs::{Fault, FaultFs, FaultOp, FaultRule};
use crate::io::ErrorKind;
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
let fault = FaultFs::new(StdFs);
fault.injector().arm(
FaultRule::new(FaultOp::Rename, Fault::Error(ErrorKind::Other)).on_path("restrict-bound"),
);
assert!(write(&fault, &sst, None, 7, b"k00130", SyncMode::Normal).is_err());
assert!(
!sidecar_tmp_path(&sst).exists(),
"the synced temp must be cleaned up after a failed publish",
);
assert!(matches!(
read(&StdFs, &sst, None).unwrap(),
SidecarRead::Missing
));
}
#[cfg(feature = "encryption")]
#[test]
fn encrypted_sidecar_roundtrips_and_rejects_a_wrong_key() {
use crate::encryption::{Aes256GcmProvider, EncryptionProvider};
use std::sync::Arc;
let dir = tempfile::tempdir().unwrap();
let sst = dir.path().join("7");
let provider: Arc<dyn EncryptionProvider> = Arc::new(Aes256GcmProvider::new(&[3u8; 32]));
write(
&StdFs,
&sst,
Some(provider.as_ref()),
7,
b"k00130",
SyncMode::Normal,
)
.unwrap();
match read(&StdFs, &sst, Some(provider.as_ref())).unwrap() {
SidecarRead::Present(id, bound) => {
assert_eq!(id, 7);
assert_eq!(bound, b"k00130");
}
_ => panic!("expected Present with the right key"),
}
let wrong: Arc<dyn EncryptionProvider> = Arc::new(Aes256GcmProvider::new(&[9u8; 32]));
assert!(matches!(
read(&StdFs, &sst, Some(wrong.as_ref())).unwrap(),
SidecarRead::Corrupt,
));
}
fn batch_fixture() -> (crate::fs::FaultFs<crate::fs::MemFs>, Vec<PathBuf>) {
let fs = crate::fs::FaultFs::new(crate::fs::MemFs::new());
fs.create_dir_all(Path::new("/db/a")).unwrap();
fs.create_dir_all(Path::new("/db/b")).unwrap();
let paths = ["/db/a/1", "/db/a/2", "/db/a/3", "/db/b/4"]
.iter()
.map(PathBuf::from)
.collect();
(fs, paths)
}
fn batch<'a>(fs: &'a dyn Fs, paths: &'a [PathBuf], bounds: &'a [Vec<u8>]) -> Vec<SidecarWrite<'a>> {
paths
.iter()
.zip(bounds)
.zip(1u64..)
.map(|((table_path, bound), table_id)| SidecarWrite {
fs,
table_path,
encryption: None,
table_id,
bound,
})
.collect()
}
#[test]
fn write_many_syncs_each_folder_once() {
let (fs, paths) = batch_fixture();
let bounds: Vec<Vec<u8>> = (1..=4).map(|i| format!("k{i}").into_bytes()).collect();
drop(fs.injector().take_syncs());
let results = write_many(&batch(&fs, &paths, &bounds), SyncMode::Normal);
assert!(results.iter().all(Result::is_ok), "every sidecar published");
for ((path, bound), id) in paths.iter().zip(&bounds).zip(1u64..) {
match read(&fs, path, None).unwrap() {
SidecarRead::Present(read_id, read_bound) => {
assert_eq!((id, bound), (read_id, &read_bound), "{}", path.display());
}
_ => panic!("{} has no sidecar", path.display()),
}
}
let folders: Vec<PathBuf> = fs
.injector()
.take_syncs()
.into_iter()
.filter(|(_, kind)| matches!(kind, crate::fs::SyncKind::Directory))
.map(|(path, _)| path)
.collect();
assert_eq!(
vec![PathBuf::from("/db/a"), PathBuf::from("/db/b")],
folders,
"one sync per folder",
);
}
#[test]
fn write_many_fails_every_sidecar_of_a_folder_whose_sync_fails() {
use crate::fs::{Fault, FaultOp, FaultRule};
let (fs, paths) = batch_fixture();
let bounds: Vec<Vec<u8>> = (1..=4).map(|i| format!("k{i}").into_bytes()).collect();
fs.injector().arm(
FaultRule::new(
FaultOp::SyncDirectory,
Fault::Error(crate::io::ErrorKind::Other),
)
.on_path("/db/a")
.once(),
);
let results = write_many(&batch(&fs, &paths, &bounds), SyncMode::Normal);
let ok: Vec<bool> = results.iter().map(Result::is_ok).collect();
assert_eq!(
vec![false, false, false, true],
ok,
"the failed folder's three fail"
);
for path in &paths {
assert!(
!fs.exists(&sidecar_tmp_path(path)).unwrap(),
"no temp left beside {}",
path.display(),
);
}
}
#[test]
fn write_many_fails_only_the_sidecar_that_cannot_be_written() {
use crate::fs::{Fault, FaultOp, FaultRule};
let (fs, paths) = batch_fixture();
let mut bounds: Vec<Vec<u8>> = (1..=4).map(|i| format!("k{i}").into_bytes()).collect();
bounds[3] = vec![b'x'; usize::from(u16::MAX) + 1];
fs.injector().arm(
FaultRule::new(FaultOp::Open, Fault::Error(crate::io::ErrorKind::Other))
.on_path("2.restrict-bound.tmp")
.once(),
);
let results = write_many(&batch(&fs, &paths, &bounds), SyncMode::Normal);
let ok: Vec<bool> = results.iter().map(Result::is_ok).collect();
assert_eq!(vec![true, false, true, false], ok);
assert!(matches!(
read(&fs, &paths[1], None).unwrap(),
SidecarRead::Missing
));
assert!(matches!(
read(&fs, &paths[3], None).unwrap(),
SidecarRead::Missing
));
assert!(matches!(
read(&fs, &paths[2], None).unwrap(),
SidecarRead::Present(3, _)
));
}