在Rust中,当断言失败时,如何以一种消耗局部变量的方式打印数据?

huangapple go评论60阅读模式
英文:

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

问题

这是你要翻译的部分:

我有一些测试,其中包含一些保存重要数据的变量,当断言失败时,我想打印它们的数据。获取我需要的数据会消耗这些变量,因此打印代码必须拥有这些变量。在这个示例中,我想在断言失败时调用 `dump_foo_data` 一次:

```rust
struct Foo();

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

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

    // 进行一个测试
    &f;
    let success = true;
    assert!(success);

    // 进行另一个测试
    &f;
    let success = false;
    assert!(success);
}

我可以通过使 dump_foo_data 不返回并引发 panic 来制作一个非常糟糕的解决方案:

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

然后,我不使用 assert!,而是使用 if 来检查失败,并可能调用 dump_foo_data

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

这太多行代码了,而且我需要指定 f。实际上,我有多个类似 f 的变量,我需要从中获取数据,因此在每次检查中列出单个相关的本地变量并不是很好。

我无法弄清楚如何编写宏来改进此过程,因为我仍然需要将每个相关的本地变量传递给宏。

我也想不出如何使用 std::panicupdate_hook 需要获取 f 的所有权,然后我不能在测试中使用它。

在Rust中是否有任何好的方法来实现这一点?

编辑:我想到了另一种方法:将每个相关的本地变量放入 Rc 中,然后将它们传递给 std::panic::update_hook。我还没有确认这是否会起作用。

编辑2:也许我可以滥用 break 来执行我在评论中用 goto 解释的操作。

英文:

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.

答案1

得分: 3

这是一个解决方案,没有使用宏或内部可变性,并且不需要在每次检查时列出所有变量。受到了 [这个答案](https://stackoverflow.com/a/55756926/5397009) 的启发:

```rust
struct Foo();

fn dump_foo_data(_f: Foo) {
    eprintln!("瞧,Foo 的数据:");
}

#[test]
fn my_test() {
    let f = Foo();
    let doit = || -> Option<()> {
        eprintln!("开始");

        // 进行一个测试
        &f;
        let success = true;
        success.then_some(())?;

        // 进行另一个测试
        &f;
        let success = false;
        success.then_some(())?;
        
        Some(())
    };
    
    if let None = doit() {
        dump_foo_data(f);
        panic!("测试失败");
    }
}

Playground


<details>
<summary>英文:</summary>

Here&#39;s a solution without macro or interior mutability and that doesn&#39;t require you to list all the variables on each check. It is inspired by [this answer](https://stackoverflow.com/a/55756926/5397009):

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
    &amp;f;
    let success = true;
    success.then_some(())?;

    // do another test
    &amp;f;
    let success = false;
    success.then_some(())?;
    
    Some(())
};

if let None = doit() {
    dump_foo_data (f);
    panic!(&quot;Test failure&quot;);
}

}

[Playground](https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2021&amp;gist=809ed2a3b358f178b791959d6b888e5d)

</details>



# 答案2
**得分**: 2

不使用任何宏或共享内部可变引用魔法的一种方法可能是重新获取 `f`:

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

您可以这样使用它:

let f = Foo();
let success = true;
let f = check_or_dump(success, f);
let success = false;
let f = check_or_dump(success, f);
// 等等。
英文:

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) -&gt; Foo {
   match success {
     true =&gt; f,
     false =&gt; panic!(&quot;Behold foo data: {:?}&quot;, 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.

答案3

得分: 0

我使用 panic 处理程序找到了一个解决方案:

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() { }

基本上,您将相关数据放入 Rc 中,保持本地引用并将一个放入 TLS 用于 panic 处理程序。您需要将其放入 Cell 中的 Option 中,以便可以移出它。

那些不需要拥有权以打印相关数据的类型也可以注册,您不需要在 Cell<Option<T>> 上实现 PanicTrigger,只需在 T 上实现即可。

这是线程安全的。

由于数据被封装得如此紧密,因此在测试体中更难操纵。但现在您可以使用正常的 assert!。这是一种权衡。

英文:

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&lt;Vec&lt;Rc&lt;dyn PanicTrigger&gt;&gt;&gt; = RefCell::new(vec![]);
}

pub trait PanicTrigger {
    fn panic_trigger(self: Rc&lt;Self&gt;);
}

pub fn register_panic_trigger&lt;P: PanicTrigger + &#39;static&gt;(p: Rc&lt;P&gt;) {
    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: &amp;PanicInfo| {
        run_panic_triggers(pi);
        old_hook(pi);
    }));
}

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

struct Foo();

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

impl PanicTrigger for Cell&lt;Option&lt;Foo&gt;&gt; {
    fn panic_trigger(self: Rc&lt;Self&gt;) {
        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&lt;Option&lt;T&gt;&gt;, 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.

huangapple
  • 本文由 发表于 2023年2月8日 09:29:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/75380578.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定