use std::collections::HashMap;
use proptest::collection::vec;
use proptest::prelude::*;
use holt::{RangeEntry, Tree, TreeConfig};
fn collect_kv(
iter: impl IntoIterator<Item = Result<RangeEntry, holt::Error>>,
) -> Vec<(Vec<u8>, Vec<u8>)> {
iter.into_iter()
.filter_map(|r| match r.unwrap() {
RangeEntry::Key { key, value, .. } => Some((key, value)),
RangeEntry::CommonPrefix(_) => None,
_ => panic!("RangeEntry got a new variant"),
})
.collect()
}
#[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 existed = tree.delete(k).unwrap();
assert_eq!(existed, oracle.contains_key(k));
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
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OracleAtomicErr {
NotFound,
DstExists,
}
fn apply_atomic_to_oracle(
oracle: &mut HashMap<Vec<u8>, Vec<u8>>,
ops: &[Op],
) -> std::result::Result<(), OracleAtomicErr> {
let mut staged = oracle.clone();
for op in ops {
match op {
Op::Put(k, v) => {
staged.insert(k.clone(), v.clone());
}
Op::Delete(k) => {
staged.remove(k);
}
Op::Rename(s, d, force) => {
let Some(v) = staged.get(s).cloned() else {
return Err(OracleAtomicErr::NotFound);
};
if s == d {
continue;
}
if !*force && staged.contains_key(d) {
return Err(OracleAtomicErr::DstExists);
}
staged.remove(s);
staged.insert(d.clone(), v);
}
}
}
*oracle = staged;
Ok(())
}
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 = true;
let oracle = {
let tree = Tree::open(cfg.clone()).unwrap();
apply(&tree, &ops)
};
let tree = Tree::open(cfg).unwrap();
check(&tree, &oracle);
}
#[test]
fn batch_round_trips_against_oracle(
batches in vec(vec(op_strategy(), 1..=8), 1..=30),
) {
let tree = Tree::open(TreeConfig::memory()).unwrap();
let mut oracle: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
for batch in &batches {
let expected = apply_atomic_to_oracle(&mut oracle, batch);
let got = tree.atomic(|tx| {
for op in batch {
match op {
Op::Put(k, v) => tx.put(k, v),
Op::Delete(k) => tx.delete(k),
Op::Rename(s, d, force) => tx.rename(s, d, *force),
}
}
});
match (got, expected) {
(Ok(true), Ok(())) => {}
(Err(holt::Error::NotFound), Err(OracleAtomicErr::NotFound)) => {}
(Err(holt::Error::DstExists), Err(OracleAtomicErr::DstExists)) => {}
(other, expected) => {
prop_assert!(
false,
"atomic result mismatch: tree={other:?}, oracle={expected:?}",
);
}
}
check(&tree, &oracle);
}
}
#[test]
fn range_iteration_matches_oracle(
ops in vec(op_strategy(), 1..=200),
) {
use std::collections::BTreeMap;
let tree = Tree::open(TreeConfig::memory()).unwrap();
let oracle_map = apply(&tree, &ops);
let oracle: BTreeMap<Vec<u8>, Vec<u8>> = oracle_map.into_iter().collect();
let actual = collect_kv(tree.range());
let expected: Vec<(Vec<u8>, Vec<u8>)> = oracle
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
prop_assert_eq!(&actual, &expected);
}
#[test]
fn scan_matches_oracle(
ops in vec(op_strategy(), 1..=100),
prefix in prop::collection::vec(prop::sample::select(vec![b'a', b'b', b'/', b'0']), 0..=3),
) {
use std::collections::BTreeMap;
let tree = Tree::open(TreeConfig::memory()).unwrap();
let oracle_map = apply(&tree, &ops);
let oracle: BTreeMap<Vec<u8>, Vec<u8>> = oracle_map.into_iter().collect();
let actual = collect_kv(tree.scan(&prefix));
let expected: Vec<(Vec<u8>, Vec<u8>)> = oracle
.iter()
.filter(|(k, _)| k.starts_with(&prefix))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
prop_assert_eq!(&actual, &expected);
}
}