How to write doctest that `panic!` with an expected message

I want the following doctest to fails:

//! ```should_panic(expected = "not the expected message")
//! panic!("the expected message");
//! ```

but it doesn't.

Unfortunately while you can check for expected panic messages in tests, there isn't an easy way to do so in doc tests.

However if you really want to check for an expected panic message in a doctest you can do so with a helper function:

#[cfg(doctest)]
use std::panic::{catch_unwind, UnwindSafe};

#[cfg(doctest)]
pub fn assert_panics_with_message(func: impl FnOnce() + UnwindSafe, msg: &'static str) {
    let err = catch_unwind(func).expect_err("Didn't panic!");

    let chk = |panic_msg: &'_ str| if !panic_msg.contains(msg) {
        panic!("Expected a panic message containing `{}`; got: `{}`.", msg, panic_msg);
    };

    err.downcast::<String>()
        .map(|s| chk(&**s))
        .or_else(|err| err.downcast::<&'static str>().map(|s| chk(*s)))
        .expect("Unexpected panic type!");
}

/// ```rust
/// # use playground::{foo, assert_panics_with_message};
/// # assert_panics_with_message(|| {
/// foo();
/// # }, "oops");
/// ```
///
/// ```rust
/// # use playground::{foo, assert_panics_with_message};
/// # assert_panics_with_message(|| {
/// foo();
/// # }, "error!");
/// ```
pub fn foo() { panic!("whoops") }

(Playground)


Note that the doctests above are not marked with should_panic; this means that in the generated HTML documentation the code blocks above won't be marked as panicking like this code block, for example, is.

If this is important to you, you can use a function like this one which suppresses a panic if the message doesn't match what's expected and otherwise propagates the panic:

#[cfg(doctest)]
use std::panic::{catch_unwind, resume_unwind, UnwindSafe};

#[cfg(doctest)]
pub fn check_panic_message(func: impl FnOnce() + UnwindSafe, msg: &'static str) {
    let chk = |panic_msg: &'_ str| panic_msg.contains(msg);
    if let Err(err) = catch_unwind(func) {
        let resume_panicking = if let Some(s) = err.downcast_ref::<String>() {
            chk(&*s)
        } else if let Some(s) = err.downcast_ref::<&'static str>() {
            chk(*s)
        } else {
            false
        };

        if resume_panicking {
            resume_unwind(err)
        }
    }
}

/// ```rust,should_panic
/// # use playground::{foo, check_panic_message};
/// # check_panic_message(|| {
/// foo();
/// # }, "oops");
/// ```
///
/// ```rust,should_panic
/// # use playground::{foo, check_panic_message};
/// # check_panic_message(|| {
/// foo();
/// # }, "error!");
/// ```
pub fn foo() { panic!("whoops") }

(Playground)

It's potentially a little more confusing to use (errors where the code panics but the message doesn't match will yield an error saying the doctest didn't panic) but it does work.

1 Like