Search code examples
testingrustassert

How can I print data in a way that consumes local variables when an assert fails in Rust?


I have some tests which have some variables that hold some important data and I'd like to print their data when an assertion fails. Getting the data I need consumes the variables, so the printing code must own the variables. In this example, I'd want to call dump_foo_data once an assertion fails:

struct Foo();

fn dump_foo_data(f: Foo) {
    eprintln!("Behold, Foo data: ");
}

#[test]
fn my_test() {
    let f = Foo();
    eprintln!("begin");

    // do a test
    &f;
    let success = true;
    assert!(success);

    // do another test
    &f;
    let success = false;
    assert!(success);
}

I can make a very bad solution by making dump_foo_data non-returning and panic:

fn dump_foo_data(f: Foo) -> ! {
    eprintln!("Behold, Foo data: ");
    panic!();
}

Then instead of using assert!, I check the failure with an if and maybe call dump_foo_data:

    let success = true;
    if !success {
        dump_foo_data(f);
    }

This is too many lines of code, and I need to specify f. In reality, I have more than one variable like f that I need to dump data from, so it's not very nice to list out single relevant local variable in every check.

I couldn't figure out how to write a macro to make this better because I'd still need to pass every relevant local variable to the macro.

I couldn't think of a way to use std::panic either. update_hook would need to take ownership of f, then I couldn't use it in tests.

Is there any good way to do this in Rust?

Edit: I've thought of another approach: put each relevant local in an Rc then pass each of those to std::panic::update_hook. I've not confirmed whether this'll work yet.

Edit 2: Maybe I could abuse break to do what I explained with goto in a comment.


Solution

  • Here's a solution without macro or interior mutability and that doesn't require you to list all the variables on each check. It is inspired by this answer:

    struct Foo();
    
    fn dump_foo_data(_f: Foo) {
        eprintln!("Behold, Foo data: ");
    }
    
    #[test]
    fn my_test() {
        let f = Foo();
        let doit = || -> Option<()> {
            eprintln!("begin");
        
            // do a test
            &f;
            let success = true;
            success.then_some(())?;
        
            // do another test
            &f;
            let success = false;
            success.then_some(())?;
            
            Some(())
        };
        
        if let None = doit() {
            dump_foo_data (f);
            panic!("Test failure");
        }
    }
    

    Playground