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
136std::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 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