| //! Minimalistic snapshot testing for Rust. |
| //! |
| //! # Introduction |
| //! |
| //! `expect_test` is a small addition over plain `assert_eq!` testing approach, |
| //! which allows to automatically update tests results. |
| //! |
| //! The core of the library is the `expect!` macro. It can be though of as a |
| //! super-charged string literal, which can update itself. |
| //! |
| //! Let's see an example: |
| //! |
| //! ```no_run |
| //! use expect_test::expect; |
| //! |
| //! let actual = 2 + 2; |
| //! let expected = expect!["5"]; // or expect![["5"]] |
| //! expected.assert_eq(&actual.to_string()) |
| //! ``` |
| //! |
| //! Running this code will produce a test failure, as `"5"` is indeed not equal |
| //! to `"4"`. Running the test with `UPDATE_EXPECT=1` env variable however would |
| //! "magically" update the code to: |
| //! |
| //! ```no_run |
| //! # use expect_test::expect; |
| //! let actual = 2 + 2; |
| //! let expected = expect!["4"]; |
| //! expected.assert_eq(&actual.to_string()) |
| //! ``` |
| //! |
| //! This becomes very useful when you have a lot of tests with verbose and |
| //! potentially changing expected output. |
| //! |
| //! Under the hood, the `expect!` macro uses `file!`, `line!` and `column!` to |
| //! record source position at compile time. At runtime, this position is used |
| //! to patch the file in-place, if `UPDATE_EXPECT` is set. |
| //! |
| //! # Guide |
| //! |
| //! `expect!` returns an instance of `Expect` struct, which holds position |
| //! information and a string literal. Use `Expect::assert_eq` for string |
| //! comparison. Use `Expect::assert_debug_eq` for verbose debug comparison. Note |
| //! that leading indentation is automatically removed. |
| //! |
| //! ``` |
| //! use expect_test::expect; |
| //! |
| //! #[derive(Debug)] |
| //! struct Foo { |
| //! value: i32, |
| //! } |
| //! |
| //! let actual = Foo { value: 92 }; |
| //! let expected = expect![[" |
| //! Foo { |
| //! value: 92, |
| //! } |
| //! "]]; |
| //! expected.assert_debug_eq(&actual); |
| //! ``` |
| //! |
| //! Be careful with `assert_debug_eq` - in general, stability of the debug |
| //! representation is not guaranteed. However, even if it changes, you can |
| //! quickly update all the tests by running the test suite with `UPDATE_EXPECT` |
| //! environmental variable set. |
| //! |
| //! If the expected data is too verbose to include inline, you can store it in |
| //! an external file using the `expect_file!` macro: |
| //! |
| //! ```no_run |
| //! use expect_test::expect_file; |
| //! |
| //! let actual = 42; |
| //! let expected = expect_file!["./the-answer.txt"]; |
| //! expected.assert_eq(&actual.to_string()); |
| //! ``` |
| //! |
| //! File path is relative to the current file. |
| //! |
| //! # Suggested Workflows |
| //! |
| //! I like to use data-driven tests with `expect_test`. I usually define a |
| //! single driver function `check` and then call it from individual tests: |
| //! |
| //! ``` |
| //! use expect_test::{expect, Expect}; |
| //! |
| //! fn check(actual: i32, expect: Expect) { |
| //! let actual = actual.to_string(); |
| //! expect.assert_eq(&actual); |
| //! } |
| //! |
| //! #[test] |
| //! fn test_addition() { |
| //! check(90 + 2, expect![["92"]]); |
| //! } |
| //! |
| //! #[test] |
| //! fn test_multiplication() { |
| //! check(46 * 2, expect![["92"]]); |
| //! } |
| //! ``` |
| //! |
| //! Each test's body is a single call to `check`. All the variation in tests |
| //! comes from the input data. |
| //! |
| //! When writing a new test, I usually copy-paste an old one, leave the `expect` |
| //! blank and use `UPDATE_EXPECT` to fill the value for me: |
| //! |
| //! ``` |
| //! # use expect_test::{expect, Expect}; |
| //! # fn check(_: i32, _: Expect) {} |
| //! #[test] |
| //! fn test_division() { |
| //! check(92 / 2, expect![[]]) |
| //! } |
| //! ``` |
| //! |
| //! See |
| //! <https://blog.janestreet.com/using-ascii-waveforms-to-test-hardware-designs/> |
| //! for a cool example of snapshot testing in the wild! |
| //! |
| //! # Alternatives |
| //! |
| //! * [insta](https://crates.io/crates/insta) - a more feature full snapshot |
| //! testing library. |
| //! * [k9](https://crates.io/crates/k9) - a testing library which includes |
| //! support for snapshot testing among other things. |
| //! |
| //! # Maintenance status |
| //! |
| //! The main customer of this library is rust-analyzer. The library is stable, |
| //! it is planned to not release any major versions past 1.0. |
| //! |
| //! ## Minimal Supported Rust Version |
| //! |
| //! This crate's minimum supported `rustc` version is `1.60.0`. MSRV is updated |
| //! conservatively, supporting roughly 10 minor versions of `rustc`. MSRV bump |
| //! is not considered semver breaking, but will require at least minor version |
| //! bump. |
| use std::{ |
| collections::HashMap, |
| convert::TryInto, |
| env, fmt, fs, mem, |
| ops::Range, |
| panic, |
| path::{Path, PathBuf}, |
| sync::Mutex, |
| }; |
| |
| use once_cell::sync::{Lazy, OnceCell}; |
| |
| const HELP: &str = " |
| You can update all `expect!` tests by running: |
| |
| env UPDATE_EXPECT=1 cargo test |
| |
| To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer. |
| "; |
| |
| fn update_expect() -> bool { |
| env::var("UPDATE_EXPECT").is_ok() |
| } |
| |
| /// Creates an instance of `Expect` from string literal: |
| /// |
| /// ``` |
| /// # use expect_test::expect; |
| /// expect![[" |
| /// Foo { value: 92 } |
| /// "]]; |
| /// expect![r#"{"Foo": 92}"#]; |
| /// ``` |
| /// |
| /// Leading indentation is stripped. |
| #[macro_export] |
| macro_rules! expect { |
| [$data:literal] => { $crate::expect![[$data]] }; |
| [[$data:literal]] => {$crate::Expect { |
| position: $crate::Position { |
| file: file!(), |
| line: line!(), |
| column: column!(), |
| }, |
| data: $data, |
| indent: true, |
| }}; |
| [] => { $crate::expect![[""]] }; |
| [[]] => { $crate::expect![[""]] }; |
| } |
| |
| /// Creates an instance of `ExpectFile` from relative or absolute path: |
| /// |
| /// ``` |
| /// # use expect_test::expect_file; |
| /// expect_file!["./test_data/bar.html"]; |
| /// ``` |
| #[macro_export] |
| macro_rules! expect_file { |
| [$path:expr] => {$crate::ExpectFile { |
| path: std::path::PathBuf::from($path), |
| position: file!(), |
| }}; |
| } |
| |
| /// Self-updating string literal. |
| #[derive(Debug)] |
| pub struct Expect { |
| #[doc(hidden)] |
| pub position: Position, |
| #[doc(hidden)] |
| pub data: &'static str, |
| #[doc(hidden)] |
| pub indent: bool, |
| } |
| |
| /// Self-updating file. |
| #[derive(Debug)] |
| pub struct ExpectFile { |
| #[doc(hidden)] |
| pub path: PathBuf, |
| #[doc(hidden)] |
| pub position: &'static str, |
| } |
| |
| /// Position of original `expect!` in the source file. |
| #[derive(Debug)] |
| pub struct Position { |
| #[doc(hidden)] |
| pub file: &'static str, |
| #[doc(hidden)] |
| pub line: u32, |
| #[doc(hidden)] |
| pub column: u32, |
| } |
| |
| impl fmt::Display for Position { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}:{}:{}", self.file, self.line, self.column) |
| } |
| } |
| |
| #[derive(Clone, Copy)] |
| enum StrLitKind { |
| Normal, |
| Raw(usize), |
| } |
| |
| impl StrLitKind { |
| fn write_start(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
| match self { |
| Self::Normal => write!(w, "\""), |
| Self::Raw(n) => { |
| write!(w, "r")?; |
| for _ in 0..n { |
| write!(w, "#")?; |
| } |
| write!(w, "\"") |
| } |
| } |
| } |
| |
| fn write_end(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
| match self { |
| Self::Normal => write!(w, "\""), |
| Self::Raw(n) => { |
| write!(w, "\"")?; |
| for _ in 0..n { |
| write!(w, "#")?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| impl Expect { |
| /// Checks if this expect is equal to `actual`. |
| pub fn assert_eq(&self, actual: &str) { |
| let trimmed = self.trimmed(); |
| if trimmed == actual { |
| return; |
| } |
| Runtime::fail_expect(self, &trimmed, actual); |
| } |
| /// Checks if this expect is equal to `format!("{:#?}", actual)`. |
| pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) { |
| let actual = format!("{:#?}\n", actual); |
| self.assert_eq(&actual) |
| } |
| /// If `true` (default), in-place update will indent the string literal. |
| pub fn indent(&mut self, yes: bool) { |
| self.indent = yes; |
| } |
| |
| /// Returns the content of this expect. |
| pub fn data(&self) -> &str { |
| self.data |
| } |
| |
| fn trimmed(&self) -> String { |
| if !self.data.contains('\n') { |
| return self.data.to_string(); |
| } |
| trim_indent(self.data) |
| } |
| |
| fn locate(&self, file: &str) -> Location { |
| let mut target_line = None; |
| let mut line_start = 0; |
| for (i, line) in lines_with_ends(file).enumerate() { |
| if i == self.position.line as usize - 1 { |
| // `column` points to the first character of the macro invocation: |
| // |
| // expect![[r#""#]] expect![""] |
| // ^ ^ ^ ^ |
| // column offset offset |
| // |
| // Seek past the exclam, then skip any whitespace and |
| // the macro delimiter to get to our argument. |
| let byte_offset = line |
| .char_indices() |
| .skip((self.position.column - 1).try_into().unwrap()) |
| .skip_while(|&(_, c)| c != '!') |
| .skip(1) // ! |
| .skip_while(|&(_, c)| c.is_whitespace()) |
| .skip(1) // [({ |
| .skip_while(|&(_, c)| c.is_whitespace()) |
| .next() |
| .expect("Failed to parse macro invocation") |
| .0; |
| |
| let literal_start = line_start + byte_offset; |
| let indent = line.chars().take_while(|&it| it == ' ').count(); |
| target_line = Some((literal_start, indent)); |
| break; |
| } |
| line_start += line.len(); |
| } |
| let (literal_start, line_indent) = target_line.unwrap(); |
| |
| let lit_to_eof = &file[literal_start..]; |
| let lit_to_eof_trimmed = lit_to_eof.trim_start(); |
| |
| let literal_start = literal_start + (lit_to_eof.len() - lit_to_eof_trimmed.len()); |
| |
| let literal_len = |
| locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`."); |
| let literal_range = literal_start..literal_start + literal_len; |
| Location { line_indent, literal_range } |
| } |
| } |
| |
| fn locate_end(arg_start_to_eof: &str) -> Option<usize> { |
| match arg_start_to_eof.chars().next()? { |
| c if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`"), |
| |
| // expect![[]] |
| '[' => { |
| let str_start_to_eof = arg_start_to_eof[1..].trim_start(); |
| let str_len = find_str_lit_len(str_start_to_eof)?; |
| let str_end_to_eof = &str_start_to_eof[str_len..]; |
| let closing_brace_offset = str_end_to_eof.find(']')?; |
| Some((arg_start_to_eof.len() - str_end_to_eof.len()) + closing_brace_offset + 1) |
| } |
| |
| // expect![] | expect!{} | expect!() |
| ']' | '}' | ')' => Some(0), |
| |
| // expect!["..."] | expect![r#"..."#] |
| _ => find_str_lit_len(arg_start_to_eof), |
| } |
| } |
| |
| /// Parses a string literal, returning the byte index of its last character |
| /// (either a quote or a hash). |
| fn find_str_lit_len(str_lit_to_eof: &str) -> Option<usize> { |
| use StrLitKind::*; |
| |
| fn try_find_n_hashes( |
| s: &mut impl Iterator<Item = char>, |
| desired_hashes: usize, |
| ) -> Option<(usize, Option<char>)> { |
| let mut n = 0; |
| loop { |
| match s.next()? { |
| '#' => n += 1, |
| c => return Some((n, Some(c))), |
| } |
| |
| if n == desired_hashes { |
| return Some((n, None)); |
| } |
| } |
| } |
| |
| let mut s = str_lit_to_eof.chars(); |
| let kind = match s.next()? { |
| '"' => Normal, |
| 'r' => { |
| let (n, c) = try_find_n_hashes(&mut s, usize::MAX)?; |
| if c != Some('"') { |
| return None; |
| } |
| Raw(n) |
| } |
| _ => return None, |
| }; |
| |
| let mut oldc = None; |
| loop { |
| let c = oldc.take().or_else(|| s.next())?; |
| match (c, kind) { |
| ('\\', Normal) => { |
| let _escaped = s.next()?; |
| } |
| ('"', Normal) => break, |
| ('"', Raw(0)) => break, |
| ('"', Raw(n)) => { |
| let (seen, c) = try_find_n_hashes(&mut s, n)?; |
| if seen == n { |
| break; |
| } |
| oldc = c; |
| } |
| _ => {} |
| } |
| } |
| |
| Some(str_lit_to_eof.len() - s.as_str().len()) |
| } |
| |
| impl ExpectFile { |
| /// Checks if file contents is equal to `actual`. |
| pub fn assert_eq(&self, actual: &str) { |
| let expected = self.data(); |
| if actual == expected { |
| return; |
| } |
| Runtime::fail_file(self, &expected, actual); |
| } |
| /// Checks if file contents is equal to `format!("{:#?}", actual)`. |
| pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) { |
| let actual = format!("{:#?}\n", actual); |
| self.assert_eq(&actual) |
| } |
| /// Returns the content of this expect. |
| pub fn data(&self) -> String { |
| fs::read_to_string(self.abs_path()).unwrap_or_default().replace("\r\n", "\n") |
| } |
| fn write(&self, contents: &str) { |
| fs::write(self.abs_path(), contents).unwrap() |
| } |
| fn abs_path(&self) -> PathBuf { |
| if self.path.is_absolute() { |
| self.path.to_owned() |
| } else { |
| let dir = Path::new(self.position).parent().unwrap(); |
| to_abs_ws_path(&dir.join(&self.path)) |
| } |
| } |
| } |
| |
| #[derive(Default)] |
| struct Runtime { |
| help_printed: bool, |
| per_file: HashMap<&'static str, FileRuntime>, |
| } |
| static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default); |
| |
| impl Runtime { |
| fn fail_expect(expect: &Expect, expected: &str, actual: &str) { |
| let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
| if update_expect() { |
| println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.position); |
| rt.per_file |
| .entry(expect.position.file) |
| .or_insert_with(|| FileRuntime::new(expect)) |
| .update(expect, actual); |
| return; |
| } |
| rt.panic(expect.position.to_string(), expected, actual); |
| } |
| fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) { |
| let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
| if update_expect() { |
| println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.path.display()); |
| expect.write(actual); |
| return; |
| } |
| rt.panic(expect.path.display().to_string(), expected, actual); |
| } |
| fn panic(&mut self, position: String, expected: &str, actual: &str) { |
| let print_help = !mem::replace(&mut self.help_printed, true); |
| let help = if print_help { HELP } else { "" }; |
| |
| let diff = dissimilar::diff(expected, actual); |
| |
| println!( |
| "\n |
| \x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m |
| \x1b[1m\x1b[34m-->\x1b[0m {} |
| {} |
| \x1b[1mExpect\x1b[0m: |
| ---- |
| {} |
| ---- |
| |
| \x1b[1mActual\x1b[0m: |
| ---- |
| {} |
| ---- |
| |
| \x1b[1mDiff\x1b[0m: |
| ---- |
| {} |
| ---- |
| ", |
| position, |
| help, |
| expected, |
| actual, |
| format_chunks(diff) |
| ); |
| // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise. |
| panic::resume_unwind(Box::new(())); |
| } |
| } |
| |
| struct FileRuntime { |
| path: PathBuf, |
| original_text: String, |
| patchwork: Patchwork, |
| } |
| |
| impl FileRuntime { |
| fn new(expect: &Expect) -> FileRuntime { |
| let path = to_abs_ws_path(Path::new(expect.position.file)); |
| let original_text = fs::read_to_string(&path).unwrap(); |
| let patchwork = Patchwork::new(original_text.clone()); |
| FileRuntime { path, original_text, patchwork } |
| } |
| fn update(&mut self, expect: &Expect, actual: &str) { |
| let loc = expect.locate(&self.original_text); |
| let desired_indent = if expect.indent { Some(loc.line_indent) } else { None }; |
| let patch = format_patch(desired_indent, actual); |
| self.patchwork.patch(loc.literal_range, &patch); |
| fs::write(&self.path, &self.patchwork.text).unwrap() |
| } |
| } |
| |
| #[derive(Debug)] |
| struct Location { |
| line_indent: usize, |
| |
| /// The byte range of the argument to `expect!`, including the inner `[]` if it exists. |
| literal_range: Range<usize>, |
| } |
| |
| #[derive(Debug)] |
| struct Patchwork { |
| text: String, |
| indels: Vec<(Range<usize>, usize)>, |
| } |
| |
| impl Patchwork { |
| fn new(text: String) -> Patchwork { |
| Patchwork { text, indels: Vec::new() } |
| } |
| fn patch(&mut self, mut range: Range<usize>, patch: &str) { |
| self.indels.push((range.clone(), patch.len())); |
| self.indels.sort_by_key(|(delete, _insert)| delete.start); |
| |
| let (delete, insert) = self |
| .indels |
| .iter() |
| .take_while(|(delete, _)| delete.start < range.start) |
| .map(|(delete, insert)| (delete.end - delete.start, insert)) |
| .fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2)); |
| |
| for pos in &mut [&mut range.start, &mut range.end] { |
| **pos -= delete; |
| **pos += insert; |
| } |
| |
| self.text.replace_range(range, &patch); |
| } |
| } |
| |
| fn lit_kind_for_patch(patch: &str) -> StrLitKind { |
| let has_dquote = patch.chars().any(|c| c == '"'); |
| if !has_dquote { |
| let has_bslash_or_newline = patch.chars().any(|c| matches!(c, '\\' | '\n')); |
| return if has_bslash_or_newline { StrLitKind::Raw(1) } else { StrLitKind::Normal }; |
| } |
| |
| // Find the maximum number of hashes that follow a double quote in the string. |
| // We need to use one more than that to delimit the string. |
| let leading_hashes = |s: &str| s.chars().take_while(|&c| c == '#').count(); |
| let max_hashes = patch.split('"').map(leading_hashes).max().unwrap(); |
| StrLitKind::Raw(max_hashes + 1) |
| } |
| |
| fn format_patch(desired_indent: Option<usize>, patch: &str) -> String { |
| let lit_kind = lit_kind_for_patch(patch); |
| let indent = desired_indent.map(|it| " ".repeat(it)); |
| let is_multiline = patch.contains('\n'); |
| |
| let mut buf = String::new(); |
| if matches!(lit_kind, StrLitKind::Raw(_)) { |
| buf.push('['); |
| } |
| lit_kind.write_start(&mut buf).unwrap(); |
| if is_multiline { |
| buf.push('\n'); |
| } |
| let mut final_newline = false; |
| for line in lines_with_ends(patch) { |
| if is_multiline && !line.trim().is_empty() { |
| if let Some(indent) = &indent { |
| buf.push_str(indent); |
| buf.push_str(" "); |
| } |
| } |
| buf.push_str(line); |
| final_newline = line.ends_with('\n'); |
| } |
| if final_newline { |
| if let Some(indent) = &indent { |
| buf.push_str(indent); |
| } |
| } |
| lit_kind.write_end(&mut buf).unwrap(); |
| if matches!(lit_kind, StrLitKind::Raw(_)) { |
| buf.push(']'); |
| } |
| buf |
| } |
| |
| fn to_abs_ws_path(path: &Path) -> PathBuf { |
| if path.is_absolute() { |
| return path.to_owned(); |
| } |
| |
| static WORKSPACE_ROOT: OnceCell<PathBuf> = OnceCell::new(); |
| WORKSPACE_ROOT |
| .get_or_try_init(|| { |
| // Until https://github.com/rust-lang/cargo/issues/3946 is resolved, this |
| // is set with a hack like https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 |
| if let Ok(workspace_root) = env::var("CARGO_WORKSPACE_DIR") { |
| return Ok(workspace_root.into()); |
| } |
| |
| // If a hack isn't used, we use a heuristic to find the "top-level" workspace. |
| // This fails in some cases, see https://github.com/rust-analyzer/expect-test/issues/33 |
| let my_manifest = env::var("CARGO_MANIFEST_DIR")?; |
| let workspace_root = Path::new(&my_manifest) |
| .ancestors() |
| .filter(|it| it.join("Cargo.toml").exists()) |
| .last() |
| .unwrap() |
| .to_path_buf(); |
| |
| Ok(workspace_root) |
| }) |
| .unwrap_or_else(|_: env::VarError| { |
| panic!("No CARGO_MANIFEST_DIR env var and the path is relative: {}", path.display()) |
| }) |
| .join(path) |
| } |
| |
| fn trim_indent(mut text: &str) -> String { |
| if text.starts_with('\n') { |
| text = &text[1..]; |
| } |
| let indent = text |
| .lines() |
| .filter(|it| !it.trim().is_empty()) |
| .map(|it| it.len() - it.trim_start().len()) |
| .min() |
| .unwrap_or(0); |
| |
| lines_with_ends(text) |
| .map( |
| |line| { |
| if line.len() <= indent { |
| line.trim_start_matches(' ') |
| } else { |
| &line[indent..] |
| } |
| }, |
| ) |
| .collect() |
| } |
| |
| fn lines_with_ends(text: &str) -> LinesWithEnds { |
| LinesWithEnds { text } |
| } |
| |
| struct LinesWithEnds<'a> { |
| text: &'a str, |
| } |
| |
| impl<'a> Iterator for LinesWithEnds<'a> { |
| type Item = &'a str; |
| fn next(&mut self) -> Option<&'a str> { |
| if self.text.is_empty() { |
| return None; |
| } |
| let idx = self.text.find('\n').map_or(self.text.len(), |it| it + 1); |
| let (res, next) = self.text.split_at(idx); |
| self.text = next; |
| Some(res) |
| } |
| } |
| |
| fn format_chunks(chunks: Vec<dissimilar::Chunk>) -> String { |
| let mut buf = String::new(); |
| for chunk in chunks { |
| let formatted = match chunk { |
| dissimilar::Chunk::Equal(text) => text.into(), |
| dissimilar::Chunk::Delete(text) => format!("\x1b[4m\x1b[31m{}\x1b[0m", text), |
| dissimilar::Chunk::Insert(text) => format!("\x1b[4m\x1b[32m{}\x1b[0m", text), |
| }; |
| buf.push_str(&formatted); |
| } |
| buf |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_trivial_assert() { |
| expect!["5"].assert_eq("5"); |
| } |
| |
| #[test] |
| fn test_format_patch() { |
| let patch = format_patch(None, "hello\nworld\n"); |
| expect![[r##" |
| [r#" |
| hello |
| world |
| "#]"##]] |
| .assert_eq(&patch); |
| |
| let patch = format_patch(None, r"hello\tworld"); |
| expect![[r##"[r#"hello\tworld"#]"##]].assert_eq(&patch); |
| |
| let patch = format_patch(None, "{\"foo\": 42}"); |
| expect![[r##"[r#"{"foo": 42}"#]"##]].assert_eq(&patch); |
| |
| let patch = format_patch(Some(0), "hello\nworld\n"); |
| expect![[r##" |
| [r#" |
| hello |
| world |
| "#]"##]] |
| .assert_eq(&patch); |
| |
| let patch = format_patch(Some(4), "single line"); |
| expect![[r#""single line""#]].assert_eq(&patch); |
| } |
| |
| #[test] |
| fn test_patchwork() { |
| let mut patchwork = Patchwork::new("one two three".to_string()); |
| patchwork.patch(4..7, "zwei"); |
| patchwork.patch(0..3, "один"); |
| patchwork.patch(8..13, "3"); |
| expect![[r#" |
| Patchwork { |
| text: "один zwei 3", |
| indels: [ |
| ( |
| 0..3, |
| 8, |
| ), |
| ( |
| 4..7, |
| 4, |
| ), |
| ( |
| 8..13, |
| 1, |
| ), |
| ], |
| } |
| "#]] |
| .assert_debug_eq(&patchwork); |
| } |
| |
| #[test] |
| fn test_expect_file() { |
| expect_file!["./lib.rs"].assert_eq(include_str!("./lib.rs")) |
| } |
| |
| #[test] |
| fn smoke_test_indent() { |
| fn check_indented(input: &str, mut expect: Expect) { |
| expect.indent(true); |
| expect.assert_eq(input); |
| } |
| fn check_not_indented(input: &str, mut expect: Expect) { |
| expect.indent(false); |
| expect.assert_eq(input); |
| } |
| |
| check_indented( |
| "\ |
| line1 |
| line2 |
| ", |
| expect![[r#" |
| line1 |
| line2 |
| "#]], |
| ); |
| |
| check_not_indented( |
| "\ |
| line1 |
| line2 |
| ", |
| expect![[r#" |
| line1 |
| line2 |
| "#]], |
| ); |
| } |
| |
| #[test] |
| fn test_locate() { |
| macro_rules! check_locate { |
| ($( [[$s:literal]] ),* $(,)?) => {$({ |
| let lit = stringify!($s); |
| let with_trailer = format!("{} \t]]\n", lit); |
| assert_eq!(locate_end(&with_trailer), Some(lit.len())); |
| })*}; |
| } |
| |
| // Check that we handle string literals containing "]]" correctly. |
| check_locate!( |
| [[r#"{ arr: [[1, 2], [3, 4]], other: "foo" } "#]], |
| [["]]"]], |
| [["\"]]"]], |
| [[r#""]]"#]], |
| ); |
| |
| // Check `expect![[ ]]` as well. |
| assert_eq!(locate_end("]]"), Some(0)); |
| } |
| |
| #[test] |
| fn test_find_str_lit_len() { |
| macro_rules! check_str_lit_len { |
| ($( $s:literal ),* $(,)?) => {$({ |
| let lit = stringify!($s); |
| assert_eq!(find_str_lit_len(lit), Some(lit.len())); |
| })*} |
| } |
| |
| check_str_lit_len![ |
| r##"foa\""#"##, |
| r##" |
| |
| asdf][]]""""# |
| "##, |
| "", |
| "\"", |
| "\"\"", |
| "#\"#\"#", |
| ]; |
| } |
| } |