#![cfg_attr(
feature = "unstable-error-generic-member-access",
feature(error_generic_member_access)
)]
#![allow(
unused,
clippy::all,
clippy::pedantic,
reason = "derive-macro test fixtures intentionally trip style lints"
)]
use oopsie::{Contextual as _, Diagnostic as _, Oopsie};
use std::error::Error as _;
use std::fmt;
use std::io;
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum PayloadError<T: fmt::Debug> {
#[oopsie("payload was {payload:?}")]
Wrap { payload: T },
#[oopsie("plain: {detail}")]
Other { detail: String },
}
#[test]
fn type_param_in_display_field() {
let err: PayloadError<u32> = Wrap { payload: 7u32 }.build();
assert_eq!(err.to_string(), "payload was 7");
assert!(matches!(err, PayloadError::Wrap { payload } if payload == 7));
}
#[test]
fn non_generic_selector_on_generic_enum() {
let err: PayloadError<u32> = Other { detail: "x" }.build();
assert_eq!(err.to_string(), "plain: x");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum SourcedError<E: std::error::Error + 'static> {
#[oopsie("db query failed")]
Db { source: E, query: String },
#[oopsie("note: {text}")]
Annotation { text: String },
}
#[test]
fn param_only_in_source() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "missing");
let err: SourcedError<io::Error> = Db {
query: "SELECT 1".to_owned(),
}
.build_error(io_err);
assert_eq!(err.to_string(), "db query failed");
let src = err.source().expect("Db exposes its source");
assert_eq!(src.to_string(), "missing");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum BorrowedError<'a> {
#[oopsie("borrowed note: {note}")]
Note { note: &'a str },
}
#[test]
fn lifetime_param_borrowed_field() {
let text = String::from("transient");
let err: BorrowedError<'_> = Note {
note: text.as_str(),
}
.build();
assert_eq!(err.to_string(), "borrowed note: transient");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum ArrayError<const N: usize> {
#[oopsie("fixed buffer of {} bytes", N)]
Buffer { data: [u8; N] },
}
#[test]
fn const_param_in_field() {
let err: ArrayError<4> = Buffer { data: [1, 2, 3, 4] }.build();
assert_eq!(err.to_string(), "fixed buffer of 4 bytes");
assert!(matches!(err, ArrayError::Buffer { data } if data == [1, 2, 3, 4]));
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum WhereError<T>
where
T: fmt::Display + fmt::Debug,
{
#[oopsie("value: {value}")]
Value { value: T },
}
#[test]
fn where_clause_on_generic_enum() {
let err: WhereError<i64> = Value { value: -3 }.build();
assert_eq!(err.to_string(), "value: -3");
}
#[derive(Debug, Oopsie)]
struct ParseFailure<T: fmt::Debug> {
token: T,
line: usize,
}
#[test]
fn generic_struct_module_selector() {
let err: ParseFailure<char> = ParseFailureOopsie {
token: '{',
line: 12usize,
}
.build();
assert!(matches!(err, ParseFailure { token, line } if token == '{' && line == 12));
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum Wrapper<E: std::error::Error + 'static> {
#[oopsie(display("wrapped"), transparent)]
Inner { source: E },
}
#[test]
fn transparent_generic_wrapper() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
let err: Wrapper<io::Error> = Wrapper::from(io_err);
assert_eq!(err.to_string(), "wrapped");
let src = err.source().expect("transparent exposes source");
assert_eq!(src.to_string(), "denied");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum BatchError<T: fmt::Debug> {
#[oopsie("batch of {count} items")]
Batch { items: Vec<T>, count: usize },
}
#[test]
fn selector_field_split_concrete_and_into() {
let err: BatchError<u8> = Batch {
items: vec![1u8, 2, 3],
count: 3u8,
}
.build();
assert_eq!(err.to_string(), "batch of 3 items");
}
#[cfg(feature = "tracing")]
mod traced_generics {
use super::*;
#[oopsie::oopsie(traced)]
#[oopsie(module(false))]
pub enum TracedError<T: fmt::Debug + 'static> {
#[oopsie("traced payload {payload:?}")]
Payload { payload: T },
}
#[test]
fn traced_generic_enum_builds_and_displays() {
let err: TracedError<String> = Payload {
payload: "hi".to_owned(),
}
.build();
assert_eq!(err.to_string(), "traced payload \"hi\"");
}
#[test]
fn traced_generic_diagnostic_accessors() {
let err: TracedError<u8> = Payload { payload: 9u8 }.build();
let _ = err.oopsie_backtrace();
let code = err.oopsie_error_code().expect("auto code present");
assert!(code.as_str().contains("TracedError::Payload"), "{code:?}");
}
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum CacheError<K: fmt::Debug, E: std::error::Error + 'static> {
#[oopsie("query for {key:?} failed")]
Query { source: E, key: K },
#[oopsie("decode error")]
Decode { detail: String },
}
#[test]
fn multi_param_sourced_variant_references_all_params() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "miss");
let err: CacheError<&str, io::Error> = Query { key: "users:7" }.build_error(io_err);
assert_eq!(err.to_string(), "query for \"users:7\" failed");
assert_eq!(
err.source().expect("Query exposes source").to_string(),
"miss"
);
}
#[derive(Debug, Oopsie)]
struct LoadError<E: std::error::Error + 'static> {
source: E,
path: String,
}
#[test]
fn generic_struct_with_source() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "nope");
let err: LoadError<io::Error> = LoadOopsie {
path: "/etc/conf".to_owned(),
}
.build_error(io_err);
assert_eq!(err.path, "/etc/conf");
assert_eq!(err.source().unwrap().to_string(), "nope");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum InlineBoundError<T: fmt::Debug + Clone, U: From<T> + fmt::Debug> {
#[oopsie("converted {value:?}")]
InlineConvert { value: U },
#[oopsie("seed {seed:?}")]
InlineSeed { seed: T },
}
#[test]
fn inline_inter_parameter_bound_leaf() {
let err: InlineBoundError<u8, u16> = InlineConvert { value: 300u16 }.build();
assert_eq!(err.to_string(), "converted 300");
assert!(matches!(err, InlineBoundError::InlineConvert { value } if value == 300));
let res: Result<(), InlineBoundError<u8, u16>> = InlineConvert { value: 7u16 }.fail();
assert_eq!(res.unwrap_err().to_string(), "converted 7");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum WhereBoundError<T, U>
where
T: fmt::Debug + Clone,
U: From<T> + fmt::Debug,
{
#[oopsie("converted {value:?}")]
WhereConvert { value: U },
#[oopsie("seed {seed:?}")]
WhereSeed { seed: T },
}
#[test]
fn where_clause_inter_parameter_bound_leaf() {
let err: WhereBoundError<u8, u16> = WhereConvert { value: 256u16 }.build();
assert_eq!(err.to_string(), "converted 256");
let err: WhereBoundError<u8, u16> = WhereSeed { seed: 9u8 }.build();
assert_eq!(err.to_string(), "seed 9");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum SourcedInterBound<
T: fmt::Debug + Clone,
U: From<T> + fmt::Debug,
E: std::error::Error + 'static,
> {
#[oopsie("converted {value:?} from {seed:?}")]
SourcedConvert { source: E, value: U, seed: T },
}
#[test]
fn sourced_inter_parameter_bound_all_params() {
let io_err = io::Error::new(io::ErrorKind::Other, "boom");
let err: SourcedInterBound<u8, u16, io::Error> = SourcedConvert {
value: 42u16,
seed: 1u8,
}
.build_error(io_err);
assert_eq!(err.to_string(), "converted 42 from 1");
assert_eq!(err.source().expect("exposes source").to_string(), "boom");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum FreeLifetimeError<'a, T: fmt::Debug> {
#[oopsie("ref: {note}")]
RefNote { note: &'a str },
#[oopsie("detached: {value:?}")]
Detached { value: T },
}
#[test]
fn leaf_free_of_outer_lifetime() {
let err: FreeLifetimeError<'_, u32> = Detached { value: 5u32 }.build();
assert_eq!(err.to_string(), "detached: 5");
let res: Result<(), FreeLifetimeError<'_, u32>> = Detached { value: 7u32 }.fail();
assert_eq!(res.unwrap_err().to_string(), "detached: 7");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum MixedFreeError<'a, T: fmt::Debug, const N: usize> {
#[oopsie("anchor {note} x{}", N)]
Anchor { note: &'a str, data: [u8; N] },
#[oopsie("free {value:?}")]
FreeLeaf { value: T },
}
#[test]
fn leaf_free_of_lifetime_and_const() {
let err: MixedFreeError<'_, u32, 4> = FreeLeaf { value: 9u32 }.build();
assert_eq!(err.to_string(), "free 9");
let res: Result<(), MixedFreeError<'_, u32, 4>> = FreeLeaf { value: 1u32 }.fail();
assert_eq!(res.unwrap_err().to_string(), "free 1");
}