Files
announcement/tests/panic_safety_tests.rs
T
swaits e8e55b7676 chore: initial release v0.1.0
Runtime-agnostic oneshot broadcast channel for Rust.
2025-10-13 19:36:37 -06:00

911 lines
26 KiB
Rust

//! Panic Safety Tests
//!
//! These tests verify that the library maintains safety invariants even when panics occur.
//! Panic safety means:
//! 1. No memory leaks
//! 2. No undefined behavior
//! 3. Data structures remain consistent
//! 4. Resources are properly cleaned up
//! 5. No deadlocks or hangs after panic recovery
use announcement::Announcement;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
// =============================================================================
// Basic Panic Safety Tests
// =============================================================================
/// Test: Panic safety: panic during Drop implementation
///
/// Verifies: System remains safe when announced value panics during Drop
/// Category: panic
#[test]
fn test_panic_during_drop_in_value() {
// Test that panicking during Drop of the announced value doesn't break things
#[derive(Debug)]
struct PanicOnDrop {
should_panic: bool,
}
impl Drop for PanicOnDrop {
fn drop(&mut self) {
if self.should_panic {
panic!("Intentional panic in Drop");
}
}
}
// Create announcement with panic-on-drop value
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let _listener = announcement.listener();
announcer
.announce(Arc::new(PanicOnDrop { should_panic: true }))
.unwrap();
// When Arc drops, it should panic, but we catch it
}));
// The panic should be caught
assert!(result.is_err());
// Create another announcement to verify system still works
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: panic during Clone/Drop interaction
///
/// Verifies: System remains safe when panic occurs during value cloning or dropping
/// Category: panic
#[test]
fn test_panic_in_clone_implementation() {
// Test panic during T::clone() doesn't break announcement
#[derive(Debug, Clone)]
struct PanicOnClone {
panic_count: Arc<std::sync::atomic::AtomicUsize>,
panic_on_nth: usize,
}
impl Drop for PanicOnClone {
fn drop(&mut self) {
let count = self
.panic_count
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if count == self.panic_on_nth {
panic!("Panic on Nth drop");
}
}
}
let panic_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let _result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let value = PanicOnClone {
panic_count: panic_count.clone(),
panic_on_nth: 2, // Panic on 3rd drop
};
announcer.announce(value).unwrap();
// First try_listen - might panic
let _v1 = listener.try_listen();
}));
// System should still work after panic
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: explicit announcer drop
///
/// Verifies: Dropping announcer without announcing properly closes listeners
/// Category: panic
#[test]
fn test_announcer_drop_with_panic_guard() {
// Test that dropping announcer without announcing works
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
assert!(!listener.is_closed());
// Drop announcer explicitly
drop(announcer);
// Listener should see closed
assert!(listener.is_closed());
}
// =============================================================================
// Arc Reference Counting Under Panic
// =============================================================================
/// Test: Panic safety: Arc reference counting
///
/// Verifies: Arc refcounts remain correct when panics occur while holding references
/// Category: panic
#[test]
fn test_arc_refcount_with_panic() {
// Verify Arc refcounts are correct even when panics occur
let (announcer, announcement) = Announcement::new();
let data = Arc::new(vec![1, 2, 3, 4, 5]);
announcer.announce(data.clone()).unwrap();
// Create listeners
let l1 = announcement.listener();
let l2 = announcement.listener();
// Get values
let v1 = l1.try_listen().unwrap();
let _v2 = l2.try_listen().unwrap();
// Refcount should be: data + announcement storage + v1 + v2 = 4
assert_eq!(Arc::strong_count(&v1), 4);
// Cause panic while holding references
let result = catch_unwind(AssertUnwindSafe(|| {
let _v3 = l1.try_listen().unwrap();
panic!("Test panic");
}));
assert!(result.is_err());
// Refcount should still be correct
assert_eq!(Arc::strong_count(&v1), 4);
}
/// Test: Panic safety: announcement clone with panic
///
/// Verifies: Cloned announcements remain functional after panic in sibling clone
/// Category: panic
#[test]
fn test_announcement_clone_with_panic() {
let (announcer, announcement) = Announcement::new();
// Clone announcement
let announcement2 = announcement.clone();
// Panic while one is in scope
let result = catch_unwind(AssertUnwindSafe(|| {
let _l = announcement2.listener();
panic!("Test panic");
}));
assert!(result.is_err());
// Original announcement should still work
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: listener clone with panic
///
/// Verifies: Cloned listeners remain functional after panic in sibling clone
/// Category: panic
#[test]
fn test_listener_clone_with_panic() {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
// Clone listener
let listener2 = listener.clone();
// Announce value
announcer.announce(42).unwrap();
// Panic while using clone
let result = catch_unwind(AssertUnwindSafe(|| {
let _v = listener2.try_listen();
panic!("Test panic");
}));
assert!(result.is_err());
// Original listener should still work
assert_eq!(listener.try_listen(), Some(42));
}
// =============================================================================
// Multiple Panic Scenarios
// =============================================================================
/// Test: Panic safety: sequential panics
///
/// Verifies: System handles multiple panics in sequence without breaking
/// Category: panic
#[test]
fn test_sequential_panics() {
// Multiple panics in sequence shouldn't break the system
for i in 0..5 {
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(i).unwrap();
if i % 2 == 0 {
panic!("Even panic");
}
assert_eq!(listener.try_listen(), Some(i));
}));
if i % 2 == 0 {
assert!(result.is_err());
} else {
assert!(result.is_ok());
}
}
}
/// Test: Panic safety: nested panic scenarios
///
/// Verifies: System handles panic within panic handler context
/// Category: panic
#[test]
fn test_nested_panics() {
// Panic within panic handler
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::<i32>::new();
let _listener = announcement.listener();
// Drop announcer (will set closed flag)
drop(announcer);
// Now cause a panic
panic!("First panic");
}));
assert!(result.is_err());
// System should still work
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
// =============================================================================
// Async Panic Safety Tests (requires async feature)
// =============================================================================
/// Test: Panic safety: panic during async listen
///
/// Verifies: System remains functional when async task panics after listen completes
/// Category: panic
#[tokio::test]
async fn test_panic_during_async_listen() {
use std::time::Duration;
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
// Spawn task that will panic
let handle = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
let _value = listener.listen().await.unwrap();
panic!("Panic after listen");
});
// Announce value
announcer.announce(42).unwrap();
// Task should panic
let result = handle.await;
assert!(result.is_err());
// System should still work
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.listen().await, Ok(42));
}
/// Test: Panic safety: panic before async listen completion
///
/// Verifies: System remains functional when async task panics before listen completes
/// Category: panic
#[tokio::test]
async fn test_panic_before_async_listen_completes() {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
// Start listening but panic before completion
let handle = tokio::spawn(async move {
tokio::select! {
_ = listener.listen() => {
panic!("Should not reach here");
}
_ = tokio::time::sleep(tokio::time::Duration::from_millis(5)) => {
panic!("Timeout panic");
}
}
});
// Task should panic
let result = handle.await;
assert!(result.is_err());
// Announcer should still work
announcer.announce(42).unwrap();
}
/// Test: Panic safety: multiple async tasks with selective panics
///
/// Verifies: Some async tasks can panic while others complete successfully
/// Category: panic
#[tokio::test]
async fn test_multiple_async_tasks_with_panics() {
let (announcer, announcement) = Announcement::new();
// Spawn multiple tasks, some will panic
let mut handles = vec![];
for i in 0..10 {
let listener = announcement.listener();
handles.push(tokio::spawn(async move {
let value = listener.listen().await.unwrap();
if i % 3 == 0 {
panic!("Task {} panic", i);
}
value
}));
}
// Announce value
announcer.announce(42).unwrap();
// Check results - some panic, some succeed
for (i, handle) in handles.into_iter().enumerate() {
let result = handle.await;
if i % 3 == 0 {
assert!(result.is_err(), "Task {} should have panicked", i);
} else {
assert_eq!(result.unwrap(), 42, "Task {} should have succeeded", i);
}
}
}
// =============================================================================
// Blocking Mode Panic Safety (requires std feature)
// =============================================================================
/// Test: Panic safety: panic during blocking listen
///
/// Verifies: System remains functional when blocking thread panics after listen
/// Category: panic
#[cfg(feature = "std")]
#[test]
fn test_panic_during_blocking_listen() {
use std::thread;
use std::time::Duration;
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle = thread::spawn(move || {
let _value = listener.listen_timeout_blocking(Duration::from_millis(100));
panic!("Panic after blocking listen with timeout");
});
// Give thread time to start
std::thread::sleep(std::time::Duration::from_millis(10));
// Announce value
announcer.announce(42).unwrap();
// Thread should panic
let result = handle.join();
assert!(result.is_err());
}
/// Test: Panic safety: multiple blocking threads with selective panics
///
/// Verifies: Some blocking threads can panic while others complete successfully
/// Category: panic
#[cfg(feature = "std")]
#[test]
fn test_multiple_blocking_threads_with_panics() {
use std::thread;
use std::time::Duration;
let (announcer, announcement) = Announcement::new();
let mut handles = vec![];
for i in 0..5 {
let listener = announcement.listener();
handles.push(thread::spawn(move || {
let value = listener.listen_timeout_blocking(Duration::from_millis(100));
if i % 2 == 0 {
panic!("Thread {} panic", i);
}
value
}));
}
thread::sleep(Duration::from_millis(10));
announcer.announce(42).unwrap();
// Check results
for (i, handle) in handles.into_iter().enumerate() {
let result = handle.join();
if i % 2 == 0 {
assert!(result.is_err());
} else {
assert_eq!(result.unwrap(), Ok(42));
}
}
}
// =============================================================================
// Edge Cases and Stress Tests
// =============================================================================
/// Test: Panic safety: panic with many listeners
///
/// Verifies: Panicking while iterating over many listeners doesn't affect others
/// Category: panic
#[test]
fn test_panic_with_large_number_of_listeners() {
let (announcer, announcement) = Announcement::new();
// Create many listeners
let listeners: Vec<_> = (0..1000).map(|_| announcement.listener()).collect();
announcer.announce(42).unwrap();
// Panic while iterating
let result = catch_unwind(AssertUnwindSafe(|| {
for (i, listener) in listeners.iter().enumerate() {
let _value = listener.try_listen();
if i == 500 {
panic!("Panic at 500");
}
}
}));
assert!(result.is_err());
// Remaining listeners should still work
for listener in listeners.iter().skip(501) {
assert_eq!(listener.try_listen(), Some(42));
}
}
/// Test: Panic safety: panic during is_announced checks
///
/// Verifies: System remains functional when panic occurs during state checks
/// Category: panic
#[test]
fn test_panic_during_is_announced_checks() {
let (announcer, announcement) = Announcement::new();
let result = catch_unwind(AssertUnwindSafe(|| {
assert!(!announcement.is_announced());
panic!("Panic before announce");
}));
assert!(result.is_err());
// Should still be able to announce
announcer.announce(42).unwrap();
assert!(announcement.is_announced());
}
/// Test: Panic safety: panic during is_closed checks
///
/// Verifies: System remains functional when panic occurs during closure checks
/// Category: panic
#[test]
fn test_panic_during_is_closed_checks() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
let result = catch_unwind(AssertUnwindSafe(|| {
assert!(!announcement.is_closed());
panic!("Panic before close");
}));
assert!(result.is_err());
// Should still be able to close
announcer.close();
assert!(announcement.is_closed());
assert!(listener.is_closed());
}
/// Test: Panic safety: no memory leaks on panic
///
/// Verifies: Repeated panics during announcement don't cause memory leaks
/// Category: panic
#[test]
fn test_memory_not_leaked_on_panic() {
// Create and drop many announcements with panics
for _ in 0..100 {
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let _listener = announcement.listener();
announcer.announce(vec![1; 1000]).unwrap();
panic!("Test panic");
}));
assert!(result.is_err());
}
// If there were memory leaks, this would show up in memory profilers
// For now, we just verify the test completes
}
/// Test: Panic safety: close with panic recovery
///
/// Verifies: System remains functional after panic following close operation
/// Category: panic
#[test]
fn test_announcer_close_with_panic_recovery() {
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
announcer.close();
assert!(listener.is_closed());
panic!("Panic after close");
}));
assert!(result.is_err());
// System should still work
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: UnwindSafe trait implementation
///
/// Verifies: All types implement UnwindSafe and RefUnwindSafe traits
/// Category: panic
#[test]
fn test_unwind_safe_trait_impl() {
// Verify types implement UnwindSafe and RefUnwindSafe
fn assert_unwind_safe<T: std::panic::UnwindSafe>() {}
fn assert_ref_unwind_safe<T: std::panic::RefUnwindSafe>() {}
assert_unwind_safe::<announcement::Announcement<i32>>();
assert_unwind_safe::<announcement::Announcer<i32>>();
assert_unwind_safe::<announcement::Listener<i32>>();
assert_ref_unwind_safe::<announcement::Announcement<i32>>();
assert_ref_unwind_safe::<announcement::Listener<i32>>();
}
// =============================================================================
// Complex Panic Scenarios
// =============================================================================
/// Test: Panic safety: panic in complex type Drop
///
/// Verifies: System handles panics during drop of complex nested types
/// Category: panic
#[test]
fn test_panic_in_complex_type_drop() {
#[derive(Debug)]
struct ComplexType {
_data: Vec<Vec<String>>,
should_panic: bool,
}
impl Drop for ComplexType {
fn drop(&mut self) {
if self.should_panic {
panic!("Complex drop panic");
}
}
}
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let complex = ComplexType {
_data: vec![vec!["test".to_string(); 10]; 10],
should_panic: false,
};
announcer.announce(Arc::new(complex)).unwrap();
let value = listener.try_listen().unwrap();
// Modify to panic on drop
// (Actually, we can't modify Arc contents, so this tests Arc safety)
drop(value);
}));
assert!(result.is_ok());
}
/// Test: Panic safety: panic recovery in async loop
///
/// Verifies: Async system recovers from panics across multiple iterations
/// Category: panic
#[tokio::test]
async fn test_panic_recovery_in_async_loop() {
// Test that async system recovers from panics in loop
for i in 0..5 {
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(i).unwrap();
if i == 2 {
panic!("Panic on iteration 2");
}
assert_eq!(listener.try_listen(), Some(i));
}));
if i == 2 {
assert!(result.is_err());
} else {
assert!(result.is_ok());
}
}
// Async system should still work
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.listen().await, Ok(42));
}
// Note: Double-panic test removed because double panics abort the process by design.
// This is correct Rust behavior to prevent undefined behavior during unwinding.
// =============================================================================
// Additional Panic Safety Tests
// =============================================================================
/// Test: Panic safety: panic with cloned announcements
///
/// Verifies: Cloned announcements remain functional after panic in siblings
/// Category: panic
#[test]
fn test_panic_with_cloned_announcements() {
let (announcer, announcement) = Announcement::new();
let announcement2 = announcement.clone();
let announcement3 = announcement.clone();
let result = catch_unwind(AssertUnwindSafe(|| {
let _l1 = announcement.listener();
let _l2 = announcement2.listener();
panic!("Test panic with clones");
}));
assert!(result.is_err());
// announcement3 should still work
let listener = announcement3.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: panic during listener creation
///
/// Verifies: System handles panics occurring during listener creation process
/// Category: panic
#[test]
fn test_panic_during_listener_creation() {
for _ in 0..10 {
let result = catch_unwind(AssertUnwindSafe(|| {
let (_announcer, announcement) = Announcement::<i32>::new();
// Create some listeners
let _l1 = announcement.listener();
let _l2 = announcement.listener();
// Panic during creation
panic!("Panic during listener creation");
}));
assert!(result.is_err());
}
}
/// Test: Panic safety: panic with many clones
///
/// Verifies: System handles panic while iterating over many clones
/// Category: panic
#[test]
fn test_panic_with_many_clones() {
let (announcer, announcement) = Announcement::new();
// Create many clones
let clones: Vec<_> = (0..100).map(|_| announcement.clone()).collect();
let result = catch_unwind(AssertUnwindSafe(|| {
for (i, clone) in clones.iter().enumerate() {
let _listener = clone.listener();
if i == 50 {
panic!("Panic at clone 50");
}
}
}));
assert!(result.is_err());
// Original should still work
let listener = announcement.listener();
announcer.announce(42).unwrap();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Panic safety: panic during concurrent announces
///
/// Verifies: Panics in one announcement don't affect concurrent announcements
/// Category: panic
#[tokio::test]
async fn test_panic_during_concurrent_announces() {
// Test that panics don't affect concurrent operations
for _ in 0..10 {
let (announcer1, announcement1) = Announcement::new();
let (announcer2, announcement2) = Announcement::new();
let l1 = announcement1.listener();
let l2 = announcement2.listener();
// Spawn task that will panic
let h1 = tokio::spawn(async move {
announcer1.announce(42).unwrap();
panic!("Panic after first announce");
});
// Spawn task that should succeed
let h2 = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
announcer2.announce(100).unwrap();
});
// First task should panic
assert!(h1.await.is_err());
// Second task should succeed
assert!(h2.await.is_ok());
// Both values should be available
assert_eq!(l1.listen().await, Ok(42));
assert_eq!(l2.listen().await, Ok(100));
}
}
/// Test: Panic safety: panic with nested Arc structures
///
/// Verifies: System handles panics with complex nested Arc data structures
/// Category: panic
#[test]
fn test_panic_in_nested_structures() {
#[derive(Debug, Clone)]
struct Nested {
_data: Arc<Vec<Arc<String>>>,
}
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let nested = Nested {
_data: Arc::new(vec![
Arc::new("a".to_string()),
Arc::new("b".to_string()),
Arc::new("c".to_string()),
]),
};
announcer.announce(nested).unwrap();
let _value = listener.try_listen();
panic!("Panic with nested structure");
}));
assert!(result.is_err());
}
/// Test: Panic safety: panic mixing blocking and async
///
/// Verifies: Panics in blocking threads don't affect async operations
/// Category: panic
#[cfg(feature = "std")]
#[test]
fn test_panic_mixing_blocking_and_async() {
use std::thread;
use std::time::Duration;
let (announcer, announcement) = Announcement::new();
let listener1 = announcement.listener();
let listener2 = announcement.listener();
// Blocking thread with timeout
let h1 = thread::spawn(move || {
let result = catch_unwind(AssertUnwindSafe(|| {
let _value = listener1.listen_timeout_blocking(Duration::from_millis(100));
panic!("Panic in blocking thread with timeout");
}));
assert!(result.is_err());
});
// Announce from main thread
thread::sleep(std::time::Duration::from_millis(10));
announcer.announce(42).unwrap();
h1.join().unwrap();
// Other listener should still work
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async { listener2.listen().await });
assert_eq!(result, Ok(42));
}
/// Test: Panic safety: panic after immediate announcer drop
///
/// Verifies: System handles panic occurring after announce and immediate drop
/// Category: panic
#[test]
fn test_panic_with_immediate_drop() {
// Test panic when announcer is dropped immediately after announce
for _ in 0..20 {
let result = catch_unwind(AssertUnwindSafe(|| {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
// Announcer dropped here
assert_eq!(listener.try_listen(), Some(42));
panic!("Panic after announce and drop");
}));
assert!(result.is_err());
}
// System should still work
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(100).unwrap();
assert_eq!(listener.try_listen(), Some(100));
}