effectcraft/crates/engine/src/guard.rs

66 lines
2.5 KiB
Rust

//! The last-resort guard: a panic that escapes a command (a bug, never an expected error) is
//! turned into an [`EngineError`] instead of taking the app, and the user's open project, down
//! with it. Edits work on a copy of the project (see [`crate::Session::edit`]), so an edit that
//! panics leaves the project as it was. This is a safety net, not a substitute for returning
//! errors (AGENTS.md, "Never crash").
use crate::{EngineError, Result};
/// Run `f`; a panic inside it becomes an error naming `what`. The panic itself is logged by
/// the panic hook ([`crate::logging::install_panic_hook`]).
pub fn guarded<T>(what: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(r) => r,
Err(payload) => {
let msg = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown error".into());
log::error!("{what} panicked: {msg}");
Err(EngineError::Other(format!("{what} failed with an internal error ({msg}); it was stopped and your project is still open")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_panic_becomes_an_error() {
let r: Result<()> = guarded("command `test.panic`", || panic!("boom"));
let e = r.unwrap_err().to_string();
assert!(e.contains("test.panic") && e.contains("boom"), "{e}");
let r: Result<u8> = guarded("x", || {
let v: Vec<u8> = vec![];
Ok(v[3])
});
assert!(r.is_err());
assert_eq!(guarded("ok", || Ok(7)).unwrap(), 7);
assert!(matches!(guarded::<()>("err", || Err(EngineError::NoComp)), Err(EngineError::NoComp)));
}
#[test]
fn the_session_survives_a_panicking_edit() {
let mut s = crate::Session::new();
let before = s.project.clone();
let r: Result<()> = guarded("command `test.edit`", || {
s.edit("Broken", None, |p, _| {
p.next_id += 1;
panic!("bug in the middle of an edit")
})
});
assert!(r.is_err());
assert!(std::sync::Arc::ptr_eq(&before, &s.project), "the project is untouched");
// Later edits still work.
let next = s.project.next_id;
s.edit("Fine", None, |p, _| {
p.next_id += 1;
Ok(())
})
.unwrap();
assert_eq!(s.project.next_id, next + 1);
}
}