0

When I run the following program with cargo test:

use std::panic;

fn assert_panic_func(f: fn() -> (), msg: String) {
    let result = panic::catch_unwind(|| {
        f();
    });
    assert!(result.is_err(), msg);
}

macro_rules! assert_panic {
    ($test:expr , $msg:tt) => {{
        fn wrapper() {
            $test;
        }
        assert_panic_func(wrapper, $msg.to_string())
    }};
}

fn main() {
    println!("running main()");
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn t_1() {
        assert_panic!(
            panic!("We are forcing a panic"),
            "This is asserted within function (raw panic)."
        );
        // assert_panic!(true, "This is asserted within function (raw true).");
    }
}

I get the expected output:

running 1 test
test tests::t_1 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

If I uncomment the second assert_panic!(...) line, and rerun cargo test, I get the following output:

running 1 test
test tests::t_1 ... FAILED

failures:

---- tests::t_1 stdout ----
thread 'tests::t_1' panicked at 'We are forcing a panic', src/lib.rs:29:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'tests::t_1' panicked at 'This is asserted within function (raw true).', src/lib.rs:7:5


failures:
    tests::t_1

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

The second panic is legitimate, and that is what I am looking for, but the first panic seems to be being triggered by the line that was not triggering a panic in the first invocation.

What is going on, and how do I fix it?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
ggb
  • 1
  • [How do I write a Rust unit test that ensures that a panic has occurred?](https://stackoverflow.com/q/26469715/155423) – Shepmaster Apr 01 '20 at 13:53
  • It looks like your question might be answered by the answers of [Suppress panic output in Rust when using panic::catch_unwind](https://stackoverflow.com/q/35559267/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Apr 01 '20 at 13:54

3 Answers3

0

Even if std::panic::catch_unwind catches the panic, any output from that panic will be printed. The reason you don't see anything with the first test (with the commented out second panic) is that cargo test doesn't print output from successful tests.

To see this behavior more clearly, you can use main instead of a test. (playground)

fn main() {
    let _ = std::panic::catch_unwind(|| {
        panic!("I don't feel so good Mr. catch_unwind");
    });

    println!("But the execution of main still continues.");
}

Running this gives the output

thread 'main' panicked at 'I don't feel so good Mr. catch_unwind', src/main.rs:3:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
But the execution of main still continues.

Note that panics usually output to stderr, rather than stdout, so it's possible to filter these out.

See also Suppress panic output in Rust when using panic::catch_unwind.


I'm not sure if this is what you're trying to do, but if you want to ensure that a test panics, use the should_panic attribute. For example,

#[test]
#[should_panic]
fn panics() {
    panic!("Successfully panicked");
}
SCappella
  • 9,534
  • 1
  • 26
  • 35
  • I disagree that `should_panic` by itself is an appropriate solution as it's simply too broad. Unrelated panics will cause the test to pass. See [How do I write a Rust unit test that ensures that a panic has occurred?](https://stackoverflow.com/q/26469715/155423) for better solutions – Shepmaster Apr 01 '20 at 13:53
0

The stderr output

thread 'tests::t_1' panicked at 'We are forcing a panic', src/main.rs:30:23

is logged independently of whether a panic is caught, the test running just does not show any logged output unless a test fails. To suppress that text entirely, you would need to separately swap out the panic notification hook using std::panic::set_hook.

fn assert_panic_func(f:fn()->(), msg: String) {
    let previous_hook = panic::take_hook();
    // Override the default hook to avoid logging panic location info.
    panic::set_hook(Box::new(|_| {}));
    let result = panic::catch_unwind(|| {
        f();
    });
    panic::set_hook(previous_hook);
    assert!(result.is_err(), msg );
}

All that said, I second @SCappella's answer about using #[should_panic].

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
0

At the time I was not aware that unit tests would suppress output messages. I later became aware of the suppression of output messages when researching why println!(...) would not work within unit tests. That this might also be an answer to why panics sometimes display and sometimes do not does make sense.

Nonetheless, it does seem to me to be perverse that panics produce output even when I explicitly tell Rust that I wish to prevent the panic from having any effect, but if that is what Rust does, however perverse that might seem, then one has to live with it.

I was aware of the #[should_panic] attribute, but was not happy with this solution for two reasons:

Firstly, it requires that each test becomes a separate function, whereas I tend to put a number of closely related tests (many of the tests being no more than a single assert!(...) statement) into one function.

Secondly, it would be nice to have a single model to express each test. To my mind, testing whether an expression raises a panic (or fails to raise a panic) is no different from testing whether the result is equal to, or unequal to, some particular value. It makes far more sense to me to create a single model to express both tests, hence my desire to have an assert_panic!(...) macro that behaved analogous to the assert!(...) or assert_eq!(...) macro. It seems that this is simply not an achievable objective within Rust.

Thank you for clearing that up.

ggb
  • 1