1use std::io::IsTerminal;
16
17use rich::protocol::{Support, TargetCapabilities};
18use rich::{ColorSystem, Console, Renderable};
19use serde::Serialize;
20
21use super::{DiffView, Layout};
22use crate::target::{RenderTarget, TargetKind};
23use crate::testing::RenderSnapshot;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Report {
28 pub layout: Layout,
29 pub color: bool,
30 pub width: usize,
31}
32
33impl Report {
34 pub fn from_env() -> Self {
37 let var = |name: &str| std::env::var(name).ok();
38 let layout = match var("RICH_ASSERT_LAYOUT").as_deref() {
39 Some("side-by-side" | "side_by_side" | "sbs") => Layout::SideBySide,
40 _ => Layout::Unified,
41 };
42 let color = match var("RICH_ASSERT_COLOR").as_deref() {
43 Some("1") => true,
44 Some("0") => false,
45 _ => std::io::stdout().is_terminal() && var("CI").is_none(),
46 };
47 Report {
48 layout,
49 color,
50 width: 100,
51 }
52 }
53
54 fn console(&self) -> Console {
55 let builder = Console::builder().width(self.width).height(10_000);
56 if self.color {
57 builder
58 .force_terminal(true)
59 .color_system(Some(ColorSystem::Standard))
60 .theme(crate::theme::extended_theme())
61 .build()
62 } else {
63 builder
64 .force_terminal(false)
65 .no_color(true)
66 .ascii_only(true)
67 .build()
68 }
69 }
70
71 pub fn render(&self, view: &DiffView) -> String {
73 let console = self.console();
74 let view = view.clone().layout(self.layout);
75 let segments = view.rich_render(&console, &console.options());
76 console.segments_to_string(&segments)
77 }
78}
79
80#[track_caller]
81fn fail(what: &str, view: DiffView, message: Option<String>) -> ! {
82 let rendered = Report::from_env().render(&view);
83 match message {
84 Some(message) => panic!("assertion `{what}` failed: {message}\n{rendered}"),
85 None => panic!("assertion `{what}` failed\n{rendered}"),
86 }
87}
88
89#[track_caller]
91pub fn assert_str_eq(left: &str, right: &str, message: Option<String>) {
92 if left != right {
93 fail(
94 "left == right",
95 DiffView::new(left, right).titles("left", "right"),
96 message,
97 );
98 }
99}
100
101#[track_caller]
104pub fn assert_json_eq<L: Serialize + ?Sized, R: Serialize + ?Sized>(
105 left: &L,
106 right: &R,
107 message: Option<String>,
108) {
109 let json =
110 |v: serde_json::Result<String>| v.unwrap_or_else(|e| format!("<not serializable: {e}>"));
111 let (l, r) = (
112 serde_json::to_value(left).and_then(|v| serde_json::to_string_pretty(&v)),
113 serde_json::to_value(right).and_then(|v| serde_json::to_string_pretty(&v)),
114 );
115 let (l, r) = (json(l) + "\n", json(r) + "\n");
116 if l != r {
117 fail(
118 "left == right",
119 DiffView::new(&l, &r).titles("left", "right"),
120 message,
121 );
122 }
123}
124
125pub fn render_plain(renderable: &dyn Renderable, width: usize) -> String {
128 let target = RenderTarget::new(
129 TargetKind::PlainStream,
130 TargetCapabilities {
131 width,
132 height: 10_000,
133 color_system: None,
134 interactive: false,
135 unicode: true,
136 hyperlinks: false,
137 sixel: Support::Unsupported,
138 },
139 crate::theme::extended_theme(),
140 );
141 normalize(&target.console().render_to_string(renderable))
142}
143
144fn normalize(text: &str) -> String {
145 let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
146 let end = lines
147 .iter()
148 .rposition(|l| !l.is_empty())
149 .map_or(0, |i| i + 1);
150 let mut out = lines[..end].join("\n");
151 if !out.is_empty() {
152 out.push('\n');
153 }
154 out
155}
156
157#[track_caller]
159pub fn assert_render_eq(
160 renderable: &dyn Renderable,
161 expected: &str,
162 width: usize,
163 message: Option<String>,
164) {
165 let actual = render_plain(renderable, width);
166 let expected = normalize(expected);
167 if actual != expected {
168 fail(
169 "rendered == expected",
170 DiffView::new(&expected, &actual).titles("expected", "rendered"),
171 message,
172 );
173 }
174}
175
176#[track_caller]
179pub fn assert_snapshot_eq(left: &RenderSnapshot, right: &RenderSnapshot, message: Option<String>) {
180 if let Some(difference) = left.diff(right) {
181 let message = match message {
182 Some(m) => format!("{m}\n{difference}"),
183 None => difference,
184 };
185 fail(
186 "left == right",
187 DiffView::snapshots(left, right).titles("left", "right"),
188 Some(message),
189 );
190 }
191}
192
193#[macro_export]
199macro_rules! assert_rich_eq {
200 ($left:expr, $right:expr $(,)?) => {
201 $crate::diff::assert::assert_str_eq(
202 ::core::convert::AsRef::<str>::as_ref(&$left),
203 ::core::convert::AsRef::<str>::as_ref(&$right),
204 ::core::option::Option::None,
205 )
206 };
207 ($left:expr, $right:expr, $($arg:tt)+) => {
208 $crate::diff::assert::assert_str_eq(
209 ::core::convert::AsRef::<str>::as_ref(&$left),
210 ::core::convert::AsRef::<str>::as_ref(&$right),
211 ::core::option::Option::Some(::std::format!($($arg)+)),
212 )
213 };
214}
215
216#[macro_export]
223macro_rules! assert_rich_json_eq {
224 ($left:expr, $right:expr $(,)?) => {
225 $crate::diff::assert::assert_json_eq(&$left, &$right, ::core::option::Option::None)
226 };
227 ($left:expr, $right:expr, $($arg:tt)+) => {
228 $crate::diff::assert::assert_json_eq(
229 &$left,
230 &$right,
231 ::core::option::Option::Some(::std::format!($($arg)+)),
232 )
233 };
234}
235
236#[macro_export]
243macro_rules! assert_render_eq {
244 ($renderable:expr, $expected:expr, width = $width:expr $(,)?) => {
245 $crate::diff::assert::assert_render_eq(
246 &$renderable,
247 ::core::convert::AsRef::<str>::as_ref(&$expected),
248 $width,
249 ::core::option::Option::None,
250 )
251 };
252 ($renderable:expr, $expected:expr, width = $width:expr, $($arg:tt)+) => {
253 $crate::diff::assert::assert_render_eq(
254 &$renderable,
255 ::core::convert::AsRef::<str>::as_ref(&$expected),
256 $width,
257 ::core::option::Option::Some(::std::format!($($arg)+)),
258 )
259 };
260 ($renderable:expr, $expected:expr $(,)?) => {
261 $crate::assert_render_eq!($renderable, $expected, width = 80)
262 };
263 ($renderable:expr, $expected:expr, $($arg:tt)+) => {
264 $crate::diff::assert::assert_render_eq(
265 &$renderable,
266 ::core::convert::AsRef::<str>::as_ref(&$expected),
267 80,
268 ::core::option::Option::Some(::std::format!($($arg)+)),
269 )
270 };
271}
272
273#[macro_export]
276macro_rules! assert_snapshot_eq {
277 ($left:expr, $right:expr $(,)?) => {
278 $crate::diff::assert::assert_snapshot_eq(&$left, &$right, ::core::option::Option::None)
279 };
280 ($left:expr, $right:expr, $($arg:tt)+) => {
281 $crate::diff::assert::assert_snapshot_eq(
282 &$left,
283 &$right,
284 ::core::option::Option::Some(::std::format!($($arg)+)),
285 )
286 };
287}