Files
announcement/tests/no_receiver_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

542 lines
15 KiB
Rust

//! Tests for announcing with no receivers - critical edge cases
//!
//! These tests verify that the announcement system handles gracefully:
//! - Announcing with 0 listeners
//! - Listeners created but dropped before announce
//! - Ensuring no memory leaks or panics
use announcement::Announcement;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Instant;
/// Test: Announce with 0 listeners created at all
///
/// Verifies: Announce with 0 listeners created at all
/// Category: close
#[test]
fn test_announce_with_zero_listeners_created() {
let (announcer, _announcement) = Announcement::<i32>::new();
// No listeners created at all
// Should succeed without panic
announcer.announce(42).unwrap();
}
/// Test: Announce then drop announcement (no listeners exist)
///
/// Verifies: Announce then drop announcement (no listeners exist)
/// Category: close
#[test]
fn test_announce_then_drop_announcement_no_listeners() {
let (announcer, announcement) = Announcement::<i32>::new();
// Drop the announcement immediately
drop(announcement);
// Should still succeed
announcer.announce(42).unwrap();
}
/// Test: Announce with listeners created but all dropped before announce
///
/// Verifies: Announce with listeners created but all dropped before announce
/// Category: close
#[test]
fn test_all_listeners_dropped_before_announce() {
let (announcer, announcement) = Announcement::<String>::new();
// Create listeners then immediately drop them
{
let _listener1 = announcement.listener();
let _listener2 = announcement.listener();
let _listener3 = announcement.listener();
let _listener4 = announcement.listener();
let _listener5 = announcement.listener();
// All listeners dropped here
}
// Should still announce successfully
announcer.announce("test".to_string()).unwrap();
// Verify new listeners can still get the value
let new_listener = announcement.listener();
assert_eq!(new_listener.try_listen(), Some("test".to_string()));
}
/// Test: Announce with listeners created but all dropped after announce
///
/// Verifies: Announce with listeners created but all dropped after announce
/// Category: close
#[test]
fn test_all_listeners_dropped_after_announce() {
let (announcer, announcement) = Announcement::<Vec<u8>>::new();
let mut listeners = Vec::new();
for _ in 0..10 {
listeners.push(announcement.listener());
}
announcer.announce(vec![1, 2, 3]).unwrap();
// Drop all listeners after announce
drop(listeners);
// New listener should still get the value
let new_listener = announcement.listener();
assert_eq!(new_listener.try_listen(), Some(vec![1, 2, 3]));
}
/// Test: Verify no memory leaks when value should be dropped
///
/// Verifies: no memory leaks when value should be dropped
/// Category: close
#[test]
fn test_no_memory_leak_value_dropped_with_last_reference() {
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone, Debug)]
struct DropTracker {
_value: i32,
}
impl Drop for DropTracker {
/// Test: No-receiver test: drop
///
/// Verifies: Verifies drop
/// Category: no-receiver
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
DROP_COUNT.store(0, Ordering::SeqCst);
// Test 1: No listeners at all
{
let (announcer, announcement) = Announcement::new();
let tracker = DropTracker { _value: 1 };
announcer.announce(tracker).unwrap();
drop(announcement);
}
let drops_after_no_listeners = DROP_COUNT.load(Ordering::SeqCst);
assert!(
drops_after_no_listeners >= 1,
"DropTracker should be dropped when no references exist"
);
DROP_COUNT.store(0, Ordering::SeqCst);
// Test 2: Listeners created and dropped
{
let (announcer, announcement) = Announcement::new();
let tracker = DropTracker { _value: 2 };
let listener1 = announcement.listener();
let listener2 = announcement.listener();
announcer.announce(tracker).unwrap();
// Get values to ensure clones are made
let _val1 = listener1.try_listen();
let _val2 = listener2.try_listen();
// Drop everything
drop(listener1);
drop(listener2);
drop(announcement);
drop(_val1);
drop(_val2);
}
let drops_after_listeners = DROP_COUNT.load(Ordering::SeqCst);
assert!(
drops_after_listeners >= 1,
"All DropTracker instances should be dropped"
);
}
/// Test: Multiple announcements to nobody in a loop (stress test)
///
/// Verifies: Multiple announcements to nobody in a loop (stress test)
/// Category: close
#[test]
fn test_multiple_announcements_to_nobody_stress() {
for i in 0..1000 {
let (announcer, _announcement) = Announcement::<i32>::new();
announcer.announce(i).unwrap();
}
}
/// Test: Large data announced with no listeners (memory stress)
///
/// Verifies: Large data announced with no listeners (memory stress)
/// Category: close
#[test]
fn test_announce_large_data_no_listeners() {
let (announcer, _announcement) = Announcement::<Vec<u8>>::new();
// Announce 10MB of data with no listeners
let large_data = vec![0u8; 10 * 1024 * 1024];
announcer.announce(large_data).unwrap();
}
/// Test: Arc-wrapped data with no listeners
///
/// Verifies: Arc-wrapped data with no listeners
/// Category: close
#[test]
fn test_announce_arc_data_no_listeners() {
let (announcer, _announcement) = Announcement::<Arc<String>>::new();
let data = Arc::new("shared data".to_string());
let initial_count = Arc::strong_count(&data);
announcer.announce(data.clone()).unwrap();
// After announce with no listeners, Arc count should only increase by 1 (stored in OnceLock)
assert_eq!(Arc::strong_count(&data), initial_count + 1);
}
/// Test: Announcement dropped before announcer with no listeners
///
/// Verifies: Announcement dropped before announcer with no listeners
/// Category: close
#[test]
fn test_announcement_dropped_before_announcer_no_listeners() {
let (announcer, announcement) = Announcement::<i32>::new();
// Drop announcement first
drop(announcement);
// Announce should still succeed
announcer.announce(42).unwrap();
}
/// Test: Rapid creation and dropping of empty announcements
///
/// Verifies: Rapid creation and dropping of empty announcements
/// Category: close
#[test]
fn test_rapid_empty_announcement_creation() {
for _ in 0..10000 {
let (_announcer, announcement) = Announcement::<()>::new();
drop(announcement);
}
}
/// Test: Threaded announce with no listeners
///
/// Verifies: Threaded announce with no listeners
/// Category: close
#[test]
fn test_threaded_announce_no_listeners() {
let (announcer, announcement) = Announcement::<i32>::new();
let handle = thread::spawn(move || {
announcer.announce(42).unwrap();
});
// No listeners created
handle.join().unwrap();
// New listener after announce in different thread
let listener = announcement.listener();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Many announcements with no listeners in parallel
///
/// Verifies: Many announcements with no listeners in parallel
/// Category: close
#[test]
fn test_parallel_announcements_no_listeners() {
let mut handles = vec![];
for i in 0..100 {
handles.push(thread::spawn(move || {
let (announcer, _announcement) = Announcement::<i32>::new();
announcer.announce(i).unwrap();
}));
}
for handle in handles {
handle.join().unwrap();
}
}
/// Test: Create and drop listeners in a tight loop, then announce
///
/// Verifies: Create and drop listeners in a tight loop, then announce
/// Category: close
#[test]
fn test_listener_churn_before_announce() {
let (announcer, announcement) = Announcement::<i32>::new();
// Create and immediately drop 100,000 listeners
for _ in 0..100_000 {
drop(announcement.listener());
}
// Should still work fine
announcer.announce(42).unwrap();
// New listener should get value
let listener = announcement.listener();
assert_eq!(listener.try_listen(), Some(42));
}
/// Test: Listeners created in threads, all dropped before announce
///
/// Verifies: Listeners created in threads, all dropped before announce
/// Category: close
#[test]
fn test_threaded_listeners_all_dropped() {
let (announcer, announcement) = Announcement::<String>::new();
let mut handles = vec![];
// Create listeners in 10 threads
for _ in 0..10 {
let ann = announcement.clone();
handles.push(thread::spawn(move || {
// Create 100 listeners and immediately drop them
for _ in 0..100 {
drop(ann.listener());
}
}));
}
// Wait for all threads to finish
for handle in handles {
handle.join().unwrap();
}
// Announce after all listeners are gone
announcer.announce("all dropped".to_string()).unwrap();
// New listener should work
let listener = announcement.listener();
assert_eq!(listener.try_listen(), Some("all dropped".to_string()));
}
/// Test: Close test: zero overhead no listeners
///
/// Verifies: zero overhead no listeners
/// Category: close
#[test]
fn test_zero_overhead_no_listeners() {
// Warm up
for _ in 0..10 {
let (announcer, _) = Announcement::<i32>::new();
announcer.announce(42).unwrap();
}
// Measure time for 1000 announcements with no listeners
let start = Instant::now();
for i in 0..1000 {
let (announcer, _) = Announcement::<i32>::new();
announcer.announce(i).unwrap();
}
let duration = start.elapsed();
// Should be very fast (sub-millisecond for 1000 announcements)
assert!(
duration.as_millis() < 10,
"Announcing to no listeners should be nearly zero overhead"
);
}
/// Test: Create 1M listeners, drop them all, then announce
///
/// Verifies: Create 1M listeners, drop them all, then announce
/// Category: close
#[test]
fn test_value_dropped_when_announcement_dropped() {
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone, Debug)]
struct ValueDropTracker {
_data: Vec<u8>,
}
impl Drop for ValueDropTracker {
/// Test: No-receiver test: drop
///
/// Verifies: Verifies drop
/// Category: no-receiver
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
DROP_COUNT.store(0, Ordering::SeqCst);
{
let (announcer, announcement) = Announcement::new();
let tracker = ValueDropTracker {
_data: vec![1, 2, 3],
};
announcer.announce(tracker).unwrap();
// No listeners, drop announcement
drop(announcement);
}
// Value should be dropped
let drops = DROP_COUNT.load(Ordering::SeqCst);
assert!(
drops >= 1,
"Value should be dropped when announcement is dropped with no references"
);
}
/// Test: Complex scenario - mixed listener lifecycle with no final listeners
///
/// Verifies: Complex scenario - mixed listener lifecycle with no final listeners
/// Category: close
#[test]
fn test_complex_no_listener_scenario() {
use std::collections::HashMap;
let (announcer, announcement) = Announcement::<HashMap<String, Vec<i32>>>::new();
// Phase 1: Create some listeners
let listeners: Vec<_> = (0..5).map(|_| announcement.listener()).collect();
// Phase 2: Drop half of them
drop(listeners);
// Phase 3: Create more and drop immediately
for _ in 0..10 {
drop(announcement.listener());
}
// Phase 4: Create listeners in a thread that exits immediately
let ann = announcement.clone();
thread::spawn(move || {
let _l1 = ann.listener();
let _l2 = ann.listener();
// Thread exits, dropping listeners
})
.join()
.unwrap();
// Phase 5: Announce with no active listeners
let mut data = HashMap::new();
data.insert("key".to_string(), vec![1, 2, 3]);
announcer.announce(data.clone()).unwrap();
// Phase 6: Verify new listener still works
let final_listener = announcement.listener();
assert_eq!(final_listener.try_listen(), Some(data));
}
/// Test: Announcer with custom Drop impl and no listeners
///
/// Verifies: Announcer with custom Drop impl and no listeners
/// Category: close
#[test]
fn test_custom_drop_type_no_listeners() {
static CUSTOM_DROP: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone, Debug)]
struct CustomDrop {
id: usize,
}
impl Drop for CustomDrop {
/// Test: No-receiver test: drop
///
/// Verifies: Verifies drop
/// Category: no-receiver
fn drop(&mut self) {
CUSTOM_DROP.fetch_add(self.id, Ordering::SeqCst);
}
}
CUSTOM_DROP.store(0, Ordering::SeqCst);
{
let (announcer, announcement) = Announcement::new();
let custom = CustomDrop { id: 100 };
announcer.announce(custom).unwrap();
drop(announcement);
}
// Should have been dropped (at least the original + the clone in OnceLock)
assert!(
CUSTOM_DROP.load(Ordering::SeqCst) >= 100,
"CustomDrop should be dropped at least once"
);
}
/// Test: Close test: announce no listeners no blocking
///
/// Verifies: announce no listeners no blocking
/// Category: close
#[test]
fn test_announce_no_listeners_no_blocking() {
let (announcer, _announcement) = Announcement::<String>::new();
let start = Instant::now();
announcer.announce("immediate".to_string()).unwrap();
let elapsed = start.elapsed();
// Should complete in microseconds
assert!(
elapsed.as_micros() < 1000,
"Announce should not block when there are no listeners"
);
}
/// Test: Close test: async no listeners
///
/// Verifies: async no listeners
/// Category: close
#[tokio::test]
async fn test_async_no_listeners() {
let (announcer, _announcement) = Announcement::<i32>::new();
// Announce with no async listeners
announcer.announce(42).unwrap();
}
/// Test: Memory safety with no listeners and large recursive structure
///
/// Verifies: Memory safety with no listeners and large recursive structure
/// Category: close
#[test]
fn test_recursive_structure_no_listeners() {
#[derive(Clone, Debug)]
#[allow(dead_code)]
enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>),
}
fn create_deep_tree(depth: usize) -> Tree {
if depth == 0 {
Tree::Leaf(42)
} else {
Tree::Node(
Box::new(create_deep_tree(depth - 1)),
Box::new(create_deep_tree(depth - 1)),
)
}
}
let (announcer, _announcement) = Announcement::new();
// Create a tree with depth 10 (1024 leaf nodes)
let tree = create_deep_tree(10);
// Announce with no listeners - should handle the recursive structure properly
announcer.announce(tree).unwrap();
}