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}