use core::cell::Cell;
use core::time::Duration;
use relentless::Stop;
use relentless::stop;
const MAX_ATTEMPTS: u32 = 5;
const DEADLINE: Duration = Duration::from_secs(30);
fn make_state(attempt: u32) -> relentless::RetryState {
relentless::RetryState::new(attempt, None)
}
fn make_state_with_elapsed(attempt: u32, elapsed: Duration) -> relentless::RetryState {
relentless::RetryState::new(attempt, Some(elapsed))
}
struct StopAfter {
max: u32,
}
impl Stop for StopAfter {
fn should_stop(&self, state: &relentless::RetryState) -> bool {
state.attempt >= self.max
}
}
#[test]
fn stop_should_stop_takes_ref_self_and_retry_state() {
let stop = StopAfter { max: 3 };
let state = make_state(1);
assert!(
!stop.should_stop(&state),
"attempt 1 < max, should not stop"
);
let state = make_state(3);
assert!(stop.should_stop(&state), "attempt == max, should stop");
}
#[test]
fn stop_and_wait_are_not_generic_over_result_type() {
let stop = StopAfter { max: 3 };
let state = make_state(1);
assert!(!stop.should_stop(&state));
}
#[test]
fn attempts_does_not_fire_below_threshold() {
let s = stop::attempts(MAX_ATTEMPTS);
for attempt in 1..MAX_ATTEMPTS {
let state = make_state(attempt);
assert!(
!s.should_stop(&state),
"should not stop at attempt {attempt}"
);
}
}
#[test]
fn attempts_fires_at_threshold() {
let s = stop::attempts(MAX_ATTEMPTS);
let state = make_state(MAX_ATTEMPTS);
assert!(s.should_stop(&state));
}
#[test]
fn attempts_fires_above_threshold() {
let s = stop::attempts(MAX_ATTEMPTS);
let state = make_state(MAX_ATTEMPTS + 1);
assert!(s.should_stop(&state));
}
#[test]
fn attempts_with_one_stops_immediately() {
let s = stop::attempts(1);
let state = make_state(1);
assert!(s.should_stop(&state));
}
#[test]
fn attempts_with_zero_panics() {
let panic_result = std::panic::catch_unwind(|| {
let _ = stop::attempts(0);
});
let panic_message = panic_result
.as_ref()
.err()
.and_then(|payload| payload.downcast_ref::<&str>().copied())
.or_else(|| {
panic_result
.as_ref()
.err()
.and_then(|payload| payload.downcast_ref::<String>().map(String::as_str))
})
.unwrap_or("<non-string panic payload>");
assert!(
panic_result.is_err(),
"stop::attempts(0) should panic with invalid configuration"
);
assert!(
panic_message.contains("stop::attempts requires max >= 1"),
"panic message should explain invalid attempts count, got: {panic_message}"
);
}
#[test]
fn elapsed_does_not_fire_below_deadline() {
let s = stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(1, DEADLINE.checked_sub(Duration::from_millis(1)).unwrap());
assert!(!s.should_stop(&state));
}
#[test]
fn elapsed_fires_at_deadline() {
let s = stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(1, DEADLINE);
assert!(s.should_stop(&state));
}
#[test]
fn elapsed_fires_above_deadline() {
let s = stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(1, DEADLINE + Duration::from_secs(1));
assert!(s.should_stop(&state));
}
#[test]
fn elapsed_never_fires_when_no_clock() {
let s = stop::elapsed(DEADLINE);
let state = make_state(1);
assert!(
!s.should_stop(&state),
"elapsed should never fire when clock is unavailable"
);
}
#[test]
fn never_always_returns_false() {
let s = stop::never();
for &attempt in &[1, 100, u32::MAX] {
let state = make_state(attempt);
assert!(
!s.should_stop(&state),
"never() should not stop at attempt {attempt}"
);
}
}
#[test]
fn never_returns_false_even_with_elapsed() {
let s = stop::never();
let state = make_state_with_elapsed(u32::MAX, Duration::MAX);
assert!(!s.should_stop(&state));
}
#[test]
fn stop_any_fires_when_left_fires() {
let s = stop::attempts(1) | stop::elapsed(DEADLINE);
let state = make_state(1);
assert!(s.should_stop(&state));
}
#[test]
fn stop_any_fires_when_right_fires() {
let s = stop::attempts(u32::MAX) | stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(1, DEADLINE);
assert!(s.should_stop(&state));
}
#[test]
fn stop_any_does_not_fire_when_neither_fires() {
let s = stop::attempts(MAX_ATTEMPTS) | stop::elapsed(DEADLINE);
let state = make_state(1); assert!(!s.should_stop(&state));
}
#[test]
fn stop_all_fires_when_both_fire() {
let s = stop::attempts(MAX_ATTEMPTS) & stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(MAX_ATTEMPTS, DEADLINE);
assert!(s.should_stop(&state));
}
#[test]
fn stop_all_does_not_fire_when_only_left_fires() {
let s = stop::attempts(MAX_ATTEMPTS) & stop::elapsed(DEADLINE);
let state = make_state(MAX_ATTEMPTS); assert!(!s.should_stop(&state));
}
#[test]
fn stop_all_does_not_fire_when_only_right_fires() {
let s = stop::attempts(MAX_ATTEMPTS) & stop::elapsed(DEADLINE);
let state = make_state_with_elapsed(1, DEADLINE); assert!(!s.should_stop(&state));
}
#[test]
fn three_way_or_composition() {
let s = stop::attempts(MAX_ATTEMPTS) | stop::elapsed(DEADLINE) | stop::never();
let state = make_state(MAX_ATTEMPTS);
assert!(s.should_stop(&state));
let state = make_state_with_elapsed(1, DEADLINE);
assert!(s.should_stop(&state));
let state = make_state(1);
assert!(!s.should_stop(&state));
}
#[test]
fn mixed_and_or_composition() {
let s = (stop::attempts(MAX_ATTEMPTS) & stop::elapsed(DEADLINE)) | stop::never();
let state = make_state_with_elapsed(MAX_ATTEMPTS, DEADLINE);
assert!(s.should_stop(&state));
let state = make_state(MAX_ATTEMPTS);
assert!(!s.should_stop(&state));
}
#[test]
fn attempts_is_clone_and_debug() {
let s = stop::attempts(MAX_ATTEMPTS);
fn assert_clone<T: Clone>(_value: &T) {}
assert_clone(&s);
let debug = format!("{s:?}");
assert!(debug.contains("StopAfterAttempts"));
}
#[test]
fn elapsed_is_clone_and_debug() {
let s = stop::elapsed(DEADLINE);
fn assert_clone<T: Clone>(_value: &T) {}
assert_clone(&s);
let debug = format!("{s:?}");
assert!(debug.contains("StopAfterElapsed"));
}
#[test]
fn never_is_clone_and_debug() {
let s = stop::never();
fn assert_clone<T: Clone>(_value: &T) {}
assert_clone(&s);
let debug = format!("{s:?}");
assert!(debug.contains("StopNever"));
}
#[test]
fn stop_any_is_clone_and_debug() {
let s = stop::attempts(MAX_ATTEMPTS) | stop::elapsed(DEADLINE);
let s2 = s.clone();
let debug = format!("{s2:?}");
assert!(debug.contains("StopAny"));
}
#[test]
fn stop_all_is_clone_and_debug() {
let s = stop::attempts(MAX_ATTEMPTS) & stop::elapsed(DEADLINE);
let s2 = s.clone();
let debug = format!("{s2:?}");
assert!(debug.contains("StopAll"));
}
#[test]
fn cloned_strategy_is_independent() {
let original = stop::attempts(MAX_ATTEMPTS);
let cloned = original;
let state = make_state(MAX_ATTEMPTS);
assert!(original.should_stop(&state));
assert!(cloned.should_stop(&state));
let state = make_state(1);
assert!(!original.should_stop(&state));
assert!(!cloned.should_stop(&state));
}
struct StopAfterConsultations {
threshold: u32,
count: Cell<u32>,
}
impl StopAfterConsultations {
const fn new(threshold: u32) -> Self {
Self {
threshold,
count: Cell::new(0),
}
}
}
impl Stop for StopAfterConsultations {
fn should_stop(&self, _state: &relentless::RetryState) -> bool {
let n = self.count.get() + 1;
self.count.set(n);
n >= self.threshold
}
}
const CONSULTATION_THRESHOLD: u32 = 3;
#[test]
fn no_short_circuit_in_stop_any() {
let composite = stop::attempts(1) | StopAfterConsultations::new(CONSULTATION_THRESHOLD);
let state = make_state(1);
assert!(composite.should_stop(&state)); assert!(composite.should_stop(&state)); assert!(composite.should_stop(&state)); }
#[test]
fn no_short_circuit_in_stop_all() {
let composite = stop::never() & StopAfterConsultations::new(CONSULTATION_THRESHOLD);
let state = make_state(1);
assert!(!composite.should_stop(&state)); assert!(!composite.should_stop(&state)); assert!(!composite.should_stop(&state)); assert!(
!composite.should_stop(&state),
"StopAll fires only when both fire; left=never keeps it from firing"
);
}
#[test]
fn stop_named_combinators_match_operator_forms() {
let named_or = stop::attempts(3).or(stop::elapsed(Duration::from_secs(2)));
let op_or = stop::attempts(3) | stop::elapsed(Duration::from_secs(2));
let early = make_state_with_elapsed(1, Duration::from_secs(1));
let elapsed_hit = make_state_with_elapsed(1, Duration::from_secs(2));
let attempt_hit = make_state(3);
assert_eq!(named_or.should_stop(&early), op_or.should_stop(&early));
assert_eq!(
named_or.should_stop(&elapsed_hit),
op_or.should_stop(&elapsed_hit)
);
assert_eq!(
named_or.should_stop(&attempt_hit),
op_or.should_stop(&attempt_hit)
);
let named_and = stop::attempts(3).and(stop::elapsed(Duration::from_secs(2)));
let op_and = stop::attempts(3) & stop::elapsed(Duration::from_secs(2));
let both_hit = make_state_with_elapsed(3, Duration::from_secs(2));
assert_eq!(named_and.should_stop(&early), op_and.should_stop(&early));
assert_eq!(
named_and.should_stop(&elapsed_hit),
op_and.should_stop(&elapsed_hit)
);
assert_eq!(
named_and.should_stop(&both_hit),
op_and.should_stop(&both_hit)
);
}
#[test]
fn stop_any_bitand_produces_stop_all() {
let s = (stop::attempts(MAX_ATTEMPTS) | stop::elapsed(DEADLINE)) & stop::attempts(MAX_ATTEMPTS);
let state = make_state(MAX_ATTEMPTS);
assert!(s.should_stop(&state));
let state = make_state_with_elapsed(1, DEADLINE);
assert!(!s.should_stop(&state));
}