#![cfg(feature = "tokio")]
use std::time::Duration;
use qubit_retry::{AttemptFailure, Retry, RetryErrorReason};
use crate::support::TestError;
#[tokio::test]
#[should_panic(expected = "async operation panic")]
async fn test_run_async_panic_propagates() {
let retry = Retry::<TestError>::builder()
.max_attempts(2)
.no_delay()
.build()
.expect("retry should build");
let _ = retry
.run_async::<(), _, _>(|| async { panic!("async operation panic") })
.await;
}
#[tokio::test]
async fn test_run_async_attempt_timeout_can_abort() {
let retry = Retry::<TestError>::builder()
.max_attempts(3)
.attempt_timeout(Some(Duration::from_millis(1)))
.abort_on_timeout()
.no_delay()
.build()
.expect("retry should build");
let error = retry
.run_async(|| async {
tokio::time::sleep(Duration::from_millis(20)).await;
Ok::<(), TestError>(())
})
.await
.expect_err("timeout should abort");
assert_eq!(error.reason(), RetryErrorReason::Aborted);
assert!(matches!(
error.last_failure(),
Some(AttemptFailure::Timeout)
));
assert_eq!(
error.context().attempt_timeout(),
Some(Duration::from_millis(1))
);
}
#[tokio::test]
async fn test_run_async_max_elapsed_caps_in_flight_attempt_before_configured_timeout() {
let retry = Retry::<TestError>::builder()
.max_attempts(1)
.max_elapsed(Some(Duration::from_millis(20)))
.attempt_timeout(Some(Duration::from_millis(200)))
.no_delay()
.build()
.expect("retry should build");
let started = std::time::Instant::now();
let error = retry
.run_async(|| async {
tokio::time::sleep(Duration::from_millis(120)).await;
Ok::<_, TestError>("late")
})
.await
.expect_err("max elapsed should stop the in-flight async attempt");
let elapsed = started.elapsed();
assert_eq!(error.reason(), RetryErrorReason::MaxElapsedExceeded);
assert_eq!(error.attempts(), 1);
assert!(matches!(
error.last_failure(),
Some(AttemptFailure::Timeout)
));
assert_eq!(
error.context().attempt_timeout(),
Some(Duration::from_millis(20))
);
assert!(
elapsed < Duration::from_millis(100),
"max elapsed should stop before the configured timeout, elapsed: {elapsed:?}"
);
}
#[tokio::test]
async fn test_run_async_configured_timeout_wins_when_shorter_than_max_elapsed() {
let retry = Retry::<TestError>::builder()
.max_attempts(1)
.max_elapsed(Some(Duration::from_millis(200)))
.attempt_timeout(Some(Duration::from_millis(20)))
.abort_on_timeout()
.no_delay()
.build()
.expect("retry should build");
let error = retry
.run_async(|| async {
tokio::time::sleep(Duration::from_millis(120)).await;
Ok::<_, TestError>("late")
})
.await
.expect_err("configured attempt timeout should abort first");
assert_eq!(error.reason(), RetryErrorReason::Aborted);
assert_eq!(
error.context().attempt_timeout(),
Some(Duration::from_millis(20))
);
assert!(matches!(
error.last_failure(),
Some(AttemptFailure::Timeout)
));
}
#[tokio::test]
async fn test_run_async_error_before_remaining_elapsed_timeout_can_retry() {
let retry = Retry::<TestError>::builder()
.max_attempts(2)
.max_elapsed(Some(Duration::from_millis(200)))
.no_delay()
.build()
.expect("retry should build");
let mut attempts = 0;
let value = retry
.run_async(|| {
attempts += 1;
async move {
if attempts == 1 {
Err(TestError("transient"))
} else {
Ok("done")
}
}
})
.await
.expect("ordinary error should retry before remaining elapsed timeout");
assert_eq!(value, "done");
assert_eq!(attempts, 2);
}
#[tokio::test(start_paused = true)]
async fn test_run_async_without_timeout_retries_until_success() {
let retry = Retry::<TestError>::builder()
.max_attempts(2)
.fixed_delay(Duration::from_millis(1))
.build()
.expect("retry should build");
let mut attempts = 0;
let value = retry
.run_async(|| {
attempts += 1;
let current_attempt = attempts;
async move {
if current_attempt == 1 {
Err(TestError("temporary"))
} else {
Ok("done")
}
}
})
.await
.expect("second async attempt should succeed");
assert_eq!(value, "done");
assert_eq!(attempts, 2);
}
#[tokio::test(start_paused = true)]
async fn test_run_async_with_attempt_timeout_allows_fast_success() {
let retry = Retry::<TestError>::builder()
.max_attempts(1)
.attempt_timeout(Some(Duration::from_millis(10)))
.no_delay()
.build()
.expect("retry should build");
let value = retry
.run_async(|| async { Ok::<_, TestError>("fast") })
.await
.expect("fast async attempt should succeed");
assert_eq!(value, "fast");
}
#[tokio::test]
async fn test_run_async_max_elapsed_can_stop_before_first_attempt() {
let retry = Retry::<TestError>::builder()
.max_elapsed(Some(Duration::ZERO))
.attempt_timeout(Some(Duration::from_millis(1)))
.no_delay()
.build()
.expect("retry should build");
let error = retry
.run_async::<(), _, _>(|| async { panic!("operation must not run") })
.await
.expect_err("zero elapsed budget should stop before first attempt");
assert_eq!(error.reason(), RetryErrorReason::MaxElapsedExceeded);
assert_eq!(error.attempts(), 0);
assert_eq!(
error.context().attempt_timeout(),
Some(Duration::from_millis(1))
);
}
#[tokio::test]
async fn test_run_async_zero_delay_retry_skips_sleep() {
let retry = Retry::<TestError>::builder()
.max_attempts(2)
.no_delay()
.build()
.expect("retry should build");
let mut attempts = 0;
let value = retry
.run_async(|| {
attempts += 1;
let current_attempt = attempts;
async move {
if current_attempt == 1 {
Err(TestError("temporary"))
} else {
Ok("done")
}
}
})
.await
.expect("second async attempt should succeed");
assert_eq!(value, "done");
assert_eq!(attempts, 2);
}