Skip to main content

snapbox/assert/
mod.rs

1mod action;
2mod error;
3
4#[cfg(feature = "color")]
5use anstream::panic;
6#[cfg(feature = "color")]
7use anstream::stderr;
8#[cfg(not(feature = "color"))]
9use std::io::stderr;
10
11use crate::IntoData;
12use crate::filter::{Filter as _, FilterNewlines, FilterPaths, NormalizeToExpected};
13
14pub use action::Action;
15pub use action::DEFAULT_ACTION_ENV;
16pub use error::Error;
17pub use error::Result;
18
19/// Snapshot assertion against a file's contents
20///
21/// Useful for one-off assertions with the snapshot stored in a file
22///
23/// # Examples
24///
25/// ```rust,no_run
26/// # use snapbox::Assert;
27/// # use snapbox::file;
28/// let actual = "something";
29/// Assert::new().eq(actual, file!["output.txt"]);
30/// ```
31#[derive(Clone, Debug)]
32pub struct Assert {
33    pub(crate) action: Action,
34    action_var: Option<String>,
35    normalize_paths: bool,
36    substitutions: crate::Redactions,
37    pub(crate) palette: crate::report::Palette,
38}
39
40/// # Assertions
41impl Assert {
42    pub fn new() -> Self {
43        Default::default()
44    }
45
46    /// Check if a value is the same as an expected value
47    ///
48    /// By default [`filters`][crate::filter] are applied, including:
49    /// - `...` is a line-wildcard when on a line by itself
50    /// - `[..]` is a character-wildcard when inside a line
51    /// - `[EXE]` matches `.exe` on Windows
52    /// - `"{...}"` is a JSON value wildcard
53    /// - `"...": "{...}"` is a JSON key-value wildcard
54    /// - `\` to `/`
55    /// - Newlines
56    ///
57    /// To limit this to newline normalization for text, call [`Data::raw`][crate::Data::raw] on `expected`.
58    ///
59    /// # Examples
60    ///
61    /// ```rust
62    /// # use snapbox::Assert;
63    /// let actual = "something";
64    /// let expected = "so[..]g";
65    /// Assert::new().eq(actual, expected);
66    /// ```
67    ///
68    /// Can combine this with [`file!`][crate::file]
69    /// ```rust,no_run
70    /// # use snapbox::Assert;
71    /// # use snapbox::file;
72    /// let actual = "something";
73    /// Assert::new().eq(actual, file!["output.txt"]);
74    /// ```
75    #[track_caller]
76    pub fn eq(&self, actual: impl IntoData, expected: impl IntoData) {
77        let expected = expected.into_data();
78        let actual = actual.into_data();
79        if let Err(err) = self.try_eq(Some(&"In-memory"), actual, expected) {
80            err.panic();
81        }
82    }
83
84    #[track_caller]
85    #[deprecated(since = "0.6.0", note = "Replaced with `Assert::eq`")]
86    pub fn eq_(&self, actual: impl IntoData, expected: impl IntoData) {
87        self.eq(actual, expected);
88    }
89
90    pub fn try_eq(
91        &self,
92        actual_name: Option<&dyn std::fmt::Display>,
93        actual: crate::Data,
94        expected: crate::Data,
95    ) -> Result<()> {
96        if expected.source().is_none() && actual.source().is_some() {
97            panic!("received `(actual, expected)`, expected `(expected, actual)`");
98        }
99        match self.action {
100            Action::Skip => {
101                return Ok(());
102            }
103            Action::Ignore | Action::Verify | Action::Overwrite => {}
104        }
105
106        let (actual, expected) = self.normalize(actual, expected);
107
108        self.do_action(actual_name, actual, expected)
109    }
110
111    pub fn normalize(
112        &self,
113        mut actual: crate::Data,
114        mut expected: crate::Data,
115    ) -> (crate::Data, crate::Data) {
116        if expected.inner.filters.is_newlines_set() {
117            expected = FilterNewlines.filter(expected);
118        }
119
120        // On `expected` being an error, make a best guess
121        actual = actual.coerce_to(expected.against_format());
122        actual = actual.coerce_to(expected.intended_format());
123
124        if self.normalize_paths && expected.inner.filters.is_paths_set() {
125            actual = FilterPaths.filter(actual);
126        }
127        if expected.inner.filters.is_newlines_set() {
128            actual = FilterNewlines.filter(actual);
129        }
130
131        let mut normalize = NormalizeToExpected::new();
132        if expected.inner.filters.is_redaction_set() {
133            normalize = normalize.redact_with(&self.substitutions);
134        }
135        if expected.inner.filters.is_unordered_set() {
136            normalize = normalize.unordered();
137        }
138        actual = normalize.normalize(actual, &expected);
139
140        (actual, expected)
141    }
142
143    fn do_action(
144        &self,
145        actual_name: Option<&dyn std::fmt::Display>,
146        actual: crate::Data,
147        expected: crate::Data,
148    ) -> Result<()> {
149        let result = self.try_verify(actual_name, &actual, &expected);
150        let Err(err) = result else {
151            return Ok(());
152        };
153        match self.action {
154            Action::Skip => unreachable!("Bailed out earlier"),
155            Action::Ignore => {
156                use std::io::Write;
157
158                let _ = writeln!(
159                    stderr(),
160                    "{}: {}",
161                    self.palette.warn("Ignoring failure"),
162                    err
163                );
164                Ok(())
165            }
166            Action::Verify => {
167                let message = if expected.source().is_none() {
168                    crate::report::Styled::new(String::new(), Default::default())
169                } else if let Some(action_var) = self.action_var.as_deref() {
170                    self.palette
171                        .hint(format!("Update with {action_var}=overwrite"))
172                } else {
173                    crate::report::Styled::new(String::new(), Default::default())
174                };
175                Err(Error::new(format_args!("{err}{message}")))
176            }
177            Action::Overwrite => {
178                use std::io::Write;
179
180                if let Some(source) = expected.source() {
181                    if let Err(message) = actual.write_to(source) {
182                        Err(Error::new(format_args!("{err}Update failed: {message}")))
183                    } else {
184                        let _ = writeln!(stderr(), "{}: {}", self.palette.warn("Fixing"), err);
185                        Ok(())
186                    }
187                } else {
188                    Err(Error::new(format_args!("{err}")))
189                }
190            }
191        }
192    }
193
194    fn try_verify(
195        &self,
196        actual_name: Option<&dyn std::fmt::Display>,
197        actual: &crate::Data,
198        expected: &crate::Data,
199    ) -> Result<()> {
200        if actual != expected {
201            let mut buf = String::new();
202            crate::report::write_diff(
203                &mut buf,
204                expected,
205                actual,
206                expected.source().map(|s| s as &dyn std::fmt::Display),
207                actual_name,
208                self.palette,
209            )
210            .map_err(|e| e.to_string())?;
211            Err(buf.into())
212        } else {
213            Ok(())
214        }
215    }
216}
217
218/// # Directory Assertions
219#[cfg(feature = "dir")]
220impl Assert {
221    #[track_caller]
222    pub fn subset_eq(
223        &self,
224        expected_root: impl Into<std::path::PathBuf>,
225        actual_root: impl Into<std::path::PathBuf>,
226    ) {
227        let expected_root = expected_root.into();
228        let actual_root = actual_root.into();
229        self.subset_eq_inner(expected_root, actual_root);
230    }
231
232    #[track_caller]
233    fn subset_eq_inner(&self, expected_root: std::path::PathBuf, actual_root: std::path::PathBuf) {
234        match self.action {
235            Action::Skip => {
236                return;
237            }
238            Action::Ignore | Action::Verify | Action::Overwrite => {}
239        }
240
241        let checks: Vec<_> =
242            crate::dir::PathDiff::subset_eq_iter_inner(expected_root, actual_root).collect();
243        self.verify(checks);
244    }
245
246    #[track_caller]
247    pub fn subset_matches(
248        &self,
249        pattern_root: impl Into<std::path::PathBuf>,
250        actual_root: impl Into<std::path::PathBuf>,
251    ) {
252        let pattern_root = pattern_root.into();
253        let actual_root = actual_root.into();
254        self.subset_matches_inner(pattern_root, actual_root);
255    }
256
257    #[track_caller]
258    fn subset_matches_inner(
259        &self,
260        expected_root: std::path::PathBuf,
261        actual_root: std::path::PathBuf,
262    ) {
263        match self.action {
264            Action::Skip => {
265                return;
266            }
267            Action::Ignore | Action::Verify | Action::Overwrite => {}
268        }
269
270        let checks: Vec<_> = crate::dir::PathDiff::subset_matches_iter_inner(
271            expected_root,
272            actual_root,
273            &self.substitutions,
274            self.normalize_paths,
275        )
276        .collect();
277        self.verify(checks);
278    }
279
280    #[track_caller]
281    fn verify(
282        &self,
283        mut checks: Vec<Result<(std::path::PathBuf, std::path::PathBuf), crate::dir::PathDiff>>,
284    ) {
285        if checks.iter().all(Result::is_ok) {
286            for check in checks {
287                let (_expected_path, _actual_path) = check.unwrap();
288                crate::debug!(
289                    "{}: is {}",
290                    _expected_path.display(),
291                    self.palette.info("good")
292                );
293            }
294        } else {
295            checks.sort_by_key(|c| match c {
296                Ok((expected_path, _actual_path)) => Some(expected_path.clone()),
297                Err(diff) => diff.expected_path().map(|p| p.to_owned()),
298            });
299
300            let mut buffer = String::new();
301            let mut ok = true;
302            for check in checks {
303                use std::fmt::Write;
304                match check {
305                    Ok((expected_path, _actual_path)) => {
306                        let _ = writeln!(
307                            &mut buffer,
308                            "{}: is {}",
309                            expected_path.display(),
310                            self.palette.info("good"),
311                        );
312                    }
313                    Err(diff) => {
314                        let _ = diff.write(&mut buffer, self.palette);
315                        match self.action {
316                            Action::Skip => unreachable!("Bailed out earlier"),
317                            Action::Ignore | Action::Verify => {
318                                ok = false;
319                            }
320                            Action::Overwrite => {
321                                if let Err(err) = diff.overwrite() {
322                                    ok = false;
323                                    let path = diff
324                                        .expected_path()
325                                        .expect("always present when overwrite can fail");
326                                    let _ = writeln!(
327                                        &mut buffer,
328                                        "{} to overwrite {}: {}",
329                                        self.palette.error("Failed"),
330                                        path.display(),
331                                        err
332                                    );
333                                }
334                            }
335                        }
336                    }
337                }
338            }
339            if ok {
340                use std::io::Write;
341                let _ = write!(stderr(), "{buffer}");
342                match self.action {
343                    Action::Skip => unreachable!("Bailed out earlier"),
344                    Action::Ignore => {
345                        let _ =
346                            write!(stderr(), "{}", self.palette.warn("Ignoring above failures"));
347                    }
348                    Action::Verify => unreachable!("Something had to fail to get here"),
349                    Action::Overwrite => {
350                        let _ = write!(
351                            stderr(),
352                            "{}",
353                            self.palette.warn("Overwrote above failures")
354                        );
355                    }
356                }
357            } else {
358                match self.action {
359                    Action::Skip => unreachable!("Bailed out earlier"),
360                    Action::Ignore => unreachable!("Shouldn't be able to fail"),
361                    Action::Verify => {
362                        use std::fmt::Write;
363                        if let Some(action_var) = self.action_var.as_deref() {
364                            writeln!(
365                                &mut buffer,
366                                "{}",
367                                self.palette
368                                    .hint(format_args!("Update with {action_var}=overwrite"))
369                            )
370                            .unwrap();
371                        }
372                    }
373                    Action::Overwrite => {}
374                }
375                panic!("{}", buffer);
376            }
377        }
378    }
379}
380
381/// # Customize Behavior
382impl Assert {
383    /// Override the color palette
384    pub fn palette(mut self, palette: crate::report::Palette) -> Self {
385        self.palette = palette;
386        self
387    }
388
389    /// Read the failure action from an environment variable
390    pub fn action_env(mut self, var_name: &str) -> Self {
391        let action = Action::with_env_var(var_name);
392        self.action = action.unwrap_or(self.action);
393        self.action_var = Some(var_name.to_owned());
394        self
395    }
396
397    /// Override the failure action
398    pub fn action(mut self, action: Action) -> Self {
399        self.action = action;
400        self.action_var = None;
401        self
402    }
403
404    /// Override the default [`Redactions`][crate::Redactions]
405    pub fn redact_with(mut self, substitutions: crate::Redactions) -> Self {
406        self.substitutions = substitutions;
407        self
408    }
409
410    /// Override the default [`Redactions`][crate::Redactions]
411    #[deprecated(since = "0.6.2", note = "Replaced with `Assert::redact_with`")]
412    pub fn substitutions(self, substitutions: crate::Redactions) -> Self {
413        self.redact_with(substitutions)
414    }
415
416    /// Specify whether text should have path separators normalized
417    ///
418    /// The default is normalized
419    pub fn normalize_paths(mut self, yes: bool) -> Self {
420        self.normalize_paths = yes;
421        self
422    }
423}
424
425impl Assert {
426    pub fn selected_action(&self) -> Action {
427        self.action
428    }
429
430    pub fn redactions(&self) -> &crate::Redactions {
431        &self.substitutions
432    }
433}
434
435impl Default for Assert {
436    fn default() -> Self {
437        Self {
438            action: Default::default(),
439            action_var: Default::default(),
440            normalize_paths: true,
441            substitutions: Default::default(),
442            palette: crate::report::Palette::color(),
443        }
444        .redact_with(crate::Redactions::with_exe())
445    }
446}