use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use bison_db::{Db, DocId, Document, Value};
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed | 1)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: u64) -> u64 {
self.next_u64() % n
}
}
fn temp_path() -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bison_db_stress_{pid}_{n}.bison"));
let _ = std::fs::remove_file(&path);
path
}
const GROUPS: i64 = 8;
fn make_doc(rng: &mut Rng, group: i64) -> Document {
let mut d = Document::new();
d.set("group", group)
.set("payload", rng.next_u64() as i64)
.set("tag", "stress");
d
}
fn verify(db: &Db, model: &HashMap<u64, Document>) {
assert_eq!(db.len(), model.len(), "live count diverged from model");
for (raw, expected) in model {
let got = db.get(DocId::from(*raw)).unwrap();
assert_eq!(
got.as_ref(),
Some(expected),
"doc {raw} diverged from model"
);
}
for raw in [u64::MAX, u64::MAX - 1, 10_000_000] {
if !model.contains_key(&raw) {
assert!(db.get(DocId::from(raw)).unwrap().is_none());
}
}
}
fn verify_index(db: &Db, model: &HashMap<u64, Document>) {
for group in 0..GROUPS {
let mut expected: Vec<u64> = model
.iter()
.filter(|(_, doc)| doc.get("group").and_then(Value::as_int) == Some(group))
.map(|(raw, _)| *raw)
.collect();
expected.sort_unstable();
let mut got: Vec<u64> = db
.find("group", &Value::from(group))
.unwrap()
.into_iter()
.map(DocId::get)
.collect();
got.sort_unstable();
assert_eq!(got, expected, "index for group {group} diverged from model");
}
}
#[test]
fn test_stress_mixed_operations_match_model() {
let path = temp_path();
let mut rng = Rng::new(0x5EED_1234_ABCD_0001);
let mut db = Db::open(&path).unwrap();
db.create_index("group").unwrap();
let mut model: HashMap<u64, Document> = HashMap::new();
let mut live: Vec<u64> = Vec::new();
for step in 0..4_000_u64 {
match rng.below(100) {
0..=44 => {
let group = (rng.below(GROUPS as u64)) as i64;
let doc = make_doc(&mut rng, group);
let id = db.insert(doc.clone()).unwrap();
let _ = model.insert(id.get(), doc);
live.push(id.get());
}
45..=69 if !live.is_empty() => {
let idx = (rng.below(live.len() as u64)) as usize;
let raw = live[idx];
let group = (rng.below(GROUPS as u64)) as i64;
let doc = make_doc(&mut rng, group);
assert!(db.update(DocId::from(raw), doc.clone()).unwrap());
let _ = model.insert(raw, doc);
}
70..=89 if !live.is_empty() => {
let idx = (rng.below(live.len() as u64)) as usize;
let raw = live.swap_remove(idx);
assert!(db.delete(DocId::from(raw)).unwrap());
let _ = model.remove(&raw);
}
90..=92 => {
db.compact().unwrap();
verify(&db, &model);
verify_index(&db, &model);
}
93..=95 => {
db.flush().unwrap();
drop(db);
db = Db::open(&path).unwrap();
db.create_index("group").unwrap();
verify(&db, &model);
}
_ => {
if !live.is_empty() {
let raw = live[(rng.below(live.len() as u64)) as usize];
let got = db.get(DocId::from(raw)).unwrap();
assert_eq!(got.as_ref(), model.get(&raw));
}
}
}
if step % 500 == 0 {
verify(&db, &model);
verify_index(&db, &model);
}
}
db.flush().unwrap();
drop(db);
let mut db = Db::open(&path).unwrap();
db.create_index("group").unwrap();
verify(&db, &model);
verify_index(&db, &model);
let _ = std::fs::remove_file(&path);
}