//! Tests `DatabaseExt::merge_with_backup()`. use proptest::{ collection::vec, prop_assert, prop_oneof, proptest, strategy, strategy::Strategy, test_runner::{Config, TestCaseResult}, }; use std::collections::{HashMap, HashSet}; use metaldb::{ access::{Access, AccessExt}, Database, DatabaseExt, IndexAddress, IndexType, TemporaryDB, }; mod work; use self::work::*; const ACTIONS_MAX_LEN: usize = 20; #[derive(Debug, Clone)] enum Action { WorkOnIndex { addr: IndexAddress, index_type: IndexType, value: Option>, }, FlushFork, } fn generate_action() -> impl Strategy + Clone { let work = (generate_address(), generate_index_type(), generate_value()).prop_map( |(addr, index_type, value)| Action::WorkOnIndex { addr, index_type, value, }, ); prop_oneof![ 4 => work, 1 => strategy::Just(Action::FlushFork), ] } fn check_index_does_not_exist(snapshot: S, addr: IndexAddress) -> TestCaseResult { if let Some(index_type) = snapshot.index_type(addr) { prop_assert!(false, "{:?}", index_type); } Ok(()) } fn apply_actions( db: &TemporaryDB, initial_actions: Vec, main_actions: Vec, ) -> TestCaseResult { let fork = db.fork(); let mut initial_data = HashMap::new(); for action in initial_actions { if let Action::WorkOnIndex { addr, index_type, value, } = action { let real_type = work_on_index(&fork, addr.clone(), index_type, value.clone()); let entry = initial_data.entry(addr).or_insert_with(|| IndexData { ty: real_type, values: vec![], }); if let Some(value) = value { entry.values.push(value); } else { entry.values.clear(); } } } // This is the initial database state. let patch = fork.into_patch(); // Check the index contents just in case. for (addr, data) in &initial_data { data.check(&patch, addr.to_owned())?; } db.merge(patch).unwrap(); // Perform main actions. let mut fork = db.fork(); let mut index_data = initial_data.clone(); let mut new_indexes = HashSet::new(); for action in main_actions { match action { Action::WorkOnIndex { addr, index_type, value, } => { let real_type = work_on_index(&fork, addr.clone(), index_type, value.clone()); let entry = index_data.entry(addr.clone()).or_insert_with(|| { new_indexes.insert(addr); IndexData { ty: real_type, values: vec![], } }); if let Some(value) = value { entry.values.push(value); } else { entry.values.clear(); } } Action::FlushFork => fork.flush(), } } let backup = db.merge_with_backup(fork.into_patch()).unwrap(); // Check the new data in the DB. let new_snapshot = db.snapshot(); for (addr, data) in &index_data { data.check(&new_snapshot, addr.to_owned())?; } // Check old data in the backup. for (addr, data) in &initial_data { data.check(&backup, addr.to_owned())?; } // Check aggregation in the backup. for new_addr in &new_indexes { check_index_does_not_exist(&backup, new_addr.to_owned())?; } // Merge the backup into the DB and run the checks on the snapshot. db.merge(backup).unwrap(); let snapshot = db.snapshot(); for new_addr in new_indexes { check_index_does_not_exist(&snapshot, new_addr)?; } Ok(()) } #[test] fn backup_with_honest_db_initialization() { let config = Config::with_cases(Config::default().cases / 4); let actions = vec(generate_action(), 1..ACTIONS_MAX_LEN); proptest!(config, |(init_actions in actions.clone(), main_actions in actions)| { let db = TemporaryDB::new(); apply_actions(&db, init_actions, main_actions)?; }); } #[test] fn backup_with_db_clearing() { let db = TemporaryDB::new(); let actions = vec(generate_action(), 1..ACTIONS_MAX_LEN); proptest!(|(init_actions in actions.clone(), main_actions in actions)| { apply_actions(&db, init_actions, main_actions)?; db.clear().unwrap(); }); }