use super::*;
use proptest::prelude::*;
use std::sync::Arc;
use std::thread;
prop_compose! {
fn buffer_config_strategy()(
buffer_power in 10u32..20u32,
slot_size in 64usize..65536usize,
) -> RingBufferConfig {
let buffer_size = 1usize << buffer_power; RingBufferConfig {
buffer_size: BufferSize::try_new(buffer_size).unwrap(),
slot_size: SlotSize::try_new(slot_size).unwrap(),
}
}
}
prop_compose! {
fn data_strategy(max_size: usize)(
data in prop::collection::vec(any::<u8>(), 1..=max_size)
) -> Vec<u8> {
data
}
}
proptest! {
#[test]
fn prop_ring_buffer_never_loses_data_under_capacity(
config in buffer_config_strategy(),
data_sets in prop::collection::vec(
prop::collection::vec(any::<u8>(), 1..=1024), 1..=10 )
) {
let ring_buffer = RingBuffer::new(&config);
let mut written_data = Vec::new();
let mut request_ids = Vec::new();
let actual_slot_count = {
let calculated = config.buffer_size.as_ref() / config.slot_size.as_ref();
let next_pow2 = calculated.next_power_of_two();
if next_pow2 > calculated {
next_pow2 / 2
} else {
next_pow2
}.max(1)
};
for (i, data) in data_sets.iter().enumerate() {
if i >= actual_slot_count {
break;
}
let request_id = RequestId::new();
request_ids.push(request_id);
if data.len() <= *config.slot_size.as_ref() {
prop_assert!(ring_buffer.write(request_id, data).is_ok());
written_data.push((request_id, data.clone()));
}
}
let mut read_data = Vec::new();
while let Some((id, data)) = ring_buffer.read() {
read_data.push((id, data));
}
prop_assert_eq!(written_data.len(), read_data.len());
for (id, data) in &written_data {
prop_assert!(read_data.iter().any(|(read_id, read_data)|
read_id == id && read_data == data
));
}
}
#[test]
fn prop_concurrent_writes_are_thread_safe(
config in buffer_config_strategy(),
thread_count in 2usize..=8usize,
writes_per_thread in 10usize..=50usize,
) {
let ring_buffer = Arc::new(RingBuffer::new(&config));
let mut handles = Vec::new();
for thread_id in 0..thread_count {
let rb = Arc::clone(&ring_buffer);
let handle = thread::spawn(move || {
let mut successful_writes = 0;
for i in 0..writes_per_thread {
let data = format!("thread-{thread_id}-write-{i}").into_bytes();
let request_id = RequestId::new();
if rb.write(request_id, &data).is_ok() {
successful_writes += 1;
}
}
successful_writes
});
handles.push(handle);
}
let total_successful_writes: usize = handles
.into_iter()
.map(|h| h.join().unwrap())
.sum();
let mut read_count = 0;
while ring_buffer.read().is_some() {
read_count += 1;
}
prop_assert_eq!(total_successful_writes, read_count);
}
#[test]
fn prop_data_too_large_is_truncated(
config in buffer_config_strategy(),
excess in 1usize..=1000usize,
) {
let ring_buffer = RingBuffer::new(&config);
let request_id = RequestId::new();
let max_data_size = config.slot_size.as_ref();
let oversized_data = vec![42u8; max_data_size + excess];
prop_assert!(ring_buffer.write(request_id, &oversized_data).is_ok());
if let Some((read_id, read_data)) = ring_buffer.read() {
prop_assert_eq!(read_id, request_id);
prop_assert_eq!(read_data.len(), *max_data_size);
prop_assert_eq!(&read_data[..], &oversized_data[..*max_data_size]);
} else {
panic!("Expected to read data back");
}
}
#[test]
fn prop_ring_buffer_handles_wraparound(
config in buffer_config_strategy().prop_filter("Need multiple slots", |c| {
let slot_count = c.buffer_size.as_ref() / c.slot_size.as_ref();
slot_count >= 4
}),
write_count in 100usize..=1000usize,
) {
let ring_buffer = RingBuffer::new(&config);
let slot_count = config.buffer_size.as_ref() / config.slot_size.as_ref();
let small_data = vec![1u8; 32]; let mut successful_writes = 0;
for _ in 0..write_count {
let request_id = RequestId::new();
if ring_buffer.write(request_id, &small_data).is_ok() {
successful_writes += 1;
}
}
prop_assert!(successful_writes > 0);
prop_assert!(successful_writes <= write_count);
let mut read_count = 0;
while ring_buffer.read().is_some() {
read_count += 1;
}
prop_assert!(read_count <= slot_count);
prop_assert!(read_count > 0);
}
#[test]
fn prop_stats_are_accurate(
config in buffer_config_strategy(),
operations in prop::collection::vec(
prop::bool::ANY, // true = write, false = read
1..=100
),
) {
let ring_buffer = RingBuffer::new(&config);
let _slot_count = config.buffer_size.as_ref() / config.slot_size.as_ref();
let small_data = vec![1u8; 32];
let initial_stats = ring_buffer.stats();
prop_assert_eq!(initial_stats.total_writes, 0);
prop_assert_eq!(initial_stats.total_reads, 0);
prop_assert_eq!(initial_stats.dropped_events.as_ref(), &0);
let mut expected_writes = 0;
let mut expected_reads = 0;
let mut available_to_read = 0;
for is_write in operations {
if is_write {
let request_id = RequestId::new();
if ring_buffer.write(request_id, &small_data).is_ok() {
expected_writes += 1;
available_to_read += 1;
}
} else if available_to_read > 0 && ring_buffer.read().is_some() {
expected_reads += 1;
available_to_read -= 1;
}
}
let final_stats = ring_buffer.stats();
prop_assert_eq!(final_stats.total_writes, expected_writes);
prop_assert_eq!(final_stats.total_reads, expected_reads);
}
}
#[test]
fn test_ring_buffer_memory_layout() {
let config = RingBufferConfig {
buffer_size: BufferSize::try_new(BYTES_1KB).unwrap(),
slot_size: SlotSize::try_new(SLOT_SIZE_SMALL).unwrap(),
};
let ring_buffer = RingBuffer::new(&config);
let _expected_slots = BYTES_1KB / SLOT_SIZE_SMALL;
let mut written_ids = Vec::new();
for i in 0..5 {
let request_id = RequestId::new();
written_ids.push(request_id);
let data = vec![i as u8; 32];
assert!(
ring_buffer.write(request_id, &data).is_ok(),
"Write {i} should succeed"
);
}
for (i, expected_id) in written_ids.iter().enumerate() {
let (id, data) = ring_buffer.read().expect("Should be able to read");
assert_eq!(id, *expected_id);
assert_eq!(data[0], i as u8);
}
assert!(ring_buffer.read().is_none());
for i in 5..10 {
let request_id = RequestId::new();
let data = vec![i as u8; 32];
assert!(
ring_buffer.write(request_id, &data).is_ok(),
"Write {i} should succeed"
);
}
let mut read_count = 0;
while ring_buffer.read().is_some() {
read_count += 1;
}
assert_eq!(read_count, 5);
}
#[test]
fn test_ring_buffer_concurrent_stress() {
let config = RingBufferConfig {
buffer_size: BufferSize::try_new(BYTES_1MB).unwrap(),
slot_size: SlotSize::try_new(BYTES_1KB).unwrap(),
};
let ring_buffer = Arc::new(RingBuffer::new(&config));
let mut write_handles = Vec::new();
let mut read_handles = Vec::new();
for thread_id in 0..TEST_THREAD_COUNT {
let rb = Arc::clone(&ring_buffer);
let handle = thread::spawn(move || {
let mut successful_writes = 0;
for i in 0..TEST_ITERATIONS_LARGE {
let data = format!("thread-{thread_id}-msg-{i}").into_bytes();
let request_id = RequestId::new();
if rb.write(request_id, &data).is_ok() {
successful_writes += 1;
}
thread::yield_now();
}
successful_writes
});
write_handles.push(handle);
}
let shutdown_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
for _ in 0..5 {
let rb = Arc::clone(&ring_buffer);
let shutdown = Arc::clone(&shutdown_flag);
let handle = thread::spawn(move || {
let mut read_count = 0;
while !shutdown.load(std::sync::atomic::Ordering::Relaxed) {
if rb.read().is_some() {
read_count += 1;
}
thread::yield_now();
}
while rb.read().is_some() {
read_count += 1;
}
read_count
});
read_handles.push(handle);
}
let _total_writes: usize = write_handles.into_iter().map(|h| h.join().unwrap()).sum();
shutdown_flag.store(true, std::sync::atomic::Ordering::Relaxed);
let total_reads: usize = read_handles.into_iter().map(|h| h.join().unwrap()).sum();
let stats = ring_buffer.stats();
assert!(stats.total_writes > 0);
assert!(stats.total_reads > 0);
assert!(total_reads > 0);
assert!(stats.total_writes >= total_reads as u64);
}