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.
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.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)]
466struct Runtime {
467 help_printed: bool,
468 per_file: HashMap<&'static str, FileRuntime>,
469}
470static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
471
472impl 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
531struct FileRuntime {
532 path: PathBuf,
533 original_text: String,
534 patchwork: Patchwork,
535}
536
537impl 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)]
554struct 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)]
562struct Patchwork {
563 text: String,
564 indels: Vec<(Range<usize>, usize)>,
565}
566
567impl 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
591fn 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
605fn 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
641fn 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
673fn 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
697fn lines_with_ends(text: &str) -> LinesWithEnds {
698 LinesWithEnds { text }
699}
700
701struct LinesWithEnds<'a> {
702 text: &'a str,
703}
704
705impl<'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
718fn 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)]
732mod 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 "\
814line1
815 line2
816",
817 expect![[r#"
818 line1
819 line2
820 "#]],
821 );
822
823 check_not_indented(
824 "\
825line1
826 line2
827",
828 expect![[r#"
829line1
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