Skip to main content

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}