use super::alloc_guard::*;
use serial_test::serial;
use std::alloc::{dealloc, Layout};
#[test]
fn test_alloc_guard_basic() {
let layout = Layout::from_size_align(1024, 8).unwrap();
let guard = AllocGuard::new(layout).expect("allocation failed");
assert!(!guard.as_ptr().is_null());
assert_eq!(guard.layout().size(), 1024);
assert_eq!(guard.layout().align(), 8);
}
#[test]
fn test_alloc_guard_into_raw() {
let layout = Layout::from_size_align(64, 8).unwrap();
let guard = AllocGuard::new(layout).expect("allocation failed");
let ptr = guard.into_raw();
assert!(!ptr.is_null());
unsafe {
dealloc(ptr, layout);
}
}
#[test]
fn test_alloc_guard_zero_size() {
let layout = Layout::from_size_align(0, 1).unwrap();
assert!(AllocGuard::new(layout).is_none());
}
#[test]
fn test_alloc_guard_aligned() {
let layout = Layout::from_size_align(256, 64).unwrap();
let guard = AllocGuard::new(layout).expect("allocation failed");
let addr = guard.as_ptr() as usize;
assert_eq!(addr % 64, 0, "Not cache-line aligned");
}
#[test]
fn test_alloc_guard_cast() {
let layout =
Layout::from_size_align(std::mem::size_of::<f32>() * 10, std::mem::align_of::<f32>())
.unwrap();
let guard = AllocGuard::new(layout).expect("allocation failed");
let float_ptr: *mut f32 = guard.cast();
#[allow(clippy::cast_precision_loss)]
unsafe {
for i in 0..10 {
*float_ptr.add(i) = i as f32;
}
}
#[allow(clippy::cast_precision_loss, clippy::float_cmp)]
unsafe {
for i in 0..10 {
assert_eq!(*float_ptr.add(i), i as f32);
}
}
}
#[test]
fn test_alloc_guard_drop_frees_memory() {
for _ in 0..1000 {
let layout = Layout::from_size_align(1024, 8).unwrap();
let guard = AllocGuard::new(layout);
assert!(
guard.is_some(),
"1 KiB allocation must succeed under default ceiling"
);
}
}
#[test]
fn test_alloc_guard_panic_safety() {
use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
static GUARD_BUILT: AtomicBool = AtomicBool::new(false);
let layout = Layout::from_size_align(1024, 8).unwrap();
GUARD_BUILT.store(false, Ordering::SeqCst);
let result = panic::catch_unwind(|| {
let guard = AllocGuard::new(layout).expect("allocation failed");
assert!(!guard.as_ptr().is_null());
GUARD_BUILT.store(true, Ordering::SeqCst);
panic!("simulated panic");
});
assert!(result.is_err());
assert!(
GUARD_BUILT.load(Ordering::SeqCst),
"AllocGuard::new must produce a valid allocation before the panic, so its \
Drop runs during unwind"
);
}
#[test]
#[serial]
fn test_default_ceiling_is_high_backstop() {
let saved = alloc_byte_limit();
set_alloc_byte_limit(0); assert_eq!(alloc_byte_limit(), DEFAULT_ALLOC_BYTE_LIMIT);
assert_eq!(DEFAULT_ALLOC_BYTE_LIMIT, 1024 * 1024 * 1024 * 1024);
set_alloc_byte_limit(saved);
}
#[test]
#[serial]
fn test_alloc_guard_rejects_above_ceiling() {
let saved = alloc_byte_limit();
set_alloc_byte_limit(0);
let limit = alloc_byte_limit();
assert_eq!(limit, DEFAULT_ALLOC_BYTE_LIMIT);
let oversized = Layout::from_size_align(limit + 1, 8).unwrap();
assert!(AllocGuard::new(oversized).is_none());
assert!(AllocGuard::new_zeroed(oversized).is_none());
let ok = Layout::from_size_align(4096, 64).unwrap();
assert!(AllocGuard::new(ok).is_some());
assert!(AllocGuard::new_zeroed(ok).is_some());
set_alloc_byte_limit(saved);
}
#[test]
#[serial]
fn test_check_alloc_bound() {
let saved = alloc_byte_limit();
set_alloc_byte_limit(0);
let limit = alloc_byte_limit();
assert!(check_alloc_bound(limit).is_ok());
assert!(check_alloc_bound(0).is_ok());
assert!(check_alloc_bound(limit + 1).is_err());
set_alloc_byte_limit(saved);
}
#[test]
#[serial]
fn test_large_legit_buffer_not_falsely_rejected() {
const GIB: usize = 1024 * 1024 * 1024;
let saved = alloc_byte_limit();
set_alloc_byte_limit(0);
for gib in [20usize, 64, 128, 512] {
let bytes = gib * GIB;
assert!(
check_alloc_bound(bytes).is_ok(),
"{gib} GiB single buffer must not be falsely rejected"
);
}
set_alloc_byte_limit(saved);
}
#[test]
fn test_load_path_bound_allows_realistic_large_count() {
with_alloc_byte_limit(4096, || {
let file_backed_bytes = 30usize * 1024 * 1024 * 1024;
let inner = with_min_alloc_byte_limit(file_backed_bytes, || {
assert!(check_alloc_bound(file_backed_bytes).is_ok());
alloc_byte_limit()
});
assert_eq!(inner, file_backed_bytes, "ceiling raised within load scope");
assert_eq!(alloc_byte_limit(), 4096);
});
}
#[test]
#[serial]
fn test_with_min_alloc_byte_limit_passthrough() {
let saved = alloc_byte_limit();
set_alloc_byte_limit(0); let before = alloc_byte_limit();
let observed = with_min_alloc_byte_limit(1024, alloc_byte_limit);
assert_eq!(
observed, before,
"no raise needed; ceiling unchanged in scope"
);
assert_eq!(alloc_byte_limit(), before);
set_alloc_byte_limit(saved);
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_scoped_raise_is_not_visible_to_other_threads() {
const TWO_TIB: usize = 2 * 1024 * 1024 * 1024 * 1024;
let expected = alloc_byte_limit();
let observed = with_min_alloc_byte_limit(TWO_TIB, || {
assert_eq!(alloc_byte_limit(), TWO_TIB, "raise applies to this thread");
std::thread::spawn(|| (alloc_byte_limit(), check_alloc_bound(TWO_TIB).is_err()))
.join()
.expect("observer thread panicked")
});
assert_eq!(
observed.0, expected,
"scoped raise leaked to another thread: the allocation backstop was \
silently lifted process-wide for the duration of the scope"
);
assert!(
observed.1,
"an oversized allocation must still be rejected on threads outside the scope"
);
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_overlapping_scoped_raises_do_not_clobber() {
use std::sync::{Arc, Barrier};
const TWO_TIB: usize = 2 * 1024 * 1024 * 1024 * 1024;
const THREE_TIB: usize = 3 * 1024 * 1024 * 1024 * 1024;
let before = alloc_byte_limit();
let (entered_a, entered_b, exited_a) = (
Arc::new(Barrier::new(2)),
Arc::new(Barrier::new(2)),
Arc::new(Barrier::new(2)),
);
let (a1, b1, x1) = (
Arc::clone(&entered_a),
Arc::clone(&entered_b),
Arc::clone(&exited_a),
);
let thread_a = std::thread::spawn(move || {
with_min_alloc_byte_limit(TWO_TIB, || {
a1.wait(); b1.wait(); });
x1.wait(); });
let thread_b = std::thread::spawn(move || {
entered_a.wait(); with_min_alloc_byte_limit(THREE_TIB, || {
entered_b.wait(); exited_a.wait(); });
});
thread_a.join().expect("thread A panicked");
thread_b.join().expect("thread B panicked");
assert_eq!(
alloc_byte_limit(),
before,
"overlapping scopes corrupted the ceiling: it must be exactly as it was \
before both scopes ran, not a value republished by a lost update"
);
}
#[test]
fn test_low_scoped_ceiling_does_not_break_other_threads() {
const LEGITIMATE_BYTES: usize = 4 * 100_000 * std::mem::size_of::<f32>();
let admitted = with_alloc_byte_limit(4096, || {
assert!(
check_alloc_bound(LEGITIMATE_BYTES).is_err(),
"the pinned ceiling must still be enforced on the pinning thread"
);
std::thread::spawn(|| check_alloc_bound(LEGITIMATE_BYTES).is_ok())
.join()
.expect("observer thread panicked")
});
assert!(
admitted,
"a ceiling pinned low for one operation must not reject legitimate \
allocations on unrelated threads"
);
}
#[test]
fn test_scoped_alloc_byte_limit_nests() {
with_alloc_byte_limit(8192, || {
assert_eq!(alloc_byte_limit(), 8192);
with_alloc_byte_limit(4096, || {
assert_eq!(alloc_byte_limit(), 4096);
});
assert_eq!(
alloc_byte_limit(),
8192,
"inner scope restored the outer one"
);
});
}
#[test]
fn test_with_min_alloc_byte_limit_restores_on_panic() {
use std::panic;
with_alloc_byte_limit(4096, || {
let huge = 30usize * 1024 * 1024 * 1024;
let result = panic::catch_unwind(|| {
with_min_alloc_byte_limit(huge, || {
panic!("simulated load failure");
});
});
assert!(result.is_err());
assert_eq!(alloc_byte_limit(), 4096, "ceiling restored after panic");
});
}