snapbox-0.4.17/.cargo_vcs_info.json0000644000000001540000000000100125770ustar { "git": { "sha1": "1b3ebc09d0dc14396594040264ced5e089cdd276" }, "path_in_vcs": "crates/snapbox" }snapbox-0.4.17/Cargo.lock0000644000000625000000000000100105550ustar # 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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" 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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", "windows-sys 0.52.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 = "escape8259" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" dependencies = [ "rustversion", ] [[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.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f0f4c6f44ecfd52e8b443f2ad18f2b996540135771561283c2352ce56a1c70b" dependencies = [ "clap", "escape8259", "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 = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[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.17" 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.52.0", ] [[package]] name = "snapbox-macros" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1c4b838b05d15ab22754068cb73500b2f3b07bf09d310e15b27f88160f1de40" 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 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.0", ] [[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-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[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_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[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_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[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_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[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_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[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_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[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_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[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" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" snapbox-0.4.17/Cargo.toml0000644000000077160000000000100106100ustar # 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.17" 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 = """ ## [Unreleased] - ReleaseDate """ search = "" [[package.metadata.release.pre-release-replacements]] exactly = 1 file = "CHANGELOG.md" replace = """ [Unreleased]: https://github.com/assert-rs/trycmd/compare/{{tag_name}}...HEAD""" search = "" [[bin]] name = "snap-fixture" [[example]] name = "diff" required-features = ["diff"] [dependencies.anstream] version = "0.6.7" 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.escargot] version = "0.5.7" optional = true [dependencies.filetime] version = "0.2" optional = true [dependencies.ignore] version = "0.4" optional = true [dependencies.libtest-mimic] version = "0.7.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.8" [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"] examples = ["dep:escargot"] 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.52.0" features = ["Win32_Foundation"] optional = true snapbox-0.4.17/Cargo.toml.orig000064400000000000000000000065231046102023000142640ustar 00000000000000[package] name = "snapbox" version = "0.4.17" description = "Snapshot testing toolbox" repository = "https://github.com/assert-rs/trycmd/" homepage = "https://github.com/assert-rs/trycmd/tree/main/crates/snapbox" documentation = "http://docs.rs/snapbox/" readme = "README.md" categories = ["development-tools::testing"] keywords = ["cli", "test", "assert", "command"] license.workspace = true edition.workspace = true rust-version.workspace = true include.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] [package.metadata.release] pre-release-replacements = [ {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/assert-rs/trycmd/compare/{{tag_name}}...HEAD", exactly=1}, ] [features] default = ["color-auto", "diff"] #! Feature Flags ## Simple input/output test harness harness = ["dep:libtest-mimic", "dep:ignore"] ## Smarter binary file detection detect-encoding = ["dep:content_inspector"] ## Snapshotting of paths path = ["dep:tempfile", "dep:walkdir", "dep:dunce", "detect-encoding", "dep:filetime"] ## Snapshotting of commands cmd = ["dep:os_pipe", "dep:wait-timeout", "dep:libc", "dep:windows-sys"] ## Building of examples for snapshotting examples = ["dep:escargot"] ## Snapshotting of json json = ["structured-data", "dep:serde_json"] ## Snapshotting of structured data structured-data = ["dep:serde_json"] ## Extra debugging information debug = ["snapbox-macros/debug", "dep:backtrace"] #! Default Feature Flags ## Fancy diffs on failure diff = ["dep:similar"] ## Colored output support color = ["dep:anstream", "snapbox-macros/color"] ## Auto-detect whether to use colors color-auto = ["color"] [[bin]] name = "snap-fixture" # For `snapbox`s tests only [dependencies] normalize-line-endings = "0.3.0" snapbox-macros = { path = "../snapbox-macros", version = "0.3.8" } libtest-mimic = { version = "0.7.0", optional = true } ignore = { version = "0.4", optional = true } content_inspector = { version = "0.2.4", optional = true } tempfile = { version = "3.0", optional = true } walkdir = { version = "2.3.2", optional = true } dunce = { version = "1.0", optional = true } filetime = { version = "0.2", optional = true } os_pipe = { version = "1.0", optional = true } wait-timeout = { version = "0.2.0", optional = true } escargot = { version = "0.5.7", optional = true } backtrace = { version = "0.3", optional = true } similar = { version = "2.1.0", features = ["inline"], optional = true } anstyle = "1.0.0" anstream = { version = "0.6.7", optional = true } document-features = { version = "0.2.6", optional = true } serde_json = { version = "1.0.85", optional = true} [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.52.0", features = ["Win32_Foundation"], optional = true } [target.'cfg(unix)'.dependencies] libc = { version = "0.2.137", optional = true } [[example]] name = "diff" required-features = ["diff"] snapbox-0.4.17/LICENSE-APACHE000064400000000000000000000261361046102023000133230ustar 00000000000000 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. snapbox-0.4.17/LICENSE-MIT000064400000000000000000000020461046102023000130250ustar 00000000000000Copyright (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. snapbox-0.4.17/README.md000064400000000000000000000023201046102023000126430ustar 00000000000000# 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 snapbox-0.4.17/examples/diff.rs000064400000000000000000000013721046102023000144660ustar 00000000000000fn main() { let mut args = std::env::args(); let _ = args.next().expect("expects `$ diff `"); let old_path = args.next().expect("expects `$ diff `"); let new_path = args.next().expect("expects `$ diff `"); if args.next().is_some() { panic!("expects `$ diff `"); } let old = snapbox::Data::text(std::fs::read_to_string(&old_path).unwrap()); let new = snapbox::Data::text(std::fs::read_to_string(&new_path).unwrap()); let mut output = String::new(); snapbox::report::write_diff( &mut output, &old, &new, Some(&old_path), Some(&new_path), snapbox::report::Palette::color(), ) .unwrap(); println!("{output}"); } snapbox-0.4.17/examples/snap-example-fixture.rs000064400000000000000000000027631046102023000176410ustar 00000000000000//! 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> { 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::()) .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); } snapbox-0.4.17/src/action.rs000064400000000000000000000017401046102023000140030ustar 00000000000000pub 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) -> Option { 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) -> Option { 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 } } snapbox-0.4.17/src/assert.rs000064400000000000000000000414341046102023000140330ustar 00000000000000#[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, normalize_paths: bool, substitutions: crate::Substitutions, pub(crate) palette: crate::report::Palette, pub(crate) data_format: Option, } /// # 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, actual: impl Into) { 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, actual: impl Into) { 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, actual: impl Into, ) { 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, actual: impl Into, ) { 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, mut actual: crate::Data, ) -> (crate::Result, 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, mut actual: crate::Data, ) -> (crate::Result, 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, 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, actual_root: impl Into, ) { 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, actual_root: impl Into, ) { 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>, ) { 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 { 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()) } } snapbox-0.4.17/src/bin/snap-fixture.rs000064400000000000000000000027631046102023000157310ustar 00000000000000//! 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> { 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::()) .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); } snapbox-0.4.17/src/cmd.rs000064400000000000000000001112171046102023000132720ustar 00000000000000//! 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, timeout: Option, _stderr_to_stdout: bool, config: crate::Assert, } /// # Builder API impl Command { pub fn new(program: impl AsRef) -> 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) -> 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>) -> 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, value: impl AsRef, ) -> 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 = /// 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, impl AsRef)>, ) -> 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) -> 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) -> 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) -> 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 { if self._stderr_to_stdout { self.single_output() } else { self.split_output() } } #[cfg(not(feature = "cmd"))] pub fn output(self) -> Result { self.split_output() } #[cfg(feature = "cmd")] fn single_output(mut self) -> Result { 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 { 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>, ) -> std::io::Result<(Option, Option)> { 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>, ) -> std::io::Result { 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, std::io::Error>>; fn threaded_read(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 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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 { 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 { 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::io::Result { 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::io::Result { 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, ) -> Result { 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::>(); /// ``` #[cfg(feature = "examples")] pub fn compile_examples<'a>( args: impl IntoIterator, ) -> Result< impl Iterator)>, 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), 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"] } } snapbox-0.4.17/src/data.rs000064400000000000000000000710551046102023000134450ustar 00000000000000/// 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), 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>) -> Self { Self { inner: DataInner::Binary(raw.into()), } } /// Mark the data as text (post-processing) pub fn text(raw: impl Into) -> Self { Self { inner: DataInner::Text(raw.into()), } } #[cfg(feature = "json")] pub fn json(raw: impl Into) -> 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, ) -> Result { 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::(&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 { 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 { 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::(&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> for Data { fn from(other: Vec) -> Self { Self::binary(other) } } impl<'b> From<&'b [u8]> for Data { fn from(other: &'b [u8]) -> Self { other.to_owned().into() } } impl From 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); } } } snapbox-0.4.17/src/error.rs000064400000000000000000000040041046102023000136530ustar 00000000000000#[derive(Clone, Debug)] pub struct Error { inner: String, backtrace: Option, } 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 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 { 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 { None } } #[cfg(not(feature = "debug"))] impl std::fmt::Display for Backtrace { fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(()) } } snapbox-0.4.17/src/harness.rs000064400000000000000000000137111046102023000141720ustar 00000000000000//! [`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> { //! let raw = std::fs::read_to_string(input_path)?; //! let num = raw.parse::()?; //! //! let actual = num + 10; //! //! Ok(actual) //! } //! ``` use crate::data::{DataFormat, NormalizeNewlines}; use crate::Action; use libtest_mimic::Trial; pub struct Harness { root: std::path::PathBuf, overrides: Option, setup: S, test: T, action: Action, } impl Harness where I: std::fmt::Display, E: std::fmt::Display, S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static, T: Fn(&std::path::Path) -> Result + Send + Sync + 'static + Clone, { pub fn new(root: impl Into, 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) -> 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, } snapbox-0.4.17/src/lib.rs000064400000000000000000000167071046102023000133050ustar 00000000000000//! # 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> { //! let raw = std::fs::read_to_string(input_path)?; //! let num = raw.parse::()?; //! //! 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 = std::result::Result; /// 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, actual: impl Into) { 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, actual: impl Into) { 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, actual: impl Into) { 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, actual: impl Into, ) { 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, actual_root: impl Into, ) { 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, actual_root: impl Into, ) { Assert::new() .action_env(DEFAULT_ACTION_ENV) .subset_matches(pattern_root, actual_root); } snapbox-0.4.17/src/path.rs000064400000000000000000000544231046102023000134700ustar 00000000000000//! 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 { 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 { 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 { 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, actual_root: impl Into, ) -> impl Iterator> { 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> { 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, actual_root: impl Into, substitutions: &crate::Substitutions, ) -> impl Iterator> + '_ { 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> + '_ { 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; fn next(&mut self) -> Option { 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, dest: impl AsRef, ) -> 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, ) -> Result { 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 { #[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) -> 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); } } snapbox-0.4.17/src/report/color.rs000064400000000000000000000056111046102023000151600ustar 00000000000000#[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::Red.on_default() | anstyle::Effects::UNDERLINE, actual: anstyle::AnsiColor::Green.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(self, item: D) -> Styled { Styled::new(item, self.info) } pub fn warn(self, item: D) -> Styled { Styled::new(item, self.warn) } pub fn error(self, item: D) -> Styled { Styled::new(item, self.error) } pub fn hint(self, item: D) -> Styled { Styled::new(item, self.hint) } pub fn expected(self, item: D) -> Styled { Styled::new(item, self.expected) } pub fn actual(self, item: D) -> Styled { 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 { display: D, style: anstyle::Style, } impl Styled { pub(crate) fn new(display: D, style: anstyle::Style) -> Self { Self { display, style } } } impl std::fmt::Display for Styled { #[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(()) } } snapbox-0.4.17/src/report/diff.rs000064400000000000000000000245621046102023000147600ustar 00000000000000use 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::>(); 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, 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); } } snapbox-0.4.17/src/report/mod.rs000064400000000000000000000002531046102023000146160ustar 00000000000000//! 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; snapbox-0.4.17/src/substitutions.rs000064400000000000000000000320071046102023000154650ustar 00000000000000use 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>, ) -> 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>)>, ) -> 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> = 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); } } } snapbox-0.4.17/src/utils/lines.rs000064400000000000000000000013721046102023000150010ustar 00000000000000#[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) } } } } snapbox-0.4.17/src/utils/mod.rs000064400000000000000000000014441046102023000144460ustar 00000000000000mod 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) -> impl Iterator { 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) -> impl Iterator { 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() }