Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions src/hyperlight_guest/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,294 @@ impl From<serde_json::Error> for HyperlightGuestError {
}
}
}

/// Extension trait to add context to `Option<T>` and `Result<T, E>` types in guest code,
/// converting them to `Result<T, HyperlightGuestError>`.
///
/// This is similar to anyhow::Context.
pub trait GuestErrorContext {
type Ok;
/// Adds context to the error if `self` is `None` or `Err`.
fn context(self, ctx: impl Into<String>) -> Result<Self::Ok>;
/// Adds context and a specific error code to the error if `self` is `None` or `Err`.
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<Self::Ok>;
/// Lazily adds context to the error if `self` is `None` or `Err`.
///
/// This is useful if constructing the context message is expensive.
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<Self::Ok>;
/// Lazily adds context and a specific error code to the error if `self` is `None` or `Err`.
///
/// This is useful if constructing the context message is expensive.
fn with_context_and_code<S: Into<String>>(
self,
ec: ErrorCode,
ctx: impl FnOnce() -> S,
) -> Result<Self::Ok>;
}

impl<T> GuestErrorContext for Option<T> {
type Ok = T;
#[inline]
fn context(self, ctx: impl Into<String>) -> Result<T> {
self.with_context_and_code(ErrorCode::GuestError, || ctx)
}
#[inline]
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
self.with_context_and_code(ec, || ctx)
}
#[inline]
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
self.with_context_and_code(ErrorCode::GuestError, ctx)
}
#[inline]
fn with_context_and_code<S: Into<String>>(
self,
ec: ErrorCode,
ctx: impl FnOnce() -> S,
) -> Result<Self::Ok> {
match self {
Some(s) => Ok(s),
None => Err(HyperlightGuestError::new(ec, ctx().into())),
}
}
}

impl<T, E: core::fmt::Debug> GuestErrorContext for core::result::Result<T, E> {
type Ok = T;
#[inline]
fn context(self, ctx: impl Into<String>) -> Result<T> {
self.with_context_and_code(ErrorCode::GuestError, || ctx)
}
#[inline]
fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
self.with_context_and_code(ec, || ctx)
}
#[inline]
fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
self.with_context_and_code(ErrorCode::GuestError, ctx)
}
#[inline]
fn with_context_and_code<S: Into<String>>(
self,
ec: ErrorCode,
ctx: impl FnOnce() -> S,
) -> Result<T> {
match self {
Ok(s) => Ok(s),
Err(e) => Err(HyperlightGuestError::new(
ec,
format!("{}.\nCaused by: {e:?}", ctx().into()),
)),
}
}
}

/// Macro to return early with a `Err(HyperlightGuestError)`.
/// Usage:
/// ```ignore
/// bail!(ErrorCode::UnknownError => "An error occurred: {}", details);
/// // or
/// bail!("A guest error occurred: {}", details); // defaults to ErrorCode::GuestError
/// ```
#[macro_export]
macro_rules! bail {
($ec:expr => $($msg:tt)*) => {
return ::core::result::Result::Err($crate::error::HyperlightGuestError::new($ec, ::alloc::format!($($msg)*)));
};
($($msg:tt)*) => {
$crate::bail!($crate::error::ErrorCode::GuestError => $($msg)*);
};
}

