#![cfg(feature = "fancy")]
#![cfg_attr(
feature = "unstable-error-generic-member-access",
feature(error_generic_member_access)
)]
#![allow(
clippy::all,
reason = "integration test fixtures intentionally trip style lints"
)]
mod common;
use std::fmt;
use std::process::Termination as _;
use oopsie::trace_printer::{BacktraceFrame, BacktraceProvider, TracePrinter};
use oopsie::{Contextual as _, Report, RustBacktrace, Theme, get_theme, oopsie, set_theme};
use oopsie_core::{redact, snap_name};
#[oopsie(traced)]
#[oopsie("Test error: {message}")]
pub struct TestError {
message: String,
}
#[oopsie(traced)]
#[oopsie("Outer error")]
pub struct OuterError {
source: TestError,
}
fn strip_ansi(s: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(s)).unwrap()
}
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_basic() {
common::force_backtrace();
assert_eq!(
oopsie::backtrace::current(),
RustBacktrace::Enabled,
"backtrace override should be enabled for deterministic snapshots"
);
let error = TestOopsie {
message: "something failed",
}
.build();
let report = Report::new(error).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_basic"), report);
});
}
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_chain() {
common::force_backtrace();
let inner = TestOopsie {
message: "root cause",
}
.build();
let outer: OuterError = OuterOopsie.build_error(inner);
let report = Report::new(outer).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_chain"), report);
});
}
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_colored() {
common::force_backtrace();
let error = TestOopsie {
message: "colored test",
}
.build();
let report = Report::new(error).force_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_colored_stripped"), report);
});
}
#[test]
fn test_report_colored_emits_ansi() {
common::force_backtrace();
let error = TestOopsie {
message: "colored test",
}
.build();
let output = Report::new(error).force_colors().to_string();
assert!(
output.contains('\u{1b}'),
"force_colors() output should contain ANSI escapes, got: {output:?}"
);
assert!(
output.contains("\u{1b}[38;2;243;139;168;1m"),
"expected the bold Catppuccin-red `Error` header in colored output, got: {output:?}"
);
assert!(
strip_ansi(&output).contains("Error[report::TestError]: Test error: colored test"),
"stripping ANSI must leave the rendered text intact"
);
}
#[test]
fn report_theme_override_changes_output() {
let base = Report::new(TestOopsie { message: "themed" }.build()).force_colors();
let default_render = base.to_string();
let nord_render = base.with_theme(Theme::NORD).to_string();
assert_ne!(
default_render, nord_render,
"with_theme(NORD) must change the colored output vs the global default"
);
}
#[test]
fn report_follows_global_theme() {
let report = Report::new(TestOopsie { message: "themed" }.build()).force_colors();
let original = get_theme();
set_theme(Theme::NORD);
let nord_render = report.to_string();
set_theme(Theme::CATPPUCCIN_MOCHA);
let mocha_render = report.to_string();
set_theme(original);
assert_ne!(
nord_render, mocha_render,
"set_theme must change a report that carries no per-report override"
);
}
#[test]
fn test_report_from() {
let error = TestOopsie {
message: "from test",
}
.build();
let report: Report<_> = error.into();
assert!(report.to_string().contains("from test"));
}
#[oopsie(traced)]
#[oopsie("Something went wrong: {message}")]
#[oopsie(help = "Try restarting the service")]
pub struct ErrorWithHelp {
message: String,
}
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_with_help() {
common::force_backtrace();
let error = ErrorWithHelpOopsie {
message: "connection refused",
}
.build();
let report = Report::new(error).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_with_help"), report.to_string());
});
}
#[cfg(all(feature = "tracing", feature = "serde"))]
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_with_spantrace_serde() {
let error = common::make_error();
let report = Report::new(error).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_with_spantrace_serde"), report);
});
}
#[cfg(all(feature = "tracing", not(feature = "serde")))]
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_with_spantrace_no_serde() {
let error = common::make_error();
let report = Report::new(error).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_with_spantrace_no_serde"), report);
});
}
#[cfg(feature = "tracing")]
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_with_spantrace_debug() {
let error = common::make_error();
redact!(backtrace, {
insta::assert_snapshot!(
snap_name!("report_with_spantrace_debug"),
format!("{error:#}")
);
});
}
#[cfg(feature = "tracing")]
#[test]
fn test_report_colored_spantrace_renders_frames() {
let error = common::make_error();
let raw = Report::new(error).force_colors().to_string();
let stripped = strip_ansi(&raw);
assert!(
stripped.contains("1: sys::inner_function"),
"colored path renders 1-based span frames; got:\n{stripped}"
);
assert!(
stripped.contains("2: controller::outer_function"),
"colored path renders 1-based span frames; got:\n{stripped}"
);
assert!(
stripped.contains("with ") && stripped.contains("at "),
"span frames should render their fields (`with`) and location (`at`)"
);
let span_start = raw
.find("SPANTRACE")
.expect("colored output has a SPANTRACE header");
let after = &raw[span_start..];
let span_section = after.find("BACKTRACE").map_or(after, |i| &after[..i]);
assert!(
span_section.contains("\u{1b}[38;2;243;139;168m"),
"span frame names should be styled with the function-name color"
);
}
#[cfg(feature = "tracing")]
#[test]
fn test_report_renders_unsupported_spantrace() {
oopsie::backtrace::set_override(RustBacktrace::Disabled);
let error = {
let _guard = common::init_test_subscriber_without_error_layer();
let _span = tracing::info_span!("unsupported").entered();
TestOopsie {
message: "unsupported render",
}
.build()
};
let report = Report::new(error).no_colors().to_string();
insta::with_settings!({ filters => vec![(r"report\.rs:\d+:\d+", "report.rs:[LOC]")] }, {
insta::assert_snapshot!(report, @"
Error[report::TestError]: Test error: unsupported render
at crates/oopsie/tests/report.rs:[LOC]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
... span traces unsupported ...
");
});
}
#[test]
fn test_error_returns_some_when_err() {
let error = TestOopsie {
message: "accessor test",
}
.build();
let report = Report::new(error);
assert!(report.error().is_some());
}
#[test]
fn test_error_returns_none_when_ok() {
let report = Report::<TestError>::ok();
assert!(report.error().is_none());
}
#[test]
fn test_into_error_returns_some_when_err() {
let error = TestOopsie {
message: "into_error test",
}
.build();
let report = Report::new(error);
let err = report.into_error();
assert!(err.is_some());
assert!(err.unwrap().to_string().contains("into_error test"));
}
#[test]
fn test_into_error_returns_none_when_ok() {
let report = Report::<TestError>::ok();
assert!(report.into_error().is_none());
}
#[test]
fn test_debug_fmt_non_empty() {
let error = TestOopsie {
message: "debug test",
}
.build();
let report = Report::new(error).no_colors();
let debug_output = format!("{report:?}");
assert!(!debug_output.is_empty());
assert!(debug_output.contains("debug test"));
}
#[test]
fn test_display_ok_is_empty() {
let report = Report::<TestError>::ok();
let output = report.to_string();
assert!(output.is_empty());
}
#[test]
fn test_termination_report_ok() {
let _code = Report::<TestError>::ok().report();
}
#[test]
fn test_termination_report_error() {
let error = TestOopsie {
message: "termination test",
}
.build();
let _code = Report::new(error).no_colors().report();
}
#[test]
fn test_termination_report_honors_declared_exit_code() {
use oopsie::Diagnostic as _;
let error = ExitCodedOopsie { message: "boom" }.build();
assert_eq!(
error.oopsie_exit_code().map(core::num::NonZeroU8::get),
Some(78)
);
let _code = Report::new(error).no_colors().report();
}
#[test]
fn test_report_run_ok() {
let report = Report::<TestError>::run(|| Ok(()));
assert!(report.error().is_none());
assert!(report.to_string().is_empty());
}
#[test]
fn test_report_run_err() {
let report = Report::run(|| {
Err(TestOopsie {
message: "run failed",
}
.build())
});
assert!(report.error().is_some());
assert!(report.to_string().contains("run failed"));
}
#[test]
fn run_restores_prior_hook_even_on_unwind() {
use std::sync::atomic::{AtomicBool, Ordering};
static PRIOR_HOOK_FIRED: AtomicBool = AtomicBool::new(false);
std::panic::set_hook(Box::new(|_| {
PRIOR_HOOK_FIRED.store(true, Ordering::SeqCst);
}));
let _ = std::panic::catch_unwind(|| {
let _report: Report<TestError> = Report::run(|| panic!("boom"));
});
PRIOR_HOOK_FIRED.store(false, Ordering::SeqCst);
let _ = std::panic::catch_unwind(|| panic!("again"));
assert!(PRIOR_HOOK_FIRED.load(Ordering::SeqCst));
}
#[test]
fn run_concurrent_overlap_restores_prior_hook_after_last_exit() {
use std::sync::Barrier;
use std::sync::atomic::{AtomicBool, Ordering};
static PRIOR_HOOK_FIRED: AtomicBool = AtomicBool::new(false);
std::panic::set_hook(Box::new(|_| {
PRIOR_HOOK_FIRED.store(true, Ordering::SeqCst);
}));
let both_inside = Barrier::new(2);
let a_exited = Barrier::new(2);
std::thread::scope(|s| {
s.spawn(|| {
let _report: Report<TestError> = Report::run(|| {
both_inside.wait();
Ok(())
});
a_exited.wait();
});
s.spawn(|| {
let _report: Report<TestError> = Report::run(|| {
both_inside.wait();
a_exited.wait();
Ok(())
});
});
});
let _ = std::panic::catch_unwind(|| panic!("probe"));
assert!(
PRIOR_HOOK_FIRED.load(Ordering::SeqCst),
"prior hook must be restored after the last overlapping run exits"
);
}
#[test]
fn run_nested_restores_prior_hook() {
use std::sync::atomic::{AtomicBool, Ordering};
static PRIOR_HOOK_FIRED: AtomicBool = AtomicBool::new(false);
std::panic::set_hook(Box::new(|_| {
PRIOR_HOOK_FIRED.store(true, Ordering::SeqCst);
}));
let _outer: Report<TestError> = Report::run(|| {
let inner: Report<TestError> = Report::run(|| Ok(()));
assert!(inner.error().is_none());
Ok(())
});
let _ = std::panic::catch_unwind(|| panic!("probe"));
assert!(PRIOR_HOOK_FIRED.load(Ordering::SeqCst));
}
#[test]
fn test_no_colors_never_no_ansi() {
let error = TestOopsie {
message: "no_colors test",
}
.build();
let report = Report::new(error).no_colors();
let output = report.to_string();
assert!(output.contains("no_colors test"));
assert!(!output.contains("\x1b["));
}
struct FixedFrames(Vec<BacktraceFrame>);
impl BacktraceProvider for FixedFrames {
fn frames(&self) -> Vec<BacktraceFrame> {
self.0.iter().map(frame_clone).collect()
}
}
fn frame(name: &str, lineno: Option<u32>, colno: Option<u32>) -> BacktraceFrame {
BacktraceFrame::new(
0,
Some(name.into()),
Some(std::path::Path::new("src/lib.rs").into()),
lineno,
colno,
)
}
fn frame_clone(f: &BacktraceFrame) -> BacktraceFrame {
BacktraceFrame::new(f.ip, f.name.clone(), f.filename.clone(), f.lineno, f.colno)
}
struct RenderBacktrace<'a, P>(&'a TracePrinter, &'a P);
impl<P: BacktraceProvider> fmt::Display for RenderBacktrace<'_, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_backtrace(f, self.1)
}
}
fn render_backtrace<P: BacktraceProvider>(printer: &TracePrinter, provider: &P) -> String {
RenderBacktrace(printer, provider).to_string()
}
#[test]
fn test_backtrace_hidden_frame_count_message() {
let provider = FixedFrames(vec![
frame("std::backtrace_rs::backtrace::libunwind::trace", None, None),
frame("<std::backtrace::Backtrace>::create::inner", None, None),
frame("my_crate::function_a", Some(10), None),
frame("my_crate::function_b", Some(20), None),
frame("std::rt::lang_start_internal::invoke", None, None),
]);
let printer = TracePrinter::new().plain();
let output = render_backtrace(&printer, &provider);
let top_notice = "... 2 frames hidden ...";
let bottom_notice = "... 1 frames hidden ...";
assert!(
output.contains(top_notice),
"expected top hidden-frame notice (count 2), got:\n{output}"
);
assert!(
output.contains(bottom_notice),
"expected bottom hidden-frame notice (count 1), got:\n{output}"
);
assert!(output.contains("my_crate::function_a"));
assert!(output.contains("my_crate::function_b"));
assert!(
output.find(top_notice).unwrap() < output.find("my_crate::function_a").unwrap(),
"top notice must precede the first kept frame, got:\n{output}"
);
assert!(
output.rfind(bottom_notice).unwrap() > output.find("my_crate::function_b").unwrap(),
"bottom notice must follow the last kept frame, got:\n{output}"
);
assert!(
!output.contains("lang_start_internal"),
"runtime-init frame should be filtered out"
);
}
#[test]
fn test_backtrace_no_hidden_frames_no_message() {
let provider = FixedFrames(vec![
frame("my_crate::function_a", Some(10), None),
frame("my_crate::function_b", Some(20), None),
]);
let printer = TracePrinter::new().plain();
let output = render_backtrace(&printer, &provider);
assert!(
!output.contains("frames hidden"),
"no frames were trimmed, so no notice should render, got:\n{output}"
);
}
#[test]
fn test_backtrace_frame_renders_colno() {
let provider = FixedFrames(vec![frame("my_crate::function_a", Some(42), Some(7))]);
let printer = TracePrinter::new().plain();
let output = render_backtrace(&printer, &provider);
assert!(
output.contains("src/lib.rs:42:7"),
"expected line:col `:42:7` in output, got:\n{output}"
);
}
#[test]
fn test_backtrace_frame_no_colno_renders_only_lineno() {
let provider = FixedFrames(vec![frame("my_crate::function_a", Some(42), None)]);
let printer = TracePrinter::new().plain();
let output = render_backtrace(&printer, &provider);
assert!(
output.contains("src/lib.rs:42"),
"expected `:42` in output, got:\n{output}"
);
assert!(
!output.contains("src/lib.rs:42:"),
"no column number means no trailing `:`, got:\n{output}"
);
}
#[test]
fn test_trace_printer_with_filter_custom_filter() {
let provider = FixedFrames(vec![
frame("keep::alpha", Some(1), None),
frame("drop::beta", Some(2), None),
frame("keep::gamma", Some(3), None),
]);
let printer = TracePrinter::with_filter(|frames: &mut [Option<&BacktraceFrame>]| {
for slot in frames.iter_mut() {
if slot.is_some_and(|frame| {
frame
.name
.as_deref()
.is_some_and(|name| name.starts_with("drop::"))
}) {
*slot = None;
}
}
});
let output = render_backtrace(&printer, &provider);
assert!(output.contains("keep::alpha"), "got:\n{output}");
assert!(output.contains("keep::gamma"), "got:\n{output}");
assert!(
!output.contains("drop::beta"),
"custom filter should remove `drop::beta`, got:\n{output}"
);
let alpha = output.find("keep::alpha").unwrap();
let notice = output.find("... 1 frames hidden ...").unwrap();
let gamma = output.find("keep::gamma").unwrap();
assert!(
alpha < notice && notice < gamma,
"notice must sit at the gap, got:\n{output}"
);
}
#[test]
fn test_trace_printer_add_frame_filter_composes() {
let provider = FixedFrames(vec![
frame("std::backtrace_rs::backtrace::libunwind::trace", None, None),
frame("keep::alpha", Some(1), None),
frame("drop::beta", Some(2), None),
frame("std::rt::lang_start_internal::invoke", None, None),
]);
let printer = TracePrinter::new().plain().add_frame_filter(|frames| {
for slot in frames.iter_mut() {
if slot.is_some_and(|frame| {
frame
.name
.as_deref()
.is_some_and(|name| name.starts_with("drop::"))
}) {
*slot = None;
}
}
});
let output = render_backtrace(&printer, &provider);
assert!(output.contains("keep::alpha"), "got:\n{output}");
assert!(
!output.contains("libunwind"),
"default filter (under) should still trim capture frames, got:\n{output}"
);
assert!(
!output.contains("lang_start_internal"),
"default filter (under) should still trim runtime frames, got:\n{output}"
);
assert!(
!output.contains("drop::beta"),
"added filter (over) should remove `drop::beta`, got:\n{output}"
);
assert!(
output.contains("... 1 frames hidden ..."),
"expected top notice (count 1), got:\n{output}"
);
assert!(
output.contains("... 2 frames hidden ..."),
"expected bottom notice (count 2), got:\n{output}"
);
}
#[test]
fn test_trace_printer_unfiltered_keeps_all_frames() {
let frames = vec![
frame("std::backtrace_rs::backtrace::libunwind::trace", None, None),
frame("my_crate::function_a", Some(10), None),
frame("std::rt::lang_start_internal::invoke", None, None),
];
let unfiltered_out = render_backtrace(
&TracePrinter::unfiltered().plain(),
&FixedFrames(frames.iter().map(frame_clone).collect()),
);
let filtered_out = render_backtrace(
&TracePrinter::new().plain(),
&FixedFrames(frames.iter().map(frame_clone).collect()),
);
assert!(
unfiltered_out.contains("libunwind"),
"got:\n{unfiltered_out}"
);
assert!(
unfiltered_out.contains("lang_start_internal"),
"got:\n{unfiltered_out}"
);
assert!(
!unfiltered_out.contains("frames hidden"),
"unfiltered must never report hidden frames, got:\n{unfiltered_out}"
);
assert!(!filtered_out.contains("libunwind"), "got:\n{filtered_out}");
assert_eq!(
filtered_out.matches("frames hidden").count(),
2,
"expected a top and a bottom hidden-frames notice, got:\n{filtered_out}"
);
assert!(
filtered_out.contains("... 1 frames hidden ..."),
"got:\n{filtered_out}"
);
assert!(unfiltered_out.len() > filtered_out.len());
}
#[test]
fn test_report_backtrace_full_renders_without_hidden_notice() {
common::force_backtrace();
oopsie::backtrace::set_override(RustBacktrace::Full);
let error = TestOopsie {
message: "full backtrace",
}
.build();
let output = Report::new(error).no_colors().to_string();
oopsie::backtrace::set_override(RustBacktrace::Enabled);
assert!(
output.contains("BACKTRACE"),
"Full mode should still render a backtrace section, got:\n{output}"
);
assert!(
!output.contains("frames hidden"),
"unfiltered (Full) render must not emit a hidden-frames notice, got:\n{output}"
);
}
#[cfg(feature = "unstable-try-trait-v2")]
#[test]
fn test_report_from_residual_question_mark() {
fn fallible(fail: bool) -> Result<u8, TestError> {
if fail {
TestOopsie {
message: "residual failure",
}
.fail()
} else {
Ok(7)
}
}
fn run(fail: bool) -> Report<TestError> {
let value = fallible(fail)?;
assert_eq!(value, 7);
Report::ok()
}
common::force_backtrace();
let err_report = run(true);
assert!(err_report.error().is_some());
assert!(
err_report
.into_error()
.unwrap()
.to_string()
.contains("residual failure")
);
let ok_report = run(false);
assert!(ok_report.error().is_none());
assert!(ok_report.to_string().is_empty());
}
#[derive(Debug, oopsie::Oopsie)]
#[oopsie(module(false))]
#[oopsie(
display("leaf failed: {what}"),
code = "leaf::failed",
help = "turn it off and on again"
)]
pub struct LeafError {
what: String,
}
#[derive(Debug, oopsie::Oopsie)]
#[oopsie(module(false))]
#[oopsie(transparent)]
pub struct TransparentRootError {
source: LeafError,
}
#[test]
fn test_report_transparent_forwards_code_and_help() {
let leaf = LeafOopsie { what: "disk" }.build();
let root: TransparentRootError = TransparentRootError::from(leaf);
let report = Report::new(root).no_colors();
let rendered = strip_ansi(&report.to_string());
assert_eq!(
rendered,
"\
Error[leaf::failed]: leaf failed: disk
╰─▶ leaf failed: disk
help: turn it off and on again
"
);
}
#[derive(Debug)]
struct Cyclic;
impl std::fmt::Display for Cyclic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("cyclic")
}
}
impl std::error::Error for Cyclic {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self)
}
}
impl oopsie::Diagnostic for Cyclic {}
#[test]
fn cyclic_source_chain_terminates_with_truncation_note() {
let rendered = oopsie::Report::new(Cyclic).no_colors().to_string();
assert!(rendered.contains("source chain truncated"), "{rendered}");
}
#[derive(Debug)]
struct PlainWrapper(TestError);
impl fmt::Display for PlainWrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("plain wrapper")
}
}
impl std::error::Error for PlainWrapper {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
impl oopsie::Diagnostic for PlainWrapper {}
#[test]
fn report_does_not_search_chain_for_traces() {
common::force_backtrace();
let traced = TestOopsie { message: "root" }.build();
let rendered = Report::new(PlainWrapper(traced)).no_colors().to_string();
assert!(rendered.contains("╰─▶"), "chain messages still render");
assert!(
!rendered.contains("BACKTRACE"),
"no chain search for traces:\n{rendered}"
);
}
#[test]
fn test_report_welp_message_free_wraps_io_error() {
use oopsie::WelpResultExt as _;
oopsie::backtrace::set_override(RustBacktrace::Disabled);
let result: Result<(), std::io::Error> = Err(std::io::Error::other("connection reset by peer"));
let welp = result.welp().unwrap_err();
let report = Report::new(welp).no_colors();
insta::with_settings!({ filters => vec![(r"report\.rs:\d+:\d+", "report.rs:[LOC]")] }, {
insta::assert_snapshot!(snap_name!("report_welp_message_free"), report);
});
}
#[cfg(feature = "unstable-error-generic-member-access")]
#[test]
fn termination_prefers_provider_exit_code_over_declarative() {
use std::process::ExitCode;
#[derive(Debug)]
struct DualExit(ExitCode);
impl fmt::Display for DualExit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("dual exit")
}
}
impl std::error::Error for DualExit {
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
request.provide_ref::<ExitCode>(&self.0);
}
}
impl oopsie::Diagnostic for DualExit {
fn oopsie_exit_code(&self) -> Option<core::num::NonZeroU8> {
core::num::NonZeroU8::new(5)
}
}
let code = Report::new(DualExit(ExitCode::from(99)))
.no_colors()
.report();
assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::from(99)));
}
#[test]
fn termination_uses_declarative_exit_code_when_no_provider() {
use std::process::ExitCode;
#[derive(Debug)]
struct DeclaredExit;
impl fmt::Display for DeclaredExit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("declared exit")
}
}
impl std::error::Error for DeclaredExit {}
impl oopsie::Diagnostic for DeclaredExit {
fn oopsie_exit_code(&self) -> Option<core::num::NonZeroU8> {
core::num::NonZeroU8::new(42)
}
}
let code = Report::new(DeclaredExit).no_colors().report();
assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::from(42)));
}
#[oopsie(traced)]
#[oopsie(exit_code = 78)]
#[oopsie("Exit-coded error")]
pub struct ExitCodedError {
message: String,
}
#[oopsie(traced)]
#[oopsie("forward leaf: {msg}")]
pub struct FwdLeafError {
msg: String,
}
#[oopsie(traced)]
#[oopsie("forward wrap")]
pub struct FwdWrapError {
#[oopsie(forward)]
source: FwdLeafError,
}
#[test]
#[test_with::env(OOPSIE_BACKTRACE_SNAPSHOT_TESTS)]
fn test_report_forward_chain() {
common::force_backtrace();
let leaf = FwdLeafOopsie { msg: "root cause" }.build();
let wrap: FwdWrapError = FwdWrapOopsie.build_error(leaf);
let report = Report::new(wrap).no_colors();
redact!(backtrace, {
insta::assert_snapshot!(snap_name!("report_forward_chain"), report);
});
}