summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:47:55 +0000
commit2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4 (patch)
tree033cc839730fda84ff08db877037977be94e5e3a /vendor/snapbox
parentInitial commit. (diff)
downloadcargo-2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4.tar.xz
cargo-2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4.zip
Adding upstream version 0.70.1+ds1.upstream/0.70.1+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/snapbox')
-rw-r--r--vendor/snapbox/.cargo-checksum.json1
-rw-r--r--vendor/snapbox/Cargo.lock886
-rw-r--r--vendor/snapbox/Cargo.toml201
-rw-r--r--vendor/snapbox/LICENSE-APACHE202
-rw-r--r--vendor/snapbox/LICENSE-MIT19
-rw-r--r--vendor/snapbox/README.md36
-rw-r--r--vendor/snapbox/debian/patches/remove-escargot.patch21
-rw-r--r--vendor/snapbox/debian/patches/remove-windows-deps.patch20
-rw-r--r--vendor/snapbox/debian/patches/series2
-rw-r--r--vendor/snapbox/examples/snap-example-fixture.rs61
-rw-r--r--vendor/snapbox/src/action.rs39
-rw-r--r--vendor/snapbox/src/assert.rs520
-rw-r--r--vendor/snapbox/src/bin/snap-fixture.rs61
-rw-r--r--vendor/snapbox/src/cmd.rs1170
-rw-r--r--vendor/snapbox/src/data.rs940
-rw-r--r--vendor/snapbox/src/error.rs95
-rw-r--r--vendor/snapbox/src/harness.rs214
-rw-r--r--vendor/snapbox/src/lib.rs246
-rw-r--r--vendor/snapbox/src/path.rs687
-rw-r--r--vendor/snapbox/src/report/color.rs111
-rw-r--r--vendor/snapbox/src/report/diff.rs393
-rw-r--r--vendor/snapbox/src/report/mod.rs9
-rw-r--r--vendor/snapbox/src/substitutions.rs420
-rw-r--r--vendor/snapbox/src/utils/lines.rs31
-rw-r--r--vendor/snapbox/src/utils/mod.rs30
25 files changed, 6415 insertions, 0 deletions
diff --git a/vendor/snapbox/.cargo-checksum.json b/vendor/snapbox/.cargo-checksum.json
new file mode 100644
index 0000000..e6d1717
--- /dev/null
+++ b/vendor/snapbox/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{},"package":"4b377c0b6e4715c116473d8e40d51e3fa5b0a2297ca9b2a931ba800667b259ed"} \ No newline at end of file
diff --git a/vendor/snapbox/Cargo.lock b/vendor/snapbox/Cargo.lock
new file mode 100644
index 0000000..184963e
--- /dev/null
+++ b/vendor/snapbox/Cargo.lock
@@ -0,0 +1,886 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff2cf94a3dbe2d57cbd56485e1bd7436455058034d6c2d47be51d4e5e4bc6ab"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0acbd8d28a0a60d7108d7ae850af6ba34cf2d1257fc646980e5f97ce14275966"
+dependencies = [
+ "bitflags 1.3.2",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "content_inspector"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3267e1ade4f1f6ddd35fed44a04b6514e244ffeda90c6a14a9ee30f9c9fd7a1"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
+
+[[package]]
+name = "errno"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "escargot"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5584ba17d7ab26a8a7284f13e5bd196294dd2f2d79773cff29b9e9edef601a6"
+dependencies = [
+ "log",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "gimli"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+
+[[package]]
+name = "globset"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "ignore"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+dependencies = [
+ "hermit-abi 0.3.2",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libtest-mimic"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7b603516767d1ab23d0de09d023e62966c3322f7148297c35cf3d97aa8b37fa"
+dependencies = [
+ "clap",
+ "termcolor",
+ "threadpool",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
+
+[[package]]
+name = "litrs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+
+[[package]]
+name = "os_pipe"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c92f2b54f081d635c77e7120862d48db8e91f7f21cef23ab1b4fe9971c59f55"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
+[[package]]
+name = "rustix"
+version = "0.38.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
+dependencies = [
+ "bitflags 2.4.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "similar"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
+
+[[package]]
+name = "snapbox"
+version = "0.4.14"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "backtrace",
+ "content_inspector",
+ "document-features",
+ "dunce",
+ "escargot",
+ "filetime",
+ "ignore",
+ "libc",
+ "libtest-mimic",
+ "normalize-line-endings",
+ "os_pipe",
+ "serde_json",
+ "similar",
+ "snapbox-macros",
+ "tempfile",
+ "wait-timeout",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "snapbox-macros"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e"
+dependencies = [
+ "anstream",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/vendor/snapbox/Cargo.toml b/vendor/snapbox/Cargo.toml
new file mode 100644
index 0000000..a8ade72
--- /dev/null
+++ b/vendor/snapbox/Cargo.toml
@@ -0,0 +1,201 @@
+# 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 = "2021"
+rust-version = "1.70.0"
+name = "snapbox"
+version = "0.4.14"
+include = [
+ "build.rs",
+ "src/**/*",
+ "Cargo.toml",
+ "LICENSE*",
+ "README.md",
+ "benches/**/*",
+ "examples/**/*",
+]
+description = "Snapshot testing toolbox"
+homepage = "https://github.com/assert-rs/trycmd/tree/main/crates/snapbox"
+documentation = "http://docs.rs/snapbox/"
+readme = "README.md"
+keywords = [
+ "cli",
+ "test",
+ "assert",
+ "command",
+]
+categories = ["development-tools::testing"]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/assert-rs/trycmd/"
+
+[package.metadata.docs.rs]
+all-features = true
+cargo-args = [
+ "-Zunstable-options",
+ "-Zrustdoc-scrape-examples",
+]
+rustdoc-args = [
+ "--cfg",
+ "docsrs",
+]
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+min = 1
+replace = "{{version}}"
+search = "Unreleased"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = "...{{tag_name}}"
+search = '\.\.\.HEAD'
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+min = 1
+replace = "{{date}}"
+search = "ReleaseDate"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = """
+<!-- next-header -->
+## [Unreleased] - ReleaseDate
+"""
+search = "<!-- next-header -->"
+
+[[package.metadata.release.pre-release-replacements]]
+exactly = 1
+file = "CHANGELOG.md"
+replace = """
+<!-- next-url -->
+[Unreleased]: https://github.com/assert-rs/trycmd/compare/{{tag_name}}...HEAD"""
+search = "<!-- next-url -->"
+
+[[bin]]
+name = "snap-fixture"
+
+[dependencies.anstream]
+version = "0.6.0"
+optional = true
+
+[dependencies.anstyle]
+version = "1.0.0"
+
+[dependencies.backtrace]
+version = "0.3"
+optional = true
+
+[dependencies.content_inspector]
+version = "0.2.4"
+optional = true
+
+[dependencies.document-features]
+version = "0.2.6"
+optional = true
+
+[dependencies.dunce]
+version = "1.0"
+optional = true
+
+[dependencies.filetime]
+version = "0.2"
+optional = true
+
+[dependencies.ignore]
+version = "0.4"
+optional = true
+
+[dependencies.libtest-mimic]
+version = "0.6.0"
+optional = true
+
+[dependencies.normalize-line-endings]
+version = "0.3.0"
+
+[dependencies.os_pipe]
+version = "1.0"
+optional = true
+
+[dependencies.serde_json]
+version = "1.0.85"
+optional = true
+
+[dependencies.similar]
+version = "2.1.0"
+features = ["inline"]
+optional = true
+
+[dependencies.snapbox-macros]
+version = "0.3.6"
+
+[dependencies.tempfile]
+version = "3.0"
+optional = true
+
+[dependencies.wait-timeout]
+version = "0.2.0"
+optional = true
+
+[dependencies.walkdir]
+version = "2.3.2"
+optional = true
+
+[features]
+cmd = [
+ "dep:os_pipe",
+ "dep:wait-timeout",
+ "dep:libc",
+ #"dep:windows-sys",
+]
+color = [
+ "dep:anstream",
+ "snapbox-macros/color",
+]
+color-auto = ["color"]
+debug = [
+ "snapbox-macros/debug",
+ "dep:backtrace",
+]
+default = [
+ "color-auto",
+ "diff",
+]
+detect-encoding = ["dep:content_inspector"]
+diff = ["dep:similar"]
+harness = [
+ "dep:libtest-mimic",
+ "dep:ignore",
+]
+json = [
+ "structured-data",
+ "dep:serde_json",
+]
+path = [
+ "dep:tempfile",
+ "dep:walkdir",
+ "dep:dunce",
+ "detect-encoding",
+ "dep:filetime",
+]
+structured-data = ["dep:serde_json"]
+
+[target."cfg(unix)".dependencies.libc]
+version = "0.2.137"
+optional = true
+
+#[target."cfg(windows)".dependencies.windows-sys]
+#version = "0.48.0"
+#features = ["Win32_Foundation"]
+#optional = true
diff --git a/vendor/snapbox/LICENSE-APACHE b/vendor/snapbox/LICENSE-APACHE
new file mode 100644
index 0000000..8f71f43
--- /dev/null
+++ b/vendor/snapbox/LICENSE-APACHE
@@ -0,0 +1,202 @@
+ 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/snapbox/LICENSE-MIT b/vendor/snapbox/LICENSE-MIT
new file mode 100644
index 0000000..a2d0108
--- /dev/null
+++ b/vendor/snapbox/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) Individual contributors
+
+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/snapbox/README.md b/vendor/snapbox/README.md
new file mode 100644
index 0000000..3e6c9b6
--- /dev/null
+++ b/vendor/snapbox/README.md
@@ -0,0 +1,36 @@
+# snapbox
+
+> When you have to treat your tests like pets, instead of [cattle][trycmd]
+
+[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation]
+![License](https://img.shields.io/crates/l/snapbox.svg)
+[![Crates Status](https://img.shields.io/crates/v/snapbox.svg)](https://crates.io/crates/snapbox)
+
+`snapbox` is a snapshot-testing toolbox that is ready to use for verifying output from
+- Function return values
+- CLI stdout/stderr
+- Filesystem changes
+
+It is also flexible enough to build your own test harness like [trycmd].
+
+See the [docs](http://docs.rs/snapbox) for more.
+
+## License
+
+Licensed under either of
+
+ * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+ * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally
+submitted for inclusion in the work by you, as defined in the Apache-2.0
+license, shall be dual licensed as above, without any additional terms or
+conditions.
+
+[Crates.io]: https://crates.io/crates/snapbox
+[Documentation]: https://docs.rs/snapbox
+[trycmd]: https://crates.io/crates/trycmd
diff --git a/vendor/snapbox/debian/patches/remove-escargot.patch b/vendor/snapbox/debian/patches/remove-escargot.patch
new file mode 100644
index 0000000..9b78b53
--- /dev/null
+++ b/vendor/snapbox/debian/patches/remove-escargot.patch
@@ -0,0 +1,21 @@
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -109,10 +109,6 @@
+ version = "1.0"
+ optional = true
+
+-[dependencies.escargot]
+-version = "0.5.7"
+-optional = true
+-
+ [dependencies.filetime]
+ version = "0.2"
+ optional = true
+@@ -178,7 +174,6 @@
+ ]
+ detect-encoding = ["dep:content_inspector"]
+ diff = ["dep:similar"]
+-examples = ["dep:escargot"]
+ harness = [
+ "dep:libtest-mimic",
+ "dep:ignore",
diff --git a/vendor/snapbox/debian/patches/remove-windows-deps.patch b/vendor/snapbox/debian/patches/remove-windows-deps.patch
new file mode 100644
index 0000000..4773339
--- /dev/null
+++ b/vendor/snapbox/debian/patches/remove-windows-deps.patch
@@ -0,0 +1,20 @@
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -162,5 +162,5 @@
+ "dep:wait-timeout",
+ "dep:libc",
+- "dep:windows-sys",
++ #"dep:windows-sys",
+ ]
+ color = [
+@@ -201,6 +201,6 @@
+ optional = true
+
+-[target."cfg(windows)".dependencies.windows-sys]
+-version = "0.48.0"
+-features = ["Win32_Foundation"]
+-optional = true
++#[target."cfg(windows)".dependencies.windows-sys]
++#version = "0.48.0"
++#features = ["Win32_Foundation"]
++#optional = true
diff --git a/vendor/snapbox/debian/patches/series b/vendor/snapbox/debian/patches/series
new file mode 100644
index 0000000..47fb12d
--- /dev/null
+++ b/vendor/snapbox/debian/patches/series
@@ -0,0 +1,2 @@
+remove-windows-deps.patch
+remove-escargot.patch
diff --git a/vendor/snapbox/examples/snap-example-fixture.rs b/vendor/snapbox/examples/snap-example-fixture.rs
new file mode 100644
index 0000000..7e21fc6
--- /dev/null
+++ b/vendor/snapbox/examples/snap-example-fixture.rs
@@ -0,0 +1,61 @@
+//! For `snapbox`s tests only
+
+use std::env;
+use std::error::Error;
+use std::io;
+use std::io::Write;
+use std::process;
+
+fn run() -> Result<(), Box<dyn Error>> {
+ if let Ok(text) = env::var("stdout") {
+ println!("{}", text);
+ }
+ if let Ok(text) = env::var("stderr") {
+ eprintln!("{}", text);
+ }
+
+ if env::var("echo_large").as_deref() == Ok("1") {
+ for i in 0..(128 * 1024) {
+ println!("{}", i);
+ }
+ }
+
+ if env::var("echo_cwd").as_deref() == Ok("1") {
+ if let Ok(cwd) = std::env::current_dir() {
+ eprintln!("{}", cwd.display());
+ }
+ }
+
+ if let Ok(raw) = env::var("write") {
+ let (path, text) = raw.split_once('=').unwrap_or((raw.as_str(), ""));
+ std::fs::write(path.trim(), text.trim()).unwrap();
+ }
+
+ if let Ok(path) = env::var("cat") {
+ let text = std::fs::read_to_string(path).unwrap();
+ eprintln!("{}", text);
+ }
+
+ if let Some(timeout) = env::var("sleep").ok().and_then(|s| s.parse().ok()) {
+ std::thread::sleep(std::time::Duration::from_secs(timeout));
+ }
+
+ let code = env::var("exit")
+ .ok()
+ .map(|v| v.parse::<i32>())
+ .map(|r| r.map(Some))
+ .unwrap_or(Ok(None))?
+ .unwrap_or(0);
+ process::exit(code);
+}
+
+fn main() {
+ let code = match run() {
+ Ok(_) => 0,
+ Err(ref e) => {
+ write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail");
+ 1
+ }
+ };
+ process::exit(code);
+}
diff --git a/vendor/snapbox/src/action.rs b/vendor/snapbox/src/action.rs
new file mode 100644
index 0000000..a4b8499
--- /dev/null
+++ b/vendor/snapbox/src/action.rs
@@ -0,0 +1,39 @@
+pub const DEFAULT_ACTION_ENV: &str = "SNAPSHOTS";
+
+/// Test action, see [`Assert`][crate::Assert]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Action {
+ /// Do not run the test
+ Skip,
+ /// Ignore test failures
+ Ignore,
+ /// Fail on mismatch
+ Verify,
+ /// Overwrite on mismatch
+ Overwrite,
+}
+
+impl Action {
+ pub fn with_env_var(var: impl AsRef<std::ffi::OsStr>) -> Option<Self> {
+ let var = var.as_ref();
+ let value = std::env::var_os(var)?;
+ Self::with_env_value(value)
+ }
+
+ pub fn with_env_value(value: impl AsRef<std::ffi::OsStr>) -> Option<Self> {
+ let value = value.as_ref();
+ match value.to_str()? {
+ "skip" => Some(Action::Skip),
+ "ignore" => Some(Action::Ignore),
+ "verify" => Some(Action::Verify),
+ "overwrite" => Some(Action::Overwrite),
+ _ => None,
+ }
+ }
+}
+
+impl Default for Action {
+ fn default() -> Self {
+ Self::Verify
+ }
+}
diff --git a/vendor/snapbox/src/assert.rs b/vendor/snapbox/src/assert.rs
new file mode 100644
index 0000000..f0267c0
--- /dev/null
+++ b/vendor/snapbox/src/assert.rs
@@ -0,0 +1,520 @@
+#[cfg(feature = "color")]
+use anstream::panic;
+#[cfg(feature = "color")]
+use anstream::stderr;
+#[cfg(not(feature = "color"))]
+use std::io::stderr;
+
+use crate::data::{DataFormat, NormalizeMatches, NormalizeNewlines, NormalizePaths};
+use crate::Action;
+
+/// Snapshot assertion against a file's contents
+///
+/// Useful for one-off assertions with the snapshot stored in a file
+///
+/// # Examples
+///
+/// ```rust,no_run
+/// let actual = "...";
+/// snapbox::Assert::new()
+/// .action_env("SNAPSHOTS")
+/// .matches_path(actual, "tests/fixtures/help_output_is_clean.txt");
+/// ```
+#[derive(Clone, Debug)]
+pub struct Assert {
+ action: Action,
+ action_var: Option<String>,
+ normalize_paths: bool,
+ substitutions: crate::Substitutions,
+ pub(crate) palette: crate::report::Palette,
+ pub(crate) data_format: Option<DataFormat>,
+}
+
+/// # Assertions
+impl Assert {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Check if a value is the same as an expected value
+ ///
+ /// When the content is text, newlines are normalized.
+ ///
+ /// ```rust
+ /// let output = "something";
+ /// let expected = "something";
+ /// snapbox::Assert::new().eq(expected, output);
+ /// ```
+ #[track_caller]
+ pub fn eq(&self, expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ let expected = expected.into();
+ let actual = actual.into();
+ self.eq_inner(expected, actual);
+ }
+
+ #[track_caller]
+ fn eq_inner(&self, expected: crate::Data, actual: crate::Data) {
+ let (pattern, actual) = self.normalize_eq(Ok(expected), actual);
+ if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) {
+ panic!("{}: {}", self.palette.error("Eq failed"), desc);
+ }
+ }
+
+ /// Check if a value matches a pattern
+ ///
+ /// Pattern syntax:
+ /// - `...` is a line-wildcard when on a line by itself
+ /// - `[..]` is a character-wildcard when inside a line
+ /// - `[EXE]` matches `.exe` on Windows
+ ///
+ /// Normalization:
+ /// - Newlines
+ /// - `\` to `/`
+ ///
+ /// ```rust
+ /// let output = "something";
+ /// let expected = "so[..]g";
+ /// snapbox::Assert::new().matches(expected, output);
+ /// ```
+ #[track_caller]
+ pub fn matches(&self, pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ let pattern = pattern.into();
+ let actual = actual.into();
+ self.matches_inner(pattern, actual);
+ }
+
+ #[track_caller]
+ fn matches_inner(&self, pattern: crate::Data, actual: crate::Data) {
+ let (pattern, actual) = self.normalize_match(Ok(pattern), actual);
+ if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) {
+ panic!("{}: {}", self.palette.error("Match failed"), desc);
+ }
+ }
+
+ /// Check if a value matches the content of a file
+ ///
+ /// When the content is text, newlines are normalized.
+ ///
+ /// ```rust,no_run
+ /// let output = "something";
+ /// let expected_path = "tests/snapshots/output.txt";
+ /// snapbox::Assert::new().eq_path(output, expected_path);
+ /// ```
+ #[track_caller]
+ pub fn eq_path(
+ &self,
+ expected_path: impl AsRef<std::path::Path>,
+ actual: impl Into<crate::Data>,
+ ) {
+ let expected_path = expected_path.as_ref();
+ let actual = actual.into();
+ self.eq_path_inner(expected_path, actual);
+ }
+
+ #[track_caller]
+ fn eq_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let expected = crate::Data::read_from(pattern_path, self.data_format());
+ let (expected, actual) = self.normalize_eq(expected, actual);
+
+ self.do_action(
+ expected,
+ actual,
+ Some(&crate::path::display_relpath(pattern_path)),
+ Some(&"In-memory"),
+ pattern_path,
+ );
+ }
+
+ /// Check if a value matches the pattern in a file
+ ///
+ /// Pattern syntax:
+ /// - `...` is a line-wildcard when on a line by itself
+ /// - `[..]` is a character-wildcard when inside a line
+ /// - `[EXE]` matches `.exe` on Windows (override with [`Assert::substitutions`])
+ ///
+ /// Normalization:
+ /// - Newlines
+ /// - `\` to `/`
+ ///
+ /// ```rust,no_run
+ /// let output = "something";
+ /// let expected_path = "tests/snapshots/output.txt";
+ /// snapbox::Assert::new().matches_path(expected_path, output);
+ /// ```
+ #[track_caller]
+ pub fn matches_path(
+ &self,
+ pattern_path: impl AsRef<std::path::Path>,
+ actual: impl Into<crate::Data>,
+ ) {
+ let pattern_path = pattern_path.as_ref();
+ let actual = actual.into();
+ self.matches_path_inner(pattern_path, actual);
+ }
+
+ #[track_caller]
+ fn matches_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let expected = crate::Data::read_from(pattern_path, self.data_format());
+ let (expected, actual) = self.normalize_match(expected, actual);
+
+ self.do_action(
+ expected,
+ actual,
+ Some(&crate::path::display_relpath(pattern_path)),
+ Some(&"In-memory"),
+ pattern_path,
+ );
+ }
+
+ pub(crate) fn normalize_eq(
+ &self,
+ expected: crate::Result<crate::Data>,
+ mut actual: crate::Data,
+ ) -> (crate::Result<crate::Data>, crate::Data) {
+ let expected = expected.map(|d| d.normalize(NormalizeNewlines));
+ // On `expected` being an error, make a best guess
+ let format = expected
+ .as_ref()
+ .map(|d| d.format())
+ .unwrap_or(DataFormat::Text);
+
+ actual = actual.try_coerce(format).normalize(NormalizeNewlines);
+
+ (expected, actual)
+ }
+
+ pub(crate) fn normalize_match(
+ &self,
+ expected: crate::Result<crate::Data>,
+ mut actual: crate::Data,
+ ) -> (crate::Result<crate::Data>, crate::Data) {
+ let expected = expected.map(|d| d.normalize(NormalizeNewlines));
+ // On `expected` being an error, make a best guess
+ let format = expected.as_ref().map(|e| e.format()).unwrap_or_default();
+ actual = actual.try_coerce(format);
+
+ if self.normalize_paths {
+ actual = actual.normalize(NormalizePaths);
+ }
+ // Always normalize new lines
+ actual = actual.normalize(NormalizeNewlines);
+
+ // If expected is not an error normalize matches
+ if let Ok(expected) = expected.as_ref() {
+ actual = actual.normalize(NormalizeMatches::new(&self.substitutions, expected));
+ }
+
+ (expected, actual)
+ }
+
+ #[track_caller]
+ pub(crate) fn do_action(
+ &self,
+ expected: crate::Result<crate::Data>,
+ actual: crate::Data,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ expected_path: &std::path::Path,
+ ) {
+ let result =
+ expected.and_then(|e| self.try_verify(&e, &actual, expected_name, actual_name));
+ if let Err(err) = result {
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => {
+ use std::io::Write;
+
+ let _ = writeln!(
+ stderr(),
+ "{}: {}",
+ self.palette.warn("Ignoring failure"),
+ err
+ );
+ }
+ Action::Verify => {
+ let message = if let Some(action_var) = self.action_var.as_deref() {
+ self.palette
+ .hint(format!("Update with {}=overwrite", action_var))
+ } else {
+ crate::report::Styled::new(String::new(), Default::default())
+ };
+ panic!("{err}{message}");
+ }
+ Action::Overwrite => {
+ use std::io::Write;
+
+ let _ = writeln!(stderr(), "{}: {}", self.palette.warn("Fixing"), err);
+ actual.write_to(expected_path).unwrap();
+ }
+ }
+ }
+ }
+
+ pub(crate) fn try_verify(
+ &self,
+ expected: &crate::Data,
+ actual: &crate::Data,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ ) -> crate::Result<()> {
+ if expected != actual {
+ let mut buf = String::new();
+ crate::report::write_diff(
+ &mut buf,
+ expected,
+ actual,
+ expected_name,
+ actual_name,
+ self.palette,
+ )
+ .map_err(|e| e.to_string())?;
+ Err(buf.into())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+/// # Directory Assertions
+#[cfg(feature = "path")]
+impl Assert {
+ #[track_caller]
+ pub fn subset_eq(
+ &self,
+ expected_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) {
+ let expected_root = expected_root.into();
+ let actual_root = actual_root.into();
+ self.subset_eq_inner(expected_root, actual_root)
+ }
+
+ #[track_caller]
+ fn subset_eq_inner(&self, expected_root: std::path::PathBuf, actual_root: std::path::PathBuf) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let checks: Vec<_> =
+ crate::path::PathDiff::subset_eq_iter_inner(expected_root, actual_root).collect();
+ self.verify(checks);
+ }
+
+ #[track_caller]
+ pub fn subset_matches(
+ &self,
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ self.subset_matches_inner(pattern_root, actual_root)
+ }
+
+ #[track_caller]
+ fn subset_matches_inner(
+ &self,
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ ) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let checks: Vec<_> = crate::path::PathDiff::subset_matches_iter_inner(
+ expected_root,
+ actual_root,
+ &self.substitutions,
+ )
+ .collect();
+ self.verify(checks);
+ }
+
+ #[track_caller]
+ fn verify(
+ &self,
+ mut checks: Vec<Result<(std::path::PathBuf, std::path::PathBuf), crate::path::PathDiff>>,
+ ) {
+ if checks.iter().all(Result::is_ok) {
+ for check in checks {
+ let (_expected_path, _actual_path) = check.unwrap();
+ crate::debug!(
+ "{}: is {}",
+ _expected_path.display(),
+ self.palette.info("good")
+ );
+ }
+ } else {
+ checks.sort_by_key(|c| match c {
+ Ok((expected_path, _actual_path)) => Some(expected_path.clone()),
+ Err(diff) => diff.expected_path().map(|p| p.to_owned()),
+ });
+
+ let mut buffer = String::new();
+ let mut ok = true;
+ for check in checks {
+ use std::fmt::Write;
+ match check {
+ Ok((expected_path, _actual_path)) => {
+ let _ = writeln!(
+ &mut buffer,
+ "{}: is {}",
+ expected_path.display(),
+ self.palette.info("good"),
+ );
+ }
+ Err(diff) => {
+ let _ = diff.write(&mut buffer, self.palette);
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore | Action::Verify => {
+ ok = false;
+ }
+ Action::Overwrite => {
+ if let Err(err) = diff.overwrite() {
+ ok = false;
+ let path = diff
+ .expected_path()
+ .expect("always present when overwrite can fail");
+ let _ = writeln!(
+ &mut buffer,
+ "{} to overwrite {}: {}",
+ self.palette.error("Failed"),
+ path.display(),
+ err
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ if ok {
+ use std::io::Write;
+ let _ = write!(stderr(), "{}", buffer);
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => {
+ let _ =
+ write!(stderr(), "{}", self.palette.warn("Ignoring above failures"));
+ }
+ Action::Verify => unreachable!("Something had to fail to get here"),
+ Action::Overwrite => {
+ let _ = write!(
+ stderr(),
+ "{}",
+ self.palette.warn("Overwrote above failures")
+ );
+ }
+ }
+ } else {
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => unreachable!("Shouldn't be able to fail"),
+ Action::Verify => {
+ use std::fmt::Write;
+ if let Some(action_var) = self.action_var.as_deref() {
+ writeln!(
+ &mut buffer,
+ "{}",
+ self.palette
+ .hint(format_args!("Update with {}=overwrite", action_var))
+ )
+ .unwrap();
+ }
+ }
+ Action::Overwrite => {}
+ }
+ panic!("{}", buffer);
+ }
+ }
+ }
+}
+
+/// # Customize Behavior
+impl Assert {
+ /// Override the color palette
+ pub fn palette(mut self, palette: crate::report::Palette) -> Self {
+ self.palette = palette;
+ self
+ }
+
+ /// Read the failure action from an environment variable
+ pub fn action_env(mut self, var_name: &str) -> Self {
+ let action = Action::with_env_var(var_name);
+ self.action = action.unwrap_or(self.action);
+ self.action_var = Some(var_name.to_owned());
+ self
+ }
+
+ /// Override the failure action
+ pub fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self.action_var = None;
+ self
+ }
+
+ /// Override the default [`Substitutions`][crate::Substitutions]
+ pub fn substitutions(mut self, substitutions: crate::Substitutions) -> Self {
+ self.substitutions = substitutions;
+ self
+ }
+
+ /// Specify whether text should have path separators normalized
+ ///
+ /// The default is normalized
+ pub fn normalize_paths(mut self, yes: bool) -> Self {
+ self.normalize_paths = yes;
+ self
+ }
+
+ /// Specify whether the content should be treated as binary or not
+ ///
+ /// The default is to auto-detect
+ pub fn binary(mut self, yes: bool) -> Self {
+ self.data_format = if yes {
+ Some(DataFormat::Binary)
+ } else {
+ Some(DataFormat::Text)
+ };
+ self
+ }
+
+ pub(crate) fn data_format(&self) -> Option<DataFormat> {
+ self.data_format
+ }
+}
+
+impl Default for Assert {
+ fn default() -> Self {
+ Self {
+ action: Default::default(),
+ action_var: Default::default(),
+ normalize_paths: true,
+ substitutions: Default::default(),
+ palette: crate::report::Palette::color(),
+ data_format: Default::default(),
+ }
+ .substitutions(crate::Substitutions::with_exe())
+ }
+}
diff --git a/vendor/snapbox/src/bin/snap-fixture.rs b/vendor/snapbox/src/bin/snap-fixture.rs
new file mode 100644
index 0000000..7e21fc6
--- /dev/null
+++ b/vendor/snapbox/src/bin/snap-fixture.rs
@@ -0,0 +1,61 @@
+//! For `snapbox`s tests only
+
+use std::env;
+use std::error::Error;
+use std::io;
+use std::io::Write;
+use std::process;
+
+fn run() -> Result<(), Box<dyn Error>> {
+ if let Ok(text) = env::var("stdout") {
+ println!("{}", text);
+ }
+ if let Ok(text) = env::var("stderr") {
+ eprintln!("{}", text);
+ }
+
+ if env::var("echo_large").as_deref() == Ok("1") {
+ for i in 0..(128 * 1024) {
+ println!("{}", i);
+ }
+ }
+
+ if env::var("echo_cwd").as_deref() == Ok("1") {
+ if let Ok(cwd) = std::env::current_dir() {
+ eprintln!("{}", cwd.display());
+ }
+ }
+
+ if let Ok(raw) = env::var("write") {
+ let (path, text) = raw.split_once('=').unwrap_or((raw.as_str(), ""));
+ std::fs::write(path.trim(), text.trim()).unwrap();
+ }
+
+ if let Ok(path) = env::var("cat") {
+ let text = std::fs::read_to_string(path).unwrap();
+ eprintln!("{}", text);
+ }
+
+ if let Some(timeout) = env::var("sleep").ok().and_then(|s| s.parse().ok()) {
+ std::thread::sleep(std::time::Duration::from_secs(timeout));
+ }
+
+ let code = env::var("exit")
+ .ok()
+ .map(|v| v.parse::<i32>())
+ .map(|r| r.map(Some))
+ .unwrap_or(Ok(None))?
+ .unwrap_or(0);
+ process::exit(code);
+}
+
+fn main() {
+ let code = match run() {
+ Ok(_) => 0,
+ Err(ref e) => {
+ write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail");
+ 1
+ }
+ };
+ process::exit(code);
+}
diff --git a/vendor/snapbox/src/cmd.rs b/vendor/snapbox/src/cmd.rs
new file mode 100644
index 0000000..3b2a19f
--- /dev/null
+++ b/vendor/snapbox/src/cmd.rs
@@ -0,0 +1,1170 @@
+//! Run commands and assert on their behavior
+
+#[cfg(feature = "color")]
+use anstream::panic;
+
+/// Process spawning for testing of non-interactive commands
+#[derive(Debug)]
+pub struct Command {
+ cmd: std::process::Command,
+ stdin: Option<crate::Data>,
+ timeout: Option<std::time::Duration>,
+ _stderr_to_stdout: bool,
+ config: crate::Assert,
+}
+
+/// # Builder API
+impl Command {
+ pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
+ Self {
+ cmd: std::process::Command::new(program),
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Constructs a new `Command` from a `std` `Command`.
+ pub fn from_std(cmd: std::process::Command) -> Self {
+ Self {
+ cmd,
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Adds an argument to pass to the program.
+ ///
+ /// Only one argument can be passed per use. So instead of:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C /path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// usage would be:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C")
+ /// .arg("/path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// To pass multiple arguments see [`args`].
+ ///
+ /// [`args`]: Command::args()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .arg("-l")
+ /// .arg("-a")
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.arg(arg);
+ self
+ }
+
+ /// Adds multiple arguments to pass to the program.
+ ///
+ /// To pass a single argument see [`arg`].
+ ///
+ /// [`arg`]: Command::arg()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .args(&["-l", "-a"])
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
+ self.cmd.args(args);
+ self
+ }
+
+ /// Inserts or updates an environment variable mapping.
+ ///
+ /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
+ /// and case-sensitive on all other platforms.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env("PATH", "/bin")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env(
+ mut self,
+ key: impl AsRef<std::ffi::OsStr>,
+ value: impl AsRef<std::ffi::OsStr>,
+ ) -> Self {
+ self.cmd.env(key, value);
+ self
+ }
+
+ /// Adds or updates multiple environment variable mappings.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ /// use std::process::Stdio;
+ /// use std::env;
+ /// use std::collections::HashMap;
+ ///
+ /// let filtered_env : HashMap<String, String> =
+ /// env::vars().filter(|&(ref k, _)|
+ /// k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
+ /// ).collect();
+ ///
+ /// Command::new("printenv")
+ /// .env_clear()
+ /// .envs(&filtered_env)
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn envs(
+ mut self,
+ vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
+ ) -> Self {
+ self.cmd.envs(vars);
+ self
+ }
+
+ /// Removes an environment variable mapping.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_remove("PATH")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.env_remove(key);
+ self
+ }
+
+ /// Clears the entire environment map for the child process.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_clear()
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_clear(mut self) -> Self {
+ self.cmd.env_clear();
+ self
+ }
+
+ /// Sets the working directory for the child process.
+ ///
+ /// # Platform-specific behavior
+ ///
+ /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
+ /// whether it should be interpreted relative to the parent's working
+ /// directory or relative to `current_dir`. The behavior in this case is
+ /// platform specific and unstable, and it's recommended to use
+ /// [`canonicalize`] to get an absolute program path instead.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .current_dir("/bin")
+ /// .assert()
+ /// .success();
+ /// ```
+ ///
+ /// [`canonicalize`]: std::fs::canonicalize()
+ pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
+ self.cmd.current_dir(dir);
+ self
+ }
+
+ /// Write `buffer` to `stdin` when the `Command` is run.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ pub fn stdin(mut self, stream: impl Into<crate::Data>) -> Self {
+ self.stdin = Some(stream.into());
+ self
+ }
+
+ /// Error out if a timeout is reached
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .timeout(std::time::Duration::from_secs(1))
+ /// .env("sleep", "100")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[cfg(feature = "cmd")]
+ pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
+ self.timeout = Some(timeout);
+ self
+ }
+
+ /// Merge `stderr` into `stdout`
+ #[cfg(feature = "cmd")]
+ pub fn stderr_to_stdout(mut self) -> Self {
+ self._stderr_to_stdout = true;
+ self
+ }
+}
+
+/// # Run Command
+impl Command {
+ /// Run the command and assert on the results
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ #[track_caller]
+ pub fn assert(self) -> OutputAssert {
+ let config = self.config.clone();
+ match self.output() {
+ Ok(output) => OutputAssert::new(output).with_assert(config),
+ Err(err) => {
+ panic!("Failed to spawn: {}", err)
+ }
+ }
+ }
+
+ /// Run the command and capture the `Output`
+ #[cfg(feature = "cmd")]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ if self._stderr_to_stdout {
+ self.single_output()
+ } else {
+ self.split_output()
+ }
+ }
+
+ #[cfg(not(feature = "cmd"))]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ self.split_output()
+ }
+
+ #[cfg(feature = "cmd")]
+ fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ let (reader, writer) = os_pipe::pipe()?;
+ let writer_clone = writer.try_clone()?;
+ self.cmd.stdout(writer);
+ self.cmd.stderr(writer_clone);
+ let mut child = self.cmd.spawn()?;
+ // Avoid a deadlock! This parent process is still holding open pipe
+ // writers (inside the Command object), and we have to close those
+ // before we read. Here we do this by dropping the Command object.
+ drop(self.cmd);
+
+ let stdout = process_single_io(
+ &mut child,
+ reader,
+ self.stdin.as_ref().map(|d| d.to_bytes()),
+ )?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout.join().unwrap().ok().unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr: Default::default(),
+ })
+ }
+
+ fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ self.cmd.stdout(std::process::Stdio::piped());
+ self.cmd.stderr(std::process::Stdio::piped());
+ let mut child = self.cmd.spawn()?;
+
+ let (stdout, stderr) =
+ process_split_io(&mut child, self.stdin.as_ref().map(|d| d.to_bytes()))?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+ let stderr = stderr
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr,
+ })
+ }
+}
+
+fn process_split_io(
+ child: &mut std::process::Child,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = child.stdout.take().map(threaded_read);
+ let stderr = child.stderr.take().map(threaded_read);
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok((stdout, stderr))
+}
+
+#[cfg(feature = "cmd")]
+fn process_single_io(
+ child: &mut std::process::Child,
+ stdout: os_pipe::PipeReader,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<Stream> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = threaded_read(stdout);
+ debug_assert!(child.stdout.is_none());
+ debug_assert!(child.stderr.is_none());
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok(stdout)
+}
+
+type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;
+
+fn threaded_read<R>(mut input: R) -> Stream
+where
+ R: std::io::Read + Send + 'static,
+{
+ std::thread::spawn(move || {
+ let mut ret = Vec::new();
+ input.read_to_end(&mut ret).map(|_| ret)
+ })
+}
+
+impl From<std::process::Command> for Command {
+ fn from(cmd: std::process::Command) -> Self {
+ Self::from_std(cmd)
+ }
+}
+
+/// Assert the state of a [`Command`]'s [`Output`].
+///
+/// Create an `OutputAssert` through the [`Command::assert`].
+///
+/// [`Output`]: std::process::Output
+pub struct OutputAssert {
+ output: std::process::Output,
+ config: crate::Assert,
+}
+
+impl OutputAssert {
+ /// Create an `Assert` for a given [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn new(output: std::process::Output) -> Self {
+ Self {
+ output,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Access the contained [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn get_output(&self) -> &std::process::Output {
+ &self.output
+ }
+
+ /// Ensure the command succeeded.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .assert()
+ /// .success();
+ /// ```
+ #[track_caller]
+ pub fn success(self) -> Self {
+ if !self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("success"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command failed.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "1")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[track_caller]
+ pub fn failure(self) -> Self {
+ if self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("failure"),
+ self.config.palette.error("success")
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command aborted before returning a code.
+ #[track_caller]
+ pub fn interrupted(self) -> Self {
+ if self.output.status.code().is_some() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("interrupted"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command returned the expected code.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "42")
+ /// .assert()
+ /// .code(42);
+ /// ```
+ #[track_caller]
+ pub fn code(self, expected: i32) -> Self {
+ if self.output.status.code() != Some(expected) {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info(expected),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq("hello");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches("he[..]o");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq("world");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches("wo[..]d");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ fn write_status(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ writeln!(
+ writer,
+ "Exit status: {}",
+ display_exit_status(self.output.status)
+ )?;
+ Ok(())
+ }
+
+ fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stdout.is_empty() {
+ writeln!(writer, "stdout:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+
+ fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stderr.is_empty() {
+ writeln!(writer, "stderr:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(not(feature = "cmd"))]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ basic_exit_status(status)
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(feature = "cmd")]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ #[cfg(unix)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use std::os::unix::process::*;
+
+ let signal = status.signal()?;
+ let name = match signal as libc::c_int {
+ libc::SIGABRT => ", SIGABRT: process abort signal",
+ libc::SIGALRM => ", SIGALRM: alarm clock",
+ libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
+ libc::SIGHUP => ", SIGHUP: hangup",
+ libc::SIGILL => ", SIGILL: illegal instruction",
+ libc::SIGINT => ", SIGINT: terminal interrupt signal",
+ libc::SIGKILL => ", SIGKILL: kill",
+ libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
+ libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
+ libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
+ libc::SIGTERM => ", SIGTERM: termination signal",
+ libc::SIGBUS => ", SIGBUS: access to undefined memory",
+ #[cfg(not(target_os = "haiku"))]
+ libc::SIGSYS => ", SIGSYS: bad system call",
+ libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
+ _ => "",
+ };
+ Some(format!("signal: {}{}", signal, name))
+ }
+
+ #[cfg(windows)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use windows_sys::Win32::Foundation::*;
+
+ let extra = match status.code().unwrap() as NTSTATUS {
+ STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
+ STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
+ STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
+ STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
+ STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
+ STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
+ STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
+ STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
+ STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
+ STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
+ STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
+ STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
+ STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
+ STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
+ STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
+ STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
+ STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
+ STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
+ STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
+ STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
+ STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
+ STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
+ STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
+ STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
+ STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
+ STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
+ STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
+ STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
+ STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
+ STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
+ STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
+ _ => return None,
+ };
+ Some(extra.to_owned())
+ }
+
+ if let Some(extra) = detailed_exit_status(status) {
+ format!("{} ({})", basic_exit_status(status), extra)
+ } else {
+ basic_exit_status(status)
+ }
+}
+
+fn basic_exit_status(status: std::process::ExitStatus) -> String {
+ if let Some(code) = status.code() {
+ code.to_string()
+ } else {
+ "interrupted".to_owned()
+ }
+}
+
+#[cfg(feature = "cmd")]
+fn wait(
+ mut child: std::process::Child,
+ timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ if let Some(timeout) = timeout {
+ wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
+ .transpose()
+ .unwrap_or_else(|| {
+ let _ = child.kill();
+ child.wait()
+ })
+ } else {
+ child.wait()
+ }
+}
+
+#[cfg(not(feature = "cmd"))]
+fn wait(
+ mut child: std::process::Child,
+ _timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ child.wait()
+}
+
+pub use snapbox_macros::cargo_bin;
+
+/// Look up the path to a cargo-built binary within an integration test.
+///
+/// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo
+pub fn cargo_bin(name: &str) -> std::path::PathBuf {
+ let file_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX);
+ let target_dir = target_dir();
+ target_dir.join(file_name)
+}
+
+// Adapted from
+// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
+fn target_dir() -> std::path::PathBuf {
+ std::env::current_exe()
+ .ok()
+ .map(|mut path| {
+ path.pop();
+ if path.ends_with("deps") {
+ path.pop();
+ }
+ path
+ })
+ .unwrap()
+}
+
+#[cfg(feature = "examples")]
+pub use examples::{compile_example, compile_examples};
+
+#[cfg(feature = "examples")]
+pub(crate) mod examples {
+ /// Prepare an example for testing
+ ///
+ /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It
+ /// will match the current target and profile but will not get feature flags. Pass those arguments
+ /// to the compiler via `args`.
+ ///
+ /// ## Example
+ ///
+ /// ```rust,no_run
+ /// snapbox::cmd::compile_example("snap-example-fixture", []);
+ /// ```
+ #[cfg(feature = "examples")]
+ pub fn compile_example<'a>(
+ target_name: &str,
+ args: impl IntoIterator<Item = &'a str>,
+ ) -> Result<std::path::PathBuf, crate::Error> {
+ crate::debug!("Compiling example {}", target_name);
+ let messages = escargot::CargoBuild::new()
+ .current_target()
+ .current_release()
+ .example(target_name)
+ .args(args)
+ .exec()
+ .map_err(|e| crate::Error::new(e.to_string()))?;
+ for message in messages {
+ let message = message.map_err(|e| crate::Error::new(e.to_string()))?;
+ let message = message
+ .decode()
+ .map_err(|e| crate::Error::new(e.to_string()))?;
+ crate::debug!("Message: {:?}", message);
+ if let Some(bin) = decode_example_message(&message) {
+ let (name, bin) = bin?;
+ assert_eq!(target_name, name);
+ return bin;
+ }
+ }
+
+ Err(crate::Error::new(format!(
+ "Unknown error building example {}",
+ target_name
+ )))
+ }
+
+ /// Prepare all examples for testing
+ ///
+ /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It
+ /// will match the current target and profile but will not get feature flags. Pass those arguments
+ /// to the compiler via `args`.
+ ///
+ /// ## Example
+ ///
+ /// ```rust,no_run
+ /// let examples = snapbox::cmd::compile_examples([]).unwrap().collect::<Vec<_>>();
+ /// ```
+ #[cfg(feature = "examples")]
+ pub fn compile_examples<'a>(
+ args: impl IntoIterator<Item = &'a str>,
+ ) -> Result<
+ impl Iterator<Item = (String, Result<std::path::PathBuf, crate::Error>)>,
+ crate::Error,
+ > {
+ crate::debug!("Compiling examples");
+ let mut examples = std::collections::BTreeMap::new();
+
+ let messages = escargot::CargoBuild::new()
+ .current_target()
+ .current_release()
+ .examples()
+ .args(args)
+ .exec()
+ .map_err(|e| crate::Error::new(e.to_string()))?;
+ for message in messages {
+ let message = message.map_err(|e| crate::Error::new(e.to_string()))?;
+ let message = message
+ .decode()
+ .map_err(|e| crate::Error::new(e.to_string()))?;
+ crate::debug!("Message: {:?}", message);
+ if let Some(bin) = decode_example_message(&message) {
+ let (name, bin) = bin?;
+ examples.insert(name.to_owned(), bin);
+ }
+ }
+
+ Ok(examples.into_iter())
+ }
+
+ #[allow(clippy::type_complexity)]
+ fn decode_example_message<'m>(
+ message: &'m escargot::format::Message,
+ ) -> Option<Result<(&'m str, Result<std::path::PathBuf, crate::Error>), crate::Error>> {
+ match message {
+ escargot::format::Message::CompilerMessage(msg) => {
+ let level = msg.message.level;
+ if level == escargot::format::diagnostic::DiagnosticLevel::Ice
+ || level == escargot::format::diagnostic::DiagnosticLevel::Error
+ {
+ let output = msg
+ .message
+ .rendered
+ .as_deref()
+ .unwrap_or_else(|| msg.message.message.as_ref())
+ .to_owned();
+ if is_example_target(&msg.target) {
+ let bin = Err(crate::Error::new(output));
+ Some(Ok((msg.target.name.as_ref(), bin)))
+ } else {
+ Some(Err(crate::Error::new(output)))
+ }
+ } else {
+ None
+ }
+ }
+ escargot::format::Message::CompilerArtifact(artifact) => {
+ if !artifact.profile.test && is_example_target(&artifact.target) {
+ let path = artifact
+ .executable
+ .clone()
+ .expect("cargo is new enough for this to be present");
+ let bin = Ok(path.into_owned());
+ Some(Ok((artifact.target.name.as_ref(), bin)))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+ }
+
+ fn is_example_target(target: &escargot::format::Target) -> bool {
+ target.crate_types == ["bin"] && target.kind == ["example"]
+ }
+}
diff --git a/vendor/snapbox/src/data.rs b/vendor/snapbox/src/data.rs
new file mode 100644
index 0000000..b892ffd
--- /dev/null
+++ b/vendor/snapbox/src/data.rs
@@ -0,0 +1,940 @@
+/// Test fixture, actual output, or expected result
+///
+/// This provides conveniences for tracking the intended format (binary vs text).
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Data {
+ inner: DataInner,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum DataInner {
+ Binary(Vec<u8>),
+ Text(String),
+ #[cfg(feature = "json")]
+ Json(serde_json::Value),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)]
+pub enum DataFormat {
+ Binary,
+ #[default]
+ Text,
+ #[cfg(feature = "json")]
+ Json,
+}
+
+impl Data {
+ /// Mark the data as binary (no post-processing)
+ pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
+ Self {
+ inner: DataInner::Binary(raw.into()),
+ }
+ }
+
+ /// Mark the data as text (post-processing)
+ pub fn text(raw: impl Into<String>) -> Self {
+ Self {
+ inner: DataInner::Text(raw.into()),
+ }
+ }
+
+ #[cfg(feature = "json")]
+ pub fn json(raw: impl Into<serde_json::Value>) -> Self {
+ Self {
+ inner: DataInner::Json(raw.into()),
+ }
+ }
+
+ /// Empty test data
+ pub fn new() -> Self {
+ Self::text("")
+ }
+
+ /// Load test data from a file
+ pub fn read_from(
+ path: &std::path::Path,
+ data_format: Option<DataFormat>,
+ ) -> Result<Self, crate::Error> {
+ let data = match data_format {
+ Some(df) => match df {
+ DataFormat::Binary => {
+ let data = std::fs::read(path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::binary(data)
+ }
+ DataFormat::Text => {
+ let data = std::fs::read_to_string(path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::text(data)
+ }
+ #[cfg(feature = "json")]
+ DataFormat::Json => {
+ let data = std::fs::read_to_string(path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::json(serde_json::from_str::<serde_json::Value>(&data).unwrap())
+ }
+ },
+ None => {
+ let data = std::fs::read(path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ let data = Self::binary(data);
+ match path
+ .extension()
+ .and_then(|e| e.to_str())
+ .unwrap_or_default()
+ {
+ #[cfg(feature = "json")]
+ "json" => data.try_coerce(DataFormat::Json),
+ _ => data.try_coerce(DataFormat::Text),
+ }
+ }
+ };
+ Ok(data)
+ }
+
+ /// Overwrite a snapshot
+ pub fn write_to(&self, path: &std::path::Path) -> Result<(), crate::Error> {
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent).map_err(|e| {
+ format!("Failed to create parent dir for {}: {}", path.display(), e)
+ })?;
+ }
+ std::fs::write(path, self.to_bytes())
+ .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
+ }
+
+ /// Post-process text
+ ///
+ /// See [utils][crate::utils]
+ pub fn normalize(self, op: impl Normalize) -> Self {
+ op.normalize(self)
+ }
+
+ /// Return the underlying `String`
+ ///
+ /// Note: this will not inspect binary data for being a valid `String`.
+ pub fn render(&self) -> Option<String> {
+ match &self.inner {
+ DataInner::Binary(_) => None,
+ DataInner::Text(data) => Some(data.to_owned()),
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()),
+ }
+ }
+
+ pub fn to_bytes(&self) -> Vec<u8> {
+ match &self.inner {
+ DataInner::Binary(data) => data.clone(),
+ DataInner::Text(data) => data.clone().into_bytes(),
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => serde_json::to_vec_pretty(value).unwrap(),
+ }
+ }
+
+ pub fn try_coerce(self, format: DataFormat) -> Self {
+ match (self.inner, format) {
+ (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner),
+ (DataInner::Text(inner), DataFormat::Text) => Self::text(inner),
+ #[cfg(feature = "json")]
+ (DataInner::Json(inner), DataFormat::Json) => Self::json(inner),
+ (DataInner::Binary(inner), _) => {
+ if is_binary(&inner) {
+ Self::binary(inner)
+ } else {
+ match String::from_utf8(inner) {
+ Ok(str) => {
+ let coerced = Self::text(str).try_coerce(format);
+ // if the Text cannot be coerced into the correct format
+ // reset it back to Binary
+ if coerced.format() != format {
+ coerced.try_coerce(DataFormat::Binary)
+ } else {
+ coerced
+ }
+ }
+ Err(err) => {
+ let bin = err.into_bytes();
+ Self::binary(bin)
+ }
+ }
+ }
+ }
+ #[cfg(feature = "json")]
+ (DataInner::Text(inner), DataFormat::Json) => {
+ match serde_json::from_str::<serde_json::Value>(&inner) {
+ Ok(json) => Self::json(json),
+ Err(_) => Self::text(inner),
+ }
+ }
+ (inner, DataFormat::Binary) => Self::binary(Self { inner }.to_bytes()),
+ // This variant is already covered unless structured data is enabled
+ #[cfg(feature = "structured-data")]
+ (inner, DataFormat::Text) => {
+ let remake = Self { inner };
+ if let Some(str) = remake.render() {
+ Self::text(str)
+ } else {
+ remake
+ }
+ }
+ }
+ }
+
+ /// Outputs the current `DataFormat` of the underlying data
+ pub fn format(&self) -> DataFormat {
+ match &self.inner {
+ DataInner::Binary(_) => DataFormat::Binary,
+ DataInner::Text(_) => DataFormat::Text,
+ #[cfg(feature = "json")]
+ DataInner::Json(_) => DataFormat::Json,
+ }
+ }
+}
+
+impl std::fmt::Display for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self.inner {
+ DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f),
+ DataInner::Text(data) => data.fmt(f),
+ #[cfg(feature = "json")]
+ DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
+ }
+ }
+}
+
+impl Default for Data {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'d> From<&'d Data> for Data {
+ fn from(other: &'d Data) -> Self {
+ other.clone()
+ }
+}
+
+impl From<Vec<u8>> for Data {
+ fn from(other: Vec<u8>) -> Self {
+ Self::binary(other)
+ }
+}
+
+impl<'b> From<&'b [u8]> for Data {
+ fn from(other: &'b [u8]) -> Self {
+ other.to_owned().into()
+ }
+}
+
+impl From<String> for Data {
+ fn from(other: String) -> Self {
+ Self::text(other)
+ }
+}
+
+impl<'s> From<&'s String> for Data {
+ fn from(other: &'s String) -> Self {
+ other.clone().into()
+ }
+}
+
+impl<'s> From<&'s str> for Data {
+ fn from(other: &'s str) -> Self {
+ other.to_owned().into()
+ }
+}
+
+pub trait Normalize {
+ fn normalize(&self, data: Data) -> Data;
+}
+
+pub struct NormalizeNewlines;
+impl Normalize for NormalizeNewlines {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = crate::utils::normalize_lines(&text);
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ normalize_value(&mut value, crate::utils::normalize_lines);
+ Data::json(value)
+ }
+ }
+ }
+}
+
+pub struct NormalizePaths;
+impl Normalize for NormalizePaths {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = crate::utils::normalize_paths(&text);
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ normalize_value(&mut value, crate::utils::normalize_paths);
+ Data::json(value)
+ }
+ }
+ }
+}
+
+pub struct NormalizeMatches<'a> {
+ substitutions: &'a crate::Substitutions,
+ pattern: &'a Data,
+}
+
+impl<'a> NormalizeMatches<'a> {
+ pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self {
+ NormalizeMatches {
+ substitutions,
+ pattern,
+ }
+ }
+}
+
+impl Normalize for NormalizeMatches<'_> {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = self
+ .substitutions
+ .normalize(&text, &self.pattern.render().unwrap());
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ if let DataInner::Json(exp) = &self.pattern.inner {
+ normalize_value_matches(&mut value, exp, self.substitutions);
+ }
+ Data::json(value)
+ }
+ }
+ }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) {
+ match value {
+ serde_json::Value::String(str) => {
+ *str = op(str);
+ }
+ serde_json::Value::Array(arr) => {
+ for value in arr.iter_mut() {
+ normalize_value(value, op)
+ }
+ }
+ serde_json::Value::Object(obj) => {
+ for (_, value) in obj.iter_mut() {
+ normalize_value(value, op)
+ }
+ }
+ _ => {}
+ }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value_matches(
+ actual: &mut serde_json::Value,
+ expected: &serde_json::Value,
+ substitutions: &crate::Substitutions,
+) {
+ use serde_json::Value::*;
+ match (actual, expected) {
+ // "{...}" is a wildcard
+ (act, String(exp)) if exp == "{...}" => {
+ *act = serde_json::json!("{...}");
+ }
+ (String(act), String(exp)) => {
+ *act = substitutions.normalize(act, exp);
+ }
+ (Array(act), Array(exp)) => {
+ let wildcard = String("{...}".to_string());
+ let mut sections = exp.split(|e| e == &wildcard).peekable();
+ let mut processed = 0;
+ while let Some(expected_subset) = sections.next() {
+ // Process all values in the current section
+ if !expected_subset.is_empty() {
+ let actual_subset = &mut act[processed..processed + expected_subset.len()];
+ for (a, e) in actual_subset.iter_mut().zip(expected_subset) {
+ normalize_value_matches(a, e, substitutions);
+ }
+ processed += expected_subset.len();
+ }
+
+ if let Some(next_section) = sections.peek() {
+ // If the next section has nothing in it, replace from processed to end with
+ // a single "{...}"
+ if next_section.is_empty() {
+ act.splice(processed.., vec![wildcard.clone()]);
+ processed += 1;
+ } else {
+ let first = next_section.first().unwrap();
+ // Replace everything up until the value we are looking for with
+ // a single "{...}".
+ if let Some(index) = act.iter().position(|v| v == first) {
+ act.splice(processed..index, vec![wildcard.clone()]);
+ processed += 1;
+ } else {
+ // If we cannot find the value we are looking for return early
+ break;
+ }
+ }
+ }
+ }
+ }
+ (Object(act), Object(exp)) => {
+ for (a, e) in act.iter_mut().zip(exp).filter(|(a, e)| a.0 == e.0) {
+ normalize_value_matches(a.1, e.1, substitutions)
+ }
+ }
+ (_, _) => {}
+ }
+}
+
+#[cfg(feature = "detect-encoding")]
+fn is_binary(data: &[u8]) -> bool {
+ match content_inspector::inspect(data) {
+ content_inspector::ContentType::BINARY |
+ // We don't support these
+ content_inspector::ContentType::UTF_16LE |
+ content_inspector::ContentType::UTF_16BE |
+ content_inspector::ContentType::UTF_32LE |
+ content_inspector::ContentType::UTF_32BE => {
+ true
+ },
+ content_inspector::ContentType::UTF_8 |
+ content_inspector::ContentType::UTF_8_BOM => {
+ false
+ },
+ }
+}
+
+#[cfg(not(feature = "detect-encoding"))]
+fn is_binary(_data: &[u8]) -> bool {
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[cfg(feature = "json")]
+ use serde_json::json;
+
+ // Tests for checking to_bytes and render produce the same results
+ #[test]
+ fn text_to_bytes_render() {
+ let d = Data::text(String::from("test"));
+ let bytes = d.to_bytes();
+ let bytes = String::from_utf8(bytes).unwrap();
+ let rendered = d.render().unwrap();
+ assert_eq!(bytes, rendered);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_bytes_render() {
+ let d = Data::json(json!({"name": "John\\Doe\r\n"}));
+ let bytes = d.to_bytes();
+ let bytes = String::from_utf8(bytes).unwrap();
+ let rendered = d.render().unwrap();
+ assert_eq!(bytes, rendered);
+ }
+
+ // Tests for checking all types are coercible to each other and
+ // for when the coercion should fail
+ #[test]
+ fn binary_to_text() {
+ let binary = String::from("test").into_bytes();
+ let d = Data::binary(binary);
+ let text = d.try_coerce(DataFormat::Text);
+ assert_eq!(DataFormat::Text, text.format())
+ }
+
+ #[test]
+ fn binary_to_text_not_utf8() {
+ let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Text);
+ assert_ne!(DataFormat::Text, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let binary = serde_json::to_vec_pretty(&value).unwrap();
+ let d = Data::binary(binary);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Json, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json_not_utf8() {
+ let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Json);
+ assert_ne!(DataFormat::Json, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json_not_json() {
+ let binary = String::from("test").into_bytes();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Json);
+ assert_ne!(DataFormat::Json, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ fn text_to_binary() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let binary = d.try_coerce(DataFormat::Binary);
+ assert_eq!(DataFormat::Binary, binary.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn text_to_json() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let text = serde_json::to_string_pretty(&value).unwrap();
+ let d = Data::text(text);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Json, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn text_to_json_not_json() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Text, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_binary() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(value);
+ let binary = d.try_coerce(DataFormat::Binary);
+ assert_eq!(DataFormat::Binary, binary.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_text() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(value);
+ let text = d.try_coerce(DataFormat::Text);
+ assert_eq!(DataFormat::Text, text.format());
+ }
+
+ // Tests for coercible conversions create the same output as to_bytes/render
+ //
+ // render does not need to be checked against bin -> text since render
+ // outputs None for binary
+ #[test]
+ fn text_to_bin_coerce_equals_to_bytes() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let binary = d.clone().try_coerce(DataFormat::Binary);
+ assert_eq!(Data::binary(d.to_bytes()), binary);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_bin_coerce_equals_to_bytes() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(json);
+ let binary = d.clone().try_coerce(DataFormat::Binary);
+ assert_eq!(Data::binary(d.to_bytes()), binary);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_text_coerce_equals_render() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(json);
+ let text = d.clone().try_coerce(DataFormat::Text);
+ assert_eq!(Data::text(d.render().unwrap()), text);
+ }
+
+ // Tests for normalization on json
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_paths_and_lines() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data);
+ let data = data.normalize(NormalizeNewlines);
+ assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_obj_paths_and_lines() {
+ let json = json!({
+ "person": {
+ "name": "John\\Doe\r\n",
+ "nickname": "Jo\\hn\r\n",
+ }
+ });
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let assert = json!({
+ "person": {
+ "name": "John/Doe\r\n",
+ "nickname": "Jo/hn\r\n",
+ }
+ });
+ assert_eq!(Data::json(assert), data);
+ let data = data.normalize(NormalizeNewlines);
+ let assert = json!({
+ "person": {
+ "name": "John/Doe\n",
+ "nickname": "Jo/hn\n",
+ }
+ });
+ assert_eq!(Data::json(assert), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_array_paths_and_lines() {
+ let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]});
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]});
+ assert_eq!(Data::json(paths), data);
+ let data = data.normalize(NormalizeNewlines);
+ let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]});
+ assert_eq!(Data::json(new_lines), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_array_obj_paths_and_lines() {
+ let json = json!({
+ "people": [
+ {
+ "name": "John\\Doe\r\n",
+ "nickname": "Jo\\hn\r\n",
+ }
+ ]
+ });
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let paths = json!({
+ "people": [
+ {
+ "name": "John/Doe\r\n",
+ "nickname": "Jo/hn\r\n",
+ }
+ ]
+ });
+ assert_eq!(Data::json(paths), data);
+ let data = data.normalize(NormalizeNewlines);
+ let new_lines = json!({
+ "people": [
+ {
+ "name": "John/Doe\n",
+ "nickname": "Jo/hn\n",
+ }
+ ]
+ });
+ assert_eq!(Data::json(new_lines), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_string() {
+ let exp = json!({"name": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({"name": "JohnDoe"});
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_array() {
+ let exp = json!({"people": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": [
+ {
+ "name": "JohnDoe",
+ "nickname": "John",
+ }
+ ]
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_obj() {
+ let exp = json!({"people": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": {
+ "name": "JohnDoe",
+ "nickname": "John",
+ }
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_diff_order_array() {
+ let exp = json!({
+ "people": ["John", "Jane"]
+ });
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": ["Jane", "John"]
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_ne!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_wildcard_object_first() {
+ let exp = json!({
+ "people": [
+ "{...}",
+ {
+ "name": "three",
+ "nickname": "3",
+ }
+ ]
+ });
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": [
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ {
+ "name": "three",
+ "nickname": "3",
+ }
+ ]
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_wildcard_array_first() {
+ let exp = json!([
+ "{...}",
+ {
+ "name": "three",
+ "nickname": "3",
+ }
+ ]);
+ let expected = Data::json(exp);
+ let actual = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ {
+ "name": "three",
+ "nickname": "3",
+ }
+ ]);
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_wildcard_array_first_last() {
+ let exp = json!([
+ "{...}",
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ "{...}"
+ ]);
+ let expected = Data::json(exp);
+ let actual = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ {
+ "name": "three",
+ "nickname": "3",
+ },
+ {
+ "name": "four",
+ "nickname": "4",
+ }
+ ]);
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_wildcard_array_middle_last() {
+ let exp = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ "{...}",
+ {
+ "name": "three",
+ "nickname": "3",
+ },
+ "{...}"
+ ]);
+ let expected = Data::json(exp);
+ let actual = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ {
+ "name": "three",
+ "nickname": "3",
+ },
+ {
+ "name": "four",
+ "nickname": "4",
+ },
+ {
+ "name": "five",
+ "nickname": "5",
+ }
+ ]);
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_wildcard_array_middle_last_early_return() {
+ let exp = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ "{...}",
+ {
+ "name": "three",
+ "nickname": "3",
+ },
+ "{...}"
+ ]);
+ let expected = Data::json(exp);
+ let actual = json!([
+ {
+ "name": "one",
+ "nickname": "1",
+ },
+ {
+ "name": "two",
+ "nickname": "2",
+ },
+ {
+ "name": "four",
+ "nickname": "4",
+ },
+ {
+ "name": "five",
+ "nickname": "5",
+ }
+ ]);
+ let actual_normalized = Data::json(actual.clone()).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let DataInner::Json(act) = actual_normalized.inner {
+ assert_eq!(act, actual);
+ }
+ }
+}
diff --git a/vendor/snapbox/src/error.rs b/vendor/snapbox/src/error.rs
new file mode 100644
index 0000000..55e9018
--- /dev/null
+++ b/vendor/snapbox/src/error.rs
@@ -0,0 +1,95 @@
+#[derive(Clone, Debug)]
+pub struct Error {
+ inner: String,
+ backtrace: Option<Backtrace>,
+}
+
+impl Error {
+ pub fn new(inner: impl std::fmt::Display) -> Self {
+ Self::with_string(inner.to_string())
+ }
+
+ fn with_string(inner: String) -> Self {
+ Self {
+ inner,
+ backtrace: Backtrace::new(),
+ }
+ }
+}
+
+impl PartialEq for Error {
+ fn eq(&self, other: &Self) -> bool {
+ self.inner == other.inner
+ }
+}
+
+impl Eq for Error {}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{}", self.inner)?;
+ if let Some(backtrace) = self.backtrace.as_ref() {
+ writeln!(f)?;
+ writeln!(f, "Backtrace:")?;
+ writeln!(f, "{}", backtrace)?;
+ }
+ Ok(())
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl<'s> From<&'s str> for Error {
+ fn from(other: &'s str) -> Self {
+ Self::with_string(other.to_owned())
+ }
+}
+
+impl<'s> From<&'s String> for Error {
+ fn from(other: &'s String) -> Self {
+ Self::with_string(other.clone())
+ }
+}
+
+impl From<String> for Error {
+ fn from(other: String) -> Self {
+ Self::with_string(other)
+ }
+}
+
+#[cfg(feature = "debug")]
+#[derive(Debug, Clone)]
+struct Backtrace(backtrace::Backtrace);
+
+#[cfg(feature = "debug")]
+impl Backtrace {
+ fn new() -> Option<Self> {
+ Some(Self(backtrace::Backtrace::new()))
+ }
+}
+
+#[cfg(feature = "debug")]
+impl std::fmt::Display for Backtrace {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ // `backtrace::Backtrace` uses `Debug` instead of `Display`
+ write!(f, "{:?}", self.0)
+ }
+}
+
+#[cfg(not(feature = "debug"))]
+#[derive(Debug, Copy, Clone)]
+struct Backtrace;
+
+#[cfg(not(feature = "debug"))]
+impl Backtrace {
+ fn new() -> Option<Self> {
+ None
+ }
+}
+
+#[cfg(not(feature = "debug"))]
+impl std::fmt::Display for Backtrace {
+ fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result {
+ Ok(())
+ }
+}
diff --git a/vendor/snapbox/src/harness.rs b/vendor/snapbox/src/harness.rs
new file mode 100644
index 0000000..77e085e
--- /dev/null
+++ b/vendor/snapbox/src/harness.rs
@@ -0,0 +1,214 @@
+//! [`Harness`] for discovering test inputs and asserting against snapshot files
+//!
+//! # Examples
+//!
+//! ```rust,no_run
+//! snapbox::harness::Harness::new(
+//! "tests/fixtures/invalid",
+//! setup,
+//! test,
+//! )
+//! .select(["tests/cases/*.in"])
+//! .action_env("SNAPSHOTS")
+//! .test();
+//!
+//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case {
+//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned();
+//! let expected = input_path.with_extension("out");
+//! snapbox::harness::Case {
+//! name,
+//! fixture: input_path,
+//! expected,
+//! }
+//! }
+//!
+//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> {
+//! let raw = std::fs::read_to_string(input_path)?;
+//! let num = raw.parse::<usize>()?;
+//!
+//! let actual = num + 10;
+//!
+//! Ok(actual)
+//! }
+//! ```
+
+use crate::data::{DataFormat, NormalizeNewlines};
+use crate::Action;
+
+use libtest_mimic::Trial;
+
+pub struct Harness<S, T> {
+ root: std::path::PathBuf,
+ overrides: Option<ignore::overrides::Override>,
+ setup: S,
+ test: T,
+ action: Action,
+}
+
+impl<S, T, I, E> Harness<S, T>
+where
+ I: std::fmt::Display,
+ E: std::fmt::Display,
+ S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static,
+ T: Fn(&std::path::Path) -> Result<I, E> + Send + Sync + 'static + Clone,
+{
+ pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
+ Self {
+ root: root.into(),
+ overrides: None,
+ setup,
+ test,
+ action: Action::Verify,
+ }
+ }
+
+ /// Path patterns for selecting input files
+ ///
+ /// This used gitignore syntax
+ pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
+ let mut overrides = ignore::overrides::OverrideBuilder::new(&self.root);
+ for line in patterns {
+ overrides.add(line).unwrap();
+ }
+ self.overrides = Some(overrides.build().unwrap());
+ self
+ }
+
+ /// Read the failure action from an environment variable
+ pub fn action_env(mut self, var_name: &str) -> Self {
+ let action = Action::with_env_var(var_name);
+ self.action = action.unwrap_or(self.action);
+ self
+ }
+
+ /// Override the failure action
+ pub fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self
+ }
+
+ /// Run tests
+ pub fn test(self) -> ! {
+ let mut walk = ignore::WalkBuilder::new(&self.root);
+ walk.standard_filters(false);
+ let tests = walk.build().filter_map(|entry| {
+ let entry = entry.unwrap();
+ let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+ if let Some(overrides) = &self.overrides {
+ overrides
+ .matched(&path, is_dir)
+ .is_whitelist()
+ .then_some(path)
+ } else {
+ Some(path)
+ }
+ });
+
+ let tests: Vec<_> = tests
+ .into_iter()
+ .map(|path| {
+ let case = (self.setup)(path);
+ let test = self.test.clone();
+ Trial::test(case.name.clone(), move || {
+ let actual = (test)(&case.fixture)?;
+ let actual = actual.to_string();
+ let actual = crate::Data::text(actual).normalize(NormalizeNewlines);
+ #[allow(deprecated)]
+ let verify = Verifier::new()
+ .palette(crate::report::Palette::auto())
+ .action(self.action);
+ verify.verify(&case.expected, actual)?;
+ Ok(())
+ })
+ .with_ignored_flag(self.action == Action::Ignore)
+ })
+ .collect();
+
+ let args = libtest_mimic::Arguments::from_args();
+ libtest_mimic::run(&args, tests).exit()
+ }
+}
+
+struct Verifier {
+ palette: crate::report::Palette,
+ action: Action,
+}
+
+impl Verifier {
+ fn new() -> Self {
+ Default::default()
+ }
+
+ fn palette(mut self, palette: crate::report::Palette) -> Self {
+ self.palette = palette;
+ self
+ }
+
+ fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self
+ }
+
+ fn verify(&self, expected_path: &std::path::Path, actual: crate::Data) -> crate::Result<()> {
+ match self.action {
+ Action::Skip => Ok(()),
+ Action::Ignore => {
+ let _ = self.try_verify(expected_path, actual);
+ Ok(())
+ }
+ Action::Verify => self.try_verify(expected_path, actual),
+ Action::Overwrite => self.try_overwrite(expected_path, actual),
+ }
+ }
+
+ fn try_overwrite(
+ &self,
+ expected_path: &std::path::Path,
+ actual: crate::Data,
+ ) -> crate::Result<()> {
+ actual.write_to(expected_path)?;
+ Ok(())
+ }
+
+ fn try_verify(
+ &self,
+ expected_path: &std::path::Path,
+ actual: crate::Data,
+ ) -> crate::Result<()> {
+ let expected = crate::Data::read_from(expected_path, Some(DataFormat::Text))?
+ .normalize(NormalizeNewlines);
+
+ if expected != actual {
+ let mut buf = String::new();
+ crate::report::write_diff(
+ &mut buf,
+ &expected,
+ &actual,
+ Some(&expected_path.display()),
+ None,
+ self.palette,
+ )
+ .map_err(|e| e.to_string())?;
+ Err(buf.into())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl Default for Verifier {
+ fn default() -> Self {
+ Self {
+ #[allow(deprecated)]
+ palette: crate::report::Palette::auto(),
+ action: Action::Verify,
+ }
+ }
+}
+
+pub struct Case {
+ pub name: String,
+ pub fixture: std::path::PathBuf,
+ pub expected: std::path::PathBuf,
+}
diff --git a/vendor/snapbox/src/lib.rs b/vendor/snapbox/src/lib.rs
new file mode 100644
index 0000000..7084c15
--- /dev/null
+++ b/vendor/snapbox/src/lib.rs
@@ -0,0 +1,246 @@
+//! # Snapshot testing toolbox
+//!
+//! > When you have to treat your tests like pets, instead of [cattle][trycmd]
+//!
+//! `snapbox` is a snapshot-testing toolbox that is ready to use for verifying output from
+//! - Function return values
+//! - CLI stdout/stderr
+//! - Filesystem changes
+//!
+//! It is also flexible enough to build your own test harness like [trycmd](https://crates.io/crates/trycmd).
+//!
+//! ## Which tool is right
+//!
+//! - [cram](https://bitheap.org/cram/): End-to-end CLI snapshotting agnostic of any programming language
+//! - [trycmd](https://crates.io/crates/trycmd): For running a lot of blunt tests (limited test predicates)
+//! - Particular attention is given to allow the test data to be pulled into documentation, like
+//! with [mdbook](https://rust-lang.github.io/mdBook/)
+//! - `snapbox`: When you want something like `trycmd` in one off
+//! cases or you need to customize `trycmd`s behavior.
+//! - [assert_cmd](https://crates.io/crates/assert_cmd) +
+//! [assert_fs](https://crates.io/crates/assert_fs): Test cases follow a certain pattern but
+//! special attention is needed in how to verify the results.
+//! - Hand-written test cases: for peculiar circumstances
+//!
+//! ## Getting Started
+//!
+//! Testing Functions:
+//! - [`assert_eq`][crate::assert_eq] and [`assert_matches`] for reusing diffing / pattern matching for non-snapshot testing
+//! - [`assert_eq_path`][crate::assert_eq_path] and [`assert_matches_path`] for one-off assertions with the snapshot stored in a file
+//! - [`harness::Harness`] for discovering test inputs and asserting against snapshot files:
+//!
+//! Testing Commands:
+//! - [`cmd::Command`]: Process spawning for testing of non-interactive commands
+//! - [`cmd::OutputAssert`]: Assert the state of a [`Command`][cmd::Command]'s
+//! [`Output`][std::process::Output].
+//!
+//! Testing Filesystem Interactions:
+//! - [`path::PathFixture`]: Working directory for tests
+//! - [`Assert`]: Diff a directory against files present in a pattern directory
+//!
+//! You can also build your own version of these with the lower-level building blocks these are
+//! made of.
+//!
+#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
+//!
+//! # Examples
+//!
+//! [`assert_matches`]
+//! ```rust
+//! snapbox::assert_matches("Hello [..] people!", "Hello many people!");
+//! ```
+//!
+//! [`Assert`]
+//! ```rust,no_run
+//! let actual = "...";
+//! let expected_path = "tests/fixtures/help_output_is_clean.txt";
+//! snapbox::Assert::new()
+//! .action_env("SNAPSHOTS")
+//! .matches_path(expected_path, actual);
+//! ```
+//!
+//! [`harness::Harness`]
+#![cfg_attr(not(feature = "harness"), doc = " ```rust,ignore")]
+#![cfg_attr(feature = "harness", doc = " ```rust,no_run")]
+//! snapbox::harness::Harness::new(
+//! "tests/fixtures/invalid",
+//! setup,
+//! test,
+//! )
+//! .select(["tests/cases/*.in"])
+//! .action_env("SNAPSHOTS")
+//! .test();
+//!
+//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case {
+//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned();
+//! let expected = input_path.with_extension("out");
+//! snapbox::harness::Case {
+//! name,
+//! fixture: input_path,
+//! expected,
+//! }
+//! }
+//!
+//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> {
+//! let raw = std::fs::read_to_string(input_path)?;
+//! let num = raw.parse::<usize>()?;
+//!
+//! let actual = num + 10;
+//!
+//! Ok(actual)
+//! }
+//! ```
+//!
+//! [trycmd]: https://docs.rs/trycmd
+
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+
+mod action;
+mod assert;
+mod data;
+mod error;
+mod substitutions;
+
+pub mod cmd;
+pub mod path;
+pub mod report;
+pub mod utils;
+
+#[cfg(feature = "harness")]
+pub mod harness;
+
+pub use action::Action;
+pub use action::DEFAULT_ACTION_ENV;
+pub use assert::Assert;
+pub use data::Data;
+pub use data::DataFormat;
+pub use data::{Normalize, NormalizeMatches, NormalizeNewlines, NormalizePaths};
+pub use error::Error;
+pub use snapbox_macros::debug;
+pub use substitutions::Substitutions;
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+/// Check if a value is the same as an expected value
+///
+/// When the content is text, newlines are normalized.
+///
+/// ```rust
+/// let output = "something";
+/// let expected = "something";
+/// snapbox::assert_eq(expected, output);
+/// ```
+#[track_caller]
+pub fn assert_eq(expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ Assert::new().eq(expected, actual);
+}
+
+/// Check if a value matches a pattern
+///
+/// Pattern syntax:
+/// - `...` is a line-wildcard when on a line by itself
+/// - `[..]` is a character-wildcard when inside a line
+/// - `[EXE]` matches `.exe` on Windows
+///
+/// Normalization:
+/// - Newlines
+/// - `\` to `/`
+///
+/// ```rust
+/// let output = "something";
+/// let expected = "so[..]g";
+/// snapbox::assert_matches(expected, output);
+/// ```
+#[track_caller]
+pub fn assert_matches(pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ Assert::new().matches(pattern, actual);
+}
+
+/// Check if a value matches the content of a file
+///
+/// When the content is text, newlines are normalized.
+///
+/// ```rust,no_run
+/// let output = "something";
+/// let expected_path = "tests/snapshots/output.txt";
+/// snapbox::assert_eq_path(expected_path, output);
+/// ```
+#[track_caller]
+pub fn assert_eq_path(expected_path: impl AsRef<std::path::Path>, actual: impl Into<crate::Data>) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .eq_path(expected_path, actual);
+}
+
+/// Check if a value matches the pattern in a file
+///
+/// Pattern syntax:
+/// - `...` is a line-wildcard when on a line by itself
+/// - `[..]` is a character-wildcard when inside a line
+/// - `[EXE]` matches `.exe` on Windows
+///
+/// Normalization:
+/// - Newlines
+/// - `\` to `/`
+///
+/// ```rust,no_run
+/// let output = "something";
+/// let expected_path = "tests/snapshots/output.txt";
+/// snapbox::assert_matches_path(expected_path, output);
+/// ```
+#[track_caller]
+pub fn assert_matches_path(
+ pattern_path: impl AsRef<std::path::Path>,
+ actual: impl Into<crate::Data>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .matches_path(pattern_path, actual);
+}
+
+/// Check if a path matches the content of another path, recursively
+///
+/// When the content is text, newlines are normalized.
+///
+/// ```rust,no_run
+/// let output_root = "...";
+/// let expected_root = "tests/snapshots/output.txt";
+/// snapbox::assert_subset_eq(expected_root, output_root);
+/// ```
+#[cfg(feature = "path")]
+#[track_caller]
+pub fn assert_subset_eq(
+ expected_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .subset_eq(expected_root, actual_root);
+}
+
+/// Check if a path matches the pattern of another path, recursively
+///
+/// Pattern syntax:
+/// - `...` is a line-wildcard when on a line by itself
+/// - `[..]` is a character-wildcard when inside a line
+/// - `[EXE]` matches `.exe` on Windows
+///
+/// Normalization:
+/// - Newlines
+/// - `\` to `/`
+///
+/// ```rust,no_run
+/// let output_root = "...";
+/// let expected_root = "tests/snapshots/output.txt";
+/// snapbox::assert_subset_matches(expected_root, output_root);
+/// ```
+#[cfg(feature = "path")]
+#[track_caller]
+pub fn assert_subset_matches(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .subset_matches(pattern_root, actual_root);
+}
diff --git a/vendor/snapbox/src/path.rs b/vendor/snapbox/src/path.rs
new file mode 100644
index 0000000..523b038
--- /dev/null
+++ b/vendor/snapbox/src/path.rs
@@ -0,0 +1,687 @@
+//! Initialize working directories and assert on how they've changed
+
+#[cfg(feature = "path")]
+use crate::data::{NormalizeMatches, NormalizeNewlines, NormalizePaths};
+/// Working directory for tests
+#[derive(Debug)]
+pub struct PathFixture(PathFixtureInner);
+
+#[derive(Debug)]
+enum PathFixtureInner {
+ None,
+ Immutable(std::path::PathBuf),
+ #[cfg(feature = "path")]
+ MutablePath(std::path::PathBuf),
+ #[cfg(feature = "path")]
+ MutableTemp {
+ temp: tempfile::TempDir,
+ path: std::path::PathBuf,
+ },
+}
+
+impl PathFixture {
+ pub fn none() -> Self {
+ Self(PathFixtureInner::None)
+ }
+
+ pub fn immutable(target: &std::path::Path) -> Self {
+ Self(PathFixtureInner::Immutable(target.to_owned()))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn mutable_temp() -> Result<Self, crate::Error> {
+ let temp = tempfile::tempdir().map_err(|e| e.to_string())?;
+ // We need to get the `/private` prefix on Mac so variable substitutions work
+ // correctly
+ let path = canonicalize(temp.path())
+ .map_err(|e| format!("Failed to canonicalize {}: {}", temp.path().display(), e))?;
+ Ok(Self(PathFixtureInner::MutableTemp { temp, path }))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn mutable_at(target: &std::path::Path) -> Result<Self, crate::Error> {
+ let _ = std::fs::remove_dir_all(target);
+ std::fs::create_dir_all(target)
+ .map_err(|e| format!("Failed to create {}: {}", target.display(), e))?;
+ Ok(Self(PathFixtureInner::MutablePath(target.to_owned())))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn with_template(self, template_root: &std::path::Path) -> Result<Self, crate::Error> {
+ match &self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => {
+ return Err("Sandboxing is disabled".into());
+ }
+ PathFixtureInner::MutablePath(path) | PathFixtureInner::MutableTemp { path, .. } => {
+ crate::debug!(
+ "Initializing {} from {}",
+ path.display(),
+ template_root.display()
+ );
+ copy_template(template_root, path)?;
+ }
+ }
+
+ Ok(self)
+ }
+
+ pub fn is_mutable(&self) -> bool {
+ match &self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => false,
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(_) => true,
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { .. } => true,
+ }
+ }
+
+ pub fn path(&self) -> Option<&std::path::Path> {
+ match &self.0 {
+ PathFixtureInner::None => None,
+ PathFixtureInner::Immutable(path) => Some(path.as_path()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(path) => Some(path.as_path()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { path, .. } => Some(path.as_path()),
+ }
+ }
+
+ /// Explicitly close to report errors
+ pub fn close(self) -> Result<(), std::io::Error> {
+ match self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => Ok(()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(_) => Ok(()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { temp, .. } => temp.close(),
+ }
+ }
+}
+
+impl Default for PathFixture {
+ fn default() -> Self {
+ Self::none()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum PathDiff {
+ Failure(crate::Error),
+ TypeMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_type: FileType,
+ actual_type: FileType,
+ },
+ LinkMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_target: std::path::PathBuf,
+ actual_target: std::path::PathBuf,
+ },
+ ContentMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_content: crate::Data,
+ actual_content: crate::Data,
+ },
+}
+
+impl PathDiff {
+ /// Report differences between `actual_root` and `pattern_root`
+ ///
+ /// Note: Requires feature flag `path`
+ #[cfg(feature = "path")]
+ pub fn subset_eq_iter(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ Self::subset_eq_iter_inner(pattern_root, actual_root)
+ }
+
+ #[cfg(feature = "path")]
+ pub(crate) fn subset_eq_iter_inner(
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
+ let walker = Walk::new(&expected_root);
+ walker.map(move |r| {
+ let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
+ let rel = expected_path.strip_prefix(&expected_root).unwrap();
+ let actual_path = actual_root.join(rel);
+
+ let expected_type = FileType::from_path(&expected_path);
+ let actual_type = FileType::from_path(&actual_path);
+ if expected_type != actual_type {
+ return Err(Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type,
+ actual_type,
+ });
+ }
+
+ match expected_type {
+ FileType::Symlink => {
+ let expected_target = std::fs::read_link(&expected_path).ok();
+ let actual_target = std::fs::read_link(&actual_path).ok();
+ if expected_target != actual_target {
+ return Err(Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: expected_target.unwrap(),
+ actual_target: actual_target.unwrap(),
+ });
+ }
+ }
+ FileType::File => {
+ let mut actual =
+ crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?;
+
+ let expected = crate::Data::read_from(&expected_path, None)
+ .map(|d| d.normalize(NormalizeNewlines))
+ .map_err(Self::Failure)?;
+
+ actual = actual
+ .try_coerce(expected.format())
+ .normalize(NormalizeNewlines);
+
+ if expected != actual {
+ return Err(Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content: expected,
+ actual_content: actual,
+ });
+ }
+ }
+ FileType::Dir | FileType::Unknown | FileType::Missing => {}
+ }
+
+ Ok((expected_path, actual_path))
+ })
+ }
+
+ /// Report differences between `actual_root` and `pattern_root`
+ ///
+ /// Note: Requires feature flag `path`
+ #[cfg(feature = "path")]
+ pub fn subset_matches_iter(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ substitutions: &crate::Substitutions,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ Self::subset_matches_iter_inner(pattern_root, actual_root, substitutions)
+ }
+
+ #[cfg(feature = "path")]
+ pub(crate) fn subset_matches_iter_inner(
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ substitutions: &crate::Substitutions,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
+ let walker = Walk::new(&expected_root);
+ walker.map(move |r| {
+ let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
+ let rel = expected_path.strip_prefix(&expected_root).unwrap();
+ let actual_path = actual_root.join(rel);
+
+ let expected_type = FileType::from_path(&expected_path);
+ let actual_type = FileType::from_path(&actual_path);
+ if expected_type != actual_type {
+ return Err(Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type,
+ actual_type,
+ });
+ }
+
+ match expected_type {
+ FileType::Symlink => {
+ let expected_target = std::fs::read_link(&expected_path).ok();
+ let actual_target = std::fs::read_link(&actual_path).ok();
+ if expected_target != actual_target {
+ return Err(Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: expected_target.unwrap(),
+ actual_target: actual_target.unwrap(),
+ });
+ }
+ }
+ FileType::File => {
+ let mut actual =
+ crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?;
+
+ let expected = crate::Data::read_from(&expected_path, None)
+ .map(|d| d.normalize(NormalizeNewlines))
+ .map_err(Self::Failure)?;
+
+ actual = actual
+ .try_coerce(expected.format())
+ .normalize(NormalizePaths)
+ .normalize(NormalizeNewlines)
+ .normalize(NormalizeMatches::new(substitutions, &expected));
+
+ if expected != actual {
+ return Err(Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content: expected,
+ actual_content: actual,
+ });
+ }
+ }
+ FileType::Dir | FileType::Unknown | FileType::Missing => {}
+ }
+
+ Ok((expected_path, actual_path))
+ })
+ }
+}
+
+impl PathDiff {
+ pub fn expected_path(&self) -> Option<&std::path::Path> {
+ match &self {
+ Self::Failure(_msg) => None,
+ Self::TypeMismatch {
+ expected_path,
+ actual_path: _,
+ expected_type: _,
+ actual_type: _,
+ } => Some(expected_path),
+ Self::LinkMismatch {
+ expected_path,
+ actual_path: _,
+ expected_target: _,
+ actual_target: _,
+ } => Some(expected_path),
+ Self::ContentMismatch {
+ expected_path,
+ actual_path: _,
+ expected_content: _,
+ actual_content: _,
+ } => Some(expected_path),
+ }
+ }
+
+ pub fn write(
+ &self,
+ f: &mut dyn std::fmt::Write,
+ palette: crate::report::Palette,
+ ) -> Result<(), std::fmt::Error> {
+ match &self {
+ Self::Failure(msg) => {
+ writeln!(f, "{}", palette.error(msg))?;
+ }
+ Self::TypeMismatch {
+ expected_path,
+ actual_path: _actual_path,
+ expected_type,
+ actual_type,
+ } => {
+ writeln!(
+ f,
+ "{}: Expected {}, was {}",
+ expected_path.display(),
+ palette.info(expected_type),
+ palette.error(actual_type)
+ )?;
+ }
+ Self::LinkMismatch {
+ expected_path,
+ actual_path: _actual_path,
+ expected_target,
+ actual_target,
+ } => {
+ writeln!(
+ f,
+ "{}: Expected {}, was {}",
+ expected_path.display(),
+ palette.info(expected_target.display()),
+ palette.error(actual_target.display())
+ )?;
+ }
+ Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content,
+ actual_content,
+ } => {
+ crate::report::write_diff(
+ f,
+ expected_content,
+ actual_content,
+ Some(&expected_path.display()),
+ Some(&actual_path.display()),
+ palette,
+ )?;
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn overwrite(&self) -> Result<(), crate::Error> {
+ match self {
+ // Not passing the error up because users most likely want to treat a processing error
+ // differently than an overwrite error
+ Self::Failure(_err) => Ok(()),
+ Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type: _,
+ actual_type,
+ } => {
+ match actual_type {
+ FileType::Dir => {
+ std::fs::remove_dir_all(expected_path).map_err(|e| {
+ format!("Failed to remove {}: {}", expected_path.display(), e)
+ })?;
+ }
+ FileType::File | FileType::Symlink => {
+ std::fs::remove_file(expected_path).map_err(|e| {
+ format!("Failed to remove {}: {}", expected_path.display(), e)
+ })?;
+ }
+ FileType::Unknown | FileType::Missing => {}
+ }
+ shallow_copy(expected_path, actual_path)
+ }
+ Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: _,
+ actual_target: _,
+ } => shallow_copy(expected_path, actual_path),
+ Self::ContentMismatch {
+ expected_path,
+ actual_path: _,
+ expected_content: _,
+ actual_content,
+ } => actual_content.write_to(expected_path),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum FileType {
+ Dir,
+ File,
+ Symlink,
+ Unknown,
+ Missing,
+}
+
+impl FileType {
+ pub fn from_path(path: &std::path::Path) -> Self {
+ let meta = path.symlink_metadata();
+ match meta {
+ Ok(meta) => {
+ if meta.is_dir() {
+ Self::Dir
+ } else if meta.is_file() {
+ Self::File
+ } else {
+ let target = std::fs::read_link(path).ok();
+ if target.is_some() {
+ Self::Symlink
+ } else {
+ Self::Unknown
+ }
+ }
+ }
+ Err(err) => match err.kind() {
+ std::io::ErrorKind::NotFound => Self::Missing,
+ _ => Self::Unknown,
+ },
+ }
+ }
+}
+
+impl FileType {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Dir => "dir",
+ Self::File => "file",
+ Self::Symlink => "symlink",
+ Self::Unknown => "unknown",
+ Self::Missing => "missing",
+ }
+ }
+}
+
+impl std::fmt::Display for FileType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_str().fmt(f)
+ }
+}
+
+/// Recursively walk a path
+///
+/// Note: Ignores `.keep` files
+#[cfg(feature = "path")]
+pub struct Walk {
+ inner: walkdir::IntoIter,
+}
+
+#[cfg(feature = "path")]
+impl Walk {
+ pub fn new(path: &std::path::Path) -> Self {
+ Self {
+ inner: walkdir::WalkDir::new(path).into_iter(),
+ }
+ }
+}
+
+#[cfg(feature = "path")]
+impl Iterator for Walk {
+ type Item = Result<std::path::PathBuf, std::io::Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while let Some(entry) = self.inner.next().map(|e| {
+ e.map(walkdir::DirEntry::into_path)
+ .map_err(std::io::Error::from)
+ }) {
+ if entry.as_ref().ok().and_then(|e| e.file_name())
+ != Some(std::ffi::OsStr::new(".keep"))
+ {
+ return Some(entry);
+ }
+ }
+ None
+ }
+}
+
+/// Copy a template into a [`PathFixture`]
+///
+/// Note: Generally you'll use [`PathFixture::with_template`] instead.
+///
+/// Note: Ignores `.keep` files
+#[cfg(feature = "path")]
+pub fn copy_template(
+ source: impl AsRef<std::path::Path>,
+ dest: impl AsRef<std::path::Path>,
+) -> Result<(), crate::Error> {
+ let source = source.as_ref();
+ let dest = dest.as_ref();
+ let source = canonicalize(source)
+ .map_err(|e| format!("Failed to canonicalize {}: {}", source.display(), e))?;
+ std::fs::create_dir_all(dest)
+ .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
+ let dest = canonicalize(dest)
+ .map_err(|e| format!("Failed to canonicalize {}: {}", dest.display(), e))?;
+
+ for current in Walk::new(&source) {
+ let current = current.map_err(|e| e.to_string())?;
+ let rel = current.strip_prefix(&source).unwrap();
+ let target = dest.join(rel);
+
+ shallow_copy(&current, &target)?;
+ }
+
+ Ok(())
+}
+
+/// Copy a file system entry, without recursing
+fn shallow_copy(source: &std::path::Path, dest: &std::path::Path) -> Result<(), crate::Error> {
+ let meta = source
+ .symlink_metadata()
+ .map_err(|e| format!("Failed to read metadata from {}: {}", source.display(), e))?;
+ if meta.is_dir() {
+ std::fs::create_dir_all(dest)
+ .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
+ } else if meta.is_file() {
+ std::fs::copy(source, dest).map_err(|e| {
+ format!(
+ "Failed to copy {} to {}: {}",
+ source.display(),
+ dest.display(),
+ e
+ )
+ })?;
+ // Avoid a mtime check race where:
+ // - Copy files
+ // - Test checks mtime
+ // - Test writes
+ // - Test checks mtime
+ //
+ // If all of this happens too close to each other, then the second mtime check will think
+ // nothing was written by the test.
+ //
+ // Instead of just setting 1s in the past, we'll just respect the existing mtime.
+ copy_stats(&meta, dest).map_err(|e| {
+ format!(
+ "Failed to copy {} metadata to {}: {}",
+ source.display(),
+ dest.display(),
+ e
+ )
+ })?;
+ } else if let Ok(target) = std::fs::read_link(source) {
+ symlink_to_file(dest, &target)
+ .map_err(|e| format!("Failed to create symlink {}: {}", dest.display(), e))?;
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "path")]
+fn copy_stats(
+ source_meta: &std::fs::Metadata,
+ dest: &std::path::Path,
+) -> Result<(), std::io::Error> {
+ let src_mtime = filetime::FileTime::from_last_modification_time(source_meta);
+ filetime::set_file_mtime(dest, src_mtime)?;
+
+ Ok(())
+}
+
+#[cfg(not(feature = "path"))]
+fn copy_stats(
+ _source_meta: &std::fs::Metadata,
+ _dest: &std::path::Path,
+) -> Result<(), std::io::Error> {
+ Ok(())
+}
+
+#[cfg(windows)]
+fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
+ std::os::windows::fs::symlink_file(target, link)
+}
+
+#[cfg(not(windows))]
+fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
+ std::os::unix::fs::symlink(target, link)
+}
+
+pub fn resolve_dir(
+ path: impl AsRef<std::path::Path>,
+) -> Result<std::path::PathBuf, std::io::Error> {
+ let path = path.as_ref();
+ let meta = std::fs::symlink_metadata(path)?;
+ if meta.is_dir() {
+ canonicalize(path)
+ } else if meta.is_file() {
+ // Git might checkout symlinks as files
+ let target = std::fs::read_to_string(path)?;
+ let target_path = path.parent().unwrap().join(target);
+ resolve_dir(target_path)
+ } else {
+ canonicalize(path)
+ }
+}
+
+fn canonicalize(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
+ #[cfg(feature = "path")]
+ {
+ dunce::canonicalize(path)
+ }
+ #[cfg(not(feature = "path"))]
+ {
+ // Hope for the best
+ Ok(strip_trailing_slash(path).to_owned())
+ }
+}
+
+pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
+ path.components().as_path()
+}
+
+pub(crate) fn display_relpath(path: impl AsRef<std::path::Path>) -> String {
+ let path = path.as_ref();
+ let relpath = if let Ok(cwd) = std::env::current_dir() {
+ match path.strip_prefix(cwd) {
+ Ok(path) => path,
+ Err(_) => path,
+ }
+ } else {
+ path
+ };
+ relpath.display().to_string()
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn strips_trailing_slash() {
+ let path = std::path::Path::new("/foo/bar/");
+ let rendered = path.display().to_string();
+ assert_eq!(rendered.as_bytes()[rendered.len() - 1], b'/');
+
+ let stripped = strip_trailing_slash(path);
+ let rendered = stripped.display().to_string();
+ assert_eq!(rendered.as_bytes()[rendered.len() - 1], b'r');
+ }
+
+ #[test]
+ fn file_type_detect_file() {
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
+ dbg!(&path);
+ let actual = FileType::from_path(&path);
+ assert_eq!(actual, FileType::File);
+ }
+
+ #[test]
+ fn file_type_detect_dir() {
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+ dbg!(path);
+ let actual = FileType::from_path(path);
+ assert_eq!(actual, FileType::Dir);
+ }
+
+ #[test]
+ fn file_type_detect_missing() {
+ let path = std::path::Path::new("this-should-never-exist");
+ dbg!(path);
+ let actual = FileType::from_path(path);
+ assert_eq!(actual, FileType::Missing);
+ }
+}
diff --git a/vendor/snapbox/src/report/color.rs b/vendor/snapbox/src/report/color.rs
new file mode 100644
index 0000000..38f4f8f
--- /dev/null
+++ b/vendor/snapbox/src/report/color.rs
@@ -0,0 +1,111 @@
+#[derive(Copy, Clone, Debug, Default)]
+pub struct Palette {
+ pub(crate) info: anstyle::Style,
+ pub(crate) warn: anstyle::Style,
+ pub(crate) error: anstyle::Style,
+ pub(crate) hint: anstyle::Style,
+ pub(crate) expected: anstyle::Style,
+ pub(crate) actual: anstyle::Style,
+}
+
+impl Palette {
+ pub fn color() -> Self {
+ if cfg!(feature = "color") {
+ Self {
+ info: anstyle::AnsiColor::Green.on_default(),
+ warn: anstyle::AnsiColor::Yellow.on_default(),
+ error: anstyle::AnsiColor::Red.on_default(),
+ hint: anstyle::Effects::DIMMED.into(),
+ expected: anstyle::AnsiColor::Green.on_default() | anstyle::Effects::UNDERLINE,
+ actual: anstyle::AnsiColor::Red.on_default() | anstyle::Effects::UNDERLINE,
+ }
+ } else {
+ Self::plain()
+ }
+ }
+
+ pub fn plain() -> Self {
+ Self::default()
+ }
+
+ #[deprecated(since = "0.4.9", note = "Renamed to `Palette::color")]
+ pub fn always() -> Self {
+ Self::color()
+ }
+
+ #[deprecated(since = "0.4.9", note = "Renamed to `Palette::plain")]
+ pub fn never() -> Self {
+ Self::plain()
+ }
+
+ #[deprecated(
+ since = "0.4.9",
+ note = "Use `Palette::always`, `auto` behavior is now implicit"
+ )]
+ pub fn auto() -> Self {
+ if is_colored() {
+ Self::color()
+ } else {
+ Self::plain()
+ }
+ }
+
+ pub fn info<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.info)
+ }
+
+ pub fn warn<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.warn)
+ }
+
+ pub fn error<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.error)
+ }
+
+ pub fn hint<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.hint)
+ }
+
+ pub fn expected<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.expected)
+ }
+
+ pub fn actual<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ Styled::new(item, self.actual)
+ }
+}
+
+fn is_colored() -> bool {
+ #[cfg(feature = "color")]
+ {
+ anstream::AutoStream::choice(&std::io::stderr()) != anstream::ColorChoice::Never
+ }
+ #[cfg(not(feature = "color"))]
+ {
+ false
+ }
+}
+
+pub(crate) use anstyle::Style;
+
+#[derive(Debug)]
+pub struct Styled<D> {
+ display: D,
+ style: anstyle::Style,
+}
+
+impl<D: std::fmt::Display> Styled<D> {
+ pub(crate) fn new(display: D, style: anstyle::Style) -> Self {
+ Self { display, style }
+ }
+}
+
+impl<D: std::fmt::Display> std::fmt::Display for Styled<D> {
+ #[inline]
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.style.render())?;
+ self.display.fmt(f)?;
+ write!(f, "{}", self.style.render_reset())?;
+ Ok(())
+ }
+}
diff --git a/vendor/snapbox/src/report/diff.rs b/vendor/snapbox/src/report/diff.rs
new file mode 100644
index 0000000..b980733
--- /dev/null
+++ b/vendor/snapbox/src/report/diff.rs
@@ -0,0 +1,393 @@
+use crate::report::Styled;
+
+pub fn write_diff(
+ writer: &mut dyn std::fmt::Write,
+ expected: &crate::Data,
+ actual: &crate::Data,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ #[allow(unused_mut)]
+ let mut rendered = false;
+ #[cfg(feature = "diff")]
+ if let (Some(expected), Some(actual)) = (expected.render(), actual.render()) {
+ write_diff_inner(
+ writer,
+ &expected,
+ &actual,
+ expected_name,
+ actual_name,
+ palette,
+ )?;
+ rendered = true;
+ }
+
+ if !rendered {
+ if let Some(expected_name) = expected_name {
+ writeln!(writer, "{} {}:", expected_name, palette.error("(expected)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.error("Expected"))?;
+ }
+ writeln!(writer, "{}", palette.error(&expected))?;
+ if let Some(actual_name) = actual_name {
+ writeln!(writer, "{} {}:", actual_name, palette.info("(actual)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.info("Actual"))?;
+ }
+ writeln!(writer, "{}", palette.info(&actual))?;
+ }
+ Ok(())
+}
+
+#[cfg(feature = "diff")]
+fn write_diff_inner(
+ writer: &mut dyn std::fmt::Write,
+ expected: &str,
+ actual: &str,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ let timeout = std::time::Duration::from_millis(500);
+ let min_elide = 20;
+ let context = 5;
+
+ let changes = similar::TextDiff::configure()
+ .algorithm(similar::Algorithm::Patience)
+ .timeout(timeout)
+ .newline_terminated(false)
+ .diff_lines(expected, actual);
+
+ writeln!(writer)?;
+ if let Some(expected_name) = expected_name {
+ writeln!(
+ writer,
+ "{}",
+ palette.error(format_args!("{:->4} expected: {}", "", expected_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.error(format_args!("--- Expected")))?;
+ }
+ if let Some(actual_name) = actual_name {
+ writeln!(
+ writer,
+ "{}",
+ palette.info(format_args!("{:+>4} actual: {}", "", actual_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.info(format_args!("+++ Actual")))?;
+ }
+ let changes = changes
+ .ops()
+ .iter()
+ .flat_map(|op| changes.iter_inline_changes(op))
+ .collect::<Vec<_>>();
+ let tombstones = if min_elide < changes.len() {
+ let mut tombstones = vec![true; changes.len()];
+
+ let mut counter = context;
+ for (i, change) in changes.iter().enumerate() {
+ match change.tag() {
+ similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
+ counter = context;
+ tombstones[i] = false;
+ }
+ similar::ChangeTag::Equal => {
+ if counter != 0 {
+ tombstones[i] = false;
+ counter -= 1;
+ }
+ }
+ }
+ }
+
+ let mut counter = context;
+ for (i, change) in changes.iter().enumerate().rev() {
+ match change.tag() {
+ similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
+ counter = context;
+ tombstones[i] = false;
+ }
+ similar::ChangeTag::Equal => {
+ if counter != 0 {
+ tombstones[i] = false;
+ counter -= 1;
+ }
+ }
+ }
+ }
+ tombstones
+ } else {
+ Vec::new()
+ };
+
+ let mut elided = false;
+ for (i, change) in changes.into_iter().enumerate() {
+ if tombstones.get(i).copied().unwrap_or(false) {
+ if !elided {
+ let sign = "⋮";
+
+ write!(writer, "{:>4} ", " ",)?;
+ write!(writer, "{:>4} ", " ",)?;
+ writeln!(writer, "{}", palette.hint(sign))?;
+ }
+ elided = true;
+ } else {
+ elided = false;
+ match change.tag() {
+ similar::ChangeTag::Insert => {
+ write_change(writer, change, "+", palette.actual, palette.info, palette)?;
+ }
+ similar::ChangeTag::Delete => {
+ write_change(
+ writer,
+ change,
+ "-",
+ palette.expected,
+ palette.error,
+ palette,
+ )?;
+ }
+ similar::ChangeTag::Equal => {
+ write_change(writer, change, "|", palette.hint, palette.hint, palette)?;
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "diff")]
+fn write_change(
+ writer: &mut dyn std::fmt::Write,
+ change: similar::InlineChange<str>,
+ sign: &str,
+ em_style: crate::report::Style,
+ style: crate::report::Style,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ if let Some(index) = change.old_index() {
+ write!(writer, "{:>4} ", palette.hint(index + 1),)?;
+ } else {
+ write!(writer, "{:>4} ", " ",)?;
+ }
+ if let Some(index) = change.new_index() {
+ write!(writer, "{:>4} ", palette.hint(index + 1),)?;
+ } else {
+ write!(writer, "{:>4} ", " ",)?;
+ }
+ write!(writer, "{} ", Styled::new(sign, style))?;
+ for &(emphasized, change) in change.values() {
+ let cur_style = if emphasized { em_style } else { style };
+ write!(writer, "{}", Styled::new(change, cur_style))?;
+ }
+ if change.missing_newline() {
+ writeln!(writer, "{}", Styled::new("∅", em_style))?;
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\nWorld\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::plain();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 2 | World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_ne_line_missing() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::plain();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_trailing_extra_newline() {
+ let expected = "Hello\nWorld";
+ let expected_name = "A";
+ let actual = "Hello\nWorld\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::plain();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World∅
+ 2 + World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_trailing_newline_missing() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\nWorld";
+ let actual_name = "B";
+ let palette = crate::report::Palette::plain();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World
+ 2 + World∅
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_elided() {
+ let mut expected = String::new();
+ expected.push_str("Hello\n");
+ for i in 0..20 {
+ expected.push_str(&i.to_string());
+ expected.push('\n');
+ }
+ expected.push_str("World\n");
+ for i in 0..20 {
+ expected.push_str(&i.to_string());
+ expected.push('\n');
+ }
+ expected.push_str("!\n");
+ let expected_name = "A";
+
+ let mut actual = String::new();
+ actual.push_str("Goodbye\n");
+ for i in 0..20 {
+ actual.push_str(&i.to_string());
+ actual.push('\n');
+ }
+ actual.push_str("Moon\n");
+ for i in 0..20 {
+ actual.push_str(&i.to_string());
+ actual.push('\n');
+ }
+ actual.push_str("?\n");
+ let actual_name = "B";
+
+ let palette = crate::report::Palette::plain();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ &expected,
+ &actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 - Hello
+ 1 + Goodbye
+ 2 2 | 0
+ 3 3 | 1
+ 4 4 | 2
+ 5 5 | 3
+ 6 6 | 4
+ ⋮
+ 17 17 | 15
+ 18 18 | 16
+ 19 19 | 17
+ 20 20 | 18
+ 21 21 | 19
+ 22 - World
+ 22 + Moon
+ 23 23 | 0
+ 24 24 | 1
+ 25 25 | 2
+ 26 26 | 3
+ 27 27 | 4
+ ⋮
+ 38 38 | 15
+ 39 39 | 16
+ 40 40 | 17
+ 41 41 | 18
+ 42 42 | 19
+ 43 - !
+ 43 + ?
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+}
diff --git a/vendor/snapbox/src/report/mod.rs b/vendor/snapbox/src/report/mod.rs
new file mode 100644
index 0000000..6c9a238
--- /dev/null
+++ b/vendor/snapbox/src/report/mod.rs
@@ -0,0 +1,9 @@
+//! Utilities to report test results to users
+
+mod color;
+mod diff;
+
+pub use color::Palette;
+pub(crate) use color::Style;
+pub use color::Styled;
+pub use diff::write_diff;
diff --git a/vendor/snapbox/src/substitutions.rs b/vendor/snapbox/src/substitutions.rs
new file mode 100644
index 0000000..d005743
--- /dev/null
+++ b/vendor/snapbox/src/substitutions.rs
@@ -0,0 +1,420 @@
+use std::borrow::Cow;
+
+/// Match pattern expressions, see [`Assert`][crate::Assert]
+///
+/// Built-in expressions:
+/// - `...` on a line of its own: match multiple complete lines
+/// - `[..]`: match multiple characters within a line
+#[derive(Default, Clone, Debug, PartialEq, Eq)]
+pub struct Substitutions {
+ vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>,
+ unused: std::collections::BTreeSet<&'static str>,
+}
+
+impl Substitutions {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_exe() -> Self {
+ let mut substitutions = Self::new();
+ substitutions
+ .insert("[EXE]", std::env::consts::EXE_SUFFIX)
+ .unwrap();
+ substitutions
+ }
+
+ /// Insert an additional match pattern
+ ///
+ /// `key` must be enclosed in `[` and `]`.
+ ///
+ /// ```rust
+ /// let mut subst = snapbox::Substitutions::new();
+ /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX);
+ /// ```
+ pub fn insert(
+ &mut self,
+ key: &'static str,
+ value: impl Into<Cow<'static, str>>,
+ ) -> Result<(), crate::Error> {
+ let key = validate_key(key)?;
+ let value = value.into();
+ if value.is_empty() {
+ self.unused.insert(key);
+ } else {
+ self.vars
+ .insert(key, crate::utils::normalize_text(value.as_ref()).into());
+ }
+ Ok(())
+ }
+
+ /// Insert additional match patterns
+ ///
+ /// keys must be enclosed in `[` and `]`.
+ pub fn extend(
+ &mut self,
+ vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
+ ) -> Result<(), crate::Error> {
+ for (key, value) in vars {
+ self.insert(key, value)?;
+ }
+ Ok(())
+ }
+
+ /// Apply match pattern to `input`
+ ///
+ /// If `pattern` matches `input`, then `pattern` is returned.
+ ///
+ /// Otherwise, `input`, with as many patterns replaced as possible, will be returned.
+ ///
+ /// ```rust
+ /// let subst = snapbox::Substitutions::new();
+ /// let output = subst.normalize("Hello World!", "Hello [..]!");
+ /// assert_eq!(output, "Hello [..]!");
+ /// ```
+ pub fn normalize(&self, input: &str, pattern: &str) -> String {
+ normalize(input, pattern, self)
+ }
+
+ fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
+ let mut value = Cow::Borrowed(value);
+ for (var, replace) in self.vars.iter() {
+ debug_assert!(!replace.is_empty());
+ value = Cow::Owned(value.replace(replace.as_ref(), var));
+ }
+ value
+ }
+
+ fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
+ if pattern.contains('[') {
+ let mut pattern = Cow::Borrowed(pattern);
+ for var in self.unused.iter() {
+ pattern = Cow::Owned(pattern.replace(var, ""));
+ }
+ pattern
+ } else {
+ Cow::Borrowed(pattern)
+ }
+ }
+}
+
+fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> {
+ if !key.starts_with('[') || !key.ends_with(']') {
+ return Err(format!("Key `{}` is not enclosed in []", key).into());
+ }
+
+ if key[1..(key.len() - 1)]
+ .find(|c: char| !c.is_ascii_uppercase())
+ .is_some()
+ {
+ return Err(format!("Key `{}` can only be A-Z but ", key).into());
+ }
+
+ Ok(key)
+}
+
+fn normalize(input: &str, pattern: &str, substitutions: &Substitutions) -> String {
+ if input == pattern {
+ return input.to_owned();
+ }
+
+ let mut normalized: Vec<Cow<str>> = Vec::new();
+ let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect();
+ let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect();
+
+ let mut input_index = 0;
+ let mut pattern_index = 0;
+ 'outer: loop {
+ let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) {
+ *pattern_line
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ };
+ let next_pattern_index = pattern_index + 1;
+
+ let input_line = if let Some(input_line) = input_lines.get(input_index) {
+ *input_line
+ } else {
+ break 'outer;
+ };
+ let next_input_index = input_index + 1;
+
+ if line_matches(input_line, pattern_line, substitutions) {
+ pattern_index = next_pattern_index;
+ input_index = next_input_index;
+ normalized.push(Cow::Borrowed(pattern_line));
+ continue 'outer;
+ } else if is_line_elide(pattern_line) {
+ let next_pattern_line: &str =
+ if let Some(pattern_line) = pattern_lines.get(next_pattern_index) {
+ pattern_line
+ } else {
+ normalized.push(Cow::Borrowed(pattern_line));
+ break 'outer;
+ };
+ if let Some(future_input_index) = input_lines[input_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == next_pattern_line)
+ .map(|(i, _)| input_index + i)
+ {
+ normalized.push(Cow::Borrowed(pattern_line));
+ pattern_index = next_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ } else {
+ // Find where we can pick back up for normalizing
+ for future_input_index in next_input_index..input_lines.len() {
+ let future_input_line = input_lines[future_input_index];
+ if let Some(future_pattern_index) = pattern_lines[next_pattern_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == future_input_line || is_line_elide(l))
+ .map(|(i, _)| next_pattern_index + i)
+ {
+ normalized.extend(
+ input_lines[input_index..future_input_index]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ pattern_index = future_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ }
+ }
+
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ }
+
+ normalized.join("")
+}
+
+fn is_line_elide(line: &str) -> bool {
+ line == "...\n" || line == "..."
+}
+
+fn line_matches(line: &str, pattern: &str, substitutions: &Substitutions) -> bool {
+ if line == pattern {
+ return true;
+ }
+
+ let subbed = substitutions.substitute(line);
+ let mut line = subbed.as_ref();
+
+ let pattern = substitutions.clear(pattern);
+
+ let mut sections = pattern.split("[..]").peekable();
+ while let Some(section) = sections.next() {
+ if let Some(remainder) = line.strip_prefix(section) {
+ if let Some(next_section) = sections.peek() {
+ if next_section.is_empty() {
+ line = "";
+ } else if let Some(restart_index) = remainder.find(next_section) {
+ line = &remainder[restart_index..];
+ }
+ } else {
+ return remainder.is_empty();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ let input = "";
+ let pattern = "";
+ let expected = "";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn literals_match() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn pattern_shorter() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\n";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn input_shorter() {
+ let input = "Hello\n";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\n";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn all_different() {
+ let input = "Hello\nWorld";
+ let pattern = "Goodbye\nMoon";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middles_diverge() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\nMoon\nGoodbye";
+ let expected = "Hello\nWorld\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn leading_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "...\nGoodbye";
+ let expected = "...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn trailing_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...";
+ let expected = "Hello\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middle_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...\nGoodbye";
+ let expected = "Hello\n...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_elide_diverge() {
+ let input = "Hello\nSun\nAnd\nWorld";
+ let pattern = "Hello\n...\nMoon";
+ let expected = "Hello\nSun\nAnd\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_diverge_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nMoon\nGoodbye\n...";
+ let expected = "Hello\nWorld\nGoodbye\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn inline_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nW[..]d\nGoodbye\nSir";
+ let expected = "Hello\nW[..]d\nGoodbye\nSir";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn line_matches_cases() {
+ let cases = [
+ ("", "", true),
+ ("", "[..]", true),
+ ("hello", "hello", true),
+ ("hello", "goodbye", false),
+ ("hello", "[..]", true),
+ ("hello", "he[..]", true),
+ ("hello", "go[..]", false),
+ ("hello", "[..]o", true),
+ ("hello", "[..]e", false),
+ ("hello", "he[..]o", true),
+ ("hello", "he[..]e", false),
+ ("hello", "go[..]o", false),
+ ("hello", "go[..]e", false),
+ (
+ "hello world, goodbye moon",
+ "hello [..], goodbye [..]",
+ true,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], goodbye [..]",
+ false,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], hello [..]",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] moon", true),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], [..] moon",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] world", false),
+ ];
+ for (line, pattern, expected) in cases {
+ let actual = line_matches(line, pattern, &Substitutions::new());
+ assert_eq!(expected, actual, "line={:?} pattern={:?}", line, pattern);
+ }
+ }
+
+ #[test]
+ fn test_validate_key() {
+ let cases = [
+ ("[HELLO", false),
+ ("HELLO]", false),
+ ("[HELLO]", true),
+ ("[hello]", false),
+ ("[HE O]", false),
+ ];
+ for (key, expected) in cases {
+ let actual = validate_key(key).is_ok();
+ assert_eq!(expected, actual, "key={:?}", key);
+ }
+ }
+}
diff --git a/vendor/snapbox/src/utils/lines.rs b/vendor/snapbox/src/utils/lines.rs
new file mode 100644
index 0000000..f564084
--- /dev/null
+++ b/vendor/snapbox/src/utils/lines.rs
@@ -0,0 +1,31 @@
+#[derive(Clone, Debug)]
+pub struct LinesWithTerminator<'a> {
+ data: &'a str,
+}
+
+impl<'a> LinesWithTerminator<'a> {
+ pub fn new(data: &'a str) -> LinesWithTerminator<'a> {
+ LinesWithTerminator { data }
+ }
+}
+
+impl<'a> Iterator for LinesWithTerminator<'a> {
+ type Item = &'a str;
+
+ #[inline]
+ fn next(&mut self) -> Option<&'a str> {
+ match self.data.find('\n') {
+ None if self.data.is_empty() => None,
+ None => {
+ let line = self.data;
+ self.data = "";
+ Some(line)
+ }
+ Some(end) => {
+ let line = &self.data[..end + 1];
+ self.data = &self.data[end + 1..];
+ Some(line)
+ }
+ }
+ }
+}
diff --git a/vendor/snapbox/src/utils/mod.rs b/vendor/snapbox/src/utils/mod.rs
new file mode 100644
index 0000000..d519241
--- /dev/null
+++ b/vendor/snapbox/src/utils/mod.rs
@@ -0,0 +1,30 @@
+mod lines;
+
+pub use lines::LinesWithTerminator;
+
+/// Normalize line endings
+pub fn normalize_lines(data: &str) -> String {
+ normalize_lines_chars(data.chars()).collect()
+}
+
+fn normalize_lines_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
+ normalize_line_endings::normalized(data)
+}
+
+/// Normalize path separators
+pub fn normalize_paths(data: &str) -> String {
+ normalize_paths_chars(data.chars()).collect()
+}
+
+fn normalize_paths_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
+ data.map(|c| if c == '\\' { '/' } else { c })
+}
+
+/// "Smart" text normalization
+///
+/// This includes
+/// - Line endings
+/// - Path separators
+pub fn normalize_text(data: &str) -> String {
+ normalize_paths_chars(normalize_lines_chars(data.chars())).collect()
+}