From e26dd21af742818ede2d489b1a7a1107f45c3e2e Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 13:47:04 +0200 Subject: [PATCH] CLI: a failed render under run/exec exits 1, like render --queue (part of #1426) --- apps/effectcraft-cli/src/main.rs | 21 +++++++++++++++++++++ apps/effectcraft-cli/tests/cli.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs index fe523807..b355e820 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -493,6 +493,9 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> { }; let mut b = backend(args, true)?; let r = b.exec(&id, params)?; + if let Some(e) = failed_render(&id, &r) { + return Err(Failure::Error(e)); + } let saved = maybe_save(&mut b, args)?; emit(&with_saved(r, saved), json_out); } @@ -515,6 +518,9 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> { }; i += 1; let r = b.exec(id, params).map_err(|e| Failure::Error(format!("{id}: {e}")))?; + if let Some(e) = failed_render(id, &r) { + return Err(Failure::Error(format!("{id}: {e}"))); + } if !json_out { emit(&r, false); } @@ -637,6 +643,21 @@ fn positional(args: &Args, usage: &str) -> Result<[String; N], F Ok(std::array::from_fn(|i| args.pos[i].clone())) } +/// `renderQueue.render` reports a failed item in its result, not as an error. Under `run`/`exec` +/// that item fails the command (#1426), as it fails `render --queue`. +fn failed_render(id: &str, r: &Value) -> Option { + if id != "renderQueue.render" { + return None; + } + let failed: Vec = r["items"] + .as_array() + .into_iter() + .flatten() + .filter_map(|i| Some(format!("{}: {}", i["compName"].as_str().unwrap_or("?"), i["status"].get("Failed")?.as_str().unwrap_or("failed")))) + .collect(); + (!failed.is_empty()).then(|| failed.join("; ")) +} + fn with_saved(v: Value, saved: Option) -> Value { match saved { None => v, diff --git a/apps/effectcraft-cli/tests/cli.rs b/apps/effectcraft-cli/tests/cli.rs index 62aefd5d..9ae64a69 100644 --- a/apps/effectcraft-cli/tests/cli.rs +++ b/apps/effectcraft-cli/tests/cli.rs @@ -462,6 +462,32 @@ fn mcp_autosave_is_opt_in_and_rejects_invalid_combinations() { std::fs::remove_dir_all(config).unwrap(); } +/// `run`/`exec renderQueue.render` (#1426): a failed item is a command error, exit 1 like +/// `render --queue`, not a success with the failure buried in the JSON. +#[test] +fn a_failed_render_under_run_and_exec_exits_1() { + let root = tmp("render-fails"); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + // The output's folder is a file, so the encoder cannot open the output. + let blocker = root.join("not-a-folder"); + std::fs::write(&blocker, "x").unwrap(); + let out = blocker.join("out.gif"); + let add = json!({"comp": "T", "format": "gif", "output": out.to_str().unwrap()}).to_string(); + let project = root.join("q.ecproj"); + let steps = ["comp.new", r#"{"name":"T","width":16,"height":16,"duration":0.1}"#, "layer.newSolid", "{}", "renderQueue.add", &add]; + ok_json(&[&["run", "--empty", "--save-as", project.to_str().unwrap()][..], &steps[..]].concat()); + + let (code, v) = run_json(&[&["run", "--empty"][..], &steps[..], &["renderQueue.render", r#"{"wait":true}"#][..]].concat()); + assert_eq!(code, 1, "{v}"); + let e = v["error"].as_str().unwrap_or_else(|| panic!("{v}")); + assert!(e.starts_with("renderQueue.render: T: "), "{e}"); + + let (code, v) = run_json(&["exec", "renderQueue.render", "--project", project.to_str().unwrap()]); + assert_eq!(code, 1, "{v}"); + assert!(v["error"].as_str().is_some_and(|e| e.starts_with("T: ")), "{v}"); +} + /// `render --bitrate` (#440) targets the bitrate in VP9 WebM. #[test] fn render_bitrate_applies_to_vp9_webm() {