From c26cc235c0b0c568afb850787d93bfe3b2a391f2 Mon Sep 17 00:00:00 2001 From: Tom Milligan Date: Tue, 2 Feb 2021 10:22:49 +0000 Subject: [PATCH] internal: replace difference with diff --- CHANGELOG.md | 5 + Cargo.toml | 2 +- src/format_changeset.rs | 176 ------------------------- src/lib.rs | 107 +++++++++++---- src/printer.rs | 284 ++++++++++++++++++++++++++++++++++++++++ tests/assert_eq.rs | 69 ++++++---- tests/assert_ne.rs | 4 +- tests/pretty_string.rs | 5 +- 8 files changed, 416 insertions(+), 236 deletions(-) delete mode 100644 src/format_changeset.rs create mode 100644 src/printer.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bb97405..df3f85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Unreleased +## Changed + +- Move from `difference` to `diff` for calculating diffs. The exact assertion messages generated may differ from previous versions. (#52, @tommilligan) + ## Added - Support for unsized values (#42, @stanislav-tkach) +- Document the `Comparison` struct, which was previously hidden. This can be used to generate a pretty diff of two values without panicking. (#52, @tommilligan) ## Fixed diff --git a/Cargo.toml b/Cargo.toml index c44283b..cfee1c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ readme = "README.md" travis-ci = { repository = "colin-kiegel/rust-pretty-assertions" } [dependencies] -difference = "2.0.0" ansi_term = "0.12.1" +diff = "0.1.12" [target.'cfg(windows)'.dependencies] output_vt100 = "0.1.2" diff --git a/src/format_changeset.rs b/src/format_changeset.rs deleted file mode 100644 index fb77428..0000000 --- a/src/format_changeset.rs +++ /dev/null @@ -1,176 +0,0 @@ -use ansi_term::Colour::{Fixed, Green, Red}; -use ansi_term::Style; -use difference::{Changeset, Difference}; -use std::fmt; - -macro_rules! paint { - ($f:ident, $colour:expr, $fmt:expr, $($args:tt)*) => ( - write!($f, "{}", $colour.paint(format!($fmt, $($args)*))) - ) -} - -const SIGN_RIGHT: char = '>'; // + > → -const SIGN_LEFT: char = '<'; // - < ← - -// Adapted from: -// https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs -// Credits johannhof (MIT License) - -pub fn format_changeset(f: &mut fmt::Formatter, changeset: &Changeset) -> fmt::Result { - let diffs = &changeset.diffs; - - writeln!( - f, - "{} {} / {} :", - Style::new().bold().paint("Diff"), - Red.paint(format!("{} left", SIGN_LEFT)), - Green.paint(format!("right {}", SIGN_RIGHT)) - )?; - for i in 0..diffs.len() { - match diffs[i] { - Difference::Same(ref same) => { - // Have to split line by line in order to have the extra whitespace - // at the beginning. - for line in same.split('\n') { - writeln!(f, " {}", line)?; - } - } - Difference::Add(ref added) => { - let prev = i.checked_sub(1).and_then(|x| diffs.get(x)); - match prev { - Some(&Difference::Rem(ref removed)) => { - // The addition is preceded by an removal. - // - // Let's highlight the character-differences in this replaced - // chunk. Note that this chunk can span over multiple lines. - format_replacement(f, added, removed)?; - } - _ => { - for line in added.split('\n') { - paint!(f, Green, "{}{}\n", SIGN_RIGHT, line)?; - } - } - }; - } - Difference::Rem(ref removed) => { - let next = i.checked_add(1).and_then(|x| diffs.get(x)); - match next { - Some(&Difference::Add(_)) => { - // The removal is followed by an addition. - // - // ... we'll handle both in the next iteration. - } - _ => { - for line in removed.split('\n') { - paint!(f, Red, "{}{}\n", SIGN_LEFT, line)?; - } - } - } - } - } - } - Ok(()) -} - -macro_rules! join { - ( - $elem:ident in ($iter:expr) { - $( $body:tt )* - } separated by { - $( $separator:tt )* - } - ) => ( - let mut iter = $iter; - - if let Some($elem) = iter.next() { - $( $body )* - } - - for $elem in iter { - $( $separator )* - $( $body )* - } - ) -} - -pub fn format_replacement(f: &mut dyn fmt::Write, added: &str, removed: &str) -> fmt::Result { - let Changeset { diffs, .. } = Changeset::new(removed, added, ""); - - // LEFT side (==what's been) - paint!(f, Red, "{}", SIGN_LEFT)?; - for c in &diffs { - match *c { - Difference::Same(ref word_diff) => { - join!(chunk in (word_diff.split('\n')) { - paint!(f, Red, "{}", chunk)?; - } separated by { - writeln!(f)?; - paint!(f, Red, "{}", SIGN_LEFT)?; - }); - } - Difference::Rem(ref word_diff) => { - join!(chunk in (word_diff.split('\n')) { - paint!(f, Red.on(Fixed(52)).bold(), "{}", chunk)?; - } separated by { - writeln!(f)?; - paint!(f, Red.bold(), "{}", SIGN_LEFT)?; - }); - } - _ => (), - } - } - writeln!(f)?; - - // RIGHT side (==what's new) - paint!(f, Green, "{}", SIGN_RIGHT)?; - for c in &diffs { - match *c { - Difference::Same(ref word_diff) => { - join!(chunk in (word_diff.split('\n')) { - paint!(f, Green, "{}", chunk)?; - } separated by { - writeln!(f)?; - paint!(f, Green, "{}", SIGN_RIGHT)?; - }); - } - Difference::Add(ref word_diff) => { - join!(chunk in (word_diff.split('\n')) { - paint!(f, Green.on(Fixed(22)).bold(), "{}", chunk)?; - } separated by { - writeln!(f)?; - paint!(f, Green.bold(), "{}", SIGN_RIGHT)?; - }); - } - _ => (), - } - } - - writeln!(f) -} - -#[test] -fn test_format_replacement() { - let added = " 84,\ - \n 248,"; - let removed = " 0,\ - \n 0,\ - \n 128,"; - - let mut buf = String::new(); - let _ = format_replacement(&mut buf, added, removed); - - println!( - "## removed ##\ - \n{}\ - \n## added ##\ - \n{}\ - \n## diff ##\ - \n{}", - removed, added, buf - ); - - assert_eq!( - buf, - "\u{1b}[31m<\u{1b}[0m\u{1b}[31m \u{1b}[0m\u{1b}[1;48;5;52;31m0\u{1b}[0m\u{1b}[31m,\u{1b}[0m\n\u{1b}[31m<\u{1b}[0m\u{1b}[31m \u{1b}[0m\u{1b}[1;48;5;52;31m0,\u{1b}[0m\n\u{1b}[1;31m<\u{1b}[0m\u{1b}[1;48;5;52;31m 1\u{1b}[0m\u{1b}[31m2\u{1b}[0m\u{1b}[31m8,\u{1b}[0m\n\u{1b}[32m>\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[1;48;5;22;32m84\u{1b}[0m\u{1b}[32m,\u{1b}[0m\n\u{1b}[32m>\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[32m2\u{1b}[0m\u{1b}[1;48;5;22;32m4\u{1b}[0m\u{1b}[32m8,\u{1b}[0m\n" - ); -} diff --git a/src/lib.rs b/src/lib.rs index 10c4ec2..b936b4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,21 +63,12 @@ //! * `assert_ne` is also switched to multi-line presentation, but does _not_ show //! a diff. -extern crate ansi_term; -extern crate difference; +#![deny(clippy::all, missing_docs, unsafe_code)] -#[cfg(windows)] -extern crate ctor; -#[cfg(windows)] -extern crate output_vt100; - -mod format_changeset; - -use difference::Changeset; +pub use ansi_term::Style; use std::fmt::{self, Debug, Display}; -use crate::format_changeset::format_changeset; -pub use ansi_term::Style; +mod printer; #[cfg(windows)] use ctor::*; @@ -87,28 +78,73 @@ fn init() { output_vt100::try_init().ok(); // Do not panic on fail } -#[doc(hidden)] -pub struct Comparison(Changeset); - -impl Comparison { - pub fn new( - left: &TLeft, - right: &TRight, - ) -> Comparison { - let left_dbg = format!("{:#?}", &*left); - let right_dbg = format!("{:#?}", &*right); - let changeset = Changeset::new(&left_dbg, &right_dbg, "\n"); +/// A comparison of two values. +/// +/// Where both values implement `Debug`, the comparison can be displayed as a pretty diff. +/// +/// ``` +/// use pretty_assertions::Comparison; +/// +/// print!("{}", Comparison::new(&123, &134)); +/// ``` +/// +/// The values may have different types, although in practice they are usually the same. +pub struct Comparison<'a, TLeft, TRight> +where + TLeft: ?Sized, + TRight: ?Sized, +{ + left: &'a TLeft, + right: &'a TRight, +} - Comparison(changeset) +impl<'a, TLeft, TRight> Comparison<'a, TLeft, TRight> +where + TLeft: ?Sized, + TRight: ?Sized, +{ + /// Store two values to be compared in future. + /// + /// Expensive diffing is deferred until calling `Debug::fmt`. + pub fn new(left: &'a TLeft, right: &'a TRight) -> Comparison<'a, TLeft, TRight> { + Comparison { left, right } } } -impl Display for Comparison { +impl<'a, TLeft, TRight> Display for Comparison<'a, TLeft, TRight> +where + TLeft: Debug + ?Sized, + TRight: Debug + ?Sized, +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - format_changeset(f, &self.0) + // To diff arbitary types, render them as debug strings + let left_debug = format!("{:#?}", self.left); + let right_debug = format!("{:#?}", self.right); + // And then diff the debug output + printer::write_header(f)?; + printer::write_lines(f, &left_debug, &right_debug) } } +/// Asserts that two expressions are equal to each other (using [`PartialEq`]). +/// +/// On panic, this macro will print a diff derived from [`Debug`] representation of +/// each value. +/// +/// This is a drop in replacement for [`std::assert_eq!`]. +/// You can provide a custom panic message if desired. +/// +/// # Examples +/// +/// ``` +/// use pretty_assertions::assert_eq; +/// +/// let a = 3; +/// let b = 1 + 2; +/// assert_eq!(a, b); +/// +/// assert_eq!(a, b, "we are testing addition with {} and {}", a, b); +/// ``` #[macro_export] macro_rules! assert_eq { ($left:expr , $right:expr,) => ({ @@ -143,6 +179,25 @@ macro_rules! assert_eq { }); } +/// Asserts that two expressions are not equal to each other (using [`PartialEq`]). +/// +/// On panic, this macro will print the values of the expressions with their +/// [`Debug`] representations. +/// +/// This is a drop in replacement for [`std::assert_ne!`]. +/// You can provide a custom panic message if desired. +/// +/// # Examples +/// +/// ``` +/// use pretty_assertions::assert_ne; +/// +/// let a = 3; +/// let b = 2; +/// assert_ne!(a, b); +/// +/// assert_ne!(a, b, "we are testing that the values are not equal"); +/// ``` #[macro_export] macro_rules! assert_ne { ($left:expr, $right:expr) => ({ diff --git a/src/printer.rs b/src/printer.rs new file mode 100644 index 0000000..57b53ca --- /dev/null +++ b/src/printer.rs @@ -0,0 +1,284 @@ +use ansi_term::{ + Colour::{Fixed, Green, Red}, + Style, +}; +use std::fmt; + +macro_rules! paint { + ($f:expr, $colour:expr, $fmt:expr, $($args:tt)*) => ( + write!($f, "{}", $colour.paint(format!($fmt, $($args)*))) + ) +} + +const SIGN_RIGHT: char = '>'; // + > → +const SIGN_LEFT: char = '<'; // - < ← + +/// Present the diff output for two mutliline strings in a pretty, colorised manner. +pub(crate) fn write_header(f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "{} {} / {} :", + Style::new().bold().paint("Diff"), + Red.paint(format!("{} left", SIGN_LEFT)), + Green.paint(format!("right {}", SIGN_RIGHT)) + ) +} + +/// Delay formatting this deleted chunk until later. +/// +/// It can be formatted as a whole chunk by calling `flush`, or the inner value +/// obtained with `take` for further processing. +#[derive(Default)] +struct LatentDeletion<'a> { + value: Option<&'a str>, +} + +impl<'a> LatentDeletion<'a> { + /// Set the chunk value. + fn set(&mut self, value: &'a str) { + self.value = Some(value); + } + + /// Take the underlying chunk value. + fn take(&mut self) -> Option<&'a str> { + self.value.take() + } + + /// If a value is set, print it as a whole chunk, using the given formatter. + /// + /// Resets the internal state to default. + fn flush(&mut self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(value) = self.value { + paint!(f, Red, "{}{}", SIGN_LEFT, value)?; + writeln!(f)?; + } + self.value = None; + Ok(()) + } +} + +// Adapted from: +// https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs +// Credits johannhof (MIT License) + +/// Present the diff output for two mutliline strings in a pretty, colorised manner. +pub(crate) fn write_lines(f: &mut fmt::Formatter, left: &str, right: &str) -> fmt::Result { + let diff = ::diff::lines(left, right); + + // Keep track of if the previous chunk in the iteration was a deletion. + // + // We defer writing all deletions to the subsequent loop, to find out if + // we need to write a character-level diff instead. + let mut previous_deletion = LatentDeletion::default(); + + for change in diff.into_iter() { + match change { + ::diff::Result::Both(value, _) => { + // Handle the previous deletion, if it exists + previous_deletion.flush(f)?; + + // Print this line with a space at the front to preserve indentation. + writeln!(f, " {}", value)?; + } + ::diff::Result::Right(inserted) => { + if let Some(deleted) = previous_deletion.take() { + // The insertion is preceded by an deletion. + // + // Let's highlight the character-differences in this replaced + // chunk. Note that this chunk can span over multiple lines. + write_inline_diff(f, deleted, inserted)?; + } else { + paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?; + writeln!(f)?; + } + } + ::diff::Result::Left(deleted) => { + // Handle the previous deletion, if it exists + previous_deletion.flush(f)?; + + // If we get a deletion, defer writing it to the next loop + // as we need to know what the next tag is. + previous_deletion.set(deleted); + } + } + } + + // Handle the previous deletion, if it exists + previous_deletion.flush(f)?; + + Ok(()) +} + +/// Group character styling for an inline diff, to prevent wrapping each single +/// character in terminal styling codes. +/// +/// Styles are applied automatically each time a new style is given in `write_with_style`. +struct InlineWriter<'a, Writer> { + f: &'a mut Writer, + style: Style, +} + +impl<'a, Writer> InlineWriter<'a, Writer> +where + Writer: fmt::Write, +{ + fn new(f: &'a mut Writer) -> Self { + InlineWriter { + f, + style: Style::new(), + } + } + + /// Push a new character into the buffer, specifying the style it should be written in. + fn write_with_style(&mut self, c: &char, style: Style) -> fmt::Result { + // If the style is the same as previously, just write character + if style == self.style { + write!(self.f, "{}", c)?; + } else { + // Close out previous style + write!(self.f, "{}", self.style.suffix())?; + + // Store new style and start writing it + write!(self.f, "{}{}", style.prefix(), c)?; + self.style = style; + } + Ok(()) + } + + /// Finish any existing style and reset to default state. + fn finish(&mut self) -> fmt::Result { + // Close out previous style + writeln!(self.f, "{}", self.style.suffix())?; + self.style = Default::default(); + Ok(()) + } +} + +/// Format a single line to show an inline diff of the two strings given. +/// +/// The given strings should not have a trailing newline. +/// +/// The output of this function will be two lines, each with a trailing newline. +fn write_inline_diff(f: &mut TWrite, left: &str, right: &str) -> fmt::Result { + let diff = ::diff::chars(left, right); + let mut writer = InlineWriter::new(f); + + // Print the left string on one line, with differences highlighted + let light = Red.into(); + let heavy = Red.on(Fixed(52)).bold(); + writer.write_with_style(&SIGN_LEFT, light)?; + for change in diff.iter() { + match change { + ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?, + ::diff::Result::Left(value) => writer.write_with_style(value, heavy)?, + _ => (), + } + } + writer.finish()?; + + // Print the right string on one line, with differences highlighted + let light = Green.into(); + let heavy = Green.on(Fixed(22)).bold(); + writer.write_with_style(&SIGN_RIGHT, light)?; + for change in diff.iter() { + match change { + ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?, + ::diff::Result::Right(value) => writer.write_with_style(value, heavy)?, + _ => (), + } + } + writer.finish() +} + +#[cfg(test)] +mod test { + use super::*; + + const RED_LIGHT: &str = "\u{1b}[31m"; + const GREEN_LIGHT: &str = "\u{1b}[32m"; + const RED_HEAVY: &str = "\u{1b}[1;48;5;52;31m"; + const GREEN_HEAVY: &str = "\u{1b}[1;48;5;22;32m"; + const RESET: &str = "\u{1b}[0m"; + + fn check_inline_diff(left: &str, right: &str, expected: &str) { + let mut actual = String::new(); + write_inline_diff(&mut actual, left, right).unwrap(); + + println!( + "## left ##\n\ + {}\n\ + ## right ##\n\ + {}\n\ + ## actual diff ##\n\ + {}\n\ + ## expected diff ##\n\ + {}", + left, right, actual, expected + ); + assert_eq!(actual, expected); + } + + #[test] + fn write_inline_diff_newline_only() { + let left = ""; + let right = ""; + let expected = format!( + "{red_light}<{reset}\n\ + {green_light}>{reset}\n", + red_light = RED_LIGHT, + green_light = GREEN_LIGHT, + reset = RESET, + ); + + check_inline_diff(left, right, &expected); + } + + #[test] + fn write_inline_diff_added() { + let left = ""; + let right = "polymerase"; + let expected = format!( + "{red_light}<{reset}\n\ + {green_light}>{reset}{green_heavy}polymerase{reset}\n", + red_light = RED_LIGHT, + green_light = GREEN_LIGHT, + green_heavy = GREEN_HEAVY, + reset = RESET, + ); + + check_inline_diff(left, right, &expected); + } + + #[test] + fn write_inline_diff_removed() { + let left = "polyacrylamide"; + let right = ""; + let expected = format!( + "{red_light}<{reset}{red_heavy}polyacrylamide{reset}\n\ + {green_light}>{reset}\n", + red_light = RED_LIGHT, + green_light = GREEN_LIGHT, + red_heavy = RED_HEAVY, + reset = RESET, + ); + + check_inline_diff(left, right, &expected); + } + + #[test] + fn write_inline_diff_changed() { + let left = "polymerase"; + let right = "polyacrylamide"; + let expected = format!( + "{red_light}poly{reset}{green_heavy}ac{reset}{green_light}r{reset}{green_heavy}yl{reset}{green_light}a{reset}{green_heavy}mid{reset}{green_light}e{reset}\n", + red_light = RED_LIGHT, + green_light = GREEN_LIGHT, + red_heavy = RED_HEAVY, + green_heavy = GREEN_HEAVY, + reset = RESET, + ); + + check_inline_diff(left, right, &expected); + } +} diff --git a/tests/assert_eq.rs b/tests/assert_eq.rs index 087e5c6..c2bcee5 100644 --- a/tests/assert_eq.rs +++ b/tests/assert_eq.rs @@ -6,12 +6,12 @@ Diff < left / right > : Some( Foo { -< lorem: "Hello World!", -> lorem: "Hello Wrold!", +< lorem: "Hello World!", +> lorem: "Hello Wrold!", ipsum: 42, dolor: Ok( -< "hey", -> "hey ho!", +< "hey", +> "hey ho!", ), }, ) @@ -46,12 +46,12 @@ fn assert_eq() { Diff < left / right > : Some( Foo { -< lorem: "Hello World!", -> lorem: "Hello Wrold!", +< lorem: "Hello World!", +> lorem: "Hello Wrold!", ipsum: 42, dolor: Ok( -< "hey", -> "hey ho!", +< "hey", +> "hey ho!", ), }, ) @@ -90,18 +90,31 @@ fn assert_eq_with_comparable_types() { #[test] #[should_panic(expected = r#"assertion failed: `(left == right)` +Diff < left / right > : +<9 +>8 +"#)] +fn inline_diff_simple() { + let left = 9; + let right = 8; + pretty_assertions::assert_eq!(left, right); +} + +#[test] +#[should_panic(expected = r#"assertion failed: `(left == right)` + Diff < left / right > : [ -< 0, -< 0, -< 0, -< 128, -< 10, -< 191, -< 5, -> 84, -> 248, -> 45, +< 0, +< 0, +< 0, +< 128, +< 10, +< 191, +< 5, +> 84, +> 248, +> 45, 64, ] @@ -118,12 +131,12 @@ fn issue12() { Diff < left / right > : Some( Foo { -< lorem: "Hello World!", -> lorem: "Hello Wrold!", +< lorem: "Hello World!", +> lorem: "Hello Wrold!", ipsum: 42, dolor: Ok( -< "hey", -> "hey ho!", +< "hey", +> "hey ho!", ), }, ) @@ -158,12 +171,12 @@ fn assert_eq_trailing_comma() { Diff < left / right > : Some( Foo { -< lorem: "Hello World!", -> lorem: "Hello Wrold!", +< lorem: "Hello World!", +> lorem: "Hello Wrold!", ipsum: 42, dolor: Ok( -< "hey", -> "hey ho!", +< "hey", +> "hey ho!", ), }, ) @@ -204,8 +217,8 @@ fn assert_eq_unsized() { Diff < left / right > : [ 101, -> 101, - ] +> 101, + ] "#)] fn assert_eq_unsized_panic() { diff --git a/tests/assert_ne.rs b/tests/assert_ne.rs index 6166032..dc58131 100644 --- a/tests/assert_ne.rs +++ b/tests/assert_ne.rs @@ -72,7 +72,7 @@ fn assert_ne_non_empty_return() { fn not_zero(x: u32) -> u32 { pretty_assertions::assert_ne!(x, 0); x - }; + } not_zero(0); } @@ -81,7 +81,7 @@ fn assert_ne_non_empty_return() { Diff < left / right > : <-0.0 ->0.0 +>0.0 Note: According to the `PartialEq` implementation, both of the values are partially equivalent, even if the `Debug` outputs differ. diff --git a/tests/pretty_string.rs b/tests/pretty_string.rs index 5ab7607..4b91252 100644 --- a/tests/pretty_string.rs +++ b/tests/pretty_string.rs @@ -4,7 +4,6 @@ use std::fmt; /// Used in different `assert*!` macros in combination with `pretty_assertions` crate to make /// test failures to show nice diffs. #[derive(PartialEq, Eq)] -#[doc(hidden)] pub struct PrettyString<'a>(pub &'a str); /// Make diff to display string as multi-line string @@ -18,8 +17,8 @@ impl<'a> fmt::Debug for PrettyString<'a> { #[should_panic(expected = r#"assertion failed: `(left == right)` Diff < left / right > : ->foo - +>foo + "#)] fn assert_eq_empty_first() { pretty_assertions::assert_eq!(PrettyString(""), PrettyString("foo"));