use std::collections::BTreeMap;
use std::error::Error;
use proptest::prelude::*;
use super::{Database, DatabaseConfig};
use crate::tree::{Hash, empty_root_hash};
fn config_for(path: &std::path::Path, shard_count: usize) -> DatabaseConfig {
DatabaseConfig {
data_dir: path.to_path_buf(),
shard_count,
distributed: None,
executor_threads: None,
}
}
#[test]
fn empty_shard_commit_equals_empty_root_constant() -> Result<(), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let shard_count = 64;
let db = Database::create(config_for(&dir.path().join("db"), shard_count))?;
db.put(b"alpha".to_vec(), b"1".to_vec())?;
db.put(b"beta".to_vec(), b"2".to_vec())?;
db.put(b"gamma".to_vec(), b"3".to_vec())?;
let mut written = std::collections::BTreeSet::new();
for key in [b"alpha".as_slice(), b"beta".as_slice(), b"gamma".as_slice()] {
written.insert(db.shard_for(key));
}
let roots = db.commit()?;
assert_eq!(roots.len(), shard_count);
let empty = empty_root_hash();
let mut saw_empty = false;
for (shard_id, root) in &roots {
if written.contains(shard_id) {
assert_ne!(*root, empty, "written shard {shard_id} must not be empty");
} else {
assert_eq!(*root, empty, "empty shard {shard_id} must equal constant");
saw_empty = true;
}
}
assert!(saw_empty, "with 3 keys over 64 shards some shard is empty");
Ok(())
}
#[test]
fn global_root_identical_lazy_vs_force_materialised() -> Result<(), Box<dyn Error>> {
let shard_count = 8;
let distributions: [&[&[u8]]; 4] = [
&[],
&[b"alpha"],
&[b"alpha", b"beta", b"gamma"],
&[b"a", b"bb", b"ccc", b"dddd", b"eeeee", b"ffffff"],
];
for keys in distributions {
let dir = tempfile::tempdir()?;
let db_a = Database::create(config_for(&dir.path().join("a"), shard_count))?;
for key in keys {
db_a.put(key.to_vec(), key.to_vec())?;
}
let mut touched = std::collections::BTreeSet::new();
for key in keys {
touched.insert(db_a.shard_for(key));
}
assert_eq!(db_a.materialised_shard_ids(), touched_ids(&touched));
let lazy_roots = db_a.commit()?;
let db_b = Database::create(config_for(&dir.path().join("b"), shard_count))?;
for key in keys {
db_b.put(key.to_vec(), key.to_vec())?;
}
for shard_id in 0..shard_count {
let _handle = db_b.handle_for_shard(shard_id)?;
}
assert_eq!(
db_b.materialised_shard_ids(),
(0..shard_count).collect::<Vec<_>>()
);
let eager_roots = db_b.commit()?;
assert_eq!(
lazy_roots,
eager_roots,
"lazy synthesised global root must equal force-materialised eager root \
for {} key(s)",
keys.len(),
);
assert!(touched.len() < shard_count || keys.len() >= shard_count);
}
Ok(())
}
fn touched_ids(touched: &std::collections::BTreeSet<usize>) -> Vec<usize> {
touched.iter().copied().collect()
}
fn eager_ordered_roots(db: &Database) -> Result<Vec<Hash>, Box<dyn Error>> {
let roots: BTreeMap<usize, Hash> = db.commit()?;
Ok(roots.into_values().collect())
}
fn synthesised_ordered_roots(shard_count: usize, non_empty: &BTreeMap<usize, Hash>) -> Vec<Hash> {
let empty = empty_root_hash();
(0..shard_count)
.map(|shard_id| non_empty.get(&shard_id).copied().unwrap_or(empty))
.collect()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(24))]
#[test]
fn slot_fill_maps_non_empty_roots_and_synthesises_empties(
keys in prop::collection::btree_set(
prop::collection::vec(any::<u8>(), 1..24),
0..24,
),
shard_count in 24usize..=48,
) {
let dir = tempfile::tempdir().map_err(fail)?;
let db = Database::create(config_for(&dir.path().join("db"), shard_count))
.map_err(fail)?;
let mut touched = std::collections::BTreeSet::new();
for key in &keys {
db.put(key.clone(), key.clone()).map_err(fail)?;
touched.insert(db.shard_for(key));
}
let eager = eager_ordered_roots(&db).map_err(fail)?;
prop_assert_eq!(eager.len(), shard_count);
let non_empty: BTreeMap<usize, Hash> = eager
.iter()
.copied()
.enumerate()
.filter(|(shard_id, _)| touched.contains(shard_id))
.collect();
let synthesised = synthesised_ordered_roots(shard_count, &non_empty);
prop_assert_eq!(eager, synthesised);
}
}
fn fail<E: std::fmt::Display>(error: E) -> proptest::test_runner::TestCaseError {
proptest::test_runner::TestCaseError::fail(error.to_string())
}