use crate::Buffer;
use crate::allocator::{Allocator, BufferOptions, CpuAllocator, LruAllocator};
use morok_dtype::test::proptests::generators;
use morok_dtype::{DType, ScalarDType};
use proptest::prelude::*;
use std::sync::Arc;
use strum::VariantArray;
use tinyvec::ArrayVec;
fn allocator() -> Arc<LruAllocator> {
Arc::new(LruAllocator::new(Box::new(CpuAllocator)))
}
#[derive(Debug, Clone)]
struct BufferSpec {
dtype: DType,
shape: ArrayVec<[usize; 4]>,
zero_init: bool,
}
impl BufferSpec {
fn size(&self) -> usize {
self.dtype.bytes() * self.shape.iter().product::<usize>()
}
fn alloc<A: Allocator + 'static>(&self, allocator: Arc<A>) -> Result<Buffer, crate::Error> {
let options = BufferOptions { zero_init: self.zero_init, ..Default::default() };
Buffer::allocate(allocator, self.dtype.clone(), self.shape.to_vec(), options)
}
}
impl Arbitrary for BufferSpec {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
generators::scalar_generator().prop_map(DType::Scalar),
prop::collection::vec(1usize..50, 1..=4),
any::<bool>(),
)
.prop_map(|(dtype, shape, zero_init)| BufferSpec { dtype, shape: ArrayVec::from_iter(shape), zero_init })
.prop_filter("total size must be reasonable", |spec| (1..=10 * 1024 * 1024).contains(&spec.size()))
.boxed()
}
}
fn same_size_dtypes(dtype: DType) -> impl Strategy<Value = DType> {
let dtypes = ScalarDType::VARIANTS
.iter()
.filter(|s| s.bytes() == dtype.bytes())
.map(|s| DType::Scalar(*s))
.filter(|d| *d != dtype)
.collect::<Vec<_>>();
proptest::sample::select(dtypes)
}
fn same_size_specs() -> impl Strategy<Value = (BufferSpec, BufferSpec)> {
any::<BufferSpec>().prop_flat_map(|spec| {
let total_bytes = spec.size();
let dtype = spec.dtype.clone();
same_size_dtypes(dtype).prop_map(move |dtype| {
let num_elements = total_bytes / dtype.bytes();
let shape = ArrayVec::from_iter(vec![num_elements]);
let spec2 = BufferSpec { dtype, shape, zero_init: spec.zero_init };
(spec.clone(), spec2)
})
})
}
proptest! {
#[test]
fn dtype_shape_isolation_on_reuse((spec1, spec2) in same_size_specs()) {
let alloc = allocator();
let ptr1 = {
spec1.alloc(alloc.clone())?.raw_data_ptr()
};
let buffer2 = spec2.alloc(alloc)?;
prop_assert_eq!(ptr1, buffer2.raw_data_ptr(), "buffer2 should reuse buffer1's RawBuffer from cache");
prop_assert_eq!(buffer2.dtype(), spec2.dtype.clone(), "dtype must be from spec2, not spec1");
prop_assert_eq!(buffer2.size(), spec2.size(), "size must be from spec2, not spec1");
}
#[test]
fn cache_respects_capacity(specs in prop::collection::vec(any::<BufferSpec>(), 1..=4)) {
let alloc = allocator();
for spec in specs {
spec.alloc(Arc::clone(&alloc))?;
}
}
#[test]
fn views_share_backing_buffer(spec: BufferSpec) {
prop_assume!(spec.size() >= 20);
let alloc = allocator();
{
let buffer = spec.alloc(alloc.clone())?;
let _view = buffer.view(0, spec.size().min(10))?;
}
prop_assert_eq!(alloc.cache_count(spec.size(), true), 1, "Expected exactly 1 buffer cached after dropping buffer+view");
let _buffer2 = spec.alloc(alloc.clone())?;
prop_assert_eq!(alloc.cache_count(spec.size(), true), 0, "Expected cache to be empty after reusing buffer");
}
#[test]
fn zero_init_with_cache_reuse(spec: BufferSpec) {
let alloc = allocator();
{
let _buffer1 = Buffer::allocate(
alloc.clone() as Arc<dyn Allocator>,
spec.dtype.clone(),
spec.shape.to_vec(),
BufferOptions { zero_init: false, cpu_accessible: false },
)?;
}
prop_assert_eq!(alloc.cache_count(spec.size(), false), 1, "Buffer should be cached");
let _buffer2 = Buffer::allocate(
alloc.clone() as Arc<dyn Allocator>,
spec.dtype.clone(),
spec.shape.to_vec(),
BufferOptions { zero_init: true, cpu_accessible: false },
)?;
prop_assert_eq!(alloc.cache_count(spec.size(), false), 0, "Cache should be empty after reuse");
}
}