summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
commit7e5d7eea9c580ef4b41a765bde624af431942b96 (patch)
tree2c0d9ca12878fc4525650aa4e54d77a81a07cc09 /vendor/snapbox
parentAdding debian version 1.70.0+dfsg1-9. (diff)
downloadrustc-7e5d7eea9c580ef4b41a765bde624af431942b96.tar.xz
rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.zip
Merging upstream version 1.70.0+dfsg2.
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.lock727
-rw-r--r--vendor/snapbox/Cargo.toml206
-rw-r--r--vendor/snapbox/README.md36
-rw-r--r--vendor/snapbox/src/action.rs39
-rw-r--r--vendor/snapbox/src/assert.rs527
-rw-r--r--vendor/snapbox/src/bin/snap-fixture.rs60
-rw-r--r--vendor/snapbox/src/cmd.rs1030
-rw-r--r--vendor/snapbox/src/data.rs712
-rw-r--r--vendor/snapbox/src/error.rs95
-rw-r--r--vendor/snapbox/src/harness.rs212
-rw-r--r--vendor/snapbox/src/lib.rs246
-rw-r--r--vendor/snapbox/src/path.rs686
-rw-r--r--vendor/snapbox/src/report/color.rs127
-rw-r--r--vendor/snapbox/src/report/diff.rs384
-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
19 files changed, 5578 insertions, 0 deletions
diff --git a/vendor/snapbox/.cargo-checksum.json b/vendor/snapbox/.cargo-checksum.json
new file mode 100644
index 000000000..656d9137e
--- /dev/null
+++ b/vendor/snapbox/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.lock":"774033150fcec11c62319d0f8b2922cb5ff1ee966a6993a67e1d1be48b905499","Cargo.toml":"7902b577196e9b505bd9767950993dcb2c8d754b9dbc8416cf274d336b209723","README.md":"8ae45cfbc76984049c97e7bdee5e0a6709f2bff442895e0c08701a71338aa0f9","src/action.rs":"a451e85e74fe43b5162a144927151e3785ef36c8d67f16f6ec3da3c6f371ca12","src/assert.rs":"2cdbcdae79eed9de512e5c1e4cd9730bbdf60e52e7c4571d0912d58a4ed5a2be","src/bin/snap-fixture.rs":"3e60c11a785b24b3e9997d5e66e0839179f23b105451509d6faf280819e75fdb","src/cmd.rs":"c5cbd67d3011ef938bed7879f776ed8112841577e36deef4950e5adf190b6ac5","src/data.rs":"15fff2855ff6cc3d8ae3ddc95867dbc82d21d24a10697ad95497f81c9252f09d","src/error.rs":"ed67eaa4e134937e1f014a0f5555985d26a021bd7034e2928ff3e46f73d7834a","src/harness.rs":"7b1edc9d9154fda3740e561faf4e474f5519448bba8c9bb170efc45f4fa9a8b9","src/lib.rs":"2599d022ee5b3f0230f6e4927fece30cd25688aeefdbfb37300ac5a2b26cb15e","src/path.rs":"f3bb7f1378fcd4ad1e54e63067cff304d15e83d40c116c83043258ef644feb56","src/report/color.rs":"4e2c6b178af0557e8e843fdaf64bfc1f73a7c46ad74505117b58f7037769ba2f","src/report/diff.rs":"7949c36e1d14849eb318355abf55e82dbf9fdeb8b2a47ae2838852c2af7cab91","src/report/mod.rs":"84bd389fff126b5a22790865417c3b8942cced343d56659ad9f8b568e3d62902","src/substitutions.rs":"e31dd94d4d2426384f8995ad3e827205c7fe836859f1e48be411998f0b5c2035","src/utils/lines.rs":"9a8eff80b4eced8042cecd2a5bf689365a9ceeea8b4239dcee22a6daad2522ec","src/utils/mod.rs":"61e6a1c2bbefa33a7f8af4068a42a7ad31bfefa90803610374f28be4a2f3b9c3"},"package":"827c00e91b15e2674d8a5270bae91f898693cbf9561cbb58d8eaa31974597293"} \ No newline at end of file
diff --git a/vendor/snapbox/Cargo.lock b/vendor/snapbox/Cargo.lock
new file mode 100644
index 000000000..60d7b2ef0
--- /dev/null
+++ b/vendor/snapbox/Cargo.lock
@@ -0,0 +1,727 @@
+# 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.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[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 = "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.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "3.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "concolor"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b90f9dcd9490a97db91a85ccd79e38a87e14323f0bb824659ee3274e9143ba37"
+dependencies = [
+ "atty",
+ "bitflags",
+ "concolor-query",
+]
+
+[[package]]
+name = "concolor-query"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a90734b3d5dcf656e7624cca6bce9c3a90ee11f900e80141a7427ccfb3d317"
+
+[[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.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+dependencies = [
+ "cfg-if",
+]
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
+
+[[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",
+]
+
+[[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 = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[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 = "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 = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[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.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libtest-mimic"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79529479c298f5af41375b0c1a77ef670d450b4c9cd7949d2b43af08121b20ec"
+dependencies = [
+ "clap",
+ "termcolor",
+ "threadpool",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+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.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "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.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "os_pipe"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dceb7e43f59c35ee1548045b2c72945a5a3bb6ce6d6f07cdc13dc8f6bc4930a"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
+
+[[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.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+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",
+]
+
+[[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 = "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.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+
+[[package]]
+name = "serde_json"
+version = "1.0.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+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.1"
+dependencies = [
+ "backtrace",
+ "concolor",
+ "content_inspector",
+ "document-features",
+ "dunce",
+ "filetime",
+ "ignore",
+ "libc",
+ "libtest-mimic",
+ "normalize-line-endings",
+ "os_pipe",
+ "serde_json",
+ "similar",
+ "snapbox-macros",
+ "tempfile",
+ "wait-timeout",
+ "walkdir",
+ "winapi",
+ "yansi",
+]
+
+[[package]]
+name = "snapbox-macros"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "485e65c1203eb37244465e857d15a26d3a85a5410648ccb53b18bd44cb3a7336"
+
+[[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.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+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 = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
+[[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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[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",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
diff --git a/vendor/snapbox/Cargo.toml b/vendor/snapbox/Cargo.toml
new file mode 100644
index 000000000..47ddda15f
--- /dev/null
+++ b/vendor/snapbox/Cargo.toml
@@ -0,0 +1,206 @@
+# 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.60.0"
+name = "snapbox"
+version = "0.4.1"
+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
+rustdoc-args = [
+ "--cfg",
+ "docsrs",
+]
+cargo-args = [
+ "-Zunstable-options",
+ "-Zrustdoc-scrape-examples=examples",
+]
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+search = "Unreleased"
+replace = "{{version}}"
+min = 1
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+search = '\.\.\.HEAD'
+replace = "...{{tag_name}}"
+exactly = 1
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+search = "ReleaseDate"
+replace = "{{date}}"
+min = 1
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+search = "<!-- next-header -->"
+replace = """
+<!-- next-header -->
+## [Unreleased] - ReleaseDate
+"""
+exactly = 1
+
+[[package.metadata.release.pre-release-replacements]]
+file = "CHANGELOG.md"
+search = "<!-- next-url -->"
+replace = """
+<!-- next-url -->
+[Unreleased]: https://github.com/assert-rs/trycmd/compare/{{tag_name}}...HEAD"""
+exactly = 1
+
+[[bin]]
+name = "snap-fixture"
+
+[dependencies.backtrace]
+version = "0.3"
+optional = true
+
+[dependencies.concolor]
+version = "0.0.9"
+features = ["std"]
+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.libc]
+version = "0.2.137"
+optional = true
+
+[dependencies.libtest-mimic]
+version = "0.5.2"
+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.0"
+
+[dependencies.tempfile]
+version = "3.0"
+optional = true
+
+[dependencies.wait-timeout]
+version = "0.2.0"
+optional = true
+
+[dependencies.walkdir]
+version = "2.3.2"
+optional = true
+
+[dependencies.winapi]
+version = "0.3.9"
+features = [
+ "consoleapi",
+ "minwindef",
+]
+optional = true
+
+[dependencies.yansi]
+version = "0.5.0"
+optional = true
+
+[features]
+cmd = [
+ "dep:os_pipe",
+ "dep:wait-timeout",
+ "dep:libc",
+ "dep:winapi",
+]
+color = [
+ "dep:yansi",
+ "concolor",
+]
+color-auto = [
+ "color",
+ "concolor/auto",
+]
+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"]
+path = [
+ "dep:tempfile",
+ "dep:walkdir",
+ "dep:dunce",
+ "detect-encoding",
+ "dep:filetime",
+]
+structured-data = ["dep:serde_json"]
diff --git a/vendor/snapbox/README.md b/vendor/snapbox/README.md
new file mode 100644
index 000000000..3e6c9b68d
--- /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/src/action.rs b/vendor/snapbox/src/action.rs
new file mode 100644
index 000000000..a4b849919
--- /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 000000000..ab87f1554
--- /dev/null
+++ b/vendor/snapbox/src/assert.rs
@@ -0,0 +1,527 @@
+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!(
+ std::io::stderr(),
+ "{}: {}",
+ self.palette.warn("Ignoring failure"),
+ err
+ );
+ }
+ Action::Verify => {
+ use std::fmt::Write;
+ let mut buffer = String::new();
+ write!(&mut buffer, "{}", err).unwrap();
+ if let Some(action_var) = self.action_var.as_deref() {
+ writeln!(
+ &mut buffer,
+ "{}",
+ self.palette
+ .hint(format_args!("Update with {}=overwrite", action_var))
+ )
+ .unwrap();
+ }
+ panic!("{}", buffer);
+ }
+ Action::Overwrite => {
+ use std::io::Write;
+
+ let _ = writeln!(
+ std::io::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!(std::io::stderr(), "{}", buffer);
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => {
+ let _ = write!(
+ std::io::stderr(),
+ "{}",
+ self.palette.warn("Ignoring above failures")
+ );
+ }
+ Action::Verify => unreachable!("Something had to fail to get here"),
+ Action::Overwrite => {
+ let _ = write!(
+ std::io::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::auto(),
+ 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 000000000..6e13448a7
--- /dev/null
+++ b/vendor/snapbox/src/bin/snap-fixture.rs
@@ -0,0 +1,60 @@
+//! 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_or(Ok(None), |r| r.map(Some))?
+ .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 000000000..72de3563c
--- /dev/null
+++ b/vendor/snapbox/src/cmd.rs
@@ -0,0 +1,1030 @@
+//! Run commands and assert on their behavior
+
+/// 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 winapi::shared::minwindef::DWORD;
+ use winapi::um::winnt::*;
+
+ let extra = match status.code().unwrap() as DWORD {
+ 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()
+}
diff --git a/vendor/snapbox/src/data.rs b/vendor/snapbox/src/data.rs
new file mode 100644
index 000000000..aa5f9b1ed
--- /dev/null
+++ b/vendor/snapbox/src/data.rs
@@ -0,0 +1,712 @@
+/// 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 = "structured-data")]
+ Json(serde_json::Value),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash)]
+pub enum DataFormat {
+ Binary,
+ Text,
+ #[cfg(feature = "json")]
+ Json,
+}
+
+impl Default for DataFormat {
+ fn default() -> Self {
+ DataFormat::Text
+ }
+}
+
+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) => {
+ arr.iter_mut().for_each(|value| normalize_value(value, op));
+ }
+ serde_json::Value::Object(obj) => {
+ obj.iter_mut()
+ .for_each(|(_, value)| 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)) => {
+ act.iter_mut()
+ .zip(exp)
+ .for_each(|(a, e)| normalize_value_matches(a, e, substitutions));
+ }
+ (Object(act), Object(exp)) => {
+ act.iter_mut()
+ .zip(exp)
+ .filter(|(a, e)| a.0 == e.0)
+ .for_each(|(a, e)| 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);
+ }
+ }
+}
diff --git a/vendor/snapbox/src/error.rs b/vendor/snapbox/src/error.rs
new file mode 100644
index 000000000..55e901883
--- /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 000000000..ee1035aaa
--- /dev/null
+++ b/vendor/snapbox/src/harness.rs
@@ -0,0 +1,212 @@
+//! [`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(|| 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);
+ 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 {
+ 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 000000000..61419fd5e
--- /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_matches(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 000000000..16e4ef653
--- /dev/null
+++ b/vendor/snapbox/src/path.rs
@@ -0,0 +1,686 @@
+//! Initialize working directories and assert on how they've changed
+
+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 000000000..f1cd363b4
--- /dev/null
+++ b/vendor/snapbox/src/report/color.rs
@@ -0,0 +1,127 @@
+#[derive(Copy, Clone, Debug)]
+#[allow(dead_code)]
+pub struct Palette {
+ pub(crate) info: styled::Style,
+ pub(crate) warn: styled::Style,
+ pub(crate) error: styled::Style,
+ pub(crate) hint: styled::Style,
+ pub(crate) expected: styled::Style,
+ pub(crate) actual: styled::Style,
+}
+
+impl Palette {
+ #[cfg(feature = "color")]
+ pub fn always() -> Self {
+ Self {
+ info: styled::Style(yansi::Style::new(yansi::Color::Green)),
+ warn: styled::Style(yansi::Style::new(yansi::Color::Yellow)),
+ error: styled::Style(yansi::Style::new(yansi::Color::Red)),
+ hint: styled::Style(yansi::Style::new(yansi::Color::Unset).dimmed()),
+ expected: styled::Style(yansi::Style::new(yansi::Color::Green).underline()),
+ actual: styled::Style(yansi::Style::new(yansi::Color::Red).underline()),
+ }
+ }
+
+ #[cfg(not(feature = "color"))]
+ pub fn always() -> Self {
+ Self::never()
+ }
+
+ pub fn never() -> Self {
+ Self {
+ info: Default::default(),
+ warn: Default::default(),
+ error: Default::default(),
+ hint: Default::default(),
+ expected: Default::default(),
+ actual: Default::default(),
+ }
+ }
+
+ pub fn auto() -> Self {
+ if is_colored() {
+ Self::always()
+ } else {
+ Self::never()
+ }
+ }
+
+ pub fn info<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.info.paint(item)
+ }
+
+ pub fn warn<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.warn.paint(item)
+ }
+
+ pub fn error<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.error.paint(item)
+ }
+
+ pub fn hint<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.hint.paint(item)
+ }
+
+ pub fn expected<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.expected.paint(item)
+ }
+
+ pub fn actual<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.actual.paint(item)
+ }
+}
+
+fn is_colored() -> bool {
+ #[cfg(feature = "color")]
+ {
+ concolor::get(concolor::Stream::Either).ansi_color()
+ }
+
+ #[cfg(not(feature = "color"))]
+ {
+ false
+ }
+}
+
+pub(crate) use styled::Style;
+pub use styled::Styled;
+
+#[cfg(feature = "color")]
+mod styled {
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct Style(pub(crate) yansi::Style);
+
+ impl Style {
+ pub(crate) fn paint<T: std::fmt::Display>(self, item: T) -> Styled<T> {
+ Styled(self.0.paint(item))
+ }
+ }
+
+ pub struct Styled<D: std::fmt::Display>(yansi::Paint<D>);
+
+ impl<D: std::fmt::Display> std::fmt::Display for Styled<D> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+}
+
+#[cfg(not(feature = "color"))]
+mod styled {
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct Style;
+
+ impl Style {
+ pub(crate) fn paint<T: std::fmt::Display>(self, item: T) -> Styled<T> {
+ Styled(item)
+ }
+ }
+
+ pub struct Styled<D: std::fmt::Display>(D);
+
+ impl<D: std::fmt::Display> std::fmt::Display for Styled<D> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+}
diff --git a/vendor/snapbox/src/report/diff.rs b/vendor/snapbox/src/report/diff.rs
new file mode 100644
index 000000000..adc9f7935
--- /dev/null
+++ b/vendor/snapbox/src/report/diff.rs
@@ -0,0 +1,384 @@
+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.info("(expected)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.info("Expected"))?;
+ }
+ writeln!(writer, "{}", palette.info(&expected))?;
+ if let Some(actual_name) = actual_name {
+ writeln!(writer, "{} {}:", actual_name, palette.error("(actual)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.error("Actual"))?;
+ }
+ writeln!(writer, "{}", palette.error(&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.info(format_args!("{:->4} expected: {}", "", expected_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.info(format_args!("--- Expected")))?;
+ }
+ if let Some(actual_name) = actual_name {
+ writeln!(
+ writer,
+ "{}",
+ palette.error(format_args!("{:+>4} actual: {}", "", actual_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.error(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.error, palette)?;
+ }
+ similar::ChangeTag::Delete => {
+ write_change(writer, change, "-", palette.expected, palette.info, 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, "{} ", style.paint(sign))?;
+ for &(emphasized, change) in change.values() {
+ let cur_style = if emphasized { em_style } else { style };
+ write!(writer, "{}", cur_style.paint(change))?;
+ }
+ if change.missing_newline() {
+ writeln!(writer, "{}", em_style.paint("∅"))?;
+ }
+
+ 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::never();
+
+ 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::never();
+
+ 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::never();
+
+ 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::never();
+
+ 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::never();
+
+ 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 000000000..6c9a238b8
--- /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 000000000..9c228172b
--- /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 000000000..f56408483
--- /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 000000000..d51924196
--- /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()
+}