async-safe-defer 0.2.0

Runtime-independent LIFO cleanup scopes for synchronous and asynchronous Rust
Documentation
use async_safe_defer::{defer, guard, DeferGuard, ScopeGuard, Strategy};
use std::{
    cell::{Cell, RefCell},
    panic::{catch_unwind, AssertUnwindSafe},
};

#[cfg(feature = "std")]
use async_safe_defer::{defer_on_success, defer_on_unwind, guard_on_success, guard_on_unwind};

enum Never {}

impl Strategy for Never {
    fn should_run() -> bool {
        false
    }
}

enum Panics {}

impl Strategy for Panics {
    fn should_run() -> bool {
        panic!("policy failed")
    }
}

struct DropProbe<'a>(&'a Cell<usize>);

impl Drop for DropProbe<'_> {
    fn drop(&mut self) {
        self.0.set(self.0.get() + 1);
    }
}

struct PanickingDropProbe<'a>(&'a Cell<usize>);

impl Drop for PanickingDropProbe<'_> {
    fn drop(&mut self) {
        self.0.set(self.0.get() + 1);
        panic!("drop failed");
    }
}

#[test]
fn guard_runs_when_dropped() {
    let value = Cell::new(0);
    {
        let _guard = defer(|| value.set(42));
        assert_eq!(value.get(), 0);
    }
    assert_eq!(value.get(), 42);
}

#[test]
fn defer_guard_keeps_single_parameter_type_usage() {
    let guard: DeferGuard<_> = defer(|| {});
    drop(guard);
}

#[test]
fn guard_can_be_disarmed() {
    let value = Cell::new(0);
    let action = defer(|| value.set(42)).disarm();

    assert_eq!(value.get(), 0);
    action();
    assert_eq!(value.get(), 42);
}

#[test]
fn guard_can_run_immediately() {
    let value = Cell::new(0);
    DeferGuard::new(|| value.set(42)).run_now();
    assert_eq!(value.get(), 42);
}

#[test]
fn custom_strategy_can_skip_deferred_action() {
    let calls = Cell::new(0);
    let guard = DeferGuard::<_, Never>::with_strategy(|| calls.set(calls.get() + 1));

    drop(guard);
    assert_eq!(calls.get(), 0);
}

#[test]
fn immediate_and_disarmed_guards_do_not_evaluate_strategy() {
    let calls = Cell::new(0);
    DeferGuard::<_, Panics>::with_strategy(|| calls.set(calls.get() + 1)).run_now();
    let action = DeferGuard::<_, Panics>::with_strategy(|| calls.set(calls.get() + 1)).disarm();
    action();

    assert_eq!(calls.get(), 2);
}

#[test]
fn captured_action_is_dropped_when_strategy_panics() {
    let drops = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let probe = DropProbe(&drops);
        drop(DeferGuard::<_, Panics>::with_strategy(move || drop(probe)));
    }));

    assert!(outcome.is_err());
    assert_eq!(drops.get(), 1);
}

#[test]
fn panicking_deferred_action_runs_only_once() {
    let calls = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        drop(defer(|| {
            calls.set(calls.get() + 1);
            panic!("action failed");
        }));
    }));

    assert!(outcome.is_err());
    assert_eq!(calls.get(), 1);
}

#[test]
fn macro_runs_actions_in_lifo_order() {
    let order = RefCell::new(Vec::new());
    {
        async_safe_defer::defer!(order.borrow_mut().push(1));
        async_safe_defer::defer!(order.borrow_mut().push(2));
    }
    assert_eq!(*order.borrow(), vec![2, 1]);
}

#[test]
fn macro_supports_move_capture() {
    let output = RefCell::new(None);
    {
        let value = String::from("owned");
        let output = &output;
        async_safe_defer::defer!(move {
            output.replace(Some(value));
        });
    }
    assert_eq!(output.into_inner().as_deref(), Some("owned"));
}

#[test]
fn guard_runs_during_unwind() {
    let ran = Cell::new(false);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let _guard = defer(|| ran.set(true));
        panic!("trigger unwind");
    }));

    assert!(outcome.is_err());
    assert!(ran.get());
}

#[test]
fn debug_output_reports_armed_state() {
    let guard = defer(|| {});
    assert_eq!(format!("{guard:?}"), "DeferGuard { armed: true }");
}

#[test]
fn scope_guard_exposes_and_passes_its_mutated_value() {
    let output = RefCell::new(Vec::new());
    {
        let mut guarded = guard(vec![1], |value| {
            output.replace(value);
        });
        guarded.push(2);
        assert_eq!(&*guarded, &[1, 2]);
    }

    assert_eq!(output.into_inner(), vec![1, 2]);
}

#[test]
fn scope_guard_into_inner_skips_action() {
    let calls = Cell::new(0);
    let mut guarded = guard(String::from("value"), |_| calls.set(calls.get() + 1));
    guarded.push('!');

    assert_eq!(guarded.into_inner(), "value!");
    assert_eq!(calls.get(), 0);
}

#[test]
fn scope_guard_into_parts_returns_value_and_action() {
    let calls = Cell::new(0);
    let guarded = guard(41, |value| calls.set(value));
    let (value, action) = guarded.into_parts();

    assert_eq!(value, 41);
    assert_eq!(calls.get(), 0);
    action(value + 1);
    assert_eq!(calls.get(), 42);
}

#[test]
fn scope_guard_run_now_does_not_evaluate_strategy() {
    let output = Cell::new(0);
    ScopeGuard::<_, _, Panics>::with_strategy(42, |value| output.set(value)).run_now();
    assert_eq!(output.get(), 42);
}

