[Crate] Add rich error type (#80)

* Add rich error type with source and backtrace

Adds a rich error type that will take a backtrace and allow capturing
the source of the error. The best way to use this is with the included
helpers that will automatically capture the backtrace and convert the
source error:

```
GDRichError::packet_bad_from_into("Reason packet was bad")
```

* [Crate] Bump MSRV to 1.65.0

This is required for backtraces in rich errors.

* Remove leftover debug logging

* [Errors] Replace variant overloads with single .rich method on kind enum

This overhaul replaces the exhaustive impls of each variant as multiple
methods on the rich error type with a singular .rich() method on the
kind enum. This consumes the variant and converts it to a rich error
with a source (.into() can be used if a source is not needed).

I also took the liberty of replacing all usages with the this new method
as I saw fit (adding various error messages) and converting a few
PacketBad errors to TypeParse errors when they are the result of parse
failing.

* Fix problem with rustdoc

* Remove BadGame's owned string

* Rename GDError to GDErrorKind

* Rename GDRichError to GDError

* Remove error impl from GDErrorKind

* Fix tests not passing

* Use error context everywhere map_err is used

* Improve GDError display formatter

* Add tests for new error type
This commit is contained in:
Tom 2023-08-05 09:36:48 +00:00 committed by GitHub
parent f7b5463073
commit 5e7e010d24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 417 additions and 217 deletions

View file

@ -1,4 +1,5 @@
use std::{
backtrace,
error::Error,
fmt::{self, Formatter},
};
@ -8,7 +9,7 @@ pub type GDResult<T> = Result<T, GDError>;
/// GameDig Error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GDError {
pub enum GDErrorKind {
/// The received packet was bigger than the buffer size.
PacketOverflow,
/// The received packet was shorter than the expected one.
@ -28,7 +29,7 @@ pub enum GDError {
/// Invalid input.
InvalidInput,
/// The server queried is not the queried game server.
BadGame(String),
BadGame,
/// Couldn't automatically query.
AutoQuery,
/// A protocol-defined expected format was not met.
@ -41,12 +42,108 @@ pub enum GDError {
TypeParse,
}
impl Error for GDError {}
impl GDErrorKind {
/// Convert error kind into a full error with a source (and implicit
/// backtrace)
///
/// ```
/// use gamedig::{GDErrorKind, GDResult};
/// let _: GDResult<u32> = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
/// ```
pub fn context<E: Into<Box<dyn std::error::Error + 'static>>>(self, source: E) -> GDError {
GDError::from_error(self, source)
}
}
type ErrorSource = Box<dyn std::error::Error + 'static>;
/// Gamedig error type
///
/// Can be created in three ways (all of which will implicitly generate a
/// backtrace):
///
/// Directly from an [error kind](crate::errors::GDErrorKind) (without a source)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.into();
/// ```
///
/// [From an error kind with a source](crate::errors::GDErrorKind::context) (any
/// type that implements `Into<Box<dyn std::error::Error + 'static>>)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
/// ```
///
/// Using the [new helper](crate::errors::GDError::new)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
/// ```
pub struct GDError {
pub kind: GDErrorKind,
pub source: Option<ErrorSource>,
pub backtrace: Option<std::backtrace::Backtrace>,
}
impl From<GDErrorKind> for GDError {
fn from(value: GDErrorKind) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind: value,
source: None,
backtrace,
}
}
}
impl PartialEq for GDError {
fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
}
impl Error for GDError {
fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(Box::as_ref) }
}
impl fmt::Debug for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "GDError{{ kind={:?}", self.kind)?;
if let Some(source) = &self.source {
writeln!(f, " source={:?}", source)?;
}
if let Some(backtrace) = &self.backtrace {
let bt = format!("{:#?}", backtrace);
writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
}
writeln!(f, "}}")?;
Ok(())
}
}
impl fmt::Display for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) }
}
impl GDError {
/// Create a new error (with automatic backtrace)
pub fn new(kind: GDErrorKind, source: Option<ErrorSource>) -> Self {
let backtrace = Some(std::backtrace::Backtrace::capture());
Self {
kind,
source,
backtrace,
}
}
/// Create a new error using any type that can be converted to an error
pub fn from_error<E: Into<Box<dyn std::error::Error + 'static>>>(kind: GDErrorKind, source: E) -> Self {
Self::new(kind, Some(source.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -61,29 +158,71 @@ mod tests {
// Testing Err variant of the GDResult type
#[test]
fn test_gdresult_err() {
let result: GDResult<u32> = Err(GDError::InvalidInput);
let result: GDResult<u32> = Err(GDErrorKind::InvalidInput.into());
assert!(result.is_err());
}
// Testing the Display trait for the GDError type
#[test]
fn test_display() {
let error = GDError::PacketOverflow;
assert_eq!(format!("{}", error), "PacketOverflow");
}
// Testing the Error trait for the GDError type
#[test]
fn test_error_trait() {
let error = GDError::PacketBad;
assert!(error.source().is_none());
}
// Testing cloning the GDError type
// Testing cloning the GDErrorKind type
#[test]
fn test_cloning() {
let error = GDError::BadGame(String::from("MyGame"));
let error = GDErrorKind::BadGame;
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
}
// test display GDError
#[test]
fn test_display() {
let err = GDErrorKind::BadGame.context("Rust is not a game");
let s = format!("{}", err);
println!("{}", s);
assert_eq!(
s,
"GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=<disabled>\n}\n"
);
}
// test error trait GDError
#[test]
fn test_error_trait() {
let source: Result<u32, _> = "nan".parse();
let source_err = source.unwrap_err();
let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
assert!(error_with_context.source().is_some());
assert_eq!(
format!("{}", error_with_context.source().unwrap()),
format!("{}", source_err)
);
let error_without_context: GDError = GDErrorKind::TypeParse.into();
assert!(error_without_context.source().is_none());
}
// Test creating GDError with GDError::new
#[test]
fn test_create_new() {
let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
assert!(error_from_new.backtrace.is_some());
assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
assert!(error_from_new.source.is_none());
}
// Test creating GDError with GDErrorKind::context
#[test]
fn test_create_context() {
let error_from_context = GDErrorKind::InvalidInput.context("test");
assert!(error_from_context.backtrace.is_some());
assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
assert!(error_from_context.source.is_some());
}
// Test creating GDError with From<GDErrorKind> for GDError
#[test]
fn test_create_into() {
let error_from_into: GDError = GDErrorKind::InvalidInput.into();
assert!(error_from_into.backtrace.is_some());
assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
assert!(error_from_into.source.is_none());
}
}