1use crate::report::Styled;
2
3pub fn write_diff(
4 writer: &mut dyn std::fmt::Write,
5 expected: &crate::Data,
6 actual: &crate::Data,
7 expected_name: Option<&dyn std::fmt::Display>,
8 actual_name: Option<&dyn std::fmt::Display>,
9 palette: crate::report::Palette,
10) -> Result<(), std::fmt::Error> {
11 #[allow(unused_mut)]
12 let mut rendered = false;
13 #[cfg(feature = "diff")]
14 if let (Some(expected_relevant), Some(actual_relevant)) =
15 (expected.relevant(), actual.relevant())
16 {
17 let expected_rendered = expected.render().unwrap();
18 let expected_line_offset = expected_rendered[..expected_rendered
19 .find(expected_relevant)
20 .unwrap_or(expected_rendered.len())]
21 .lines()
22 .count();
23 let actual_rendered = actual.render().unwrap();
24 let actual_line_offset = actual_rendered[..actual_rendered
25 .find(actual_relevant)
26 .unwrap_or(actual_rendered.len())]
27 .lines()
28 .count();
29 write_diff_inner(
30 writer,
31 expected_relevant,
32 actual_relevant,
33 expected_name,
34 actual_name,
35 palette,
36 expected_line_offset,
37 actual_line_offset,
38 )?;
39 rendered = true;
40 } else if let (Some(expected), Some(actual)) = (expected.render(), actual.render()) {
41 let expected_line_offset = 0;
42 let actual_line_offset = 0;
43 write_diff_inner(
44 writer,
45 &expected,
46 &actual,
47 expected_name,
48 actual_name,
49 palette,
50 expected_line_offset,
51 actual_line_offset,
52 )?;
53 rendered = true;
54 }
55
56 if !rendered {
57 if let Some(expected_name) = expected_name {
58 writeln!(writer, "{} {}:", expected_name, palette.error("(expected)"))?;
59 } else {
60 writeln!(writer, "{}:", palette.error("Expected"))?;
61 }
62 writeln!(writer, "{}", palette.error(&expected))?;
63 if let Some(actual_name) = actual_name {
64 writeln!(writer, "{} {}:", actual_name, palette.info("(actual)"))?;
65 } else {
66 writeln!(writer, "{}:", palette.info("Actual"))?;
67 }
68 writeln!(writer, "{}", palette.info(&actual))?;
69 }
70 Ok(())
71}
72
73#[cfg(feature = "diff")]
74#[allow(clippy::too_many_arguments)]
75fn write_diff_inner(
76 writer: &mut dyn std::fmt::Write,
77 expected: &str,
78 actual: &str,
79 expected_name: Option<&dyn std::fmt::Display>,
80 actual_name: Option<&dyn std::fmt::Display>,
81 palette: crate::report::Palette,
82 expected_line_offset: usize,
83 actual_line_offset: usize,
84) -> Result<(), std::fmt::Error> {
85 let timeout = std::time::Duration::from_millis(500);
86 let min_elide = 20;
87 let context = 5;
88
89 let changes = similar::TextDiff::configure()
90 .algorithm(similar::Algorithm::Patience)
91 .timeout(timeout)
92 .newline_terminated(false)
93 .diff_lines(expected, actual);
94
95 writeln!(writer)?;
96 if let Some(expected_name) = expected_name {
97 writeln!(
98 writer,
99 "{}",
100 palette.error(format_args!("{:->4} expected: {}", "", expected_name))
101 )?;
102 } else {
103 writeln!(writer, "{}", palette.error(format_args!("--- Expected")))?;
104 }
105 if let Some(actual_name) = actual_name {
106 writeln!(
107 writer,
108 "{}",
109 palette.info(format_args!("{:+>4} actual: {}", "", actual_name))
110 )?;
111 } else {
112 writeln!(writer, "{}", palette.info(format_args!("+++ Actual")))?;
113 }
114 let changes = changes
115 .ops()
116 .iter()
117 .flat_map(|op| changes.iter_inline_changes(op))
118 .collect::<Vec<_>>();
119 let tombstones = if min_elide < changes.len() {
120 let mut tombstones = vec![true; changes.len()];
121
122 let mut counter = context;
123 for (i, change) in changes.iter().enumerate() {
124 match change.tag() {
125 similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
126 counter = context;
127 tombstones[i] = false;
128 }
129 similar::ChangeTag::Equal => {
130 if counter != 0 {
131 tombstones[i] = false;
132 counter -= 1;
133 }
134 }
135 }
136 }
137
138 let mut counter = context;
139 for (i, change) in changes.iter().enumerate().rev() {
140 match change.tag() {
141 similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
142 counter = context;
143 tombstones[i] = false;
144 }
145 similar::ChangeTag::Equal => {
146 if counter != 0 {
147 tombstones[i] = false;
148 counter -= 1;
149 }
150 }
151 }
152 }
153 tombstones
154 } else {
155 Vec::new()
156 };
157
158 let mut elided = false;
159 for (i, change) in changes.into_iter().enumerate() {
160 if tombstones.get(i).copied().unwrap_or(false) {
161 if !elided {
162 let sign = "⋮";
163
164 write!(writer, "{:>4} ", " ",)?;
165 write!(writer, "{:>4} ", " ",)?;
166 writeln!(writer, "{}", palette.hint(sign))?;
167 }
168 elided = true;
169 } else {
170 elided = false;
171 match change.tag() {
172 similar::ChangeTag::Insert => {
173 write_change(
174 writer,
175 change,
176 "+",
177 palette.actual,
178 palette.info,
179 palette,
180 expected_line_offset,
181 actual_line_offset,
182 )?;
183 }
184 similar::ChangeTag::Delete => {
185 write_change(
186 writer,
187 change,
188 "-",
189 palette.expected,
190 palette.error,
191 palette,
192 expected_line_offset,
193 actual_line_offset,
194 )?;
195 }
196 similar::ChangeTag::Equal => {
197 write_change(
198 writer,
199 change,
200 "|",
201 palette.hint,
202 palette.hint,
203 palette,
204 expected_line_offset,
205 actual_line_offset,
206 )?;
207 }
208 }
209 }
210 }
211
212 Ok(())
213}
214
215#[cfg(feature = "diff")]
216#[allow(clippy::too_many_arguments)]
217fn write_change(
218 writer: &mut dyn std::fmt::Write,
219 change: similar::InlineChange<'_, str>,
220 sign: &str,
221 em_style: crate::report::Style,
222 style: crate::report::Style,
223 palette: crate::report::Palette,
224 expected_line_offset: usize,
225 actual_line_offset: usize,
226) -> Result<(), std::fmt::Error> {
227 if let Some(index) = change.old_index() {
228 write!(
229 writer,
230 "{:>4} ",
231 palette.hint(index + 1 + expected_line_offset),
232 )?;
233 } else {
234 write!(writer, "{:>4} ", " ",)?;
235 }
236 if let Some(index) = change.new_index() {
237 write!(
238 writer,
239 "{:>4} ",
240 palette.hint(index + 1 + actual_line_offset),
241 )?;
242 } else {
243 write!(writer, "{:>4} ", " ",)?;
244 }
245 write!(writer, "{} ", Styled::new(sign, style))?;
246 for &(emphasized, change) in change.values() {
247 let cur_style = if emphasized { em_style } else { style };
248 write!(writer, "{}", Styled::new(change, cur_style))?;
249 }
250 if change.missing_newline() {
251 writeln!(writer, "{}", Styled::new("∅", em_style))?;
252 }
253
254 Ok(())
255}
256
257#[cfg(test)]
258mod test {
259 use super::*;
260
261 #[cfg(feature = "diff")]
262 #[test]
263 fn diff_eq() {
264 let expected = "Hello\nWorld\n";
265 let expected_name = "A";
266 let actual = "Hello\nWorld\n";
267 let actual_name = "B";
268 let palette = crate::report::Palette::plain();
269
270 let mut actual_diff = String::new();
271 write_diff_inner(
272 &mut actual_diff,
273 expected,
274 actual,
275 Some(&expected_name),
276 Some(&actual_name),
277 palette,
278 0,
279 0,
280 )
281 .unwrap();
282 let expected_diff = "
283---- expected: A
284++++ actual: B
285 1 1 | Hello
286 2 2 | World
287";
288
289 assert_eq!(expected_diff, actual_diff);
290 }
291
292 #[cfg(feature = "diff")]
293 #[test]
294 fn diff_ne_line_missing() {
295 let expected = "Hello\nWorld\n";
296 let expected_name = "A";
297 let actual = "Hello\n";
298 let actual_name = "B";
299 let palette = crate::report::Palette::plain();
300
301 let mut actual_diff = String::new();
302 write_diff_inner(
303 &mut actual_diff,
304 expected,
305 actual,
306 Some(&expected_name),
307 Some(&actual_name),
308 palette,
309 0,
310 0,
311 )
312 .unwrap();
313 let expected_diff = "
314---- expected: A
315++++ actual: B
316 1 1 | Hello
317 2 - World
318";
319
320 assert_eq!(expected_diff, actual_diff);
321 }
322
323 #[cfg(feature = "diff")]
324 #[test]
325 fn diff_eq_trailing_extra_newline() {
326 let expected = "Hello\nWorld";
327 let expected_name = "A";
328 let actual = "Hello\nWorld\n";
329 let actual_name = "B";
330 let palette = crate::report::Palette::plain();
331
332 let mut actual_diff = String::new();
333 write_diff_inner(
334 &mut actual_diff,
335 expected,
336 actual,
337 Some(&expected_name),
338 Some(&actual_name),
339 palette,
340 0,
341 0,
342 )
343 .unwrap();
344 let expected_diff = "
345---- expected: A
346++++ actual: B
347 1 1 | Hello
348 2 - World∅
349 2 + World
350";
351
352 assert_eq!(expected_diff, actual_diff);
353 }
354
355 #[cfg(feature = "diff")]
356 #[test]
357 fn diff_eq_trailing_newline_missing() {
358 let expected = "Hello\nWorld\n";
359 let expected_name = "A";
360 let actual = "Hello\nWorld";
361 let actual_name = "B";
362 let palette = crate::report::Palette::plain();
363
364 let mut actual_diff = String::new();
365 write_diff_inner(
366 &mut actual_diff,
367 expected,
368 actual,
369 Some(&expected_name),
370 Some(&actual_name),
371 palette,
372 0,
373 0,
374 )
375 .unwrap();
376 let expected_diff = "
377---- expected: A
378++++ actual: B
379 1 1 | Hello
380 2 - World
381 2 + World∅
382";
383
384 assert_eq!(expected_diff, actual_diff);
385 }
386
387 #[cfg(feature = "diff")]
388 #[test]
389 fn diff_eq_elided() {
390 let mut expected = String::new();
391 expected.push_str("Hello\n");
392 for i in 0..20 {
393 expected.push_str(&i.to_string());
394 expected.push('\n');
395 }
396 expected.push_str("World\n");
397 for i in 0..20 {
398 expected.push_str(&i.to_string());
399 expected.push('\n');
400 }
401 expected.push_str("!\n");
402 let expected_name = "A";
403
404 let mut actual = String::new();
405 actual.push_str("Goodbye\n");
406 for i in 0..20 {
407 actual.push_str(&i.to_string());
408 actual.push('\n');
409 }
410 actual.push_str("Moon\n");
411 for i in 0..20 {
412 actual.push_str(&i.to_string());
413 actual.push('\n');
414 }
415 actual.push_str("?\n");
416 let actual_name = "B";
417
418 let palette = crate::report::Palette::plain();
419
420 let mut actual_diff = String::new();
421 write_diff_inner(
422 &mut actual_diff,
423 &expected,
424 &actual,
425 Some(&expected_name),
426 Some(&actual_name),
427 palette,
428 0,
429 0,
430 )
431 .unwrap();
432 let expected_diff = "
433---- expected: A
434++++ actual: B
435 1 - Hello
436 1 + Goodbye
437 2 2 | 0
438 3 3 | 1
439 4 4 | 2
440 5 5 | 3
441 6 6 | 4
442 ⋮
443 17 17 | 15
444 18 18 | 16
445 19 19 | 17
446 20 20 | 18
447 21 21 | 19
448 22 - World
449 22 + Moon
450 23 23 | 0
451 24 24 | 1
452 25 25 | 2
453 26 26 | 3
454 27 27 | 4
455 ⋮
456 38 38 | 15
457 39 39 | 16
458 40 40 | 17
459 41 41 | 18
460 42 42 | 19
461 43 - !
462 43 + ?
463";
464
465 assert_eq!(expected_diff, actual_diff);
466 }
467
468 #[cfg(feature = "diff")]
469 #[cfg(feature = "term-svg")]
470 #[test]
471 fn diff_ne_ignore_irrelevant_details() {
472 let expected = "<svg width='100px' height='200px'>
473<text>
474Hello Moon
475</text>
476</svg>";
477 let expected_name = "A";
478 let actual = "<svg width='200px' height='400px'>
479<text>
480Hello World
481</text>
482</svg>";
483 let actual_name = "B";
484 let palette = crate::report::Palette::plain();
485
486 let mut actual_diff = String::new();
487 write_diff(
488 &mut actual_diff,
489 &crate::Data::with_value(crate::data::DataValue::TermSvg(expected.to_owned())),
490 &crate::Data::with_value(crate::data::DataValue::TermSvg(actual.to_owned())),
491 Some(&expected_name),
492 Some(&actual_name),
493 palette,
494 )
495 .unwrap();
496 let expected_diff = "
497---- expected: A
498++++ actual: B
499 2 2 | <text>
500 3 - Hello Moon
501 3 + Hello World
502 4 4 | </text>
503";
504
505 assert_eq!(expected_diff, actual_diff);
506 }
507}