Skip to main content

miniextendr_api/
deferred_condition.rs

1//! Deferred conditions: signal an R warning, message or condition **and**
2//! still return a value.
3//!
4//! [`crate::warning!`], [`crate::message!`] and [`crate::condition!`] unwind
5//! the Rust function: R sees the signal, the call returns `invisible(NULL)`.
6//! That is the right shape for "stop here, but softly", not for "here is the
7//! result, and by the way …". The `defer_*` family queues the condition
8//! instead; the generated C wrapper signals everything queued during the call
9//! once the Rust body has returned, in queue order, and then hands the value
10//! to R.
11//!
12//! ```ignore
13//! use miniextendr_api::{defer_warning, miniextendr};
14//! use miniextendr_api::RCondition;
15//!
16//! #[derive(Debug, RCondition)]
17//! #[condition(class = "pkg_warning")]
18//! enum PkgWarning {
19//!     #[condition(message = "dropped {dropped} of {total} rows")]
20//!     Truncated { dropped: i32, total: i32 },
21//! }
22//!
23//! #[miniextendr]
24//! fn trim(n: i32) -> i32 {
25//!     defer_warning(PkgWarning::Truncated { dropped: 2, total: n });
26//!     n - 2
27//! }
28//! ```
29//!
30//! ```r
31//! trim(5L)
32//! # [1] 3
33//! # Warning message:
34//! # In trim(5L) : dropped 2 of 5 rows
35//! w <- tryCatch(trim(5L), pkg_warning_truncated = function(w) w)
36//! w$dropped               # 2
37//! suppressWarnings(trim(5L))   # 3
38//! ```
39//!
40//! # What R sees
41//!
42//! Exactly what `warning()` / `message()` / `signalCondition()` called from
43//! an R function would produce, because that is what runs: the same R helper
44//! the generated wrappers use for the panicking macros re-raises each queued
45//! condition. Class layering (`c(<classes…>, "rust_warning", "simpleWarning",
46//! "warning", "condition")`), `data` fields as `w$<name>` and
47//! `conditionCall()` (the wrapper's call, or `NULL` for a condition queued
48//! with `call = none`) are identical to the immediate forms. `tryCatch(warning = )` exits the call before the value is returned;
49//! `withCallingHandlers` + `invokeRestart("muffleWarning")` keeps it;
50//! `suppressWarnings()` / `suppressMessages()` work as usual.
51//!
52//! # Ordering
53//!
54//! Queued conditions are signalled after the Rust body finished and before R
55//! sees its outcome, so a deferred warning followed by `rust_error!` (or an
56//! `Err` return) is signalled first and the error second, and two deferred
57//! conditions keep their queue order. Nested calls (R code evaluated from
58//! Rust that calls back into another `#[miniextendr]` function) flush only
59//! what they queued themselves.
60//!
61//! # Capturing a block's conditions
62//!
63//! [`capture`] runs a closure and returns what it queued instead of
64//! signalling it, for code whose diagnostics the caller already has (a
65//! replay of the steps that built a saved object). Conditions queued outside
66//! the closure are untouched.
67//!
68//! # Warning placement
69//!
70//! Conditions queued before a failure still signal. If a body must not warn on
71//! failure, collect warning payloads locally and queue them only after its last
72//! fallible step (including any `?`). Moving a warning before a later `?` changes
73//! that behavior: returning `Err` does not cancel the queued warning.
74//!
75//! ```no_run
76//! use miniextendr_api::{defer_warning, miniextendr};
77//! # miniextendr_api::miniextendr_init!(pkg);
78//!
79//! #[miniextendr]
80//! pub fn parse_trimmed(text: String) -> Result<i32, std::num::ParseIntError> {
81//!     let trimmed = text.trim();
82//!     let value = trimmed.parse::<i32>()?;
83//!     if trimmed != text {
84//!         defer_warning!(class = "pkg_trimmed", "removed surrounding whitespace");
85//!     }
86//!     Ok(value)
87//! }
88//! # fn main() {}
89//! ```
90//!
91//! Here, parsing `" bad "` returns an error without a warning; parsing `" 42 "`
92//! queues the whitespace warning and returns `42`.
93//!
94//! # Payloads
95//!
96//! [`defer_warning()`], [`defer_message()`] and [`defer_condition()`] take any
97//! [`RConditionData`] value: a `#[derive(RCondition)]` enum or struct
98//! (class from the type and variant names, message from a `format!` string or
99//! `Display`, fields as `data`), or a hand-built [`crate::condition::RError`].
100//! The [`defer_warning!`](crate::defer_warning!) /
101//! [`defer_message!`](crate::defer_message!) /
102//! [`defer_condition!`](crate::defer_condition!) macros take the
103//! [`crate::warning!`] grammar (`class = …`, `data = …`, format message) for
104//! one-off conditions.
105//!
106//! # Threads and boundaries
107//!
108//! The queue is a process-wide `Mutex`, so `#[miniextendr(worker)]` bodies
109//! push from the worker thread; the signalling always happens on R's main
110//! thread after the worker result is back. Every generated `.Call` entry
111//! point and every trait-ABI vtable shim flushes what was queued inside it.
112//! ALTREP `RUnwind` / `RustUnwind` callbacks and connection I/O callbacks also
113//! signal locally, with `conditionCall() = NULL`; raising guards use their
114//! explicit `call` argument. An R-origin error discards the abandoned queue.
115//!
116//! External-pointer finalizers and connection close/destroy/Drop callbacks
117//! suppress deferred conditions: R finalization removes the caller's handlers.
118//! No R signalling or delayed delivery is attempted from destructors guarded
119//! by `drop_catching_panic`. Unguarded raw FFI / `AltrepGuard::Unsafe` callbacks
120//! must not queue conditions; choose a guarded callback instead.
121
122use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
123
124use crate::SEXP;
125use crate::condition::{RCondition, RConditionData};
126
127// region: Queue
128
129/// Conditions queued during the current guarded callback (and enclosing calls).
130///
131/// A plain static rather than a thread-local so worker-thread pushes land in
132/// the same queue the main thread drains. The mutex is released before any R
133/// signalling, so handlers can re-enter Rust without retaining the queue lock.
134static PENDING: Mutex<Vec<RCondition>> = Mutex::new(Vec::new());
135
136const SUPPRESSED: ::std::thread::LocalKey<std::cell::Cell<usize>> =
    {
        const __RUST_STD_INTERNAL_INIT: std::cell::Cell<usize> =
            { std::cell::Cell::new(0) };
        unsafe {
            ::std::thread::LocalKey::new(const {
                        if ::std::mem::needs_drop::<std::cell::Cell<usize>>() {
                            |__rust_std_internal_init|
                                {
                                    #[thread_local]
                                    static __RUST_STD_INTERNAL_VAL:
                                        ::std::thread::local_impl::EagerStorage<std::cell::Cell<usize>>
                                        =
                                        ::std::thread::local_impl::EagerStorage::new(__RUST_STD_INTERNAL_INIT);
                                    __RUST_STD_INTERNAL_VAL.get()
                                }
                        } else {
                            |__rust_std_internal_init|
                                {
                                    #[thread_local]
                                    static __RUST_STD_INTERNAL_VAL: std::cell::Cell<usize> =
                                        __RUST_STD_INTERNAL_INIT;
                                    &__RUST_STD_INTERNAL_VAL
                                }
                        }
                    })
        }
    };std::thread_local! {
137    // Suppression belongs to this destructor's thread, not unrelated worker
138    // threads that may still be queueing diagnostics during an R-thread visit.
139    static SUPPRESSED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
140}
141
142fn pending() -> MutexGuard<'static, Vec<RCondition>> {
143    PENDING.lock().unwrap_or_else(PoisonError::into_inner)
144}
145
146/// Queue a warning to be signalled when the current guarded call or callback
147/// returns; the call still returns its value.
148///
149/// `payload` supplies the message, the user classes (prepended to
150/// `rust_warning`) and the `data` fields (`w$<name>`). See the
151/// [module docs](self) for the R-side behaviour and [`defer_warning!`](crate::defer_warning!)
152/// for the macro form.
153///
154/// A later `Err` does not cancel this warning. To avoid warnings on failure,
155/// queue after the last fallible step; see [warning placement](self#warning-placement).
156pub fn defer_warning(payload: impl RConditionData) {
157    defer(RCondition::Warning {
158        message: payload.message(),
159        class: payload.class(),
160        data: payload.data(),
161        call: payload.call(),
162    });
163}
164
165/// Queue a message (`message()`, muffled by `suppressMessages()`) to be
166/// emitted when the current guarded call or callback returns.
167///
168/// Unlike [`crate::message!`], the payload's classes are honoured: they are
169/// layered in front of `rust_message` so handlers can dispatch on them.
170pub fn defer_message(payload: impl RConditionData) {
171    defer(RCondition::Message {
172        message: payload.message(),
173        class: payload.class(),
174        data: payload.data(),
175        call: payload.call(),
176    });
177}
178
179/// Queue a plain condition (`signalCondition()`: silent without a handler)
180/// to be signalled when the current guarded call or callback returns.
181pub fn defer_condition(payload: impl RConditionData) {
182    defer(RCondition::Condition {
183        message: payload.message(),
184        class: payload.class(),
185        data: payload.data(),
186        call: payload.call(),
187    });
188}
189
190/// Run `f` and return the conditions it queued, oldest first, instead of
191/// signalling them when the guarded call returns.
192///
193/// For code that queues diagnostics a caller already has: replaying the
194/// steps that built a saved object repeats warnings the object recorded.
195/// `capture(f).0` discards them; each [`RCondition`](enum@RCondition) can also be inspected,
196/// queued again with [`RCondition::defer`] or raised with
197/// [`RCondition::raise`].
198///
199/// ```ignore
200/// use miniextendr_api::deferred_condition::capture;
201///
202/// let (rebuilt, warnings) = capture(|| rebuild(&snapshot));
203/// debug_assert_eq!(warnings.len(), snapshot.warnings.len());
204/// ```
205///
206/// - Conditions queued before the block, or by enclosing calls, stay queued.
207/// - Conditions a nested `#[miniextendr]` call queued were signalled when
208///   that call returned, as always.
209/// - If `f` unwinds (`error!`, `Err` through a nested raise, an R error), the
210///   block's conditions stay queued and follow the usual rules: a Rust
211///   failure signals them, an R-origin error discards them. To keep them on
212///   failure too, catch the failure inside the block:
213///   `capture(|| catch_condition(|| rebuild(&snapshot)))` (see
214///   [`catch_condition`](crate::condition::catch_condition)).
215/// - Where deferred conditions are suppressed (finalizers, see the
216///   [module docs](self#threads-and-boundaries)), nothing is queued and the
217///   vector is empty.
218pub fn capture<T>(f: impl FnOnce() -> T) -> (T, Vec<RCondition>) {
219    let start = mark();
220    let value = f();
221    (value, take_pending(start))
222}
223
224/// Queue an already-built condition payload. Used by the `defer_*!` macros.
225#[doc(hidden)]
226pub fn defer(condition: RCondition) {
227    if SUPPRESSED.get() == 0 {
228        pending().push(condition);
229    }
230}
231
232/// The queue position at the start of a call. Pair with [`finish`].
233#[doc(hidden)]
234#[inline]
235pub fn mark() -> usize {
236    pending().len()
237}
238
239/// Remove and return everything queued since `mark`, oldest first. Entries
240/// below `mark` belong to enclosing calls and stay.
241#[doc(hidden)]
242pub fn take_pending(mark: usize) -> Vec<RCondition> {
243    // A nested guard inside a finalizer must not drain concurrent worker
244    // entries either: they belong to a later, signal-capable boundary.
245    if SUPPRESSED.get() != 0 {
246        return Vec::new();
247    }
248    let mut queue = pending();
249    if queue.len() <= mark {
250        return Vec::new();
251    }
252    queue.split_off(mark)
253}
254
255/// Discard conditions whose Rust/R call was abandoned by an R longjmp.
256pub(crate) fn discard(mark: usize) {
257    if SUPPRESSED.get() == 0 {
258        pending().truncate(mark);
259    }
260}
261
262/// Finalization has no caller condition handlers. Suppress queues throughout
263/// the destructor, including any nested framework boundaries, without R calls.
264pub(crate) struct Suppress(std::marker::PhantomData<std::rc::Rc<()>>);
265
266impl Suppress {
267    pub(crate) fn new() -> Self {
268        SUPPRESSED.set(SUPPRESSED.get() + 1);
269        Self(std::marker::PhantomData)
270    }
271}
272
273impl Drop for Suppress {
274    fn drop(&mut self) {
275        SUPPRESSED.set(SUPPRESSED.get() - 1);
276    }
277}
278
279// endregion
280
281// region: Flush
282
283/// Signal everything queued since `mark`, then return `value`.
284///
285/// Called by the generated C wrappers with the wrapper's outcome (the value
286/// SEXP, or a tagged condition value when the body failed) and by
287/// [`crate::unwind_protect::with_r_unwind_protect_shim`]. The queued
288/// conditions are materialised as tagged condition values and handed to the
289/// R helper (`stop()` / `warning()` / `message()` / `signalCondition()`), so
290/// their R-side shape is identical to the panicking macros'. The signalling
291/// runs under its own `R_UnwindProtect`: an exiting handler unwinds through
292/// it like any R error, and a Rust panic inside (a framework bug) comes back
293/// as a tagged error value, which replaces `value`.
294///
295/// `call` is the wrapper's call object (`conditionCall()` of every signalled
296/// condition); `None` gives `NULL`.
297///
298/// # Safety
299///
300/// Must run on R's main thread. `value` need not be protected: it is rooted
301/// for the duration of the signalling.
302#[doc(hidden)]
303pub unsafe fn finish(mark: usize, value: SEXP, call: Option<SEXP>) -> SEXP {
304    let queued = take_pending(mark);
305    if queued.is_empty() {
306        return value;
307    }
308    let outcome = crate::unwind_protect::with_r_unwind_protect(
309        || {
310            // SAFETY: main thread (caller contract), inside R_UnwindProtect.
311            unsafe {
312                let _value_root = crate::OwnedProtect::new(value);
313                signal_now(queued, call);
314            }
315            SEXP::nil()
316        },
317        call,
318    );
319    if outcome.is_nil() { value } else { outcome }
320}
321
322/// Flush a non-raising callback's conditions after its Rust guard has finished.
323/// R objects in `value` must already be rooted (the same contract as `or_raise`).
324pub(crate) fn finish_guarded<R>(mark: usize, value: R) -> R {
325    let queued = take_pending(mark);
326    if queued.is_empty() {
327        return value;
328    }
329    crate::unwind_protect::with_r_unwind_protect_or_raise(
330        move || {
331            // SAFETY: callback boundary is on R's main thread; this closure is
332            // inside R_UnwindProtect, including the owned result's destructor.
333            unsafe { signal_now(queued, None) };
334            value
335        },
336        None,
337    )
338}
339
340/// Materialise each queued condition and hand it to the R helper.
341///
342/// # Safety
343///
344/// Main thread, inside `R_UnwindProtect` (handlers may longjmp).
345pub(crate) unsafe fn signal_now(queued: Vec<RCondition>, call: Option<SEXP>) {
346    let helper = raise_condition_helper();
347    let call_sexp = call.unwrap_or(SEXP::nil());
348    for condition in queued {
349        // SAFETY: main thread inside R_UnwindProtect (caller contract). The
350        // tagged value and the call object are rooted across `Rf_eval`. A
351        // call-less condition carries the "no call" marker, which the helper
352        // maps to `call = NULL` instead of falling back to `call_sexp`.
353        unsafe {
354            let tagged =
355                crate::OwnedProtect::new(crate::error_value::rust_condition_value(condition, call));
356            let expr =
357                crate::OwnedProtect::new(crate::sys::Rf_lang3(helper, tagged.get(), call_sexp));
358            crate::sys::Rf_eval(expr.get(), crate::sys::R_BaseEnv);
359        }
360    }
361}
362
363/// The `.miniextendr_raise_condition` closure, evaluated once from the shared
364/// source ([`crate::registry::RAISE_CONDITION_HELPER_FN`]) in the base
365/// namespace and preserved for the life of the process.
366///
367/// Evaluating the source here, rather than looking the helper up in a package
368/// namespace, keeps this path independent of which package (or embedded
369/// engine) is running: the helper only needs base and `utils::modifyList`.
370/// The raising guards raise their conditions with it too
371/// (`unwind_protect::raise_rust_condition_via_stop`).
372pub(crate) fn raise_condition_helper() -> SEXP {
373    static HELPER: OnceLock<SEXP> = OnceLock::new();
374    *HELPER.get_or_init(|| {
375        // SAFETY: only reached from `signal_now` and
376        // `raise_rust_condition_via_stop`, on the main thread.
377        unsafe {
378            let closure = crate::expression::r_eval_str(
379                crate::registry::RAISE_CONDITION_HELPER_FN,
380                crate::sys::R_BaseNamespace,
381            )
382            .unwrap_or_else(|err| {
383                {
    ::core::panicking::panic_fmt(format_args!("miniextendr: could not define the deferred-condition helper: {0}",
            err));
}panic!("miniextendr: could not define the deferred-condition helper: {err}")
384            });
385            crate::sys::R_PreserveObject(closure);
386            closure
387        }
388    })
389}
390
391// endregion
392
393// region: Macros
394
395/// Queue an R warning with `rust_warning` class layering and return normally;
396/// the surrounding guarded call or callback signals it after its value is
397/// computed.
398///
399/// Same grammar as [`crate::warning!`]: optional `call = none` (no call, like
400/// R's `warning(..., call. = FALSE)`), optional `class = …` (one class or a
401/// vector, most specific first), optional `data = …` (a pair, a bracketed
402/// list of pairs, or `{ name = value }` sugar), then the `format!` message.
403/// For a typed payload use [`crate::defer_warning()`] with a
404/// `#[derive(RCondition)]` type (`#[condition(call = none)]` on the type
405/// or a variant drops the call there).
406///
407/// A later `Err` does not cancel this warning. To avoid warnings on failure,
408/// queue after the last fallible step; see [warning placement](crate::deferred_condition#warning-placement).
409///
410/// ```ignore
411/// use miniextendr_api::defer_warning;
412///
413/// #[miniextendr]
414/// fn parse_all(xs: Vec<String>) -> Vec<i32> {
415///     let (ok, bad): (Vec<_>, Vec<_>) = xs.iter().map(|s| s.parse::<i32>()).partition(Result::is_ok);
416///     if !bad.is_empty() {
417///         defer_warning!(class = "pkg_unparsed", data = ("n", bad.len() as i32), "{} values could not be parsed", bad.len());
418///     }
419///     ok.into_iter().map(Result::unwrap).collect()
420/// }
421/// ```
422#[macro_export]
423macro_rules! defer_warning {
424    ($($t:tt)*) => {{
425        let (__mx_call, __mx_class, __mx_data, __mx_message) =
426            $crate::__mx_condition_parts!($($t)*);
427        $crate::deferred_condition::defer($crate::condition::RCondition::Warning {
428            message: __mx_message,
429            class: __mx_class,
430            data: __mx_data,
431            call: __mx_call,
432        });
433    }};
434}
435
436/// Queue an R message (`rust_message` layering, muffled by
437/// `suppressMessages()`) and return normally; emitted when the surrounding
438/// guarded call or callback returns.
439///
440/// Same grammar as [`crate::defer_warning!`]. Unlike [`crate::message!`] a
441/// `class = …` part is accepted and layered in front of `rust_message`.
442/// `call = none` is accepted for the shared grammar and changes nothing: an R
443/// message carries no call, so `conditionCall(m)` is `NULL` either way.
444#[macro_export]
445macro_rules! defer_message {
446    ($($t:tt)*) => {{
447        let (__mx_call, __mx_class, __mx_data, __mx_message) =
448            $crate::__mx_condition_parts!($($t)*);
449        $crate::deferred_condition::defer($crate::condition::RCondition::Message {
450            message: __mx_message,
451            class: __mx_class,
452            data: __mx_data,
453            call: __mx_call,
454        });
455    }};
456}
457
458/// Queue a plain R condition (`rust_condition` layering, silent without a
459/// handler) and return normally; signalled when the surrounding
460/// guarded call or callback returns.
461///
462/// Same grammar as [`crate::defer_warning!`]. Useful for progress or audit
463/// events a caller may opt into with `withCallingHandlers()`.
464#[macro_export]
465macro_rules! defer_condition {
466    ($($t:tt)*) => {{
467        let (__mx_call, __mx_class, __mx_data, __mx_message) =
468            $crate::__mx_condition_parts!($($t)*);
469        $crate::deferred_condition::defer($crate::condition::RCondition::Condition {
470            message: __mx_message,
471            class: __mx_class,
472            data: __mx_data,
473            call: __mx_call,
474        });
475    }};
476}
477
478// endregion
479
480// region: Tests — queue semantics (no R runtime needed)
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use crate::condition::{ConditionCall, ConditionData, RError};
486    use crate::error_value::kind;
487
488    /// `(name, i32)` view of a data payload (`RValue` has no `PartialEq`).
489    fn ints(data: &Option<ConditionData>) -> Vec<(&str, Option<i32>)> {
490        data.as_ref()
491            .map(|fields| {
492                fields
493                    .iter()
494                    .map(|(name, value)| (name.as_str(), value.as_i32()))
495                    .collect()
496            })
497            .unwrap_or_default()
498    }
499
500    /// The queue is process-wide; serialise the tests that touch it.
501    static TEST_LOCK: Mutex<()> = Mutex::new(());
502
503    fn with_empty_queue<T>(f: impl FnOnce() -> T) -> T {
504        let _lock = TEST_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
505        take_pending(0);
506        f()
507    }
508
509    #[test]
510    fn typed_payloads_keep_kind_class_and_data() {
511        with_empty_queue(|| {
512            let m = mark();
513            defer_warning(RError::new("w").class(["a", "b"]).data("n", 1));
514            defer_message(RError::new("m").class("c"));
515            defer_condition(RError::new("c"));
516            let parts: Vec<_> = take_pending(m)
517                .into_iter()
518                .map(RCondition::into_parts)
519                .collect();
520            assert_eq!(parts.len(), 3);
521            assert_eq!(parts[0].0, kind::WARNING);
522            assert_eq!(parts[0].1.message, "w");
523            assert_eq!(parts[0].1.class, vec!["a", "b"]);
524            assert_eq!(ints(&parts[0].1.data), vec![("n", Some(1))]);
525            assert_eq!(parts[1].0, kind::MESSAGE);
526            assert_eq!(parts[1].1.class, vec!["c"]);
527            assert!(parts[1].1.data.is_none());
528            assert_eq!(parts[2].0, kind::CONDITION);
529            assert!(parts[2].1.class.is_empty());
530            assert!(
531                parts
532                    .iter()
533                    .all(|(_, parts)| parts.call == ConditionCall::Inherit)
534            );
535            assert!(take_pending(m).is_empty());
536        });
537    }
538
539    #[test]
540    fn nested_marks_take_only_their_own_entries() {
541        with_empty_queue(|| {
542            let outer = mark();
543            crate::defer_warning!("outer");
544            let inner = mark();
545            crate::defer_warning!(class = "t", data = ("n", 2), "inner {}", 2);
546            let inner_taken = take_pending(inner);
547            assert_eq!(inner_taken.len(), 1);
548            let (kind, parts) = inner_taken.into_iter().next().unwrap().into_parts();
549            assert_eq!((kind, parts.message.as_str()), (kind::WARNING, "inner 2"));
550            assert_eq!(parts.class, vec!["t"]);
551            assert_eq!(ints(&parts.data), vec![("n", Some(2))]);
552            let outer_taken = take_pending(outer);
553            assert_eq!(outer_taken.len(), 1);
554            assert_eq!(outer_taken.into_iter().next().unwrap().message(), "outer");
555        });
556    }
557
558    #[test]
559    fn capture_takes_only_the_block_s_conditions() {
560        with_empty_queue(|| {
561            let outer = mark();
562            crate::defer_warning!("before");
563            let (value, captured) = capture(|| {
564                crate::defer_warning!(class = "replayed", "inside {}", 1);
565                let ((), nested) = capture(|| crate::defer_message!("nested"));
566                assert_eq!(nested.len(), 1);
567                assert_eq!(nested[0].message(), "nested");
568                crate::defer_condition!("last");
569                7
570            });
571            assert_eq!(value, 7);
572            let parts: Vec<_> = captured.into_iter().map(RCondition::into_parts).collect();
573            assert_eq!(parts.len(), 2);
574            assert_eq!(parts[0].0, kind::WARNING);
575            assert_eq!(parts[0].1.message, "inside 1");
576            assert_eq!(parts[0].1.class, vec!["replayed"]);
577            assert_eq!(parts[1].0, kind::CONDITION);
578            let remaining = take_pending(outer);
579            assert_eq!(remaining.len(), 1);
580            assert_eq!(remaining[0].message(), "before");
581        });
582    }
583
584    #[test]
585    fn capture_leaves_the_queue_on_unwind() {
586        with_empty_queue(|| {
587            let start = mark();
588            let caught = crate::condition::catch_condition(|| {
589                capture(|| {
590                    crate::defer_warning!("queued before the error");
591                    crate::error!("failed");
592                })
593            });
594            assert_eq!(caught.unwrap_err().message(), "failed");
595            let remaining = take_pending(start);
596            assert_eq!(remaining.len(), 1);
597            assert_eq!(remaining[0].message(), "queued before the error");
598
599            // Catching inside the capture keeps the block's conditions with it.
600            let (outcome, captured) = capture(|| {
601                crate::condition::catch_condition(|| {
602                    crate::defer_warning!("kept");
603                    crate::error!("failed");
604                })
605            });
606            assert_eq!(outcome.unwrap_err().message(), "failed");
607            assert_eq!(captured.len(), 1);
608            assert!(take_pending(start).is_empty());
609        });
610    }
611
612    #[test]
613    fn macro_forms_cover_message_and_condition() {
614        with_empty_queue(|| {
615            let m = mark();
616            crate::defer_message!(data = { step = 1 }, "step {}", 1);
617            crate::defer_condition!(class = ["member", "family"], "tick");
618            let parts: Vec<_> = take_pending(m)
619                .into_iter()
620                .map(RCondition::into_parts)
621                .collect();
622            assert_eq!(parts[0].0, kind::MESSAGE);
623            assert_eq!(parts[0].1.message, "step 1");
624            assert_eq!(parts[1].0, kind::CONDITION);
625            assert_eq!(parts[1].1.class, vec!["member", "family"]);
626        });
627    }
628
629    #[test]
630    fn call_none_is_queued_with_the_payload() {
631        with_empty_queue(|| {
632            let m = mark();
633            crate::defer_warning!(call = none, class = "t", data = { n = 3 }, "plain {}", 1);
634            crate::defer_condition!(call = none, "plain condition");
635            crate::defer_message!(call = none, "accepted, a message has no call anyway");
636            crate::defer_warning!("sibling keeps its call");
637            defer_warning(RError::new("typed").without_call());
638            let parts: Vec<_> = take_pending(m)
639                .into_iter()
640                .map(RCondition::into_parts)
641                .collect();
642            let calls: Vec<_> = parts.iter().map(|(_, parts)| parts.call).collect();
643            assert_eq!(
644                calls,
645                [
646                    ConditionCall::None,
647                    ConditionCall::None,
648                    ConditionCall::None,
649                    ConditionCall::Inherit,
650                    ConditionCall::None,
651                ]
652            );
653            assert_eq!(parts[0].0, kind::WARNING);
654            assert_eq!(parts[0].1.message, "plain 1");
655            assert_eq!(parts[0].1.class, vec!["t"]);
656            assert_eq!(ints(&parts[0].1.data), vec![("n", Some(3))]);
657            assert_eq!(parts[1].0, kind::CONDITION);
658            assert_eq!(parts[2].0, kind::MESSAGE);
659        });
660    }
661
662    #[test]
663    fn finalizer_suppression_nests_and_preserves_the_enclosing_queue() {
664        with_empty_queue(|| {
665            crate::defer_warning!("outer");
666            {
667                let _outer = Suppress::new();
668                crate::defer_warning!("finalizer");
669                {
670                    let _inner = Suppress::new();
671                    crate::defer_message!("nested finalizer");
672                }
673                crate::defer_condition!("still suppressed");
674            }
675            crate::defer_message!("after");
676            let messages: Vec<_> = take_pending(0)
677                .into_iter()
678                .map(|condition| condition.message().to_string())
679                .collect();
680            assert_eq!(messages, ["outer", "after"]);
681        });
682    }
683
684    #[test]
685    fn finalizer_suppression_does_not_drop_another_threads_conditions() {
686        with_empty_queue(|| {
687            {
688                let _suppressed = Suppress::new();
689                std::thread::spawn(|| crate::defer_warning!("worker"))
690                    .join()
691                    .unwrap();
692                crate::defer_warning!("finalizer");
693                assert!(take_pending(0).is_empty());
694                discard(0);
695                assert_eq!(mark(), 1);
696            }
697            let remaining = take_pending(0);
698            assert_eq!(remaining.len(), 1);
699            assert_eq!(remaining.into_iter().next().unwrap().message(), "worker");
700        });
701    }
702
703    #[test]
704    fn abandoned_inner_queue_does_not_discard_outer_conditions() {
705        with_empty_queue(|| {
706            crate::defer_warning!("outer");
707            let inner = mark();
708            crate::defer_warning!("abandoned");
709            discard(inner);
710            let remaining = take_pending(0);
711            assert_eq!(remaining.len(), 1);
712            assert_eq!(remaining.into_iter().next().unwrap().message(), "outer");
713        });
714    }
715
716    #[test]
717    fn take_pending_past_the_end_is_empty() {
718        with_empty_queue(|| {
719            assert!(take_pending(usize::MAX).is_empty());
720            assert!(take_pending(mark() + 1).is_empty());
721        });
722    }
723}
724
725// endregion