ohno 0.3.9

High-quality Rust error handling.
Documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// The assertion logic lives in `assert_error_message_impl` (a real function),
// not inline in the macro body, so this module-level `coverage(off)` actually
// suppresses it. A macro body is re-instrumented at every expansion site and
// attributed back to this file, so `coverage(off)` here never reached it.
#![cfg_attr(coverage_nightly, coverage(off))]

//! Test utilities for the ohno crate.
//!
//! This module is only available when the `test-util` feature is enabled.

/// Assert that an error message matches the expected value, accounting for potential backtraces.
///
/// This macro checks if the error's display representation exactly matches the expected string,
/// or if it starts with the expected string followed by a backtrace section.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "test-util")]
/// # {
/// use ohno::assert_error_message;
///
/// #[derive(ohno::Error)]
/// struct MyError {
///     inner: ohno::OhnoCore,
/// }
///
/// let error = MyError::caused_by("something went wrong");
/// assert_error_message!(error, "something went wrong");
/// # }
/// ```
#[macro_export]
macro_rules! assert_error_message {
    ($error:expr, $expected:expr) => {{ $crate::test_util::assert_error_message_impl(&$error.to_string(), $expected) }};
}

/// Implementation backing [`assert_error_message!`].
///
/// Kept as a function (rather than inline in the macro) so that the
/// module-level `#[coverage(off)]` applies to the assertion's untested
/// branches; inlined in the macro they would be instrumented at every
/// expansion site and reported as coverage gaps in this file.
///
/// Not part of the public API; invoke [`assert_error_message!`] instead.
#[doc(hidden)]
#[track_caller]
pub fn assert_error_message_impl(error_string: &str, expected: &str) {
    let matches = error_string == expected
        || error_string.strip_prefix(expected).is_some_and(|remainder| {
            // backtrace, caused by, or error trace indicators
            remainder.starts_with("\n\nBacktrace:\n") || remainder.starts_with("\ncaused by: ") || remainder.starts_with("\n> ")
        });
    assert!(matches, "left : {expected}\nright: {error_string}");
}

#[cfg(test)]
mod tests {
    use crate::OhnoCore;
    use crate::backtrace::Backtrace;

    #[derive(crate::Error)]
    struct MyTestError {
        inner: OhnoCore,
    }

    #[test]
    fn test_assert_error_message_exact_match() {
        let error = MyTestError::caused_by("test message");
        assert_error_message!(error, "test message");
    }

    #[test]
    fn test_assert_error_message_with_backtrace() {
        let mut error = MyTestError::caused_by("test message");
        // Force a backtrace (this will be empty in test mode without RUST_BACKTRACE)
        error.inner.data.backtrace = Backtrace::disabled();
        assert_error_message!(error, "test message");
    }

    #[test]
    #[should_panic(expected = "left : expected message\nright: actual message")]
    fn test_assert_error_message_mismatch() {
        let error = MyTestError::caused_by("actual message");
        assert_error_message!(error, "expected message");
    }

    // The three following tests each make exactly one branch of the
    // accepted-continuation chain in `assert_error_message_impl` the
    // deciding one (the others false), so the assertion only passes while
    // those `||`s remain `||`. They exercise the impl directly (rather than
    // synthesizing real backtrace/caused-by/trace output) because the
    // strings are what the impl actually inspects.
    #[test]
    fn impl_accepts_backtrace_continuation() {
        super::assert_error_message_impl("base\n\nBacktrace:\n at ...", "base");
    }

    #[test]
    fn impl_accepts_caused_by_continuation() {
        super::assert_error_message_impl("base\ncaused by: inner", "base");
    }

    #[test]
    fn impl_accepts_error_trace_continuation() {
        super::assert_error_message_impl("base\n> trace entry", "base");
    }

    #[test]
    #[should_panic(expected = "left : base\nright: base mismatch")]
    fn impl_rejects_unrelated_continuation() {
        // A remainder that matches none of the accepted prefixes must fail.
        super::assert_error_message_impl("base mismatch", "base");
    }
}