cargo_test_support/
compare.rs

1//! Routines for comparing and diffing output.
2//!
3//! # Deprecated comparisons
4//!
5//! Cargo's tests are in transition from internal-only pattern and normalization routines used in
6//! asserts like [`crate::Execs::with_stdout_contains`] to [`assert_e2e`] and [`assert_ui`].
7//!
8//! ## Patterns
9//!
10//! Many of these functions support special markup to assist with comparing
11//! text that may vary or is otherwise uninteresting for the test at hand. The
12//! supported patterns are:
13//!
14//! - `[..]` is a wildcard that matches 0 or more characters on the same line
15//!   (similar to `.*` in a regex). It is non-greedy.
16//! - `[EXE]` optionally adds `.exe` on Windows (empty string on other
17//!   platforms).
18//! - `[ROOT]` is the path to the test directory's root.
19//! - `[CWD]` is the working directory of the process that was run.
20//! - There is a wide range of substitutions (such as `[COMPILING]` or
21//!   `[WARNING]`) to match cargo's "status" output and allows you to ignore
22//!   the alignment. See the source of `substitute_macros` for a complete list
23//!   of substitutions.
24//! - `[DIRTY-MSVC]` (only when the line starts with it) would be replaced by
25//!   `[DIRTY]` when `cfg(target_env = "msvc")` or the line will be ignored otherwise.
26//!   Tests that work around [issue 7358](https://github.com/rust-lang/cargo/issues/7358)
27//!   can use this to avoid duplicating the `with_stderr` call like:
28//!   `if cfg!(target_env = "msvc") {e.with_stderr("...[DIRTY]...");} else {e.with_stderr("...");}`.
29//!
30//! ## Normalization
31//!
32//! In addition to the patterns described above, the strings are normalized
33//! in such a way to avoid unwanted differences. The normalizations are:
34//!
35//! - Raw tab characters are converted to the string `<tab>`. This is helpful
36//!   so that raw tabs do not need to be written in the expected string, and
37//!   to avoid confusion of tabs vs spaces.
38//! - Backslashes are converted to forward slashes to deal with Windows paths.
39//!   This helps so that all tests can be written assuming forward slashes.
40//!   Other heuristics are applied to try to ensure Windows-style paths aren't
41//!   a problem.
42//! - Carriage returns are removed, which can help when running on Windows.
43
44use crate::cross_compile::try_alternate;
45use crate::paths;
46use crate::rustc_host;
47use anyhow::{bail, Result};
48use snapbox::Data;
49use snapbox::IntoData;
50use std::fmt;
51use std::path::Path;
52use std::path::PathBuf;
53use std::str;
54
55/// This makes it easier to write regex replacements that are guaranteed to only
56/// get compiled once
57macro_rules! regex {
58    ($re:literal $(,)?) => {{
59        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
60        RE.get_or_init(|| regex::Regex::new($re).unwrap())
61    }};
62}
63
64/// Assertion policy for UI tests
65///
66/// This emphasizes showing as much content as possible at the cost of more brittleness
67///
68/// # Snapshots
69///
70/// Updating of snapshots is controlled with the `SNAPSHOTS` environment variable:
71///
72/// - `skip`: do not run the tests
73/// - `ignore`: run the tests but ignore their failure
74/// - `verify`: run the tests
75/// - `overwrite`: update the snapshots based on the output of the tests
76///
77/// # Patterns
78///
79/// - `[..]` is a character wildcard, stopping at line breaks
80/// - `\n...\n` is a multi-line wildcard
81/// - `[EXE]` matches the exe suffix for the current platform
82/// - `[ROOT]` matches [`paths::root()`][crate::paths::root]
83/// - `[ROOTURL]` matches [`paths::root()`][crate::paths::root] as a URL
84///
85/// # Normalization
86///
87/// In addition to the patterns described above, text is normalized
88/// in such a way to avoid unwanted differences. The normalizations are:
89///
90/// - Backslashes are converted to forward slashes to deal with Windows paths.
91///   This helps so that all tests can be written assuming forward slashes.
92///   Other heuristics are applied to try to ensure Windows-style paths aren't
93///   a problem.
94/// - Carriage returns are removed, which can help when running on Windows.
95///
96/// # Example
97///
98/// ```no_run
99/// # use cargo_test_support::compare::assert_e2e;
100/// # use cargo_test_support::file;
101/// # let p = cargo_test_support::project().build();
102/// # let stdout = "";
103/// assert_e2e().eq(stdout, file!["stderr.term.svg"]);
104/// ```
105/// ```console
106/// $ SNAPSHOTS=overwrite cargo test
107/// ```
108pub fn assert_ui() -> snapbox::Assert {
109    let mut subs = snapbox::Redactions::new();
110    subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned())
111        .unwrap();
112    add_test_support_redactions(&mut subs);
113    add_regex_redactions(&mut subs);
114
115    snapbox::Assert::new()
116        .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
117        .redact_with(subs)
118}
119
120/// Assertion policy for functional end-to-end tests
121///
122/// This emphasizes showing as much content as possible at the cost of more brittleness
123///
124/// # Snapshots
125///
126/// Updating of snapshots is controlled with the `SNAPSHOTS` environment variable:
127///
128/// - `skip`: do not run the tests
129/// - `ignore`: run the tests but ignore their failure
130/// - `verify`: run the tests
131/// - `overwrite`: update the snapshots based on the output of the tests
132///
133/// # Patterns
134///
135/// - `[..]` is a character wildcard, stopping at line breaks
136/// - `\n...\n` is a multi-line wildcard
137/// - `[EXE]` matches the exe suffix for the current platform
138/// - `[ROOT]` matches [`paths::root()`][crate::paths::root]
139/// - `[ROOTURL]` matches [`paths::root()`][crate::paths::root] as a URL
140///
141/// # Normalization
142///
143/// In addition to the patterns described above, text is normalized
144/// in such a way to avoid unwanted differences. The normalizations are:
145///
146/// - Backslashes are converted to forward slashes to deal with Windows paths.
147///   This helps so that all tests can be written assuming forward slashes.
148///   Other heuristics are applied to try to ensure Windows-style paths aren't
149///   a problem.
150/// - Carriage returns are removed, which can help when running on Windows.
151///
152/// # Example
153///
154/// ```no_run
155/// # use cargo_test_support::compare::assert_e2e;
156/// # use cargo_test_support::str;
157/// # let p = cargo_test_support::project().build();
158/// assert_e2e().eq(p.read_lockfile(), str![]);
159/// ```
160/// ```console
161/// $ SNAPSHOTS=overwrite cargo test
162/// ```
163pub fn assert_e2e() -> snapbox::Assert {
164    let mut subs = snapbox::Redactions::new();
165    subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned())
166        .unwrap();
167    subs.extend(E2E_LITERAL_REDACTIONS.into_iter().cloned())
168        .unwrap();
169    add_test_support_redactions(&mut subs);
170    add_regex_redactions(&mut subs);
171
172    snapbox::Assert::new()
173        .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
174        .redact_with(subs)
175}
176
177fn add_test_support_redactions(subs: &mut snapbox::Redactions) {
178    let root = paths::root();
179    // Use `from_file_path` instead of `from_dir_path` so the trailing slash is
180    // put in the users output, rather than hidden in the variable
181    let root_url = url::Url::from_file_path(&root).unwrap().to_string();
182
183    subs.insert("[ROOT]", root).unwrap();
184    subs.insert("[ROOTURL]", root_url).unwrap();
185    subs.insert("[HOST_TARGET]", rustc_host()).unwrap();
186    if let Some(alt_target) = try_alternate() {
187        subs.insert("[ALT_TARGET]", alt_target).unwrap();
188    }
189}
190
191fn add_regex_redactions(subs: &mut snapbox::Redactions) {
192    // For e2e tests
193    subs.insert(
194        "[ELAPSED]",
195        regex!(r"\[FINISHED\].*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
196    )
197    .unwrap();
198    // for UI tests
199    subs.insert(
200        "[ELAPSED]",
201        regex!(r"Finished.*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
202    )
203    .unwrap();
204    // output from libtest
205    subs.insert(
206        "[ELAPSED]",
207        regex!(r"; finished in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"),
208    )
209    .unwrap();
210    subs.insert(
211        "[FILE_NUM]",
212        regex!(r"\[(REMOVED|SUMMARY)\] (?<redacted>[1-9][0-9]*) files"),
213    )
214    .unwrap();
215    subs.insert(
216        "[FILE_SIZE]",
217        regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?([a-zA-Z]i)?)B\s"),
218    )
219    .unwrap();
220    subs.insert(
221        "[HASH]",
222        regex!(r"home/\.cargo/registry/(cache|index|src)/-(?<redacted>[a-z0-9]+)"),
223    )
224    .unwrap();
225    subs.insert(
226        "[HASH]",
227        regex!(r"\.cargo/target/(?<redacted>[0-9a-f]{2}/[0-9a-f]{14})"),
228    )
229    .unwrap();
230    subs.insert("[HASH]", regex!(r"/[a-z0-9\-_]+-(?<redacted>[0-9a-f]{16})"))
231        .unwrap();
232    subs.insert(
233        "[AVG_ELAPSED]",
234        regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?) ns/iter"),
235    )
236    .unwrap();
237    subs.insert(
238        "[JITTER]",
239        regex!(r"ns/iter \(\+/- (?<redacted>[0-9]+(\.[0-9]+)?)\)"),
240    )
241    .unwrap();
242
243    // Following 3 subs redact:
244    //   "1719325877.527949100s, 61549498ns after last build at 1719325877.466399602s"
245    //   "1719503592.218193216s, 1h 1s after last build at 1719499991.982681034s"
246    // into "[DIRTY_REASON_NEW_TIME], [DIRTY_REASON_DIFF] after last build at [DIRTY_REASON_OLD_TIME]"
247    subs.insert(
248        "[TIME_DIFF_AFTER_LAST_BUILD]",
249        regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?s, (\s?[0-9]+(\.[0-9]+)?(s|ns|h))+ after last build at [0-9]+(\.[0-9]+)?s)"),
250       )
251       .unwrap();
252}
253
254static MIN_LITERAL_REDACTIONS: &[(&str, &str)] = &[
255    ("[EXE]", std::env::consts::EXE_SUFFIX),
256    ("[BROKEN_PIPE]", "Broken pipe (os error 32)"),
257    ("[BROKEN_PIPE]", "The pipe is being closed. (os error 232)"),
258    // Unix message for an entity was not found
259    ("[NOT_FOUND]", "No such file or directory (os error 2)"),
260    // Windows message for an entity was not found
261    (
262        "[NOT_FOUND]",
263        "The system cannot find the file specified. (os error 2)",
264    ),
265    (
266        "[NOT_FOUND]",
267        "The system cannot find the path specified. (os error 3)",
268    ),
269    ("[NOT_FOUND]", "Access is denied. (os error 5)"),
270    ("[NOT_FOUND]", "program not found"),
271    // Unix message for exit status
272    ("[EXIT_STATUS]", "exit status"),
273    // Windows message for exit status
274    ("[EXIT_STATUS]", "exit code"),
275];
276static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
277    ("[RUNNING]", "     Running"),
278    ("[COMPILING]", "   Compiling"),
279    ("[CHECKING]", "    Checking"),
280    ("[COMPLETED]", "   Completed"),
281    ("[CREATED]", "     Created"),
282    ("[CREATING]", "    Creating"),
283    ("[CREDENTIAL]", "  Credential"),
284    ("[DOWNGRADING]", " Downgrading"),
285    ("[FINISHED]", "    Finished"),
286    ("[ERROR]", "error:"),
287    ("[WARNING]", "warning:"),
288    ("[NOTE]", "note:"),
289    ("[HELP]", "help:"),
290    ("[DOCUMENTING]", " Documenting"),
291    ("[SCRAPING]", "    Scraping"),
292    ("[FRESH]", "       Fresh"),
293    ("[DIRTY]", "       Dirty"),
294    ("[LOCKING]", "     Locking"),
295    ("[UPDATING]", "    Updating"),
296    ("[UPGRADING]", "   Upgrading"),
297    ("[ADDING]", "      Adding"),
298    ("[REMOVING]", "    Removing"),
299    ("[REMOVED]", "     Removed"),
300    ("[UNCHANGED]", "   Unchanged"),
301    ("[DOCTEST]", "   Doc-tests"),
302    ("[PACKAGING]", "   Packaging"),
303    ("[PACKAGED]", "    Packaged"),
304    ("[DOWNLOADING]", " Downloading"),
305    ("[DOWNLOADED]", "  Downloaded"),
306    ("[UPLOADING]", "   Uploading"),
307    ("[UPLOADED]", "    Uploaded"),
308    ("[VERIFYING]", "   Verifying"),
309    ("[ARCHIVING]", "   Archiving"),
310    ("[INSTALLING]", "  Installing"),
311    ("[REPLACING]", "   Replacing"),
312    ("[UNPACKING]", "   Unpacking"),
313    ("[SUMMARY]", "     Summary"),
314    ("[FIXED]", "       Fixed"),
315    ("[FIXING]", "      Fixing"),
316    ("[IGNORED]", "     Ignored"),
317    ("[INSTALLED]", "   Installed"),
318    ("[REPLACED]", "    Replaced"),
319    ("[BUILDING]", "    Building"),
320    ("[LOGIN]", "       Login"),
321    ("[LOGOUT]", "      Logout"),
322    ("[YANK]", "        Yank"),
323    ("[OWNER]", "       Owner"),
324    ("[MIGRATING]", "   Migrating"),
325    ("[EXECUTABLE]", "  Executable"),
326    ("[SKIPPING]", "    Skipping"),
327    ("[WAITING]", "     Waiting"),
328    ("[PUBLISHED]", "   Published"),
329    ("[BLOCKING]", "    Blocking"),
330    ("[GENERATED]", "   Generated"),
331    ("[OPENING]", "     Opening"),
332];
333
334/// Checks that the given string contains the given contiguous lines
335/// somewhere.
336///
337/// See [Patterns](index.html#patterns) for more information on pattern matching.
338pub(crate) fn match_contains(
339    expected: &str,
340    actual: &str,
341    redactions: &snapbox::Redactions,
342) -> Result<()> {
343    let expected = normalize_expected(expected, redactions);
344    let actual = normalize_actual(actual, redactions);
345    let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
346    let a: Vec<_> = actual.lines().collect();
347    if e.len() == 0 {
348        bail!("expected length must not be zero");
349    }
350    for window in a.windows(e.len()) {
351        if e == window {
352            return Ok(());
353        }
354    }
355    bail!(
356        "expected to find:\n\
357         {}\n\n\
358         did not find in output:\n\
359         {}",
360        expected,
361        actual
362    );
363}
364
365/// Checks that the given string does not contain the given contiguous lines
366/// anywhere.
367///
368/// See [Patterns](index.html#patterns) for more information on pattern matching.
369pub(crate) fn match_does_not_contain(
370    expected: &str,
371    actual: &str,
372    redactions: &snapbox::Redactions,
373) -> Result<()> {
374    if match_contains(expected, actual, redactions).is_ok() {
375        bail!(
376            "expected not to find:\n\
377             {}\n\n\
378             but found in output:\n\
379             {}",
380            expected,
381            actual
382        );
383    } else {
384        Ok(())
385    }
386}
387
388/// Checks that the given string has a line that contains the given patterns,
389/// and that line also does not contain the `without` patterns.
390///
391/// See [Patterns](index.html#patterns) for more information on pattern matching.
392///
393/// See [`crate::Execs::with_stderr_line_without`] for an example and cautions
394/// against using.
395pub(crate) fn match_with_without(
396    actual: &str,
397    with: &[String],
398    without: &[String],
399    redactions: &snapbox::Redactions,
400) -> Result<()> {
401    let actual = normalize_actual(actual, redactions);
402    let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, redactions));
403    let with: Vec<_> = with.iter().map(norm).collect();
404    let without: Vec<_> = without.iter().map(norm).collect();
405    let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
406    let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
407
408    let matches: Vec<_> = actual
409        .lines()
410        .filter(|line| with_wild.iter().all(|with| with == line))
411        .filter(|line| !without_wild.iter().any(|without| without == line))
412        .collect();
413    match matches.len() {
414        0 => bail!(
415            "Could not find expected line in output.\n\
416             With contents: {:?}\n\
417             Without contents: {:?}\n\
418             Actual stderr:\n\
419             {}\n",
420            with,
421            without,
422            actual
423        ),
424        1 => Ok(()),
425        _ => bail!(
426            "Found multiple matching lines, but only expected one.\n\
427             With contents: {:?}\n\
428             Without contents: {:?}\n\
429             Matching lines:\n\
430             {}\n",
431            with,
432            without,
433            itertools::join(matches, "\n")
434        ),
435    }
436}
437
438/// Normalizes the output so that it can be compared against the expected value.
439fn normalize_actual(content: &str, redactions: &snapbox::Redactions) -> String {
440    use snapbox::filter::Filter as _;
441    let content = snapbox::filter::FilterPaths.filter(content.into_data());
442    let content = snapbox::filter::FilterNewlines.filter(content);
443    let content = content.render().expect("came in as a String");
444    let content = redactions.redact(&content);
445    content
446}
447
448/// Normalizes the expected string so that it can be compared against the actual output.
449fn normalize_expected(content: &str, redactions: &snapbox::Redactions) -> String {
450    use snapbox::filter::Filter as _;
451    let content = snapbox::filter::FilterPaths.filter(content.into_data());
452    let content = snapbox::filter::FilterNewlines.filter(content);
453    // Remove any conditionally absent redactions like `[EXE]`
454    let content = content.render().expect("came in as a String");
455    let content = redactions.clear_unused(&content);
456    content.into_owned()
457}
458
459/// A single line string that supports `[..]` wildcard matching.
460struct WildStr<'a> {
461    has_meta: bool,
462    line: &'a str,
463}
464
465impl<'a> WildStr<'a> {
466    fn new(line: &'a str) -> WildStr<'a> {
467        WildStr {
468            has_meta: line.contains("[..]"),
469            line,
470        }
471    }
472}
473
474impl PartialEq<&str> for WildStr<'_> {
475    fn eq(&self, other: &&str) -> bool {
476        if self.has_meta {
477            meta_cmp(self.line, other)
478        } else {
479            self.line == *other
480        }
481    }
482}
483
484fn meta_cmp(a: &str, mut b: &str) -> bool {
485    for (i, part) in a.split("[..]").enumerate() {
486        match b.find(part) {
487            Some(j) => {
488                if i == 0 && j != 0 {
489                    return false;
490                }
491                b = &b[j + part.len()..];
492            }
493            None => return false,
494        }
495    }
496    b.is_empty() || a.ends_with("[..]")
497}
498
499impl fmt::Display for WildStr<'_> {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        f.write_str(&self.line)
502    }
503}
504
505impl fmt::Debug for WildStr<'_> {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        write!(f, "{:?}", self.line)
508    }
509}
510
511pub struct InMemoryDir {
512    files: Vec<(PathBuf, Data)>,
513}
514
515impl InMemoryDir {
516    pub fn paths(&self) -> impl Iterator<Item = &Path> {
517        self.files.iter().map(|(p, _)| p.as_path())
518    }
519
520    #[track_caller]
521    pub fn assert_contains(&self, expected: &Self) {
522        use std::fmt::Write as _;
523        let assert = assert_e2e();
524        let mut errs = String::new();
525        for (path, expected_data) in &expected.files {
526            let actual_data = self
527                .files
528                .iter()
529                .find_map(|(p, d)| (path == p).then(|| d.clone()))
530                .unwrap_or_else(|| Data::new());
531            if let Err(err) =
532                assert.try_eq(Some(&path.display()), actual_data, expected_data.clone())
533            {
534                let _ = write!(&mut errs, "{err}");
535            }
536        }
537        if !errs.is_empty() {
538            panic!("{errs}")
539        }
540    }
541}
542
543impl<P, D> FromIterator<(P, D)> for InMemoryDir
544where
545    P: Into<std::path::PathBuf>,
546    D: IntoData,
547{
548    fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self {
549        let files = files
550            .into_iter()
551            .map(|(p, d)| (p.into(), d.into_data()))
552            .collect();
553        Self { files }
554    }
555}
556
557impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir
558where
559    P: Into<PathBuf>,
560    D: IntoData,
561{
562    fn from(files: [(P, D); N]) -> Self {
563        let files = files
564            .into_iter()
565            .map(|(p, d)| (p.into(), d.into_data()))
566            .collect();
567        Self { files }
568    }
569}
570
571impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir
572where
573    P: Into<PathBuf>,
574    D: IntoData,
575{
576    fn from(files: std::collections::HashMap<P, D>) -> Self {
577        let files = files
578            .into_iter()
579            .map(|(p, d)| (p.into(), d.into_data()))
580            .collect();
581        Self { files }
582    }
583}
584
585impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir
586where
587    P: Into<PathBuf>,
588    D: IntoData,
589{
590    fn from(files: std::collections::BTreeMap<P, D>) -> Self {
591        let files = files
592            .into_iter()
593            .map(|(p, d)| (p.into(), d.into_data()))
594            .collect();
595        Self { files }
596    }
597}
598
599impl From<()> for InMemoryDir {
600    fn from(_files: ()) -> Self {
601        let files = Vec::new();
602        Self { files }
603    }
604}
605
606/// Create an `impl _ for InMemoryDir` for a generic tuple
607///
608/// Must pass in names for each tuple parameter for
609/// - internal variable name
610/// - `Path` type
611/// - `Data` type
612macro_rules! impl_from_tuple_for_inmemorydir {
613    ($($var:ident $path:ident $data:ident),+) => {
614        impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir {
615            fn from(files: ($(($path, $data)),+,)) -> Self {
616                let ($($var),+ ,) = files;
617                let files = [$(($var.0.into(), $var.1.into_data())),+];
618                files.into()
619            }
620        }
621    };
622}
623
624/// Extend `impl_from_tuple_for_inmemorydir` to generate for the specified tuple and all smaller
625/// tuples
626macro_rules! impl_from_tuples_for_inmemorydir {
627    ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => {
628        impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+);
629    };
630    (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => {
631        impl_from_tuple_for_inmemorydir!($($var $path $data),+);
632        impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*);
633    };
634    (__impl $($var:ident $path:ident $data:ident),+;) => {
635        impl_from_tuple_for_inmemorydir!($($var $path $data),+);
636    }
637}
638
639// Generate for tuples of size `1..=7`
640impl_from_tuples_for_inmemorydir!(
641    s1 P1 D1,
642    s2 P2 D2,
643    s3 P3 D3,
644    s4 P4 D4,
645    s5 P5 D5,
646    s6 P6 D6,
647    s7 P7 D7
648);
649
650#[cfg(test)]
651mod test {
652    use snapbox::assert_data_eq;
653    use snapbox::prelude::*;
654    use snapbox::str;
655
656    use super::*;
657
658    #[test]
659    fn wild_str_cmp() {
660        for (a, b) in &[
661            ("a b", "a b"),
662            ("a[..]b", "a b"),
663            ("a[..]", "a b"),
664            ("[..]", "a b"),
665            ("[..]b", "a b"),
666        ] {
667            assert_eq!(WildStr::new(a), b);
668        }
669        for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
670            assert_ne!(WildStr::new(a), b);
671        }
672    }
673
674    #[test]
675    fn redact_elapsed_time() {
676        let mut subs = snapbox::Redactions::new();
677        add_regex_redactions(&mut subs);
678
679        assert_data_eq!(
680            subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"),
681            str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
682        );
683        assert_data_eq!(
684            subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"),
685            str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
686        );
687    }
688}