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

619 lines
17 KiB
Rust

//! Tests for dropped announcer scenarios - critical edge cases
//!
//! These tests verify graceful behavior when the announcer is dropped
//! without announcing, ensuring no deadlocks or panics occur.
use announcement::Announcement;
use announcement::ListenError;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
/// Test: Announcer dropped without announcing, listeners wait (verify timeout needed)
///
/// Verifies: Announcer dropped without announcing, listeners wait (timeout needed)
/// Category: drop
#[test]
fn test_announcer_dropped_listeners_wait() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
// Drop the announcer without announcing
drop(announcer);
// try_listen should return None
assert_eq!(listener.try_listen(), None);
// is_announced should be false
assert!(!announcement.is_announced());
}
/// Test: Announcer dropped, then try_listen returns None as expected
///
/// Verifies: Announcer dropped, then try_listen returns None as expected
/// Category: drop
#[test]
fn test_announcer_dropped_try_listen_none() {
let (announcer, announcement) = Announcement::<String>::new();
// Create multiple listeners
let listener1 = announcement.listener();
let listener2 = announcement.listener();
let listener3 = announcement.listener();
// Drop announcer
drop(announcer);
// All try_listen calls should return None
assert_eq!(listener1.try_listen(), None);
assert_eq!(listener2.try_listen(), None);
assert_eq!(listener3.try_listen(), None);
// Even new listeners created after drop should get None
let listener4 = announcement.listener();
assert_eq!(listener4.try_listen(), None);
}
/// Test: Multiple listeners waiting when announcer dropped
///
/// Verifies: Multiple listeners waiting when announcer dropped
/// Category: drop
#[test]
fn test_multiple_listeners_announcer_dropped() {
let (announcer, announcement) = Announcement::<Vec<u8>>::new();
// Create many listeners
let mut listeners = Vec::new();
for _ in 0..100 {
listeners.push(announcement.listener());
}
// Drop announcer
drop(announcer);
// All listeners should get None
for listener in &listeners {
assert_eq!(listener.try_listen(), None);
}
}
/// Test: Drop test: no deadlock timeout blocking announcer dropped
///
/// Verifies: no deadlock timeout blocking announcer dropped
/// Category: drop
#[cfg(feature = "std")]
#[test]
fn test_no_deadlock_timeout_blocking_announcer_dropped() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
// Drop announcer in another thread
thread::spawn(move || {
thread::sleep(Duration::from_millis(10));
drop(announcer); // Sets closed flag and notifies listeners
});
// With closed detection, this returns Closed, not Timeout
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
assert_eq!(result, Err(ListenError::Closed));
assert!(announcement.is_closed());
}
/// Test: Verify graceful behavior, no panics when announcer dropped
///
/// Verifies: graceful behavior, no panics when announcer dropped
/// Category: drop
#[test]
fn test_no_panic_announcer_dropped() {
let result = std::panic::catch_unwind(|| {
let (announcer, announcement) = Announcement::<i32>::new();
let listener1 = announcement.listener();
let listener2 = announcement.listener();
drop(announcer);
// These should not panic
let _ = listener1.try_listen();
let _ = listener2.try_listen();
let _ = announcement.is_announced();
let _ = announcement.listener();
});
assert!(result.is_ok(), "Should not panic when announcer is dropped");
}
/// Test: Document behavior - announcer dropped means value never arrives
///
/// Verifies: Document behavior - announcer dropped means value never arrives
/// Category: drop
#[test]
fn test_document_dropped_announcer_behavior() {
// This test documents the expected behavior when announcer is dropped
let (announcer, announcement) = Announcement::<String>::new();
let listener = announcement.listener();
// When announcer is dropped without announcing:
drop(announcer);
// 1. try_listen returns None
assert_eq!(listener.try_listen(), None);
// 2. is_announced returns false
assert!(!announcement.is_announced());
// 3. New listeners also get None
let new_listener = announcement.listener();
assert_eq!(new_listener.try_listen(), None);
// 4. The value will NEVER arrive (no way to announce after announcer is dropped)
}
/// Test: Announcer dropped in spawned thread
///
/// Verifies: Announcer dropped in spawned thread
/// Category: drop
#[test]
fn test_announcer_dropped_in_thread() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
thread::spawn(move || {
// Announcer dropped when thread exits
let _ = announcer;
})
.join()
.unwrap();
assert_eq!(listener.try_listen(), None);
}
/// Test: Race between announce and drop
///
/// Verifies: Race between announce and drop
/// Category: drop
#[test]
fn test_announcer_dropped_arc_value() {
let (announcer, announcement) = Announcement::<Arc<Vec<String>>>::new();
let listener = announcement.listener();
// Create an Arc value that would have been announced
let _would_be_value = Arc::new(vec!["test".to_string()]);
// Drop announcer instead of announcing
drop(announcer);
assert_eq!(listener.try_listen(), None);
}
/// Test: Many announcers dropped in parallel
///
/// Verifies: Many announcers dropped in parallel
/// Category: drop
#[test]
fn test_many_announcers_dropped_parallel() {
let mut handles = vec![];
for _ in 0..100 {
handles.push(thread::spawn(|| {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
// Drop announcer
drop(announcer);
// Verify listener gets None
assert_eq!(listener.try_listen(), None);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
/// Test: Announcer dropped after listeners created in multiple threads
///
/// Verifies: Announcer dropped after listeners created in multiple threads
/// Category: drop
#[test]
fn test_announcer_dropped_multi_thread_listeners() {
let (announcer, announcement) = Announcement::<String>::new();
let mut handles = vec![];
// Create listeners in multiple threads
for _ in 0..10 {
let ann = announcement.clone();
handles.push(thread::spawn(move || {
let listener = ann.listener();
thread::sleep(Duration::from_millis(50));
listener.try_listen()
}));
}
// Drop announcer after a short delay
thread::sleep(Duration::from_millis(10));
drop(announcer);
// All threads should get None
for handle in handles {
let result = handle.join().unwrap();
assert_eq!(result, None);
}
}
/// Test: Memory is properly freed when announcer dropped
///
/// Verifies: Memory is properly freed when announcer dropped
/// Category: drop
#[test]
fn test_memory_freed_announcer_dropped() {
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
struct MemoryTracker {
_data: Vec<u8>,
}
impl Drop for MemoryTracker {
/// Test: Drop test: drop
///
/// Verifies: Verifies drop behavior
/// Category: drop
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
DROP_COUNT.store(0, Ordering::SeqCst);
{
let (announcer, announcement) = Announcement::<MemoryTracker>::new();
let _listener = announcement.listener();
// Would have announced this, but drop instead
let _would_announce = MemoryTracker {
_data: vec![0; 1024],
};
drop(announcer);
// MemoryTracker never gets into the announcement
}
// Only the local MemoryTracker should have been dropped
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
}
/// Test: Drop test: listener blocking wait announcer already dropped
///
/// Verifies: listener blocking wait announcer already dropped
/// Category: drop
#[cfg(feature = "std")]
#[test]
fn test_listener_blocking_wait_announcer_already_dropped() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
// Drop announcer first - sets closed flag
drop(announcer);
// With closed detection, this returns Closed immediately, not Timeout
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
assert_eq!(result, Err(ListenError::Closed));
assert!(announcement.is_closed());
}
/// Test: Clone chain with dropped announcer
///
/// Verifies: Clone chain with dropped announcer
/// Category: drop
#[test]
fn test_clone_chain_dropped_announcer() {
let (announcer, announcement) = Announcement::<i32>::new();
let ann2 = announcement.clone();
let ann3 = ann2.clone();
let l1 = announcement.listener();
let l2 = ann2.listener();
let l3 = ann3.listener();
drop(announcer);
assert_eq!(l1.try_listen(), None);
assert_eq!(l2.try_listen(), None);
assert_eq!(l3.try_listen(), None);
}
/// Test: Dropped announcer with zero-sized type
///
/// Verifies: Dropped announcer with zero-sized type
/// Category: drop
#[test]
fn test_dropped_announcer_zst() {
let (announcer, announcement) = Announcement::<()>::new();
let listener = announcement.listener();
drop(announcer);
assert_eq!(listener.try_listen(), None);
}
/// Test: Dropped announcer with large type
///
/// Verifies: Dropped announcer with large type
/// Category: drop
#[test]
fn test_dropped_announcer_large_type() {
#[derive(Clone, Debug, PartialEq)]
struct LargeType {
data: [u8; 10_000],
}
let (announcer, announcement) = Announcement::<LargeType>::new();
let listener = announcement.listener();
drop(announcer);
assert_eq!(listener.try_listen(), None);
}
/// Test: is_announced remains false forever after announcer dropped
///
/// Verifies: is_announced remains false forever after announcer dropped
/// Category: drop
#[test]
fn test_is_announced_false_forever() {
let (announcer, announcement) = Announcement::<i32>::new();
drop(announcer);
// Check multiple times - should always be false
for _ in 0..1000 {
assert!(!announcement.is_announced());
}
// Even after creating new listeners
let _l1 = announcement.listener();
assert!(!announcement.is_announced());
let _l2 = announcement.listener();
assert!(!announcement.is_announced());
}
/// Test: Stress test - rapid announcer drops
///
/// Verifies: Stress test - rapid announcer drops
/// Category: drop
#[test]
fn test_stress_rapid_announcer_drops() {
for _ in 0..10_000 {
let (announcer, announcement) = Announcement::<i32>::new();
let _listener = announcement.listener();
drop(announcer);
}
}
/// Test: Announcer dropped with Option type
///
/// Verifies: Announcer dropped with Option type
/// Category: drop
#[test]
fn test_dropped_announcer_option() {
let (announcer, announcement) = Announcement::<Option<String>>::new();
let listener = announcement.listener();
drop(announcer);
assert_eq!(listener.try_listen(), None);
// Note: try_listen returns None (no value), not Some(None)
}
/// Test: Announcer dropped with Result type
///
/// Verifies: Announcer dropped with Result type
/// Category: drop
#[test]
fn test_dropped_announcer_result() {
let (announcer, announcement) = Announcement::<Result<i32, String>>::new();
let listener = announcement.listener();
drop(announcer);
assert_eq!(listener.try_listen(), None);
}
/// Test: Drop test: async listen announcer dropped
///
/// Verifies: async listen announcer dropped
/// Category: drop
#[tokio::test]
async fn test_async_listen_announcer_dropped() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
// Drop announcer - this sets closed flag and notifies listeners
drop(announcer);
// With closed detection, listen() returns Closed immediately (with timeout to prevent hanging in mutation tests)
let result = tokio::time::timeout(Duration::from_millis(100), listener.listen())
.await
.expect("listen should not timeout when announcer is dropped");
assert_eq!(result, Err(ListenError::Closed));
// Verify closed state
assert!(announcement.is_closed());
}
/// Test: Verify announcer can't be used after drop (compile-time safety)
///
/// Verifies: announcer can't be used after drop (compile-time safety)
/// Category: drop
#[test]
fn test_compile_time_safety_announcer_consumed() {
let (announcer, announcement) = Announcement::<i32>::new();
let _listener = announcement.listener();
// This consumes the announcer
drop(announcer);
// The following would not compile:
// announcer.announce(42).unwrap(); // Error: announcer was moved
// This is enforced at compile time by Rust's ownership system
assert!(!announcement.is_announced());
}
/// Test: Complex scenario with conditionally dropped announcer
///
/// Verifies: Complex scenario with conditionally dropped announcer
/// Category: drop
#[test]
fn test_conditional_announcer_drop() {
fn maybe_announce(should_drop: bool) -> (Announcement<i32>, Option<i32>) {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
if should_drop {
drop(announcer);
(announcement, listener.try_listen())
} else {
announcer.announce(42).unwrap();
(announcement, listener.try_listen())
}
}
// Test both paths
let (_ann1, result1) = maybe_announce(true);
assert_eq!(result1, None);
let (_ann2, result2) = maybe_announce(false);
assert_eq!(result2, Some(42));
}
/// Test: Announcer dropped in destructor of another type
///
/// Verifies: Announcer dropped in destructor of another type
/// Category: drop
#[test]
fn test_announcer_dropped_in_destructor() {
struct AnnouncerHolder<T> {
announcer: Option<announcement::Announcer<T>>,
}
impl<T> Drop for AnnouncerHolder<T> {
fn drop(&mut self) {
// Announcer dropped here without announcing
self.announcer.take();
}
}
let (announcer, announcement) = Announcement::<String>::new();
let listener = announcement.listener();
{
let _holder = AnnouncerHolder {
announcer: Some(announcer),
};
// Holder dropped here, taking announcer with it
}
assert_eq!(listener.try_listen(), None);
}
/// Test: Thread panic while holding announcer
///
/// Verifies: Thread panic while holding announcer
/// Category: drop
#[test]
fn test_thread_panic_with_announcer() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
let handle = thread::spawn(move || {
let _announcer = announcer;
panic!("Intentional panic while holding announcer");
});
// Thread panicked, dropping announcer
assert!(handle.join().is_err());
assert_eq!(listener.try_listen(), None);
}
/// Test: Announcer dropped with recursive structure
///
/// Verifies: Announcer dropped with recursive structure
/// Category: drop
#[test]
fn test_dropped_announcer_recursive() {
#[derive(Clone, Debug, PartialEq)]
struct Node {
value: i32,
next: Option<Box<Node>>,
}
let (announcer, announcement) = Announcement::<Node>::new();
let listener = announcement.listener();
drop(announcer);
assert_eq!(listener.try_listen(), None);
}
/// Test: Drop test: document blocking behavior
///
/// Verifies: document blocking behavior
/// Category: drop
#[cfg(feature = "std")]
#[test]
fn test_document_blocking_behavior() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
thread::spawn(move || {
thread::sleep(Duration::from_millis(10));
drop(announcer); // Drop instead of announce
});
// This will timeout because announcer was dropped
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
match result {
Err(ListenError::Timeout) => {
// Expected: timeout because announcer was dropped
}
Err(ListenError::Closed) => {
// Also acceptable: closed because announcer was dropped
}
Ok(_) => panic!("Should not receive value when announcer is dropped"),
}
}
/// Test: Multiple announcement channels with some dropped announcers
///
/// Verifies: Multiple announcement channels with some dropped announcers
/// Category: drop
#[test]
fn test_multiple_channels_some_dropped() {
let (a1, ann1) = Announcement::<i32>::new();
let (a2, ann2) = Announcement::<i32>::new();
let (a3, ann3) = Announcement::<i32>::new();
let l1 = ann1.listener();
let l2 = ann2.listener();
let l3 = ann3.listener();
// Announce on first, drop others
a1.announce(42).unwrap();
drop(a2);
drop(a3);
assert_eq!(l1.try_listen(), Some(42));
assert_eq!(l2.try_listen(), None);
assert_eq!(l3.try_listen(), None);
}