use super::{BStackAllocator, BStackBulkAllocator, BStackSlice};
use crate::BStack;
use std::collections::HashSet;
use std::io;
use std::ops::Range;
use std::sync::Mutex;
fn overlaps(a: &Range<u64>, b: &Range<u64>) -> bool {
!a.is_empty() && !b.is_empty() && a.start.max(b.start) < a.end.min(b.end)
}
fn check_overlap(region: &Range<u64>, set: &HashSet<Range<u64>>) -> Option<Range<u64>> {
set.iter().find(|r| overlaps(region, r)).cloned()
}
fn check_deallocation(
region: &Range<u64>,
state: &DebugState,
pending_freed: &HashSet<Range<u64>>,
) -> bool {
if let Some(overlap) = check_overlap(region, &state.freed) {
panic!(
"DebugCheckingAllocator: Attempting to free region [{}, {}) which overlaps \
with already freed region [{}, {}). This indicates a double-free bug.",
region.start, region.end, overlap.start, overlap.end
);
}
if let Some(overlap) = check_overlap(region, pending_freed) {
panic!(
"DebugCheckingAllocator: Attempting to free region [{}, {}) which overlaps \
with region [{}, {}) already queued in the same bulk deallocation. \
This indicates a double-free bug.",
region.start, region.end, overlap.start, overlap.end
);
}
let overlapping_allocated: Vec<Range<u64>> = state
.allocated
.iter()
.filter(|r| overlaps(region, r))
.cloned()
.collect();
match overlapping_allocated.as_slice() {
[] => false,
[exact] if *exact == *region => true,
[single] => panic!(
"DebugCheckingAllocator: Attempting to partially free region [{}, {}) \
which is a subset or overlap of allocated region [{}, {}). \
Partial deallocations are not allowed.",
region.start, region.end, single.start, single.end,
),
_ => panic!(
"DebugCheckingAllocator: Attempting to free region [{}, {}) which spans \
multiple allocated regions. This is not a valid deallocation.",
region.start, region.end,
),
}
}
fn record_allocated_region(
state: &mut DebugState,
region: Range<u64>,
operation: &str,
allocator_context: &str,
) {
if region.is_empty() {
return;
}
if let Some(overlap) = check_overlap(®ion, &state.allocated) {
panic!(
"DebugCheckingAllocator: {operation} [{}, {}) overlaps with \
existing allocated region [{}, {}). This indicates a bug in the underlying \
allocator{allocator_context}.",
region.start, region.end, overlap.start, overlap.end
);
}
let overlapping_freed: Vec<Range<u64>> = state
.freed
.iter()
.filter(|r| overlaps(®ion, r))
.cloned()
.collect();
for freed_region in overlapping_freed {
state.freed.remove(&freed_region);
if freed_region.start < region.start {
state.freed.insert(freed_region.start..region.start);
}
if region.end < freed_region.end {
state.freed.insert(region.end..freed_region.end);
}
}
state.allocated.insert(region);
}
fn record_freed_region(state: &mut DebugState, region: Range<u64>) {
if region.is_empty() {
return;
}
if let Some(overlap) = check_overlap(®ion, &state.freed) {
panic!(
"DebugCheckingAllocator: Attempting to free region [{}, {}) which overlaps \
with already freed region [{}, {}). This indicates a double-free bug.",
region.start, region.end, overlap.start, overlap.end
);
}
state.freed.insert(region);
}
fn validate_initial_state(allocated: &mut HashSet<Range<u64>>, freed: &mut HashSet<Range<u64>>) {
allocated.retain(|r| !r.is_empty());
freed.retain(|r| !r.is_empty());
let allocated_vec: Vec<_> = allocated.iter().cloned().collect();
for i in 0..allocated_vec.len() {
for j in (i + 1)..allocated_vec.len() {
if overlaps(&allocated_vec[i], &allocated_vec[j]) {
panic!(
"DebugCheckingAllocator::with_state: Initial allocated set contains \
overlapping ranges [{}, {}) and [{}, {}). The initial state must be \
consistent.",
allocated_vec[i].start,
allocated_vec[i].end,
allocated_vec[j].start,
allocated_vec[j].end
);
}
}
}
let freed_vec: Vec<_> = freed.iter().cloned().collect();
for i in 0..freed_vec.len() {
for j in (i + 1)..freed_vec.len() {
if overlaps(&freed_vec[i], &freed_vec[j]) {
panic!(
"DebugCheckingAllocator::with_state: Initial freed set contains \
overlapping ranges [{}, {}) and [{}, {}). The initial state must be \
consistent.",
freed_vec[i].start, freed_vec[i].end, freed_vec[j].start, freed_vec[j].end
);
}
}
}
for alloc_range in allocated.iter() {
if let Some(freed_range) = check_overlap(alloc_range, freed) {
panic!(
"DebugCheckingAllocator::with_state: Initial state has allocated range \
[{}, {}) overlapping with freed range [{}, {}). The initial state must be \
consistent.",
alloc_range.start, alloc_range.end, freed_range.start, freed_range.end
);
}
}
}
pub struct DebugHandle<'a, A>
where
A: BStackAllocator<Error = io::Error>,
{
alloc: &'a DebugCheckingAllocator<A>,
inner: A::Allocated<'a>,
}
impl<'a, A> Clone for DebugHandle<'a, A>
where
A: BStackAllocator<Error = io::Error>,
{
fn clone(&self) -> Self {
*self
}
}
impl<'a, A> std::fmt::Debug for DebugHandle<'a, A>
where
A: BStackAllocator<Error = io::Error>,
A::Allocated<'a>: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DebugHandle")
.field("inner", &self.inner)
.finish()
}
}
impl<'a, A> Copy for DebugHandle<'a, A> where A: BStackAllocator<Error = io::Error> {}
impl<'a, A> DebugHandle<'a, A>
where
A: BStackAllocator<Error = io::Error>,
{
fn new(alloc: &'a DebugCheckingAllocator<A>, inner: A::Allocated<'a>) -> Self {
Self { alloc, inner }
}
pub fn inner(&self) -> &A::Allocated<'a> {
&self.inner
}
}
impl<'a, A> TryInto<BStackSlice<'a, DebugCheckingAllocator<A>>> for DebugHandle<'a, A>
where
A: BStackAllocator<Error = io::Error>,
{
type Error = io::Error;
fn try_into(self) -> Result<BStackSlice<'a, DebugCheckingAllocator<A>>, Self::Error> {
let slice: BStackSlice<'_, A> = self.inner.try_into().map_err(|e| {
io::Error::other(format!(
"inner handle is not convertible to BStackSlice: {e}"
))
})?;
Ok(unsafe { BStackSlice::from_raw_parts(self.alloc, slice.start(), slice.len()) })
}
}
struct DebugState {
allocated: HashSet<Range<u64>>,
freed: HashSet<Range<u64>>,
}
pub struct DebugCheckingAllocator<A>
where
A: BStackAllocator<Error = io::Error>,
{
inner: A,
state: Mutex<DebugState>,
}
impl<A> std::fmt::Debug for DebugCheckingAllocator<A>
where
A: BStackAllocator<Error = io::Error> + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
f.debug_struct("DebugCheckingAllocator")
.field("inner", &self.inner)
.field("allocated_count", &state.allocated.len())
.field("freed_count", &state.freed.len())
.finish()
}
}
impl<A> DebugCheckingAllocator<A>
where
A: BStackAllocator<Error = io::Error>,
{
pub fn new(inner: A) -> Self {
Self {
inner,
state: Mutex::new(DebugState {
allocated: HashSet::new(),
freed: HashSet::new(),
}),
}
}
pub fn with_state(
inner: A,
allocated: impl IntoIterator<Item = Range<u64>>,
freed: impl IntoIterator<Item = Range<u64>>,
) -> Self {
let mut allocated_set = allocated.into_iter().collect::<HashSet<_>>();
let mut freed_set = freed.into_iter().collect::<HashSet<_>>();
validate_initial_state(&mut allocated_set, &mut freed_set);
Self {
inner,
state: Mutex::new(DebugState {
allocated: allocated_set,
freed: freed_set,
}),
}
}
pub fn inner(&self) -> &A {
&self.inner
}
pub fn into_inner(self) -> A {
self.inner
}
fn record_allocation(&self, offset: u64, len: u64) {
let region = offset..offset + len;
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
record_allocated_region(&mut state, region, "Newly allocated region", "");
}
fn record_deallocation(&self, offset: u64, len: u64) {
if len == 0 {
return;
}
let region = offset..offset + len;
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
let was_in_allocated = check_deallocation(®ion, &state, &HashSet::new());
if was_in_allocated {
state.allocated.remove(®ion);
}
state.freed.insert(region);
}
}
impl<A> BStackAllocator for DebugCheckingAllocator<A>
where
A: BStackAllocator<Error = io::Error>,
{
type Error = io::Error;
type Allocated<'a>
= DebugHandle<'a, A>
where
A: 'a;
fn stack(&self) -> &BStack {
self.inner.stack()
}
fn into_stack(self) -> BStack {
self.inner.into_stack()
}
fn alloc(&self, len: u64) -> io::Result<Self::Allocated<'_>> {
let handle = self.inner.alloc(len)?;
let slice: BStackSlice<'_, A> = match handle.try_into() {
Ok(slice) => slice,
Err(e) => {
return match self.inner.dealloc(handle) {
Ok(()) => Err(io::Error::other(format!(
"allocated handle is not convertible to BStackSlice: {e}"
))),
Err(rollback_err) => Err(io::Error::other(format!(
"allocated handle is not convertible to BStackSlice: {e}; rollback via dealloc failed: {rollback_err}"
))),
};
}
};
self.record_allocation(slice.start(), slice.len());
Ok(DebugHandle::new(self, handle))
}
fn realloc<'a>(
&'a self,
handle: Self::Allocated<'a>,
new_len: u64,
) -> io::Result<Self::Allocated<'a>> {
let old_slice: BStackSlice<'_, A> = handle.inner.try_into().map_err(|e| {
io::Error::other(format!(
"handle is not convertible to BStackSlice before realloc: {e}"
))
})?;
let old_region = old_slice.start()..old_slice.start() + old_slice.len();
let new_inner_handle = self.inner.realloc(handle.inner, new_len)?;
let new_slice: BStackSlice<'_, A> = match new_inner_handle.try_into() {
Ok(slice) => slice,
Err(e) => {
return match self.inner.dealloc(new_inner_handle) {
Ok(()) => Err(io::Error::other(format!(
"reallocated handle is not convertible to BStackSlice: {e}"
))),
Err(rollback_err) => Err(io::Error::other(format!(
"reallocated handle is not convertible to BStackSlice: {e}; rollback via dealloc failed: {rollback_err}"
))),
};
}
};
let new_region = new_slice.start()..new_slice.start() + new_slice.len();
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
let overlapping_allocation = state
.allocated
.iter()
.find(|region| **region != old_region && overlaps(&new_region, region))
.cloned();
if let Some(overlap) = overlapping_allocation {
panic!(
"DebugCheckingAllocator: Reallocated region [{}, {}) overlaps with \
existing allocated region [{}, {}). This indicates a bug in the underlying \
allocator's realloc.",
new_region.start, new_region.end, overlap.start, overlap.end
);
}
state.allocated.remove(&old_region);
record_freed_region(&mut state, old_region);
record_allocated_region(&mut state, new_region, "Reallocated region", "'s realloc");
Ok(DebugHandle::new(self, new_inner_handle))
}
fn dealloc(&self, handle: Self::Allocated<'_>) -> io::Result<()> {
let slice: BStackSlice<'_, A> = match handle.inner.try_into() {
Ok(slice) => slice,
Err(e) => {
return match self.inner.dealloc(handle.inner) {
Ok(()) => Err(io::Error::other(format!(
"handle is not convertible to BStackSlice: {e}"
))),
Err(dealloc_err) => Err(io::Error::other(format!(
"handle is not convertible to BStackSlice: {e}; dealloc also failed: {dealloc_err}"
))),
};
}
};
let (offset, len) = (slice.start(), slice.len());
{
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
check_deallocation(&(offset..offset + len), &state, &HashSet::new());
}
self.inner.dealloc(handle.inner)?;
self.record_deallocation(offset, len);
Ok(())
}
}
impl<A> BStackBulkAllocator for DebugCheckingAllocator<A>
where
A: BStackBulkAllocator<Error = io::Error>,
{
fn alloc_bulk(&self, lengths: impl AsRef<[u64]>) -> io::Result<Vec<Self::Allocated<'_>>> {
let inner_handles = self.inner.alloc_bulk(lengths)?;
let mut slices: Vec<BStackSlice<'_, A>> = Vec::with_capacity(inner_handles.len());
for (i, &h) in inner_handles.iter().enumerate() {
match h.try_into() {
Ok(slice) => slices.push(slice),
Err(e) => {
return match self.inner.dealloc_bulk(&inner_handles) {
Ok(()) => Err(io::Error::other(format!(
"bulk-allocated handle {i} is not convertible to BStackSlice: {e}"
))),
Err(rollback_err) => Err(io::Error::other(format!(
"bulk-allocated handle {i} is not convertible to BStackSlice: {e}; rollback via dealloc_bulk failed: {rollback_err}"
))),
};
}
}
}
let mut result = Vec::with_capacity(inner_handles.len());
for (&h, slice) in inner_handles.iter().zip(slices) {
self.record_allocation(slice.start(), slice.len());
result.push(DebugHandle::new(self, h));
}
Ok(result)
}
fn dealloc_bulk<'a>(&'a self, handles: impl AsRef<[Self::Allocated<'a>]>) -> io::Result<()> {
let handles = handles.as_ref();
let mut slices: Vec<BStackSlice<'_, A>> = Vec::with_capacity(handles.len());
let mut pending_freed: HashSet<Range<u64>> = HashSet::with_capacity(handles.len());
{
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
for handle in handles {
let slice: BStackSlice<'_, A> = handle.inner.try_into().map_err(|e| {
io::Error::other(format!(
"handle is not convertible to BStackSlice during bulk dealloc: {e}"
))
})?;
let region = slice.start()..slice.start() + slice.len();
check_deallocation(®ion, &state, &pending_freed);
pending_freed.insert(region);
slices.push(slice);
}
}
let inner_handles: Vec<A::Allocated<'a>> = handles.iter().map(|h| h.inner).collect();
self.inner.dealloc_bulk(inner_handles)?;
for slice in &slices {
self.record_deallocation(slice.start(), slice.len());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockAllocator;
impl crate::alloc::BStackAllocator for MockAllocator {
type Error = io::Error;
type Allocated<'a> = crate::alloc::BStackSlice<'a, Self>;
fn stack(&self) -> &crate::BStack {
unimplemented!()
}
fn into_stack(self) -> crate::BStack {
unimplemented!()
}
fn alloc(&self, _len: u64) -> io::Result<Self::Allocated<'_>> {
unimplemented!()
}
fn realloc<'a>(
&'a self,
_: Self::Allocated<'a>,
_: u64,
) -> io::Result<Self::Allocated<'a>> {
unimplemented!()
}
fn dealloc(&self, _: Self::Allocated<'_>) -> io::Result<()> {
unimplemented!()
}
}
#[test]
fn test_region_overlap() {
assert!(overlaps(&(0..10), &(5..15))); assert!(overlaps(&(5..15), &(0..10))); assert!(!overlaps(&(0..10), &(10..20))); assert!(!overlaps(&(0..10), &(20..30))); assert!(overlaps(&(5..15), &(10..20))); }
#[test]
fn test_zero_length_regions() {
assert!(!overlaps(&(0..0), &(0..10))); assert!(!overlaps(&(0..10), &(0..0))); assert!(!overlaps(&(0..0), &(5..5))); }
fn checker() -> DebugCheckingAllocator<MockAllocator> {
DebugCheckingAllocator::new(MockAllocator)
}
#[test]
fn test_alloc_dealloc_basic() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
}
#[test]
fn test_adjacent_allocs_do_not_overlap() {
let c = checker();
c.record_allocation(0, 50);
c.record_allocation(50, 50); }
#[test]
fn test_untracked_disjoint_alloc_is_allowed() {
let c = DebugCheckingAllocator::with_state(MockAllocator, [0..100], [200..300]);
c.record_allocation(120, 50);
let state = c.state.lock().unwrap();
assert!(state.allocated.contains(&(0..100)));
assert!(state.allocated.contains(&(120..170)));
assert!(state.freed.contains(&(200..300)));
}
#[test]
fn test_dealloc_untracked_region_is_allowed() {
let c = checker();
c.record_deallocation(0, 100);
}
#[test]
#[should_panic(expected = "double-free")]
fn test_double_free_panics() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
c.record_deallocation(0, 100); }
#[test]
#[should_panic(expected = "double-free")]
fn test_partial_overlap_with_freed_region_panics() {
let c = checker();
c.record_deallocation(0, 100);
c.record_deallocation(50, 25);
}
#[test]
#[should_panic(expected = "overlaps with existing allocated region")]
fn test_overlapping_alloc_panics() {
let c = checker();
c.record_allocation(0, 100);
c.record_allocation(50, 100); }
#[test]
#[should_panic(expected = "Partial deallocations are not allowed")]
fn test_partial_free_panics() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 50); }
#[test]
#[should_panic(expected = "Partial deallocations are not allowed")]
fn test_superset_free_panics() {
let c = checker();
c.record_allocation(20, 50);
c.record_deallocation(0, 100); }
#[test]
#[should_panic(expected = "spans multiple allocated regions")]
fn test_spanning_free_panics() {
let c = checker();
c.record_allocation(0, 50);
c.record_allocation(50, 50);
c.record_deallocation(0, 100); }
#[test]
fn test_freed_region_split_on_reallocation() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
c.record_allocation(20, 30);
let state = c.state.lock().unwrap();
assert!(state.freed.contains(&(0..20)));
assert!(state.freed.contains(&(50..100)));
assert!(!state.freed.contains(&(0..100)));
assert!(state.allocated.contains(&(20..50)));
}
#[test]
fn test_freed_region_split_left_edge() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
c.record_allocation(0, 30);
let state = c.state.lock().unwrap();
assert!(!state.freed.contains(&(0..100)));
assert!(!state.freed.iter().any(|r| r.start < 30));
assert!(state.freed.contains(&(30..100)));
}
#[test]
fn test_freed_region_split_right_edge() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
c.record_allocation(70, 30);
let state = c.state.lock().unwrap();
assert!(!state.freed.contains(&(0..100)));
assert!(state.freed.contains(&(0..70)));
assert!(!state.freed.iter().any(|r| r.end > 70));
}
#[test]
fn test_freed_region_exact_reuse_removes_entry() {
let c = checker();
c.record_allocation(0, 100);
c.record_deallocation(0, 100);
c.record_allocation(0, 100);
let state = c.state.lock().unwrap();
assert!(state.freed.is_empty());
assert!(state.allocated.contains(&(0..100)));
}
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug, Clone, Default)]
struct MockAllocatorConfig {
fail_dealloc: bool,
fail_dealloc_bulk: bool,
fail_realloc: bool,
realloc_in_place: bool,
}
#[derive(Clone, Copy)]
struct MockHandle<'a> {
alloc: &'a ControllableMockAllocator,
offset: u64,
len: u64,
}
impl<'a> MockHandle<'a> {
fn new(alloc: &'a ControllableMockAllocator, offset: u64, len: u64) -> Self {
Self { alloc, offset, len }
}
}
impl<'a> TryInto<BStackSlice<'a, ControllableMockAllocator>> for MockHandle<'a> {
type Error = io::Error;
fn try_into(self) -> Result<BStackSlice<'a, ControllableMockAllocator>, Self::Error> {
Ok(unsafe { BStackSlice::from_raw_parts(self.alloc, self.offset, self.len) })
}
}
struct ControllableMockAllocator {
stack: BStack,
next_offset: Cell<u64>,
allocated: RefCell<HashSet<Range<u64>>>,
config: Rc<RefCell<MockAllocatorConfig>>,
}
impl ControllableMockAllocator {
fn new(stack: BStack, config: Rc<RefCell<MockAllocatorConfig>>) -> Self {
Self {
stack,
next_offset: Cell::new(0),
allocated: RefCell::new(HashSet::new()),
config,
}
}
}
impl BStackAllocator for ControllableMockAllocator {
type Error = io::Error;
type Allocated<'a> = MockHandle<'a>;
fn stack(&self) -> &BStack {
&self.stack
}
fn into_stack(self) -> BStack {
self.stack
}
fn alloc(&self, len: u64) -> io::Result<Self::Allocated<'_>> {
let offset = self.next_offset.get();
self.next_offset.set(offset + len);
let region = offset..offset + len;
self.allocated.borrow_mut().insert(region.clone());
Ok(MockHandle::new(self, offset, len))
}
fn realloc<'a>(
&'a self,
handle: Self::Allocated<'a>,
new_len: u64,
) -> io::Result<Self::Allocated<'a>> {
let config = self.config.borrow();
if config.fail_realloc {
return Err(io::Error::other("mock realloc failure"));
}
let realloc_in_place = config.realloc_in_place;
drop(config);
let old_region = handle.offset..handle.offset + handle.len;
self.allocated.borrow_mut().remove(&old_region);
let new_offset = if realloc_in_place {
let new_end = handle.offset + new_len;
if self.next_offset.get() < new_end {
self.next_offset.set(new_end);
}
handle.offset
} else {
let new_offset = self.next_offset.get();
self.next_offset.set(new_offset + new_len);
new_offset
};
let new_region = new_offset..new_offset + new_len;
self.allocated.borrow_mut().insert(new_region);
Ok(MockHandle::new(self, new_offset, new_len))
}
fn dealloc(&self, handle: Self::Allocated<'_>) -> io::Result<()> {
if self.config.borrow().fail_dealloc {
return Err(io::Error::other("mock dealloc failure"));
}
let region = handle.offset..handle.offset + handle.len;
self.allocated.borrow_mut().remove(®ion);
Ok(())
}
}
impl BStackBulkAllocator for ControllableMockAllocator {
fn alloc_bulk(&self, lengths: impl AsRef<[u64]>) -> io::Result<Vec<Self::Allocated<'_>>> {
lengths
.as_ref()
.iter()
.map(|&len| self.alloc(len))
.collect()
}
fn dealloc_bulk<'a>(
&'a self,
handles: impl AsRef<[Self::Allocated<'a>]>,
) -> io::Result<()> {
if self.config.borrow().fail_dealloc_bulk {
return Err(io::Error::other("mock dealloc_bulk failure"));
}
for &handle in handles.as_ref() {
let region = handle.offset..handle.offset + handle.len;
self.allocated.borrow_mut().remove(®ion);
}
Ok(())
}
}
fn create_test_stack() -> io::Result<(BStack, std::path::PathBuf)> {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_debug_test_{pid}_{id}.bin"));
let stack = BStack::open(&path)?;
Ok((stack, path))
}
struct TestGuard(std::path::PathBuf);
impl Drop for TestGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn test_alloc_success_updates_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handle1 = alloc.alloc(100)?;
let handle2 = alloc.alloc(200)?;
let state = alloc.state.lock().unwrap();
assert_eq!(state.allocated.len(), 2);
assert!(state.allocated.contains(&(0..100)));
assert!(state.allocated.contains(&(100..300)));
drop(state);
alloc.dealloc(handle1)?;
alloc.dealloc(handle2)?;
Ok(())
}
#[test]
fn test_realloc_success() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.contains(&(0..100)));
}
let new_handle = alloc.realloc(handle, 200)?;
{
let state = alloc.state.lock().unwrap();
assert!(!state.allocated.contains(&(0..100)));
assert!(state.allocated.contains(&(100..300)));
}
alloc.dealloc(new_handle)?;
Ok(())
}
#[test]
fn test_realloc_into_freed_region_updates_freed_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::with_state(inner, [], [150..300]);
let handle = alloc.alloc(100)?;
alloc.inner().next_offset.set(150);
let new_handle = alloc.realloc(handle, 100)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.contains(&(150..250)));
assert!(!state.allocated.contains(&(0..100)));
assert!(state.freed.contains(&(0..100)));
assert!(state.freed.contains(&(250..300)));
assert!(!state.freed.contains(&(150..300)));
}
alloc.dealloc(new_handle)?;
Ok(())
}
#[test]
fn test_realloc_in_place_shrink_marks_released_tail_as_freed() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig {
realloc_in_place: true,
..Default::default()
}));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100)?;
let new_handle = alloc.realloc(handle, 60)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.contains(&(0..60)));
assert!(!state.allocated.contains(&(0..100)));
assert!(state.freed.contains(&(60..100)));
}
alloc.dealloc(new_handle)?;
Ok(())
}
#[test]
#[should_panic(expected = "overlaps with already freed region")]
fn test_realloc_stale_handle_after_shrink_panics() {
let (stack, path) = create_test_stack().unwrap();
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig {
realloc_in_place: true,
..Default::default()
}));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100).unwrap();
let stale_handle = handle.clone();
let _new_handle = alloc.realloc(handle, 60).unwrap();
alloc.dealloc(stale_handle).unwrap();
}
#[test]
fn test_realloc_inner_failure_preserves_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.contains(&(0..100)));
}
config.borrow_mut().fail_realloc = true;
let result = alloc.realloc(handle, 200);
assert!(result.is_err());
{
let state = alloc.state.lock().unwrap();
assert!(
state.allocated.contains(&(0..100)),
"Original allocation should still be tracked after realloc failure"
);
}
Ok(())
}
#[test]
fn test_dealloc_success_updates_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.contains(&(0..100)));
assert!(!state.freed.contains(&(0..100)));
}
alloc.dealloc(handle)?;
{
let state = alloc.state.lock().unwrap();
assert!(!state.allocated.contains(&(0..100)));
assert!(state.freed.contains(&(0..100)));
}
Ok(())
}
#[test]
fn test_dealloc_inner_failure_preserves_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100)?;
config.borrow_mut().fail_dealloc = true;
let result = alloc.dealloc(handle);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("mock dealloc failure")
);
{
let state = alloc.state.lock().unwrap();
assert!(
state.allocated.contains(&(0..100)),
"Allocation should still be tracked after dealloc failure"
);
assert!(
!state.freed.contains(&(0..100)),
"Failed dealloc should not add region to freed set"
);
}
Ok(())
}
#[test]
#[should_panic(expected = "double-free")]
fn test_dealloc_double_free_via_public_api() {
let (stack, path) = create_test_stack().unwrap();
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handle = alloc.alloc(100).unwrap();
alloc.dealloc(handle).unwrap();
alloc.record_deallocation(0, 100);
}
#[test]
fn test_dealloc_untracked_region_via_public_api_is_allowed() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let fake_inner_handle = MockHandle::new(alloc.inner(), 500, 100);
let fake_handle = DebugHandle::new(&alloc, fake_inner_handle);
alloc.dealloc(fake_handle)?;
let state = alloc.state.lock().unwrap();
assert!(state.freed.contains(&(500..600)));
Ok(())
}
#[test]
fn test_alloc_bulk_success() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handles = alloc.alloc_bulk(&[100, 200, 300])?;
assert_eq!(handles.len(), 3);
{
let state = alloc.state.lock().unwrap();
assert_eq!(state.allocated.len(), 3);
assert!(state.allocated.contains(&(0..100)));
assert!(state.allocated.contains(&(100..300)));
assert!(state.allocated.contains(&(300..600)));
}
alloc.dealloc_bulk(handles)?;
Ok(())
}
#[test]
fn test_dealloc_bulk_success() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handles = alloc.alloc_bulk(&[100, 200, 300])?;
alloc.dealloc_bulk(handles)?;
{
let state = alloc.state.lock().unwrap();
assert!(state.allocated.is_empty());
assert_eq!(state.freed.len(), 3);
assert!(state.freed.contains(&(0..100)));
assert!(state.freed.contains(&(100..300)));
assert!(state.freed.contains(&(300..600)));
}
Ok(())
}
#[test]
fn test_dealloc_bulk_inner_failure_preserves_tracking() -> io::Result<()> {
let (stack, path) = create_test_stack()?;
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config.clone());
let alloc = DebugCheckingAllocator::new(inner);
let handles = alloc.alloc_bulk(&[100, 200, 300])?;
config.borrow_mut().fail_dealloc_bulk = true;
let result = alloc.dealloc_bulk(&handles);
assert!(result.is_err());
{
let state = alloc.state.lock().unwrap();
assert_eq!(
state.allocated.len(),
3,
"all regions should still be allocated"
);
assert!(
state.freed.is_empty(),
"no regions should be freed after inner failure"
);
}
Ok(())
}
#[test]
#[should_panic(expected = "double-free")]
fn test_dealloc_bulk_double_free_panics() {
let (stack, path) = create_test_stack().unwrap();
let _guard = TestGuard(path);
let config = Rc::new(RefCell::new(MockAllocatorConfig::default()));
let inner = ControllableMockAllocator::new(stack, config);
let alloc = DebugCheckingAllocator::new(inner);
let handles = alloc.alloc_bulk(&[100, 200]).unwrap();
alloc.dealloc_bulk(&handles).unwrap();
alloc.record_deallocation(0, 100);
}
#[test]
fn test_with_state_valid_disjoint_ranges() {
let c = DebugCheckingAllocator::with_state(MockAllocator, [0..100, 200..300], [400..500]);
let state = c.state.lock().unwrap();
assert_eq!(state.allocated.len(), 2);
assert_eq!(state.freed.len(), 1);
}
#[test]
fn test_with_state_filters_empty_ranges() {
let c = DebugCheckingAllocator::with_state(
MockAllocator,
[0..100, 100..100, 200..300],
[400..400, 500..600],
);
let state = c.state.lock().unwrap();
assert_eq!(state.allocated.len(), 2);
assert_eq!(state.freed.len(), 1);
assert!(!state.allocated.contains(&(100..100)));
assert!(!state.freed.contains(&(400..400)));
}
#[test]
#[should_panic(expected = "Initial allocated set contains overlapping ranges")]
fn test_with_state_panics_on_overlapping_allocated() {
DebugCheckingAllocator::with_state(
MockAllocator,
[0..100, 50..150], [],
);
}
#[test]
#[should_panic(expected = "Initial freed set contains overlapping ranges")]
fn test_with_state_panics_on_overlapping_freed() {
DebugCheckingAllocator::with_state(
MockAllocator,
[],
[0..100, 50..150], );
}
#[test]
#[should_panic(expected = "allocated range")]
fn test_with_state_panics_on_allocated_freed_overlap() {
DebugCheckingAllocator::with_state(
MockAllocator,
[0..100, 200..300],
[50..150, 400..500], );
}
#[test]
fn test_with_state_valid_adjacent_ranges() {
let c = DebugCheckingAllocator::with_state(
MockAllocator,
[0..100, 100..200],
[200..300, 300..400],
);
let state = c.state.lock().unwrap();
assert_eq!(state.allocated.len(), 2);
assert_eq!(state.freed.len(), 2);
}
}