use std::time::{Duration, Instant};
use std::{sync::Arc, thread};
use rsignals::sync::{
create, Disconnected, NoReceivers, Signaler, TryWaitError, WaitTimeoutError, Waiter,
};
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: u64) -> u64 {
self.next_u64() % n.max(1)
}
}
#[test]
fn fuzz_state_machine_against_model() {
for seed in 0..2000u64 {
let mut rng = Rng::new(seed);
let (tx0, rx0) = create();
let mut sigs: Vec<Signaler> = vec![tx0];
let mut waiters: Vec<Waiter> = vec![rx0];
let mut fired = false;
let mut disconnected = false;
for _step in 0..40 {
match rng.below(6) {
0 => {
if let Some(s) = sigs.first() {
sigs.push(s.clone());
}
}
1 => {
if !sigs.is_empty() {
let i = rng.below(sigs.len() as u64) as usize;
sigs.remove(i);
if sigs.is_empty() && !fired {
disconnected = true;
}
}
}
2 => {
if let Some(w) = waiters.first() {
waiters.push(w.clone());
}
}
3 => {
if !waiters.is_empty() {
let i = rng.below(waiters.len() as u64) as usize;
waiters.remove(i);
}
}
4 => {
if let Some(s) = sigs.first() {
let expected = if fired {
Ok(false)
} else if waiters.is_empty() {
Err(NoReceivers)
} else {
fired = true;
Ok(true)
};
assert_eq!(s.signal(), expected, "seed {seed}: signal outcome");
}
}
_ => { }
}
assert!(!(fired && disconnected), "seed {seed}: model invariant");
if let Some(s) = sigs.first() {
assert_eq!(s.is_signaled(), fired, "seed {seed}: signaler.is_signaled");
assert_eq!(
s.signaler_count(),
sigs.len(),
"seed {seed}: signaler_count"
);
assert_eq!(s.waiter_count(), waiters.len(), "seed {seed}: waiter_count");
}
if let Some(w) = waiters.first() {
assert_eq!(w.is_signaled(), fired, "seed {seed}: waiter.is_signaled");
assert_eq!(
w.is_disconnected(),
disconnected,
"seed {seed}: is_disconnected"
);
let expected = if fired {
Ok(())
} else if disconnected {
Err(TryWaitError::Disconnected)
} else {
Err(TryWaitError::Pending)
};
assert_eq!(w.try_wait(), expected, "seed {seed}: try_wait");
assert_eq!(
w.signaler_count(),
sigs.len(),
"seed {seed}: w.signaler_count"
);
assert_eq!(
w.waiter_count(),
waiters.len(),
"seed {seed}: w.waiter_count"
);
}
}
}
}
#[test]
fn fuzz_wait_timeout_scenarios() {
for seed in 0..300u64 {
let mut rng = Rng::new(seed ^ 0xA5A5_A5A5);
let (tx, rx) = create();
let timeout = Duration::from_millis(rng.below(25));
match rng.below(3) {
0 => {
assert_eq!(tx.signal(), Ok(true));
assert_eq!(rx.wait_timeout(timeout), Ok(()), "seed {seed}: fired-first");
}
1 => {
drop(tx);
assert_eq!(
rx.wait_timeout(timeout),
Err(WaitTimeoutError::Disconnected),
"seed {seed}: disconnected-first"
);
}
_ => {
let delay = Duration::from_millis(rng.below(25));
let txc = tx.clone();
let h = thread::spawn(move || {
thread::sleep(delay);
let _ = txc.signal();
});
let r = rx.wait_timeout(timeout);
assert!(
matches!(r, Ok(()) | Err(WaitTimeoutError::Timeout)),
"seed {seed}: race gave {r:?}"
);
h.join().unwrap();
assert_eq!(rx.wait(), Ok(()), "seed {seed}: fire eventually observed");
drop(tx);
}
}
}
}
#[test]
fn fuzz_wait_timeout_never_fires() {
for seed in 0..200u64 {
let mut rng = Rng::new(seed ^ 0x1234_5678);
let (tx, rx) = create();
let timeout = Duration::from_millis(5 + rng.below(15));
let start = Instant::now();
assert_eq!(
rx.wait_timeout(timeout),
Err(WaitTimeoutError::Timeout),
"seed {seed}"
);
assert!(
start.elapsed() + Duration::from_millis(2) >= timeout,
"seed {seed}: returned before the deadline"
);
drop(tx); }
}
#[test]
fn fuzz_many_waiters_random_timeouts_all_released() {
for seed in 0..60u64 {
let mut rng = Rng::new(seed ^ 0xDEAD_BEEF);
let (tx, rx) = create();
let handles: Vec<_> = (0..12)
.map(|_| {
let timeout = Duration::from_secs(5 + rng.below(3));
let rx = rx.clone();
thread::spawn(move || rx.wait_timeout(timeout))
})
.collect();
thread::sleep(Duration::from_millis(10));
assert_eq!(tx.signal(), Ok(true));
for h in handles {
assert_eq!(
h.join().unwrap(),
Ok(()),
"seed {seed}: every waiter released"
);
}
}
}
#[test]
fn zero_timeout_snapshots_state() {
let (tx, rx) = create();
let start = Instant::now();
assert_eq!(
rx.wait_timeout(Duration::ZERO),
Err(WaitTimeoutError::Timeout)
);
assert!(
start.elapsed() < Duration::from_millis(50),
"zero timeout must not block"
);
assert_eq!(tx.signal(), Ok(true));
assert_eq!(rx.wait_timeout(Duration::ZERO), Ok(()));
}
#[test]
fn zero_timeout_after_disconnect() {
let (tx, rx) = create();
drop(tx);
assert_eq!(
rx.wait_timeout(Duration::ZERO),
Err(WaitTimeoutError::Disconnected)
);
}
#[test]
fn try_wait_monotonic_to_fired() {
let (tx, rx) = create();
for _ in 0..5 {
assert_eq!(rx.try_wait(), Err(TryWaitError::Pending));
}
assert_eq!(tx.signal(), Ok(true));
for _ in 0..5 {
assert_eq!(rx.try_wait(), Ok(()));
}
}
#[test]
fn try_wait_monotonic_to_disconnected() {
let (tx, rx) = create();
assert_eq!(rx.try_wait(), Err(TryWaitError::Pending));
drop(tx);
for _ in 0..5 {
assert_eq!(rx.try_wait(), Err(TryWaitError::Disconnected));
}
}
#[test]
fn huge_timeout_returns_promptly_when_fired() {
let (tx, rx) = create();
assert_eq!(tx.signal(), Ok(true));
let start = Instant::now();
assert_eq!(rx.wait_timeout(Duration::from_secs(3600)), Ok(()));
assert!(start.elapsed() < Duration::from_millis(50));
}
#[test]
fn wait_timeout_wakes_before_deadline() {
let (tx, rx) = create();
let h = thread::spawn(move || {
thread::sleep(Duration::from_millis(30));
assert_eq!(tx.signal(), Ok(true));
});
let start = Instant::now();
assert_eq!(rx.wait_timeout(Duration::from_secs(10)), Ok(()));
assert!(
start.elapsed() < Duration::from_secs(1),
"should wake on the signal, not wait the full 10s"
);
h.join().unwrap();
}
#[test]
fn clone_after_fire_observes_fired() {
let (tx, rx) = create();
assert_eq!(tx.signal(), Ok(true));
let rx2 = rx.clone();
assert_eq!(rx2.try_wait(), Ok(()));
assert_eq!(rx2.wait(), Ok(()));
assert_eq!(rx2.wait_timeout(Duration::ZERO), Ok(()));
}
#[test]
fn clone_after_disconnect_observes_disconnected() {
let (tx, rx) = create();
drop(tx);
let rx2 = rx.clone();
assert!(rx2.is_disconnected());
assert_eq!(rx2.wait(), Err(Disconnected));
assert_eq!(rx2.try_wait(), Err(TryWaitError::Disconnected));
}
#[test]
fn concurrent_try_wait_around_fire() {
let (tx, rx) = create();
let rx = Arc::new(rx);
let start = Instant::now();
let handles: Vec<_> = (0..8)
.map(|_| {
let rx = rx.clone();
thread::spawn(move || loop {
match rx.try_wait() {
Ok(()) => break, Err(TryWaitError::Pending) => {}
Err(TryWaitError::Disconnected) => panic!("never disconnected here"),
}
assert!(
start.elapsed() < Duration::from_secs(5),
"fire never observed"
);
})
})
.collect();
thread::sleep(Duration::from_millis(2));
assert_eq!(tx.signal(), Ok(true));
for h in handles {
h.join().unwrap(); }
assert_eq!(rx.try_wait(), Ok(()));
}