use super::*;
use rstest::rstest;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum RefState {
Free,
#[allow(dead_code)]
Reserved,
Live,
Retired,
}
struct RefModel {
generations: Vec<u32>,
states: Vec<RefState>,
free: Vec<u32>,
next: u32,
}
impl RefModel {
fn new() -> Self {
Self {
generations: Vec::new(),
states: Vec::new(),
free: Vec::new(),
next: 0,
}
}
fn alloc(&mut self) -> EntityId {
let slot = if let Some(slot) = self.free.pop() {
slot as usize
} else {
let slot = self.next;
self.next += 1;
self.generations.push(INITIAL_GENERATION);
self.states.push(RefState::Free);
slot as usize
};
self.states[slot] = RefState::Live;
EntityId::from_parts(slot as u32, self.generations[slot])
}
fn free(&mut self, id: EntityId) -> bool {
let slot = id.slot() as usize;
if slot >= self.generations.len()
|| self.states[slot] != RefState::Live
|| self.generations[slot] != id.generation()
{
return false;
}
let next = self.generations[slot].checked_add(1);
match next {
Some(0) | None => {
self.states[slot] = RefState::Retired;
false
}
Some(gen) => {
self.generations[slot] = gen;
self.states[slot] = RefState::Free;
self.free.push(slot as u32);
true
}
}
}
fn is_alive(&self, id: EntityId) -> bool {
let slot = id.slot() as usize;
slot < self.generations.len()
&& self.states[slot] == RefState::Live
&& self.generations[slot] == id.generation()
}
}
#[rstest]
fn initial_generation_is_one() {
let mut alloc = EntityAllocator::new();
let id = alloc.alloc();
assert_eq!(id.generation(), INITIAL_GENERATION);
}
#[rstest]
fn deterministic_initial_allocation_order() {
let mut alloc = EntityAllocator::new();
let a = alloc.alloc();
let b = alloc.alloc();
assert_eq!(a.slot(), 0);
assert_eq!(b.slot(), 1);
assert_eq!(a.generation(), 1);
assert_eq!(b.generation(), 1);
}
#[rstest]
fn owner_token_rejects_identical_slot_generation_from_another_allocator() {
let mut a = EntityAllocator::with_owner(11);
let mut b = EntityAllocator::with_owner(12);
let local = a.alloc();
let foreign = b.alloc();
assert_eq!(local.slot(), foreign.slot());
assert_eq!(local.generation(), foreign.generation());
assert_ne!(local, foreign);
assert!(a.is_alive(local));
assert!(!a.is_alive(foreign));
assert_eq!(a.free(foreign), Err(AllocatorError::StaleEntity));
}
#[rstest]
fn reuse_bumps_generation() {
let mut alloc = EntityAllocator::new();
let a = alloc.alloc();
alloc.free(a).expect("first free");
assert!(!alloc.is_alive(a));
let b = alloc.alloc();
assert_eq!(a.slot(), b.slot());
assert_eq!(b.generation(), 2);
}
#[rstest]
fn freed_but_not_reallocated_is_dead() {
let mut alloc = EntityAllocator::new();
let id = alloc.alloc();
alloc.free(id).expect("free");
assert!(!alloc.is_alive(id));
}
#[rstest]
fn double_free_is_non_destructive() {
let mut alloc = EntityAllocator::new();
let id = alloc.alloc();
alloc.free(id).expect("first free");
let before = alloc.counts();
assert_eq!(alloc.free(id), Err(AllocatorError::StaleEntity));
assert_eq!(alloc.counts(), before);
}
#[rstest]
fn released_reserved_does_not_resurrect() {
let mut alloc = EntityAllocator::new();
let reserved = alloc.reserve().expect("reserve");
alloc.release_reserved(reserved).expect("release");
assert!(!alloc.is_alive(reserved));
let live = alloc.alloc();
assert_eq!(live.slot(), reserved.slot());
assert_eq!(live.generation(), reserved.generation() + 1);
assert_ne!(live, reserved);
}
#[rstest]
fn reserved_is_not_alive() {
let mut alloc = EntityAllocator::new();
let id = alloc.reserve().expect("reserve");
assert!(!alloc.is_alive(id));
assert_eq!(alloc.counts().reserved, 1);
}
#[rstest]
fn slot_retired_rejects_live_operations() {
let mut alloc = EntityAllocator::new();
let id = alloc.alloc();
alloc.set_generation_for_test(id, u32::MAX);
let exhausted = EntityId::from_parts(id.slot(), u32::MAX);
assert_eq!(
alloc.free(exhausted),
Err(AllocatorError::GenerationOverflow)
);
assert_eq!(alloc.free(exhausted), Err(AllocatorError::SlotRetired));
assert_eq!(
alloc.commit_reserved(exhausted),
Err(AllocatorError::SlotRetired)
);
}
#[rstest]
fn generation_overflow_retires_slot() {
let mut alloc = EntityAllocator::new();
let id = alloc.alloc();
alloc.set_generation_for_test(id, u32::MAX);
let exhausted = EntityId::from_parts(id.slot(), u32::MAX);
assert_eq!(
alloc.free(exhausted),
Err(AllocatorError::GenerationOverflow)
);
assert!(!alloc.is_alive(exhausted));
let counts = alloc.counts();
assert_eq!(counts.retired, 1);
assert_eq!(counts.free, 0);
}
#[rstest]
fn capacity_growth_preserves_live_handles() {
let mut alloc = EntityAllocator::new();
let mut live = Vec::new();
for _ in 0..32 {
live.push(alloc.alloc());
}
for id in &live {
assert!(alloc.is_alive(*id));
}
}
#[rstest]
fn free_on_reserved_handle_reports_stale_entity() {
let mut alloc = EntityAllocator::new();
let reserved = alloc.reserve().expect("reserve");
assert_eq!(alloc.free(reserved), Err(AllocatorError::StaleEntity));
}
#[rstest]
fn commit_reserved_on_live_entity_reports_not_live() {
let mut alloc = EntityAllocator::new();
let live = alloc.alloc();
assert_eq!(alloc.commit_reserved(live), Err(AllocatorError::NotLive));
}
#[rstest]
fn release_reserved_on_live_entity_reports_not_live() {
let mut alloc = EntityAllocator::new();
let live = alloc.alloc();
assert_eq!(alloc.release_reserved(live), Err(AllocatorError::NotLive));
}
#[rstest]
fn commit_reserved_on_free_slot_reports_not_live() {
let mut alloc = EntityAllocator::new();
let live = alloc.alloc();
alloc.free(live).expect("free");
let next_gen = alloc.generation_for_slot(live.slot() as usize);
let future = EntityId::from_parts(live.slot(), next_gen);
assert_eq!(alloc.commit_reserved(future), Err(AllocatorError::NotLive));
}
#[rstest]
fn ensure_state_live_slot_with_mismatched_generation_is_stale() {
let mut alloc = EntityAllocator::new();
let live = alloc.alloc();
alloc.set_generation_for_test(live, live.generation().wrapping_add(1));
assert_eq!(
alloc.ensure_state_for_test(live.slot() as usize, SlotState::Live, live.generation()),
Err(AllocatorError::StaleEntity)
);
}
#[rstest]
fn ensure_state_rejects_out_of_range_slots() {
let mut alloc = EntityAllocator::new();
let missing = EntityId::from_parts(9_999, 1);
assert_eq!(alloc.free(missing), Err(AllocatorError::StaleEntity));
}
#[rstest]
fn default_allocator_matches_new() {
assert_eq!(
EntityAllocator::default().counts(),
EntityAllocator::new().counts()
);
}
#[rstest]
fn randomized_trace_matches_reference_model() {
let mut alloc = EntityAllocator::new();
let mut model = RefModel::new();
let mut live = Vec::new();
let mut seed = 0xC0FFEE_u32;
for step in 0..2_000 {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
if live.is_empty() || seed % 3 != 2 {
let id = alloc.alloc();
assert!(alloc.is_alive(id));
let model_id = model.alloc();
assert_eq!(id.slot(), model_id.slot());
assert_eq!(id.generation(), model_id.generation());
live.push(id);
} else {
let index = (seed as usize) % live.len();
let id = live[index];
let alloc_result = alloc.free(id);
let model_result = model.free(id);
assert_eq!(alloc_result.is_ok(), model_result);
live.swap_remove(index);
}
let counts = alloc.counts();
assert_eq!(counts.live as usize, live.len());
for id in &live {
assert!(alloc.is_alive(*id));
assert!(model.is_alive(*id));
}
let _ = step;
}
}