//! This module contains property-based tests against the public API: //! * API never panics. //! * Active entries cannot be overridden until removed. //! * The slab doesn't produce overlapping keys. //! * The slab doesn't leave "lost" keys. //! * `get()`, `get_owned`, and `contains()` are consistent. //! * `RESERVED_BITS` are actually not used. //! //! The test is supposed to be deterministic, so it doesn't spawn real threads //! and uses `tid::with()` to override the TID for the current thread. use std::{ops::Range, sync::Arc}; use indexmap::IndexMap; use proptest::prelude::*; use crate::{tid, Config, DefaultConfig, Slab}; const THREADS: Range = 1..4; const ACTIONS: Range = 1..1000; #[derive(Debug, Clone)] struct Action { tid: usize, kind: ActionKind, } #[derive(Debug, Clone)] enum ActionKind { Insert, VacantEntry, RemoveRandom(usize), // key RemoveExistent(usize), // seed TakeRandom(usize), // key TakeExistent(usize), // seed GetRandom(usize), // key GetExistent(usize), // seed } prop_compose! { fn action_strategy()(tid in THREADS, kind in action_kind_strategy()) -> Action { Action { tid, kind } } } fn action_kind_strategy() -> impl Strategy { prop_oneof![ 1 => Just(ActionKind::Insert), 1 => Just(ActionKind::VacantEntry), 1 => prop::num::usize::ANY.prop_map(ActionKind::RemoveRandom), 1 => prop::num::usize::ANY.prop_map(ActionKind::RemoveExistent), 1 => prop::num::usize::ANY.prop_map(ActionKind::TakeRandom), 1 => prop::num::usize::ANY.prop_map(ActionKind::TakeExistent), // Produce `GetRandom` and `GetExistent` more often. 5 => prop::num::usize::ANY.prop_map(ActionKind::GetRandom), 5 => prop::num::usize::ANY.prop_map(ActionKind::GetExistent), ] } /// Stores active entries (added and not yet removed). #[derive(Default)] struct Active { // Use `IndexMap` to preserve determinism. map: IndexMap, prev_value: u32, } impl Active { fn next_value(&mut self) -> u32 { self.prev_value += 1; self.prev_value } fn get(&self, key: usize) -> Option { self.map.get(&key).copied() } fn get_any(&self, seed: usize) -> Option<(usize, u32)> { if self.map.is_empty() { return None; } let index = seed % self.map.len(); self.map.get_index(index).map(|(k, v)| (*k, *v)) } fn insert(&mut self, key: usize, value: u32) { assert_eq!( self.map.insert(key, value), None, "keys of active entries must be unique" ); } fn remove(&mut self, key: usize) -> Option { self.map.swap_remove(&key) } fn remove_any(&mut self, seed: usize) -> Option<(usize, u32)> { if self.map.is_empty() { return None; } let index = seed % self.map.len(); self.map.swap_remove_index(index) } fn drain(&mut self) -> impl Iterator + '_ { self.map.drain(..) } } fn used_bits(key: usize) -> usize { assert_eq!( C::RESERVED_BITS + Slab::::USED_BITS, std::mem::size_of::() * 8 ); key & ((!0) >> C::RESERVED_BITS) } fn apply_action( slab: &Arc>, active: &mut Active, action: ActionKind, ) -> Result<(), TestCaseError> { match action { ActionKind::Insert => { let value = active.next_value(); let key = slab.insert(value).expect("unexpectedly exhausted slab"); prop_assert_eq!(used_bits::(key), key); active.insert(key, value); } ActionKind::VacantEntry => { let value = active.next_value(); let entry = slab.vacant_entry().expect("unexpectedly exhausted slab"); let key = entry.key(); prop_assert_eq!(used_bits::(key), key); entry.insert(value); active.insert(key, value); } ActionKind::RemoveRandom(key) => { let used_key = used_bits::(key); prop_assert_eq!(slab.get(key).map(|e| *e), slab.get(used_key).map(|e| *e)); prop_assert_eq!(slab.remove(key), active.remove(used_key).is_some()); } ActionKind::RemoveExistent(seed) => { if let Some((key, _value)) = active.remove_any(seed) { prop_assert!(slab.contains(key)); prop_assert!(slab.remove(key)); } } ActionKind::TakeRandom(key) => { let used_key = used_bits::(key); prop_assert_eq!(slab.get(key).map(|e| *e), slab.get(used_key).map(|e| *e)); prop_assert_eq!(slab.take(key), active.remove(used_key)); } ActionKind::TakeExistent(seed) => { if let Some((key, value)) = active.remove_any(seed) { prop_assert!(slab.contains(key)); prop_assert_eq!(slab.take(key), Some(value)); } } ActionKind::GetRandom(key) => { let used_key = used_bits::(key); prop_assert_eq!(slab.get(key).map(|e| *e), slab.get(used_key).map(|e| *e)); prop_assert_eq!(slab.get(key).map(|e| *e), active.get(used_key)); prop_assert_eq!( slab.clone().get_owned(key).map(|e| *e), active.get(used_key) ); } ActionKind::GetExistent(seed) => { if let Some((key, value)) = active.get_any(seed) { prop_assert!(slab.contains(key)); prop_assert_eq!(slab.get(key).map(|e| *e), Some(value)); prop_assert_eq!(slab.clone().get_owned(key).map(|e| *e), Some(value)); } } } Ok(()) } fn run(actions: Vec) -> Result<(), TestCaseError> { let mut slab = Arc::new(Slab::new_with_config::()); let mut active = Active::default(); // Apply all actions. for action in actions { // Override the TID for the current thread instead of using multiple real threads // to preserve determinism. We're not checking concurrency issues here, they should be // covered by loom tests anyway. Thus, it's fine to run all actions consequently. tid::with(action.tid, || { apply_action::(&slab, &mut active, action.kind) })?; } // Ensure the slab contains all remaining entries. let mut expected_values = Vec::new(); for (key, value) in active.drain() { prop_assert!(slab.contains(key)); prop_assert_eq!(slab.get(key).map(|e| *e), Some(value)); prop_assert_eq!(slab.clone().get_owned(key).map(|e| *e), Some(value)); expected_values.push(value); } expected_values.sort(); // Ensure `unique_iter()` returns all remaining entries. let slab = Arc::get_mut(&mut slab).unwrap(); let mut actual_values = slab.unique_iter().copied().collect::>(); actual_values.sort(); prop_assert_eq!(actual_values, expected_values); Ok(()) } proptest! { #[test] fn default_config(actions in prop::collection::vec(action_strategy(), ACTIONS)) { run::(actions)?; } #[test] fn custom_config(actions in prop::collection::vec(action_strategy(), ACTIONS)) { run::(actions)?; } } struct CustomConfig; #[cfg(target_pointer_width = "64")] impl Config for CustomConfig { const INITIAL_PAGE_SIZE: usize = 32; const MAX_PAGES: usize = 15; const MAX_THREADS: usize = 256; const RESERVED_BITS: usize = 24; } #[cfg(target_pointer_width = "32")] impl Config for CustomConfig { const INITIAL_PAGE_SIZE: usize = 16; const MAX_PAGES: usize = 6; const MAX_THREADS: usize = 128; const RESERVED_BITS: usize = 12; }