summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox/src/data.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/snapbox/src/data.rs
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz
rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/snapbox/src/data.rs')
-rw-r--r--vendor/snapbox/src/data.rs712
1 files changed, 712 insertions, 0 deletions
diff --git a/vendor/snapbox/src/data.rs b/vendor/snapbox/src/data.rs
new file mode 100644
index 000000000..aa5f9b1ed
--- /dev/null
+++ b/vendor/snapbox/src/data.rs
@@ -0,0 +1,712 @@
+/// 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),
+ #[cfg(feature = "structured-data")]
+ 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<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()),
+ }
+ }
+
+ #[cfg(feature = "json")]
+ pub fn json(raw: impl Into<serde_json::Value>) -> 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<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)
+ }
+ #[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::<serde_json::Value>(&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<String> {
+ 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<u8> {
+ 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::<serde_json::Value>(&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<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()
+ }
+}
+
+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);
+ }
+ }
+}