Skip to content

Commit 485f8e5

Browse files
authored
Add anyhow-inspired error utilities (#1052)
* Add GuestErrorContext trait Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com> * Add bail and ensure macros Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com> * Add tests Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com> * Add doc comments Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com> --------- Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
1 parent 6a9c7dd commit 485f8e5

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed

src/hyperlight_guest/src/error.rs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,294 @@ impl From<serde_json::Error> for HyperlightGuestError {
5151
}
5252
}
5353
}
54+
55+
/// Extension trait to add context to `Option<T>` and `Result<T, E>` types in guest code,
56+
/// converting them to `Result<T, HyperlightGuestError>`.
57+
///
58+
/// This is similar to anyhow::Context.
59+
pub trait GuestErrorContext {
60+
type Ok;
61+
/// Adds context to the error if `self` is `None` or `Err`.
62+
fn context(self, ctx: impl Into<String>) -> Result<Self::Ok>;
63+
/// Adds context and a specific error code to the error if `self` is `None` or `Err`.
64+
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<Self::Ok>;
65+
/// Lazily adds context to the error if `self` is `None` or `Err`.
66+
///
67+
/// This is useful if constructing the context message is expensive.
68+
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<Self::Ok>;
69+
/// Lazily adds context and a specific error code to the error if `self` is `None` or `Err`.
70+
///
71+
/// This is useful if constructing the context message is expensive.
72+
fn with_context_and_code<S: Into<String>>(
73+
self,
74+
ec: ErrorCode,
75+
ctx: impl FnOnce() -> S,
76+
) -> Result<Self::Ok>;
77+
}
78+
79+
impl<T> GuestErrorContext for Option<T> {
80+
type Ok = T;
81+
#[inline]
82+
fn context(self, ctx: impl Into<String>) -> Result<T> {
83+
self.with_context_and_code(ErrorCode::GuestError, || ctx)
84+
}
85+
#[inline]
86+
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
87+
self.with_context_and_code(ec, || ctx)
88+
}
89+
#[inline]
90+
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
91+
self.with_context_and_code(ErrorCode::GuestError, ctx)
92+
}
93+
#[inline]
94+
fn with_context_and_code<S: Into<String>>(
95+
self,
96+
ec: ErrorCode,
97+
ctx: impl FnOnce() -> S,
98+
) -> Result<Self::Ok> {
99+
match self {
100+
Some(s) => Ok(s),
101+
None => Err(HyperlightGuestError::new(ec, ctx().into())),
102+
}
103+
}
104+
}
105+
106+
impl<T, E: core::fmt::Debug> GuestErrorContext for core::result::Result<T, E> {
107+
type Ok = T;
108+
#[inline]
109+
fn context(self, ctx: impl Into<String>) -> Result<T> {
110+
self.with_context_and_code(ErrorCode::GuestError, || ctx)
111+
}
112+
#[inline]
113+
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
114+
self.with_context_and_code(ec, || ctx)
115+
}
116+
#[inline]
117+
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
118+
self.with_context_and_code(ErrorCode::GuestError, ctx)
119+
}
120+
#[inline]
121+
fn with_context_and_code<S: Into<String>>(
122+
self,
123+
ec: ErrorCode,
124+
ctx: impl FnOnce() -> S,
125+
) -> Result<T> {
126+
match self {
127+
Ok(s) => Ok(s),
128+
Err(e) => Err(HyperlightGuestError::new(
129+
ec,
130+
format!("{}.\nCaused by: {e:?}", ctx().into()),
131+
)),
132+
}
133+
}
134+
}
135+
136+
/// Macro to return early with a `Err(HyperlightGuestError)`.
137+
/// Usage:
138+
/// ```ignore
139+
/// bail!(ErrorCode::UnknownError => "An error occurred: {}", details);
140+
/// // or
141+
/// bail!("A guest error occurred: {}", details); // defaults to ErrorCode::GuestError
142+
/// ```
143+
#[macro_export]
144+
macro_rules! bail {
145+
($ec:expr => $($msg:tt)*) => {
146+
return ::core::result::Result::Err($crate::error::HyperlightGuestError::new($ec, ::alloc::format!($($msg)*)));
147+
};
148+
($($msg:tt)*) => {
149+
$crate::bail!($crate::error::ErrorCode::GuestError => $($msg)*);
150+
};
151+
}
152+
153+
/// Macro to ensure a condition is true, otherwise returns early with a `Err(HyperlightGuestError)`.
154+
/// Usage:
155+
/// ```ignore
156+
/// ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Maths is broken: {}", details);
157+
/// // or
158+
/// ensure!(1 + 1 == 3, "Maths is broken: {}", details); // defaults to ErrorCode::GuestError
159+
/// // or
160+
/// ensure!(1 + 1 == 3); // defaults to ErrorCode::GuestError with a default message
161+
/// ```
162+
#[macro_export]
163+
macro_rules! ensure {
164+
($cond:expr) => {
165+
if !($cond) {
166+
$crate::bail!(::core::concat!("Condition failed: `", ::core::stringify!($cond), "`"));
167+
}
168+
};
169+
($cond:expr, $ec:expr => $($msg:tt)*) => {
170+
if !($cond) {
171+
$crate::bail!($ec => ::core::concat!("{}\nCaused by failed condition: `", ::core::stringify!($cond), "`"), ::core::format_args!($($msg)*));
172+
}
173+
};
174+
($cond:expr, $($msg:tt)*) => {
175+
$crate::ensure!($cond, $crate::error::ErrorCode::GuestError => $($msg)*);
176+
};
177+
}
178+
179+
#[cfg(test)]
180+
mod tests {
181+
use super::*;
182+
183+
#[test]
184+
fn test_context_option_some() {
185+
let value: Option<u32> = Some(42);
186+
let result = value.context("Should be Some");
187+
assert_eq!(result.unwrap(), 42);
188+
}
189+
190+
#[test]
191+
fn test_context_option_none() {
192+
let value: Option<u32> = None;
193+
let result = value.context("Should be Some");
194+
let err = result.unwrap_err();
195+
assert_eq!(err.kind, ErrorCode::GuestError);
196+
assert_eq!(err.message, "Should be Some");
197+
}
198+
199+
#[test]
200+
fn test_context_and_code_option_none() {
201+
let value: Option<u32> = None;
202+
let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Some");
203+
let err = result.unwrap_err();
204+
assert_eq!(err.kind, ErrorCode::MallocFailed);
205+
assert_eq!(err.message, "Should be Some");
206+
}
207+
208+
#[test]
209+
fn test_with_context_option_none() {
210+
let value: Option<u32> = None;
211+
let result = value.with_context(|| "Lazy context message");
212+
let err = result.unwrap_err();
213+
assert_eq!(err.kind, ErrorCode::GuestError);
214+
assert_eq!(err.message, "Lazy context message");
215+
}
216+
217+
#[test]
218+
fn test_with_context_and_code_option_none() {
219+
let value: Option<u32> = None;
220+
let result =
221+
value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
222+
let err = result.unwrap_err();
223+
assert_eq!(err.kind, ErrorCode::MallocFailed);
224+
assert_eq!(err.message, "Lazy context message");
225+
}
226+
227+
#[test]
228+
fn test_context_result_ok() {
229+
let value: core::result::Result<u32, &str> = Ok(42);
230+
let result = value.context("Should be Ok");
231+
assert_eq!(result.unwrap(), 42);
232+
}
233+
234+
#[test]
235+
fn test_context_result_err() {
236+
let value: core::result::Result<u32, &str> = Err("Some error");
237+
let result = value.context("Should be Ok");
238+
let err = result.unwrap_err();
239+
assert_eq!(err.kind, ErrorCode::GuestError);
240+
assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
241+
}
242+
243+
#[test]
244+
fn test_context_and_code_result_err() {
245+
let value: core::result::Result<u32, &str> = Err("Some error");
246+
let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Ok");
247+
let err = result.unwrap_err();
248+
assert_eq!(err.kind, ErrorCode::MallocFailed);
249+
assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
250+
}
251+
252+
#[test]
253+
fn test_with_context_result_err() {
254+
let value: core::result::Result<u32, &str> = Err("Some error");
255+
let result = value.with_context(|| "Lazy context message");
256+
let err = result.unwrap_err();
257+
assert_eq!(err.kind, ErrorCode::GuestError);
258+
assert_eq!(
259+
err.message,
260+
"Lazy context message.\nCaused by: \"Some error\""
261+
);
262+
}
263+
264+
#[test]
265+
fn test_with_context_and_code_result_err() {
266+
let value: core::result::Result<u32, &str> = Err("Some error");
267+
let result =
268+
value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
269+
let err = result.unwrap_err();
270+
assert_eq!(err.kind, ErrorCode::MallocFailed);
271+
assert_eq!(
272+
err.message,
273+
"Lazy context message.\nCaused by: \"Some error\""
274+
);
275+
}
276+
277+
#[test]
278+
fn test_bail_macro() {
279+
let result: Result<u32> = (|| {
280+
bail!("A guest error occurred");
281+
})();
282+
let err = result.unwrap_err();
283+
assert_eq!(err.kind, ErrorCode::GuestError);
284+
assert_eq!(err.message, "A guest error occurred");
285+
}
286+
287+
#[test]
288+
fn test_bail_macro_with_error_code() {
289+
let result: Result<u32> = (|| {
290+
bail!(ErrorCode::MallocFailed => "Memory allocation failed");
291+
})();
292+
let err = result.unwrap_err();
293+
assert_eq!(err.kind, ErrorCode::MallocFailed);
294+
assert_eq!(err.message, "Memory allocation failed");
295+
}
296+
297+
#[test]
298+
fn test_ensure_macro_pass() {
299+
let result: Result<u32> = (|| {
300+
ensure!(1 + 1 == 2, "Math works");
301+
Ok(42)
302+
})();
303+
assert_eq!(result.unwrap(), 42);
304+
}
305+
306+
#[test]
307+
fn test_ensure_macro_fail() {
308+
let result: Result<u32> = (|| {
309+
ensure!(1 + 1 == 3, "Math is broken");
310+
Ok(42)
311+
})();
312+
let err = result.unwrap_err();
313+
assert_eq!(err.kind, ErrorCode::GuestError);
314+
assert_eq!(
315+
err.message,
316+
"Math is broken\nCaused by failed condition: `1 + 1 == 3`"
317+
);
318+
}
319+
320+
#[test]
321+
fn test_ensure_macro_fail_no_message() {
322+
let result: Result<u32> = (|| {
323+
ensure!(1 + 1 == 3);
324+
Ok(42)
325+
})();
326+
let err = result.unwrap_err();
327+
assert_eq!(err.kind, ErrorCode::GuestError);
328+
assert_eq!(err.message, "Condition failed: `1 + 1 == 3`");
329+
}
330+
331+
#[test]
332+
fn test_ensure_macro_fail_with_error_code() {
333+
let result: Result<u32> = (|| {
334+
ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Math is broken");
335+
Ok(42)
336+
})();
337+
let err = result.unwrap_err();
338+
assert_eq!(err.kind, ErrorCode::UnknownError);
339+
assert_eq!(
340+
err.message,
341+
"Math is broken\nCaused by failed condition: `1 + 1 == 3`"
342+
);
343+
}
344+
}

0 commit comments

Comments
 (0)