#![cfg_attr(
feature = "unstable-error-generic-member-access",
feature(error_generic_member_access)
)]
#![allow(
unused,
clippy::all,
reason = "derive-macro test fixtures intentionally trip style lints"
)]
use oopsie::{Contextual as _, NoSource, Oopsie};
use std::error::Error as _;
use std::io;
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum AppError {
NotFound { path: String },
IoFailed { source: io::Error, context: String },
Timeout { source: io::Error },
Config { host: String, port: u16 },
}
#[derive(Debug, Oopsie)]
#[oopsie(suffix)]
struct ParseError {
msg: String,
}
#[derive(Debug, Oopsie)]
#[oopsie(suffix)]
struct WrapError {
source: io::Error,
detail: String,
}
#[test]
fn leaf_enum_build() {
let err = NotFound {
path: "/tmp/missing",
}
.build();
assert!(matches!(err, AppError::NotFound { path } if path == "/tmp/missing"));
}
#[test]
fn leaf_enum_fail() {
let result: Result<(), AppError> = NotFound { path: "gone" }.fail();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, AppError::NotFound { path } if path == "gone"));
}
#[test]
fn source_enum_build_error() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
let err: AppError = IoFailed {
context: "reading config",
}
.build_error(io_err);
match &err {
AppError::IoFailed { source, context } => {
assert_eq!(source.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(context, "reading config");
}
other => panic!("expected IoFailed, got {other:?}"),
}
assert!(err.source().is_some());
}
#[test]
fn source_only_unit_selector() {
let io_err = io::Error::new(io::ErrorKind::TimedOut, "timed out");
let err: AppError = Timeout.build_error(io_err);
assert!(matches!(err, AppError::Timeout { .. }));
assert!(err.source().is_some());
}
#[test]
fn selector_into_bounds() {
let err = NotFound { path: "abc" }.build();
assert!(matches!(err, AppError::NotFound { path } if path == "abc"));
}
#[test]
fn multi_field_selector() {
let err = Config {
host: "localhost",
port: 8080u16,
}
.build();
match &err {
AppError::Config { host, port } => {
assert_eq!(host, "localhost");
assert_eq!(*port, 8080);
}
other => panic!("expected Config, got {other:?}"),
}
}
#[test]
fn struct_leaf_build_fail() {
let err = ParseOopsie {
msg: "unexpected token",
}
.build();
assert_eq!(err.msg, "unexpected token");
let result: Result<(), ParseError> = ParseOopsie { msg: "bad" }.fail();
assert!(result.is_err());
}
#[test]
fn struct_source_build_error() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
let err: WrapError = WrapOopsie {
detail: "while reading",
}
.build_error(io_err);
assert_eq!(err.detail, "while reading");
assert!(err.source().is_some());
assert_eq!(err.source().unwrap().to_string(), "file missing");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(strip_oopsies))]
enum ServiceError {
#[oopsie("connection error")]
ConnectionError { addr: String },
#[oopsie("timed out")]
TimeoutError,
#[oopsie("ok variant")]
NotAnIssue { msg: String },
}
#[test]
fn error_suffix_stripped_from_selector() {
let err = strip_oopsies::Connection { addr: "localhost" }.build();
assert!(matches!(err, ServiceError::ConnectionError { .. }));
assert_eq!(err.to_string(), "connection error");
let err = strip_oopsies::Timeout.build();
assert!(matches!(err, ServiceError::TimeoutError));
let err = strip_oopsies::NotAnIssue { msg: "fine" }.build();
assert!(matches!(err, ServiceError::NotAnIssue { .. }));
}
#[derive(Debug, Oopsie)]
#[oopsie(module(suffix_strip_oopsies), suffix)]
enum SuffixStripError {
#[oopsie("conn failed")]
ConnectionError { addr: String },
}
#[test]
fn error_suffix_stripped_with_oopsie_suffix() {
let err = suffix_strip_oopsies::ConnectionOopsie { addr: "db" }.build();
assert!(matches!(err, SuffixStripError::ConnectionError { .. }));
}
#[test]
fn leaf_selector_build_error_no_source() {
let err: AppError = NotFound { path: "x" }.build_error(NoSource);
assert!(matches!(err, AppError::NotFound { path } if path == "x"));
}
struct Meters(u16);
impl From<Meters> for u16 {
fn from(m: Meters) -> Self {
m.0
}
}
struct HostName(&'static str);
impl From<HostName> for String {
fn from(h: HostName) -> Self {
format!("host::{}", h.0)
}
}
#[test]
fn selector_accepts_custom_into_impls() {
let err = Config {
host: HostName("db"),
port: Meters(443),
}
.build();
match &err {
AppError::Config { host, port } => {
assert_eq!(host, "host::db");
assert_eq!(*port, 443);
}
other => panic!("expected Config, got {other:?}"),
}
}
#[test]
fn selector_derives_debug_clone_copy() {
let unit = Timeout;
assert_eq!(format!("{unit:?}"), "Timeout");
let unit_clone = unit.clone();
let unit2 = unit;
let _ = unit;
assert_eq!(format!("{unit2:?}"), "Timeout");
assert_eq!(format!("{unit_clone:?}"), "Timeout");
let sel = NotFound { path: "/tmp/x" };
let dbg = format!("{sel:?}");
assert!(dbg.contains("NotFound"), "debug was {dbg:?}");
assert!(dbg.contains("/tmp/x"), "debug was {dbg:?}");
let sel_clone = sel.clone();
let e1 = sel.build();
let e2 = sel.build();
assert!(matches!(e1, AppError::NotFound { .. }));
assert!(matches!(e2, AppError::NotFound { .. }));
let e3 = sel_clone.build();
assert!(matches!(e3, AppError::NotFound { .. }));
}
mod external {
use super::{AppError, Config};
use oopsie::Contextual as _;
pub fn build_via_pub_fields() -> AppError {
let sel = Config {
host: "remote",
port: 9000u16,
};
assert_eq!(sel.host, "remote");
assert_eq!(sel.port, 9000u16);
sel.build()
}
}
#[test]
fn selector_fields_are_pub() {
let sel = NotFound { path: "visible" };
let p: &str = sel.path;
assert_eq!(p, "visible");
let NotFound { path } = NotFound {
path: "destructured",
};
assert_eq!(path, "destructured");
let err = external::build_via_pub_fields();
assert!(matches!(err, AppError::Config { .. }));
}
#[test]
#[expect(
non_camel_case_types,
reason = "the raw-identifier variant under test is intentionally keyword-shaped"
)]
fn raw_identifier_variant_generates_raw_selector() {
#[oopsie::oopsie]
#[oopsie(module(false))]
enum KwError {
#[oopsie("looped")]
r#try,
}
let e = r#try.build();
assert_eq!(e.to_string(), "looped");
}
#[test]
fn capture_false_keeps_backtrace_field_on_selector() {
#[oopsie::oopsie]
#[oopsie(module(false))]
enum E {
#[oopsie("snap")]
Snap {
#[oopsie(capture = false)]
bt: oopsie::Backtrace,
msg: String,
},
}
let e = Snap {
bt: <oopsie::Backtrace as oopsie::Capturable>::capture(),
msg: "m",
}
.build();
assert_eq!(e.to_string(), "snap");
}
#[derive(Debug, Oopsie)]
#[oopsie("load failed: {what}")]
struct LoadError {
what: String,
}
#[test]
fn struct_default_module_form() {
let err = LoadOopsie { what: "config" }.build();
assert_eq!(err.to_string(), "load failed: config");
assert_eq!(err.what, "config");
}
#[oopsie::oopsie(traced)]
#[oopsie("decode failed: {stage}")]
struct DecodeError {
stage: String,
}
#[test]
fn struct_traced_default_module_form() {
let err = DecodeOopsie { stage: "header" }.fail::<()>();
let err = err.unwrap_err();
assert_eq!(err.to_string(), "decode failed: header");
assert!(oopsie::Diagnostic::oopsie_backtrace(&err).is_some());
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
#[oopsie("flat: {detail}")]
struct FlatError {
detail: String,
}
#[test]
fn struct_module_false_bare_selector() {
let err = FlatOopsie { detail: "x" }.build();
assert_eq!(err.to_string(), "flat: x");
}
#[derive(Debug, Oopsie)]
#[oopsie(module(false), suffix)]
#[oopsie("widget broke: {part}")]
struct Widget {
part: String,
}
#[test]
fn struct_suffix_disambiguates_bare_selector() {
let err = WidgetOopsie { part: "gear" }.build();
assert_eq!(err.to_string(), "widget broke: gear");
}
mod restricted {
#[expect(
clippy::redundant_pub_crate,
reason = "pub(crate) is the point: it exercises the restricted-visibility lift path"
)]
#[derive(Debug, oopsie::Oopsie)]
#[oopsie("scoped: {what}")]
pub(crate) struct ScopedError {
pub(crate) what: String,
}
}
#[test]
fn struct_pub_crate_vis_lifted_into_module() {
let err = restricted::ScopedOopsie { what: "y" }.build();
assert_eq!(err.to_string(), "scoped: y");
}