/// Test fixture, actual output, or expected result /// /// This provides conveniences for tracking the intended format (binary vs text). #[derive(Clone, Debug, PartialEq, Eq)] pub struct Data { inner: DataInner, } #[derive(Clone, Debug, PartialEq, Eq)] enum DataInner { Binary(Vec), Text(String), #[cfg(feature = "json")] Json(serde_json::Value), } #[derive(Clone, Debug, PartialEq, Eq, Copy, Hash)] pub enum DataFormat { Binary, Text, #[cfg(feature = "json")] Json, } impl Default for DataFormat { fn default() -> Self { DataFormat::Text } } impl Data { /// Mark the data as binary (no post-processing) pub fn binary(raw: impl Into>) -> Self { Self { inner: DataInner::Binary(raw.into()), } } /// Mark the data as text (post-processing) pub fn text(raw: impl Into) -> Self { Self { inner: DataInner::Text(raw.into()), } } #[cfg(feature = "json")] pub fn json(raw: impl Into) -> Self { Self { inner: DataInner::Json(raw.into()), } } /// Empty test data pub fn new() -> Self { Self::text("") } /// Load test data from a file pub fn read_from( path: &std::path::Path, data_format: Option, ) -> Result { let data = match data_format { Some(df) => match df { DataFormat::Binary => { let data = std::fs::read(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; Self::binary(data) } DataFormat::Text => { let data = std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; Self::text(data) } #[cfg(feature = "json")] DataFormat::Json => { let data = std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; Self::json(serde_json::from_str::(&data).unwrap()) } }, None => { let data = std::fs::read(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let data = Self::binary(data); match path .extension() .and_then(|e| e.to_str()) .unwrap_or_default() { #[cfg(feature = "json")] "json" => data.try_coerce(DataFormat::Json), _ => data.try_coerce(DataFormat::Text), } } }; Ok(data) } /// Overwrite a snapshot pub fn write_to(&self, path: &std::path::Path) -> Result<(), crate::Error> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|e| { format!("Failed to create parent dir for {}: {}", path.display(), e) })?; } std::fs::write(path, self.to_bytes()) .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into()) } /// Post-process text /// /// See [utils][crate::utils] pub fn normalize(self, op: impl Normalize) -> Self { op.normalize(self) } /// Return the underlying `String` /// /// Note: this will not inspect binary data for being a valid `String`. pub fn render(&self) -> Option { match &self.inner { DataInner::Binary(_) => None, DataInner::Text(data) => Some(data.to_owned()), #[cfg(feature = "json")] DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()), } } pub fn to_bytes(&self) -> Vec { match &self.inner { DataInner::Binary(data) => data.clone(), DataInner::Text(data) => data.clone().into_bytes(), #[cfg(feature = "json")] DataInner::Json(value) => serde_json::to_vec_pretty(value).unwrap(), } } pub fn try_coerce(self, format: DataFormat) -> Self { match (self.inner, format) { (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner), (DataInner::Text(inner), DataFormat::Text) => Self::text(inner), #[cfg(feature = "json")] (DataInner::Json(inner), DataFormat::Json) => Self::json(inner), (DataInner::Binary(inner), _) => { if is_binary(&inner) { Self::binary(inner) } else { match String::from_utf8(inner) { Ok(str) => { let coerced = Self::text(str).try_coerce(format); // if the Text cannot be coerced into the correct format // reset it back to Binary if coerced.format() != format { coerced.try_coerce(DataFormat::Binary) } else { coerced } } Err(err) => { let bin = err.into_bytes(); Self::binary(bin) } } } } #[cfg(feature = "json")] (DataInner::Text(inner), DataFormat::Json) => { match serde_json::from_str::(&inner) { Ok(json) => Self::json(json), Err(_) => Self::text(inner), } } (inner, DataFormat::Binary) => Self::binary(Self { inner }.to_bytes()), // This variant is already covered unless structured data is enabled #[cfg(feature = "structured-data")] (inner, DataFormat::Text) => { let remake = Self { inner }; if let Some(str) = remake.render() { Self::text(str) } else { remake } } } } /// Outputs the current `DataFormat` of the underlying data pub fn format(&self) -> DataFormat { match &self.inner { DataInner::Binary(_) => DataFormat::Binary, DataInner::Text(_) => DataFormat::Text, #[cfg(feature = "json")] DataInner::Json(_) => DataFormat::Json, } } } impl std::fmt::Display for Data { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.inner { DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f), DataInner::Text(data) => data.fmt(f), #[cfg(feature = "json")] DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f), } } } impl Default for Data { fn default() -> Self { Self::new() } } impl<'d> From<&'d Data> for Data { fn from(other: &'d Data) -> Self { other.clone() } } impl From> for Data { fn from(other: Vec) -> Self { Self::binary(other) } } impl<'b> From<&'b [u8]> for Data { fn from(other: &'b [u8]) -> Self { other.to_owned().into() } } impl From for Data { fn from(other: String) -> Self { Self::text(other) } } impl<'s> From<&'s String> for Data { fn from(other: &'s String) -> Self { other.clone().into() } } impl<'s> From<&'s str> for Data { fn from(other: &'s str) -> Self { other.to_owned().into() } } pub trait Normalize { fn normalize(&self, data: Data) -> Data; } pub struct NormalizeNewlines; impl Normalize for NormalizeNewlines { fn normalize(&self, data: Data) -> Data { match data.inner { 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) } } } } pub struct NormalizePaths; impl Normalize for NormalizePaths { fn normalize(&self, data: Data) -> Data { match data.inner { 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) } } } } 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 { match data.inner { 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) } } } } #[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) => { arr.iter_mut().for_each(|value| normalize_value(value, op)); } serde_json::Value::Object(obj) => { obj.iter_mut() .for_each(|(_, value)| 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)) => { act.iter_mut() .zip(exp) .for_each(|(a, e)| normalize_value_matches(a, e, substitutions)); } (Object(act), Object(exp)) => { act.iter_mut() .zip(exp) .filter(|(a, e)| a.0 == e.0) .for_each(|(a, e)| normalize_value_matches(a.1, e.1, substitutions)); } (_, _) => {} } } #[cfg(feature = "detect-encoding")] fn is_binary(data: &[u8]) -> bool { match content_inspector::inspect(data) { content_inspector::ContentType::BINARY | // We don't support these content_inspector::ContentType::UTF_16LE | content_inspector::ContentType::UTF_16BE | content_inspector::ContentType::UTF_32LE | content_inspector::ContentType::UTF_32BE => { true }, content_inspector::ContentType::UTF_8 | content_inspector::ContentType::UTF_8_BOM => { false }, } } #[cfg(not(feature = "detect-encoding"))] 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(); 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(); 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()), 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()), 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); } } }