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}