use colored::Colorize;
use std::borrow::Borrow;
#[inline]
pub fn io_error<S: ToString>(err: S) -> std::io::Error {
std::io::Error::other(err.to_string())
}
#[inline]
pub fn into_io_error<E: Into<anyhow::Error>>(err: E) -> std::io::Error {
let err: anyhow::Error = err.into();
std::io::Error::other(flatten_error(&err))
}
#[inline]
pub fn flatten_error<E: Borrow<anyhow::Error>>(error: E) -> String {
let error = error.borrow();
let chain = error.chain().skip(1).map(|next| next.to_string()).collect::<Vec<String>>().join(" — ");
format!("{error}{}", format!(" — {chain}").dimmed())
}
#[track_caller]
#[inline]
pub fn display_error<E: Borrow<anyhow::Error>>(error: E) {
let error = error.borrow();
eprintln!("⚠️ {error}");
error.chain().skip(1).for_each(|cause| eprintln!(" ↳ {cause}"));
}
#[macro_export]
macro_rules! ensure_equals {
($actual:expr, $expected:expr, $message:expr $(, $format_args:tt)*) => {
if $actual != $expected {
anyhow::bail!("{}: Was {} but expected {}.", format!($message $(, $format_args)*), $actual, $expected);
}
};
}
pub trait PrettyUnwrap {
type Inner;
fn pretty_unwrap(self) -> Self::Inner;
fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner;
}
#[track_caller]
#[inline]
fn pretty_panic(error: &anyhow::Error) -> ! {
let mut string = format!("⚠️ {error}");
error.chain().skip(1).for_each(|cause| string.push_str(&format!("\n ↳ {cause}")));
let caller = std::panic::Location::caller();
tracing::error!("[{}:{}] {string}", caller.file(), caller.line());
panic!("{string}");
}
impl<T> PrettyUnwrap for anyhow::Result<T> {
type Inner = T;
#[track_caller]
fn pretty_unwrap(self) -> Self::Inner {
match self {
Ok(result) => result,
Err(error) => {
pretty_panic(&error);
}
}
}
#[track_caller]
fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner {
match self {
Ok(result) => result,
Err(error) => {
pretty_panic(&error.context(context.to_string()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::{PrettyUnwrap, flatten_error, pretty_panic};
use anyhow::{Context, Result, anyhow, bail};
use colored::Colorize;
const ERRORS: [&str; 3] = ["Third error", "Second error", "First error"];
#[test]
fn test_flatten_error() {
let expected = format!("{}{}", ERRORS[0], format!(" — {} — {}", ERRORS[1], ERRORS[2]).dimmed());
let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
let result = flatten_error(&my_error);
assert_eq!(result, expected);
}
#[test]
fn chained_error_panic_format() {
let expected = format!("⚠️ {}\n ↳ {}\n ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
let result = std::panic::catch_unwind(|| {
let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
pretty_panic(&my_error);
})
.unwrap_err();
assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
}
#[test]
fn chained_pretty_unwrap_format() {
let expected = format!("⚠️ {}\n ↳ {}\n ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
let result = std::panic::catch_unwind(|| {
fn level2() -> Result<()> {
bail!(ERRORS[2]);
}
fn level1() -> Result<()> {
level2().with_context(|| ERRORS[1])?;
Ok(())
}
fn level0() -> Result<()> {
level1().with_context(|| ERRORS[0])?;
Ok(())
}
level0().pretty_unwrap();
})
.unwrap_err();
assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
}
#[test]
fn test_nested_with_try_vm_runtime() {
use crate::try_vm_runtime;
let result = std::panic::catch_unwind(|| {
let vm_result = try_vm_runtime!(|| {
panic!("VM operation failed!");
});
assert!(vm_result.is_err(), "try_vm_runtime should catch VM panic");
"handled_vm_error"
});
assert!(result.is_ok(), "Should handle VM error gracefully");
assert_eq!(result.unwrap(), "handled_vm_error");
}
#[test]
fn ensure_equals_with_format_string() {
let correct = "correct";
let error = || -> Result<()> {
ensure_equals!(1, 2, "Test value {} {correct}", "is not");
Ok(())
}()
.unwrap_err();
assert_eq!(error.to_string(), "Test value is not correct: Was 1 but expected 2.");
}
}