diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/snapbox | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.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.json | 1 | ||||
-rw-r--r-- | vendor/snapbox/Cargo.lock | 727 | ||||
-rw-r--r-- | vendor/snapbox/Cargo.toml | 206 | ||||
-rw-r--r-- | vendor/snapbox/README.md | 36 | ||||
-rw-r--r-- | vendor/snapbox/src/action.rs | 39 | ||||
-rw-r--r-- | vendor/snapbox/src/assert.rs | 527 | ||||
-rw-r--r-- | vendor/snapbox/src/bin/snap-fixture.rs | 60 | ||||
-rw-r--r-- | vendor/snapbox/src/cmd.rs | 1030 | ||||
-rw-r--r-- | vendor/snapbox/src/data.rs | 712 | ||||
-rw-r--r-- | vendor/snapbox/src/error.rs | 95 | ||||
-rw-r--r-- | vendor/snapbox/src/harness.rs | 212 | ||||
-rw-r--r-- | vendor/snapbox/src/lib.rs | 246 | ||||
-rw-r--r-- | vendor/snapbox/src/path.rs | 686 | ||||
-rw-r--r-- | vendor/snapbox/src/report/color.rs | 127 | ||||
-rw-r--r-- | vendor/snapbox/src/report/diff.rs | 384 | ||||
-rw-r--r-- | vendor/snapbox/src/report/mod.rs | 9 | ||||
-rw-r--r-- | vendor/snapbox/src/substitutions.rs | 420 | ||||
-rw-r--r-- | vendor/snapbox/src/utils/lines.rs | 31 | ||||
-rw-r--r-- | vendor/snapbox/src/utils/mod.rs | 30 |
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(¤t, &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() +} |