Skip to main content

snapbox/report/
diff.rs

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
442443  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
455456  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}