#test-macro #macro

macro one_assert

One assert! macro to replace them all

1 unstable release

0.1.0 Jul 15, 2024

#466 in Testing

24 downloads per month

MIT/Apache

49KB
699 lines

A Rust crate with a more powerful assert!() macro

Tests Crates.io Documentation Dependency status

One Assert

TL;DR

Why have separate macros for assert_eq and assert_ne (and assert_gt etc. with other crates) when you can just get the same output with assert!(a == b) (or assert!(a != b), assert!(a > b), …)? This crate provides a single assert! macro that analyzes the expression to provide more detailed output on failure.

Introduction

Rust’s standard library provides the assert, assert_eq and assert_ne macros. There are however some inconveniences with these, like how there are no specialization for other inequalities, like assert_ge for >= etc, or how the names only differ in one or two letters (assert_eq, assert_ne, assert_ge, assert_gt, …) and are thus easy to mix up at a glance.

The main reason for not adding more macros is that they can be represented just fine with assert!(a >= b), so there is no need for a separate macro for every use case.

But that begs the question: Why do we have assert_eq and assert_ne in the first place?

The practical reason: assert_eq!(a, b) provides better output than assert!(a == b):

let x = 1;
let msg = catch_panic!({ assert!(x == 2); });
assert_eq!(msg, "assertion failed: x == 2");

let msg = catch_panic!({ assert_eq!(x, 2); });
assert_eq!(msg, "assertion `left == right` failed
  left: 1
 right: 2"
);

As you can see, assert_eq is able to provide detailed info on what the individual values were. But: That doesn’t have to be the case. Rust has hygienic and procedural macros, so we can just make assert!(a == b) work the same as assert_eq!(a, b):

let x = 1;
let msg = catch_panic!({ one_assert::assert!(x == 2); });
assert_eq!(msg, "assertion `x == 2` failed
     left: 1
    right: 2"
);

And now we can expand this to as many operators as we want:

let x = 1;
let msg = catch_panic!({ one_assert::assert!(x > 2); });
assert_eq!(msg, "assertion `x > 2` failed
     left: 1
    right: 2"
);

Examples

let x = 1;
let msg = catch_panic!({ one_assert::assert!(x > 2); });
assert_eq!(msg, "assertion `x > 2` failed
     left: 1
    right: 2"
);

let msg = catch_panic!({ one_assert::assert!(x != 1, "x ({}) should not be 1", x); });
assert_eq!(msg, "assertion `x != 1` failed: x (1) should not be 1
     left: 1
    right: 1"
);

let s = "Hello World";
let msg = catch_panic!({ one_assert::assert!(s.starts_with("hello")); });
assert_eq!(msg, r#"assertion `s.starts_with("hello")` failed
     self: "Hello World"
    arg 0: "hello""#
);

Limitations

  • Several Components need to implement Debug
    • The macro will take whatever part of the expression is considered useful and debug print it. This means that those parts need to implement Debug.
    • What is printed as part of any given expression type is subject to change, so it is recommended to only use this in code where pretty much everything implements Debug.
  • Debug printing happens even if the assertion passes
    • Because this macro prints more than just the two sides of an == or != comparison, it has to deal with the fact that some values are moved during the evaluation of the expression. This means that the values have to be printed in advance.
    • Consequence: Don’t use this macro in performance-critical code.
    • Note however, that the expression and each part of it is only evaluated once.
      • (Though it is worth noting that fail-fast operators like && might normally only evaluate the left side and stop, but with this macro it will always evaluate both sides)

Dependencies

~1.3–2MB
~35K SLoC