use super::invariants::{ModelState, SimEvent};
use super::Sim;
use crate::store::fault::InjectionPoint;
use std::path::Path;
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fold(digest: u64, token: u64) -> u64 {
let mut d = digest;
for byte in token.to_le_bytes() {
d ^= u64::from(byte);
d = d.wrapping_mul(FNV_PRIME);
}
d
}
fn chain_hash(prev: u64, seq: u64, payload: u64) -> u64 {
fold(fold(fold(FNV_OFFSET, prev), seq), payload)
}
fn usize_token(n: usize) -> u64 {
u64::try_from(n).unwrap_or(u64::MAX)
}
pub(crate) fn run(sim: &Sim, steps: usize) -> Result<u64, String> {
let mut wl = fastrand::Rng::with_seed(sim.seed);
let mut model = ModelState::default();
let mut digest = FNV_OFFSET;
let mut next_seq = 0u64;
let seg = Path::new("sim-seg.fbat");
digest = fold(digest, sim.seed);
for step in 0..steps {
let prev_frontier = model.visible_frontier;
let dt = i64::from(wl.u16(..)) + 1;
let now = sim.clock.advance_us(dt);
digest = fold(digest, now.cast_unsigned());
match wl.u32(..) % 6 {
0..=2 => {
let payload = wl.u64(..);
let prev = model.chain_head();
let hash = chain_hash(prev, next_seq, payload);
let point = InjectionPoint::SingleAppendStart {
entity: "sim".to_string(),
};
let landed = sim.fs.write_bytes(seg, &point, &payload.to_le_bytes());
let full = landed == 8;
let synced = full
&& sim.fs.fsync(
seg,
&InjectionPoint::SingleAppendWritten {
entity: "sim".to_string(),
},
);
model.append(SimEvent {
seq: next_seq,
prev,
hash,
durable: synced,
});
digest = fold(fold(digest, next_seq), hash);
next_seq += 1;
}
3 => {
let honored = sim
.fs
.fsync(seg, &InjectionPoint::BatchFsync { batch_id: next_seq });
digest = fold(digest, u64::from(honored));
}
4 => {
let token = wl.u64(..);
let handle = sim.scheduler.spawn_owned(Box::new(move || {
std::hint::black_box(token);
}));
let joined = handle.join().is_ok();
digest = fold(fold(digest, token), u64::from(joined));
}
_ => {
let pre: Vec<SimEvent> = model.log.clone();
let durable_bytes = sim.fs.durable_len(seg);
let live_bytes = sim.fs.len(seg);
let recovered_bytes = sim
.fs
.read_bytes(
seg,
&InjectionPoint::ColdStartScanFrame {
segment_id: 0,
frame_index: step,
},
0,
durable_bytes,
)
.len();
digest = fold(
fold(digest, usize_token(live_bytes)),
usize_token(recovered_bytes),
);
sim.fs.crash();
let recovered: Vec<SimEvent> = pre.iter().copied().filter(|e| e.durable).collect();
let mut rebuilt = ModelState::default();
for ev in &recovered {
rebuilt.append(*ev);
}
rebuilt
.check_no_loss(&pre)
.map_err(|v| format!("no-loss violation (seed={}): {v:?}", sim.seed))?;
model = rebuilt;
digest = fold(digest, usize_token(durable_bytes));
}
}
model.check(prev_frontier).map_err(|v| {
format!(
"invariant violation at step {step} (seed={}): {v:?}",
sim.seed
)
})?;
digest = fold(digest, model.visible_frontier);
}
sim.scheduler.run_all();
Ok(digest)
}
#[cfg(test)]
mod tests {
use super::super::Sim;
use super::usize_token;
#[test]
fn usize_token_widens_value_losslessly() {
assert_eq!(
usize_token(5),
5,
"PROPERTY: usize_token widens the length value, it does not zero it"
);
assert_eq!(
usize_token(0),
0,
"PROPERTY: zero widens to zero (boundary)"
);
assert_eq!(
usize_token(usize::MAX),
u64::try_from(usize::MAX).unwrap_or(u64::MAX),
"PROPERTY: a 64-bit-or-narrower platform widens usize::MAX without truncation"
);
}
#[test]
fn workload_digest_is_stable_across_runs() {
let a = Sim::new(0xABCD).run_workload(128).expect("invariants hold");
let b = Sim::new(0xABCD).run_workload(128).expect("invariants hold");
assert_eq!(
a, b,
"PROPERTY: a seeded workload yields a byte-stable op-trace digest"
);
}
#[test]
fn step_count_affects_digest() {
let short = Sim::new(5).run_workload(8).expect("invariants hold");
let long = Sim::new(5).run_workload(64).expect("invariants hold");
assert_ne!(
short, long,
"PROPERTY: more steps fold more ops into the digest"
);
}
#[test]
fn append_seq_increment_pins_digest() {
let digest = Sim::new(0xABCD).run_workload(128).expect("invariants hold");
assert_eq!(
digest, 0x1E78_4376_CC78_14B4,
"PROPERTY: the seeded 128-step workload folds the strictly \
incrementing append sequence (next_seq += 1, not *= 1) into a \
byte-stable op-trace digest"
);
}
}