#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
use core::panic::Location;
use core::{
convert::Infallible,
ops::{FromResidual, Try},
};
use crate::Report;
#[derive(Debug)]
enum BombState {
Panic,
Warn(
#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
&'static Location<'static>,
),
Defused,
}
impl Default for BombState {
#[track_caller]
fn default() -> Self {
Self::Warn(
#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
Location::caller(),
)
}
}
#[derive(Debug, Default)]
struct Bomb(BombState);
impl Bomb {
const fn panic() -> Self {
Self(BombState::Panic)
}
#[track_caller]
const fn warn() -> Self {
Self(BombState::Warn(
#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
Location::caller(),
))
}
const fn defuse(&mut self) {
self.0 = BombState::Defused;
}
}
impl Drop for Bomb {
fn drop(&mut self) {
if !cfg!(debug_assertions) {
return;
}
match self.0 {
BombState::Panic => panic!("ReportSink was dropped without being consumed"),
#[cfg_attr(not(feature = "tracing"), expect(clippy::print_stderr))]
#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
BombState::Warn(location) => {
#[cfg(feature = "tracing")]
tracing::warn!(
target: "error_stack",
%location,
"`ReportSink` was dropped without being consumed"
);
#[cfg(not(feature = "tracing"))]
eprintln!("`ReportSink` was dropped without being consumed at {location}");
}
_ => {}
}
}
}
#[must_use]
pub struct ReportSink<C> {
report: Option<Report<[C]>>,
bomb: Bomb,
}
impl<C> ReportSink<C> {
#[track_caller]
pub const fn new() -> Self {
Self {
report: None,
bomb: Bomb::warn(),
}
}
pub const fn new_armed() -> Self {
Self {
report: None,
bomb: Bomb::panic(),
}
}
#[track_caller]
pub fn append(&mut self, report: impl Into<Report<[C]>>) {
let report = report.into();
match self.report.as_mut() {
Some(existing) => existing.append(report),
None => self.report = Some(report),
}
}
#[track_caller]
pub fn capture(&mut self, error: impl Into<Report<C>>) {
let report = error.into();
match self.report.as_mut() {
Some(existing) => existing.push(report),
None => self.report = Some(report.into()),
}
}
#[track_caller]
pub fn attempt<T, R>(&mut self, result: Result<T, R>) -> Option<T>
where
R: Into<Report<C>>,
{
match result {
Ok(value) => Some(value),
Err(error) => {
self.capture(error);
None
}
}
}
pub fn finish(mut self) -> Result<(), Report<[C]>> {
self.bomb.defuse();
self.report.map_or(Ok(()), Err)
}
pub fn finish_with<T>(mut self, ok: impl FnOnce() -> T) -> Result<T, Report<[C]>> {
self.bomb.defuse();
self.report.map_or_else(|| Ok(ok()), Err)
}
pub fn finish_default<T: Default>(mut self) -> Result<T, Report<[C]>> {
self.bomb.defuse();
self.report.map_or_else(|| Ok(T::default()), Err)
}
pub fn finish_ok<T>(mut self, ok: T) -> Result<T, Report<[C]>> {
self.bomb.defuse();
self.report.map_or(Ok(ok), Err)
}
}
impl<C> Default for ReportSink<C> {
fn default() -> Self {
Self::new()
}
}
#[cfg(nightly)]
impl<C> FromResidual for ReportSink<C> {
fn from_residual(residual: <Self as Try>::Residual) -> Self {
match residual {
Err(report) => Self {
report: Some(report),
bomb: Bomb::default(),
},
}
}
}
#[cfg(nightly)]
impl<C> Try for ReportSink<C> {
type Output = ();
type Residual = Result<Infallible, Report<[C]>>;
fn from_output((): ()) -> Self {
Self {
report: None,
bomb: Bomb::default(),
}
}
fn branch(mut self) -> core::ops::ControlFlow<Self::Residual, Self::Output> {
self.bomb.defuse();
self.report.map_or(
core::ops::ControlFlow::Continue(()), |report| core::ops::ControlFlow::Break(Err(report)),
)
}
}
#[cfg(test)]
mod test {
use alloc::collections::BTreeSet;
use core::fmt::Display;
use crate::{Report, sink::ReportSink};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct TestError(u8);
impl Display for TestError {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fmt.write_str("TestError(")?;
core::fmt::Display::fmt(&self.0, fmt)?;
fmt.write_str(")")
}
}
impl core::error::Error for TestError {}
#[test]
fn add_single() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
let report = sink.finish().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&TestError(0)));
}
#[test]
fn add_multiple() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
let report = sink.finish().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn capture_single() {
let mut sink = ReportSink::new();
sink.capture(TestError(0));
let report = sink.finish().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&TestError(0)));
}
#[test]
fn capture_multiple() {
let mut sink = ReportSink::new();
sink.capture(TestError(0));
sink.capture(TestError(1));
let report = sink.finish().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn new_does_not_panic() {
let _sink: ReportSink<TestError> = ReportSink::new();
}
#[cfg(nightly)]
#[test]
fn try_none() {
fn sink() -> Result<(), Report<[TestError]>> {
let sink = ReportSink::new();
sink?;
Ok(())
}
sink().expect("should not have failed");
}
#[cfg(nightly)]
#[test]
fn try_single() {
fn sink() -> Result<(), Report<[TestError]>> {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink?;
Ok(())
}
let report = sink().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&TestError(0)));
}
#[cfg(nightly)]
#[test]
fn try_multiple() {
fn sink() -> Result<(), Report<[TestError]>> {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
sink?;
Ok(())
}
let report = sink().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[cfg(nightly)]
#[test]
fn try_arbitrary_return() {
fn sink() -> Result<u8, Report<[TestError]>> {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink?;
Ok(8)
}
let report = sink().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&TestError(0)));
}
#[test]
#[should_panic(expected = "without being consumed")]
fn panic_on_unused() {
#[expect(clippy::unnecessary_wraps)]
fn sink() -> Result<(), Report<[TestError]>> {
let mut sink = ReportSink::new_armed();
sink.append(Report::new(TestError(0)));
Ok(())
}
let _result = sink();
}
#[test]
fn panic_on_unused_with_defuse() {
fn sink() -> Result<(), Report<[TestError]>> {
let mut sink = ReportSink::new_armed();
sink.append(Report::new(TestError(0)));
sink?;
Ok(())
}
let report = sink().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 1);
assert!(contexts.contains(&TestError(0)));
}
#[test]
fn finish() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
let report = sink.finish().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn finish_ok() {
let sink: ReportSink<TestError> = ReportSink::new();
sink.finish().expect("should have succeeded");
}
#[test]
fn finish_with() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
let report = sink.finish_with(|| 8).expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn finish_with_ok() {
let sink: ReportSink<TestError> = ReportSink::new();
let value = sink.finish_with(|| 8).expect("should have succeeded");
assert_eq!(value, 8);
}
#[test]
fn finish_default() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
let report = sink.finish_default::<u8>().expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn finish_default_ok() {
let sink: ReportSink<TestError> = ReportSink::new();
let value = sink.finish_default::<u8>().expect("should have succeeded");
assert_eq!(value, 0);
}
#[test]
fn finish_with_value() {
let mut sink = ReportSink::new();
sink.append(Report::new(TestError(0)));
sink.append(Report::new(TestError(1)));
let report = sink.finish_ok(8).expect_err("should have failed");
let contexts: BTreeSet<_> = report.current_contexts().collect();
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&TestError(0)));
assert!(contexts.contains(&TestError(1)));
}
#[test]
fn finish_with_value_ok() {
let sink: ReportSink<TestError> = ReportSink::new();
let value = sink.finish_ok(8).expect("should have succeeded");
assert_eq!(value, 8);
}
}