542 lines
15 KiB
Rust
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();
|
|
}
|