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- minimum support rustc 1.65.0 (897e37553 2022-11-02)
15
16*/
17
18/// Asserts that two text expressions are equal.
19///
20/// If the texts are not equal, it prints a GitHub-style diff and panics.
21///
22/// # Arguments
23///
24/// * `$left` - The first text expression.
25/// * `$right` - The second text expression.
26///
27/// # Examples
28///
29/// ```
30/// use assert_text::assert_text_eq;
31/// assert_text_eq!("hello", "hello");
32/// ```
33///
34/// ```should_panic
35/// use assert_text::assert_text_eq;
36/// assert_text_eq!("hello", "world");
37/// ```
38#[macro_export]
39macro_rules! assert_text_eq {
40    ($left: expr, $right: expr) => {
41        if $left != $right {
42            let orig = $right;
43            let edit = &$left[0..];
44            $crate::print_diff_github_style(orig, edit);
45            panic!("assertion failed")
46        };
47    };
48}
49
50/// Asserts that the first text expression starts with the second text expression.
51///
52/// If the first text does not start with the second, it prints a GitHub-style diff
53/// of the differing prefix and panics.
54///
55/// # Arguments
56///
57/// * `$left` - The text expression to check.
58/// * `$right` - The prefix to check against.
59///
60/// # Examples
61///
62/// ```
63/// use assert_text::assert_text_starts_with;
64/// assert_text_starts_with!("hello world", "hello ");
65/// ```
66///
67/// ```should_panic
68/// use assert_text::assert_text_starts_with;
69/// assert_text_starts_with!("hello world", "goodbye");
70/// ```
71#[macro_export]
72macro_rules! assert_text_starts_with {
73    ($left: expr, $right: expr) => {
74        if !$left.starts_with($right) {
75            let ll = $left.len();
76            let rl = $right.len();
77            let orig = $right;
78            let edit = &$left[0..ll.min(rl)];
79            $crate::print_diff_github_style(orig, edit);
80            panic!("assertion failed")
81        };
82    };
83}
84
85/// Asserts that the first text expression ends with the second text expression.
86///
87/// If the first text does not end with the second, it prints a GitHub-style diff
88/// of the differing suffix and panics.
89///
90/// # Arguments
91///
92/// * `$left` - The text expression to check.
93/// * `$right` - The suffix to check against.
94///
95/// # Examples
96///
97/// ```
98/// use assert_text::assert_text_ends_with;
99/// assert_text_ends_with!("hello world", " world");
100/// ```
101///
102/// ```should_panic
103/// use assert_text::assert_text_ends_with;
104/// assert_text_ends_with!("hello world", "goodbye");
105/// ```
106#[macro_export]
107macro_rules! assert_text_ends_with {
108    ($left: expr, $right: expr) => {
109        if !$left.ends_with($right) {
110            let ll = $left.len();
111            let rl = $right.len();
112            let orig = $right;
113            let edit = &$left[if ll > rl { ll - rl } else { 0 }..];
114            $crate::print_diff_github_style(orig, edit);
115            panic!("assertion failed")
116        };
117    };
118}
119
120/// Asserts that the first text contains the given second text.
121///
122/// If the text does not contains second text, it panics.
123///
124/// # Arguments
125///
126/// * `$left` - The text expression to check.
127/// * `$right` - The second text expression.
128///
129/// # Examples
130///
131/// ```
132/// use assert_text::assert_text_contains;
133/// assert_text_contains!("hello world", "o w");
134/// ```
135///
136/// ```should_panic
137/// use assert_text::assert_text_contains;
138/// assert_text_contains!("hello world", "apple");
139/// ```
140#[macro_export]
141macro_rules! assert_text_contains {
142    ($left: expr, $right: expr) => {
143        if !$left.contains($right) {
144            panic!(
145                concat!("assertion failed\n", "  left: \"{}\"\n", " right: \"{}\""),
146                $left.escape_debug(),
147                $right.escape_debug(),
148            );
149        };
150    };
151}
152
153/// Asserts that the first text expression matches the given regular expression.
154///
155/// If the text does not match the regex, it panics.
156///
157/// # Arguments
158///
159/// * `$left` - The text expression to check.
160/// * `$right` - The regular expression string.
161///
162/// # Panics
163///
164/// Panics if the `$right` string is not a valid regular expression.
165///
166/// # Examples
167///
168/// ```
169/// use assert_text::assert_text_match;
170/// assert_text_match!("hello world", r"^h.+d$");
171/// ```
172///
173/// ```should_panic
174/// use assert_text::assert_text_match;
175/// assert_text_match!("hello world", r"^goodbye.*");
176/// ```
177#[macro_export]
178macro_rules! assert_text_match {
179    ($left: expr, $right: expr) => {
180        let re = regex::Regex::new($right).unwrap();
181        if !re.is_match($left) {
182            panic!(
183                concat!("assertion failed\n", "  left: \"{}\"\n", " regex: \"{}\""),
184                $left.escape_debug(),
185                $right.escape_debug(),
186            );
187        };
188    };
189}
190
191use difference::{Changeset, Difference};
192use std::string::ToString;
193
194/// Prints a GitHub-style diff between two text slices to stdout.
195///
196/// This function highlights additions in green and removals in red.
197///
198/// # Arguments
199///
200/// * `text1` - The original text.
201/// * `text2` - The modified text.
202///
203/// # Examples
204///
205/// ```
206/// use assert_text::print_diff_github_style;
207/// print_diff_github_style("hello world", "Hello orld");
208/// ```
209pub fn print_diff_github_style(text1: &str, text2: &str) {
210    //
211    let color_green = "\x1b[32m";
212    let color_red = "\x1b[31m";
213    let color_bright_green = "\x1b[1;32m";
214    let color_reverse_red = "\x1b[31;7m";
215    let color_reverse_green = "\x1b[32;7m";
216    let color_end = "\x1b[0m";
217    //
218    let mut out_s = String::new();
219    //
220    let Changeset { diffs, .. } = Changeset::new(text1, text2, "\n");
221    //
222    for i in 0..diffs.len() {
223        let s = match diffs[i] {
224            Difference::Same(ref y) => format_diff_line_same(y),
225            Difference::Add(ref y) => {
226                let opt = if i > 0 {
227                    if let Difference::Rem(ref x) = diffs[i - 1] {
228                        Some(format_diff_add_rem(
229                            "+",
230                            x,
231                            y,
232                            color_green,
233                            color_reverse_green,
234                            color_end,
235                        ))
236                    } else {
237                        None
238                    }
239                } else {
240                    None
241                };
242                match opt {
243                    Some(a) => a,
244                    None => format_diff_line_mark("+", y, color_bright_green, color_end),
245                }
246            }
247            Difference::Rem(ref y) => {
248                let opt = if i < diffs.len() - 1 {
249                    if let Difference::Add(ref x) = diffs[i + 1] {
250                        Some(format_diff_add_rem(
251                            "-",
252                            x,
253                            y,
254                            color_red,
255                            color_reverse_red,
256                            color_end,
257                        ))
258                    } else {
259                        None
260                    }
261                } else {
262                    None
263                };
264                match opt {
265                    Some(a) => a,
266                    None => format_diff_line_mark("-", y, color_red, color_end),
267                }
268            }
269        };
270        out_s.push_str(s.as_str());
271    }
272    //
273    print!("{}", out_s.as_str());
274}
275
276/// Formats a line that is the same in both texts for diff output.
277/// Prepends a space to the line.
278#[inline(never)]
279fn format_diff_line_same(y: &str) -> String {
280    let mut s = String::with_capacity(y.len() + 2);
281    for line in y.split_terminator('\n') {
282        s.reserve(line.len() + 2);
283        s.push(' ');
284        s.push_str(line);
285        s.push('\n');
286    }
287    s
288}
289
290/// Formats a line that is either added or removed, with a specific mark and color.
291#[inline(never)]
292fn format_diff_line_mark(
293    mark: &str, // "+" or "-"
294    y: &str,
295    color_start: &str,
296    color_end: &str,
297) -> String {
298    let mut s = String::with_capacity(y.len() + 2);
299    for line in y.split_terminator('\n') {
300        s.reserve(line.len() + 2);
301        s.push_str(color_start);
302        s.push_str(mark);
303        s.push_str(line);
304        s.push_str(color_end);
305        s.push('\n');
306    }
307    s
308}
309
310/// Formats a line that has been changed (both added and removed parts) for diff output.
311#[inline(never)]
312fn format_diff_add_rem(
313    mark: &str, // "+" or "-"
314    x: &str,
315    y: &str,
316    color_fore: &str,
317    color_reverse: &str,
318    color_end: &str,
319) -> String {
320    //
321    #[derive(PartialEq, Copy, Clone)]
322    enum Cattr {
323        None,
324        Fore,
325        Reve,
326    }
327    //
328    let mut ca_v: Vec<(Cattr, String)> = vec![(Cattr::Fore, mark.to_string())];
329    //
330    let Changeset { diffs, .. } = Changeset::new(x, y, " ");
331    for c in diffs {
332        match c {
333            Difference::Same(ref z) => {
334                for line in z.split_terminator('\n') {
335                    ca_v.push((Cattr::Fore, line.to_string()));
336                    ca_v.push((Cattr::None, "\n".to_string()));
337                    ca_v.push((Cattr::Fore, mark.to_string()));
338                }
339                let bytes = z.as_bytes();
340                let len = bytes.len();
341                if len >= 1 && bytes[len - 1] != b'\n' {
342                    ca_v.pop();
343                    ca_v.pop();
344                }
345                ca_v.push((Cattr::Fore, " ".to_string()));
346            }
347            Difference::Add(ref z) => {
348                for line in z.split_terminator('\n') {
349                    ca_v.push((Cattr::Reve, line.to_string()));
350                    ca_v.push((Cattr::None, "\n".to_string()));
351                    ca_v.push((Cattr::Fore, mark.to_string()));
352                }
353                let bytes = z.as_bytes();
354                let len = bytes.len();
355                if len >= 1 && bytes[len - 1] != b'\n' {
356                    ca_v.pop();
357                    ca_v.pop();
358                }
359                ca_v.push((Cattr::Fore, " ".to_string()));
360            }
361            _ => {}
362        };
363    }
364    //
365    let mut out_s = String::with_capacity(x.len().max(y.len()) * 2);
366    let mut prev_a: Cattr = Cattr::None;
367    for (cat, st) in &ca_v {
368        //
369        if prev_a != *cat {
370            if prev_a != Cattr::None {
371                out_s.push_str(color_end)
372            }
373            if *cat == Cattr::Fore {
374                out_s.push_str(color_fore);
375            } else if *cat == Cattr::Reve {
376                out_s.push_str(color_reverse);
377            }
378            prev_a = *cat;
379        }
380        out_s.push_str(st.as_str());
381    }
382    if prev_a != Cattr::None {
383        out_s.push_str(color_end);
384    }
385    out_s.push('\n');
386    //
387    out_s
388}