tor_memquota/
drop_bomb.rs

1//! Drop bombs, for assurance of postconditions when types are dropped
2//!
3//! Provides two drop bomb types: [`DropBomb`] and [`DropBombCondition`].
4//!
5//! These help assure that our algorithms are correct,
6//! by detecting when types that contain the bomb are dropped inappropriately.
7//!
8//! # No-op outside `#[cfg(test)]`
9//!
10//! When used outside test code, these types are unit ZSTs,
11//! and are completely inert.
12//! They won't cause panics or detect bugs, in production.
13//!
14//! # Panics (in tests), and simulation
15//!
16//! These types work by panicking in drop, when a bug is detected.
17//! This will then cause a test failure.
18//! Such panics are described as "explodes (panics)" in the documentation.
19//!
20//! There are also simulated drop bombs, whose explosions do not actually panic.
21//! Instead, they record that a panic would have occurred,
22//! and print a message to stderr.
23//! The constructors provide a handle to allow the caller to enquire about explosions.
24//! This allows for testing a containing type's drop bomb logic.
25//!
26//! Certain misuses result in actual panics, even with simulated bombs.
27//! This is described as "panics (actually)".
28//!
29//! # Choosing a bomb
30//!
31//! [`DropBomb`] is for assuring the runtime context or appropriate timing of drops
32//! (and could be used for implementing general conditions).
33//!
34//! [`DropBombCondition`] is for assuring the properties of a value that is being dropped.
35
36use crate::internal_prelude::*;
37
38#[cfg(test)]
39use std::sync::atomic::{AtomicBool, Ordering};
40
41//---------- macros used in this module, and supporting trait ----------
42
43define_derive_deftly! {
44    /// Helper for common impls on bombs
45    ///
46    ///  * Provides `fn new_armed`
47    ///  * Provides `fn new_simulated`
48    ///  * Implements `Drop`, using `TestableDrop::drop_impl`
49    BombImpls:
50
51    impl $ttype {
52        /// Create a new drop bomb, which must be properly disposed of
53        pub(crate) const fn new_armed() -> Self {
54            let status = Status::ARMED_IN_TESTS;
55            $ttype { status }
56        }
57    }
58
59    #[cfg(test)]
60    impl $ttype {
61        /// Create a simulated drop bomb
62        pub(crate) fn new_simulated() -> (Self, SimulationHandle) {
63            let handle = SimulationHandle::new();
64            let status = S::ArmedSimulated(handle.clone());
65            ($ttype { status }, handle)
66        }
67
68        /// Turn an existing armed drop bomb into a simulated one
69        ///
70        /// This is useful for writing test cases, without having to make a `new_simulated`
71        /// constructor for whatever type contains the drop bomb.
72        /// Instead, construct it normally, and then reach in and call this on the bomb.
73        ///
74        /// # Panics
75        ///
76        /// `self` must be armed.  Otherwise, (actually) panics.
77        pub(crate) fn make_simulated(&mut self) -> SimulationHandle {
78            let handle = SimulationHandle::new();
79            let new_status = S::ArmedSimulated(handle.clone());
80            let old_status = mem::replace(&mut self.status, new_status);
81            assert!(matches!(old_status, S::Armed));
82            handle
83        }
84
85        /// Implemnetation of `Drop::drop`, split out for testability.
86        ///
87        /// Calls `drop_status`, and replaces `self.status` with `S::Disarmed`,
88        /// so that `self` can be actually dropped (if we didn't panic).
89        fn drop_impl(&mut self) {
90            // Do the replacement first, so that if drop_status unwinds, we don't panic in panic.
91            let status = mem::replace(&mut self.status, S::Disarmed);
92            <$ttype as DropStatus>::drop_status(status);
93        }
94    }
95
96
97    #[cfg(test)]
98    impl Drop for $ttype {
99        fn drop(&mut self) {
100            // We don't check for unwinding.
101            // We shouldn't drop a nonzero one of these even if we're panicking.
102            // If we do, it'll be a double panic => abort.
103            self.drop_impl();
104        }
105    }
106}
107
108/// Core of `Drop`, that can be called separately, for testing
109///
110/// To use: implement this, and derive deftly
111/// [`BombImpls`](derive_deftly_template_BombImpls).
112#[allow(unused)]
113trait DropStatus {
114    /// Handles dropping of a `Self` with this `status` field value
115    fn drop_status(status: Status);
116}
117
118//---------- public types ----------
119
120/// Drop bomb: for assuring that drops happen only when expected
121///
122/// Obtained from [`DropBomb::new_armed()`].
123///
124/// # Explosions
125///
126/// Explodes (panicking) if dropped,
127/// unless [`.disarm()`](DropBomb::disarm) is called first.
128#[derive(Deftly, Debug)]
129#[derive_deftly(BombImpls)]
130pub(crate) struct DropBomb {
131    /// What state are we in
132    status: Status,
133}
134
135/// Drop condition: for ensuring that a condition is true, on drop
136///
137/// Obtained from [`DropBombCondition::new_armed()`].
138///
139/// Instead of dropping this, you must call
140/// `drop_bomb_disarm_assert!`
141/// (or its internal function `disarm_assert()`.
142// rustdoc can't manage to make a link to this crate-private macro or cfg-test item.
143///
144/// It will often be necessary to add `#[allow(dead_code)]`
145/// on the `DropBombCondition` field of a containing type,
146/// since outside tests, the `Drop` impl will usually be configured out,
147/// and that's the only place this field is actually read.
148///
149/// # Panics
150///
151/// Panics (actually) if it is simply dropped.
152#[derive(Deftly, Debug)]
153#[derive_deftly(BombImpls)]
154pub(crate) struct DropBombCondition {
155    /// What state are we in
156    #[allow(dead_code)] // not read outside tests
157    status: Status,
158}
159
160/// Handle onto a simulated [`DropBomb`] or [`DropCondition`]
161///
162/// Can be used to tell whether the bomb "exploded"
163/// (ie, whether `drop` would have panicked, if this had been a non-simulated bomb).
164#[cfg(test)]
165#[derive(Debug)]
166pub(crate) struct SimulationHandle {
167    exploded: Arc<AtomicBool>,
168}
169
170/// Unit token indicating that a simulated drop bomb did explode, and would have panicked
171#[cfg(test)]
172#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
173pub(crate) struct SimulationExploded;
174
175//---------- internal types ----------
176
177/// State of some kind of drop bomb
178///
179/// This type is inert; the caller is responsible for exploding or panicking.
180#[derive(Debug)]
181enum Status {
182    /// This bomb is disarmed and will not panic.
183    ///
184    /// This is always the case outside `#[cfg(test)]`
185    Disarmed,
186
187    /// This bomb is armed.  It will (or may) panic on drop.
188    #[cfg(test)]
189    Armed,
190
191    /// This bomb is armed, but we're running in simulation.
192    #[cfg(test)]
193    ArmedSimulated(SimulationHandle),
194}
195
196use Status as S;
197
198//---------- DropBomb impls ----------
199
200impl DropBomb {
201    /// Disarm this bomb.
202    ///
203    /// It will no longer explode (panic) when dropped.
204    pub(crate) fn disarm(&mut self) {
205        self.status = S::Disarmed;
206    }
207}
208
209#[cfg(test)]
210impl DropStatus for DropBomb {
211    fn drop_status(status: Status) {
212        match status {
213            S::Disarmed => {}
214            S::Armed => panic!("DropBomb dropped without a previous call to .disarm()"),
215            S::ArmedSimulated(handle) => handle.set_exploded(),
216        }
217    }
218}
219
220//---------- DropCondition impls ----------
221
222/// Check the condition, and disarm the bomb
223///
224/// If `CONDITION` is true, disarms the bomb; otherwise, explodes (panics).
225///
226/// # Syntax
227///
228/// ```
229/// drop_bomb_disarm_assert!(BOMB, CONDITION);
230/// drop_bomb_disarm_assert!(BOMB, CONDITION, "FORMAT", FORMAT_ARGS..);
231/// ```
232///
233/// where
234///
235///  * `BOMB: &mut DropCondition` (or something that derefs to that).
236///  * `CONDITION: bool`
237///
238/// # Example
239///
240/// ```
241/// # struct S { drop_bomb: DropCondition };
242/// # impl S { fn f(&mut self) {
243/// drop_bomb_disarm_assert!(self.drop_bomb, self.raw, Qty(0));
244/// # } }
245/// ```
246///
247/// # Explodes
248///
249/// Explodes unless the condition is satisfied.
250//
251// This macro has this long name because we can't do scoping of macro-rules macros.
252#[cfg(test)] // Should not be used outside tests, since the drop impls should be conditional
253macro_rules! drop_bomb_disarm_assert {
254    { $bomb:expr, $condition:expr $(,)? } => {
255        $bomb.disarm_assert(
256            || $condition,
257            format_args!(concat!("condition = ", stringify!($condition))),
258        )
259    };
260    { $bomb:expr, $condition:expr, $fmt:literal $($rest:tt)* } => {
261        $bomb.disarm_assert(
262            || $condition,
263            format_args!(concat!("condition = ", stringify!($condition), ": ", $fmt),
264                         $($rest)*),
265        )
266    };
267}
268
269impl DropBombCondition {
270    /// Check a condition, and disarm the bomb
271    ///
272    /// If `call()` returns true, disarms the bomb; otherwise, explodes (panics).
273    ///
274    /// # Explodes
275    ///
276    /// Explodes unless the condition is satisfied.
277    #[inline]
278    #[cfg(test)] // Should not be used outside tests, since the drop impls should be conditional
279    pub(crate) fn disarm_assert(&mut self, call: impl FnOnce() -> bool, msg: fmt::Arguments) {
280        match mem::replace(&mut self.status, S::Disarmed) {
281            S::Disarmed => {
282                // outside cfg(test), this is the usual path.
283                // placate the compiler: we ignore all our arguments
284                let _ = call;
285                let _ = msg;
286
287                #[cfg(test)]
288                panic!("disarm_assert called more than once!");
289            }
290            #[cfg(test)]
291            S::Armed => {
292                if !call() {
293                    panic!("drop condition violated: dropped, but condition is false: {msg}");
294                }
295            }
296            #[cfg(test)]
297            #[allow(clippy::print_stderr)]
298            S::ArmedSimulated(handle) => {
299                if !call() {
300                    eprintln!("drop condition violated in simulation: {msg}");
301                    handle.set_exploded();
302                }
303            }
304        }
305    }
306}
307
308/// Ideally, if you use this, your struct's other default values meet your drop condition!
309impl Default for DropBombCondition {
310    fn default() -> DropBombCondition {
311        Self::new_armed()
312    }
313}
314
315#[cfg(test)]
316impl DropStatus for DropBombCondition {
317    fn drop_status(status: Status) {
318        assert!(matches!(status, S::Disarmed));
319    }
320}
321
322//---------- SimulationHandle impls ----------
323
324#[cfg(test)]
325impl SimulationHandle {
326    /// Determine whether a drop bomb would have been triggered
327    ///
328    /// If the corresponding [`DropBomb]` or [`DropCondition`]
329    /// would have panicked (if we weren't simulating),
330    /// returns `Err`.
331    ///
332    /// # Panics
333    ///
334    /// The corresponding `DropBomb` or `DropCondition` must have been dropped.
335    /// Otherwise, calling `outcome` will (actually) panic.
336    pub(crate) fn outcome(mut self) -> Result<(), SimulationExploded> {
337        let panicked = Arc::into_inner(mem::take(&mut self.exploded))
338            .expect("bomb has not yet been dropped")
339            .into_inner();
340        if panicked {
341            Err(SimulationExploded)
342        } else {
343            Ok(())
344        }
345    }
346
347    /// Require that this bomb did *not* explode
348    ///
349    /// # Panics
350    ///
351    /// Panics if corresponding `DropBomb` hasn't yet been dropped,
352    /// or if it exploded when it was dropped.
353    pub(crate) fn expect_ok(self) {
354        let () = self.outcome().expect("bomb unexpectedly exploded");
355    }
356
357    /// Require that this bomb *did* explode
358    ///
359    /// # Panics
360    ///
361    /// Panics if corresponding `DropBomb` hasn't yet been dropped,
362    /// or if it did *not* explode when it was dropped.
363    pub(crate) fn expect_exploded(self) {
364        let SimulationExploded = self
365            .outcome()
366            .expect_err("bomb unexpectedly didn't explode");
367    }
368
369    /// Return a new handle with no explosion recorded
370    fn new() -> Self {
371        SimulationHandle {
372            exploded: Default::default(),
373        }
374    }
375
376    /// Return a clone of this handle
377    //
378    // Deliberately not a public Clone impl
379    fn clone(&self) -> Self {
380        SimulationHandle {
381            exploded: self.exploded.clone(),
382        }
383    }
384
385    /// Mark this simulated bomb as having exploded
386    fn set_exploded(&self) {
387        self.exploded.store(true, Ordering::Release);
388    }
389}
390
391//---------- internal impls ----------
392
393impl Status {
394    /// Armed, in tests
395    #[cfg(test)]
396    const ARMED_IN_TESTS: Status = S::Armed;
397
398    /// "Armed", outside tests, is in fact not armed
399    #[cfg(not(test))]
400    const ARMED_IN_TESTS: Status = S::Disarmed;
401}
402
403#[cfg(test)]
404mod test {
405    // @@ begin test lint list maintained by maint/add_warning @@
406    #![allow(clippy::bool_assert_comparison)]
407    #![allow(clippy::clone_on_copy)]
408    #![allow(clippy::dbg_macro)]
409    #![allow(clippy::mixed_attributes_style)]
410    #![allow(clippy::print_stderr)]
411    #![allow(clippy::print_stdout)]
412    #![allow(clippy::single_char_pattern)]
413    #![allow(clippy::unwrap_used)]
414    #![allow(clippy::unchecked_duration_subtraction)]
415    #![allow(clippy::useless_vec)]
416    #![allow(clippy::needless_pass_by_value)]
417    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
418    #![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
419
420    use super::*;
421    use std::any::Any;
422    use std::panic::catch_unwind;
423
424    #[test]
425    fn bomb_disarmed() {
426        let mut b = DropBomb::new_armed();
427        b.disarm();
428        drop(b);
429    }
430
431    #[test]
432    fn bomb_panic() {
433        let mut b = DropBomb::new_armed();
434        let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| b.drop_impl())).unwrap_err();
435    }
436
437    #[test]
438    fn bomb_sim_disarmed() {
439        let (mut b, h) = DropBomb::new_simulated();
440        b.disarm();
441        drop(b);
442        h.expect_ok();
443    }
444
445    #[test]
446    fn bomb_sim_explosion() {
447        let (b, h) = DropBomb::new_simulated();
448        drop(b);
449        h.expect_exploded();
450    }
451
452    #[test]
453    fn bomb_make_sim_explosion() {
454        let mut b = DropBomb::new_armed();
455        let h = b.make_simulated();
456        drop(b);
457        h.expect_exploded();
458    }
459
460    struct HasBomb {
461        on_drop: Result<(), ()>,
462        bomb: DropBombCondition,
463    }
464
465    impl Drop for HasBomb {
466        fn drop(&mut self) {
467            drop_bomb_disarm_assert!(self.bomb, self.on_drop.is_ok());
468        }
469    }
470
471    #[test]
472    fn cond_ok() {
473        let hb = HasBomb {
474            on_drop: Ok(()),
475            bomb: DropBombCondition::new_armed(),
476        };
477        drop(hb);
478    }
479
480    #[test]
481    fn cond_sim_explosion() {
482        let (bomb, h) = DropBombCondition::new_simulated();
483        let hb = HasBomb {
484            on_drop: Err(()),
485            bomb,
486        };
487        drop(hb);
488        h.expect_exploded();
489    }
490
491    #[test]
492    fn cond_explosion_panic() {
493        // make an actual panic
494        let mut bomb = DropBombCondition::new_armed();
495        let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| {
496            bomb.disarm_assert(|| false, format_args!("testing"));
497        }))
498        .unwrap_err();
499    }
500
501    #[test]
502    fn cond_forgot_drop_impl() {
503        // pretend that we put a DropBombCondition on this,
504        // but we forgot to impl Drop and call drop_bomb_disarm_assert
505        struct ForgotDropImpl {
506            bomb: DropBombCondition,
507        }
508        let fdi = ForgotDropImpl {
509            bomb: DropBombCondition::new_armed(),
510        };
511        // pretend that fdi is being dropped
512        let mut bomb = fdi.bomb; // move out
513
514        let _: Box<dyn Any> = catch_unwind(AssertUnwindSafe(|| bomb.drop_impl())).unwrap_err();
515    }
516}