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, 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     match edit_distance::edit_list(actual_debug.lines(), expected_debug.lines(), diff_mode) {
42         edit_distance::Difference::Equal => "No difference found between debug strings.".into(),
43         edit_distance::Difference::Editable(edit_list) => {
44             format!("\n{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
45                 .into()
46         }
47         edit_distance::Difference::Unrelated => "".into(),
48     }
49 }
50 
51 /// Returns a string describing how the expected and actual differ after
52 /// reversing the lines in each.
53 ///
54 /// This is similar to [`create_diff`] except that it first reverses the lines
55 /// in both the expected and actual values, then reverses the constructed edit
56 /// list. When `diff_mode` is [`edit_distance::Mode::Prefix`], this becomes a
57 /// 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>58 pub(crate) fn create_diff_reversed(
59     actual_debug: &str,
60     expected_debug: &str,
61     diff_mode: edit_distance::Mode,
62 ) -> Cow<'static, str> {
63     if actual_debug.lines().count() < 2 {
64         // If the actual debug is only one line, then there is no point in doing a
65         // line-by-line diff.
66         return "".into();
67     }
68     let mut actual_lines_reversed = actual_debug.lines().collect::<Vec<_>>();
69     let mut expected_lines_reversed = expected_debug.lines().collect::<Vec<_>>();
70     actual_lines_reversed.reverse();
71     expected_lines_reversed.reverse();
72     match edit_distance::edit_list(actual_lines_reversed, expected_lines_reversed, diff_mode) {
73         edit_distance::Difference::Equal => "No difference found between debug strings.".into(),
74         edit_distance::Difference::Editable(mut edit_list) => {
75             edit_list.reverse();
76             format!("\n{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
77                 .into()
78         }
79         edit_distance::Difference::Unrelated => "".into(),
80     }
81 }
82 
83 // Produces the header, with or without coloring depending on
84 // stdout_supports_color()
summary_header() -> Cow<'static, str>85 fn summary_header() -> Cow<'static, str> {
86     if stdout_supports_color() {
87         format!(
88             "Difference(-{ACTUAL_ONLY_STYLE}actual{RESET_ALL} / +{EXPECTED_ONLY_STYLE}expected{RESET_ALL}):"
89         ).into()
90     } else {
91         "Difference(-actual / +expected):".into()
92     }
93 }
94 
95 // Aggregator collecting the lines to be printed in the difference summary.
96 //
97 // This is buffered in order to allow a future line to potentially impact how
98 // the current line would be printed.
99 #[derive(Default)]
100 struct BufferedSummary<'a> {
101     summary: SummaryBuilder,
102     buffer: Buffer<'a>,
103 }
104 
105 impl<'a> BufferedSummary<'a> {
106     // Appends a new line which is common to both actual and expected.
feed_common_lines(&mut self, common_line: &'a str)107     fn feed_common_lines(&mut self, common_line: &'a str) {
108         if let Buffer::CommonLineBuffer(ref mut common_lines) = self.buffer {
109             common_lines.push(common_line);
110         } else {
111             self.flush_buffer();
112             self.buffer = Buffer::CommonLineBuffer(vec![common_line]);
113         }
114     }
115 
116     // Appends a new line which is found only in the actual string.
feed_extra_actual(&mut self, extra_actual: &'a str)117     fn feed_extra_actual(&mut self, extra_actual: &'a str) {
118         if let Buffer::ExtraExpectedLineChunk(extra_expected) = self.buffer {
119             self.print_inline_diffs(extra_actual, extra_expected);
120             self.buffer = Buffer::Empty;
121         } else {
122             self.flush_buffer();
123             self.buffer = Buffer::ExtraActualLineChunk(extra_actual);
124         }
125     }
126 
127     // Appends a new line which is found only in the expected string.
feed_extra_expected(&mut self, extra_expected: &'a str)128     fn feed_extra_expected(&mut self, extra_expected: &'a str) {
129         if let Buffer::ExtraActualLineChunk(extra_actual) = self.buffer {
130             self.print_inline_diffs(extra_actual, extra_expected);
131             self.buffer = Buffer::Empty;
132         } else {
133             self.flush_buffer();
134             self.buffer = Buffer::ExtraExpectedLineChunk(extra_expected);
135         }
136     }
137 
138     // Appends a comment for the additional line at the start or the end of the
139     // actual string which should be omitted.
feed_additional_actual(&mut self)140     fn feed_additional_actual(&mut self) {
141         self.flush_buffer();
142         self.summary.new_line();
143         self.summary.push_str_as_comment("<---- remaining lines omitted ---->");
144     }
145 
flush_buffer(&mut self)146     fn flush_buffer(&mut self) {
147         self.buffer.flush(&mut self.summary);
148     }
149 
print_inline_diffs(&mut self, actual_line: &str, expected_line: &str)150     fn print_inline_diffs(&mut self, actual_line: &str, expected_line: &str) {
151         let line_edits = edit_distance::edit_list(
152             actual_line.chars(),
153             expected_line.chars(),
154             edit_distance::Mode::Exact,
155         );
156 
157         if let edit_distance::Difference::Editable(edit_list) = line_edits {
158             let mut actual_summary = SummaryBuilder::default();
159             actual_summary.new_line_for_actual();
160             let mut expected_summary = SummaryBuilder::default();
161             expected_summary.new_line_for_expected();
162             for edit in &edit_list {
163                 match edit {
164                     edit_distance::Edit::ExtraActual(c) => actual_summary.push_actual_only(*c),
165                     edit_distance::Edit::ExtraExpected(c) => {
166                         expected_summary.push_expected_only(*c)
167                     }
168                     edit_distance::Edit::Both(c) => {
169                         actual_summary.push_actual_with_match(*c);
170                         expected_summary.push_expected_with_match(*c);
171                     }
172                     edit_distance::Edit::AdditionalActual => {
173                         // Calling edit_distance::edit_list(_, _, Mode::Exact) should never return
174                         // this enum
175                         panic!("This should not happen. This is a bug in gtest_rust")
176                     }
177                 }
178             }
179             actual_summary.reset_ansi();
180             expected_summary.reset_ansi();
181             self.summary.push_str(&actual_summary.summary);
182             self.summary.push_str(&expected_summary.summary);
183         } else {
184             self.summary.new_line_for_actual();
185             self.summary.push_str_actual_only(actual_line);
186             self.summary.new_line_for_expected();
187             self.summary.push_str_expected_only(expected_line);
188         }
189     }
190 }
191 
192 impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> {
from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self193     fn from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self {
194         let mut buffered_summary = BufferedSummary::default();
195         for edit in iter {
196             match edit {
197                 edit_distance::Edit::Both(same) => {
198                     buffered_summary.feed_common_lines(same);
199                 }
200                 edit_distance::Edit::ExtraActual(actual) => {
201                     buffered_summary.feed_extra_actual(actual);
202                 }
203                 edit_distance::Edit::ExtraExpected(expected) => {
204                     buffered_summary.feed_extra_expected(expected);
205                 }
206                 edit_distance::Edit::AdditionalActual => {
207                     buffered_summary.feed_additional_actual();
208                 }
209             };
210         }
211         buffered_summary.flush_buffer();
212         buffered_summary.summary.reset_ansi();
213 
214         buffered_summary
215     }
216 }
217 
218 impl<'a> Display for BufferedSummary<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result219     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220         if !matches!(self.buffer, Buffer::Empty) {
221             panic!("Buffer is not empty. This is a bug in gtest_rust.")
222         }
223         if !self.summary.last_ansi_style.is_empty() {
224             panic!("ANSI style has not been reset. This is a bug in gtest_rust.")
225         }
226         self.summary.summary.fmt(f)
227     }
228 }
229 
230 enum Buffer<'a> {
231     Empty,
232     CommonLineBuffer(Vec<&'a str>),
233     ExtraActualLineChunk(&'a str),
234     ExtraExpectedLineChunk(&'a str),
235 }
236 
237 impl<'a> Buffer<'a> {
flush(&mut self, summary: &mut SummaryBuilder)238     fn flush(&mut self, summary: &mut SummaryBuilder) {
239         match self {
240             Buffer::Empty => {}
241             Buffer::CommonLineBuffer(common_lines) => {
242                 Self::flush_common_lines(std::mem::take(common_lines), summary);
243             }
244             Buffer::ExtraActualLineChunk(extra_actual) => {
245                 summary.new_line_for_actual();
246                 summary.push_str_actual_only(extra_actual);
247             }
248             Buffer::ExtraExpectedLineChunk(extra_expected) => {
249                 summary.new_line_for_expected();
250                 summary.push_str_expected_only(extra_expected);
251             }
252         };
253         *self = Buffer::Empty;
254     }
255 
flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder)256     fn flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder) {
257         // The number of the lines kept before and after the compressed lines.
258         const COMMON_LINES_CONTEXT_SIZE: usize = 2;
259 
260         if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 {
261             for line in common_lines {
262                 summary.new_line();
263                 summary.push_str(line);
264             }
265             return;
266         }
267 
268         let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE];
269 
270         for line in start_context {
271             summary.new_line();
272             summary.push_str(line);
273         }
274 
275         summary.new_line();
276         summary.push_str_as_comment(&format!(
277             "<---- {} common lines omitted ---->",
278             common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE,
279         ));
280 
281         let end_context =
282             &common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()];
283 
284         for line in end_context {
285             summary.new_line();
286             summary.push_str(line);
287         }
288     }
289 }
290 
291 impl<'a> Default for Buffer<'a> {
default() -> Self292     fn default() -> Self {
293         Self::Empty
294     }
295 }
296 
297 #[rustversion::since(1.70)]
stdout_supports_color() -> bool298 fn stdout_supports_color() -> bool {
299     match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) {
300         (true, _) => false,
301         (false, true) => true,
302         (false, false) => std::io::stdout().is_terminal(),
303     }
304 }
305 
306 #[rustversion::not(since(1.70))]
stdout_supports_color() -> bool307 fn stdout_supports_color() -> bool {
308     is_env_var_set("FORCE_COLOR")
309 }
310 
is_env_var_set(var: &'static str) -> bool311 fn is_env_var_set(var: &'static str) -> bool {
312     std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false)
313 }
314 
315 // Font in italic
316 const COMMENT_STYLE: &str = "\x1B[3m";
317 // Font in green and bold
318 const EXPECTED_ONLY_STYLE: &str = "\x1B[1;32m";
319 // Font in red and bold
320 const ACTUAL_ONLY_STYLE: &str = "\x1B[1;31m";
321 // Font in green onlyh
322 const EXPECTED_WITH_MATCH_STYLE: &str = "\x1B[32m";
323 // Font in red only
324 const ACTUAL_WITH_MATCH_STYLE: &str = "\x1B[31m";
325 // Reset all ANSI formatting
326 const RESET_ALL: &str = "\x1B[0m";
327 
328 #[derive(Default)]
329 struct SummaryBuilder {
330     summary: String,
331     last_ansi_style: &'static str,
332 }
333 
334 impl SummaryBuilder {
push_str(&mut self, element: &str)335     fn push_str(&mut self, element: &str) {
336         self.reset_ansi();
337         self.summary.push_str(element);
338     }
339 
push_str_as_comment(&mut self, element: &str)340     fn push_str_as_comment(&mut self, element: &str) {
341         self.set_ansi(COMMENT_STYLE);
342         self.summary.push_str(element);
343     }
344 
push_str_actual_only(&mut self, element: &str)345     fn push_str_actual_only(&mut self, element: &str) {
346         self.set_ansi(ACTUAL_ONLY_STYLE);
347         self.summary.push_str(element);
348     }
349 
push_str_expected_only(&mut self, element: &str)350     fn push_str_expected_only(&mut self, element: &str) {
351         self.set_ansi(EXPECTED_ONLY_STYLE);
352         self.summary.push_str(element);
353     }
354 
push_actual_only(&mut self, element: char)355     fn push_actual_only(&mut self, element: char) {
356         self.set_ansi(ACTUAL_ONLY_STYLE);
357         self.summary.push(element);
358     }
359 
push_expected_only(&mut self, element: char)360     fn push_expected_only(&mut self, element: char) {
361         self.set_ansi(EXPECTED_ONLY_STYLE);
362         self.summary.push(element);
363     }
364 
push_actual_with_match(&mut self, element: char)365     fn push_actual_with_match(&mut self, element: char) {
366         self.set_ansi(ACTUAL_WITH_MATCH_STYLE);
367         self.summary.push(element);
368     }
369 
push_expected_with_match(&mut self, element: char)370     fn push_expected_with_match(&mut self, element: char) {
371         self.set_ansi(EXPECTED_WITH_MATCH_STYLE);
372         self.summary.push(element);
373     }
374 
new_line(&mut self)375     fn new_line(&mut self) {
376         self.reset_ansi();
377         self.summary.push_str("\n ");
378     }
379 
new_line_for_actual(&mut self)380     fn new_line_for_actual(&mut self) {
381         self.reset_ansi();
382         self.summary.push_str("\n-");
383     }
384 
new_line_for_expected(&mut self)385     fn new_line_for_expected(&mut self) {
386         self.reset_ansi();
387         self.summary.push_str("\n+");
388     }
389 
reset_ansi(&mut self)390     fn reset_ansi(&mut self) {
391         if !self.last_ansi_style.is_empty() && stdout_supports_color() {
392             self.summary.push_str(RESET_ALL);
393             self.last_ansi_style = "";
394         }
395     }
396 
set_ansi(&mut self, ansi_style: &'static str)397     fn set_ansi(&mut self, ansi_style: &'static str) {
398         if !stdout_supports_color() || self.last_ansi_style == ansi_style {
399             return;
400         }
401         if !self.last_ansi_style.is_empty() {
402             self.summary.push_str(RESET_ALL);
403         }
404         self.summary.push_str(ansi_style);
405         self.last_ansi_style = ansi_style;
406     }
407 }
408 
409 #[cfg(test)]
410 mod tests {
411     use super::*;
412     use crate::{matcher_support::edit_distance::Mode, prelude::*};
413     use indoc::indoc;
414     use serial_test::{parallel, serial};
415     use std::fmt::Write;
416 
417     // Make a long text with each element of the iterator on one line.
418     // `collection` must contains at least one element.
build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String419     fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String {
420         let mut text = String::new();
421         write!(&mut text, "{}", collection.next().expect("Provided collection without elements"))
422             .unwrap();
423         for item in collection {
424             write!(&mut text, "\n{}", item).unwrap();
425         }
426         text
427     }
428 
429     #[test]
430     #[parallel]
create_diff_smaller_than_one_line() -> Result<()>431     fn create_diff_smaller_than_one_line() -> Result<()> {
432         verify_that!(create_diff("One", "Two", Mode::Exact), eq(""))
433     }
434 
435     #[test]
436     #[parallel]
create_diff_exact_same() -> Result<()>437     fn create_diff_exact_same() -> Result<()> {
438         let expected = indoc! {"
439             One
440             Two
441             "};
442         let actual = indoc! {"
443         One
444         Two
445         "};
446         verify_that!(
447             create_diff(expected, actual, Mode::Exact),
448             eq("No difference found between debug strings.")
449         )
450     }
451 
452     #[test]
453     #[parallel]
create_diff_multiline_diff() -> Result<()>454     fn create_diff_multiline_diff() -> Result<()> {
455         let expected = indoc! {"
456             prefix
457             Actual#1
458             Actual#2
459             Actual#3
460             suffix"};
461         let actual = indoc! {"
462             prefix
463             Expected@one
464             Expected@two
465             suffix"};
466         // TODO: It would be better to have all the Actual together followed by all the
467         // Expected together.
468         verify_that!(
469             create_diff(expected, actual, Mode::Exact),
470             eq(indoc!(
471                 "
472 
473                 Difference(-actual / +expected):
474                  prefix
475                 -Actual#1
476                 +Expected@one
477                 -Actual#2
478                 +Expected@two
479                 -Actual#3
480                  suffix"
481             ))
482         )
483     }
484 
485     #[test]
486     #[parallel]
create_diff_exact_unrelated() -> Result<()>487     fn create_diff_exact_unrelated() -> Result<()> {
488         verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq(""))
489     }
490 
491     #[test]
492     #[parallel]
create_diff_exact_small_difference() -> Result<()>493     fn create_diff_exact_small_difference() -> Result<()> {
494         verify_that!(
495             create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
496             eq(indoc! {
497                 "
498 
499                 Difference(-actual / +expected):
500                  1
501                  2
502                  <---- 45 common lines omitted ---->
503                  48
504                  49
505                 +50"
506             })
507         )
508     }
509 
510     // Test with color enabled.
511 
512     struct ForceColor;
513 
force_color() -> ForceColor514     fn force_color() -> ForceColor {
515         std::env::set_var("FORCE_COLOR", "1");
516         std::env::remove_var("NO_COLOR");
517         ForceColor
518     }
519 
520     impl Drop for ForceColor {
drop(&mut self)521         fn drop(&mut self) {
522             std::env::remove_var("FORCE_COLOR");
523             std::env::set_var("NO_COLOR", "1");
524         }
525     }
526 
527     #[test]
528     #[serial]
create_diff_exact_small_difference_with_color() -> Result<()>529     fn create_diff_exact_small_difference_with_color() -> Result<()> {
530         let _keep = force_color();
531 
532         verify_that!(
533             create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
534             eq(indoc! {
535                 "
536 
537                 Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
538                  1
539                  2
540                  \x1B[3m<---- 45 common lines omitted ---->\x1B[0m
541                  48
542                  49
543                 +\x1B[1;32m50\x1B[0m"
544             })
545         )
546     }
547 
548     #[test]
549     #[serial]
create_diff_exact_difference_with_inline_color() -> Result<()>550     fn create_diff_exact_difference_with_inline_color() -> Result<()> {
551         let _keep = force_color();
552         let actual = indoc!(
553             "There is a home in Nouvelle Orleans
554             They say, it is the rising sons
555             And it has been the ruin of many a po'boy"
556         );
557 
558         let expected = indoc!(
559             "There is a house way down in New Orleans
560             They call the rising sun
561             And it has been the ruin of many a poor boy"
562         );
563 
564         verify_that!(
565             create_diff(actual, expected, Mode::Exact),
566             eq(indoc! {
567                 "
568 
569                 Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
570                 -\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
571                 +\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
572                 -\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
573                 +\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
574                 -\x1B[31mAnd it has been the ruin of many a po\x1B[0m\x1B[1;31m'\x1B[0m\x1B[31mboy\x1B[0m
575                 +\x1B[32mAnd it has been the ruin of many a po\x1B[0m\x1B[1;32mor \x1B[0m\x1B[32mboy\x1B[0m"
576             })
577         )
578     }
579 }
580