use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const SIZE: usize = 4 * 1024 * 1024;
fn refuse_every_part_with(status: usize) -> (Option<i32>, Duration, String) {
let mut server = mockito::Server::new();
let url = format!("{}/file", server.url());
let _head = server
.mock("HEAD", "/file")
.with_status(200)
.with_header("content-length", &SIZE.to_string())
.with_header("accept-ranges", "bytes")
.with_header("etag", "statusetag")
.create();
let _get = server
.mock("GET", "/file")
.expect_at_least(1)
.with_status(status)
.with_body("no")
.create();
let data_dir = tempfile::tempdir().unwrap();
let save_dir = tempfile::tempdir().unwrap();
let started = Instant::now();
let output = Command::new(env!("CARGO_BIN_EXE_odl"))
.arg(&url)
.arg("-o")
.arg(save_dir.path().join("file"))
.arg("--download-dir")
.arg(data_dir.path())
.arg("--max-connections")
.arg("4")
.arg("--format")
.arg("json")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.expect("failed to spawn odl binary");
(
output.status.code(),
started.elapsed(),
String::from_utf8_lossy(&output.stdout).into_owned(),
)
}
const CONFLICT: Option<i32> = Some(4);
const RETRYABLE: Option<i32> = Some(3);
#[test]
fn a_settled_refusal_fails_at_once_and_is_not_retryable() {
for (status, expected) in [
(404, CONFLICT), (410, CONFLICT), (403, CONFLICT), (401, CONFLICT), (416, CONFLICT), (400, Some(1)), ] {
let (code, _, out) = refuse_every_part_with(status);
assert_eq!(code, expected, "HTTP {status} classified wrong: {out}");
}
}
#[test]
fn a_busy_server_is_still_retried() {
for status in [429, 500, 503] {
let (code, _, out) = refuse_every_part_with(status);
assert_eq!(code, RETRYABLE, "HTTP {status} must stay retryable: {out}");
}
}
#[test]
fn a_settled_refusal_does_not_spend_the_retry_budget() {
let (_, terminal, _) = refuse_every_part_with(404);
let (_, transient, _) = refuse_every_part_with(503);
assert!(
terminal * 2 < transient,
"a 404 should cost far less than a 503: {terminal:?} vs {transient:?}"
);
}