assert_text/lib.rs
1/*!
2the testing macro tools.
3
4This checks that strings are equal.
5You will see different characters if that is different.
6
7# Features
8
9- assert_text_eq!(txt1, txt2)
10- assert_text_contains!(txt1, txt2)
11- assert_text_starts_with!(txt1, txt2)
12- assert_text_ends_with!(txt1, txt2)
13- assert_text_match!(txt1, regex_text2)
14- supports custom panic messages
15- minimum support rustc 1.65.0 (897e37553 2022-11-02)
16
17*/
18
19/// Asserts that two text expressions are equal.
20///
21/// If the texts are not equal, it prints a GitHub-style diff and panics.
22///
23/// # Arguments
24///
25/// * `$left` - The first text expression.
26/// * `$right` - The second text expression.
27///
28/// # Examples
29///
30/// ```
31/// use assert_text::assert_text_eq;
32/// assert_text_eq!("hello", "hello");
33/// ```
34///
35/// ```should_panic
36/// use assert_text::assert_text_eq;
37/// assert_text_eq!("hello", "world");
38/// ```
39///
40/// ```should_panic
41/// use assert_text::assert_text_eq;
42/// assert_text_eq!("hello", "world", "custom message: {}", "foo");
43/// ```
44#[macro_export]
45macro_rules! assert_text_eq {
46 ($left: expr, $right: expr $(,)?) => {
47 $crate::assert_text_eq!($left, $right, "assertion failed")
48 };
49 ($left: expr, $right: expr, $($arg:tt)+) => {
50 match (&$left, &$right) {
51 (left_val, right_val) => {
52 let left_val: &str = left_val.as_ref();
53 let right_val: &str = right_val.as_ref();
54 if left_val != right_val {
55 $crate::print_diff_github_style(right_val, left_val);
56 panic!($($arg)+)
57 }
58 }
59 }
60 };
61}
62
63/// Asserts that the first text expression starts with the second text expression.
64///
65/// If the first text does not start with the second, it prints a GitHub-style diff
66/// of the differing prefix and panics.
67///
68/// # Arguments
69///
70/// * `$left` - The text expression to check.
71/// * `$right` - The prefix to check against.
72///
73/// # Examples
74///
75/// ```
76/// use assert_text::assert_text_starts_with;
77/// assert_text_starts_with!("hello world", "hello ");
78/// ```
79///
80/// ```should_panic
81/// use assert_text::assert_text_starts_with;
82/// assert_text_starts_with!("hello world", "goodbye");
83/// ```
84///
85/// ```should_panic
86/// use assert_text::assert_text_starts_with;
87/// assert_text_starts_with!("hello world", "goodbye", "custom message: {}", "foo");
88/// ```
89#[macro_export]
90macro_rules! assert_text_starts_with {
91 ($left: expr, $right: expr $(,)?) => {
92 $crate::assert_text_starts_with!($left, $right, "assertion failed")
93 };
94 ($left: expr, $right: expr, $($arg:tt)+) => {
95 match (&$left, &$right) {
96 (left_val, right_val) => {
97 let left_val: &str = left_val.as_ref();
98 let right_val: &str = right_val.as_ref();
99 if !left_val.starts_with(right_val) {
100 let right_chars = right_val.chars().count();
101 let limit = left_val
102 .char_indices()
103 .nth(right_chars)
104 .map(|(idx, _)| idx)
105 .unwrap_or_else(|| left_val.len());
106 let edit = &left_val[..limit];
107 $crate::print_diff_github_style(right_val, edit);
108 panic!($($arg)+)
109 }
110 }
111 }
112 };
113}
114
115/// Asserts that the first text expression ends with the second text expression.
116///
117/// If the first text does not end with the second, it prints a GitHub-style diff
118/// of the differing suffix and panics.
119///
120/// # Arguments
121///
122/// * `$left` - The text expression to check.
123/// * `$right` - The suffix to check against.
124///
125/// # Examples
126///
127/// ```
128/// use assert_text::assert_text_ends_with;
129/// assert_text_ends_with!("hello world", " world");
130/// ```
131///
132/// ```should_panic
133/// use assert_text::assert_text_ends_with;
134/// assert_text_ends_with!("hello world", "goodbye");
135/// ```
136///
137/// ```should_panic
138/// use assert_text::assert_text_ends_with;
139/// assert_text_ends_with!("hello world", "goodbye", "custom message: {}", "foo");
140/// ```
141#[macro_export]
142macro_rules! assert_text_ends_with {
143 ($left: expr, $right: expr $(,)?) => {
144 $crate::assert_text_ends_with!($left, $right, "assertion failed")
145 };
146 ($left: expr, $right: expr, $($arg:tt)+) => {
147 match (&$left, &$right) {
148 (left_val, right_val) => {
149 let left_val: &str = left_val.as_ref();
150 let right_val: &str = right_val.as_ref();
151 if !left_val.ends_with(right_val) {
152 let right_chars = right_val.chars().count();
153 let total_chars = left_val.chars().count();
154 let skip_chars = total_chars.saturating_sub(right_chars);
155 let limit = left_val
156 .char_indices()
157 .nth(skip_chars)
158 .map(|(idx, _)| idx)
159 .unwrap_or(0);
160 let edit = &left_val[limit..];
161 $crate::print_diff_github_style(right_val, edit);
162 panic!($($arg)+)
163 }
164 }
165 }
166 };
167}
168
169/// Asserts that the first text contains the given second text.
170///
171/// If the text does not contains second text, it panics.
172///
173/// # Arguments
174///
175/// * `$left` - The text expression to check.
176/// * `$right` - The second text expression.
177///
178/// # Examples
179///
180/// ```
181/// use assert_text::assert_text_contains;
182/// assert_text_contains!("hello world", "o w");
183/// ```
184///
185/// ```should_panic
186/// use assert_text::assert_text_contains;
187/// assert_text_contains!("hello world", "apple");
188/// ```
189///
190/// ```should_panic
191/// use assert_text::assert_text_contains;
192/// assert_text_contains!("hello world", "apple", "custom message: {}", "foo");
193/// ```
194#[macro_export]
195macro_rules! assert_text_contains {
196 ($left: expr, $right: expr $(,)?) => {
197 match (&$left, &$right) {
198 (left_val, right_val) => {
199 let left_val: &str = left_val.as_ref();
200 let right_val: &str = right_val.as_ref();
201 if !left_val.contains(right_val) {
202 $crate::assert_text_contains!(
203 left_val,
204 right_val,
205 concat!("assertion failed\n", " left: \"{}\"\n", " right: \"{}\""),
206 left_val.escape_debug(),
207 right_val.escape_debug(),
208 )
209 }
210 }
211 }
212 };
213 ($left: expr, $right: expr, $($arg:tt)+) => {
214 match (&$left, &$right) {
215 (left_val, right_val) => {
216 let left_val: &str = left_val.as_ref();
217 let right_val: &str = right_val.as_ref();
218 if !left_val.contains(right_val) {
219 panic!($($arg)+);
220 }
221 }
222 }
223 };
224}
225
226/// Asserts that the first text expression matches the given regular expression.
227///
228/// If the text does not match the regex, it panics.
229///
230/// # Arguments
231///
232/// * `$left` - The text expression to check.
233/// * `$right` - The regular expression string.
234///
235/// # Panics
236///
237/// Panics if the `$right` string is not a valid regular expression.
238///
239/// # Examples
240///
241/// ```
242/// use assert_text::assert_text_match;
243/// assert_text_match!("hello world", r"^h.+d$");
244/// ```
245///
246/// ```should_panic
247/// use assert_text::assert_text_match;
248/// assert_text_match!("hello world", r"^goodbye.*");
249/// ```
250///
251/// ```should_panic
252/// use assert_text::assert_text_match;
253/// assert_text_match!("hello world", r"^goodbye.*", "custom message: {}", "foo");
254/// ```
255#[macro_export]
256macro_rules! assert_text_match {
257 ($left: expr, $right: expr $(,)?) => {
258 match (&$left, &$right) {
259 (left_val, right_val) => {
260 let left_val: &str = left_val.as_ref();
261 let right_val: &str = right_val.as_ref();
262 let re = regex::Regex::new(right_val).unwrap();
263 if !re.is_match(left_val) {
264 $crate::assert_text_match!(
265 left_val,
266 right_val,
267 concat!("assertion failed\n", " left: \"{}\"\n", " regex: \"{}\""),
268 left_val.escape_debug(),
269 right_val.escape_debug(),
270 )
271 }
272 }
273 }
274 };
275 ($left: expr, $right: expr, $($arg:tt)+) => {
276 match (&$left, &$right) {
277 (left_val, right_val) => {
278 let left_val: &str = left_val.as_ref();
279 let right_val: &str = right_val.as_ref();
280 let re = regex::Regex::new(right_val).unwrap();
281 if !re.is_match(left_val) {
282 panic!($($arg)+);
283 }
284 }
285 }
286 };
287}
288
289use difference::{Changeset, Difference};
290
291/// Prints a GitHub-style diff between two text slices to stdout.
292///
293/// This function highlights additions in green and removals in red.
294///
295/// # Arguments
296///
297/// * `text1` - The original text.
298/// * `text2` - The modified text.
299///
300/// # Examples
301///
302/// ```
303/// use assert_text::print_diff_github_style;
304/// print_diff_github_style("hello world", "Hello orld");
305/// ```
306pub fn print_diff_github_style(text1: &str, text2: &str) {
307 //
308 let use_color = std::env::var("NO_COLOR").is_err();
309 let color_green = if use_color { "\x1b[32m" } else { "" };
310 let color_red = if use_color { "\x1b[31m" } else { "" };
311 let color_bright_green = if use_color { "\x1b[1;32m" } else { "" };
312 let color_reverse_red = if use_color { "\x1b[31;7m" } else { "" };
313 let color_reverse_green = if use_color { "\x1b[32;7m" } else { "" };
314 let color_end = if use_color { "\x1b[0m" } else { "" };
315 //
316 let mut out_s = String::new();
317 //
318 let Changeset { diffs, .. } = Changeset::new(text1, text2, "\n");
319 //
320 for i in 0..diffs.len() {
321 let s = match diffs[i] {
322 Difference::Same(ref y) => format_diff_line_same(y),
323 Difference::Add(ref y) => {
324 let opt = if i > 0 {
325 if let Difference::Rem(ref x) = diffs[i - 1] {
326 Some(format_diff_add_rem(
327 "+",
328 x,
329 y,
330 color_green,
331 color_reverse_green,
332 color_end,
333 ))
334 } else {
335 None
336 }
337 } else {
338 None
339 };
340 match opt {
341 Some(a) => a,
342 None => format_diff_line_mark("+", y, color_bright_green, color_end),
343 }
344 }
345 Difference::Rem(ref y) => {
346 let opt = if i < diffs.len() - 1 {
347 if let Difference::Add(ref x) = diffs[i + 1] {
348 Some(format_diff_add_rem(
349 "-",
350 x,
351 y,
352 color_red,
353 color_reverse_red,
354 color_end,
355 ))
356 } else {
357 None
358 }
359 } else {
360 None
361 };
362 match opt {
363 Some(a) => a,
364 None => format_diff_line_mark("-", y, color_red, color_end),
365 }
366 }
367 };
368 out_s.push_str(s.as_str());
369 }
370 //
371 print!("{}", out_s.as_str());
372}
373
374/// Formats a line that is the same in both texts for diff output.
375/// Prepends a space to the line.
376#[inline(never)]
377fn format_diff_line_same(y: &str) -> String {
378 let mut s = String::with_capacity(y.len() + 2);
379 for line in y.split_terminator('\n') {
380 s.reserve(line.len() + 2);
381 s.push(' ');
382 s.push_str(line);
383 s.push('\n');
384 }
385 s
386}
387
388/// Formats a line that is either added or removed, with a specific mark and color.
389#[inline(never)]
390fn format_diff_line_mark(
391 mark: &str, // "+" or "-"
392 y: &str,
393 color_start: &str,
394 color_end: &str,
395) -> String {
396 let line_count = y.split_terminator('\n').count();
397 let extra_per_line = color_start.len() + mark.len() + color_end.len() + 1;
398 let mut s = String::with_capacity(y.len() + (line_count * extra_per_line));
399 for line in y.split_terminator('\n') {
400 s.push_str(color_start);
401 s.push_str(mark);
402 s.push_str(line);
403 s.push_str(color_end);
404 s.push('\n');
405 }
406 s
407}
408
409/// Formats a line that has been changed (both added and removed parts) for diff output.
410#[inline(never)]
411fn format_diff_add_rem(
412 mark: &str, // "+" or "-"
413 x: &str,
414 y: &str,
415 color_fore: &str,
416 color_reverse: &str,
417 color_end: &str,
418) -> String {
419 //
420 #[derive(PartialEq, Copy, Clone)]
421 enum Cattr {
422 None,
423 Fore,
424 Reve,
425 }
426 //
427 let mut ca_v: Vec<(Cattr, &str)> = vec![(Cattr::Fore, mark)];
428 //
429 let changeset = Changeset::new(x, y, " ");
430 for c in &changeset.diffs {
431 match c {
432 Difference::Same(ref z) => {
433 for line in z.split_terminator('\n') {
434 ca_v.push((Cattr::Fore, line));
435 ca_v.push((Cattr::None, "\n"));
436 ca_v.push((Cattr::Fore, mark));
437 }
438 let bytes = z.as_bytes();
439 let len = bytes.len();
440 if len >= 1 && bytes[len - 1] != b'\n' {
441 ca_v.pop();
442 ca_v.pop();
443 }
444 ca_v.push((Cattr::Fore, " "));
445 }
446 Difference::Add(ref z) => {
447 for line in z.split_terminator('\n') {
448 ca_v.push((Cattr::Reve, line));
449 ca_v.push((Cattr::None, "\n"));
450 ca_v.push((Cattr::Fore, mark));
451 }
452 let bytes = z.as_bytes();
453 let len = bytes.len();
454 if len >= 1 && bytes[len - 1] != b'\n' {
455 ca_v.pop();
456 ca_v.pop();
457 }
458 ca_v.push((Cattr::Fore, " "));
459 }
460 _ => {}
461 };
462 }
463 //
464 let mut out_s = String::with_capacity(x.len().max(y.len()) * 2);
465 let mut prev_a: Cattr = Cattr::None;
466 for (cat, st) in &ca_v {
467 //
468 if prev_a != *cat {
469 if prev_a != Cattr::None {
470 out_s.push_str(color_end)
471 }
472 if *cat == Cattr::Fore {
473 out_s.push_str(color_fore);
474 } else if *cat == Cattr::Reve {
475 out_s.push_str(color_reverse);
476 }
477 prev_a = *cat;
478 }
479 out_s.push_str(st);
480 }
481 if prev_a != Cattr::None {
482 out_s.push_str(color_end);
483 }
484 out_s.push('\n');
485 //
486 out_s
487}