use std::os::windows::io::AsRawHandle;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use windows_sys::Win32::System::Threading::{ResetEvent, SetEvent};
use crate::callback_env::CallbackEnviron;
use crate::pool::ThreadpoolPool;
use crate::wait::{ThreadpoolWait, WaitResult, WaitableHandle};
const ACTIVATION_TIMEOUT: Duration = Duration::from_secs(30);
fn event(manual_reset: bool) -> WaitableHandle {
WaitableHandle::event(manual_reset, false).expect("create an event")
}
fn signal(handle: std::os::windows::io::BorrowedHandle<'_>) {
let ok = unsafe { SetEvent(handle.as_raw_handle()) };
assert_ne!(
ok,
0,
"SetEvent failed: {}",
std::io::Error::last_os_error()
);
}
fn reset(handle: std::os::windows::io::BorrowedHandle<'_>) {
let ok = unsafe { ResetEvent(handle.as_raw_handle()) };
assert_ne!(
ok,
0,
"ResetEvent failed: {}",
std::io::Error::last_os_error()
);
}
struct Activations {
seen: Mutex<Vec<WaitResult>>,
arrived: Condvar,
}
impl Activations {
fn new() -> Arc<Self> {
Arc::new(Self {
seen: Mutex::new(Vec::new()),
arrived: Condvar::new(),
})
}
fn record(&self, result: WaitResult) {
let mut seen = self.seen.lock().expect("record an activation");
seen.push(result);
self.arrived.notify_all();
}
fn count(&self) -> usize {
self.seen.lock().expect("read activations").len()
}
fn results(&self) -> Vec<WaitResult> {
self.seen.lock().expect("read activations").clone()
}
fn wait_for(&self, target: usize) -> Vec<WaitResult> {
let seen = self.seen.lock().expect("await activations");
let (seen, timeout) = self
.arrived
.wait_timeout_while(seen, ACTIVATION_TIMEOUT, |seen| seen.len() < target)
.expect("await activations");
assert!(
!timeout.timed_out(),
"timed out waiting for {target} activation(s); saw {}",
seen.len()
);
seen.clone()
}
}
fn recording_wait(manual_reset: bool) -> (ThreadpoolWait, Arc<Activations>) {
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(manual_reset),
move |activation| recorder.record(activation.result()),
None,
)
.expect("create wait");
(wait, seen)
}
#[test]
fn new_wait_succeeds() {
assert!(ThreadpoolWait::new(event(true), |_| {}, None).is_ok());
}
#[test]
fn new_wait_with_env_succeeds() {
let mut env = CallbackEnviron::new();
assert!(ThreadpoolWait::new(event(true), |_| {}, Some(&mut env)).is_ok());
}
#[test]
fn drop_without_arming_is_clean() {
let _wait = ThreadpoolWait::new(event(true), |_| {}, None).expect("create wait");
}
#[test]
fn the_wait_exposes_its_handle() {
let (wait, _seen) = recording_wait(true);
signal(wait.handle());
}
#[test]
fn signalling_activates_the_callback() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
signal(wait.handle());
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn an_already_signalled_handle_activates_on_arming() {
let (wait, seen) = recording_wait(true);
signal(wait.handle());
wait.arm(None);
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn an_activation_reports_signalled() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
signal(wait.handle());
let results = seen.wait_for(1);
wait.wait();
assert!(results[0] == WaitResult::Signalled);
}
#[test]
fn one_arming_yields_one_activation() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
signal(wait.handle());
seen.wait_for(1);
wait.wait();
std::thread::sleep(Duration::from_millis(60));
assert_eq!(
seen.count(),
1,
"an activation consumes the arming; it must not repeat"
);
}
#[test]
fn a_timeout_activates_the_callback() {
let (wait, seen) = recording_wait(true);
wait.arm(Some(Duration::from_millis(20)));
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::TimedOut]);
}
#[test]
fn signalling_before_the_timeout_reports_signalled() {
let (wait, seen) = recording_wait(true);
wait.arm(Some(Duration::from_secs(30)));
signal(wait.handle());
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn a_zero_timeout_activates_promptly() {
let (wait, seen) = recording_wait(true);
wait.arm(Some(Duration::ZERO));
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::TimedOut]);
}
#[test]
fn a_rearming_callback_activates_repeatedly() {
const SIGNALS: usize = 10;
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(false),
move |activation| {
recorder.record(activation.result());
activation.rearm(None);
},
None,
)
.expect("create wait");
wait.arm(None);
for expected in 1..=SIGNALS {
signal(wait.handle());
seen.wait_for(expected);
}
wait.disarm();
wait.wait();
let results = seen.results();
assert!(results.len() >= SIGNALS);
assert!(
results.iter().all(|r| *r == WaitResult::Signalled),
"every activation should be a signal, got {results:?}"
);
}
#[test]
fn a_callback_that_does_not_rearm_stops_watching() {
let (wait, seen) = recording_wait(false);
wait.arm(None);
signal(wait.handle());
seen.wait_for(1);
wait.wait();
signal(wait.handle());
std::thread::sleep(Duration::from_millis(60));
assert_eq!(seen.count(), 1);
}
#[test]
fn rearming_with_a_timeout_activates_on_timeout() {
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let rearmed = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&rearmed);
let wait = ThreadpoolWait::new(
event(false),
move |activation| {
recorder.record(activation.result());
if counter.fetch_add(1, Ordering::SeqCst) < 2 {
activation.rearm(Some(Duration::from_millis(10)));
}
},
None,
)
.expect("create wait");
wait.arm(Some(Duration::from_millis(10)));
let results = seen.wait_for(3);
wait.disarm();
wait.wait();
assert!(
results.iter().all(|r| *r == WaitResult::TimedOut),
"expected timeouts, got {results:?}"
);
}
#[test]
fn arming_again_from_outside_replaces_the_previous_arming() {
let (wait, seen) = recording_wait(true);
wait.arm(Some(Duration::from_secs(30)));
wait.arm(Some(Duration::from_millis(20)));
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::TimedOut]);
}
#[test]
fn disarming_prevents_activation() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
wait.disarm();
signal(wait.handle());
std::thread::sleep(Duration::from_millis(60));
assert_eq!(seen.count(), 0, "a disarmed wait must not activate");
}
#[test]
fn disarming_an_idle_wait_is_a_no_op() {
let (wait, seen) = recording_wait(true);
wait.disarm();
wait.disarm();
assert_eq!(seen.count(), 0);
}
#[test]
fn a_wait_can_be_rearmed_after_disarming() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
wait.disarm();
reset(wait.handle());
wait.arm(None);
signal(wait.handle());
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
fn measure_overlap(manual_reset: bool, reset_before_rearm: bool) -> (usize, usize) {
let inside = Arc::new(std::sync::atomic::AtomicBool::new(false));
let overlaps = Arc::new(AtomicUsize::new(0));
let entries = Arc::new(AtomicUsize::new(0));
let in_callback = Arc::clone(&inside);
let violations = Arc::clone(&overlaps);
let counter = Arc::clone(&entries);
let wait = ThreadpoolWait::new(
WaitableHandle::event(manual_reset, true).expect("create a signalled event"),
move |activation| {
if in_callback.swap(true, Ordering::SeqCst) {
violations.fetch_add(1, Ordering::SeqCst);
}
counter.fetch_add(1, Ordering::SeqCst);
if reset_before_rearm {
reset(activation.handle());
}
activation.rearm(None);
std::thread::sleep(Duration::from_millis(10));
in_callback.store(false, Ordering::SeqCst);
},
None,
)
.expect("create wait");
wait.arm(None);
std::thread::sleep(Duration::from_millis(200));
wait.stop_and_drain();
(
entries.load(Ordering::SeqCst),
overlaps.load(Ordering::SeqCst),
)
}
#[test]
fn rearming_a_still_signalled_wait_overlaps_the_callback() {
let (entries, overlaps) = measure_overlap(true, false);
assert!(entries > 1, "the wait should have re-activated repeatedly");
assert!(
overlaps > 0,
"expected the callback to overlap itself ({entries} entries, {overlaps} overlapping)"
);
}
#[test]
fn rearming_an_auto_reset_wait_does_not_overlap() {
let (_entries, overlaps) = measure_overlap(false, false);
assert_eq!(overlaps, 0, "an auto-reset wait should not overlap");
}
#[test]
fn resetting_before_rearming_avoids_the_overlap() {
let (_entries, overlaps) = measure_overlap(true, true);
assert_eq!(
overlaps, 0,
"resetting before re-arming should stop the overlap"
);
}
fn always_rearming_wait() -> (ThreadpoolWait, Arc<Activations>) {
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
WaitableHandle::event(true, true).expect("create a signalled event"),
move |activation| {
recorder.record(activation.result());
std::thread::sleep(Duration::from_millis(60));
activation.rearm(None);
},
None,
)
.expect("create wait");
(wait, seen)
}
fn wait_until_activated(seen: &Activations) {
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while seen.count() == 0 {
assert!(
std::time::Instant::now() < deadline,
"the wait never activated"
);
std::thread::sleep(Duration::from_millis(1));
}
}
#[test]
fn stop_and_drain_quiesces_a_self_rearming_wait() {
let (wait, seen) = always_rearming_wait();
wait.arm(None);
wait_until_activated(&seen);
wait.stop_and_drain();
let settled = seen.count();
std::thread::sleep(Duration::from_millis(150));
assert_eq!(
seen.count(),
settled,
"the wait kept activating after stop_and_drain"
);
}
#[test]
fn a_wait_is_reusable_after_stop_and_drain() {
let (wait, seen) = always_rearming_wait();
wait.arm(None);
wait_until_activated(&seen);
wait.stop_and_drain();
let settled = seen.count();
wait.arm(None);
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while seen.count() == settled {
assert!(
std::time::Instant::now() < deadline,
"the wait never activated again after stop_and_drain"
);
std::thread::sleep(Duration::from_millis(1));
}
wait.stop_and_drain();
}
#[test]
fn concurrent_stop_and_drain_calls_all_quiesce_a_wait() {
let (wait, seen) = always_rearming_wait();
let wait = Arc::new(wait);
wait.arm(None);
wait_until_activated(&seen);
let callers: Vec<_> = (0..4)
.map(|_| {
let wait = Arc::clone(&wait);
std::thread::spawn(move || wait.stop_and_drain())
})
.collect();
for caller in callers {
caller.join().expect("stop_and_drain thread");
}
let settled = seen.count();
std::thread::sleep(Duration::from_millis(150));
assert_eq!(
seen.count(),
settled,
"the wait kept activating after concurrent stop_and_drain calls"
);
}
#[test]
fn stop_and_drain_on_an_idle_wait_is_a_no_op() {
let (wait, seen) = recording_wait(true);
wait.stop_and_drain();
assert_eq!(seen.count(), 0);
wait.arm(None);
signal(wait.handle());
seen.wait_for(1);
wait.stop_and_drain();
}
#[test]
fn a_manual_reset_event_is_a_valid_wait_target() {
assert!(WaitableHandle::event(true, false).is_ok());
}
#[test]
fn an_auto_reset_event_is_a_valid_wait_target() {
assert!(WaitableHandle::event(false, false).is_ok());
}
#[test]
fn an_initially_signalled_event_activates_on_arming() {
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let handle = WaitableHandle::event(true, true).expect("create a signalled event");
let wait = ThreadpoolWait::new(
handle,
move |activation| recorder.record(activation.result()),
None,
)
.expect("create wait");
wait.arm(None);
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn drop_waits_for_an_executing_callback() {
let done = Arc::new(AtomicUsize::new(0));
let flag = Arc::clone(&done);
let entered = Activations::new();
let started = Arc::clone(&entered);
let handle = event(true);
{
let wait = ThreadpoolWait::new(
handle,
move |activation| {
started.record(activation.result());
std::thread::sleep(Duration::from_millis(30));
flag.fetch_add(1, Ordering::SeqCst);
},
None,
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
entered.wait_for(1);
}
assert_eq!(
done.load(Ordering::SeqCst),
1,
"Drop returned while a callback was still executing"
);
}
#[test]
fn drop_of_a_rearming_wait_terminates() {
let started = std::time::Instant::now();
{
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(false),
move |activation| {
recorder.record(activation.result());
activation.rearm(Some(Duration::from_millis(1)));
},
None,
)
.expect("create wait");
wait.arm(Some(Duration::from_millis(1)));
seen.wait_for(3);
}
assert!(
started.elapsed() < Duration::from_secs(10),
"dropping a self-rearming wait appears to have hung"
);
}
#[test]
fn rearming_outside_teardown_is_honoured() {
let outcomes = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&outcomes);
let started = Activations::new();
let entered = Arc::clone(&started);
let first = Arc::new(AtomicUsize::new(0));
let selector = Arc::clone(&first);
let wait = ThreadpoolWait::new(
event(true),
move |activation| {
if selector.fetch_add(1, Ordering::SeqCst) == 0 {
let armed = activation.rearm_reporting(None);
recorder.lock().unwrap().push(armed);
}
entered.record(activation.result());
},
None,
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
started.wait_for(2);
wait.disarm();
wait.wait();
assert_eq!(*outcomes.lock().unwrap(), vec![true]);
}
#[test]
fn rearming_during_teardown_is_suppressed() {
let outcomes = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&outcomes);
let started = Activations::new();
let entered = Arc::clone(&started);
let elapsed = std::time::Instant::now();
{
let wait = ThreadpoolWait::new(
event(true),
move |activation| {
entered.record(activation.result());
std::thread::sleep(Duration::from_millis(200));
let armed = activation.rearm_reporting(None);
recorder.lock().unwrap().push(armed);
},
None,
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
started.wait_for(1);
}
assert!(
elapsed.elapsed() < Duration::from_secs(10),
"teardown appears to have hung"
);
assert_eq!(
*outcomes.lock().unwrap(),
vec![false],
"the re-arm should have been suppressed by teardown"
);
}
#[test]
fn drop_while_armed_but_not_signalled_is_clean() {
let (wait, seen) = recording_wait(true);
wait.arm(None);
drop(wait);
assert_eq!(seen.count(), 0);
}
#[test]
fn the_callback_may_own_heap_state() {
let data = Arc::new(vec![10_u64, 20, 30]);
let sum = Arc::new(AtomicUsize::new(0));
let total = Arc::clone(&sum);
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(true),
move |activation| {
total.fetch_add(data.iter().sum::<u64>() as usize, Ordering::SeqCst);
recorder.record(activation.result());
},
None,
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
seen.wait_for(1);
wait.wait();
assert_eq!(sum.load(Ordering::SeqCst), 60);
}
#[test]
fn a_wait_runs_on_a_private_pool() {
let pool = ThreadpoolPool::new().expect("create pool");
let mut env = CallbackEnviron::new();
env.set_pool(&pool);
let seen = Activations::new();
let recorder = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(true),
move |activation| recorder.record(activation.result()),
Some(&mut env),
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn a_wait_is_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ThreadpoolWait>();
assert_sync::<ThreadpoolWait>();
}
#[test]
fn a_wait_can_be_armed_from_another_thread() {
let (wait, seen) = recording_wait(true);
std::thread::scope(|scope| {
let wait = &wait;
scope.spawn(move || {
wait.arm(None);
signal(wait.handle());
});
});
let results = seen.wait_for(1);
wait.wait();
assert_eq!(results, vec![WaitResult::Signalled]);
}
#[test]
fn wait_result_maps_the_documented_values() {
let (wait, seen) = recording_wait(true);
wait.arm(Some(Duration::from_millis(10)));
let timed_out = seen.wait_for(1);
wait.wait();
assert_eq!(timed_out[0], WaitResult::TimedOut);
assert!(!matches!(timed_out[0], WaitResult::Other(_)));
reset(wait.handle());
wait.arm(None);
signal(wait.handle());
let signalled = seen.wait_for(2);
wait.wait();
assert_eq!(signalled[1], WaitResult::Signalled);
}
#[test]
fn is_signalled_agrees_with_the_result() {
let flags = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&flags);
let seen = Activations::new();
let counter = Arc::clone(&seen);
let wait = ThreadpoolWait::new(
event(true),
move |activation| {
recorder
.lock()
.expect("record")
.push((activation.is_signalled(), activation.result()));
counter.record(activation.result());
},
None,
)
.expect("create wait");
wait.arm(Some(Duration::from_millis(10)));
seen.wait_for(1);
wait.wait();
let recorded = flags.lock().expect("read").clone();
assert_eq!(recorded[0], (false, WaitResult::TimedOut));
}
unsafe fn custom_event(close: crate::wait::WaitCloseFn) -> WaitableHandle {
let raw = unsafe {
windows_sys::Win32::System::Threading::CreateEventW(
std::ptr::null(),
1, 0, std::ptr::null(),
)
};
assert!(!raw.is_null(), "CreateEventW failed");
unsafe { WaitableHandle::assume_waitable_with(raw, close) }
}
#[test]
fn a_custom_closer_runs_exactly_once_on_drop() {
static CLOSES: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn close(handle: windows_sys::Win32::Foundation::HANDLE) -> i32 {
CLOSES.fetch_add(1, Ordering::SeqCst);
unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) }
}
let handle = unsafe { custom_event(close) };
let wait = ThreadpoolWait::new(handle, |_| {}, None).expect("create wait");
assert_eq!(CLOSES.load(Ordering::SeqCst), 0, "not closed while alive");
drop(wait);
assert_eq!(
CLOSES.load(Ordering::SeqCst),
1,
"the custom closer must run exactly once"
);
}
#[test]
fn a_custom_closer_runs_only_after_the_wait_is_drained() {
static CLOSES: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn close(handle: windows_sys::Win32::Foundation::HANDLE) -> i32 {
CLOSES.fetch_add(1, Ordering::SeqCst);
unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) }
}
let started = Activations::new();
let entered = Arc::clone(&started);
let seen_at_exit = Arc::new(AtomicUsize::new(usize::MAX));
let recorder = Arc::clone(&seen_at_exit);
let handle = unsafe { custom_event(close) };
let wait = ThreadpoolWait::new(
handle,
move |activation| {
entered.record(activation.result());
std::thread::sleep(Duration::from_millis(100));
recorder.store(CLOSES.load(Ordering::SeqCst), Ordering::SeqCst);
},
None,
)
.expect("create wait");
wait.arm(None);
signal(wait.handle());
started.wait_for(1);
let entered_drop = std::time::Instant::now();
drop(wait);
let blocked_for = entered_drop.elapsed();
assert!(
blocked_for >= Duration::from_millis(50),
"drop returned in {blocked_for:?}, so it did not drain a running callback"
);
assert_eq!(
seen_at_exit.load(Ordering::SeqCst),
0,
"the handle was closed while a callback was still executing"
);
assert_eq!(
CLOSES.load(Ordering::SeqCst),
1,
"the custom closer must run exactly once, after the drain"
);
}
#[test]
fn the_default_path_still_closes_with_close_handle() {
let wait = ThreadpoolWait::new(event(true), |_| {}, None).expect("create wait");
wait.arm(None);
signal(wait.handle());
wait.wait();
drop(wait);
}
#[test]
fn into_handle_returns_the_handle_for_the_default_path() {
let handle = event(true);
assert!(
handle.into_handle().is_ok(),
"an OwnedHandle-backed target must hand its handle back"
);
}
#[test]
fn into_handle_declines_a_custom_close_target() {
static CLOSES: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn close(handle: windows_sys::Win32::Foundation::HANDLE) -> i32 {
CLOSES.fetch_add(1, Ordering::SeqCst);
unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) }
}
let handle = unsafe { custom_event(close) };
let returned = handle
.into_handle()
.expect_err("a custom-close target has no correct OwnedHandle to give");
assert_eq!(
CLOSES.load(Ordering::SeqCst),
0,
"declining must not close the handle"
);
drop(returned);
assert_eq!(
CLOSES.load(Ordering::SeqCst),
1,
"the returned wrapper still owns the handle and closes it once"
);
}