use std::io::IsTerminal;
use rich::protocol::{Support, TargetCapabilities};
use rich::{ColorSystem, Console, Renderable};
use serde::Serialize;
use super::{DiffView, Layout};
use crate::target::{RenderTarget, TargetKind};
use crate::testing::RenderSnapshot;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Report {
pub layout: Layout,
pub color: bool,
pub width: usize,
}
impl Report {
pub fn from_env() -> Self {
let var = |name: &str| std::env::var(name).ok();
let layout = match var("RICH_ASSERT_LAYOUT").as_deref() {
Some("side-by-side" | "side_by_side" | "sbs") => Layout::SideBySide,
_ => Layout::Unified,
};
let color = match var("RICH_ASSERT_COLOR").as_deref() {
Some("1") => true,
Some("0") => false,
_ => std::io::stdout().is_terminal() && var("CI").is_none(),
};
Report {
layout,
color,
width: 100,
}
}
fn console(&self) -> Console {
let builder = Console::builder().width(self.width).height(10_000);
if self.color {
builder
.force_terminal(true)
.color_system(Some(ColorSystem::Standard))
.theme(crate::theme::extended_theme())
.build()
} else {
builder
.force_terminal(false)
.no_color(true)
.ascii_only(true)
.build()
}
}
pub fn render(&self, view: &DiffView) -> String {
let console = self.console();
let view = view.clone().layout(self.layout);
let segments = view.rich_render(&console, &console.options());
console.segments_to_string(&segments)
}
}
#[track_caller]
fn fail(what: &str, view: DiffView, message: Option<String>) -> ! {
let rendered = Report::from_env().render(&view);
match message {
Some(message) => panic!("assertion `{what}` failed: {message}\n{rendered}"),
None => panic!("assertion `{what}` failed\n{rendered}"),
}
}
#[track_caller]
pub fn assert_str_eq(left: &str, right: &str, message: Option<String>) {
if left != right {
fail(
"left == right",
DiffView::new(left, right).titles("left", "right"),
message,
);
}
}
#[track_caller]
pub fn assert_json_eq<L: Serialize + ?Sized, R: Serialize + ?Sized>(
left: &L,
right: &R,
message: Option<String>,
) {
let json =
|v: serde_json::Result<String>| v.unwrap_or_else(|e| format!("<not serializable: {e}>"));
let (l, r) = (
serde_json::to_value(left).and_then(|v| serde_json::to_string_pretty(&v)),
serde_json::to_value(right).and_then(|v| serde_json::to_string_pretty(&v)),
);
let (l, r) = (json(l) + "\n", json(r) + "\n");
if l != r {
fail(
"left == right",
DiffView::new(&l, &r).titles("left", "right"),
message,
);
}
}
pub fn render_plain(renderable: &dyn Renderable, width: usize) -> String {
let target = RenderTarget::new(
TargetKind::PlainStream,
TargetCapabilities {
width,
height: 10_000,
color_system: None,
interactive: false,
unicode: true,
hyperlinks: false,
sixel: Support::Unsupported,
},
crate::theme::extended_theme(),
);
normalize(&target.console().render_to_string(renderable))
}
fn normalize(text: &str) -> String {
let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
let end = lines
.iter()
.rposition(|l| !l.is_empty())
.map_or(0, |i| i + 1);
let mut out = lines[..end].join("\n");
if !out.is_empty() {
out.push('\n');
}
out
}
#[track_caller]
pub fn assert_render_eq(
renderable: &dyn Renderable,
expected: &str,
width: usize,
message: Option<String>,
) {
let actual = render_plain(renderable, width);
let expected = normalize(expected);
if actual != expected {
fail(
"rendered == expected",
DiffView::new(&expected, &actual).titles("expected", "rendered"),
message,
);
}
}
#[track_caller]
pub fn assert_snapshot_eq(left: &RenderSnapshot, right: &RenderSnapshot, message: Option<String>) {
if let Some(difference) = left.diff(right) {
let message = match message {
Some(m) => format!("{m}\n{difference}"),
None => difference,
};
fail(
"left == right",
DiffView::snapshots(left, right).titles("left", "right"),
Some(message),
);
}
}
#[macro_export]
macro_rules! assert_rich_eq {
($left:expr, $right:expr $(,)?) => {
$crate::diff::assert::assert_str_eq(
::core::convert::AsRef::<str>::as_ref(&$left),
::core::convert::AsRef::<str>::as_ref(&$right),
::core::option::Option::None,
)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::diff::assert::assert_str_eq(
::core::convert::AsRef::<str>::as_ref(&$left),
::core::convert::AsRef::<str>::as_ref(&$right),
::core::option::Option::Some(::std::format!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_rich_json_eq {
($left:expr, $right:expr $(,)?) => {
$crate::diff::assert::assert_json_eq(&$left, &$right, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::diff::assert::assert_json_eq(
&$left,
&$right,
::core::option::Option::Some(::std::format!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_render_eq {
($renderable:expr, $expected:expr, width = $width:expr $(,)?) => {
$crate::diff::assert::assert_render_eq(
&$renderable,
::core::convert::AsRef::<str>::as_ref(&$expected),
$width,
::core::option::Option::None,
)
};
($renderable:expr, $expected:expr, width = $width:expr, $($arg:tt)+) => {
$crate::diff::assert::assert_render_eq(
&$renderable,
::core::convert::AsRef::<str>::as_ref(&$expected),
$width,
::core::option::Option::Some(::std::format!($($arg)+)),
)
};
($renderable:expr, $expected:expr $(,)?) => {
$crate::assert_render_eq!($renderable, $expected, width = 80)
};
($renderable:expr, $expected:expr, $($arg:tt)+) => {
$crate::diff::assert::assert_render_eq(
&$renderable,
::core::convert::AsRef::<str>::as_ref(&$expected),
80,
::core::option::Option::Some(::std::format!($($arg)+)),
)
};
}
#[macro_export]
macro_rules! assert_snapshot_eq {
($left:expr, $right:expr $(,)?) => {
$crate::diff::assert::assert_snapshot_eq(&$left, &$right, ::core::option::Option::None)
};
($left:expr, $right:expr, $($arg:tt)+) => {
$crate::diff::assert::assert_snapshot_eq(
&$left,
&$right,
::core::option::Option::Some(::std::format!($($arg)+)),
)
};
}