| //! Routines for comparing and diffing output. |
| //! |
| //! # Deprecated comparisons |
| //! |
| //! Cargo's tests are in transition from internal-only pattern and normalization routines used in |
| //! asserts like [`crate::Execs::with_stdout_contains`] to [`assert_e2e`] and [`assert_ui`]. |
| //! |
| //! ## Patterns |
| //! |
| //! Many of these functions support special markup to assist with comparing |
| //! text that may vary or is otherwise uninteresting for the test at hand. The |
| //! supported patterns are: |
| //! |
| //! - `[..]` is a wildcard that matches 0 or more characters on the same line |
| //! (similar to `.*` in a regex). It is non-greedy. |
| //! - `[EXE]` optionally adds `.exe` on Windows (empty string on other |
| //! platforms). |
| //! - `[ROOT]` is the path to the test directory's root. |
| //! - `[CWD]` is the working directory of the process that was run. |
| //! - There is a wide range of substitutions (such as `[COMPILING]` or |
| //! `[WARNING]`) to match cargo's "status" output and allows you to ignore |
| //! the alignment. See the source of `substitute_macros` for a complete list |
| //! of substitutions. |
| //! - `[DIRTY-MSVC]` (only when the line starts with it) would be replaced by |
| //! `[DIRTY]` when `cfg(target_env = "msvc")` or the line will be ignored otherwise. |
| //! Tests that work around [issue 7358](https://github.com/rust-lang/cargo/issues/7358) |
| //! can use this to avoid duplicating the `with_stderr` call like: |
| //! `if cfg!(target_env = "msvc") {e.with_stderr("...[DIRTY]...");} else {e.with_stderr("...");}`. |
| //! |
| //! ## Normalization |
| //! |
| //! In addition to the patterns described above, the strings are normalized |
| //! in such a way to avoid unwanted differences. The normalizations are: |
| //! |
| //! - Raw tab characters are converted to the string `<tab>`. This is helpful |
| //! so that raw tabs do not need to be written in the expected string, and |
| //! to avoid confusion of tabs vs spaces. |
| //! - Backslashes are converted to forward slashes to deal with Windows paths. |
| //! This helps so that all tests can be written assuming forward slashes. |
| //! Other heuristics are applied to try to ensure Windows-style paths aren't |
| //! a problem. |
| //! - Carriage returns are removed, which can help when running on Windows. |
| |
| use crate::cross_compile::try_alternate; |
| use crate::paths; |
| use crate::rustc_host; |
| use anyhow::{bail, Result}; |
| use snapbox::Data; |
| use snapbox::IntoData; |
| use std::fmt; |
| use std::path::Path; |
| use std::path::PathBuf; |
| use std::str; |
| use url::Url; |
| |
| /// This makes it easier to write regex replacements that are guaranteed to only |
| /// get compiled once |
| macro_rules! regex { |
| ($re:literal $(,)?) => {{ |
| static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new(); |
| RE.get_or_init(|| regex::Regex::new($re).unwrap()) |
| }}; |
| } |
| |
| /// Assertion policy for UI tests |
| /// |
| /// This emphasizes showing as much content as possible at the cost of more brittleness |
| /// |
| /// # Snapshots |
| /// |
| /// Updating of snapshots is controlled with the `SNAPSHOTS` environment variable: |
| /// |
| /// - `skip`: do not run the tests |
| /// - `ignore`: run the tests but ignore their failure |
| /// - `verify`: run the tests |
| /// - `overwrite`: update the snapshots based on the output of the tests |
| /// |
| /// # Patterns |
| /// |
| /// - `[..]` is a character wildcard, stopping at line breaks |
| /// - `\n...\n` is a multi-line wildcard |
| /// - `[EXE]` matches the exe suffix for the current platform |
| /// - `[ROOT]` matches [`paths::root()`][crate::paths::root] |
| /// - `[ROOTURL]` matches [`paths::root()`][crate::paths::root] as a URL |
| /// |
| /// # Normalization |
| /// |
| /// In addition to the patterns described above, text is normalized |
| /// in such a way to avoid unwanted differences. The normalizations are: |
| /// |
| /// - Backslashes are converted to forward slashes to deal with Windows paths. |
| /// This helps so that all tests can be written assuming forward slashes. |
| /// Other heuristics are applied to try to ensure Windows-style paths aren't |
| /// a problem. |
| /// - Carriage returns are removed, which can help when running on Windows. |
| /// |
| /// # Example |
| /// |
| /// ```no_run |
| /// # use cargo_test_support::compare::assert_e2e; |
| /// # use cargo_test_support::file; |
| /// # let p = cargo_test_support::project().build(); |
| /// # let stdout = ""; |
| /// assert_e2e().eq(stdout, file!["stderr.term.svg"]); |
| /// ``` |
| /// ```console |
| /// $ SNAPSHOTS=overwrite cargo test |
| /// ``` |
| pub fn assert_ui() -> snapbox::Assert { |
| let mut subs = snapbox::Redactions::new(); |
| subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned()) |
| .unwrap(); |
| add_test_support_redactions(&mut subs); |
| add_regex_redactions(&mut subs); |
| |
| snapbox::Assert::new() |
| .action_env(snapbox::assert::DEFAULT_ACTION_ENV) |
| .redact_with(subs) |
| } |
| |
| /// Assertion policy for functional end-to-end tests |
| /// |
| /// This emphasizes showing as much content as possible at the cost of more brittleness |
| /// |
| /// # Snapshots |
| /// |
| /// Updating of snapshots is controlled with the `SNAPSHOTS` environment variable: |
| /// |
| /// - `skip`: do not run the tests |
| /// - `ignore`: run the tests but ignore their failure |
| /// - `verify`: run the tests |
| /// - `overwrite`: update the snapshots based on the output of the tests |
| /// |
| /// # Patterns |
| /// |
| /// - `[..]` is a character wildcard, stopping at line breaks |
| /// - `\n...\n` is a multi-line wildcard |
| /// - `[EXE]` matches the exe suffix for the current platform |
| /// - `[ROOT]` matches [`paths::root()`][crate::paths::root] |
| /// - `[ROOTURL]` matches [`paths::root()`][crate::paths::root] as a URL |
| /// |
| /// # Normalization |
| /// |
| /// In addition to the patterns described above, text is normalized |
| /// in such a way to avoid unwanted differences. The normalizations are: |
| /// |
| /// - Backslashes are converted to forward slashes to deal with Windows paths. |
| /// This helps so that all tests can be written assuming forward slashes. |
| /// Other heuristics are applied to try to ensure Windows-style paths aren't |
| /// a problem. |
| /// - Carriage returns are removed, which can help when running on Windows. |
| /// |
| /// # Example |
| /// |
| /// ```no_run |
| /// # use cargo_test_support::compare::assert_e2e; |
| /// # use cargo_test_support::str; |
| /// # let p = cargo_test_support::project().build(); |
| /// assert_e2e().eq(p.read_lockfile(), str![]); |
| /// ``` |
| /// ```console |
| /// $ SNAPSHOTS=overwrite cargo test |
| /// ``` |
| pub fn assert_e2e() -> snapbox::Assert { |
| let mut subs = snapbox::Redactions::new(); |
| subs.extend(MIN_LITERAL_REDACTIONS.into_iter().cloned()) |
| .unwrap(); |
| subs.extend(E2E_LITERAL_REDACTIONS.into_iter().cloned()) |
| .unwrap(); |
| add_test_support_redactions(&mut subs); |
| add_regex_redactions(&mut subs); |
| |
| snapbox::Assert::new() |
| .action_env(snapbox::assert::DEFAULT_ACTION_ENV) |
| .redact_with(subs) |
| } |
| |
| fn add_test_support_redactions(subs: &mut snapbox::Redactions) { |
| let root = paths::root(); |
| // Use `from_file_path` instead of `from_dir_path` so the trailing slash is |
| // put in the users output, rather than hidden in the variable |
| let root_url = url::Url::from_file_path(&root).unwrap().to_string(); |
| |
| subs.insert("[ROOT]", root).unwrap(); |
| subs.insert("[ROOTURL]", root_url).unwrap(); |
| subs.insert("[HOST_TARGET]", rustc_host()).unwrap(); |
| if let Some(alt_target) = try_alternate() { |
| subs.insert("[ALT_TARGET]", alt_target).unwrap(); |
| } |
| } |
| |
| fn add_regex_redactions(subs: &mut snapbox::Redactions) { |
| // For e2e tests |
| subs.insert( |
| "[ELAPSED]", |
| regex!(r"\[FINISHED\].*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"), |
| ) |
| .unwrap(); |
| // for UI tests |
| subs.insert( |
| "[ELAPSED]", |
| regex!(r"Finished.*in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"), |
| ) |
| .unwrap(); |
| // output from libtest |
| subs.insert( |
| "[ELAPSED]", |
| regex!(r"; finished in (?<redacted>[0-9]+(\.[0-9]+)?(m [0-9]+)?)s"), |
| ) |
| .unwrap(); |
| subs.insert( |
| "[FILE_NUM]", |
| regex!(r"\[(REMOVED|SUMMARY)\] (?<redacted>[1-9][0-9]*) files"), |
| ) |
| .unwrap(); |
| subs.insert( |
| "[FILE_SIZE]", |
| regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?([a-zA-Z]i)?)B\s"), |
| ) |
| .unwrap(); |
| subs.insert( |
| "[HASH]", |
| regex!(r"home/\.cargo/registry/(cache|index|src)/-(?<redacted>[a-z0-9]+)"), |
| ) |
| .unwrap(); |
| subs.insert( |
| "[HASH]", |
| regex!(r"\.cargo/target/(?<redacted>[0-9a-f]{2}/[0-9a-f]{14})"), |
| ) |
| .unwrap(); |
| subs.insert("[HASH]", regex!(r"/[a-z0-9\-_]+-(?<redacted>[0-9a-f]{16})")) |
| .unwrap(); |
| subs.insert( |
| "[AVG_ELAPSED]", |
| regex!(r"(?<redacted>[0-9]+(\.[0-9]+)?) ns/iter"), |
| ) |
| .unwrap(); |
| subs.insert( |
| "[JITTER]", |
| regex!(r"ns/iter \(\+/- (?<redacted>[0-9]+(\.[0-9]+)?)\)"), |
| ) |
| .unwrap(); |
| |
| // Following 3 subs redact: |
| // "1719325877.527949100s, 61549498ns after last build at 1719325877.466399602s" |
| // "1719503592.218193216s, 1h 1s after last build at 1719499991.982681034s" |
| // into "[DIRTY_REASON_NEW_TIME], [DIRTY_REASON_DIFF] after last build at [DIRTY_REASON_OLD_TIME]" |
| subs.insert( |
| "[TIME_DIFF_AFTER_LAST_BUILD]", |
| 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)"), |
| ) |
| .unwrap(); |
| } |
| |
| static MIN_LITERAL_REDACTIONS: &[(&str, &str)] = &[ |
| ("[EXE]", std::env::consts::EXE_SUFFIX), |
| ("[BROKEN_PIPE]", "Broken pipe (os error 32)"), |
| ("[BROKEN_PIPE]", "The pipe is being closed. (os error 232)"), |
| // Unix message for an entity was not found |
| ("[NOT_FOUND]", "No such file or directory (os error 2)"), |
| // Windows message for an entity was not found |
| ( |
| "[NOT_FOUND]", |
| "The system cannot find the file specified. (os error 2)", |
| ), |
| ( |
| "[NOT_FOUND]", |
| "The system cannot find the path specified. (os error 3)", |
| ), |
| ("[NOT_FOUND]", "Access is denied. (os error 5)"), |
| ("[NOT_FOUND]", "program not found"), |
| // Unix message for exit status |
| ("[EXIT_STATUS]", "exit status"), |
| // Windows message for exit status |
| ("[EXIT_STATUS]", "exit code"), |
| ]; |
| static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[ |
| ("[RUNNING]", " Running"), |
| ("[COMPILING]", " Compiling"), |
| ("[CHECKING]", " Checking"), |
| ("[COMPLETED]", " Completed"), |
| ("[CREATED]", " Created"), |
| ("[CREATING]", " Creating"), |
| ("[CREDENTIAL]", " Credential"), |
| ("[DOWNGRADING]", " Downgrading"), |
| ("[FINISHED]", " Finished"), |
| ("[ERROR]", "error:"), |
| ("[WARNING]", "warning:"), |
| ("[NOTE]", "note:"), |
| ("[HELP]", "help:"), |
| ("[DOCUMENTING]", " Documenting"), |
| ("[SCRAPING]", " Scraping"), |
| ("[FRESH]", " Fresh"), |
| ("[DIRTY]", " Dirty"), |
| ("[LOCKING]", " Locking"), |
| ("[UPDATING]", " Updating"), |
| ("[UPGRADING]", " Upgrading"), |
| ("[ADDING]", " Adding"), |
| ("[REMOVING]", " Removing"), |
| ("[REMOVED]", " Removed"), |
| ("[UNCHANGED]", " Unchanged"), |
| ("[DOCTEST]", " Doc-tests"), |
| ("[PACKAGING]", " Packaging"), |
| ("[PACKAGED]", " Packaged"), |
| ("[DOWNLOADING]", " Downloading"), |
| ("[DOWNLOADED]", " Downloaded"), |
| ("[UPLOADING]", " Uploading"), |
| ("[UPLOADED]", " Uploaded"), |
| ("[VERIFYING]", " Verifying"), |
| ("[ARCHIVING]", " Archiving"), |
| ("[INSTALLING]", " Installing"), |
| ("[REPLACING]", " Replacing"), |
| ("[UNPACKING]", " Unpacking"), |
| ("[SUMMARY]", " Summary"), |
| ("[FIXED]", " Fixed"), |
| ("[FIXING]", " Fixing"), |
| ("[IGNORED]", " Ignored"), |
| ("[INSTALLED]", " Installed"), |
| ("[REPLACED]", " Replaced"), |
| ("[BUILDING]", " Building"), |
| ("[LOGIN]", " Login"), |
| ("[LOGOUT]", " Logout"), |
| ("[YANK]", " Yank"), |
| ("[OWNER]", " Owner"), |
| ("[MIGRATING]", " Migrating"), |
| ("[EXECUTABLE]", " Executable"), |
| ("[SKIPPING]", " Skipping"), |
| ("[WAITING]", " Waiting"), |
| ("[PUBLISHED]", " Published"), |
| ("[BLOCKING]", " Blocking"), |
| ("[GENERATED]", " Generated"), |
| ("[OPENING]", " Opening"), |
| ]; |
| |
| /// Normalizes the output so that it can be compared against the expected value. |
| fn normalize_actual(actual: &str, cwd: Option<&Path>) -> String { |
| // It's easier to read tabs in outputs if they don't show up as literal |
| // hidden characters |
| let actual = actual.replace('\t', "<tab>"); |
| if cfg!(windows) { |
| // Let's not deal with \r\n vs \n on windows... |
| let actual = actual.replace('\r', ""); |
| normalize_windows(&actual, cwd) |
| } else { |
| actual |
| } |
| } |
| |
| /// Normalizes the expected string so that it can be compared against the actual output. |
| fn normalize_expected(expected: &str, cwd: Option<&Path>) -> String { |
| let expected = replace_dirty_msvc(expected); |
| let expected = substitute_macros(&expected); |
| |
| if cfg!(windows) { |
| normalize_windows(&expected, cwd) |
| } else { |
| let expected = match cwd { |
| None => expected, |
| Some(cwd) => expected.replace("[CWD]", &cwd.display().to_string()), |
| }; |
| let expected = expected.replace("[ROOT]", &paths::root().display().to_string()); |
| expected |
| } |
| } |
| |
| fn replace_dirty_msvc_impl(s: &str, is_msvc: bool) -> String { |
| if is_msvc { |
| s.replace("[DIRTY-MSVC]", "[DIRTY]") |
| } else { |
| use itertools::Itertools; |
| |
| let mut new = s |
| .lines() |
| .filter(|it| !it.starts_with("[DIRTY-MSVC]")) |
| .join("\n"); |
| |
| if s.ends_with("\n") { |
| new.push_str("\n"); |
| } |
| |
| new |
| } |
| } |
| |
| fn replace_dirty_msvc(s: &str) -> String { |
| replace_dirty_msvc_impl(s, cfg!(target_env = "msvc")) |
| } |
| |
| /// Normalizes text for both actual and expected strings on Windows. |
| fn normalize_windows(text: &str, cwd: Option<&Path>) -> String { |
| // Let's not deal with / vs \ (windows...) |
| let text = text.replace('\\', "/"); |
| |
| // Weirdness for paths on Windows extends beyond `/` vs `\` apparently. |
| // Namely paths like `c:\` and `C:\` are equivalent and that can cause |
| // issues. The return value of `env::current_dir()` may return a |
| // lowercase drive name, but we round-trip a lot of values through `Url` |
| // which will auto-uppercase the drive name. To just ignore this |
| // distinction we try to canonicalize as much as possible, taking all |
| // forms of a path and canonicalizing them to one. |
| let replace_path = |s: &str, path: &Path, with: &str| { |
| let path_through_url = Url::from_file_path(path).unwrap().to_file_path().unwrap(); |
| let path1 = path.display().to_string().replace('\\', "/"); |
| let path2 = path_through_url.display().to_string().replace('\\', "/"); |
| s.replace(&path1, with) |
| .replace(&path2, with) |
| .replace(with, &path1) |
| }; |
| |
| let text = match cwd { |
| None => text, |
| Some(p) => replace_path(&text, p, "[CWD]"), |
| }; |
| |
| // Similar to cwd above, perform similar treatment to the root path |
| // which in theory all of our paths should otherwise get rooted at. |
| let root = paths::root(); |
| let text = replace_path(&text, &root, "[ROOT]"); |
| |
| text |
| } |
| |
| fn substitute_macros(input: &str) -> String { |
| let mut result = input.to_owned(); |
| for &(pat, subst) in MIN_LITERAL_REDACTIONS { |
| result = result.replace(pat, subst) |
| } |
| for &(pat, subst) in E2E_LITERAL_REDACTIONS { |
| result = result.replace(pat, subst) |
| } |
| result |
| } |
| |
| /// Checks that the given string contains the given contiguous lines |
| /// somewhere. |
| /// |
| /// See [Patterns](index.html#patterns) for more information on pattern matching. |
| pub(crate) fn match_contains(expected: &str, actual: &str, cwd: Option<&Path>) -> Result<()> { |
| let expected = normalize_expected(expected, cwd); |
| let actual = normalize_actual(actual, cwd); |
| let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect(); |
| let a: Vec<_> = actual.lines().map(|line| WildStr::new(line)).collect(); |
| if e.len() == 0 { |
| bail!("expected length must not be zero"); |
| } |
| for window in a.windows(e.len()) { |
| if window == e { |
| return Ok(()); |
| } |
| } |
| bail!( |
| "expected to find:\n\ |
| {}\n\n\ |
| did not find in output:\n\ |
| {}", |
| expected, |
| actual |
| ); |
| } |
| |
| /// Checks that the given string does not contain the given contiguous lines |
| /// anywhere. |
| /// |
| /// See [Patterns](index.html#patterns) for more information on pattern matching. |
| pub(crate) fn match_does_not_contain( |
| expected: &str, |
| actual: &str, |
| cwd: Option<&Path>, |
| ) -> Result<()> { |
| if match_contains(expected, actual, cwd).is_ok() { |
| bail!( |
| "expected not to find:\n\ |
| {}\n\n\ |
| but found in output:\n\ |
| {}", |
| expected, |
| actual |
| ); |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Checks that the given string has a line that contains the given patterns, |
| /// and that line also does not contain the `without` patterns. |
| /// |
| /// See [Patterns](index.html#patterns) for more information on pattern matching. |
| /// |
| /// See [`crate::Execs::with_stderr_line_without`] for an example and cautions |
| /// against using. |
| pub(crate) fn match_with_without( |
| actual: &str, |
| with: &[String], |
| without: &[String], |
| cwd: Option<&Path>, |
| ) -> Result<()> { |
| let actual = normalize_actual(actual, cwd); |
| let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, cwd)); |
| let with: Vec<_> = with.iter().map(norm).collect(); |
| let without: Vec<_> = without.iter().map(norm).collect(); |
| let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect(); |
| let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect(); |
| |
| let matches: Vec<_> = actual |
| .lines() |
| .map(WildStr::new) |
| .filter(|line| with_wild.iter().all(|with| with == line)) |
| .filter(|line| !without_wild.iter().any(|without| without == line)) |
| .collect(); |
| match matches.len() { |
| 0 => bail!( |
| "Could not find expected line in output.\n\ |
| With contents: {:?}\n\ |
| Without contents: {:?}\n\ |
| Actual stderr:\n\ |
| {}\n", |
| with, |
| without, |
| actual |
| ), |
| 1 => Ok(()), |
| _ => bail!( |
| "Found multiple matching lines, but only expected one.\n\ |
| With contents: {:?}\n\ |
| Without contents: {:?}\n\ |
| Matching lines:\n\ |
| {}\n", |
| with, |
| without, |
| itertools::join(matches, "\n") |
| ), |
| } |
| } |
| |
| /// A single line string that supports `[..]` wildcard matching. |
| pub(crate) struct WildStr<'a> { |
| has_meta: bool, |
| line: &'a str, |
| } |
| |
| impl<'a> WildStr<'a> { |
| pub fn new(line: &'a str) -> WildStr<'a> { |
| WildStr { |
| has_meta: line.contains("[..]"), |
| line, |
| } |
| } |
| } |
| |
| impl<'a> PartialEq for WildStr<'a> { |
| fn eq(&self, other: &Self) -> bool { |
| match (self.has_meta, other.has_meta) { |
| (false, false) => self.line == other.line, |
| (true, false) => meta_cmp(self.line, other.line), |
| (false, true) => meta_cmp(other.line, self.line), |
| (true, true) => panic!("both lines cannot have [..]"), |
| } |
| } |
| } |
| |
| fn meta_cmp(a: &str, mut b: &str) -> bool { |
| for (i, part) in a.split("[..]").enumerate() { |
| match b.find(part) { |
| Some(j) => { |
| if i == 0 && j != 0 { |
| return false; |
| } |
| b = &b[j + part.len()..]; |
| } |
| None => return false, |
| } |
| } |
| b.is_empty() || a.ends_with("[..]") |
| } |
| |
| impl fmt::Display for WildStr<'_> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.write_str(&self.line) |
| } |
| } |
| |
| impl fmt::Debug for WildStr<'_> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{:?}", self.line) |
| } |
| } |
| |
| pub struct InMemoryDir { |
| files: Vec<(PathBuf, Data)>, |
| } |
| |
| impl InMemoryDir { |
| pub fn paths(&self) -> impl Iterator<Item = &Path> { |
| self.files.iter().map(|(p, _)| p.as_path()) |
| } |
| |
| #[track_caller] |
| pub fn assert_contains(&self, expected: &Self) { |
| use std::fmt::Write as _; |
| let assert = assert_e2e(); |
| let mut errs = String::new(); |
| for (path, expected_data) in &expected.files { |
| let actual_data = self |
| .files |
| .iter() |
| .find_map(|(p, d)| (path == p).then(|| d.clone())) |
| .unwrap_or_else(|| Data::new()); |
| if let Err(err) = |
| assert.try_eq(Some(&path.display()), actual_data, expected_data.clone()) |
| { |
| let _ = write!(&mut errs, "{err}"); |
| } |
| } |
| if !errs.is_empty() { |
| panic!("{errs}") |
| } |
| } |
| } |
| |
| impl<P, D> FromIterator<(P, D)> for InMemoryDir |
| where |
| P: Into<std::path::PathBuf>, |
| D: IntoData, |
| { |
| fn from_iter<I: IntoIterator<Item = (P, D)>>(files: I) -> Self { |
| let files = files |
| .into_iter() |
| .map(|(p, d)| (p.into(), d.into_data())) |
| .collect(); |
| Self { files } |
| } |
| } |
| |
| impl<const N: usize, P, D> From<[(P, D); N]> for InMemoryDir |
| where |
| P: Into<PathBuf>, |
| D: IntoData, |
| { |
| fn from(files: [(P, D); N]) -> Self { |
| let files = files |
| .into_iter() |
| .map(|(p, d)| (p.into(), d.into_data())) |
| .collect(); |
| Self { files } |
| } |
| } |
| |
| impl<P, D> From<std::collections::HashMap<P, D>> for InMemoryDir |
| where |
| P: Into<PathBuf>, |
| D: IntoData, |
| { |
| fn from(files: std::collections::HashMap<P, D>) -> Self { |
| let files = files |
| .into_iter() |
| .map(|(p, d)| (p.into(), d.into_data())) |
| .collect(); |
| Self { files } |
| } |
| } |
| |
| impl<P, D> From<std::collections::BTreeMap<P, D>> for InMemoryDir |
| where |
| P: Into<PathBuf>, |
| D: IntoData, |
| { |
| fn from(files: std::collections::BTreeMap<P, D>) -> Self { |
| let files = files |
| .into_iter() |
| .map(|(p, d)| (p.into(), d.into_data())) |
| .collect(); |
| Self { files } |
| } |
| } |
| |
| impl From<()> for InMemoryDir { |
| fn from(_files: ()) -> Self { |
| let files = Vec::new(); |
| Self { files } |
| } |
| } |
| |
| /// Create an `impl _ for InMemoryDir` for a generic tuple |
| /// |
| /// Must pass in names for each tuple parameter for |
| /// - internal variable name |
| /// - `Path` type |
| /// - `Data` type |
| macro_rules! impl_from_tuple_for_inmemorydir { |
| ($($var:ident $path:ident $data:ident),+) => { |
| impl<$($path: Into<PathBuf>, $data: IntoData),+> From<($(($path, $data)),+ ,)> for InMemoryDir { |
| fn from(files: ($(($path, $data)),+,)) -> Self { |
| let ($($var),+ ,) = files; |
| let files = [$(($var.0.into(), $var.1.into_data())),+]; |
| files.into() |
| } |
| } |
| }; |
| } |
| |
| /// Extend `impl_from_tuple_for_inmemorydir`` to generate for the specified tuple and all smaller |
| /// tuples |
| macro_rules! impl_from_tuples_for_inmemorydir { |
| ($var1:ident $path1:ident $data1:ident, $($var:ident $path:ident $data:ident),+) => { |
| impl_from_tuples_for_inmemorydir!(__impl $var1 $path1 $data1; $($var $path $data),+); |
| }; |
| (__impl $($var:ident $path:ident $data:ident),+; $var1:ident $path1:ident $data1:ident $(,$var2:ident $path2:ident $data2:ident)*) => { |
| impl_from_tuple_for_inmemorydir!($($var $path $data),+); |
| impl_from_tuples_for_inmemorydir!(__impl $($var $path $data),+, $var1 $path1 $data1; $($var2 $path2 $data2),*); |
| }; |
| (__impl $($var:ident $path:ident $data:ident),+;) => { |
| impl_from_tuple_for_inmemorydir!($($var $path $data),+); |
| } |
| } |
| |
| // Generate for tuples of size `1..=7` |
| impl_from_tuples_for_inmemorydir!( |
| s1 P1 D1, |
| s2 P2 D2, |
| s3 P3 D3, |
| s4 P4 D4, |
| s5 P5 D5, |
| s6 P6 D6, |
| s7 P7 D7 |
| ); |
| |
| #[cfg(test)] |
| mod test { |
| use snapbox::assert_data_eq; |
| use snapbox::prelude::*; |
| use snapbox::str; |
| |
| use super::*; |
| |
| #[test] |
| fn wild_str_cmp() { |
| for (a, b) in &[ |
| ("a b", "a b"), |
| ("a[..]b", "a b"), |
| ("a[..]", "a b"), |
| ("[..]", "a b"), |
| ("[..]b", "a b"), |
| ] { |
| assert_eq!(WildStr::new(a), WildStr::new(b)); |
| } |
| for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] { |
| assert_ne!(WildStr::new(a), WildStr::new(b)); |
| } |
| } |
| |
| #[test] |
| fn dirty_msvc() { |
| let case = |expected: &str, wild: &str, msvc: bool| { |
| assert_eq!(expected, &replace_dirty_msvc_impl(wild, msvc)); |
| }; |
| |
| // no replacements |
| case("aa", "aa", false); |
| case("aa", "aa", true); |
| |
| // with replacements |
| case( |
| "\ |
| [DIRTY] a", |
| "\ |
| [DIRTY-MSVC] a", |
| true, |
| ); |
| case( |
| "", |
| "\ |
| [DIRTY-MSVC] a", |
| false, |
| ); |
| case( |
| "\ |
| [DIRTY] a |
| [COMPILING] a", |
| "\ |
| [DIRTY-MSVC] a |
| [COMPILING] a", |
| true, |
| ); |
| case( |
| "\ |
| [COMPILING] a", |
| "\ |
| [DIRTY-MSVC] a |
| [COMPILING] a", |
| false, |
| ); |
| |
| // test trailing newline behavior |
| case( |
| "\ |
| A |
| B |
| ", "\ |
| A |
| B |
| ", true, |
| ); |
| |
| case( |
| "\ |
| A |
| B |
| ", "\ |
| A |
| B |
| ", false, |
| ); |
| |
| case( |
| "\ |
| A |
| B", "\ |
| A |
| B", true, |
| ); |
| |
| case( |
| "\ |
| A |
| B", "\ |
| A |
| B", false, |
| ); |
| |
| case( |
| "\ |
| [DIRTY] a |
| ", |
| "\ |
| [DIRTY-MSVC] a |
| ", |
| true, |
| ); |
| case( |
| "\n", |
| "\ |
| [DIRTY-MSVC] a |
| ", |
| false, |
| ); |
| |
| case( |
| "\ |
| [DIRTY] a", |
| "\ |
| [DIRTY-MSVC] a", |
| true, |
| ); |
| case( |
| "", |
| "\ |
| [DIRTY-MSVC] a", |
| false, |
| ); |
| } |
| |
| #[test] |
| fn redact_elapsed_time() { |
| let mut subs = snapbox::Redactions::new(); |
| add_regex_redactions(&mut subs); |
| |
| assert_data_eq!( |
| subs.redact("[FINISHED] `release` profile [optimized] target(s) in 5.5s"), |
| str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw() |
| ); |
| assert_data_eq!( |
| subs.redact("[FINISHED] `release` profile [optimized] target(s) in 1m 05s"), |
| str!["[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s"].raw() |
| ); |
| } |
| } |