From d9b8f65d7cc563ce8fb57f6b3695e264caa3dc03 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 13:23:06 +0200 Subject: [PATCH] Render Queue: the failure toast says why the render failed (#568) --- crates/engine/src/render_queue.rs | 13 ++++++++++--- crates/engine/src/rq_tests.rs | 26 +++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/render_queue.rs b/crates/engine/src/render_queue.rs index aae05a38..56754e7c 100644 --- a/crates/engine/src/render_queue.rs +++ b/crates/engine/src/render_queue.rs @@ -610,10 +610,17 @@ impl Session { let _ = t.join(); } let st = job.shared.snapshot(); - let failed = updates.iter().filter(|u| matches!(u, ItemUpdate::Finished { status: RenderStatus::Failed(_), .. })).count(); + let failures: Vec<&str> = updates + .iter() + .filter_map(|u| if let ItemUpdate::Finished { status: RenderStatus::Failed(e), .. } = u { Some(e.as_str()) } else { None }) + .collect(); + let failed = failures.len(); let stopped = updates.iter().any(|u| matches!(u, ItemUpdate::Finished { status: RenderStatus::UserStopped, .. })); - let msg = if failed > 0 { - format!("Render finished with {failed} failed item(s)") + // The reason is otherwise only in the Status column's tooltip (#568). + let msg = if let [only] = failures.as_slice() { + format!("Render finished with 1 failed item(s): {only}") + } else if let Some(first) = failures.first() { + format!("Render finished with {failed} failed item(s), first: {first}") } else if stopped { "Render stopped".to_string() } else { diff --git a/crates/engine/src/rq_tests.rs b/crates/engine/src/rq_tests.rs index 88c7a6e8..81a18722 100644 --- a/crates/engine/src/rq_tests.rs +++ b/crates/engine/src/rq_tests.rs @@ -87,7 +87,9 @@ fn add_configure_render() { assert_eq!(std::path::Path::new(q[0].last_output.as_deref().unwrap()), std::path::PathBuf::from(crate::tests_stubs::tmp_out("rq/out.mov"))); assert_eq!(q[1].status, RenderStatus::Failed("boom".into())); assert_eq!(q[2].status.label(), "Unqueued"); - assert!(s.drain_events().iter().any(|e| matches!(e, Event::Toast { error: true, .. }))); + let events = s.drain_events(); + let toast = events.iter().find_map(|e| if let Event::Toast { message, error: true } = e { Some(message.as_str()) } else { None }); + assert_eq!(toast, Some("Render finished with 1 failed item(s): boom"), "the toast says why (#568)"); // Reorder, duplicate, remove, undo. s.execute("renderQueue.move", json!({"item": b["item"], "to": 1})).unwrap(); assert_eq!(s.project.render_queue[0].id, b["item"].as_u64().unwrap()); @@ -114,6 +116,28 @@ fn persists_in_project_file() { assert!(effectcraft_project::Project::from_json(&old).unwrap().render_queue.is_empty()); } +#[test] +fn failure_toast_names_the_first_reason() { + struct Failing; + impl Exporter for Failing { + fn formats(&self) -> Vec { + OutputFormat::ALL.to_vec() + } + fn export(&self, job: &ExportJob, _: &mut dyn FnMut(u64, u64) -> bool) -> Result { + let name = std::path::Path::new(job.path).file_name().unwrap().to_string_lossy().into_owned(); + Err(format!("cannot write {name}")) + } + } + let mut s = demo(); + s.exporter = Some(Arc::new(Failing)); + s.execute("renderQueue.add", json!({"format": "gif", "output": crate::tests_stubs::tmp_out("rq/first.gif")})).unwrap(); + s.execute("renderQueue.add", json!({"format": "gif", "output": crate::tests_stubs::tmp_out("rq/second.gif")})).unwrap(); + s.execute("renderQueue.render", json!({})).unwrap(); + let events = s.drain_events(); + let toast = events.iter().find_map(|e| if let Event::Toast { message, error: true } = e { Some(message.as_str()) } else { None }); + assert_eq!(toast, Some("Render finished with 2 failed item(s), first: cannot write first.gif")); +} + #[test] fn background_render_and_stop() { struct Slow;