619 lines
17 KiB
Rust
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);
|
|
}
|