1#![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#[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#[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#[derive(Debug)]
89pub struct AssertFormat<'a> {
90 original: &'a str,
92 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 #[must_use]
106 pub fn remaining_expected(&self) -> usize {
107 self.remaining.len()
108 }
109
110 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 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}