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

            
36
use crate::internal_prelude::*;
37

            
38
#[cfg(test)]
39
use std::sync::atomic::{AtomicBool, Ordering};
40

            
41
//---------- macros used in this module, and supporting trait ----------
42

            
43
define_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
355818
        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
6
        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
6
        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
111500
        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
111496
        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)]
113
trait 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)]
130
pub(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)]
154
pub(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)]
166
pub(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)]
173
pub(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)]
181
enum 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

            
196
use Status as S;
197

            
198
//---------- DropBomb impls ----------
199

            
200
impl DropBomb {
201
    /// Disarm this bomb.
202
    ///
203
    /// It will no longer explode (panic) when dropped.
204
378
    pub(crate) fn disarm(&mut self) {
205
378
        self.status = S::Disarmed;
206
378
    }
207
}
208

            
209
#[cfg(test)]
210
impl DropStatus for DropBomb {
211
388
    fn drop_status(status: Status) {
212
388
        match status {
213
380
            S::Disarmed => {}
214
2
            S::Armed => panic!("DropBomb dropped without a previous call to .disarm()"),
215
6
            S::ArmedSimulated(handle) => handle.set_exploded(),
216
        }
217
386
    }
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
253
macro_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

            
269
impl 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
111108
    pub(crate) fn disarm_assert(&mut self, call: impl FnOnce() -> bool, msg: fmt::Arguments) {
280
111108
        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
111104
                if !call() {
293
2
                    panic!("drop condition violated: dropped, but condition is false: {msg}");
294
111102
                }
295
            }
296
            #[cfg(test)]
297
            #[allow(clippy::print_stderr)]
298
4
            S::ArmedSimulated(handle) => {
299
4
                if !call() {
300
4
                    eprintln!("drop condition violated in simulation: {msg}");
301
4
                    handle.set_exploded();
302
4
                }
303
            }
304
        }
305
111106
    }
306
}
307

            
308
/// Ideally, if you use this, your struct's other default values meet your drop condition!
309
impl Default for DropBombCondition {
310
285244
    fn default() -> DropBombCondition {
311
285244
        Self::new_armed()
312
285244
    }
313
}
314

            
315
#[cfg(test)]
316
impl DropStatus for DropBombCondition {
317
111112
    fn drop_status(status: Status) {
318
111112
        assert!(matches!(status, S::Disarmed));
319
111110
    }
320
}
321

            
322
//---------- SimulationHandle impls ----------
323

            
324
#[cfg(test)]
325
impl 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
12
    pub(crate) fn outcome(mut self) -> Result<(), SimulationExploded> {
337
12
        let panicked = Arc::into_inner(mem::take(&mut self.exploded))
338
12
            .expect("bomb has not yet been dropped")
339
12
            .into_inner();
340
12
        if panicked {
341
10
            Err(SimulationExploded)
342
        } else {
343
2
            Ok(())
344
        }
345
12
    }
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
2
    pub(crate) fn expect_ok(self) {
354
2
        let () = self.outcome().expect("bomb unexpectedly exploded");
355
2
    }
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
10
    pub(crate) fn expect_exploded(self) {
364
10
        let SimulationExploded = self
365
10
            .outcome()
366
10
            .expect_err("bomb unexpectedly didn't explode");
367
10
    }
368

            
369
    /// Return a new handle with no explosion recorded
370
12
    fn new() -> Self {
371
12
        SimulationHandle {
372
12
            exploded: Default::default(),
373
12
        }
374
12
    }
375

            
376
    /// Return a clone of this handle
377
    //
378
    // Deliberately not a public Clone impl
379
12
    fn clone(&self) -> Self {
380
12
        SimulationHandle {
381
12
            exploded: self.exploded.clone(),
382
12
        }
383
12
    }
384

            
385
    /// Mark this simulated bomb as having exploded
386
10
    fn set_exploded(&self) {
387
10
        self.exploded.store(true, Ordering::Release);
388
10
    }
389
}
390

            
391
//---------- internal impls ----------
392

            
393
impl 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)]
404
mod 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
}