Skip to content

← Modules, Visibility, Testing and Docs step 10 of 22

Hard Framework

macro_rules! scoping is not item scoping

Fix the visibility and behavior of my_assert_eq!, then use it to collect assertion outcomes:

pub fn run(cases: Vec<(i64, i64, Option<String>)>) -> Vec<Option<String>>

Define the macro inside mod asserts, make it available within the crate with pub(crate) use, and import it where run calls it. Add two arms: one for two expressions, with an optional trailing comma, and one for two expressions followed by formatting arguments.

For each case, compare the operands. Equal values return None. Unequal values panic inside catch_unwind; return the captured message as Some(String). Use the message arm whenever the case carries Some(message), including an empty string. Preserve case order, and return an empty vector for no cases.

Match the required message exactly

Without a message, comparing 4 and 5 must produce:

assertion `left == right` failed
  left: 4
 right: 5

With the message ctx, it must produce:

assertion `left == right` failed: ctx
  left: 4
 right: 5

There are two spaces before left, one before right, and no final newline. These are the strings this exercise requires. Panic-hook output such as a thread name, source location or backtrace is not part of the captured payload.

Give the macro a path

The starter places an unqualified macro definition after its uses. Unlike a function item, a macro_rules! definition has textual scope starting after the definition. That scope can extend into later nested modules, but ends with the enclosing scope unless explicitly extended or imported.

Give this macro a module path instead:

mod asserts {
    macro_rules! my_assert_eq { /* your two arms */ }
    pub(crate) use my_assert_eq;
}

use asserts::my_assert_eq;

The re-export makes the macro available by path within the crate. Its original textual scope still exists; the import provides an additional way to find it. A path-based import does not need to appear before the calls that use it.

#[macro_export] serves a different purpose: it exposes a macro at the crate root for other crates. It is unnecessary for this task. When an exported macro needs an item from its defining crate, $crate::... names that crate; ordinary crate::... refers to the invoking crate. $crate does not bypass privacy.

Borrow each operand once

Both arms should evaluate each operand once and borrow the results. This avoids moving a value just to compare it and avoids repeating side effects:

let (l, r) = (&$left, &$right);

Compare the borrowed values and format them with Debug only on failure. Keep format_args!($($arg)+) inside that failure branch too, so message arguments are not evaluated for a passing assertion. Pass it directly to panic! rather than storing it in a temporary with borrowed formatting data.

The two patterns can be written as:

($left:expr, $right:expr, $($arg:tt)+) => { /* message arm */ };
($left:expr, $right:expr $(,)?) => { /* plain arm */ };

Either order works for these patterns. Rust chooses the first arm that matches the entire invocation; a two-argument call cannot match the message arm.

Capture the payload and restore the hook

catch_unwind captures unwinding panics, not aborting panics. The supplied runner temporarily installs a silent panic hook so expected failures do not print diagnostics. Preserve its restoration of the previous hook after all cases, including an empty input.

Hooks are process-global. This temporary replacement suits the exercise's sequential runner; it is not isolated from other threads and is not a general pattern for concurrently running tests. The supplied restoration also assumes the runner reaches its final cleanup rather than unexpectedly unwinding first.

Keep the supplied describe helper. It handles the string payloads used here and provides a fallback for other payload types. The tests compare returned values; they do not enforce the requested module layout or prove that a hook was restored, so follow those structural requirements as well.