1 | //! Minimalistic snapshot testing for Rust. |
2 | //! |
3 | //! # Introduction |
4 | //! |
5 | //! `expect_test` is a small addition over plain `assert_eq!` testing approach, |
6 | //! which allows to automatically update tests results. |
7 | //! |
8 | //! The core of the library is the `expect!` macro. It can be though of as a |
9 | //! super-charged string literal, which can update itself. |
10 | //! |
11 | //! Let's see an example: |
12 | //! |
13 | //! ```no_run |
14 | //! use expect_test::expect; |
15 | //! |
16 | //! let actual = 2 + 2; |
17 | //! let expected = expect!["5" ]; // or expect![["5"]] |
18 | //! expected.assert_eq(&actual.to_string()) |
19 | //! ``` |
20 | //! |
21 | //! Running this code will produce a test failure, as `"5"` is indeed not equal |
22 | //! to `"4"`. Running the test with `UPDATE_EXPECT=1` env variable however would |
23 | //! "magically" update the code to: |
24 | //! |
25 | //! ```no_run |
26 | //! # use expect_test::expect; |
27 | //! let actual = 2 + 2; |
28 | //! let expected = expect!["4" ]; |
29 | //! expected.assert_eq(&actual.to_string()) |
30 | //! ``` |
31 | //! |
32 | //! This becomes very useful when you have a lot of tests with verbose and |
33 | //! potentially changing expected output. |
34 | //! |
35 | //! Under the hood, the `expect!` macro uses `file!`, `line!` and `column!` to |
36 | //! record source position at compile time. At runtime, this position is used |
37 | //! to patch the file in-place, if `UPDATE_EXPECT` is set. |
38 | //! |
39 | //! # Guide |
40 | //! |
41 | //! `expect!` returns an instance of `Expect` struct, which holds position |
42 | //! information and a string literal. Use `Expect::assert_eq` for string |
43 | //! comparison. Use `Expect::assert_debug_eq` for verbose debug comparison. Note |
44 | //! that leading indentation is automatically removed. |
45 | //! |
46 | //! ``` |
47 | //! use expect_test::expect; |
48 | //! |
49 | //! #[derive(Debug)] |
50 | //! struct Foo { |
51 | //! value: i32, |
52 | //! } |
53 | //! |
54 | //! let actual = Foo { value: 92 }; |
55 | //! let expected = expect![[" |
56 | //! Foo { |
57 | //! value: 92, |
58 | //! } |
59 | //! " ]]; |
60 | //! expected.assert_debug_eq(&actual); |
61 | //! ``` |
62 | //! |
63 | //! Be careful with `assert_debug_eq` - in general, stability of the debug |
64 | //! representation is not guaranteed. However, even if it changes, you can |
65 | //! quickly update all the tests by running the test suite with `UPDATE_EXPECT` |
66 | //! environmental variable set. |
67 | //! |
68 | //! If the expected data is too verbose to include inline, you can store it in |
69 | //! an external file using the `expect_file!` macro: |
70 | //! |
71 | //! ```no_run |
72 | //! use expect_test::expect_file; |
73 | //! |
74 | //! let actual = 42; |
75 | //! let expected = expect_file!["./the-answer.txt" ]; |
76 | //! expected.assert_eq(&actual.to_string()); |
77 | //! ``` |
78 | //! |
79 | //! File path is relative to the current file. |
80 | //! |
81 | //! # Suggested Workflows |
82 | //! |
83 | //! I like to use data-driven tests with `expect_test`. I usually define a |
84 | //! single driver function `check` and then call it from individual tests: |
85 | //! |
86 | //! ``` |
87 | //! use expect_test::{expect, Expect}; |
88 | //! |
89 | //! fn check(actual: i32, expect: Expect) { |
90 | //! let actual = actual.to_string(); |
91 | //! expect.assert_eq(&actual); |
92 | //! } |
93 | //! |
94 | //! #[test] |
95 | //! fn test_addition() { |
96 | //! check(90 + 2, expect![["92" ]]); |
97 | //! } |
98 | //! |
99 | //! #[test] |
100 | //! fn test_multiplication() { |
101 | //! check(46 * 2, expect![["92" ]]); |
102 | //! } |
103 | //! ``` |
104 | //! |
105 | //! Each test's body is a single call to `check`. All the variation in tests |
106 | //! comes from the input data. |
107 | //! |
108 | //! When writing a new test, I usually copy-paste an old one, leave the `expect` |
109 | //! blank and use `UPDATE_EXPECT` to fill the value for me: |
110 | //! |
111 | //! ``` |
112 | //! # use expect_test::{expect, Expect}; |
113 | //! # fn check(_: i32, _: Expect) {} |
114 | //! #[test] |
115 | //! fn test_division() { |
116 | //! check(92 / 2, expect![["" ]]) |
117 | //! } |
118 | //! ``` |
119 | //! |
120 | //! See |
121 | //! <https://blog.janestreet.com/using-ascii-waveforms-to-test-hardware-designs/> |
122 | //! for a cool example of snapshot testing in the wild! |
123 | //! |
124 | //! # Alternatives |
125 | //! |
126 | //! * [insta](https://crates.io/crates/insta) - a more feature full snapshot |
127 | //! testing library. |
128 | //! * [k9](https://crates.io/crates/k9) - a testing library which includes |
129 | //! support for snapshot testing among other things. |
130 | //! |
131 | //! # Maintenance status |
132 | //! |
133 | //! The main customer of this library is rust-analyzer. The library is stable, |
134 | //! it is planned to not release any major versions past 1.0. |
135 | //! |
136 | //! ## Minimal Supported Rust Version |
137 | //! |
138 | //! This crate's minimum supported `rustc` version is `1.60.0`. MSRV is updated |
139 | //! conservatively, supporting roughly 10 minor versions of `rustc`. MSRV bump |
140 | //! is not considered semver breaking, but will require at least minor version |
141 | //! bump. |
142 | use std::{ |
143 | collections::HashMap, |
144 | convert::TryInto, |
145 | env, fmt, fs, mem, |
146 | ops::Range, |
147 | panic, |
148 | path::{Path, PathBuf}, |
149 | sync::Mutex, |
150 | }; |
151 | |
152 | use once_cell::sync::{Lazy, OnceCell}; |
153 | |
154 | const HELP: &str = " |
155 | You can update all `expect!` tests by running: |
156 | |
157 | env UPDATE_EXPECT=1 cargo test |
158 | |
159 | To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer. |
160 | " ; |
161 | |
162 | fn update_expect() -> bool { |
163 | env::var(key:"UPDATE_EXPECT" ).is_ok() |
164 | } |
165 | |
166 | /// Creates an instance of `Expect` from string literal: |
167 | /// |
168 | /// ``` |
169 | /// # use expect_test::expect; |
170 | /// expect![[" |
171 | /// Foo { value: 92 } |
172 | /// " ]]; |
173 | /// expect![r#"{"Foo": 92}"# ]; |
174 | /// ``` |
175 | /// |
176 | /// Leading indentation is stripped. |
177 | #[macro_export ] |
178 | macro_rules! expect { |
179 | [$data:literal] => { $crate::expect![[$data]] }; |
180 | [[$data:literal]] => {$crate::Expect { |
181 | position: $crate::Position { |
182 | file: file!(), |
183 | line: line!(), |
184 | column: column!(), |
185 | }, |
186 | data: $data, |
187 | indent: true, |
188 | }}; |
189 | [] => { $crate::expect![["" ]] }; |
190 | [[]] => { $crate::expect![["" ]] }; |
191 | } |
192 | |
193 | /// Creates an instance of `ExpectFile` from relative or absolute path: |
194 | /// |
195 | /// ``` |
196 | /// # use expect_test::expect_file; |
197 | /// expect_file!["./test_data/bar.html" ]; |
198 | /// ``` |
199 | #[macro_export ] |
200 | macro_rules! expect_file { |
201 | [$path:expr] => {$crate::ExpectFile { |
202 | path: std::path::PathBuf::from($path), |
203 | position: file!(), |
204 | }}; |
205 | } |
206 | |
207 | /// Self-updating string literal. |
208 | #[derive (Debug)] |
209 | pub struct Expect { |
210 | #[doc (hidden)] |
211 | pub position: Position, |
212 | #[doc (hidden)] |
213 | pub data: &'static str, |
214 | #[doc (hidden)] |
215 | pub indent: bool, |
216 | } |
217 | |
218 | /// Self-updating file. |
219 | #[derive (Debug)] |
220 | pub struct ExpectFile { |
221 | #[doc (hidden)] |
222 | pub path: PathBuf, |
223 | #[doc (hidden)] |
224 | pub position: &'static str, |
225 | } |
226 | |
227 | /// Position of original `expect!` in the source file. |
228 | #[derive (Debug)] |
229 | pub struct Position { |
230 | #[doc (hidden)] |
231 | pub file: &'static str, |
232 | #[doc (hidden)] |
233 | pub line: u32, |
234 | #[doc (hidden)] |
235 | pub column: u32, |
236 | } |
237 | |
238 | impl fmt::Display for Position { |
239 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
240 | write!(f, " {}: {}: {}" , self.file, self.line, self.column) |
241 | } |
242 | } |
243 | |
244 | #[derive (Clone, Copy)] |
245 | enum StrLitKind { |
246 | Normal, |
247 | Raw(usize), |
248 | } |
249 | |
250 | impl StrLitKind { |
251 | fn write_start(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
252 | match self { |
253 | Self::Normal => write!(w, " \"" ), |
254 | Self::Raw(n) => { |
255 | write!(w, "r" )?; |
256 | for _ in 0..n { |
257 | write!(w, "#" )?; |
258 | } |
259 | write!(w, " \"" ) |
260 | } |
261 | } |
262 | } |
263 | |
264 | fn write_end(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
265 | match self { |
266 | Self::Normal => write!(w, " \"" ), |
267 | Self::Raw(n) => { |
268 | write!(w, " \"" )?; |
269 | for _ in 0..n { |
270 | write!(w, "#" )?; |
271 | } |
272 | Ok(()) |
273 | } |
274 | } |
275 | } |
276 | } |
277 | |
278 | impl Expect { |
279 | /// Checks if this expect is equal to `actual`. |
280 | pub fn assert_eq(&self, actual: &str) { |
281 | let trimmed = self.trimmed(); |
282 | if trimmed == actual { |
283 | return; |
284 | } |
285 | Runtime::fail_expect(self, &trimmed, actual); |
286 | } |
287 | /// Checks if this expect is equal to `format!("{:#?}", actual)`. |
288 | pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) { |
289 | let actual = format!(" {:#?}\n" , actual); |
290 | self.assert_eq(&actual) |
291 | } |
292 | /// If `true` (default), in-place update will indent the string literal. |
293 | pub fn indent(&mut self, yes: bool) { |
294 | self.indent = yes; |
295 | } |
296 | |
297 | /// Returns the content of this expect. |
298 | pub fn data(&self) -> &str { |
299 | self.data |
300 | } |
301 | |
302 | fn trimmed(&self) -> String { |
303 | if !self.data.contains(' \n' ) { |
304 | return self.data.to_string(); |
305 | } |
306 | trim_indent(self.data) |
307 | } |
308 | |
309 | fn locate(&self, file: &str) -> Location { |
310 | let mut target_line = None; |
311 | let mut line_start = 0; |
312 | for (i, line) in lines_with_ends(file).enumerate() { |
313 | if i == self.position.line as usize - 1 { |
314 | // `column` points to the first character of the macro invocation: |
315 | // |
316 | // expect![[r#""#]] expect![""] |
317 | // ^ ^ ^ ^ |
318 | // column offset offset |
319 | // |
320 | // Seek past the exclam, then skip any whitespace and |
321 | // the macro delimiter to get to our argument. |
322 | let byte_offset = line |
323 | .char_indices() |
324 | .skip((self.position.column - 1).try_into().unwrap()) |
325 | .skip_while(|&(_, c)| c != '!' ) |
326 | .skip(1) // ! |
327 | .skip_while(|&(_, c)| c.is_whitespace()) |
328 | .skip(1) // [({ |
329 | .skip_while(|&(_, c)| c.is_whitespace()) |
330 | .next() |
331 | .expect("Failed to parse macro invocation" ) |
332 | .0; |
333 | |
334 | let literal_start = line_start + byte_offset; |
335 | let indent = line.chars().take_while(|&it| it == ' ' ).count(); |
336 | target_line = Some((literal_start, indent)); |
337 | break; |
338 | } |
339 | line_start += line.len(); |
340 | } |
341 | let (literal_start, line_indent) = target_line.unwrap(); |
342 | |
343 | let lit_to_eof = &file[literal_start..]; |
344 | let lit_to_eof_trimmed = lit_to_eof.trim_start(); |
345 | |
346 | let literal_start = literal_start + (lit_to_eof.len() - lit_to_eof_trimmed.len()); |
347 | |
348 | let literal_len = |
349 | locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`." ); |
350 | let literal_range = literal_start..literal_start + literal_len; |
351 | Location { line_indent, literal_range } |
352 | } |
353 | } |
354 | |
355 | fn locate_end(arg_start_to_eof: &str) -> Option<usize> { |
356 | match arg_start_to_eof.chars().next()? { |
357 | c: char if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`" ), |
358 | |
359 | // expect![[]] |
360 | '[' => { |
361 | let str_start_to_eof: &str = arg_start_to_eof[1..].trim_start(); |
362 | let str_len: usize = find_str_lit_len(str_lit_to_eof:str_start_to_eof)?; |
363 | let str_end_to_eof: &str = &str_start_to_eof[str_len..]; |
364 | let closing_brace_offset: usize = str_end_to_eof.find(']' )?; |
365 | Some((arg_start_to_eof.len() - str_end_to_eof.len()) + closing_brace_offset + 1) |
366 | } |
367 | |
368 | // expect![] | expect!{} | expect!() |
369 | ']' | '}' | ')' => Some(0), |
370 | |
371 | // expect!["..."] | expect![r#"..."#] |
372 | _ => find_str_lit_len(str_lit_to_eof:arg_start_to_eof), |
373 | } |
374 | } |
375 | |
376 | /// Parses a string literal, returning the byte index of its last character |
377 | /// (either a quote or a hash). |
378 | fn find_str_lit_len(str_lit_to_eof: &str) -> Option<usize> { |
379 | use StrLitKind::*; |
380 | |
381 | fn try_find_n_hashes( |
382 | s: &mut impl Iterator<Item = char>, |
383 | desired_hashes: usize, |
384 | ) -> Option<(usize, Option<char>)> { |
385 | let mut n = 0; |
386 | loop { |
387 | match s.next()? { |
388 | '#' => n += 1, |
389 | c => return Some((n, Some(c))), |
390 | } |
391 | |
392 | if n == desired_hashes { |
393 | return Some((n, None)); |
394 | } |
395 | } |
396 | } |
397 | |
398 | let mut s = str_lit_to_eof.chars(); |
399 | let kind = match s.next()? { |
400 | '"' => Normal, |
401 | 'r' => { |
402 | let (n, c) = try_find_n_hashes(&mut s, usize::MAX)?; |
403 | if c != Some('"' ) { |
404 | return None; |
405 | } |
406 | Raw(n) |
407 | } |
408 | _ => return None, |
409 | }; |
410 | |
411 | let mut oldc = None; |
412 | loop { |
413 | let c = oldc.take().or_else(|| s.next())?; |
414 | match (c, kind) { |
415 | (' \\' , Normal) => { |
416 | let _escaped = s.next()?; |
417 | } |
418 | ('"' , Normal) => break, |
419 | ('"' , Raw(0)) => break, |
420 | ('"' , Raw(n)) => { |
421 | let (seen, c) = try_find_n_hashes(&mut s, n)?; |
422 | if seen == n { |
423 | break; |
424 | } |
425 | oldc = c; |
426 | } |
427 | _ => {} |
428 | } |
429 | } |
430 | |
431 | Some(str_lit_to_eof.len() - s.as_str().len()) |
432 | } |
433 | |
434 | impl ExpectFile { |
435 | /// Checks if file contents is equal to `actual`. |
436 | pub fn assert_eq(&self, actual: &str) { |
437 | let expected = self.data(); |
438 | if actual == expected { |
439 | return; |
440 | } |
441 | Runtime::fail_file(self, &expected, actual); |
442 | } |
443 | /// Checks if file contents is equal to `format!("{:#?}", actual)`. |
444 | pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) { |
445 | let actual = format!(" {:#?}\n" , actual); |
446 | self.assert_eq(&actual) |
447 | } |
448 | /// Returns the content of this expect. |
449 | pub fn data(&self) -> String { |
450 | fs::read_to_string(self.abs_path()).unwrap_or_default().replace(" \r\n" , " \n" ) |
451 | } |
452 | fn write(&self, contents: &str) { |
453 | fs::write(self.abs_path(), contents).unwrap() |
454 | } |
455 | fn abs_path(&self) -> PathBuf { |
456 | if self.path.is_absolute() { |
457 | self.path.to_owned() |
458 | } else { |
459 | let dir = Path::new(self.position).parent().unwrap(); |
460 | to_abs_ws_path(&dir.join(&self.path)) |
461 | } |
462 | } |
463 | } |
464 | |
465 | #[derive (Default)] |
466 | struct Runtime { |
467 | help_printed: bool, |
468 | per_file: HashMap<&'static str, FileRuntime>, |
469 | } |
470 | static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default); |
471 | |
472 | impl Runtime { |
473 | fn fail_expect(expect: &Expect, expected: &str, actual: &str) { |
474 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
475 | if update_expect() { |
476 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.position); |
477 | rt.per_file |
478 | .entry(expect.position.file) |
479 | .or_insert_with(|| FileRuntime::new(expect)) |
480 | .update(expect, actual); |
481 | return; |
482 | } |
483 | rt.panic(expect.position.to_string(), expected, actual); |
484 | } |
485 | fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) { |
486 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
487 | if update_expect() { |
488 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.path.display()); |
489 | expect.write(actual); |
490 | return; |
491 | } |
492 | rt.panic(expect.path.display().to_string(), expected, actual); |
493 | } |
494 | fn panic(&mut self, position: String, expected: &str, actual: &str) { |
495 | let print_help = !mem::replace(&mut self.help_printed, true); |
496 | let help = if print_help { HELP } else { "" }; |
497 | |
498 | let diff = dissimilar::diff(expected, actual); |
499 | |
500 | println!( |
501 | " \n |
502 | \x1b[1m \x1b[91merror \x1b[97m: expect test failed \x1b[0m |
503 | \x1b[1m \x1b[34m--> \x1b[0m {} |
504 | {} |
505 | \x1b[1mExpect \x1b[0m: |
506 | ---- |
507 | {} |
508 | ---- |
509 | |
510 | \x1b[1mActual \x1b[0m: |
511 | ---- |
512 | {} |
513 | ---- |
514 | |
515 | \x1b[1mDiff \x1b[0m: |
516 | ---- |
517 | {} |
518 | ---- |
519 | " , |
520 | position, |
521 | help, |
522 | expected, |
523 | actual, |
524 | format_chunks(diff) |
525 | ); |
526 | // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise. |
527 | panic::resume_unwind(Box::new(())); |
528 | } |
529 | } |
530 | |
531 | struct FileRuntime { |
532 | path: PathBuf, |
533 | original_text: String, |
534 | patchwork: Patchwork, |
535 | } |
536 | |
537 | impl FileRuntime { |
538 | fn new(expect: &Expect) -> FileRuntime { |
539 | let path: PathBuf = to_abs_ws_path(Path::new(expect.position.file)); |
540 | let original_text: String = fs::read_to_string(&path).unwrap(); |
541 | let patchwork: Patchwork = Patchwork::new(original_text.clone()); |
542 | FileRuntime { path, original_text, patchwork } |
543 | } |
544 | fn update(&mut self, expect: &Expect, actual: &str) { |
545 | let loc: Location = expect.locate(&self.original_text); |
546 | let desired_indent: Option = if expect.indent { Some(loc.line_indent) } else { None }; |
547 | let patch: String = format_patch(desired_indent, patch:actual); |
548 | self.patchwork.patch(loc.literal_range, &patch); |
549 | fs::write(&self.path, &self.patchwork.text).unwrap() |
550 | } |
551 | } |
552 | |
553 | #[derive (Debug)] |
554 | struct Location { |
555 | line_indent: usize, |
556 | |
557 | /// The byte range of the argument to `expect!`, including the inner `[]` if it exists. |
558 | literal_range: Range<usize>, |
559 | } |
560 | |
561 | #[derive (Debug)] |
562 | struct Patchwork { |
563 | text: String, |
564 | indels: Vec<(Range<usize>, usize)>, |
565 | } |
566 | |
567 | impl Patchwork { |
568 | fn new(text: String) -> Patchwork { |
569 | Patchwork { text, indels: Vec::new() } |
570 | } |
571 | fn patch(&mut self, mut range: Range<usize>, patch: &str) { |
572 | self.indels.push((range.clone(), patch.len())); |
573 | self.indels.sort_by_key(|(delete: &Range, _insert: &usize)| delete.start); |
574 | |
575 | let (delete: usize, insert: usize) = self |
576 | .indels |
577 | .iter() |
578 | .take_while(|(delete, _)| delete.start < range.start) |
579 | .map(|(delete, insert)| (delete.end - delete.start, insert)) |
580 | .fold((0usize, 0usize), |(x1: usize, y1: usize), (x2: usize, y2: &usize)| (x1 + x2, y1 + y2)); |
581 | |
582 | for pos: &mut &mut usize in &mut [&mut range.start, &mut range.end] { |
583 | **pos -= delete; |
584 | **pos += insert; |
585 | } |
586 | |
587 | self.text.replace_range(range, &patch); |
588 | } |
589 | } |
590 | |
591 | fn lit_kind_for_patch(patch: &str) -> StrLitKind { |
592 | let has_dquote: bool = patch.chars().any(|c: char| c == '"' ); |
593 | if !has_dquote { |
594 | let has_bslash_or_newline: bool = patch.chars().any(|c: char| matches!(c, ' \\' | ' \n' )); |
595 | return if has_bslash_or_newline { StrLitKind::Raw(1) } else { StrLitKind::Normal }; |
596 | } |
597 | |
598 | // Find the maximum number of hashes that follow a double quote in the string. |
599 | // We need to use one more than that to delimit the string. |
600 | let leading_hashes: impl Fn(&str) -> usize = |s: &str| s.chars().take_while(|&c: char| c == '#' ).count(); |
601 | let max_hashes: usize = patch.split('"' ).map(leading_hashes).max().unwrap(); |
602 | StrLitKind::Raw(max_hashes + 1) |
603 | } |
604 | |
605 | fn format_patch(desired_indent: Option<usize>, patch: &str) -> String { |
606 | let lit_kind = lit_kind_for_patch(patch); |
607 | let indent = desired_indent.map(|it| " " .repeat(it)); |
608 | let is_multiline = patch.contains(' \n' ); |
609 | |
610 | let mut buf = String::new(); |
611 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
612 | buf.push('[' ); |
613 | } |
614 | lit_kind.write_start(&mut buf).unwrap(); |
615 | if is_multiline { |
616 | buf.push(' \n' ); |
617 | } |
618 | let mut final_newline = false; |
619 | for line in lines_with_ends(patch) { |
620 | if is_multiline && !line.trim().is_empty() { |
621 | if let Some(indent) = &indent { |
622 | buf.push_str(indent); |
623 | buf.push_str(" " ); |
624 | } |
625 | } |
626 | buf.push_str(line); |
627 | final_newline = line.ends_with(' \n' ); |
628 | } |
629 | if final_newline { |
630 | if let Some(indent) = &indent { |
631 | buf.push_str(indent); |
632 | } |
633 | } |
634 | lit_kind.write_end(&mut buf).unwrap(); |
635 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
636 | buf.push(']' ); |
637 | } |
638 | buf |
639 | } |
640 | |
641 | fn to_abs_ws_path(path: &Path) -> PathBuf { |
642 | if path.is_absolute() { |
643 | return path.to_owned(); |
644 | } |
645 | |
646 | static WORKSPACE_ROOT: OnceCell<PathBuf> = OnceCell::new(); |
647 | WORKSPACE_ROOT |
648 | .get_or_try_init(|| { |
649 | // Until https://github.com/rust-lang/cargo/issues/3946 is resolved, this |
650 | // is set with a hack like https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 |
651 | if let Ok(workspace_root) = env::var("CARGO_WORKSPACE_DIR" ) { |
652 | return Ok(workspace_root.into()); |
653 | } |
654 | |
655 | // If a hack isn't used, we use a heuristic to find the "top-level" workspace. |
656 | // This fails in some cases, see https://github.com/rust-analyzer/expect-test/issues/33 |
657 | let my_manifest = env::var("CARGO_MANIFEST_DIR" )?; |
658 | let workspace_root = Path::new(&my_manifest) |
659 | .ancestors() |
660 | .filter(|it| it.join("Cargo.toml" ).exists()) |
661 | .last() |
662 | .unwrap() |
663 | .to_path_buf(); |
664 | |
665 | Ok(workspace_root) |
666 | }) |
667 | .unwrap_or_else(|_: env::VarError| { |
668 | panic!("No CARGO_MANIFEST_DIR env var and the path is relative: {}" , path.display()) |
669 | }) |
670 | .join(path) |
671 | } |
672 | |
673 | fn trim_indent(mut text: &str) -> String { |
674 | if text.starts_with(' \n' ) { |
675 | text = &text[1..]; |
676 | } |
677 | let indent: usize = text |
678 | .lines() |
679 | .filter(|it| !it.trim().is_empty()) |
680 | .map(|it| it.len() - it.trim_start().len()) |
681 | .min() |
682 | .unwrap_or(default:0); |
683 | |
684 | lines_with_endsimpl Iterator (text) |
685 | .map( |
686 | |line: &str| { |
687 | if line.len() <= indent { |
688 | line.trim_start_matches(' ' ) |
689 | } else { |
690 | &line[indent..] |
691 | } |
692 | }, |
693 | ) |
694 | .collect() |
695 | } |
696 | |
697 | fn lines_with_ends(text: &str) -> LinesWithEnds { |
698 | LinesWithEnds { text } |
699 | } |
700 | |
701 | struct LinesWithEnds<'a> { |
702 | text: &'a str, |
703 | } |
704 | |
705 | impl<'a> Iterator for LinesWithEnds<'a> { |
706 | type Item = &'a str; |
707 | fn next(&mut self) -> Option<&'a str> { |
708 | if self.text.is_empty() { |
709 | return None; |
710 | } |
711 | let idx: usize = self.text.find(' \n' ).map_or(self.text.len(), |it: usize| it + 1); |
712 | let (res: &str, next: &str) = self.text.split_at(mid:idx); |
713 | self.text = next; |
714 | Some(res) |
715 | } |
716 | } |
717 | |
718 | fn format_chunks(chunks: Vec<dissimilar::Chunk>) -> String { |
719 | let mut buf: String = String::new(); |
720 | for chunk: Chunk<'_> in chunks { |
721 | let formatted: String = match chunk { |
722 | dissimilar::Chunk::Equal(text: &str) => text.into(), |
723 | dissimilar::Chunk::Delete(text: &str) => format!(" \x1b[4m \x1b[31m {}\x1b[0m" , text), |
724 | dissimilar::Chunk::Insert(text: &str) => format!(" \x1b[4m \x1b[32m {}\x1b[0m" , text), |
725 | }; |
726 | buf.push_str(&formatted); |
727 | } |
728 | buf |
729 | } |
730 | |
731 | #[cfg (test)] |
732 | mod tests { |
733 | use super::*; |
734 | |
735 | #[test ] |
736 | fn test_trivial_assert() { |
737 | expect!["5" ].assert_eq("5" ); |
738 | } |
739 | |
740 | #[test ] |
741 | fn test_format_patch() { |
742 | let patch = format_patch(None, "hello \nworld \n" ); |
743 | expect![[r##" |
744 | [r#" |
745 | hello |
746 | world |
747 | "#]"## ]] |
748 | .assert_eq(&patch); |
749 | |
750 | let patch = format_patch(None, r"hello\tworld" ); |
751 | expect![[r##"[r#"hello\tworld"#]"## ]].assert_eq(&patch); |
752 | |
753 | let patch = format_patch(None, "{ \"foo \": 42}" ); |
754 | expect![[r##"[r#"{"foo": 42}"#]"## ]].assert_eq(&patch); |
755 | |
756 | let patch = format_patch(Some(0), "hello \nworld \n" ); |
757 | expect![[r##" |
758 | [r#" |
759 | hello |
760 | world |
761 | "#]"## ]] |
762 | .assert_eq(&patch); |
763 | |
764 | let patch = format_patch(Some(4), "single line" ); |
765 | expect![[r#""single line""# ]].assert_eq(&patch); |
766 | } |
767 | |
768 | #[test ] |
769 | fn test_patchwork() { |
770 | let mut patchwork = Patchwork::new("one two three" .to_string()); |
771 | patchwork.patch(4..7, "zwei" ); |
772 | patchwork.patch(0..3, "один" ); |
773 | patchwork.patch(8..13, "3" ); |
774 | expect![[r#" |
775 | Patchwork { |
776 | text: "один zwei 3", |
777 | indels: [ |
778 | ( |
779 | 0..3, |
780 | 8, |
781 | ), |
782 | ( |
783 | 4..7, |
784 | 4, |
785 | ), |
786 | ( |
787 | 8..13, |
788 | 1, |
789 | ), |
790 | ], |
791 | } |
792 | "# ]] |
793 | .assert_debug_eq(&patchwork); |
794 | } |
795 | |
796 | #[test ] |
797 | fn test_expect_file() { |
798 | expect_file!["./lib.rs" ].assert_eq(include_str!("./lib.rs" )) |
799 | } |
800 | |
801 | #[test ] |
802 | fn smoke_test_indent() { |
803 | fn check_indented(input: &str, mut expect: Expect) { |
804 | expect.indent(true); |
805 | expect.assert_eq(input); |
806 | } |
807 | fn check_not_indented(input: &str, mut expect: Expect) { |
808 | expect.indent(false); |
809 | expect.assert_eq(input); |
810 | } |
811 | |
812 | check_indented( |
813 | "\ |
814 | line1 |
815 | line2 |
816 | " , |
817 | expect![[r#" |
818 | line1 |
819 | line2 |
820 | "# ]], |
821 | ); |
822 | |
823 | check_not_indented( |
824 | "\ |
825 | line1 |
826 | line2 |
827 | " , |
828 | expect![[r#" |
829 | line1 |
830 | line2 |
831 | "# ]], |
832 | ); |
833 | } |
834 | |
835 | #[test ] |
836 | fn test_locate() { |
837 | macro_rules! check_locate { |
838 | ($( [[$s:literal]] ),* $(,)?) => {$({ |
839 | let lit = stringify!($s); |
840 | let with_trailer = format!("{} \t]] \n" , lit); |
841 | assert_eq!(locate_end(&with_trailer), Some(lit.len())); |
842 | })*}; |
843 | } |
844 | |
845 | // Check that we handle string literals containing "]]" correctly. |
846 | check_locate!( |
847 | [[r#"{ arr: [[1, 2], [3, 4]], other: "foo" } "# ]], |
848 | [["]]" ]], |
849 | [[" \"]]" ]], |
850 | [[r#""]]"# ]], |
851 | ); |
852 | |
853 | // Check `expect![[ ]]` as well. |
854 | assert_eq!(locate_end("]]" ), Some(0)); |
855 | } |
856 | |
857 | #[test ] |
858 | fn test_find_str_lit_len() { |
859 | macro_rules! check_str_lit_len { |
860 | ($( $s:literal ),* $(,)?) => {$({ |
861 | let lit = stringify!($s); |
862 | assert_eq!(find_str_lit_len(lit), Some(lit.len())); |
863 | })*} |
864 | } |
865 | |
866 | check_str_lit_len![ |
867 | r##"foa\""#"## , |
868 | r##" |
869 | |
870 | asdf][]]""""# |
871 | "## , |
872 | "" , |
873 | " \"" , |
874 | " \"\"" , |
875 | "# \"# \"#" , |
876 | ]; |
877 | } |
878 | } |
879 | |