#[cfg(feature = "colored")]
#[cfg_attr(docsrs, doc(cfg(feature = "colored")))]
pub use with_colored_feature::{
diff_format_for_mode, DIFF_FORMAT_BOLD, DIFF_FORMAT_RED_BLUE, DIFF_FORMAT_RED_GREEN,
DIFF_FORMAT_RED_YELLOW,
};
use crate::spec::{DiffFormat, Highlight};
use crate::std::fmt::Debug;
use crate::std::format;
use crate::std::string::{String, ToString};
use crate::std::vec::Vec;
use hashbrown::HashSet;
#[cfg(feature = "colored")]
use with_colored_feature::{
configured_diff_format_impl, mark_diff_impl, mark_missing_char_impl, mark_missing_impl,
mark_missing_string_impl, mark_unexpected_char_impl, mark_unexpected_impl,
mark_unexpected_string_impl,
};
#[cfg(not(feature = "colored"))]
use without_colored_feature::{
configured_diff_format_impl, mark_diff_impl, mark_missing_char_impl, mark_missing_impl,
mark_missing_string_impl, mark_unexpected_char_impl, mark_unexpected_impl,
mark_unexpected_string_impl,
};
const NO_HIGHLIGHT: Highlight = Highlight { start: "", end: "" };
pub const DIFF_FORMAT_NO_HIGHLIGHT: DiffFormat = DiffFormat {
unexpected: NO_HIGHLIGHT,
missing: NO_HIGHLIGHT,
};
pub const DEFAULT_DIFF_FORMAT: DiffFormat = {
#[cfg(not(feature = "colored"))]
{
without_colored_feature::DEFAULT_DIFF_FORMAT
}
#[cfg(feature = "colored")]
{
with_colored_feature::DEFAULT_DIFF_FORMAT
}
};
#[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn configured_diff_format() -> DiffFormat {
configured_diff_format_impl()
}
pub fn mark_diff<S, E>(actual: &S, expected: &E, format: &DiffFormat) -> (String, String)
where
S: Debug + ?Sized,
E: Debug + ?Sized,
{
let actual = format!("{actual:?}");
let expected = format!("{expected:?}");
mark_diff_impl(&actual, &expected, format)
}
pub fn mark_diff_str(actual: &str, expected: &str, format: &DiffFormat) -> (String, String) {
mark_diff_impl(actual, expected, format)
}
pub fn mark_unexpected<T>(value: &T, format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
mark_unexpected_impl(value, format)
}
pub fn mark_missing<T>(value: &T, format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
mark_missing_impl(value, format)
}
pub fn mark_unexpected_string(string: &str, format: &DiffFormat) -> String {
mark_unexpected_string_impl(string, format)
}
pub fn mark_missing_string(string: &str, format: &DiffFormat) -> String {
mark_missing_string_impl(string, format)
}
pub fn mark_unexpected_char(character: char, format: &DiffFormat) -> String {
mark_unexpected_char_impl(character, format)
}
pub fn mark_missing_char(character: char, format: &DiffFormat) -> String {
mark_missing_char_impl(character, format)
}
pub fn mark_unexpected_substring_in_string(
string: &str,
substring: &str,
format: &DiffFormat,
) -> String {
mark_substring_in_string(string, substring, format, mark_unexpected_string)
}
pub fn mark_missing_substring_in_string(
string: &str,
substring: &str,
format: &DiffFormat,
) -> String {
mark_substring_in_string(string, substring, format, mark_missing_string)
}
fn mark_substring_in_string<F>(
string: &str,
substring: &str,
format: &DiffFormat,
mark: F,
) -> String
where
F: Fn(&str, &DiffFormat) -> String,
{
if let Some(position) = string.find(substring) {
let length = substring.len();
let begin = &string[..position];
let end = &string[position + length..];
let marked_substr = mark(substring, format);
format!("{begin}{marked_substr}{end}")
} else {
string.to_string()
}
}
pub fn mark_unexpected_char_in_string(
string: &str,
character: char,
format: &DiffFormat,
) -> String {
mark_char_in_string(string, character, format, mark_unexpected_string)
}
pub fn mark_missing_char_in_string(string: &str, character: char, format: &DiffFormat) -> String {
mark_char_in_string(string, character, format, mark_missing_string)
}
fn mark_char_in_string<F>(string: &str, character: char, format: &DiffFormat, mark: F) -> String
where
F: Fn(&str, &DiffFormat) -> String,
{
let mut marked_string = String::with_capacity(string.len());
let mut parts = string.split(character);
let mut chars_to_mark = String::new();
parts
.next()
.iter()
.for_each(|part| marked_string.push_str(part));
for part in parts {
chars_to_mark.push(character);
if !part.is_empty() {
marked_string.push_str(&mark(&chars_to_mark, format));
chars_to_mark.clear();
marked_string.push_str(part);
}
}
marked_string
}
pub fn mark_selected_chars_in_string_as_unexpected(
string: &str,
selected: &HashSet<usize>,
format: &DiffFormat,
) -> String {
mark_selected_chars_in_string(string, selected, &format.unexpected)
}
pub fn mark_selected_chars_in_string_as_missing(
string: &str,
selected: &HashSet<usize>,
format: &DiffFormat,
) -> String {
mark_selected_chars_in_string(string, selected, &format.missing)
}
fn mark_selected_chars_in_string(
string: &str,
selected: &HashSet<usize>,
highlight: &Highlight,
) -> String {
let mut marked_string = String::with_capacity(string.len());
let mut to_mark = selected.iter().copied().collect::<Vec<_>>();
to_mark.sort_unstable();
let mut to_mark = to_mark.into_iter();
let mut last_sel_idx = to_mark.next().unwrap_or(usize::MAX);
let mut start_idx = last_sel_idx;
let mut end_idx = last_sel_idx;
for sel_idx in to_mark.by_ref() {
let last_plus_one = last_sel_idx + 1;
last_sel_idx = sel_idx;
if sel_idx == last_plus_one {
end_idx = sel_idx;
} else {
break;
}
}
for (chr_idx, chr) in string.chars().enumerate() {
if chr_idx == start_idx {
marked_string.push_str(highlight.start);
if last_sel_idx == end_idx {
start_idx = usize::MAX;
} else {
start_idx = last_sel_idx;
}
}
marked_string.push(chr);
if chr_idx == end_idx {
marked_string.push_str(highlight.end);
end_idx = last_sel_idx;
for sel_idx in to_mark.by_ref() {
let last_plus_one = last_sel_idx + 1;
last_sel_idx = sel_idx;
if sel_idx == last_plus_one {
end_idx = sel_idx;
} else {
break;
}
}
}
}
marked_string
}
pub fn mark_selected_items_in_collection<T, F>(
collection: &[T],
selected_indices: &HashSet<usize>,
format: &DiffFormat,
mark: F,
) -> String
where
T: Debug,
F: Fn(&T, &DiffFormat) -> String,
{
let mut marked_collection = String::with_capacity(collection.len() + 2);
marked_collection.push('[');
collection
.iter()
.enumerate()
.map(|(index, item)| {
if selected_indices.contains(&index) {
mark(item, format)
} else {
format!("{item:?}")
}
})
.for_each(|item| {
marked_collection.push_str(&item);
marked_collection.push_str(", ");
});
if marked_collection.len() >= 3 {
marked_collection.pop();
marked_collection.pop();
}
marked_collection.push(']');
marked_collection
}
pub fn mark_all_items_in_collection<T, F>(collection: &[T], format: &DiffFormat, mark: F) -> String
where
T: Debug,
F: Fn(&T, &DiffFormat) -> String,
{
let mut marked_collection = String::with_capacity(collection.len() + 2);
marked_collection.push('[');
collection
.iter()
.map(|item| mark(item, format))
.for_each(|item| {
marked_collection.push_str(&item);
marked_collection.push_str(", ");
});
if marked_collection.len() >= 3 {
marked_collection.pop();
marked_collection.pop();
}
marked_collection.push(']');
marked_collection
}
pub fn mark_selected_entries_in_map<K, V, F>(
map_entries: &[(K, V)],
selected_indices: &HashSet<usize>,
format: &DiffFormat,
mark: F,
) -> String
where
K: Debug,
V: Debug,
F: Fn(&str, &DiffFormat) -> String,
{
let mut marked_map_entries = String::with_capacity(map_entries.len() + 2);
marked_map_entries.push('{');
map_entries
.iter()
.enumerate()
.map(|(index, entry)| {
let key_value_pair = format!("{:?}: {:?}", entry.0, entry.1);
if selected_indices.contains(&index) {
mark(&key_value_pair, format)
} else {
key_value_pair
}
})
.for_each(|entry| {
marked_map_entries.push_str(&entry);
marked_map_entries.push_str(", ");
});
if marked_map_entries.len() >= 3 {
marked_map_entries.pop();
marked_map_entries.pop();
}
marked_map_entries.push('}');
marked_map_entries
}
pub fn mark_all_entries_in_map<K, V, F>(
map_entries: &[(K, V)],
format: &DiffFormat,
mark: F,
) -> String
where
K: Debug,
V: Debug,
F: Fn(&str, &DiffFormat) -> String,
{
let mut marked_map_entries = String::with_capacity(map_entries.len() + 2);
marked_map_entries.push('{');
map_entries
.iter()
.map(|entry| {
let key_value_pair = format!("{:?}: {:?}", entry.0, entry.1);
mark(&key_value_pair, format)
})
.for_each(|entry| {
marked_map_entries.push_str(&entry);
marked_map_entries.push_str(", ");
});
if marked_map_entries.len() >= 3 {
marked_map_entries.pop();
marked_map_entries.pop();
}
marked_map_entries.push('}');
marked_map_entries
}
#[cfg(not(feature = "colored"))]
mod without_colored_feature {
use super::DIFF_FORMAT_NO_HIGHLIGHT;
use crate::spec::DiffFormat;
use crate::std::{
fmt::Debug,
format,
string::{String, ToString},
};
pub const DEFAULT_DIFF_FORMAT: DiffFormat = DIFF_FORMAT_NO_HIGHLIGHT;
#[must_use]
#[inline]
pub const fn configured_diff_format_impl() -> DiffFormat {
DEFAULT_DIFF_FORMAT
}
#[inline]
pub fn mark_diff_impl(actual: &str, expected: &str, _format: &DiffFormat) -> (String, String) {
(actual.to_string(), expected.to_string())
}
#[inline]
pub fn mark_unexpected_impl<T>(value: &T, _format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
format!("{value:?}")
}
#[inline]
pub fn mark_missing_impl<T>(value: &T, _format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
format!("{value:?}")
}
#[inline]
pub fn mark_unexpected_string_impl(string: &str, _format: &DiffFormat) -> String {
string.to_string()
}
#[inline]
pub fn mark_missing_string_impl(string: &str, _format: &DiffFormat) -> String {
string.to_string()
}
#[inline]
pub fn mark_unexpected_char_impl(character: char, _format: &DiffFormat) -> String {
format!("{character}")
}
#[inline]
pub fn mark_missing_char_impl(character: char, _format: &DiffFormat) -> String {
format!("{character}")
}
}
#[cfg(feature = "colored")]
mod with_colored_feature {
use super::DIFF_FORMAT_NO_HIGHLIGHT;
use crate::spec::{DiffFormat, Highlight};
use crate::std::{fmt::Debug, format, string::String};
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
const ENV_VAR_NO_COLOR: &str = "NO_COLOR";
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub const ENV_VAR_HIGHLIGHT_DIFFS: &str = "ASSERTING_HIGHLIGHT_DIFFS";
const HIGHLIGHT_MODE_RED_BLUE: &str = "red-blue";
const HIGHLIGHT_MODE_RED_GREEN: &str = "red-green";
const HIGHLIGHT_MODE_RED_YELLOW: &str = "red-yellow";
const HIGHLIGHT_MODE_BOLD: &str = "bold";
const HIGHLIGHT_MODE_OFF: &str = "off";
const TERM_FONT_BOLD: &str = "\u{1b}[1m";
const TERM_COLOR_RED: &str = "\u{1b}[31m";
const TERM_COLOR_GREEN: &str = "\u{1b}[32m";
const TERM_COLOR_BLUE: &str = "\u{1b}[34m";
const TERM_COLOR_YELLOW: &str = "\u{1b}[33m";
const TERM_RESET: &str = "\u{1b}[0m";
#[cfg(feature = "std")]
const DEFAULT_HIGHLIGHT_MODE: &str = HIGHLIGHT_MODE_RED_GREEN;
pub const DEFAULT_DIFF_FORMAT: DiffFormat = DIFF_FORMAT_RED_GREEN;
const TERM_HIGHLIGHT_BOLD: Highlight = Highlight {
start: TERM_FONT_BOLD,
end: TERM_RESET,
};
const TERM_HIGHLIGHT_RED: Highlight = Highlight {
start: TERM_COLOR_RED,
end: TERM_RESET,
};
const TERM_HIGHLIGHT_GREEN: Highlight = Highlight {
start: TERM_COLOR_GREEN,
end: TERM_RESET,
};
const TERM_HIGHLIGHT_BLUE: Highlight = Highlight {
start: TERM_COLOR_BLUE,
end: TERM_RESET,
};
const TERM_HIGHLIGHT_YELLOW: Highlight = Highlight {
start: TERM_COLOR_YELLOW,
end: TERM_RESET,
};
const TERM_NO_HIGHLIGHT: Highlight = Highlight { start: "", end: "" };
pub const DIFF_FORMAT_RED_BLUE: DiffFormat = DiffFormat {
unexpected: TERM_HIGHLIGHT_RED,
missing: TERM_HIGHLIGHT_BLUE,
};
pub const DIFF_FORMAT_RED_GREEN: DiffFormat = DiffFormat {
unexpected: TERM_HIGHLIGHT_RED,
missing: TERM_HIGHLIGHT_GREEN,
};
pub const DIFF_FORMAT_RED_YELLOW: DiffFormat = DiffFormat {
unexpected: TERM_HIGHLIGHT_RED,
missing: TERM_HIGHLIGHT_YELLOW,
};
pub const DIFF_FORMAT_BOLD: DiffFormat = DiffFormat {
unexpected: TERM_HIGHLIGHT_BOLD,
missing: TERM_NO_HIGHLIGHT,
};
#[must_use]
pub fn diff_format_for_mode(mode: &str) -> Option<DiffFormat> {
match mode.to_lowercase().as_str() {
HIGHLIGHT_MODE_RED_BLUE => Some(DIFF_FORMAT_RED_BLUE),
HIGHLIGHT_MODE_RED_GREEN => Some(DIFF_FORMAT_RED_GREEN),
HIGHLIGHT_MODE_RED_YELLOW => Some(DIFF_FORMAT_RED_YELLOW),
HIGHLIGHT_MODE_BOLD => Some(DIFF_FORMAT_BOLD),
HIGHLIGHT_MODE_OFF => Some(DIFF_FORMAT_NO_HIGHLIGHT),
_ => None,
}
}
#[cfg(feature = "std")]
fn is_color_mode(mode: &str) -> bool {
!matches!(
mode.to_lowercase().as_str(),
HIGHLIGHT_MODE_BOLD | HIGHLIGHT_MODE_OFF
)
}
#[cfg(feature = "std")]
fn is_no_color_env_var_set() -> bool {
use crate::env;
match env::var(ENV_VAR_NO_COLOR) {
Ok(value) => !value.is_empty(),
Err(env::VarError::NotPresent) => false,
Err(env::VarError::NotUnicode(value)) => !value.is_empty(),
}
}
#[cfg(not(feature = "std"))]
pub const fn configured_diff_format_impl() -> DiffFormat {
DEFAULT_DIFF_FORMAT
}
#[cfg(feature = "std")]
#[allow(clippy::print_stderr)]
#[must_use]
#[inline]
pub fn configured_diff_format_impl() -> DiffFormat {
use crate::env;
match env::var(ENV_VAR_HIGHLIGHT_DIFFS) {
Ok(value) => {
if is_color_mode(&value) && is_no_color_env_var_set() {
DIFF_FORMAT_NO_HIGHLIGHT
} else {
diff_format_for_mode(&value).unwrap_or_else(|| {
#[cfg(feature = "std")]
eprintln!(
"WARNING: the environment variable `{ENV_VAR_HIGHLIGHT_DIFFS}` is set to the unrecognized value {value:?}.\n\t=> Default highlight mode \"{DEFAULT_HIGHLIGHT_MODE}\" is used."
);
DEFAULT_DIFF_FORMAT
})
}
},
Err(env::VarError::NotPresent) => {
if is_no_color_env_var_set() {
DIFF_FORMAT_NO_HIGHLIGHT
} else {
DEFAULT_DIFF_FORMAT
}
},
Err(env::VarError::NotUnicode(value)) => {
#[cfg(feature = "std")]
eprintln!(
"WARNING: the environment variable `{ENV_VAR_HIGHLIGHT_DIFFS}` is set to the unrecognized value {value:?}.\n\t=> Default highlight mode \"{DEFAULT_HIGHLIGHT_MODE}\" is used."
);
DEFAULT_DIFF_FORMAT
},
}
}
#[inline]
pub fn mark_diff_impl(actual: &str, expected: &str, format: &DiffFormat) -> (String, String) {
use crate::std::vec::Vec;
use sdiff::Diff;
let actual = actual.chars().collect::<Vec<_>>();
let expected = expected.chars().collect::<Vec<_>>();
let mut marked_actual = Vec::with_capacity(actual.len());
let mut marked_expected = Vec::with_capacity(expected.len());
let diffs = sdiff::diff(&actual, &expected);
for diff in diffs {
match diff {
Diff::Left { index, length } => {
marked_actual.extend(format.unexpected.start.chars());
marked_actual.extend_from_slice(&actual[index..(index + length)]);
marked_actual.extend(format.unexpected.end.chars());
},
Diff::Both {
left_index,
right_index,
length,
} => {
marked_actual.extend_from_slice(&actual[left_index..left_index + length]);
marked_expected.extend_from_slice(&expected[right_index..right_index + length]);
},
Diff::Right { index, length } => {
marked_expected.extend(format.missing.start.chars());
marked_expected.extend_from_slice(&expected[index..(index + length)]);
marked_expected.extend(format.missing.end.chars());
},
}
}
(
String::from_iter(marked_actual),
String::from_iter(marked_expected),
)
}
#[inline]
pub fn mark_unexpected_impl<T>(value: &T, format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
format!(
"{}{value:?}{}",
format.unexpected.start, format.unexpected.end
)
}
#[inline]
pub fn mark_missing_impl<T>(value: &T, format: &DiffFormat) -> String
where
T: Debug + ?Sized,
{
format!("{}{value:?}{}", format.missing.start, format.missing.end)
}
#[inline]
pub fn mark_unexpected_string_impl(string: &str, format: &DiffFormat) -> String {
format!(
"{}{string}{}",
format.unexpected.start, format.unexpected.end
)
}
#[inline]
pub fn mark_missing_string_impl(string: &str, format: &DiffFormat) -> String {
format!("{}{string}{}", format.missing.start, format.missing.end)
}
#[inline]
pub fn mark_unexpected_char_impl(character: char, format: &DiffFormat) -> String {
format!(
"{}{character}{}",
format.unexpected.start, format.unexpected.end
)
}
#[inline]
pub fn mark_missing_char_impl(character: char, format: &DiffFormat) -> String {
format!("{}{character}{}", format.missing.start, format.missing.end)
}
}
#[cfg(test)]
mod tests;