#[test]
fn value_is_dropped_if_action_drop_panics_during_into_inner() {
    let value_drops = Cell::new(0);
    let action_drops = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let value = DropProbe(&value_drops);
        let captured = PanickingDropProbe(&action_drops);
        let guarded = guard(value, move |_| drop(captured));
        let _value = guarded.into_inner();
    }));

    assert!(outcome.is_err());
    assert_eq!(value_drops.get(), 1);
    assert_eq!(action_drops.get(), 1);
}

#[test]
fn value_is_dropped_if_skipped_action_drop_panics() {
    let value_drops = Cell::new(0);
    let action_drops = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let value = DropProbe(&value_drops);
        let captured = PanickingDropProbe(&action_drops);
        let guarded = ScopeGuard::<_, _, Never>::with_strategy(value, move |_| drop(captured));
        drop(guarded);
    }));

    assert!(outcome.is_err());
    assert_eq!(value_drops.get(), 1);
    assert_eq!(action_drops.get(), 1);
}

#[test]
fn skipped_scope_guard_drops_value_and_action() {
    let value_drops = Cell::new(0);
    let action_drops = Cell::new(0);
    let value = DropProbe(&value_drops);
    let captured = DropProbe(&action_drops);
    let guarded = ScopeGuard::<_, _, Never>::with_strategy(value, move |_| drop(captured));

    drop(guarded);
    assert_eq!(value_drops.get(), 1);
    assert_eq!(action_drops.get(), 1);
}

#[test]
fn panicking_scope_strategy_drops_value_and_action() {
    let value_drops = Cell::new(0);
    let action_drops = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let value = DropProbe(&value_drops);
        let captured = DropProbe(&action_drops);
        let guarded = ScopeGuard::<_, _, Panics>::with_strategy(value, move |_| drop(captured));
        drop(guarded);
    }));

    assert!(outcome.is_err());
    assert_eq!(value_drops.get(), 1);
    assert_eq!(action_drops.get(), 1);
}

#[test]
fn disarmed_scope_guard_does_not_evaluate_strategy() {
    let guarded = ScopeGuard::<_, _, Panics>::with_strategy(42, |_| {});
    assert_eq!(guarded.into_inner(), 42);
}

#[test]
fn panicking_scope_action_drops_value_and_runs_once() {
    let value_drops = Cell::new(0);
    let calls = Cell::new(0);
    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let guarded = guard(DropProbe(&value_drops), |value| {
            calls.set(calls.get() + 1);
            drop(value);
            panic!("action failed");
        });
        drop(guarded);
    }));

    assert!(outcome.is_err());
    assert_eq!(calls.get(), 1);
    assert_eq!(value_drops.get(), 1);
}

#[test]
fn scope_guard_debug_shows_guarded_value() {
    let guarded = guard(42, |_| {});
    assert_eq!(format!("{guarded:?}"), "ScopeGuard { value: 42 }");
}

#[cfg(feature = "std")]
#[test]
fn success_and_unwind_defer_policies_follow_panic_state() {
    let successes = Cell::new(0);
    let unwinds = Cell::new(0);
    {
        let _success = defer_on_success(|| successes.set(successes.get() + 1));
        let _unwind = defer_on_unwind(|| unwinds.set(unwinds.get() + 1));
    }
    assert_eq!(successes.get(), 1);
    assert_eq!(unwinds.get(), 0);

    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let _success = defer_on_success(|| successes.set(successes.get() + 1));
        let _unwind = defer_on_unwind(|| unwinds.set(unwinds.get() + 1));
        panic!("trigger unwind");
    }));
    assert!(outcome.is_err());
    assert_eq!(successes.get(), 1);
    assert_eq!(unwinds.get(), 1);
}

#[cfg(feature = "std")]
#[test]
fn success_and_unwind_value_guards_follow_panic_state() {
    let output = RefCell::new(Vec::new());
    {
        let _success = guard_on_success(1, |value| output.borrow_mut().push(value));
        let _unwind = guard_on_unwind(2, |value| output.borrow_mut().push(value));
    }

    let outcome = catch_unwind(AssertUnwindSafe(|| {
        let _success = guard_on_success(3, |value| output.borrow_mut().push(value));
        let _unwind = guard_on_unwind(4, |value| output.borrow_mut().push(value));
        panic!("trigger unwind");
    }));

    assert!(outcome.is_err());
    assert_eq!(*output.borrow(), vec![1, 4]);
}

#[cfg(feature = "std")]
#[test]
fn conditional_macros_follow_panic_state_and_lifo_order() {
    let output = RefCell::new(Vec::new());
    {
        async_safe_defer::defer_on_unwind!(output.borrow_mut().push(1));
        async_safe_defer::defer_on_success!(output.borrow_mut().push(2));
    }

    let outcome = catch_unwind(AssertUnwindSafe(|| {
        async_safe_defer::defer_on_success!(output.borrow_mut().push(3));
        async_safe_defer::defer_on_unwind!(output.borrow_mut().push(4));
        panic!("trigger unwind");
    }));

    assert!(outcome.is_err());
    assert_eq!(*output.borrow(), vec![2, 4]);
}

#[cfg(feature = "std")]
#[test]
fn conditional_macro_supports_move_capture() {
    let output = RefCell::new(None);
    {
        let value = String::from("owned");
        let output = &output;
        async_safe_defer::defer_on_success!(move {
            output.replace(Some(value));
        });
    }

    assert_eq!(output.into_inner().as_deref(), Some("owned"));
}

#[cfg(feature = "std")]
#[test]
fn returning_error_counts_as_successful_drop() {
    fn fail(ran: &Cell<bool>) -> Result<(), ()> {
        let _guard = defer_on_success(|| ran.set(true));
        Err(())
    }

    let ran = Cell::new(false);
    assert_eq!(fail(&ran), Err(()));
    assert!(ran.get());
}