Writing a macro for assert_panic

I am currently writing some Unit Tests & want to ensure that some functions throw (i.e. panic) when the inputs are nonsensical. So for example:

#[test]
fn test_func_can_panic() {
    let result = panic::catch_unwind(|| func(...));
    assert_eq!(result.is_err(), true);
}

However, I wanted to shorten this by using something like assert_panic!(func(...)). Hence I tried writing this macro:

#[macro_export]
macro_rules! assert_panic {
    ($left:expr, $right:expr $(,)?) => ({
        let result = panic::catch_unwind(|| $left );
        assert_eq!(result.is_err(), $right);
    });
}

Now I can update my unit test:

#[test]
fn test_func_can_panic() {
    assert_panic!(func(...), true);
}

This all works. However, a few questions:

  1. Is the macro written correctly? E.g. are there certain syntax conventions I am missing?
  2. Ideally, I wanted to convert the assert_eq!(result.is_err(), $right); part to something more explicit. Something along the lines of this:
if !(result.is_err() == $right) {
    let kind = panicking::AssertKind::Panic;
    if $right {
        panicking::assert_failed(kind, "Function was meant to panic, but it did not");
    } else {
        panicking::assert_failed(kind, "Function was not meant to panic, but it did: " + result.panic_message());
    }
}

I know the above does not compile, but I hope I got the idea across that wanted to achieve. Is that possible?

  1. Is there already a function/macro in the standard that achieves assert_panic?

Try

#[test]
#[should_panic]
fn test_func_can_panic() {
    func(...);
}
4 Likes
  1. Since you already allow trailing commas with $(,)? it looks fine to me.

  2. assert_eq can take a format string and other arguments as additional parameters, so adding a custom message like in your example is straightforward. You could also use the panic macro directly in the same way if you don’t want the equality check mentioned in the output.

If you do that, it may be worth it to assign $result to a variable inside the macro, so the expression will not be evaluated twice. Alternatively if you only want to allow literals you can use literal instead of expr.

  1. It’s not exactly identical but there is the should_panic attribute. Testing - The Rust Reference

Unrelated to the immediate issue, if you know your functions can receive untrusted inputs, you should try really-really-really hard to write your code in a way that it returns Result instead of panicking. Panicking isn't meant to be the general handling mechanism for expected errors, Result is.

1 Like

I wanted assert_panic so that I can put multiple assert statements in the same unit test:

#[test]
fn test_func_can_panic() {
    assert_panic!(func(...), true);
    assert_panic!(func(...), true);
    assert_panic!(func(...), true);
}

It seems #[should_panic] requires me to write multiple 1 test per function call. But it's good to know this option exists.

I am probably still lacking some Rust fundamentals. In C++ / Python I often see try-catch / try-except blocks used. Why is this discouraged in Rust?

For the area I work in, it is almost impossible to child-proof all functions by letting them return a Result object. For example:

import datetime
datetime.date(2021, 12, 50)

validly throws ValueError: day is out of range for month. Or similarly

#include <vector>

int main() {
    std::vector<int> v = {5,7,9};
    return v.at(100);
}

validly throws std::out_of_range. How does Rust handle these situation? Should all "throwable" functions return Result and never panic? When is panicking allowed?

Rust uses the Result type instead of exceptions for recoverable errors like a file not being found. Panics are meant for unrecoverable errors that likely indicate a bug in your program. Panicking causes a panic message to be printed and if the RUST_BACKTRACE env var is set to 1 a backtrace to be printed. Using Result instead of panics ensures that you will either handle the error or explicitly pass it along to the caller. You may want to read the docs at Error Handling - The Rust Programming Language

In rust the equivalent would likely return a Result.

Out of bounds indexing using v[100] panics. If out of bounds indexing is expected you can use v.get(100) which returns an Option.

In general, panic is for non-recoverable errors, i.e. when it'd be the correct behavior to abort the program immediately. If you can do something (even as simple as showing a customized error message to the user) - it's better to stick to Result and use question mark operators to bubble the errors up the stack.

Speaking of your own examples:

This is recoverable error - calling code might be able to adjust the data and retry again, so it's idiomatic to return Result - unless this code is in the main or close to it, and you know that you won't try to recover.

For this, Rust has two ways: indexing (which panic on out-of-bounds access, since this is treated as a bug) and .get/.get_mut, which return Result (if caller thinks that this particular indexing operation have some valid cases to fail).

1 Like

I think this made it clear to me. So panic in Rust is the same as calling v[100] out of bounds (which is UB and one cannot catch UB). Whereas if something needs to be catch-able (in the C++ or Python sense), then in Rust we must return Result, Optional, etc. and the user must handle it?

When indexing rust checks if the index is in bounds and otherwise panics. Indexing out of bounds is not UB, but guaranteed to panic.

Panics can be catched, but it is generally a bad idea to use it for control flow rather than to somewhat gracefully recover from a bug or to turn a panic into an error when it would otherwise unwind into C or C++ code (which is UB). The later use case is what the function to catch panics was originally stabilized for. Another thing is that panics may not be catchable in certain cases like when using panic=abort (this option reduces binary size and slightly improves performance).

1 Like

Note that this is a compiler option, which is to say, you can't assume you can catch panics (if your library is for general consumption). I.e. you don't choose if panic=abort is used or not. There are also a number of gotchas around catching panics.

Indeed, you shouldn't panic in the case of foreseeable errors, and you should return Result instead. There are some scenarios where it's conventional to panic merely because they are so common and errors are unlikely to mean anything but a bug (e.g. out-of-bounds array indexing), but they are the exception rather than the rule, and they usually still have non-panicking counterparts.

In rust, this would idiomatically return an error instead of panicking.

  1. Panicks are implicit and untyped, so they occur unexpectedly. This is bad for correctness and debugging.
  2. Panics can't be caught reliably.
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.