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