use candystore::{CandyStore, Config};
use proptest::prelude::*;
use std::collections::BTreeMap;
use tempfile::TempDir;
#[derive(Debug, Clone)]
enum Op {
Set(String, String),
Get(String),
Remove(String),
CleanShutdown,
SimulateCrash,
}
fn op_strategy() -> impl Strategy<Value = Op> {
let key_strat = "[a-d]{1,2}";
let val_strat = "[a-zA-Z0-9]{0,50}";
prop_oneof![
40 => (key_strat, val_strat).prop_map(|(k, v)| Op::Set(k, v)),
40 => key_strat.prop_map(Op::Get),
20 => key_strat.prop_map(Op::Remove),
8 => Just(Op::CleanShutdown),
2 => Just(Op::SimulateCrash),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn test_candystore_state_machine(ops in proptest::collection::vec(op_strategy(), 1..2000)) {
let dir = TempDir::new().unwrap();
let config = Config {
max_data_file_size: 1024 * 4, ..Default::default()
};
let mut oracle = BTreeMap::new();
let mut db_opt = Some(CandyStore::open(dir.path(), config).unwrap());
for (op_idx, op) in ops.iter().enumerate() {
match op {
Op::Set(k, v) => {
oracle.insert(k.clone(), v.clone());
let db = db_opt.as_ref().unwrap();
let _ = db
.set(k.as_bytes(), v.as_bytes())
.unwrap_or_else(|err| panic!("set failed at op {op_idx}: {op:?}: {err}"));
}
Op::Get(k) => {
let db = db_opt.as_ref().unwrap();
let expected = oracle.get(k);
let actual = db
.get(k.as_bytes())
.unwrap_or_else(|err| panic!("get failed at op {op_idx}: {op:?}: {err}"));
match expected {
Some(v) => assert_eq!(
Some(v.as_bytes()),
actual.as_deref(),
"get mismatch at op {op_idx}: {op:?}"
),
None => assert_eq!(None, actual, "get mismatch at op {op_idx}: {op:?}"),
}
}
Op::Remove(k) => {
oracle.remove(k);
let db = db_opt.as_ref().unwrap();
let _ = db
.remove(k.as_bytes())
.unwrap_or_else(|err| panic!("remove failed at op {op_idx}: {op:?}: {err}"));
}
Op::CleanShutdown => {
drop(db_opt.take().unwrap());
db_opt = Some(
CandyStore::open(dir.path(), config)
.unwrap_or_else(|err| panic!("reopen after clean shutdown failed at op {op_idx}: {op:?}: {err}")),
);
}
Op::SimulateCrash => {
db_opt.take().unwrap()._abort_for_testing();
db_opt = Some(
CandyStore::open(dir.path(), config)
.unwrap_or_else(|err| panic!("reopen after simulated crash failed at op {op_idx}: {op:?}: {err}")),
);
}
}
}
let db = db_opt.as_ref().unwrap();
for (k, v) in oracle.iter() {
let actual = db
.get(k.as_bytes())
.unwrap_or_else(|err| panic!("final get failed for key {k:?}: {err}"))
.unwrap_or_else(|| panic!("final verification missing key {k:?}"));
assert_eq!(v.as_bytes(), actual.as_slice(), "final verification mismatch for key {k:?}");
}
}
}