use crate::value::Value;
const LIMIT: usize = 8;
pub fn describe(expected: &Value, actual: &Value) -> Vec<String> {
let mut out = Vec::new();
walk("", expected, actual, &mut out);
if out.len() > LIMIT {
let extra = out.len() - LIMIT;
out.truncate(LIMIT);
out.push(format!("... and {extra} more"));
}
out
}
pub fn describe_text(expected: &str, actual: &str) -> Vec<String> {
let (expected, actual) = (expected.trim(), actual.trim());
if expected == actual {
return Vec::new();
}
let common = expected
.char_indices()
.zip(actual.chars())
.take_while(|((_, e), a)| e == a)
.count();
vec![
format!("first difference at character {common}"),
format!("expected: {}", elide(expected)),
format!("actual: {}", elide(actual)),
]
}
fn walk(path: &str, expected: &Value, actual: &Value, out: &mut Vec<String>) {
let here = if path.is_empty() { "(root)" } else { path };
match (expected, actual) {
(Value::Seq(expected_items), Value::Seq(actual_items)) => {
if expected_items.len() != actual_items.len() {
out.push(format!(
"{here}: expected {} items, got {}",
expected_items.len(),
actual_items.len()
));
}
for (index, (expected_item, actual_item)) in
expected_items.iter().zip(actual_items).enumerate()
{
walk(&format!("{path}[{index}]"), expected_item, actual_item, out);
}
}
(Value::Map(expected_entries), Value::Map(_)) => {
for (key, expected_value) in expected_entries {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
match actual.get(key) {
Some(actual_value) => walk(&child, expected_value, actual_value, out),
None => out.push(format!(
"{child}: missing, expected {}",
elide(&expected_value.to_json())
)),
}
}
if let Value::Map(actual_entries) = actual {
for (key, actual_value) in actual_entries {
if expected.get(key).is_none() {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
out.push(format!(
"{child}: unexpected, got {}",
elide(&actual_value.to_json())
));
}
}
}
}
_ => {
if expected != actual {
out.push(format!(
"{here}: expected {}, got {}",
elide(&expected.to_json()),
elide(&actual.to_json())
));
}
}
}
}
fn elide(value: &str) -> String {
const MAX: usize = 120;
let mut out = String::new();
for (count, ch) in value.chars().enumerate() {
if count == MAX {
out.push_str("...");
break;
}
match ch {
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out
}