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.45.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.read(); |
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 | fn read(&self) -> String { |
449 | fs::read_to_string(self.abs_path()).unwrap_or_default().replace(" \r\n" , " \n" ) |
450 | } |
451 | fn write(&self, contents: &str) { |
452 | fs::write(self.abs_path(), contents).unwrap() |
453 | } |
454 | fn abs_path(&self) -> PathBuf { |
455 | if self.path.is_absolute() { |
456 | self.path.to_owned() |
457 | } else { |
458 | let dir = Path::new(self.position).parent().unwrap(); |
459 | to_abs_ws_path(&dir.join(&self.path)) |
460 | } |
461 | } |
462 | } |
463 | |
464 | #[derive (Default)] |
465 | struct Runtime { |
466 | help_printed: bool, |
467 | per_file: HashMap<&'static str, FileRuntime>, |
468 | } |
469 | static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default); |
470 | |
471 | impl Runtime { |
472 | fn fail_expect(expect: &Expect, expected: &str, actual: &str) { |
473 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
474 | if update_expect() { |
475 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.position); |
476 | rt.per_file |
477 | .entry(expect.position.file) |
478 | .or_insert_with(|| FileRuntime::new(expect)) |
479 | .update(expect, actual); |
480 | return; |
481 | } |
482 | rt.panic(expect.position.to_string(), expected, actual); |
483 | } |
484 | fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) { |
485 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
486 | if update_expect() { |
487 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.path.display()); |
488 | expect.write(actual); |
489 | return; |
490 | } |
491 | rt.panic(expect.path.display().to_string(), expected, actual); |
492 | } |
493 | fn panic(&mut self, position: String, expected: &str, actual: &str) { |
494 | let print_help = !mem::replace(&mut self.help_printed, true); |
495 | let help = if print_help { HELP } else { "" }; |
496 | |
497 | let diff = dissimilar::diff(expected, actual); |
498 | |
499 | println!( |
500 | " \n |
501 | \x1b[1m \x1b[91merror \x1b[97m: expect test failed \x1b[0m |
502 | \x1b[1m \x1b[34m--> \x1b[0m {} |
503 | {} |
504 | \x1b[1mExpect \x1b[0m: |
505 | ---- |
506 | {} |
507 | ---- |
508 | |
509 | \x1b[1mActual \x1b[0m: |
510 | ---- |
511 | {} |
512 | ---- |
513 | |
514 | \x1b[1mDiff \x1b[0m: |
515 | ---- |
516 | {} |
517 | ---- |
518 | " , |
519 | position, |
520 | help, |
521 | expected, |
522 | actual, |
523 | format_chunks(diff) |
524 | ); |
525 | // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise. |
526 | panic::resume_unwind(Box::new(())); |
527 | } |
528 | } |
529 | |
530 | struct FileRuntime { |
531 | path: PathBuf, |
532 | original_text: String, |
533 | patchwork: Patchwork, |
534 | } |
535 | |
536 | impl FileRuntime { |
537 | fn new(expect: &Expect) -> FileRuntime { |
538 | let path: PathBuf = to_abs_ws_path(Path::new(expect.position.file)); |
539 | let original_text: String = fs::read_to_string(&path).unwrap(); |
540 | let patchwork: Patchwork = Patchwork::new(original_text.clone()); |
541 | FileRuntime { path, original_text, patchwork } |
542 | } |
543 | fn update(&mut self, expect: &Expect, actual: &str) { |
544 | let loc: Location = expect.locate(&self.original_text); |
545 | let desired_indent: Option = if expect.indent { Some(loc.line_indent) } else { None }; |
546 | let patch: String = format_patch(desired_indent, patch:actual); |
547 | self.patchwork.patch(loc.literal_range, &patch); |
548 | fs::write(&self.path, &self.patchwork.text).unwrap() |
549 | } |
550 | } |
551 | |
552 | #[derive (Debug)] |
553 | struct Location { |
554 | line_indent: usize, |
555 | |
556 | /// The byte range of the argument to `expect!`, including the inner `[]` if it exists. |
557 | literal_range: Range<usize>, |
558 | } |
559 | |
560 | #[derive (Debug)] |
561 | struct Patchwork { |
562 | text: String, |
563 | indels: Vec<(Range<usize>, usize)>, |
564 | } |
565 | |
566 | impl Patchwork { |
567 | fn new(text: String) -> Patchwork { |
568 | Patchwork { text, indels: Vec::new() } |
569 | } |
570 | fn patch(&mut self, mut range: Range<usize>, patch: &str) { |
571 | self.indels.push((range.clone(), patch.len())); |
572 | self.indels.sort_by_key(|(delete: &Range, _insert: &usize)| delete.start); |
573 | |
574 | let (delete: usize, insert: usize) = self |
575 | .indels |
576 | .iter() |
577 | .take_while(|(delete, _)| delete.start < range.start) |
578 | .map(|(delete, insert)| (delete.end - delete.start, insert)) |
579 | .fold((0usize, 0usize), |(x1: usize, y1: usize), (x2: usize, y2: &usize)| (x1 + x2, y1 + y2)); |
580 | |
581 | for pos: &mut &mut usize in &mut [&mut range.start, &mut range.end] { |
582 | **pos -= delete; |
583 | **pos += insert; |
584 | } |
585 | |
586 | self.text.replace_range(range, &patch); |
587 | } |
588 | } |
589 | |
590 | fn lit_kind_for_patch(patch: &str) -> StrLitKind { |
591 | let has_dquote: bool = patch.chars().any(|c: char| c == '"' ); |
592 | if !has_dquote { |
593 | let has_bslash_or_newline: bool = patch.chars().any(|c: char| matches!(c, ' \\' | ' \n' )); |
594 | return if has_bslash_or_newline { StrLitKind::Raw(1) } else { StrLitKind::Normal }; |
595 | } |
596 | |
597 | // Find the maximum number of hashes that follow a double quote in the string. |
598 | // We need to use one more than that to delimit the string. |
599 | let leading_hashes: impl Fn(&str) -> usize = |s: &str| s.chars().take_while(|&c: char| c == '#' ).count(); |
600 | let max_hashes: usize = patch.split('"' ).map(leading_hashes).max().unwrap(); |
601 | StrLitKind::Raw(max_hashes + 1) |
602 | } |
603 | |
604 | fn format_patch(desired_indent: Option<usize>, patch: &str) -> String { |
605 | let lit_kind = lit_kind_for_patch(patch); |
606 | let indent = desired_indent.map(|it| " " .repeat(it)); |
607 | let is_multiline = patch.contains(' \n' ); |
608 | |
609 | let mut buf = String::new(); |
610 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
611 | buf.push('[' ); |
612 | } |
613 | lit_kind.write_start(&mut buf).unwrap(); |
614 | if is_multiline { |
615 | buf.push(' \n' ); |
616 | } |
617 | let mut final_newline = false; |
618 | for line in lines_with_ends(patch) { |
619 | if is_multiline && !line.trim().is_empty() { |
620 | if let Some(indent) = &indent { |
621 | buf.push_str(indent); |
622 | buf.push_str(" " ); |
623 | } |
624 | } |
625 | buf.push_str(line); |
626 | final_newline = line.ends_with(' \n' ); |
627 | } |
628 | if final_newline { |
629 | if let Some(indent) = &indent { |
630 | buf.push_str(indent); |
631 | } |
632 | } |
633 | lit_kind.write_end(&mut buf).unwrap(); |
634 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
635 | buf.push(']' ); |
636 | } |
637 | buf |
638 | } |
639 | |
640 | fn to_abs_ws_path(path: &Path) -> PathBuf { |
641 | if path.is_absolute() { |
642 | return path.to_owned(); |
643 | } |
644 | |
645 | static WORKSPACE_ROOT: OnceCell<PathBuf> = OnceCell::new(); |
646 | WORKSPACE_ROOT |
647 | .get_or_try_init(|| { |
648 | // Until https://github.com/rust-lang/cargo/issues/3946 is resolved, this |
649 | // is set with a hack like https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 |
650 | if let Ok(workspace_root) = env::var("CARGO_WORKSPACE_DIR" ) { |
651 | return Ok(workspace_root.into()); |
652 | } |
653 | |
654 | // If a hack isn't used, we use a heuristic to find the "top-level" workspace. |
655 | // This fails in some cases, see https://github.com/rust-analyzer/expect-test/issues/33 |
656 | let my_manifest = env::var("CARGO_MANIFEST_DIR" )?; |
657 | let workspace_root = Path::new(&my_manifest) |
658 | .ancestors() |
659 | .filter(|it| it.join("Cargo.toml" ).exists()) |
660 | .last() |
661 | .unwrap() |
662 | .to_path_buf(); |
663 | |
664 | Ok(workspace_root) |
665 | }) |
666 | .unwrap_or_else(|_: env::VarError| { |
667 | panic!("No CARGO_MANIFEST_DIR env var and the path is relative: {}" , path.display()) |
668 | }) |
669 | .join(path) |
670 | } |
671 | |
672 | fn trim_indent(mut text: &str) -> String { |
673 | if text.starts_with(' \n' ) { |
674 | text = &text[1..]; |
675 | } |
676 | let indent: usize = text |
677 | .lines() |
678 | .filter(|it| !it.trim().is_empty()) |
679 | .map(|it| it.len() - it.trim_start().len()) |
680 | .min() |
681 | .unwrap_or(default:0); |
682 | |
683 | lines_with_endsimpl Iterator (text) |
684 | .map( |
685 | |line: &str| { |
686 | if line.len() <= indent { |
687 | line.trim_start_matches(' ' ) |
688 | } else { |
689 | &line[indent..] |
690 | } |
691 | }, |
692 | ) |
693 | .collect() |
694 | } |
695 | |
696 | fn lines_with_ends(text: &str) -> LinesWithEnds { |
697 | LinesWithEnds { text } |
698 | } |
699 | |
700 | struct LinesWithEnds<'a> { |
701 | text: &'a str, |
702 | } |
703 | |
704 | impl<'a> Iterator for LinesWithEnds<'a> { |
705 | type Item = &'a str; |
706 | fn next(&mut self) -> Option<&'a str> { |
707 | if self.text.is_empty() { |
708 | return None; |
709 | } |
710 | let idx: usize = self.text.find(' \n' ).map_or(self.text.len(), |it: usize| it + 1); |
711 | let (res: &str, next: &str) = self.text.split_at(mid:idx); |
712 | self.text = next; |
713 | Some(res) |
714 | } |
715 | } |
716 | |
717 | fn format_chunks(chunks: Vec<dissimilar::Chunk>) -> String { |
718 | let mut buf: String = String::new(); |
719 | for chunk: Chunk<'_> in chunks { |
720 | let formatted: String = match chunk { |
721 | dissimilar::Chunk::Equal(text: &str) => text.into(), |
722 | dissimilar::Chunk::Delete(text: &str) => format!(" \x1b[4m \x1b[31m {}\x1b[0m" , text), |
723 | dissimilar::Chunk::Insert(text: &str) => format!(" \x1b[4m \x1b[32m {}\x1b[0m" , text), |
724 | }; |
725 | buf.push_str(&formatted); |
726 | } |
727 | buf |
728 | } |
729 | |
730 | #[cfg (test)] |
731 | mod tests { |
732 | use super::*; |
733 | |
734 | #[test ] |
735 | fn test_trivial_assert() { |
736 | expect!["5" ].assert_eq("5" ); |
737 | } |
738 | |
739 | #[test ] |
740 | fn test_format_patch() { |
741 | let patch = format_patch(None, "hello \nworld \n" ); |
742 | expect![[r##" |
743 | [r#" |
744 | hello |
745 | world |
746 | "#]"## ]] |
747 | .assert_eq(&patch); |
748 | |
749 | let patch = format_patch(None, r"hello\tworld" ); |
750 | expect![[r##"[r#"hello\tworld"#]"## ]].assert_eq(&patch); |
751 | |
752 | let patch = format_patch(None, "{ \"foo \": 42}" ); |
753 | expect![[r##"[r#"{"foo": 42}"#]"## ]].assert_eq(&patch); |
754 | |
755 | let patch = format_patch(Some(0), "hello \nworld \n" ); |
756 | expect![[r##" |
757 | [r#" |
758 | hello |
759 | world |
760 | "#]"## ]] |
761 | .assert_eq(&patch); |
762 | |
763 | let patch = format_patch(Some(4), "single line" ); |
764 | expect![[r#""single line""# ]].assert_eq(&patch); |
765 | } |
766 | |
767 | #[test ] |
768 | fn test_patchwork() { |
769 | let mut patchwork = Patchwork::new("one two three" .to_string()); |
770 | patchwork.patch(4..7, "zwei" ); |
771 | patchwork.patch(0..3, "один" ); |
772 | patchwork.patch(8..13, "3" ); |
773 | expect![[r#" |
774 | Patchwork { |
775 | text: "один zwei 3", |
776 | indels: [ |
777 | ( |
778 | 0..3, |
779 | 8, |
780 | ), |
781 | ( |
782 | 4..7, |
783 | 4, |
784 | ), |
785 | ( |
786 | 8..13, |
787 | 1, |
788 | ), |
789 | ], |
790 | } |
791 | "# ]] |
792 | .assert_debug_eq(&patchwork); |
793 | } |
794 | |
795 | #[test ] |
796 | fn test_expect_file() { |
797 | expect_file!["./lib.rs" ].assert_eq(include_str!("./lib.rs" )) |
798 | } |
799 | |
800 | #[test ] |
801 | fn smoke_test_indent() { |
802 | fn check_indented(input: &str, mut expect: Expect) { |
803 | expect.indent(true); |
804 | expect.assert_eq(input); |
805 | } |
806 | fn check_not_indented(input: &str, mut expect: Expect) { |
807 | expect.indent(false); |
808 | expect.assert_eq(input); |
809 | } |
810 | |
811 | check_indented( |
812 | "\ |
813 | line1 |
814 | line2 |
815 | " , |
816 | expect![[r#" |
817 | line1 |
818 | line2 |
819 | "# ]], |
820 | ); |
821 | |
822 | check_not_indented( |
823 | "\ |
824 | line1 |
825 | line2 |
826 | " , |
827 | expect![[r#" |
828 | line1 |
829 | line2 |
830 | "# ]], |
831 | ); |
832 | } |
833 | |
834 | #[test ] |
835 | fn test_locate() { |
836 | macro_rules! check_locate { |
837 | ($( [[$s:literal]] ),* $(,)?) => {$({ |
838 | let lit = stringify!($s); |
839 | let with_trailer = format!("{} \t]] \n" , lit); |
840 | assert_eq!(locate_end(&with_trailer), Some(lit.len())); |
841 | })*}; |
842 | } |
843 | |
844 | // Check that we handle string literals containing "]]" correctly. |
845 | check_locate!( |
846 | [[r#"{ arr: [[1, 2], [3, 4]], other: "foo" } "# ]], |
847 | [["]]" ]], |
848 | [[" \"]]" ]], |
849 | [[r#""]]"# ]], |
850 | ); |
851 | |
852 | // Check `expect![[ ]]` as well. |
853 | assert_eq!(locate_end("]]" ), Some(0)); |
854 | } |
855 | |
856 | #[test ] |
857 | fn test_find_str_lit_len() { |
858 | macro_rules! check_str_lit_len { |
859 | ($( $s:literal ),* $(,)?) => {$({ |
860 | let lit = stringify!($s); |
861 | assert_eq!(find_str_lit_len(lit), Some(lit.len())); |
862 | })*} |
863 | } |
864 | |
865 | check_str_lit_len![ |
866 | r##"foa\""#"## , |
867 | r##" |
868 | |
869 | asdf][]]""""# |
870 | "## , |
871 | "" , |
872 | " \"" , |
873 | " \"\"" , |
874 | "# \"# \"#" , |
875 | ]; |
876 | } |
877 | } |
878 | |