From ccea7a8db372a4fb553103f09268ffd3bb04200c Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:19:50 -0400 Subject: [PATCH] Engine: macro debugger reports a step that panics as the step's failure When a debugged step panicked, the nested execute rolled the run back and dropped the debugger before advance could reattach the machine, so the caller got "No macro is being debugged" instead of the failed step. For example, stepping into a step whose command panics lost the step path and the panic message. advance now sees that the run was already rolled back and returns the usual failed result with the step error. --- crates/engine/src/macros/debug.rs | 65 ++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/macros/debug.rs b/crates/engine/src/macros/debug.rs index 1e599ab01..04da19834 100644 --- a/crates/engine/src/macros/debug.rs +++ b/crates/engine/src/macros/debug.rs @@ -188,6 +188,7 @@ fn all_paths(steps: &[gridcraft_model::Step], prefix: &str) -> BTreeSet /// Runs one step (`Step`), or steps until a breakpoint, the target step or the end. fn advance(s: &mut Session, mode: Mode) -> Result { let d = s.macro_debug.as_mut().ok_or_else(not_debugging)?; + let name = d.name.clone(); let breakpoints = d.breakpoints.clone(); let only_listed = d.only_listed; let mut machine = d.machine.take().ok_or_else(not_debugging)?; @@ -219,8 +220,16 @@ fn advance(s: &mut Session, mode: Mode) -> Result { } } let done = machine.is_done(); - if let Some(d) = s.macro_debug.as_mut() { - d.machine = Some(machine); + let executed = machine.executed; + match s.macro_debug.as_mut() { + Some(d) => d.machine = Some(machine), + // A step panicked and `recover` has already rolled the run back. + None => { + return match outcome { + Err(e) => Ok(failed(&name, executed, &e)), + Ok(()) => Err(not_debugging()), + }; + } } note_touched(s, &before); if let Err(e) = outcome { @@ -247,19 +256,17 @@ fn finish(s: &mut Session, error: Option) -> Result { // The group is gone only if its workbook was closed; then there is nothing to end. let _ = if rollback { s.group_rollback(token) } else { s.group_end(token).map(|_| ()) }; } + if let Some(e) = &error { + return Ok(failed(&d.name, executed, e)); + } let mut out = json!({"macro": d.name, "executed": executed}); - match (&error, &stopped) { - (Some(e), _) => { - out["state"] = json!("failed"); - out["error"] = serde_json::to_value(e).unwrap_or(Json::Null); - out["message"] = json!(format!("{} Nothing was changed.", e.describe())); - } - (None, Some((msg, rolled))) => { + match &stopped { + Some((msg, rolled)) => { out["state"] = json!("stopped"); out["stopped"] = json!(msg); out["kept"] = json!(!rolled); } - (None, None) => out["state"] = json!("finished"), + None => out["state"] = json!("finished"), } if !rollback { out["changed"] = json!(changed); @@ -267,6 +274,17 @@ fn finish(s: &mut Session, error: Option) -> Result { Ok(out) } +/// The result of a run that a failed step rolled back. +fn failed(name: &str, executed: u64, e: &StepError) -> Json { + json!({ + "macro": name, + "executed": executed, + "state": "failed", + "error": serde_json::to_value(e).unwrap_or(Json::Null), + "message": format!("{} Nothing was changed.", e.describe()), + }) +} + /// Stops the run: keeps what it did as one undo step, or rolls it back. fn stop(s: &mut Session, keep: bool) -> Result { let Some(mut d) = s.macro_debug.take() else { return Err(not_debugging()) }; @@ -360,6 +378,33 @@ mod tests { s.execute("cell.set", json!({"cell": "B1", "input": "free"})).unwrap(); } + #[test] + fn a_step_that_panics_reports_the_step_failure() { + crate::register_test_command("test.debugStepPanics", |_, _| panic!("boom")); + let mut s = Session::new(); + s.new_workbook(); + let m = crate::macros::parse_macro(&json!({"name": "M", "output": {"onConflict": "overwrite"}, "steps": [ + {"command": "cell.set", "params": {"cell": "A1", "input": "x"}}, + {"command": "test.debugStepPanics", "params": {}} + ]})) + .unwrap(); + let d = s.doc_mut().unwrap(); + let mut wb = (*d.wb).clone(); + wb.macros.push(m); + d.wb = std::sync::Arc::new(wb); + let start = s.doc().unwrap().wb.clone(); + s.execute("macro.debug", json!({"name": "M"})).unwrap(); + s.execute("macro.debug", json!({"action": "step"})).unwrap(); + let r = s.execute("macro.debug", json!({"action": "step"})).unwrap(); + assert_eq!(r["state"], "failed", "{r}"); + assert_eq!(r["macro"], "M"); + assert_eq!(r["error"]["step"], "2"); + assert!(r["message"].as_str().unwrap().contains("boom"), "{r}"); + assert!(s.macro_debug.is_none()); + assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().wb, &start)); + assert!(!s.in_group(s.doc().unwrap().uid)); + } + #[test] fn a_workbook_in_the_touched_log_waits_until_the_run_ends() { let mut s = Session::new();