use std::collections::HashMap;
use proptest::collection::vec;
use proptest::prelude::*;
use holt::{Tree, TreeConfig};
#[derive(Debug, Clone)]
enum Op {
Put(Vec<u8>, Vec<u8>),
Delete(Vec<u8>),
Rename(Vec<u8>, Vec<u8>, bool),
}
fn key_strategy() -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(prop::sample::select(vec![b'a', b'b', b'/', b'0']), 1..=8)
}
fn value_strategy() -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(any::<u8>(), 0..=64)
}
fn op_strategy() -> impl Strategy<Value = Op> {
prop_oneof![
5 => (key_strategy(), value_strategy()).prop_map(|(k, v)| Op::Put(k, v)),
3 => key_strategy().prop_map(Op::Delete),
1 => (key_strategy(), key_strategy()).prop_map(|(s, d)| Op::Rename(s, d, false)),
1 => (key_strategy(), key_strategy()).prop_map(|(s, d)| Op::Rename(s, d, true)),
]
}
fn apply(tree: &Tree, ops: &[Op]) -> HashMap<Vec<u8>, Vec<u8>> {
let mut oracle: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
for op in ops {
match op {
Op::Put(k, v) => {
tree.put(k, v).unwrap();
oracle.insert(k.clone(), v.clone());
}
Op::Delete(k) => {
let prev = tree.delete(k).unwrap();
assert_eq!(prev.as_deref(), oracle.get(k).map(|v| v.as_slice()));
oracle.remove(k);
}
Op::Rename(s, d, force) => {
let src_present = oracle.contains_key(s);
let dst_present = oracle.contains_key(d);
let result = tree.rename(s, d, *force);
match result {
Ok(()) => {
assert!(src_present, "rename Ok but oracle had no src");
if s == d {
} else {
assert!(*force || !dst_present);
let v = oracle.remove(s).unwrap();
oracle.insert(d.clone(), v);
}
}
Err(holt::Error::NotFound) => {
assert!(!src_present);
}
Err(holt::Error::DstExists) => {
assert!(src_present && dst_present && !force && s != d);
}
Err(e) => panic!("unexpected rename error: {e:?}"),
}
}
}
}
oracle
}
fn check(tree: &Tree, oracle: &HashMap<Vec<u8>, Vec<u8>>) {
for (k, v) in oracle {
let got = tree.get(k).unwrap();
assert_eq!(
got.as_deref(),
Some(v.as_slice()),
"tree.get({k:?}) returned {got:?}, oracle had {v:?}",
);
}
let absent: &[u8] = b"_PROPTEST_SENTINEL_ABSENT_/zzz";
if !oracle.contains_key(absent) {
assert_eq!(tree.get(absent).unwrap(), None);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 64,
// Mutation traces with many key collisions catch the most
// bugs; deeper sequences also exercise the Prefix split /
// collapse paths. Cap at 200 ops to keep total runtime
// reasonable in CI.
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn memory_round_trips_against_oracle(
ops in vec(op_strategy(), 1..=200),
) {
let tree = Tree::open(TreeConfig::memory()).unwrap();
let oracle = apply(&tree, &ops);
check(&tree, &oracle);
}
#[test]
fn persistent_round_trips_via_wal_replay(
ops in vec(op_strategy(), 1..=100),
) {
let dir = tempfile::tempdir().unwrap();
let mut cfg = TreeConfig::new(dir.path());
cfg.wal_sync_on_commit = true;
let oracle = {
let tree = Tree::open(cfg.clone()).unwrap();
apply(&tree, &ops)
};
let tree = Tree::open(cfg).unwrap();
check(&tree, &oracle);
}
}