Highlight Differences In `assert_eq!()`

let result = function();
let expectation = [...];
assert_eq!(result, expectation);

when assert_eq!() panics for having inequality between result and expectation, there can be large bodies of text in result and expectation. Pretty time-consuming to have to go through it all just to find one little detail that differs between them, somewhere in that mess.

Proposal: put a red highlight on the parts that differ between result and expectation, while keeping everything else white (as is today)

You may be looking for pretty_assertions - Rust

10 Likes

Also, note that the text you see in the panic is the Debug print of each type, but the actual comparison is done by PartialEq before that. Hopefully it is the case that the difference will show up in Debug too, but it's not necessarily so.

For example, assert_eq!(f64::NAN, f64::NAN); will print NaN on both, no textual difference. It may also be the case that a type has a field that's not considered by PartialEq, yet would show up in Debug output as something that may be different, though irrelevant -- like if a container printed its capacity.

Another example: assert_eq!((f64::NAN, 0.0), (f64::NAN, -0.0)); prints:

assertion `left == right` failed
  left: (NaN, 0.0)
 right: (NaN, -0.0)

... but the textual difference is not the part that made it unequal.

27 Likes

IEEE 754 floating point, proudly providing counterexamples since 1985!

26 Likes

How about using pretty_assertions in standard library instead of the current impl?

This has been suggested in this thread. Perhaps the original question could be rephrased as "why don't we pull the relevant parts of pretty_assertions in for the standard assert_x macros". One reason I can see is that the standard assert_x macros aren't strictly for test code, or only available there, and I wouldn't necessarily want to pull in several extra crates to have that functionality on call outside of test runs. Although that functionality could easily be behind #[cfg(test)]' s[1], at the cost of a little extra complexity. Maybe worthwhile, it's worth discussing anyway.

This is all still true of the current assert_x macros, as you know. pretty_assertions may only be useful in 99.9% or so of test cases, and it remains true that any dev writing assert_eq!(f64::NAN, x) was going to have to learn about floats sooner or later.


  1. Or to be especially cute, `#[cfg(debug_assertions)]). â†Šī¸Ž

3 Likes

Yes, it's plainly wrong to compare NaN directly that way, but don't focus on that detail of the example. It could just as well be something like:

assert_eq!(optimized_method(x, y), naive_method(x, y));

Besides, floats are just a convenient way to demonstrate the point that PartialEq is independent of the Debug representation, but this kind of thing can happen with any type. That's not to say that highlighting differences is bad either, just that it comes with caveats.

2 Likes

Originally I thought we could call PartialEq on each field of the struct/tuple, like the compiler does for mismatched types.

Then I realised we don't have any functions that visit individual fields. (As far as assert_eq!() is concerned, PartialEq::eq() is a single opaque call.)

Well, derive(PartialEq) does work like that, field by field. It could be interesting if we added a new PartialEq method that returned the difference somehow, then the derive and custom impls could provide more info. I'm not sure what exactly we would want that to return though. Or maybe there could be a DebugEq trait, but we'd want those assert macros to still work without that too.

Maybe something like this:

enum Field {
    Offset(usize), // Structs (to avoid heap alloc)
    Index(usize), // For arrays
    Named(String), // For HashMaps or similar
    // Dyn(Box<dyn Debug + PartialEq>) // Allows the most flexibility
}

/// Compares self to other and returns the first difference found
/// 
/// # Return values
/// 1. Reference into self
/// 2. Corresponding reference into other
/// 3. Summary over the location where the difference happened
///    in reverse order (index 0 is the inner-most field)
fn diff<'a, 'b>(&'a self, other: &'b Rhs) -> Option<(
    Box<dyn Debug + PartialEq + 'a>,
    Box<dyn Debug + PartialEq + 'b>,
    Vec<Field>, // The path where it differs (usually relevant, too)
)> {
    // Potential default impl (when not derived).
    // We don't have more data we can give if this isn't implemented.
    if self.eq(other) {
        None
    } else {
        Some(self, other, vec![])
    }
}

Might want an additional counter/depth argument to know the size of the Vec when allocating it.

We can't use Vec or any other allocation from core at all.

1 Like