#![allow(clippy::clone_on_ref_ptr, reason = "tests prefer concise method-call form")]
#![allow(clippy::std_instead_of_core, reason = "tests use std")]
#![allow(clippy::unwrap_used, reason = "test code")]
#![allow(clippy::large_stack_arrays, reason = "test allocations are intentional")]
#![allow(clippy::collection_is_never_read, reason = "tests retain smart pointers to keep chunks alive")]
#![allow(clippy::cast_possible_truncation, reason = "test data is small")]
#![allow(clippy::needless_range_loop, reason = "test indexing is intentional")]
#![allow(clippy::missing_asserts_for_indexing, reason = "test code")]
#![allow(clippy::redundant_clone, reason = "test code")]
#![allow(clippy::needless_lifetimes, reason = "explicit lifetimes clarify the test's intent")]
#![allow(clippy::assertions_on_result_states, reason = "tests assert error returns")]
#![allow(clippy::used_underscore_binding, reason = "intentional drop-after binding")]
#![allow(unused_results, reason = "test code")]
mod common;
use multitude::Arena;
#[test]
fn alloc_returns_mutable_reference() {
let arena = Arena::new();
let x: &mut u32 = arena.alloc(42);
assert_eq!(*x, 42);
*x = 100;
assert_eq!(*x, 100);
}
#[test]
fn alloc_with_constructs_in_place() {
let arena = Arena::new();
let v: &mut std::vec::Vec<i32> = arena.alloc_with(|| vec![1, 2, 3]);
v.push(4);
assert_eq!(v.as_slice(), &[1, 2, 3, 4]);
}
#[test]
fn alloc_many_disjoint_mutable_refs_coexist() {
let arena = Arena::new();
let a: &mut u64 = arena.alloc(1);
let b: &mut u64 = arena.alloc(2);
let c: &mut u64 = arena.alloc(3);
*a += 10;
*b += 20;
*c += 30;
assert_eq!(*a, 11);
assert_eq!(*b, 22);
assert_eq!(*c, 33);
}
#[test]
fn alloc_str_copies_and_returns_mut_str() {
let arena = Arena::new();
let s: &mut str = arena.alloc_str("hello");
assert_eq!(s, "hello");
s.make_ascii_uppercase();
assert_eq!(s, "HELLO");
}
#[test]
fn alloc_str_empty() {
let arena = Arena::new();
let s: &mut str = arena.alloc_str("");
assert_eq!(s, "");
}
#[test]
fn alloc_slice_copy_mutable() {
let arena = Arena::new();
let s: &mut [u32] = arena.alloc_slice_copy([1, 2, 3, 4, 5]);
assert_eq!(s, &[1, 2, 3, 4, 5][..]);
s[2] = 99;
assert_eq!(s, &[1, 2, 99, 4, 5][..]);
}
#[test]
fn alloc_slice_clone_works() {
let arena = Arena::new();
let originals = [
std::string::String::from("a"),
std::string::String::from("b"),
std::string::String::from("c"),
];
let s: &mut [String] = arena.alloc_slice_clone(&originals);
assert_eq!(s.len(), 3);
assert_eq!(s[0], "a");
s[0].push('!');
assert_eq!(s[0], "a!");
assert_eq!(originals[0], "a");
}
#[test]
fn alloc_slice_fill_with_works() {
let arena = Arena::new();
let s: &mut [u32] = arena.alloc_slice_fill_with(10, |i| (i as u32) * (i as u32));
assert_eq!(s.len(), 10);
for i in 0..10 {
assert_eq!(s[i], (i as u32) * (i as u32));
}
}
#[test]
fn try_alloc_slice_clone_works() {
let arena = Arena::new();
let originals = [std::string::String::from("x"), std::string::String::from("y")];
let s: &mut [String] = arena.try_alloc_slice_clone(&originals).unwrap();
assert_eq!(s.len(), 2);
assert_eq!(s[0], "x");
assert_eq!(s[1], "y");
}
#[test]
fn alloc_slice_fill_iter_works() {
let arena = Arena::new();
let s: &mut [u64] = arena.alloc_slice_fill_iter([0_u64, 1, 2, 3, 4]);
assert_eq!(s, &[0, 1, 2, 3, 4]);
}
#[test]
fn try_alloc_slice_fill_iter_works() {
let arena = Arena::new();
let s: &mut [i32] = arena.try_alloc_slice_fill_iter([10, 20, 30]).unwrap();
assert_eq!(s, &[10, 20, 30]);
}
#[test]
fn alloc_slice_fill_iter_empty() {
let arena = Arena::new();
let s: &mut [u32] = arena.alloc_slice_fill_iter(core::iter::empty::<u32>());
assert!(s.is_empty());
}
#[test]
fn alloc_survives_chunk_rotation() {
let arena = Arena::builder().build();
let pinned_value: &mut [u8] = arena.alloc_slice_copy([0xAB; 1024]);
pinned_value[0] = 0xCD;
for _ in 0..10 {
let _filler = arena.alloc_slice_copy([0_u8; 1024]);
}
assert_eq!(pinned_value[0], 0xCD);
assert_eq!(pinned_value[1023], 0xAB);
assert_eq!(pinned_value.len(), 1024);
}
#[test]
fn alloc_works_across_many_rotations() {
let arena = Arena::builder().build();
let mut refs: std::vec::Vec<&mut u32> = std::vec::Vec::with_capacity(1000);
for i in 0..1000_u32 {
refs.push(arena.alloc(i));
}
for (i, r) in refs.iter().enumerate() {
assert_eq!(**r, i as u32);
}
}
#[test]
fn alloc_drop_runs_at_arena_drop() {
use std::sync::atomic::{AtomicUsize, Ordering};
struct DropCounter(std::sync::Arc<AtomicUsize>);
impl Drop for DropCounter {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let counter = std::sync::Arc::new(AtomicUsize::new(0));
{
let arena = Arena::new();
let _r1: &mut DropCounter = arena.alloc(DropCounter(std::sync::Arc::clone(&counter)));
let _r2: &mut DropCounter = arena.alloc(DropCounter(std::sync::Arc::clone(&counter)));
let _r3: &mut DropCounter = arena.alloc(DropCounter(std::sync::Arc::clone(&counter)));
assert_eq!(counter.load(Ordering::SeqCst), 0, "drop must not run before arena drop");
}
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn alloc_slice_fill_with_drop_runs_at_arena_drop() {
use std::sync::atomic::{AtomicUsize, Ordering};
struct DropCounter(std::sync::Arc<AtomicUsize>);
impl Drop for DropCounter {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let counter = std::sync::Arc::new(AtomicUsize::new(0));
{
let arena = Arena::new();
let _slice: &mut [DropCounter] = arena.alloc_slice_fill_with(7, |_| DropCounter(std::sync::Arc::clone(&counter)));
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
assert_eq!(counter.load(Ordering::SeqCst), 7);
}
#[test]
fn alloc_lifetime_bound_by_arena_borrow() {
fn use_arena<'a>(arena: &'a Arena) -> &'a mut u32 {
arena.alloc(7)
}
let arena = Arena::new();
let r = use_arena(&arena);
assert_eq!(*r, 7);
}
#[cfg(feature = "stats")]
#[test]
fn alloc_charges_stats() {
let arena = Arena::new();
let _r: &mut u64 = arena.alloc(42);
assert!(arena.stats().total_bytes_allocated >= 8);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_bytes_is_live_and_returns_to_zero_after_reset() {
let mut arena = Arena::new();
assert_eq!(arena.stats().wasted_tail_bytes, 0, "fresh arena has no chunks");
for _ in 0..32 {
let _r: &mut u64 = arena.alloc(0);
}
assert!(
arena.stats().wasted_tail_bytes > 0,
"active chunk's free tail must contribute to wasted_tail_bytes",
);
arena.reset();
assert_eq!(
arena.stats().wasted_tail_bytes,
0,
"reset returned every chunk and reinstalled the empty sentinel; \
wasted-tail must be zero again",
);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_includes_active_chunks_and_shrinks_with_allocs() {
let arena = Arena::new();
let _: &mut u8 = arena.alloc(0);
let chunks_before = arena.stats().normal_local_chunks_allocated;
let after_one = arena.stats().wasted_tail_bytes;
assert!(after_one > 0, "active local chunk's free tail must be included even with 0 retires");
for _ in 0..16 {
let _: &mut u64 = arena.alloc(0);
}
assert_eq!(
arena.stats().normal_local_chunks_allocated,
chunks_before,
"test relies on no refill happening; tighten the loop if this fires",
);
let after_many = arena.stats().wasted_tail_bytes;
assert!(
after_many < after_one,
"subsequent allocs consumed bump space; the active chunk's \
contribution to wasted_tail_bytes must shrink (before={after_one}, \
after={after_many})",
);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_bytes_is_held_by_outstanding_arc() {
let arena = Arena::new();
let pinned = arena.alloc_arc::<u64>(7);
let initial_shared = arena.stats().normal_shared_chunks_allocated;
let mut tries = 0;
while arena.stats().normal_shared_chunks_allocated == initial_shared {
drop(arena.alloc_slice_copy_arc::<u8>(&[0_u8; 2048]));
tries += 1;
assert!(tries < 1_000, "shared chunk never refilled — retire path appears broken");
}
let held_wasted = arena.stats().wasted_tail_bytes;
drop(pinned);
let after_drop = arena.stats().wasted_tail_bytes;
assert!(
after_drop < held_wasted,
"dropping the last handle must release the retired chunk's wasted tail \
(before={held_wasted}, after={after_drop})",
);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_grows_on_local_refill_and_clears_on_reset() {
let mut arena = Arena::new();
let _: &mut u8 = arena.alloc(0);
let baseline = arena.stats().wasted_tail_bytes;
let chunks_before = arena.stats().normal_local_chunks_allocated;
let mut refills_observed = 0u64;
let mut saw_growth_over_baseline = false;
let mut allocs = 0u64;
while refills_observed < 8 {
let _: &mut [u8] = arena.alloc_slice_fill_with(509, |_| 0_u8);
allocs += 1;
assert!(
allocs < 100_000,
"after {allocs} allocations only {refills_observed}/8 refills were observed — \
chunk-allocation accounting (stats/normal_local) or slice fill appears broken",
);
let now_chunks = arena.stats().normal_local_chunks_allocated;
if now_chunks > chunks_before + refills_observed {
refills_observed += 1;
if arena.stats().wasted_tail_bytes > baseline {
saw_growth_over_baseline = true;
}
}
}
assert!(
saw_growth_over_baseline,
"across {refills_observed} refills with a prime allocation size, \
the wasted-tail counter never exceeded its single-active-chunk \
baseline — retire-side accounting is broken",
);
arena.reset();
assert_eq!(
arena.stats().wasted_tail_bytes,
0,
"reset must release every chunk and reinstall the empty sentinels, \
taking the gauge back to zero",
);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_returns_to_exactly_baseline_across_full_cycle() {
let mut arena = Arena::new();
for cycle in 0..10 {
let before = arena.stats().wasted_tail_bytes;
assert_eq!(before, 0, "cycle {cycle}: baseline must be 0 before allocations begin");
for _ in 0..4 {
let _: &mut u64 = arena.alloc(42);
let _: &mut [u8] = arena.alloc_slice_fill_with(256, |_| 0);
}
arena.reset();
let after = arena.stats().wasted_tail_bytes;
assert_eq!(
after, 0,
"cycle {cycle}: after reset, the local counter must return to exactly 0 \
(got {after}) — asymmetric add/subtract would leave a residue",
);
}
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_correct_after_cache_reuse_cycles() {
let mut arena = Arena::new();
let mut acquired_chunks_total = 0u64;
for _ in 0..8 {
for _ in 0..64 {
let _: &mut u64 = arena.alloc(0);
}
let stats = arena.stats();
acquired_chunks_total = stats.normal_local_chunks_allocated;
arena.reset();
assert_eq!(arena.stats().wasted_tail_bytes, 0, "cache reuse leaked into wasted-tail counter");
}
assert!(acquired_chunks_total >= 1, "test did not allocate any chunks");
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_decreases_monotonically_as_pinned_arcs_drop() {
let arena = Arena::new();
let mut pins = std::vec::Vec::new();
for _ in 0..4 {
pins.push(arena.alloc_arc::<u64>(99));
for _ in 0..3 {
drop(arena.alloc_slice_copy_arc::<u8>(&[0_u8; 2048]));
}
}
let peak = arena.stats().wasted_tail_bytes;
assert!(peak > 0, "expected outstanding pins to keep retired chunks counted");
let mut prev = peak;
while let Some(p) = pins.pop() {
drop(p);
let cur = arena.stats().wasted_tail_bytes;
assert!(cur <= prev, "dropping a pin must never grow the counter (prev={prev}, cur={cur})");
assert!(
cur < u64::MAX / 2,
"counter underflowed (cur={cur}); subtract was unbalanced from add",
);
prev = cur;
}
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_handles_oversized_local_retire() {
let mut arena = Arena::new();
let _: &mut [u8] = arena.alloc_slice_fill_with(20 * 1024, |_| 0_u8);
let mid = arena.stats().wasted_tail_bytes;
let _: &mut [u8] = arena.alloc_slice_fill_with(20 * 1024, |_| 0_u8);
let _: &mut [u8] = arena.alloc_slice_fill_with(20 * 1024, |_| 0_u8);
let after = arena.stats().wasted_tail_bytes;
assert!(
after >= mid,
"more oversized retires must not shrink the counter (mid={mid}, after={after})",
);
arena.reset();
assert_eq!(
arena.stats().wasted_tail_bytes,
0,
"reset must release every oversized-retired chunk",
);
}
#[cfg(feature = "stats")]
#[test]
fn wasted_tail_never_underflows_under_stress() {
let mut arena = Arena::new();
let filler = [0_u8; 64];
for _ in 0..10 {
let _: &mut u64 = arena.alloc(0);
let _: &mut [u8] = arena.alloc_slice_copy(filler);
drop(arena.alloc_arc::<u64>(0));
drop(arena.alloc_box::<u64>(0));
drop(arena.alloc_slice_copy_arc::<u8>(&[0_u8; 4096]));
let stats = arena.stats();
assert!(
stats.wasted_tail_bytes <= stats.total_bytes_allocated,
"wasted tail ({}) must never exceed total bytes outstanding ({}) — \
an underflow would wrap it near u64::MAX",
stats.wasted_tail_bytes,
stats.total_bytes_allocated,
);
}
arena.reset();
let stats = arena.stats();
assert!(stats.wasted_tail_bytes <= stats.total_bytes_allocated);
}
use crate::common::FailingAllocator;
#[test]
fn try_alloc_returns_err_on_failing_allocator() {
let arena: Arena<FailingAllocator> = Arena::new_in(FailingAllocator::new(0));
assert!(arena.try_alloc(0_u32).is_err());
assert!(arena.try_alloc_with(|| 0_u32).is_err());
assert!(arena.try_alloc_slice_copy::<u8>(&[1, 2, 3]).is_err());
assert!(arena.try_alloc_slice_fill_with::<u32, _>(3, |i| i as u32).is_err());
}
#[test]
#[should_panic(expected = "multitude: allocator returned AllocError")]
fn alloc_panics_on_failing_allocator() {
let arena: Arena<FailingAllocator> = Arena::new_in(FailingAllocator::new(0));
let _ = arena.alloc(0_u32);
}
#[test]
fn pinned_chunk_with_allocator_api2_vec_drops_cleanly() {
let arena: Arena = Arena::builder().build();
let _bump_ref: &mut u32 = arena.alloc(123);
let mut v: allocator_api2::vec::Vec<u8, &Arena> = allocator_api2::vec::Vec::new_in(&arena);
for _ in 0..5_000_u32 {
v.push(0);
}
assert!(_bump_ref == &mut 123);
drop(v);
}