// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![doc(hidden)] use crate::matcher_support::edit_distance; #[rustversion::since(1.70)] use std::io::IsTerminal; use std::{borrow::Cow, cell::Cell, fmt::Display}; /// Returns a string describing how the expected and actual lines differ. /// /// This is included in a match explanation for [`EqMatcher`] and /// [`crate::matchers::str_matcher::StrMatcher`]. /// /// If the actual value has less than two lines, or the two differ by more than /// the maximum edit distance, then this returns the empty string. If the two /// are equal, it returns a simple statement that they are equal. Otherwise, /// this constructs a unified diff view of the actual and expected values. pub(crate) fn create_diff( actual_debug: &str, expected_debug: &str, diff_mode: edit_distance::Mode, ) -> Cow<'static, str> { if actual_debug.lines().count() < 2 { // If the actual debug is only one line, then there is no point in doing a // line-by-line diff. return "".into(); } match edit_distance::edit_list(actual_debug.lines(), expected_debug.lines(), diff_mode) { edit_distance::Difference::Equal => { // str.lines() is oblivious to the last newline in a // string, so we need to check this to make sure we don't spuriously // claim that 'hello' and 'hello\n' are identical debug strings. // // Although we would have liked to resolve by replacing // str::lines() with str::split('\n'), the potentially // empty last element interferes with good diff output for // "contains" checks. let actual_newline_terminated = actual_debug.ends_with('\n'); let expected_newline_terminated = expected_debug.ends_with('\n'); if actual_newline_terminated && !expected_newline_terminated { "Actual includes a terminating newline that is absent from expected.".into() } else if !actual_newline_terminated && expected_newline_terminated { "Actual omits a terminating newline that is present in expected.".into() } else { "No difference found between debug strings.".into() } } edit_distance::Difference::Editable(edit_list) => { format!("{}{}", summary_header(), edit_list.into_iter().collect::(),) .into() } edit_distance::Difference::Unrelated => "".into(), } } /// Returns a string describing how the expected and actual differ after /// reversing the lines in each. /// /// This is similar to [`create_diff`] except that it first reverses the lines /// in both the expected and actual values, then reverses the constructed edit /// list. When `diff_mode` is [`edit_distance::Mode::Prefix`], this becomes a /// diff of the suffix for use by [`ends_with`][crate::matchers::ends_with]. pub(crate) fn create_diff_reversed( actual_debug: &str, expected_debug: &str, diff_mode: edit_distance::Mode, ) -> Cow<'static, str> { if actual_debug.lines().count() < 2 { // If the actual debug is only one line, then there is no point in doing a // line-by-line diff. return "".into(); } let mut actual_lines_reversed = actual_debug.lines().collect::>(); let mut expected_lines_reversed = expected_debug.lines().collect::>(); actual_lines_reversed.reverse(); expected_lines_reversed.reverse(); match edit_distance::edit_list(actual_lines_reversed, expected_lines_reversed, diff_mode) { edit_distance::Difference::Equal => "No difference found between debug strings.".into(), edit_distance::Difference::Editable(mut edit_list) => { edit_list.reverse(); format!("{}{}", summary_header(), edit_list.into_iter().collect::(),) .into() } edit_distance::Difference::Unrelated => "".into(), } } // Produces the header, with or without coloring depending on // USE_COLOR fn summary_header() -> Cow<'static, str> { if USE_COLOR.with(Cell::get) { format!( "Difference(-{ACTUAL_ONLY_STYLE}actual{RESET_ALL} / +{EXPECTED_ONLY_STYLE}expected{RESET_ALL}):" ).into() } else { "Difference(-actual / +expected):".into() } } // Aggregator collecting the lines to be printed in the difference summary. // // This is buffered in order to allow a future line to potentially impact how // the current line would be printed. #[derive(Default)] struct BufferedSummary<'a> { summary: SummaryBuilder, buffer: Buffer<'a>, } impl<'a> BufferedSummary<'a> { // Appends a new line which is common to both actual and expected. fn feed_common_lines(&mut self, common_line: &'a str) { if let Buffer::CommonLines(ref mut common_lines) = self.buffer { common_lines.push(common_line); } else { self.flush_buffer(); self.buffer = Buffer::CommonLines(vec![common_line]); } } // Appends a new line which is found only in the actual string. fn feed_extra_actual(&mut self, extra_actual: &'a str) { if let Buffer::ExtraExpectedLineChunk(extra_expected) = self.buffer { self.print_inline_diffs(extra_actual, extra_expected); self.buffer = Buffer::Empty; } else { self.flush_buffer(); self.buffer = Buffer::ExtraActualLineChunk(extra_actual); } } // Appends a new line which is found only in the expected string. fn feed_extra_expected(&mut self, extra_expected: &'a str) { if let Buffer::ExtraActualLineChunk(extra_actual) = self.buffer { self.print_inline_diffs(extra_actual, extra_expected); self.buffer = Buffer::Empty; } else { self.flush_buffer(); self.buffer = Buffer::ExtraExpectedLineChunk(extra_expected); } } // Appends a comment for the additional line at the start or the end of the // actual string which should be omitted. fn feed_additional_actual(&mut self) { self.flush_buffer(); self.summary.new_line(); self.summary.push_str_as_comment("<---- remaining lines omitted ---->"); } fn flush_buffer(&mut self) { self.buffer.flush(&mut self.summary); } fn print_inline_diffs(&mut self, actual_line: &str, expected_line: &str) { let line_edits = edit_distance::edit_list( actual_line.chars(), expected_line.chars(), edit_distance::Mode::Exact, ); if let edit_distance::Difference::Editable(edit_list) = line_edits { let mut actual_summary = SummaryBuilder::default(); actual_summary.new_line_for_actual(); let mut expected_summary = SummaryBuilder::default(); expected_summary.new_line_for_expected(); for edit in &edit_list { match edit { edit_distance::Edit::ExtraActual(c) => actual_summary.push_actual_only(*c), edit_distance::Edit::ExtraExpected(c) => { expected_summary.push_expected_only(*c) } edit_distance::Edit::Both(c) => { actual_summary.push_actual_with_match(*c); expected_summary.push_expected_with_match(*c); } edit_distance::Edit::AdditionalActual => { // Calling edit_distance::edit_list(_, _, Mode::Exact) should never return // this enum panic!("This should not happen. This is a bug in gtest_rust") } } } actual_summary.reset_ansi(); expected_summary.reset_ansi(); self.summary.push_str(&actual_summary.summary); self.summary.push_str(&expected_summary.summary); } else { self.summary.new_line_for_actual(); self.summary.push_str_actual_only(actual_line); self.summary.new_line_for_expected(); self.summary.push_str_expected_only(expected_line); } } } impl<'a> FromIterator> for BufferedSummary<'a> { fn from_iter>>(iter: T) -> Self { let mut buffered_summary = BufferedSummary::default(); for edit in iter { match edit { edit_distance::Edit::Both(same) => { buffered_summary.feed_common_lines(same); } edit_distance::Edit::ExtraActual(actual) => { buffered_summary.feed_extra_actual(actual); } edit_distance::Edit::ExtraExpected(expected) => { buffered_summary.feed_extra_expected(expected); } edit_distance::Edit::AdditionalActual => { buffered_summary.feed_additional_actual(); } }; } buffered_summary.flush_buffer(); buffered_summary.summary.reset_ansi(); buffered_summary } } impl<'a> Display for BufferedSummary<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if !matches!(self.buffer, Buffer::Empty) { panic!("Buffer is not empty. This is a bug in gtest_rust.") } if !self.summary.last_ansi_style.is_empty() { panic!("ANSI style has not been reset. This is a bug in gtest_rust.") } self.summary.summary.fmt(f) } } enum Buffer<'a> { Empty, CommonLines(Vec<&'a str>), ExtraActualLineChunk(&'a str), ExtraExpectedLineChunk(&'a str), } impl<'a> Buffer<'a> { fn flush(&mut self, summary: &mut SummaryBuilder) { match self { Buffer::Empty => {} Buffer::CommonLines(common_lines) => { Self::flush_common_lines(std::mem::take(common_lines), summary); } Buffer::ExtraActualLineChunk(extra_actual) => { summary.new_line_for_actual(); summary.push_str_actual_only(extra_actual); } Buffer::ExtraExpectedLineChunk(extra_expected) => { summary.new_line_for_expected(); summary.push_str_expected_only(extra_expected); } }; *self = Buffer::Empty; } fn flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder) { // The number of the lines kept before and after the compressed lines. const COMMON_LINES_CONTEXT_SIZE: usize = 2; if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 { for line in common_lines { summary.new_line(); summary.push_str(line); } return; } let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE]; for line in start_context { summary.new_line(); summary.push_str(line); } summary.new_line(); summary.push_str_as_comment(&format!( "<---- {} common lines omitted ---->", common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE, )); let end_context = &common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()]; for line in end_context { summary.new_line(); summary.push_str(line); } } } impl<'a> Default for Buffer<'a> { fn default() -> Self { Self::Empty } } thread_local! { pub(crate) static USE_COLOR: Cell = Cell::new(stdout_supports_color()); } #[rustversion::since(1.70)] fn stdout_supports_color() -> bool { #[allow(clippy::incompatible_msrv)] match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) { (true, _) => false, (false, true) => true, (false, false) => std::io::stdout().is_terminal(), } } #[rustversion::not(since(1.70))] fn stdout_supports_color() -> bool { is_env_var_set("FORCE_COLOR") && !is_env_var_set("NO_COLOR") } fn is_env_var_set(var: &'static str) -> bool { std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false) } // Font in italic const COMMENT_STYLE: &str = "\x1B[3m"; // Font in green and bold const EXPECTED_ONLY_STYLE: &str = "\x1B[1;32m"; // Font in red and bold const ACTUAL_ONLY_STYLE: &str = "\x1B[1;31m"; // Font in green onlyh const EXPECTED_WITH_MATCH_STYLE: &str = "\x1B[32m"; // Font in red only const ACTUAL_WITH_MATCH_STYLE: &str = "\x1B[31m"; // Reset all ANSI formatting const RESET_ALL: &str = "\x1B[0m"; #[derive(Default)] struct SummaryBuilder { summary: String, last_ansi_style: &'static str, } impl SummaryBuilder { fn push_str(&mut self, element: &str) { self.reset_ansi(); self.summary.push_str(element); } fn push_str_as_comment(&mut self, element: &str) { self.set_ansi(COMMENT_STYLE); self.summary.push_str(element); } fn push_str_actual_only(&mut self, element: &str) { self.set_ansi(ACTUAL_ONLY_STYLE); self.summary.push_str(element); } fn push_str_expected_only(&mut self, element: &str) { self.set_ansi(EXPECTED_ONLY_STYLE); self.summary.push_str(element); } fn push_actual_only(&mut self, element: char) { self.set_ansi(ACTUAL_ONLY_STYLE); self.summary.push(element); } fn push_expected_only(&mut self, element: char) { self.set_ansi(EXPECTED_ONLY_STYLE); self.summary.push(element); } fn push_actual_with_match(&mut self, element: char) { self.set_ansi(ACTUAL_WITH_MATCH_STYLE); self.summary.push(element); } fn push_expected_with_match(&mut self, element: char) { self.set_ansi(EXPECTED_WITH_MATCH_STYLE); self.summary.push(element); } fn new_line(&mut self) { self.reset_ansi(); self.summary.push_str("\n "); } fn new_line_for_actual(&mut self) { self.reset_ansi(); self.summary.push_str("\n-"); } fn new_line_for_expected(&mut self) { self.reset_ansi(); self.summary.push_str("\n+"); } fn reset_ansi(&mut self) { if !self.last_ansi_style.is_empty() && USE_COLOR.with(Cell::get) { self.summary.push_str(RESET_ALL); self.last_ansi_style = ""; } } fn set_ansi(&mut self, ansi_style: &'static str) { if !USE_COLOR.with(Cell::get) || self.last_ansi_style == ansi_style { return; } if !self.last_ansi_style.is_empty() { self.summary.push_str(RESET_ALL); } self.summary.push_str(ansi_style); self.last_ansi_style = ansi_style; } } #[cfg(test)] mod tests { use super::*; use crate::{matcher_support::edit_distance::Mode, prelude::*}; use indoc::indoc; use std::fmt::Write; // Make a long text with each element of the iterator on one line. // `collection` must contains at least one element. fn build_text(mut collection: impl Iterator) -> String { let mut text = String::new(); write!(&mut text, "{}", collection.next().expect("Provided collection without elements")) .unwrap(); for item in collection { write!(&mut text, "\n{}", item).unwrap(); } text } #[test] fn create_diff_smaller_than_one_line() -> Result<()> { verify_that!(create_diff("One", "Two", Mode::Exact), eq("")) } #[test] fn create_diff_exact_same() -> Result<()> { let expected = indoc! {" One Two "}; let actual = indoc! {" One Two "}; verify_that!( create_diff(expected, actual, Mode::Exact), eq("No difference found between debug strings.") ) } #[test] fn create_diff_multiline_diff() -> Result<()> { let expected = indoc! {" prefix Actual#1 Actual#2 Actual#3 suffix"}; let actual = indoc! {" prefix Expected@one Expected@two suffix"}; // TODO: It would be better to have all the Actual together followed by all the // Expected together. verify_that!( create_diff(expected, actual, Mode::Exact), eq(indoc!( " Difference(-actual / +expected): prefix -Actual#1 +Expected@one -Actual#2 +Expected@two -Actual#3 suffix" )) ) } #[test] fn create_diff_exact_unrelated() -> Result<()> { verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq("")) } #[test] fn create_diff_exact_small_difference() -> Result<()> { verify_that!( create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), eq(indoc! { " Difference(-actual / +expected): 1 2 <---- 45 common lines omitted ----> 48 49 +50" }) ) } #[test] fn create_diff_exact_small_difference_with_color() -> Result<()> { USE_COLOR.with(|cell| cell.set(true)); verify_that!( create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact), eq(indoc! { " Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): 1 2 \x1B[3m<---- 45 common lines omitted ---->\x1B[0m 48 49 +\x1B[1;32m50\x1B[0m" }) ) } #[test] fn create_diff_exact_difference_with_inline_color() -> Result<()> { USE_COLOR.with(|cell| cell.set(true)); let actual = indoc!( "There is a home in Nouvelle Orleans They say, it is the rising sons And it has been the ruin of many a po'boy" ); let expected = indoc!( "There is a house way down in New Orleans They call the rising sun And it has been the ruin of many a poor boy" ); verify_that!( create_diff(actual, expected, Mode::Exact), eq(indoc! { " Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m): -\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 +\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 -\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 +\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 -\x1B[31mAnd it has been the ruin of many a po\x1B[0m\x1B[1;31m'\x1B[0m\x1B[31mboy\x1B[0m +\x1B[32mAnd it has been the ruin of many a po\x1B[0m\x1B[1;32mor \x1B[0m\x1B[32mboy\x1B[0m" }) ) } #[test] fn create_diff_line_termination_diff() -> Result<()> { verify_that!( create_diff("1\n2\n3", "1\n2\n3\n", Mode::Exact), eq("Actual omits a terminating newline that is present in expected.") )?; verify_that!( create_diff("1\n2\n3\n", "1\n2\n3", Mode::Exact), eq("Actual includes a terminating newline that is absent from expected.") )?; verify_that!( create_diff("1\n2\n3\n", "1\n2\n3\n", Mode::Exact), eq("No difference found between debug strings.") ) } }