test-format 0.1.1

Provides the assert_debug_fmt and assert_display_fmt macros for testing Debug (or Display) implementations on no_std
Documentation
//! # Test `Debug` and `Display` implementations (with `no_std`)
//!
//! Testing of Debug and Display format implementations with support for `no_std` via
//! [`assert_debug_fmt`] and [`assert_display_fmt`] macros.
//!
//! ## `std` vs `no_std`
//! This crate builds in `no_std` mode by default.
//!
//! ## Examples
//!
//! Assume the following type `Test` that we will use to provide some test data:
//!
//! ```
//! struct Test<'a>(&'a str, char, &'a str);
//! ```
//!
//! ```
//! # struct Test<'a>(&'a str, char, &'a str);
//! use core::fmt::{Debug, Write};
//! use test_format::assert_debug_fmt;
//!
//! impl<'a> Debug for Test<'a> {
//!     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
//!         f.write_str(self.0)?;
//!         f.write_char(self.1)?;
//!         f.write_str(self.2)
//!     }
//! }
//!
//! let input = Test("valid", ' ', "input");
//! assert_debug_fmt!(input, "valid input");
//! ```
//!
//! If the formatting fails, the assertion will fail:
//!
//! ```should_panic
//! # struct Test<'a>(&'a str, char, &'a str);
//! # use core::fmt::{Debug, Write};
//! # use test_format::assert_debug_fmt;
//! #
//! # impl<'a> Debug for Test<'a> {
//! #     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
//! #         f.write_str(self.0)?;
//! #         f.write_char(self.1)?;
//! #         f.write_str(self.2)
//! #     }
//! # }
//! #
//! let input = Test("valid", ' ', "inputs");
//! assert_debug_fmt!(input, "valid input"); // panics
//! ```

#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![deny(warnings, clippy::pedantic)]
#![warn(
    clippy::expect_used,
    clippy::missing_errors_doc,
    clippy::unwrap_used,
    missing_docs,
    rust_2018_idioms,
    rust_2021_compatibility,
    unused_qualifications
)]
#![cfg_attr(docsrs, feature(doc_cfg))]

use core::fmt::{Debug, Write};

/// Asserts that the [`Debug`] trait is correctly implemented.
#[macro_export]
macro_rules! assert_debug_fmt {
    ($input:expr, $expectation:expr) => {
        let input = $input;
        $crate::AssertFormat::assert_debug_fmt(&input, $expectation);
    };
}

/// Asserts that the `Display` trait is correctly implemented.
#[macro_export]
macro_rules! assert_display_fmt {
    ($input:expr, $expectation:expr) => {
        let input = $input;
        $crate::AssertFormat::assert_display_fmt(&input, $expectation);
    };
}

/// Functionality for testing [`Debug`] or `Display` implementations.
#[derive(Debug)]
pub struct AssertFormat<'a> {
    /// The original string to compare.
    original: &'a str,
    /// The remaining text to compare.
    remaining: &'a str,
}

impl<'a> AssertFormat<'a> {
    fn new(s: &'a str) -> Self {
        Self {
            original: s,
            remaining: s,
        }
    }

    /// Indicates how many of the expected bytes remain to be written for a success case.
    #[must_use]
    pub fn remaining_expected(&self) -> usize {
        self.remaining.len()
    }

    /// Asserts that the `Display` trait is correctly implemented.
    ///
    /// ## Panics
    /// This call panics if the output generated by the `Display` implementation
    /// differs from the `expected` value.
    pub fn assert_display_fmt<D>(instance: D, expected: &str)
    where
        D: core::fmt::Display,
    {
        let mut test = AssertFormat::new(expected);
        let _ = write!(&mut test, "{instance}");
        test.assert_all_written();
    }

    /// Asserts that the `Debug` trait is correctly implemented.
    ///
    /// ## Panics
    /// This call panics if the output generated by the `Debug` implementation
    /// differs from the `expected` value.
    pub fn assert_debug_fmt<D>(instance: D, expected: &str)
    where
        D: Debug,
    {
        let mut test = AssertFormat::new(expected);
        let _ = write!(&mut test, "{instance:?}");
        test.assert_all_written();
    }

    fn assert_all_written(&self) {
        let written = &self.original[0..self.remaining.len()];
        assert_eq!(
            self.remaining_expected(),
            0,
            "assertion failed: Expected \"{}\" but got \"{}\": missing {} characters.",
            self.original,
            written,
            self.remaining.len() + 1
        );
    }
}

impl<'a> Write for AssertFormat<'a> {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        let _ = self.original;
        let length_ok = self.remaining.len() >= s.len();
        if length_ok && self.remaining.starts_with(s) {
            self.remaining = &self.remaining[s.len()..];
        } else {
            let position = self.original.len() - self.remaining.len();
            match first_diff_position(self.remaining, s) {
                None => unreachable!(),
                Some(pos) => {
                    let offending_index = position + pos;
                    panic!(
                        "assertion failed: Expected \"{}\" but found \"{}\" starting at position {}: mismatch at position {}.",
                        self.original, s, position, offending_index
                    );
                }
            }
        }
        Ok(())
    }
}

#[allow(unused)]
fn first_diff_position(s1: &str, s2: &str) -> Option<usize> {
    let pos = s1.chars().zip(s2.chars()).position(|(a, b)| a != b);
    match pos {
        Some(_pos) => pos,
        None => {
            if s2.len() == s1.len() {
                None
            } else {
                Some(core::cmp::min(s1.chars().count(), s2.chars().count()))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn diff_pos() {
        assert_eq!(first_diff_position("same", "same"), None);
        assert_eq!(first_diff_position("same", "some"), Some(1));
        assert_eq!(first_diff_position("some", "same"), Some(1));
        assert_eq!(first_diff_position("abcd", "abc"), Some(3));
        assert_eq!(first_diff_position("abc", "abcd"), Some(3));
    }
}