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