Skip to content

Commit

Permalink
Merge pull request #232 from google:color-difference
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 544649686
  • Loading branch information
copybara-github committed Jun 30, 2023
2 parents 30cfca0 + 8ed42d0 commit 933c8bc
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 201 deletions.
2 changes: 2 additions & 0 deletions googletest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ googletest_macro = { path = "../googletest_macro", version = "0.8.1" }
anyhow = { version = "1", optional = true }
num-traits = "0.2.15"
regex = "1.6.0"
ansi_term = "0.12.0"

[dev-dependencies]
indoc = "2"
quickcheck = "1.0.3"
serial_test = "2.0.0"
1 change: 1 addition & 0 deletions googletest/src/matcher_support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
pub(crate) mod count_elements;
pub mod description;
pub(crate) mod edit_distance;
pub(crate) mod summarize_diff;
pub(crate) mod zipped_iterator;
303 changes: 303 additions & 0 deletions googletest/src/matcher_support/summarize_diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// 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 std::borrow::Cow;
use std::fmt::{Display, Write};

use ansi_term::{Color, Style};

use crate::matcher_support::edit_distance;

/// Environment variable controlling the usage of ansi color in difference
/// summary.
const NO_COLOR_VAR: &str = "GTEST_RUST_NO_COLOR";

/// 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(
expected_debug: &str,
actual_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 => "No difference found between debug strings.".into(),
edit_distance::Difference::Editable(edit_list) => {
format!("\nDifference:{}", edit_list_summary(&edit_list)).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(
expected_debug: &str,
actual_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::<Vec<_>>();
let mut expected_lines_reversed = expected_debug.lines().collect::<Vec<_>>();
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!("\nDifference:{}", edit_list_summary(&edit_list)).into()
}
edit_distance::Difference::Unrelated => "".into(),
}
}

fn edit_list_summary(edit_list: &[edit_distance::Edit<&str>]) -> String {
let mut summary = String::new();
// Use to collect common line and compress them.
let mut common_line_buffer = vec![];
for edit in edit_list {
let (style, line) = match edit {
edit_distance::Edit::Both(left) => {
common_line_buffer.push(*left);
continue;
}
edit_distance::Edit::ExtraLeft(left) => (LineStyle::extra_left_style(), *left),
edit_distance::Edit::ExtraRight(right) => (LineStyle::extra_right_style(), *right),
edit_distance::Edit::AdditionalLeft => {
(LineStyle::comment_style(), "<---- remaining lines omitted ---->")
}
};
summary.push_str(&compress_common_lines(std::mem::take(&mut common_line_buffer)));

write!(&mut summary, "\n{}", style.style(line)).unwrap();
}
summary.push_str(&compress_common_lines(common_line_buffer));

summary
}

// The number of the lines kept before and after the compressed lines.
const COMMON_LINES_CONTEXT_SIZE: usize = 2;

fn compress_common_lines(common_lines: Vec<&str>) -> String {
if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 {
let mut all_lines = String::new();
for line in common_lines {
write!(&mut all_lines, "\n{}", LineStyle::unchanged_style().style(line)).unwrap();
}
return all_lines;
}

let mut truncated_lines = String::new();

for line in &common_lines[0..COMMON_LINES_CONTEXT_SIZE] {
write!(&mut truncated_lines, "\n{}", LineStyle::unchanged_style().style(line)).unwrap();
}

write!(
&mut truncated_lines,
"\n{}",
LineStyle::comment_style().style(&format!(
"<---- {} common lines omitted ---->",
common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE
)),
)
.unwrap();

for line in &common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()] {
write!(&mut truncated_lines, "\n{}", LineStyle::unchanged_style().style(line)).unwrap();
}
truncated_lines
}

struct LineStyle {
ansi: Style,
header: char,
}

