summaryrefslogtreecommitdiffstats
path: root/vendor/expect-test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /vendor/expect-test
parentInitial commit. (diff)
downloadrustc-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.json1
-rw-r--r--vendor/expect-test/CHANGELOG.md24
-rw-r--r--vendor/expect-test/Cargo.toml37
-rw-r--r--vendor/expect-test/LICENSE-APACHE201
-rw-r--r--vendor/expect-test/LICENSE-MIT23
-rw-r--r--vendor/expect-test/README.md14
-rw-r--r--vendor/expect-test/src/lib.rs877
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][]]""""#
+ "##,
+ "",
+ "\"",
+ "\"\"",
+ "#\"#\"#",
+ ];
+ }
+}