/// Macro to ensure a condition is true, otherwise returns early with a `Err(HyperlightGuestError)`.
/// Usage:
/// ```ignore
/// ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Maths is broken: {}", details);
/// // or
/// ensure!(1 + 1 == 3, "Maths is broken: {}", details); // defaults to ErrorCode::GuestError
/// // or
/// ensure!(1 + 1 == 3); // defaults to ErrorCode::GuestError with a default message
/// ```
#[macro_export]
macro_rules! ensure {
($cond:expr) => {
if !($cond) {
$crate::bail!(::core::concat!("Condition failed: `", ::core::stringify!($cond), "`"));
}
};
($cond:expr, $ec:expr => $($msg:tt)*) => {
if !($cond) {
$crate::bail!($ec => ::core::concat!("{}\nCaused by failed condition: `", ::core::stringify!($cond), "`"), ::core::format_args!($($msg)*));
}
};
($cond:expr, $($msg:tt)*) => {
$crate::ensure!($cond, $crate::error::ErrorCode::GuestError => $($msg)*);
};
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_context_option_some() {
let value: Option<u32> = Some(42);
let result = value.context("Should be Some");
assert_eq!(result.unwrap(), 42);
}

#[test]
fn test_context_option_none() {
let value: Option<u32> = None;
let result = value.context("Should be Some");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(err.message, "Should be Some");
}

#[test]
fn test_context_and_code_option_none() {
let value: Option<u32> = None;
let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Some");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::MallocFailed);
assert_eq!(err.message, "Should be Some");
}

#[test]
fn test_with_context_option_none() {
let value: Option<u32> = None;
let result = value.with_context(|| "Lazy context message");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(err.message, "Lazy context message");
}

#[test]
fn test_with_context_and_code_option_none() {
let value: Option<u32> = None;
let result =
value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::MallocFailed);
assert_eq!(err.message, "Lazy context message");
}

#[test]
fn test_context_result_ok() {
let value: core::result::Result<u32, &str> = Ok(42);
let result = value.context("Should be Ok");
assert_eq!(result.unwrap(), 42);
}

#[test]
fn test_context_result_err() {
let value: core::result::Result<u32, &str> = Err("Some error");
let result = value.context("Should be Ok");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
}

#[test]
fn test_context_and_code_result_err() {
let value: core::result::Result<u32, &str> = Err("Some error");
let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Ok");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::MallocFailed);
assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
}

#[test]
fn test_with_context_result_err() {
let value: core::result::Result<u32, &str> = Err("Some error");
let result = value.with_context(|| "Lazy context message");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(
err.message,
"Lazy context message.\nCaused by: \"Some error\""
);
}

#[test]
fn test_with_context_and_code_result_err() {
let value: core::result::Result<u32, &str> = Err("Some error");
let result =
value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::MallocFailed);
assert_eq!(
err.message,
"Lazy context message.\nCaused by: \"Some error\""
);
}

#[test]
fn test_bail_macro() {
let result: Result<u32> = (|| {
bail!("A guest error occurred");
})();
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(err.message, "A guest error occurred");
}

#[test]
fn test_bail_macro_with_error_code() {
let result: Result<u32> = (|| {
bail!(ErrorCode::MallocFailed => "Memory allocation failed");
})();
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::MallocFailed);
assert_eq!(err.message, "Memory allocation failed");
}

#[test]
fn test_ensure_macro_pass() {
let result: Result<u32> = (|| {
ensure!(1 + 1 == 2, "Math works");
Ok(42)
})();
assert_eq!(result.unwrap(), 42);
}

#[test]
fn test_ensure_macro_fail() {
let result: Result<u32> = (|| {
ensure!(1 + 1 == 3, "Math is broken");
Ok(42)
})();
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(
err.message,
"Math is broken\nCaused by failed condition: `1 + 1 == 3`"
);
}

#[test]
fn test_ensure_macro_fail_no_message() {
let result: Result<u32> = (|| {
ensure!(1 + 1 == 3);
Ok(42)
})();
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::GuestError);
assert_eq!(err.message, "Condition failed: `1 + 1 == 3`");
}

#[test]
fn test_ensure_macro_fail_with_error_code() {
let result: Result<u32> = (|| {
ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Math is broken");
Ok(42)
})();
let err = result.unwrap_err();
assert_eq!(err.kind, ErrorCode::UnknownError);
assert_eq!(
err.message,
"Math is broken\nCaused by failed condition: `1 + 1 == 3`"
);
}
}
Loading