Coverage Report

Created: 2026-02-26 07:32

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