use super::*;
fn refusal() -> Error {
return Error::from_raw_os_error(libc::EAGAIN);
}
fn drive(
budget: Duration,
mut outcome: impl FnMut(u32) -> Result<i32>,
) -> (Result<i32>, Vec<Duration>, u32) {
let mut pauses: Vec<Duration> = Vec::new();
let mut tries: u32 = 0;
let result: Result<i32> = retry_refused_start_with(
"a test child",
budget,
&mut |wait: Duration| { pauses.push(wait); },
&mut || {
tries += 1;
return outcome(tries);
},
);
return (result, pauses, tries);
}
fn millis(pauses: &[Duration]) -> Vec<u128> {
return pauses.iter().map(Duration::as_millis).collect();
}
fn assert_growing_pauses(pauses: &[Duration]) {
let mut expected: Duration = FIRST_PAUSE;
for (index, pause) in pauses.iter().enumerate() {
assert_eq!(*pause, expected, "pause {index}: {pauses:?}");
expected = (expected * 2).min(LONGEST_PAUSE);
}
}
#[test]
fn a_start_that_succeeds_at_once_never_pauses() {
let (result, pauses, tries) = drive(START_RETRY_BUDGET, |_| { return Ok(7); });
assert_eq!(result.expect("started"), 7);
assert_eq!(pauses, Vec::<Duration>::new());
assert_eq!(tries, 1);
}
#[test]
fn a_refused_start_is_tried_again_after_growing_pauses() {
let (result, pauses, tries) = drive(START_RETRY_BUDGET, |attempt| {
return if attempt <= 3 { Err(refusal()) } else { Ok(9) };
});
assert_eq!(result.expect("started on the fourth try"), 9);
assert_eq!(pauses.len(), 3, "{:?}", millis(&pauses));
assert_growing_pauses(&pauses);
assert_eq!(tries, 4);
}
#[test]
fn pauses_double_up_to_the_longest_one() {
let (result, pauses, _) = drive(Duration::from_secs(600), |attempt| {
return if attempt <= 12 { Err(refusal()) } else { Ok(1) };
});
assert!(result.is_ok());
assert_eq!(pauses.len(), 12, "{:?}", millis(&pauses));
assert_growing_pauses(&pauses);
assert_eq!(pauses.last(), Some(&LONGEST_PAUSE));
}
#[test]
fn a_start_refused_past_the_budget_fails_and_counts_the_refusals() {
let (result, pauses, tries) = drive(START_RETRY_BUDGET, |_| { return Err(refusal()); });
let error: Error = result.expect_err("the budget ran out");
assert_eq!(error.kind(), ErrorKind::WouldBlock);
let text: String = error.to_string();
assert!(text.contains("Resource temporarily unavailable"), "{text}");
let waited: Duration = pauses.iter().sum();
let mut budgeted: Duration = Duration::ZERO;
let mut delay: Duration = FIRST_PAUSE;
while budgeted + delay <= START_RETRY_BUDGET {
budgeted += delay;
delay = (delay * 2).min(LONGEST_PAUSE);
}
assert_eq!(waited, budgeted, "the wait is the pauses the budget holds");
let refusals: u32 = u32::try_from(pauses.len()).expect("pause count") + 1;
assert_eq!(tries, refusals);
assert!(
text.contains(format!("refused {refusals} times over {} ms", waited.as_millis()).as_str()),
"{text}"
);
assert!(waited <= START_RETRY_BUDGET, "the pauses never pass the budget");
}
#[test]
fn another_error_is_returned_at_once() {
let (result, pauses, tries) = drive(START_RETRY_BUDGET, |_| { return Err(Error::from(ErrorKind::NotFound)); });
assert_eq!(result.expect_err("not retried").kind(), ErrorKind::NotFound);
assert_eq!(pauses, Vec::<Duration>::new());
assert_eq!(tries, 1);
}
#[test]
fn real_commands_start_through_the_retrying_calls() {
assert!(Command::new("true").status_retrying().expect("true starts").success());
let output: Output = Command::new("echo").arg("kept").output_retrying().expect("echo starts");
assert_eq!(String::from_utf8_lossy(output.stdout.as_slice()), "kept\n");
let mut child: Child = Command::new("true").spawn_retrying().expect("true spawns");
assert!(child.wait().expect("true ends").success());
let error: Error = Command::new("/nonexistent-cli-git-program").status_retrying().expect_err("no such program");
assert_eq!(error.kind(), ErrorKind::NotFound);
}
#[cfg(unix)]
#[test]
fn a_failed_exec_returns_its_error_at_once() {
let error: Error = exec_retrying(&mut Command::new("/nonexistent-cli-git-program"));
assert_eq!(error.kind(), ErrorKind::NotFound);
}