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 optimziation for them 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::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// 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: AtomicUsize = AtomicUsize::new(!0);
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: AtomicUsize = AtomicUsize::new(!0);
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 atomic.
108fn 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
121/// A console printable diff.
122///
123/// The [`Display`](std::fmt::Display) implementation of this type renders out a
124/// diff with ANSI markers so it creates a nice colored diff. This can be used to
125/// build your own custom assertions in addition to the ones from this crate.
126///
127/// It does not provide much customization beyond what's possible done by default.
128pub 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    /// Creates a diff from two strings.
139    ///
140    /// `left_label` and `right_label` are the labels used for the two sides.
141    /// `"left"` and `"right"` are sensible defaults if you don't know what
142    /// to pick.
143    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    /// Returns the left side as string.
179    fn left(&self) -> &str {
180        self.left_expanded.as_deref().unwrap_or(&self.left_short)
181    }
182
183    /// Returns the right side as string.
184    fn right(&self) -> &str {
185        self.right_expanded.as_deref().unwrap_or(&self.right_short)
186    }
187
188    /// Returns the label padding
189    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        // prefer the shortened version here.
200        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/// Asserts that two expressions are equal to each other (using [`PartialEq`]).
421///
422/// On panic, this macro will print the values of the expressions with their
423/// [`Debug`] or [`ToString`] representations with a colorized diff of the
424/// changes in the debug output.  It picks [`Debug`] for all types that are
425/// not strings themselves and [`ToString`] for [`str`] and [`String`].
426///
427/// Like [`assert!`], this macro has a second form, where a custom panic
428/// message can be provided.
429///
430/// ```rust
431/// use similar_asserts::assert_eq;
432/// assert_eq!((1..3).collect::<Vec<_>>(), vec![1, 2]);
433/// ```
434#[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/// Deprecated macro.  Use [`assert_eq!`] instead.
451#[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}