Skip to main content

truce_core/
rt.rs

1//! Paranoid real-time allocation checking (the `rt-paranoid` feature).
2//!
3//! A wrapping global allocator (`RtCheckAlloc`) plus a thread-local
4//! "audio section" guard ([`RtSection`]). While the audio thread is
5//! inside a section, any allocation it makes is a real-time contract
6//! violation and gets recorded and reported.
7//!
8//! The section is entered around the single `plugin.process()` call in
9//! `chunked_process`, which every format wrapper and the test driver
10//! route through, so one guard covers all of them. The allocator is
11//! installed by the artifact (a plugin cdylib via `truce::plugin!`, or a
12//! test binary) with `truce::enable_rt_paranoid!`; a library cannot set
13//! a downstream binary's global allocator.
14//!
15//! Everything here is inert unless the `rt-paranoid` feature is on:
16//! [`RtSection::enter`] is a zero-sized no-op and [`allow_alloc`] just
17//! calls the closure, so release builds are unaffected.
18
19#[cfg(feature = "rt-paranoid")]
20mod imp {
21    use std::alloc::{GlobalAlloc, Layout, System};
22    use std::cell::Cell;
23    use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
24
25    const MAX_FRAMES: usize = 32;
26
27    #[derive(Clone, Copy)]
28    struct FrameBuf {
29        ips: [usize; MAX_FRAMES],
30        len: usize,
31    }
32
33    impl FrameBuf {
34        const EMPTY: Self = Self {
35            ips: [0; MAX_FRAMES],
36            len: 0,
37        };
38    }
39
40    /// What kind of real-time violation the checker caught, for the report.
41    #[derive(Clone, Copy)]
42    enum Kind {
43        Alloc,
44        Free,
45        Lock,
46    }
47
48    impl Kind {
49        fn noun(self) -> &'static str {
50            match self {
51                Kind::Alloc => "allocation",
52                Kind::Free => "free",
53                Kind::Lock => "lock",
54            }
55        }
56    }
57
58    // Const-initialized so access never lazily allocates - critical,
59    // since these are read from inside the allocator hook.
60    thread_local! {
61        static DEPTH: Cell<u32> = const { Cell::new(0) };
62        static RECORDING: Cell<bool> = const { Cell::new(false) };
63        static VIOLATIONS: Cell<u32> = const { Cell::new(0) };
64        static FIRST: Cell<FrameBuf> = const { Cell::new(FrameBuf::EMPTY) };
65        // Kind of the first violation this section, named in the report.
66        static FIRST_KIND: Cell<Kind> = const { Cell::new(Kind::Alloc) };
67        // `Some(n)` while inside `audit`: section violations accumulate
68        // here and the normal report/panic is suppressed, so a test can
69        // assert on the count instead.
70        static AUDIT: Cell<Option<u32>> = const { Cell::new(None) };
71    }
72
73    /// What the checker does when the audio thread allocates inside a
74    /// `process` section.
75    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
76    pub enum Mode {
77        /// Log the count and a backtrace after the block; keep running.
78        Count,
79        /// Panic - fails the block, gating a whole test suite.
80        Panic,
81        /// Abort at the offending allocation (catch the live stack in a
82        /// debugger).
83        Trap,
84    }
85
86    impl Mode {
87        const fn to_u8(self) -> u8 {
88            match self {
89                Mode::Count => 0,
90                Mode::Panic => 1,
91                Mode::Trap => 2,
92            }
93        }
94        fn from_u8(v: u8) -> Self {
95            match v {
96                1 => Mode::Panic,
97                2 => Mode::Trap,
98                _ => Mode::Count,
99            }
100        }
101    }
102
103    // Defaults to `Count`; `set_mode` overrides it. Reading it on the audio
104    // thread is a plain atomic load with no allocation.
105    static MODE: AtomicU8 = AtomicU8::new(Mode::Count.to_u8());
106
107    /// Set the reaction the checker takes on a violation. Call before the
108    /// first audio block (a test harness, `main`, or a `#[ctor]`); the last
109    /// call wins. Defaults to [`Mode::Count`].
110    pub fn set_mode(mode: Mode) {
111        MODE.store(mode.to_u8(), Ordering::Relaxed);
112    }
113
114    fn mode() -> Mode {
115        Mode::from_u8(MODE.load(Ordering::Relaxed))
116    }
117
118    // Opt-in: also flag frees the audio thread makes inside a section, not
119    // just allocations. Off by default - a value allocated in a prior block
120    // and dropped inside `process` frees here, so always-on would flag that
121    // common shape; enabling it catches that class deliberately.
122    static CHECK_DEALLOC: AtomicBool = AtomicBool::new(false);
123
124    /// Also flag deallocations (frees), not only allocations, that the audio
125    /// thread makes inside `process`. Off by default; call before the first
126    /// audio block, like [`set_mode`].
127    pub fn set_check_dealloc(enabled: bool) {
128        CHECK_DEALLOC.store(enabled, Ordering::Relaxed);
129    }
130
131    /// Whether dealloc flagging is currently enabled. Lets a scoped helper
132    /// save and restore the setting.
133    #[must_use]
134    pub fn check_dealloc() -> bool {
135        CHECK_DEALLOC.load(Ordering::Relaxed)
136    }
137
138    #[cfg(test)]
139    pub(crate) fn current_mode() -> Mode {
140        mode()
141    }
142
143    /// Guard around a real-time section (one `plugin.process()` call).
144    /// Nesting composes via a depth counter; the report fires only when
145    /// the outermost guard drops.
146    pub struct RtSection {
147        _private: (),
148    }
149
150    impl RtSection {
151        #[inline]
152        #[must_use]
153        pub fn enter() -> Self {
154            DEPTH.with(|d| d.set(d.get().wrapping_add(1)));
155            Self { _private: () }
156        }
157    }
158
159    impl Drop for RtSection {
160        fn drop(&mut self) {
161            let depth = DEPTH.with(|d| {
162                let n = d.get().wrapping_sub(1);
163                d.set(n);
164                n
165            });
166            // Report only on full exit, where DEPTH is 0 so the report's
167            // own allocations aren't re-flagged.
168            if depth == 0 {
169                let count = VIOLATIONS.with(|v| v.replace(0));
170                if count > 0 {
171                    // Inside `audit`, accumulate and stay quiet so the
172                    // test decides what the count means. Otherwise report
173                    // per the global mode.
174                    if AUDIT.with(Cell::get).is_some() {
175                        AUDIT.with(|a| a.set(a.get().map(|n| n.saturating_add(count))));
176                    } else {
177                        report(count);
178                    }
179                }
180            }
181        }
182    }
183
184    /// Run `f` and return `(result, allocations)` where `allocations` is
185    /// the number of audio-thread allocations made inside `process`
186    /// sections during `f`, with the normal per-section report/panic
187    /// suppressed. Same-thread only (the test driver runs `process` on
188    /// the calling thread). Underpins the `truce-test` audio-alloc
189    /// assertions.
190    pub fn audit<R>(f: impl FnOnce() -> R) -> (R, u32) {
191        let prev = AUDIT.with(|a| a.replace(Some(0)));
192        let r = f();
193        let count = AUDIT.with(|a| a.replace(prev)).unwrap_or(0);
194        (r, count)
195    }
196
197    /// Whether the checker is compiled in (the `rt-paranoid` feature).
198    /// A test asserting that code *does* allocate skips its assertion
199    /// when this is false, so it doesn't fail an ordinary build.
200    #[must_use]
201    pub fn is_active() -> bool {
202        true
203    }
204
205    /// Enter a section, run `f`, and return how many allocations it made,
206    /// skipping the reporting path so tests can assert on the count
207    /// directly. Only compiled for the crate's own tests.
208    #[cfg(test)]
209    pub(crate) fn count_allocs<R>(f: impl FnOnce() -> R) -> u32 {
210        DEPTH.with(|d| d.set(d.get().wrapping_add(1)));
211        VIOLATIONS.with(|v| v.set(0));
212        let _ = f();
213        let n = VIOLATIONS.with(|v| v.replace(0));
214        DEPTH.with(|d| d.set(d.get().wrapping_sub(1)));
215        n
216    }
217
218    /// Suspend checking for `f`, for a region inside `process` that must
219    /// legitimately allocate (a debug-only measurement, a first-block
220    /// lazy init). Restores on return or panic.
221    pub fn allow_alloc<R>(f: impl FnOnce() -> R) -> R {
222        struct Restore(u32);
223        impl Drop for Restore {
224            fn drop(&mut self) {
225                DEPTH.with(|d| d.set(self.0));
226            }
227        }
228        let _restore = Restore(DEPTH.with(|d| d.replace(0)));
229        f()
230    }
231
232    /// Records a violation of `kind` when the current thread is inside a
233    /// section. Called from the allocator hook (alloc / free) and from the
234    /// instrumented lock types. Must not allocate: the `RECORDING`
235    /// re-entrancy flag makes any allocation triggered by the recording
236    /// path itself a no-op instead of infinite recursion.
237    #[inline]
238    fn note_violation(kind: Kind) {
239        if DEPTH.with(Cell::get) == 0 || RECORDING.with(Cell::get) {
240            return;
241        }
242        RECORDING.with(|r| r.set(true));
243        let n = VIOLATIONS.with(|v| {
244            let n = v.get().wrapping_add(1);
245            v.set(n);
246            n
247        });
248        if n == 1 {
249            FIRST_KIND.with(|k| k.set(kind));
250            capture_first();
251        }
252        if mode() == Mode::Trap {
253            // SIGABRT stops a debugger on the offending operation with the
254            // live audio-thread stack.
255            std::process::abort();
256        }
257        RECORDING.with(|r| r.set(false));
258    }
259
260    /// Flag a lock acquisition on the audio thread inside a section. Called
261    /// by the instrumented [`Mutex`](super::Mutex) / [`RwLock`](super::RwLock)
262    /// wrappers; a no-op outside a section.
263    pub(super) fn note_lock() {
264        note_violation(Kind::Lock);
265    }
266
267    /// Walk the stack into a fixed thread-local buffer. The raw address
268    /// walk does not allocate; symbol resolution is deferred to
269    /// `report`, which runs after the section with allocation allowed.
270    fn capture_first() {
271        let mut buf = FrameBuf::EMPTY;
272        backtrace::trace(|frame| {
273            if buf.len < MAX_FRAMES {
274                buf.ips[buf.len] = frame.ip() as usize;
275                buf.len += 1;
276                true
277            } else {
278                false
279            }
280        });
281        FIRST.with(|f| f.set(buf));
282    }
283
284    fn report(count: u32) {
285        use std::fmt::Write as _;
286
287        let buf = FIRST.with(|f| f.replace(FrameBuf::EMPTY));
288        let noun = FIRST_KIND.with(Cell::get).noun();
289        // Resolve into a separate buffer so the "first violation" header
290        // is only emitted when at least one frame resolves - macOS test
291        // builds without a dSYM resolve to nothing, and a dangling header
292        // reads as broken.
293        let mut frames = String::new();
294        for &ip in &buf.ips[..buf.len] {
295            backtrace::resolve(ip as *mut _, |s| {
296                let name = s.name().map(|n| n.to_string()).unwrap_or_default();
297                if name.starts_with("truce_core::rt") || name.starts_with("backtrace") {
298                    return; // skip our own hook / capture frames
299                }
300                match (s.filename(), s.lineno()) {
301                    (Some(file), Some(line)) => {
302                        let _ = write!(frames, "\n    {name} ({}:{line})", file.display());
303                    }
304                    _ if !name.is_empty() => {
305                        let _ = write!(frames, "\n    {name}");
306                    }
307                    _ => {}
308                }
309            });
310        }
311        let mut msg = format!(
312            "truce rt-paranoid: {count} real-time violation(s) on the audio thread in process()"
313        );
314        if !frames.is_empty() {
315            let _ = write!(msg, "\n  first violation ({noun}):");
316            msg.push_str(&frames);
317        }
318        // Panicking in `RtSection::drop` while the thread is already
319        // unwinding (process itself panicked) would abort; downgrade to
320        // a log in that case.
321        match mode() {
322            Mode::Panic if !std::thread::panicking() => panic!("{msg}"),
323            _ => eprintln!("{msg}"),
324        }
325    }
326
327    /// Global allocator that flags allocations made on the audio thread
328    /// inside an [`RtSection`]. Delegates to [`System`] for the actual
329    /// allocation so the program keeps running (in `count` mode).
330    ///
331    /// Install it in the artifact with `truce::enable_rt_paranoid!`.
332    pub struct RtCheckAlloc;
333
334    impl RtCheckAlloc {
335        #[must_use]
336        pub const fn new() -> Self {
337            Self
338        }
339    }
340
341    impl Default for RtCheckAlloc {
342        fn default() -> Self {
343            Self::new()
344        }
345    }
346
347    // SAFETY: every method forwards to the global `System` allocator
348    // with the same arguments; `note_violation` only reads/writes thread-
349    // local `Cell`s and never itself allocates (guarded by `RECORDING`),
350    // so it cannot violate the `GlobalAlloc` contract.
351    unsafe impl GlobalAlloc for RtCheckAlloc {
352        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
353            note_violation(Kind::Alloc);
354            unsafe { System.alloc(layout) }
355        }
356        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
357            note_violation(Kind::Alloc);
358            unsafe { System.alloc_zeroed(layout) }
359        }
360        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
361            note_violation(Kind::Alloc);
362            unsafe { System.realloc(ptr, layout, new_size) }
363        }
364        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
365            // Freeing on the audio thread is also non-RT, but flagging every
366            // drop of a value moved in from a prior block is noisy, so it is
367            // opt-in via `set_check_dealloc`.
368            if CHECK_DEALLOC.load(Ordering::Relaxed) {
369                note_violation(Kind::Free);
370            }
371            unsafe { System.dealloc(ptr, layout) }
372        }
373    }
374}
375
376#[cfg(not(feature = "rt-paranoid"))]
377mod imp {
378    /// No-op real-time section guard. With `rt-paranoid` off this is a
379    /// zero-sized type whose `enter`/drop compile away.
380    pub struct RtSection {
381        _private: (),
382    }
383
384    impl RtSection {
385        #[inline]
386        #[must_use]
387        pub fn enter() -> Self {
388            Self { _private: () }
389        }
390    }
391
392    /// No-op with `rt-paranoid` off: just calls `f`.
393    #[inline]
394    pub fn allow_alloc<R>(f: impl FnOnce() -> R) -> R {
395        f()
396    }
397
398    /// No-op with `rt-paranoid` off: runs `f`, reports zero allocations.
399    #[inline]
400    pub fn audit<R>(f: impl FnOnce() -> R) -> (R, u32) {
401        (f(), 0)
402    }
403
404    /// The checker is not compiled in.
405    #[must_use]
406    #[inline]
407    pub fn is_active() -> bool {
408        false
409    }
410
411    /// What the checker does on a violation. Present with the feature off
412    /// so `set_mode` call sites compile unconditionally; the checker is
413    /// inert, so it has no effect.
414    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
415    pub enum Mode {
416        Count,
417        Panic,
418        Trap,
419    }
420
421    /// No-op with `rt-paranoid` off.
422    #[inline]
423    pub fn set_mode(_mode: Mode) {}
424
425    /// No-op with `rt-paranoid` off.
426    #[inline]
427    pub fn set_check_dealloc(_enabled: bool) {}
428
429    /// Always `false` with `rt-paranoid` off.
430    #[must_use]
431    #[inline]
432    pub fn check_dealloc() -> bool {
433        false
434    }
435
436    /// No-op with `rt-paranoid` off: the instrumented lock types just
437    /// delegate to std.
438    #[inline]
439    pub(super) fn note_lock() {}
440}
441
442pub use imp::{
443    Mode, RtSection, allow_alloc, audit, check_dealloc, is_active, set_check_dealloc, set_mode,
444};
445
446#[cfg(feature = "rt-paranoid")]
447pub use imp::RtCheckAlloc;
448
449/// A [`std::sync::Mutex`] that, with `rt-paranoid` on, flags a lock taken on
450/// the audio thread inside a `process` section. A plugin that wants lock
451/// checking uses this in place of the std type; with the feature off it is a
452/// zero-cost newtype that just delegates.
453///
454/// It catches only locks taken through this type - not `std::sync::Mutex`,
455/// `parking_lot`, or an OS primitive reached directly.
456pub struct Mutex<T: ?Sized> {
457    inner: std::sync::Mutex<T>,
458}
459
460impl<T> Mutex<T> {
461    /// Create a new mutex holding `value`.
462    pub const fn new(value: T) -> Self {
463        Self {
464            inner: std::sync::Mutex::new(value),
465        }
466    }
467
468    /// Consume the mutex, returning the inner value.
469    ///
470    /// # Errors
471    /// Returns the poison error if a holder panicked while holding the lock.
472    pub fn into_inner(self) -> std::sync::LockResult<T> {
473        self.inner.into_inner()
474    }
475}
476
477impl<T: ?Sized> Mutex<T> {
478    /// Acquire the mutex, blocking the current thread until it can. Flags a
479    /// real-time violation if taken inside a `process` section.
480    ///
481    /// # Errors
482    /// Returns the poison error if a holder panicked while holding the lock.
483    pub fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'_, T>> {
484        imp::note_lock();
485        self.inner.lock()
486    }
487
488    /// Attempt to acquire the mutex without blocking. Flags a real-time
489    /// violation if attempted inside a `process` section.
490    ///
491    /// # Errors
492    /// Returns `WouldBlock` if held elsewhere, or the poison error.
493    pub fn try_lock(&self) -> std::sync::TryLockResult<std::sync::MutexGuard<'_, T>> {
494        imp::note_lock();
495        self.inner.try_lock()
496    }
497
498    /// Borrow the inner value mutably; no lock is taken (unique access).
499    ///
500    /// # Errors
501    /// Returns the poison error if a holder panicked while holding the lock.
502    pub fn get_mut(&mut self) -> std::sync::LockResult<&mut T> {
503        self.inner.get_mut()
504    }
505}
506
507/// A [`std::sync::RwLock`] that, with `rt-paranoid` on, flags a `read` or
508/// `write` taken on the audio thread inside a `process` section. Same
509/// coverage caveat as [`Mutex`].
510pub struct RwLock<T: ?Sized> {
511    inner: std::sync::RwLock<T>,
512}
513
514impl<T> RwLock<T> {
515    /// Create a new read-write lock holding `value`.
516    pub const fn new(value: T) -> Self {
517        Self {
518            inner: std::sync::RwLock::new(value),
519        }
520    }
521
522    /// Consume the lock, returning the inner value.
523    ///
524    /// # Errors
525    /// Returns the poison error if a writer panicked while holding the lock.
526    pub fn into_inner(self) -> std::sync::LockResult<T> {
527        self.inner.into_inner()
528    }
529}
530
531impl<T: ?Sized> RwLock<T> {
532    /// Acquire a shared read lock. Flags a real-time violation if taken
533    /// inside a `process` section.
534    ///
535    /// # Errors
536    /// Returns the poison error if a writer panicked while holding the lock.
537    pub fn read(&self) -> std::sync::LockResult<std::sync::RwLockReadGuard<'_, T>> {
538        imp::note_lock();
539        self.inner.read()
540    }
541
542    /// Acquire an exclusive write lock. Flags a real-time violation if taken
543    /// inside a `process` section.
544    ///
545    /// # Errors
546    /// Returns the poison error if a writer panicked while holding the lock.
547    pub fn write(&self) -> std::sync::LockResult<std::sync::RwLockWriteGuard<'_, T>> {
548        imp::note_lock();
549        self.inner.write()
550    }
551
552    /// Attempt a shared read lock without blocking. Flags a real-time
553    /// violation if attempted inside a `process` section.
554    ///
555    /// # Errors
556    /// Returns `WouldBlock` if a writer holds it, or the poison error.
557    pub fn try_read(&self) -> std::sync::TryLockResult<std::sync::RwLockReadGuard<'_, T>> {
558        imp::note_lock();
559        self.inner.try_read()
560    }
561
562    /// Attempt an exclusive write lock without blocking. Flags a real-time
563    /// violation if attempted inside a `process` section.
564    ///
565    /// # Errors
566    /// Returns `WouldBlock` if held elsewhere, or the poison error.
567    pub fn try_write(&self) -> std::sync::TryLockResult<std::sync::RwLockWriteGuard<'_, T>> {
568        imp::note_lock();
569        self.inner.try_write()
570    }
571
572    /// Borrow the inner value mutably; no lock is taken (unique access).
573    ///
574    /// # Errors
575    /// Returns the poison error if a writer panicked while holding the lock.
576    pub fn get_mut(&mut self) -> std::sync::LockResult<&mut T> {
577        self.inner.get_mut()
578    }
579}
580
581// Install the checking allocator for this crate's own test binary so the
582// mechanism can be exercised. A `#[global_allocator]` in a lib applies to
583// that lib's test/bench binaries only, never to downstream crates.
584#[cfg(all(test, feature = "rt-paranoid"))]
585#[global_allocator]
586static TEST_ALLOC: RtCheckAlloc = RtCheckAlloc::new();
587
588#[cfg(all(test, feature = "rt-paranoid"))]
589mod tests {
590    use super::allow_alloc;
591    use super::imp::count_allocs;
592    use std::hint::black_box;
593
594    #[test]
595    fn alloc_in_section_is_flagged() {
596        let n = count_allocs(|| {
597            let v: Vec<u8> = Vec::with_capacity(4096);
598            black_box(v.as_ptr());
599        });
600        assert!(n >= 1, "expected the in-section allocation to be flagged");
601    }
602
603    #[test]
604    fn no_alloc_in_section_is_clean() {
605        let n = count_allocs(|| {
606            let x = black_box(2) + black_box(3);
607            black_box(x);
608        });
609        assert_eq!(n, 0);
610    }
611
612    #[test]
613    fn dealloc_flagged_only_when_enabled() {
614        use super::set_check_dealloc;
615
616        // A buffer allocated outside the section, freed inside it. The alloc
617        // happens before `count_allocs`, so only the in-section free counts.
618        // One test, not two, so the on/off windows never race each other.
619        let outside = Vec::<u8>::with_capacity(4096);
620        let off = count_allocs(|| drop(black_box(outside)));
621        assert_eq!(
622            off, 0,
623            "a free must not be flagged with dealloc checking off"
624        );
625
626        set_check_dealloc(true);
627        let outside = Vec::<u8>::with_capacity(4096);
628        let on = count_allocs(|| drop(black_box(outside)));
629        set_check_dealloc(false);
630        assert!(on >= 1, "a free must be flagged with dealloc checking on");
631    }
632
633    #[test]
634    fn lock_in_section_is_flagged() {
635        use super::Mutex;
636
637        let m = Mutex::new(0u32);
638        // Warm the lock first: macOS std `Mutex` boxes its `pthread_mutex_t`
639        // lazily, which would otherwise show up as an allocation too.
640        drop(m.lock());
641        let n = count_allocs(|| {
642            let _g = m.lock().unwrap();
643        });
644        assert!(n >= 1, "a lock taken inside a section must be flagged");
645    }
646
647    #[test]
648    fn rwlock_write_in_section_is_flagged() {
649        use super::RwLock;
650
651        let rw = RwLock::new(0u32);
652        drop(rw.write());
653        let n = count_allocs(|| {
654            let _g = rw.write().unwrap();
655        });
656        assert!(
657            n >= 1,
658            "a write lock taken inside a section must be flagged"
659        );
660    }
661
662    #[test]
663    fn allow_alloc_suppresses_flagging() {
664        let n = count_allocs(|| {
665            allow_alloc(|| {
666                let v: Vec<u8> = Vec::with_capacity(4096);
667                black_box(v.as_ptr());
668            });
669        });
670        assert_eq!(n, 0, "allow_alloc should suspend checking for its scope");
671    }
672
673    #[test]
674    fn shared_plugin_warms_the_mediation_lock() {
675        use super::imp::count_allocs;
676        use crate::wrapper::{lock_plugin, shared_plugin};
677
678        // `shared_plugin` locks once at construction so the first lock on
679        // the audio thread doesn't allocate (macOS std `Mutex` boxes its
680        // `pthread_mutex_t` lazily). Taking the lock here must be clean.
681        let shared = shared_plugin(vec![0u8; 16]);
682        let n = count_allocs(|| {
683            drop(lock_plugin(&shared));
684        });
685        assert_eq!(n, 0, "the mediation lock's first lock must be warmed");
686    }
687
688    #[test]
689    fn set_mode_overrides_the_default() {
690        use super::imp::current_mode;
691        use super::{Mode, set_mode};
692
693        // `Panic` is safe to leave briefly: `count_allocs` (the other
694        // tests) never routes through the report path. Restore `Count`
695        // so nothing else in the binary is affected. Never set `Trap` -
696        // it aborts the process.
697        set_mode(Mode::Panic);
698        assert_eq!(current_mode(), Mode::Panic);
699        set_mode(Mode::Count);
700        assert_eq!(current_mode(), Mode::Count);
701    }
702}