use rash::backoff::Backoff;
use rash::supervise::{Death, Reason, Verdict, classify};
use std::time::Duration;
const GATE: Duration = Duration::from_secs(30);
const NO_GATE: Duration = Duration::ZERO;
const QUICK: Duration = Duration::from_secs(2);
const SETTLED: Duration = Duration::from_secs(600);
#[test]
fn a_killed_child_is_always_restarted() {
for sig in [libc::SIGTERM, libc::SIGKILL, libc::SIGINT, libc::SIGHUP] {
assert_eq!(
classify(Death::Signal(sig), 1, QUICK, GATE),
(Verdict::Restart, Reason::Signalled),
"signal {sig}"
);
}
}
#[test]
fn the_starting_gate_catches_a_first_session_that_dies_at_once() {
for code in [0, 1, 2, 42, 255] {
assert_eq!(
classify(Death::Exit(code), 1, QUICK, GATE),
(Verdict::ExitErr, Reason::PrematureExit),
"code {code}"
);
}
assert_eq!(
classify(Death::Exit(255), 1, SETTLED, GATE),
(Verdict::Restart, Reason::ConnectionLost)
);
assert_eq!(
classify(Death::Exit(255), 2, QUICK, GATE),
(Verdict::Restart, Reason::ConnectionLost)
);
assert_eq!(
classify(Death::Exit(255), 1, QUICK, NO_GATE),
(Verdict::Restart, Reason::ConnectionLost)
);
}
#[test]
fn exit_status_table() {
assert_eq!(
classify(Death::Exit(255), 3, SETTLED, GATE),
(Verdict::Restart, Reason::ConnectionLost)
);
assert_eq!(
classify(Death::Exit(0), 3, SETTLED, GATE),
(Verdict::ExitOk, Reason::CleanExit)
);
for code in [1, 2] {
assert_eq!(
classify(Death::Exit(code), 2, SETTLED, GATE),
(Verdict::Restart, Reason::ConnectionLost),
"code {code} on a restart"
);
}
assert_eq!(
classify(Death::Exit(1), 1, SETTLED, GATE),
(Verdict::ExitErr, Reason::Failed)
);
for code in [3, 42, 127] {
assert_eq!(
classify(Death::Exit(code), 5, SETTLED, GATE),
(Verdict::ExitErr, Reason::Failed),
"code {code}"
);
}
}
#[test]
fn gate_boundary_is_inclusive() {
assert_eq!(
classify(Death::Exit(1), 1, GATE, GATE),
(Verdict::ExitErr, Reason::PrematureExit)
);
assert_eq!(
classify(Death::Exit(1), 1, GATE + Duration::from_secs(1), GATE),
(Verdict::ExitErr, Reason::Failed)
);
}
#[test]
fn backoff_curve_matches_the_documented_table() {
let poll = Duration::from_secs(600);
let mut b = Backoff::default();
for n in 1..=5 {
assert_eq!(
b.next_delay(Duration::ZERO, poll),
Duration::ZERO,
"try {n} should not sleep"
);
assert_eq!(b.tries(), n);
}
for (tries, expected) in [(6, 2), (7, 8), (8, 18), (9, 32), (10, 50)] {
let d = b.next_delay(Duration::ZERO, poll);
assert_eq!(b.tries(), tries);
assert_eq!(d, Duration::from_secs(expected), "tries = {tries}");
}
let wind_to = |b: &mut Backoff, n: u32| {
while b.tries() < n - 1 {
b.next_delay(Duration::ZERO, poll);
}
b.next_delay(Duration::ZERO, poll)
};
assert_eq!(wind_to(&mut b, 15), Duration::from_secs(200));
assert_eq!(wind_to(&mut b, 20), Duration::from_secs(450));
assert_eq!(wind_to(&mut b, 23), poll, "capped at the poll interval");
assert_eq!(wind_to(&mut b, 40), poll, "and stays capped");
}
#[test]
fn staying_up_long_enough_resets_the_backoff() {
let poll = Duration::from_secs(600);
let mut b = Backoff::default();
for _ in 0..10 {
b.next_delay(Duration::ZERO, poll);
}
assert!(b.tries() > 5);
assert_eq!(b.next_delay(Duration::from_secs(60), poll), Duration::ZERO);
assert_eq!(b.tries(), 0);
let mut b = Backoff::default();
b.next_delay(Duration::from_secs(59), poll);
assert_eq!(b.tries(), 1);
}
#[test]
fn min_time_has_a_floor_of_ten_seconds() {
let poll = Duration::from_secs(5);
let mut b = Backoff::default();
b.next_delay(Duration::from_secs(9), poll);
assert_eq!(b.tries(), 1, "9s is below the 10s floor");
b.next_delay(Duration::from_secs(10), poll);
assert_eq!(b.tries(), 0, "10s meets the floor");
}
#[test]
fn the_first_start_never_sleeps() {
let mut b = Backoff::default();
assert_eq!(
b.next_delay(Duration::MAX, Duration::from_secs(600)),
Duration::ZERO
);
assert_eq!(b.tries(), 0);
}