Skip to main content

insta/
comparator.rs

1//! Provides the [`Comparator`] trait, which provides a mechanism for specifying
2//! how [`Snapshot`] data should be compared.
3
4use crate::snapshot::{Snapshot, SnapshotContents, TextSnapshotKind};
5
6/// Allows specific behavior to be invoked when [`Snapshot`]s are compared.
7///
8/// This is intended for when custom `Snapshot` comparison behavior is
9/// desired. For example, two binary files that contain the same logical data
10/// but have different representations on disk (as might be the case with
11/// compressed images) could be compared with a `Comparator` that decompresses
12/// `Snapshot` data before comparing it.
13///
14/// To make a custom `Comparator` active, pass it to
15/// [`crate::settings::Settings::set_comparator`] or call [`with_settings!`] and
16/// provide an appropriate `Comparator` instance.
17///
18/// This trait requires `'static` so that implementing structs can be stored in
19/// [`crate::settings::Settings`].
20// TODO: `Send + Sync` is required because `Settings` currently uses `Arc`
21// internally. Consider removing these bounds if `Settings` switches to `Rc`
22// in the next breaking change.
23pub trait Comparator: Send + Sync + 'static {
24    /// Returns `true` if the contents of `reference` and `test` match.
25    ///
26    /// This is the standard comparison used by [`assert_snapshot!`].
27    fn matches(&self, reference: &Snapshot, test: &Snapshot) -> bool;
28
29    /// Returns `true` if `reference` and `test` match fully, including metadata.
30    ///
31    /// This is used when `INSTA_REQUIRE_FULL_MATCH` is enabled. The default
32    /// implementation delegates to [`matches`](Self::matches).
33    fn matches_fully(&self, reference: &Snapshot, test: &Snapshot) -> bool {
34        self.matches(reference, test)
35    }
36
37    /// Returns a type-erased clone of `self`.
38    ///
39    /// This is needed so that [`crate::settings::Settings`] (which provides the
40    /// usual mechanism for setting a custom `Comparator`) can implement
41    /// [`Clone`].
42    fn dyn_clone(&self) -> Box<dyn Comparator>;
43}
44
45/// Provides default comparison semantics for [`Snapshot`]s. Binary snapshots
46/// are compared on the basis of their contents (including file extension). Text
47/// snapshots are compared on the basis of their deserialized representation.
48#[derive(Clone)]
49pub struct DefaultComparator;
50
51impl Comparator for DefaultComparator {
52    fn matches(&self, reference: &Snapshot, test: &Snapshot) -> bool {
53        reference.contents() == test.contents()
54            // For binary snapshots the extension also needs to be the same:
55            && reference.metadata().snapshot_kind == test.metadata().snapshot_kind
56    }
57
58    fn matches_fully(&self, reference: &Snapshot, test: &Snapshot) -> bool {
59        match (reference.contents(), test.contents()) {
60            (SnapshotContents::Text(ref_contents), SnapshotContents::Text(test_contents)) => {
61                // Note that we previously would match the exact values of the
62                // unnormalized text. But that's too strict — it means we can
63                // never match a snapshot that has leading/trailing whitespace.
64                // So instead we check it matches on the latest format.
65                // Generally those should be the same — latest should be doing
66                // the minimum normalization; if they diverge we could update
67                // this to be stricter.
68                let contents_match_exact = ref_contents.matches_latest(test_contents);
69                match ref_contents.kind {
70                    TextSnapshotKind::File => {
71                        reference.metadata().trim_for_persistence()
72                            == test.metadata().trim_for_persistence()
73                            && contents_match_exact
74                    }
75                    TextSnapshotKind::Inline => contents_match_exact,
76                }
77            }
78            _ => self.matches(reference, test),
79        }
80    }
81
82    fn dyn_clone(&self) -> Box<dyn Comparator> {
83        Box::new(self.clone())
84    }
85}
86
87#[cfg(test)]
88mod test {
89    use super::DefaultComparator;
90
91    use crate::comparator::Comparator;
92    use crate::snapshot::{
93        MetaData, Snapshot, SnapshotContents, TextSnapshotContents, TextSnapshotKind,
94    };
95
96    const TEXT: &str =
97        "The sky above the port was the color of a television, tuned to a dead channel.";
98
99    #[test]
100    fn default_comparator_matches() {
101        let comparator = DefaultComparator;
102        let a = Snapshot::from_components(
103            String::from("test"),
104            None,
105            MetaData::default(),
106            SnapshotContents::Text(TextSnapshotContents::new(
107                String::from(TEXT),
108                TextSnapshotKind::Inline,
109            )),
110        );
111        let b = a.clone();
112        assert!(comparator.matches(&a, &b));
113        assert!(comparator.matches_fully(&a, &b));
114    }
115
116    #[test]
117    fn default_comparator_matches_fully() {
118        let comparator = DefaultComparator;
119        let a = Snapshot::from_components(
120            String::from("test"),
121            None,
122            MetaData::default(),
123            SnapshotContents::Text(TextSnapshotContents::new(
124                String::from(TEXT),
125                TextSnapshotKind::File,
126            )),
127        );
128        let mut b = Snapshot::from_components(
129            String::from("test"),
130            None,
131            MetaData::default(),
132            SnapshotContents::Text(TextSnapshotContents::new(
133                String::from(TEXT),
134                TextSnapshotKind::Inline,
135            )),
136        );
137        b.metadata.description = Some(String::from("wintermute")); // Differs from None in a.
138
139        // Comparing contents alone passes.
140        assert!(comparator.matches(&a, &b));
141
142        // Comparing contents alone still passes.
143        assert!(comparator.matches_fully(&a, &a));
144        // Comparing snapshots with differing metadata fails.
145        assert!(!comparator.matches_fully(&a, &b));
146    }
147}