| /// 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<u8>), |
| Text(String), |
| } |
| |
| #[derive(Clone, Debug, PartialEq, Eq, Copy, Hash)] |
| pub enum DataFormat { |
| Binary, |
| Text, |
| } |
| |
| impl Data { |
| /// Mark the data as binary (no post-processing) |
| pub fn binary(raw: impl Into<Vec<u8>>) -> Self { |
| Self { |
| inner: DataInner::Binary(raw.into()), |
| } |
| } |
| |
| /// Mark the data as text (post-processing) |
| pub fn text(raw: impl Into<String>) -> Self { |
| Self { |
| inner: DataInner::Text(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<DataFormat>, |
| ) -> Result<Self, crate::Error> { |
| 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) |
| } |
| }, |
| None => { |
| let data = std::fs::read(&path) |
| .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; |
| Self::binary(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 map_text(self, op: impl FnOnce(&str) -> String) -> Self { |
| match self.inner { |
| DataInner::Binary(data) => Self::binary(data), |
| DataInner::Text(data) => Self::text(op(&data)), |
| } |
| } |
| |
| /// Coerce to a string |
| /// |
| /// Note: this will **not** do a binary-content check |
| pub fn make_text(&mut self) -> Result<(), std::str::Utf8Error> { |
| *self = Self::text(std::mem::take(self).into_string()?); |
| Ok(()) |
| } |
| |
| /// Coerce to a string |
| /// |
| /// Note: this will **not** do a binary-content check |
| pub fn into_string(self) -> Result<String, std::str::Utf8Error> { |
| match self.inner { |
| DataInner::Binary(data) => { |
| let data = String::from_utf8(data).map_err(|e| e.utf8_error())?; |
| Ok(data) |
| } |
| DataInner::Text(data) => Ok(data), |
| } |
| } |
| |
| /// Return the underlying `str` |
| /// |
| /// Note: this will not inspect binary data for being a valid `str`. |
| pub fn as_str(&self) -> Option<&str> { |
| match &self.inner { |
| DataInner::Binary(_) => None, |
| DataInner::Text(data) => Some(data.as_str()), |
| } |
| } |
| |
| pub fn to_bytes(&self) -> Vec<u8> { |
| match &self.inner { |
| DataInner::Binary(data) => data.clone(), |
| DataInner::Text(data) => data.clone().into_bytes(), |
| } |
| } |
| |
| pub fn try_coerce(self, format: DataFormat) -> Self { |
| match format { |
| DataFormat::Binary => Self::binary(self.to_bytes()), |
| DataFormat::Text => match self.inner { |
| DataInner::Binary(data) => { |
| if is_binary(&data) { |
| Self::binary(data) |
| } else { |
| match String::from_utf8(data) { |
| Ok(data) => Self::text(data), |
| Err(err) => { |
| let data = err.into_bytes(); |
| Self::binary(data) |
| } |
| } |
| } |
| } |
| DataInner::Text(data) => Self::text(data), |
| }, |
| } |
| } |
| |
| /// Outputs the current `DataFormat` of the underlying data |
| pub fn format(&self) -> DataFormat { |
| match &self.inner { |
| DataInner::Binary(_) => DataFormat::Binary, |
| DataInner::Text(_) => DataFormat::Text, |
| } |
| } |
| } |
| |
| 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), |
| } |
| } |
| } |
| |
| 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<Vec<u8>> for Data { |
| fn from(other: Vec<u8>) -> Self { |
| Self::binary(other) |
| } |
| } |
| |
| impl<'b> From<&'b [u8]> for Data { |
| fn from(other: &'b [u8]) -> Self { |
| other.to_owned().into() |
| } |
| } |
| |
| impl From<String> 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() |
| } |
| } |
| |
| #[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 |
| } |