test(jimeng): cover poll state machine edges
Exercise the decision logic and async driver beyond the happy path: - decide() consults the business code before data.status (fatal and retryable codes win over a bogus done status) - not_found is terminal - exponential backoff grows 1,2,4,8 and caps at 8x the interval - backoff multiplier resets after a normal generating wait - a retryable code followed by a fatal one stops the loop
This commit is contained in:
@@ -349,4 +349,122 @@ mod tests {
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, JimengError::Cancelled));
|
||||
}
|
||||
|
||||
/// A non-success code carrying a (bogus) `done` status in `data`. Used to
|
||||
/// prove `decide` consults the business code *before* `data.status`.
|
||||
fn err_code_with_done_status(code: i64) -> GetResultResponse {
|
||||
GetResultResponse {
|
||||
code,
|
||||
message: Some("m".into()),
|
||||
data: Some(GetResultData {
|
||||
status: Some("done".into()),
|
||||
image_urls: Some(vec!["https://x/should-be-ignored.png".into()]),
|
||||
binary_data_base64: None,
|
||||
video_url: Some("https://x/should-be-ignored.mp4".into()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decide_checks_code_before_status() {
|
||||
// Fatal code wins over a "done" status: must Fail, never Done.
|
||||
assert!(matches!(
|
||||
decide(JimengCapability::Image, &err_code_with_done_status(50411)),
|
||||
PollDecision::Fail(JimengError::Api { code: 50411, .. })
|
||||
));
|
||||
// Retryable code wins over a "done" status: must Backoff, never Done.
|
||||
assert_eq!(
|
||||
decide(JimengCapability::Video, &err_code_with_done_status(50429)),
|
||||
PollDecision::Backoff(50429)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decide_not_found_is_terminal() {
|
||||
assert!(matches!(
|
||||
decide(JimengCapability::Video, &resp(10000, Some("not_found"), None)),
|
||||
PollDecision::Fail(JimengError::NotFound)
|
||||
));
|
||||
}
|
||||
|
||||
/// Driver variant that records every sleep duration instead of sleeping, so
|
||||
/// backoff math is observable. `interval` is 1s to make the multipliers read
|
||||
/// as whole seconds.
|
||||
async fn run_sequence_capturing_sleeps(
|
||||
capability: JimengCapability,
|
||||
seq: Vec<GetResultResponse>,
|
||||
) -> (Result<JimengOutput, JimengError>, Vec<Duration>) {
|
||||
let idx = RefCell::new(0usize);
|
||||
let seq = RefCell::new(seq);
|
||||
let sleeps = RefCell::new(Vec::new());
|
||||
let out = poll_until_done(
|
||||
capability,
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(3600),
|
||||
|| async {
|
||||
let mut i = idx.borrow_mut();
|
||||
let s = seq.borrow();
|
||||
let item = s.get(*i).cloned().unwrap_or_else(|| s.last().cloned().unwrap());
|
||||
*i += 1;
|
||||
Ok(item)
|
||||
},
|
||||
|| async { false },
|
||||
|d| {
|
||||
sleeps.borrow_mut().push(d);
|
||||
async {}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
(out, sleeps.into_inner())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn driver_backoff_grows_exponentially_and_caps() {
|
||||
// Five retryable codes then done: delays double (1,2,4,8) and cap at 8.
|
||||
let seq = vec![
|
||||
resp(50429, None, None),
|
||||
resp(50429, None, None),
|
||||
resp(50429, None, None),
|
||||
resp(50429, None, None),
|
||||
resp(50429, None, None),
|
||||
resp(10000, Some("done"), Some("https://x/v.mp4")),
|
||||
];
|
||||
let (out, sleeps) = run_sequence_capturing_sleeps(JimengCapability::Video, seq).await;
|
||||
assert!(out.is_ok());
|
||||
let secs: Vec<u64> = sleeps.iter().map(Duration::as_secs).collect();
|
||||
assert_eq!(secs, vec![1, 2, 4, 8, 8]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn driver_backoff_resets_after_a_normal_wait() {
|
||||
// retryable, retryable, then a normal Wait (generating) must reset the
|
||||
// multiplier back to 1 before the next retryable backs off afresh.
|
||||
let seq = vec![
|
||||
resp(50429, None, None), // backoff -> sleep 1, mult 1->2
|
||||
resp(50429, None, None), // backoff -> sleep 2, mult 2->4
|
||||
resp(10000, Some("generating"), None), // Wait -> sleep 1, mult reset to 1
|
||||
resp(50429, None, None), // backoff -> sleep 1, mult 1->2
|
||||
resp(10000, Some("done"), Some("https://x/v.mp4")),
|
||||
];
|
||||
let (out, sleeps) = run_sequence_capturing_sleeps(JimengCapability::Video, seq).await;
|
||||
assert!(out.is_ok());
|
||||
let secs: Vec<u64> = sleeps.iter().map(Duration::as_secs).collect();
|
||||
assert_eq!(secs, vec![1, 2, 1, 1]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn driver_retryable_then_fatal_stops() {
|
||||
// A transient code may be followed by a fatal one; the fatal must end
|
||||
// the loop with its Api error rather than retrying forever.
|
||||
let seq = vec![
|
||||
resp(50429, None, None), // retryable QPS
|
||||
resp(50411, None, None), // fatal input moderation
|
||||
];
|
||||
let (out, _sleeps) =
|
||||
run_sequence_capturing_sleeps(JimengCapability::Image, seq).await;
|
||||
assert!(matches!(
|
||||
out,
|
||||
Err(JimengError::Api { code: 50411, retryable: false, .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user