use super::*;
#[test]
fn warn_fires_on_the_first_event_then_waits_out_the_window() {
let interval = Duration::from_mins(5);
assert!(should_warn_now(0, 0, interval));
assert!(should_warn_now(1_000_000, 0, interval));
let last = 1_000_000;
assert!(!should_warn_now(last, last, interval));
assert!(!should_warn_now(last + 299_999, last, interval));
assert!(should_warn_now(last + 300_000, last, interval));
assert!(should_warn_now(last + 600_000, last, interval));
}
#[test]
fn warn_suppresses_when_now_reads_behind_the_last_line() {
let interval = Duration::from_mins(5);
assert!(!should_warn_now(500, 1_000_000, interval));
}
#[test]
fn racing_callers_admit_one_line_and_count_every_event() {
const THREADS: usize = 8;
const EVENTS_PER_THREAD: u64 = 500;
let throttle = WarnThrottle::new(Duration::from_mins(5));
let admitted = std::sync::atomic::AtomicU64::new(0);
let reported = std::sync::atomic::AtomicU64::new(0);
let start = std::sync::Barrier::new(THREADS);
std::thread::scope(|scope| {
for _ in 0..THREADS {
scope.spawn(|| {
start.wait();
for _ in 0..EVENTS_PER_THREAD {
if let Some(suppressed) = throttle.admit() {
admitted.fetch_add(1, Ordering::Relaxed);
reported.fetch_add(suppressed, Ordering::Relaxed);
}
}
});
}
});
assert_eq!(admitted.load(Ordering::Relaxed), 1, "one line per window");
throttle.force_open();
let winner = reported.load(Ordering::Relaxed);
let expected = u64::try_from(THREADS)
.ok()
.map(|threads| threads * EVENTS_PER_THREAD - 1);
assert_eq!(
throttle.admit().map(|successor| winner + successor),
expected,
"the winner's count plus its successor's covers every swallowed event"
);
}
#[test]
fn racing_first_events_never_both_warn() {
use std::sync::atomic::AtomicBool;
const THREADS: usize = 8;
const ROUNDS: usize = 100;
for round in 0..ROUNDS {
let throttle = WarnThrottle::new(Duration::from_mins(5));
let admitted = std::sync::atomic::AtomicU64::new(0);
let ready = std::sync::atomic::AtomicUsize::new(0);
let go = AtomicBool::new(false);
std::thread::scope(|scope| {
for _ in 0..THREADS {
scope.spawn(|| {
ready.fetch_add(1, Ordering::Relaxed);
while !go.load(Ordering::Relaxed) {
std::hint::spin_loop();
}
if throttle.admit().is_some() {
admitted.fetch_add(1, Ordering::Relaxed);
}
});
}
while ready.load(Ordering::Relaxed) < THREADS {
std::hint::spin_loop();
}
go.store(true, Ordering::Relaxed);
});
assert_eq!(
admitted.load(Ordering::Relaxed),
1,
"round {round}: exactly one racing first event warns"
);
}
}
#[test]
fn admit_reports_exactly_what_it_swallowed() {
let throttle = WarnThrottle::new(Duration::from_mins(5));
assert_eq!(throttle.admit(), Some(0));
assert_eq!(throttle.admit(), None);
assert_eq!(throttle.admit(), None);
throttle.force_open();
assert_eq!(
throttle.admit(),
Some(2),
"the line must report the two it swallowed, not counting itself"
);
assert_eq!(throttle.admit(), None);
throttle.force_open();
assert_eq!(throttle.admit(), Some(1));
}
#[test]
fn separate_throttles_keep_separate_windows() {
let a = WarnThrottle::new(Duration::from_mins(5));
let b = WarnThrottle::new(Duration::from_mins(5));
assert_eq!(a.admit(), Some(0));
assert_eq!(a.admit(), None);
assert_eq!(
b.admit(),
Some(0),
"b's first event warns despite a's open window"
);
}