-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy patherrors.rs
More file actions
310 lines (258 loc) · 10.8 KB
/
errors.rs
File metadata and controls
310 lines (258 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// Copyright (c) 2019-2025 Provable Inc.
// This file is part of the snarkVM library.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use colored::Colorize;
use std::{
any::Any,
backtrace::Backtrace,
borrow::Borrow,
cell::Cell,
panic,
sync::atomic::{AtomicBool, Ordering},
};
thread_local! {
/// The message backtrace of the last panic on this thread (if any).
///
/// We store this information here instead of directly processing it in a panic hook, because panic hooks are global whereas this can be processed on a per-thread basis.
/// For example, one thread may execute a program where panics should *not* cause the entire process to terminate, while in another thread there is a panic due to a bug.
static PANIC_INFO: Cell<Option<(String, Backtrace)>> = const { Cell::new(None) };
}
/// Keeps track of whether a panic hook was installed already.
static PANIC_HOOK_INSTALLED: AtomicBool = const { AtomicBool::new(false) };
/// Generates an `io::Error` from the given string.
#[inline]
pub fn io_error<S: ToString>(err: S) -> std::io::Error {
std::io::Error::other(err.to_string())
}
/// Generates an `io::Error` from the given `anyhow::Error`.
///
/// This will flatten the existing error chain so that it fits in a single-line 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))
}
/// Converts an `anyhow::Error` into a single-line string.
///
/// This follows the existing convention in the codebase that joins errors using em dashes.
/// For example, an error "Invalid transaction" with a cause "Proof failed" would be logged
/// as "Invalid transaction — Proof failed".
#[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())
}
/// Displays an `anyhow::Error`'s main error and its error chain to stderr.
///
/// This can be used to show a "pretty" error to the end user.
#[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}"));
}
/// Ensures that two values are equal, otherwise bails with a formatted error message.
///
/// # Arguments
/// * `actual` - The actual value
/// * `expected` - The expected value
/// * `message` - A description of what was being checked
#[macro_export]
macro_rules! ensure_equals {
($actual:expr, $expected:expr, $message:expr) => {
if $actual != $expected {
anyhow::bail!("{}: Was {} but expected {}.", $message, $actual, $expected);
}
};
}
/// A trait to provide a nicer way to unwarp `anyhow::Result`.
pub trait PrettyUnwrap {
type Inner;
/// Behaves like [`std::result::Result::unwrap`] but will print the entire anyhow chain to stderr.
fn pretty_unwrap(self) -> Self::Inner;
/// Behaves like [`std::result::Result::expect`] but will print the entire anyhow chain to stderr.
fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner;
}
/// Set the global panic hook for the process.
///
/// This function should be called once at startup. Subsequent calls to it have no effect.
pub fn set_panic_hook() {
// Check if the hook was already installed.
// Note, that this allows for a small race condition, where the hook is installed by another thread after the check, but before the load.
// However, that is safe as the installed hook will be indentical, and this check merely exists for performance reasons.
if PANIC_HOOK_INSTALLED.load(Ordering::Acquire) {
return;
}
// Install the hook.
std::panic::set_hook(Box::new(|err| {
let msg = err.to_string();
let trace = Backtrace::force_capture();
PANIC_INFO.with(move |info| info.set(Some((msg, trace))));
}));
// Mark the hook as installed.
PANIC_HOOK_INSTALLED.store(true, Ordering::Release);
}
/// Helper for `PrettyUnwrap`:
/// Creates a panic with the `anyhow::Error` nicely formatted.
#[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}");
}
/// Implement the trait for `anyhow::Result`.
impl<T> PrettyUnwrap for anyhow::Result<T> {
type Inner = T;
#[track_caller]
#[inline]
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()));
}
}
}
}
/// `try_vm_runtime` executes the given closure in an environment which will safely halt
/// without producing logs that look like unexpected behavior.
/// In debug mode, it prints to stderr using the format: "VM safely halted at {location}: {halt message}".
///
/// Note: For this to work as expected, panics must be set to `unwind` during compilation (default), and the closure cannot invoke any async code that may potentially execute in a different OS thread.
#[track_caller]
#[inline]
pub fn try_vm_runtime<R, F: FnMut() -> R>(f: F) -> Result<R, Box<dyn Any + Send>> {
// Perform the operation that may panic.
let result = std::panic::catch_unwind(panic::AssertUnwindSafe(f));
if result.is_err() {
// Get the stored panic and backtrace from the thread-local variable.
let (msg, _) = PANIC_INFO.with(|info| info.take()).expect("No panic information stored?");
#[cfg(debug_assertions)]
{
// Remove all words up to "panicked".
// And prepend with "VM Safely halted"
let msg = msg
.to_string()
.split_ascii_whitespace()
.skip_while(|&word| word != "panicked")
.collect::<Vec<&str>>()
.join(" ")
.replacen("panicked", "VM safely halted", 1);
eprintln!("{msg}");
}
#[cfg(not(debug_assertions))]
{
// Discard message
let _ = msg;
}
}
// Return the result, allowing regular error-handling.
result
}
/// `catch_unwind` calls the given closure `f` and, if `f` panics, returns the panic message and backtrace.
#[inline]
pub fn catch_unwind<R, F: FnMut() -> R>(f: F) -> Result<R, (String, Backtrace)> {
// Perform the operation that may panic.
std::panic::catch_unwind(panic::AssertUnwindSafe(f)).map_err(|_| {
// Get the stored panic and backtrace from the thread-local variable.
PANIC_INFO.with(|info| info.take()).expect("No panic information stored?")
})
}
#[cfg(test)]
mod tests {
use super::{PrettyUnwrap, catch_unwind, flatten_error, pretty_panic, set_panic_hook, try_vm_runtime};
use anyhow::{Context, Result, anyhow, bail};
use colored::Colorize;
const ERRORS: [&str; 3] = ["Third error", "Second error", "First error"];
#[test]
fn test_flatten_error() {
// First error should be printed regularly, the other two dimmed.
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]);
// Also test `pretty_unwrap` and chaining errors across functions.
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);
}
// Ensure catch_unwind stores the panic message as expected.
#[test]
fn test_catch_unwind() {
set_panic_hook();
let result = catch_unwind(move || {
panic!("This is my message");
});
// Remove hook so test asserts work normally again.
let _ = std::panic::take_hook();
let (msg, bt) = result.expect_err("No panic caught");
assert!(msg.ends_with("This is my message"));
// This function should be in the panics backtrace
assert!(bt.to_string().contains("test_catch_unwind"));
}
/// Ensure catch_unwind does not break `try_vm_runtime`.
#[test]
fn test_nested_with_try_vm_runtime() {
set_panic_hook();
let result = std::panic::catch_unwind(|| {
// try_vm_runtime uses catch_unwind internally
let vm_result = try_vm_runtime(|| {
panic!("VM operation failed!");
});
assert!(vm_result.is_err(), "try_vm_runtime should catch VM panic");
// We can handle the VM error gracefully
"handled_vm_error"
});
assert!(result.is_ok(), "Should handle VM error gracefully");
assert_eq!(result.unwrap(), "handled_vm_error");
}
}