mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-20 08:16:04 +00:00
195 lines
6.0 KiB
Rust
195 lines
6.0 KiB
Rust
use proptest::prelude::*;
|
|
use std::path::Path;
|
|
|
|
use tranquil_store::{
|
|
FaultConfig, HEADER_SIZE, OpenOptions, Probability, ReadRecord, RecordReader, RecordWriter,
|
|
SimulatedIO, StorageIO, run_crash_test, run_pristine_comparison, sim_proptest_cases,
|
|
};
|
|
|
|
fn arb_payloads(max_count: usize, max_size: usize) -> BoxedStrategy<Vec<Vec<u8>>> {
|
|
proptest::collection::vec(
|
|
proptest::collection::vec(any::<u8>(), 0..max_size),
|
|
1..max_count,
|
|
)
|
|
.boxed()
|
|
}
|
|
|
|
fn sim_with_dir(seed: u64, config: FaultConfig) -> SimulatedIO {
|
|
let sim = SimulatedIO::new(seed, config);
|
|
sim.mkdir(Path::new("/test")).unwrap();
|
|
sim.sync_dir(Path::new("/test")).unwrap();
|
|
sim
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(sim_proptest_cases()))]
|
|
|
|
#[test]
|
|
fn synced_records_survive_crash(
|
|
seed in any::<u64>(),
|
|
payloads in arb_payloads(20, 256),
|
|
) {
|
|
let result = run_crash_test(
|
|
seed,
|
|
FaultConfig::none(),
|
|
&payloads,
|
|
5,
|
|
).unwrap();
|
|
|
|
prop_assert_eq!(result.records_recovered, result.records_synced);
|
|
prop_assert_eq!(result.corrupted_detected, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn recovered_never_exceeds_written(
|
|
seed in any::<u64>(),
|
|
payloads in arb_payloads(15, 128),
|
|
) {
|
|
let Ok(result) = run_crash_test(
|
|
seed,
|
|
FaultConfig::moderate(),
|
|
&payloads,
|
|
3,
|
|
) else { return Ok(()); };
|
|
|
|
prop_assert!(
|
|
result.records_recovered <= result.records_written,
|
|
"seed {}: recovered {} > written {}",
|
|
seed, result.records_recovered, result.records_written,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_phantom_records_after_crash(
|
|
seed in any::<u64>(),
|
|
payloads in arb_payloads(10, 512),
|
|
) {
|
|
let Ok(result) = run_crash_test(
|
|
seed,
|
|
FaultConfig::aggressive(),
|
|
&payloads,
|
|
0,
|
|
) else { return Ok(()); };
|
|
|
|
prop_assert!(
|
|
result.records_recovered <= result.records_synced,
|
|
"seed {}: recovered {} records but only {} were synced",
|
|
seed, result.records_recovered, result.records_synced,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pristine_prefix_holds_under_faults(
|
|
seed in any::<u64>(),
|
|
payloads in arb_payloads(12, 200),
|
|
) {
|
|
let Ok(result) = run_pristine_comparison(
|
|
seed,
|
|
FaultConfig::moderate(),
|
|
&payloads,
|
|
4,
|
|
) else { return Ok(()); };
|
|
|
|
prop_assert!(result.recovery_within_bounds);
|
|
}
|
|
|
|
#[test]
|
|
fn bit_flip_detected_by_u32_checksum(
|
|
seed in any::<u64>(),
|
|
payload in proptest::collection::vec(any::<u8>(), 8..1024),
|
|
flip_offset in any::<usize>(),
|
|
flip_bit in 0u8..8,
|
|
) {
|
|
let sim = sim_with_dir(seed, FaultConfig::none());
|
|
let fd = sim.open(Path::new("/test/bitflip.dat"), OpenOptions::read_write()).unwrap();
|
|
|
|
let mut writer = RecordWriter::new(&sim, fd).unwrap();
|
|
writer.append(&payload).unwrap();
|
|
writer.sync().unwrap();
|
|
|
|
let mut contents = sim.durable_contents(fd).unwrap();
|
|
let record_region = &mut contents[HEADER_SIZE..];
|
|
let idx = flip_offset % record_region.len();
|
|
record_region[idx] ^= 1 << flip_bit;
|
|
|
|
let sim2 = sim_with_dir(seed.wrapping_add(1), FaultConfig::none());
|
|
let fd2 = sim2.open(Path::new("/test/check.dat"), OpenOptions::read_write()).unwrap();
|
|
sim2.write_all_at(fd2, 0, &contents).unwrap();
|
|
sim2.sync(fd2).unwrap();
|
|
|
|
let mut reader = RecordReader::open(&sim2, fd2).unwrap();
|
|
match reader.next() {
|
|
Some(ReadRecord::Valid { payload: ref recovered, .. }) => {
|
|
prop_assert_eq!(recovered, &payload, "bit flip produced valid record with wrong data");
|
|
}
|
|
Some(ReadRecord::Corrupted { .. }) => {}
|
|
Some(ReadRecord::Truncated { .. }) => {}
|
|
None => {}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn aggressive_faults_many_seeds(
|
|
seed in 0u64..10_000,
|
|
) {
|
|
let payloads: Vec<Vec<u8>> = (0..5)
|
|
.map(|i| format!("aggressive-test-{i}-{seed}").into_bytes())
|
|
.collect();
|
|
let Ok(result) = run_crash_test(
|
|
seed,
|
|
FaultConfig::aggressive(),
|
|
&payloads,
|
|
2,
|
|
) else { return Ok(()); };
|
|
|
|
prop_assert!(result.records_recovered <= payloads.len());
|
|
}
|
|
|
|
#[test]
|
|
fn write_all_at_handles_partial_writes(
|
|
seed in any::<u64>(),
|
|
data in proptest::collection::vec(any::<u8>(), 64..4096),
|
|
) {
|
|
let config = FaultConfig {
|
|
partial_write_probability: Probability::new(0.5),
|
|
..FaultConfig::none()
|
|
};
|
|
let dir = Path::new("/test");
|
|
let path = Path::new("/test/partial.dat");
|
|
let sim = sim_with_dir(seed, config);
|
|
let fd = sim.open(path, OpenOptions::read_write()).unwrap();
|
|
sim.sync_dir(dir).unwrap();
|
|
|
|
sim.write_all_at(fd, 0, &data).unwrap();
|
|
sim.sync(fd).unwrap();
|
|
|
|
sim.crash();
|
|
|
|
let fd = sim.open(path, OpenOptions::read()).unwrap();
|
|
let mut buf = vec![0u8; data.len()];
|
|
sim.read_exact_at(fd, 0, &mut buf).unwrap();
|
|
prop_assert_eq!(&buf, &data, "write_all_at must produce complete writes");
|
|
}
|
|
|
|
#[test]
|
|
fn dir_sync_required_for_file_survival(
|
|
seed in any::<u64>(),
|
|
data in proptest::collection::vec(any::<u8>(), 1..256),
|
|
) {
|
|
let sim = SimulatedIO::pristine(seed);
|
|
sim.mkdir(Path::new("/ephemeral")).unwrap();
|
|
|
|
let fd = sim.open(
|
|
Path::new("/ephemeral/file.dat"),
|
|
OpenOptions::read_write(),
|
|
).unwrap();
|
|
sim.write_all_at(fd, 0, &data).unwrap();
|
|
sim.sync(fd).unwrap();
|
|
|
|
sim.crash();
|
|
|
|
let result = sim.open(Path::new("/ephemeral/file.dat"), OpenOptions::read());
|
|
prop_assert!(result.is_err(), "file must vanish without dir sync");
|
|
}
|
|
}
|