1#![cfg_attr(
9 feature = "serde",
10 doc = r#"
11- [`assert_serde_eq!`]: diffs `Serialize` on assertion failure.
12"#
13)]
14use std::borrow::Cow;
74use std::fmt::{self, Display};
75use std::time::Duration;
76
77use console::{style, Style};
78use similar::{Algorithm, ChangeTag, TextDiff};
79
80#[cfg(feature = "serde")]
81#[doc(hidden)]
82pub mod serde_impl;
83
84#[doc(hidden)]
86pub mod print;
87
88fn get_max_string_length() -> usize {
90 use std::sync::atomic::{AtomicUsize, Ordering};
91 static TRUNCATE: AtomicUsize = AtomicUsize::new(!0);
92 let rv = TRUNCATE.load(Ordering::Relaxed);
93 if rv != !0 {
94 return rv;
95 }
96 let rv: usize = std::env::var("SIMILAR_ASSERTS_MAX_STRING_LENGTH")
97 .ok()
98 .and_then(|x| x.parse().ok())
99 .unwrap_or(200);
100 TRUNCATE.store(rv, Ordering::Relaxed);
101 rv
102}
103
104pub struct SimpleDiff<'a> {
112 pub(crate) left_short: Cow<'a, str>,
113 pub(crate) right_short: Cow<'a, str>,
114 pub(crate) left_expanded: Option<Cow<'a, str>>,
115 pub(crate) right_expanded: Option<Cow<'a, str>>,
116 pub(crate) left_label: &'a str,
117 pub(crate) right_label: &'a str,
118}
119
120impl<'a> SimpleDiff<'a> {
121 pub fn from_str(
127 left: &'a str,
128 right: &'a str,
129 left_label: &'a str,
130 right_label: &'a str,
131 ) -> SimpleDiff<'a> {
132 SimpleDiff {
133 left_short: left.into(),
134 right_short: right.into(),
135 left_expanded: None,
136 right_expanded: None,
137 left_label,
138 right_label,
139 }
140 }
141
142 #[doc(hidden)]
143 pub fn __from_macro(
144 left_short: Option<Cow<'a, str>>,
145 right_short: Option<Cow<'a, str>>,
146 left_expanded: Option<Cow<'a, str>>,
147 right_expanded: Option<Cow<'a, str>>,
148 left_label: &'a str,
149 right_label: &'a str,
150 ) -> SimpleDiff<'a> {
151 SimpleDiff {
152 left_short: left_short.unwrap_or_else(|| "<unprintable object>".into()),
153 right_short: right_short.unwrap_or_else(|| "<unprintable object>".into()),
154 left_expanded,
155 right_expanded,
156 left_label,
157 right_label,
158 }
159 }
160
161 fn left(&self) -> &str {
163 self.left_expanded.as_deref().unwrap_or(&self.left_short)
164 }
165
166 fn right(&self) -> &str {
168 self.right_expanded.as_deref().unwrap_or(&self.right_short)
169 }
170
171 fn label_padding(&self) -> usize {
173 self.left_label
174 .chars()
175 .count()
176 .max(self.right_label.chars().count())
177 }
178
179 #[doc(hidden)]
180 #[track_caller]
181 pub fn fail_assertion(&self, hint: &dyn Display) {
182 let len = get_max_string_length();
184 let (left, left_truncated) = truncate_str(&self.left_short, len);
185 let (right, right_truncated) = truncate_str(&self.right_short, len);
186
187 panic!(
188 "assertion failed: `({} == {})`{}'\
189 \n {:>label_padding$}: `{:?}`{}\
190 \n {:>label_padding$}: `{:?}`{}\
191 \n\n{}\n",
192 self.left_label,
193 self.right_label,
194 hint,
195 self.left_label,
196 DebugStrTruncated(left, left_truncated),
197 if left_truncated { " (truncated)" } else { "" },
198 self.right_label,
199 DebugStrTruncated(right, right_truncated),
200 if right_truncated { " (truncated)" } else { "" },
201 &self,
202 label_padding = self.label_padding(),
203 );
204 }
205}
206
207fn truncate_str(s: &str, chars: usize) -> (&str, bool) {
208 if chars == 0 {
209 return (s, false);
210 }
211 s.char_indices()
212 .enumerate()
213 .find_map(|(idx, (offset, _))| {
214 if idx == chars {
215 Some((&s[..offset], true))
216 } else {
217 None
218 }
219 })
220 .unwrap_or((s, false))
221}
222
223struct DebugStrTruncated<'s>(&'s str, bool);
224
225impl<'s> fmt::Debug for DebugStrTruncated<'s> {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 if self.1 {
228 let s = format!("{}...", self.0);
229 fmt::Debug::fmt(&s, f)
230 } else {
231 fmt::Debug::fmt(&self.0, f)
232 }
233 }
234}
235
236fn trailing_newline(s: &str) -> &str {
237 if s.ends_with("\r\n") {
238 "\r\n"
239 } else if s.ends_with("\r") {
240 "\r"
241 } else if s.ends_with("\n") {
242 "\n"
243 } else {
244 ""
245 }
246}
247
248fn detect_newlines(s: &str) -> (bool, bool, bool) {
249 let mut last_char = None;
250 let mut detected_crlf = false;
251 let mut detected_cr = false;
252 let mut detected_lf = false;
253
254 for c in s.chars() {
255 if c == '\n' {
256 if last_char.take() == Some('\r') {
257 detected_crlf = true;
258 } else {
259 detected_lf = true;
260 }
261 }
262 if last_char == Some('\r') {
263 detected_cr = true;
264 }
265 last_char = Some(c);
266 }
267 if last_char == Some('\r') {
268 detected_cr = true;
269 }
270
271 (detected_cr, detected_crlf, detected_lf)
272}
273
274#[allow(clippy::match_like_matches_macro)]
275fn newlines_matter(left: &str, right: &str) -> bool {
276 if trailing_newline(left) != trailing_newline(right) {
277 return true;
278 }
279
280 let (cr1, crlf1, lf1) = detect_newlines(left);
281 let (cr2, crlf2, lf2) = detect_newlines(right);
282
283 match (cr1 || cr2, crlf1 || crlf2, lf1 || lf2) {
284 (false, false, false) => false,
285 (true, false, false) => false,
286 (false, true, false) => false,
287 (false, false, true) => false,
288 _ => true,
289 }
290}
291
292impl<'a> fmt::Display for SimpleDiff<'a> {
293 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294 let left = self.left();
295 let right = self.right();
296 let newlines_matter = newlines_matter(left, right);
297
298 if left == right {
299 writeln!(
300 f,
301 "{}: the two values are the same in string form.",
302 style("Invisible differences").bold(),
303 )?;
304 return Ok(());
305 }
306
307 let diff = TextDiff::configure()
308 .timeout(Duration::from_millis(200))
309 .algorithm(Algorithm::Patience)
310 .diff_lines(left, right);
311
312 writeln!(
313 f,
314 "{} ({}{}|{}{}):",
315 style("Differences").bold(),
316 style("-").red().dim(),
317 style(self.left_label).red(),
318 style("+").green().dim(),
319 style(self.right_label).green(),
320 )?;
321 for (idx, group) in diff.grouped_ops(4).into_iter().enumerate() {
322 if idx > 0 {
323 writeln!(f, "@ {}", style("~~~").dim())?;
324 }
325 for op in group {
326 for change in diff.iter_inline_changes(&op) {
327 let (marker, style) = match change.tag() {
328 ChangeTag::Delete => ('-', Style::new().red()),
329 ChangeTag::Insert => ('+', Style::new().green()),
330 ChangeTag::Equal => (' ', Style::new().dim()),
331 };
332 write!(f, "{}", style.apply_to(marker).dim().bold())?;
333 for &(emphasized, value) in change.values() {
334 let value = if newlines_matter {
335 Cow::Owned(
336 value
337 .replace("\r", "␍\r")
338 .replace("\n", "␊\n")
339 .replace("␍\r␊\n", "␍␊\r\n"),
340 )
341 } else {
342 Cow::Borrowed(value)
343 };
344 if emphasized {
345 write!(f, "{}", style.clone().underlined().bold().apply_to(value))?;
346 } else {
347 write!(f, "{}", style.apply_to(value))?;
348 }
349 }
350 if change.missing_newline() {
351 writeln!(f)?;
352 }
353 }
354 }
355 }
356
357 Ok(())
358 }
359}
360
361#[doc(hidden)]
362#[macro_export]
363macro_rules! __assert_eq {
364 (
365 $method:ident,
366 $left_label:ident,
367 $left:expr,
368 $right_label:ident,
369 $right:expr,
370 $hint_suffix:expr
371 ) => {{
372 match (&($left), &($right)) {
373 (left_val, right_val) =>
374 {
375 #[allow(unused_mut)]
376 if !(*left_val == *right_val) {
377 use $crate::print::{PrintMode, PrintObject};
378 let left_label = stringify!($left_label);
379 let right_label = stringify!($right_label);
380 let mut left_val_tup1 = (&left_val,);
381 let mut right_val_tup1 = (&right_val,);
382 let mut left_val_tup2 = (&left_val,);
383 let mut right_val_tup2 = (&right_val,);
384 let left_short = left_val_tup1.print_object(PrintMode::Default);
385 let right_short = right_val_tup1.print_object(PrintMode::Default);
386 let left_expanded = left_val_tup2.print_object(PrintMode::Expanded);
387 let right_expanded = right_val_tup2.print_object(PrintMode::Expanded);
388 let diff = $crate::SimpleDiff::__from_macro(
389 left_short,
390 right_short,
391 left_expanded,
392 right_expanded,
393 left_label,
394 right_label,
395 );
396 diff.fail_assertion(&$hint_suffix);
397 }
398 }
399 }
400 }};
401}
402
403#[macro_export]
418macro_rules! assert_eq {
419 ($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({
420 $crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, "");
421 });
422 ($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({
423 $crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, format_args!(": {}", format_args!($($arg)*)));
424 });
425 ($left:expr, $right:expr $(,)?) => ({
426 $crate::assert_eq!(left: $left, right: $right);
427 });
428 ($left:expr, $right:expr, $($arg:tt)*) => ({
429 $crate::assert_eq!(left: $left, right: $right, $($arg)*);
430 });
431}
432
433#[macro_export]
435#[doc(hidden)]
436#[deprecated(since = "1.4.0", note = "use assert_eq! instead")]
437macro_rules! assert_str_eq {
438 ($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({
439 $crate::assert_eq!($left_label: $left, $right_label: $right);
440 });
441 ($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({
442 $crate::assert_eq!($left_label: $left, $right_label: $right, $($arg)*);
443 });
444 ($left:expr, $right:expr $(,)?) => ({
445 $crate::assert_eq!($left, $right);
446 });
447 ($left:expr, $right:expr, $($arg:tt)*) => ({
448 $crate::assert_eq!($left, $right, $($arg)*);
449 });
450}
451
452#[test]
453fn test_newlines_matter() {
454 assert!(newlines_matter("\r\n", "\n"));
455 assert!(newlines_matter("foo\n", "foo"));
456 assert!(newlines_matter("foo\r\nbar", "foo\rbar"));
457 assert!(newlines_matter("foo\r\nbar", "foo\nbar"));
458 assert!(newlines_matter("foo\r\nbar\n", "foobar"));
459 assert!(newlines_matter("foo\nbar\r\n", "foo\nbar\r\n"));
460 assert!(newlines_matter("foo\nbar\n", "foo\nbar"));
461
462 assert!(!newlines_matter("foo\nbar", "foo\nbar"));
463 assert!(!newlines_matter("foo\nbar\n", "foo\nbar\n"));
464 assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar"));
465 assert!(!newlines_matter("foo\r\nbar\r\n", "foo\r\nbar\r\n"));
466 assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar"));
467}
468
469#[test]
470fn test_truncate_str() {
471 assert_eq!(truncate_str("foobar", 20), ("foobar", false));
472 assert_eq!(truncate_str("foobar", 2), ("fo", true));
473 assert_eq!(truncate_str("🔥🔥🔥🔥🔥", 2), ("🔥🔥", true));
474}