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}