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

416 lines
13 KiB
Rust

use announcement::Announcement;
#[cfg(feature = "std")]
use announcement::ListenError;
use std::sync::Arc;
use std::time::Duration;
// =============================================================================
// Basic async tests
// =============================================================================
/// Test: Basic async listen operation
///
/// Verifies: Listener can wait asynchronously and receive announced value
/// Runtime: tokio
/// Category: integration
/// Coverage: Async listen path, task spawning, value reception
#[tokio::test]
async fn test_async_listen() {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
announcer.announce(42).unwrap();
});
let result = listener.listen().await.unwrap();
assert_eq!(result, 42);
}
/// Test: Multiple async listeners receive same broadcast value
///
/// Verifies: Broadcast semantics work correctly across multiple async tasks
/// Runtime: tokio
/// Category: integration
/// Coverage: Concurrent async listeners, task spawning, value cloning
#[tokio::test]
async fn test_multiple_async_listeners() {
let (announcer, announcement) = Announcement::new();
let l1 = announcement.listener();
let l2 = announcement.listener();
let l3 = announcement.listener();
let h1 = tokio::spawn(async move { l1.listen().await.unwrap() });
let h2 = tokio::spawn(async move { l2.listen().await.unwrap() });
let h3 = tokio::spawn(async move { l3.listen().await.unwrap() });
tokio::time::sleep(Duration::from_millis(10)).await;
announcer.announce(42).unwrap();
assert_eq!(h1.await.unwrap(), 42);
assert_eq!(h2.await.unwrap(), 42);
assert_eq!(h3.await.unwrap(), 42);
}
/// Test: Listener created after announce receives value immediately
///
/// Verifies: Fast path works - listeners get value immediately if already announced
/// Runtime: tokio
/// Category: integration
/// Coverage: Fast path optimization, post-announce listener creation
#[tokio::test]
async fn test_listen_after_announce() {
let (announcer, announcement) = Announcement::new();
announcer.announce(42).unwrap();
let listener = announcement.listener();
let result = listener.listen().await.unwrap(); // Returns immediately
assert_eq!(result, 42);
}
/// Test: Cloned announcement channels share the same value
///
/// Verifies: Cloning announcement preserves broadcast semantics
/// Runtime: tokio
/// Category: integration
/// Coverage: Announcement cloning, shared state, value propagation
#[tokio::test]
async fn test_clone_announcement() {
let (announcer, announcement) = Announcement::new();
let announcement2 = announcement.clone();
let l1 = announcement.listener();
let l2 = announcement2.listener();
announcer.announce(42).unwrap();
assert_eq!(l1.listen().await.unwrap(), 42);
assert_eq!(l2.listen().await.unwrap(), 42);
}
// =============================================================================
// Arc usage test with refcount verification
// =============================================================================
/// Test: Arc-wrapped values maintain correct refcount
///
/// Verifies: Arc reference counting works correctly with shared values
/// Runtime: tokio
/// Category: memory
/// Coverage: Arc refcount tracking, value sharing, memory efficiency
#[tokio::test]
async fn test_with_arc() {
#[derive(Clone, Debug, PartialEq)]
struct Data {
value: Vec<u8>,
}
let (announcer, announcement) = Announcement::new();
let data = Arc::new(Data {
value: vec![1, 2, 3],
});
let l1 = announcement.listener();
let l2 = announcement.listener();
announcer.announce(data.clone()).unwrap();
let r1 = l1.listen().await.unwrap();
let r2 = l2.listen().await.unwrap();
assert_eq!(r1, data);
assert_eq!(r2, data);
// Verify Arc refcount is 4 (original + announcement + r1 + r2)
assert_eq!(Arc::strong_count(&r1), 4);
}
// =============================================================================
// Blocking tests (requires std feature)
// =============================================================================
/// Test: Blocking listen with timeout succeeds when value is announced
///
/// Verifies: Blocking listen API works correctly in multi-threaded context
/// Category: integration
/// Coverage: Blocking listen path, thread spawning, timeout handling
#[cfg(feature = "std")]
#[test]
fn test_blocking_listen() {
use std::thread;
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle =
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_millis(100)));
thread::sleep(Duration::from_millis(10));
announcer.announce(42).unwrap();
assert_eq!(handle.join().unwrap(), Ok(42));
}
/// Test: Blocking listen with longer timeout succeeds
///
/// Verifies: Timeout parameter is respected and value is received before timeout
/// Category: integration
/// Coverage: Timeout configuration, successful receive path
#[cfg(feature = "std")]
#[test]
fn test_blocking_timeout_success() {
use std::thread;
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let handle = thread::spawn(move || listener.listen_timeout_blocking(Duration::from_secs(1)));
thread::sleep(Duration::from_millis(10));
announcer.announce(42).unwrap();
assert_eq!(handle.join().unwrap(), Ok(42));
}
/// Test: Blocking listen detects closed channel immediately
///
/// Verifies: Closed detection works correctly in blocking mode
/// Category: integration
/// Coverage: Channel closure detection, error propagation
#[cfg(feature = "std")]
#[test]
fn test_blocking_timeout_failure() {
let (_, announcement) = Announcement::<i32>::new();
// Note: Announcer dropped immediately via _ pattern
let listener = announcement.listener();
// With closed detection, this returns Closed immediately, not Timeout
let result = listener.listen_timeout_blocking(Duration::from_millis(50));
assert!(matches!(result, Err(ListenError::Closed)));
assert!(announcement.is_closed());
}
// =============================================================================
// CRITICAL: Race condition protection test (TOCTOU)
// =============================================================================
/// Test: TOCTOU race condition protection with tight timing
///
/// Verifies: Double-check pattern prevents race between listen setup and announce
/// Runtime: tokio
/// Category: race
/// Coverage: TOCTOU protection, concurrent announce during listen setup
/// Note: Runs 1000 iterations to catch race conditions
#[tokio::test]
async fn test_race_condition_protection() {
// Test the TOCTOU protection in listen()
for _ in 0..1000 {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let listen_handle = tokio::spawn(async move { listener.listen().await.unwrap() });
// Announce immediately (race with listen setup)
announcer.announce(42).unwrap();
assert_eq!(listen_handle.await.unwrap(), 42);
}
}
// =============================================================================
// Additional edge case tests
// =============================================================================
/// Test: Multiple listeners created after announce all receive value
///
/// Verifies: Fast path works for multiple post-announce listeners
/// Runtime: tokio
/// Category: edge
/// Coverage: Multiple fast-path retrievals, late listener creation
#[tokio::test]
async fn test_multiple_listeners_created_after_announce() {
let (announcer, announcement) = Announcement::new();
// Announce first
announcer.announce(42).unwrap();
// Create listeners after announcement
let l1 = announcement.listener();
let l2 = announcement.listener();
let l3 = announcement.listener();
// All should get the value immediately
assert_eq!(l1.listen().await.unwrap(), 42);
assert_eq!(l2.listen().await.unwrap(), 42);
assert_eq!(l3.listen().await.unwrap(), 42);
}
/// Test: Listener can be called multiple times with same result
///
/// Verifies: Listen operation is idempotent and reusable
/// Runtime: tokio
/// Category: edge
/// Coverage: Multiple listen calls on same listener, value stability
#[tokio::test]
async fn test_listener_multiple_calls() {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
announcer.announce(42).unwrap();
// Calling listen multiple times should return the same value
assert_eq!(listener.listen().await.unwrap(), 42);
assert_eq!(listener.listen().await.unwrap(), 42);
assert_eq!(listener.listen().await.unwrap(), 42);
}
/// Test: High concurrency with 100 simultaneous listeners
///
/// Verifies: Broadcast scales correctly with many concurrent listeners
/// Runtime: tokio
/// Category: stress
/// Coverage: High listener count, concurrent wakeup, value distribution
#[tokio::test]
async fn test_concurrent_listeners() {
let (announcer, announcement) = Announcement::new();
// Create many listeners
let mut handles = Vec::new();
for i in 0..100 {
let listener = announcement.listener();
handles.push(tokio::spawn(async move {
(i, listener.listen().await.unwrap())
}));
}
// Small delay to ensure listeners are waiting
tokio::time::sleep(Duration::from_millis(10)).await;
// Announce
announcer.announce(999).unwrap();
// All should receive the same value
for handle in handles {
let (_, value) = handle.await.unwrap();
assert_eq!(value, 999);
}
}
/// Test: Large data broadcast with Arc maintains efficient refcounting
///
/// Verifies: Large payloads work efficiently via Arc sharing without copying
/// Runtime: tokio
/// Category: memory
/// Coverage: Large data handling, Arc efficiency, memory usage
#[tokio::test]
async fn test_announcement_with_large_data() {
#[derive(Clone, Debug, PartialEq)]
struct LargeData {
data: Vec<u8>,
}
let (announcer, announcement) = Announcement::new();
let large_data = Arc::new(LargeData {
data: vec![42; 1_000_000], // 1MB of data
});
let listener = announcement.listener();
announcer.announce(large_data.clone()).unwrap();
let result = listener.listen().await.unwrap();
assert_eq!(result, large_data);
// With Arc, we should have 3 references (original + announcement storage + result)
assert_eq!(Arc::strong_count(&result), 3);
}
// =============================================================================
// Memory leak tests
// =============================================================================
/// Test: No memory leak when announcer drops without announcing
///
/// Verifies: Channel cleanup happens correctly without announcement
/// Category: memory
/// Coverage: Resource cleanup, drop behavior, leak prevention
/// Note: Use valgrind/miri to verify
#[test]
fn test_no_leak_when_announcer_dropped() {
let (announcer, announcement) = Announcement::<i32>::new();
let listener = announcement.listener();
drop(announcer); // Drop without announcing
drop(listener);
drop(announcement);
// All memory should be freed
// Use valgrind/miri to verify
}
/// Test: No memory leak when value is announced but no listeners exist
///
/// Verifies: Announced values are properly dropped when unused
/// Category: memory
/// Coverage: Unused value cleanup, memory management
#[test]
fn test_no_leak_when_no_listeners() {
let (announcer, announcement) = Announcement::<Vec<u8>>::new();
announcer.announce(vec![1; 1000]).unwrap();
drop(announcement);
// Value should be dropped
}
// =============================================================================
// smol tests
// =============================================================================
/// Test: Basic async operation with smol runtime
///
/// Verifies: Announcement works correctly with smol async runtime
/// Runtime: smol
/// Category: integration
/// Coverage: Smol runtime compatibility, async listen, task spawning
#[test]
fn test_smol_basic() {
smol::block_on(async {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
smol::spawn(async move {
smol::Timer::after(Duration::from_millis(10)).await;
announcer.announce(42).unwrap();
})
.detach();
let result = listener.listen().await.unwrap();
assert_eq!(result, 42);
});
}
/// Test: Multiple listeners with smol runtime
///
/// Verifies: Broadcast semantics work correctly with smol runtime
/// Runtime: smol
/// Category: integration
/// Coverage: Multiple smol tasks, concurrent listeners, value sharing
#[test]
fn test_smol_multiple_listeners() {
smol::block_on(async {
let (announcer, announcement) = Announcement::new();
let l1 = announcement.listener();
let l2 = announcement.listener();
let h1 = smol::spawn(async move { l1.listen().await.unwrap() });
let h2 = smol::spawn(async move { l2.listen().await.unwrap() });
announcer.announce(42).unwrap();
assert_eq!(h1.await, 42);
assert_eq!(h2.await, 42);
});
}