#![cfg(all(test, feature = "cuda"))]
use super::context::CudaContext;
use super::memory::GpuBuffer;
use crate::GpuError;
use proptest::prelude::*;
#[test]
fn test_zero_sized_buffer() {
let ctx = CudaContext::new(0).expect("Context");
let buf_result = GpuBuffer::<f32>::new(&ctx, 0);
if let Ok(mut buf) = buf_result {
assert_eq!(buf.len(), 0);
let src: Vec<f32> = vec![];
buf.copy_from_host(&src)
.expect("Zero-byte copy should succeed");
}
}
#[test]
fn test_unaligned_byte_copy() {
let ctx = CudaContext::new(0).expect("Context");
let len = 1024;
let mut buf = GpuBuffer::<u8>::new(&ctx, len).expect("Alloc");
let data: Vec<u8> = (0..len).map(|i| (i % 255) as u8).collect();
buf.copy_from_host(&data).expect("Copy");
let mut out = vec![0u8; len];
buf.copy_to_host(&mut out).expect("Download");
assert_eq!(data, out);
}
#[test]
fn test_oom_resilience() {
let ctx = CudaContext::new(0).expect("Context");
let (free_start, _) = ctx.memory_info().expect("Mem info");
let mut allocations = Vec::new();
let chunk_size = 1024 * 1024 * 1024 / 4;
let mut hit_oom = false;
for i in 0..30 {
match GpuBuffer::<f32>::new(&ctx, chunk_size) {
Ok(buf) => allocations.push(buf),
Err(GpuError::OutOfMemory { .. }) => {
hit_oom = true;
println!("Hit OOM at chunk {}", i);
break;
}
Err(GpuError::MemoryAllocation(msg)) if msg.contains("OUT_OF_MEMORY") => {
hit_oom = true;
println!("Hit OOM (MemoryAllocation) at chunk {}", i);
break;
}
Err(e) => panic!("Unexpected error during OOM stress: {:?}", e),
}
}
drop(allocations);
let (free_end, _) = ctx.memory_info().expect("Mem info");
let diff = if free_start > free_end {
free_start - free_end
} else {
0
};
assert!(
diff < 100 * 1024 * 1024,
"Memory leak detected! {} bytes missing",
diff
);
}
proptest! {
#[test]
fn test_buffer_roundtrip_fuzz(
len in 1usize..100_000usize,
val in any::<f32>()
) {
if let Ok(ctx) = CudaContext::new(0) {
let mut buf = GpuBuffer::<f32>::new(&ctx, len).unwrap();
let data = vec![val; len];
buf.copy_from_host(&data).unwrap();
let mut out = vec![0.0; len];
buf.copy_to_host(&mut out).unwrap();
prop_assert_eq!(data[0], out[0]);
prop_assert_eq!(data[len/2], out[len/2]);
prop_assert_eq!(data[len-1], out[len-1]);
}
}
}
#[test]
fn test_alloc_oversize_100gb() {
let ctx = CudaContext::new(0).expect("Context");
let oversize = 25_000_000_000usize;
let result = GpuBuffer::<f32>::new(&ctx, oversize);
match result {
Err(GpuError::OutOfMemory { .. }) => {
}
Err(GpuError::MemoryAllocation(_)) => {
}
Err(e) => {
println!("Oversize alloc returned: {:?}", e);
}
Ok(_) => {
panic!("CRITICAL: 100GB allocation succeeded - this should be impossible on RTX 4090!");
}
}
}
#[test]
fn test_copy_from_host_too_small() {
let ctx = CudaContext::new(0).expect("Context");
let mut buf = GpuBuffer::<f32>::new(&ctx, 1000).expect("Alloc");
let small_data = vec![1.0f32; 500];
let result = buf.copy_from_host(&small_data);
assert!(
result.is_err(),
"copy_from_host should fail when host buffer is smaller"
);
if let Err(e) = result {
assert!(
format!("{:?}", e).contains("mismatch") || format!("{:?}", e).contains("Transfer"),
"Error should mention size mismatch: {:?}",
e
);
}
}
#[test]
fn test_copy_to_host_too_large() {
let ctx = CudaContext::new(0).expect("Context");
let buf = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc");
let mut large_data = vec![0.0f32; 500];
let result = buf.copy_to_host(&mut large_data);
assert!(
result.is_err(),
"copy_to_host should fail when host buffer size doesn't match"
);
}
#[test]
fn test_copy_from_host_at_out_of_bounds() {
let ctx = CudaContext::new(0).expect("Context");
let mut buf = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc");
let data = vec![1.0f32; 50];
let result = buf.copy_from_host_at(&data, 60);
assert!(
result.is_err(),
"copy_from_host_at should fail when offset+len > buffer size"
);
}
#[test]
fn test_copy_to_host_at_out_of_bounds() {
let ctx = CudaContext::new(0).expect("Context");
let data = vec![1.0f32; 100];
let buf = GpuBuffer::from_host(&ctx, &data).expect("Alloc");
let mut result = vec![0.0f32; 50];
let copy_result = buf.copy_to_host_at(&mut result, 60);
assert!(
copy_result.is_err(),
"copy_to_host_at should fail when offset+len > buffer size"
);
}
#[test]
fn test_d2d_copy_size_mismatch() {
let ctx = CudaContext::new(0).expect("Context");
let src = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 200).expect("Alloc dst");
let result = dst.copy_from_buffer(&src);
assert!(
result.is_err(),
"D2D copy should fail when buffer sizes don't match"
);
}
#[test]
fn test_d2d_copy_at_dst_out_of_bounds() {
let ctx = CudaContext::new(0).expect("Context");
let src = GpuBuffer::<f32>::new(&ctx, 50).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc dst");
let result = dst.copy_from_buffer_at(&src, 60, 0, 50);
assert!(
result.is_err(),
"D2D copy_at should fail when dst_offset+count > dst.len"
);
}
#[test]
fn test_d2d_copy_at_src_out_of_bounds() {
let ctx = CudaContext::new(0).expect("Context");
let src = GpuBuffer::<f32>::new(&ctx, 50).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc dst");
let result = dst.copy_from_buffer_at(&src, 0, 30, 50);
assert!(
result.is_err(),
"D2D copy_at should fail when src_offset+count > src.len"
);
}
#[test]
fn test_raii_cleanup_single_buffer() {
let ctx = CudaContext::new(0).expect("Context");
let (free_before, _) = ctx.memory_info().expect("Memory info");
let size = 25_000_000; {
let _buf = GpuBuffer::<f32>::new(&ctx, size).expect("Alloc");
let (free_during, _) = ctx.memory_info().expect("Memory info");
assert!(
free_during < free_before,
"Memory should decrease after allocation: before={}, during={}",
free_before,
free_during
);
}
let (free_after, _) = ctx.memory_info().expect("Memory info");
let tolerance = 10 * 1024 * 1024;
assert!(
free_after >= free_before - tolerance,
"Memory leak detected! before={}, after={}, diff={}",
free_before,
free_after,
free_before.saturating_sub(free_after)
);
}
#[test]
fn test_async_d2d_copy_size_mismatch() {
use super::stream::CudaStream;
let ctx = CudaContext::new(0).expect("Context");
let stream = CudaStream::new(&ctx).expect("Stream");
let src = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 200).expect("Alloc dst");
let result = unsafe { dst.copy_from_buffer_async(&src, &stream) };
assert!(
result.is_err(),
"Async D2D copy should fail when buffer sizes don't match"
);
}
#[test]
fn test_async_d2d_copy_at_out_of_bounds() {
use super::stream::CudaStream;
let ctx = CudaContext::new(0).expect("Context");
let stream = CudaStream::new(&ctx).expect("Stream");
let src = GpuBuffer::<f32>::new(&ctx, 50).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc dst");
let result = unsafe { dst.copy_from_buffer_at_async(&src, 60, 0, 50, &stream) };
assert!(
result.is_err(),
"Async D2D copy_at should fail when dst out of bounds"
);
let result = unsafe { dst.copy_from_buffer_at_async(&src, 0, 30, 50, &stream) };
assert!(
result.is_err(),
"Async D2D copy_at should fail when src out of bounds"
);
}
#[test]
fn test_async_h2d_copy_size_mismatch() {
use super::stream::CudaStream;
let ctx = CudaContext::new(0).expect("Context");
let stream = CudaStream::new(&ctx).expect("Stream");
let mut buf = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc");
let small_data = vec![1.0f32; 50];
let result = unsafe { buf.copy_from_host_async(&small_data, &stream) };
assert!(
result.is_err(),
"Async H2D copy should fail when host buffer size doesn't match"
);
}
#[test]
fn test_async_d2h_copy_size_mismatch() {
use super::stream::CudaStream;
let ctx = CudaContext::new(0).expect("Context");
let stream = CudaStream::new(&ctx).expect("Stream");
let buf = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc");
let mut large_data = vec![0.0f32; 200];
let result = unsafe { buf.copy_to_host_async(&mut large_data, &stream) };
assert!(
result.is_err(),
"Async D2H copy should fail when host buffer size doesn't match"
);
}
#[test]
fn test_empty_buffer_operations() {
let ctx = CudaContext::new(0).expect("Context");
let mut empty_buf = GpuBuffer::<f32>::new(&ctx, 0).expect("Alloc empty");
assert!(empty_buf.is_empty());
assert_eq!(empty_buf.len(), 0);
assert_eq!(empty_buf.size_bytes(), 0);
let empty_data: Vec<f32> = vec![];
empty_buf
.copy_from_host(&empty_data)
.expect("Empty H2D should succeed");
let mut empty_out: Vec<f32> = vec![];
empty_buf
.copy_to_host(&mut empty_out)
.expect("Empty D2H should succeed");
let mut empty_dst = GpuBuffer::<f32>::new(&ctx, 0).expect("Alloc empty dst");
empty_dst
.copy_from_buffer(&empty_buf)
.expect("Empty D2D should succeed");
}
#[test]
fn test_partial_copy_zero_count() {
let ctx = CudaContext::new(0).expect("Context");
let src = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc dst");
dst.copy_from_buffer_at(&src, 0, 0, 0)
.expect("Zero count D2D should succeed");
dst.copy_from_buffer_at(&src, 50, 50, 0)
.expect("Zero count D2D with offsets should succeed");
}
#[test]
fn test_async_raw_copy_bounds_check() {
use super::stream::CudaStream;
let ctx = CudaContext::new(0).expect("Context");
let stream = CudaStream::new(&ctx).expect("Stream");
let src = GpuBuffer::<f32>::new(&ctx, 50).expect("Alloc src");
let mut dst = GpuBuffer::<f32>::new(&ctx, 100).expect("Alloc dst");
let stream_handle = stream.raw();
let result = unsafe { dst.copy_from_buffer_at_async_raw(&src, 60, 0, 50, stream_handle) };
assert!(
result.is_err(),
"Async raw D2D should fail when dst out of bounds"
);
let result = unsafe { dst.copy_from_buffer_at_async_raw(&src, 0, 30, 50, stream_handle) };
assert!(
result.is_err(),
"Async raw D2D should fail when src out of bounds"
);
unsafe {
dst.copy_from_buffer_at_async_raw(&src, 0, 0, 0, stream_handle)
.expect("Zero count should succeed");
}
}
#[test]
fn test_buffer_view_properties() {
let ctx = CudaContext::new(0).expect("Context");
let buf = GpuBuffer::<f32>::new(&ctx, 256).expect("Alloc");
let view = buf.clone_metadata();
assert_eq!(view.as_ptr(), buf.as_ptr());
assert_eq!(view.len(), buf.len());
assert_eq!(view.is_empty(), buf.is_empty());
assert_eq!(view.size_bytes(), buf.size_bytes());
drop(view);
assert_eq!(buf.len(), 256);
}
#[test]
fn test_stress_alloc_dealloc_cycle() {
let ctx = CudaContext::new(0).expect("Context");
let (free_start, _) = ctx.memory_info().expect("Memory info");
for i in 0..100 {
let size = (i + 1) * 1000; let _buf = GpuBuffer::<f32>::new(&ctx, size).expect("Alloc");
}
let (free_end, _) = ctx.memory_info().expect("Memory info");
let tolerance = 50 * 1024 * 1024; assert!(
free_end >= free_start - tolerance,
"Memory leak after 100 alloc/dealloc cycles! start={}, end={}, leaked={}",
free_start,
free_end,
free_start.saturating_sub(free_end)
);
}