Skip to content

← Modules, Visibility, Testing and Docs step 20 of 22

Hard Framework

Build a test runner that catches panics

Implement a small test runner that executes cases, catches unwinding panics and reports whether each result matches its expectation.

The site compiles submissions as ordinary binaries, without rustc --test. Its current compiler and Clippy commands do not enable cfg(test), so code inside a #[cfg(test)] module is not type-checked or run by those commands. This exercise exposes the runner as a function the grading harness can call.

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>

Keep run_op unchanged. Return one outcome per input case, in input order, preserving each name exactly. Duplicate and empty names are allowed; an empty suite returns an empty vector.

Choosing the status

Check ignore before executing a case. Otherwise call run_op inside catch_unwind and compare the result with should_panic.

Condition Status
ignore is true ignored, without running the body
Returned normally and no panic was expected ok
Panicked with no expectation, or returned normally when a panic was expected failed
Panicked and its message contains the expected substring panicked_as_expected
Panicked but its message lacks the expected substring wrong_panic_message

Matching is case-sensitive substring containment, not equality. For example, Some("boom") matches "boom 7", while Some("BOOM") does not. Some("") accepts any panic because every message contains the empty string. It still fails when the body returns normally. Use a meaningful substring when you need to distinguish an intended panic from an unrelated one.

The supplied operations

op Behavior
ok, or any unrecognized name Sum args; an empty list sums to zero
div Evaluate args[0] / args[1]
idx Index [10_i64, 20, 30] using args[0] as usize
boom Panic with "boom"
boom_fmt Panic with the formatted message "boom <args[0]>"
halt Exit the process with status 97

Missing required arguments cause indexing panics. Division by zero and i64::MIN / -1 also panic. An index outside the three-element table panics; the cast to usize follows the target's integer-cast rules. Sum inputs are chosen so their intermediate additions fit in i64.

The graded halt cases are ignored. They check that ignored bodies really are skipped: catch_unwind cannot catch process exit. If you run halt, there is no suite result to return.

Reading a panic message

catch_unwind returns an error payload of type Box<dyn Any + Send>. Handle both &'static str and String. Do not rely on every operation always choosing the same payload representation. For a payload of neither type, use an empty message in this runner.

This complete example supplies both string types explicitly and recovers their messages. Its default panic hook may still print diagnostics:

use std::any::Any;
use std::panic::{catch_unwind, panic_any};

fn message(payload: &(dyn Any + Send)) -> &str {
    if let Some(text) = payload.downcast_ref::<&'static str>() {
        text
    } else if let Some(text) = payload.downcast_ref::<String>() {
        text.as_str()
    } else {
        ""
    }
}

fn main() {
    let borrowed = catch_unwind(|| -> () { panic_any("boom") });
    let owned = catch_unwind(|| -> () { panic_any(String::from("boom 7")) });
    if let (Err(a), Err(b)) = (borrowed, owned) {
        assert_eq!(message(&*a), "boom");
        assert_eq!(message(&*b), "boom 7");
    } else {
        panic!("both calls should panic");
    }
}

Suppressing and restoring the hook

The panic hook runs before unwinding, even when catch_unwind later catches the panic. The default hook prints a diagnostic and may print a backtrace, depending on configuration.

Save the existing hook with take_hook, install a silent hook, and restore the saved hook before returning. Restore it for an empty suite too. This exercise runs the suite serially with no other code changing the hook. Hooks are process-global: changing one in a general concurrent runner can affect unrelated threads, even if you restore it afterward.

What catching a panic does not guarantee

UnwindSafe is an advisory trait that draws attention to state which might be left inconsistent by a panic. It does not prove the absence of logic bugs. The supplied closure only reads its case data and already meets the bound; it needs no AssertUnwindSafe wrapper.

If you later capture mutable state, review what happens to that state before using AssertUnwindSafe. The wrapper overrides the trait check; it does not restore invariants or roll back changes.

catch_unwind catches unwinding panics, not process exit, aborts or panics compiled to abort. The supplied operations use string payloads. In more general code, dropping a custom panic payload can itself panic; a new panic during an active unwind can abort the process. Prefer Result for ordinary recoverable failures.

Submissions must compile, pass the tests and pass clippy -D warnings.