-1

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.

jrpear
  • 232
  • 2
  • 6
  • How do you expect to take ownership of a subset of the local variables without explicitly listing them? – ShadowRanger Feb 08 '23 at 01:39
  • 1
    Why can't your printing code just take references? – tadman Feb 08 '23 at 01:42
  • @ShadowRanger I don't know what to expect the Rust answer to be. Perhaps it's impossible. In C you could do this with a goto. You make a label to a call to `dump_foo_data` at the end of the test, then `goto` it in the true arm of the `if`. Or just write a macro that has the names of the relevant locals hard-coded and be careful to always use the same names. – jrpear Feb 08 '23 at 01:47
  • 1
    @tadman Because the data I need can't be obtained from a reference. I have a `std::process::Child` and on it I call `kill` then `wait_with_output`. `wait_with_output` can't take a reference. – jrpear Feb 08 '23 at 01:50
  • If the assert! macro doesn't do what you need, create your own – John Feb 08 '23 at 06:05

3 Answers3

2

One way that doesn't use any macro or shared-interior-mutability-reference magic might be to repossess f:

fn check_or_dump(success: bool, f: Foo) -> Foo {
   match success {
     true => f,
     false => panic!("Behold foo data: {:?}", dump_foo_data(f)),
   }
}

You use it like this:

let f = Foo();
let success = true;
let f = check_or_dump(success, f);
let success = false;
let f = check_or_dump(success, f);
// and so on.
Caesar
  • 6,733
  • 4
  • 38
  • 44
  • Thanks, that's certainly better than needing to mess with a macro. Though there's still the issue of needing to specify `f` each time, maybe that's inevitable. – jrpear Feb 08 '23 at 03:43
  • Thanks to macro hygiene, you probably even have to do that with macros. – Caesar Feb 08 '23 at 04:18
  • Yep. I'm trying my utmost to find a way around it though haha. – jrpear Feb 08 '23 at 04:29
2

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

Jmb
  • 18,893
  • 2
  • 28
  • 55
0

I've worked out a solution using the panic handler:

use std::rc::Rc;
use std::cell::{Cell, RefCell};
use std::panic::PanicInfo;

thread_local! {
    static TL_PANIC_TARGETS: RefCell<Vec<Rc<dyn PanicTrigger>>> = RefCell::new(vec![]);
}

pub trait PanicTrigger {
    fn panic_trigger(self: Rc<Self>);
}

pub fn register_panic_trigger<P: PanicTrigger + 'static>(p: Rc<P>) {
    TL_PANIC_TARGETS.with(|v: _| {
        v.borrow_mut().push(p.clone());
    });
}

#[ctor::ctor]
fn set_panic_hook() {
    let old_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |pi: &PanicInfo| {
        run_panic_triggers(pi);
        old_hook(pi);
    }));
}

fn run_panic_triggers(_: &PanicInfo) {
    TL_PANIC_TARGETS.with(|v: _| {
        for pt in v.take() {
            pt.panic_trigger();
        }
    });
}

struct Foo();

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

impl PanicTrigger for Cell<Option<Foo>> {
    fn panic_trigger(self: Rc<Self>) {
        if let Some(f) = self.take() {
            dump_foo_data(f);
        }
    }
}

#[test]
fn my_test() {
    let f = Rc::new(Cell::new(Some(Foo())));
    register_panic_trigger(f.clone());

    let success = true;
    assert!(success);

    let success = false;
    assert!(success);
}

fn main() { }

Basically, you put the relevant data in an Rc and keep a local reference and put one in TLS for the panic handler. You need to put it in an Option in a Cell so that you can move out of it.

Types that don't need to be owned to print relevant data can be registered too, and you don't need to implement PanicTrigger on a Cell<Option<T>>, just T.

This is thread-safe.

Because the data is so wrapped up, it's harder to manipulate in the test body. But now you can use normal assert!. It's a trade-off.

jrpear
  • 232
  • 2
  • 6