Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨"now configurable!"; customizable auto-labels and diff style #38

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 12 additions & 3 deletions Cargo.toml
Expand Up @@ -16,10 +16,19 @@ readme = "README.md"
[badges]
travis-ci = { repository = "colin-kiegel/rust-pretty-assertions" }

[features]
diffstyle_git = []
labels = []

[dependencies]
difference = "2.0.0"
ansi_term = "0.11.0"
ansi_term = "= 0.11.0"

[dev-dependencies.maybe-unwind]
version = "= 0.1.2"
# * avoid including unneeded "futures" feature to decrease build time
default-features = false

[target.'cfg(windows)'.dependencies]
output_vt100 = "0.1.2"
ctor = "0.1.9"
output_vt100 = "= 0.1.2"
ctor = "= 0.1.9"
17 changes: 17 additions & 0 deletions examples/pretty_assertion_custom.rs
@@ -0,0 +1,17 @@
#[allow(unused_imports)]
use pretty_assertions;

macro_rules! assert_eq { ($($arg:tt)+) => ({
pretty_assertions::with_config_assert_eq!(
pretty_assertions::Config {
auto_label: true,
style: pretty_assertions::Color::Yellow.normal(),
prefix: "<=>",
prefix_left: "<<=", prefix_right: "=>>",
..Default::default()
},
$($arg)+
)
})}

include!("standard_assertion.rs");
272 changes: 272 additions & 0 deletions src/comparison.rs
@@ -0,0 +1,272 @@
use difference::{Changeset, Difference};
use std::fmt::{self, Debug, Display};

use crate::config::Config;

#[doc(hidden)]
pub struct Comparison {
changeset: Changeset,
config: Config,
}

impl Comparison {
pub fn new<TLeft: Debug, TRight: Debug>(
config: Config,
left: &TLeft,
right: &TRight,
) -> Comparison {
let left_dbg = format!("{:#?}", *left);
let right_dbg = format!("{:#?}", *right);
let changeset = Changeset::new(&left_dbg, &right_dbg, "\n");

Comparison { changeset, config }
}
}

impl Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
format_comparison(f, &self.changeset, &self.config)
}
}

// Adapted from:
// https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs
// Credits johannhof (MIT License)

macro_rules! paint {
($f:ident, $colour:expr, $fmt:expr, $($args:tt)*) => (
write!($f, "{}", painted!($colour, format!($fmt, $($args)*)))
)
}

macro_rules! painted {
($colour:expr, $formatted:expr) => {
$colour.paint($formatted)
};
}

