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.
142use 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
152use once_cell::sync::{Lazy, OnceCell};
153
154const HELP: &str = "
155You can update all `expect!` tests by running:
156
157 env UPDATE_EXPECT=1 cargo test
158
159To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
160";
161
162fn 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]
178macro_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]
200macro_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)]
209pub 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)]
220pub 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)]
229pub 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
238impl 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)]
245enum StrLitKind {
246 Normal,
247 Raw(usize),
248}
249
250impl 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
278impl 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
355fn 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).
378fn 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
434impl 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)]
465struct Runtime {
466 help_printed: bool,
467 per_file: HashMap<&'static str, FileRuntime>,
468}
469static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
470
471impl 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
530struct FileRuntime {
531 path: PathBuf,
532 original_text: String,
533 patchwork: Patchwork,
534}
535
536impl 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)]
553struct 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)]
561struct Patchwork {
562 text: String,
563 indels: Vec<(Range<usize>, usize)>,
564}
565
566impl 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
590fn 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
604fn 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
640fn 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
672fn 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
696fn lines_with_ends(text: &str) -> LinesWithEnds {
697 LinesWithEnds { text }
698}
699
700struct LinesWithEnds<'a> {
701 text: &'a str,
702}
703
704impl<'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
717fn 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)]
731mod 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 "\
813line1
814 line2
815",
816 expect![[r#"
817 line1
818 line2
819 "#]],
820 );
821
822 check_not_indented(
823 "\
824line1
825 line2
826",
827 expect![[r#"
828line1
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