Skip to main content

similar_asserts/
lib.rs

1//! `similar-asserts` is a crate that enhances the default assertion
2//! experience by using [similar](https://crates.io/crates/similar) for diffing.
3//! On failed assertions it renders out a colorized diff to the terminal.
4//!
5//! It comes with a handful of macros to replace [`std::assert_eq!`] with:
6//!
7//! - [`assert_eq!`]: diffs `Debug` on assertion failure.
8#![cfg_attr(
9    feature = "serde",
10    doc = r#"
11- [`assert_serde_eq!`]: diffs `Serialize` on assertion failure.
12"#
13)]
14//!
15//! ![](https://raw.githubusercontent.com/mitsuhiko/similar-asserts/main/assets/screenshot.png)
16//!
17//! # Usage
18//!
19//! ```rust
20//! use similar_asserts::assert_eq;
21//! assert_eq!((1..3).collect::<Vec<_>>(), vec![1, 2]);
22//! ```
23//!
24//! Optionally the assertion macros also let you "name" the left and right
25//! side which will produce slightly more explicit output:
26//!
27//! ```rust
28//! use similar_asserts::assert_eq;
29//! assert_eq!(expected: vec![1, 2], actual: (1..3).collect::<Vec<_>>());
30//! ```
31//!
32//! # Feature Flags
33//!
34//! * `unicode` enable improved character matching (enabled by default)
35//! * `serde` turns on support for serde.
36//!
37//! # Faster Builds
38//!
39//! This crate works best if you add it as `dev-dependency` only.  To make your code
40//! still compile you can use conditional uses that override the default uses for the
41//! `assert_eq!` macro from the stdlib:
42//!
43//! ```
44//! #[cfg(test)]
45//! use similar_asserts::assert_eq;
46//! ```
47//!
48//! Since `similar_asserts` uses the `similar` library for diffing you can also
49//! enable optimization for it in all build types for quicker diffing. Add
50//! this to your `Cargo.toml`:
51//!
52//! ```toml
53//! [profile.dev.package.similar]
54//! opt-level = 3
55//! ```
56//!
57//! # String Truncation
58//!
59//! By default the assertion only shows 200 characters.  This can be changed with the
60//! `SIMILAR_ASSERTS_MAX_STRING_LENGTH` environment variable.  Setting it to `0` disables
61//! all truncation, otherwise it sets the maximum number of characters before truncation
62//! kicks in.
63//!
64//! # Context Size
65//!
66//! Diffs displayed by assertions have a default context size of 4 (show up to 4 lines above and
67//! below changes).   This can be changed with the `SIMILAR_ASSERTS_CONTEXT_SIZE` environment
68//! variable.
69//!
70//! # Manual Diff Printing
71//!
72//! If you want to build your own comparison macros and you need a quick and simple
73//! way to render diffs, you can use the [`SimpleDiff`] type and display it:
74//!
75//! ```should_panic
76//! use similar_asserts::SimpleDiff;
77//! panic!("Not equal\n\n{}", SimpleDiff::from_str("a\nb\n", "b\nb\n", "left", "right"));
78//! ```
79use 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// This needs to be public as we are using it internally in a macro.
92#[doc(hidden)]
93pub mod print;
94
95/// The maximum number of characters a string can be long before truncating.
96fn 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
101/// The context size for diff groups.
102fn 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
107/// Parse a `usize` value from an environment variable, cached in a static once cell.
108fn 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
117/// A console printable diff.
118///
119/// The [`Display`](std::fmt::Display) implementation of this type renders out a
120/// diff with ANSI markers so it creates a nice colored diff. This can be used to
121/// build your own custom assertions in addition to the ones from this crate.
122///
123/// It does not provide much customization beyond what's possible done by default.
124pub 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    /// Creates a diff from two strings.
135    ///
136    /// `left_label` and `right_label` are the labels used for the two sides.
137    /// `"left"` and `"right"` are sensible defaults if you don't know what
138    /// to pick.
139    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    /// Returns the left side as string.
175    fn left(&self) -> &str {
176        self.left_expanded.as_deref().unwrap_or(&self.left_short)
177    }
178
179    /// Returns the right side as string.
180    fn right(&self) -> &str {
181        self.right_expanded.as_deref().unwrap_or(&self.right_short)
182    }
183
184    /// Returns the label padding
185    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        // prefer the shortened version here.
196        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/// Asserts that two expressions are equal to each other (using [`PartialEq`]).
415///
416/// On panic, this macro will print the values of the expressions with their
417/// [`Debug`] or [`ToString`] representations with a colorized diff of the
418/// changes in the debug output.  It picks [`Debug`] for all types that are
419/// not strings themselves and [`ToString`] for [`str`] and [`String`].
420///
421/// Like [`assert!`], this macro has a second form, where a custom panic
422/// message can be provided.
423///
424/// ```rust
425/// use similar_asserts::assert_eq;
426/// assert_eq!((1..3).collect::<Vec<_>>(), vec![1, 2]);
427/// ```
428#[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/// Deprecated macro.  Use [`assert_eq!`] instead.
445#[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}