blob: b892ffd1e47954cd8b9efa9e2c27096833d4ff7f [file] [log] [blame]
/// 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 = "json")]
Json(serde_json::Value),
}
#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)]
pub enum DataFormat {
Binary,
#[default]
Text,
#[cfg(feature = "json")]
Json,
}
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) => {
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) {
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);
}
}
#[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);
}
}
}