• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #![doc(hidden)]
16 
17 use crate::matcher_support::edit_distance;
18 #[rustversion::since(1.70)]
19 use std::io::IsTerminal;
20 use std::{borrow::Cow, cell::Cell, fmt::Display};
21 
22 /// Returns a string describing how the expected and actual lines differ.
23 ///
24 /// This is included in a match explanation for [`EqMatcher`] and
25 /// [`crate::matchers::str_matcher::StrMatcher`].
26 ///
27 /// If the actual value has less than two lines, or the two differ by more than
28 /// the maximum edit distance, then this returns the empty string. If the two
29 /// are equal, it returns a simple statement that they are equal. Otherwise,
30 /// this constructs a unified diff view of the actual and expected values.
create_diff( actual_debug: &str, expected_debug: &str, diff_mode: edit_distance::Mode, ) -> Cow<'static, str>31 pub(crate) fn create_diff(
32     actual_debug: &str,
33     expected_debug: &str,
34     diff_mode: edit_distance::Mode,
35 ) -> Cow<'static, str> {
36     if actual_debug.lines().count() < 2 {
37         // If the actual debug is only one line, then there is no point in doing a
38         // line-by-line diff.
39         return "".into();
40     }
41 
42     match edit_distance::edit_list(actual_debug.lines(), expected_debug.lines(), diff_mode) {
43         edit_distance::Difference::Equal => {
44             // str.lines() is oblivious to the last newline in a
45             // string, so we need to check this to make sure we don't spuriously
46             // claim that 'hello' and 'hello\n' are identical debug strings.
47             //
48             // Although we would have liked to resolve by replacing
49             // str::lines() with str::split('\n'), the potentially
50             // empty last element interferes with good diff output for
51             // "contains" checks.
52             let actual_newline_terminated = actual_debug.ends_with('\n');
53             let expected_newline_terminated = expected_debug.ends_with('\n');
54             if actual_newline_terminated && !expected_newline_terminated {
55                 "Actual includes a terminating newline that is absent from expected.".into()
56             } else if !actual_newline_terminated && expected_newline_terminated {
57                 "Actual omits a terminating newline that is present in expected.".into()
58             } else {
59                 "No difference found between debug strings.".into()
60             }
61         }
62         edit_distance::Difference::Editable(edit_list) => {
63             format!("{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
64                 .into()
65         }
66         edit_distance::Difference::Unrelated => "".into(),
67     }
68 }
69 
70 /// Returns a string describing how the expected and actual differ after
71 /// reversing the lines in each.
72 ///
73 /// This is similar to [`create_diff`] except that it first reverses the lines
74 /// in both the expected and actual values, then reverses the constructed edit
75 /// list. When `diff_mode` is [`edit_distance::Mode::Prefix`], this becomes a
76 /// diff of the suffix for use by [`ends_with`][crate::matchers::ends_with].
create_diff_reversed( actual_debug: &str, expected_debug: &str, diff_mode: edit_distance::Mode, ) -> Cow<'static, str>77 pub(crate) fn create_diff_reversed(
78     actual_debug: &str,
79     expected_debug: &str,
80     diff_mode: edit_distance::Mode,
81 ) -> Cow<'static, str> {
82     if actual_debug.lines().count() < 2 {
83         // If the actual debug is only one line, then there is no point in doing a
84         // line-by-line diff.
85         return "".into();
86     }
87     let mut actual_lines_reversed = actual_debug.lines().collect::<Vec<_>>();
88     let mut expected_lines_reversed = expected_debug.lines().collect::<Vec<_>>();
89     actual_lines_reversed.reverse();
90     expected_lines_reversed.reverse();
91     match edit_distance::edit_list(actual_lines_reversed, expected_lines_reversed, diff_mode) {
92         edit_distance::Difference::Equal => "No difference found between debug strings.".into(),
93         edit_distance::Difference::Editable(mut edit_list) => {
94             edit_list.reverse();
95             format!("{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
96                 .into()
97         }
98         edit_distance::Difference::Unrelated => "".into(),
99     }
100 }
101 
102 // Produces the header, with or without coloring depending on
103 // USE_COLOR
summary_header() -> Cow<'static, str>104 fn summary_header() -> Cow<'static, str> {
105     if USE_COLOR.with(Cell::get) {
106         format!(
107             "Difference(-{ACTUAL_ONLY_STYLE}actual{RESET_ALL} / +{EXPECTED_ONLY_STYLE}expected{RESET_ALL}):"
108         ).into()
109     } else {
110         "Difference(-actual / +expected):".into()
111     }
112 }
113 
114 // Aggregator collecting the lines to be printed in the difference summary.
115 //
116 // This is buffered in order to allow a future line to potentially impact how
117 // the current line would be printed.
118 #[derive(Default)]
119 struct BufferedSummary<'a> {
120     summary: SummaryBuilder,
121     buffer: Buffer<'a>,
122 }
123 
124 impl<'a> BufferedSummary<'a> {
125     // Appends a new line which is common to both actual and expected.
feed_common_lines(&mut self, common_line: &'a str)126     fn feed_common_lines(&mut self, common_line: &'a str) {
127         if let Buffer::CommonLines(ref mut common_lines) = self.buffer {
128             common_lines.push(common_line);
129         } else {
130             self.flush_buffer();
131             self.buffer = Buffer::CommonLines(vec![common_line]);
132         }
133     }
134 
135     // Appends a new line which is found only in the actual string.
feed_extra_actual(&mut self, extra_actual: &'a str)136     fn feed_extra_actual(&mut self, extra_actual: &'a str) {
137         if let Buffer::ExtraExpectedLineChunk(extra_expected) = self.buffer {
138             self.print_inline_diffs(extra_actual, extra_expected);
139             self.buffer = Buffer::Empty;
140         } else {
141             self.flush_buffer();
142             self.buffer = Buffer::ExtraActualLineChunk(extra_actual);
143         }
144     }
145 
146     // Appends a new line which is found only in the expected string.
feed_extra_expected(&mut self, extra_expected: &'a str)147     fn feed_extra_expected(&mut self, extra_expected: &'a str) {
148         if let Buffer::ExtraActualLineChunk(extra_actual) = self.buffer {
149             self.print_inline_diffs(extra_actual, extra_expected);
150             self.buffer = Buffer::Empty;
151         } else {
152             self.flush_buffer();
153             self.buffer = Buffer::ExtraExpectedLineChunk(extra_expected);
154         }
155     }
156 
157     // Appends a comment for the additional line at the start or the end of the
158     // actual string which should be omitted.
feed_additional_actual(&mut self)159     fn feed_additional_actual(&mut self) {
160         self.flush_buffer();
161         self.summary.new_line();
162         self.summary.push_str_as_comment("<---- remaining lines omitted ---->");
163     }
164 
flush_buffer(&mut self)165     fn flush_buffer(&mut self) {
166         self.buffer.flush(&mut self.summary);
167     }
168 
print_inline_diffs(&mut self, actual_line: &str, expected_line: &str)169     fn print_inline_diffs(&mut self, actual_line: &str, expected_line: &str) {
170         let line_edits = edit_distance::edit_list(
171             actual_line.chars(),
172             expected_line.chars(),
173             edit_distance::Mode::Exact,
174         );
175 
176         if let edit_distance::Difference::Editable(edit_list) = line_edits {
177             let mut actual_summary = SummaryBuilder::default();
178             actual_summary.new_line_for_actual();
179             let mut expected_summary = SummaryBuilder::default();
180             expected_summary.new_line_for_expected();
181             for edit in &edit_list {
182                 match edit {
183                     edit_distance::Edit::ExtraActual(c) => actual_summary.push_actual_only(*c),
184                     edit_distance::Edit::ExtraExpected(c) => {
185                         expected_summary.push_expected_only(*c)
186                     }
187                     edit_distance::Edit::Both(c) => {
188                         actual_summary.push_actual_with_match(*c);
189                         expected_summary.push_expected_with_match(*c);
190                     }
191                     edit_distance::Edit::AdditionalActual => {
192                         // Calling edit_distance::edit_list(_, _, Mode::Exact) should never return
193                         // this enum
194                         panic!("This should not happen. This is a bug in gtest_rust")
195                     }
196                 }
197             }
198             actual_summary.reset_ansi();
199             expected_summary.reset_ansi();
200             self.summary.push_str(&actual_summary.summary);
201             self.summary.push_str(&expected_summary.summary);
202         } else {
203             self.summary.new_line_for_actual();
204             self.summary.push_str_actual_only(actual_line);
205             self.summary.new_line_for_expected();
206             self.summary.push_str_expected_only(expected_line);
207         }
208     }
209 }
210 
211 impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> {
from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self212     fn from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self {
213         let mut buffered_summary = BufferedSummary::default();
214         for edit in iter {
215             match edit {
216                 edit_distance::Edit::Both(same) => {
217                     buffered_summary.feed_common_lines(same);
218                 }
219                 edit_distance::Edit::ExtraActual(actual) => {
220                     buffered_summary.feed_extra_actual(actual);
221                 }
222                 edit_distance::Edit::ExtraExpected(expected) => {
223                     buffered_summary.feed_extra_expected(expected);
224                 }
225                 edit_distance::Edit::AdditionalActual => {
226                     buffered_summary.feed_additional_actual();
227                 }
228             };
229         }
230         buffered_summary.flush_buffer();
231         buffered_summary.summary.reset_ansi();
232 
233         buffered_summary
234     }
235 }
236 
237 impl<'a> Display for BufferedSummary<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result238     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239         if !matches!(self.buffer, Buffer::Empty) {
240             panic!("Buffer is not empty. This is a bug in gtest_rust.")
241         }
242         if !self.summary.last_ansi_style.is_empty() {
243             panic!("ANSI style has not been reset. This is a bug in gtest_rust.")
244         }
245         self.summary.summary.fmt(f)
246     }
247 }
248 
249 enum Buffer<'a> {
250     Empty,
251     CommonLines(Vec<&'a str>),
252     ExtraActualLineChunk(&'a str),
253     ExtraExpectedLineChunk(&'a str),
254 }
255 
256 impl<'a> Buffer<'a> {
flush(&mut self, summary: &mut SummaryBuilder)257     fn flush(&mut self, summary: &mut SummaryBuilder) {
258         match self {
259             Buffer::Empty => {}
260             Buffer::CommonLines(common_lines) => {
261                 Self::flush_common_lines(std::mem::take(common_lines), summary);
262             }
263             Buffer::ExtraActualLineChunk(extra_actual) => {
264                 summary.new_line_for_actual();
265                 summary.push_str_actual_only(extra_actual);
266             }
267             Buffer::ExtraExpectedLineChunk(extra_expected) => {
268                 summary.new_line_for_expected();
269                 summary.push_str_expected_only(extra_expected);
270             }
271         };
272         *self = Buffer::Empty;
273     }
274 
flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder)275     fn flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder) {
276         // The number of the lines kept before and after the compressed lines.
277         const COMMON_LINES_CONTEXT_SIZE: usize = 2;
278 
279         if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 {
280             for line in common_lines {
281                 summary.new_line();
282                 summary.push_str(line);
283             }
284             return;
285         }
286 
287         let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE];
288 
289         for line in start_context {
290             summary.new_line();
291             summary.push_str(line);
292         }
293 
294         summary.new_line();
295         summary.push_str_as_comment(&format!(
296             "<---- {} common lines omitted ---->",
297             common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE,
298         ));
299 
300         let end_context =
301             &common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()];
302 
303         for line in end_context {
304             summary.new_line();
305             summary.push_str(line);
306         }
307     }
308 }
309 
310 impl<'a> Default for Buffer<'a> {
default() -> Self311     fn default() -> Self {
312         Self::Empty
313     }
314 }
315 
316 thread_local! {
317   pub(crate) static USE_COLOR: Cell<bool> = Cell::new(stdout_supports_color());
318 }
319 
320 #[rustversion::since(1.70)]
stdout_supports_color() -> bool321 fn stdout_supports_color() -> bool {
322     #[allow(clippy::incompatible_msrv)]
323     match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) {
324         (true, _) => false,
325         (false, true) => true,
326         (false, false) => std::io::stdout().is_terminal(),
327     }
328 }
329 
330 #[rustversion::not(since(1.70))]
stdout_supports_color() -> bool331 fn stdout_supports_color() -> bool {
332     is_env_var_set("FORCE_COLOR") && !is_env_var_set("NO_COLOR")
333 }
334 
is_env_var_set(var: &'static str) -> bool335 fn is_env_var_set(var: &'static str) -> bool {
336     std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false)
337 }
338 
339 // Font in italic
340 const COMMENT_STYLE: &str = "\x1B[3m";
341 // Font in green and bold
342 const EXPECTED_ONLY_STYLE: &str = "\x1B[1;32m";
343 // Font in red and bold
344 const ACTUAL_ONLY_STYLE: &str = "\x1B[1;31m";
345 // Font in green onlyh
346 const EXPECTED_WITH_MATCH_STYLE: &str = "\x1B[32m";
347 // Font in red only
348 const ACTUAL_WITH_MATCH_STYLE: &str = "\x1B[31m";
349 // Reset all ANSI formatting
350 const RESET_ALL: &str = "\x1B[0m";
351 
352 #[derive(Default)]
353 struct SummaryBuilder {
354     summary: String,
355     last_ansi_style: &'static str,
356 }
357 
358 impl SummaryBuilder {
push_str(&mut self, element: &str)359     fn push_str(&mut self, element: &str) {
360         self.reset_ansi();
361         self.summary.push_str(element);
362     }
363 
push_str_as_comment(&mut self, element: &str)364     fn push_str_as_comment(&mut self, element: &str) {
365         self.set_ansi(COMMENT_STYLE);
366         self.summary.push_str(element);
367     }
368 
push_str_actual_only(&mut self, element: &str)369     fn push_str_actual_only(&mut self, element: &str) {
370         self.set_ansi(ACTUAL_ONLY_STYLE);
371         self.summary.push_str(element);
372     }
373 
push_str_expected_only(&mut self, element: &str)374     fn push_str_expected_only(&mut self, element: &str) {
375         self.set_ansi(EXPECTED_ONLY_STYLE);
376         self.summary.push_str(element);
377     }
378 
push_actual_only(&mut self, element: char)379     fn push_actual_only(&mut self, element: char) {
380         self.set_ansi(ACTUAL_ONLY_STYLE);
381         self.summary.push(element);
382     }
383 
push_expected_only(&mut self, element: char)384     fn push_expected_only(&mut self, element: char) {
385         self.set_ansi(EXPECTED_ONLY_STYLE);
386         self.summary.push(element);
387     }
388 
push_actual_with_match(&mut self, element: char)389     fn push_actual_with_match(&mut self, element: char) {
390         self.set_ansi(ACTUAL_WITH_MATCH_STYLE);
391         self.summary.push(element);
392     }
393 
push_expected_with_match(&mut self, element: char)394     fn push_expected_with_match(&mut self, element: char) {
395         self.set_ansi(EXPECTED_WITH_MATCH_STYLE);
396         self.summary.push(element);
397     }
398 
new_line(&mut self)399     fn new_line(&mut self) {
400         self.reset_ansi();
401         self.summary.push_str("\n ");
402     }
403 
new_line_for_actual(&mut self)404     fn new_line_for_actual(&mut self) {
405         self.reset_ansi();
406         self.summary.push_str("\n-");
407     }
408 
new_line_for_expected(&mut self)409     fn new_line_for_expected(&mut self) {
410         self.reset_ansi();
411         self.summary.push_str("\n+");
412     }
413 
reset_ansi(&mut self)414     fn reset_ansi(&mut self) {
415         if !self.last_ansi_style.is_empty() && USE_COLOR.with(Cell::get) {
416             self.summary.push_str(RESET_ALL);
417             self.last_ansi_style = "";
418         }
419     }
420 
set_ansi(&mut self, ansi_style: &'static str)421     fn set_ansi(&mut self, ansi_style: &'static str) {
422         if !USE_COLOR.with(Cell::get) || self.last_ansi_style == ansi_style {
423             return;
424         }
425         if !self.last_ansi_style.is_empty() {
426             self.summary.push_str(RESET_ALL);
427         }
428         self.summary.push_str(ansi_style);
429         self.last_ansi_style = ansi_style;
430     }
431 }
432 
433 #[cfg(test)]
434 mod tests {
435     use super::*;
436     use crate::{matcher_support::edit_distance::Mode, prelude::*};
437     use indoc::indoc;
438     use std::fmt::Write;
439 
440     // Make a long text with each element of the iterator on one line.
441     // `collection` must contains at least one element.
build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String442     fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String {
443         let mut text = String::new();
444         write!(&mut text, "{}", collection.next().expect("Provided collection without elements"))
445             .unwrap();
446         for item in collection {
447             write!(&mut text, "\n{}", item).unwrap();
448         }
449         text
450     }
451 
452     #[test]
create_diff_smaller_than_one_line() -> Result<()>453     fn create_diff_smaller_than_one_line() -> Result<()> {
454         verify_that!(create_diff("One", "Two", Mode::Exact), eq(""))
455     }
456 
457     #[test]
create_diff_exact_same() -> Result<()>458     fn create_diff_exact_same() -> Result<()> {
459         let expected = indoc! {"
460             One
461             Two
462             "};
463         let actual = indoc! {"
464         One
465         Two
466         "};
467         verify_that!(
468             create_diff(expected, actual, Mode::Exact),
469             eq("No difference found between debug strings.")
470         )
471     }
472 
473     #[test]
create_diff_multiline_diff() -> Result<()>474     fn create_diff_multiline_diff() -> Result<()> {
475         let expected = indoc! {"
476             prefix
477             Actual#1
478             Actual#2
479             Actual#3
480             suffix"};
481         let actual = indoc! {"
482             prefix
483             Expected@one
484             Expected@two
485             suffix"};
486         // TODO: It would be better to have all the Actual together followed by all the
487         // Expected together.
488         verify_that!(
489             create_diff(expected, actual, Mode::Exact),
490             eq(indoc!(
491                 "
492                 Difference(-actual / +expected):
493                  prefix
494                 -Actual#1
495                 +Expected@one
496                 -Actual#2
497                 +Expected@two
498                 -Actual#3
499                  suffix"
500             ))
501         )
502     }
503 
504     #[test]
create_diff_exact_unrelated() -> Result<()>505     fn create_diff_exact_unrelated() -> Result<()> {
506         verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq(""))
507     }
508 
509     #[test]
create_diff_exact_small_difference() -> Result<()>510     fn create_diff_exact_small_difference() -> Result<()> {
511         verify_that!(
512             create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
513             eq(indoc! {
514                 "
515                 Difference(-actual / +expected):
516                  1
517                  2
518                  <---- 45 common lines omitted ---->
519                  48
520                  49
521                 +50"
522             })
523         )
524     }
525 
526     #[test]
create_diff_exact_small_difference_with_color() -> Result<()>527     fn create_diff_exact_small_difference_with_color() -> Result<()> {
528         USE_COLOR.with(|cell| cell.set(true));
529 
530         verify_that!(
531             create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
532             eq(indoc! {
533                 "
534                 Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
535                  1
536                  2
537                  \x1B[3m<---- 45 common lines omitted ---->\x1B[0m
538                  48
539                  49
540                 +\x1B[1;32m50\x1B[0m"
541             })
542         )
543     }
544 
545     #[test]
create_diff_exact_difference_with_inline_color() -> Result<()>546     fn create_diff_exact_difference_with_inline_color() -> Result<()> {
547         USE_COLOR.with(|cell| cell.set(true));
548 
549         let actual = indoc!(
550             "There is a home in Nouvelle Orleans
551             They say, it is the rising sons
552             And it has been the ruin of many a po'boy"
553         );
554 
555         let expected = indoc!(
556             "There is a house way down in New Orleans
557             They call the rising sun
558             And it has been the ruin of many a poor boy"
559         );
560 
561         verify_that!(
562             create_diff(actual, expected, Mode::Exact),
563             eq(indoc! {
564                 "
565                 Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
566                 -\x1B[31mThere is a ho\x1B[0m\x1B[1;31mm\x1B[0m\x1B[31me in N\x1B[0m\x1B[1;31mouv\x1B[0m\x1B[31me\x1B[0m\x1B[1;31mlle\x1B[0m\x1B[31m Orleans\x1B[0m
567                 +\x1B[32mThere is a ho\x1B[0m\x1B[1;32mus\x1B[0m\x1B[32me \x1B[0m\x1B[1;32mway down \x1B[0m\x1B[32min Ne\x1B[0m\x1B[1;32mw\x1B[0m\x1B[32m Orleans\x1B[0m
568                 -\x1B[31mThey \x1B[0m\x1B[1;31ms\x1B[0m\x1B[31ma\x1B[0m\x1B[1;31my,\x1B[0m\x1B[31m \x1B[0m\x1B[1;31mi\x1B[0m\x1B[31mt\x1B[0m\x1B[1;31m is t\x1B[0m\x1B[31mhe rising s\x1B[0m\x1B[1;31mo\x1B[0m\x1B[31mn\x1B[0m\x1B[1;31ms\x1B[0m
569                 +\x1B[32mThey \x1B[0m\x1B[1;32mc\x1B[0m\x1B[32ma\x1B[0m\x1B[1;32mll\x1B[0m\x1B[32m the rising s\x1B[0m\x1B[1;32mu\x1B[0m\x1B[32mn\x1B[0m
570                 -\x1B[31mAnd it has been the ruin of many a po\x1B[0m\x1B[1;31m'\x1B[0m\x1B[31mboy\x1B[0m
571                 +\x1B[32mAnd it has been the ruin of many a po\x1B[0m\x1B[1;32mor \x1B[0m\x1B[32mboy\x1B[0m"
572             })
573         )
574     }
575 
576     #[test]
create_diff_line_termination_diff() -> Result<()>577     fn create_diff_line_termination_diff() -> Result<()> {
578         verify_that!(
579             create_diff("1\n2\n3", "1\n2\n3\n", Mode::Exact),
580             eq("Actual omits a terminating newline that is present in expected.")
581         )?;
582         verify_that!(
583             create_diff("1\n2\n3\n", "1\n2\n3", Mode::Exact),
584             eq("Actual includes a terminating newline that is absent from expected.")
585         )?;
586         verify_that!(
587             create_diff("1\n2\n3\n", "1\n2\n3\n", Mode::Exact),
588             eq("No difference found between debug strings.")
589         )
590     }
591 }
592