test_format/
lib.rs

1//! # Test `Debug` and `Display` implementations (with `no_std`)
2//!
3//! Testing of Debug and Display format implementations with support for `no_std` via
4//! [`assert_debug_fmt`] and [`assert_display_fmt`] macros.
5//!
6//! ## `std` vs `no_std`
7//! This crate builds in `no_std` mode by default.
8//!
9//! ## Examples
10//!
11//! Assume the following type `Test` that we will use to provide some test data:
12//!
13//! ```
14//! struct Test<'a>(&'a str, char, &'a str);
15//! ```
16//!
17//! ```
18//! # struct Test<'a>(&'a str, char, &'a str);
19//! use core::fmt::{Debug, Write};
20//! use test_format::assert_debug_fmt;
21//!
22//! impl<'a> Debug for Test<'a> {
23//!     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24//!         f.write_str(self.0)?;
25//!         f.write_char(self.1)?;
26//!         f.write_str(self.2)
27//!     }
28//! }
29//!
30//! let input = Test("valid", ' ', "input");
31//! assert_debug_fmt!(input, "valid input");
32//! ```
33//!
34//! If the formatting fails, the assertion will fail:
35//!
36//! ```should_panic
37//! # struct Test<'a>(&'a str, char, &'a str);
38//! # use core::fmt::{Debug, Write};
39//! # use test_format::assert_debug_fmt;
40//! #
41//! # impl<'a> Debug for Test<'a> {
42//! #     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43//! #         f.write_str(self.0)?;
44//! #         f.write_char(self.1)?;
45//! #         f.write_str(self.2)
46//! #     }
47//! # }
48//! #
49//! let input = Test("valid", ' ', "inputs");
50//! assert_debug_fmt!(input, "valid input"); // panics
51//! ```
52
53#![cfg_attr(not(feature = "std"), no_std)]
54#![forbid(unsafe_code)]
55#![deny(warnings, clippy::pedantic)]
56#![warn(
57    clippy::expect_used,
58    clippy::missing_errors_doc,
59    clippy::unwrap_used,
60    missing_docs,
61    rust_2018_idioms,
62    rust_2021_compatibility,
63    unused_qualifications
64)]
65#![cfg_attr(docsrs, feature(doc_cfg))]
66
67use core::fmt::{Debug, Write};
68
69/// Asserts that the [`Debug`] trait is correctly implemented.
70#[macro_export]
71macro_rules! assert_debug_fmt {
72    ($input:expr, $expectation:expr) => {
73        let input = $input;
74        $crate::AssertFormat::assert_debug_fmt(&input, $expectation);
75    };
76}
77
78/// Asserts that the `Display` trait is correctly implemented.
79#[macro_export]
80macro_rules! assert_display_fmt {
81    ($input:expr, $expectation:expr) => {
82        let input = $input;
83        $crate::AssertFormat::assert_display_fmt(&input, $expectation);
84    };
85}
86
87/// Functionality for testing [`Debug`] or `Display` implementations.
88#[derive(Debug)]
89pub struct AssertFormat<'a> {
90    /// The original string to compare.
91    original: &'a str,
92    /// The remaining text to compare.
93    remaining: &'a str,
94}
95
96impl<'a> AssertFormat<'a> {
97    fn new(s: &'a str) -> Self {
98        Self {
99            original: s,
100            remaining: s,
101        }
102    }
103
104    /// Indicates how many of the expected bytes remain to be written for a success case.
105    #[must_use]
106    pub fn remaining_expected(&self) -> usize {
107        self.remaining.len()
108    }
109
110    /// Asserts that the `Display` trait is correctly implemented.
111    ///
112    /// ## Panics
113    /// This call panics if the output generated by the `Display` implementation
114    /// differs from the `expected` value.
115    pub fn assert_display_fmt<D>(instance: D, expected: &str)
116    where
117        D: core::fmt::Display,
118    {
119        let mut test = AssertFormat::new(expected);
120        let _ = write!(&mut test, "{instance}");
121        test.assert_all_written();
122    }
123
124    /// Asserts that the `Debug` trait is correctly implemented.
125    ///
126    /// ## Panics
127    /// This call panics if the output generated by the `Debug` implementation
128    /// differs from the `expected` value.
129    pub fn assert_debug_fmt<D>(instance: D, expected: &str)
130    where
131        D: Debug,
132    {
133        let mut test = AssertFormat::new(expected);
134        let _ = write!(&mut test, "{instance:?}");
135        test.assert_all_written();
136    }
137
138    fn assert_all_written(&self) {
139        let written = &self.original[0..self.remaining.len()];
140        assert_eq!(
141            self.remaining_expected(),
142            0,
143            "assertion failed: Expected \"{}\" but got \"{}\": missing {} characters.",
144            self.original,
145            written,
146            self.remaining.len() + 1
147        );
148    }
149}
150
151impl<'a> Write for AssertFormat<'a> {
152    fn write_str(&mut self, s: &str) -> core::fmt::Result {
153        let _ = self.original;
154        let length_ok = self.remaining.len() >= s.len();
155        if length_ok && self.remaining.starts_with(s) {
156            self.remaining = &self.remaining[s.len()..];
157        } else {
158            let position = self.original.len() - self.remaining.len();
159            match first_diff_position(self.remaining, s) {
160                None => unreachable!(),
161                Some(pos) => {
162                    let offending_index = position + pos;
163                    panic!(
164                        "assertion failed: Expected \"{}\" but found \"{}\" starting at position {}: mismatch at position {}.",
165                        self.original, s, position, offending_index
166                    );
167                }
168            }
169        }
170        Ok(())
171    }
172}
173
174#[allow(unused)]
175fn first_diff_position(s1: &str, s2: &str) -> Option<usize> {
176    let pos = s1.chars().zip(s2.chars()).position(|(a, b)| a != b);
177    match pos {
178        Some(_pos) => pos,
179        None => {
180            if s2.len() == s1.len() {
181                None
182            } else {
183                Some(core::cmp::min(s1.chars().count(), s2.chars().count()))
184            }
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn diff_pos() {
195        assert_eq!(first_diff_position("same", "same"), None);
196        assert_eq!(first_diff_position("same", "some"), Some(1));
197        assert_eq!(first_diff_position("some", "same"), Some(1));
198        assert_eq!(first_diff_position("abcd", "abc"), Some(3));
199        assert_eq!(first_diff_position("abc", "abcd"), Some(3));
200    }
201}