From 113db9dcfb853fd11aa7550ec0baf81c673730a9 Mon Sep 17 00:00:00 2001 From: piekstra Date: Thu, 8 Oct 2026 12:26:05 -0400 Subject: [PATCH] fix: say why a failed attempt is retried --- crates/lest-core/src/runner/mod.rs | 20 ++++++++++++++++++-- crates/lest-core/tests/runner.rs | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/lest-core/src/runner/mod.rs b/crates/lest-core/src/runner/mod.rs index 2103f76..659b4b6 100644 --- a/crates/lest-core/src/runner/mod.rs +++ b/crates/lest-core/src/runner/mod.rs @@ -1268,7 +1268,7 @@ impl<'e> Runner<'e> { if let Some(until) = retry.and_then(|r| r.until.as_ref()) { if !outcome.passed && !outcome.cancelled { // The attempt itself failed (exit code, expect): retry. - retry_reason = Some(outcome.error.clone().unwrap_or_else(|| "attempt failed".into())); + retry_reason = Some(attempt_reason(step.kind(), &outcome)); } else if !outcome.cancelled { match expr::eval_bool(until, &self_scope) { Ok(true) => { @@ -1291,7 +1291,7 @@ impl<'e> Runner<'e> { } } } else if !outcome.passed && !outcome.cancelled { - retry_reason = Some(outcome.error.clone().unwrap_or_else(|| "attempt failed".into())); + retry_reason = Some(attempt_reason(step.kind(), &outcome)); } let done = outcome.passed || outcome.cancelled || attempt == max || retry_reason.is_none(); @@ -1566,3 +1566,19 @@ impl<'a> LineSink<'a> { self.ctx.emitter.emit(EventBody::StepOutput { step_id: self.step_id.clone(), stream, line }); } } + +/// Why a failed attempt failed, in the words the step's headline would use. +fn attempt_reason(kind: StepKind, o: &steps::AttemptOutcome) -> String { + let probe = StepReport { + kind: Some(kind), + status: Some(if o.errored { StepStatus::Errored } else { StepStatus::Failed }), + error: o.error.clone(), + exit_code: o.exit_code, + http_status: o.http_status, + resolved: o.resolved.clone(), + stdout: o.stdout.clone(), + stderr: o.stderr.clone(), + ..Default::default() + }; + headline(&probe).unwrap_or_else(|| "failed".into()) +} diff --git a/crates/lest-core/tests/runner.rs b/crates/lest-core/tests/runner.rs index 7061d56..a2578de 100644 --- a/crates/lest-core/tests/runner.rs +++ b/crates/lest-core/tests/runner.rs @@ -195,6 +195,31 @@ steps: assert_eq!(ex.attempts, 2); } +#[tokio::test] +async fn a_failed_attempt_says_why_it_is_retried() { + let f = Fixture::new(&[( + "a.lest.yaml", + r#" +apiVersion: lest/v1 +id: retry-why +name: Retry why +steps: + - id: poll + run: 'echo "no match yet" >&2; exit 4' + retry: { attempts: 2, delay: 10ms } +"#, + )]); + let (_, events) = run(&f.engine(&[]), "retry-why", &[]).await; + let reason = events + .iter() + .find_map(|e| match &e.body { + EventBody::StepRetrying { reason, .. } => Some(reason.clone()), + _ => None, + }) + .unwrap(); + assert_eq!(reason, "exited 4: no match yet"); +} + #[tokio::test] async fn http_step_with_expect_and_outputs() { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();