use super::*;
use std::sync::mpsc;
use std::time::Duration;
const NEVER: Duration = Duration::from_secs(3600);
const SOON: Duration = Duration::from_secs(5);
const QUIET: Duration = Duration::from_millis(50);
fn spawn_loop(first: Duration, interval: Duration) -> (mpsc::Sender<()>, mpsc::Receiver<()>) {
let (signal_tx, signal_rx) = mpsc::channel::<()>();
let (tick_tx, tick_rx) = mpsc::channel::<()>();
std::thread::spawn(move || {
run_polling_loop(&signal_rx, first, interval, || {
let _ = tick_tx.send(());
});
});
(signal_tx, tick_rx)
}
#[test]
fn no_cache_fetches_now() {
assert_eq!(
first_delay(None, 60_000, Duration::from_secs(300)),
Duration::ZERO,
"an absent or unreadable cache has nothing to wait on"
);
}
#[test]
fn a_fresh_stamp_waits_out_the_rest_of_the_interval() {
assert_eq!(
first_delay(Some(60_000), 70_000, Duration::from_secs(300)),
Duration::from_secs(290),
"10s into a 300s cadence leaves 290s of cached data"
);
}
#[test]
fn a_stamp_exactly_at_the_interval_fetches_now() {
assert_eq!(
first_delay(Some(60_000), 360_000, Duration::from_secs(300)),
Duration::ZERO,
"the boundary belongs to the fetch side — the cache is spent"
);
}
#[test]
fn an_expired_stamp_fetches_now() {
assert_eq!(
first_delay(
Some(60_000),
60_000 + 24 * 60 * 60 * 1000,
Duration::from_secs(300)
),
Duration::ZERO,
"an age past the cadence must not underflow into a wait"
);
}
#[test]
fn a_future_stamp_fetches_now() {
assert_eq!(
first_delay(Some(360_000), 60_000, Duration::from_secs(300)),
Duration::ZERO,
"a stamp in the future is not trustworthy freshness"
);
}
#[test]
fn a_zero_first_delay_ticks_immediately() {
let (signal_tx, tick_rx) = spawn_loop(Duration::ZERO, NEVER);
tick_rx
.recv_timeout(SOON)
.expect("zero delay must tick without a signal and without waiting the cadence");
assert_eq!(
tick_rx.recv_timeout(QUIET),
Err(mpsc::RecvTimeoutError::Timeout),
"the second tick waits the full interval"
);
drop(signal_tx);
}
#[test]
fn a_pending_first_delay_holds_the_tick_until_a_signal() {
let (signal_tx, tick_rx) = spawn_loop(NEVER, NEVER);
assert_eq!(
tick_rx.recv_timeout(QUIET),
Err(mpsc::RecvTimeoutError::Timeout),
"a still-fresh cache must not be re-fetched at startup"
);
signal_tx.send(()).expect("loop is listening");
tick_rx
.recv_timeout(SOON)
.expect("a manual refresh during the first delay must tick at once");
drop(signal_tx);
}
#[test]
fn signals_queued_during_the_first_delay_coalesce_into_one_tick() {
let (signal_tx, signal_rx) = mpsc::channel::<()>();
let (tick_tx, tick_rx) = mpsc::channel::<()>();
for _ in 0..3 {
signal_tx.send(()).expect("buffered channel");
}
std::thread::spawn(move || {
run_polling_loop(&signal_rx, NEVER, NEVER, || {
let _ = tick_tx.send(());
});
});
tick_rx.recv_timeout(SOON).expect("first tick");
assert_eq!(
tick_rx.recv_timeout(QUIET),
Err(mpsc::RecvTimeoutError::Timeout),
"three queued signals must collapse into a single fetch"
);
drop(signal_tx);
}
#[test]
fn a_disconnect_during_the_first_delay_ends_the_loop_without_ticking() {
let (signal_tx, signal_rx) = mpsc::channel::<()>();
drop(signal_tx);
let (done_tx, done_rx) = mpsc::channel::<usize>();
std::thread::spawn(move || {
let mut ticks = 0usize;
run_polling_loop(&signal_rx, NEVER, NEVER, || ticks += 1);
let _ = done_tx.send(ticks);
});
let ticks = done_rx
.recv_timeout(SOON)
.expect("a disconnected channel must end the loop, not wait out the first delay");
assert_eq!(ticks, 0, "a disconnected channel must not trigger a fetch");
}