diff --git a/crates/snapbox/src/assert.rs b/crates/snapbox/src/assert.rs index d9c583d9..8c7927f0 100644 --- a/crates/snapbox/src/assert.rs +++ b/crates/snapbox/src/assert.rs @@ -133,7 +133,7 @@ impl Assert { ) -> (crate::Data, crate::Data) { let expected = expected.normalize(NormalizeNewlines); // On `expected` being an error, make a best guess - let format = expected.format(); + let format = expected.intended_format(); actual = actual.coerce_to(format).normalize(NormalizeNewlines); @@ -147,7 +147,7 @@ impl Assert { ) -> (crate::Data, crate::Data) { let expected = expected.normalize(NormalizeNewlines); // On `expected` being an error, make a best guess - let format = expected.format(); + let format = expected.intended_format(); actual = actual.coerce_to(format); if self.normalize_paths { diff --git a/crates/snapbox/src/data/mod.rs b/crates/snapbox/src/data/mod.rs index 0abcf349..d9519698 100644 --- a/crates/snapbox/src/data/mod.rs +++ b/crates/snapbox/src/data/mod.rs @@ -77,6 +77,8 @@ macro_rules! file { /// ``` /// /// Leading indentation is stripped. +/// +/// See [`Inline::is`] for declaring the data to be of a certain format. #[macro_export] macro_rules! str { [$data:literal] => { $crate::str![[$data]] }; @@ -108,7 +110,7 @@ pub struct Data { #[derive(Clone, Debug)] pub(crate) enum DataInner { - Error(crate::Error), + Error(DataError), Binary(Vec), Text(String), #[cfg(feature = "json")] @@ -133,8 +135,12 @@ impl Data { DataInner::Json(raw.into()).into() } - fn error(raw: impl Into) -> Self { - DataInner::Error(raw.into()).into() + fn error(raw: impl Into, intended: DataFormat) -> Self { + DataError { + error: raw.into(), + intended, + } + .into() } /// Empty test data @@ -151,50 +157,25 @@ impl Data { self.with_source(path.into()) } - /// Load test data from a file + /// Load `expected` data from a file pub fn read_from(path: &std::path::Path, data_format: Option) -> Self { match Self::try_read_from(path, data_format) { Ok(data) => data, - Err(err) => Self::error(err).with_path(path), + Err(err) => Self::error(err, data_format.unwrap_or_default()).with_path(path), } } - /// Load test data from a file + /// Load `expected` data from a file pub fn try_read_from( path: &std::path::Path, data_format: Option, ) -> Result { + let data = + std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + let data = Self::binary(data); let data = match data_format { - Some(df) => match df { - DataFormat::Error => Self::error("unknown error"), - 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()) - } - #[cfg(feature = "term-svg")] - DataFormat::TermSvg => { - let data = std::fs::read_to_string(path) - .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - Self::from(DataInner::TermSvg(data)) - } - }, + Some(df) => data.is(df), None => { - let data = std::fs::read(path) - .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - let data = Self::binary(data); - let file_name = path .file_name() .and_then(|e| e.to_str()) @@ -273,7 +254,7 @@ impl Data { pub fn to_bytes(&self) -> Result, crate::Error> { match &self.inner { - DataInner::Error(err) => Err(err.clone()), + DataInner::Error(err) => Err(err.error.clone()), DataInner::Binary(data) => Ok(data.clone()), DataInner::Text(data) => Ok(data.clone().into_bytes()), #[cfg(feature = "json")] @@ -285,9 +266,63 @@ impl Data { } } + /// Initialize `Self` as [`format`][DataFormat] or [`Error`][DataFormat::Error] + /// + /// This is generally used for `expected` data + pub fn is(self, format: DataFormat) -> Self { + match self.try_is(format) { + Ok(new) => new, + Err(err) => Self::error(err, format), + } + } + + fn try_is(self, format: DataFormat) -> Result { + let original = self.format(); + let source = self.source; + let inner = match (self.inner, format) { + (DataInner::Error(inner), _) => DataInner::Error(inner), + (DataInner::Binary(inner), DataFormat::Binary) => DataInner::Binary(inner), + (DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner), + #[cfg(feature = "json")] + (DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner), + #[cfg(feature = "term-svg")] + (DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner), + (DataInner::Binary(inner), _) => { + let inner = String::from_utf8(inner).map_err(|_err| "invalid UTF-8".to_owned())?; + Self::text(inner).try_is(format)?.inner + } + #[cfg(feature = "json")] + (DataInner::Text(inner), DataFormat::Json) => { + let inner = serde_json::from_str::(&inner) + .map_err(|err| err.to_string())?; + DataInner::Json(inner) + } + #[cfg(feature = "term-svg")] + (DataInner::Text(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner), + (inner, DataFormat::Binary) => { + let remake: Self = inner.into(); + DataInner::Binary(remake.to_bytes().expect("error case handled")) + } + // This variant is already covered unless structured data is enabled + #[cfg(feature = "structured-data")] + (inner, DataFormat::Text) => { + if let Some(str) = Data::from(inner).render() { + DataInner::Text(str) + } else { + return Err(format!("cannot convert {original:?} to {format:?}").into()); + } + } + (_, _) => return Err(format!("cannot convert {original:?} to {format:?}").into()), + }; + Ok(Self { inner, source }) + } + + /// Convert `Self` to [`format`][DataFormat] if possible + /// + /// This is generally used on `actual` data to make it match `expected` pub fn coerce_to(self, format: DataFormat) -> Self { let mut data = match (self.inner, format) { - (DataInner::Error(inner), _) => Self::error(inner), + (DataInner::Error(inner), _) => inner.into(), (inner, DataFormat::Error) => inner.into(), (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner), (DataInner::Text(inner), DataFormat::Text) => Self::text(inner), @@ -368,6 +403,18 @@ impl Data { } } + pub(crate) fn intended_format(&self) -> DataFormat { + match &self.inner { + DataInner::Error(DataError { intended, .. }) => *intended, + DataInner::Binary(_) => DataFormat::Binary, + DataInner::Text(_) => DataFormat::Text, + #[cfg(feature = "json")] + DataInner::Json(_) => DataFormat::Json, + #[cfg(feature = "term-svg")] + DataInner::TermSvg(_) => DataFormat::TermSvg, + } + } + pub(crate) fn relevant(&self) -> Option<&str> { match &self.inner { DataInner::Error(_) => None, @@ -390,10 +437,19 @@ impl From for Data { } } +impl From for Data { + fn from(inner: DataError) -> Self { + Data { + inner: DataInner::Error(inner), + source: None, + } + } +} + impl std::fmt::Display for Data { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.inner { - DataInner::Error(err) => err.fmt(f), + DataInner::Error(data) => data.fmt(f), DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f), DataInner::Text(data) => data.fmt(f), #[cfg(feature = "json")] @@ -424,6 +480,18 @@ impl PartialEq for Data { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct DataError { + error: crate::Error, + intended: DataFormat, +} + +impl std::fmt::Display for DataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} + #[cfg(feature = "term-svg")] fn text_elem(svg: &str) -> Option<&str> { let open_elem_start_idx = svg.find(" Data { let mut new = match data.inner { - DataInner::Error(err) => Data::error(err), + DataInner::Error(err) => err.into(), DataInner::Binary(bin) => Data::binary(bin), DataInner::Text(text) => { let lines = crate::utils::normalize_lines(&text); @@ -36,7 +36,7 @@ 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::Error(err) => err.into(), DataInner::Binary(bin) => Data::binary(bin), DataInner::Text(text) => { let lines = crate::utils::normalize_paths(&text); @@ -76,7 +76,7 @@ impl<'a> NormalizeMatches<'a> { impl Normalize for NormalizeMatches<'_> { fn normalize(&self, data: Data) -> Data { let mut new = match data.inner { - DataInner::Error(err) => Data::error(err), + DataInner::Error(err) => err.into(), DataInner::Binary(bin) => Data::binary(bin), DataInner::Text(text) => { let lines = self diff --git a/crates/snapbox/src/data/source.rs b/crates/snapbox/src/data/source.rs index c1d83009..a650b190 100644 --- a/crates/snapbox/src/data/source.rs +++ b/crates/snapbox/src/data/source.rs @@ -86,6 +86,16 @@ impl Inline { self } + /// Initialize `Self` as [`format`][crate::data::DataFormat] or [`Error`][crate::data::DataFormat::Error] + /// + /// This is generally used for `expected` data + 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) diff --git a/crates/snapbox/src/path.rs b/crates/snapbox/src/path.rs index 99efb098..b307c1c8 100644 --- a/crates/snapbox/src/path.rs +++ b/crates/snapbox/src/path.rs @@ -191,7 +191,7 @@ impl PathDiff { crate::Data::read_from(&expected_path, None).normalize(NormalizeNewlines); actual = actual - .coerce_to(expected.format()) + .coerce_to(expected.intended_format()) .normalize(NormalizeNewlines); if expected != actual { @@ -268,7 +268,7 @@ impl PathDiff { crate::Data::read_from(&expected_path, None).normalize(NormalizeNewlines); actual = actual - .coerce_to(expected.format()) + .coerce_to(expected.intended_format()) .normalize(NormalizePaths) .normalize(NormalizeNewlines) .normalize(NormalizeMatches::new(substitutions, &expected));