use super::Rb; use crate::{storage::Array, traits::*}; use alloc::rc::Rc; #[test] fn skip() { // Initialize ringbuffer, prod and cons let mut rb = Rb::>::default(); let (mut prod, mut cons) = rb.split_ref(); let mut i = 0; // Fill the buffer for _ in 0..10 { prod.try_push(i).unwrap(); i += 1; } // Pop in the middle of the buffer assert_eq!(cons.skip(5), 5); // Make sure changes are taken into account assert_eq!(cons.try_pop().unwrap(), 5); // Fill the buffer again for _ in 0..5 { prod.try_push(i).unwrap(); i += 1; } assert_eq!(cons.skip(6), 6); assert_eq!(cons.try_pop().unwrap(), 12); // Fill the buffer again for _ in 0..7 { prod.try_push(i).unwrap(); i += 1; } // Ask too much, delete the max number of items assert_eq!(cons.skip(10), 9); // Try to remove more than possible assert_eq!(cons.skip(1), 0); // Make sure it is still usable assert_eq!(cons.try_pop(), None); assert_eq!(prod.try_push(0), Ok(())); assert_eq!(cons.try_pop(), Some(0)); } #[test] fn skip_drop() { let rc = Rc::<()>::new(()); const CAP: usize = 10; let mut rb = Rb::, CAP>>::default(); let (mut prod, mut cons) = rb.split_ref(); for _ in 0..CAP { prod.try_push(rc.clone()).unwrap(); } assert_eq!(cons.occupied_len(), CAP); assert_eq!(Rc::strong_count(&rc), CAP + 1); assert_eq!(cons.skip(CAP), CAP); // Check ring buffer is empty assert_eq!(cons.occupied_len(), 0); // Check that items are dropped assert_eq!(Rc::strong_count(&rc), 1); } /// A panicking destructor must be removed exactly once, while later items /// remain initialized and the buffer can still be used after unwinding. #[cfg(feature = "std")] #[test] #[allow(deprecated)] fn skip_panicking_drop() { use core::cell::Cell; use std::panic::{AssertUnwindSafe, catch_unwind}; const CAP: usize = 4; std::thread_local! { static DROPS: Cell<[usize; CAP + 1]> = const { Cell::new([0; CAP + 1]) }; static ARMED: Cell> = const { Cell::new(None) }; } struct Boom(usize); impl Drop for Boom { fn drop(&mut self) { DROPS.with(|d| { let mut drops = d.get(); drops[self.0] += 1; d.set(drops); }); if ARMED.with(|a| { if a.get() == Some(self.0) { a.set(None); true } else { false } }) { panic!("item Drop panics"); } } } fn check(cons: &mut impl Consumer, clear: bool, panic_at: usize) { ARMED.with(|a| a.set(Some(panic_at))); let result = catch_unwind(AssertUnwindSafe(|| if clear { cons.clear() } else { cons.skip(CAP - 1) })); ARMED.with(|a| a.set(None)); assert!(result.is_err()); assert_eq!(cons.occupied_len(), CAP - panic_at - 1); assert!(cons.iter().map(|item| item.0).eq(panic_at + 1..CAP)); DROPS.with(|d| { for (id, drops) in d.get().into_iter().enumerate() { assert_eq!(drops, usize::from(id <= panic_at), "item {id}"); } }); } for clear in [false, true] { for offset in [0, CAP - 1] { for panic_at in 0..if clear { CAP } else { CAP - 1 } { // Exercise the owner, a split consumer, and a frozen consumer. for mode in 0..3 { let mut rb = Rb::>::default(); for _ in 0..offset { rb.try_push(Boom(CAP)).ok().unwrap(); drop(rb.try_pop().unwrap()); } DROPS.with(|d| d.set([0; CAP + 1])); for id in 0..CAP { rb.try_push(Boom(id)).ok().unwrap(); } match mode { 0 => check(&mut rb, clear, panic_at), 1 => { let (_prod, mut cons) = rb.split_ref(); check(&mut cons, clear, panic_at); } _ => { let (_prod, cons) = rb.split_ref(); check(&mut cons.freeze(), clear, panic_at); } } rb.try_push(Boom(CAP)).ok().unwrap(); drop(rb); DROPS.with(|d| assert_eq!(d.get(), [1; CAP + 1])); } } } } } /// A slot must remain occupied while its in-place destructor is running. #[cfg(feature = "std")] #[test] fn skip_does_not_release_slot_during_drop() { use crate::SharedRb; use std::{sync::mpsc, thread}; struct WaitOnDrop { entered: mpsc::Sender<()>, release: mpsc::Receiver<()>, } impl Drop for WaitOnDrop { fn drop(&mut self) { self.entered.send(()).unwrap(); self.release.recv().unwrap(); } } for clear in [false, true] { let (entered_tx, entered_rx) = mpsc::channel(); let (release_tx, release_rx) = mpsc::channel(); let mut rb = SharedRb::>::default(); let (mut prod, mut cons) = rb.split_ref(); prod.try_push(WaitOnDrop { entered: entered_tx, release: release_rx, }) .ok() .unwrap(); thread::scope(|scope| { let consumer = scope.spawn(move || if clear { cons.clear() } else { cons.skip(1) }); entered_rx.recv().unwrap(); // Observe the slot without writing to it, so this test remains safe // even if a broken consumer publishes it before destruction ends. let vacant = prod.vacant_len(); release_tx.send(()).unwrap(); assert_eq!(consumer.join().unwrap(), 1); assert_eq!(vacant, 0, "slot released while its destructor was running"); assert_eq!(prod.vacant_len(), 1); }); } } /// Batch removal only visits the items occupied when it starts. #[cfg(feature = "std")] #[test] fn skip_preserves_items_pushed_during_drop() { use crate::SharedRb; use alloc::boxed::Box; use std::{sync::mpsc, thread}; struct OnDrop(Option>); impl Drop for OnDrop { fn drop(&mut self) { if let Some(action) = self.0.take() { action(); } } } for clear in [false, true] { let (entered_tx, entered_rx) = mpsc::channel(); let (release_tx, release_rx) = mpsc::channel(); let mut rb = SharedRb::>::default(); let (mut prod, mut cons) = rb.split_ref(); prod.try_push(OnDrop(Some(Box::new(move || { entered_tx.send(()).unwrap(); release_rx.recv().unwrap(); })))) .ok() .unwrap(); thread::scope(|scope| { let consumer = scope.spawn(move || { let removed = if clear { cons.clear() } else { cons.skip(usize::MAX) }; (cons, removed) }); entered_rx.recv().unwrap(); let pushed = prod.try_push(OnDrop(None)).is_ok(); release_tx.send(()).unwrap(); let (mut cons, removed) = consumer.join().unwrap(); assert!(pushed); assert_eq!(removed, 1); assert!(cons.try_pop().is_some()); assert!(cons.is_empty()); }); } }