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