#[cfg(feature = "trace")]
use std::sync::mpsc;
use std::time::Duration;
use super::{Buffer, Record, dump, enabled, wants};
#[cfg(feature = "trace")]
use super::counted;
fn record(a: u64) -> Record {
Record {
at: Duration::from_micros(a),
thread: 1,
target: "test",
event: "filler",
a,
b: 0,
}
}
fn held(buffer: &Buffer) -> Vec<u64> {
buffer.iter().map(|record| record.a).collect()
}
#[test]
fn a_buffer_below_its_capacity_keeps_everything_and_drops_nothing() {
let mut buffer = Buffer::with_capacity(4);
for a in 0..4 {
assert!(
!buffer.push(record(a), 4),
"a push that fits is not an eviction"
);
}
assert_eq!(held(&buffer), vec![0, 1, 2, 3]);
assert_eq!(buffer.dropped, 0);
assert!(
!buffer.announced,
"nothing has been lost, so nothing may be announced"
);
}
#[test]
fn a_full_buffer_evicts_the_oldest_and_keeps_the_newest() {
let mut buffer = Buffer::with_capacity(4);
for a in 0..7 {
buffer.push(record(a), 4);
}
assert_eq!(
held(&buffer),
vec![3, 4, 5, 6],
"the window must slide, not stop: a buffer that stopped recording when full would \
report the same length and lose the end of the run instead of the start"
);
assert_eq!(buffer.dropped, 3, "one drop per push past the capacity");
}
#[test]
fn the_window_stays_in_order_across_repeated_wraps() {
let mut buffer = Buffer::with_capacity(4);
for a in 0..4 {
buffer.push(record(a), 4);
}
assert_eq!(held(&buffer), vec![0, 1, 2, 3], "filled, not yet wrapped");
for a in 4..20 {
buffer.push(record(a), 4);
let oldest = a - 3;
assert_eq!(
held(&buffer),
(oldest..=a).collect::<Vec<_>>(),
"after pushing {a} the window must be {oldest}..={a}; the same records in a \
different order would read as a valid capture of a sequence that never happened"
);
assert_eq!(
buffer.records.len(),
4,
"the ring overwrites, so the storage cannot grow past the capacity it was given"
);
}
assert_eq!(buffer.dropped, 16, "one drop per push past the capacity");
}
#[test]
fn only_the_first_eviction_is_announced() {
let mut buffer = Buffer::with_capacity(2);
buffer.push(record(0), 2);
buffer.push(record(1), 2);
assert!(
buffer.push(record(2), 2),
"the first eviction is the one worth reporting"
);
for a in 3..10 {
assert!(
!buffer.push(record(a), 2),
"reporting every later eviction would put a formatted line on the traced path \
once per record, which is what this facility exists not to do"
);
}
assert_eq!(buffer.dropped, 8, "silence is not the same as not counting");
}
#[test]
fn a_cleared_buffer_will_announce_an_overflow_again() {
let mut buffer = Buffer::with_capacity(1);
buffer.push(record(0), 1);
assert!(buffer.push(record(1), 1));
buffer.clear();
assert!(held(&buffer).is_empty());
assert_eq!(buffer.dropped, 0);
buffer.push(record(2), 1);
assert!(
buffer.push(record(3), 1),
"a clear starts a new capture, and a new capture that overflows is as worth \
reporting as the first one was"
);
}
#[test]
fn a_zero_capacity_buffer_stores_nothing_rather_than_panicking() {
let mut buffer = Buffer::with_capacity(0);
assert!(
buffer.push(record(0), 0),
"the first loss is still announced"
);
assert!(!buffer.push(record(1), 0));
assert!(held(&buffer).is_empty());
assert_eq!(buffer.dropped, 2, "everything offered was dropped");
}
#[test]
fn a_build_without_the_feature_reports_nothing_and_says_so() {
if cfg!(feature = "trace") {
return;
}
assert!(!enabled(), "tracing cannot be on without the feature");
assert!(
!wants("anything"),
"no target is traced without the feature"
);
assert!(
dump().contains("without the `trace` feature"),
"a dump must say why it is empty rather than look like a clean run -- an empty trace and \
a trace that was never compiled in are very different findings"
);
}
#[test]
fn recording_is_inert_without_the_feature() {
if cfg!(feature = "trace") {
return;
}
crate::trace_record!("test", "inert", 1, 2);
assert!(!dump().contains("inert"));
}
#[test]
fn the_filter_narrows_to_the_targets_named() {
if !cfg!(feature = "trace") {
return;
}
for target in ["wait", "delivery", "something-else"] {
if wants(target) {
assert!(
enabled(),
"a wanted target implies the trace is enabled: {target}"
);
}
}
}
#[cfg(feature = "trace")]
#[test]
fn the_exception_observer_notes_a_first_chance_exception() {
crate::trace::in_a_trace_armed_child(
"trace::tests::the_exception_observer_notes_a_first_chance_exception",
"exception",
|| {
assert!(
wants("exception"),
"the child was launched with the trace armed for `exception` but `wants` \
disagrees, so nothing below would be observed"
);
const RAISES: usize = 64;
let before = counted("exception", "raised");
let text = c"windows-threadpool-sys exception observer probe";
for _ in 0..RAISES {
unsafe {
windows_sys::Win32::System::Diagnostics::Debug::OutputDebugStringA(
text.as_ptr().cast(),
)
};
if counted("exception", "raised") > before {
return;
}
}
panic!(
"the trace is narrowed to `exception` and {RAISES} first-chance exceptions have \
just been raised with none recorded, so either the handler is not installed or \
it is losing every record"
);
},
);
}
#[cfg(feature = "trace")]
const PROBE_BOUND: Duration = Duration::from_secs(5);
#[cfg(feature = "trace")]
#[test]
fn every_pool_object_records_its_creation_establishment_callbacks_and_teardown() {
let expected: [(&str, &[&str]); 6] = [
(
"wait",
&[
"created",
"armed",
"disarmed",
"trampoline-entered",
"trampoline-left",
"rearm-entered",
"rearm-left",
"suppress-and-disarm",
"drop-begin",
"drop-drained",
"drop-closed",
],
),
(
"work",
&[
"created",
"submitted",
"trampoline-entered",
"trampoline-left",
"drop-begin",
"drop-drained",
"drop-closed",
],
),
(
"timer",
&[
"created",
"armed",
"disarmed",
"trampoline-entered",
"trampoline-left",
"rearm-requested",
"rearm-entered",
"rearm-left",
"suppress-and-disarm",
"drop-begin",
"drop-drained",
"drop-closed",
],
),
(
"timer-periodic",
&[
"created",
"trampoline-entered",
"trampoline-left",
"drop-begin",
"drop-drained",
"drop-closed",
],
),
(
"syscall-enter",
&[
"CreateThreadpoolWait",
"SetThreadpoolWait",
"WaitForThreadpoolWaitCallbacks",
"CloseThreadpoolWait",
"CreateThreadpoolWork",
"SubmitThreadpoolWork",
"WaitForThreadpoolWorkCallbacks",
"CloseThreadpoolWork",
"CreateThreadpoolTimer",
"SetThreadpoolTimer",
"WaitForThreadpoolTimerCallbacks",
"CloseThreadpoolTimer",
],
),
(
"syscall-leave",
&[
"CreateThreadpoolWait",
"SetThreadpoolWait",
"WaitForThreadpoolWaitCallbacks",
"CloseThreadpoolWait",
"CreateThreadpoolWork",
"SubmitThreadpoolWork",
"WaitForThreadpoolWorkCallbacks",
"CloseThreadpoolWork",
"CreateThreadpoolTimer",
"SetThreadpoolTimer",
"WaitForThreadpoolTimerCallbacks",
"CloseThreadpoolTimer",
],
),
];
crate::trace::in_a_trace_armed_child(
"trace::tests::every_pool_object_records_its_creation_establishment_callbacks_and_teardown",
"*",
|| {
assert!(
expected.iter().all(|(target, _)| wants(target)),
"the child was launched with the trace armed for everything but `wants` \
disagrees, so nothing below would be observed"
);
exercise_a_wait();
exercise_a_work_item();
exercise_a_timer();
exercise_a_periodic_timer();
for (target, events) in expected {
for event in events {
assert!(
counted(target, event) > 0,
"`{target}` recorded no `{event}`; the trace is narrowed to it and the \
corresponding step has just run, so the call site is missing"
);
}
}
},
);
}
#[cfg(feature = "trace")]
fn exercise_a_wait() {
use crate::wait::{ThreadpoolWait, WaitableHandle};
let event = WaitableHandle::event(false, false).expect("create a probe event");
let (tx, rx) = mpsc::channel();
let tx = std::sync::Mutex::new(tx);
let wait = ThreadpoolWait::new(
event,
move |activation| {
activation.rearm(None);
if let Ok(tx) = tx.lock() {
let _ = tx.send(());
}
},
None,
)
.expect("create a probe wait");
wait.arm(None);
unsafe {
windows_sys::Win32::System::Threading::SetEvent(
std::os::windows::io::AsRawHandle::as_raw_handle(&wait.handle()),
)
};
rx.recv_timeout(PROBE_BOUND).expect("the wait callback ran");
wait.stop_and_drain();
}
#[cfg(feature = "trace")]
fn exercise_a_work_item() {
use crate::work::ThreadpoolWork;
let (tx, rx) = mpsc::channel();
let tx = std::sync::Mutex::new(tx);
let work = ThreadpoolWork::new(
move || {
if let Ok(tx) = tx.lock() {
let _ = tx.send(());
}
},
None,
)
.expect("create a probe work item");
work.submit();
rx.recv_timeout(PROBE_BOUND).expect("the work callback ran");
work.wait();
}
#[cfg(feature = "trace")]
fn exercise_a_timer() {
use crate::timer::ThreadpoolTimer;
let (tx, rx) = mpsc::channel();
let tx = std::sync::Mutex::new(tx);
let firings = std::sync::atomic::AtomicUsize::new(0);
let timer = ThreadpoolTimer::new(
move |firing| {
match firings.fetch_add(1, std::sync::atomic::Ordering::SeqCst) {
0 => firing.rearm_after(Duration::from_millis(1)),
1 => firing.rearm_at(std::time::SystemTime::now() + Duration::from_millis(1)),
_ => {
if let Ok(tx) = tx.lock() {
let _ = tx.send(());
}
}
}
},
None,
)
.expect("create a probe timer");
timer.set_after(Duration::from_millis(1));
rx.recv_timeout(PROBE_BOUND)
.expect("the timer fired, re-armed twice, and fired again");
timer.stop_and_drain();
}
#[cfg(feature = "trace")]
fn exercise_a_periodic_timer() {
use crate::timer::ThreadpoolPeriodicTimer;
let (tx, rx) = mpsc::channel();
let tx = std::sync::Mutex::new(tx);
let timer = ThreadpoolPeriodicTimer::new(
Duration::from_millis(1),
move |_| {
if let Ok(tx) = tx.lock() {
let _ = tx.send(());
}
},
None,
)
.expect("create a probe periodic timer");
timer.start();
rx.recv_timeout(PROBE_BOUND)
.expect("the periodic timer ticked");
timer.stop_and_drain();
}
#[cfg(feature = "trace")]
#[test]
fn the_trace_arms_itself_before_main() {
assert!(
super::armed_before_main(),
"the .CRT$XCU initialiser did not run, so the trace armed lazily instead -- recording \
would begin inside the first test rather than before any thread exists, which is the \
failure this arrangement exists to prevent"
);
}