We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modules, Visibility, Testing and Docs step 20 of 22
Build your own #[test]: catching panics deterministically
#[test] cannot be graded on this site. cfg(test) is set only by
rustc --test, and submissions are compiled with plain rustc -O, so a
#[cfg(test)] mod tests { … } block is deleted before type-checking: it
compiles clean, never runs, and clippy never even looks inside it. You could put
assert_eq!(1, 2) in there and pass.
So instead of writing #[test] twenty times, you are going to rebuild it.
That turns out to be the better lesson anyway: by the end you will understand
unwinding, panic payloads and the recoverable/unrecoverable split far more
concretely than any amount of annotating would have taught you.
pub struct Case {
pub name: String,
pub op: String,
pub args: Vec<i64>,
pub should_panic: Option<String>,
pub ignore: bool,
}
pub struct Outcome {
pub name: String,
pub status: String,
}
pub fn run_suite(cases: Vec<Case>) -> Vec<Outcome>
Output order is input order. Nothing here is timing-dependent, and that is deliberate.
The statuses
| status | when |
|---|---|
ignored |
ignore is set. The body is not executed. |
ok |
ran, did not panic, and no panic was expected |
failed |
panicked when none was expected, or did not panic when one was |
panicked_as_expected |
panicked, and the payload contains the expected substring |
wrong_panic_message |
panicked, but the payload does not contain it |
should_panic: Some(expected) models #[should_panic(expected = "…")], and the
comparison really is substring containment, not equality — that is what the
real attribute does.
Some("") models bare #[should_panic] with no expected, and notice what
falls out: the empty string is contained in everything, so any panic passes.
Including one thrown by a completely unrelated line earlier in the test. That is
a real source of false confidence, and it is why
clippy::should_panic_without_expect exists.
The ops
op selects one of six bodies. They are uninteresting except that they fail in
different ways:
| op | behaviour |
|---|---|
ok |
sums args, never panics |
div |
args[0] / args[1] |
idx |
indexes a three-element array by args[0] |
boom |
panic!("boom") |
boom_fmt |
panic!("boom {}", args[0]) |
halt |
calls std::process::exit(97) |
halt is how “ignored means not executed” gets graded
#[ignore] does not mean “run it and throw the result away”. The body never
happens. That is normally impossible to observe from the outside — so one test
case marks a halt case as ignored. If your runner executes it, the process
disappears mid-suite and every case fails at once. Not subtle, and not catchable
by catch_unwind either: exit does not unwind.
The payload downcast, which must try both types
A panic payload is a Box<dyn Any + Send>, and the concrete type depends on how
the panic was written:
panic!("boom") // payload is &'static str
panic!("boom {}", n) // payload is String (n is a runtime value)
Handling only one of the two silently misreads half of all panics. div and
boom give you &'static str; idx and boom_fmt give you String. The
starter handles only String, which is why it gets wrong_panic_message on
things that panicked exactly as asked.
Unwinding, and what AssertUnwindSafe actually asserts
catch_unwind requires its closure to be UnwindSafe. That is not a safety
property in the unsafe sense — it is a logical one. A panic can leave data
half-updated: a &mut you were partway through mutating, an invariant restored
on the next line that never ran. UnwindSafe marks the types where observing
that half-state after catching cannot cause a logic bug.
Most useful closures are rejected, and AssertUnwindSafe(f) is the normal
answer. What you are asserting is “I have thought about the state this closure
touches, and looking at it after a panic is fine.” Do not treat it as
boilerplate — that is the one habit that turns a compiler question into a real
bug later.
Two more things worth carrying:
-
Suppress the hook, then put it back.
catch_unwindcatches the unwind, but the default hook still prints a backtrace on the way past.take_hook()returns the old one;set_hook(Box::new(|_| {}))installs silence;set_hook(previous)restores it. The hook is process-global, so restoring it is not politeness. -
Do not build error handling out of this.
catch_unwindis a test-harness and FFI-boundary tool. Recoverable failure isResult; panics are for bugs. Also, dropping a panic payload can itself panic, and a panic during a panic aborts the process with no unwinding at all.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.