From e6c6b66f5158e59e87f20d528f05e2410c253e06 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 14 Feb 2024 11:00:40 -0600 Subject: [PATCH] refactor(snap): Split up data mod --- crates/snapbox/src/data/format.rs | 21 + crates/snapbox/src/data/mod.rs | 754 +-------------------------- crates/snapbox/src/data/normalize.rs | 168 ++++++ crates/snapbox/src/data/source.rs | 35 ++ crates/snapbox/src/data/tests.rs | 493 ++++++++++++++++++ 5 files changed, 731 insertions(+), 740 deletions(-) create mode 100644 crates/snapbox/src/data/format.rs create mode 100644 crates/snapbox/src/data/normalize.rs create mode 100644 crates/snapbox/src/data/source.rs create mode 100644 crates/snapbox/src/data/tests.rs diff --git a/crates/snapbox/src/data/format.rs b/crates/snapbox/src/data/format.rs new file mode 100644 index 00000000..3bfea284 --- /dev/null +++ b/crates/snapbox/src/data/format.rs @@ -0,0 +1,21 @@ +#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)] +pub enum DataFormat { + Error, + Binary, + #[default] + Text, + #[cfg(feature = "json")] + Json, +} + +impl DataFormat { + pub fn ext(self) -> &'static str { + match self { + Self::Error => "txt", + Self::Binary => "bin", + Self::Text => "txt", + #[cfg(feature = "json")] + Self::Json => "json", + } + } +} diff --git a/crates/snapbox/src/data/mod.rs b/crates/snapbox/src/data/mod.rs index 4e6107b4..b2123c81 100644 --- a/crates/snapbox/src/data/mod.rs +++ b/crates/snapbox/src/data/mod.rs @@ -1,3 +1,16 @@ +mod format; +mod normalize; +mod source; +#[cfg(test)] +mod tests; + +pub use format::DataFormat; +pub use normalize::Normalize; +pub use normalize::NormalizeMatches; +pub use normalize::NormalizeNewlines; +pub use normalize::NormalizePaths; +pub use source::DataSource; + /// Declare an expected value for an assert from a file /// /// This is relative to the source file the macro is run from @@ -159,7 +172,7 @@ impl Data { /// Overwrite a snapshot pub fn write_to(&self, source: &DataSource) -> Result<(), crate::Error> { match &source.inner { - DataSourceInner::Path(p) => self.write_to_path(p), + source::DataSourceInner::Path(p) => self.write_to_path(p), } } @@ -347,230 +360,6 @@ impl<'s> From<&'s str> for Data { } } -#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)] -pub enum DataFormat { - Error, - Binary, - #[default] - Text, - #[cfg(feature = "json")] - Json, -} - -impl DataFormat { - pub fn ext(self) -> &'static str { - match self { - Self::Error => "txt", - Self::Binary => "bin", - Self::Text => "txt", - #[cfg(feature = "json")] - Self::Json => "json", - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DataSource { - inner: DataSourceInner, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum DataSourceInner { - Path(std::path::PathBuf), -} - -impl DataSource { - pub fn path(path: impl Into) -> Self { - Self { - inner: DataSourceInner::Path(path.into()), - } - } - - pub fn is_path(&self) -> bool { - self.as_path().is_some() - } - - pub fn as_path(&self) -> Option<&std::path::Path> { - match &self.inner { - DataSourceInner::Path(path) => Some(path.as_ref()), - } - } -} - -impl std::fmt::Display for DataSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.inner { - DataSourceInner::Path(path) => crate::path::display_relpath(path).fmt(f), - } - } -} - -pub trait Normalize { - fn normalize(&self, data: Data) -> Data; -} - -pub struct NormalizeNewlines; -impl Normalize for NormalizeNewlines { - fn normalize(&self, data: Data) -> Data { - let mut new = match data.inner { - DataInner::Error(err) => Data::error(err), - DataInner::Binary(bin) => Data::binary(bin), - DataInner::Text(text) => { - let lines = crate::utils::normalize_lines(&text); - Data::text(lines) - } - #[cfg(feature = "json")] - DataInner::Json(value) => { - let mut value = value; - normalize_value(&mut value, crate::utils::normalize_lines); - Data::json(value) - } - }; - new.source = data.source; - new - } -} - -pub struct NormalizePaths; -impl Normalize for NormalizePaths { - fn normalize(&self, data: Data) -> Data { - let mut new = match data.inner { - DataInner::Error(err) => Data::error(err), - DataInner::Binary(bin) => Data::binary(bin), - DataInner::Text(text) => { - let lines = crate::utils::normalize_paths(&text); - Data::text(lines) - } - #[cfg(feature = "json")] - DataInner::Json(value) => { - let mut value = value; - normalize_value(&mut value, crate::utils::normalize_paths); - Data::json(value) - } - }; - new.source = data.source; - new - } -} - -pub struct NormalizeMatches<'a> { - substitutions: &'a crate::Substitutions, - pattern: &'a Data, -} - -impl<'a> NormalizeMatches<'a> { - pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self { - NormalizeMatches { - substitutions, - pattern, - } - } -} - -impl Normalize for NormalizeMatches<'_> { - fn normalize(&self, data: Data) -> Data { - let mut new = match data.inner { - DataInner::Error(err) => Data::error(err), - DataInner::Binary(bin) => Data::binary(bin), - DataInner::Text(text) => { - let lines = self - .substitutions - .normalize(&text, &self.pattern.render().unwrap()); - Data::text(lines) - } - #[cfg(feature = "json")] - DataInner::Json(value) => { - let mut value = value; - if let DataInner::Json(exp) = &self.pattern.inner { - normalize_value_matches(&mut value, exp, self.substitutions); - } - Data::json(value) - } - }; - new.source = data.source; - new - } -} - -#[cfg(feature = "structured-data")] -fn normalize_value(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) - } - } - serde_json::Value::Object(obj) => { - for (_, value) in obj.iter_mut() { - normalize_value(value, op) - } - } - _ => {} - } -} - -#[cfg(feature = "structured-data")] -fn normalize_value_matches( - actual: &mut serde_json::Value, - expected: &serde_json::Value, - substitutions: &crate::Substitutions, -) { - use serde_json::Value::*; - match (actual, expected) { - // "{...}" is a wildcard - (act, String(exp)) if exp == "{...}" => { - *act = serde_json::json!("{...}"); - } - (String(act), String(exp)) => { - *act = substitutions.normalize(act, exp); - } - (Array(act), Array(exp)) => { - let wildcard = String("{...}".to_string()); - let mut sections = exp.split(|e| e == &wildcard).peekable(); - let mut processed = 0; - while let Some(expected_subset) = sections.next() { - // Process all values in the current section - if !expected_subset.is_empty() { - let actual_subset = &mut act[processed..processed + expected_subset.len()]; - for (a, e) in actual_subset.iter_mut().zip(expected_subset) { - normalize_value_matches(a, e, substitutions); - } - processed += expected_subset.len(); - } - - if let Some(next_section) = sections.peek() { - // If the next section has nothing in it, replace from processed to end with - // a single "{...}" - if next_section.is_empty() { - act.splice(processed.., vec![wildcard.clone()]); - processed += 1; - } else { - let first = next_section.first().unwrap(); - // Replace everything up until the value we are looking for with - // a single "{...}". - if let Some(index) = act.iter().position(|v| v == first) { - act.splice(processed..index, vec![wildcard.clone()]); - processed += 1; - } else { - // If we cannot find the value we are looking for return early - break; - } - } - } - } - } - (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) - } - } - (_, _) => {} - } -} - #[cfg(feature = "detect-encoding")] fn is_binary(data: &[u8]) -> bool { match content_inspector::inspect(data) { @@ -593,518 +382,3 @@ fn is_binary(data: &[u8]) -> bool { fn is_binary(_data: &[u8]) -> bool { false } - -#[cfg(test)] -mod test { - use super::*; - #[cfg(feature = "json")] - use serde_json::json; - - // Tests for checking to_bytes and render produce the same results - #[test] - fn text_to_bytes_render() { - let d = Data::text(String::from("test")); - let bytes = d.to_bytes().unwrap(); - let bytes = String::from_utf8(bytes).unwrap(); - let rendered = d.render().unwrap(); - assert_eq!(bytes, rendered); - } - - #[test] - #[cfg(feature = "json")] - fn json_to_bytes_render() { - let d = Data::json(json!({"name": "John\\Doe\r\n"})); - let bytes = d.to_bytes().unwrap(); - let bytes = String::from_utf8(bytes).unwrap(); - let rendered = d.render().unwrap(); - assert_eq!(bytes, rendered); - } - - // Tests for checking all types are coercible to each other and - // for when the coercion should fail - #[test] - fn binary_to_text() { - let binary = String::from("test").into_bytes(); - let d = Data::binary(binary); - let text = d.try_coerce(DataFormat::Text); - assert_eq!(DataFormat::Text, text.format()) - } - - #[test] - fn binary_to_text_not_utf8() { - let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); - let d = Data::binary(binary); - let d = d.try_coerce(DataFormat::Text); - assert_ne!(DataFormat::Text, d.format()); - assert_eq!(DataFormat::Binary, d.format()); - } - - #[test] - #[cfg(feature = "json")] - fn binary_to_json() { - let value = json!({"name": "John\\Doe\r\n"}); - let binary = serde_json::to_vec_pretty(&value).unwrap(); - let d = Data::binary(binary); - let json = d.try_coerce(DataFormat::Json); - assert_eq!(DataFormat::Json, json.format()); - } - - #[test] - #[cfg(feature = "json")] - fn binary_to_json_not_utf8() { - let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); - let d = Data::binary(binary); - let d = d.try_coerce(DataFormat::Json); - assert_ne!(DataFormat::Json, d.format()); - assert_eq!(DataFormat::Binary, d.format()); - } - - #[test] - #[cfg(feature = "json")] - fn binary_to_json_not_json() { - let binary = String::from("test").into_bytes(); - let d = Data::binary(binary); - let d = d.try_coerce(DataFormat::Json); - assert_ne!(DataFormat::Json, d.format()); - assert_eq!(DataFormat::Binary, d.format()); - } - - #[test] - fn text_to_binary() { - let text = String::from("test"); - let d = Data::text(text); - let binary = d.try_coerce(DataFormat::Binary); - assert_eq!(DataFormat::Binary, binary.format()); - } - - #[test] - #[cfg(feature = "json")] - fn text_to_json() { - let value = json!({"name": "John\\Doe\r\n"}); - let text = serde_json::to_string_pretty(&value).unwrap(); - let d = Data::text(text); - let json = d.try_coerce(DataFormat::Json); - assert_eq!(DataFormat::Json, json.format()); - } - - #[test] - #[cfg(feature = "json")] - fn text_to_json_not_json() { - let text = String::from("test"); - let d = Data::text(text); - let json = d.try_coerce(DataFormat::Json); - assert_eq!(DataFormat::Text, json.format()); - } - - #[test] - #[cfg(feature = "json")] - fn json_to_binary() { - let value = json!({"name": "John\\Doe\r\n"}); - let d = Data::json(value); - let binary = d.try_coerce(DataFormat::Binary); - assert_eq!(DataFormat::Binary, binary.format()); - } - - #[test] - #[cfg(feature = "json")] - fn json_to_text() { - let value = json!({"name": "John\\Doe\r\n"}); - let d = Data::json(value); - let text = d.try_coerce(DataFormat::Text); - assert_eq!(DataFormat::Text, text.format()); - } - - // Tests for coercible conversions create the same output as to_bytes/render - // - // render does not need to be checked against bin -> text since render - // outputs None for binary - #[test] - fn text_to_bin_coerce_equals_to_bytes() { - let text = String::from("test"); - let d = Data::text(text); - let binary = d.clone().try_coerce(DataFormat::Binary); - assert_eq!(Data::binary(d.to_bytes().unwrap()), binary); - } - - #[test] - #[cfg(feature = "json")] - fn json_to_bin_coerce_equals_to_bytes() { - let json = json!({"name": "John\\Doe\r\n"}); - let d = Data::json(json); - let binary = d.clone().try_coerce(DataFormat::Binary); - assert_eq!(Data::binary(d.to_bytes().unwrap()), binary); - } - - #[test] - #[cfg(feature = "json")] - fn json_to_text_coerce_equals_render() { - let json = json!({"name": "John\\Doe\r\n"}); - let d = Data::json(json); - let text = d.clone().try_coerce(DataFormat::Text); - assert_eq!(Data::text(d.render().unwrap()), text); - } - - // Tests for normalization on json - #[test] - #[cfg(feature = "json")] - fn json_normalize_paths_and_lines() { - let json = json!({"name": "John\\Doe\r\n"}); - let data = Data::json(json); - let data = data.normalize(NormalizePaths); - assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data); - let data = data.normalize(NormalizeNewlines); - assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data); - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_obj_paths_and_lines() { - let json = json!({ - "person": { - "name": "John\\Doe\r\n", - "nickname": "Jo\\hn\r\n", - } - }); - let data = Data::json(json); - let data = data.normalize(NormalizePaths); - let assert = json!({ - "person": { - "name": "John/Doe\r\n", - "nickname": "Jo/hn\r\n", - } - }); - assert_eq!(Data::json(assert), data); - let data = data.normalize(NormalizeNewlines); - let assert = json!({ - "person": { - "name": "John/Doe\n", - "nickname": "Jo/hn\n", - } - }); - assert_eq!(Data::json(assert), data); - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_array_paths_and_lines() { - let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]}); - let data = Data::json(json); - let data = data.normalize(NormalizePaths); - let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]}); - assert_eq!(Data::json(paths), data); - let data = data.normalize(NormalizeNewlines); - let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]}); - assert_eq!(Data::json(new_lines), data); - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_array_obj_paths_and_lines() { - let json = json!({ - "people": [ - { - "name": "John\\Doe\r\n", - "nickname": "Jo\\hn\r\n", - } - ] - }); - let data = Data::json(json); - let data = data.normalize(NormalizePaths); - let paths = json!({ - "people": [ - { - "name": "John/Doe\r\n", - "nickname": "Jo/hn\r\n", - } - ] - }); - assert_eq!(Data::json(paths), data); - let data = data.normalize(NormalizeNewlines); - let new_lines = json!({ - "people": [ - { - "name": "John/Doe\n", - "nickname": "Jo/hn\n", - } - ] - }); - assert_eq!(Data::json(new_lines), data); - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_matches_string() { - let exp = json!({"name": "{...}"}); - let expected = Data::json(exp); - let actual = json!({"name": "JohnDoe"}); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_matches_array() { - let exp = json!({"people": "{...}"}); - let expected = Data::json(exp); - let actual = json!({ - "people": [ - { - "name": "JohnDoe", - "nickname": "John", - } - ] - }); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_matches_obj() { - let exp = json!({"people": "{...}"}); - let expected = Data::json(exp); - let actual = json!({ - "people": { - "name": "JohnDoe", - "nickname": "John", - } - }); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_matches_diff_order_array() { - let exp = json!({ - "people": ["John", "Jane"] - }); - let expected = Data::json(exp); - let actual = json!({ - "people": ["Jane", "John"] - }); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_ne!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_wildcard_object_first() { - let exp = json!({ - "people": [ - "{...}", - { - "name": "three", - "nickname": "3", - } - ] - }); - let expected = Data::json(exp); - let actual = json!({ - "people": [ - { - "name": "one", - "nickname": "1", - }, - { - "name": "two", - "nickname": "2", - }, - { - "name": "three", - "nickname": "3", - } - ] - }); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_wildcard_array_first() { - let exp = json!([ - "{...}", - { - "name": "three", - "nickname": "3", - } - ]); - let expected = Data::json(exp); - let actual = json!([ - { - "name": "one", - "nickname": "1", - }, - { - "name": "two", - "nickname": "2", - }, - { - "name": "three", - "nickname": "3", - } - ]); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_wildcard_array_first_last() { - let exp = json!([ - "{...}", - { - "name": "two", - "nickname": "2", - }, - "{...}" - ]); - let expected = Data::json(exp); - let actual = json!([ - { - "name": "one", - "nickname": "1", - }, - { - "name": "two", - "nickname": "2", - }, - { - "name": "three", - "nickname": "3", - }, - { - "name": "four", - "nickname": "4", - } - ]); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_wildcard_array_middle_last() { - let exp = json!([ - { - "name": "one", - "nickname": "1", - }, - "{...}", - { - "name": "three", - "nickname": "3", - }, - "{...}" - ]); - let expected = Data::json(exp); - let actual = json!([ - { - "name": "one", - "nickname": "1", - }, - { - "name": "two", - "nickname": "2", - }, - { - "name": "three", - "nickname": "3", - }, - { - "name": "four", - "nickname": "4", - }, - { - "name": "five", - "nickname": "5", - } - ]); - let actual = Data::json(actual).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { - assert_eq!(exp, act); - } - } - - #[test] - #[cfg(feature = "json")] - fn json_normalize_wildcard_array_middle_last_early_return() { - let exp = json!([ - { - "name": "one", - "nickname": "1", - }, - "{...}", - { - "name": "three", - "nickname": "3", - }, - "{...}" - ]); - let expected = Data::json(exp); - let actual = json!([ - { - "name": "one", - "nickname": "1", - }, - { - "name": "two", - "nickname": "2", - }, - { - "name": "four", - "nickname": "4", - }, - { - "name": "five", - "nickname": "5", - } - ]); - let actual_normalized = Data::json(actual.clone()).normalize(NormalizeMatches { - substitutions: &Default::default(), - pattern: &expected, - }); - if let DataInner::Json(act) = actual_normalized.inner { - assert_eq!(act, actual); - } - } -} diff --git a/crates/snapbox/src/data/normalize.rs b/crates/snapbox/src/data/normalize.rs new file mode 100644 index 00000000..316bcba7 --- /dev/null +++ b/crates/snapbox/src/data/normalize.rs @@ -0,0 +1,168 @@ +use super::Data; +use super::DataInner; + +pub trait Normalize { + fn normalize(&self, data: Data) -> Data; +} + +pub struct NormalizeNewlines; +impl Normalize for NormalizeNewlines { + fn normalize(&self, data: Data) -> Data { + let mut new = match data.inner { + DataInner::Error(err) => Data::error(err), + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = crate::utils::normalize_lines(&text); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_lines); + Data::json(value) + } + }; + new.source = data.source; + new + } +} + +pub struct NormalizePaths; +impl Normalize for NormalizePaths { + fn normalize(&self, data: Data) -> Data { + let mut new = match data.inner { + DataInner::Error(err) => Data::error(err), + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = crate::utils::normalize_paths(&text); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_paths); + Data::json(value) + } + }; + new.source = data.source; + new + } +} + +pub struct NormalizeMatches<'a> { + substitutions: &'a crate::Substitutions, + pattern: &'a Data, +} + +impl<'a> NormalizeMatches<'a> { + pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self { + NormalizeMatches { + substitutions, + pattern, + } + } +} + +impl Normalize for NormalizeMatches<'_> { + fn normalize(&self, data: Data) -> Data { + let mut new = match data.inner { + DataInner::Error(err) => Data::error(err), + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = self + .substitutions + .normalize(&text, &self.pattern.render().unwrap()); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + if let DataInner::Json(exp) = &self.pattern.inner { + normalize_value_matches(&mut value, exp, self.substitutions); + } + Data::json(value) + } + }; + new.source = data.source; + new + } +} + +#[cfg(feature = "structured-data")] +fn normalize_value(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) + } + } + serde_json::Value::Object(obj) => { + for (_, value) in obj.iter_mut() { + normalize_value(value, op) + } + } + _ => {} + } +} + +#[cfg(feature = "structured-data")] +fn normalize_value_matches( + actual: &mut serde_json::Value, + expected: &serde_json::Value, + substitutions: &crate::Substitutions, +) { + use serde_json::Value::*; + match (actual, expected) { + // "{...}" is a wildcard + (act, String(exp)) if exp == "{...}" => { + *act = serde_json::json!("{...}"); + } + (String(act), String(exp)) => { + *act = substitutions.normalize(act, exp); + } + (Array(act), Array(exp)) => { + let wildcard = String("{...}".to_string()); + let mut sections = exp.split(|e| e == &wildcard).peekable(); + let mut processed = 0; + while let Some(expected_subset) = sections.next() { + // Process all values in the current section + if !expected_subset.is_empty() { + let actual_subset = &mut act[processed..processed + expected_subset.len()]; + for (a, e) in actual_subset.iter_mut().zip(expected_subset) { + normalize_value_matches(a, e, substitutions); + } + processed += expected_subset.len(); + } + + if let Some(next_section) = sections.peek() { + // If the next section has nothing in it, replace from processed to end with + // a single "{...}" + if next_section.is_empty() { + act.splice(processed.., vec![wildcard.clone()]); + processed += 1; + } else { + let first = next_section.first().unwrap(); + // Replace everything up until the value we are looking for with + // a single "{...}". + if let Some(index) = act.iter().position(|v| v == first) { + act.splice(processed..index, vec![wildcard.clone()]); + processed += 1; + } else { + // If we cannot find the value we are looking for return early + break; + } + } + } + } + } + (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) + } + } + (_, _) => {} + } +} diff --git a/crates/snapbox/src/data/source.rs b/crates/snapbox/src/data/source.rs new file mode 100644 index 00000000..1d57518b --- /dev/null +++ b/crates/snapbox/src/data/source.rs @@ -0,0 +1,35 @@ +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DataSource { + pub(crate) inner: DataSourceInner, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum DataSourceInner { + Path(std::path::PathBuf), +} + +impl DataSource { + pub fn path(path: impl Into) -> Self { + Self { + inner: DataSourceInner::Path(path.into()), + } + } + + pub fn is_path(&self) -> bool { + self.as_path().is_some() + } + + pub fn as_path(&self) -> Option<&std::path::Path> { + match &self.inner { + DataSourceInner::Path(path) => Some(path.as_ref()), + } + } +} + +impl std::fmt::Display for DataSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner { + DataSourceInner::Path(path) => crate::path::display_relpath(path).fmt(f), + } + } +} diff --git a/crates/snapbox/src/data/tests.rs b/crates/snapbox/src/data/tests.rs new file mode 100644 index 00000000..bd53707e --- /dev/null +++ b/crates/snapbox/src/data/tests.rs @@ -0,0 +1,493 @@ +use super::*; +#[cfg(feature = "json")] +use serde_json::json; + +// Tests for checking to_bytes and render produce the same results +#[test] +fn text_to_bytes_render() { + let d = Data::text(String::from("test")); + let bytes = d.to_bytes().unwrap(); + let bytes = String::from_utf8(bytes).unwrap(); + let rendered = d.render().unwrap(); + assert_eq!(bytes, rendered); +} + +#[test] +#[cfg(feature = "json")] +fn json_to_bytes_render() { + let d = Data::json(json!({"name": "John\\Doe\r\n"})); + let bytes = d.to_bytes().unwrap(); + let bytes = String::from_utf8(bytes).unwrap(); + let rendered = d.render().unwrap(); + assert_eq!(bytes, rendered); +} + +// Tests for checking all types are coercible to each other and +// for when the coercion should fail +#[test] +fn binary_to_text() { + let binary = String::from("test").into_bytes(); + let d = Data::binary(binary); + let text = d.try_coerce(DataFormat::Text); + assert_eq!(DataFormat::Text, text.format()) +} + +#[test] +fn binary_to_text_not_utf8() { + let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Text); + assert_ne!(DataFormat::Text, d.format()); + assert_eq!(DataFormat::Binary, d.format()); +} + +#[test] +#[cfg(feature = "json")] +fn binary_to_json() { + let value = json!({"name": "John\\Doe\r\n"}); + let binary = serde_json::to_vec_pretty(&value).unwrap(); + let d = Data::binary(binary); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Json, json.format()); +} + +#[test] +#[cfg(feature = "json")] +fn binary_to_json_not_utf8() { + let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Json); + assert_ne!(DataFormat::Json, d.format()); + assert_eq!(DataFormat::Binary, d.format()); +} + +#[test] +#[cfg(feature = "json")] +fn binary_to_json_not_json() { + let binary = String::from("test").into_bytes(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Json); + assert_ne!(DataFormat::Json, d.format()); + assert_eq!(DataFormat::Binary, d.format()); +} + +#[test] +fn text_to_binary() { + let text = String::from("test"); + let d = Data::text(text); + let binary = d.try_coerce(DataFormat::Binary); + assert_eq!(DataFormat::Binary, binary.format()); +} + +#[test] +#[cfg(feature = "json")] +fn text_to_json() { + let value = json!({"name": "John\\Doe\r\n"}); + let text = serde_json::to_string_pretty(&value).unwrap(); + let d = Data::text(text); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Json, json.format()); +} + +#[test] +#[cfg(feature = "json")] +fn text_to_json_not_json() { + let text = String::from("test"); + let d = Data::text(text); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Text, json.format()); +} + +#[test] +#[cfg(feature = "json")] +fn json_to_binary() { + let value = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(value); + let binary = d.try_coerce(DataFormat::Binary); + assert_eq!(DataFormat::Binary, binary.format()); +} + +#[test] +#[cfg(feature = "json")] +fn json_to_text() { + let value = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(value); + let text = d.try_coerce(DataFormat::Text); + assert_eq!(DataFormat::Text, text.format()); +} + +// Tests for coercible conversions create the same output as to_bytes/render +// +// render does not need to be checked against bin -> text since render +// outputs None for binary +#[test] +fn text_to_bin_coerce_equals_to_bytes() { + let text = String::from("test"); + let d = Data::text(text); + let binary = d.clone().try_coerce(DataFormat::Binary); + assert_eq!(Data::binary(d.to_bytes().unwrap()), binary); +} + +#[test] +#[cfg(feature = "json")] +fn json_to_bin_coerce_equals_to_bytes() { + let json = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(json); + let binary = d.clone().try_coerce(DataFormat::Binary); + assert_eq!(Data::binary(d.to_bytes().unwrap()), binary); +} + +#[test] +#[cfg(feature = "json")] +fn json_to_text_coerce_equals_render() { + let json = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(json); + let text = d.clone().try_coerce(DataFormat::Text); + assert_eq!(Data::text(d.render().unwrap()), text); +} + +// Tests for normalization on json +#[test] +#[cfg(feature = "json")] +fn json_normalize_paths_and_lines() { + let json = json!({"name": "John\\Doe\r\n"}); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data); + let data = data.normalize(NormalizeNewlines); + assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data); +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_obj_paths_and_lines() { + let json = json!({ + "person": { + "name": "John\\Doe\r\n", + "nickname": "Jo\\hn\r\n", + } + }); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let assert = json!({ + "person": { + "name": "John/Doe\r\n", + "nickname": "Jo/hn\r\n", + } + }); + assert_eq!(Data::json(assert), data); + let data = data.normalize(NormalizeNewlines); + let assert = json!({ + "person": { + "name": "John/Doe\n", + "nickname": "Jo/hn\n", + } + }); + assert_eq!(Data::json(assert), data); +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_array_paths_and_lines() { + let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]}); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]}); + assert_eq!(Data::json(paths), data); + let data = data.normalize(NormalizeNewlines); + let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]}); + assert_eq!(Data::json(new_lines), data); +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_array_obj_paths_and_lines() { + let json = json!({ + "people": [ + { + "name": "John\\Doe\r\n", + "nickname": "Jo\\hn\r\n", + } + ] + }); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let paths = json!({ + "people": [ + { + "name": "John/Doe\r\n", + "nickname": "Jo/hn\r\n", + } + ] + }); + assert_eq!(Data::json(paths), data); + let data = data.normalize(NormalizeNewlines); + let new_lines = json!({ + "people": [ + { + "name": "John/Doe\n", + "nickname": "Jo/hn\n", + } + ] + }); + assert_eq!(Data::json(new_lines), data); +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_matches_string() { + let exp = json!({"name": "{...}"}); + let expected = Data::json(exp); + let actual = json!({"name": "JohnDoe"}); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_matches_array() { + let exp = json!({"people": "{...}"}); + let expected = Data::json(exp); + let actual = json!({ + "people": [ + { + "name": "JohnDoe", + "nickname": "John", + } + ] + }); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_matches_obj() { + let exp = json!({"people": "{...}"}); + let expected = Data::json(exp); + let actual = json!({ + "people": { + "name": "JohnDoe", + "nickname": "John", + } + }); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_matches_diff_order_array() { + let exp = json!({ + "people": ["John", "Jane"] + }); + let expected = Data::json(exp); + let actual = json!({ + "people": ["Jane", "John"] + }); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_ne!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_object_first() { + let exp = json!({ + "people": [ + "{...}", + { + "name": "three", + "nickname": "3", + } + ] + }); + let expected = Data::json(exp); + let actual = json!({ + "people": [ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + } + ] + }); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_array_first() { + let exp = json!([ + "{...}", + { + "name": "three", + "nickname": "3", + } + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + } + ]); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_array_first_last() { + let exp = json!([ + "{...}", + { + "name": "two", + "nickname": "2", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + }, + { + "name": "four", + "nickname": "4", + } + ]); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_array_middle_last() { + let exp = json!([ + { + "name": "one", + "nickname": "1", + }, + "{...}", + { + "name": "three", + "nickname": "3", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + }, + { + "name": "four", + "nickname": "4", + }, + { + "name": "five", + "nickname": "5", + } + ]); + let actual = + Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } +} + +#[test] +#[cfg(feature = "json")] +fn json_normalize_wildcard_array_middle_last_early_return() { + let exp = json!([ + { + "name": "one", + "nickname": "1", + }, + "{...}", + { + "name": "three", + "nickname": "3", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "four", + "nickname": "4", + }, + { + "name": "five", + "nickname": "5", + } + ]); + let actual_normalized = + Data::json(actual.clone()).normalize(NormalizeMatches::new(&Default::default(), &expected)); + if let DataInner::Json(act) = actual_normalized.inner { + assert_eq!(act, actual); + } +}