#panic #testing

panic-control

Utilities to test code with intentional panics

5 releases

Uses old Rust 2015

0.1.4 Apr 14, 2018
0.1.3 Dec 1, 2017
0.1.2 Nov 29, 2017
0.1.1 Nov 28, 2017
0.1.0 Nov 28, 2017

#431 in Testing

Download history 62/week @ 2022-08-13 30/week @ 2022-08-20 36/week @ 2022-08-27 50/week @ 2022-09-03 55/week @ 2022-09-10 43/week @ 2022-09-17 31/week @ 2022-09-24 88/week @ 2022-10-01 64/week @ 2022-10-08 25/week @ 2022-10-15 37/week @ 2022-10-22 70/week @ 2022-10-29 84/week @ 2022-11-05 44/week @ 2022-11-12 54/week @ 2022-11-19 27/week @ 2022-11-26

228 downloads per month
Used in sarek

Apache-2.0/MIT

37KB
358 lines

Controlled Rust Panics Using Dynamic Type Checking

The panic_control crate provides utilities to test code behaviors in intentionally caused panics, while discriminating between the expected and unexpected panics so as to be able to catch assertion failures and the like.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.


lib.rs:

Controlled panics using dynamic type checking.

Sometimes there is a need to test how Rust code behaves on occurrence of a panic. A panic can be invoked on purpose in a thread spawned by the test and the effects observed after the thread is joined. The problem with "benign" panics is that it may be cumbersome to tell them apart from panics indicating actual errors, such as assertion failures.

Another issue is the behavior of the default panic hook. It is very useful for getting information about the cause of an unexpected thread panic, but for tests causing panics on purpose it produces annoying output noise. The panic hook can be overridden, but custom panic hooks affect the entire program, which in typical usage is the test runner; it is easy to misuse them causing important error information to go unreported.

The simplest way, as provided by the standard library, to propagate a panic that occurred in a child thread to the thread that spawned it is to call unwrap on the result of JoinHandle::join. Unfortunately, due to an issue with the implementation of Any, the resulting panic message does not relay information from the child thread's panic.

This crate provides utilities and an ergonomic interface for testing panics in a controlled and output-friendly way using dynamic type checks to discern between expected and unexpected panics.

Expected Panic Type

The recommended way to designate panics as expected is by using values of a custom type as the parameter for panic!. The type could be as simple as a token unit-like struct, or it can be equipped to carry additional information from the panic site. Any panic value type shall be Sized, 'static, and Send. For the value to be usable in testing, it should also implement at least Debug and PartialEq.

Examples

use panic_control::{Context, Outcome};
use panic_control::{chain_hook_ignoring, spawn_quiet};
use panic_control::ThreadResultExt;

use std::thread;

#[derive(Debug, PartialEq, Eq)]
enum Expected {
    Token,
    Int(i32),
    String(String)
}

// Rust's stock test runner does not provide a way to do global
// initialization and the tests are run in parallel in a random
// order by default. So this is our solution, to be called at
// the beginning of every test exercising a panic with an
// Expected value.
fn silence_expected_panics() {
    use std::sync::{Once, ONCE_INIT};
    static HOOK_ONCE: Once = ONCE_INIT;
    HOOK_ONCE.call_once(|| {
        chain_hook_ignoring::<Expected>()
    });
}

# struct TypeUnderTest;
# impl TypeUnderTest {
#     fn new() -> TypeUnderTest { TypeUnderTest }
#     fn doing_fine(&self) -> bool { true }
# }
// ...

silence_expected_panics();
let thread_builder = thread::Builder::new()
                     .name("My panicky thread".into());
let ctx = Context::<Expected>::from(thread_builder);
let h = ctx.spawn(|| {
    let unwind_me = TypeUnderTest::new();
    assert!(unwind_me.doing_fine());
         // ^-- If this fails, join() will return Err
    panic!(Expected::String("Rainbows and unicorns!".into()));
});
let outcome = h.join().unwrap_or_propagate();
match outcome {
    Outcome::Panicked(Expected::String(s)) => {
        println!("thread panicked as expected: {}", s);
    }
    _ => panic!("unexpected value returned from join()")
}

let ctx = Context::<Expected>::new();
let h = ctx.spawn_quiet(|| {
    let h = spawn_quiet(|| {
        panic!("Sup dawg, we heard you like panics \
                so we put a panic in your panic!");
    });
    h.join().unwrap_or_propagate();
});
let res = h.join();
let msg = res.panic_value_as_str().unwrap();
assert!(msg.contains("panic in your panic"));

No runtime deps