diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:47:55 +0000 |
commit | 2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4 (patch) | |
tree | 033cc839730fda84ff08db877037977be94e5e3a /vendor/snapbox | |
parent | Initial commit. (diff) | |
download | cargo-2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4.tar.xz cargo-2aadc03ef15cb5ca5cc2af8a7c08e070742f0ac4.zip |
Adding upstream version 0.70.1+ds1.upstream/0.70.1+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/snapbox')
25 files changed, 6415 insertions, 0 deletions
diff --git a/vendor/snapbox/.cargo-checksum.json b/vendor/snapbox/.cargo-checksum.json new file mode 100644 index 0000000..e6d1717 --- /dev/null +++ b/vendor/snapbox/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{},"package":"4b377c0b6e4715c116473d8e40d51e3fa5b0a2297ca9b2a931ba800667b259ed"}
\ No newline at end of file diff --git a/vendor/snapbox/Cargo.lock b/vendor/snapbox/Cargo.lock new file mode 100644 index 0000000..184963e --- /dev/null +++ b/vendor/snapbox/Cargo.lock @@ -0,0 +1,886 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff2cf94a3dbe2d57cbd56485e1bd7436455058034d6c2d47be51d4e5e4bc6ab" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acbd8d28a0a60d7108d7ae850af6ba34cf2d1257fc646980e5f97ce14275966" +dependencies = [ + "bitflags 1.3.2", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "document-features" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3267e1ade4f1f6ddd35fed44a04b6514e244ffeda90c6a14a9ee30f9c9fd7a1" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "escargot" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5584ba17d7ab26a8a7284f13e5bd196294dd2f2d79773cff29b9e9edef601a6" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.42.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libtest-mimic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b603516767d1ab23d0de09d023e62966c3322f7148297c35cf3d97aa8b37fa" +dependencies = [ + "clap", + "termcolor", + "threadpool", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "litrs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi 0.1.19", + "libc", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "os_pipe" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c92f2b54f081d635c77e7120862d48db8e91f7f21cef23ab1b4fe9971c59f55" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "similar" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" + +[[package]] +name = "snapbox" +version = "0.4.14" +dependencies = [ + "anstream", + "anstyle", + "backtrace", + "content_inspector", + "document-features", + "dunce", + "escargot", + "filetime", + "ignore", + "libc", + "libtest-mimic", + "normalize-line-endings", + "os_pipe", + "serde_json", + "similar", + "snapbox-macros", + "tempfile", + "wait-timeout", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "snapbox-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" +dependencies = [ + "anstream", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/vendor/snapbox/Cargo.toml b/vendor/snapbox/Cargo.toml new file mode 100644 index 0000000..a8ade72 --- /dev/null +++ b/vendor/snapbox/Cargo.toml @@ -0,0 +1,201 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.70.0" +name = "snapbox" +version = "0.4.14" +include = [ + "build.rs", + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "benches/**/*", + "examples/**/*", +] +description = "Snapshot testing toolbox" +homepage = "https://github.com/assert-rs/trycmd/tree/main/crates/snapbox" +documentation = "http://docs.rs/snapbox/" +readme = "README.md" +keywords = [ + "cli", + "test", + "assert", + "command", +] +categories = ["development-tools::testing"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/assert-rs/trycmd/" + +[package.metadata.docs.rs] +all-features = true +cargo-args = [ + "-Zunstable-options", + "-Zrustdoc-scrape-examples", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +min = 1 +replace = "{{version}}" +search = "Unreleased" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = "...{{tag_name}}" +search = '\.\.\.HEAD' + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +min = 1 +replace = "{{date}}" +search = "ReleaseDate" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = """ +<!-- next-header --> +## [Unreleased] - ReleaseDate +""" +search = "<!-- next-header -->" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = """ +<!-- next-url --> +[Unreleased]: https://github.com/assert-rs/trycmd/compare/{{tag_name}}...HEAD""" +search = "<!-- next-url -->" + +[[bin]] +name = "snap-fixture" + +[dependencies.anstream] +version = "0.6.0" +optional = true + +[dependencies.anstyle] +version = "1.0.0" + +[dependencies.backtrace] +version = "0.3" +optional = true + +[dependencies.content_inspector] +version = "0.2.4" +optional = true + +[dependencies.document-features] +version = "0.2.6" +optional = true + +[dependencies.dunce] +version = "1.0" +optional = true + +[dependencies.filetime] +version = "0.2" +optional = true + +[dependencies.ignore] +version = "0.4" +optional = true + +[dependencies.libtest-mimic] +version = "0.6.0" +optional = true + +[dependencies.normalize-line-endings] +version = "0.3.0" + +[dependencies.os_pipe] +version = "1.0" +optional = true + +[dependencies.serde_json] +version = "1.0.85" +optional = true + +[dependencies.similar] +version = "2.1.0" +features = ["inline"] +optional = true + +[dependencies.snapbox-macros] +version = "0.3.6" + +[dependencies.tempfile] +version = "3.0" +optional = true + +[dependencies.wait-timeout] +version = "0.2.0" +optional = true + +[dependencies.walkdir] +version = "2.3.2" +optional = true + +[features] +cmd = [ + "dep:os_pipe", + "dep:wait-timeout", + "dep:libc", + #"dep:windows-sys", +] +color = [ + "dep:anstream", + "snapbox-macros/color", +] +color-auto = ["color"] +debug = [ + "snapbox-macros/debug", + "dep:backtrace", +] +default = [ + "color-auto", + "diff", +] +detect-encoding = ["dep:content_inspector"] +diff = ["dep:similar"] +harness = [ + "dep:libtest-mimic", + "dep:ignore", +] +json = [ + "structured-data", + "dep:serde_json", +] +path = [ + "dep:tempfile", + "dep:walkdir", + "dep:dunce", + "detect-encoding", + "dep:filetime", +] +structured-data = ["dep:serde_json"] + +[target."cfg(unix)".dependencies.libc] +version = "0.2.137" +optional = true + +#[target."cfg(windows)".dependencies.windows-sys] +#version = "0.48.0" +#features = ["Win32_Foundation"] +#optional = true diff --git a/vendor/snapbox/LICENSE-APACHE b/vendor/snapbox/LICENSE-APACHE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/vendor/snapbox/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/snapbox/LICENSE-MIT b/vendor/snapbox/LICENSE-MIT new file mode 100644 index 0000000..a2d0108 --- /dev/null +++ b/vendor/snapbox/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/snapbox/README.md b/vendor/snapbox/README.md new file mode 100644 index 0000000..3e6c9b6 --- /dev/null +++ b/vendor/snapbox/README.md @@ -0,0 +1,36 @@ +# snapbox + +> When you have to treat your tests like pets, instead of [cattle][trycmd] + +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] +![License](https://img.shields.io/crates/l/snapbox.svg) +[![Crates Status](https://img.shields.io/crates/v/snapbox.svg)](https://crates.io/crates/snapbox) + +`snapbox` is a snapshot-testing toolbox that is ready to use for verifying output from +- Function return values +- CLI stdout/stderr +- Filesystem changes + +It is also flexible enough to build your own test harness like [trycmd]. + +See the [docs](http://docs.rs/snapbox) for more. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + +[Crates.io]: https://crates.io/crates/snapbox +[Documentation]: https://docs.rs/snapbox +[trycmd]: https://crates.io/crates/trycmd diff --git a/vendor/snapbox/debian/patches/remove-escargot.patch b/vendor/snapbox/debian/patches/remove-escargot.patch new file mode 100644 index 0000000..9b78b53 --- /dev/null +++ b/vendor/snapbox/debian/patches/remove-escargot.patch @@ -0,0 +1,21 @@ +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -109,10 +109,6 @@ + version = "1.0" + optional = true + +-[dependencies.escargot] +-version = "0.5.7" +-optional = true +- + [dependencies.filetime] + version = "0.2" + optional = true +@@ -178,7 +174,6 @@ + ] + detect-encoding = ["dep:content_inspector"] + diff = ["dep:similar"] +-examples = ["dep:escargot"] + harness = [ + "dep:libtest-mimic", + "dep:ignore", diff --git a/vendor/snapbox/debian/patches/remove-windows-deps.patch b/vendor/snapbox/debian/patches/remove-windows-deps.patch new file mode 100644 index 0000000..4773339 --- /dev/null +++ b/vendor/snapbox/debian/patches/remove-windows-deps.patch @@ -0,0 +1,20 @@ +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -162,5 +162,5 @@ + "dep:wait-timeout", + "dep:libc", +- "dep:windows-sys", ++ #"dep:windows-sys", + ] + color = [ +@@ -201,6 +201,6 @@ + optional = true + +-[target."cfg(windows)".dependencies.windows-sys] +-version = "0.48.0" +-features = ["Win32_Foundation"] +-optional = true ++#[target."cfg(windows)".dependencies.windows-sys] ++#version = "0.48.0" ++#features = ["Win32_Foundation"] ++#optional = true diff --git a/vendor/snapbox/debian/patches/series b/vendor/snapbox/debian/patches/series new file mode 100644 index 0000000..47fb12d --- /dev/null +++ b/vendor/snapbox/debian/patches/series @@ -0,0 +1,2 @@ +remove-windows-deps.patch +remove-escargot.patch diff --git a/vendor/snapbox/examples/snap-example-fixture.rs b/vendor/snapbox/examples/snap-example-fixture.rs new file mode 100644 index 0000000..7e21fc6 --- /dev/null +++ b/vendor/snapbox/examples/snap-example-fixture.rs @@ -0,0 +1,61 @@ +//! For `snapbox`s tests only + +use std::env; +use std::error::Error; +use std::io; +use std::io::Write; +use std::process; + +fn run() -> Result<(), Box<dyn Error>> { + if let Ok(text) = env::var("stdout") { + println!("{}", text); + } + if let Ok(text) = env::var("stderr") { + eprintln!("{}", text); + } + + if env::var("echo_large").as_deref() == Ok("1") { + for i in 0..(128 * 1024) { + println!("{}", i); + } + } + + if env::var("echo_cwd").as_deref() == Ok("1") { + if let Ok(cwd) = std::env::current_dir() { + eprintln!("{}", cwd.display()); + } + } + + if let Ok(raw) = env::var("write") { + let (path, text) = raw.split_once('=').unwrap_or((raw.as_str(), "")); + std::fs::write(path.trim(), text.trim()).unwrap(); + } + + if let Ok(path) = env::var("cat") { + let text = std::fs::read_to_string(path).unwrap(); + eprintln!("{}", text); + } + + if let Some(timeout) = env::var("sleep").ok().and_then(|s| s.parse().ok()) { + std::thread::sleep(std::time::Duration::from_secs(timeout)); + } + + let code = env::var("exit") + .ok() + .map(|v| v.parse::<i32>()) + .map(|r| r.map(Some)) + .unwrap_or(Ok(None))? + .unwrap_or(0); + process::exit(code); +} + +fn main() { + let code = match run() { + Ok(_) => 0, + Err(ref e) => { + write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail"); + 1 + } + }; + process::exit(code); +} diff --git a/vendor/snapbox/src/action.rs b/vendor/snapbox/src/action.rs new file mode 100644 index 0000000..a4b8499 --- /dev/null +++ b/vendor/snapbox/src/action.rs @@ -0,0 +1,39 @@ +pub const DEFAULT_ACTION_ENV: &str = "SNAPSHOTS"; + +/// Test action, see [`Assert`][crate::Assert] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Action { + /// Do not run the test + Skip, + /// Ignore test failures + Ignore, + /// Fail on mismatch + Verify, + /// Overwrite on mismatch + Overwrite, +} + +impl Action { + pub fn with_env_var(var: impl AsRef<std::ffi::OsStr>) -> Option<Self> { + let var = var.as_ref(); + let value = std::env::var_os(var)?; + Self::with_env_value(value) + } + + pub fn with_env_value(value: impl AsRef<std::ffi::OsStr>) -> Option<Self> { + let value = value.as_ref(); + match value.to_str()? { + "skip" => Some(Action::Skip), + "ignore" => Some(Action::Ignore), + "verify" => Some(Action::Verify), + "overwrite" => Some(Action::Overwrite), + _ => None, + } + } +} + +impl Default for Action { + fn default() -> Self { + Self::Verify + } +} diff --git a/vendor/snapbox/src/assert.rs b/vendor/snapbox/src/assert.rs new file mode 100644 index 0000000..f0267c0 --- /dev/null +++ b/vendor/snapbox/src/assert.rs @@ -0,0 +1,520 @@ +#[cfg(feature = "color")] +use anstream::panic; +#[cfg(feature = "color")] +use anstream::stderr; +#[cfg(not(feature = "color"))] +use std::io::stderr; + +use crate::data::{DataFormat, NormalizeMatches, NormalizeNewlines, NormalizePaths}; +use crate::Action; + +/// Snapshot assertion against a file's contents +/// +/// Useful for one-off assertions with the snapshot stored in a file +/// +/// # Examples +/// +/// ```rust,no_run +/// let actual = "..."; +/// snapbox::Assert::new() +/// .action_env("SNAPSHOTS") +/// .matches_path(actual, "tests/fixtures/help_output_is_clean.txt"); +/// ``` +#[derive(Clone, Debug)] +pub struct Assert { + action: Action, + action_var: Option<String>, + normalize_paths: bool, + substitutions: crate::Substitutions, + pub(crate) palette: crate::report::Palette, + pub(crate) data_format: Option<DataFormat>, +} + +/// # Assertions +impl Assert { + pub fn new() -> Self { + Default::default() + } + + /// Check if a value is the same as an expected value + /// + /// When the content is text, newlines are normalized. + /// + /// ```rust + /// let output = "something"; + /// let expected = "something"; + /// snapbox::Assert::new().eq(expected, output); + /// ``` + #[track_caller] + pub fn eq(&self, expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) { + let expected = expected.into(); + let actual = actual.into(); + self.eq_inner(expected, actual); + } + + #[track_caller] + fn eq_inner(&self, expected: crate::Data, actual: crate::Data) { + let (pattern, actual) = self.normalize_eq(Ok(expected), actual); + if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) { + panic!("{}: {}", self.palette.error("Eq failed"), desc); + } + } + + /// Check if a value matches a pattern + /// + /// Pattern syntax: + /// - `...` is a line-wildcard when on a line by itself + /// - `[..]` is a character-wildcard when inside a line + /// - `[EXE]` matches `.exe` on Windows + /// + /// Normalization: + /// - Newlines + /// - `\` to `/` + /// + /// ```rust + /// let output = "something"; + /// let expected = "so[..]g"; + /// snapbox::Assert::new().matches(expected, output); + /// ``` + #[track_caller] + pub fn matches(&self, pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) { + let pattern = pattern.into(); + let actual = actual.into(); + self.matches_inner(pattern, actual); + } + + #[track_caller] + fn matches_inner(&self, pattern: crate::Data, actual: crate::Data) { + let (pattern, actual) = self.normalize_match(Ok(pattern), actual); + if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) { + panic!("{}: {}", self.palette.error("Match failed"), desc); + } + } + + /// Check if a value matches the content of a file + /// + /// When the content is text, newlines are normalized. + /// + /// ```rust,no_run + /// let output = "something"; + /// let expected_path = "tests/snapshots/output.txt"; + /// snapbox::Assert::new().eq_path(output, expected_path); + /// ``` + #[track_caller] + pub fn eq_path( + &self, + expected_path: impl AsRef<std::path::Path>, + actual: impl Into<crate::Data>, + ) { + let expected_path = expected_path.as_ref(); + let actual = actual.into(); + self.eq_path_inner(expected_path, actual); + } + + #[track_caller] + fn eq_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) { + match self.action { + Action::Skip => { + return; + } + Action::Ignore | Action::Verify | Action::Overwrite => {} + } + + let expected = crate::Data::read_from(pattern_path, self.data_format()); + let (expected, actual) = self.normalize_eq(expected, actual); + + self.do_action( + expected, + actual, + Some(&crate::path::display_relpath(pattern_path)), + Some(&"In-memory"), + pattern_path, + ); + } + + /// Check if a value matches the pattern in a file + /// + /// Pattern syntax: + /// - `...` is a line-wildcard when on a line by itself + /// - `[..]` is a character-wildcard when inside a line + /// - `[EXE]` matches `.exe` on Windows (override with [`Assert::substitutions`]) + /// + /// Normalization: + /// - Newlines + /// - `\` to `/` + /// + /// ```rust,no_run + /// let output = "something"; + /// let expected_path = "tests/snapshots/output.txt"; + /// snapbox::Assert::new().matches_path(expected_path, output); + /// ``` + #[track_caller] + pub fn matches_path( + &self, + pattern_path: impl AsRef<std::path::Path>, + actual: impl Into<crate::Data>, + ) { + let pattern_path = pattern_path.as_ref(); + let actual = actual.into(); + self.matches_path_inner(pattern_path, actual); + } + + #[track_caller] + fn matches_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) { + match self.action { + Action::Skip => { + return; + } + Action::Ignore | Action::Verify | Action::Overwrite => {} + } + + let expected = crate::Data::read_from(pattern_path, self.data_format()); + let (expected, actual) = self.normalize_match(expected, actual); + + self.do_action( + expected, + actual, + Some(&crate::path::display_relpath(pattern_path)), + Some(&"In-memory"), + pattern_path, + ); + } + + pub(crate) fn normalize_eq( + &self, + expected: crate::Result<crate::Data>, + mut actual: crate::Data, + ) -> (crate::Result<crate::Data>, crate::Data) { + let expected = expected.map(|d| d.normalize(NormalizeNewlines)); + // On `expected` being an error, make a best guess + let format = expected + .as_ref() + .map(|d| d.format()) + .unwrap_or(DataFormat::Text); + + actual = actual.try_coerce(format).normalize(NormalizeNewlines); + + (expected, actual) + } + + pub(crate) fn normalize_match( + &self, + expected: crate::Result<crate::Data>, + mut actual: crate::Data, + ) -> (crate::Result<crate::Data>, crate::Data) { + let expected = expected.map(|d| d.normalize(NormalizeNewlines)); + // On `expected` being an error, make a best guess + let format = expected.as_ref().map(|e| e.format()).unwrap_or_default(); + actual = actual.try_coerce(format); + + if self.normalize_paths { + actual = actual.normalize(NormalizePaths); + } + // Always normalize new lines + actual = actual.normalize(NormalizeNewlines); + + // If expected is not an error normalize matches + if let Ok(expected) = expected.as_ref() { + actual = actual.normalize(NormalizeMatches::new(&self.substitutions, expected)); + } + + (expected, actual) + } + + #[track_caller] + pub(crate) fn do_action( + &self, + expected: crate::Result<crate::Data>, + actual: crate::Data, + expected_name: Option<&dyn std::fmt::Display>, + actual_name: Option<&dyn std::fmt::Display>, + expected_path: &std::path::Path, + ) { + let result = + expected.and_then(|e| self.try_verify(&e, &actual, expected_name, actual_name)); + if let Err(err) = result { + match self.action { + Action::Skip => unreachable!("Bailed out earlier"), + Action::Ignore => { + use std::io::Write; + + let _ = writeln!( + stderr(), + "{}: {}", + self.palette.warn("Ignoring failure"), + err + ); + } + Action::Verify => { + let message = if let Some(action_var) = self.action_var.as_deref() { + self.palette + .hint(format!("Update with {}=overwrite", action_var)) + } else { + crate::report::Styled::new(String::new(), Default::default()) + }; + panic!("{err}{message}"); + } + Action::Overwrite => { + use std::io::Write; + + let _ = writeln!(stderr(), "{}: {}", self.palette.warn("Fixing"), err); + actual.write_to(expected_path).unwrap(); + } + } + } + } + + pub(crate) fn try_verify( + &self, + expected: &crate::Data, + actual: &crate::Data, + expected_name: Option<&dyn std::fmt::Display>, + actual_name: Option<&dyn std::fmt::Display>, + ) -> crate::Result<()> { + if expected != actual { + let mut buf = String::new(); + crate::report::write_diff( + &mut buf, + expected, + actual, + expected_name, + actual_name, + self.palette, + ) + .map_err(|e| e.to_string())?; + Err(buf.into()) + } else { + Ok(()) + } + } +} + +/// # Directory Assertions +#[cfg(feature = "path")] +impl Assert { + #[track_caller] + pub fn subset_eq( + &self, + expected_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, + ) { + let expected_root = expected_root.into(); + let actual_root = actual_root.into(); + self.subset_eq_inner(expected_root, actual_root) + } + + #[track_caller] + fn subset_eq_inner(&self, expected_root: std::path::PathBuf, actual_root: std::path::PathBuf) { + match self.action { + Action::Skip => { + return; + } + Action::Ignore | Action::Verify | Action::Overwrite => {} + } + + let checks: Vec<_> = + crate::path::PathDiff::subset_eq_iter_inner(expected_root, actual_root).collect(); + self.verify(checks); + } + + #[track_caller] + pub fn subset_matches( + &self, + pattern_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, + ) { + let pattern_root = pattern_root.into(); + let actual_root = actual_root.into(); + self.subset_matches_inner(pattern_root, actual_root) + } + + #[track_caller] + fn subset_matches_inner( + &self, + expected_root: std::path::PathBuf, + actual_root: std::path::PathBuf, + ) { + match self.action { + Action::Skip => { + return; + } + Action::Ignore | Action::Verify | Action::Overwrite => {} + } + + let checks: Vec<_> = crate::path::PathDiff::subset_matches_iter_inner( + expected_root, + actual_root, + &self.substitutions, + ) + .collect(); + self.verify(checks); + } + + #[track_caller] + fn verify( + &self, + mut checks: Vec<Result<(std::path::PathBuf, std::path::PathBuf), crate::path::PathDiff>>, + ) { + if checks.iter().all(Result::is_ok) { + for check in checks { + let (_expected_path, _actual_path) = check.unwrap(); + crate::debug!( + "{}: is {}", + _expected_path.display(), + self.palette.info("good") + ); + } + } else { + checks.sort_by_key(|c| match c { + Ok((expected_path, _actual_path)) => Some(expected_path.clone()), + Err(diff) => diff.expected_path().map(|p| p.to_owned()), + }); + + let mut buffer = String::new(); + let mut ok = true; + for check in checks { + use std::fmt::Write; + match check { + Ok((expected_path, _actual_path)) => { + let _ = writeln!( + &mut buffer, + "{}: is {}", + expected_path.display(), + self.palette.info("good"), + ); + } + Err(diff) => { + let _ = diff.write(&mut buffer, self.palette); + match self.action { + Action::Skip => unreachable!("Bailed out earlier"), + Action::Ignore | Action::Verify => { + ok = false; + } + Action::Overwrite => { + if let Err(err) = diff.overwrite() { + ok = false; + let path = diff + .expected_path() + .expect("always present when overwrite can fail"); + let _ = writeln!( + &mut buffer, + "{} to overwrite {}: {}", + self.palette.error("Failed"), + path.display(), + err + ); + } + } + } + } + } + } + if ok { + use std::io::Write; + let _ = write!(stderr(), "{}", buffer); + match self.action { + Action::Skip => unreachable!("Bailed out earlier"), + Action::Ignore => { + let _ = + write!(stderr(), "{}", self.palette.warn("Ignoring above failures")); + } + Action::Verify => unreachable!("Something had to fail to get here"), + Action::Overwrite => { + let _ = write!( + stderr(), + "{}", + self.palette.warn("Overwrote above failures") + ); + } + } + } else { + match self.action { + Action::Skip => unreachable!("Bailed out earlier"), + Action::Ignore => unreachable!("Shouldn't be able to fail"), + Action::Verify => { + use std::fmt::Write; + if let Some(action_var) = self.action_var.as_deref() { + writeln!( + &mut buffer, + "{}", + self.palette + .hint(format_args!("Update with {}=overwrite", action_var)) + ) + .unwrap(); + } + } + Action::Overwrite => {} + } + panic!("{}", buffer); + } + } + } +} + +/// # Customize Behavior +impl Assert { + /// Override the color palette + pub fn palette(mut self, palette: crate::report::Palette) -> Self { + self.palette = palette; + self + } + + /// Read the failure action from an environment variable + pub fn action_env(mut self, var_name: &str) -> Self { + let action = Action::with_env_var(var_name); + self.action = action.unwrap_or(self.action); + self.action_var = Some(var_name.to_owned()); + self + } + + /// Override the failure action + pub fn action(mut self, action: Action) -> Self { + self.action = action; + self.action_var = None; + self + } + + /// Override the default [`Substitutions`][crate::Substitutions] + pub fn substitutions(mut self, substitutions: crate::Substitutions) -> Self { + self.substitutions = substitutions; + self + } + + /// Specify whether text should have path separators normalized + /// + /// The default is normalized + pub fn normalize_paths(mut self, yes: bool) -> Self { + self.normalize_paths = yes; + self + } + + /// Specify whether the content should be treated as binary or not + /// + /// The default is to auto-detect + pub fn binary(mut self, yes: bool) -> Self { + self.data_format = if yes { + Some(DataFormat::Binary) + } else { + Some(DataFormat::Text) + }; + self + } + + pub(crate) fn data_format(&self) -> Option<DataFormat> { + self.data_format + } +} + +impl Default for Assert { + fn default() -> Self { + Self { + action: Default::default(), + action_var: Default::default(), + normalize_paths: true, + substitutions: Default::default(), + palette: crate::report::Palette::color(), + data_format: Default::default(), + } + .substitutions(crate::Substitutions::with_exe()) + } +} diff --git a/vendor/snapbox/src/bin/snap-fixture.rs b/vendor/snapbox/src/bin/snap-fixture.rs new file mode 100644 index 0000000..7e21fc6 --- /dev/null +++ b/vendor/snapbox/src/bin/snap-fixture.rs @@ -0,0 +1,61 @@ +//! For `snapbox`s tests only + +use std::env; +use std::error::Error; +use std::io; +use std::io::Write; +use std::process; + +fn run() -> Result<(), Box<dyn Error>> { + if let Ok(text) = env::var("stdout") { + println!("{}", text); + } + if let Ok(text) = env::var("stderr") { + eprintln!("{}", text); + } + + if env::var("echo_large").as_deref() == Ok("1") { + for i in 0..(128 * 1024) { + println!("{}", i); + } + } + + if env::var("echo_cwd").as_deref() == Ok("1") { + if let Ok(cwd) = std::env::current_dir() { + eprintln!("{}", cwd.display()); + } + } + + if let Ok(raw) = env::var("write") { + let (path, text) = raw.split_once('=').unwrap_or((raw.as_str(), "")); + std::fs::write(path.trim(), text.trim()).unwrap(); + } + + if let Ok(path) = env::var("cat") { + let text = std::fs::read_to_string(path).unwrap(); + eprintln!("{}", text); + } + + if let Some(timeout) = env::var("sleep").ok().and_then(|s| s.parse().ok()) { + std::thread::sleep(std::time::Duration::from_secs(timeout)); + } + + let code = env::var("exit") + .ok() + .map(|v| v.parse::<i32>()) + .map(|r| r.map(Some)) + .unwrap_or(Ok(None))? + .unwrap_or(0); + process::exit(code); +} + +fn main() { + let code = match run() { + Ok(_) => 0, + Err(ref e) => { + write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail"); + 1 + } + }; + process::exit(code); +} diff --git a/vendor/snapbox/src/cmd.rs b/vendor/snapbox/src/cmd.rs new file mode 100644 index 0000000..3b2a19f --- /dev/null +++ b/vendor/snapbox/src/cmd.rs @@ -0,0 +1,1170 @@ +//! Run commands and assert on their behavior + +#[cfg(feature = "color")] +use anstream::panic; + +/// Process spawning for testing of non-interactive commands +#[derive(Debug)] +pub struct Command { + cmd: std::process::Command, + stdin: Option<crate::Data>, + timeout: Option<std::time::Duration>, + _stderr_to_stdout: bool, + config: crate::Assert, +} + +/// # Builder API +impl Command { + pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self { + Self { + cmd: std::process::Command::new(program), + stdin: None, + timeout: None, + _stderr_to_stdout: false, + config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV), + } + } + + /// Constructs a new `Command` from a `std` `Command`. + pub fn from_std(cmd: std::process::Command) -> Self { + Self { + cmd, + stdin: None, + timeout: None, + _stderr_to_stdout: false, + config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV), + } + } + + /// Customize the assertion behavior + pub fn with_assert(mut self, config: crate::Assert) -> Self { + self.config = config; + self + } + + /// Adds an argument to pass to the program. + /// + /// Only one argument can be passed per use. So instead of: + /// + /// ```no_run + /// # snapbox::cmd::Command::new("sh") + /// .arg("-C /path/to/repo") + /// # ; + /// ``` + /// + /// usage would be: + /// + /// ```no_run + /// # snapbox::cmd::Command::new("sh") + /// .arg("-C") + /// .arg("/path/to/repo") + /// # ; + /// ``` + /// + /// To pass multiple arguments see [`args`]. + /// + /// [`args`]: Command::args() + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .arg("-l") + /// .arg("-a") + /// .assert() + /// .success(); + /// ``` + pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self { + self.cmd.arg(arg); + self + } + + /// Adds multiple arguments to pass to the program. + /// + /// To pass a single argument see [`arg`]. + /// + /// [`arg`]: Command::arg() + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .args(&["-l", "-a"]) + /// .assert() + /// .success(); + /// ``` + pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self { + self.cmd.args(args); + self + } + + /// Inserts or updates an environment variable mapping. + /// + /// Note that environment variable names are case-insensitive (but case-preserving) on Windows, + /// and case-sensitive on all other platforms. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .env("PATH", "/bin") + /// .assert() + /// .failure(); + /// ``` + pub fn env( + mut self, + key: impl AsRef<std::ffi::OsStr>, + value: impl AsRef<std::ffi::OsStr>, + ) -> Self { + self.cmd.env(key, value); + self + } + + /// Adds or updates multiple environment variable mappings. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// use std::process::Stdio; + /// use std::env; + /// use std::collections::HashMap; + /// + /// let filtered_env : HashMap<String, String> = + /// env::vars().filter(|&(ref k, _)| + /// k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH" + /// ).collect(); + /// + /// Command::new("printenv") + /// .env_clear() + /// .envs(&filtered_env) + /// .assert() + /// .success(); + /// ``` + pub fn envs( + mut self, + vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>, + ) -> Self { + self.cmd.envs(vars); + self + } + + /// Removes an environment variable mapping. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .env_remove("PATH") + /// .assert() + /// .failure(); + /// ``` + pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self { + self.cmd.env_remove(key); + self + } + + /// Clears the entire environment map for the child process. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .env_clear() + /// .assert() + /// .failure(); + /// ``` + pub fn env_clear(mut self) -> Self { + self.cmd.env_clear(); + self + } + + /// Sets the working directory for the child process. + /// + /// # Platform-specific behavior + /// + /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous + /// whether it should be interpreted relative to the parent's working + /// directory or relative to `current_dir`. The behavior in this case is + /// platform specific and unstable, and it's recommended to use + /// [`canonicalize`] to get an absolute program path instead. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use snapbox::cmd::Command; + /// + /// Command::new("ls") + /// .current_dir("/bin") + /// .assert() + /// .success(); + /// ``` + /// + /// [`canonicalize`]: std::fs::canonicalize() + pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self { + self.cmd.current_dir(dir); + self + } + + /// Write `buffer` to `stdin` when the `Command` is run. + /// + /// # Examples + /// + /// ```rust + /// use snapbox::cmd::Command; + /// + /// let mut cmd = Command::new("cat") + /// .arg("-et") + /// .stdin("42") + /// .assert() + /// .stdout_eq("42"); + /// ``` + pub fn stdin(mut self, stream: impl Into<crate::Data>) -> Self { + self.stdin = Some(stream.into()); + self + } + + /// Error out if a timeout is reached + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .timeout(std::time::Duration::from_secs(1)) + /// .env("sleep", "100") + /// .assert() + /// .failure(); + /// ``` + #[cfg(feature = "cmd")] + pub fn timeout(mut self, timeout: std::time::Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Merge `stderr` into `stdout` + #[cfg(feature = "cmd")] + pub fn stderr_to_stdout(mut self) -> Self { + self._stderr_to_stdout = true; + self + } +} + +/// # Run Command +impl Command { + /// Run the command and assert on the results + /// + /// ```rust + /// use snapbox::cmd::Command; + /// + /// let mut cmd = Command::new("cat") + /// .arg("-et") + /// .stdin("42") + /// .assert() + /// .stdout_eq("42"); + /// ``` + #[track_caller] + pub fn assert(self) -> OutputAssert { + let config = self.config.clone(); + match self.output() { + Ok(output) => OutputAssert::new(output).with_assert(config), + Err(err) => { + panic!("Failed to spawn: {}", err) + } + } + } + + /// Run the command and capture the `Output` + #[cfg(feature = "cmd")] + pub fn output(self) -> Result<std::process::Output, std::io::Error> { + if self._stderr_to_stdout { + self.single_output() + } else { + self.split_output() + } + } + + #[cfg(not(feature = "cmd"))] + pub fn output(self) -> Result<std::process::Output, std::io::Error> { + self.split_output() + } + + #[cfg(feature = "cmd")] + fn single_output(mut self) -> Result<std::process::Output, std::io::Error> { + self.cmd.stdin(std::process::Stdio::piped()); + let (reader, writer) = os_pipe::pipe()?; + let writer_clone = writer.try_clone()?; + self.cmd.stdout(writer); + self.cmd.stderr(writer_clone); + let mut child = self.cmd.spawn()?; + // Avoid a deadlock! This parent process is still holding open pipe + // writers (inside the Command object), and we have to close those + // before we read. Here we do this by dropping the Command object. + drop(self.cmd); + + let stdout = process_single_io( + &mut child, + reader, + self.stdin.as_ref().map(|d| d.to_bytes()), + )?; + + let status = wait(child, self.timeout)?; + let stdout = stdout.join().unwrap().ok().unwrap_or_default(); + + Ok(std::process::Output { + status, + stdout, + stderr: Default::default(), + }) + } + + fn split_output(mut self) -> Result<std::process::Output, std::io::Error> { + self.cmd.stdin(std::process::Stdio::piped()); + self.cmd.stdout(std::process::Stdio::piped()); + self.cmd.stderr(std::process::Stdio::piped()); + let mut child = self.cmd.spawn()?; + + let (stdout, stderr) = + process_split_io(&mut child, self.stdin.as_ref().map(|d| d.to_bytes()))?; + + let status = wait(child, self.timeout)?; + let stdout = stdout + .and_then(|t| t.join().unwrap().ok()) + .unwrap_or_default(); + let stderr = stderr + .and_then(|t| t.join().unwrap().ok()) + .unwrap_or_default(); + + Ok(std::process::Output { + status, + stdout, + stderr, + }) + } +} + +fn process_split_io( + child: &mut std::process::Child, + input: Option<Vec<u8>>, +) -> std::io::Result<(Option<Stream>, Option<Stream>)> { + use std::io::Write; + + let stdin = input.and_then(|i| { + child + .stdin + .take() + .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i))) + }); + let stdout = child.stdout.take().map(threaded_read); + let stderr = child.stderr.take().map(threaded_read); + + // Finish writing stdin before waiting, because waiting drops stdin. + stdin.and_then(|t| t.join().unwrap().ok()); + + Ok((stdout, stderr)) +} + +#[cfg(feature = "cmd")] +fn process_single_io( + child: &mut std::process::Child, + stdout: os_pipe::PipeReader, + input: Option<Vec<u8>>, +) -> std::io::Result<Stream> { + use std::io::Write; + + let stdin = input.and_then(|i| { + child + .stdin + .take() + .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i))) + }); + let stdout = threaded_read(stdout); + debug_assert!(child.stdout.is_none()); + debug_assert!(child.stderr.is_none()); + + // Finish writing stdin before waiting, because waiting drops stdin. + stdin.and_then(|t| t.join().unwrap().ok()); + + Ok(stdout) +} + +type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>; + +fn threaded_read<R>(mut input: R) -> Stream +where + R: std::io::Read + Send + 'static, +{ + std::thread::spawn(move || { + let mut ret = Vec::new(); + input.read_to_end(&mut ret).map(|_| ret) + }) +} + +impl From<std::process::Command> for Command { + fn from(cmd: std::process::Command) -> Self { + Self::from_std(cmd) + } +} + +/// Assert the state of a [`Command`]'s [`Output`]. +/// +/// Create an `OutputAssert` through the [`Command::assert`]. +/// +/// [`Output`]: std::process::Output +pub struct OutputAssert { + output: std::process::Output, + config: crate::Assert, +} + +impl OutputAssert { + /// Create an `Assert` for a given [`Output`]. + /// + /// [`Output`]: std::process::Output + pub fn new(output: std::process::Output) -> Self { + Self { + output, + config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV), + } + } + + /// Customize the assertion behavior + pub fn with_assert(mut self, config: crate::Assert) -> Self { + self.config = config; + self + } + + /// Access the contained [`Output`]. + /// + /// [`Output`]: std::process::Output + pub fn get_output(&self) -> &std::process::Output { + &self.output + } + + /// Ensure the command succeeded. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .assert() + /// .success(); + /// ``` + #[track_caller] + pub fn success(self) -> Self { + if !self.output.status.success() { + let desc = format!( + "Expected {}, was {}", + self.config.palette.info("success"), + self.config + .palette + .error(display_exit_status(self.output.status)) + ); + + use std::fmt::Write; + let mut buf = String::new(); + writeln!(&mut buf, "{}", desc).unwrap(); + self.write_stdout(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + self + } + + /// Ensure the command failed. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("exit", "1") + /// .assert() + /// .failure(); + /// ``` + #[track_caller] + pub fn failure(self) -> Self { + if self.output.status.success() { + let desc = format!( + "Expected {}, was {}", + self.config.palette.info("failure"), + self.config.palette.error("success") + ); + + use std::fmt::Write; + let mut buf = String::new(); + writeln!(&mut buf, "{}", desc).unwrap(); + self.write_stdout(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + self + } + + /// Ensure the command aborted before returning a code. + #[track_caller] + pub fn interrupted(self) -> Self { + if self.output.status.code().is_some() { + let desc = format!( + "Expected {}, was {}", + self.config.palette.info("interrupted"), + self.config + .palette + .error(display_exit_status(self.output.status)) + ); + + use std::fmt::Write; + let mut buf = String::new(); + writeln!(&mut buf, "{}", desc).unwrap(); + self.write_stdout(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + self + } + + /// Ensure the command returned the expected code. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("exit", "42") + /// .assert() + /// .code(42); + /// ``` + #[track_caller] + pub fn code(self, expected: i32) -> Self { + if self.output.status.code() != Some(expected) { + let desc = format!( + "Expected {}, was {}", + self.config.palette.info(expected), + self.config + .palette + .error(display_exit_status(self.output.status)) + ); + + use std::fmt::Write; + let mut buf = String::new(); + writeln!(&mut buf, "{}", desc).unwrap(); + self.write_stdout(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + self + } + + /// Ensure the command wrote the expected data to `stdout`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stdout_eq("hello"); + /// ``` + #[track_caller] + pub fn stdout_eq(self, expected: impl Into<crate::Data>) -> Self { + let expected = expected.into(); + self.stdout_eq_inner(expected) + } + + #[track_caller] + fn stdout_eq_inner(self, expected: crate::Data) -> Self { + let actual = crate::Data::from(self.output.stdout.as_slice()); + let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual); + if let Err(desc) = + pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout"))) + { + use std::fmt::Write; + let mut buf = String::new(); + write!(&mut buf, "{}", desc).unwrap(); + self.write_status(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + + self + } + + /// Ensure the command wrote the expected data to `stdout`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stdout_eq_path("tests/snapshots/output.txt"); + /// ``` + #[track_caller] + pub fn stdout_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self { + let expected_path = expected_path.as_ref(); + self.stdout_eq_path_inner(expected_path) + } + + #[track_caller] + fn stdout_eq_path_inner(self, expected_path: &std::path::Path) -> Self { + let actual = crate::Data::from(self.output.stdout.as_slice()); + let expected = crate::Data::read_from(expected_path, self.config.data_format()); + let (pattern, actual) = self.config.normalize_eq(expected, actual); + self.config.do_action( + pattern, + actual, + Some(&crate::path::display_relpath(expected_path)), + Some(&"stdout"), + expected_path, + ); + + self + } + + /// Ensure the command wrote the expected data to `stdout`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stdout_matches("he[..]o"); + /// ``` + #[track_caller] + pub fn stdout_matches(self, expected: impl Into<crate::Data>) -> Self { + let expected = expected.into(); + self.stdout_matches_inner(expected) + } + + #[track_caller] + fn stdout_matches_inner(self, expected: crate::Data) -> Self { + let actual = crate::Data::from(self.output.stdout.as_slice()); + let (pattern, actual) = self.config.normalize_match(Ok(expected), actual); + if let Err(desc) = + pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout"))) + { + use std::fmt::Write; + let mut buf = String::new(); + write!(&mut buf, "{}", desc).unwrap(); + self.write_status(&mut buf).unwrap(); + self.write_stderr(&mut buf).unwrap(); + panic!("{}", buf); + } + + self + } + + /// Ensure the command wrote the expected data to `stdout`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stdout_matches_path("tests/snapshots/output.txt"); + /// ``` + #[track_caller] + pub fn stdout_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self { + let expected_path = expected_path.as_ref(); + self.stdout_matches_path_inner(expected_path) + } + + #[track_caller] + fn stdout_matches_path_inner(self, expected_path: &std::path::Path) -> Self { + let actual = crate::Data::from(self.output.stdout.as_slice()); + let expected = crate::Data::read_from(expected_path, self.config.data_format()); + let (pattern, actual) = self.config.normalize_match(expected, actual); + self.config.do_action( + pattern, + actual, + Some(&expected_path.display()), + Some(&"stdout"), + expected_path, + ); + + self + } + + /// Ensure the command wrote the expected data to `stderr`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stderr_eq("world"); + /// ``` + #[track_caller] + pub fn stderr_eq(self, expected: impl Into<crate::Data>) -> Self { + let expected = expected.into(); + self.stderr_eq_inner(expected) + } + + #[track_caller] + fn stderr_eq_inner(self, expected: crate::Data) -> Self { + let actual = crate::Data::from(self.output.stderr.as_slice()); + let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual); + if let Err(desc) = + pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr"))) + { + use std::fmt::Write; + let mut buf = String::new(); + write!(&mut buf, "{}", desc).unwrap(); + self.write_status(&mut buf).unwrap(); + self.write_stdout(&mut buf).unwrap(); + panic!("{}", buf); + } + + self + } + + /// Ensure the command wrote the expected data to `stderr`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stderr_eq_path("tests/snapshots/err.txt"); + /// ``` + #[track_caller] + pub fn stderr_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self { + let expected_path = expected_path.as_ref(); + self.stderr_eq_path_inner(expected_path) + } + + #[track_caller] + fn stderr_eq_path_inner(self, expected_path: &std::path::Path) -> Self { + let actual = crate::Data::from(self.output.stderr.as_slice()); + let expected = crate::Data::read_from(expected_path, self.config.data_format()); + let (pattern, actual) = self.config.normalize_eq(expected, actual); + self.config.do_action( + pattern, + actual, + Some(&expected_path.display()), + Some(&"stderr"), + expected_path, + ); + + self + } + + /// Ensure the command wrote the expected data to `stderr`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stderr_matches("wo[..]d"); + /// ``` + #[track_caller] + pub fn stderr_matches(self, expected: impl Into<crate::Data>) -> Self { + let expected = expected.into(); + self.stderr_matches_inner(expected) + } + + #[track_caller] + fn stderr_matches_inner(self, expected: crate::Data) -> Self { + let actual = crate::Data::from(self.output.stderr.as_slice()); + let (pattern, actual) = self.config.normalize_match(Ok(expected), actual); + if let Err(desc) = + pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr"))) + { + use std::fmt::Write; + let mut buf = String::new(); + write!(&mut buf, "{}", desc).unwrap(); + self.write_status(&mut buf).unwrap(); + self.write_stdout(&mut buf).unwrap(); + panic!("{}", buf); + } + + self + } + + /// Ensure the command wrote the expected data to `stderr`. + /// + /// ```rust,no_run + /// use snapbox::cmd::Command; + /// use snapbox::cmd::cargo_bin; + /// + /// let assert = Command::new(cargo_bin("snap-fixture")) + /// .env("stdout", "hello") + /// .env("stderr", "world") + /// .assert() + /// .stderr_matches_path("tests/snapshots/err.txt"); + /// ``` + #[track_caller] + pub fn stderr_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self { + let expected_path = expected_path.as_ref(); + self.stderr_matches_path_inner(expected_path) + } + + #[track_caller] + fn stderr_matches_path_inner(self, expected_path: &std::path::Path) -> Self { + let actual = crate::Data::from(self.output.stderr.as_slice()); + let expected = crate::Data::read_from(expected_path, self.config.data_format()); + let (pattern, actual) = self.config.normalize_match(expected, actual); + self.config.do_action( + pattern, + actual, + Some(&crate::path::display_relpath(expected_path)), + Some(&"stderr"), + expected_path, + ); + + self + } + + fn write_status(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> { + writeln!( + writer, + "Exit status: {}", + display_exit_status(self.output.status) + )?; + Ok(()) + } + + fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> { + if !self.output.stdout.is_empty() { + writeln!(writer, "stdout:")?; + writeln!(writer, "```")?; + writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?; + writeln!(writer, "```")?; + } + Ok(()) + } + + fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> { + if !self.output.stderr.is_empty() { + writeln!(writer, "stderr:")?; + writeln!(writer, "```")?; + writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?; + writeln!(writer, "```")?; + } + Ok(()) + } +} + +/// Converts an [`std::process::ExitStatus`] to a human-readable value +#[cfg(not(feature = "cmd"))] +pub fn display_exit_status(status: std::process::ExitStatus) -> String { + basic_exit_status(status) +} + +/// Converts an [`std::process::ExitStatus`] to a human-readable value +#[cfg(feature = "cmd")] +pub fn display_exit_status(status: std::process::ExitStatus) -> String { + #[cfg(unix)] + fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> { + use std::os::unix::process::*; + + let signal = status.signal()?; + let name = match signal as libc::c_int { + libc::SIGABRT => ", SIGABRT: process abort signal", + libc::SIGALRM => ", SIGALRM: alarm clock", + libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation", + libc::SIGHUP => ", SIGHUP: hangup", + libc::SIGILL => ", SIGILL: illegal instruction", + libc::SIGINT => ", SIGINT: terminal interrupt signal", + libc::SIGKILL => ", SIGKILL: kill", + libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read", + libc::SIGQUIT => ", SIGQUIT: terminal quit signal", + libc::SIGSEGV => ", SIGSEGV: invalid memory reference", + libc::SIGTERM => ", SIGTERM: termination signal", + libc::SIGBUS => ", SIGBUS: access to undefined memory", + #[cfg(not(target_os = "haiku"))] + libc::SIGSYS => ", SIGSYS: bad system call", + libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap", + _ => "", + }; + Some(format!("signal: {}{}", signal, name)) + } + + #[cfg(windows)] + fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> { + use windows_sys::Win32::Foundation::*; + + let extra = match status.code().unwrap() as NTSTATUS { + STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION", + STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR", + STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE", + STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER", + STATUS_NO_MEMORY => "STATUS_NO_MEMORY", + STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION", + STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION", + STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION", + STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED", + STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND", + STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO", + STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT", + STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION", + STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW", + STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK", + STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW", + STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO", + STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW", + STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION", + STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW", + STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND", + STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND", + STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND", + STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT", + STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED", + STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS", + STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS", + STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION", + STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION", + STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN", + STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE", + _ => return None, + }; + Some(extra.to_owned()) + } + + if let Some(extra) = detailed_exit_status(status) { + format!("{} ({})", basic_exit_status(status), extra) + } else { + basic_exit_status(status) + } +} + +fn basic_exit_status(status: std::process::ExitStatus) -> String { + if let Some(code) = status.code() { + code.to_string() + } else { + "interrupted".to_owned() + } +} + +#[cfg(feature = "cmd")] +fn wait( + mut child: std::process::Child, + timeout: Option<std::time::Duration>, +) -> std::io::Result<std::process::ExitStatus> { + if let Some(timeout) = timeout { + wait_timeout::ChildExt::wait_timeout(&mut child, timeout) + .transpose() + .unwrap_or_else(|| { + let _ = child.kill(); + child.wait() + }) + } else { + child.wait() + } +} + +#[cfg(not(feature = "cmd"))] +fn wait( + mut child: std::process::Child, + _timeout: Option<std::time::Duration>, +) -> std::io::Result<std::process::ExitStatus> { + child.wait() +} + +pub use snapbox_macros::cargo_bin; + +/// Look up the path to a cargo-built binary within an integration test. +/// +/// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo +pub fn cargo_bin(name: &str) -> std::path::PathBuf { + let file_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX); + let target_dir = target_dir(); + target_dir.join(file_name) +} + +// Adapted from +// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507 +fn target_dir() -> std::path::PathBuf { + std::env::current_exe() + .ok() + .map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + .unwrap() +} + +#[cfg(feature = "examples")] +pub use examples::{compile_example, compile_examples}; + +#[cfg(feature = "examples")] +pub(crate) mod examples { + /// Prepare an example for testing + /// + /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It + /// will match the current target and profile but will not get feature flags. Pass those arguments + /// to the compiler via `args`. + /// + /// ## Example + /// + /// ```rust,no_run + /// snapbox::cmd::compile_example("snap-example-fixture", []); + /// ``` + #[cfg(feature = "examples")] + pub fn compile_example<'a>( + target_name: &str, + args: impl IntoIterator<Item = &'a str>, + ) -> Result<std::path::PathBuf, crate::Error> { + crate::debug!("Compiling example {}", target_name); + let messages = escargot::CargoBuild::new() + .current_target() + .current_release() + .example(target_name) + .args(args) + .exec() + .map_err(|e| crate::Error::new(e.to_string()))?; + for message in messages { + let message = message.map_err(|e| crate::Error::new(e.to_string()))?; + let message = message + .decode() + .map_err(|e| crate::Error::new(e.to_string()))?; + crate::debug!("Message: {:?}", message); + if let Some(bin) = decode_example_message(&message) { + let (name, bin) = bin?; + assert_eq!(target_name, name); + return bin; + } + } + + Err(crate::Error::new(format!( + "Unknown error building example {}", + target_name + ))) + } + + /// Prepare all examples for testing + /// + /// Unlike `cargo_bin!`, this does not inherit all of the current compiler settings. It + /// will match the current target and profile but will not get feature flags. Pass those arguments + /// to the compiler via `args`. + /// + /// ## Example + /// + /// ```rust,no_run + /// let examples = snapbox::cmd::compile_examples([]).unwrap().collect::<Vec<_>>(); + /// ``` + #[cfg(feature = "examples")] + pub fn compile_examples<'a>( + args: impl IntoIterator<Item = &'a str>, + ) -> Result< + impl Iterator<Item = (String, Result<std::path::PathBuf, crate::Error>)>, + crate::Error, + > { + crate::debug!("Compiling examples"); + let mut examples = std::collections::BTreeMap::new(); + + let messages = escargot::CargoBuild::new() + .current_target() + .current_release() + .examples() + .args(args) + .exec() + .map_err(|e| crate::Error::new(e.to_string()))?; + for message in messages { + let message = message.map_err(|e| crate::Error::new(e.to_string()))?; + let message = message + .decode() + .map_err(|e| crate::Error::new(e.to_string()))?; + crate::debug!("Message: {:?}", message); + if let Some(bin) = decode_example_message(&message) { + let (name, bin) = bin?; + examples.insert(name.to_owned(), bin); + } + } + + Ok(examples.into_iter()) + } + + #[allow(clippy::type_complexity)] + fn decode_example_message<'m>( + message: &'m escargot::format::Message, + ) -> Option<Result<(&'m str, Result<std::path::PathBuf, crate::Error>), crate::Error>> { + match message { + escargot::format::Message::CompilerMessage(msg) => { + let level = msg.message.level; + if level == escargot::format::diagnostic::DiagnosticLevel::Ice + || level == escargot::format::diagnostic::DiagnosticLevel::Error + { + let output = msg + .message + .rendered + .as_deref() + .unwrap_or_else(|| msg.message.message.as_ref()) + .to_owned(); + if is_example_target(&msg.target) { + let bin = Err(crate::Error::new(output)); + Some(Ok((msg.target.name.as_ref(), bin))) + } else { + Some(Err(crate::Error::new(output))) + } + } else { + None + } + } + escargot::format::Message::CompilerArtifact(artifact) => { + if !artifact.profile.test && is_example_target(&artifact.target) { + let path = artifact + .executable + .clone() + .expect("cargo is new enough for this to be present"); + let bin = Ok(path.into_owned()); + Some(Ok((artifact.target.name.as_ref(), bin))) + } else { + None + } + } + _ => None, + } + } + + fn is_example_target(target: &escargot::format::Target) -> bool { + target.crate_types == ["bin"] && target.kind == ["example"] + } +} diff --git a/vendor/snapbox/src/data.rs b/vendor/snapbox/src/data.rs new file mode 100644 index 0000000..b892ffd --- /dev/null +++ b/vendor/snapbox/src/data.rs @@ -0,0 +1,940 @@ +/// Test fixture, actual output, or expected result +/// +/// This provides conveniences for tracking the intended format (binary vs text). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Data { + inner: DataInner, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum DataInner { + Binary(Vec<u8>), + Text(String), + #[cfg(feature = "json")] + Json(serde_json::Value), +} + +#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)] +pub enum DataFormat { + Binary, + #[default] + Text, + #[cfg(feature = "json")] + Json, +} + +impl Data { + /// Mark the data as binary (no post-processing) + pub fn binary(raw: impl Into<Vec<u8>>) -> Self { + Self { + inner: DataInner::Binary(raw.into()), + } + } + + /// Mark the data as text (post-processing) + pub fn text(raw: impl Into<String>) -> Self { + Self { + inner: DataInner::Text(raw.into()), + } + } + + #[cfg(feature = "json")] + pub fn json(raw: impl Into<serde_json::Value>) -> Self { + Self { + inner: DataInner::Json(raw.into()), + } + } + + /// Empty test data + pub fn new() -> Self { + Self::text("") + } + + /// Load test data from a file + pub fn read_from( + path: &std::path::Path, + data_format: Option<DataFormat>, + ) -> Result<Self, crate::Error> { + let data = match data_format { + Some(df) => match df { + DataFormat::Binary => { + let data = std::fs::read(path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + Self::binary(data) + } + DataFormat::Text => { + let data = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + Self::text(data) + } + #[cfg(feature = "json")] + DataFormat::Json => { + let data = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + Self::json(serde_json::from_str::<serde_json::Value>(&data).unwrap()) + } + }, + None => { + let data = std::fs::read(path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + let data = Self::binary(data); + match path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default() + { + #[cfg(feature = "json")] + "json" => data.try_coerce(DataFormat::Json), + _ => data.try_coerce(DataFormat::Text), + } + } + }; + Ok(data) + } + + /// Overwrite a snapshot + pub fn write_to(&self, path: &std::path::Path) -> Result<(), crate::Error> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + format!("Failed to create parent dir for {}: {}", path.display(), e) + })?; + } + std::fs::write(path, self.to_bytes()) + .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into()) + } + + /// Post-process text + /// + /// See [utils][crate::utils] + pub fn normalize(self, op: impl Normalize) -> Self { + op.normalize(self) + } + + /// Return the underlying `String` + /// + /// Note: this will not inspect binary data for being a valid `String`. + pub fn render(&self) -> Option<String> { + match &self.inner { + DataInner::Binary(_) => None, + DataInner::Text(data) => Some(data.to_owned()), + #[cfg(feature = "json")] + DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()), + } + } + + pub fn to_bytes(&self) -> Vec<u8> { + match &self.inner { + DataInner::Binary(data) => data.clone(), + DataInner::Text(data) => data.clone().into_bytes(), + #[cfg(feature = "json")] + DataInner::Json(value) => serde_json::to_vec_pretty(value).unwrap(), + } + } + + pub fn try_coerce(self, format: DataFormat) -> Self { + match (self.inner, format) { + (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner), + (DataInner::Text(inner), DataFormat::Text) => Self::text(inner), + #[cfg(feature = "json")] + (DataInner::Json(inner), DataFormat::Json) => Self::json(inner), + (DataInner::Binary(inner), _) => { + if is_binary(&inner) { + Self::binary(inner) + } else { + match String::from_utf8(inner) { + Ok(str) => { + let coerced = Self::text(str).try_coerce(format); + // if the Text cannot be coerced into the correct format + // reset it back to Binary + if coerced.format() != format { + coerced.try_coerce(DataFormat::Binary) + } else { + coerced + } + } + Err(err) => { + let bin = err.into_bytes(); + Self::binary(bin) + } + } + } + } + #[cfg(feature = "json")] + (DataInner::Text(inner), DataFormat::Json) => { + match serde_json::from_str::<serde_json::Value>(&inner) { + Ok(json) => Self::json(json), + Err(_) => Self::text(inner), + } + } + (inner, DataFormat::Binary) => Self::binary(Self { inner }.to_bytes()), + // This variant is already covered unless structured data is enabled + #[cfg(feature = "structured-data")] + (inner, DataFormat::Text) => { + let remake = Self { inner }; + if let Some(str) = remake.render() { + Self::text(str) + } else { + remake + } + } + } + } + + /// Outputs the current `DataFormat` of the underlying data + pub fn format(&self) -> DataFormat { + match &self.inner { + DataInner::Binary(_) => DataFormat::Binary, + DataInner::Text(_) => DataFormat::Text, + #[cfg(feature = "json")] + DataInner::Json(_) => DataFormat::Json, + } + } +} + +impl std::fmt::Display for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner { + DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f), + DataInner::Text(data) => data.fmt(f), + #[cfg(feature = "json")] + DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f), + } + } +} + +impl Default for Data { + fn default() -> Self { + Self::new() + } +} + +impl<'d> From<&'d Data> for Data { + fn from(other: &'d Data) -> Self { + other.clone() + } +} + +impl From<Vec<u8>> for Data { + fn from(other: Vec<u8>) -> Self { + Self::binary(other) + } +} + +impl<'b> From<&'b [u8]> for Data { + fn from(other: &'b [u8]) -> Self { + other.to_owned().into() + } +} + +impl From<String> for Data { + fn from(other: String) -> Self { + Self::text(other) + } +} + +impl<'s> From<&'s String> for Data { + fn from(other: &'s String) -> Self { + other.clone().into() + } +} + +impl<'s> From<&'s str> for Data { + fn from(other: &'s str) -> Self { + other.to_owned().into() + } +} + +pub trait Normalize { + fn normalize(&self, data: Data) -> Data; +} + +pub struct NormalizeNewlines; +impl Normalize for NormalizeNewlines { + fn normalize(&self, data: Data) -> Data { + match data.inner { + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = crate::utils::normalize_lines(&text); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_lines); + Data::json(value) + } + } + } +} + +pub struct NormalizePaths; +impl Normalize for NormalizePaths { + fn normalize(&self, data: Data) -> Data { + match data.inner { + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = crate::utils::normalize_paths(&text); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_paths); + Data::json(value) + } + } + } +} + +pub struct NormalizeMatches<'a> { + substitutions: &'a crate::Substitutions, + pattern: &'a Data, +} + +impl<'a> NormalizeMatches<'a> { + pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self { + NormalizeMatches { + substitutions, + pattern, + } + } +} + +impl Normalize for NormalizeMatches<'_> { + fn normalize(&self, data: Data) -> Data { + match data.inner { + DataInner::Binary(bin) => Data::binary(bin), + DataInner::Text(text) => { + let lines = self + .substitutions + .normalize(&text, &self.pattern.render().unwrap()); + Data::text(lines) + } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + if let DataInner::Json(exp) = &self.pattern.inner { + normalize_value_matches(&mut value, exp, self.substitutions); + } + Data::json(value) + } + } + } +} + +#[cfg(feature = "structured-data")] +fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) { + match value { + serde_json::Value::String(str) => { + *str = op(str); + } + serde_json::Value::Array(arr) => { + for value in arr.iter_mut() { + normalize_value(value, op) + } + } + serde_json::Value::Object(obj) => { + for (_, value) in obj.iter_mut() { + normalize_value(value, op) + } + } + _ => {} + } +} + +#[cfg(feature = "structured-data")] +fn normalize_value_matches( + actual: &mut serde_json::Value, + expected: &serde_json::Value, + substitutions: &crate::Substitutions, +) { + use serde_json::Value::*; + match (actual, expected) { + // "{...}" is a wildcard + (act, String(exp)) if exp == "{...}" => { + *act = serde_json::json!("{...}"); + } + (String(act), String(exp)) => { + *act = substitutions.normalize(act, exp); + } + (Array(act), Array(exp)) => { + let wildcard = String("{...}".to_string()); + let mut sections = exp.split(|e| e == &wildcard).peekable(); + let mut processed = 0; + while let Some(expected_subset) = sections.next() { + // Process all values in the current section + if !expected_subset.is_empty() { + let actual_subset = &mut act[processed..processed + expected_subset.len()]; + for (a, e) in actual_subset.iter_mut().zip(expected_subset) { + normalize_value_matches(a, e, substitutions); + } + processed += expected_subset.len(); + } + + if let Some(next_section) = sections.peek() { + // If the next section has nothing in it, replace from processed to end with + // a single "{...}" + if next_section.is_empty() { + act.splice(processed.., vec![wildcard.clone()]); + processed += 1; + } else { + let first = next_section.first().unwrap(); + // Replace everything up until the value we are looking for with + // a single "{...}". + if let Some(index) = act.iter().position(|v| v == first) { + act.splice(processed..index, vec![wildcard.clone()]); + processed += 1; + } else { + // If we cannot find the value we are looking for return early + break; + } + } + } + } + } + (Object(act), Object(exp)) => { + for (a, e) in act.iter_mut().zip(exp).filter(|(a, e)| a.0 == e.0) { + normalize_value_matches(a.1, e.1, substitutions) + } + } + (_, _) => {} + } +} + +#[cfg(feature = "detect-encoding")] +fn is_binary(data: &[u8]) -> bool { + match content_inspector::inspect(data) { + content_inspector::ContentType::BINARY | + // We don't support these + content_inspector::ContentType::UTF_16LE | + content_inspector::ContentType::UTF_16BE | + content_inspector::ContentType::UTF_32LE | + content_inspector::ContentType::UTF_32BE => { + true + }, + content_inspector::ContentType::UTF_8 | + content_inspector::ContentType::UTF_8_BOM => { + false + }, + } +} + +#[cfg(not(feature = "detect-encoding"))] +fn is_binary(_data: &[u8]) -> bool { + false +} + +#[cfg(test)] +mod test { + use super::*; + #[cfg(feature = "json")] + use serde_json::json; + + // Tests for checking to_bytes and render produce the same results + #[test] + fn text_to_bytes_render() { + let d = Data::text(String::from("test")); + let bytes = d.to_bytes(); + let bytes = String::from_utf8(bytes).unwrap(); + let rendered = d.render().unwrap(); + assert_eq!(bytes, rendered); + } + + #[test] + #[cfg(feature = "json")] + fn json_to_bytes_render() { + let d = Data::json(json!({"name": "John\\Doe\r\n"})); + let bytes = d.to_bytes(); + let bytes = String::from_utf8(bytes).unwrap(); + let rendered = d.render().unwrap(); + assert_eq!(bytes, rendered); + } + + // Tests for checking all types are coercible to each other and + // for when the coercion should fail + #[test] + fn binary_to_text() { + let binary = String::from("test").into_bytes(); + let d = Data::binary(binary); + let text = d.try_coerce(DataFormat::Text); + assert_eq!(DataFormat::Text, text.format()) + } + + #[test] + fn binary_to_text_not_utf8() { + let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Text); + assert_ne!(DataFormat::Text, d.format()); + assert_eq!(DataFormat::Binary, d.format()); + } + + #[test] + #[cfg(feature = "json")] + fn binary_to_json() { + let value = json!({"name": "John\\Doe\r\n"}); + let binary = serde_json::to_vec_pretty(&value).unwrap(); + let d = Data::binary(binary); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Json, json.format()); + } + + #[test] + #[cfg(feature = "json")] + fn binary_to_json_not_utf8() { + let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Json); + assert_ne!(DataFormat::Json, d.format()); + assert_eq!(DataFormat::Binary, d.format()); + } + + #[test] + #[cfg(feature = "json")] + fn binary_to_json_not_json() { + let binary = String::from("test").into_bytes(); + let d = Data::binary(binary); + let d = d.try_coerce(DataFormat::Json); + assert_ne!(DataFormat::Json, d.format()); + assert_eq!(DataFormat::Binary, d.format()); + } + + #[test] + fn text_to_binary() { + let text = String::from("test"); + let d = Data::text(text); + let binary = d.try_coerce(DataFormat::Binary); + assert_eq!(DataFormat::Binary, binary.format()); + } + + #[test] + #[cfg(feature = "json")] + fn text_to_json() { + let value = json!({"name": "John\\Doe\r\n"}); + let text = serde_json::to_string_pretty(&value).unwrap(); + let d = Data::text(text); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Json, json.format()); + } + + #[test] + #[cfg(feature = "json")] + fn text_to_json_not_json() { + let text = String::from("test"); + let d = Data::text(text); + let json = d.try_coerce(DataFormat::Json); + assert_eq!(DataFormat::Text, json.format()); + } + + #[test] + #[cfg(feature = "json")] + fn json_to_binary() { + let value = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(value); + let binary = d.try_coerce(DataFormat::Binary); + assert_eq!(DataFormat::Binary, binary.format()); + } + + #[test] + #[cfg(feature = "json")] + fn json_to_text() { + let value = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(value); + let text = d.try_coerce(DataFormat::Text); + assert_eq!(DataFormat::Text, text.format()); + } + + // Tests for coercible conversions create the same output as to_bytes/render + // + // render does not need to be checked against bin -> text since render + // outputs None for binary + #[test] + fn text_to_bin_coerce_equals_to_bytes() { + let text = String::from("test"); + let d = Data::text(text); + let binary = d.clone().try_coerce(DataFormat::Binary); + assert_eq!(Data::binary(d.to_bytes()), binary); + } + + #[test] + #[cfg(feature = "json")] + fn json_to_bin_coerce_equals_to_bytes() { + let json = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(json); + let binary = d.clone().try_coerce(DataFormat::Binary); + assert_eq!(Data::binary(d.to_bytes()), binary); + } + + #[test] + #[cfg(feature = "json")] + fn json_to_text_coerce_equals_render() { + let json = json!({"name": "John\\Doe\r\n"}); + let d = Data::json(json); + let text = d.clone().try_coerce(DataFormat::Text); + assert_eq!(Data::text(d.render().unwrap()), text); + } + + // Tests for normalization on json + #[test] + #[cfg(feature = "json")] + fn json_normalize_paths_and_lines() { + let json = json!({"name": "John\\Doe\r\n"}); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data); + let data = data.normalize(NormalizeNewlines); + assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data); + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_obj_paths_and_lines() { + let json = json!({ + "person": { + "name": "John\\Doe\r\n", + "nickname": "Jo\\hn\r\n", + } + }); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let assert = json!({ + "person": { + "name": "John/Doe\r\n", + "nickname": "Jo/hn\r\n", + } + }); + assert_eq!(Data::json(assert), data); + let data = data.normalize(NormalizeNewlines); + let assert = json!({ + "person": { + "name": "John/Doe\n", + "nickname": "Jo/hn\n", + } + }); + assert_eq!(Data::json(assert), data); + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_array_paths_and_lines() { + let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]}); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]}); + assert_eq!(Data::json(paths), data); + let data = data.normalize(NormalizeNewlines); + let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]}); + assert_eq!(Data::json(new_lines), data); + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_array_obj_paths_and_lines() { + let json = json!({ + "people": [ + { + "name": "John\\Doe\r\n", + "nickname": "Jo\\hn\r\n", + } + ] + }); + let data = Data::json(json); + let data = data.normalize(NormalizePaths); + let paths = json!({ + "people": [ + { + "name": "John/Doe\r\n", + "nickname": "Jo/hn\r\n", + } + ] + }); + assert_eq!(Data::json(paths), data); + let data = data.normalize(NormalizeNewlines); + let new_lines = json!({ + "people": [ + { + "name": "John/Doe\n", + "nickname": "Jo/hn\n", + } + ] + }); + assert_eq!(Data::json(new_lines), data); + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_matches_string() { + let exp = json!({"name": "{...}"}); + let expected = Data::json(exp); + let actual = json!({"name": "JohnDoe"}); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_matches_array() { + let exp = json!({"people": "{...}"}); + let expected = Data::json(exp); + let actual = json!({ + "people": [ + { + "name": "JohnDoe", + "nickname": "John", + } + ] + }); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_matches_obj() { + let exp = json!({"people": "{...}"}); + let expected = Data::json(exp); + let actual = json!({ + "people": { + "name": "JohnDoe", + "nickname": "John", + } + }); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_matches_diff_order_array() { + let exp = json!({ + "people": ["John", "Jane"] + }); + let expected = Data::json(exp); + let actual = json!({ + "people": ["Jane", "John"] + }); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_ne!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_wildcard_object_first() { + let exp = json!({ + "people": [ + "{...}", + { + "name": "three", + "nickname": "3", + } + ] + }); + let expected = Data::json(exp); + let actual = json!({ + "people": [ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + } + ] + }); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_wildcard_array_first() { + let exp = json!([ + "{...}", + { + "name": "three", + "nickname": "3", + } + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + } + ]); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_wildcard_array_first_last() { + let exp = json!([ + "{...}", + { + "name": "two", + "nickname": "2", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + }, + { + "name": "four", + "nickname": "4", + } + ]); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_wildcard_array_middle_last() { + let exp = json!([ + { + "name": "one", + "nickname": "1", + }, + "{...}", + { + "name": "three", + "nickname": "3", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "three", + "nickname": "3", + }, + { + "name": "four", + "nickname": "4", + }, + { + "name": "five", + "nickname": "5", + } + ]); + let actual = Data::json(actual).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) { + assert_eq!(exp, act); + } + } + + #[test] + #[cfg(feature = "json")] + fn json_normalize_wildcard_array_middle_last_early_return() { + let exp = json!([ + { + "name": "one", + "nickname": "1", + }, + "{...}", + { + "name": "three", + "nickname": "3", + }, + "{...}" + ]); + let expected = Data::json(exp); + let actual = json!([ + { + "name": "one", + "nickname": "1", + }, + { + "name": "two", + "nickname": "2", + }, + { + "name": "four", + "nickname": "4", + }, + { + "name": "five", + "nickname": "5", + } + ]); + let actual_normalized = Data::json(actual.clone()).normalize(NormalizeMatches { + substitutions: &Default::default(), + pattern: &expected, + }); + if let DataInner::Json(act) = actual_normalized.inner { + assert_eq!(act, actual); + } + } +} diff --git a/vendor/snapbox/src/error.rs b/vendor/snapbox/src/error.rs new file mode 100644 index 0000000..55e9018 --- /dev/null +++ b/vendor/snapbox/src/error.rs @@ -0,0 +1,95 @@ +#[derive(Clone, Debug)] +pub struct Error { + inner: String, + backtrace: Option<Backtrace>, +} + +impl Error { + pub fn new(inner: impl std::fmt::Display) -> Self { + Self::with_string(inner.to_string()) + } + + fn with_string(inner: String) -> Self { + Self { + inner, + backtrace: Backtrace::new(), + } + } +} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.inner)?; + if let Some(backtrace) = self.backtrace.as_ref() { + writeln!(f)?; + writeln!(f, "Backtrace:")?; + writeln!(f, "{}", backtrace)?; + } + Ok(()) + } +} + +impl std::error::Error for Error {} + +impl<'s> From<&'s str> for Error { + fn from(other: &'s str) -> Self { + Self::with_string(other.to_owned()) + } +} + +impl<'s> From<&'s String> for Error { + fn from(other: &'s String) -> Self { + Self::with_string(other.clone()) + } +} + +impl From<String> for Error { + fn from(other: String) -> Self { + Self::with_string(other) + } +} + +#[cfg(feature = "debug")] +#[derive(Debug, Clone)] +struct Backtrace(backtrace::Backtrace); + +#[cfg(feature = "debug")] +impl Backtrace { + fn new() -> Option<Self> { + Some(Self(backtrace::Backtrace::new())) + } +} + +#[cfg(feature = "debug")] +impl std::fmt::Display for Backtrace { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + // `backtrace::Backtrace` uses `Debug` instead of `Display` + write!(f, "{:?}", self.0) + } +} + +#[cfg(not(feature = "debug"))] +#[derive(Debug, Copy, Clone)] +struct Backtrace; + +#[cfg(not(feature = "debug"))] +impl Backtrace { + fn new() -> Option<Self> { + None + } +} + +#[cfg(not(feature = "debug"))] +impl std::fmt::Display for Backtrace { + fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result { + Ok(()) + } +} diff --git a/vendor/snapbox/src/harness.rs b/vendor/snapbox/src/harness.rs new file mode 100644 index 0000000..77e085e --- /dev/null +++ b/vendor/snapbox/src/harness.rs @@ -0,0 +1,214 @@ +//! [`Harness`] for discovering test inputs and asserting against snapshot files +//! +//! # Examples +//! +//! ```rust,no_run +//! snapbox::harness::Harness::new( +//! "tests/fixtures/invalid", +//! setup, +//! test, +//! ) +//! .select(["tests/cases/*.in"]) +//! .action_env("SNAPSHOTS") +//! .test(); +//! +//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case { +//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned(); +//! let expected = input_path.with_extension("out"); +//! snapbox::harness::Case { +//! name, +//! fixture: input_path, +//! expected, +//! } +//! } +//! +//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> { +//! let raw = std::fs::read_to_string(input_path)?; +//! let num = raw.parse::<usize>()?; +//! +//! let actual = num + 10; +//! +//! Ok(actual) +//! } +//! ``` + +use crate::data::{DataFormat, NormalizeNewlines}; +use crate::Action; + +use libtest_mimic::Trial; + +pub struct Harness<S, T> { + root: std::path::PathBuf, + overrides: Option<ignore::overrides::Override>, + setup: S, + test: T, + action: Action, +} + +impl<S, T, I, E> Harness<S, T> +where + I: std::fmt::Display, + E: std::fmt::Display, + S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static, + T: Fn(&std::path::Path) -> Result<I, E> + Send + Sync + 'static + Clone, +{ + pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self { + Self { + root: root.into(), + overrides: None, + setup, + test, + action: Action::Verify, + } + } + + /// Path patterns for selecting input files + /// + /// This used gitignore syntax + pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self { + let mut overrides = ignore::overrides::OverrideBuilder::new(&self.root); + for line in patterns { + overrides.add(line).unwrap(); + } + self.overrides = Some(overrides.build().unwrap()); + self + } + + /// Read the failure action from an environment variable + pub fn action_env(mut self, var_name: &str) -> Self { + let action = Action::with_env_var(var_name); + self.action = action.unwrap_or(self.action); + self + } + + /// Override the failure action + pub fn action(mut self, action: Action) -> Self { + self.action = action; + self + } + + /// Run tests + pub fn test(self) -> ! { + let mut walk = ignore::WalkBuilder::new(&self.root); + walk.standard_filters(false); + let tests = walk.build().filter_map(|entry| { + let entry = entry.unwrap(); + let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false); + let path = entry.into_path(); + if let Some(overrides) = &self.overrides { + overrides + .matched(&path, is_dir) + .is_whitelist() + .then_some(path) + } else { + Some(path) + } + }); + + let tests: Vec<_> = tests + .into_iter() + .map(|path| { + let case = (self.setup)(path); + let test = self.test.clone(); + Trial::test(case.name.clone(), move || { + let actual = (test)(&case.fixture)?; + let actual = actual.to_string(); + let actual = crate::Data::text(actual).normalize(NormalizeNewlines); + #[allow(deprecated)] + let verify = Verifier::new() + .palette(crate::report::Palette::auto()) + .action(self.action); + verify.verify(&case.expected, actual)?; + Ok(()) + }) + .with_ignored_flag(self.action == Action::Ignore) + }) + .collect(); + + let args = libtest_mimic::Arguments::from_args(); + libtest_mimic::run(&args, tests).exit() + } +} + +struct Verifier { + palette: crate::report::Palette, + action: Action, +} + +impl Verifier { + fn new() -> Self { + Default::default() + } + + fn palette(mut self, palette: crate::report::Palette) -> Self { + self.palette = palette; + self + } + + fn action(mut self, action: Action) -> Self { + self.action = action; + self + } + + fn verify(&self, expected_path: &std::path::Path, actual: crate::Data) -> crate::Result<()> { + match self.action { + Action::Skip => Ok(()), + Action::Ignore => { + let _ = self.try_verify(expected_path, actual); + Ok(()) + } + Action::Verify => self.try_verify(expected_path, actual), + Action::Overwrite => self.try_overwrite(expected_path, actual), + } + } + + fn try_overwrite( + &self, + expected_path: &std::path::Path, + actual: crate::Data, + ) -> crate::Result<()> { + actual.write_to(expected_path)?; + Ok(()) + } + + fn try_verify( + &self, + expected_path: &std::path::Path, + actual: crate::Data, + ) -> crate::Result<()> { + let expected = crate::Data::read_from(expected_path, Some(DataFormat::Text))? + .normalize(NormalizeNewlines); + + if expected != actual { + let mut buf = String::new(); + crate::report::write_diff( + &mut buf, + &expected, + &actual, + Some(&expected_path.display()), + None, + self.palette, + ) + .map_err(|e| e.to_string())?; + Err(buf.into()) + } else { + Ok(()) + } + } +} + +impl Default for Verifier { + fn default() -> Self { + Self { + #[allow(deprecated)] + palette: crate::report::Palette::auto(), + action: Action::Verify, + } + } +} + +pub struct Case { + pub name: String, + pub fixture: std::path::PathBuf, + pub expected: std::path::PathBuf, +} diff --git a/vendor/snapbox/src/lib.rs b/vendor/snapbox/src/lib.rs new file mode 100644 index 0000000..7084c15 --- /dev/null +++ b/vendor/snapbox/src/lib.rs @@ -0,0 +1,246 @@ +//! # Snapshot testing toolbox +//! +//! > When you have to treat your tests like pets, instead of [cattle][trycmd] +//! +//! `snapbox` is a snapshot-testing toolbox that is ready to use for verifying output from +//! - Function return values +//! - CLI stdout/stderr +//! - Filesystem changes +//! +//! It is also flexible enough to build your own test harness like [trycmd](https://crates.io/crates/trycmd). +//! +//! ## Which tool is right +//! +//! - [cram](https://bitheap.org/cram/): End-to-end CLI snapshotting agnostic of any programming language +//! - [trycmd](https://crates.io/crates/trycmd): For running a lot of blunt tests (limited test predicates) +//! - Particular attention is given to allow the test data to be pulled into documentation, like +//! with [mdbook](https://rust-lang.github.io/mdBook/) +//! - `snapbox`: When you want something like `trycmd` in one off +//! cases or you need to customize `trycmd`s behavior. +//! - [assert_cmd](https://crates.io/crates/assert_cmd) + +//! [assert_fs](https://crates.io/crates/assert_fs): Test cases follow a certain pattern but +//! special attention is needed in how to verify the results. +//! - Hand-written test cases: for peculiar circumstances +//! +//! ## Getting Started +//! +//! Testing Functions: +//! - [`assert_eq`][crate::assert_eq] and [`assert_matches`] for reusing diffing / pattern matching for non-snapshot testing +//! - [`assert_eq_path`][crate::assert_eq_path] and [`assert_matches_path`] for one-off assertions with the snapshot stored in a file +//! - [`harness::Harness`] for discovering test inputs and asserting against snapshot files: +//! +//! Testing Commands: +//! - [`cmd::Command`]: Process spawning for testing of non-interactive commands +//! - [`cmd::OutputAssert`]: Assert the state of a [`Command`][cmd::Command]'s +//! [`Output`][std::process::Output]. +//! +//! Testing Filesystem Interactions: +//! - [`path::PathFixture`]: Working directory for tests +//! - [`Assert`]: Diff a directory against files present in a pattern directory +//! +//! You can also build your own version of these with the lower-level building blocks these are +//! made of. +//! +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +//! +//! # Examples +//! +//! [`assert_matches`] +//! ```rust +//! snapbox::assert_matches("Hello [..] people!", "Hello many people!"); +//! ``` +//! +//! [`Assert`] +//! ```rust,no_run +//! let actual = "..."; +//! let expected_path = "tests/fixtures/help_output_is_clean.txt"; +//! snapbox::Assert::new() +//! .action_env("SNAPSHOTS") +//! .matches_path(expected_path, actual); +//! ``` +//! +//! [`harness::Harness`] +#![cfg_attr(not(feature = "harness"), doc = " ```rust,ignore")] +#![cfg_attr(feature = "harness", doc = " ```rust,no_run")] +//! snapbox::harness::Harness::new( +//! "tests/fixtures/invalid", +//! setup, +//! test, +//! ) +//! .select(["tests/cases/*.in"]) +//! .action_env("SNAPSHOTS") +//! .test(); +//! +//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case { +//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned(); +//! let expected = input_path.with_extension("out"); +//! snapbox::harness::Case { +//! name, +//! fixture: input_path, +//! expected, +//! } +//! } +//! +//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> { +//! let raw = std::fs::read_to_string(input_path)?; +//! let num = raw.parse::<usize>()?; +//! +//! let actual = num + 10; +//! +//! Ok(actual) +//! } +//! ``` +//! +//! [trycmd]: https://docs.rs/trycmd + +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +mod action; +mod assert; +mod data; +mod error; +mod substitutions; + +pub mod cmd; +pub mod path; +pub mod report; +pub mod utils; + +#[cfg(feature = "harness")] +pub mod harness; + +pub use action::Action; +pub use action::DEFAULT_ACTION_ENV; +pub use assert::Assert; +pub use data::Data; +pub use data::DataFormat; +pub use data::{Normalize, NormalizeMatches, NormalizeNewlines, NormalizePaths}; +pub use error::Error; +pub use snapbox_macros::debug; +pub use substitutions::Substitutions; + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +/// Check if a value is the same as an expected value +/// +/// When the content is text, newlines are normalized. +/// +/// ```rust +/// let output = "something"; +/// let expected = "something"; +/// snapbox::assert_eq(expected, output); +/// ``` +#[track_caller] +pub fn assert_eq(expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) { + Assert::new().eq(expected, actual); +} + +/// Check if a value matches a pattern +/// +/// Pattern syntax: +/// - `...` is a line-wildcard when on a line by itself +/// - `[..]` is a character-wildcard when inside a line +/// - `[EXE]` matches `.exe` on Windows +/// +/// Normalization: +/// - Newlines +/// - `\` to `/` +/// +/// ```rust +/// let output = "something"; +/// let expected = "so[..]g"; +/// snapbox::assert_matches(expected, output); +/// ``` +#[track_caller] +pub fn assert_matches(pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) { + Assert::new().matches(pattern, actual); +} + +/// Check if a value matches the content of a file +/// +/// When the content is text, newlines are normalized. +/// +/// ```rust,no_run +/// let output = "something"; +/// let expected_path = "tests/snapshots/output.txt"; +/// snapbox::assert_eq_path(expected_path, output); +/// ``` +#[track_caller] +pub fn assert_eq_path(expected_path: impl AsRef<std::path::Path>, actual: impl Into<crate::Data>) { + Assert::new() + .action_env(DEFAULT_ACTION_ENV) + .eq_path(expected_path, actual); +} + +/// Check if a value matches the pattern in a file +/// +/// Pattern syntax: +/// - `...` is a line-wildcard when on a line by itself +/// - `[..]` is a character-wildcard when inside a line +/// - `[EXE]` matches `.exe` on Windows +/// +/// Normalization: +/// - Newlines +/// - `\` to `/` +/// +/// ```rust,no_run +/// let output = "something"; +/// let expected_path = "tests/snapshots/output.txt"; +/// snapbox::assert_matches_path(expected_path, output); +/// ``` +#[track_caller] +pub fn assert_matches_path( + pattern_path: impl AsRef<std::path::Path>, + actual: impl Into<crate::Data>, +) { + Assert::new() + .action_env(DEFAULT_ACTION_ENV) + .matches_path(pattern_path, actual); +} + +/// Check if a path matches the content of another path, recursively +/// +/// When the content is text, newlines are normalized. +/// +/// ```rust,no_run +/// let output_root = "..."; +/// let expected_root = "tests/snapshots/output.txt"; +/// snapbox::assert_subset_eq(expected_root, output_root); +/// ``` +#[cfg(feature = "path")] +#[track_caller] +pub fn assert_subset_eq( + expected_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, +) { + Assert::new() + .action_env(DEFAULT_ACTION_ENV) + .subset_eq(expected_root, actual_root); +} + +/// Check if a path matches the pattern of another path, recursively +/// +/// Pattern syntax: +/// - `...` is a line-wildcard when on a line by itself +/// - `[..]` is a character-wildcard when inside a line +/// - `[EXE]` matches `.exe` on Windows +/// +/// Normalization: +/// - Newlines +/// - `\` to `/` +/// +/// ```rust,no_run +/// let output_root = "..."; +/// let expected_root = "tests/snapshots/output.txt"; +/// snapbox::assert_subset_matches(expected_root, output_root); +/// ``` +#[cfg(feature = "path")] +#[track_caller] +pub fn assert_subset_matches( + pattern_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, +) { + Assert::new() + .action_env(DEFAULT_ACTION_ENV) + .subset_matches(pattern_root, actual_root); +} diff --git a/vendor/snapbox/src/path.rs b/vendor/snapbox/src/path.rs new file mode 100644 index 0000000..523b038 --- /dev/null +++ b/vendor/snapbox/src/path.rs @@ -0,0 +1,687 @@ +//! Initialize working directories and assert on how they've changed + +#[cfg(feature = "path")] +use crate::data::{NormalizeMatches, NormalizeNewlines, NormalizePaths}; +/// Working directory for tests +#[derive(Debug)] +pub struct PathFixture(PathFixtureInner); + +#[derive(Debug)] +enum PathFixtureInner { + None, + Immutable(std::path::PathBuf), + #[cfg(feature = "path")] + MutablePath(std::path::PathBuf), + #[cfg(feature = "path")] + MutableTemp { + temp: tempfile::TempDir, + path: std::path::PathBuf, + }, +} + +impl PathFixture { + pub fn none() -> Self { + Self(PathFixtureInner::None) + } + + pub fn immutable(target: &std::path::Path) -> Self { + Self(PathFixtureInner::Immutable(target.to_owned())) + } + + #[cfg(feature = "path")] + pub fn mutable_temp() -> Result<Self, crate::Error> { + let temp = tempfile::tempdir().map_err(|e| e.to_string())?; + // We need to get the `/private` prefix on Mac so variable substitutions work + // correctly + let path = canonicalize(temp.path()) + .map_err(|e| format!("Failed to canonicalize {}: {}", temp.path().display(), e))?; + Ok(Self(PathFixtureInner::MutableTemp { temp, path })) + } + + #[cfg(feature = "path")] + pub fn mutable_at(target: &std::path::Path) -> Result<Self, crate::Error> { + let _ = std::fs::remove_dir_all(target); + std::fs::create_dir_all(target) + .map_err(|e| format!("Failed to create {}: {}", target.display(), e))?; + Ok(Self(PathFixtureInner::MutablePath(target.to_owned()))) + } + + #[cfg(feature = "path")] + pub fn with_template(self, template_root: &std::path::Path) -> Result<Self, crate::Error> { + match &self.0 { + PathFixtureInner::None | PathFixtureInner::Immutable(_) => { + return Err("Sandboxing is disabled".into()); + } + PathFixtureInner::MutablePath(path) | PathFixtureInner::MutableTemp { path, .. } => { + crate::debug!( + "Initializing {} from {}", + path.display(), + template_root.display() + ); + copy_template(template_root, path)?; + } + } + + Ok(self) + } + + pub fn is_mutable(&self) -> bool { + match &self.0 { + PathFixtureInner::None | PathFixtureInner::Immutable(_) => false, + #[cfg(feature = "path")] + PathFixtureInner::MutablePath(_) => true, + #[cfg(feature = "path")] + PathFixtureInner::MutableTemp { .. } => true, + } + } + + pub fn path(&self) -> Option<&std::path::Path> { + match &self.0 { + PathFixtureInner::None => None, + PathFixtureInner::Immutable(path) => Some(path.as_path()), + #[cfg(feature = "path")] + PathFixtureInner::MutablePath(path) => Some(path.as_path()), + #[cfg(feature = "path")] + PathFixtureInner::MutableTemp { path, .. } => Some(path.as_path()), + } + } + + /// Explicitly close to report errors + pub fn close(self) -> Result<(), std::io::Error> { + match self.0 { + PathFixtureInner::None | PathFixtureInner::Immutable(_) => Ok(()), + #[cfg(feature = "path")] + PathFixtureInner::MutablePath(_) => Ok(()), + #[cfg(feature = "path")] + PathFixtureInner::MutableTemp { temp, .. } => temp.close(), + } + } +} + +impl Default for PathFixture { + fn default() -> Self { + Self::none() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PathDiff { + Failure(crate::Error), + TypeMismatch { + expected_path: std::path::PathBuf, + actual_path: std::path::PathBuf, + expected_type: FileType, + actual_type: FileType, + }, + LinkMismatch { + expected_path: std::path::PathBuf, + actual_path: std::path::PathBuf, + expected_target: std::path::PathBuf, + actual_target: std::path::PathBuf, + }, + ContentMismatch { + expected_path: std::path::PathBuf, + actual_path: std::path::PathBuf, + expected_content: crate::Data, + actual_content: crate::Data, + }, +} + +impl PathDiff { + /// Report differences between `actual_root` and `pattern_root` + /// + /// Note: Requires feature flag `path` + #[cfg(feature = "path")] + pub fn subset_eq_iter( + pattern_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, + ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> { + let pattern_root = pattern_root.into(); + let actual_root = actual_root.into(); + Self::subset_eq_iter_inner(pattern_root, actual_root) + } + + #[cfg(feature = "path")] + pub(crate) fn subset_eq_iter_inner( + expected_root: std::path::PathBuf, + actual_root: std::path::PathBuf, + ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> { + let walker = Walk::new(&expected_root); + walker.map(move |r| { + let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?; + let rel = expected_path.strip_prefix(&expected_root).unwrap(); + let actual_path = actual_root.join(rel); + + let expected_type = FileType::from_path(&expected_path); + let actual_type = FileType::from_path(&actual_path); + if expected_type != actual_type { + return Err(Self::TypeMismatch { + expected_path, + actual_path, + expected_type, + actual_type, + }); + } + + match expected_type { + FileType::Symlink => { + let expected_target = std::fs::read_link(&expected_path).ok(); + let actual_target = std::fs::read_link(&actual_path).ok(); + if expected_target != actual_target { + return Err(Self::LinkMismatch { + expected_path, + actual_path, + expected_target: expected_target.unwrap(), + actual_target: actual_target.unwrap(), + }); + } + } + FileType::File => { + let mut actual = + crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?; + + let expected = crate::Data::read_from(&expected_path, None) + .map(|d| d.normalize(NormalizeNewlines)) + .map_err(Self::Failure)?; + + actual = actual + .try_coerce(expected.format()) + .normalize(NormalizeNewlines); + + if expected != actual { + return Err(Self::ContentMismatch { + expected_path, + actual_path, + expected_content: expected, + actual_content: actual, + }); + } + } + FileType::Dir | FileType::Unknown | FileType::Missing => {} + } + + Ok((expected_path, actual_path)) + }) + } + + /// Report differences between `actual_root` and `pattern_root` + /// + /// Note: Requires feature flag `path` + #[cfg(feature = "path")] + pub fn subset_matches_iter( + pattern_root: impl Into<std::path::PathBuf>, + actual_root: impl Into<std::path::PathBuf>, + substitutions: &crate::Substitutions, + ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ { + let pattern_root = pattern_root.into(); + let actual_root = actual_root.into(); + Self::subset_matches_iter_inner(pattern_root, actual_root, substitutions) + } + + #[cfg(feature = "path")] + pub(crate) fn subset_matches_iter_inner( + expected_root: std::path::PathBuf, + actual_root: std::path::PathBuf, + substitutions: &crate::Substitutions, + ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ { + let walker = Walk::new(&expected_root); + walker.map(move |r| { + let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?; + let rel = expected_path.strip_prefix(&expected_root).unwrap(); + let actual_path = actual_root.join(rel); + + let expected_type = FileType::from_path(&expected_path); + let actual_type = FileType::from_path(&actual_path); + if expected_type != actual_type { + return Err(Self::TypeMismatch { + expected_path, + actual_path, + expected_type, + actual_type, + }); + } + + match expected_type { + FileType::Symlink => { + let expected_target = std::fs::read_link(&expected_path).ok(); + let actual_target = std::fs::read_link(&actual_path).ok(); + if expected_target != actual_target { + return Err(Self::LinkMismatch { + expected_path, + actual_path, + expected_target: expected_target.unwrap(), + actual_target: actual_target.unwrap(), + }); + } + } + FileType::File => { + let mut actual = + crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?; + + let expected = crate::Data::read_from(&expected_path, None) + .map(|d| d.normalize(NormalizeNewlines)) + .map_err(Self::Failure)?; + + actual = actual + .try_coerce(expected.format()) + .normalize(NormalizePaths) + .normalize(NormalizeNewlines) + .normalize(NormalizeMatches::new(substitutions, &expected)); + + if expected != actual { + return Err(Self::ContentMismatch { + expected_path, + actual_path, + expected_content: expected, + actual_content: actual, + }); + } + } + FileType::Dir | FileType::Unknown | FileType::Missing => {} + } + + Ok((expected_path, actual_path)) + }) + } +} + +impl PathDiff { + pub fn expected_path(&self) -> Option<&std::path::Path> { + match &self { + Self::Failure(_msg) => None, + Self::TypeMismatch { + expected_path, + actual_path: _, + expected_type: _, + actual_type: _, + } => Some(expected_path), + Self::LinkMismatch { + expected_path, + actual_path: _, + expected_target: _, + actual_target: _, + } => Some(expected_path), + Self::ContentMismatch { + expected_path, + actual_path: _, + expected_content: _, + actual_content: _, + } => Some(expected_path), + } + } + + pub fn write( + &self, + f: &mut dyn std::fmt::Write, + palette: crate::report::Palette, + ) -> Result<(), std::fmt::Error> { + match &self { + Self::Failure(msg) => { + writeln!(f, "{}", palette.error(msg))?; + } + Self::TypeMismatch { + expected_path, + actual_path: _actual_path, + expected_type, + actual_type, + } => { + writeln!( + f, + "{}: Expected {}, was {}", + expected_path.display(), + palette.info(expected_type), + palette.error(actual_type) + )?; + } + Self::LinkMismatch { + expected_path, + actual_path: _actual_path, + expected_target, + actual_target, + } => { + writeln!( + f, + "{}: Expected {}, was {}", + expected_path.display(), + palette.info(expected_target.display()), + palette.error(actual_target.display()) + )?; + } + Self::ContentMismatch { + expected_path, + actual_path, + expected_content, + actual_content, + } => { + crate::report::write_diff( + f, + expected_content, + actual_content, + Some(&expected_path.display()), + Some(&actual_path.display()), + palette, + )?; + } + } + + Ok(()) + } + + pub fn overwrite(&self) -> Result<(), crate::Error> { + match self { + // Not passing the error up because users most likely want to treat a processing error + // differently than an overwrite error + Self::Failure(_err) => Ok(()), + Self::TypeMismatch { + expected_path, + actual_path, + expected_type: _, + actual_type, + } => { + match actual_type { + FileType::Dir => { + std::fs::remove_dir_all(expected_path).map_err(|e| { + format!("Failed to remove {}: {}", expected_path.display(), e) + })?; + } + FileType::File | FileType::Symlink => { + std::fs::remove_file(expected_path).map_err(|e| { + format!("Failed to remove {}: {}", expected_path.display(), e) + })?; + } + FileType::Unknown | FileType::Missing => {} + } + shallow_copy(expected_path, actual_path) + } + Self::LinkMismatch { + expected_path, + actual_path, + expected_target: _, + actual_target: _, + } => shallow_copy(expected_path, actual_path), + Self::ContentMismatch { + expected_path, + actual_path: _, + expected_content: _, + actual_content, + } => actual_content.write_to(expected_path), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FileType { + Dir, + File, + Symlink, + Unknown, + Missing, +} + +impl FileType { + pub fn from_path(path: &std::path::Path) -> Self { + let meta = path.symlink_metadata(); + match meta { + Ok(meta) => { + if meta.is_dir() { + Self::Dir + } else if meta.is_file() { + Self::File + } else { + let target = std::fs::read_link(path).ok(); + if target.is_some() { + Self::Symlink + } else { + Self::Unknown + } + } + } + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => Self::Missing, + _ => Self::Unknown, + }, + } + } +} + +impl FileType { + fn as_str(self) -> &'static str { + match self { + Self::Dir => "dir", + Self::File => "file", + Self::Symlink => "symlink", + Self::Unknown => "unknown", + Self::Missing => "missing", + } + } +} + +impl std::fmt::Display for FileType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } +} + +/// Recursively walk a path +/// +/// Note: Ignores `.keep` files +#[cfg(feature = "path")] +pub struct Walk { + inner: walkdir::IntoIter, +} + +#[cfg(feature = "path")] +impl Walk { + pub fn new(path: &std::path::Path) -> Self { + Self { + inner: walkdir::WalkDir::new(path).into_iter(), + } + } +} + +#[cfg(feature = "path")] +impl Iterator for Walk { + type Item = Result<std::path::PathBuf, std::io::Error>; + + fn next(&mut self) -> Option<Self::Item> { + while let Some(entry) = self.inner.next().map(|e| { + e.map(walkdir::DirEntry::into_path) + .map_err(std::io::Error::from) + }) { + if entry.as_ref().ok().and_then(|e| e.file_name()) + != Some(std::ffi::OsStr::new(".keep")) + { + return Some(entry); + } + } + None + } +} + +/// Copy a template into a [`PathFixture`] +/// +/// Note: Generally you'll use [`PathFixture::with_template`] instead. +/// +/// Note: Ignores `.keep` files +#[cfg(feature = "path")] +pub fn copy_template( + source: impl AsRef<std::path::Path>, + dest: impl AsRef<std::path::Path>, +) -> Result<(), crate::Error> { + let source = source.as_ref(); + let dest = dest.as_ref(); + let source = canonicalize(source) + .map_err(|e| format!("Failed to canonicalize {}: {}", source.display(), e))?; + std::fs::create_dir_all(dest) + .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?; + let dest = canonicalize(dest) + .map_err(|e| format!("Failed to canonicalize {}: {}", dest.display(), e))?; + + for current in Walk::new(&source) { + let current = current.map_err(|e| e.to_string())?; + let rel = current.strip_prefix(&source).unwrap(); + let target = dest.join(rel); + + shallow_copy(¤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 0000000..38f4f8f --- /dev/null +++ b/vendor/snapbox/src/report/color.rs @@ -0,0 +1,111 @@ +#[derive(Copy, Clone, Debug, Default)] +pub struct Palette { + pub(crate) info: anstyle::Style, + pub(crate) warn: anstyle::Style, + pub(crate) error: anstyle::Style, + pub(crate) hint: anstyle::Style, + pub(crate) expected: anstyle::Style, + pub(crate) actual: anstyle::Style, +} + +impl Palette { + pub fn color() -> Self { + if cfg!(feature = "color") { + Self { + info: anstyle::AnsiColor::Green.on_default(), + warn: anstyle::AnsiColor::Yellow.on_default(), + error: anstyle::AnsiColor::Red.on_default(), + hint: anstyle::Effects::DIMMED.into(), + expected: anstyle::AnsiColor::Green.on_default() | anstyle::Effects::UNDERLINE, + actual: anstyle::AnsiColor::Red.on_default() | anstyle::Effects::UNDERLINE, + } + } else { + Self::plain() + } + } + + pub fn plain() -> Self { + Self::default() + } + + #[deprecated(since = "0.4.9", note = "Renamed to `Palette::color")] + pub fn always() -> Self { + Self::color() + } + + #[deprecated(since = "0.4.9", note = "Renamed to `Palette::plain")] + pub fn never() -> Self { + Self::plain() + } + + #[deprecated( + since = "0.4.9", + note = "Use `Palette::always`, `auto` behavior is now implicit" + )] + pub fn auto() -> Self { + if is_colored() { + Self::color() + } else { + Self::plain() + } + } + + pub fn info<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.info) + } + + pub fn warn<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.warn) + } + + pub fn error<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.error) + } + + pub fn hint<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.hint) + } + + pub fn expected<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.expected) + } + + pub fn actual<D: std::fmt::Display>(self, item: D) -> Styled<D> { + Styled::new(item, self.actual) + } +} + +fn is_colored() -> bool { + #[cfg(feature = "color")] + { + anstream::AutoStream::choice(&std::io::stderr()) != anstream::ColorChoice::Never + } + #[cfg(not(feature = "color"))] + { + false + } +} + +pub(crate) use anstyle::Style; + +#[derive(Debug)] +pub struct Styled<D> { + display: D, + style: anstyle::Style, +} + +impl<D: std::fmt::Display> Styled<D> { + pub(crate) fn new(display: D, style: anstyle::Style) -> Self { + Self { display, style } + } +} + +impl<D: std::fmt::Display> std::fmt::Display for Styled<D> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.style.render())?; + self.display.fmt(f)?; + write!(f, "{}", self.style.render_reset())?; + Ok(()) + } +} diff --git a/vendor/snapbox/src/report/diff.rs b/vendor/snapbox/src/report/diff.rs new file mode 100644 index 0000000..b980733 --- /dev/null +++ b/vendor/snapbox/src/report/diff.rs @@ -0,0 +1,393 @@ +use crate::report::Styled; + +pub fn write_diff( + writer: &mut dyn std::fmt::Write, + expected: &crate::Data, + actual: &crate::Data, + expected_name: Option<&dyn std::fmt::Display>, + actual_name: Option<&dyn std::fmt::Display>, + palette: crate::report::Palette, +) -> Result<(), std::fmt::Error> { + #[allow(unused_mut)] + let mut rendered = false; + #[cfg(feature = "diff")] + if let (Some(expected), Some(actual)) = (expected.render(), actual.render()) { + write_diff_inner( + writer, + &expected, + &actual, + expected_name, + actual_name, + palette, + )?; + rendered = true; + } + + if !rendered { + if let Some(expected_name) = expected_name { + writeln!(writer, "{} {}:", expected_name, palette.error("(expected)"))?; + } else { + writeln!(writer, "{}:", palette.error("Expected"))?; + } + writeln!(writer, "{}", palette.error(&expected))?; + if let Some(actual_name) = actual_name { + writeln!(writer, "{} {}:", actual_name, palette.info("(actual)"))?; + } else { + writeln!(writer, "{}:", palette.info("Actual"))?; + } + writeln!(writer, "{}", palette.info(&actual))?; + } + Ok(()) +} + +#[cfg(feature = "diff")] +fn write_diff_inner( + writer: &mut dyn std::fmt::Write, + expected: &str, + actual: &str, + expected_name: Option<&dyn std::fmt::Display>, + actual_name: Option<&dyn std::fmt::Display>, + palette: crate::report::Palette, +) -> Result<(), std::fmt::Error> { + let timeout = std::time::Duration::from_millis(500); + let min_elide = 20; + let context = 5; + + let changes = similar::TextDiff::configure() + .algorithm(similar::Algorithm::Patience) + .timeout(timeout) + .newline_terminated(false) + .diff_lines(expected, actual); + + writeln!(writer)?; + if let Some(expected_name) = expected_name { + writeln!( + writer, + "{}", + palette.error(format_args!("{:->4} expected: {}", "", expected_name)) + )?; + } else { + writeln!(writer, "{}", palette.error(format_args!("--- Expected")))?; + } + if let Some(actual_name) = actual_name { + writeln!( + writer, + "{}", + palette.info(format_args!("{:+>4} actual: {}", "", actual_name)) + )?; + } else { + writeln!(writer, "{}", palette.info(format_args!("+++ Actual")))?; + } + let changes = changes + .ops() + .iter() + .flat_map(|op| changes.iter_inline_changes(op)) + .collect::<Vec<_>>(); + let tombstones = if min_elide < changes.len() { + let mut tombstones = vec![true; changes.len()]; + + let mut counter = context; + for (i, change) in changes.iter().enumerate() { + match change.tag() { + similar::ChangeTag::Insert | similar::ChangeTag::Delete => { + counter = context; + tombstones[i] = false; + } + similar::ChangeTag::Equal => { + if counter != 0 { + tombstones[i] = false; + counter -= 1; + } + } + } + } + + let mut counter = context; + for (i, change) in changes.iter().enumerate().rev() { + match change.tag() { + similar::ChangeTag::Insert | similar::ChangeTag::Delete => { + counter = context; + tombstones[i] = false; + } + similar::ChangeTag::Equal => { + if counter != 0 { + tombstones[i] = false; + counter -= 1; + } + } + } + } + tombstones + } else { + Vec::new() + }; + + let mut elided = false; + for (i, change) in changes.into_iter().enumerate() { + if tombstones.get(i).copied().unwrap_or(false) { + if !elided { + let sign = "⋮"; + + write!(writer, "{:>4} ", " ",)?; + write!(writer, "{:>4} ", " ",)?; + writeln!(writer, "{}", palette.hint(sign))?; + } + elided = true; + } else { + elided = false; + match change.tag() { + similar::ChangeTag::Insert => { + write_change(writer, change, "+", palette.actual, palette.info, palette)?; + } + similar::ChangeTag::Delete => { + write_change( + writer, + change, + "-", + palette.expected, + palette.error, + palette, + )?; + } + similar::ChangeTag::Equal => { + write_change(writer, change, "|", palette.hint, palette.hint, palette)?; + } + } + } + } + + Ok(()) +} + +#[cfg(feature = "diff")] +fn write_change( + writer: &mut dyn std::fmt::Write, + change: similar::InlineChange<str>, + sign: &str, + em_style: crate::report::Style, + style: crate::report::Style, + palette: crate::report::Palette, +) -> Result<(), std::fmt::Error> { + if let Some(index) = change.old_index() { + write!(writer, "{:>4} ", palette.hint(index + 1),)?; + } else { + write!(writer, "{:>4} ", " ",)?; + } + if let Some(index) = change.new_index() { + write!(writer, "{:>4} ", palette.hint(index + 1),)?; + } else { + write!(writer, "{:>4} ", " ",)?; + } + write!(writer, "{} ", Styled::new(sign, style))?; + for &(emphasized, change) in change.values() { + let cur_style = if emphasized { em_style } else { style }; + write!(writer, "{}", Styled::new(change, cur_style))?; + } + if change.missing_newline() { + writeln!(writer, "{}", Styled::new("∅", em_style))?; + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(feature = "diff")] + #[test] + fn diff_eq() { + let expected = "Hello\nWorld\n"; + let expected_name = "A"; + let actual = "Hello\nWorld\n"; + let actual_name = "B"; + let palette = crate::report::Palette::plain(); + + let mut actual_diff = String::new(); + write_diff_inner( + &mut actual_diff, + expected, + actual, + Some(&expected_name), + Some(&actual_name), + palette, + ) + .unwrap(); + let expected_diff = " +---- expected: A +++++ actual: B + 1 1 | Hello + 2 2 | World +"; + + assert_eq!(expected_diff, actual_diff); + } + + #[cfg(feature = "diff")] + #[test] + fn diff_ne_line_missing() { + let expected = "Hello\nWorld\n"; + let expected_name = "A"; + let actual = "Hello\n"; + let actual_name = "B"; + let palette = crate::report::Palette::plain(); + + let mut actual_diff = String::new(); + write_diff_inner( + &mut actual_diff, + expected, + actual, + Some(&expected_name), + Some(&actual_name), + palette, + ) + .unwrap(); + let expected_diff = " +---- expected: A +++++ actual: B + 1 1 | Hello + 2 - World +"; + + assert_eq!(expected_diff, actual_diff); + } + + #[cfg(feature = "diff")] + #[test] + fn diff_eq_trailing_extra_newline() { + let expected = "Hello\nWorld"; + let expected_name = "A"; + let actual = "Hello\nWorld\n"; + let actual_name = "B"; + let palette = crate::report::Palette::plain(); + + let mut actual_diff = String::new(); + write_diff_inner( + &mut actual_diff, + expected, + actual, + Some(&expected_name), + Some(&actual_name), + palette, + ) + .unwrap(); + let expected_diff = " +---- expected: A +++++ actual: B + 1 1 | Hello + 2 - World∅ + 2 + World +"; + + assert_eq!(expected_diff, actual_diff); + } + + #[cfg(feature = "diff")] + #[test] + fn diff_eq_trailing_newline_missing() { + let expected = "Hello\nWorld\n"; + let expected_name = "A"; + let actual = "Hello\nWorld"; + let actual_name = "B"; + let palette = crate::report::Palette::plain(); + + let mut actual_diff = String::new(); + write_diff_inner( + &mut actual_diff, + expected, + actual, + Some(&expected_name), + Some(&actual_name), + palette, + ) + .unwrap(); + let expected_diff = " +---- expected: A +++++ actual: B + 1 1 | Hello + 2 - World + 2 + World∅ +"; + + assert_eq!(expected_diff, actual_diff); + } + + #[cfg(feature = "diff")] + #[test] + fn diff_eq_elided() { + let mut expected = String::new(); + expected.push_str("Hello\n"); + for i in 0..20 { + expected.push_str(&i.to_string()); + expected.push('\n'); + } + expected.push_str("World\n"); + for i in 0..20 { + expected.push_str(&i.to_string()); + expected.push('\n'); + } + expected.push_str("!\n"); + let expected_name = "A"; + + let mut actual = String::new(); + actual.push_str("Goodbye\n"); + for i in 0..20 { + actual.push_str(&i.to_string()); + actual.push('\n'); + } + actual.push_str("Moon\n"); + for i in 0..20 { + actual.push_str(&i.to_string()); + actual.push('\n'); + } + actual.push_str("?\n"); + let actual_name = "B"; + + let palette = crate::report::Palette::plain(); + + let mut actual_diff = String::new(); + write_diff_inner( + &mut actual_diff, + &expected, + &actual, + Some(&expected_name), + Some(&actual_name), + palette, + ) + .unwrap(); + let expected_diff = " +---- expected: A +++++ actual: B + 1 - Hello + 1 + Goodbye + 2 2 | 0 + 3 3 | 1 + 4 4 | 2 + 5 5 | 3 + 6 6 | 4 + ⋮ + 17 17 | 15 + 18 18 | 16 + 19 19 | 17 + 20 20 | 18 + 21 21 | 19 + 22 - World + 22 + Moon + 23 23 | 0 + 24 24 | 1 + 25 25 | 2 + 26 26 | 3 + 27 27 | 4 + ⋮ + 38 38 | 15 + 39 39 | 16 + 40 40 | 17 + 41 41 | 18 + 42 42 | 19 + 43 - ! + 43 + ? +"; + + assert_eq!(expected_diff, actual_diff); + } +} diff --git a/vendor/snapbox/src/report/mod.rs b/vendor/snapbox/src/report/mod.rs new file mode 100644 index 0000000..6c9a238 --- /dev/null +++ b/vendor/snapbox/src/report/mod.rs @@ -0,0 +1,9 @@ +//! Utilities to report test results to users + +mod color; +mod diff; + +pub use color::Palette; +pub(crate) use color::Style; +pub use color::Styled; +pub use diff::write_diff; diff --git a/vendor/snapbox/src/substitutions.rs b/vendor/snapbox/src/substitutions.rs new file mode 100644 index 0000000..d005743 --- /dev/null +++ b/vendor/snapbox/src/substitutions.rs @@ -0,0 +1,420 @@ +use std::borrow::Cow; + +/// Match pattern expressions, see [`Assert`][crate::Assert] +/// +/// Built-in expressions: +/// - `...` on a line of its own: match multiple complete lines +/// - `[..]`: match multiple characters within a line +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct Substitutions { + vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>, + unused: std::collections::BTreeSet<&'static str>, +} + +impl Substitutions { + pub fn new() -> Self { + Default::default() + } + + pub(crate) fn with_exe() -> Self { + let mut substitutions = Self::new(); + substitutions + .insert("[EXE]", std::env::consts::EXE_SUFFIX) + .unwrap(); + substitutions + } + + /// Insert an additional match pattern + /// + /// `key` must be enclosed in `[` and `]`. + /// + /// ```rust + /// let mut subst = snapbox::Substitutions::new(); + /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX); + /// ``` + pub fn insert( + &mut self, + key: &'static str, + value: impl Into<Cow<'static, str>>, + ) -> Result<(), crate::Error> { + let key = validate_key(key)?; + let value = value.into(); + if value.is_empty() { + self.unused.insert(key); + } else { + self.vars + .insert(key, crate::utils::normalize_text(value.as_ref()).into()); + } + Ok(()) + } + + /// Insert additional match patterns + /// + /// keys must be enclosed in `[` and `]`. + pub fn extend( + &mut self, + vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>, + ) -> Result<(), crate::Error> { + for (key, value) in vars { + self.insert(key, value)?; + } + Ok(()) + } + + /// Apply match pattern to `input` + /// + /// If `pattern` matches `input`, then `pattern` is returned. + /// + /// Otherwise, `input`, with as many patterns replaced as possible, will be returned. + /// + /// ```rust + /// let subst = snapbox::Substitutions::new(); + /// let output = subst.normalize("Hello World!", "Hello [..]!"); + /// assert_eq!(output, "Hello [..]!"); + /// ``` + pub fn normalize(&self, input: &str, pattern: &str) -> String { + normalize(input, pattern, self) + } + + fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> { + let mut value = Cow::Borrowed(value); + for (var, replace) in self.vars.iter() { + debug_assert!(!replace.is_empty()); + value = Cow::Owned(value.replace(replace.as_ref(), var)); + } + value + } + + fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> { + if pattern.contains('[') { + let mut pattern = Cow::Borrowed(pattern); + for var in self.unused.iter() { + pattern = Cow::Owned(pattern.replace(var, "")); + } + pattern + } else { + Cow::Borrowed(pattern) + } + } +} + +fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> { + if !key.starts_with('[') || !key.ends_with(']') { + return Err(format!("Key `{}` is not enclosed in []", key).into()); + } + + if key[1..(key.len() - 1)] + .find(|c: char| !c.is_ascii_uppercase()) + .is_some() + { + return Err(format!("Key `{}` can only be A-Z but ", key).into()); + } + + Ok(key) +} + +fn normalize(input: &str, pattern: &str, substitutions: &Substitutions) -> String { + if input == pattern { + return input.to_owned(); + } + + let mut normalized: Vec<Cow<str>> = Vec::new(); + let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect(); + let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect(); + + let mut input_index = 0; + let mut pattern_index = 0; + 'outer: loop { + let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) { + *pattern_line + } else { + normalized.extend( + input_lines[input_index..] + .iter() + .copied() + .map(|s| substitutions.substitute(s)), + ); + break 'outer; + }; + let next_pattern_index = pattern_index + 1; + + let input_line = if let Some(input_line) = input_lines.get(input_index) { + *input_line + } else { + break 'outer; + }; + let next_input_index = input_index + 1; + + if line_matches(input_line, pattern_line, substitutions) { + pattern_index = next_pattern_index; + input_index = next_input_index; + normalized.push(Cow::Borrowed(pattern_line)); + continue 'outer; + } else if is_line_elide(pattern_line) { + let next_pattern_line: &str = + if let Some(pattern_line) = pattern_lines.get(next_pattern_index) { + pattern_line + } else { + normalized.push(Cow::Borrowed(pattern_line)); + break 'outer; + }; + if let Some(future_input_index) = input_lines[input_index..] + .iter() + .enumerate() + .find(|(_, l)| **l == next_pattern_line) + .map(|(i, _)| input_index + i) + { + normalized.push(Cow::Borrowed(pattern_line)); + pattern_index = next_pattern_index; + input_index = future_input_index; + continue 'outer; + } else { + normalized.extend( + input_lines[input_index..] + .iter() + .copied() + .map(|s| substitutions.substitute(s)), + ); + break 'outer; + } + } else { + // Find where we can pick back up for normalizing + for future_input_index in next_input_index..input_lines.len() { + let future_input_line = input_lines[future_input_index]; + if let Some(future_pattern_index) = pattern_lines[next_pattern_index..] + .iter() + .enumerate() + .find(|(_, l)| **l == future_input_line || is_line_elide(l)) + .map(|(i, _)| next_pattern_index + i) + { + normalized.extend( + input_lines[input_index..future_input_index] + .iter() + .copied() + .map(|s| substitutions.substitute(s)), + ); + pattern_index = future_pattern_index; + input_index = future_input_index; + continue 'outer; + } + } + + normalized.extend( + input_lines[input_index..] + .iter() + .copied() + .map(|s| substitutions.substitute(s)), + ); + break 'outer; + } + } + + normalized.join("") +} + +fn is_line_elide(line: &str) -> bool { + line == "...\n" || line == "..." +} + +fn line_matches(line: &str, pattern: &str, substitutions: &Substitutions) -> bool { + if line == pattern { + return true; + } + + let subbed = substitutions.substitute(line); + let mut line = subbed.as_ref(); + + let pattern = substitutions.clear(pattern); + + let mut sections = pattern.split("[..]").peekable(); + while let Some(section) = sections.next() { + if let Some(remainder) = line.strip_prefix(section) { + if let Some(next_section) = sections.peek() { + if next_section.is_empty() { + line = ""; + } else if let Some(restart_index) = remainder.find(next_section) { + line = &remainder[restart_index..]; + } + } else { + return remainder.is_empty(); + } + } else { + return false; + } + } + + false +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty() { + let input = ""; + let pattern = ""; + let expected = ""; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn literals_match() { + let input = "Hello\nWorld"; + let pattern = "Hello\nWorld"; + let expected = "Hello\nWorld"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn pattern_shorter() { + let input = "Hello\nWorld"; + let pattern = "Hello\n"; + let expected = "Hello\nWorld"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn input_shorter() { + let input = "Hello\n"; + let pattern = "Hello\nWorld"; + let expected = "Hello\n"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn all_different() { + let input = "Hello\nWorld"; + let pattern = "Goodbye\nMoon"; + let expected = "Hello\nWorld"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn middles_diverge() { + let input = "Hello\nWorld\nGoodbye"; + let pattern = "Hello\nMoon\nGoodbye"; + let expected = "Hello\nWorld\nGoodbye"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn leading_elide() { + let input = "Hello\nWorld\nGoodbye"; + let pattern = "...\nGoodbye"; + let expected = "...\nGoodbye"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn trailing_elide() { + let input = "Hello\nWorld\nGoodbye"; + let pattern = "Hello\n..."; + let expected = "Hello\n..."; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn middle_elide() { + let input = "Hello\nWorld\nGoodbye"; + let pattern = "Hello\n...\nGoodbye"; + let expected = "Hello\n...\nGoodbye"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn post_elide_diverge() { + let input = "Hello\nSun\nAnd\nWorld"; + let pattern = "Hello\n...\nMoon"; + let expected = "Hello\nSun\nAnd\nWorld"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn post_diverge_elide() { + let input = "Hello\nWorld\nGoodbye\nSir"; + let pattern = "Hello\nMoon\nGoodbye\n..."; + let expected = "Hello\nWorld\nGoodbye\n..."; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn inline_elide() { + let input = "Hello\nWorld\nGoodbye\nSir"; + let pattern = "Hello\nW[..]d\nGoodbye\nSir"; + let expected = "Hello\nW[..]d\nGoodbye\nSir"; + let actual = normalize(input, pattern, &Substitutions::new()); + assert_eq!(expected, actual); + } + + #[test] + fn line_matches_cases() { + let cases = [ + ("", "", true), + ("", "[..]", true), + ("hello", "hello", true), + ("hello", "goodbye", false), + ("hello", "[..]", true), + ("hello", "he[..]", true), + ("hello", "go[..]", false), + ("hello", "[..]o", true), + ("hello", "[..]e", false), + ("hello", "he[..]o", true), + ("hello", "he[..]e", false), + ("hello", "go[..]o", false), + ("hello", "go[..]e", false), + ( + "hello world, goodbye moon", + "hello [..], goodbye [..]", + true, + ), + ( + "hello world, goodbye moon", + "goodbye [..], goodbye [..]", + false, + ), + ( + "hello world, goodbye moon", + "goodbye [..], hello [..]", + false, + ), + ("hello world, goodbye moon", "hello [..], [..] moon", true), + ( + "hello world, goodbye moon", + "goodbye [..], [..] moon", + false, + ), + ("hello world, goodbye moon", "hello [..], [..] world", false), + ]; + for (line, pattern, expected) in cases { + let actual = line_matches(line, pattern, &Substitutions::new()); + assert_eq!(expected, actual, "line={:?} pattern={:?}", line, pattern); + } + } + + #[test] + fn test_validate_key() { + let cases = [ + ("[HELLO", false), + ("HELLO]", false), + ("[HELLO]", true), + ("[hello]", false), + ("[HE O]", false), + ]; + for (key, expected) in cases { + let actual = validate_key(key).is_ok(); + assert_eq!(expected, actual, "key={:?}", key); + } + } +} diff --git a/vendor/snapbox/src/utils/lines.rs b/vendor/snapbox/src/utils/lines.rs new file mode 100644 index 0000000..f564084 --- /dev/null +++ b/vendor/snapbox/src/utils/lines.rs @@ -0,0 +1,31 @@ +#[derive(Clone, Debug)] +pub struct LinesWithTerminator<'a> { + data: &'a str, +} + +impl<'a> LinesWithTerminator<'a> { + pub fn new(data: &'a str) -> LinesWithTerminator<'a> { + LinesWithTerminator { data } + } +} + +impl<'a> Iterator for LinesWithTerminator<'a> { + type Item = &'a str; + + #[inline] + fn next(&mut self) -> Option<&'a str> { + match self.data.find('\n') { + None if self.data.is_empty() => None, + None => { + let line = self.data; + self.data = ""; + Some(line) + } + Some(end) => { + let line = &self.data[..end + 1]; + self.data = &self.data[end + 1..]; + Some(line) + } + } + } +} diff --git a/vendor/snapbox/src/utils/mod.rs b/vendor/snapbox/src/utils/mod.rs new file mode 100644 index 0000000..d519241 --- /dev/null +++ b/vendor/snapbox/src/utils/mod.rs @@ -0,0 +1,30 @@ +mod lines; + +pub use lines::LinesWithTerminator; + +/// Normalize line endings +pub fn normalize_lines(data: &str) -> String { + normalize_lines_chars(data.chars()).collect() +} + +fn normalize_lines_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> { + normalize_line_endings::normalized(data) +} + +/// Normalize path separators +pub fn normalize_paths(data: &str) -> String { + normalize_paths_chars(data.chars()).collect() +} + +fn normalize_paths_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> { + data.map(|c| if c == '\\' { '/' } else { c }) +} + +/// "Smart" text normalization +/// +/// This includes +/// - Line endings +/// - Path separators +pub fn normalize_text(data: &str) -> String { + normalize_paths_chars(normalize_lines_chars(data.chars())).collect() +} |