diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /vendor/expect-test/src | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/expect-test/src')
-rw-r--r-- | vendor/expect-test/src/lib.rs | 877 |
1 files changed, 877 insertions, 0 deletions
diff --git a/vendor/expect-test/src/lib.rs b/vendor/expect-test/src/lib.rs new file mode 100644 index 000000000..83fcf35bf --- /dev/null +++ b/vendor/expect-test/src/lib.rs @@ -0,0 +1,877 @@ +//! 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.45.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.read(); + 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) + } + fn read(&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[41m{}\x1b[0m", text), + dissimilar::Chunk::Insert(text) => format!("\x1b[42m{}\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][]]""""# + "##, + "", + "\"", + "\"\"", + "#\"#\"#", + ]; + } +} |