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