impl LineStyle {
fn extra_left_style() -> Self {
Self { ansi: Style::new().fg(Color::Red).bold(), header: '+' }
}

fn extra_right_style() -> Self {
Self { ansi: Style::new().fg(Color::Blue).bold(), header: '-' }
}

fn comment_style() -> Self {
Self { ansi: Style::new().italic(), header: ' ' }
}

fn unchanged_style() -> Self {
Self { ansi: Style::new(), header: ' ' }
}

fn style(self, line: &str) -> StyledLine<'_> {
StyledLine { style: self, line }
}
}

struct StyledLine<'a> {
style: LineStyle,
line: &'a str,
}

impl<'a> Display for StyledLine<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if std::env::var(NO_COLOR_VAR).is_err() {
write!(
f,
"{}{}{}{}",
self.style.header,
self.style.ansi.prefix(),
self.line,
self.style.ansi.suffix()
)
} else {
write!(f, "{}{}", self.style.header, self.line)
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{matcher_support::edit_distance::Mode, prelude::*};
use indoc::indoc;
use serial_test::serial;

#[must_use]
fn remove_var() -> TempVar {
let old_value = std::env::var(NO_COLOR_VAR);
std::env::remove_var(NO_COLOR_VAR);
TempVar(old_value.ok())
}

#[must_use]
fn set_var(var: &str) -> TempVar {
let old_value = std::env::var(NO_COLOR_VAR);
std::env::set_var(NO_COLOR_VAR, var);
TempVar(old_value.ok())
}
struct TempVar(Option<String>);

impl Drop for TempVar {
fn drop(&mut self) {
match &self.0 {
Some(old_var) => std::env::set_var(NO_COLOR_VAR, old_var),
None => std::env::remove_var(NO_COLOR_VAR),
}
}
}

// Make a long text with each element of the iterator on one line.
// `collection` must contains at least one element.
fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> 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_exact_unrelated() -> Result<()> {
verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq(""))
}

#[test]
#[serial]
fn create_diff_exact_small_difference() -> Result<()> {
let _cleanup = remove_var();

verify_that!(
create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
eq(indoc! {
"
Difference:
1
2
\x1B[3m<---- 45 common lines omitted ---->\x1B[0m
48
49
+\x1B[1;31m50\x1B[0m"
})
)
}
#[test]
#[serial]
fn create_diff_exact_small_difference_no_color() -> Result<()> {
let _cleanup = set_var("NO_COLOR");

verify_that!(
create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
eq(indoc! {
"
Difference:
1
2
<---- 45 common lines omitted ---->
48
49
+50"
})
)
}
}
4 changes: 2 additions & 2 deletions googletest/src/matchers/display_matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ mod tests {
which displays as a string which isn't equal to \"123\\n345\"
Difference:
123
+234
-345
+\x1B[1;31m234\x1B[0m
-\x1B[1;34m345\x1B[0m
"
))))
)
Expand Down
19 changes: 9 additions & 10 deletions googletest/src/matchers/eq_deref_of_matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

use crate::{
matcher::{Matcher, MatcherResult},
matcher_support::edit_distance,
matchers::eq_matcher::create_diff,
matcher_support::{edit_distance, summarize_diff::create_diff},
};
use std::{fmt::Debug, marker::PhantomData, ops::Deref};

Expand Down Expand Up @@ -136,17 +135,17 @@ mod tests {
verify_that!(
result,
err(displays_as(contains_substring(indoc! {
r#"
Actual: Strukt { int: 123, string: "something" },
which isn't equal to Strukt { int: 321, string: "someone" }
"
Actual: Strukt { int: 123, string: \"something\" },
which isn't equal to Strukt { int: 321, string: \"someone\" }
Difference:
Strukt {
+ int: 123,
- int: 321,
+ string: "something",
- string: "someone",
+\x1B[1;31m int: 123,\x1B[0m
-\x1B[1;34m int: 321,\x1B[0m
+\x1B[1;31m string: \"something\",\x1B[0m
-\x1B[1;34m string: \"someone\",\x1B[0m
}
"#})))
"})))
)
}
}
Loading

0 comments on commit 933c8bc

Please sign in to comment.