pub fn format_comparison(
f: &mut fmt::Formatter,
changeset: &Changeset,
config: &Config,
) -> fmt::Result {
let diffs = &changeset.diffs;

writeln!(
f,
"{} {} / {} :",
config.style.bold().paint("Diff"),
painted!(
config.style_left,
format!(
"{} {}",
config.prefix_left,
config
._maybe_label_left
.unwrap_or(config.default_label_left)
)
),
painted!(
config.style_right,
format!(
"{} {}",
config
._maybe_label_right
.unwrap_or(config.default_label_right),
config.prefix_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,
"{}{}",
painted!(config.style, config.prefix),
painted!(config.style, 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, &config)?;
}
_ => {
for line in added.split('\n') {
paint!(f, config.style_right, "{}{}\n", config.prefix_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, config.style_left, "{}{}\n", config.prefix_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,
config: &Config,
) -> fmt::Result {
let mut removed_lines = removed.split('\n');
let removed_line = removed_lines.next_back().unwrap();
let mut added_lines = added.split('\n');
let added_line = added_lines.next().unwrap();

let Changeset { diffs, .. } = Changeset::new(removed_line, added_line, "");

for line in removed_lines {
paint!(f, config.style_left, "{}{}\n", config.prefix_left, line)?;
}

// LEFT side (==what's been)
paint!(f, config.style_left, "{}", config.prefix_left)?;
for c in &diffs {
match *c {
Difference::Same(ref word_diff) => {
join!(chunk in (word_diff.split('\n')) {
paint!(f, config.style_left, "{}", chunk)?;
} separated by {
writeln!(f)?;
paint!(f, config.style_left, "{}", config.prefix_left)?;
});
}
Difference::Rem(ref word_diff) => {
join!(chunk in (word_diff.split('\n')) {
paint!(f, config.style_left_diff, "{}", chunk)?;
} separated by {
writeln!(f)?;
paint!(f, config.style_left, "{}", config.prefix_left)?;
});
}
_ => (),
}
}
writeln!(f, "{}", config.style.paint(""))?;

// RIGHT side (==what's new)
paint!(f, config.style_right, "{}", config.prefix_right)?;
for c in &diffs {
match *c {
Difference::Same(ref word_diff) => {
join!(chunk in (word_diff.split('\n')) {
paint!(f, config.style_right, "{}", chunk)?;
} separated by {
writeln!(f)?;
paint!(f, config.style_right, "{}", config.prefix_right)?;
});
}
Difference::Add(ref word_diff) => {
join!(chunk in (word_diff.split('\n')) {
paint!(f, config.style_right_diff, "{}", chunk)?;
} separated by {
writeln!(f)?;
paint!(f, config.style_right, "{}", config.prefix_right)?;
});
}
_ => (),
}
}
writeln!(f, "{}", painted!(config.style, ""))?;

for line in added_lines {
paint!(f, config.style_right, "{}{}\n", config.prefix_right, line)?;
}

Ok(())
}

#[test]
fn test_format_replacement() {
let added = " 84,\
\n 248,";
let removed = " 0,\
\n 0,\
\n 128,";

let expect_template = "\u{1b}[31m{{<}} 0,\n\u{1b}[0m\u{1b}[31m{{<}} 0,\n\u{1b}[0m\u{1b}[31m{{<}}\u{1b}[0m\u{1b}[31m \u{1b}[0m\u{1b}[1;48;5;52;31m12\u{1b}[0m\u{1b}[31m8\u{1b}[0m\u{1b}[31m,\u{1b}[0m\n\u{1b}[32m{{>}}\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[32m8\u{1b}[0m\u{1b}[1;48;5;22;32m4\u{1b}[0m\u{1b}[32m,\u{1b}[0m\n\u{1b}[32m{{>}} 248,\n\u{1b}[0m";

let mut expect = expect_template.to_string();

#[cfg(not(any(feature = "diffstyle_git")))]
{
expect = expect.replace("{{<}}", "<").replace("{{>}}", ">");
}
#[cfg(feature = "diffstyle_git")]
{
expect = expect.replace("{{<}}", "-").replace("{{>}}", "+");
}

let mut actual = String::new();
let config = Config::new();
let _ = format_replacement(&mut actual, added, removed, &config);

println!(
"## removed ##\
\n{}\
\n## added ##\
\n{}\
\n## diff ##\
\n{}",
removed, added, actual
);

println!("actual={}", actual);
println!("expect={}", expect);

crate::with_config_assert_eq!(
Config {
auto_label: true,
..Default::default()
},
actual,
expect
);
}
67 changes: 67 additions & 0 deletions src/config.rs
@@ -0,0 +1,67 @@
use crate::{Color, Style};

/// Configuration structure for pretty assertions
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Config {
pub auto_label: bool, // automatically label assertion arguments (when possible)

pub default_label_left: &'static str, // default label for "left"
pub default_label_right: &'static str, // default label for "right"

pub prefix: &'static str, // prefix for lines which don't differ between the assert arguments
pub prefix_left: &'static str, // prefix text for left/first (aka prior) argument differences
pub prefix_right: &'static str, // prefix text for right/second (aka after) argument differences

pub style: Style, // style for baseline assertion output
pub style_left: Style, // style for left/first (aka prior) argument (line-by-line) differences
pub style_right: Style, // style for right/second (aka after) argument (line-by-line) differences
pub style_left_diff: Style, // style for left/first (aka prior) argument intra-line differences
pub style_right_diff: Style, // style for right/second (aka after) argument intra-line differences

// "private"; but must be pub accessible for use in exported macros
#[doc(hidden)]
pub _maybe_label_left: Option<&'static str>, // left/first (aka prior) label, if available
#[doc(hidden)]
pub _maybe_label_right: Option<&'static str>, // right/second (aka after) label, if available
}

const PREFIX: &str = " ";

#[cfg(not(any(feature = "diffstyle_git")))]
const PREFIX_RIGHT: &str = ">"; // + > →
#[cfg(not(any(feature = "diffstyle_git")))]
const PREFIX_LEFT: &str = "<"; // - < ←

#[cfg(feature = "diffstyle_git")]
const PREFIX_RIGHT: &str = "+"; // + > →
#[cfg(feature = "diffstyle_git")]
const PREFIX_LEFT: &str = "-"; // - < ←

impl Default for Config {
fn default() -> Self {
Config {
auto_label: cfg!(feature = "labels"),
default_label_left: "left",
default_label_right: "right",
//
prefix: PREFIX,
prefix_left: PREFIX_LEFT,
prefix_right: PREFIX_RIGHT,
//
style: crate::Style::default(),
style_left: Color::Red.normal(), // (dark) red ("Maroon"), aka Color::Fixed(1)
style_right: Color::Green.normal(), // (dark) green ("Green"), aka Color::Fixed(2)
style_left_diff: Color::Red.on(Color::Fixed(52)).bold(), // bold bright red ("Red") on "DarkRed"
style_right_diff: Color::Green.on(Color::Fixed(22)).bold(), // bold bright green ("Lime") on "DarkGreen"
// "private"
_maybe_label_left: None,
_maybe_label_right: None,
}
}
}

impl Config {
pub fn new() -> Config {
Config::default()
}
}