chore: initial release v0.1.0
Runtime-agnostic oneshot broadcast channel for Rust.
This commit is contained in:
64 files changed
+23966
No files matched your search
@@ -0,0 +1,618 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in new issue
Block a user