use hojicha_core::commands;
use hojicha_core::core::{Cmd, Model};
use hojicha_core::event::Event;
use log::error;
use std::panic::{self, AssertUnwindSafe};
#[derive(Debug, Clone, Copy)]
pub enum PanicRecoveryStrategy {
Continue,
Reset,
ShowError,
Quit,
}
impl Default for PanicRecoveryStrategy {
fn default() -> Self {
Self::Continue
}
}
pub fn safe_init<M: Model>(model: &mut M, strategy: PanicRecoveryStrategy) -> Cmd<M::Message> {
let result = panic::catch_unwind(AssertUnwindSafe(|| model.init()));
match result {
Ok(cmd) => cmd,
Err(panic_info) => {
let msg = format_panic_message(&panic_info, "Model::init");
error!("Panic in Model::init: {}", msg);
match strategy {
PanicRecoveryStrategy::Continue => Cmd::noop(),
PanicRecoveryStrategy::Reset => Cmd::noop(),
PanicRecoveryStrategy::ShowError => {
Cmd::noop()
}
PanicRecoveryStrategy::Quit => commands::quit(),
}
}
}
}
pub fn safe_update<M: Model>(
model: &mut M,
event: Event<M::Message>,
strategy: PanicRecoveryStrategy,
) -> Cmd<M::Message> {
let result = panic::catch_unwind(AssertUnwindSafe(move || model.update(event)));
match result {
Ok(cmd) => cmd,
Err(panic_info) => {
let msg = format_panic_message(&panic_info, "Model::update");
error!("Panic in Model::update: {}", msg);
match strategy {
PanicRecoveryStrategy::Continue => Cmd::noop(),
PanicRecoveryStrategy::Reset => {
Cmd::noop()
}
PanicRecoveryStrategy::ShowError => {
Cmd::noop()
}
PanicRecoveryStrategy::Quit => commands::quit(),
}
}
}
}
pub fn safe_view<M: Model>(model: &M, strategy: PanicRecoveryStrategy) -> (String, bool) {
let result = panic::catch_unwind(AssertUnwindSafe(|| model.view()));
match result {
Ok(view_string) => (view_string, false), Err(panic_info) => {
let msg = format_panic_message(&panic_info, "Model::view");
error!("Panic in Model::view: {}", msg);
let error_view = match strategy {
PanicRecoveryStrategy::ShowError => {
format!("ERROR: {}\nPress 'q' to quit", msg)
}
_ => "An error occurred. Press 'q' to quit.".to_string(),
};
let should_quit = matches!(strategy, PanicRecoveryStrategy::Quit);
(error_view, should_quit)
}
}
}
fn format_panic_message(panic_info: &dyn std::any::Any, context: &str) -> String {
if let Some(s) = panic_info.downcast_ref::<String>() {
format!("{} panicked: {}", context, s)
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("{} panicked: {}", context, s)
} else {
format!("{} panicked with unknown error", context)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct PanickyModel {
should_panic_init: bool,
should_panic_update: bool,
should_panic_view: bool,
}
impl Model for PanickyModel {
type Message = ();
fn init(&mut self) -> Cmd<Self::Message> {
if self.should_panic_init {
panic!("Init panic!");
}
Cmd::noop()
}
fn update(&mut self, _event: Event<Self::Message>) -> Cmd<Self::Message> {
if self.should_panic_update {
panic!("Update panic!");
}
Cmd::noop()
}
fn view(&self) -> String {
if self.should_panic_view {
panic!("View panic!");
}
"Test view".to_string()
}
}
#[test]
fn test_safe_init_catches_panic() {
let mut model = PanickyModel {
should_panic_init: true,
should_panic_update: false,
should_panic_view: false,
};
let cmd = safe_init(&mut model, PanicRecoveryStrategy::Continue);
assert!(cmd.is_noop());
}
#[test]
fn test_safe_update_catches_panic() {
let mut model = PanickyModel {
should_panic_init: false,
should_panic_update: true,
should_panic_view: false,
};
let cmd = safe_update(&mut model, Event::Tick, PanicRecoveryStrategy::Continue);
assert!(cmd.is_noop());
}
}