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
+396
@@ -0,0 +1,396 @@
|
||||
# Test Documentation Standard
|
||||
|
||||
This document defines the standard format for documenting tests in the `announcement` crate. Every test in this codebase MUST have a properly formatted header following these conventions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Header Format](#header-format)
|
||||
2. [Category Definitions](#category-definitions)
|
||||
3. [Writing Guidelines](#writing-guidelines)
|
||||
4. [Examples](#examples)
|
||||
5. [Maintenance](#maintenance)
|
||||
|
||||
## Header Format
|
||||
|
||||
### Standard Test Header
|
||||
|
||||
```rust
|
||||
/// Test: [One-line summary of what is tested]
|
||||
///
|
||||
/// Verifies: [What behavior/invariant is verified]
|
||||
/// Category: [Type: unit/integration/property/stress/timing/etc]
|
||||
/// Coverage: [What code paths/scenarios are exercised]
|
||||
#[test]
|
||||
fn test_name() {
|
||||
// test body
|
||||
}
|
||||
```
|
||||
|
||||
### Property Test Header
|
||||
|
||||
```rust
|
||||
/// Property: [Invariant being tested]
|
||||
///
|
||||
/// Verifies: [What this property guarantees]
|
||||
/// Strategy: [What input generation strategy is used]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_name(inputs...) {
|
||||
// property test body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Test Header
|
||||
|
||||
```rust
|
||||
/// Test: [One-line summary]
|
||||
///
|
||||
/// Verifies: [Behavior being verified]
|
||||
/// Runtime: tokio/smol
|
||||
/// Category: [Type]
|
||||
#[tokio::test]
|
||||
async fn test_name() {
|
||||
// async test body
|
||||
}
|
||||
```
|
||||
|
||||
### Test Header with Notes
|
||||
|
||||
For tests with special conditions or important context:
|
||||
|
||||
```rust
|
||||
/// Test: [Summary]
|
||||
///
|
||||
/// Verifies: [Behavior]
|
||||
/// Category: [Type]
|
||||
/// Note: [Any important context - e.g., "Runs 1000 iterations to catch race conditions"]
|
||||
#[test]
|
||||
fn test_name() {
|
||||
// test body
|
||||
}
|
||||
```
|
||||
|
||||
**Historical Note**: Previously, timing-sensitive tests used `#[cfg(not(tarpaulin))]` to exclude them from coverage due to `cargo-tarpaulin`'s high instrumentation overhead. With the migration to `llvm-cov`, all tests now run during coverage analysis.
|
||||
|
||||
## Field Definitions
|
||||
|
||||
### Test / Property
|
||||
**Required:** Yes
|
||||
**Format:** One sentence, imperative mood
|
||||
**Purpose:** Immediately tells the reader what this test does
|
||||
**Examples:**
|
||||
- "Test: Basic async listen operation"
|
||||
- "Test: Multiple listeners receive same value"
|
||||
- "Property: All listeners receive announced value"
|
||||
|
||||
### Verifies
|
||||
**Required:** Yes
|
||||
**Format:** One sentence describing the guarantee or behavior
|
||||
**Purpose:** Explains what invariant or behavior this test validates
|
||||
**Examples:**
|
||||
- "Verifies: Listeners can wait asynchronously and receive values"
|
||||
- "Verifies: Broadcast semantics work correctly"
|
||||
- "Verifies: All N listeners receive the same announced value regardless of N"
|
||||
|
||||
### Category
|
||||
**Required:** Yes
|
||||
**Format:** One of the defined categories (see below)
|
||||
**Purpose:** Classifies the test for filtering and organization
|
||||
**Examples:**
|
||||
- "Category: integration"
|
||||
- "Category: property"
|
||||
- "Category: stress"
|
||||
|
||||
### Coverage / Strategy
|
||||
**Optional:** Use "Coverage" for regular tests, "Strategy" for property tests
|
||||
**Format:** Brief description of scope or approach
|
||||
**Purpose:** Explains what code paths or input strategies are used
|
||||
**Examples:**
|
||||
- "Coverage: Async listen path, event notification, value cloning"
|
||||
- "Strategy: Generates 1-100 listeners with random values"
|
||||
|
||||
### Runtime
|
||||
**Optional:** For async tests only
|
||||
**Format:** tokio/smol/both
|
||||
**Purpose:** Specifies which async runtime(s) are tested
|
||||
**Example:** "Runtime: tokio"
|
||||
|
||||
### Excluded
|
||||
**Optional:** Only for tests excluded from coverage or CI
|
||||
**Format:** "Reason: explanation"
|
||||
**Purpose:** Explains why test is excluded
|
||||
**Example:** "Excluded: Coverage: timing-sensitive"
|
||||
|
||||
### Note
|
||||
**Optional:** For additional context
|
||||
**Format:** Any relevant information
|
||||
**Purpose:** Provides context that doesn't fit other fields
|
||||
**Example:** "Note: Runs 1000 iterations to catch race conditions"
|
||||
|
||||
## Category Definitions
|
||||
|
||||
### unit
|
||||
Tests of individual functions or methods in isolation. Should be fast and deterministic.
|
||||
|
||||
### integration
|
||||
End-to-end tests combining multiple components. Tests public API behavior.
|
||||
|
||||
### property
|
||||
Property-based tests using `proptest` or `quickcheck`. Validates invariants across many generated inputs.
|
||||
|
||||
### stress
|
||||
High-volume tests with many iterations or large inputs. May take longer to run.
|
||||
|
||||
### timing
|
||||
Tests dependent on specific timing or delays. Often excluded from coverage due to flakiness.
|
||||
|
||||
### race
|
||||
Tests for race conditions, TOCTOU bugs, or concurrency edge cases.
|
||||
|
||||
### concurrency
|
||||
Multi-threaded tests verifying thread safety and concurrent access patterns.
|
||||
|
||||
### panic
|
||||
Tests verifying correct behavior when panics occur. Tests panic safety and resource cleanup.
|
||||
|
||||
### edge
|
||||
Tests of boundary conditions, extreme values, and unusual inputs.
|
||||
|
||||
### type
|
||||
Tests verifying correct behavior with different Rust types.
|
||||
|
||||
### memory
|
||||
Tests verifying memory safety, leak prevention, or Arc refcounting.
|
||||
|
||||
### mutation
|
||||
Tests designed to detect semantic bugs through mutation testing.
|
||||
|
||||
## Writing Guidelines
|
||||
|
||||
### Be Concise
|
||||
Test headers should be scannable. Aim for clarity over completeness.
|
||||
|
||||
**Good:**
|
||||
```rust
|
||||
/// Test: Listener receives value after announce
|
||||
///
|
||||
/// Verifies: Basic async listen operation works correctly
|
||||
/// Category: integration
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```rust
|
||||
/// Test: This test creates an announcement channel with an announcer and an announcement,
|
||||
/// then creates a listener from the announcement, spawns an async task that announces a value,
|
||||
/// and verifies that the listener can successfully receive that value through the async
|
||||
/// listen() method...
|
||||
///
|
||||
/// [excessive verbosity]
|
||||
```
|
||||
|
||||
### Be Specific
|
||||
Say exactly what's being tested, not vague generalities.
|
||||
|
||||
**Good:**
|
||||
```rust
|
||||
/// Property: Arc refcount equals 1 + N listeners after announce
|
||||
///
|
||||
/// Verifies: No reference leaks in Arc-wrapped values
|
||||
/// Strategy: Tests with 0-50 listeners
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```rust
|
||||
/// Property: Arc stuff works
|
||||
///
|
||||
/// Verifies: Reference counting
|
||||
/// Strategy: Random inputs
|
||||
```
|
||||
|
||||
### Use Imperative Mood
|
||||
Start test summaries with verbs.
|
||||
|
||||
**Good:**
|
||||
- "Test: Multiple listeners receive same value"
|
||||
- "Test: Panic in Drop doesn't leak memory"
|
||||
|
||||
**Bad:**
|
||||
- "Test: When multiple listeners are created..."
|
||||
- "Test: The case where Drop panics..."
|
||||
|
||||
### Keep Categories Consistent
|
||||
Use defined categories. Don't invent new ones without updating this document.
|
||||
|
||||
### Document the Why
|
||||
The "Verifies" field should explain WHY the test exists, not just WHAT it does.
|
||||
|
||||
**Good:**
|
||||
```rust
|
||||
/// Verifies: Channel cleanup happens correctly when announcer drops without announcing
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
```rust
|
||||
/// Verifies: Test passes
|
||||
```
|
||||
|
||||
### Note Special Conditions
|
||||
If a test has unusual requirements, exclusions, or setup, note it.
|
||||
|
||||
```rust
|
||||
/// Test: Timeout behavior under load
|
||||
///
|
||||
/// Verifies: Timeouts work correctly even with many concurrent operations
|
||||
/// Category: stress
|
||||
/// Excluded: Coverage: takes too long (5+ seconds)
|
||||
/// Note: Runs 10,000 iterations to ensure timing is reliable
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Unit Test
|
||||
|
||||
```rust
|
||||
/// Test: is_announced returns false before announce, true after
|
||||
///
|
||||
/// Verifies: State transition from unannounced to announced
|
||||
/// Category: unit
|
||||
#[test]
|
||||
fn test_is_announced() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
assert!(!announcement.is_announced());
|
||||
announcer.announce(42).unwrap();
|
||||
assert!(announcement.is_announced());
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Integration Test
|
||||
|
||||
```rust
|
||||
/// Test: Multiple async listeners receive broadcast value
|
||||
///
|
||||
/// Verifies: Broadcast semantics work across multiple async tasks
|
||||
/// Runtime: tokio
|
||||
/// Category: integration
|
||||
/// Coverage: Async listen, concurrent task spawning, value cloning
|
||||
#[tokio::test]
|
||||
async fn test_multiple_async_listeners() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
// test body...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Property Test
|
||||
|
||||
```rust
|
||||
/// Property: All N listeners receive the same announced value
|
||||
///
|
||||
/// Verifies: Broadcast semantics hold for arbitrary listener counts
|
||||
/// Strategy: Generates 1-100 listeners with random i32 values
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_all_listeners_receive_value(value: i32, listener_count in 1usize..100) {
|
||||
// property test body...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Stress Test
|
||||
|
||||
```rust
|
||||
/// Test: TOCTOU protection under extreme contention
|
||||
///
|
||||
/// Verifies: Double-check pattern prevents races even with many threads
|
||||
/// Category: race
|
||||
/// Note: Runs 1000 iterations with tight timing windows to catch race conditions
|
||||
#[test]
|
||||
fn test_toctou_extreme_contention() {
|
||||
// stress test body...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 5: Panic Safety Test
|
||||
|
||||
```rust
|
||||
/// Test: Arc refcount remains correct after panic
|
||||
///
|
||||
/// Verifies: Reference counting is panic-safe
|
||||
/// Category: panic
|
||||
/// Coverage: Arc refcount tracking, panic recovery, Drop behavior
|
||||
#[test]
|
||||
fn test_arc_refcount_with_panic() {
|
||||
// panic safety test body...
|
||||
}
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Adding New Tests
|
||||
|
||||
When adding a new test:
|
||||
|
||||
1. **Choose the right category** - Review [Category Definitions](#category-definitions)
|
||||
2. **Write the header first** - Document what you're testing before writing the test
|
||||
3. **Keep it concise** - One-line summaries, focused "Verifies" statements
|
||||
4. **Be consistent** - Follow existing patterns in the same test file
|
||||
|
||||
### Updating Existing Tests
|
||||
|
||||
When modifying a test:
|
||||
|
||||
1. **Update the header** - If the test behavior changes, update the documentation
|
||||
2. **Maintain format** - Keep the same structure as other tests
|
||||
3. **Check categories** - Ensure the category is still accurate
|
||||
|
||||
### Reviewing Test Documentation
|
||||
|
||||
When reviewing PRs:
|
||||
|
||||
1. **Check for headers** - Every test must have a header
|
||||
2. **Verify clarity** - Test purpose should be immediately obvious
|
||||
3. **Validate categories** - Categories should match the definitions
|
||||
4. **Ensure consistency** - Format should match examples in this document
|
||||
|
||||
### Cross-Reference with TEST_DOCUMENTATION.md
|
||||
|
||||
This document defines the **in-code** header format. The file `TEST_DOCUMENTATION.md` provides a **high-level overview** of the entire test suite. When adding tests:
|
||||
|
||||
1. Update in-code headers (this document)
|
||||
2. Update TEST_DOCUMENTATION.md if adding new test categories or files
|
||||
3. Keep both documents in sync
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Use Headers?
|
||||
|
||||
1. **Immediate Clarity** - Know what a test does without reading the entire body
|
||||
2. **Easy Scanning** - Quickly find tests by category or purpose
|
||||
3. **Better Maintenance** - Clear documentation helps future developers
|
||||
4. **Professional Polish** - Shows attention to detail and code quality
|
||||
5. **Onboarding** - New contributors understand tests faster
|
||||
|
||||
### Why This Format?
|
||||
|
||||
1. **Rust Conventions** - Uses standard `///` doc comments
|
||||
2. **Concise** - Respects the codebase's preference for simplicity
|
||||
3. **Structured** - Fields provide consistent organization
|
||||
4. **Scannable** - Visual consistency makes scanning fast
|
||||
5. **Flexible** - Optional fields allow customization without clutter
|
||||
|
||||
### Why Mandatory?
|
||||
|
||||
Test documentation is not optional in this codebase. Every test MUST have a header because:
|
||||
|
||||
1. **Code Quality** - Documentation is part of code quality
|
||||
2. **Maintainability** - Undocumented tests are technical debt
|
||||
3. **Consistency** - Partial documentation is worse than none
|
||||
4. **Professional Standards** - We hold ourselves to high standards
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-13
|
||||
**Test Count**: ~628 tests across 24 files
|
||||
**Compliance**: 100% (in progress)
|
||||
|
||||
For high-level test suite overview, see [TEST_DOCUMENTATION.md](../TEST_DOCUMENTATION.md).
|
||||
@@ -0,0 +1,128 @@
|
||||
//! Race condition test: Immediate announcer drop
|
||||
//!
|
||||
//! This test verifies that all waiting listeners successfully receive
|
||||
//! a value even when the announcer is dropped immediately after announce().
|
||||
//!
|
||||
//! Pattern being tested:
|
||||
//! 1. Create announcement and listeners
|
||||
//! 2. Spawn N tasks calling .listen().await
|
||||
//! 3. Call announce()
|
||||
//! 4. IMMEDIATELY drop announcer (before awaiting task handles)
|
||||
//! 5. Verify ALL listeners successfully receive the value
|
||||
//!
|
||||
//! If this test FAILS, there's a race condition where the announcer dropping
|
||||
//! can close the channel before all listeners read the value.
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Test: Race: try_listen after drop
|
||||
///
|
||||
/// Verifies: Verifies try_listen() works after announcer has dropped
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_immediate_drop_after_announce() {
|
||||
// Test with various listener counts
|
||||
for num_listeners in [1, 10, 100, 1000] {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Spawn N listeners
|
||||
let mut handles = Vec::with_capacity(num_listeners);
|
||||
for i in 0..num_listeners {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
listener.listen().await.map(|v: Arc<i32>| (*v, i))
|
||||
}));
|
||||
}
|
||||
|
||||
// Small delay to ensure tasks are spawned and waiting
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
||||
|
||||
// Announce (announcer is consumed and dropped by announce())
|
||||
let value = Arc::new(42);
|
||||
announcer.announce(value.clone()).unwrap();
|
||||
|
||||
// Verify ALL listeners received the value successfully
|
||||
for handle in handles {
|
||||
let result = handle.await.expect("Task should not panic");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Listener should successfully receive value after immediate announcer drop"
|
||||
);
|
||||
let (received_value, _) = result.unwrap();
|
||||
assert_eq!(
|
||||
received_value, 42,
|
||||
"Received value should match announced value"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Race: try_listen after drop
|
||||
///
|
||||
/// Verifies: Verifies try_listen() works after announcer has dropped
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_announce_then_drop_no_delay() {
|
||||
// Even more aggressive: no delay between spawn and announce
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = Vec::with_capacity(100);
|
||||
for i in 0..100 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
listener.listen().await.map(|v: Arc<i32>| (*v, i))
|
||||
}));
|
||||
}
|
||||
|
||||
// NO DELAY - announce immediately (announcer consumed by announce)
|
||||
let value = Arc::new(999);
|
||||
announcer.announce(value.clone()).unwrap();
|
||||
|
||||
// All should still receive
|
||||
for handle in handles {
|
||||
let result = handle.await.expect("Task should not panic");
|
||||
assert!(result.is_ok(), "All listeners should receive value");
|
||||
assert_eq!(result.unwrap().0, 999);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Race: try_listen after drop
|
||||
///
|
||||
/// Verifies: Verifies try_listen() works after announcer has dropped
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_drop_before_listen_completes() {
|
||||
// Test: Does listen() after announce + drop work?
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let value = Arc::new(777);
|
||||
announcer.announce(value.clone()).unwrap();
|
||||
// Announcer consumed by announce()
|
||||
|
||||
// This should still work - value is already available
|
||||
let result = listener.listen().await;
|
||||
assert!(result.is_ok(), "Should receive value even after drop");
|
||||
assert_eq!(*result.unwrap(), 777);
|
||||
}
|
||||
|
||||
/// Test: Race: try_listen after drop
|
||||
///
|
||||
/// Verifies: Verifies try_listen() works after announcer has dropped
|
||||
/// Category: race
|
||||
#[test]
|
||||
fn test_try_listen_after_drop() {
|
||||
// Synchronous version: try_listen after drop
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let value = Arc::new(555);
|
||||
announcer.announce(value.clone()).unwrap();
|
||||
// Announcer consumed by announce()
|
||||
|
||||
// try_listen should work
|
||||
let result = listener.try_listen();
|
||||
assert!(result.is_some(), "Should get value via try_listen");
|
||||
assert_eq!(*result.unwrap(), 555);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
//! Arc refcount stress test - rapid announce/drop cycles
|
||||
//! Tests for memory leaks in rapid channel creation/destruction with Arc values.
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
/// Test: Arc refcount: cycle detection
|
||||
///
|
||||
/// Verifies: Arc reference cycles are handled correctly
|
||||
/// Category: refcount
|
||||
// Test 1: Rapid Arc refcount cycles (single-threaded)
|
||||
#[test]
|
||||
fn test_arc_refcount_rapid_cycles() {
|
||||
let data = Arc::new(vec![0u8; 1024]); // 1KB test data
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
// Run 10,000 create/announce/drop cycles
|
||||
for i in 0..10_000 {
|
||||
{
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create a few listeners
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
let l3 = announcement.listener();
|
||||
|
||||
// Announce the Arc
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// Retrieve values
|
||||
let _v1 = l1.try_listen().unwrap();
|
||||
let _v2 = l2.try_listen().unwrap();
|
||||
let _v3 = l3.try_listen().unwrap();
|
||||
|
||||
// Everything drops here when block ends
|
||||
}
|
||||
|
||||
// Periodic check after everything has dropped
|
||||
if i % 1000 == 0 {
|
||||
let current_count = Arc::strong_count(&data);
|
||||
assert_eq!(
|
||||
current_count, initial_count,
|
||||
"Refcount leak detected at iteration {}: expected {}, got {}",
|
||||
i, initial_count, current_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Final verification
|
||||
assert_eq!(
|
||||
Arc::strong_count(&data),
|
||||
initial_count,
|
||||
"Final refcount mismatch after 10k cycles"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Refcount test: arc refcount mixed drop patterns
|
||||
///
|
||||
/// Verifies: Verifies arc refcount mixed drop patterns
|
||||
/// Category: refcount
|
||||
// Test 2: Rapid Arc cycles with mixed drop patterns
|
||||
#[test]
|
||||
fn test_arc_refcount_mixed_drop_patterns() {
|
||||
let data = Arc::new(vec![42u8; 512]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
for i in 0..5_000 {
|
||||
{
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create various numbers of listeners
|
||||
let listener_count = (i % 10) + 1;
|
||||
let mut listeners = Vec::new();
|
||||
for _ in 0..listener_count {
|
||||
listeners.push(announcement.listener());
|
||||
}
|
||||
|
||||
// Announce
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// Drop listeners in different patterns
|
||||
match i % 4 {
|
||||
0 => drop(listeners), // Drop all at once
|
||||
1 => {
|
||||
// Drop one at a time
|
||||
for l in listeners {
|
||||
drop(l);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Retrieve then drop
|
||||
let values: Vec<_> =
|
||||
listeners.iter().map(|l| l.try_listen().unwrap()).collect();
|
||||
drop(values);
|
||||
drop(listeners);
|
||||
}
|
||||
_ => {
|
||||
// Drop half, retrieve from rest
|
||||
let mid = listeners.len() / 2;
|
||||
drop(listeners.drain(0..mid).collect::<Vec<_>>());
|
||||
for l in &listeners {
|
||||
let _ = l.try_listen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Block ends - everything drops including announcement
|
||||
}
|
||||
|
||||
// Verify refcount periodically after everything dropped
|
||||
if i % 500 == 0 {
|
||||
assert_eq!(Arc::strong_count(&data), initial_count);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(Arc::strong_count(&data), initial_count);
|
||||
}
|
||||
|
||||
/// Test: Arc refcount: cycle detection
|
||||
///
|
||||
/// Verifies: Arc reference cycles are handled correctly
|
||||
/// Category: refcount
|
||||
// Test 3: Concurrent Arc refcount stress
|
||||
#[test]
|
||||
fn test_arc_refcount_concurrent_cycles() {
|
||||
let data = Arc::new(vec![0u8; 2048]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// 8 threads all doing rapid cycles
|
||||
for thread_id in 0..8 {
|
||||
let data_clone = data.clone();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
for i in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut listeners = Vec::new();
|
||||
for _ in 0..5 {
|
||||
listeners.push(announcement.listener());
|
||||
}
|
||||
|
||||
announcer.announce(data_clone.clone()).unwrap();
|
||||
|
||||
// Mix of operations
|
||||
match i % 3 {
|
||||
0 => {
|
||||
// Just drop everything
|
||||
drop(listeners);
|
||||
}
|
||||
1 => {
|
||||
// Retrieve and verify
|
||||
for l in &listeners {
|
||||
let v = l.try_listen().unwrap();
|
||||
assert_eq!(v.len(), 2048);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Mixed
|
||||
let _ = listeners[0].try_listen();
|
||||
drop(listeners);
|
||||
}
|
||||
}
|
||||
|
||||
if i % 100 == 0 && thread_id == 0 {
|
||||
println!("Thread {} completed {} iterations", thread_id, i);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all threads
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// Final refcount should be back to initial
|
||||
assert_eq!(
|
||||
Arc::strong_count(&data),
|
||||
initial_count,
|
||||
"Refcount leak after concurrent stress test"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Refcount test: arc refcount with announcer drop
|
||||
///
|
||||
/// Verifies: Verifies arc refcount with announcer drop
|
||||
/// Category: refcount
|
||||
// Test 4: Arc refcount with announcer drop (no announce)
|
||||
#[test]
|
||||
fn test_arc_refcount_with_announcer_drop() {
|
||||
let data = Arc::new(vec![1u8; 256]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
for _ in 0..10_000 {
|
||||
let (announcer, announcement) = Announcement::<Arc<Vec<u8>>>::new();
|
||||
|
||||
let _l1 = announcement.listener();
|
||||
let _l2 = announcement.listener();
|
||||
|
||||
// Drop announcer WITHOUT announcing
|
||||
drop(announcer);
|
||||
|
||||
// Listeners should see closed, no Arc refcount change for data
|
||||
// since it was never announced
|
||||
}
|
||||
|
||||
// Refcount should be unchanged
|
||||
assert_eq!(Arc::strong_count(&data), initial_count);
|
||||
}
|
||||
|
||||
/// Test: Refcount test: arc refcount with listener cloning
|
||||
///
|
||||
/// Verifies: Verifies arc refcount with listener cloning
|
||||
/// Category: refcount
|
||||
// Test 5: Arc refcount with listener cloning stress
|
||||
#[test]
|
||||
fn test_arc_refcount_with_listener_cloning() {
|
||||
let data = Arc::new(vec![7u8; 512]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
for _ in 0..5_000 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let l1 = announcement.listener();
|
||||
|
||||
// Clone listener many times
|
||||
let mut clones = vec![l1.clone()];
|
||||
for _ in 0..10 {
|
||||
clones.push(clones[0].clone());
|
||||
}
|
||||
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// All clones retrieve
|
||||
for l in &clones {
|
||||
let _v = l.try_listen().unwrap();
|
||||
}
|
||||
|
||||
// Everything drops
|
||||
}
|
||||
|
||||
assert_eq!(Arc::strong_count(&data), initial_count);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
//! Tests specifically for close() behavior and mutation testing coverage
|
||||
//!
|
||||
//! These tests target gaps in mutation testing, particularly around the
|
||||
//! explicit close() method.
|
||||
|
||||
use announcement::{Announcement, ListenError};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test: Verify close() actually sets the closed flag
|
||||
///
|
||||
/// Verifies: close() actually sets the closed flag
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_sets_closed_flag() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Initially not closed
|
||||
assert!(!announcement.is_closed());
|
||||
|
||||
// Call close()
|
||||
announcer.close();
|
||||
|
||||
// Verify closed flag is set
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Verify close() notifies waiting async listeners
|
||||
///
|
||||
/// Verifies: close() notifies waiting async listeners
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_close_notifies_async_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn task that waits
|
||||
let handle = tokio::spawn(async move { listener.listen().await });
|
||||
|
||||
// Give listener time to start waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Close the announcer
|
||||
announcer.close();
|
||||
|
||||
// Listener should receive Closed error quickly
|
||||
let result = tokio::time::timeout(Duration::from_millis(100), handle).await;
|
||||
assert!(result.is_ok(), "Listener should be notified immediately");
|
||||
|
||||
let listen_result = result.unwrap().unwrap();
|
||||
assert_eq!(listen_result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Close test: close notifies blocking listeners
|
||||
///
|
||||
/// Verifies: close notifies blocking listeners
|
||||
/// Category: close
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_close_notifies_blocking_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn thread that waits
|
||||
let handle =
|
||||
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_millis(100)));
|
||||
|
||||
// Give listener time to start waiting
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Close the announcer
|
||||
announcer.close();
|
||||
|
||||
// Listener should receive Closed error quickly
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Verify close() notifies multiple waiting listeners
|
||||
///
|
||||
/// Verifies: close() notifies multiple waiting listeners
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_close_notifies_all_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Create multiple listeners
|
||||
let listeners: Vec<_> = (0..10).map(|_| announcement.listener()).collect();
|
||||
|
||||
// Spawn tasks for all listeners
|
||||
let mut handles = Vec::new();
|
||||
for listener in listeners {
|
||||
handles.push(tokio::spawn(async move { listener.listen().await }));
|
||||
}
|
||||
|
||||
// Give listeners time to start waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Close the announcer
|
||||
announcer.close();
|
||||
|
||||
// All listeners should receive Closed error
|
||||
for handle in handles {
|
||||
let result = tokio::time::timeout(Duration::from_millis(100), handle).await;
|
||||
assert!(result.is_ok(), "All listeners should be notified");
|
||||
|
||||
let listen_result = result.unwrap().unwrap();
|
||||
assert_eq!(listen_result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Verify close() works even with no listeners
|
||||
///
|
||||
/// Verifies: close() works even with no listeners
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_with_no_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Close without creating any listeners
|
||||
announcer.close();
|
||||
|
||||
// Verify closed flag is set
|
||||
assert!(announcement.is_closed());
|
||||
|
||||
// Create a listener after close
|
||||
let listener = announcement.listener();
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Verify close() prevents future listeners from waiting
|
||||
///
|
||||
/// Verifies: close() prevents future listeners from waiting
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_close_prevents_future_waiting() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Close immediately
|
||||
announcer.close();
|
||||
|
||||
// Create listener after close
|
||||
let listener = announcement.listener();
|
||||
|
||||
// listen() should return immediately with Closed
|
||||
let result = tokio::time::timeout(Duration::from_millis(50), listener.listen()).await;
|
||||
assert!(result.is_ok(), "listen() should not hang after close");
|
||||
|
||||
let listen_result = result.unwrap();
|
||||
assert_eq!(listen_result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Verify close() interacts correctly with try_listen()
|
||||
///
|
||||
/// Verifies: close() interacts correctly with try_listen()
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_try_listen_interaction() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// try_listen() returns None before close
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
assert!(!listener.is_closed());
|
||||
|
||||
// Close the announcer
|
||||
announcer.close();
|
||||
|
||||
// try_listen() still returns None after close
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
// But is_closed() returns true
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close followed by listener checks in rapid succession
|
||||
///
|
||||
/// Verifies: Close followed by listener checks in rapid succession
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_rapid_checks() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.close();
|
||||
|
||||
// Rapidly check closed status
|
||||
for _ in 0..1000 {
|
||||
assert!(listener.is_closed());
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Verify close() with Arc-wrapped value type
|
||||
///
|
||||
/// Verifies: close() with Arc-wrapped value type
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_with_arc_type() {
|
||||
let (announcer, announcement) = Announcement::<Arc<String>>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.close();
|
||||
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
}
|
||||
|
||||
/// Test: Multiple threads checking closed status after close()
|
||||
///
|
||||
/// Verifies: Multiple threads checking closed status after close()
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_close_concurrent_status_checks() {
|
||||
use std::sync::Barrier;
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let barrier = Arc::new(Barrier::new(11)); // 10 threads + main thread
|
||||
|
||||
// Create listeners in multiple threads
|
||||
let handles: Vec<_> = (0..10)
|
||||
.map(|_| {
|
||||
let ann = announcement.clone();
|
||||
let barrier = Arc::clone(&barrier);
|
||||
thread::spawn(move || {
|
||||
let listener = ann.listener();
|
||||
// Wait for close() to complete via barrier synchronization
|
||||
barrier.wait();
|
||||
listener.is_closed()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Close the announcer
|
||||
announcer.close();
|
||||
|
||||
// Synchronize: ensure close() completes before threads check
|
||||
barrier.wait();
|
||||
|
||||
// All threads should see closed = true
|
||||
for handle in handles {
|
||||
let is_closed = handle.join().unwrap();
|
||||
assert!(is_closed, "All threads should see closed flag");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
//! Close signal race condition tests
|
||||
//! Tests that closing an announcer properly signals listeners even during active listen operations.
|
||||
|
||||
use announcement::{Announcement, ListenError};
|
||||
#[cfg(feature = "std")]
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Barrier,
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test: Race condition test: close during blocking listen
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close during blocking listen
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_close_during_active_listen() {
|
||||
for iteration in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn task that will listen
|
||||
let listen_handle = tokio::spawn(async move { listener.listen().await });
|
||||
|
||||
// Close at varying times
|
||||
match iteration % 5 {
|
||||
0 => {
|
||||
// Close immediately
|
||||
announcer.close();
|
||||
}
|
||||
1 => {
|
||||
// Short delay
|
||||
tokio::time::sleep(Duration::from_micros(1)).await;
|
||||
announcer.close();
|
||||
}
|
||||
2 => {
|
||||
// Medium delay
|
||||
tokio::time::sleep(Duration::from_micros(10)).await;
|
||||
announcer.close();
|
||||
}
|
||||
3 => {
|
||||
// Drop instead of explicit close
|
||||
drop(announcer);
|
||||
}
|
||||
_ => {
|
||||
// Yield and close
|
||||
tokio::task::yield_now().await;
|
||||
announcer.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Listener should see Closed error
|
||||
let result = listen_handle.await.unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed), "Iteration {}", iteration);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Race condition test: close during blocking listen
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close during blocking listen
|
||||
/// Category: race
|
||||
#[test]
|
||||
fn test_close_during_blocking_listen() {
|
||||
for iteration in 0..500 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Thread that will block
|
||||
let handle = thread::spawn(move || listener.listen_blocking());
|
||||
|
||||
// Close at varying times
|
||||
match iteration % 4 {
|
||||
0 => {
|
||||
// Immediate
|
||||
announcer.close();
|
||||
}
|
||||
1 => {
|
||||
// Short delay
|
||||
thread::sleep(Duration::from_micros(100));
|
||||
announcer.close();
|
||||
}
|
||||
2 => {
|
||||
// Drop
|
||||
thread::sleep(Duration::from_micros(50));
|
||||
drop(announcer);
|
||||
}
|
||||
_ => {
|
||||
// Yield and close
|
||||
thread::yield_now();
|
||||
announcer.close();
|
||||
}
|
||||
}
|
||||
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Race condition test: close during timeout blocking
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close during timeout blocking
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_close_with_concurrent_listeners() {
|
||||
for _ in 0..500 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// Spawn 10 listeners
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move { listener.listen().await }));
|
||||
}
|
||||
|
||||
// Give them time to set up
|
||||
tokio::time::sleep(Duration::from_micros(10)).await;
|
||||
|
||||
// Close
|
||||
announcer.close();
|
||||
|
||||
// All should see Closed
|
||||
for handle in handles {
|
||||
let result = handle.await.unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Race condition test: close during timeout blocking
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close during timeout blocking
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_close_in_toctou_window() {
|
||||
for iteration in 0..2_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let announcement_clone = announcement.clone();
|
||||
|
||||
// Thread that closes at random time
|
||||
let close_handle = tokio::spawn(async move {
|
||||
// Vary timing to hit different points
|
||||
match iteration % 7 {
|
||||
0 => {}
|
||||
1 => tokio::task::yield_now().await,
|
||||
2 => tokio::time::sleep(Duration::from_nanos(1)).await,
|
||||
3 => tokio::time::sleep(Duration::from_nanos(10)).await,
|
||||
4 => tokio::time::sleep(Duration::from_nanos(100)).await,
|
||||
5 => tokio::time::sleep(Duration::from_micros(1)).await,
|
||||
_ => tokio::time::sleep(Duration::from_micros(10)).await,
|
||||
}
|
||||
announcer.close();
|
||||
});
|
||||
|
||||
// Task that listens
|
||||
let listen_handle = tokio::spawn(async move {
|
||||
let listener = announcement_clone.listener();
|
||||
listener.listen().await
|
||||
});
|
||||
|
||||
close_handle.await.unwrap();
|
||||
let result = listen_handle.await.unwrap();
|
||||
|
||||
// Must see either Closed error or timeout (but not hang)
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Race condition test: close during timeout blocking
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close during timeout blocking
|
||||
/// Category: race
|
||||
#[test]
|
||||
fn test_close_during_timeout_blocking() {
|
||||
for iteration in 0..500 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle =
|
||||
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_secs(2)));
|
||||
|
||||
// Close at varying times
|
||||
if iteration % 2 == 0 {
|
||||
thread::sleep(Duration::from_micros(50));
|
||||
}
|
||||
announcer.close();
|
||||
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Race condition test: close with barrier sync
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close with barrier sync
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_rapid_close_listen_cycles() {
|
||||
for _ in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let listen_handle = tokio::spawn(async move { listener.listen().await });
|
||||
|
||||
// Close immediately
|
||||
announcer.close();
|
||||
|
||||
let result = listen_handle.await.unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Race condition test: close with barrier sync
|
||||
///
|
||||
/// Verifies: Verifies no race condition in close with barrier sync
|
||||
/// Category: race
|
||||
#[test]
|
||||
fn test_close_with_barrier_sync() {
|
||||
for _ in 0..500 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let barrier = Arc::new(Barrier::new(11)); // 10 listeners + closer
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// 10 threads that will block
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
let barrier_clone = barrier.clone();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
barrier_clone.wait(); // Sync start
|
||||
listener.listen_blocking()
|
||||
}));
|
||||
}
|
||||
|
||||
// Closer thread
|
||||
let closer = thread::spawn(move || {
|
||||
barrier.wait(); // Sync with listeners
|
||||
// Close immediately after barrier
|
||||
announcer.close();
|
||||
});
|
||||
|
||||
closer.join().unwrap();
|
||||
|
||||
// All listeners should see Closed
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Stress test: close propagation stress
|
||||
///
|
||||
/// Verifies: Verifies system handles close propagation stress correctly under stress
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_close_detection_is_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
assert!(!announcement.is_closed());
|
||||
assert!(!listener.is_closed());
|
||||
|
||||
// Close in background
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
announcer.close();
|
||||
});
|
||||
|
||||
// Poll until closed
|
||||
let start = tokio::time::Instant::now();
|
||||
loop {
|
||||
if listener.is_closed() {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
panic!("is_closed() never became true");
|
||||
}
|
||||
tokio::time::sleep(Duration::from_micros(100)).await;
|
||||
}
|
||||
|
||||
// Now listen() should return Closed immediately
|
||||
let result = listener.listen().await;
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Stress test: close propagation stress
|
||||
///
|
||||
/// Verifies: Verifies system handles close propagation stress correctly under stress
|
||||
/// Category: race
|
||||
#[test]
|
||||
fn test_close_propagation_stress() {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// Run for 5 seconds
|
||||
for _ in 0..10 {
|
||||
let running_clone = running.clone();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
let mut count = 0;
|
||||
|
||||
while running_clone.load(Ordering::Relaxed) {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn blocking thread
|
||||
let h = thread::spawn(move || listener.listen_blocking());
|
||||
|
||||
// Close quickly
|
||||
thread::yield_now();
|
||||
announcer.close();
|
||||
|
||||
let result = h.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
count
|
||||
}));
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
running.store(false, Ordering::Relaxed);
|
||||
|
||||
let total: usize = handles.into_iter().map(|h| h.join().unwrap()).sum();
|
||||
println!("Close propagation stress: {} successful cycles", total);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_close_with_try_listen_checks() {
|
||||
for _ in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Task doing rapid try_listen
|
||||
let check_handle = tokio::spawn(async move {
|
||||
for _ in 0..100 {
|
||||
let _ = listener.try_listen();
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
listener
|
||||
});
|
||||
|
||||
// Close
|
||||
tokio::task::yield_now().await;
|
||||
announcer.close();
|
||||
|
||||
// Get listener back and verify it sees closed
|
||||
let listener = check_handle.await.unwrap();
|
||||
assert!(listener.is_closed());
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
//! Closed Detection Tests
|
||||
//!
|
||||
//! Tests for the closed detection API that notifies listeners when
|
||||
//! an announcer is dropped or explicitly closed without announcing.
|
||||
//!
|
||||
//! NOTE: All async listen() calls use short timeouts (100ms) to prevent
|
||||
//! hanging if Drop impl is mutated. This ensures fast failure detection
|
||||
//! in mutation testing.
|
||||
|
||||
use announcement::{Announcement, ListenError};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Timeout for listen operations in tests (short to catch Drop impl mutations)
|
||||
const LISTEN_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
|
||||
// =============================================================================
|
||||
// Basic Closed Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: dropped announcer notifies listener
|
||||
///
|
||||
/// Verifies: Verifies dropped announcer notifies listener behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_dropped_announcer_notifies_listener() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Drop announcer without announcing
|
||||
drop(announcer);
|
||||
|
||||
// Listener should receive Closed error (with timeout to prevent hanging in mutation tests)
|
||||
let result = tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout when announcer is dropped");
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Close test: explicit close notifies listener
|
||||
///
|
||||
/// Verifies: Verifies explicit close notifies listener behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_explicit_close_notifies_listener() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Explicitly close
|
||||
announcer.close();
|
||||
|
||||
// Listener should receive Closed error (with timeout to prevent hanging)
|
||||
let result = tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout when announcer is closed");
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Close test: multiple listeners all notified on drop
|
||||
///
|
||||
/// Verifies: Verifies multiple listeners all notified on drop behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_multiple_listeners_all_notified_on_drop() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
let l3 = announcement.listener();
|
||||
|
||||
// Drop announcer
|
||||
drop(announcer);
|
||||
|
||||
// All listeners should receive Closed error
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l1.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l2.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l3.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Close test: multiple listeners all notified on close
|
||||
///
|
||||
/// Verifies: Verifies multiple listeners all notified on close behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_multiple_listeners_all_notified_on_close() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
let l3 = announcement.listener();
|
||||
|
||||
// Explicit close
|
||||
announcer.close();
|
||||
|
||||
// All listeners should receive Closed error
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l1.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l2.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l3.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Close test: is closed after drop
|
||||
///
|
||||
/// Verifies: Verifies is closed after drop behavior
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_is_closed_after_drop() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
assert!(!announcement.is_closed());
|
||||
assert!(!listener.is_closed());
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close test: is closed after explicit close
|
||||
///
|
||||
/// Verifies: Verifies is closed after explicit close behavior
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_is_closed_after_explicit_close() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
assert!(!announcement.is_closed());
|
||||
assert!(!listener.is_closed());
|
||||
|
||||
announcer.close();
|
||||
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Concurrent Closed Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: concurrent listeners waiting when closed
|
||||
///
|
||||
/// Verifies: Verifies concurrent listeners waiting when closed behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_listeners_waiting_when_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Spawn multiple waiting listeners
|
||||
let mut handles = vec![];
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout")
|
||||
}));
|
||||
}
|
||||
|
||||
// Give them time to start waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Drop announcer - all should wake with Closed error
|
||||
drop(announcer);
|
||||
|
||||
// Verify all received Closed error
|
||||
for handle in handles {
|
||||
assert_eq!(handle.await.unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Close test: race drop during listen setup
|
||||
///
|
||||
/// Verifies: Verifies race drop during listen setup behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_race_drop_during_listen_setup() {
|
||||
// Test the race condition where announcer is dropped while listener
|
||||
// is setting up (between checking and registering event listener)
|
||||
for _ in 0..100 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout")
|
||||
});
|
||||
|
||||
// Drop immediately (race with listen setup)
|
||||
drop(announcer);
|
||||
|
||||
// Should still receive Closed error
|
||||
assert_eq!(handle.await.unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Close test: mixed waiting and polling listeners
|
||||
///
|
||||
/// Verifies: Verifies mixed waiting and polling listeners behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_mixed_waiting_and_polling_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Some listeners waiting
|
||||
let mut async_handles = vec![];
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
async_handles.push(tokio::spawn(async move {
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout")
|
||||
}));
|
||||
}
|
||||
|
||||
// Some listeners will poll after close
|
||||
let poll_listeners: Vec<_> = (0..5).map(|_| announcement.listener()).collect();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Close announcer
|
||||
announcer.close();
|
||||
|
||||
// Verify waiting listeners get Closed
|
||||
for handle in async_handles {
|
||||
assert_eq!(handle.await.unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
// Verify polling listeners also get Closed
|
||||
for listener in poll_listeners {
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Blocking Mode Closed Detection Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: listen blocking returns closed
|
||||
///
|
||||
/// Verifies: Verifies listen blocking returns closed behavior
|
||||
/// Category: close
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_listen_blocking_returns_closed() {
|
||||
use std::thread;
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = thread::spawn(move || listener.listen_blocking());
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
drop(announcer);
|
||||
|
||||
assert_eq!(handle.join().unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Close test: listen timeout blocking returns closed not timeout
|
||||
///
|
||||
/// Verifies: Verifies listen timeout blocking returns closed not timeout behavior
|
||||
/// Category: close
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_listen_timeout_blocking_returns_closed_not_timeout() {
|
||||
use std::thread;
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = thread::spawn(move || listener.listen_timeout_blocking(Duration::from_secs(10)));
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
drop(announcer);
|
||||
|
||||
// Should return Closed, not Timeout (even though timeout is 10 seconds)
|
||||
assert_eq!(handle.join().unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Close test: multiple blocking threads notified
|
||||
///
|
||||
/// Verifies: Verifies multiple blocking threads notified behavior
|
||||
/// Category: close
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_multiple_blocking_threads_notified() {
|
||||
use std::thread;
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(thread::spawn(move || listener.listen_blocking()));
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
drop(announcer);
|
||||
|
||||
for handle in handles {
|
||||
assert_eq!(handle.join().unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases and Ordering Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: listener created after close
|
||||
///
|
||||
/// Verifies: Verifies listener created after close behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_listener_created_after_close() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Close first
|
||||
announcer.close();
|
||||
|
||||
// Create listener after close
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Should immediately return Closed (fast path)
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Close test: announce takes precedence over close
|
||||
///
|
||||
/// Verifies: Verifies announce takes precedence over close behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_announce_takes_precedence_over_close() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Announce a value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Listener should get the value, not Closed
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Ok(42)
|
||||
);
|
||||
|
||||
// is_closed should be false because announcement succeeded
|
||||
assert!(!announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close test: drop immediately after creation
|
||||
///
|
||||
/// Verifies: Verifies drop immediately after creation behavior
|
||||
/// Category: close
|
||||
#[test]
|
||||
fn test_drop_immediately_after_creation() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Drop immediately
|
||||
drop(announcer);
|
||||
|
||||
// Should be closed
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close test: try listen with closed announcer
|
||||
///
|
||||
/// Verifies: Verifies try listen with closed announcer behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_try_listen_with_closed_announcer() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Drop announcer
|
||||
drop(announcer);
|
||||
|
||||
// try_listen returns None (doesn't distinguish between not-announced and closed)
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
|
||||
// But can check is_closed to distinguish
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close test: cloned announcement sees closed state
|
||||
///
|
||||
/// Verifies: Verifies cloned announcement sees closed state behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_cloned_announcement_sees_closed_state() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let announcement2 = announcement.clone();
|
||||
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement2.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
// Both announcements and their listeners see closed state
|
||||
assert!(announcement.is_closed());
|
||||
assert!(announcement2.is_closed());
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l1.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, l2.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multiple Runtime Tests for Closed Detection
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: tokio closed detection
|
||||
///
|
||||
/// Verifies: Verifies tokio closed detection behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_tokio_closed_detection() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Close test: smol closed detection
|
||||
///
|
||||
/// Verifies: Verifies smol closed detection behavior
|
||||
/// Category: close
|
||||
fn test_smol_closed_detection() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert_eq!(
|
||||
smol::future::or(
|
||||
async {
|
||||
smol::Timer::after(LISTEN_TIMEOUT).await;
|
||||
panic!("listen should not timeout")
|
||||
},
|
||||
listener.listen()
|
||||
)
|
||||
.await,
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Stress Tests for Closed Detection
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: many listeners closed
|
||||
///
|
||||
/// Verifies: Verifies many listeners closed behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_many_listeners_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
for _ in 0..100 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout")
|
||||
}));
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
drop(announcer);
|
||||
|
||||
for handle in handles {
|
||||
assert_eq!(handle.await.unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Close test: repeated close scenarios
|
||||
///
|
||||
/// Verifies: Verifies repeated close scenarios behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_repeated_close_scenarios() {
|
||||
// Run the close scenario many times to catch any race conditions
|
||||
for _ in 0..1000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout")
|
||||
});
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert_eq!(handle.await.unwrap(), Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Integration with Other Features
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: closed with arc value type
|
||||
///
|
||||
/// Verifies: Verifies closed with arc value type behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_closed_with_arc_value_type() {
|
||||
let (announcer, announcement) = Announcement::<Arc<String>>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Close test: is announced vs is closed
|
||||
///
|
||||
/// Verifies: Verifies is announced vs is closed behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_is_announced_vs_is_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Initially: not announced, not closed
|
||||
assert!(!announcement.is_announced());
|
||||
assert!(!announcement.is_closed());
|
||||
|
||||
// After drop: not announced, is closed
|
||||
drop(announcer);
|
||||
assert!(!announcement.is_announced());
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Close test: announced then never closed
|
||||
///
|
||||
/// Verifies: Verifies announced then never closed behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_announced_then_never_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// After announce: is announced, not closed
|
||||
assert!(announcement.is_announced());
|
||||
assert!(!announcement.is_closed());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Handling Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Close test: closed error display
|
||||
///
|
||||
/// Verifies: Verifies closed error display behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_closed_error_display() {
|
||||
let err = ListenError::Closed;
|
||||
assert_eq!(err.to_string(), "announcer was dropped without announcing");
|
||||
}
|
||||
|
||||
/// Test: Close test: listen multiple times after closed
|
||||
///
|
||||
/// Verifies: Verifies listen multiple times after closed behavior
|
||||
/// Category: close
|
||||
#[tokio::test]
|
||||
async fn test_listen_multiple_times_after_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
// Multiple listen calls should all return Closed
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::time::timeout(LISTEN_TIMEOUT, listener.listen())
|
||||
.await
|
||||
.expect("listen should not timeout"),
|
||||
Err(ListenError::Closed)
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,317 @@
|
||||
//! Critical high-priority tests identified in quality assessment.
|
||||
//! These tests cover edge cases that could cause memory leaks, races, or UB.
|
||||
|
||||
use announcement::Announcement;
|
||||
#[cfg(feature = "std")]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
#[cfg(feature = "std")]
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test: Arc refcount: stress test
|
||||
///
|
||||
/// Verifies: Arc refcounting works under stress
|
||||
/// Category: refcount
|
||||
// Verifies no memory leaks under rapid announce/drop cycles
|
||||
#[test]
|
||||
fn test_arc_refcount_stress() {
|
||||
for iteration in 0..10_000 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data = Arc::new(vec![0u8; 1024]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Announce with Arc
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// Retrieve value
|
||||
let received = listener.try_listen().unwrap();
|
||||
|
||||
// Verify refcount is reasonable
|
||||
// Should be: original + announcement storage + received = 3
|
||||
assert_eq!(
|
||||
Arc::strong_count(&received),
|
||||
initial_count + 2,
|
||||
"Refcount mismatch at iteration {}",
|
||||
iteration
|
||||
);
|
||||
|
||||
// Drop everything and verify no leak
|
||||
drop(received);
|
||||
drop(listener);
|
||||
drop(announcement);
|
||||
|
||||
// After all drops, only original reference should remain
|
||||
assert_eq!(
|
||||
Arc::strong_count(&data),
|
||||
initial_count,
|
||||
"Memory leak detected at iteration {}",
|
||||
iteration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Arc refcount: multiple listener refs
|
||||
///
|
||||
/// Verifies: Arc refcounts correct with multiple listeners
|
||||
/// Category: refcount
|
||||
#[test]
|
||||
fn test_arc_refcount_multiple_listeners() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data = Arc::new(vec![0u8; 1024]);
|
||||
let initial_count = Arc::strong_count(&data);
|
||||
|
||||
// Create 100 listeners
|
||||
let listeners: Vec<_> = (0..100).map(|_| announcement.listener()).collect();
|
||||
|
||||
// Announce
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// All listeners retrieve
|
||||
let values: Vec<_> = listeners.iter().map(|l| l.try_listen().unwrap()).collect();
|
||||
|
||||
// Refcount should be: original + storage + 100 listeners = 102
|
||||
assert_eq!(Arc::strong_count(&values[0]), initial_count + 101);
|
||||
|
||||
// Drop listeners one by one and verify refcount decreases
|
||||
drop(listeners);
|
||||
assert_eq!(Arc::strong_count(&values[0]), initial_count + 101);
|
||||
|
||||
drop(values);
|
||||
drop(announcement);
|
||||
|
||||
// Only original should remain
|
||||
assert_eq!(Arc::strong_count(&data), initial_count);
|
||||
}
|
||||
|
||||
/// Test: Integration test: close race with active listen
|
||||
///
|
||||
/// Verifies: Verifies close race with active listen
|
||||
/// Category: integration
|
||||
#[cfg(feature = "std")]
|
||||
#[tokio::test]
|
||||
async fn test_close_race_with_active_listen() {
|
||||
use announcement::ListenError;
|
||||
|
||||
for iteration in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn task that will listen
|
||||
let listen_task = tokio::spawn(async move { listener.listen().await });
|
||||
|
||||
// Give listener a moment to start waiting
|
||||
tokio::time::sleep(Duration::from_micros(1)).await;
|
||||
|
||||
// Close while listener is actively waiting
|
||||
announcer.close();
|
||||
|
||||
// Listener should wake up with Closed error
|
||||
let result = listen_task.await.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ListenError::Closed),
|
||||
"Expected Closed error at iteration {}",
|
||||
iteration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Integration test: close during listener setup
|
||||
///
|
||||
/// Verifies: Verifies close during listener setup
|
||||
/// Category: integration
|
||||
#[cfg(feature = "std")]
|
||||
#[tokio::test]
|
||||
async fn test_close_during_listener_setup() {
|
||||
use announcement::ListenError;
|
||||
|
||||
for _ in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Spawn multiple listeners that will race with close
|
||||
let mut tasks = vec![];
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
tasks.push(tokio::spawn(async move { listener.listen().await }));
|
||||
}
|
||||
|
||||
// Close immediately
|
||||
announcer.close();
|
||||
|
||||
// All listeners should get Closed error
|
||||
for task in tasks {
|
||||
let result = task.await.unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Integration test: weak memory ordering visibility
|
||||
///
|
||||
/// Verifies: Verifies weak memory ordering visibility
|
||||
/// Category: integration
|
||||
// Verifies that happens-before relationships work correctly
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_weak_memory_ordering_visibility() {
|
||||
let shared_data = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
for iteration in 0..1_000 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data1 = shared_data.clone();
|
||||
let data2 = shared_data.clone();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let writer = thread::spawn(move || {
|
||||
// Write to shared data
|
||||
data1.store(42, Ordering::Relaxed);
|
||||
|
||||
// Announce (provides synchronization)
|
||||
announcer.announce(()).unwrap();
|
||||
});
|
||||
|
||||
let reader = thread::spawn(move || {
|
||||
// Wait for announcement
|
||||
listener.listen_blocking().unwrap();
|
||||
|
||||
// Due to happens-before, must see the write
|
||||
data2.load(Ordering::Relaxed)
|
||||
});
|
||||
|
||||
writer.join().unwrap();
|
||||
let seen_value = reader.join().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
seen_value, 42,
|
||||
"Memory ordering violation at iteration {}",
|
||||
iteration
|
||||
);
|
||||
|
||||
// Reset for next iteration
|
||||
shared_data.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Integration test: weak memory ordering multiple observers
|
||||
///
|
||||
/// Verifies: Verifies weak memory ordering multiple observers
|
||||
/// Category: integration
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_weak_memory_ordering_multiple_observers() {
|
||||
for iteration in 0..1_000 {
|
||||
let shared_data = Arc::new(AtomicUsize::new(0));
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Multiple reader threads
|
||||
let mut handles = vec![];
|
||||
for i in 0..8 {
|
||||
let data = shared_data.clone();
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
listener.listen_blocking().unwrap();
|
||||
(i, data.load(Ordering::Relaxed))
|
||||
}));
|
||||
}
|
||||
|
||||
// Writer thread
|
||||
let data_writer = shared_data.clone();
|
||||
let writer = thread::spawn(move || {
|
||||
data_writer.store(99, Ordering::Relaxed);
|
||||
announcer.announce(()).unwrap();
|
||||
});
|
||||
|
||||
writer.join().unwrap();
|
||||
|
||||
// All readers must see the write
|
||||
for handle in handles {
|
||||
let (thread_id, value) = handle.join().unwrap();
|
||||
assert_eq!(
|
||||
value, 99,
|
||||
"Thread {} didn't see write at iteration {}",
|
||||
thread_id, iteration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Integration test: arc drop race with announcement
|
||||
///
|
||||
/// Verifies: Verifies arc drop race with announcement
|
||||
/// Category: integration
|
||||
#[test]
|
||||
fn test_arc_drop_race_with_announcement() {
|
||||
for _ in 0..10_000 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data = Arc::new(vec![0u8; 1024]);
|
||||
let data_clone = data.clone();
|
||||
|
||||
// Thread that rapidly creates and drops listeners
|
||||
let ann1 = announcement.clone();
|
||||
let dropper = thread::spawn(move || {
|
||||
for _ in 0..10 {
|
||||
let listener = ann1.listener();
|
||||
let _ = listener.try_listen();
|
||||
drop(listener);
|
||||
}
|
||||
});
|
||||
|
||||
// Thread that announces
|
||||
let announcer_thread = thread::spawn(move || {
|
||||
thread::yield_now();
|
||||
announcer.announce(data_clone).unwrap();
|
||||
});
|
||||
|
||||
dropper.join().unwrap();
|
||||
announcer_thread.join().unwrap();
|
||||
|
||||
// Verify final refcount
|
||||
let listener = announcement.listener();
|
||||
let received = listener.try_listen().unwrap();
|
||||
|
||||
// Should be: original + storage + received
|
||||
assert!(Arc::strong_count(&received) >= 2);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Integration test: closed flag visibility
|
||||
///
|
||||
/// Verifies: Verifies closed flag visibility
|
||||
/// Category: integration
|
||||
#[test]
|
||||
fn test_closed_flag_visibility() {
|
||||
for iteration in 0..10_000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Multiple threads checking is_closed
|
||||
let mut handles = vec![];
|
||||
for _ in 0..4 {
|
||||
let ann = announcement.clone();
|
||||
handles.push(thread::spawn(move || {
|
||||
// Spin until closed
|
||||
while !ann.is_closed() {
|
||||
thread::yield_now();
|
||||
}
|
||||
ann.is_closed() // Must be true
|
||||
}));
|
||||
}
|
||||
|
||||
// Close after threads start
|
||||
thread::yield_now();
|
||||
drop(announcer);
|
||||
|
||||
// All threads must eventually see closed
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert!(
|
||||
result,
|
||||
"Failed to see closed flag at iteration {}",
|
||||
iteration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
//! Deadlock prevention tests for the announcement crate.
|
||||
//! Verifies that the crate cannot deadlock in any scenario.
|
||||
|
||||
use announcement::Announcement;
|
||||
#[cfg(feature = "std")]
|
||||
use announcement::ListenError;
|
||||
use std::sync::{Arc, Barrier, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: listen timeout actually times out
|
||||
///
|
||||
/// Verifies: Verifies listen timeout actually times out does not cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_listen_timeout_actually_times_out() {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Don't announce, just let it timeout
|
||||
let start = Instant::now();
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
assert_eq!(result, Err(ListenError::Timeout));
|
||||
assert!(elapsed >= Duration::from_millis(100));
|
||||
// Increased tolerance for coverage instrumentation overhead (tarpaulin adds significant delay)
|
||||
assert!(elapsed < Duration::from_secs(1)); // Very generous tolerance
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: announcer dropped listeners detect closed
|
||||
///
|
||||
/// Verifies: Verifies announcer dropped listeners detect closed does not cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_announcer_dropped_listeners_detect_closed() {
|
||||
let announcement = {
|
||||
let (announcer, announcement) = Announcement::<String>::new();
|
||||
drop(announcer); // Drop announcer immediately - sets closed flag
|
||||
announcement
|
||||
};
|
||||
|
||||
let listener = announcement.listener();
|
||||
|
||||
// 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: Deadlock prevention: no deadlock nested announcements
|
||||
///
|
||||
/// Verifies: Verifies no deadlock nested announcements cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_nested_announcements() {
|
||||
// A -> B -> C chain
|
||||
let (ann_a, announcement_a) = Announcement::<i32>::new();
|
||||
let (ann_b, announcement_b) = Announcement::<i32>::new();
|
||||
let (ann_c, announcement_c) = Announcement::<i32>::new();
|
||||
|
||||
let listener_a = announcement_a.listener();
|
||||
let listener_b = announcement_b.listener();
|
||||
let listener_c = announcement_c.listener();
|
||||
|
||||
// Thread 1: A -> B
|
||||
let h1 = thread::spawn(move || {
|
||||
ann_a.announce(1).unwrap();
|
||||
loop {
|
||||
if let Some(val) = listener_b.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
// Thread 2: B -> C
|
||||
let h2 = thread::spawn(move || {
|
||||
ann_b.announce(2).unwrap();
|
||||
loop {
|
||||
if let Some(val) = listener_c.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
// Thread 3: C -> complete
|
||||
let h3 = thread::spawn(move || {
|
||||
ann_c.announce(3).unwrap();
|
||||
loop {
|
||||
if let Some(val) = listener_a.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
// All should complete without deadlock
|
||||
assert_eq!(h1.join().unwrap(), 2);
|
||||
assert_eq!(h2.join().unwrap(), 3);
|
||||
assert_eq!(h3.join().unwrap(), 1);
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock circular deps
|
||||
///
|
||||
/// Verifies: Verifies no deadlock circular deps cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_circular_deps() {
|
||||
let (ann1, announcement1) = Announcement::<&str>::new();
|
||||
let (ann2, announcement2) = Announcement::<&str>::new();
|
||||
|
||||
let listener1_for_2 = announcement1.listener();
|
||||
let listener2_for_1 = announcement2.listener();
|
||||
|
||||
// These would deadlock with improper locking
|
||||
let h1 = thread::spawn(move || {
|
||||
ann1.announce("from1").unwrap();
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(val) = listener2_for_1.try_listen() {
|
||||
return val;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
return "timeout1";
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
let h2 = thread::spawn(move || {
|
||||
ann2.announce("from2").unwrap();
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(val) = listener1_for_2.try_listen() {
|
||||
return val;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
return "timeout2";
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
let r1 = h1.join().unwrap();
|
||||
let r2 = h2.join().unwrap();
|
||||
|
||||
assert_eq!(r1, "from2");
|
||||
assert_eq!(r2, "from1");
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock multiple mutexes
|
||||
///
|
||||
/// Verifies: Verifies no deadlock multiple mutexes cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_multiple_mutexes() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mutex1 = Arc::new(Mutex::new(0));
|
||||
let mutex2 = Arc::new(Mutex::new(0));
|
||||
|
||||
let m1_clone = mutex1.clone();
|
||||
let m2_clone = mutex2.clone();
|
||||
let listener1 = announcement.listener();
|
||||
|
||||
let h1 = thread::spawn(move || {
|
||||
let _lock1 = m1_clone.lock().unwrap();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let _lock2 = m2_clone.lock().unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(val) = listener1.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
let m1_clone2 = mutex1.clone();
|
||||
let m2_clone2 = mutex2.clone();
|
||||
let listener2 = announcement.listener();
|
||||
|
||||
let h2 = thread::spawn(move || {
|
||||
let _lock2 = m2_clone2.lock().unwrap();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let _lock1 = m1_clone2.lock().unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(val) = listener2.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
// This might deadlock with the mutexes, but announcement should still work
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Set a timeout for the test
|
||||
let start = Instant::now();
|
||||
while start.elapsed() < Duration::from_secs(2) {
|
||||
if h1.is_finished() && h2.is_finished() {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// The mutex operations might deadlock, but announcement itself doesn't
|
||||
// This test shows announcement doesn't contribute to deadlocks
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: no deadlock blocking ops
|
||||
///
|
||||
/// Verifies: Verifies no deadlock blocking ops cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_blocking_ops() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Multiple blocking listeners
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
listener.listen_timeout_blocking(Duration::from_millis(500))
|
||||
}));
|
||||
}
|
||||
|
||||
// Let them block for a bit
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Announce
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// All should complete
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock with panic
|
||||
///
|
||||
/// Verifies: Verifies no deadlock with panic cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_with_panic() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Thread that panics
|
||||
let h = thread::spawn(move || {
|
||||
let _l = announcement.listener();
|
||||
panic!("Intentional panic");
|
||||
});
|
||||
|
||||
// Wait for panic
|
||||
let _ = h.join();
|
||||
|
||||
// Should still work
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock poisoned mutex value
|
||||
///
|
||||
/// Verifies: Verifies no deadlock poisoned mutex value cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_poisoned_mutex_value() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mutex = Arc::new(Mutex::new(42));
|
||||
let mutex_clone = mutex.clone();
|
||||
|
||||
// Poison the mutex
|
||||
let _ = thread::spawn(move || {
|
||||
let _guard = mutex_clone.lock().unwrap();
|
||||
panic!("Poison the mutex");
|
||||
})
|
||||
.join();
|
||||
|
||||
// Announce the poisoned mutex
|
||||
announcer.announce(mutex.clone()).unwrap();
|
||||
|
||||
let listener = announcement.listener();
|
||||
let received = listener.try_listen().unwrap();
|
||||
|
||||
// Can still receive, even if mutex is poisoned
|
||||
assert!(received.lock().is_err()); // It's poisoned
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock recursive listeners
|
||||
///
|
||||
/// Verifies: Verifies no deadlock recursive listeners cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_recursive_listeners() {
|
||||
fn create_recursive_listeners(announcement: &Announcement<i32>, depth: usize) {
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let _listener = announcement.listener();
|
||||
create_recursive_listeners(announcement, depth - 1);
|
||||
}
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create deeply nested listeners
|
||||
create_recursive_listeners(&announcement, 1000);
|
||||
|
||||
// Should still work
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
let final_listener = announcement.listener();
|
||||
assert_eq!(final_listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock barrier sync
|
||||
///
|
||||
/// Verifies: Verifies no deadlock barrier sync cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_barrier_sync() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let barrier = Arc::new(Barrier::new(11));
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
let barrier = barrier.clone();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
// Wait at barrier
|
||||
barrier.wait();
|
||||
|
||||
// Try to receive
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(val) = listener.try_listen() {
|
||||
return val;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
panic!("Timeout waiting for value");
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Main thread also waits
|
||||
barrier.wait();
|
||||
|
||||
// Then announces
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// All should complete
|
||||
for handle in handles {
|
||||
assert_eq!(handle.join().unwrap(), 42);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock channel chains
|
||||
///
|
||||
/// Verifies: Verifies no deadlock channel chains cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_channel_chains() {
|
||||
// Create channels separately to avoid ownership issues
|
||||
let mut announcers = Vec::new();
|
||||
let mut announcements = Vec::new();
|
||||
|
||||
for _ in 0..10 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
announcers.push(Some(announcer));
|
||||
announcements.push(announcement);
|
||||
}
|
||||
|
||||
// Chain announcements
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..9 {
|
||||
let listener = announcements[i].listener();
|
||||
let next_announcer = announcers[i + 1].take().unwrap();
|
||||
|
||||
handles.push(thread::spawn(move || loop {
|
||||
if let Some(val) = listener.try_listen() {
|
||||
next_announcer.announce(val + 1).unwrap();
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}));
|
||||
}
|
||||
|
||||
// Start the chain
|
||||
announcers[0].take().unwrap().announce(1).unwrap();
|
||||
|
||||
// Wait for chain to complete
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Check final value
|
||||
let final_listener = announcements[9].listener();
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(val) = final_listener.try_listen() {
|
||||
assert_eq!(val, 10); // 1 + 9 increments
|
||||
break;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
panic!("Chain didn't complete");
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: no deadlock simultaneous timeouts
|
||||
///
|
||||
/// Verifies: Verifies no deadlock simultaneous timeouts cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_simultaneous_timeouts() {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Many threads timing out simultaneously
|
||||
for _ in 0..50 {
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
listener.listen_timeout_blocking(Duration::from_millis(100))
|
||||
}));
|
||||
}
|
||||
|
||||
// All should timeout without deadlock
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Timeout));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: no deadlock mixed blocking
|
||||
///
|
||||
/// Verifies: Verifies no deadlock mixed blocking cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_mixed_blocking() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Blocking listeners
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
listener.listen_timeout_blocking(Duration::from_secs(1))
|
||||
}));
|
||||
}
|
||||
|
||||
// Non-blocking listeners
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(val) = listener.try_listen() {
|
||||
return Ok(val);
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
return Err(ListenError::Timeout);
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// All should complete
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock rapid churn
|
||||
///
|
||||
/// Verifies: Verifies no deadlock rapid churn cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_rapid_churn() {
|
||||
let duration = Duration::from_secs(2);
|
||||
let start = Instant::now();
|
||||
|
||||
while start.elapsed() < duration {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Rapid creation
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(thread::spawn(move || listener.try_listen()));
|
||||
}
|
||||
|
||||
// Quick announce
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Rapid destruction
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: no deadlock error propagation
|
||||
///
|
||||
/// Verifies: Verifies no deadlock error propagation cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_error_propagation() {
|
||||
let (_announcer, announcement) = Announcement::<Result<i32, String>>::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
match listener.listen_timeout_blocking(Duration::from_millis(100)) {
|
||||
Ok(_val) => format!("Thread {} got value", i),
|
||||
Err(ListenError::Timeout) => format!("Thread {} timed out", i),
|
||||
Err(ListenError::Closed) => format!("Thread {} got closed", i),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// All timeout, but no deadlock
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert!(result.contains("timed out"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock signal pattern
|
||||
///
|
||||
/// Verifies: Verifies no deadlock signal pattern cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_signal_pattern() {
|
||||
let (shutdown_ann, shutdown) = Announcement::<()>::new();
|
||||
let (ready_ann, ready) = Announcement::<()>::new();
|
||||
|
||||
let shutdown_listener = shutdown.listener();
|
||||
let ready_listener = ready.listener();
|
||||
|
||||
// Worker thread
|
||||
let worker = thread::spawn(move || {
|
||||
// Signal ready
|
||||
ready_ann.announce(()).unwrap();
|
||||
|
||||
// Work until shutdown
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if shutdown_listener.try_listen().is_some() {
|
||||
return "Shutdown received";
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(2) {
|
||||
return "Timeout";
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for ready
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if ready_listener.try_listen().is_some() {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() > Duration::from_secs(1) {
|
||||
panic!("Ready signal timeout");
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
|
||||
// Send shutdown
|
||||
shutdown_ann.announce(()).unwrap();
|
||||
|
||||
let result = worker.join().unwrap();
|
||||
assert_eq!(result, "Shutdown received");
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock cross thread deps
|
||||
///
|
||||
/// Verifies: Verifies no deadlock cross thread deps cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_cross_thread_deps() {
|
||||
let (ann1, announcement1) = Announcement::new();
|
||||
let (ann2, announcement2) = Announcement::new();
|
||||
let (ann3, announcement3) = Announcement::new();
|
||||
|
||||
let l1 = announcement1.listener();
|
||||
let l2 = announcement2.listener();
|
||||
let l3 = announcement3.listener();
|
||||
|
||||
// Complex dependency graph
|
||||
let h1 = thread::spawn(move || {
|
||||
ann1.announce(1).unwrap();
|
||||
// Wait for 3
|
||||
loop {
|
||||
if let Some(val) = l3.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
let h2 = thread::spawn(move || {
|
||||
// Wait for 1
|
||||
loop {
|
||||
if l1.try_listen().is_some() {
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
ann2.announce(2).unwrap();
|
||||
2
|
||||
});
|
||||
|
||||
let h3 = thread::spawn(move || {
|
||||
// Wait for 2
|
||||
loop {
|
||||
if l2.try_listen().is_some() {
|
||||
break;
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
ann3.announce(3).unwrap();
|
||||
3
|
||||
});
|
||||
|
||||
// All should complete without deadlock
|
||||
assert_eq!(h1.join().unwrap(), 3);
|
||||
assert_eq!(h2.join().unwrap(), 2);
|
||||
assert_eq!(h3.join().unwrap(), 3);
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
/// Test: Deadlock prevention: no deadlock timeout cascade
|
||||
///
|
||||
/// Verifies: Verifies no deadlock timeout cascade cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_timeout_cascade() {
|
||||
// Create chains separately
|
||||
let mut announcers = Vec::new();
|
||||
let mut announcements = Vec::new();
|
||||
|
||||
for _ in 0..5 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
announcers.push(Some(announcer));
|
||||
announcements.push(announcement);
|
||||
}
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..4 {
|
||||
let listener = announcements[i].listener();
|
||||
let announcer = announcers[i + 1].take().unwrap();
|
||||
|
||||
handles.push(thread::spawn(move || {
|
||||
match listener.listen_timeout_blocking(Duration::from_millis(200)) {
|
||||
Ok(val) => {
|
||||
announcer.announce(val + 1).ok();
|
||||
Ok(val)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Don't announce to first, causing cascade
|
||||
// First announcer dropped at end of function -> Closed
|
||||
// Cascade means others either timeout or get closed
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
// Accept either Timeout or Closed - both are valid depending on timing
|
||||
assert!(
|
||||
result == Err(ListenError::Timeout) || result == Err(ListenError::Closed),
|
||||
"Expected Timeout or Closed, got {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Deadlock prevention: no deadlock summary
|
||||
///
|
||||
/// Verifies: Verifies no deadlock summary cannot cause deadlock
|
||||
/// Category: deadlock
|
||||
#[test]
|
||||
fn test_no_deadlock_summary() {
|
||||
// This test verifies that common deadlock scenarios are prevented
|
||||
|
||||
// 1. No internal locks that could deadlock
|
||||
let (ann, announcement) = Announcement::<i32>::new();
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
|
||||
// Concurrent operations don't deadlock
|
||||
let h1 = thread::spawn(move || l1.try_listen());
|
||||
let h2 = thread::spawn(move || l2.try_listen());
|
||||
|
||||
ann.announce(42).unwrap();
|
||||
|
||||
h1.join().unwrap();
|
||||
h2.join().unwrap();
|
||||
|
||||
// 2. No blocking infinite waits (with timeout)
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let (_ann, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(10));
|
||||
assert_eq!(result, Err(ListenError::Timeout));
|
||||
}
|
||||
|
||||
// 3. Clean panic handling
|
||||
let (ann, announcement) = Announcement::<i32>::new();
|
||||
let _listener = announcement.listener();
|
||||
|
||||
let _ = std::panic::catch_unwind(|| {
|
||||
panic!("Test panic");
|
||||
});
|
||||
|
||||
// Still works after panic
|
||||
ann.announce(42).unwrap();
|
||||
|
||||
println!("All deadlock prevention tests passed!");
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
//! Targeted tests for Drop implementation mutations
|
||||
//!
|
||||
//! These tests specifically target the Drop impl for Announcer<T> to catch
|
||||
//! mutations that would otherwise cause timeouts. They use short timeouts
|
||||
//! to fail fast instead of hanging.
|
||||
|
||||
use announcement::{Announcement, ListenError};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test: Drop sets closed flag - verify with short timeout
|
||||
///
|
||||
/// Verifies: Drop sets closed flag - with short timeout
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_sets_closed_flag_quick() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Verify not closed initially
|
||||
assert!(!announcement.is_closed());
|
||||
|
||||
// Drop the announcer
|
||||
drop(announcer);
|
||||
|
||||
// CRITICAL: Must set closed flag
|
||||
// This catches the mutant that replaces Drop body with ()
|
||||
assert!(announcement.is_closed(), "Drop must set closed flag");
|
||||
}
|
||||
|
||||
/// Test: Drop test: drop wakes blocking listeners quick
|
||||
///
|
||||
/// Verifies: drop wakes blocking listeners quick
|
||||
/// Category: drop
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_drop_wakes_blocking_listeners_quick() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn thread that will wait
|
||||
let handle =
|
||||
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_millis(100)));
|
||||
|
||||
// Give thread time to start waiting
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Drop announcer - should wake listener
|
||||
drop(announcer);
|
||||
|
||||
// Listener should receive Closed error, not Timeout
|
||||
// If Drop doesn't work, this will timeout
|
||||
let result = handle.join().expect("thread should not panic");
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ListenError::Closed),
|
||||
"Drop must notify waiting listeners with Closed error"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Drop with conditional check - verifies the condition logic
|
||||
///
|
||||
/// Verifies: Drop with conditional check - verifies the condition logic
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_conditional_already_announced() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Announce a value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Drop happens here, but should NOT set closed flag
|
||||
// because value was already announced
|
||||
|
||||
// Verify: NOT closed (because value was announced)
|
||||
assert!(
|
||||
!announcement.is_closed(),
|
||||
"Drop should not set closed flag if value was announced"
|
||||
);
|
||||
|
||||
// Verify: Can still get the value
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Drop with conditional check - verifies the !closed check
|
||||
///
|
||||
/// Verifies: Drop with conditional check - verifies the !closed check
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_conditional_already_closed() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Explicitly close first
|
||||
announcer.close();
|
||||
|
||||
// At this point, closed flag is already set
|
||||
assert!(announcement.is_closed());
|
||||
|
||||
// Drop happens here automatically
|
||||
// The condition should prevent double-notification
|
||||
|
||||
// Still closed after drop
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Multiple drops with concurrent listeners
|
||||
///
|
||||
/// Verifies: Multiple drops with concurrent listeners
|
||||
/// Category: drop
|
||||
#[tokio::test]
|
||||
async fn test_drop_wakes_multiple_async_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Create multiple listeners
|
||||
let listeners: Vec<_> = (0..5).map(|_| announcement.listener()).collect();
|
||||
|
||||
// Spawn tasks for all listeners with short timeout
|
||||
let mut handles = Vec::new();
|
||||
for listener in listeners {
|
||||
handles.push(tokio::spawn(async move {
|
||||
tokio::time::timeout(Duration::from_millis(100), listener.listen()).await
|
||||
}));
|
||||
}
|
||||
|
||||
// Give listeners time to start waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Drop announcer - should wake all listeners
|
||||
drop(announcer);
|
||||
|
||||
// All listeners should receive Closed error quickly, not timeout
|
||||
for handle in handles {
|
||||
let result = handle.await.expect("task should not panic");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"listener should receive notification, not timeout"
|
||||
);
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
Err(ListenError::Closed),
|
||||
"Drop must notify all waiting listeners"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Drop notifies then listeners can check is_closed
|
||||
///
|
||||
/// Verifies: Drop notifies then listeners can check is_closed
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_notification_visible_to_new_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Drop without announcing
|
||||
drop(announcer);
|
||||
|
||||
// Create listener AFTER drop
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Should immediately see closed status
|
||||
assert!(listener.is_closed());
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
}
|
||||
|
||||
/// Test: Verify Drop impl checks value.is_none() correctly
|
||||
///
|
||||
/// Verifies: Drop impl checks value.is_none() correctly
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_checks_value_is_none_correctly() {
|
||||
// Test 1: value is None, should close
|
||||
{
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
drop(announcer);
|
||||
assert!(announcement.is_closed(), "Should close when value is None");
|
||||
}
|
||||
|
||||
// Test 2: value is Some, should NOT close
|
||||
{
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
_announcer.announce(42).unwrap();
|
||||
assert!(
|
||||
!announcement.is_closed(),
|
||||
"Should NOT close when value is Some"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Verify Drop impl checks !closed correctly
|
||||
///
|
||||
/// Verifies: Drop impl checks !closed correctly
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_checks_not_closed_correctly() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// First close explicitly
|
||||
announcer.close();
|
||||
|
||||
// closed flag should be set
|
||||
assert!(announcement.is_closed());
|
||||
|
||||
// Drop happens automatically here
|
||||
// The !closed check should prevent re-notification
|
||||
|
||||
// Verify still closed (this is a sanity check)
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Stress test - many rapid drops
|
||||
///
|
||||
/// Verifies: Stress test - many rapid drops
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_rapid_drops_stress() {
|
||||
for _ in 0..1000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
assert_eq!(listener.try_listen(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Drop with Arc value type
|
||||
///
|
||||
/// Verifies: Drop with Arc value type
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_with_arc_value() {
|
||||
let (announcer, announcement) = Announcement::<Arc<String>>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
drop(announcer);
|
||||
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Drop test: drop ordering set closed before notify
|
||||
///
|
||||
/// Verifies: drop ordering set closed before notify
|
||||
/// Category: drop
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_drop_ordering_set_closed_before_notify() {
|
||||
// This test verifies that closed flag is set before notification
|
||||
// If the order is wrong, listeners might check closed flag before it's set
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let ann_clone = announcement.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let listener = ann_clone.listener();
|
||||
// Wait with very short timeout
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(50));
|
||||
|
||||
// If we get Closed, verify the flag is set
|
||||
if result == Err(ListenError::Closed) {
|
||||
assert!(
|
||||
ann_clone.is_closed(),
|
||||
"closed flag must be visible when Closed error is returned"
|
||||
);
|
||||
}
|
||||
result
|
||||
});
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
drop(announcer);
|
||||
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
/// Test: Drop in nested scope
|
||||
///
|
||||
/// Verifies: Drop in nested scope
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_in_nested_scope() {
|
||||
let announcement = {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
// _announcer dropped here at end of scope
|
||||
announcement
|
||||
};
|
||||
|
||||
// Outside the scope, closed flag should be set
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Drop vs explicit close comparison
|
||||
///
|
||||
/// Verifies: Drop vs explicit close comparison
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_equivalent_to_explicit_close() {
|
||||
// Test 1: explicit close
|
||||
let (announcer1, announcement1) = Announcement::<i32>::new();
|
||||
let listener1 = announcement1.listener();
|
||||
announcer1.close();
|
||||
|
||||
// Test 2: drop
|
||||
let (announcer2, announcement2) = Announcement::<i32>::new();
|
||||
let listener2 = announcement2.listener();
|
||||
drop(announcer2);
|
||||
|
||||
// Both should have same result
|
||||
assert_eq!(announcement1.is_closed(), announcement2.is_closed());
|
||||
assert_eq!(listener1.try_listen(), listener2.try_listen());
|
||||
assert_eq!(listener1.is_closed(), listener2.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Drop with panic in another thread
|
||||
///
|
||||
/// Verifies: Drop with panic in another thread
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_after_panic() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let _announcer = announcer;
|
||||
panic!("intentional panic");
|
||||
});
|
||||
|
||||
let _ = handle.join();
|
||||
|
||||
// Drop happened despite panic
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Verify closed flag atomic ordering
|
||||
///
|
||||
/// Verifies: closed flag atomic ordering
|
||||
/// Category: drop
|
||||
#[test]
|
||||
fn test_drop_closed_flag_atomic_ordering() {
|
||||
// This is a documentation test - verifying our understanding
|
||||
// The actual ordering is in the implementation
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Before drop, not closed
|
||||
assert!(!announcement.is_closed());
|
||||
|
||||
// Drop sets closed flag with Release ordering
|
||||
drop(announcer);
|
||||
|
||||
// After drop, closed flag should be visible with Acquire ordering
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+2140
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,415 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//! Tests specifically designed to catch mutations missed by cargo-mutants.
|
||||
//!
|
||||
//! These tests target specific mutations that weren't caught by existing tests.
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use announcement::{Announcement, ListenError};
|
||||
#[cfg(feature = "std")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "std")]
|
||||
use std::thread;
|
||||
#[cfg(feature = "std")]
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Test: Mutation test: close actually closes
|
||||
///
|
||||
/// Verifies: Verifies close actually closes
|
||||
/// Category: mutation
|
||||
/// Test that close() actually closes the announcer and wakes listeners.
|
||||
///
|
||||
/// This catches the mutation where close() is replaced with ().
|
||||
///
|
||||
/// This mutation is difficult to catch because close() consumes self,
|
||||
/// so Drop is called immediately after. We test the observable behavior:
|
||||
/// that calling close() results in listeners being woken up.
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_close_actually_closes() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Create listener in another thread that will block with timeout
|
||||
let handle =
|
||||
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_millis(100)));
|
||||
|
||||
// Give thread time to start blocking
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Measure time from close() call to thread completing
|
||||
let start = Instant::now();
|
||||
|
||||
// Call close() - whether the work is done by close() or its Drop
|
||||
// doesn't matter for correctness, but both paths must work
|
||||
announcer.close();
|
||||
|
||||
// Verify thread receives Closed
|
||||
let result = handle.join().expect("Thread should not panic");
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
|
||||
// Should complete quickly
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(100),
|
||||
"Wakeup took too long: {:?}",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// Verify is_closed() returns true
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Mutation test: close wakes multiple listeners
|
||||
///
|
||||
/// Verifies: Verifies close wakes multiple listeners
|
||||
/// Category: mutation
|
||||
/// Test that close() wakes multiple listeners.
|
||||
///
|
||||
/// Additional test for close() mutation - verifies multiple listeners
|
||||
/// are all woken up when close() is called.
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_close_wakes_multiple_listeners() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Create multiple listeners in separate threads with timeout
|
||||
let handles: Vec<_> = (0..3)
|
||||
.map(|_| {
|
||||
let listener = announcement.listener();
|
||||
thread::spawn(move || listener.listen_timeout_blocking(Duration::from_millis(100)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Give threads time to start blocking
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
// Close should wake ALL listeners
|
||||
announcer.close();
|
||||
|
||||
// Verify all threads receive Closed
|
||||
for handle in handles {
|
||||
let result = handle.join().expect("Thread should not panic");
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
}
|
||||
|
||||
// Verify is_closed() returns true
|
||||
assert!(announcement.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Mutation test: listen blocking non default
|
||||
///
|
||||
/// Verifies: Verifies listen blocking non default
|
||||
/// Category: mutation
|
||||
/// Test listen_timeout_blocking with value announced from another thread.
|
||||
///
|
||||
/// This catches the mutation where listen_timeout_blocking is replaced with Ok(Default::default()).
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_listen_blocking_non_default() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct NonDefault {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let test_value = NonDefault {
|
||||
data: vec![1, 2, 3, 4, 5],
|
||||
};
|
||||
|
||||
// Announce from another thread
|
||||
let announce_value = test_value.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
announcer.announce(announce_value).unwrap();
|
||||
});
|
||||
|
||||
// This blocks with timeout until value arrives
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
assert_eq!(result, Ok(test_value));
|
||||
}
|
||||
|
||||
/// Test: Mutation test: timeout actually waits
|
||||
///
|
||||
/// Verifies: Verifies timeout actually waits
|
||||
/// Category: mutation
|
||||
/// Test that listen_timeout_blocking actually times out when no value is announced.
|
||||
///
|
||||
/// This catches deadline calculation mutations (+ vs -).
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_timeout_actually_waits() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
let keep_alive = Arc::new(AtomicBool::new(true));
|
||||
let keep_alive_clone = keep_alive.clone();
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Keep announcer alive in another thread
|
||||
thread::spawn(move || {
|
||||
while keep_alive_clone.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
drop(announcer);
|
||||
});
|
||||
|
||||
let start = Instant::now();
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
keep_alive.store(false, Ordering::Relaxed);
|
||||
|
||||
// Should timeout after ~100ms
|
||||
assert_eq!(result, Err(ListenError::Timeout));
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(90),
|
||||
"Should wait at least 90ms, took {:?}",
|
||||
elapsed
|
||||
);
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(150),
|
||||
"Should timeout within 150ms, took {:?}",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Mutation test: timeout blocking correct value
|
||||
///
|
||||
/// Verifies: Verifies timeout blocking correct value
|
||||
/// Category: mutation
|
||||
/// Test listen_timeout_blocking returns correct value when announced within timeout.
|
||||
///
|
||||
/// This catches mutations that replace the function with Ok(Default::default()).
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_timeout_blocking_correct_value() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct TestData {
|
||||
id: u64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let test_data = TestData {
|
||||
id: 999999,
|
||||
name: "unique_test_data_12345".to_string(),
|
||||
};
|
||||
|
||||
// Announce after delay
|
||||
let announce_data = test_data.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
announcer.announce(announce_data).unwrap();
|
||||
});
|
||||
|
||||
// Wait with timeout - should get the exact value we announced
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(200));
|
||||
assert_eq!(result, Ok(test_data));
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
//! 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();
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
//! Panic Safety Tests
|
||||
//!
|
||||
//! These tests verify that the library maintains safety invariants even when panics occur.
|
||||
//! Panic safety means:
|
||||
//! 1. No memory leaks
|
||||
//! 2. No undefined behavior
|
||||
//! 3. Data structures remain consistent
|
||||
//! 4. Resources are properly cleaned up
|
||||
//! 5. No deadlocks or hangs after panic recovery
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Arc;
|
||||
|
||||
// =============================================================================
|
||||
// Basic Panic Safety Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic during Drop implementation
|
||||
///
|
||||
/// Verifies: System remains safe when announced value panics during Drop
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_during_drop_in_value() {
|
||||
// Test that panicking during Drop of the announced value doesn't break things
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PanicOnDrop {
|
||||
should_panic: bool,
|
||||
}
|
||||
|
||||
impl Drop for PanicOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if self.should_panic {
|
||||
panic!("Intentional panic in Drop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create announcement with panic-on-drop value
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let _listener = announcement.listener();
|
||||
|
||||
announcer
|
||||
.announce(Arc::new(PanicOnDrop { should_panic: true }))
|
||||
.unwrap();
|
||||
|
||||
// When Arc drops, it should panic, but we catch it
|
||||
}));
|
||||
|
||||
// The panic should be caught
|
||||
assert!(result.is_err());
|
||||
|
||||
// Create another announcement to verify system still works
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic during Clone/Drop interaction
|
||||
///
|
||||
/// Verifies: System remains safe when panic occurs during value cloning or dropping
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_in_clone_implementation() {
|
||||
// Test panic during T::clone() doesn't break announcement
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PanicOnClone {
|
||||
panic_count: Arc<std::sync::atomic::AtomicUsize>,
|
||||
panic_on_nth: usize,
|
||||
}
|
||||
|
||||
impl Drop for PanicOnClone {
|
||||
fn drop(&mut self) {
|
||||
let count = self
|
||||
.panic_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
if count == self.panic_on_nth {
|
||||
panic!("Panic on Nth drop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let panic_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
let _result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let value = PanicOnClone {
|
||||
panic_count: panic_count.clone(),
|
||||
panic_on_nth: 2, // Panic on 3rd drop
|
||||
};
|
||||
|
||||
announcer.announce(value).unwrap();
|
||||
|
||||
// First try_listen - might panic
|
||||
let _v1 = listener.try_listen();
|
||||
}));
|
||||
|
||||
// System should still work after panic
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: explicit announcer drop
|
||||
///
|
||||
/// Verifies: Dropping announcer without announcing properly closes listeners
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_announcer_drop_with_panic_guard() {
|
||||
// Test that dropping announcer without announcing works
|
||||
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
assert!(!listener.is_closed());
|
||||
|
||||
// Drop announcer explicitly
|
||||
drop(announcer);
|
||||
|
||||
// Listener should see closed
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Arc Reference Counting Under Panic
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: Arc reference counting
|
||||
///
|
||||
/// Verifies: Arc refcounts remain correct when panics occur while holding references
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_arc_refcount_with_panic() {
|
||||
// Verify Arc refcounts are correct even when panics occur
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data = Arc::new(vec![1, 2, 3, 4, 5]);
|
||||
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
// Create listeners
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
|
||||
// Get values
|
||||
let v1 = l1.try_listen().unwrap();
|
||||
let _v2 = l2.try_listen().unwrap();
|
||||
|
||||
// Refcount should be: data + announcement storage + v1 + v2 = 4
|
||||
assert_eq!(Arc::strong_count(&v1), 4);
|
||||
|
||||
// Cause panic while holding references
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _v3 = l1.try_listen().unwrap();
|
||||
panic!("Test panic");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Refcount should still be correct
|
||||
assert_eq!(Arc::strong_count(&v1), 4);
|
||||
}
|
||||
|
||||
/// Test: Panic safety: announcement clone with panic
|
||||
///
|
||||
/// Verifies: Cloned announcements remain functional after panic in sibling clone
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_announcement_clone_with_panic() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Clone announcement
|
||||
let announcement2 = announcement.clone();
|
||||
|
||||
// Panic while one is in scope
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _l = announcement2.listener();
|
||||
panic!("Test panic");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Original announcement should still work
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: listener clone with panic
|
||||
///
|
||||
/// Verifies: Cloned listeners remain functional after panic in sibling clone
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_listener_clone_with_panic() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Clone listener
|
||||
let listener2 = listener.clone();
|
||||
|
||||
// Announce value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Panic while using clone
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _v = listener2.try_listen();
|
||||
panic!("Test panic");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Original listener should still work
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Multiple Panic Scenarios
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: sequential panics
|
||||
///
|
||||
/// Verifies: System handles multiple panics in sequence without breaking
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_sequential_panics() {
|
||||
// Multiple panics in sequence shouldn't break the system
|
||||
|
||||
for i in 0..5 {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(i).unwrap();
|
||||
|
||||
if i % 2 == 0 {
|
||||
panic!("Even panic");
|
||||
}
|
||||
|
||||
assert_eq!(listener.try_listen(), Some(i));
|
||||
}));
|
||||
|
||||
if i % 2 == 0 {
|
||||
assert!(result.is_err());
|
||||
} else {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Panic safety: nested panic scenarios
|
||||
///
|
||||
/// Verifies: System handles panic within panic handler context
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_nested_panics() {
|
||||
// Panic within panic handler
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let _listener = announcement.listener();
|
||||
|
||||
// Drop announcer (will set closed flag)
|
||||
drop(announcer);
|
||||
|
||||
// Now cause a panic
|
||||
panic!("First panic");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// System should still work
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Async Panic Safety Tests (requires async feature)
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic during async listen
|
||||
///
|
||||
/// Verifies: System remains functional when async task panics after listen completes
|
||||
/// Category: panic
|
||||
#[tokio::test]
|
||||
async fn test_panic_during_async_listen() {
|
||||
use std::time::Duration;
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Spawn task that will panic
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
let _value = listener.listen().await.unwrap();
|
||||
panic!("Panic after listen");
|
||||
});
|
||||
|
||||
// Announce value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Task should panic
|
||||
let result = handle.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// System should still work
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await, Ok(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic before async listen completion
|
||||
///
|
||||
/// Verifies: System remains functional when async task panics before listen completes
|
||||
/// Category: panic
|
||||
#[tokio::test]
|
||||
async fn test_panic_before_async_listen_completes() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Start listening but panic before completion
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = listener.listen() => {
|
||||
panic!("Should not reach here");
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_millis(5)) => {
|
||||
panic!("Timeout panic");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Task should panic
|
||||
let result = handle.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Announcer should still work
|
||||
announcer.announce(42).unwrap();
|
||||
}
|
||||
|
||||
/// Test: Panic safety: multiple async tasks with selective panics
|
||||
///
|
||||
/// Verifies: Some async tasks can panic while others complete successfully
|
||||
/// Category: panic
|
||||
#[tokio::test]
|
||||
async fn test_multiple_async_tasks_with_panics() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Spawn multiple tasks, some will panic
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let value = listener.listen().await.unwrap();
|
||||
|
||||
if i % 3 == 0 {
|
||||
panic!("Task {} panic", i);
|
||||
}
|
||||
|
||||
value
|
||||
}));
|
||||
}
|
||||
|
||||
// Announce value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Check results - some panic, some succeed
|
||||
for (i, handle) in handles.into_iter().enumerate() {
|
||||
let result = handle.await;
|
||||
|
||||
if i % 3 == 0 {
|
||||
assert!(result.is_err(), "Task {} should have panicked", i);
|
||||
} else {
|
||||
assert_eq!(result.unwrap(), 42, "Task {} should have succeeded", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Blocking Mode Panic Safety (requires std feature)
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic during blocking listen
|
||||
///
|
||||
/// Verifies: System remains functional when blocking thread panics after listen
|
||||
/// Category: panic
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_panic_during_blocking_listen() {
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let _value = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
panic!("Panic after blocking listen with timeout");
|
||||
});
|
||||
|
||||
// Give thread time to start
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
// Announce value
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Thread should panic
|
||||
let result = handle.join();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test: Panic safety: multiple blocking threads with selective panics
|
||||
///
|
||||
/// Verifies: Some blocking threads can panic while others complete successfully
|
||||
/// Category: panic
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_multiple_blocking_threads_with_panics() {
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(thread::spawn(move || {
|
||||
let value = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
|
||||
if i % 2 == 0 {
|
||||
panic!("Thread {} panic", i);
|
||||
}
|
||||
|
||||
value
|
||||
}));
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Check results
|
||||
for (i, handle) in handles.into_iter().enumerate() {
|
||||
let result = handle.join();
|
||||
|
||||
if i % 2 == 0 {
|
||||
assert!(result.is_err());
|
||||
} else {
|
||||
assert_eq!(result.unwrap(), Ok(42));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Cases and Stress Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic with many listeners
|
||||
///
|
||||
/// Verifies: Panicking while iterating over many listeners doesn't affect others
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_with_large_number_of_listeners() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create many listeners
|
||||
let listeners: Vec<_> = (0..1000).map(|_| announcement.listener()).collect();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Panic while iterating
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
for (i, listener) in listeners.iter().enumerate() {
|
||||
let _value = listener.try_listen();
|
||||
|
||||
if i == 500 {
|
||||
panic!("Panic at 500");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Remaining listeners should still work
|
||||
for listener in listeners.iter().skip(501) {
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic during is_announced checks
|
||||
///
|
||||
/// Verifies: System remains functional when panic occurs during state checks
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_during_is_announced_checks() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
assert!(!announcement.is_announced());
|
||||
panic!("Panic before announce");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Should still be able to announce
|
||||
announcer.announce(42).unwrap();
|
||||
assert!(announcement.is_announced());
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic during is_closed checks
|
||||
///
|
||||
/// Verifies: System remains functional when panic occurs during closure checks
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_during_is_closed_checks() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
assert!(!announcement.is_closed());
|
||||
panic!("Panic before close");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Should still be able to close
|
||||
announcer.close();
|
||||
assert!(announcement.is_closed());
|
||||
assert!(listener.is_closed());
|
||||
}
|
||||
|
||||
/// Test: Panic safety: no memory leaks on panic
|
||||
///
|
||||
/// Verifies: Repeated panics during announcement don't cause memory leaks
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_memory_not_leaked_on_panic() {
|
||||
// Create and drop many announcements with panics
|
||||
|
||||
for _ in 0..100 {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let _listener = announcement.listener();
|
||||
|
||||
announcer.announce(vec![1; 1000]).unwrap();
|
||||
|
||||
panic!("Test panic");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// If there were memory leaks, this would show up in memory profilers
|
||||
// For now, we just verify the test completes
|
||||
}
|
||||
|
||||
/// Test: Panic safety: close with panic recovery
|
||||
///
|
||||
/// Verifies: System remains functional after panic following close operation
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_announcer_close_with_panic_recovery() {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.close();
|
||||
|
||||
assert!(listener.is_closed());
|
||||
panic!("Panic after close");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// System should still work
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: UnwindSafe trait implementation
|
||||
///
|
||||
/// Verifies: All types implement UnwindSafe and RefUnwindSafe traits
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_unwind_safe_trait_impl() {
|
||||
// Verify types implement UnwindSafe and RefUnwindSafe
|
||||
|
||||
fn assert_unwind_safe<T: std::panic::UnwindSafe>() {}
|
||||
fn assert_ref_unwind_safe<T: std::panic::RefUnwindSafe>() {}
|
||||
|
||||
assert_unwind_safe::<announcement::Announcement<i32>>();
|
||||
assert_unwind_safe::<announcement::Announcer<i32>>();
|
||||
assert_unwind_safe::<announcement::Listener<i32>>();
|
||||
|
||||
assert_ref_unwind_safe::<announcement::Announcement<i32>>();
|
||||
assert_ref_unwind_safe::<announcement::Listener<i32>>();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Complex Panic Scenarios
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic in complex type Drop
|
||||
///
|
||||
/// Verifies: System handles panics during drop of complex nested types
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_in_complex_type_drop() {
|
||||
#[derive(Debug)]
|
||||
struct ComplexType {
|
||||
_data: Vec<Vec<String>>,
|
||||
should_panic: bool,
|
||||
}
|
||||
|
||||
impl Drop for ComplexType {
|
||||
fn drop(&mut self) {
|
||||
if self.should_panic {
|
||||
panic!("Complex drop panic");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let complex = ComplexType {
|
||||
_data: vec![vec!["test".to_string(); 10]; 10],
|
||||
should_panic: false,
|
||||
};
|
||||
|
||||
announcer.announce(Arc::new(complex)).unwrap();
|
||||
|
||||
let value = listener.try_listen().unwrap();
|
||||
|
||||
// Modify to panic on drop
|
||||
// (Actually, we can't modify Arc contents, so this tests Arc safety)
|
||||
drop(value);
|
||||
}));
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic recovery in async loop
|
||||
///
|
||||
/// Verifies: Async system recovers from panics across multiple iterations
|
||||
/// Category: panic
|
||||
#[tokio::test]
|
||||
async fn test_panic_recovery_in_async_loop() {
|
||||
// Test that async system recovers from panics in loop
|
||||
|
||||
for i in 0..5 {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(i).unwrap();
|
||||
|
||||
if i == 2 {
|
||||
panic!("Panic on iteration 2");
|
||||
}
|
||||
|
||||
assert_eq!(listener.try_listen(), Some(i));
|
||||
}));
|
||||
|
||||
if i == 2 {
|
||||
assert!(result.is_err());
|
||||
} else {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
// Async system should still work
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await, Ok(42));
|
||||
}
|
||||
|
||||
// Note: Double-panic test removed because double panics abort the process by design.
|
||||
// This is correct Rust behavior to prevent undefined behavior during unwinding.
|
||||
|
||||
// =============================================================================
|
||||
// Additional Panic Safety Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Panic safety: panic with cloned announcements
|
||||
///
|
||||
/// Verifies: Cloned announcements remain functional after panic in siblings
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_with_cloned_announcements() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let announcement2 = announcement.clone();
|
||||
let announcement3 = announcement.clone();
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _l1 = announcement.listener();
|
||||
let _l2 = announcement2.listener();
|
||||
panic!("Test panic with clones");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// announcement3 should still work
|
||||
let listener = announcement3.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic during listener creation
|
||||
///
|
||||
/// Verifies: System handles panics occurring during listener creation process
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_during_listener_creation() {
|
||||
for _ in 0..10 {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Create some listeners
|
||||
let _l1 = announcement.listener();
|
||||
let _l2 = announcement.listener();
|
||||
|
||||
// Panic during creation
|
||||
panic!("Panic during listener creation");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic with many clones
|
||||
///
|
||||
/// Verifies: System handles panic while iterating over many clones
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_with_many_clones() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create many clones
|
||||
let clones: Vec<_> = (0..100).map(|_| announcement.clone()).collect();
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
for (i, clone) in clones.iter().enumerate() {
|
||||
let _listener = clone.listener();
|
||||
|
||||
if i == 50 {
|
||||
panic!("Panic at clone 50");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// Original should still work
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic during concurrent announces
|
||||
///
|
||||
/// Verifies: Panics in one announcement don't affect concurrent announcements
|
||||
/// Category: panic
|
||||
#[tokio::test]
|
||||
async fn test_panic_during_concurrent_announces() {
|
||||
// Test that panics don't affect concurrent operations
|
||||
|
||||
for _ in 0..10 {
|
||||
let (announcer1, announcement1) = Announcement::new();
|
||||
let (announcer2, announcement2) = Announcement::new();
|
||||
|
||||
let l1 = announcement1.listener();
|
||||
let l2 = announcement2.listener();
|
||||
|
||||
// Spawn task that will panic
|
||||
let h1 = tokio::spawn(async move {
|
||||
announcer1.announce(42).unwrap();
|
||||
panic!("Panic after first announce");
|
||||
});
|
||||
|
||||
// Spawn task that should succeed
|
||||
let h2 = tokio::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
announcer2.announce(100).unwrap();
|
||||
});
|
||||
|
||||
// First task should panic
|
||||
assert!(h1.await.is_err());
|
||||
|
||||
// Second task should succeed
|
||||
assert!(h2.await.is_ok());
|
||||
|
||||
// Both values should be available
|
||||
assert_eq!(l1.listen().await, Ok(42));
|
||||
assert_eq!(l2.listen().await, Ok(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic with nested Arc structures
|
||||
///
|
||||
/// Verifies: System handles panics with complex nested Arc data structures
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_in_nested_structures() {
|
||||
#[derive(Debug, Clone)]
|
||||
struct Nested {
|
||||
_data: Arc<Vec<Arc<String>>>,
|
||||
}
|
||||
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let nested = Nested {
|
||||
_data: Arc::new(vec![
|
||||
Arc::new("a".to_string()),
|
||||
Arc::new("b".to_string()),
|
||||
Arc::new("c".to_string()),
|
||||
]),
|
||||
};
|
||||
|
||||
announcer.announce(nested).unwrap();
|
||||
|
||||
let _value = listener.try_listen();
|
||||
|
||||
panic!("Panic with nested structure");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic mixing blocking and async
|
||||
///
|
||||
/// Verifies: Panics in blocking threads don't affect async operations
|
||||
/// Category: panic
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_panic_mixing_blocking_and_async() {
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener1 = announcement.listener();
|
||||
let listener2 = announcement.listener();
|
||||
|
||||
// Blocking thread with timeout
|
||||
let h1 = thread::spawn(move || {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let _value = listener1.listen_timeout_blocking(Duration::from_millis(100));
|
||||
panic!("Panic in blocking thread with timeout");
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
});
|
||||
|
||||
// Announce from main thread
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
h1.join().unwrap();
|
||||
|
||||
// Other listener should still work
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async { listener2.listen().await });
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
|
||||
/// Test: Panic safety: panic after immediate announcer drop
|
||||
///
|
||||
/// Verifies: System handles panic occurring after announce and immediate drop
|
||||
/// Category: panic
|
||||
#[test]
|
||||
fn test_panic_with_immediate_drop() {
|
||||
// Test panic when announcer is dropped immediately after announce
|
||||
for _ in 0..20 {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
// Announcer dropped here
|
||||
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
|
||||
panic!("Panic after announce and drop");
|
||||
}));
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// System should still work
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(100).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(100));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc ffcff4ea6a5b0fe8eed6a43f2ed183508e5248b46fbc50bded768a61f071c177 # shrinks to value = -1909181462, delays = [4, 3]
|
||||
cc e107508cad2bad5d0efbb2ce1c68d5aadec45bd310d1f602aa0f6e2ea8c84cfa # shrinks to value = -468334696, delays = [1, 5]
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,802 @@
|
||||
//! Runtime comparison tests for the announcement crate.
|
||||
//! Verifies identical behavior across different async runtimes and blocking mode.
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// === TOKIO TESTS ===
|
||||
|
||||
/// Test: Runtime test: basic announce and listen
|
||||
///
|
||||
/// Verifies: Basic announcement and listening works correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_basic_announce_listen() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await.unwrap(), 42);
|
||||
}
|
||||
|
||||
/// Test: Runtime test: listen initiated before announce
|
||||
///
|
||||
/// Verifies: Listener waiting before announce receives value correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_listen_before_announce() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = tokio::spawn(async move { listener.listen().await.unwrap() });
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
assert_eq!(handle.await.unwrap(), 42);
|
||||
}
|
||||
|
||||
/// Test: Runtime test: multiple concurrent listeners
|
||||
///
|
||||
/// Verifies: Multiple listeners all receive the same announced value on the runtime
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_multiple_listeners() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
(i, listener.listen().await.unwrap())
|
||||
}));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: multi-threaded execution
|
||||
///
|
||||
/// Verifies: Multi-threaded runtime executes announcements correctly
|
||||
/// Category: runtime
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn tokio_multi_threaded() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..20 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
(i, listener.listen().await.unwrap())
|
||||
}));
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: current-thread execution
|
||||
///
|
||||
/// Verifies: Current-thread runtime executes announcements correctly
|
||||
/// Category: runtime
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn tokio_current_thread() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..20 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(async move {
|
||||
(i, listener.listen().await.unwrap())
|
||||
}));
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: select/race pattern
|
||||
///
|
||||
/// Verifies: Runtime select patterns work correctly with multiple announcements
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_select_pattern() {
|
||||
let (announcer1, announcement1) = Announcement::new();
|
||||
let (announcer2, announcement2) = Announcement::new();
|
||||
|
||||
let listener1 = announcement1.listener();
|
||||
let listener2 = announcement2.listener();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
announcer2.announce(2).unwrap();
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
announcer1.announce(1).unwrap();
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
val = listener1.listen() => assert_eq!(val, Ok(1)),
|
||||
val = listener2.listen() => assert_eq!(val, Ok(2)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: timeout behavior
|
||||
///
|
||||
/// Verifies: Runtime timeout mechanisms work correctly with announcements
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_timeout_behavior() {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let result = tokio::time::timeout(Duration::from_millis(100), listener.listen()).await;
|
||||
|
||||
assert!(result.is_err()); // Should timeout
|
||||
}
|
||||
|
||||
/// Test: Runtime test: blocking task interoperability
|
||||
///
|
||||
/// Verifies: Runtime blocking task spawning interoperates correctly with async
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_spawn_blocking_interop() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let blocking_handle = tokio::task::spawn_blocking(move || {
|
||||
// Blocking thread operation
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
announcer.announce(42).unwrap();
|
||||
});
|
||||
|
||||
let value = listener.listen().await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
|
||||
blocking_handle.await.unwrap();
|
||||
}
|
||||
|
||||
/// Test: Runtime test: mixed async and sync listeners
|
||||
///
|
||||
/// Verifies: Mix of async and synchronous listeners work correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[tokio::test]
|
||||
async fn tokio_mixed_async_sync() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Async listeners
|
||||
let mut async_handles = vec![];
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
async_handles.push(tokio::spawn(
|
||||
async move { listener.listen().await.unwrap() },
|
||||
));
|
||||
}
|
||||
|
||||
// Sync listeners in blocking threads
|
||||
let mut sync_handles = vec![];
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
sync_handles.push(tokio::task::spawn_blocking(move || loop {
|
||||
if let Some(val) = listener.try_listen() {
|
||||
return val;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in async_handles {
|
||||
assert_eq!(handle.await.unwrap(), 42);
|
||||
}
|
||||
|
||||
for handle in sync_handles {
|
||||
assert_eq!(handle.await.unwrap(), 42);
|
||||
}
|
||||
}
|
||||
|
||||
// === SMOL TESTS (replacing async-std) ===
|
||||
|
||||
/// Test: Runtime test: basic announce and listen
|
||||
///
|
||||
/// Verifies: Basic announcement and listening works correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_basic_announce_listen() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await.unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Runtime test: listen initiated before announce
|
||||
///
|
||||
/// Verifies: Listener waiting before announce receives value correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_listen_before_announce() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = smol::spawn(async move { listener.listen().await.unwrap() });
|
||||
|
||||
smol::Timer::after(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
assert_eq!(handle.await, 42);
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Runtime test: multiple concurrent listeners
|
||||
///
|
||||
/// Verifies: Multiple listeners all receive the same announced value on the runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_multiple_listeners() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(
|
||||
async move { (i, listener.listen().await.unwrap()) },
|
||||
));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await;
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Runtime test: timeout behavior
|
||||
///
|
||||
/// Verifies: Runtime timeout mechanisms work correctly with announcements
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_timeout_behavior() {
|
||||
smol::block_on(async {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let result = smol::future::or(
|
||||
async {
|
||||
listener.listen().await?;
|
||||
Ok(())
|
||||
},
|
||||
async {
|
||||
smol::Timer::after(Duration::from_millis(100)).await;
|
||||
Err::<(), _>(announcement::ListenError::Timeout)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err()); // Should timeout
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Runtime test: blocking task interoperability
|
||||
///
|
||||
/// Verifies: Runtime blocking task spawning interoperates correctly with async
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_spawn_blocking_interop() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let blocking_handle = smol::unblock(move || {
|
||||
// Blocking thread operation
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
announcer.announce(42).unwrap();
|
||||
});
|
||||
|
||||
let value = listener.listen().await.unwrap();
|
||||
assert_eq!(value, 42);
|
||||
|
||||
blocking_handle.await;
|
||||
});
|
||||
}
|
||||
|
||||
// === SMOL TESTS (already in smol_runtime.rs, adding comparisons) ===
|
||||
|
||||
/// Test: Runtime test: Smol runtime basic operations
|
||||
///
|
||||
/// Verifies: Basic announcement and listening works on Smol runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_basic_operations() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await.unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Runtime test: concurrent tasks
|
||||
///
|
||||
/// Verifies: Concurrent tasks work correctly on the runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn smol_concurrent_tasks() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(
|
||||
async move { (i, listener.listen().await.unwrap()) },
|
||||
));
|
||||
}
|
||||
|
||||
smol::Timer::after(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await;
|
||||
assert_eq!(value, 42);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// === BLOCKING TESTS ===
|
||||
|
||||
/// Test: Runtime test: blocking mode basic operations
|
||||
///
|
||||
/// Verifies: Basic announcement and listen operations work in blocking mode
|
||||
/// Category: runtime
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn blocking_basic_operations() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(
|
||||
listener.listen_timeout_blocking(Duration::from_millis(100)),
|
||||
Ok(42)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Runtime test: blocking mode timeout
|
||||
///
|
||||
/// Verifies: Timeout behavior works correctly in blocking mode
|
||||
/// Category: runtime
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn blocking_timeout() {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
assert_eq!(result, Err(announcement::ListenError::Timeout));
|
||||
}
|
||||
|
||||
/// Test: Runtime test: blocking mode multiple threads
|
||||
///
|
||||
/// Verifies: Multiple blocking threads receive announced values correctly
|
||||
/// Category: runtime
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn blocking_multiple_threads() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(thread::spawn(move || {
|
||||
(
|
||||
i,
|
||||
listener.listen_timeout_blocking(Duration::from_millis(100)),
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
for handle in handles {
|
||||
let (_, value) = handle.join().unwrap();
|
||||
assert_eq!(value, Ok(42));
|
||||
}
|
||||
}
|
||||
|
||||
// === COMPARISON TESTS ===
|
||||
|
||||
// Test that verifies identical behavior across all runtimes
|
||||
|
||||
/// Test: Runtime test: all runtimes basic comparison
|
||||
///
|
||||
/// Verifies: All runtimes produce identical results for basic operations
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn compare_all_runtimes_basic() {
|
||||
// Tokio multi-thread
|
||||
let tokio_multi_result = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
listener.listen().await.unwrap()
|
||||
});
|
||||
|
||||
// Tokio current-thread
|
||||
let tokio_current_result = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
listener.listen().await.unwrap()
|
||||
});
|
||||
|
||||
// Smol
|
||||
let smol_result2 = smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
listener.listen().await.unwrap()
|
||||
});
|
||||
|
||||
// Smol
|
||||
let smol_result = smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
listener.listen().await.unwrap()
|
||||
});
|
||||
|
||||
// Blocking
|
||||
#[cfg(feature = "std")]
|
||||
let blocking_result = {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
listener.listen_timeout_blocking(Duration::from_millis(100))
|
||||
};
|
||||
|
||||
// All should be identical
|
||||
assert_eq!(tokio_multi_result, 42);
|
||||
assert_eq!(tokio_current_result, 42);
|
||||
assert_eq!(smol_result2, 42);
|
||||
assert_eq!(smol_result, 42);
|
||||
#[cfg(feature = "std")]
|
||||
assert_eq!(blocking_result, Ok(42));
|
||||
}
|
||||
|
||||
/// Test: Runtime test: all runtimes concurrent comparison
|
||||
///
|
||||
/// Verifies: All runtimes produce identical results for concurrent operations
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn compare_all_runtimes_concurrent() {
|
||||
let test_concurrent = |runtime: &str| -> Vec<i32> {
|
||||
match runtime {
|
||||
"tokio-multi" => tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(
|
||||
async move { listener.listen().await.unwrap() },
|
||||
));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
let mut results = vec![];
|
||||
for handle in handles {
|
||||
results.push(handle.await.unwrap());
|
||||
}
|
||||
results
|
||||
}),
|
||||
"tokio-current" => tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(tokio::spawn(
|
||||
async move { listener.listen().await.unwrap() },
|
||||
));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
let mut results = vec![];
|
||||
for handle in handles {
|
||||
results.push(handle.await.unwrap());
|
||||
}
|
||||
results
|
||||
}),
|
||||
"smol2" => smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(async move { listener.listen().await.unwrap() }));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
let mut results = vec![];
|
||||
for handle in handles {
|
||||
results.push(handle.await);
|
||||
}
|
||||
results
|
||||
}),
|
||||
"smol" => smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let mut handles = vec![];
|
||||
|
||||
for _ in 0..5 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(async move { listener.listen().await.unwrap() }));
|
||||
}
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
let mut results = vec![];
|
||||
for handle in handles {
|
||||
results.push(handle.await);
|
||||
}
|
||||
results
|
||||
}),
|
||||
_ => panic!("Unknown runtime"),
|
||||
}
|
||||
};
|
||||
|
||||
let tokio_multi = test_concurrent("tokio-multi");
|
||||
let tokio_current = test_concurrent("tokio-current");
|
||||
let smol2 = test_concurrent("smol2");
|
||||
let smol = test_concurrent("smol");
|
||||
|
||||
// All should produce same results
|
||||
assert_eq!(tokio_multi, vec![42; 5]);
|
||||
assert_eq!(tokio_current, vec![42; 5]);
|
||||
assert_eq!(smol2, vec![42; 5]);
|
||||
assert_eq!(smol, vec![42; 5]);
|
||||
}
|
||||
|
||||
// === CROSS-RUNTIME TESTS ===
|
||||
|
||||
/// Test: Runtime test: cross-runtime announcement
|
||||
///
|
||||
/// Verifies: Announcement works correctly when components use different runtimes
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn cross_runtime_announcement() {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let announcement = Arc::new(announcement);
|
||||
|
||||
// Create listener in one runtime
|
||||
let ann1 = announcement.clone();
|
||||
let tokio_listener = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move { ann1.listener() });
|
||||
|
||||
// Create listener in another runtime
|
||||
let ann2 = announcement.clone();
|
||||
let smol_listener = smol::block_on(async move { ann2.listener() });
|
||||
|
||||
// Announce in yet another runtime
|
||||
smol::block_on(async {
|
||||
announcer.announce(42).unwrap();
|
||||
});
|
||||
|
||||
// Verify in original runtimes
|
||||
let tokio_result = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async { tokio_listener.listen().await.unwrap() });
|
||||
|
||||
let smol_result = smol::block_on(async { smol_listener.listen().await.unwrap() });
|
||||
|
||||
assert_eq!(tokio_result, 42);
|
||||
assert_eq!(smol_result, 42);
|
||||
}
|
||||
|
||||
// === BEHAVIOR CONSISTENCY TESTS ===
|
||||
|
||||
/// Test: Runtime test: consistent is_announced behavior
|
||||
///
|
||||
/// Verifies: is_announced() returns consistent results across invocations
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn verify_consistent_is_announced() {
|
||||
let test_is_announced = || {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let before = announcement.is_announced();
|
||||
announcer.announce(42).unwrap();
|
||||
let after = announcement.is_announced();
|
||||
|
||||
(before, after)
|
||||
};
|
||||
|
||||
// Run multiple times to ensure consistency
|
||||
for _ in 0..10 {
|
||||
let (before, after) = test_is_announced();
|
||||
assert!(!before);
|
||||
assert!(after);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: consistent try_listen behavior
|
||||
///
|
||||
/// Verifies: try_listen() returns consistent results across invocations
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn verify_consistent_try_listen() {
|
||||
let test_try_listen = || {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let before = listener.try_listen();
|
||||
announcer.announce(42).unwrap();
|
||||
let after = listener.try_listen();
|
||||
|
||||
(before, after)
|
||||
};
|
||||
|
||||
// Run multiple times to ensure consistency
|
||||
for _ in 0..10 {
|
||||
let (before, after) = test_try_listen();
|
||||
assert_eq!(before, None);
|
||||
assert_eq!(after, Some(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Runtime test: multiple concurrent listeners
|
||||
///
|
||||
/// Verifies: Multiple listeners all receive the same announced value on the runtime
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn verify_consistent_multiple_listeners() {
|
||||
let test_multiple = || {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let listeners: Vec<_> = (0..5).map(|_| announcement.listener()).collect();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
listeners.iter().map(|l| l.try_listen()).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// Run multiple times
|
||||
for _ in 0..10 {
|
||||
let results = test_multiple();
|
||||
assert_eq!(results, vec![Some(42); 5]);
|
||||
}
|
||||
}
|
||||
|
||||
// === FINAL COMPARISON SUMMARY ===
|
||||
|
||||
/// Test: Runtime test: comparison summary
|
||||
///
|
||||
/// Verifies: All runtime comparison tests pass and produce consistent results
|
||||
/// Category: runtime
|
||||
#[test]
|
||||
fn runtime_comparison_summary() {
|
||||
println!("Runtime Comparison Test Summary:");
|
||||
println!("================================");
|
||||
|
||||
let mut all_passed = true;
|
||||
|
||||
// Test each runtime
|
||||
|
||||
{
|
||||
// Tokio
|
||||
let tokio_ok = std::panic::catch_unwind(|| {
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (ann, announcement) = Announcement::new();
|
||||
let l = announcement.listener();
|
||||
ann.announce(1).unwrap();
|
||||
assert_eq!(l.listen().await.unwrap(), 1);
|
||||
});
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
println!("Tokio: {}", if tokio_ok { "✓ PASS" } else { "✗ FAIL" });
|
||||
all_passed = all_passed && tokio_ok;
|
||||
|
||||
// Smol
|
||||
let smol_ok = std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
let (ann, announcement) = Announcement::new();
|
||||
let l = announcement.listener();
|
||||
ann.announce(3).unwrap();
|
||||
assert_eq!(l.listen().await.unwrap(), 3);
|
||||
});
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
println!("Smol: {}", if smol_ok { "✓ PASS" } else { "✗ FAIL" });
|
||||
all_passed = all_passed && smol_ok;
|
||||
}
|
||||
|
||||
// Blocking
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let blocking_ok = std::panic::catch_unwind(|| {
|
||||
let (ann, announcement) = Announcement::new();
|
||||
let l = announcement.listener();
|
||||
ann.announce(4).unwrap();
|
||||
assert_eq!(l.listen_timeout_blocking(Duration::from_millis(100)), Ok(4));
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
println!(
|
||||
"Blocking: {}",
|
||||
if blocking_ok { "✓ PASS" } else { "✗ FAIL" }
|
||||
);
|
||||
all_passed = all_passed && blocking_ok;
|
||||
}
|
||||
|
||||
println!("================================");
|
||||
println!(
|
||||
"Overall: {}",
|
||||
if all_passed {
|
||||
"✓ ALL PASS"
|
||||
} else {
|
||||
"✗ SOME FAILED"
|
||||
}
|
||||
);
|
||||
|
||||
assert!(all_passed, "Not all runtime tests passed");
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
// Skip all smol tests under Miri - it doesn't support timerfd_create syscall
|
||||
#![cfg_attr(miri, allow(dead_code))]
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
// =============================================================================
|
||||
// Smol runtime tests
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol runtime basic operations
|
||||
///
|
||||
/// Verifies: Basic announcement and listening works on Smol runtime
|
||||
/// Category: runtime
|
||||
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]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: multiple concurrent listeners
|
||||
///
|
||||
/// Verifies: Multiple listeners all receive the same announced value on the runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_multiple_listeners() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
let l3 = announcement.listener();
|
||||
|
||||
let h1 = smol::spawn(async move { l1.listen().await.unwrap() });
|
||||
let h2 = smol::spawn(async move { l2.listen().await.unwrap() });
|
||||
let h3 = smol::spawn(async move { l3.listen().await.unwrap() });
|
||||
|
||||
// Small delay before announcing
|
||||
smol::Timer::after(Duration::from_millis(10)).await;
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
assert_eq!(h1.await, 42);
|
||||
assert_eq!(h2.await, 42);
|
||||
assert_eq!(h3.await, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol concurrent announce and listen
|
||||
///
|
||||
/// Verifies: Concurrent announce and listen operations work on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_concurrent_announce_and_listen() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create many listeners concurrently
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..50 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(
|
||||
async move { (i, listener.listen().await.unwrap()) },
|
||||
));
|
||||
}
|
||||
|
||||
// Announce value concurrently
|
||||
smol::spawn(async move {
|
||||
smol::Timer::after(Duration::from_millis(5)).await;
|
||||
announcer.announce(999).unwrap();
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Verify all listeners get the value
|
||||
for handle in handles {
|
||||
let (_, value) = handle.await;
|
||||
assert_eq!(value, 999);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol listen after announce
|
||||
///
|
||||
/// Verifies: Listening after announce returns immediately on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_listen_after_announce() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Announce first
|
||||
announcer.announce(42).unwrap();
|
||||
|
||||
// Create listeners after
|
||||
let l1 = announcement.listener();
|
||||
let l2 = announcement.listener();
|
||||
|
||||
// Should return immediately
|
||||
assert_eq!(l1.listen().await.unwrap(), 42);
|
||||
assert_eq!(l2.listen().await.unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol with Arc-wrapped data
|
||||
///
|
||||
/// Verifies: Arc-wrapped data is handled correctly on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_with_arc() {
|
||||
smol::block_on(async {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Data {
|
||||
value: Vec<u8>,
|
||||
}
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let data = Arc::new(Data {
|
||||
value: vec![1, 2, 3, 4, 5],
|
||||
});
|
||||
|
||||
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
|
||||
assert_eq!(Arc::strong_count(&r1), 4);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol race condition protection
|
||||
///
|
||||
/// Verifies: Race conditions are properly handled on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_race_condition_protection() {
|
||||
smol::block_on(async {
|
||||
// Run multiple times to catch any race conditions
|
||||
for i in 0..100 {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let handle = smol::spawn(async move { listener.listen().await.unwrap() });
|
||||
|
||||
// Announce immediately (potential race)
|
||||
announcer.announce(i).unwrap();
|
||||
|
||||
assert_eq!(handle.await, i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol many concurrent tasks
|
||||
///
|
||||
/// Verifies: Many concurrent tasks all receive values correctly on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_many_concurrent_tasks() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create 100 concurrent listeners
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(smol::spawn(async move { listener.listen().await.unwrap() }));
|
||||
}
|
||||
|
||||
// Wait a bit to ensure all are waiting
|
||||
smol::Timer::after(Duration::from_millis(10)).await;
|
||||
|
||||
// Announce
|
||||
announcer.announce(12345).unwrap();
|
||||
|
||||
// All should receive the same value
|
||||
for handle in handles {
|
||||
assert_eq!(handle.await, 12345);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol dropped listener handling
|
||||
///
|
||||
/// Verifies: System works correctly when listeners are dropped on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_listener_dropped() {
|
||||
smol::block_on(async {
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
|
||||
// Create and immediately drop a listener
|
||||
{
|
||||
let _listener = announcement.listener();
|
||||
// Dropped here
|
||||
}
|
||||
|
||||
// Should still work with new listeners
|
||||
let listener = announcement.listener();
|
||||
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.listen().await.unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol announcement cloning
|
||||
///
|
||||
/// Verifies: Cloned announcements work correctly on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_clone_announcement() {
|
||||
smol::block_on(async {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: timeout behavior
|
||||
///
|
||||
/// Verifies: Runtime timeout mechanisms work correctly with announcements
|
||||
/// Category: runtime
|
||||
fn test_smol_timeout_behavior() {
|
||||
use announcement::ListenError;
|
||||
|
||||
smol::block_on(async {
|
||||
let (_, announcement) = Announcement::<i32>::new();
|
||||
// Note: Announcer dropped immediately via _ pattern - sets closed flag
|
||||
let listener = announcement.listener();
|
||||
|
||||
// With closed detection, listen() returns Closed immediately
|
||||
let result = listener.listen().await;
|
||||
assert_eq!(result, Err(ListenError::Closed));
|
||||
assert!(announcement.is_closed());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
miri,
|
||||
ignore = "Miri doesn't support timerfd_create syscall used by smol"
|
||||
)]
|
||||
/// Test: Runtime test: Smol complex data types
|
||||
///
|
||||
/// Verifies: Complex data types work correctly on Smol runtime
|
||||
/// Category: runtime
|
||||
fn test_smol_complex_types() {
|
||||
smol::block_on(async {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct ComplexData {
|
||||
name: String,
|
||||
values: Vec<i32>,
|
||||
nested: Option<Box<ComplexData>>,
|
||||
}
|
||||
|
||||
let (announcer, announcement) = Announcement::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let data = ComplexData {
|
||||
name: "test".to_string(),
|
||||
values: vec![1, 2, 3, 4, 5],
|
||||
nested: None,
|
||||
};
|
||||
|
||||
announcer.announce(data.clone()).unwrap();
|
||||
|
||||
let result = listener.listen().await.unwrap();
|
||||
assert_eq!(result, data);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! Tests specifically designed to hit TOCTOU protection code paths for coverage.
|
||||
//!
|
||||
//! These tests target the race condition protection code in listen(), listen_blocking(),
|
||||
//! and listen_timeout_blocking() that checks for values/closure after registering the listener.
|
||||
|
||||
use announcement::Announcement;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use std::thread;
|
||||
#[cfg(feature = "std")]
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test: Async listen() TOCTOU value path (line 674)
|
||||
///
|
||||
/// Verifies: Value announced during listener registration is caught
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_async_listen_toctou_value_path() {
|
||||
// Run multiple iterations to increase chance of hitting the race window
|
||||
for _ in 0..1000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let ready = Arc::new(AtomicBool::new(false));
|
||||
let ready_clone = ready.clone();
|
||||
|
||||
// Spawn task that announces as soon as listener starts waiting
|
||||
tokio::spawn(async move {
|
||||
// Spin until listener is likely setting up
|
||||
while !ready_clone.load(Ordering::SeqCst) {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
// Announce immediately to hit TOCTOU window
|
||||
let _ = announcer.announce(42);
|
||||
});
|
||||
|
||||
// Start listening with a flag to signal we're about to wait
|
||||
ready.store(true, Ordering::SeqCst);
|
||||
let result = listener.listen().await;
|
||||
|
||||
// Should get the value (either fast path or TOCTOU path)
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Async listen() TOCTOU closed path (line 681)
|
||||
///
|
||||
/// Verifies: Announcer closed during listener registration is caught
|
||||
/// Category: race
|
||||
#[tokio::test]
|
||||
async fn test_async_listen_toctou_closed_path() {
|
||||
use std::sync::Barrier;
|
||||
|
||||
// Run multiple iterations to increase chance of hitting the race window
|
||||
for _ in 0..10000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
let barrier_clone = barrier.clone();
|
||||
|
||||
// Spawn task that drops announcer at precisely the right moment
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// Wait for listener to start setup
|
||||
barrier_clone.wait();
|
||||
// Immediately drop to hit the narrow TOCTOU window
|
||||
drop(announcer);
|
||||
});
|
||||
|
||||
// Use a manual future that coordinates with the barrier
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
barrier.wait(); // Signal we're about to call listen
|
||||
// Immediately start listening to hit the race window
|
||||
tokio::runtime::Handle::current().block_on(async { listener.listen().await })
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should get Closed error (either first check or TOCTOU path)
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Blocking listen_timeout TOCTOU value path (line 819)
|
||||
///
|
||||
/// Verifies: Value announced during listener registration is caught in blocking mode
|
||||
/// Category: race
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_blocking_timeout_toctou_value_path() {
|
||||
use std::sync::Barrier;
|
||||
|
||||
// Run many iterations with precise synchronization
|
||||
for _ in 0..10000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
let barrier_clone = barrier.clone();
|
||||
|
||||
// Spawn thread that announces at precisely the right moment
|
||||
thread::spawn(move || {
|
||||
// Wait for listener to start
|
||||
barrier_clone.wait();
|
||||
// Announce immediately to hit TOCTOU window
|
||||
let _ = announcer.announce(42);
|
||||
});
|
||||
|
||||
// Synchronize and immediately start listening
|
||||
barrier.wait();
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
|
||||
// Should get the value (either fast path or TOCTOU path)
|
||||
assert_eq!(result, Ok(42));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Blocking listen_timeout TOCTOU closed path (line 823)
|
||||
///
|
||||
/// Verifies: Announcer closed during listener registration is caught in blocking mode
|
||||
/// Category: race
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_blocking_timeout_toctou_closed_path() {
|
||||
use std::sync::Barrier;
|
||||
|
||||
// Run many iterations with precise synchronization
|
||||
for _ in 0..10000 {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
let barrier = Arc::new(Barrier::new(2));
|
||||
let barrier_clone = barrier.clone();
|
||||
|
||||
// Spawn thread that drops announcer at precisely the right moment
|
||||
thread::spawn(move || {
|
||||
// Wait for listener to start
|
||||
barrier_clone.wait();
|
||||
// Drop immediately to hit TOCTOU window
|
||||
drop(announcer);
|
||||
});
|
||||
|
||||
// Synchronize and immediately start listening
|
||||
barrier.wait();
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(100));
|
||||
|
||||
// Should get Closed error (either first check or TOCTOU path)
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: Blocking listen_timeout zero remaining time path (line 828)
|
||||
///
|
||||
/// Verifies: Timeout returns error when remaining time is zero
|
||||
/// Category: unit
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_blocking_timeout_zero_remaining() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Keep announcer alive but don't announce
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
drop(announcer);
|
||||
});
|
||||
|
||||
// Use a very short timeout (1 nanosecond)
|
||||
let result = listener.listen_timeout_blocking(Duration::from_nanos(1));
|
||||
|
||||
// Should timeout
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Test: Blocking listen_timeout actual timeout path (line 828 alternative)
|
||||
///
|
||||
/// Verifies: Timeout returns error when deadline passes
|
||||
/// Category: unit
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_blocking_timeout_deadline_expires() {
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Use 1 millisecond timeout with no announcement
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(1));
|
||||
|
||||
// Should timeout (not closed)
|
||||
assert_eq!(result.unwrap_err(), announcement::ListenError::Timeout);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Tracing coverage tests
|
||||
//!
|
||||
//! These tests ensure tracing statements are executed during coverage runs.
|
||||
//! They set up a tracing subscriber and exercise all code paths that contain
|
||||
//! tracing statements.
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
#[cfg(test)]
|
||||
mod tracing_tests {
|
||||
use announcement::Announcement;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
|
||||
/// Test: Tracing test: all tracing paths
|
||||
///
|
||||
/// Verifies: Verifies all tracing paths
|
||||
/// Category: tracing
|
||||
/// Test that exercises all tracing statements in src/lib.rs
|
||||
#[test]
|
||||
fn test_all_tracing_paths() {
|
||||
// Set up tracing subscriber to capture all trace/debug messages
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_span_events(FmtSpan::FULL)
|
||||
.with_test_writer()
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
// Test 1: Announcement::new() - covers line 286-287
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
|
||||
// Test 2: announce() - covers lines 422-423, 440, 444
|
||||
let listener = announcement.listener();
|
||||
announcer.announce(42).unwrap();
|
||||
assert_eq!(listener.try_listen(), Some(42));
|
||||
|
||||
// Test 3: close() - covers lines 486, 492-493
|
||||
let (announcer2, announcement2) = Announcement::<String>::new();
|
||||
let _listener2 = announcement2.listener();
|
||||
announcer2.close();
|
||||
assert!(announcement2.is_closed());
|
||||
|
||||
// Test 4: Drop (auto-close) - covers lines 531-532, 538-539
|
||||
let (announcer3, announcement3) = Announcement::<Vec<u8>>::new();
|
||||
let _listener3 = announcement3.listener();
|
||||
drop(announcer3); // Triggers Drop impl
|
||||
assert!(announcement3.is_closed());
|
||||
|
||||
// Test 5: listen() async fast path - covers lines 653-654
|
||||
let (announcer4, announcement4) = Announcement::<String>::new();
|
||||
announcer4.announce("fast".to_string()).unwrap();
|
||||
let listener4 = announcement4.listener();
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async { listener4.listen().await });
|
||||
assert_eq!(result, Ok("fast".to_string()));
|
||||
|
||||
// Test 6: listen() async slow path - covers lines 660-661, 666-667, 675-677, 682-683, 691
|
||||
let (announcer5, announcement5) = Announcement::<i32>::new();
|
||||
let listener5 = announcement5.listener();
|
||||
|
||||
let rt2 = tokio::runtime::Runtime::new().unwrap();
|
||||
rt2.block_on(async {
|
||||
let listen_future = listener5.listen();
|
||||
|
||||
// Spawn a task to announce after a delay
|
||||
let ann5_clone = announcer5;
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
ann5_clone.announce(99).unwrap();
|
||||
});
|
||||
|
||||
let result = listen_future.await;
|
||||
assert_eq!(result, Ok(99));
|
||||
});
|
||||
|
||||
// Test 7: listen() async with closed announcer - covers lines 660-661, 666-667, 682-683
|
||||
let (announcer6, announcement6) = Announcement::<String>::new();
|
||||
let listener6 = announcement6.listener();
|
||||
|
||||
let rt3 = tokio::runtime::Runtime::new().unwrap();
|
||||
rt3.block_on(async {
|
||||
let listen_future = listener6.listen();
|
||||
|
||||
// Close the announcer
|
||||
let ann6_clone = announcer6;
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
ann6_clone.close();
|
||||
});
|
||||
|
||||
let result = listen_future.await;
|
||||
assert!(result.is_err());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Tracing test: blocking with tracing
|
||||
///
|
||||
/// Verifies: Verifies blocking with tracing
|
||||
/// Category: tracing
|
||||
/// Test blocking operations with tracing
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_blocking_with_tracing() {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_test_writer()
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
// listen_blocking() - exercises tracing in blocking wait
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
announcer.announce(123).unwrap();
|
||||
});
|
||||
|
||||
let result = listener.listen_blocking();
|
||||
assert_eq!(result, Ok(123));
|
||||
});
|
||||
}
|
||||
|
||||
/// Test: Tracing test: concurrent tracing
|
||||
///
|
||||
/// Verifies: Verifies concurrent tracing
|
||||
/// Category: tracing
|
||||
/// Test concurrent tracing paths
|
||||
#[test]
|
||||
fn test_concurrent_tracing() {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_test_writer()
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
let (announcer, announcement) = Announcement::<Arc<String>>::new();
|
||||
|
||||
// Announce first
|
||||
announcer
|
||||
.announce(Arc::new("concurrent".to_string()))
|
||||
.unwrap();
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
// Create multiple listeners concurrently after announce
|
||||
for _ in 0..10 {
|
||||
let listener = announcement.listener();
|
||||
handles.push(thread::spawn(move || listener.try_listen()));
|
||||
}
|
||||
|
||||
// Verify all threads received the value
|
||||
for handle in handles {
|
||||
let result = handle.join().unwrap();
|
||||
assert_eq!(result, Some(Arc::new("concurrent".to_string())));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
//! Tests for trait implementations (Display, Error, Debug, etc.)
|
||||
//!
|
||||
//! These tests ensure that all trait implementations are properly covered
|
||||
//! by the test suite, particularly for error types.
|
||||
|
||||
use announcement::{AnnounceError, Announcement, ListenError};
|
||||
use std::error::Error;
|
||||
|
||||
// =============================================================================
|
||||
// AnnounceError Trait Implementation Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Trait test: announce error display
|
||||
///
|
||||
/// Verifies: Verifies announce error display
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_display() {
|
||||
let err = AnnounceError::AlreadyAnnounced(42);
|
||||
let display_str = format!("{}", err);
|
||||
assert_eq!(display_str, "a value has already been announced");
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error display string type
|
||||
///
|
||||
/// Verifies: Verifies announce error display string type
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_display_string_type() {
|
||||
let err = AnnounceError::AlreadyAnnounced("test".to_string());
|
||||
let display_str = format!("{}", err);
|
||||
assert_eq!(display_str, "a value has already been announced");
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error to string
|
||||
///
|
||||
/// Verifies: Verifies announce error to string
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_to_string() {
|
||||
let err = AnnounceError::AlreadyAnnounced(99);
|
||||
assert_eq!(err.to_string(), "a value has already been announced");
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error std error trait
|
||||
///
|
||||
/// Verifies: Verifies announce error std error trait
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_std_error_trait() {
|
||||
let err = AnnounceError::AlreadyAnnounced(42);
|
||||
// Verify it implements std::error::Error by using it as a trait object
|
||||
let _err_ref: &dyn Error = &err;
|
||||
// Verify source() returns None (default implementation)
|
||||
assert!(err.source().is_none());
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error debug
|
||||
///
|
||||
/// Verifies: Verifies announce error debug
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_debug() {
|
||||
let err = AnnounceError::AlreadyAnnounced(42);
|
||||
let debug_str = format!("{:?}", err);
|
||||
assert!(debug_str.contains("AlreadyAnnounced"));
|
||||
assert!(debug_str.contains("42"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ListenError Trait Implementation Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Trait test: listen error timeout display
|
||||
///
|
||||
/// Verifies: Verifies listen error timeout display
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_timeout_display() {
|
||||
let err = ListenError::Timeout;
|
||||
let display_str = format!("{}", err);
|
||||
assert_eq!(
|
||||
display_str,
|
||||
"timeout elapsed while waiting for announcement"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error closed display
|
||||
///
|
||||
/// Verifies: Verifies listen error closed display
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_closed_display() {
|
||||
let err = ListenError::Closed;
|
||||
let display_str = format!("{}", err);
|
||||
assert_eq!(display_str, "announcer was dropped without announcing");
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error timeout to string
|
||||
///
|
||||
/// Verifies: Verifies listen error timeout to string
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_timeout_to_string() {
|
||||
let err = ListenError::Timeout;
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"timeout elapsed while waiting for announcement"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error closed to string
|
||||
///
|
||||
/// Verifies: Verifies listen error closed to string
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_closed_to_string() {
|
||||
let err = ListenError::Closed;
|
||||
assert_eq!(err.to_string(), "announcer was dropped without announcing");
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error std error trait
|
||||
///
|
||||
/// Verifies: Verifies listen error std error trait
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_std_error_trait() {
|
||||
let err = ListenError::Timeout;
|
||||
// Verify it implements std::error::Error by using it as a trait object
|
||||
let _err_ref: &dyn Error = &err;
|
||||
// Verify source() returns None (default implementation)
|
||||
assert!(err.source().is_none());
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error debug
|
||||
///
|
||||
/// Verifies: Verifies listen error debug
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_debug() {
|
||||
let timeout_err = ListenError::Timeout;
|
||||
let closed_err = ListenError::Closed;
|
||||
|
||||
let timeout_debug = format!("{:?}", timeout_err);
|
||||
let closed_debug = format!("{:?}", closed_err);
|
||||
|
||||
assert!(timeout_debug.contains("Timeout"));
|
||||
assert!(closed_debug.contains("Closed"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Clone and PartialEq Tests for Error Types
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Trait test: announce error clone
|
||||
///
|
||||
/// Verifies: Verifies announce error clone
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_clone() {
|
||||
let err1 = AnnounceError::AlreadyAnnounced(42);
|
||||
let err2 = err1.clone();
|
||||
assert_eq!(err1, err2);
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error partial eq
|
||||
///
|
||||
/// Verifies: Verifies announce error partial eq
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_partial_eq() {
|
||||
let err1 = AnnounceError::AlreadyAnnounced(42);
|
||||
let err2 = AnnounceError::AlreadyAnnounced(42);
|
||||
let err3 = AnnounceError::AlreadyAnnounced(99);
|
||||
|
||||
assert_eq!(err1, err2);
|
||||
assert_ne!(err1, err3);
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error clone
|
||||
///
|
||||
/// Verifies: Verifies listen error clone
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_clone() {
|
||||
let err1 = ListenError::Timeout;
|
||||
let err2 = err1; // Copy trait is used here, not Clone
|
||||
assert_eq!(err1, err2);
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error partial eq
|
||||
///
|
||||
/// Verifies: Verifies listen error partial eq
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_partial_eq() {
|
||||
let timeout1 = ListenError::Timeout;
|
||||
let timeout2 = ListenError::Timeout;
|
||||
let closed = ListenError::Closed;
|
||||
|
||||
assert_eq!(timeout1, timeout2);
|
||||
assert_ne!(timeout1, closed);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Usage in Result Context Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Trait test: announce error in result
|
||||
///
|
||||
/// Verifies: Verifies announce error in result
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_in_result() {
|
||||
/// Test: Trait test: returns error
|
||||
///
|
||||
/// Verifies: Verifies returns error
|
||||
/// Category: trait
|
||||
fn returns_error() -> Result<(), AnnounceError<i32>> {
|
||||
Err(AnnounceError::AlreadyAnnounced(42))
|
||||
}
|
||||
|
||||
let result = returns_error();
|
||||
assert!(result.is_err());
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(format!("{}", err), "a value has already been announced");
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error in result
|
||||
///
|
||||
/// Verifies: Verifies listen error in result
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_listen_error_in_result() {
|
||||
/// Test: Trait test: returns timeout
|
||||
///
|
||||
/// Verifies: Verifies returns timeout
|
||||
/// Category: trait
|
||||
fn returns_timeout() -> Result<i32, ListenError> {
|
||||
Err(ListenError::Timeout)
|
||||
}
|
||||
|
||||
/// Test: Trait test: returns closed
|
||||
///
|
||||
/// Verifies: Verifies returns closed
|
||||
/// Category: trait
|
||||
fn returns_closed() -> Result<i32, ListenError> {
|
||||
Err(ListenError::Closed)
|
||||
}
|
||||
|
||||
let timeout_result = returns_timeout();
|
||||
assert!(timeout_result.is_err());
|
||||
assert_eq!(
|
||||
format!("{}", timeout_result.unwrap_err()),
|
||||
"timeout elapsed while waiting for announcement"
|
||||
);
|
||||
|
||||
let closed_result = returns_closed();
|
||||
assert!(closed_result.is_err());
|
||||
assert_eq!(
|
||||
format!("{}", closed_result.unwrap_err()),
|
||||
"announcer was dropped without announcing"
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Real-World Error Scenario Tests
|
||||
// =============================================================================
|
||||
|
||||
/// Test: Trait test: listen error closed from dropped announcer
|
||||
///
|
||||
/// Verifies: Verifies listen error closed from dropped announcer
|
||||
/// Category: trait
|
||||
#[tokio::test]
|
||||
async fn test_listen_error_closed_from_dropped_announcer() {
|
||||
let (announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Drop announcer to trigger Closed error
|
||||
drop(announcer);
|
||||
|
||||
let result = listener.listen().await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(err, ListenError::Closed);
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
"announcer was dropped without announcing"
|
||||
);
|
||||
|
||||
// Verify Error trait methods
|
||||
assert!(err.source().is_none());
|
||||
}
|
||||
|
||||
/// Test: Trait test: listen error timeout from timeout blocking
|
||||
///
|
||||
/// Verifies: Verifies listen error timeout from timeout blocking
|
||||
/// Category: trait
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn test_listen_error_timeout_from_timeout_blocking() {
|
||||
use std::time::Duration;
|
||||
|
||||
let (_announcer, announcement) = Announcement::<i32>::new();
|
||||
let listener = announcement.listener();
|
||||
|
||||
// Try to listen with a very short timeout (will timeout)
|
||||
let result = listener.listen_timeout_blocking(Duration::from_millis(1));
|
||||
assert!(result.is_err());
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(err, ListenError::Timeout);
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
"timeout elapsed while waiting for announcement"
|
||||
);
|
||||
|
||||
// Verify Error trait methods
|
||||
assert!(err.source().is_none());
|
||||
}
|
||||
|
||||
/// Test: Trait test: announce error into inner preserves value
|
||||
///
|
||||
/// Verifies: Verifies announce error into inner preserves value
|
||||
/// Category: trait
|
||||
#[test]
|
||||
fn test_announce_error_into_inner_preserves_value() {
|
||||
let original_value = vec![1, 2, 3, 4, 5];
|
||||
let err = AnnounceError::AlreadyAnnounced(original_value.clone());
|
||||
|
||||
let recovered_value = err.into_inner();
|
||||
assert_eq!(recovered_value, original_value);
|
||||
}
|
||||
+2122
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user