CI / Test (beta, Linux) (push) Canceled after 0s
CI / Test (nightly, Linux) (push) Canceled after 0s
CI / Test (stable, Linux) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / MSRV (push) Canceled after 0s
CI / Documentation (push) Canceled after 0s
CI / Coverage (push) Canceled after 0s
CI / Security Audit (push) Canceled after 0s
CI / Fuzz Testing (push) Canceled after 0s
CI / Examples (push) Canceled after 0s
CI / Benchmarks (push) Canceled after 0s
215 lines
6.6 KiB
Rust
215 lines
6.6 KiB
Rust
//! 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(all(feature = "std", not(target_os = "macos")))]
|
|
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")]
|
|
#[cfg(not(target_os = "macos"))]
|
|
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));
|
|
}
|