fn retry_with_backoff<F, T>(label: &str, max_attempts: u32, mut attempt: F) -> Result<T, String>
where
F: FnMut(u32) -> Result<T, String>,
{
assert!(
max_attempts > 0,
"retry_with_backoff requires max_attempts >= 1; got 0 for label {label:?}",
);
let mut last_err: Option<String> = None;
for i in 1..=max_attempts {
println!("cargo:warning={label}: attempt {i}/{max_attempts}");
match attempt(i) {
Ok(v) => return Ok(v),
Err(e) => {
println!("cargo:warning={label}: attempt {i} failed: {e}");
last_err = Some(e);
if i < max_attempts {
let backoff = 1u64 << i;
std::thread::sleep(std::time::Duration::from_secs(backoff));
}
}
}
}
Err(last_err.expect(
"max_attempts > 0 guarded above; loop ran at least once; last_err set on every Err arm",
))
}
#[cfg(feature = "wprof")]
fn is_wprof_clone_complete(wprof_src: &std::path::Path) -> bool {
wprof_src.join(".git").join("HEAD").exists() && wprof_src.join("src").join("Makefile").exists()
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::time::Instant;
#[test]
fn succeeds_on_first_try_no_sleep() {
let calls = Cell::new(0u32);
let started = Instant::now();
let r: Result<u32, String> = retry_with_backoff("succeeds-first", 4, |_| {
calls.set(calls.get() + 1);
Ok(42)
});
assert_eq!(r.unwrap(), 42);
assert_eq!(calls.get(), 1, "must not retry on success");
assert!(
started.elapsed().as_secs() < 1,
"no sleep on first-try success",
);
}
#[test]
fn returns_last_err_after_max_attempts() {
let calls = Cell::new(0u32);
let r: Result<(), String> = retry_with_backoff("returns-last", 2, |i| {
calls.set(calls.get() + 1);
Err(format!("attempt {i} failed"))
});
assert_eq!(calls.get(), 2);
assert!(
r.unwrap_err().contains("attempt 2 failed"),
"returns the LAST err, not the first",
);
}
#[test]
#[should_panic(expected = "retry_with_backoff requires max_attempts >= 1")]
fn max_zero_panics_with_actionable_message() {
let _: Result<(), String> = retry_with_backoff("max-zero", 0, |_| Ok(()));
}
#[cfg(feature = "wprof")]
#[test]
fn is_wprof_clone_complete_rejects_missing_git_head() {
let tmp = tempfile::tempdir().expect("tempdir");
let src = tmp.path();
std::fs::create_dir_all(src.join("src")).expect("create src/");
std::fs::write(src.join("src/Makefile"), "").expect("write Makefile");
assert!(
!is_wprof_clone_complete(src),
"Makefile alone is not enough; .git/HEAD must also exist",
);
}
#[cfg(feature = "wprof")]
#[test]
fn is_wprof_clone_complete_rejects_missing_src_makefile() {
let tmp = tempfile::tempdir().expect("tempdir");
let src = tmp.path();
std::fs::create_dir_all(src.join(".git")).expect("create .git/");
std::fs::write(src.join(".git/HEAD"), "ref: refs/heads/main\n").expect("write .git/HEAD");
assert!(
!is_wprof_clone_complete(src),
".git/HEAD alone is not enough; src/Makefile must also exist",
);
}
#[cfg(feature = "wprof")]
#[test]
fn is_wprof_clone_complete_accepts_both_present() {
let tmp = tempfile::tempdir().expect("tempdir");
let src = tmp.path();
std::fs::create_dir_all(src.join(".git")).expect("create .git/");
std::fs::write(src.join(".git/HEAD"), "ref: refs/heads/main\n").expect("write .git/HEAD");
std::fs::create_dir_all(src.join("src")).expect("create src/");
std::fs::write(src.join("src/Makefile"), "").expect("write Makefile");
assert!(
is_wprof_clone_complete(src),
"both files present → clone considered complete",
);
}
}