#
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::{Result, bail};
    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    // Match multi-part hashes like `06/b451d0d6f88b1d` used in directory paths
    233    subs.insert("[HASH]", regex!(r"/(?<redacted>[a-f0-9]{2}\/[0-9a-f]{14})"))
    234        .unwrap();
    235    // Match file name hashes like `foo-06b451d0d6f88b1d`
    236    subs.insert("[HASH]", regex!(r"[a-z0-9]+-(?<redacted>[a-f0-9]{16})"))
    237        .unwrap();
    238    // Match path hashes like `../06b451d0d6f88b1d/..` used in directory paths
    239    subs.insert("[HASH]", regex!(r"\/(?<redacted>[0-9a-f]{16})\/"))
    240        .unwrap();
    241    subs.insert(
    242        "[AVG_ELAPSED]",
    243        regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?) ns/iter"),
    244    )
    245    .unwrap();
    246    subs.insert(
    247        "[JITTER]",
    248        regex!(r"ns/iter \(\+/- (?<redacted>[0-9]+(\.[0-9]+)?)\)"),
    249    )
    250    .unwrap();
    251
    252    // Following 3 subs redact:
    253    //   "1719325877.527949100s, 61549498ns after last build at 1719325877.466399602s"
    254    //   "1719503592.218193216s, 1h 1s after last build at 1719499991.982681034s"
    255    // into "[DIRTY_REASON_NEW_TIME], [DIRTY_REASON_DIFF] after last build at [DIRTY_REASON_OLD_TIME]"
    256    subs.insert(
    257        "[TIME_DIFF_AFTER_LAST_BUILD]",
    258        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)"),
    259       )
    260       .unwrap();
    261}
    262
    263static MIN_LITERAL_REDACTIONS: &[(&str, &str)] = &[
    264    ("[EXE]", std::env::consts::EXE_SUFFIX),
    265    ("[BROKEN_PIPE]", "Broken pipe (os error 32)"),
    266    ("[BROKEN_PIPE]", "The pipe is being closed. (os error 232)"),
    267    // Unix message for an entity was not found
    268    ("[NOT_FOUND]", "No such file or directory (os error 2)"),
    269    // Windows message for an entity was not found
    270    (
    271        "[NOT_FOUND]",
    272        "The system cannot find the file specified. (os error 2)",
    273    ),
    274    (
    275        "[NOT_FOUND]",
    276        "The system cannot find the path specified. (os error 3)",
    277    ),
    278    ("[NOT_FOUND]", "Access is denied. (os error 5)"),
    279    ("[NOT_FOUND]", "program not found"),
    280    // Unix message for exit status
    281    ("[EXIT_STATUS]", "exit status"),
    282    // Windows message for exit status
    283    ("[EXIT_STATUS]", "exit code"),
    284];
    285static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[
    286    ("[RUNNING]", "     Running"),
    287    ("[COMPILING]", "   Compiling"),
    288    ("[CHECKING]", "    Checking"),
    289    ("[COMPLETED]", "   Completed"),
    290    ("[CREATED]", "     Created"),
    291    ("[CREATING]", "    Creating"),
    292    ("[CREDENTIAL]", "  Credential"),
    293    ("[DOWNGRADING]", " Downgrading"),
    294    ("[FINISHED]", "    Finished"),
    295    ("[ERROR]", "error:"),
    296    ("[WARNING]", "warning:"),
    297    ("[NOTE]", "note:"),
    298    ("[HELP]", "help:"),
    299    ("[DOCUMENTING]", " Documenting"),
    300    ("[SCRAPING]", "    Scraping"),
    301    ("[FRESH]", "       Fresh"),
    302    ("[DIRTY]", "       Dirty"),
    303    ("[LOCKING]", "     Locking"),
    304    ("[UPDATING]", "    Updating"),
    305    ("[UPGRADING]", "   Upgrading"),
    306    ("[ADDING]", "      Adding"),
    307    ("[REMOVING]", "    Removing"),
    308    ("[REMOVED]", "     Removed"),
    309    ("[UNCHANGED]", "   Unchanged"),
    310    ("[DOCTEST]", "   Doc-tests"),
    311    ("[PACKAGING]", "   Packaging"),
    312    ("[PACKAGED]", "    Packaged"),
    313    ("[DOWNLOADING]", " Downloading"),
    314    ("[DOWNLOADED]", "  Downloaded"),
    315    ("[UPLOADING]", "   Uploading"),
    316    ("[UPLOADED]", "    Uploaded"),
    317    ("[VERIFYING]", "   Verifying"),
    318    ("[ARCHIVING]", "   Archiving"),
    319    ("[INSTALLING]", "  Installing"),
    320    ("[REPLACING]", "   Replacing"),
    321    ("[UNPACKING]", "   Unpacking"),
    322    ("[SUMMARY]", "     Summary"),
    323    ("[FIXED]", "       Fixed"),
    324    ("[FIXING]", "      Fixing"),
    325    ("[IGNORED]", "     Ignored"),
    326    ("[INSTALLED]", "   Installed"),
    327    ("[REPLACED]", "    Replaced"),
    328    ("[BUILDING]", "    Building"),
    329    ("[LOGIN]", "       Login"),
    330    ("[LOGOUT]", "      Logout"),
    331    ("[YANK]", "        Yank"),
    332    ("[OWNER]", "       Owner"),
    333    ("[MIGRATING]", "   Migrating"),
    334    ("[EXECUTABLE]", "  Executable"),
    335    ("[SKIPPING]", "    Skipping"),
    336    ("[WAITING]", "     Waiting"),
    337    ("[PUBLISHED]", "   Published"),
    338    ("[BLOCKING]", "    Blocking"),
    339    ("[GENERATED]", "   Generated"),
    340    ("[OPENING]", "     Opening"),
    341    ("[MERGING]", "     Merging"),
    342];
    343
    344/// Checks that the given string contains the given contiguous lines
    345/// somewhere.
    346///
    347/// See [Patterns](index.html#patterns) for more information on pattern matching.
    348pub(crate) fn match_contains(
    349    expected: &str,
    350    actual: &str,
    351    redactions: &snapbox::Redactions,
    352) -> Result<()> {
    353    let expected = normalize_expected(expected, redactions);
    354    let actual = normalize_actual(actual, redactions);
    355    let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
    356    let a: Vec<_> = actual.lines().collect();
    357    if e.len() == 0 {
    358        bail!("expected length must not be zero");
    359    }
    360    for window in a.windows(e.len()) {
    361        if e == window {
    362            return Ok(());
    363        }
    364    }
    365    bail!(
    366        "expected to find:\n\
    367         {}\n\n\
    368         did not find in output:\n\
    369         {}",
    370        expected,
    371        actual
    372    );
    373}
    374
    375/// Checks that the given string does not contain the given contiguous lines
    376/// anywhere.
    377///
    378/// See [Patterns](index.html#patterns) for more information on pattern matching.
    379pub(crate) fn match_does_not_contain(
    380    expected: &str,
    381    actual: &str,
    382    redactions: &snapbox::Redactions,
    383) -> Result<()> {
    384    if match_contains(expected, actual, redactions).is_ok() {
    385        bail!(
    386            "expected not to find:\n\
    387             {}\n\n\
    388             but found in output:\n\
    389             {}",
    390            expected,
    391            actual
    392        );
    393    } else {
    394        Ok(())
    395    }
    396}
    397
    398/// Checks that the given string has a line that contains the given patterns,
    399/// and that line also does not contain the `without` patterns.
    400///
    401/// See [Patterns](index.html#patterns) for more information on pattern matching.
    402///
    403/// See [`crate::Execs::with_stderr_line_without`] for an example and cautions
    404/// against using.
    405pub(crate) fn match_with_without(
    406    actual: &str,
    407    with: &[String],
    408    without: &[String],
    409    redactions: &snapbox::Redactions,
    410) -> Result<()> {
    411    let actual = normalize_actual(actual, redactions);
    412    let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, redactions));
    413    let with: Vec<_> = with.iter().map(norm).collect();
    414    let without: Vec<_> = without.iter().map(norm).collect();
    415    let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
    416    let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
    417
    418    let matches: Vec<_> = actual
    419        .lines()
    420        .filter(|line| with_wild.iter().all(|with| with == line))
    421        .filter(|line| !without_wild.iter().any(|without| without == line))
    422        .collect();
    423    match matches.len() {
    424        0 => bail!(
    425            "Could not find expected line in output.\n\
    426             With contents: {:?}\n\
    427             Without contents: {:?}\n\
    428             Actual stderr:\n\
    429             {}\n",
    430            with,
    431            without,
    432            actual
    433        ),
    434        1 => Ok(()),
    435        _ => bail!(
    436            "Found multiple matching lines, but only expected one.\n\
    437             With contents: {:?}\n\
    438             Without contents: {:?}\n\
    439             Matching lines:\n\
    440             {}\n",
    441            with,
    442            without,
    443            itertools::join(matches, "\n")
    444        ),
    445    }
    446}
    447
    448/// Normalizes the output so that it can be compared against the expected value.
    449fn normalize_actual(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    let content = content.render().expect("came in as a String");
    454    let content = redactions.redact(&content);
    455    content
    456}
    457
    458/// Normalizes the expected string so that it can be compared against the actual output.
    459fn normalize_expected(content: &str, redactions: &snapbox::Redactions) -> String {
    460    use snapbox::filter::Filter as _;
    461    let content = snapbox::filter::FilterPaths.filter(content.into_data());
    462    let content = snapbox::filter::FilterNewlines.filter(content);
    463    // Remove any conditionally absent redactions like `[EXE]`
    464    let content = content.render().expect("came in as a String");
    465    let content = redactions.clear_unused(&content);
    466    content.into_owned()
    467}
    468
    469/// A single line string that supports `[..]` wildcard matching.
    470struct WildStr<'a> {
    471    has_meta: bool,
    472    line: &'a str,
    473}
    474
    475impl<'a> WildStr<'a> {
    476    fn new(line: &'a str) -> WildStr<'a> {
    477        WildStr {
    478            has_meta: line.contains("[..]"),
    479            line,
    480        }
    481    }
    482}
    483
    484impl PartialEq<&str> for WildStr<'_> {
    485    fn eq(&self, other: &&str) -> bool {
    486        if self.has_meta {
    487            meta_cmp(self.line, other)
    488        } else {
    489            self.line == *other
    490        }
    491    }
    492}
    493
    494fn meta_cmp(a: &str, mut b: &str) -> bool {
    495    for (i, part) in a.split("[..]").enumerate() {
    496        match b.find(part) {
    497            Some(j) => {
    498                if i == 0 && j != 0 {
    499                    return false;
    500                }
    501                b = &b[j + part.len()..];
    502            }
    503            None => return false,
    504        }
    505    }
    506    b.is_empty() || a.ends_with("[..]")
    507}
    508
    509impl fmt::Display for WildStr<'_> {
    510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    511        f.write_str(&self.line)
    512    }
    513}
    514
    515impl fmt::Debug for WildStr<'_> {
    516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    517        write!(f, "{:?}", self.line)
    518    }
    519}
    520
    521pub struct InMemoryDir {
    522    files: Vec<(PathBuf, Data)>,
    523}
    524
    525impl InMemoryDir {
    526    pub fn paths(&self) -> impl Iterator<Item = &Path> {
    527        self.files.iter().map(|(p, _)| p.as_path())
    528    }
    529
    530    #[track_caller]
    531    pub fn assert_contains(&self, expected: &Self) {
    532        use std::fmt::Write as _;
    533        let assert = assert_e2e();
    534        let mut errs = String::new();
    535        for (path, expected_data) in &expected.files {
    536            let actual_data = self
    537                .files
    538                .iter()
    539                .find_map(|(p, d)| (path == p).then(|| d.clone()))
    540                .unwrap_or_else(|| Data::new());
    541            if let Err(err) =
    542                assert.try_eq(Some(&path.display()), actual_data, expected_data.clone())
    543            {
    544                let _ = write!(&mut errs, "{err}");
    545            }
    546        }
    547        if !errs.is_empty() {
    548            panic!("{errs}")
    549        }
    550    }
    551}
    552
    553impl<P, D> FromIterator<(P, D)> for InMemoryDir
    554where
    555    P: Into<std::path::PathBuf>,
    556    D: IntoData,
    557{
    558    fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self {
    559        let files = files
    560            .into_iter()
    561            .map(|(p, d)| (p.into(), d.into_data()))
    562            .collect();
    563        Self { files }
    564    }
    565}
    566
    567impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir
    568where
    569    P: Into<PathBuf>,
    570    D: IntoData,
    571{
    572    fn from(files: [(P, D); N]) -> Self {
    573        let files = files
    574            .into_iter()
    575            .map(|(p, d)| (p.into(), d.into_data()))
    576            .collect();
    577        Self { files }
    578    }
    579}
    580
    581impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir
    582where
    583    P: Into<PathBuf>,
    584    D: IntoData,
    585{
    586    fn from(files: std::collections::HashMap<P, D>) -> Self {
    587        let files = files
    588            .into_iter()
    589            .map(|(p, d)| (p.into(), d.into_data()))
    590            .collect();
    591        Self { files }
    592    }
    593}
    594
    595impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir
    596where
    597    P: Into<PathBuf>,
    598    D: IntoData,
    599{
    600    fn from(files: std::collections::BTreeMap<P, D>) -> Self {
    601        let files = files
    602            .into_iter()
    603            .map(|(p, d)| (p.into(), d.into_data()))
    604            .collect();
    605        Self { files }
    606    }
    607}
    608
    609impl From<()> for InMemoryDir {
    610    fn from(_files: ()) -> Self {
    611        let files = Vec::new();
    612        Self { files }
    613    }
    614}
    615
    616/// Create an `impl _ for InMemoryDir` for a generic tuple
    617///
    618/// Must pass in names for each tuple parameter for
    619/// - internal variable name
    620/// - `Path` type
    621/// - `Data` type
    622macro_rules! impl_from_tuple_for_inmemorydir {
    623    ($($var:ident $path:ident $data:ident),+) => {
    624        impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir {
    625            fn from(files: ($(($path, $data)),+,)) -> Self {
    626                let ($($var),+ ,) = files;
    627                let files = [$(($var.0.into(), $var.1.into_data())),+];
    628                files.into()
    629            }
    630        }
    631    };
    632}
    633
    634/// Extend `impl_from_tuple_for_inmemorydir` to generate for the specified tuple and all smaller
    635/// tuples
    636macro_rules! impl_from_tuples_for_inmemorydir {
    637    ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => {
    638        impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+);
    639    };
    640    (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => {
    641        impl_from_tuple_for_inmemorydir!($($var $path $data),+);
    642        impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*);
    643    };
    644    (__impl $($var:ident $path:ident $data:ident),+;) => {
    645        impl_from_tuple_for_inmemorydir!($($var $path $data),+);
    646    }
    647}
    648
    649// Generate for tuples of size `1..=7`
    650impl_from_tuples_for_inmemorydir!(
    651    s1 P1 D1,
    652    s2 P2 D2,
    653    s3 P3 D3,
    654    s4 P4 D4,
    655    s5 P5 D5,
    656    s6 P6 D6,
    657    s7 P7 D7
    658);
    659
    660#[cfg(test)]
    661mod test {
    662    use snapbox::assert_data_eq;
    663    use snapbox::prelude::*;
    664    use snapbox::str;
    665
    666    use super::*;
    667
    668    #[test]
    669    fn wild_str_cmp() {
    670        for (a, b) in &[
    671            ("a b", "a b"),
    672            ("a[..]b", "a b"),
    673            ("a[..]", "a b"),
    674            ("[..]", "a b"),
    675            ("[..]b", "a b"),
    676        ] {
    677            assert_eq!(WildStr::new(a), b);
    678        }
    679        for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
    680            assert_ne!(WildStr::new(a), b);
    681        }
    682    }
    683
    684    #[test]
    685    fn redact_elapsed_time() {
    686        let mut subs = snapbox::Redactions::new();
    687        add_regex_redactions(&mut subs);
    688
    689        assert_data_eq!(
    690            subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"),
    691            str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
    692        );
    693        assert_data_eq!(
    694            subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"),
    695            str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw()
    696        );
    697    }
    698}