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}