/ utilities / src / errors.rs
errors.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaVM library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  use colored::Colorize;
 17  use std::borrow::Borrow;
 18  
 19  /// Generates an `io::Error` from the given string.
 20  #[inline]
 21  pub fn io_error<S: ToString>(err: S) -> std::io::Error {
 22      std::io::Error::other(err.to_string())
 23  }
 24  
 25  /// Generates an `io::Error` from the given `anyhow::Error`.
 26  ///
 27  /// This will flatten the existing error chain so that it fits in a single-line string.
 28  #[inline]
 29  pub fn into_io_error<E: Into<anyhow::Error>>(err: E) -> std::io::Error {
 30      let err: anyhow::Error = err.into();
 31      std::io::Error::other(flatten_error(&err))
 32  }
 33  
 34  /// Converts an `anyhow::Error` into a single-line string.
 35  ///
 36  /// This follows the existing convention in the codebase that joins errors using em dashes.
 37  /// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
 38  /// as "Invalid transaction — Proof failed".
 39  #[inline]
 40  pub fn flatten_error<E: Borrow<anyhow::Error>>(error: E) -> String {
 41      let error = error.borrow();
 42      let chain = error.chain().skip(1).map(|next| next.to_string()).collect::<Vec<String>>().join(" — ");
 43      format!("{error}{}", format!(" — {chain}").dimmed())
 44  }
 45  
 46  /// Displays an `anyhow::Error`'s main error and its error chain to stderr.
 47  ///
 48  /// This can be used to show a "pretty" error to the end user.
 49  #[track_caller]
 50  #[inline]
 51  pub fn display_error<E: Borrow<anyhow::Error>>(error: E) {
 52      let error = error.borrow();
 53      eprintln!("⚠️ {error}");
 54      error.chain().skip(1).for_each(|cause| eprintln!("     ↳ {cause}"));
 55  }
 56  
 57  /// Ensures that two values are equal, otherwise bails with a formatted error message.
 58  ///
 59  /// # Arguments
 60  /// * `actual` - The actual value
 61  /// * `expected` - The expected value  
 62  /// * `message` - A description of what was being checked
 63  #[macro_export]
 64  macro_rules! ensure_equals {
 65      ($actual:expr, $expected:expr, $message:expr) => {
 66          if $actual != $expected {
 67              anyhow::bail!("{}: Was {} but expected {}.", $message, $actual, $expected);
 68          }
 69      };
 70  }
 71  
 72  /// A trait to provide a nicer way to unwarp `anyhow::Result`.
 73  pub trait PrettyUnwrap {
 74      type Inner;
 75  
 76      /// Behaves like [`std::result::Result::unwrap`] but will print the entire anyhow chain to stderr.
 77      fn pretty_unwrap(self) -> Self::Inner;
 78  
 79      /// Behaves like [`std::result::Result::expect`] but will print the entire anyhow chain to stderr.
 80      fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner;
 81  }
 82  
 83  /// Helper for `PrettyUnwrap`, which creates a panic with the `anyhow::Error` nicely formatted and also logs the panic.
 84  #[track_caller]
 85  #[inline]
 86  fn pretty_panic(error: &anyhow::Error) -> ! {
 87      let mut string = format!("⚠️ {error}");
 88      error.chain().skip(1).for_each(|cause| string.push_str(&format!("\n     ↳ {cause}")));
 89      let caller = std::panic::Location::caller();
 90  
 91      tracing::error!("[{}:{}] {string}", caller.file(), caller.line());
 92      panic!("{string}");
 93  }
 94  
 95  /// Implement the trait for `anyhow::Result`.
 96  impl<T> PrettyUnwrap for anyhow::Result<T> {
 97      type Inner = T;
 98  
 99      #[track_caller]
100      fn pretty_unwrap(self) -> Self::Inner {
101          match self {
102              Ok(result) => result,
103              Err(error) => {
104                  pretty_panic(&error);
105              }
106          }
107      }
108  
109      #[track_caller]
110      fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner {
111          match self {
112              Ok(result) => result,
113              Err(error) => {
114                  pretty_panic(&error.context(context.to_string()));
115              }
116          }
117      }
118  }
119  
120  #[cfg(test)]
121  mod tests {
122      use super::{PrettyUnwrap, flatten_error, pretty_panic};
123  
124      use anyhow::{Context, Result, anyhow, bail};
125      use colored::Colorize;
126  
127      const ERRORS: [&str; 3] = ["Third error", "Second error", "First error"];
128  
129      #[test]
130      fn test_flatten_error() {
131          // First error should be printed regularly, the other two dimmed.
132          let expected = format!("{}{}", ERRORS[0], format!(" — {} — {}", ERRORS[1], ERRORS[2]).dimmed());
133  
134          let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
135          let result = flatten_error(&my_error);
136  
137          assert_eq!(result, expected);
138      }
139  
140      #[test]
141      fn chained_error_panic_format() {
142          let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
143  
144          let result = std::panic::catch_unwind(|| {
145              let my_error = anyhow!(ERRORS[2]).context(ERRORS[1]).context(ERRORS[0]);
146              pretty_panic(&my_error);
147          })
148          .unwrap_err();
149  
150          assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
151      }
152  
153      #[test]
154      fn chained_pretty_unwrap_format() {
155          let expected = format!("⚠️ {}\n     ↳ {}\n     ↳ {}", ERRORS[0], ERRORS[1], ERRORS[2]);
156  
157          // Also test `pretty_unwrap` and chaining errors across functions.
158          let result = std::panic::catch_unwind(|| {
159              fn level2() -> Result<()> {
160                  bail!(ERRORS[2]);
161              }
162  
163              fn level1() -> Result<()> {
164                  level2().with_context(|| ERRORS[1])?;
165                  Ok(())
166              }
167  
168              fn level0() -> Result<()> {
169                  level1().with_context(|| ERRORS[0])?;
170                  Ok(())
171              }
172  
173              level0().pretty_unwrap();
174          })
175          .unwrap_err();
176  
177          assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
178      }
179  
180      /// Ensure catch_unwind does not break `try_vm_runtime`.
181      #[test]
182      fn test_nested_with_try_vm_runtime() {
183          use crate::try_vm_runtime;
184  
185          let result = std::panic::catch_unwind(|| {
186              // try_vm_runtime uses catch_unwind internally
187              let vm_result = try_vm_runtime!(|| {
188                  panic!("VM operation failed!");
189              });
190  
191              assert!(vm_result.is_err(), "try_vm_runtime should catch VM panic");
192  
193              // We can handle the VM error gracefully
194              "handled_vm_error"
195          });
196  
197          assert!(result.is_ok(), "Should handle VM error gracefully");
198          assert_eq!(result.unwrap(), "handled_vm_error");
199      }
200  }