diff --git a/Cargo.lock b/Cargo.lock index 49f2127c..6f750544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,11 +805,10 @@ dependencies = [ "dunce", "escargot", "filetime", - "ignore", "libc", - "libtest-mimic", "normalize-line-endings", "os_pipe", + "regex", "serde", "serde_json", "similar", diff --git a/crates/snapbox/Cargo.toml b/crates/snapbox/Cargo.toml index 6a410412..ea231fc1 100644 --- a/crates/snapbox/Cargo.toml +++ b/crates/snapbox/Cargo.toml @@ -32,8 +32,6 @@ default = ["color-auto", "diff"] #! Feature Flags -## Simple input/output test harness -harness = ["dep:libtest-mimic", "dep:ignore"] ## Smarter binary file detection detect-encoding = ["dep:content_inspector"] ## Snapshotting of directories @@ -44,6 +42,8 @@ path = ["dir"] cmd = ["dep:os_pipe", "dep:wait-timeout", "dep:libc", "dep:windows-sys"] ## Building of examples for snapshotting examples = ["dep:escargot"] +## Regex text substitutions +regex = ["dep:regex"] ## Snapshotting of json json = ["structured-data", "dep:serde_json", "dep:serde"] @@ -71,9 +71,6 @@ name = "snap-fixture" # For `snapbox`s tests only normalize-line-endings = "0.3.0" snapbox-macros = { path = "../snapbox-macros", version = "0.3.9" } -libtest-mimic = { version = "0.7.0", optional = true } -ignore = { version = "0.4", optional = true } - content_inspector = { version = "0.2.4", optional = true } tempfile = { version = "3.0", optional = true } @@ -97,6 +94,7 @@ document-features = { version = "0.2.6", optional = true } serde_json = { version = "1.0.85", optional = true} anstyle-svg = { version = "0.1.3", optional = true } serde = { version = "1.0.198", optional = true } +regex = { version = "1.10.4", optional = true, default-features = false, features = ["std"] } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.52.0", features = ["Win32_Foundation"], optional = true } diff --git a/crates/snapbox/src/assert/mod.rs b/crates/snapbox/src/assert/mod.rs index 467ba554..d4bc71d5 100644 --- a/crates/snapbox/src/assert/mod.rs +++ b/crates/snapbox/src/assert/mod.rs @@ -26,7 +26,7 @@ pub use error::Result; /// # use snapbox::Assert; /// # use snapbox::file; /// let actual = "something"; -/// Assert::new().eq_(actual, file!["output.txt"]); +/// Assert::new().eq(actual, file!["output.txt"]); /// ``` #[derive(Clone, Debug)] pub struct Assert { @@ -49,6 +49,8 @@ impl Assert { /// - `...` is a line-wildcard when on a line by itself /// - `[..]` is a character-wildcard when inside a line /// - `[EXE]` matches `.exe` on Windows + /// - `"{...}"` is a JSON value wildcard + /// - `"...": "{...}"` is a JSON key-value wildcard /// - `\` to `/` /// - Newlines /// @@ -60,7 +62,7 @@ impl Assert { /// # use snapbox::Assert; /// let actual = "something"; /// let expected = "so[..]g"; - /// Assert::new().eq_(actual, expected); + /// Assert::new().eq(actual, expected); /// ``` /// /// Can combine this with [`file!`][crate::file] @@ -68,10 +70,10 @@ impl Assert { /// # use snapbox::Assert; /// # use snapbox::file; /// let actual = "something"; - /// Assert::new().eq_(actual, file!["output.txt"]); + /// Assert::new().eq(actual, file!["output.txt"]); /// ``` #[track_caller] - pub fn eq_(&self, actual: impl IntoData, expected: impl IntoData) { + pub fn eq(&self, actual: impl IntoData, expected: impl IntoData) { let expected = expected.into_data(); let actual = actual.into_data(); if let Err(err) = self.try_eq(Some(&"In-memory"), actual, expected) { @@ -79,73 +81,10 @@ impl Assert { } } - /// Check if a value is the same as an expected value - /// - /// When the content is text, newlines are normalized. - /// - /// ```rust - /// # use snapbox::Assert; - /// let actual = "something"; - /// let expected = "something"; - /// Assert::new().eq(expected, actual); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// # use snapbox::Assert; - /// # use snapbox::file; - /// let actual = "something"; - /// Assert::new().eq(file!["output.txt"], actual); - /// ``` #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `Assert::eq_(actual, expected.raw())`" - )] - pub fn eq(&self, expected: impl Into, actual: impl Into) { - let expected = expected.into(); - let actual = actual.into(); - if let Err(err) = self.try_eq(Some(&"In-memory"), actual, expected.raw()) { - err.panic(); - } - } - - /// Check if a value matches a pattern - /// - /// Pattern syntax: - /// - `...` is a line-wildcard when on a line by itself - /// - `[..]` is a character-wildcard when inside a line - /// - `[EXE]` matches `.exe` on Windows - /// - /// Normalization: - /// - Newlines - /// - `\` to `/` - /// - /// ```rust - /// # use snapbox::Assert; - /// let actual = "something"; - /// let expected = "so[..]g"; - /// Assert::new().matches(expected, actual); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// # use snapbox::Assert; - /// # use snapbox::file; - /// let actual = "something"; - /// Assert::new().matches(file!["output.txt"], actual); - /// ``` - #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `Assert::eq_(actual, expected)`" - )] - pub fn matches(&self, pattern: impl Into, actual: impl Into) { - let pattern = pattern.into(); - let actual = actual.into(); - if let Err(err) = self.try_eq(Some(&"In-memory"), actual, pattern) { - err.panic(); - } + #[deprecated(since = "0.6.0", note = "Replaced with `Assert::eq`")] + pub fn eq_(&self, actual: impl IntoData, expected: impl IntoData) { + self.eq(actual, expected) } pub fn try_eq( diff --git a/crates/snapbox/src/cmd.rs b/crates/snapbox/src/cmd.rs index f13471a2..bd9be834 100644 --- a/crates/snapbox/src/cmd.rs +++ b/crates/snapbox/src/cmd.rs @@ -594,6 +594,8 @@ impl OutputAssert { /// - `...` is a line-wildcard when on a line by itself /// - `[..]` is a character-wildcard when inside a line /// - `[EXE]` matches `.exe` on Windows + /// - `"{...}"` is a JSON value wildcard + /// - `"...": "{...}"` is a JSON key-value wildcard /// - `\` to `/` /// - Newlines /// @@ -609,7 +611,7 @@ impl OutputAssert { /// .env("stdout", "hello") /// .env("stderr", "world") /// .assert() - /// .stdout_eq_("he[..]o"); + /// .stdout_eq("he[..]o"); /// ``` /// /// Can combine this with [`file!`][crate::file] @@ -622,82 +624,18 @@ impl OutputAssert { /// .env("stdout", "hello") /// .env("stderr", "world") /// .assert() - /// .stdout_eq_(file!["stdout.log"]); + /// .stdout_eq(file!["stdout.log"]); /// ``` #[track_caller] - pub fn stdout_eq_(self, expected: impl IntoData) -> Self { + pub fn stdout_eq(self, expected: impl IntoData) -> Self { let expected = expected.into_data(); self.stdout_eq_inner(expected) } - /// Ensure the command wrote the expected data to `stdout`. - /// - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stdout_eq("hello"); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// use snapbox::file; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stdout_eq(file!["stdout.log"]); - /// ``` - #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `OutputAssert::stdout_eq_(expected.raw())`" - )] - pub fn stdout_eq(self, expected: impl Into) -> Self { - let expected = expected.into(); - self.stdout_eq_inner(expected.raw()) - } - - /// Ensure the command wrote the expected data to `stdout`. - /// - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stdout_matches("he[..]o"); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// use snapbox::file; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stdout_matches(file!["stdout.log"]); - /// ``` #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `OutputAssert::stdout_eq_(expected)`" - )] - pub fn stdout_matches(self, expected: impl Into) -> Self { - let expected = expected.into(); - self.stdout_eq_inner(expected) + #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stdout_eq`")] + pub fn stdout_eq_(self, expected: impl IntoData) -> Self { + self.stdout_eq(expected) } #[track_caller] @@ -716,6 +654,8 @@ impl OutputAssert { /// - `...` is a line-wildcard when on a line by itself /// - `[..]` is a character-wildcard when inside a line /// - `[EXE]` matches `.exe` on Windows + /// - `"{...}"` is a JSON value wildcard + /// - `"...": "{...}"` is a JSON key-value wildcard /// - `\` to `/` /// - Newlines /// @@ -731,7 +671,7 @@ impl OutputAssert { /// .env("stdout", "hello") /// .env("stderr", "world") /// .assert() - /// .stderr_eq_("wo[..]d"); + /// .stderr_eq("wo[..]d"); /// ``` /// /// Can combine this with [`file!`][crate::file] @@ -744,82 +684,18 @@ impl OutputAssert { /// .env("stdout", "hello") /// .env("stderr", "world") /// .assert() - /// .stderr_eq_(file!["stderr.log"]); + /// .stderr_eq(file!["stderr.log"]); /// ``` #[track_caller] - pub fn stderr_eq_(self, expected: impl IntoData) -> Self { + pub fn stderr_eq(self, expected: impl IntoData) -> Self { let expected = expected.into_data(); self.stderr_eq_inner(expected) } - /// Ensure the command wrote the expected data to `stderr`. - /// - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stderr_eq("world"); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// use snapbox::file; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stderr_eq(file!["stderr.log"]); - /// ``` - #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `OutputAssert::stderr_eq_(expected.raw())`" - )] - pub fn stderr_eq(self, expected: impl Into) -> Self { - let expected = expected.into(); - self.stderr_eq_inner(expected.raw()) - } - - /// Ensure the command wrote the expected data to `stderr`. - /// - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stderr_matches("wo[..]d"); - /// ``` - /// - /// Can combine this with [`file!`][crate::file] - /// ```rust,no_run - /// use snapbox::cmd::Command; - /// use snapbox::cmd::cargo_bin; - /// use snapbox::file; - /// - /// let assert = Command::new(cargo_bin("snap-fixture")) - /// .env("stdout", "hello") - /// .env("stderr", "world") - /// .assert() - /// .stderr_matches(file!["stderr.log"]); - /// ``` #[track_caller] - #[deprecated( - since = "0.5.11", - note = "Replaced with `OutputAssert::stderr_eq_(expected)`" - )] - pub fn stderr_matches(self, expected: impl Into) -> Self { - let expected = expected.into(); - self.stderr_eq_inner(expected) + #[deprecated(since = "0.6.0", note = "Replaced with `OutputAssert::stderr_eq`")] + pub fn stderr_eq_(self, expected: impl IntoData) -> Self { + self.stderr_eq(expected) } #[track_caller] diff --git a/crates/snapbox/src/data/format.rs b/crates/snapbox/src/data/format.rs index 8af0ee6e..7ab71014 100644 --- a/crates/snapbox/src/data/format.rs +++ b/crates/snapbox/src/data/format.rs @@ -1,5 +1,6 @@ /// Describes the structure of [`Data`][crate::Data] #[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)] +#[non_exhaustive] pub enum DataFormat { /// Processing of the [`Data`][crate::Data] failed Error, @@ -9,6 +10,9 @@ pub enum DataFormat { Text, #[cfg(feature = "json")] Json, + /// Streamed JSON output according to + #[cfg(feature = "json")] + JsonLines, /// [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#DOS_and_Windows) /// rendered as [svg](https://docs.rs/anstyle-svg) #[cfg(feature = "term-svg")] @@ -24,6 +28,8 @@ impl DataFormat { Self::Text => "txt", #[cfg(feature = "json")] Self::Json => "json", + #[cfg(feature = "json")] + Self::JsonLines => "jsonl", #[cfg(feature = "term-svg")] Self::TermSvg => "term.svg", } @@ -43,6 +49,8 @@ impl From<&std::path::Path> for DataFormat { match ext { #[cfg(feature = "json")] "json" => DataFormat::Json, + #[cfg(feature = "json")] + "jsonl" => DataFormat::JsonLines, #[cfg(feature = "term-svg")] "term.svg" => Self::TermSvg, _ => DataFormat::Text, diff --git a/crates/snapbox/src/data/mod.rs b/crates/snapbox/src/data/mod.rs index e345e04b..db8e5026 100644 --- a/crates/snapbox/src/data/mod.rs +++ b/crates/snapbox/src/data/mod.rs @@ -2,20 +2,12 @@ mod filters; mod format; -mod normalize; mod runtime; mod source; #[cfg(test)] mod tests; pub use format::DataFormat; -pub use normalize::Normalize; -#[allow(deprecated)] -pub use normalize::NormalizeMatches; -#[allow(deprecated)] -pub use normalize::NormalizeNewlines; -#[allow(deprecated)] -pub use normalize::NormalizePaths; pub use source::DataSource; pub use source::Inline; #[doc(hidden)] @@ -112,12 +104,52 @@ pub trait IntoData: Sized { fn into_data(self) -> Data; } -impl IntoData for D -where - D: Into, -{ +impl IntoData for Data { fn into_data(self) -> Data { - self.into() + self + } +} + +impl IntoData for &'_ Data { + fn into_data(self) -> Data { + self.clone() + } +} + +impl IntoData for Vec { + fn into_data(self) -> Data { + Data::binary(self) + } +} + +impl IntoData for &'_ [u8] { + fn into_data(self) -> Data { + self.to_owned().into_data() + } +} + +impl IntoData for String { + fn into_data(self) -> Data { + Data::text(self) + } +} + +impl IntoData for &'_ String { + fn into_data(self) -> Data { + self.to_owned().into_data() + } +} + +impl IntoData for &'_ str { + fn into_data(self) -> Data { + self.to_owned().into_data() + } +} + +impl IntoData for Inline { + fn into_data(self) -> Data { + let trimmed = self.trimmed(); + super::Data::text(trimmed).with_source(self) } } @@ -170,8 +202,6 @@ macro_rules! file { /// "]]; /// str![r#"{"Foo": 92}"#]; /// ``` -/// -/// Leading indentation is stripped. #[macro_export] macro_rules! str { [$data:literal] => { $crate::str![[$data]] }; @@ -184,7 +214,6 @@ macro_rules! str { let inline = $crate::data::Inline { position, data: $data, - indent: true, }; inline }}; @@ -209,6 +238,9 @@ pub(crate) enum DataInner { Text(String), #[cfg(feature = "json")] Json(serde_json::Value), + // Always a `Value::Array` but using `Value` for easier bookkeeping + #[cfg(feature = "json")] + JsonLines(serde_json::Value), #[cfg(feature = "term-svg")] TermSvg(String), } @@ -238,6 +270,11 @@ impl Data { Self::with_inner(DataInner::Json(raw.into())) } + #[cfg(feature = "json")] + pub fn jsonlines(raw: impl Into>) -> Self { + Self::with_inner(DataInner::JsonLines(serde_json::Value::Array(raw.into()))) + } + fn error(raw: impl Into, intended: DataFormat) -> Self { Self::with_inner(DataInner::Error(DataError { error: raw.into(), @@ -301,7 +338,7 @@ impl Data { let inferred_format = DataFormat::from(path); match inferred_format { #[cfg(feature = "json")] - DataFormat::Json => data.coerce_to(inferred_format), + DataFormat::Json | DataFormat::JsonLines => data.coerce_to(inferred_format), #[cfg(feature = "term-svg")] DataFormat::TermSvg => { let data = data.coerce_to(DataFormat::Text); @@ -336,14 +373,6 @@ impl Data { .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into()) } - /// Post-process text - /// - /// See [utils][crate::utils] - #[deprecated(since = "0.5.11", note = "Replaced with `Normalize::normalize`")] - pub fn normalize(self, op: impl Normalize) -> Self { - op.filter(self) - } - /// Return the underlying `String` /// /// Note: this will not inspect binary data for being a valid `String`. @@ -353,7 +382,9 @@ impl Data { DataInner::Binary(_) => None, DataInner::Text(data) => Some(data.to_owned()), #[cfg(feature = "json")] - DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()), + DataInner::Json(_) => Some(self.to_string()), + #[cfg(feature = "json")] + DataInner::JsonLines(_) => Some(self.to_string()), #[cfg(feature = "term-svg")] DataInner::TermSvg(data) => Some(data.to_owned()), } @@ -365,9 +396,9 @@ impl Data { DataInner::Binary(data) => Ok(data.clone()), DataInner::Text(data) => Ok(data.clone().into_bytes()), #[cfg(feature = "json")] - DataInner::Json(value) => { - serde_json::to_vec_pretty(value).map_err(|err| format!("{err}").into()) - } + DataInner::Json(_) => Ok(self.to_string().into_bytes()), + #[cfg(feature = "json")] + DataInner::JsonLines(_) => Ok(self.to_string().into_bytes()), #[cfg(feature = "term-svg")] DataInner::TermSvg(data) => Ok(data.clone().into_bytes()), } @@ -393,6 +424,8 @@ impl Data { (DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner), #[cfg(feature = "json")] (DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner), + #[cfg(feature = "json")] + (DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner), #[cfg(feature = "term-svg")] (DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner), (DataInner::Binary(inner), _) => { @@ -405,6 +438,11 @@ impl Data { .map_err(|err| err.to_string())?; DataInner::Json(inner) } + #[cfg(feature = "json")] + (DataInner::Text(inner), DataFormat::JsonLines) => { + let inner = parse_jsonlines(&inner).map_err(|err| err.to_string())?; + DataInner::JsonLines(serde_json::Value::Array(inner)) + } #[cfg(feature = "term-svg")] (DataInner::Text(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner), (inner, DataFormat::Binary) => { @@ -442,6 +480,8 @@ impl Data { (DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner), #[cfg(feature = "json")] (DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner), + #[cfg(feature = "json")] + (DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner), #[cfg(feature = "term-svg")] (DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner), (DataInner::Binary(inner), _) => { @@ -475,6 +515,14 @@ impl Data { DataInner::Text(inner) } } + #[cfg(feature = "json")] + (DataInner::Text(inner), DataFormat::JsonLines) => { + if let Ok(jsonlines) = parse_jsonlines(&inner) { + DataInner::JsonLines(serde_json::Value::Array(jsonlines)) + } else { + DataInner::Text(inner) + } + } #[cfg(feature = "term-svg")] (DataInner::Text(inner), DataFormat::TermSvg) => { DataInner::TermSvg(anstyle_svg::Term::new().render_svg(&inner)) @@ -499,6 +547,10 @@ impl Data { (inner, DataFormat::Json) => inner, // reachable if more than one structured data format is enabled #[allow(unreachable_patterns)] + #[cfg(feature = "json")] + (inner, DataFormat::JsonLines) => inner, + // reachable if more than one structured data format is enabled + #[allow(unreachable_patterns)] #[cfg(feature = "term-svg")] (inner, DataFormat::TermSvg) => inner, }; @@ -522,6 +574,8 @@ impl Data { DataInner::Text(_) => DataFormat::Text, #[cfg(feature = "json")] DataInner::Json(_) => DataFormat::Json, + #[cfg(feature = "json")] + DataInner::JsonLines(_) => DataFormat::JsonLines, #[cfg(feature = "term-svg")] DataInner::TermSvg(_) => DataFormat::TermSvg, } @@ -534,6 +588,8 @@ impl Data { DataInner::Text(_) => DataFormat::Text, #[cfg(feature = "json")] DataInner::Json(_) => DataFormat::Json, + #[cfg(feature = "json")] + DataInner::JsonLines(_) => DataFormat::JsonLines, #[cfg(feature = "term-svg")] DataInner::TermSvg(_) => DataFormat::TermSvg, } @@ -546,6 +602,8 @@ impl Data { DataInner::Text(_) => None, #[cfg(feature = "json")] DataInner::Json(_) => None, + #[cfg(feature = "json")] + DataInner::JsonLines(_) => None, #[cfg(feature = "term-svg")] DataInner::TermSvg(data) => text_elem(data), } @@ -560,6 +618,14 @@ impl std::fmt::Display for Data { DataInner::Text(data) => data.fmt(f), #[cfg(feature = "json")] DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f), + #[cfg(feature = "json")] + DataInner::JsonLines(data) => { + let array = data.as_array().expect("jsonlines is always an array"); + for value in array { + writeln!(f, "{}", serde_json::to_string(value).unwrap())?; + } + Ok(()) + } #[cfg(feature = "term-svg")] DataInner::TermSvg(data) => data.fmt(f), } @@ -574,6 +640,8 @@ impl PartialEq for Data { (DataInner::Text(left), DataInner::Text(right)) => left == right, #[cfg(feature = "json")] (DataInner::Json(left), DataInner::Json(right)) => left == right, + #[cfg(feature = "json")] + (DataInner::JsonLines(left), DataInner::JsonLines(right)) => left == right, #[cfg(feature = "term-svg")] (DataInner::TermSvg(left), DataInner::TermSvg(right)) => { // HACK: avoid including `width` and `height` in the comparison @@ -598,6 +666,20 @@ impl std::fmt::Display for DataError { } } +#[cfg(feature = "json")] +fn parse_jsonlines(text: &str) -> Result, serde_json::Error> { + let mut lines = Vec::new(); + for line in text.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let json = serde_json::from_str::(line)?; + lines.push(json); + } + Ok(lines) +} + #[cfg(feature = "term-svg")] fn text_elem(svg: &str) -> Option<&str> { let open_elem_start_idx = svg.find(" From<&'d Data> for Data { fn from(other: &'d Data) -> Self { - other.clone() + other.into_data() } } impl From> for Data { fn from(other: Vec) -> Self { - Self::binary(other) + other.into_data() } } impl<'b> From<&'b [u8]> for Data { fn from(other: &'b [u8]) -> Self { - other.to_owned().into() + other.into_data() } } impl From for Data { fn from(other: String) -> Self { - Self::text(other) + other.into_data() } } impl<'s> From<&'s String> for Data { fn from(other: &'s String) -> Self { - other.clone().into() + other.into_data() } } impl<'s> From<&'s str> for Data { fn from(other: &'s str) -> Self { - other.to_owned().into() + other.into_data() } } impl From for super::Data { - fn from(inline: Inline) -> Self { - let trimmed = inline.trimmed(); - super::Data::text(trimmed).with_source(inline) + fn from(other: Inline) -> Self { + other.into_data() } } @@ -706,3 +787,89 @@ pub fn generate_snapshot_path(fn_path: &str, format: Option) -> std: path.push_str(format.unwrap_or(DataFormat::Text).ext()); path.into() } + +#[cfg(test)] +mod test { + use super::*; + + #[track_caller] + fn validate_cases(cases: &[(&str, bool)], input_format: DataFormat) { + for (input, valid) in cases.iter().copied() { + let (expected_is_format, expected_coerced_format) = if valid { + (input_format, input_format) + } else { + (DataFormat::Error, DataFormat::Text) + }; + + let actual_is = Data::text(input).is(input_format); + assert_eq!( + actual_is.format(), + expected_is_format, + "\n{input}\n{actual_is}" + ); + + let actual_coerced = Data::text(input).coerce_to(input_format); + assert_eq!( + actual_coerced.format(), + expected_coerced_format, + "\n{input}\n{actual_coerced}" + ); + + if valid { + assert_eq!(actual_is, actual_coerced); + + let rendered = actual_is.render().unwrap(); + let bytes = actual_is.to_bytes().unwrap(); + assert_eq!(rendered, std::str::from_utf8(&bytes).unwrap()); + + assert_eq!(Data::text(&rendered).is(input_format), actual_is); + } + } + } + + #[test] + fn text() { + let cases = [("", true), ("good", true), ("{}", true), ("\"\"", true)]; + validate_cases(&cases, DataFormat::Text); + } + + #[cfg(feature = "json")] + #[test] + fn json() { + let cases = [("", false), ("bad", false), ("{}", true), ("\"\"", true)]; + validate_cases(&cases, DataFormat::Json); + } + + #[cfg(feature = "json")] + #[test] + fn jsonlines() { + let cases = [ + ("", true), + ("bad", false), + ("{}", true), + ("\"\"", true), + ( + " +{} +{} +", true, + ), + ( + " +{} + +{} +", true, + ), + ( + " +{} +bad +{} +", + false, + ), + ]; + validate_cases(&cases, DataFormat::JsonLines); + } +} diff --git a/crates/snapbox/src/data/normalize.rs b/crates/snapbox/src/data/normalize.rs deleted file mode 100644 index da79f8e0..00000000 --- a/crates/snapbox/src/data/normalize.rs +++ /dev/null @@ -1,24 +0,0 @@ -#![allow(deprecated)] - -use super::Data; - -pub use crate::filter::Filter as Normalize; - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::FilterNewlines")] -pub struct NormalizeNewlines; -impl Normalize for NormalizeNewlines { - fn normalize(&self, data: Data) -> Data { - crate::filter::NormalizeNewlines.normalize(data) - } -} - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::FilterPaths")] -pub struct NormalizePaths; -impl Normalize for NormalizePaths { - fn normalize(&self, data: Data) -> Data { - crate::filter::NormalizePaths.normalize(data) - } -} - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::FilterRedactions")] -pub type NormalizeMatches<'a> = crate::filter::FilterRedactions<'a>; diff --git a/crates/snapbox/src/data/runtime.rs b/crates/snapbox/src/data/runtime.rs index 6e237db9..2d8e0d8e 100644 --- a/crates/snapbox/src/data/runtime.rs +++ b/crates/snapbox/src/data/runtime.rs @@ -73,12 +73,7 @@ impl SourceFileRuntime { } fn update(&mut self, actual: &str, inline: &Inline) -> std::io::Result<()> { let span = Span::from_pos(&inline.position, &self.original_text); - let desired_indent = if inline.indent { - Some(span.line_indent) - } else { - None - }; - let patch = format_patch(desired_indent, actual); + let patch = format_patch(actual); self.patchwork.patch(span.literal_range, &patch); std::fs::write(&inline.position.file, &self.patchwork.text) } @@ -135,9 +130,8 @@ fn lit_kind_for_patch(patch: &str) -> StrLitKind { StrLitKind::Raw(max_hashes + 1) } -fn format_patch(desired_indent: Option, patch: &str) -> String { +fn format_patch(patch: &str) -> String { let lit_kind = lit_kind_for_patch(patch); - let indent = desired_indent.map(|it| " ".repeat(it)); let is_multiline = patch.contains('\n'); let mut buf = String::new(); @@ -148,21 +142,9 @@ fn format_patch(desired_indent: Option, patch: &str) -> String { if is_multiline { buf.push('\n'); } - let mut final_newline = false; - for line in crate::utils::LinesWithTerminator::new(patch) { - if is_multiline && !line.trim().is_empty() { - if let Some(indent) = &indent { - buf.push_str(indent); - buf.push_str(" "); - } - } - buf.push_str(line); - final_newline = line.ends_with('\n'); - } - if final_newline { - if let Some(indent) = &indent { - buf.push_str(indent); - } + buf.push_str(patch); + if is_multiline { + buf.push('\n'); } lit_kind.write_end(&mut buf).unwrap(); if matches!(lit_kind, StrLitKind::Raw(_)) { @@ -173,8 +155,6 @@ fn format_patch(desired_indent: Option, patch: &str) -> String { #[derive(Clone, Debug)] struct Span { - line_indent: usize, - /// The byte range of the argument to `expect!`, including the inner `[]` if it exists. literal_range: std::ops::Range, } @@ -207,13 +187,12 @@ impl Span { .0; let literal_start = line_start + byte_offset; - let indent = line.chars().take_while(|&it| it == ' ').count(); - target_line = Some((literal_start, indent)); + target_line = Some(literal_start); break; } line_start += line.len(); } - let (literal_start, line_indent) = target_line.unwrap(); + let literal_start = target_line.unwrap(); let lit_to_eof = &file[literal_start..]; let lit_to_eof_trimmed = lit_to_eof.trim_start(); @@ -223,10 +202,7 @@ impl Span { let literal_len = locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`."); let literal_range = literal_start..literal_start + literal_len; - Span { - line_indent, - literal_range, - } + Span { literal_range } } } @@ -378,35 +354,24 @@ mod tests { #[test] fn test_format_patch() { - let patch = format_patch(None, "hello\nworld\n"); + let patch = format_patch("hello\nworld\n"); assert_data_eq!( patch, str![[r##" - [r#" - hello - world - "#]"##]], +[r#" +hello +world + +"#] +"##]], ); - let patch = format_patch(None, r"hello\tworld"); + let patch = format_patch(r"hello\tworld"); assert_data_eq!(patch, str![[r##"[r#"hello\tworld"#]"##]].raw()); - let patch = format_patch(None, "{\"foo\": 42}"); + let patch = format_patch("{\"foo\": 42}"); assert_data_eq!(patch, str![[r##"[r#"{"foo": 42}"#]"##]]); - - let patch = format_patch(Some(0), "hello\nworld\n"); - assert_data_eq!( - patch, - str![[r##" - [r#" - hello - world - "#]"##]], - ); - - let patch = format_patch(Some(4), "single line"); - assert_data_eq!(patch, str![[r#""single line""#]]); } #[test] @@ -418,24 +383,25 @@ mod tests { assert_data_eq!( patchwork.to_debug(), str![[r#" - Patchwork { - text: "один zwei 3", - indels: [ - ( - 0..3, - 8, - ), - ( - 4..7, - 4, - ), - ( - 8..13, - 1, - ), - ], - } - "#]], +Patchwork { + text: "один zwei 3", + indels: [ + ( + 0..3, + 8, + ), + ( + 4..7, + 4, + ), + ( + 8..13, + 1, + ), + ], +} + +"#]], ); } diff --git a/crates/snapbox/src/data/source.rs b/crates/snapbox/src/data/source.rs index 6e0aae71..b72ec41f 100644 --- a/crates/snapbox/src/data/source.rs +++ b/crates/snapbox/src/data/source.rs @@ -76,77 +76,19 @@ pub struct Inline { pub position: Position, #[doc(hidden)] pub data: &'static str, - #[doc(hidden)] - pub indent: bool, } impl Inline { - /// Indent to quote-level when overwriting the string literal (default) - pub fn indent(mut self, yes: bool) -> Self { - self.indent = yes; - self - } - - /// Initialize `Self` as [`format`][crate::data::DataFormat] or [`Error`][crate::data::DataFormat::Error] - /// - /// This is generally used for `expected` data - /// - /// ```rust - /// # #[cfg(feature = "json")] { - /// use snapbox::str; - /// - /// let expected = str![[r#"{"hello": "world"}"#]] - /// .is(snapbox::data::DataFormat::Json); - /// assert_eq!(expected.format(), snapbox::data::DataFormat::Json); - /// # } - /// ``` - // #[deprecated(since = "0.5.11", note = "Replaced with `IntoData::is`")] // can't deprecate - // because trait will always be preferred - pub fn is(self, format: super::DataFormat) -> super::Data { - let data: super::Data = self.into(); - data.is(format) - } - - /// Deprecated, replaced with [`Inline::is`] - #[deprecated(since = "0.5.2", note = "Replaced with `Inline::is`")] - pub fn coerce_to(self, format: super::DataFormat) -> super::Data { - let data: super::Data = self.into(); - data.coerce_to(format) - } - pub(crate) fn trimmed(&self) -> String { let mut data = self.data; if data.contains('\n') { - if data.starts_with('\n') { - data = &data[1..] - } - if self.indent { - return trim_indent(data); - } + data = data.strip_prefix('\n').unwrap_or(data); + data = data.strip_suffix('\n').unwrap_or(data); } data.to_owned() } } -fn trim_indent(text: &str) -> String { - let indent = text - .lines() - .filter(|it| !it.trim().is_empty()) - .map(|it| it.len() - it.trim_start().len()) - .min() - .unwrap_or(0); - - crate::utils::LinesWithTerminator::new(text) - .map(|line| { - if line.len() <= indent { - line.trim_start_matches(' ') - } else { - &line[indent..] - } - }) - .collect() -} - impl std::fmt::Display for Inline { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.position.fmt(f) diff --git a/crates/snapbox/src/filter/mod.rs b/crates/snapbox/src/filter/mod.rs index 27fd531b..570a47e5 100644 --- a/crates/snapbox/src/filter/mod.rs +++ b/crates/snapbox/src/filter/mod.rs @@ -15,35 +15,12 @@ pub use redactions::RedactedValue; pub use redactions::Redactions; pub trait Filter { - #[deprecated(since = "0.5.11", note = "Replaced with `Filter::filter`")] - fn normalize(&self, data: Data) -> Data; - fn filter(&self, data: Data) -> Data { - #[allow(deprecated)] - self.normalize(data) - } -} - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::FilterNewlines")] -pub struct NormalizeNewlines; -#[allow(deprecated)] -impl Filter for NormalizeNewlines { - fn normalize(&self, data: Data) -> Data { - FilterNewlines.normalize(data) - } -} - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::FilterPaths")] -pub struct NormalizePaths; -#[allow(deprecated)] -impl Filter for NormalizePaths { - fn normalize(&self, data: Data) -> Data { - FilterPaths.normalize(data) - } + fn filter(&self, data: Data) -> Data; } pub struct FilterNewlines; impl Filter for FilterNewlines { - fn normalize(&self, data: Data) -> Data { + fn filter(&self, data: Data) -> Data { let source = data.source; let filters = data.filters; let inner = match data.inner { @@ -56,9 +33,15 @@ impl Filter for FilterNewlines { #[cfg(feature = "json")] DataInner::Json(value) => { let mut value = value; - normalize_value(&mut value, normalize_lines); + normalize_json_string(&mut value, normalize_lines); DataInner::Json(value) } + #[cfg(feature = "json")] + DataInner::JsonLines(value) => { + let mut value = value; + normalize_json_string(&mut value, normalize_lines); + DataInner::JsonLines(value) + } #[cfg(feature = "term-svg")] DataInner::TermSvg(text) => { let lines = normalize_lines(&text); @@ -84,7 +67,7 @@ fn normalize_lines_chars(data: impl Iterator) -> impl Iterator Data { + fn filter(&self, data: Data) -> Data { let source = data.source; let filters = data.filters; let inner = match data.inner { @@ -97,9 +80,15 @@ impl Filter for FilterPaths { #[cfg(feature = "json")] DataInner::Json(value) => { let mut value = value; - normalize_value(&mut value, normalize_paths); + normalize_json_string(&mut value, normalize_paths); DataInner::Json(value) } + #[cfg(feature = "json")] + DataInner::JsonLines(value) => { + let mut value = value; + normalize_json_string(&mut value, normalize_paths); + DataInner::JsonLines(value) + } #[cfg(feature = "term-svg")] DataInner::TermSvg(text) => { let lines = normalize_paths(&text); @@ -143,7 +132,7 @@ impl<'a> FilterRedactions<'a> { } impl Filter for FilterRedactions<'_> { - fn normalize(&self, data: Data) -> Data { + fn filter(&self, data: Data) -> Data { let source = data.source; let filters = data.filters; let inner = match data.inner { @@ -165,6 +154,14 @@ impl Filter for FilterRedactions<'_> { } DataInner::Json(value) } + #[cfg(feature = "json")] + DataInner::JsonLines(value) => { + let mut value = value; + if let DataInner::Json(exp) = &self.pattern.inner { + normalize_value_matches(&mut value, exp, self.substitutions); + } + DataInner::JsonLines(value) + } #[cfg(feature = "term-svg")] DataInner::TermSvg(text) => { if let Some(pattern) = self.pattern.render() { @@ -184,19 +181,21 @@ impl Filter for FilterRedactions<'_> { } #[cfg(feature = "structured-data")] -fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) { +fn normalize_json_string(value: &mut serde_json::Value, op: fn(&str) -> String) { match value { serde_json::Value::String(str) => { *str = op(str); } serde_json::Value::Array(arr) => { for value in arr.iter_mut() { - normalize_value(value, op) + normalize_json_string(value, op) } } serde_json::Value::Object(obj) => { - for (_, value) in obj.iter_mut() { - normalize_value(value, op) + for (key, mut value) in std::mem::replace(obj, serde_json::Map::new()) { + let key = op(&key); + normalize_json_string(&mut value, op); + obj.insert(key, value); } } _ => {} @@ -211,6 +210,7 @@ fn normalize_value_matches( ) { use serde_json::Value::*; + const KEY_WILDCARD: &str = "..."; const VALUE_WILDCARD: &str = "{...}"; match (actual, expected) { @@ -255,8 +255,19 @@ fn normalize_value_matches( } } (Object(act), Object(exp)) => { - for (a, e) in act.iter_mut().zip(exp).filter(|(a, e)| a.0 == e.0) { - normalize_value_matches(a.1, e.1, substitutions) + let has_key_wildcard = + exp.get(KEY_WILDCARD).and_then(|v| v.as_str()) == Some(VALUE_WILDCARD); + for (actual_key, mut actual_value) in std::mem::replace(act, serde_json::Map::new()) { + let actual_key = substitutions.redact(&actual_key); + if let Some(expected_value) = exp.get(&actual_key) { + normalize_value_matches(&mut actual_value, expected_value, substitutions) + } else if has_key_wildcard { + continue; + } + act.insert(actual_key, actual_value); + } + if has_key_wildcard { + act.insert(KEY_WILDCARD.to_owned(), String(VALUE_WILDCARD.to_owned())); } } (_, _) => {} diff --git a/crates/snapbox/src/filter/redactions.rs b/crates/snapbox/src/filter/redactions.rs index b488b6c5..8914900f 100644 --- a/crates/snapbox/src/filter/redactions.rs +++ b/crates/snapbox/src/filter/redactions.rs @@ -1,13 +1,15 @@ use std::borrow::Cow; +use std::path::Path; +use std::path::PathBuf; -/// Match pattern expressions, see [`Assert`][crate::Assert] +/// Replace data with placeholders /// /// Built-in placeholders: /// - `...` on a line of its own: match multiple complete lines /// - `[..]`: match multiple characters within a line #[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct Redactions { - vars: std::collections::BTreeMap<&'static str, std::collections::BTreeSet>, + vars: std::collections::BTreeMap>, unused: std::collections::BTreeSet, } @@ -32,6 +34,17 @@ impl Redactions { /// let mut subst = snapbox::Redactions::new(); /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX); /// ``` + /// + /// With the `regex` feature, you can define patterns using regexes. + /// You can choose to replace a subset of the regex by giving it the named capture group + /// `redacted`. + /// + /// ```rust + /// # #[cfg(feature = "regex")] { + /// let mut subst = snapbox::Redactions::new(); + /// subst.insert("[OBJECT]", regex::Regex::new("(?(world|moon))").unwrap()); + /// # } + /// ``` pub fn insert( &mut self, placeholder: &'static str, @@ -39,8 +52,8 @@ impl Redactions { ) -> crate::assert::Result<()> { let placeholder = validate_placeholder(placeholder)?; let value = value.into(); - if let Some(inner) = value.inner { - self.vars.entry(placeholder).or_default().insert(inner); + if let Some(value) = value.inner { + self.vars.entry(value).or_default().insert(placeholder); } else { self.unused.insert(RedactedValueInner::Str(placeholder)); } @@ -49,7 +62,7 @@ impl Redactions { /// Insert additional match patterns /// - /// placeholders must be enclosed in `[` and `]`. + /// Placeholders must be enclosed in `[` and `]`. pub fn extend( &mut self, vars: impl IntoIterator)>, @@ -62,7 +75,10 @@ impl Redactions { pub fn remove(&mut self, placeholder: &'static str) -> crate::assert::Result<()> { let placeholder = validate_placeholder(placeholder)?; - self.vars.remove(placeholder); + self.vars.retain(|_value, placeholders| { + placeholders.retain(|p| *p != placeholder); + !placeholders.is_empty() + }); Ok(()) } @@ -81,15 +97,18 @@ impl Redactions { normalize(input, pattern, self) } - fn substitute<'v>(&self, input: &'v str) -> Cow<'v, str> { + /// Apply redaction only, no pattern-dependent globs + pub fn redact(&self, input: &str) -> String { let mut input = input.to_owned(); replace_many( &mut input, - self.vars - .iter() - .flat_map(|(var, replaces)| replaces.iter().map(|replace| (replace, *var))), + self.vars.iter().flat_map(|(value, placeholders)| { + placeholders + .iter() + .map(move |placeholder| (value, *placeholder)) + }), ); - Cow::Owned(input) + input } fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> { @@ -108,10 +127,16 @@ pub struct RedactedValue { inner: Option, } -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Clone, Debug)] enum RedactedValueInner { Str(&'static str), String(String), + Path { + native: String, + normalized: String, + }, + #[cfg(feature = "regex")] + Regex(regex::Regex), } impl RedactedValueInner { @@ -119,29 +144,147 @@ impl RedactedValueInner { match self { Self::Str(s) => buffer.find(s).map(|offset| offset..(offset + s.len())), Self::String(s) => buffer.find(s).map(|offset| offset..(offset + s.len())), + Self::Path { native, normalized } => { + match (buffer.find(native), buffer.find(normalized)) { + (Some(native_offset), Some(normalized_offset)) => { + if native_offset <= normalized_offset { + Some(native_offset..(native_offset + native.len())) + } else { + Some(normalized_offset..(normalized_offset + normalized.len())) + } + } + (Some(offset), None) => Some(offset..(offset + native.len())), + (None, Some(offset)) => Some(offset..(offset + normalized.len())), + (None, None) => None, + } + } + #[cfg(feature = "regex")] + Self::Regex(r) => { + let captures = r.captures(buffer)?; + let m = captures.name("redacted").or_else(|| captures.get(0))?; + Some(m.range()) + } + } + } + + fn as_cmp(&self) -> (usize, std::cmp::Reverse, &str) { + match self { + Self::Str(s) => (0, std::cmp::Reverse(s.len()), s), + Self::String(s) => (0, std::cmp::Reverse(s.len()), s), + Self::Path { normalized: s, .. } => (0, std::cmp::Reverse(s.len()), s), + #[cfg(feature = "regex")] + Self::Regex(r) => { + let s = r.as_str(); + (1, std::cmp::Reverse(s.len()), s) + } } } } -impl From for RedactedValue -where - C: Into>, -{ - fn from(inner: C) -> Self { - let inner = inner.into(); +impl From<&'static str> for RedactedValue { + fn from(inner: &'static str) -> Self { if inner.is_empty() { Self { inner: None } } else { - #[allow(deprecated)] Self { - inner: Some(RedactedValueInner::String(crate::utils::normalize_text( - &inner, - ))), + inner: Some(RedactedValueInner::Str(inner)), + } + } + } +} + +impl From for RedactedValue { + fn from(inner: String) -> Self { + if inner.is_empty() { + Self { inner: None } + } else { + Self { + inner: Some(RedactedValueInner::String(inner)), + } + } + } +} + +impl From<&'_ String> for RedactedValue { + fn from(inner: &'_ String) -> Self { + inner.clone().into() + } +} + +impl From> for RedactedValue { + fn from(inner: Cow<'static, str>) -> Self { + match inner { + Cow::Borrowed(s) => s.into(), + Cow::Owned(s) => s.into(), + } + } +} + +impl From<&'static Path> for RedactedValue { + fn from(inner: &'static Path) -> Self { + inner.to_owned().into() + } +} + +impl From for RedactedValue { + fn from(inner: PathBuf) -> Self { + if inner.as_os_str().is_empty() { + Self { inner: None } + } else { + let native = match inner.into_os_string().into_string() { + Ok(s) => s, + Err(os) => PathBuf::from(os).display().to_string(), + }; + let normalized = crate::filter::normalize_paths(&native); + Self { + inner: Some(RedactedValueInner::Path { native, normalized }), } } } } +impl From<&'_ PathBuf> for RedactedValue { + fn from(inner: &'_ PathBuf) -> Self { + inner.clone().into() + } +} + +#[cfg(feature = "regex")] +impl From for RedactedValue { + fn from(inner: regex::Regex) -> Self { + Self { + inner: Some(RedactedValueInner::Regex(inner)), + } + } +} + +#[cfg(feature = "regex")] +impl From<&'_ regex::Regex> for RedactedValue { + fn from(inner: &'_ regex::Regex) -> Self { + inner.clone().into() + } +} + +impl PartialOrd for RedactedValueInner { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.as_cmp().cmp(&other.as_cmp())) + } +} + +impl Ord for RedactedValueInner { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_cmp().cmp(&other.as_cmp()) + } +} + +impl PartialEq for RedactedValueInner { + fn eq(&self, other: &Self) -> bool { + self.as_cmp().eq(&other.as_cmp()) + } +} + +impl Eq for RedactedValueInner {} + /// Replacements is `(from, to)` fn replace_many<'a>( buffer: &mut String, @@ -177,97 +320,50 @@ fn normalize(input: &str, pattern: &str, redactions: &Redactions) -> String { return input.to_owned(); } - let mut normalized: Vec> = Vec::new(); - let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect(); - let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect(); + let input = redactions.redact(input); + let mut normalized: Vec<&str> = Vec::new(); let mut input_index = 0; - let mut pattern_index = 0; - 'outer: loop { - let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) { - *pattern_line - } else { - normalized.extend( - input_lines[input_index..] - .iter() - .copied() - .map(|s| redactions.substitute(s)), - ); - break 'outer; - }; - let next_pattern_index = pattern_index + 1; - - let input_line = if let Some(input_line) = input_lines.get(input_index) { - *input_line - } else { - break 'outer; - }; - let next_input_index = input_index + 1; - - if line_matches(input_line, pattern_line, redactions) { - pattern_index = next_pattern_index; - input_index = next_input_index; - normalized.push(Cow::Borrowed(pattern_line)); - continue 'outer; - } else if is_line_elide(pattern_line) { - let next_pattern_line: &str = - if let Some(pattern_line) = pattern_lines.get(next_pattern_index) { - pattern_line - } else { - normalized.push(Cow::Borrowed(pattern_line)); - break 'outer; - }; - if let Some(future_input_index) = input_lines[input_index..] - .iter() - .enumerate() - .find(|(_, l)| **l == next_pattern_line) - .map(|(i, _)| input_index + i) - { - normalized.push(Cow::Borrowed(pattern_line)); - pattern_index = next_pattern_index; - input_index = future_input_index; - continue 'outer; + let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(&input).collect(); + let mut pattern_lines = crate::utils::LinesWithTerminator::new(pattern).peekable(); + 'outer: while let Some(pattern_line) = pattern_lines.next() { + if is_line_elide(pattern_line) { + if let Some(next_pattern_line) = pattern_lines.peek() { + for (index_offset, next_input_line) in + input_lines[input_index..].iter().copied().enumerate() + { + if line_matches(next_input_line, next_pattern_line, redactions) { + normalized.push(pattern_line); + input_index += index_offset; + continue 'outer; + } + } + // Give up doing further normalization + break; } else { - normalized.extend( - input_lines[input_index..] - .iter() - .copied() - .map(|s| redactions.substitute(s)), - ); - break 'outer; + // Give up doing further normalization + normalized.push(pattern_line); + // captured rest so don't copy remaining lines over + input_index = input_lines.len(); + break; } } else { - // Find where we can pick back up for normalizing - for future_input_index in next_input_index..input_lines.len() { - let future_input_line = input_lines[future_input_index]; - if let Some(future_pattern_index) = pattern_lines[next_pattern_index..] - .iter() - .enumerate() - .find(|(_, l)| **l == future_input_line || is_line_elide(l)) - .map(|(i, _)| next_pattern_index + i) - { - normalized.extend( - input_lines[input_index..future_input_index] - .iter() - .copied() - .map(|s| redactions.substitute(s)), - ); - pattern_index = future_pattern_index; - input_index = future_input_index; - continue 'outer; - } + let Some(input_line) = input_lines.get(input_index) else { + // Give up doing further normalization + break; + }; + + if line_matches(input_line, pattern_line, redactions) { + input_index += 1; + normalized.push(pattern_line); + } else { + // Give up doing further normalization + break; } - - normalized.extend( - input_lines[input_index..] - .iter() - .copied() - .map(|s| redactions.substitute(s)), - ); - break 'outer; } } + normalized.extend(input_lines[input_index..].iter().copied()); normalized.join("") } @@ -275,24 +371,20 @@ fn is_line_elide(line: &str) -> bool { line == "...\n" || line == "..." } -fn line_matches(line: &str, pattern: &str, redactions: &Redactions) -> bool { - if line == pattern { +fn line_matches(mut input: &str, pattern: &str, redactions: &Redactions) -> bool { + if input == pattern { return true; } - let subbed = redactions.substitute(line); - let mut line = subbed.as_ref(); - let pattern = redactions.clear(pattern); - let mut sections = pattern.split("[..]").peekable(); while let Some(section) = sections.next() { - if let Some(remainder) = line.strip_prefix(section) { + if let Some(remainder) = input.strip_prefix(section) { if let Some(next_section) = sections.peek() { if next_section.is_empty() { - line = ""; + input = ""; } else if let Some(restart_index) = remainder.find(next_section) { - line = &remainder[restart_index..]; + input = &remainder[restart_index..]; } } else { return remainder.is_empty(); @@ -367,7 +459,7 @@ mod test { fn elide_delimited_with_sub() { let input = "Hello World\nHow are you?\nGoodbye World"; let pattern = "Hello [..]\n...\nGoodbye [..]"; - let expected = "Hello [..]\nHow are you?\nGoodbye World"; + let expected = "Hello [..]\n...\nGoodbye [..]"; let actual = normalize(input, pattern, &Redactions::new()); assert_eq!(expected, actual); } @@ -412,7 +504,7 @@ mod test { fn post_diverge_elide() { let input = "Hello\nWorld\nGoodbye\nSir"; let pattern = "Hello\nMoon\nGoodbye\n..."; - let expected = "Hello\nWorld\nGoodbye\n..."; + let expected = "Hello\nWorld\nGoodbye\nSir"; let actual = normalize(input, pattern, &Redactions::new()); assert_eq!(expected, actual); } @@ -496,6 +588,39 @@ mod test { assert_eq!(actual, pattern); } + #[test] + fn substitute_path() { + let input = "input: /home/epage"; + let pattern = "input: [HOME]"; + let mut sub = Redactions::new(); + let sep = std::path::MAIN_SEPARATOR.to_string(); + let redacted = PathBuf::from(sep).join("home").join("epage"); + sub.insert("[HOME]", redacted).unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } + + #[test] + fn substitute_overlapping_path() { + let input = "\ +a: /home/epage +b: /home/epage/snapbox"; + let pattern = "\ +a: [A] +b: [B]"; + let mut sub = Redactions::new(); + let sep = std::path::MAIN_SEPARATOR.to_string(); + let redacted = PathBuf::from(&sep).join("home").join("epage"); + sub.insert("[A]", redacted).unwrap(); + let redacted = PathBuf::from(sep) + .join("home") + .join("epage") + .join("snapbox"); + sub.insert("[B]", redacted).unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } + #[test] fn substitute_disabled() { let input = "cargo"; @@ -505,4 +630,31 @@ mod test { let actual = normalize(input, pattern, &sub); assert_eq!(actual, pattern); } + + #[test] + #[cfg(feature = "regex")] + fn substitute_regex_unnamed() { + let input = "Hello world!"; + let pattern = "Hello [OBJECT]!"; + let mut sub = Redactions::new(); + sub.insert("[OBJECT]", regex::Regex::new("world").unwrap()) + .unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } + + #[test] + #[cfg(feature = "regex")] + fn substitute_regex_named() { + let input = "Hello world!"; + let pattern = "Hello [OBJECT]!"; + let mut sub = Redactions::new(); + sub.insert( + "[OBJECT]", + regex::Regex::new("(?world)!").unwrap(), + ) + .unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } } diff --git a/crates/snapbox/src/filter/test.rs b/crates/snapbox/src/filter/test.rs index 0f8d1332..ed83bbe5 100644 --- a/crates/snapbox/src/filter/test.rs +++ b/crates/snapbox/src/filter/test.rs @@ -18,7 +18,7 @@ fn json_normalize_paths_and_lines() { #[test] #[cfg(feature = "json")] -fn json_normalize_obj_paths_and_lines() { +fn json_normalize_obj_value_paths_and_lines() { let json = json!({ "person": { "name": "John\\Doe\r\n", @@ -44,6 +44,34 @@ fn json_normalize_obj_paths_and_lines() { assert_eq!(Data::json(assert), data); } +#[test] +#[cfg(feature = "json")] +fn json_normalize_obj_key_paths_and_lines() { + let json = json!({ + "person": { + "John\\Doe\r\n": "name", + "Jo\\hn\r\n": "nickname", + } + }); + let data = Data::json(json); + let data = FilterPaths.filter(data); + let assert = json!({ + "person": { + "John/Doe\r\n": "name", + "Jo/hn\r\n": "nickname", + } + }); + assert_eq!(Data::json(assert), data); + let data = FilterNewlines.filter(data); + let assert = json!({ + "person": { + "John/Doe\n": "name", + "Jo/hn\n": "nickname", + } + }); + assert_eq!(Data::json(assert), data); +} + #[test] #[cfg(feature = "json")] fn json_normalize_array_paths_and_lines() { @@ -155,6 +183,90 @@ fn json_normalize_matches_diff_order_array() { } } +#[test] +#[cfg(feature = "json")] +fn json_obj_redact_keys() { + let expected = json!({ + "[A]": "value-a", + "[B]": "value-b", + "[C]": "value-c", + }); + let expected = Data::json(expected); + let actual = json!({ + "key-a": "value-a", + "key-b": "value-b", + "key-c": "value-c", + }); + let actual = Data::json(actual); + let mut sub = Redactions::new(); + sub.insert("[A]", "key-a").unwrap(); + sub.insert("[B]", "key-b").unwrap(); + sub.insert("[C]", "key-c").unwrap(); + let actual = FilterRedactions::new(&sub, &expected).filter(actual); + + let expected_actual = json!({ + "[A]": "value-a", + "[B]": "value-b", + "[C]": "value-c", + }); + let expected_actual = Data::json(expected_actual); + assert_eq!(actual, expected_actual); +} + +#[test] +#[cfg(feature = "json")] +fn json_obj_redact_with_disparate_keys() { + let expected = json!({ + "a": "[A]", + "b": "[B]", + "c": "[C]", + }); + let expected = Data::json(expected); + let actual = json!({ + "a": "value-a", + "c": "value-c", + }); + let actual = Data::json(actual); + let mut sub = Redactions::new(); + sub.insert("[A]", "value-a").unwrap(); + sub.insert("[B]", "value-b").unwrap(); + sub.insert("[C]", "value-c").unwrap(); + let actual = FilterRedactions::new(&sub, &expected).filter(actual); + + let expected_actual = json!({ + "a": "[A]", + "c": "[C]", + }); + let expected_actual = Data::json(expected_actual); + assert_eq!(actual, expected_actual); +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_key() { + let expected = json!({ + "a": "value-a", + "c": "value-c", + "...": "{...}", + }); + let expected = Data::json(expected); + let actual = json!({ + "a": "value-a", + "b": "value-b", + "c": "value-c", + }); + let actual = Data::json(actual); + let actual = FilterRedactions::new(&Default::default(), &expected).filter(actual); + + let expected_actual = json!({ + "a": "value-a", + "c": "value-c", + "...": "{...}", + }); + let expected_actual = Data::json(expected_actual); + assert_eq!(actual, expected_actual); +} + #[test] #[cfg(feature = "json")] fn json_normalize_wildcard_object_first() { diff --git a/crates/snapbox/src/harness.rs b/crates/snapbox/src/harness.rs deleted file mode 100644 index 0a61d76d..00000000 --- a/crates/snapbox/src/harness.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! [`Harness`] for discovering test inputs and asserting against snapshot files -//! -//! This is a custom test harness and should be put in its own test binary with -//! [`test.harness = false`](https://doc.rust-lang.org/stable/cargo/reference/cargo-targets.html#the-harness-field). -//! -//! # Examples -//! -//! ```rust,no_run -//! snapbox::harness::Harness::new( -//! "tests/fixtures/invalid", -//! setup, -//! test, -//! ) -//! .select(["tests/cases/*.in"]) -//! .test(); -//! -//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case { -//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned(); -//! let expected = input_path.with_extension("out"); -//! snapbox::harness::Case { -//! name, -//! fixture: input_path, -//! expected, -//! } -//! } -//! -//! fn test(input_path: &std::path::Path) -> Result> { -//! let raw = std::fs::read_to_string(input_path)?; -//! let num = raw.parse::()?; -//! -//! let actual = num + 10; -//! -//! Ok(actual) -//! } -//! ``` - -#![allow(deprecated)] - -use crate::data::DataFormat; -use crate::Action; - -use libtest_mimic::Trial; - -/// [`Fallback dependenciesforfallback-dependenciess -/// [`Build script directivesck-build-script-directivess -/// [`When to use packages or workspaces?ck-when-to-use-packages-or-workspacess -/// [`Cargo and rustupes?cargo-and-rustups -/// -/// See [`harness`][crate::harness] for more details -#[deprecated(since = "0.5.12", note = "Replaced with `tryfn` crate")] -pub struct Harness { - root: std::path::PathBuf, - overrides: Option, - setup: S, - test: T, - config: crate::Assert, -} - -impl Harness -where - I: std::fmt::Display, - E: std::fmt::Display, - S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static, - T: Fn(&std::path::Path) -> Result + Send + Sync + 'static + Clone, -{ - /// Specify where the test scenarios - /// - /// - `input_root`: where to find the files. See [`Self::select`] for restricting what files - /// are considered - /// - `setup`: Given a path, choose the test name and the output location - /// - `test`: Given a path, return the actual output value - pub fn new(input_root: impl Into, setup: S, test: T) -> Self { - Self { - root: input_root.into(), - overrides: None, - setup, - test, - config: crate::Assert::new().action_env(crate::assert::DEFAULT_ACTION_ENV), - } - } - - /// Path patterns for selecting input files - /// - /// This used gitignore syntax - pub fn select<'p>(mut self, patterns: impl IntoIterator) -> Self { - let mut overrides = ignore::overrides::OverrideBuilder::new(&self.root); - for line in patterns { - overrides.add(line).unwrap(); - } - self.overrides = Some(overrides.build().unwrap()); - self - } - - /// Read the failure action from an environment variable - #[deprecated(since = "0.1.0", note = "Replaced with `Harness::with_assert`")] - pub fn action_env(mut self, var_name: &str) -> Self { - self.config = self.config.action_env(var_name); - self - } - - /// Override the failure action - #[deprecated(since = "0.1.0", note = "Replaced with `Harness::with_assert`")] - pub fn action(mut self, action: Action) -> Self { - self.config = self.config.action(action); - self - } - - /// Customize the assertion behavior - pub fn with_assert(mut self, config: crate::Assert) -> Self { - self.config = config; - self - } - - /// Run tests - pub fn test(self) -> ! { - let mut walk = ignore::WalkBuilder::new(&self.root); - walk.standard_filters(false); - let tests = walk.build().filter_map(|entry| { - let entry = entry.unwrap(); - let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false); - let path = entry.into_path(); - if let Some(overrides) = &self.overrides { - overrides - .matched(&path, is_dir) - .is_whitelist() - .then_some(path) - } else { - Some(path) - } - }); - - let shared_config = std::sync::Arc::new(self.config); - let tests: Vec<_> = tests - .into_iter() - .map(|path| { - let case = (self.setup)(path); - let test = self.test.clone(); - let config = shared_config.clone(); - Trial::test(case.name.clone(), move || { - let expected = crate::Data::read_from(&case.expected, Some(DataFormat::Text)); - let actual = (test)(&case.fixture)?; - let actual = actual.to_string(); - let actual = crate::Data::text(actual); - config.try_eq(Some(&case.name), actual, expected.raw())?; - Ok(()) - }) - .with_ignored_flag(shared_config.action == Action::Ignore) - }) - .collect(); - - let args = libtest_mimic::Arguments::from_args(); - libtest_mimic::run(&args, tests).exit() - } -} - -/// A test case enumerated by the [`Harness`] with data from the `setup` function -/// -/// See [`harness`][crate::harness] for more details -pub struct Case { - /// Display name - pub name: String, - /// Input for the test - pub fixture: std::path::PathBuf, - /// What the actual output should be compared against or updated - pub expected: std::path::PathBuf, -} diff --git a/crates/snapbox/src/lib.rs b/crates/snapbox/src/lib.rs index 4916f569..b791c5f1 100644 --- a/crates/snapbox/src/lib.rs +++ b/crates/snapbox/src/lib.rs @@ -16,6 +16,7 @@ //! - [trycmd](https://crates.io/crates/trycmd): For running a lot of blunt tests (limited test predicates) //! - Particular attention is given to allow the test data to be pulled into documentation, like //! with [mdbook](https://rust-lang.github.io/mdBook/) +//! - [tryfn](https://crates.io/crates/tryfn): For running a lot of simple input/output tests //! - `snapbox`: When you want something like `trycmd` in one off //! cases or you need to customize `trycmd`s behavior. //! - [assert_cmd](https://crates.io/crates/assert_cmd) + @@ -27,7 +28,6 @@ //! //! Testing Functions: //! - [`assert_data_eq!`] for quick and dirty snapshotting -//! - [`harness::Harness`] for discovering test inputs and asserting against snapshot files: //! //! Testing Commands: //! - [`cmd::Command`]: Process spawning for testing of non-interactive commands @@ -35,7 +35,7 @@ //! [`Output`][std::process::Output]. //! //! Testing Filesystem Interactions: -//! - [`path::PathFixture`]: Working directory for tests +//! - [`dir::DirRoot`]: Working directory for tests //! - [`Assert`]: Diff a directory against files present in a pattern directory //! //! You can also build your own version of these with the lower-level building blocks these are @@ -55,39 +55,7 @@ //! let actual = "..."; //! snapbox::Assert::new() //! .action_env("SNAPSHOTS") -//! .eq_(actual, snapbox::file!["help_output_is_clean.txt"]); -//! ``` -//! -//! [`harness::Harness`] -#![cfg_attr(not(feature = "harness"), doc = " ```rust,ignore")] -#![cfg_attr(feature = "harness", doc = " ```rust,no_run")] -//! snapbox::harness::Harness::new( -//! "tests/fixtures/invalid", -//! setup, -//! test, -//! ) -//! .select(["tests/cases/*.in"]) -//! .action_env("SNAPSHOTS") -//! .test(); -//! -//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case { -//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned(); -//! let expected = input_path.with_extension("out"); -//! snapbox::harness::Case { -//! name, -//! fixture: input_path, -//! expected, -//! } -//! } -//! -//! fn test(input_path: &std::path::Path) -> Result> { -//! let raw = std::fs::read_to_string(input_path)?; -//! let num = raw.parse::()?; -//! -//! let actual = num + 10; -//! -//! Ok(actual) -//! } +//! .eq(actual, snapbox::file!["help_output_is_clean.txt"]); //! ``` //! //! [trycmd]: https://docs.rs/trycmd @@ -103,15 +71,9 @@ pub mod cmd; pub mod data; pub mod dir; pub mod filter; -pub mod path; pub mod report; pub mod utils; -#[cfg(feature = "harness")] -pub mod harness; - -#[deprecated(since = "0.5.11", note = "Replaced with `assert::Assert`")] -pub use assert::Action; pub use assert::Assert; pub use data::Data; pub use data::IntoData; @@ -123,17 +85,6 @@ pub use filter::Redactions; #[doc(hidden)] pub use snapbox_macros::debug; -#[deprecated(since = "0.5.11", note = "Replaced with `Redactions`")] -pub type Substitutions = filter::Redactions; - -#[deprecated(since = "0.5.11", note = "Replaced with `assert::DEFAULT_ACTION_ENV`")] -pub const DEFAULT_ACTION_ENV: &str = assert::DEFAULT_ACTION_ENV; - -#[deprecated(since = "0.5.11", note = "Replaced with `assert::Result`")] -pub type Result = std::result::Result; -#[deprecated(since = "0.5.11", note = "Replaced with `assert::Error`")] -pub type Error = assert::Error; - /// Easier access to common traits pub mod prelude { pub use crate::IntoData; @@ -142,71 +93,6 @@ pub mod prelude { pub use crate::ToDebug; } -/// Check if a value is the same as an expected value -/// -/// When the content is text, newlines are normalized. -/// -/// ```rust -/// # use snapbox::assert_eq; -/// let output = "something"; -/// let expected = "something"; -/// assert_eq(expected, output); -/// ``` -/// -/// Can combine this with [`file!`] -/// ```rust,no_run -/// # use snapbox::assert_eq; -/// # use snapbox::file; -/// let actual = "something"; -/// assert_eq(file!["output.txt"], actual); -/// ``` -#[track_caller] -#[deprecated( - since = "0.5.11", - note = "Replaced with `assert_data_eq!(actual, expected.raw())`" -)] -pub fn assert_eq(expected: impl Into, actual: impl Into) { - Assert::new() - .action_env(assert::DEFAULT_ACTION_ENV) - .eq_(actual, expected.into().raw()); -} - -/// Check if a value matches a pattern -/// -/// Pattern syntax: -/// - `...` is a line-wildcard when on a line by itself -/// - `[..]` is a character-wildcard when inside a line -/// - `[EXE]` matches `.exe` on Windows -/// -/// Normalization: -/// - Newlines -/// - `\` to `/` -/// -/// ```rust -/// # use snapbox::assert_matches; -/// let output = "something"; -/// let expected = "so[..]g"; -/// assert_matches(expected, output); -/// ``` -/// -/// Can combine this with [`file!`] -/// ```rust,no_run -/// # use snapbox::assert_matches; -/// # use snapbox::file; -/// let actual = "something"; -/// assert_matches(file!["output.txt"], actual); -/// ``` -#[track_caller] -#[deprecated( - since = "0.5.11", - note = "Replaced with `assert_data_eq!(actual, expected)`" -)] -pub fn assert_matches(pattern: impl Into, actual: impl Into) { - Assert::new() - .action_env(assert::DEFAULT_ACTION_ENV) - .eq_(actual, pattern); -} - /// Check if a path matches the content of another path, recursively /// /// When the content is text, newlines are normalized. diff --git a/crates/snapbox/src/macros.rs b/crates/snapbox/src/macros.rs index 6adf8483..f7978c88 100644 --- a/crates/snapbox/src/macros.rs +++ b/crates/snapbox/src/macros.rs @@ -4,6 +4,8 @@ /// - `...` is a line-wildcard when on a line by itself /// - `[..]` is a character-wildcard when inside a line /// - `[EXE]` matches `.exe` on Windows +/// - `"{...}"` is a JSON value wildcard +/// - `"...": "{...}"` is a JSON key-value wildcard /// - `\` to `/` /// - Newlines /// @@ -41,7 +43,7 @@ macro_rules! assert_data_eq { let expected = $crate::IntoData::into_data($expected); $crate::Assert::new() .action_env($crate::assert::DEFAULT_ACTION_ENV) - .eq_(actual, expected); + .eq(actual, expected); }}; } diff --git a/crates/snapbox/src/path.rs b/crates/snapbox/src/path.rs deleted file mode 100644 index de357475..00000000 --- a/crates/snapbox/src/path.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Initialize working directories and assert on how they've changed - -#[doc(inline)] -pub use crate::cargo_rustc_current_dir; -#[doc(inline)] -pub use crate::current_dir; -#[doc(inline)] -pub use crate::current_rs; - -/// Working directory for tests -#[deprecated(since = "0.5.11", note = "Replaced with dir::DirRoot")] -pub type PathFixture = crate::dir::DirRoot; - -pub use crate::dir::FileType; -pub use crate::dir::PathDiff; - -/// Recursively walk a path -/// -/// Note: Ignores `.keep` files -#[deprecated(since = "0.5.11", note = "Replaced with dir::Walk")] -#[cfg(feature = "dir")] -pub type Walk = crate::dir::Walk; - -/// Copy a template into a [`PathFixture`] -/// -/// Note: Generally you'll use [`PathFixture::with_template`] instead. -/// -/// Note: Ignores `.keep` files -#[deprecated(since = "0.5.11", note = "Replaced with dir::copy_template")] -#[cfg(feature = "dir")] -pub fn copy_template( - source: impl AsRef, - dest: impl AsRef, -) -> crate::assert::Result<()> { - crate::dir::copy_template(source, dest) -} - -#[deprecated(since = "0.5.11", note = "Replaced with dir::resolve_dir")] -pub fn resolve_dir( - path: impl AsRef, -) -> Result { - crate::dir::resolve_dir(path) -} - -#[deprecated(since = "0.5.11", note = "Replaced with dir::strip_trailing_slash")] -pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path { - crate::dir::strip_trailing_slash(path) -} diff --git a/crates/snapbox/src/report/color.rs b/crates/snapbox/src/report/color.rs index 3df14402..85bcac23 100644 --- a/crates/snapbox/src/report/color.rs +++ b/crates/snapbox/src/report/color.rs @@ -28,28 +28,6 @@ impl Palette { Self::default() } - #[deprecated(since = "0.4.9", note = "Renamed to `Palette::color")] - pub fn always() -> Self { - Self::color() - } - - #[deprecated(since = "0.4.9", note = "Renamed to `Palette::plain")] - pub fn never() -> Self { - Self::plain() - } - - #[deprecated( - since = "0.4.9", - note = "Use `Palette::always`, `auto` behavior is now implicit" - )] - pub fn auto() -> Self { - if is_colored() { - Self::color() - } else { - Self::plain() - } - } - pub fn info(self, item: D) -> Styled { Styled::new(item, self.info) } @@ -75,17 +53,6 @@ impl Palette { } } -fn is_colored() -> bool { - #[cfg(feature = "color")] - { - anstream::AutoStream::choice(&std::io::stderr()) != anstream::ColorChoice::Never - } - #[cfg(not(feature = "color"))] - { - false - } -} - pub(crate) use anstyle::Style; #[derive(Debug)] diff --git a/crates/snapbox/src/utils/mod.rs b/crates/snapbox/src/utils/mod.rs index 2dbb862e..8f3a65af 100644 --- a/crates/snapbox/src/utils/mod.rs +++ b/crates/snapbox/src/utils/mod.rs @@ -8,27 +8,3 @@ pub use crate::cargo_rustc_current_dir; pub use crate::current_dir; #[doc(inline)] pub use crate::current_rs; - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::normalize_lines`")] -pub fn normalize_lines(data: &str) -> String { - crate::filter::normalize_lines(data) -} - -#[deprecated(since = "0.5.11", note = "Replaced with `filter::normalize_paths`")] -pub fn normalize_paths(data: &str) -> String { - crate::filter::normalize_paths(data) -} - -/// "Smart" text normalization -/// -/// This includes -/// - Line endings -/// - Path separators -#[deprecated( - since = "0.5.11", - note = "Replaced with `filter::normalize_paths(filter::normalize_lines(...))`" -)] -pub fn normalize_text(data: &str) -> String { - #[allow(deprecated)] - normalize_paths(&normalize_lines(data)) -} diff --git a/crates/snapbox/tests/testsuite/assert.rs b/crates/snapbox/tests/testsuite/assert.rs index 416ec884..6c34ad3c 100644 --- a/crates/snapbox/tests/testsuite/assert.rs +++ b/crates/snapbox/tests/testsuite/assert.rs @@ -15,22 +15,10 @@ line1 line2 ", str![[r#" - line1 - line2 - "#]] - .indent(true), - ); - - assert_data_eq!( - "\ line1 line2 -", - str![[r#" -line1 - line2 -"#]] - .indent(false), + +"#]], ); } diff --git a/crates/trycmd/src/cases.rs b/crates/trycmd/src/cases.rs index 196891d7..273309fb 100644 --- a/crates/trycmd/src/cases.rs +++ b/crates/trycmd/src/cases.rs @@ -137,6 +137,8 @@ impl TestCases { var: &'static str, value: impl Into>, ) -> Result<&Self, crate::Error> { + let value = value.into(); + let value = snapbox::filter::normalize_paths(&snapbox::filter::normalize_lines(&value)); self.substitutions.borrow_mut().insert(var, value)?; Ok(self) } @@ -148,7 +150,14 @@ impl TestCases { &self, vars: impl IntoIterator>)>, ) -> Result<&Self, crate::Error> { - self.substitutions.borrow_mut().extend(vars)?; + self.substitutions + .borrow_mut() + .extend(vars.into_iter().map(|(var, value)| { + let value = value.into(); + let value = + snapbox::filter::normalize_paths(&snapbox::filter::normalize_lines(&value)); + (var, value) + }))?; Ok(self) } diff --git a/crates/trycmd/src/runner.rs b/crates/trycmd/src/runner.rs index 96159164..de2e6264 100644 --- a/crates/trycmd/src/runner.rs +++ b/crates/trycmd/src/runner.rs @@ -201,14 +201,10 @@ impl Case { }; let mut substitutions = substitutions.clone(); if let Some(root) = fs_context.path() { - substitutions - .insert("[ROOT]", root.display().to_string()) - .unwrap(); + substitutions.insert("[ROOT]", root.to_owned()).unwrap(); } if let Some(cwd) = cwd.clone().or_else(|| std::env::current_dir().ok()) { - substitutions - .insert("[CWD]", cwd.display().to_string()) - .unwrap(); + substitutions.insert("[CWD]", cwd).unwrap(); } substitutions .insert("[EXE]", std::env::consts::EXE_SUFFIX) diff --git a/crates/trycmd/tests/testsuite/schema.rs b/crates/trycmd/tests/testsuite/schema.rs index 002b5697..72de3899 100644 --- a/crates/trycmd/tests/testsuite/schema.rs +++ b/crates/trycmd/tests/testsuite/schema.rs @@ -5,5 +5,5 @@ fn dump_schema() { snapbox::cmd::Command::new(bin_path) .assert() .success() - .stdout_eq_(snapbox::file!["../../schema.json"]); + .stdout_eq(snapbox::file!["../../schema.json"]); } diff --git a/crates/tryfn/src/lib.rs b/crates/tryfn/src/lib.rs index b04ba4b6..9f76016c 100644 --- a/crates/tryfn/src/lib.rs +++ b/crates/tryfn/src/lib.rs @@ -73,6 +73,8 @@ where /// - `...` is a line-wildcard when on a line by itself /// - `[..]` is a character-wildcard when inside a line /// - `[EXE]` matches `.exe` on Windows + /// - `"{...}"` is a JSON value wildcard + /// - `"...": "{...}"` is a JSON key-value wildcard /// - `\` to `/` /// - Newlines ///