#![cfg(all(windows, feature = "trace"))]
use std::process::{Command, Stdio};
use std::time::Duration;
use windows_threadpool_sys::timer::{ThreadpoolPeriodicTimer, ThreadpoolTimer};
use windows_threadpool_sys::wait::{ThreadpoolWait, WaitableHandle};
use windows_threadpool_sys::work::ThreadpoolWork;
const CHILD_VAR: &str = "WTPS_OBLIGATION_REPORT_CHILD";
const TRACE_FILTER: &str = "wait,timer,timer-periodic,work";
const CHILD_TIMEOUT: Duration = Duration::from_secs(60);
const ASSERTION_FAILED_EXIT_CODE: i32 = 1;
const FILTER_MISSING_EXIT_CODE: i32 = 111;
const TAG: &str = "drop-obligation-owed";
fn counted(target: &str, event: &str) -> usize {
windows_threadpool_sys::trace::dump()
.lines()
.filter(|line| {
let mut fields = line.split_whitespace();
fields.next();
fields.next();
fields.next() == Some(target) && fields.next() == Some(event)
})
.count()
}
fn panicked(body: impl FnOnce()) -> bool {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body));
std::panic::set_hook(previous);
outcome.is_err()
}
fn arm_and_discharge(target: &str) {
match target {
"wait" => {
let event = WaitableHandle::event(true, false).expect("event");
let wait = ThreadpoolWait::new(event, |_| {}, None).expect("wait");
wait.arm(None);
wait.stop_and_drain();
}
"timer" => {
let timer = ThreadpoolTimer::new(|_| {}, None).expect("timer");
timer.set_after(Duration::from_secs(60));
timer.stop_and_drain();
}
"timer-periodic" => {
let timer = ThreadpoolPeriodicTimer::new(Duration::from_secs(60), |_| {}, None)
.expect("periodic timer");
timer.start();
timer.stop_and_drain();
}
"work" => {
let work = ThreadpoolWork::new(|| {}, None).expect("work");
work.submit();
work.wait();
}
other => unreachable!("unexpected target {other}"),
}
}
fn arm_and_abandon(target: &str) {
match target {
"wait" => {
let event = WaitableHandle::event(true, false).expect("event");
let wait = ThreadpoolWait::new(event, |_| {}, None).expect("wait");
wait.arm(None);
}
"timer" => {
let timer = ThreadpoolTimer::new(|_| {}, None).expect("timer");
timer.set_after(Duration::from_secs(60));
}
"timer-periodic" => {
let timer = ThreadpoolPeriodicTimer::new(Duration::from_secs(60), |_| {}, None)
.expect("periodic timer");
timer.start();
}
"work" => {
let work = ThreadpoolWork::new(|| {}, None).expect("work");
work.submit();
}
other => unreachable!("unexpected target {other}"),
}
}
fn check_every_target() {
let targets = ["wait", "timer", "timer-periodic", "work"];
for target in targets {
assert!(
windows_threadpool_sys::trace::wants(target),
"the child was launched with {CHILD_VAR} set but the trace is not \
armed for `{target}`"
);
let before = counted(target, TAG);
arm_and_discharge(target);
assert_eq!(
counted(target, TAG),
before,
"`{target}` reported an obligation the caller had already discharged"
);
let panicked_on_drop = panicked(|| arm_and_abandon(target));
if cfg!(feature = "fail-fast") {
assert!(
panicked_on_drop,
"`{target}` was dropped still owing a drain under `fail-fast`, \
and did not panic"
);
} else {
assert!(
!panicked_on_drop,
"`{target}` panicked on drop without the `fail-fast` feature, \
which must only report"
);
}
assert_eq!(
counted(target, TAG),
before + 1,
"`{target}` did not report an obligation the caller left to Drop"
);
}
}
fn dispatch_if_child() {
if std::env::var(CHILD_VAR).is_err() {
return;
}
if !windows_threadpool_sys::trace::wants("wait") {
std::process::exit(FILTER_MISSING_EXIT_CODE);
}
match std::panic::catch_unwind(check_every_target) {
Ok(()) => std::process::exit(0),
Err(_) => std::process::exit(ASSERTION_FAILED_EXIT_CODE),
}
}
#[test]
fn drop_reports_only_when_the_caller_left_the_drain_to_it() {
dispatch_if_child();
let exe = std::env::current_exe().expect("locate the test binary");
let mut child = Command::new(exe)
.env(CHILD_VAR, "1")
.env("WINDOWS_THREADPOOL_TRACE", TRACE_FILTER)
.env("RUST_TEST_THREADS", "1")
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn the child");
let deadline = std::time::Instant::now() + CHILD_TIMEOUT;
let status = loop {
if let Some(status) = child.try_wait().expect("poll the child") {
break status;
}
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("the child did not finish within {CHILD_TIMEOUT:?}");
}
std::thread::sleep(Duration::from_millis(20));
};
assert_ne!(
status.code(),
Some(FILTER_MISSING_EXIT_CODE),
"the child did not receive {TRACE_FILTER:?} through WINDOWS_THREADPOOL_TRACE, \
so it observed nothing -- the measurement did not run"
);
assert!(
status.success(),
"the child reported a failure of the obligation contract (exit {:?}); \
its output is above",
status.code()
);
}