pretty_assertions/
printer.rs

1#[cfg(feature = "alloc")]
2use alloc::format;
3use ansi_term::{
4    Colour::{Fixed, Green, Red},
5    Style,
6};
7use core::fmt;
8
9macro_rules! paint {
10    ($f:expr, $colour:expr, $fmt:expr, $($args:tt)*) => (
11        write!($f, "{}", $colour.paint(format!($fmt, $($args)*)))
12    )
13}
14
15const SIGN_RIGHT: char = '>'; // + > →
16const SIGN_LEFT: char = '<'; // - < ←
17
18/// Present the diff output for two mutliline strings in a pretty, colorised manner.
19pub(crate) fn write_header(f: &mut fmt::Formatter) -> fmt::Result {
20    writeln!(
21        f,
22        "{} {} / {} :",
23        Style::new().bold().paint("Diff"),
24        Red.paint(format!("{} left", SIGN_LEFT)),
25        Green.paint(format!("right {}", SIGN_RIGHT))
26    )
27}
28
29/// Delay formatting this deleted chunk until later.
30///
31/// It can be formatted as a whole chunk by calling `flush`, or the inner value
32/// obtained with `take` for further processing (such as an inline diff).
33#[derive(Default)]
34struct LatentDeletion<'a> {
35    // The most recent deleted line we've seen
36    value: Option<&'a str>,
37    // The number of deleted lines we've seen, including the current value
38    count: usize,
39}
40
41impl<'a> LatentDeletion<'a> {
42    /// Set the chunk value.
43    fn set(&mut self, value: &'a str) {
44        self.value = Some(value);
45        self.count += 1;
46    }
47
48    /// Take the underlying chunk value, if it's suitable for inline diffing.
49    ///
50    /// If there is no value or we've seen more than one line, return `None`.
51    fn take(&mut self) -> Option<&'a str> {
52        if self.count == 1 {
53            self.value.take()
54        } else {
55            None
56        }
57    }
58
59    /// If a value is set, print it as a whole chunk, using the given formatter.
60    ///
61    /// If a value is not set, reset the count to zero (as we've called `flush` twice,
62    /// without seeing another deletion. Therefore the line in the middle was something else).
63    fn flush<TWrite: fmt::Write>(&mut self, f: &mut TWrite) -> fmt::Result {
64        if let Some(value) = self.value {
65            paint!(f, Red, "{}{}", SIGN_LEFT, value)?;
66            writeln!(f)?;
67            self.value = None;
68        } else {
69            self.count = 0;
70        }
71
72        Ok(())
73    }
74}
75
76// Adapted from:
77// https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs
78// Credits johannhof (MIT License)
79
80/// Present the diff output for two mutliline strings in a pretty, colorised manner.
81pub(crate) fn write_lines<TWrite: fmt::Write>(
82    f: &mut TWrite,
83    left: &str,
84    right: &str,
85) -> fmt::Result {
86    let diff = ::diff::lines(left, right);
87
88    let mut changes = diff.into_iter().peekable();
89    let mut previous_deletion = LatentDeletion::default();
90
91    while let Some(change) = changes.next() {
92        match (change, changes.peek()) {
93            // If the text is unchanged, just print it plain
94            (::diff::Result::Both(value, _), _) => {
95                previous_deletion.flush(f)?;
96                writeln!(f, " {}", value)?;
97            }
98            // Defer any deletions to next loop
99            (::diff::Result::Left(deleted), _) => {
100                previous_deletion.flush(f)?;
101                previous_deletion.set(deleted);
102            }
103            // If we're being followed by more insertions, don't inline diff
104            (::diff::Result::Right(inserted), Some(::diff::Result::Right(_))) => {
105                previous_deletion.flush(f)?;
106                paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
107                writeln!(f)?;
108            }
109            // Otherwise, check if we need to inline diff with the previous line (if it was a deletion)
110            (::diff::Result::Right(inserted), _) => {
111                if let Some(deleted) = previous_deletion.take() {
112                    write_inline_diff(f, deleted, inserted)?;
113                } else {
114                    previous_deletion.flush(f)?;
115                    paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
116                    writeln!(f)?;
117                }
118            }
119        };
120    }
121
122    previous_deletion.flush(f)?;
123    Ok(())
124}
125
126/// Group character styling for an inline diff, to prevent wrapping each single
127/// character in terminal styling codes.
128///
129/// Styles are applied automatically each time a new style is given in `write_with_style`.
130struct InlineWriter<'a, Writer> {
131    f: &'a mut Writer,
132    style: Style,
133}
134
135impl<'a, Writer> InlineWriter<'a, Writer>
136where
137    Writer: fmt::Write,
138{
139    fn new(f: &'a mut Writer) -> Self {
140        InlineWriter {
141            f,
142            style: Style::new(),
143        }
144    }
145
146    /// Push a new character into the buffer, specifying the style it should be written in.
147    fn write_with_style(&mut self, c: &char, style: Style) -> fmt::Result {
148        // If the style is the same as previously, just write character
149        if style == self.style {
150            write!(self.f, "{}", c)?;
151        } else {
152            // Close out previous style
153            write!(self.f, "{}", self.style.suffix())?;
154
155            // Store new style and start writing it
156            write!(self.f, "{}{}", style.prefix(), c)?;
157            self.style = style;
158        }
159        Ok(())
160    }
161
162    /// Finish any existing style and reset to default state.
163    fn finish(&mut self) -> fmt::Result {
164        // Close out previous style
165        writeln!(self.f, "{}", self.style.suffix())?;
166        self.style = Default::default();
167        Ok(())
168    }
169}
170
171/// Format a single line to show an inline diff of the two strings given.
172///
173/// The given strings should not have a trailing newline.
174///
175/// The output of this function will be two lines, each with a trailing newline.
176fn write_inline_diff<TWrite: fmt::Write>(f: &mut TWrite, left: &str, right: &str) -> fmt::Result {
177    let diff = ::diff::chars(left, right);
178    let mut writer = InlineWriter::new(f);
179
180    // Print the left string on one line, with differences highlighted
181    let light = Red.into();
182    let heavy = Red.on(Fixed(52)).bold();
183    writer.write_with_style(&SIGN_LEFT, light)?;
184    for change in diff.iter() {
185        match change {
186            ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
187            ::diff::Result::Left(value) => writer.write_with_style(value, heavy)?,
188            _ => (),
189        }
190    }
191    writer.finish()?;
192
193    // Print the right string on one line, with differences highlighted
194    let light = Green.into();
195    let heavy = Green.on(Fixed(22)).bold();
196    writer.write_with_style(&SIGN_RIGHT, light)?;
197    for change in diff.iter() {
198        match change {
199            ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
200            ::diff::Result::Right(value) => writer.write_with_style(value, heavy)?,
201            _ => (),
202        }
203    }
204    writer.finish()
205}
206
207#[cfg(test)]
208mod test {
209    use super::*;
210
211    #[cfg(feature = "alloc")]
212    use alloc::string::String;
213
214    // ANSI terminal codes used in our outputs.
215    //
216    // Interpolate these into test strings to make expected values easier to read.
217    const RED_LIGHT: &str = "\u{1b}[31m";
218    const GREEN_LIGHT: &str = "\u{1b}[32m";
219    const RED_HEAVY: &str = "\u{1b}[1;48;5;52;31m";
220    const GREEN_HEAVY: &str = "\u{1b}[1;48;5;22;32m";
221    const RESET: &str = "\u{1b}[0m";
222
223    /// Given that both of our diff printing functions have the same
224    /// type signature, we can reuse the same test code for them.
225    ///
226    /// This could probably be nicer with traits!
227    fn check_printer<TPrint>(printer: TPrint, left: &str, right: &str, expected: &str)
228    where
229        TPrint: Fn(&mut String, &str, &str) -> fmt::Result,
230    {
231        let mut actual = String::new();
232        printer(&mut actual, left, right).expect("printer function failed");
233
234        // Cannot use IO without stdlib
235        #[cfg(feature = "std")]
236        println!(
237            "## left ##\n\
238             {}\n\
239             ## right ##\n\
240             {}\n\
241             ## actual diff ##\n\
242             {}\n\
243             ## expected diff ##\n\
244             {}",
245            left, right, actual, expected
246        );
247        assert_eq!(actual, expected);
248    }
249
250    #[test]
251    fn write_inline_diff_empty() {
252        let left = "";
253        let right = "";
254        let expected = format!(
255            "{red_light}<{reset}\n\
256             {green_light}>{reset}\n",
257            red_light = RED_LIGHT,
258            green_light = GREEN_LIGHT,
259            reset = RESET,
260        );
261
262        check_printer(write_inline_diff, left, right, &expected);
263    }
264
265    #[test]
266    fn write_inline_diff_added() {
267        let left = "";
268        let right = "polymerase";
269        let expected = format!(
270            "{red_light}<{reset}\n\
271             {green_light}>{reset}{green_heavy}polymerase{reset}\n",
272            red_light = RED_LIGHT,
273            green_light = GREEN_LIGHT,
274            green_heavy = GREEN_HEAVY,
275            reset = RESET,
276        );
277
278        check_printer(write_inline_diff, left, right, &expected);
279    }
280
281    #[test]
282    fn write_inline_diff_removed() {
283        let left = "polyacrylamide";
284        let right = "";
285        let expected = format!(
286            "{red_light}<{reset}{red_heavy}polyacrylamide{reset}\n\
287             {green_light}>{reset}\n",
288            red_light = RED_LIGHT,
289            green_light = GREEN_LIGHT,
290            red_heavy = RED_HEAVY,
291            reset = RESET,
292        );
293
294        check_printer(write_inline_diff, left, right, &expected);
295    }
296
297    #[test]
298    fn write_inline_diff_changed() {
299        let left = "polymerase";
300        let right = "polyacrylamide";
301        let expected = format!(
302            "{red_light}<poly{reset}{red_heavy}me{reset}{red_light}ra{reset}{red_heavy}s{reset}{red_light}e{reset}\n\
303             {green_light}>poly{reset}{green_heavy}ac{reset}{green_light}r{reset}{green_heavy}yl{reset}{green_light}a{reset}{green_heavy}mid{reset}{green_light}e{reset}\n",
304            red_light = RED_LIGHT,
305            green_light = GREEN_LIGHT,
306            red_heavy = RED_HEAVY,
307            green_heavy = GREEN_HEAVY,
308            reset = RESET,
309        );
310
311        check_printer(write_inline_diff, left, right, &expected);
312    }
313
314    /// If one of our strings is empty, it should not be shown at all in the output.
315    #[test]
316    fn write_lines_empty_string() {
317        let left = "";
318        let right = "content";
319        let expected = format!(
320            "{green_light}>content{reset}\n",
321            green_light = GREEN_LIGHT,
322            reset = RESET,
323        );
324
325        check_printer(write_lines, left, right, &expected);
326    }
327
328    /// Realistic multiline struct diffing case.
329    #[test]
330    fn write_lines_struct() {
331        let left = r#"Some(
332    Foo {
333        lorem: "Hello World!",
334        ipsum: 42,
335        dolor: Ok(
336            "hey",
337        ),
338    },
339)"#;
340        let right = r#"Some(
341    Foo {
342        lorem: "Hello Wrold!",
343        ipsum: 42,
344        dolor: Ok(
345            "hey ho!",
346        ),
347    },
348)"#;
349        let expected = format!(
350            r#" Some(
351     Foo {{
352{red_light}<        lorem: "Hello W{reset}{red_heavy}o{reset}{red_light}rld!",{reset}
353{green_light}>        lorem: "Hello Wr{reset}{green_heavy}o{reset}{green_light}ld!",{reset}
354         ipsum: 42,
355         dolor: Ok(
356{red_light}<            "hey",{reset}
357{green_light}>            "hey{reset}{green_heavy} ho!{reset}{green_light}",{reset}
358         ),
359     }},
360 )
361"#,
362            red_light = RED_LIGHT,
363            red_heavy = RED_HEAVY,
364            green_light = GREEN_LIGHT,
365            green_heavy = GREEN_HEAVY,
366            reset = RESET,
367        );
368
369        check_printer(write_lines, left, right, &expected);
370    }
371
372    /// Relistic multiple line chunks
373    ///
374    /// We can't support realistic line diffing in large blocks
375    /// (also, it's unclear how usefult this is)
376    ///
377    /// So if we have more than one line in a single removal chunk, disable inline diffing.
378    #[test]
379    fn write_lines_multiline_block() {
380        let left = r#"Proboscis
381Cabbage"#;
382        let right = r#"Probed
383Caravaggio"#;
384        let expected = format!(
385            r#"{red_light}<Proboscis{reset}
386{red_light}<Cabbage{reset}
387{green_light}>Probed{reset}
388{green_light}>Caravaggio{reset}
389"#,
390            red_light = RED_LIGHT,
391            green_light = GREEN_LIGHT,
392            reset = RESET,
393        );
394
395        check_printer(write_lines, left, right, &expected);
396    }
397
398    /// Single deletion line, multiple insertions - no inline diffing.
399    #[test]
400    fn write_lines_multiline_insert() {
401        let left = r#"Cabbage"#;
402        let right = r#"Probed
403Caravaggio"#;
404        let expected = format!(
405            r#"{red_light}<Cabbage{reset}
406{green_light}>Probed{reset}
407{green_light}>Caravaggio{reset}
408"#,
409            red_light = RED_LIGHT,
410            green_light = GREEN_LIGHT,
411            reset = RESET,
412        );
413
414        check_printer(write_lines, left, right, &expected);
415    }
416
417    /// Multiple deletion, single insertion - no inline diffing.
418    #[test]
419    fn write_lines_multiline_delete() {
420        let left = r#"Proboscis
421Cabbage"#;
422        let right = r#"Probed"#;
423        let expected = format!(
424            r#"{red_light}<Proboscis{reset}
425{red_light}<Cabbage{reset}
426{green_light}>Probed{reset}
427"#,
428            red_light = RED_LIGHT,
429            green_light = GREEN_LIGHT,
430            reset = RESET,
431        );
432
433        check_printer(write_lines, left, right, &expected);
434    }
435
436    /// Regression test for multiline highlighting issue
437    #[test]
438    fn write_lines_issue12() {
439        let left = r#"[
440    0,
441    0,
442    0,
443    128,
444    10,
445    191,
446    5,
447    64,
448]"#;
449        let right = r#"[
450    84,
451    248,
452    45,
453    64,
454]"#;
455        let expected = format!(
456            r#" [
457{red_light}<    0,{reset}
458{red_light}<    0,{reset}
459{red_light}<    0,{reset}
460{red_light}<    128,{reset}
461{red_light}<    10,{reset}
462{red_light}<    191,{reset}
463{red_light}<    5,{reset}
464{green_light}>    84,{reset}
465{green_light}>    248,{reset}
466{green_light}>    45,{reset}
467     64,
468 ]
469"#,
470            red_light = RED_LIGHT,
471            green_light = GREEN_LIGHT,
472            reset = RESET,
473        );
474
475        check_printer(write_lines, left, right, &expected);
476    }
477
478    mod write_lines_edge_newlines {
479        use super::*;
480
481        #[test]
482        fn both_trailing() {
483            let left = "fan\n";
484            let right = "mug\n";
485            // Note the additional space at the bottom is caused by a trailing newline
486            // adding an additional line with zero content to both sides of the diff
487            let expected = format!(
488                r#"{red_light}<{reset}{red_heavy}fan{reset}
489{green_light}>{reset}{green_heavy}mug{reset}
490 
491"#,
492                red_light = RED_LIGHT,
493                red_heavy = RED_HEAVY,
494                green_light = GREEN_LIGHT,
495                green_heavy = GREEN_HEAVY,
496                reset = RESET,
497            );
498
499            check_printer(write_lines, left, right, &expected);
500        }
501
502        #[test]
503        fn both_leading() {
504            let left = "\nfan";
505            let right = "\nmug";
506            // Note the additional space at the top is caused by a leading newline
507            // adding an additional line with zero content to both sides of the diff
508            let expected = format!(
509                r#" 
510{red_light}<{reset}{red_heavy}fan{reset}
511{green_light}>{reset}{green_heavy}mug{reset}
512"#,
513                red_light = RED_LIGHT,
514                red_heavy = RED_HEAVY,
515                green_light = GREEN_LIGHT,
516                green_heavy = GREEN_HEAVY,
517                reset = RESET,
518            );
519
520            check_printer(write_lines, left, right, &expected);
521        }
522
523        #[test]
524        fn leading_added() {
525            let left = "fan";
526            let right = "\nmug";
527            let expected = format!(
528                r#"{red_light}<fan{reset}
529{green_light}>{reset}
530{green_light}>mug{reset}
531"#,
532                red_light = RED_LIGHT,
533                green_light = GREEN_LIGHT,
534                reset = RESET,
535            );
536
537            check_printer(write_lines, left, right, &expected);
538        }
539
540        #[test]
541        fn leading_deleted() {
542            let left = "\nfan";
543            let right = "mug";
544            let expected = format!(
545                r#"{red_light}<{reset}
546{red_light}<fan{reset}
547{green_light}>mug{reset}
548"#,
549                red_light = RED_LIGHT,
550                green_light = GREEN_LIGHT,
551                reset = RESET,
552            );
553
554            check_printer(write_lines, left, right, &expected);
555        }
556
557        #[test]
558        fn trailing_added() {
559            let left = "fan";
560            let right = "mug\n";
561            let expected = format!(
562                r#"{red_light}<fan{reset}
563{green_light}>mug{reset}
564{green_light}>{reset}
565"#,
566                red_light = RED_LIGHT,
567                green_light = GREEN_LIGHT,
568                reset = RESET,
569            );
570
571            check_printer(write_lines, left, right, &expected);
572        }
573
574        /// Regression test for double abort
575        ///
576        /// See: https://github.com/colin-kiegel/rust-pretty-assertions/issues/96
577        #[test]
578        fn trailing_deleted() {
579            // The below inputs caused an abort via double panic
580            // we panicked at 'insertion followed by deletion'
581            let left = "fan\n";
582            let right = "mug";
583            let expected = format!(
584                r#"{red_light}<{reset}{red_heavy}fan{reset}
585{green_light}>{reset}{green_heavy}mug{reset}
586{red_light}<{reset}
587"#,
588                red_light = RED_LIGHT,
589                red_heavy = RED_HEAVY,
590                green_light = GREEN_LIGHT,
591                green_heavy = GREEN_HEAVY,
592                reset = RESET,
593            );
594
595            check_printer(write_lines, left, right, &expected);
596        }
597    }
598}