expect_test/
lib.rs

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("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 if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`"),
358
359        // expect![[]]
360        '[' => {
361            let str_start_to_eof = arg_start_to_eof[1..].trim_start();
362            let str_len = find_str_lit_len(str_start_to_eof)?;
363            let str_end_to_eof = &str_start_to_eof[str_len..];
364            let closing_brace_offset = 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(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 = to_abs_ws_path(Path::new(expect.position.file));
540        let original_text = fs::read_to_string(&path).unwrap();
541        let 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 = expect.locate(&self.original_text);
546        let desired_indent = if expect.indent { Some(loc.line_indent) } else { None };
547        let patch = format_patch(desired_indent, 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, _insert)| delete.start);
574
575        let (delete, insert) = 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, y1), (x2, y2)| (x1 + x2, y1 + y2));
581
582        for pos 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 = patch.chars().any(|c| c == '"');
593    if !has_dquote {
594        let has_bslash_or_newline = patch.chars().any(|c| 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 = |s: &str| s.chars().take_while(|&c| c == '#').count();
601    let max_hashes = 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 = text
678        .lines()
679        .filter(|it| !it.trim().is_empty())
680        .map(|it| it.len() - it.trim_start().len())
681        .min()
682        .unwrap_or(0);
683
684    lines_with_ends(text)
685        .map(
686            |line| {
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 = self.text.find('\n').map_or(self.text.len(), |it| it + 1);
712        let (res, next) = self.text.split_at(idx);
713        self.text = next;
714        Some(res)
715    }
716}
717
718fn format_chunks(chunks: Vec<dissimilar::Chunk>) -> String {
719    let mut buf = String::new();
720    for chunk in chunks {
721        let formatted = match chunk {
722            dissimilar::Chunk::Equal(text) => text.into(),
723            dissimilar::Chunk::Delete(text) => format!("\x1b[4m\x1b[31m{}\x1b[0m", text),
724            dissimilar::Chunk::Insert(text) => 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}