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 | |
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')
-rw-r--r-- | vendor/expect-test/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | vendor/expect-test/CHANGELOG.md | 24 | ||||
-rw-r--r-- | vendor/expect-test/Cargo.toml | 37 | ||||
-rw-r--r-- | vendor/expect-test/LICENSE-APACHE | 201 | ||||
-rw-r--r-- | vendor/expect-test/LICENSE-MIT | 23 | ||||
-rw-r--r-- | vendor/expect-test/README.md | 14 | ||||
-rw-r--r-- | vendor/expect-test/src/lib.rs | 877 |
7 files changed, 1177 insertions, 0 deletions
diff --git a/vendor/expect-test/.cargo-checksum.json b/vendor/expect-test/.cargo-checksum.json new file mode 100644 index 000000000..5bd5b89ad --- /dev/null +++ b/vendor/expect-test/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"f032a324c8763cd7a228779a6402da8c694f8858028084b07b9c2bb6765cac37","Cargo.toml":"9b4ac84b09bbf6f9ee532a9722596450bb8c3508ab2e406639967ff9c97b010a","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3","README.md":"c3bd17e3bddcdde2d0c62b1c55bf4d483183a47772267c73926bc54a9f92a851","src/lib.rs":"1030dcb8751234682195894d2d20f88351a24ef832ebfc351a3ccea05190a663"},"package":"1d4661aca38d826eb7c72fe128e4238220616de4c0cc00db7bfc38e2e1364dd3"}
\ No newline at end of file diff --git a/vendor/expect-test/CHANGELOG.md b/vendor/expect-test/CHANGELOG.md new file mode 100644 index 000000000..0d1e5e6ac --- /dev/null +++ b/vendor/expect-test/CHANGELOG.md @@ -0,0 +1,24 @@ +# 1.4.0 + +* Prefer `CARGO_WORKSPACE_DIR` if set, use heuristic as fallback to find cargo workspace ([#34]) + +# 1.3.0 + +* Add `data()` getter to Expect ([#31]) +* Support single delimiter version of `expect![]` ([#27]) +* Allow users to rebind `expect!` ([#26]) + +# 1.2.2 + +* Parse string literals to find their length ([#23]) +* Do not use `fs::canonicalize`. + +# 1.2.1 + +* No changelog until this point :-( + +[#34]: https://github.com/rust-analyzer/expect-test/pull/34 +[#31]: https://github.com/rust-analyzer/expect-test/pull/31 +[#27]: https://github.com/rust-analyzer/expect-test/pull/27 +[#26]: https://github.com/rust-analyzer/expect-test/pull/26 +[#23]: https://github.com/rust-analyzer/expect-test/pull/23 diff --git a/vendor/expect-test/Cargo.toml b/vendor/expect-test/Cargo.toml new file mode 100644 index 000000000..c02e4ddea --- /dev/null +++ b/vendor/expect-test/Cargo.toml @@ -0,0 +1,37 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "expect-test" +version = "1.4.0" +authors = ["rust-analyzer developers"] +exclude = [ + "./github", + "bors.toml", + "rustfmt.toml", +] +description = "Minimalistic snapshot testing library" +readme = "README.md" +keywords = [ + "snapshot", + "testing", + "expect", +] +categories = ["development-tools::testing"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-analyzer/expect-test" + +[dependencies.dissimilar] +version = "1" + +[dependencies.once_cell] +version = "1" diff --git a/vendor/expect-test/LICENSE-APACHE b/vendor/expect-test/LICENSE-APACHE new file mode 100644 index 000000000..16fe87b06 --- /dev/null +++ b/vendor/expect-test/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/expect-test/LICENSE-MIT b/vendor/expect-test/LICENSE-MIT new file mode 100644 index 000000000..31aa79387 --- /dev/null +++ b/vendor/expect-test/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/expect-test/README.md b/vendor/expect-test/README.md new file mode 100644 index 000000000..b5f5ca706 --- /dev/null +++ b/vendor/expect-test/README.md @@ -0,0 +1,14 @@ +# expect-test +[![Docs](https://docs.rs/expect-test/badge.svg)](https://docs.rs/expect-test) + +Minimalistic snapshot testing for Rust. + +Updating a failing test: + +https://user-images.githubusercontent.com/1711539/120119633-73b3f100-c1a1-11eb-91be-4c61a23e7060.mp4 + +Adding a new test: + +![expect-fresh](https://user-images.githubusercontent.com/1711539/85926961-306f4500-b8a3-11ea-9369-f2373e327a3f.gif) + +Checkout the docs for more: https://docs.rs/expect-test. 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][]]""""# + "##, + "", + "\"", + "\"\"", + "#\"#\"#", + ]; + } +} |