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.
This commit is contained in:
prxnvt
2026-10-22 02:26:32 -04:00
parent 45e580ad91
commit ccea7a8db3
+55 -10
View File
@@ -188,6 +188,7 @@ fn all_paths(steps: &[gridcraft_model::Step], prefix: &str) -> BTreeSet<String>
/// Runs one step (`Step`), or steps until a breakpoint, the target step or the end.
fn advance(s: &mut Session, mode: Mode) -> Result<Json> {
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<Json> {
}
}
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<StepError>) -> Result<Json> {
// 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<StepError>) -> Result<Json> {
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<Json> {
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();