1use std::fmt::Write;
23
24use super::core::{struct_fields_to_map, VmValue};
25use super::recursion::guard_recursion;
26use super::structural::values_equal;
27
28const MAX_DIFFERENCES: usize = 10;
31
32const MAX_LEAF_CHARS: usize = 120;
35
36#[derive(Debug, Clone)]
38pub enum DifferenceKind {
39 Unequal { actual: VmValue, expected: VmValue },
41 Unexpected { actual: VmValue },
44 Missing { expected: VmValue },
47}
48
49#[derive(Debug, Clone)]
51pub struct ValueDifference {
52 pub path: String,
55 pub kind: DifferenceKind,
56}
57
58pub fn diff_values(actual: &VmValue, expected: &VmValue) -> Vec<ValueDifference> {
63 let mut out = Vec::new();
64 walk(String::new(), actual, expected, &mut out);
65 out
66}
67
68fn walk(path: String, actual: &VmValue, expected: &VmValue, out: &mut Vec<ValueDifference>) {
69 if values_equal(actual, expected) {
70 return;
71 }
72 match (actual, expected) {
73 (VmValue::Dict(a), VmValue::Dict(e)) => {
74 guard_recursion(|| {
75 let mut keys: Vec<&str> = a.keys().map(|k| k.as_str()).collect();
78 keys.extend(e.keys().map(|k| k.as_str()));
79 keys.sort_unstable();
80 keys.dedup();
81 for key in keys {
82 let child = format!("{path}{}", render_key_step(key));
83 match (a.get(key), e.get(key)) {
84 (Some(av), Some(ev)) => walk(child, av, ev, out),
85 (Some(av), None) => out.push(ValueDifference {
86 path: child,
87 kind: DifferenceKind::Unexpected { actual: av.clone() },
88 }),
89 (None, Some(ev)) => out.push(ValueDifference {
90 path: child,
91 kind: DifferenceKind::Missing {
92 expected: ev.clone(),
93 },
94 }),
95 (None, None) => {}
96 }
97 }
98 });
99 }
100 (VmValue::List(a), VmValue::List(e)) => {
101 guard_recursion(|| walk_sequence(&path, a, e, out));
102 }
103 (VmValue::StructInstance(a), VmValue::StructInstance(e))
104 if a.layout.struct_name() == e.layout.struct_name() =>
105 {
106 guard_recursion(|| {
107 let a_fields = struct_fields_to_map(&a.layout, &a.fields);
108 let e_fields = struct_fields_to_map(&e.layout, &e.fields);
109 let mut keys: Vec<&str> = a_fields.keys().map(|k| k.as_str()).collect();
110 keys.extend(e_fields.keys().map(|k| k.as_str()));
111 keys.sort_unstable();
112 keys.dedup();
113 for key in keys {
114 let child = format!("{path}{}", render_key_step(key));
115 match (a_fields.get(key), e_fields.get(key)) {
116 (Some(av), Some(ev)) => walk(child, av, ev, out),
117 (Some(av), None) => out.push(ValueDifference {
118 path: child,
119 kind: DifferenceKind::Unexpected { actual: av.clone() },
120 }),
121 (None, Some(ev)) => out.push(ValueDifference {
122 path: child,
123 kind: DifferenceKind::Missing {
124 expected: ev.clone(),
125 },
126 }),
127 (None, None) => {}
128 }
129 }
130 });
131 }
132 (VmValue::EnumVariant(a), VmValue::EnumVariant(e))
133 if a.enum_name == e.enum_name
134 && a.variant == e.variant
135 && a.fields.len() == e.fields.len() =>
136 {
137 guard_recursion(|| {
142 walk_sequence(&format!("{path}.fields"), &a.fields, &e.fields, out);
143 });
144 }
145 (VmValue::Set(a), VmValue::Set(e)) => {
146 let mut extra: Vec<&VmValue> = a.iter().filter(|v| !e.contains(v)).collect();
149 let mut absent: Vec<&VmValue> = e.iter().filter(|v| !a.contains(v)).collect();
150 extra.sort_by_cached_key(|v| repr(v));
151 absent.sort_by_cached_key(|v| repr(v));
152 for value in extra {
153 out.push(ValueDifference {
154 path: format!("{path}{{{}}}", repr(value)),
155 kind: DifferenceKind::Unexpected {
156 actual: value.clone(),
157 },
158 });
159 }
160 for value in absent {
161 out.push(ValueDifference {
162 path: format!("{path}{{{}}}", repr(value)),
163 kind: DifferenceKind::Missing {
164 expected: value.clone(),
165 },
166 });
167 }
168 }
169 _ => out.push(ValueDifference {
173 path,
174 kind: DifferenceKind::Unequal {
175 actual: actual.clone(),
176 expected: expected.clone(),
177 },
178 }),
179 }
180}
181
182fn walk_sequence(path: &str, a: &[VmValue], e: &[VmValue], out: &mut Vec<ValueDifference>) {
183 for index in 0..a.len().max(e.len()) {
184 let child = format!("{path}[{index}]");
185 match (a.get(index), e.get(index)) {
186 (Some(av), Some(ev)) => walk(child, av, ev, out),
187 (Some(av), None) => out.push(ValueDifference {
188 path: child,
189 kind: DifferenceKind::Unexpected { actual: av.clone() },
190 }),
191 (None, Some(ev)) => out.push(ValueDifference {
192 path: child,
193 kind: DifferenceKind::Missing {
194 expected: ev.clone(),
195 },
196 }),
197 (None, None) => {}
198 }
199 }
200}
201
202fn render_key_step(key: &str) -> String {
206 let plain = !key.is_empty()
207 && !key.starts_with(|c: char| c.is_ascii_digit())
208 && key
209 .chars()
210 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
211 if plain {
212 format!(".{key}")
213 } else {
214 format!("[{}]", quote_string(key))
215 }
216}
217
218pub fn repr(value: &VmValue) -> String {
229 let mut out = String::new();
230 write_repr(value, &mut out);
231 out
232}
233
234fn write_repr(value: &VmValue, out: &mut String) {
235 match value {
236 VmValue::String(s) => out.push_str("e_string(s)),
237 VmValue::List(items) => {
238 out.push('[');
239 guard_recursion(|| {
240 for (i, item) in items.iter().enumerate() {
241 if i > 0 {
242 out.push_str(", ");
243 }
244 write_repr(item, out);
245 }
246 });
247 out.push(']');
248 }
249 VmValue::Dict(map) => {
250 out.push('{');
251 guard_recursion(|| {
252 for (i, (k, v)) in map.iter().enumerate() {
253 if i > 0 {
254 out.push_str(", ");
255 }
256 out.push_str("e_string(k));
257 out.push_str(": ");
258 write_repr(v, out);
259 }
260 });
261 out.push('}');
262 }
263 VmValue::Set(members) => {
264 let mut rendered: Vec<String> = members.iter().map(repr).collect();
265 rendered.sort();
266 let _ = write!(out, "set([{}])", rendered.join(", "));
267 }
268 VmValue::StructInstance(data) => {
269 let _ = write!(out, "{} {{", data.layout.struct_name());
270 guard_recursion(|| {
271 for (i, (k, v)) in struct_fields_to_map(&data.layout, &data.fields)
272 .iter()
273 .enumerate()
274 {
275 if i > 0 {
276 out.push_str(", ");
277 }
278 let _ = write!(out, "{k}: ");
279 write_repr(v, out);
280 }
281 });
282 out.push('}');
283 }
284 VmValue::EnumVariant(variant) => {
285 let _ = write!(out, "{}::{}", variant.enum_name, variant.variant);
286 if !variant.fields.is_empty() {
287 out.push('(');
288 guard_recursion(|| {
289 for (i, field) in variant.fields.iter().enumerate() {
290 if i > 0 {
291 out.push_str(", ");
292 }
293 write_repr(field, out);
294 }
295 });
296 out.push(')');
297 }
298 }
299 other => other.write_display(out),
301 }
302}
303
304fn quote_string(s: &str) -> String {
305 let mut out = String::with_capacity(s.len() + 2);
306 out.push('"');
307 for c in s.chars() {
308 match c {
309 '"' => out.push_str("\\\""),
310 '\\' => out.push_str("\\\\"),
311 '\n' => out.push_str("\\n"),
312 '\r' => out.push_str("\\r"),
313 '\t' => out.push_str("\\t"),
314 _ => out.push(c),
315 }
316 }
317 out.push('"');
318 out
319}
320
321fn repr_abbreviated(value: &VmValue) -> String {
325 let full = repr(value);
326 let chars: Vec<char> = full.chars().collect();
327 if chars.len() <= MAX_LEAF_CHARS {
328 return full;
329 }
330 let keep = MAX_LEAF_CHARS / 2 - 6;
331 let head: String = chars[..keep].iter().collect();
332 let tail: String = chars[chars.len() - keep..].iter().collect();
333 format!("{head} … {tail} ({} characters in all)", chars.len())
334}
335
336fn render_side(value: &VmValue, counterpart: Option<&VmValue>) -> String {
340 let rendered = repr_abbreviated(value);
341 match counterpart {
342 Some(other) if other.type_name() != value.type_name() => {
343 format!("{rendered} ({})", value.type_name())
344 }
345 _ => rendered,
346 }
347}
348
349fn hint_for(actual: &VmValue, expected: &VmValue) -> Option<String> {
353 match (actual, expected) {
354 (VmValue::Float(a), VmValue::Float(e)) => {
355 let gap = (a - e).abs();
356 if gap == 0.0 || !gap.is_finite() {
357 return None;
358 }
359 Some(format!(
360 "These differ by {gap:e}. Floating-point arithmetic is inexact, so exact \
361 equality on computed floats is usually a bug in the test, not the code — \
362 compare with a tolerance using assert_approx."
363 ))
364 }
365 (VmValue::String(a), VmValue::String(e)) => {
366 let index = a
367 .chars()
368 .zip(e.chars())
369 .position(|(x, y)| x != y)
370 .unwrap_or_else(|| a.chars().count().min(e.chars().count()));
371 if a.chars().count() != e.chars().count()
372 && index == a.chars().count().min(e.chars().count())
373 {
374 Some(format!(
375 "The first {index} characters match; the strings differ in length \
376 ({} vs {}).",
377 a.chars().count(),
378 e.chars().count()
379 ))
380 } else {
381 Some(format!("The strings first differ at character {index}."))
382 }
383 }
384 (VmValue::Int(_), VmValue::String(_)) | (VmValue::String(_), VmValue::Int(_)) => Some(
385 "One side is a number and the other is text. If this came from parsed input, \
386 the conversion may be missing."
387 .to_string(),
388 ),
389 _ => None,
390 }
391}
392
393pub fn render_diff(headline: Option<&str>, actual: &VmValue, expected: &VmValue) -> Option<String> {
400 let differences = diff_values(actual, expected);
401 if differences.is_empty() {
402 return None;
403 }
404 let mut out = String::new();
405
406 let root_only = differences.len() == 1 && differences[0].path.is_empty();
410 match (headline, root_only) {
411 (Some(headline), true) => {
412 let _ = writeln!(out, "{headline}.");
413 }
414 (Some(headline), false) => {
415 let _ = writeln!(
416 out,
417 "{headline}: the two values differ in {}.\n",
418 plural(differences.len(), "place", "places")
419 );
420 }
421 (None, true) => {}
422 (None, false) => {
423 let _ = writeln!(
424 out,
425 "The two values differ in {}.\n",
426 plural(differences.len(), "place", "places")
427 );
428 }
429 }
430
431 for difference in differences.iter().take(MAX_DIFFERENCES) {
432 if !difference.path.is_empty() {
433 let _ = writeln!(out, " at {}", difference.path);
434 }
435 match &difference.kind {
436 DifferenceKind::Unequal { actual, expected } => {
437 let _ = writeln!(out, " expected {}", render_side(expected, Some(actual)));
438 let _ = writeln!(out, " actual {}", render_side(actual, Some(expected)));
439 if let Some(hint) = hint_for(actual, expected) {
440 let _ = writeln!(out, " {hint}");
441 }
442 }
443 DifferenceKind::Unexpected { actual } => {
444 let _ = writeln!(out, " expected nothing here");
445 let _ = writeln!(out, " actual {}", repr_abbreviated(actual));
446 }
447 DifferenceKind::Missing { expected } => {
448 let _ = writeln!(out, " expected {}", repr_abbreviated(expected));
449 let _ = writeln!(out, " actual nothing here");
450 }
451 }
452 if !root_only {
453 out.push('\n');
454 }
455 }
456
457 if differences.len() > MAX_DIFFERENCES {
458 let suppressed = differences.len() - MAX_DIFFERENCES;
459 let _ = writeln!(
460 out,
461 " … and {suppressed} more {}.",
462 if suppressed == 1 {
463 "difference"
464 } else {
465 "differences"
466 }
467 );
468 }
469
470 Some(out.trim_end().to_string())
471}
472
473fn plural(count: usize, one: &str, many: &str) -> String {
474 if count == 1 {
475 format!("{count} {one}")
476 } else {
477 format!("{count} {many}")
478 }
479}
480
481#[cfg(test)]
482mod tests;