yo_alloc/lib.rs
1//! A global allocator that turns an accidental heap allocation on a shard path
2//! into a crash.
3//!
4//! Y7 says there is no global allocator call on a command path. That is easy to
5//! write down and impossible to keep by review once more than one person is in
6//! the codebase, because the allocating constructs in Rust are the comfortable
7//! ones: `format!`, `to_vec`, `collect`, `Box::new`, a `Vec` that grows, a
8//! `String` built to make an error message. Each costs tens of nanoseconds
9//! against a 150 ns budget, and none of them looks wrong in a diff.
10//!
11//! So the rule is enforced instead of reviewed. A shard thread marks itself
12//! [`enter_no_alloc`] before the command loop, and from that point any
13//! allocation aborts the process with a message naming the size. Setup, arena
14//! growth and anything else that legitimately needs the heap wraps itself in
15//! [`allow`], which is a visible, greppable, deliberate act.
16//!
17//! # Cost when it is off
18//!
19//! The check is one thread local load and a branch, on a path that already
20//! calls into the system allocator. It is not measurable next to `malloc`.
21//! Non shard threads never set the flag and pay the same single branch.
22//!
23//! The claims are cheaper still. [`allow`] and the three named forms of it come
24//! down to a relaxed load of a static, false in any process that has never armed
25//! a thread, which is every shipped binary. That is what lets a claim sit inside
26//! a loop where the growth actually happens rather than being hoisted to the top
27//! of a function it does not describe.
28//!
29//! # Three modes, and why an abort is not the only one
30//!
31//! An abort tells you about one violation per run, which is the wrong tool for
32//! finding out how many there are. Nothing in this project had been checked
33//! against Y7 since the rule was written down, so the first question is not
34//! "stop on the first one" but "what is the list".
35//!
36//! [`Mode::Report`] answers that. It suspends the check, captures a backtrace,
37//! prints each distinct site once and counts the repeats, and lets the
38//! allocation through. It allocates while it does this, on purpose and with the
39//! check turned off around it, because a debugging mode that cannot use the heap
40//! cannot tell you where you are.
41//!
42//! [`Mode::Abort`] is the rule as written, for a build that is expected to be
43//! clean. [`Mode::Off`] is the default, so installing the allocator does not
44//! change what a shipped binary does until somebody asks for it.
45//!
46//! Off costs nothing rather than costing a branch, because nothing arms the
47//! thread: [`guard`] is where the mode is read, and when it is off the thread
48//! flag is never set and the allocator's check is the same false it would be on
49//! any other thread.
50//!
51//! # Getting the list
52//!
53//! `yodb` installs this and `pump` wraps its dispatch in [`guard`], so
54//! `YO_ALLOC=report yodb serve` answers the question, and `cargo xtask alloc`
55//! is that with a server, a workload and a parser around it. It builds a debug
56//! `yodb`, drives it with about nine thousand commands covering every type and
57//! prints one line per distinct site. Debug on purpose: release inlines the
58//! interesting frames into each other and the report comes back naming
59//! `serve_command` for everything.
60//!
61//! Run it before touching anything here, and believe it over anything written
62//! down. Twice during this work the list contradicted what was already recorded
63//! about what was left, and both times the list was right. A report mode exists
64//! so that this is measured rather than argued about, and that only works if it
65//! is the list somebody actually looks at.
66//!
67//! It is also the gate. An empty list exits 0 and anything else exits 1, and
68//! `ci.yml` runs it on every push, so a new allocation on a command path fails
69//! the pull request that added it rather than being found a release later.
70//!
71//! # What it found, and the four piles it sorted into
72//!
73//! The first arming reported 31 distinct sites, and they were never one problem.
74//! The list is empty now, and it got there four different ways.
75//!
76//! Most of them were the first touch of a key. Creating a set, hash, list or
77//! zset allocates the body, and the slab that holds bodies of that type doubles
78//! when it fills. That is real allocation on a command path and it is also the
79//! only sensible place for it, so those sites wanted a claim written down and a
80//! [`first_touch`] around them rather than a fix. They have one now.
81//!
82//! Then the ones worth having, which were per command and in steady state, which
83//! is exactly what Y7 is about. A `to_vec` of the value in `APPEND`, `SETRANGE`,
84//! `EXPIRE`, `GETEX`, `INCRBYFLOAT` and the string arm of `COPY`. A `Vec` built
85//! per call to hold the operands of a set operation. A `Vec` of indices in
86//! `LREM`. An owned key out of `RANDOMKEY`. The record copy in `RENAME`. The
87//! engine's own list of free decoder slots. The hash table a `SUNION` walked
88//! everything into, which was the largest of them and is now a table the
89//! database keeps. And the old value out of `SET ... GET`, `GETSET` and
90//! `GETDEL`, which looked like a signature question and was not: the owning
91//! method stayed for the embedded caller and the wire took a `_with` form that
92//! hands the value over where it lies, because the wire writes it into the reply
93//! and never looks at it again. Every one of those is gone.
94//!
95//! Third, memory that is proportional to what is stored rather than to what is
96//! served. An intset run gets longer as members go into it and there is no
97//! arrangement of that code which stores ten thousand integers in the room it
98//! had for eight. That is [`for_the_data`], and the test of the claim is that a
99//! workload which stops adding data stops allocating.
100//!
101//! Fourth, scratch buffers the database keeps and refills: the `ZRANDMEMBER`
102//! permutation and the set algebra tables. Those allocate when a call is bigger
103//! than every call before it and never otherwise. That is [`high_water`], and
104//! the test of the claim is a unit test making the same call twice and counting
105//! zero the second time. Every site wrapped in it has one.
106//!
107//! So the order is: report first, sort the list into the piles, fix the ones
108//! that are per command and put the right claim on the ones that are not. The
109//! three claims are all [`allow`] underneath and differ only in what they say,
110//! which is the entire point of having three of them: `git grep first_touch` is
111//! a list of the places a key is created, and it stays one.
112//!
113//! # Using it
114//!
115//! ```no_run
116//! # use yo_alloc::YoAlloc;
117//! #[global_allocator]
118//! static ALLOC: YoAlloc = YoAlloc::new();
119//! ```
120//!
121//! The engine installs this in `yodb`. A library consumer of `yodb` does not get
122//! it, because choosing a global allocator is the application's call and never a
123//! library's.
124
125#![deny(missing_docs)]
126
127use std::alloc::{GlobalAlloc, Layout, System};
128use std::cell::Cell;
129use std::collections::BTreeMap;
130use std::sync::Mutex;
131use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
132
133thread_local! {
134 /// Zero means allocation is allowed. Anything above zero forbids it.
135 ///
136 /// A counter rather than a flag so that [`allow`] nests correctly, which
137 /// matters because arena growth can be reached from more than one depth.
138 static FORBID: Cell<u32> = const { Cell::new(0) };
139}
140
141/// What happens when a marked thread allocates.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
143pub enum Mode {
144 /// Nothing. [`guard`] does not mark the thread and the check never fires.
145 ///
146 /// The default, so that installing the allocator in a binary is not on its
147 /// own a change to what that binary does.
148 #[default]
149 Off,
150 /// Print each distinct site once, count the rest, and carry on.
151 Report,
152 /// Abort the process on the first one. Y7 as written.
153 Abort,
154}
155
156/// The mode, as a number, because a static has to be something an atomic holds.
157static MODE: AtomicU8 = AtomicU8::new(0);
158
159/// Whether any thread in this process has ever been marked.
160///
161/// Not the mode. [`enter_no_alloc`] is public and a test calls it without going
162/// near [`set_mode`], so the mode being off does not mean no thread is marked.
163/// This is the question [`allow`] actually needs answered, and it is one
164/// relaxed load of a static rather than a thread local.
165static ARMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
166
167/// How many violations have been seen in [`Mode::Report`].
168static SEEN_TOTAL: AtomicU64 = AtomicU64::new(0);
169
170/// One entry per distinct backtrace, so a site in a loop prints once.
171static SITES: Mutex<BTreeMap<u64, Site>> = Mutex::new(BTreeMap::new());
172
173/// What was seen at one site.
174#[derive(Debug)]
175struct Site {
176 /// How many allocations landed here.
177 count: u64,
178 /// The largest one, which is usually the one worth looking at first.
179 largest: usize,
180}
181
182/// What a marked thread does when it allocates.
183#[must_use]
184pub fn mode() -> Mode {
185 match MODE.load(Ordering::Relaxed) {
186 1 => Mode::Report,
187 2 => Mode::Abort,
188 _ => Mode::Off,
189 }
190}
191
192/// Set what a marked thread does when it allocates.
193///
194/// Meant to be called once, from `main`, before any thread is marked. It is an
195/// atomic store rather than a `OnceLock` so that a test can set it and put it
196/// back, which is the only reason it is allowed to happen twice.
197pub fn set_mode(m: Mode) {
198 MODE.store(
199 match m {
200 Mode::Off => 0,
201 Mode::Report => 1,
202 Mode::Abort => 2,
203 },
204 Ordering::Relaxed,
205 );
206}
207
208/// Set the mode from `YO_ALLOC`, which is `off`, `report` or `abort`.
209///
210/// Answers `None` when the variable is set to something else, and the caller is
211/// expected to refuse to start rather than carry on. A typo that silently turns
212/// the check off is precisely the failure this module exists to prevent, so it
213/// is not treated as an unset variable.
214///
215/// An unset variable is [`Mode::Off`] and is not an error.
216#[must_use]
217pub fn set_mode_from_env() -> Option<Mode> {
218 let m = parse_mode(std::env::var("YO_ALLOC").ok().as_deref())?;
219 set_mode(m);
220 Some(m)
221}
222
223/// The reading half of [`set_mode_from_env`], split out so it can be tested
224/// without a process wide environment change.
225fn parse_mode(v: Option<&str>) -> Option<Mode> {
226 match v {
227 None | Some("" | "off") => Some(Mode::Off),
228 Some("report") => Some(Mode::Report),
229 Some("abort") => Some(Mode::Abort),
230 Some(_) => None,
231 }
232}
233
234/// Mark this thread for the length of the returned value, if the mode says so.
235///
236/// This is what a command loop wraps its dispatch in. It is a guard rather than
237/// a pair of calls because a panic in the middle of a batch would otherwise
238/// leave the thread marked for the rest of the process, and a thread that can
239/// never allocate again is a worse failure than the one being looked for.
240///
241/// A no-op under [`Mode::Off`], down to not touching the thread local, so the
242/// cost of having this in the loop when nobody asked for it is one relaxed load
243/// per batch.
244#[must_use = "the mark lasts as long as the guard, so dropping it here does nothing"]
245pub fn guard() -> Guard {
246 let on = mode() != Mode::Off;
247 if on {
248 enter_no_alloc();
249 }
250 Guard(on)
251}
252
253/// The mark from [`guard`], undone when it goes out of scope.
254#[derive(Debug)]
255pub struct Guard(bool);
256
257impl Drop for Guard {
258 #[inline]
259 fn drop(&mut self) {
260 if self.0 {
261 exit_no_alloc();
262 }
263 }
264}
265
266/// Everything [`Mode::Report`] collected, as `(sites, allocations)`.
267///
268/// Both are zero in the other two modes, which is what makes this worth calling
269/// unconditionally at shutdown.
270#[must_use]
271pub fn seen() -> (usize, u64) {
272 let n = SITES.lock().map_or(0, |s| s.len());
273 (n, SEEN_TOTAL.load(Ordering::Relaxed))
274}
275
276/// Mark this thread as a shard thread: from here on, allocating aborts.
277///
278/// Called once by each shard as it enters its loop. There is no matching exit
279/// in normal operation because a shard thread never stops being one. A loop that
280/// is not a shard's, and so does want the mark to end, wants [`guard`].
281#[inline]
282pub fn enter_no_alloc() {
283 // Latched here so that [`allow`] can be free in a process where nothing is
284 // ever marked, which is every shipped binary and every benchmark. It is
285 // never cleared: a process that has armed one thread once pays the full
286 // path for the rest of its life, which is the right way round because that
287 // process is the one being measured for violations rather than for speed.
288 ARMED.store(true, Ordering::Relaxed);
289 FORBID.with(|f| f.set(f.get().saturating_add(1)));
290}
291
292/// Undo one [`enter_no_alloc`].
293///
294/// Exists for tests and for the embedded single thread mode (`15` section 7),
295/// where the caller's thread is temporarily the shard and then goes back to
296/// being the caller's thread.
297#[inline]
298pub fn exit_no_alloc() {
299 FORBID.with(|f| f.set(f.get().saturating_sub(1)));
300}
301
302/// Whether allocation is currently forbidden on this thread.
303#[inline]
304pub fn is_forbidden() -> bool {
305 FORBID.with(|f| f.get()) > 0
306}
307
308/// Run `f` with allocation permitted, then restore the previous state.
309///
310/// Every call to this is a claim that the work inside is off the command path.
311/// Wrapping a command path in it to silence an abort is the one way to misuse
312/// this module, so the calls are meant to be few and easy to find.
313///
314/// # Cost
315///
316/// One relaxed load of a static when no thread in the process has ever been
317/// marked, and a thread local read and two writes when one has.
318///
319/// The load is in front of the rest because these calls are not all at the top
320/// of a function any more. The set algebra makes its claim around the insert
321/// that grows its table, which is once per member, because the alternative is
322/// one guard around the whole walk and that hides whatever the caller's closure
323/// does. A thread local read and two writes per member of a union is not
324/// obviously free, and a laptop with other work on it could not resolve the
325/// difference either way: two runs of `setops_small` with the same code in place
326/// disagreed by more than the effect being looked for. So this stopped being an
327/// argument about how cheap a thread local is and became a load of a static that
328/// is false in every shipped binary and in every benchmark.
329#[inline]
330pub fn allow<T>(f: impl FnOnce() -> T) -> T {
331 if !ARMED.load(Ordering::Relaxed) {
332 return f();
333 }
334 let saved = FORBID.with(|c| c.replace(0));
335 let guard = Restore(saved);
336 let out = f();
337 drop(guard);
338 out
339}
340
341/// Run `f`, which is a key coming into existence for the first time.
342///
343/// [`allow`] with a name on it, and the name is the claim. Y7 says no allocation
344/// on a command path, and the first `SADD` to a key that was not there has to
345/// make a set somewhere. There is no arrangement of this code that avoids it and
346/// no reason to want one: it happens once per key rather than once per command,
347/// and a workload that creates a key on every command is one where the
348/// allocation is the smallest thing it is paying for.
349///
350/// So the rule this module enforces is the one that is actually true. Nothing on
351/// a command path allocates except a key being created, and every place that
352/// does is this call, which makes the list of them a grep rather than an
353/// argument.
354///
355/// This is the one way to misuse the module. Wrapping steady state work in it to
356/// stop an abort would leave the check passing and the rule broken, which is
357/// worse than not having the check.
358#[inline]
359pub fn first_touch<T>(f: impl FnOnce() -> T) -> T {
360 allow(f)
361}
362
363/// Run `f`, which is a collection getting bigger because more was put in it.
364///
365/// [`allow`] with a name on it, and the name is the claim. The rule is that
366/// nothing is proportional to the number of commands served, not that nothing
367/// ever calls the allocator: an intset that has taken its ten thousandth member
368/// has to have grown nine times along the way, and there is no arrangement of
369/// that code which stores ten thousand integers in the room it had for eight.
370///
371/// The test is whether a workload that stops adding data stops allocating.
372/// [`first_touch`] is the same argument for a key that was not there at all,
373/// and this is the argument for the key that was.
374#[inline]
375pub fn for_the_data<T>(f: impl FnOnce() -> T) -> T {
376 allow(f)
377}
378
379/// Run `f`, which is a buffer the database keeps reaching a size it has never
380/// been asked for before.
381///
382/// [`allow`] with a name on it, and the name is the claim. A `ZRANDMEMBER` needs
383/// somewhere to shuffle and a `SUNION` needs somewhere to check for duplicates,
384/// and both of those are cleared and refilled rather than built and dropped. So
385/// the allocation is not per command, it is per high water mark: a database that
386/// has answered a union over a million members holds a million member table, and
387/// every union after it that is no larger pays the allocator nothing.
388///
389/// The test is whether the same call made twice allocates the second time. That
390/// is a unit test rather than an argument, and every site wrapped in this has
391/// one. [`for_the_data`] is the neighbouring claim, and the difference is that
392/// this memory is not holding anything between commands: it is scratch that has
393/// grown to fit the largest question asked so far.
394#[inline]
395pub fn high_water<T>(f: impl FnOnce() -> T) -> T {
396 allow(f)
397}
398
399struct Restore(u32);
400
401impl Drop for Restore {
402 #[inline]
403 fn drop(&mut self) {
404 FORBID.with(|c| c.set(self.0));
405 }
406}
407
408/// The allocator. Delegates to the system allocator and checks the flag first.
409#[derive(Debug, Default, Clone, Copy)]
410pub struct YoAlloc;
411
412impl YoAlloc {
413 /// A new allocator.
414 pub const fn new() -> YoAlloc {
415 YoAlloc
416 }
417}
418
419/// A marked thread allocated. Report it or stop the process.
420///
421/// [`Mode::Off`] lands here too and aborts, because a thread is only ever marked
422/// because something asked for it. [`guard`] does not mark under `Off`, so the
423/// only way to reach this with the mode off is a direct [`enter_no_alloc`], and
424/// that call means what it has always meant.
425#[cold]
426#[inline(never)]
427fn violation(layout: Layout, what: &str) {
428 if mode() == Mode::Report {
429 report(layout, what);
430 return;
431 }
432 abort_now(layout, what)
433}
434
435/// Note the site and let the allocation through.
436///
437/// Runs with the check suspended, because everything here allocates: capturing a
438/// backtrace, rendering it, and keeping it in a map. That is the deal in this
439/// mode. Without the suspension the first violation would recurse until the
440/// stack ran out, which is a worse way to learn about a `format!` in a hot loop
441/// than being told where it is.
442fn report(layout: Layout, what: &str) {
443 SEEN_TOTAL.fetch_add(1, Ordering::Relaxed);
444 allow(|| {
445 let trace = std::backtrace::Backtrace::force_capture().to_string();
446 // The whole rendered trace is the identity of the site. Two allocations
447 // from the same line reached by different callers are different entries,
448 // which is what you want when the line is inside a helper.
449 let key = fnv1a(trace.as_bytes());
450 let Ok(mut sites) = SITES.lock() else {
451 return;
452 };
453 let size = layout.size();
454 match sites.entry(key) {
455 std::collections::btree_map::Entry::Occupied(mut e) => {
456 let s = e.get_mut();
457 s.count += 1;
458 s.largest = s.largest.max(size);
459 }
460 std::collections::btree_map::Entry::Vacant(e) => {
461 e.insert(Site {
462 count: 1,
463 largest: size,
464 });
465 // Printed once, on the way in, so that a run that ends in a
466 // crash still leaves the list behind.
467 eprintln!("yo: allocation on a marked thread: {what} of {size} bytes\n{trace}");
468 }
469 }
470 });
471}
472
473/// 64 bit FNV-1a. Enough to tell two backtraces apart and short enough to write
474/// out rather than take a dependency for.
475fn fnv1a(bytes: &[u8]) -> u64 {
476 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
477 for &b in bytes {
478 h ^= u64::from(b);
479 h = h.wrapping_mul(0x0000_0100_0000_01b3);
480 }
481 h
482}
483
484#[cold]
485#[inline(never)]
486fn abort_now(layout: Layout, what: &str) -> ! {
487 // No formatting machinery here on purpose. `format!` allocates, and this is
488 // the one place in the process where allocating is known to be unavailable.
489 // Two `write_str` calls and an integer written by hand cost nothing and
490 // cannot recurse.
491 use std::io::Write as _;
492 let mut buf = [0u8; 32];
493 let n = write_usize(&mut buf, layout.size());
494 let mut err = std::io::stderr().lock();
495 let _ = err.write_all(b"yo: allocation on a shard thread: ");
496 let _ = err.write_all(what.as_bytes());
497 let _ = err.write_all(b" of ");
498 let _ = err.write_all(&buf[..n]);
499 let _ = err.write_all(
500 b" bytes.\nThis is Y7: no global allocator call on a command path.\n\
501 Move the allocation to setup, or wrap it in yo_alloc::allow if it is\n\
502 genuinely off the command path.\n",
503 );
504 let _ = err.flush();
505 std::process::abort()
506}
507
508fn write_usize(buf: &mut [u8; 32], mut v: usize) -> usize {
509 if v == 0 {
510 buf[0] = b'0';
511 return 1;
512 }
513 let mut tmp = [0u8; 32];
514 let mut n = 0;
515 while v > 0 {
516 tmp[n] = b'0' + (v % 10) as u8;
517 v /= 10;
518 n += 1;
519 }
520 for i in 0..n {
521 buf[i] = tmp[n - 1 - i];
522 }
523 n
524}
525
526// SAFETY: every method forwards to `System`, which upholds the `GlobalAlloc`
527// contract. The added check only ever diverges before calling through, so no
528// pointer is created, invalidated or leaked by it.
529unsafe impl GlobalAlloc for YoAlloc {
530 #[inline]
531 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
532 if is_forbidden() {
533 violation(layout, "alloc");
534 }
535 // SAFETY: forwarding the caller's own valid layout.
536 unsafe { System.alloc(layout) }
537 }
538
539 #[inline]
540 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
541 if is_forbidden() {
542 violation(layout, "alloc_zeroed");
543 }
544 // SAFETY: forwarding the caller's own valid layout.
545 unsafe { System.alloc_zeroed(layout) }
546 }
547
548 #[inline]
549 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
550 if is_forbidden() {
551 violation(layout, "realloc");
552 }
553 // SAFETY: forwarding the caller's own valid pointer and layout.
554 unsafe { System.realloc(ptr, layout, new_size) }
555 }
556
557 #[inline]
558 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
559 // Deallocation is deliberately not checked. A value allocated during
560 // setup and dropped on the shard thread is normal and harmless, and
561 // aborting on it would make the rule unusable. What costs time is the
562 // allocation, and that is what is caught.
563 //
564 // SAFETY: forwarding the caller's own valid pointer and layout.
565 unsafe { System.dealloc(ptr, layout) }
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn starts_permitted() {
575 assert!(!is_forbidden());
576 }
577
578 #[test]
579 fn enter_and_exit_are_balanced() {
580 assert!(!is_forbidden());
581 enter_no_alloc();
582 assert!(is_forbidden());
583 enter_no_alloc();
584 assert!(is_forbidden());
585 exit_no_alloc();
586 assert!(is_forbidden(), "one exit must not undo two enters");
587 exit_no_alloc();
588 assert!(!is_forbidden());
589 }
590
591 #[test]
592 fn allow_permits_and_restores() {
593 enter_no_alloc();
594 assert!(is_forbidden());
595 let v = allow(|| {
596 assert!(!is_forbidden());
597 vec![1u8, 2, 3]
598 });
599 assert_eq!(v.len(), 3);
600 assert!(is_forbidden(), "allow must restore the previous state");
601 exit_no_alloc();
602 }
603
604 #[test]
605 fn allow_nests() {
606 enter_no_alloc();
607 allow(|| {
608 allow(|| assert!(!is_forbidden()));
609 assert!(!is_forbidden());
610 });
611 assert!(is_forbidden());
612 exit_no_alloc();
613 }
614
615 #[test]
616 fn allow_restores_when_the_body_panics() {
617 enter_no_alloc();
618 let r = std::panic::catch_unwind(|| {
619 allow(|| panic!("boom"));
620 });
621 assert!(r.is_err());
622 assert!(
623 is_forbidden(),
624 "a panic inside allow must not leave the thread permitted"
625 );
626 exit_no_alloc();
627 }
628
629 /// The flag is per thread. A shard marking itself must not affect the
630 /// accept loop or a test harness thread.
631 #[test]
632 fn the_flag_does_not_cross_threads() {
633 enter_no_alloc();
634 let other = std::thread::spawn(is_forbidden).join().unwrap();
635 assert!(!other, "another thread saw this thread's flag");
636 exit_no_alloc();
637 }
638
639 /// The mode is one static for the whole process, so the tests that move it
640 /// take turns. Without this they would race with each other rather than with
641 /// anything real.
642 static MODE_TESTS: Mutex<()> = Mutex::new(());
643
644 fn one_at_a_time() -> std::sync::MutexGuard<'static, ()> {
645 MODE_TESTS.lock().unwrap_or_else(|e| e.into_inner())
646 }
647
648 #[test]
649 fn the_mode_starts_off_and_survives_a_round_trip() {
650 let _turn = one_at_a_time();
651 assert_eq!(Mode::default(), Mode::Off, "off is the default");
652 for m in [Mode::Report, Mode::Abort, Mode::Off] {
653 set_mode(m);
654 assert_eq!(mode(), m);
655 }
656 }
657
658 #[test]
659 fn the_env_variable_reads_three_words_and_refuses_the_rest() {
660 assert_eq!(parse_mode(None), Some(Mode::Off));
661 assert_eq!(parse_mode(Some("")), Some(Mode::Off));
662 assert_eq!(parse_mode(Some("off")), Some(Mode::Off));
663 assert_eq!(parse_mode(Some("report")), Some(Mode::Report));
664 assert_eq!(parse_mode(Some("abort")), Some(Mode::Abort));
665 // A typo has to be an error rather than a quiet off, because a quiet off
666 // is the check not running while somebody believes it is.
667 assert_eq!(parse_mode(Some("abrot")), None);
668 assert_eq!(parse_mode(Some("Report")), None);
669 assert_eq!(parse_mode(Some("1")), None);
670 }
671
672 /// Both halves of the guard, in a thread of its own so that setting the mode
673 /// cannot be seen by another test's assertion about the flag.
674 #[test]
675 fn the_guard_marks_only_when_the_mode_asks() {
676 let _turn = one_at_a_time();
677 std::thread::spawn(|| {
678 set_mode(Mode::Off);
679 {
680 let _g = guard();
681 assert!(!is_forbidden(), "off must not mark the thread at all");
682 }
683 for m in [Mode::Report, Mode::Abort] {
684 set_mode(m);
685 {
686 let _g = guard();
687 assert!(is_forbidden(), "{m:?} must mark it");
688 }
689 assert!(!is_forbidden(), "and the guard must undo it");
690 }
691 set_mode(Mode::Off);
692 })
693 .join()
694 .unwrap();
695 }
696
697 #[test]
698 fn the_guard_unmarks_when_the_body_panics() {
699 let _turn = one_at_a_time();
700 std::thread::spawn(|| {
701 set_mode(Mode::Report);
702 let r = std::panic::catch_unwind(|| {
703 let _g = guard();
704 assert!(is_forbidden());
705 panic!("boom");
706 });
707 assert!(r.is_err());
708 assert!(
709 !is_forbidden(),
710 "a panic inside the guard must not leave the thread marked forever"
711 );
712 set_mode(Mode::Off);
713 })
714 .join()
715 .unwrap();
716 }
717
718 /// Report mode has to survive the thing it is reporting on, because the
719 /// reporting itself allocates on a thread where allocating is what set it
720 /// off. It is driven directly here rather than through a real allocation,
721 /// since this crate's own test binary deliberately does not install the
722 /// allocator: several of the tests above spawn threads and panic while the
723 /// flag is up, which is exactly what an installed one would abort on.
724 #[test]
725 fn report_mode_records_instead_of_aborting() {
726 let _turn = one_at_a_time();
727 std::thread::spawn(|| {
728 let (sites_before, total_before) = seen();
729 set_mode(Mode::Report);
730 {
731 let _g = guard();
732 assert!(is_forbidden());
733 // Three from one line, so the total moves by three and the site
734 // is only recorded, and printed, once.
735 for size in [8usize, 64, 4096] {
736 let layout = Layout::from_size_align(size, 8).unwrap();
737 violation(layout, "alloc");
738 }
739 assert!(is_forbidden(), "reporting must put the mark back");
740 }
741 set_mode(Mode::Off);
742
743 let (sites, total) = seen();
744 assert_eq!(total - total_before, 3, "every violation is counted");
745 assert_eq!(sites - sites_before, 1, "one line is one site");
746 })
747 .join()
748 .unwrap();
749 }
750
751 #[test]
752 fn distinct_traces_hash_apart() {
753 assert_ne!(fnv1a(b"one"), fnv1a(b"two"));
754 assert_eq!(fnv1a(b"same"), fnv1a(b"same"));
755 }
756
757 #[test]
758 fn integers_render_without_allocating() {
759 let mut buf = [0u8; 32];
760 for (v, want) in [
761 (0usize, "0"),
762 (7, "7"),
763 (1024, "1024"),
764 (2097152, "2097152"),
765 ] {
766 let n = write_usize(&mut buf, v);
767 assert_eq!(std::str::from_utf8(&buf[..n]).unwrap(), want);
768 }
769 }
770}