#![allow(deprecated)]
#![cfg(feature = "experimental")]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::scope;
use sefer_alloc::{EpochHandle, EpochRegion};
#[test]
fn single_threaded_sequence_matches_reference_model() {
let region = EpochRegion::<u32>::with_capacity(8);
let mut model: Vec<(EpochHandle<u32>, u32)> = Vec::new();
assert!(region.is_empty());
assert_eq!(region.len(), 0);
for v in [10_u32, 20, 30, 40, 50] {
let h = region
.insert(v)
.expect("region with capacity 8 must accept the first inserts");
assert_eq!(
region
.get_cloned(h)
.expect("fresh handle must resolve (I1)"),
v
);
model.push((h, v));
}
assert_eq!(region.len(), model.len());
assert!(!region.is_empty());
for (h, v) in &model {
assert_eq!(
region
.get_cloned(*h)
.expect("live handle must resolve (I1)"),
*v
);
}
let victim_idx = 2;
let (victim_h, victim_v) = model.remove(victim_idx);
assert!(region.remove(victim_h), "live handle must remove once");
assert_eq!(
region.get_cloned(victim_h),
None,
"removed handle must be None (I2)"
);
assert!(
!region.remove(victim_h),
"second remove of a stale handle is a no-op false (I2)"
);
assert_eq!(region.len(), model.len());
for (h, v) in &model {
assert_eq!(
region
.get_cloned(*h)
.expect("survivor must still resolve (I1)"),
*v
);
}
let _ = victim_v;
let new_v = 999_u32;
let new_h = region.insert(new_v).expect("a freed slot is available");
assert_eq!(
region
.get_cloned(new_h)
.expect("new handle must resolve (I1)"),
new_v
);
assert_eq!(
region.get_cloned(victim_h),
None,
"stale handle to a reused slot must not resolve (I3)"
);
assert_ne!(
new_h, victim_h,
"fresh handle differs from the stale one (generation bumped)"
);
model.push((new_h, new_v));
assert_eq!(region.len(), model.len());
while let Some((h, _v)) = model.pop() {
assert!(region.remove(h), "live handle must remove");
assert_eq!(region.get_cloned(h), None);
}
assert_eq!(region.len(), 0);
assert!(region.is_empty());
}
#[test]
fn full_region_returns_err_with_value_back() {
const CAP: usize = 4;
let region = EpochRegion::<u32>::with_capacity(CAP);
for v in 0..u32::try_from(CAP).unwrap() {
region
.insert(v)
.unwrap_or_else(|_| panic!("insert {v} into a region of cap {CAP} must succeed"));
}
assert_eq!(region.len(), CAP);
let overflow_value = 4242_u32;
let returned = region
.insert(overflow_value)
.expect_err("insert into a full region must return Err (no panic)");
assert_eq!(
returned, overflow_value,
"the overflow value is handed back"
);
assert_eq!(region.len(), CAP);
}
#[test]
fn region_drop_runs_live_value_destructors_once() {
struct DropCounter(Arc<AtomicUsize>);
impl Drop for DropCounter {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
let counter = Arc::new(AtomicUsize::new(0));
{
let region = EpochRegion::<DropCounter>::with_capacity(8);
for _ in 0..5 {
region
.insert(DropCounter(Arc::clone(&counter)))
.unwrap_or_else(|_| panic!("insert into a region of cap 8 must succeed"));
}
assert_eq!(region.len(), 5);
}
assert_eq!(
counter.load(Ordering::Relaxed),
5,
"every value live at region drop must be dropped exactly once (I5)"
);
}
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed.max(1))
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn chance(&mut self, num: u32, denom: u32) -> bool {
if denom == 0 {
return false;
}
(self.next_u64() % u64::from(denom)) < u64::from(num)
}
fn below(&mut self, n: usize) -> usize {
let n64 = u64::try_from(n).expect("index space fits u64");
usize::try_from(self.next_u64() % n64).expect("modulo result fits usize")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Tagged {
thread: usize,
seq: u64,
}
const CAPACITY: usize = 64;
const WRITERS: usize = 3;
const READERS: usize = 3;
const WRITER_OPS: usize = 1_200;
const READER_OPS: usize = 3_600;
type HandlePool<T> = Arc<Mutex<Vec<(usize, EpochHandle<T>)>>>;
#[test]
fn concurrent_readers_and_writers_never_observe_a_torn_value() {
let region = Arc::new(EpochRegion::<Tagged>::with_capacity(CAPACITY));
let pool: HandlePool<Tagged> = Arc::new(Mutex::new(Vec::new()));
let total_survivors = scope(|scope| {
let live = Arc::new(AtomicBool::new(true));
let mut writer_handles: Vec<_> = (0..WRITERS)
.map(|tid| {
let region = Arc::clone(®ion);
let pool = Arc::clone(&pool);
scope.spawn(move || writer(tid, ®ion, &pool))
})
.collect();
let mut reader_handles: Vec<_> = Vec::with_capacity(READERS);
for rid in 0..READERS {
let region = Arc::clone(®ion);
let pool = Arc::clone(&pool);
let live = Arc::clone(&live);
reader_handles.push(scope.spawn(move || reader(rid, ®ion, &pool, &live)));
}
let survivors: usize = writer_handles
.drain(..)
.map(|h| h.join().expect("writer thread panicked"))
.sum();
live.store(false, Ordering::Relaxed);
for h in reader_handles {
h.join().expect("reader thread panicked");
}
survivors
});
assert_eq!(
region.len(),
total_survivors,
"len() must equal the total live entries writers reported (I4 under concurrency)"
);
assert_eq!(region.is_empty(), total_survivors == 0);
}
fn writer(
tid: usize,
region: &EpochRegion<Tagged>,
pool: &Mutex<Vec<(usize, EpochHandle<Tagged>)>>,
) -> usize {
let mut rng = Lcg::new(
u64::try_from(tid)
.unwrap()
.wrapping_add(0x9E37_79B9_7F4A_7C15),
);
let mut my_handles: Vec<EpochHandle<Tagged>> = Vec::with_capacity(WRITER_OPS);
for seq in 0..u64::try_from(WRITER_OPS).unwrap() {
let value = Tagged { thread: tid, seq };
let Ok(h) = region.insert(value.clone()) else {
continue;
};
let got = region
.get_cloned(h)
.expect("fresh handle must resolve immediately");
assert_eq!(
got, value,
"writer {tid}: immediate re-read returned a different value"
);
my_handles.push(h);
pool.lock().expect("pool mutex poisoned").push((tid, h));
if !my_handles.is_empty() && rng.chance(1, 3) {
let idx = rng.below(my_handles.len());
let victim = my_handles.swap_remove(idx);
let removed = region.remove(victim);
assert!(
removed,
"writer {tid}: our own live handle must remove exactly once"
);
trim_pool(pool, victim);
assert_eq!(
region.get_cloned(victim),
None,
"writer {tid}: removed handle must resolve to None (I2)"
);
}
}
my_handles.len()
}
fn trim_pool(pool: &Mutex<Vec<(usize, EpochHandle<Tagged>)>>, victim: EpochHandle<Tagged>) {
let mut guard = pool.lock().expect("pool mutex poisoned");
if let Some(pos) = guard.iter().position(|(_, h)| *h == victim) {
guard.swap_remove(pos);
}
}
static PROBES: AtomicUsize = AtomicUsize::new(0);
fn reader(
rid: usize,
region: &EpochRegion<Tagged>,
pool: &Mutex<Vec<(usize, EpochHandle<Tagged>)>>,
live: &AtomicBool,
) {
let mut rng = Lcg::new(
u64::try_from(rid)
.unwrap()
.wrapping_add(0x51ED_270B_1F2C_3D4E)
.wrapping_mul(0x9E37_79B9_7F4A_7C15),
);
loop {
let done =
!live.load(Ordering::Relaxed) && PROBES.load(Ordering::Relaxed) >= READER_OPS * READERS;
if done {
break;
}
let probe = {
let guard = pool.lock().expect("pool mutex poisoned");
if guard.is_empty() {
None
} else {
let (owner, h) = guard[rng.below(guard.len())];
Some((owner, h))
}
};
if let Some((owner, h)) = probe {
PROBES.fetch_add(1, Ordering::Relaxed);
if let Some(v) = region.get_cloned(h) {
assert_eq!(
v.thread, owner,
"reader {rid}: resolved value belongs to writer {} but the handle was \
owned by writer {} — torn/cross read detected",
v.thread, owner,
);
assert!(
v.seq < u64::try_from(WRITER_OPS).unwrap(),
"reader {rid}: resolved value seq {} is out of range — torn read detected",
v.seq
);
}
}
}
}