trycmd-0.14.20/.cargo_vcs_info.json0000644000000001360000000000100125020ustar { "git": { "sha1": "18db746dc459ffb498f1821b2420460d4e42a066" }, "path_in_vcs": "" }trycmd-0.14.20/Cargo.lock0000644000000603270000000000100104650ustar # 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 = "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 = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "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 = "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-channel" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "once_cell", "scopeguard", ] [[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 = "dunce" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" [[package]] name = "dyn-clone" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[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 = "gimli" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "humantime-serde" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" dependencies = [ "humantime", "serde", ] [[package]] name = "indexmap" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown 0.12.3", "serde", ] [[package]] name = "indexmap" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown 0.14.1", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "itoa" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[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 = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[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", "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 = "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 = "rayon" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ "autocfg", "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "num_cpus", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "ryu" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schemars" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed" dependencies = [ "dyn-clone", "indexmap 1.9.1", "schemars_derive", "serde", "serde_json", ] [[package]] name = "schemars_derive" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[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_derive_internals" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" 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 = "serde_spanned" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[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.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73145a30df4935f50a7b13c1882bce7d194d7071ad0bcc36e7cacbf9ef16e3ec" dependencies = [ "anstream", "anstyle", "backtrace", "content_inspector", "dunce", "escargot", "filetime", "libc", "normalize-line-endings", "os_pipe", "similar", "snapbox-macros", "tempfile", "wait-timeout", "walkdir", "windows-sys 0.52.0", ] [[package]] name = "snapbox-macros" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ccde059aad940984ff696fe8c280900f7ea71a6fb45fce65071a3f2c40b667" dependencies = [ "anstream", ] [[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 = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "trycmd" version = "0.14.20" dependencies = [ "anstream", "escargot", "glob", "humantime", "humantime-serde", "rayon", "schemars", "serde", "serde_json", "shlex", "snapbox", "toml_edit", ] [[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 = "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" [[package]] name = "winnow" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] trycmd-0.14.20/Cargo.toml0000644000000062040000000000100105020ustar # 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 = "trycmd" version = "0.14.20" authors = ["Ed Page "] include = [ "build.rs", "src/**/*", "Cargo.toml", "LICENSE*", "README.md", "benches/**/*", "examples/**/*", ] description = "Snapshot testing for a herd of CLI tests" homepage = "https://github.com/assert-rs/trycmd" documentation = "http://docs.rs/trycmd/" readme = "README.md" keywords = [ "cli", "test", "assert", "command", "duct", ] categories = ["development-tools::testing"] license = "MIT OR Apache-2.0" repository = "https://github.com/assert-rs/trycmd.git" [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 = "bin-fixture" [[bin]] name = "trycmd-schema" required-features = ["schema"] [dependencies.anstream] version = "0.6.7" optional = true [dependencies.escargot] version = "0.5.7" optional = true [dependencies.glob] version = "0.3.0" [dependencies.humantime] version = "2" [dependencies.humantime-serde] version = "1" [dependencies.rayon] version = "1.5.1" [dependencies.schemars] version = "0.8.3" features = ["preserve_order"] optional = true [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" optional = true [dependencies.shlex] version = "1.1.0" [dependencies.snapbox] version = "0.4.16" features = ["cmd"] default-features = false [dependencies.toml_edit] version = "0.21" features = ["serde"] [features] color = [ "snapbox/color", "dep:anstream", ] color-auto = ["snapbox/color-auto"] debug = ["snapbox/debug"] default = [ "color-auto", "filesystem", "diff", ] diff = ["snapbox/diff"] examples = ["snapbox/examples"] filesystem = ["snapbox/path"] schema = [ "dep:schemars", "dep:serde_json", ] trycmd-0.14.20/Cargo.toml.orig000064400000000000000000000046451046102023000141720ustar 00000000000000[workspace] members = ["crates/*"] resolver = "2" [workspace.package] license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.70.0" # MSRV include = [ "build.rs", "src/**/*", "Cargo.toml", "LICENSE*", "README.md", "benches/**/*", "examples/**/*" ] [package] name = "trycmd" version = "0.14.20" description = "Snapshot testing for a herd of CLI tests" authors = ["Ed Page "] repository = "https://github.com/assert-rs/trycmd.git" homepage = "https://github.com/assert-rs/trycmd" documentation = "http://docs.rs/trycmd/" readme = "README.md" categories = ["development-tools::testing"] keywords = ["cli", "test", "assert", "command", "duct"] 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", "filesystem", "diff"] color = ["snapbox/color", "dep:anstream"] color-auto = ["snapbox/color-auto"] diff = ["snapbox/diff"] filesystem = ["snapbox/path"] schema = ["dep:schemars", "dep:serde_json"] examples = ["snapbox/examples"] debug = ["snapbox/debug"] [[bin]] name = "bin-fixture" [[bin]] name = "trycmd-schema" required-features = ["schema"] [dependencies] snapbox = { path = "crates/snapbox", version = "0.4.16", default-features = false, features = ["cmd"] } anstream = { version = "0.6.7", optional = true } glob = "0.3.0" rayon = "1.5.1" serde = { version = "1.0", features = ["derive"] } shlex = "1.1.0" humantime = "2" humantime-serde = "1" toml_edit = { version = "0.21", features = ["serde"] } escargot = { version = "0.5.7", optional = true } schemars = { version = "0.8.3", features = ["preserve_order"], optional = true } serde_json = { version = "1.0", optional = true } trycmd-0.14.20/LICENSE-APACHE000064400000000000000000000261361046102023000132260ustar 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. trycmd-0.14.20/LICENSE-MIT000064400000000000000000000020461046102023000127300ustar 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. trycmd-0.14.20/README.md000064400000000000000000000036471046102023000125630ustar 00000000000000# trycmd > Treat your tests like cattle, instead of [pets](https://docs.rs/snapbox) [![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] ![License](https://img.shields.io/crates/l/trycmd.svg) [![Crates Status](https://img.shields.io/crates/v/trycmd.svg)][Crates.io] `trycmd` is a test harness that will enumerate test case files and run them to verify the results, taking inspiration from [trybuild](https://crates.io/crates/trybuild) and [cram](https://bitheap.org/cram/). ## Example To create a minimal setup, create a `tests/cli_tests.rs` with ```rust,no_run #[test] fn cli_tests() { trycmd::TestCases::new() .case("tests/cmd/*.toml") .case("README.md"); } ``` and write out your test cases in `.toml` files along with examples in your `README.md`. Run this with `cargo test` like normal. `TestCases` will enumerate all test case files and run the contained commands, verifying they run as expected. See the [docs](http://docs.rs/trycmd) for more. ## Users - [typos](https://github.com/crate-ci/typos) (source code spell checker) - See [port from `assert_cmd`](https://github.com/crate-ci/typos/compare/a8ae8a5..cdfdc4084c928423211c6a80acbd24dbed7108f6) - [cargo-edit](https://github.com/killercup/cargo-edit) (`Cargo.toml` editor) - [clap](https://github.com/clap-rs/clap/) (CLI parser) to test examples ## License Licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 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/trycmd [Documentation]: https://docs.rs/trycmd trycmd-0.14.20/examples/README.md000064400000000000000000000000401046102023000143610ustar 00000000000000Examples and example test cases trycmd-0.14.20/examples/example-fixture.md000064400000000000000000000000321046102023000165440ustar 00000000000000``` $ example-fixture ``` trycmd-0.14.20/examples/example-fixture.rs000064400000000000000000000030641046102023000166000ustar 00000000000000use 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 exit = env::var("exit").ok(); if exit.as_deref() == Some("panic") { panic!("Panic requested"); } let code = exit .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); } trycmd-0.14.20/src/bin/bin-fixture.rs000064400000000000000000000030641046102023000154360ustar 00000000000000use 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 exit = env::var("exit").ok(); if exit.as_deref() == Some("panic") { panic!("Panic requested"); } let code = exit .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); } trycmd-0.14.20/src/bin/trycmd-schema.rs000064400000000000000000000003471046102023000157430ustar 00000000000000use std::io::prelude::*; fn main() { let schema = schemars::schema_for!(trycmd::schema::OneShot); let schema = serde_json::to_string_pretty(&schema).unwrap(); std::io::stdout().write_all(schema.as_bytes()).unwrap(); } trycmd-0.14.20/src/cargo.rs000064400000000000000000000030721046102023000135240ustar 00000000000000//! Interact with `cargo` #[doc(inline)] pub use snapbox::cmd::cargo_bin; /// 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 /// #[test] /// fn cli_tests() { /// trycmd::TestCases::new() /// .register_bin("example-fixture", trycmd::cargo::compile_example("example-fixture", [])) /// .case("examples/cmd/*.trycmd"); /// } /// ``` #[cfg(feature = "examples")] pub fn compile_example<'a>( target_name: &str, args: impl IntoIterator, ) -> crate::schema::Bin { snapbox::cmd::compile_example(target_name, args).into() } /// 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 /// #[test] /// fn cli_tests() { /// trycmd::TestCases::new() /// .register_bins(trycmd::cargo::compile_examples([]).unwrap()) /// .case("examples/cmd/*.trycmd"); /// } /// ``` #[cfg(feature = "examples")] pub fn compile_examples<'a>( args: impl IntoIterator, ) -> Result, crate::Error> { snapbox::cmd::compile_examples(args).map(|i| i.map(|(name, path)| (name, path.into()))) } trycmd-0.14.20/src/cases.rs000064400000000000000000000150431046102023000135300ustar 00000000000000use std::borrow::Cow; /// Entry point for running tests #[derive(Debug, Default)] pub struct TestCases { runner: std::cell::RefCell, bins: std::cell::RefCell, substitutions: std::cell::RefCell, has_run: std::cell::Cell, } impl TestCases { pub fn new() -> Self { let s = Self::default(); s.runner .borrow_mut() .include(parse_include(std::env::args_os())); s } /// Load tests from `glob` pub fn case(&self, glob: impl AsRef) -> &Self { self.runner.borrow_mut().case(glob.as_ref(), None); self } /// Overwrite expected status for a test pub fn pass(&self, glob: impl AsRef) -> &Self { self.runner .borrow_mut() .case(glob.as_ref(), Some(crate::schema::CommandStatus::Success)); self } /// Overwrite expected status for a test pub fn fail(&self, glob: impl AsRef) -> &Self { self.runner .borrow_mut() .case(glob.as_ref(), Some(crate::schema::CommandStatus::Failed)); self } /// Overwrite expected status for a test pub fn interrupted(&self, glob: impl AsRef) -> &Self { self.runner.borrow_mut().case( glob.as_ref(), Some(crate::schema::CommandStatus::Interrupted), ); self } /// Overwrite expected status for a test pub fn skip(&self, glob: impl AsRef) -> &Self { self.runner .borrow_mut() .case(glob.as_ref(), Some(crate::schema::CommandStatus::Skipped)); self } /// Set default bin, by path, for commands pub fn default_bin_path(&self, path: impl AsRef) -> &Self { let bin = Some(crate::schema::Bin::Path(path.as_ref().into())); self.runner.borrow_mut().default_bin(bin); self } /// Set default bin, by name, for commands pub fn default_bin_name(&self, name: impl AsRef) -> &Self { let bin = Some(crate::schema::Bin::Name(name.as_ref().into())); self.runner.borrow_mut().default_bin(bin); self } /// Set default timeout for commands pub fn timeout(&self, time: std::time::Duration) -> &Self { self.runner.borrow_mut().timeout(Some(time)); self } /// Set default environment variable pub fn env(&self, key: impl Into, value: impl Into) -> &Self { self.runner.borrow_mut().env(key, value); self } /// Add a bin to the "PATH" for cases to use pub fn register_bin( &self, name: impl Into, path: impl Into, ) -> &Self { self.bins .borrow_mut() .register_bin(name.into(), path.into()); self } /// Add a series of bins to the "PATH" for cases to use pub fn register_bins, B: Into>( &self, bins: impl IntoIterator, ) -> &Self { self.bins .borrow_mut() .register_bins(bins.into_iter().map(|(n, b)| (n.into(), b.into()))); self } /// Add a variable for normalizing output /// /// Variable names must be /// - Surrounded by `[]` /// - Consist of uppercase letters /// /// Variables will be preserved through `TRYCMD=overwrite` / `TRYCMD=dump`. /// /// **NOTE:** We do basic search/replaces so new any new output will blindly be replaced. /// /// Reserved names: /// - `[..]` /// - `[EXE]` /// - `[CWD]` /// - `[ROOT]` /// /// ## Example /// /// ```rust,no_run /// #[test] /// fn cli_tests() { /// trycmd::TestCases::new() /// .case("tests/cmd/*.trycmd") /// .insert_var("[VAR]", "value"); /// } /// ``` pub fn insert_var( &self, var: &'static str, value: impl Into>, ) -> Result<&Self, crate::Error> { self.substitutions.borrow_mut().insert(var, value)?; Ok(self) } /// Batch add variables for normalizing output /// /// See `insert_var`. pub fn extend_vars( &self, vars: impl IntoIterator>)>, ) -> Result<&Self, crate::Error> { self.substitutions.borrow_mut().extend(vars)?; Ok(self) } /// Run tests /// /// This will happen on `drop` if not done explicitly pub fn run(&self) { self.has_run.set(true); let mode = parse_mode(std::env::var_os("TRYCMD").as_deref()); mode.initialize().unwrap(); let runner = self.runner.borrow_mut().prepare(); runner.run(&mode, &self.bins.borrow(), &self.substitutions.borrow()); } } impl std::panic::RefUnwindSafe for TestCases {} #[doc(hidden)] impl Drop for TestCases { fn drop(&mut self) { if !self.has_run.get() && !std::thread::panicking() { self.run(); } } } // Filter which test cases are run by trybuild. // // $ cargo test -- ui trybuild=tuple_structs.rs // // The first argument after `--` must be the trybuild test name i.e. the name of // the function that has the #[test] attribute and calls trybuild. That's to get // Cargo to run the test at all. The next argument starting with `trybuild=` // provides a filename filter. Only test cases whose filename contains the // filter string will be run. #[allow(clippy::needless_collect)] // false positive https://github.com/rust-lang/rust-clippy/issues/5991 fn parse_include(args: impl IntoIterator) -> Option> { let filters = args .into_iter() .flat_map(std::ffi::OsString::into_string) .filter_map(|arg| { const PREFIX: &str = "trycmd="; if let Some(remainder) = arg.strip_prefix(PREFIX) { if remainder.is_empty() { None } else { Some(remainder.to_owned()) } } else { None } }) .collect::>(); if filters.is_empty() { None } else { Some(filters) } } fn parse_mode(var: Option<&std::ffi::OsStr>) -> crate::Mode { if var == Some(std::ffi::OsStr::new("overwrite")) { crate::Mode::Overwrite } else if var == Some(std::ffi::OsStr::new("dump")) { crate::Mode::Dump("dump".into()) } else { crate::Mode::Fail } } trycmd-0.14.20/src/lib.rs000064400000000000000000000223521046102023000132010ustar 00000000000000//! # Snapshot testing for a herd of CLI tests //! //! > Treat your tests like cattle, instead of [pets](https://docs.rs/snapbox) //! //! `trycmd` is a test harness that will enumerate test case files and run them to verify the //! results, taking inspiration from //! [trybuild](https://crates.io/crates/trybuild) and [cram](https://bitheap.org/cram/). //! //! ## Which tool is right //! //! - [cram](https://bitheap.org/cram/): End-to-end CLI snapshotting agnostic of any programming language //! - `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](https://crates.io/crates/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 //! //! To create a minimal setup, create a `tests/cli_tests.rs` with //! ```rust,no_run //! #[test] //! fn cli_tests() { //! trycmd::TestCases::new() //! .case("tests/cmd/*.toml") //! .case("README.md"); //! } //! ``` //! and write out your test cases in your `.toml` files along with examples in your `README.md`. //! //! Run this with `cargo test` like normal. [`TestCases`] will enumerate all test case files and //! run the contained commands, verifying they run as expected. //! //! To temporarily override the results, you can do: //! ```rust,no_run //! #[test] //! fn cli_tests() { //! trycmd::TestCases::new() //! .case("tests/cmd/*.toml") //! .case("README.md") //! // See Issue #314 //! .fail("tests/cmd/buggy-case.toml"); //! } //! ``` //! //! ## Workflow //! //! To generate snapshots, run //! ```console //! $ TRYCMD=dump cargo test --test cli_tests //! ``` //! This will write all of the `.stdout` and `.stderr` files in a `dump/` directory. //! //! You can then copy over to `tests/cmd` the cases you want to test //! //! To update snapshots, run //! ```console //! $ TRYCMD=overwrite cargo test --test cli_tests //! ``` //! This will overwrite any existing `.stdout` and `.stderr` file in `tests/cmd` //! //! To filter the tests to those with `name1`, `name2`, etc in their file names, you can run: //! ```console //! cargo test --test cli_tests -- cli_tests trycmd=name1 trycmd=name2... //! ``` //! //! To debug what `trycmd` is doing, run `cargo test -F trycmd/debug`. //! //! ## File Formats //! //! For `tests/cmd/help.trycmd`, `trycmd` will look for: //! - `tests/cmd/help.in/` //! - `tests/cmd/help.out/` //! //! Say you have `tests/cmd/help.toml`, `trycmd` will look for: //! - `tests/cmd/help.stdin` //! - `tests/cmd/help.stdout` //! - `tests/cmd/help.stderr` //! - `tests/cmd/help.in/` //! - `tests/cmd/help.out/` //! //! ### `*.trycmd` //! //! `*.trycmd` / `*.md` files are literate test cases good for: //! - Markdown-compatible syntax for directly rendering them //! - Terminal-like appearance for extracting subsections into documentation //! - Reducing the proliferation of files //! - Running multiple commands within the same temp dir (if a `*.out/` directory is present) //! //! The syntax is: //! - Test cases live inside of ` ``` ` fenced code blocks //! - Everything out of them is ignored //! - Blocks with info strings with an unsupported language (not `trycmd`, `console`) or the //! `ignore` attribute are ignored //! - "`$ `" line prefix starts a new command //! - "`> `" line prefix appends to the prior command //! - "`? `" line indicates the exit code (like `echo "? $?"`) and `` can be //! - An exit code //! - `success` *(default)*, `failed`, `interrupted`, `skipped` //! - All following lines are treated as stdout + stderr //! //! The command is then split with [shlex](https://crates.io/crates/shlex), allowing quoted content //! to allow spaces. The first argument is the program to run which maps to `bin.name` in the //! `.toml` file. //! //! Example: //! //! With a `[[bin]]` like: //! ```rust,ignore //! fn main() { //! println!("Hello world"); //! } //! ``` //! //! You can verify a code block like: //! ~~~md //! ```console //! $ my-cmd //! Hello world //! //! ``` //! ~~~ //! //! For a more complete example, see: //! . //! //! ### `*.toml` //! //! As an alternative to `.trycmd`, the `toml` are good for: //! - Precise control over current dir, stdin/stdout/stderr (including binary support) //! - 1-to-1 with dumped results //! - `TRYCMD=overwrite` support //! //! [See full schema](https://github.com/assert-rs/trycmd/blob/main/schema.json): //! Basic parameters: //! - `bin.name`: The name of the binary target from `Cargo.toml` to be used to find the file path //! - `args`: the arguments (including flags and option) passed to the binary //! //! #### `*.stdin` //! //! Data to pass to `stdin`. //! - If not present, nothing will be written to `stdin` //! - If `binary = false` in `*.toml` (the default), newlines and path separators will be normalized. //! //! #### `*.stdout` and `*.stderr` //! //! Expected results for `stdout` or `stderr`. //! - If not present, we'll not verify the output //! - If `binary = false` in `*.toml` (the default), newlines and path separators will be normalized before comparing //! //! **Eliding Content** //! //! Sometimes the output either includes: //! - Content that changes from run-to-run (like time) //! - Content out of scope of your tests and you want to exclude it to reduce brittleness //! //! To elide a section of content: //! - `...` as its own line: match all lines until the next one. This is equivalent of //! `\n(([^\n]*\n)*)?`. //! - `[..]` as part of a line: match any characters. This is equivalent of `[^\n]*?`. //! - `[EXE]` as part of the line: On Windows, matches `.exe`, ignored otherwise //! - `[ROOT]` as part of the line: The root directory for where the test is running //! - `[CWD]` as part of the line: The current working directory within the root //! - `[YOUR_NAME_HERE]` as part of the line: See [`TestCases::insert_var`] //! //! We will preserve these with `TRYCMD=dump` and will make a best-effort at preserving them with //! `TRYCMD=overwrite`. //! //! ### `*.in/` //! //! When present, this will automatically be picked as the CWD for the command. //! //! `.keep` files will be ignored but their parent directories will be created. //! //! Tests are assumed to not modify files in `*.in/` unless an `*.out/` is provided or //! `fs.sandbox = true` is set in the `.toml` file. //! //! ### `*.out/` //! //! When present, each file in this directory will be compared to generated or modified files. //! //! See also "Eliding Content" for `.stdout` //! //! `.keep` files will be ignored. //! //! Note: This implies `fs.sandbox = true`. //! //! ## Examples //! //! - Simple cargo binary: [trycmd's integration tests](https://github.com/assert-rs/trycmd/blob/main/tests/cli_tests.rs) //! - Simple example: [trycmd's integration tests](https://github.com/assert-rs/trycmd/blob/main/tests/example_tests.rs) //! - [typos](https://github.com/crate-ci/typos) (source code spell checker) //! - [clap](https://github.com/clap-rs/clap/) (CLI parser) to test examples //! //! ## Related crates //! //! For testing command line programs. //! - [escargot][escargot] for more control over configuring the crate's binary. //! - [duct][duct] for orchestrating multiple processes. //! - or [commandspec] for easier writing of commands //! - [`assert_cmd`][assert_cmd] for test cases that are individual pets, rather than herd of cattle //! - [`assert_fs`][assert_fs] for filesystem fixtures and assertions. //! - or [tempfile][tempfile] for scratchpad directories. //! - [rexpect][rexpect] for testing interactive programs. //! - [dir-diff][dir-diff] for testing file side-effects. //! //! For snapshot testing: //! - [insta](https://crates.io/crates/insta) //! - [fn-fixture](https://crates.io/crates/fn-fixture) //! - [runt](https://crates.io/crates/runt) //! - [turnt](https://github.com/cucapra/turnt) //! - [cram](https://bitheap.org/cram/) //! - [term-transcript](https://crates.io/crates/term-transcript): CLI snapshot testing, including colors //! //! [escargot]: http://docs.rs/escargot //! [rexpect]: https://crates.io/crates/rexpect //! [dir-diff]: https://crates.io/crates/dir-diff //! [tempfile]: https://crates.io/crates/tempfile //! [duct]: https://crates.io/crates/duct //! [assert_fs]: https://crates.io/crates/assert_fs //! [assert_cmd]: https://crates.io/crates/assert_cmd //! [commandspec]: https://crates.io/crates/commandspec #![cfg_attr(docsrs, feature(doc_auto_cfg))] // Doesn't distinguish between incidental sharing vs essential sharing #![allow(clippy::branches_sharing_code)] // Forces indentation that may not represent the logic #![allow(clippy::collapsible_else_if)] pub mod cargo; pub mod schema; mod cases; mod registry; mod runner; mod spec; pub use cases::TestCases; pub use snapbox::Error; pub(crate) use registry::BinRegistry; pub(crate) use runner::{Case, Mode, Runner}; pub(crate) use spec::RunnerSpec; pub(crate) use snapbox::Data; trycmd-0.14.20/src/registry.rs000064400000000000000000000032111046102023000142740ustar 00000000000000#[derive(Clone, Debug)] pub(crate) struct BinRegistry { bins: std::collections::BTreeMap, fallback: bool, } impl BinRegistry { pub(crate) fn new() -> Self { Self { bins: Default::default(), fallback: true, } } pub(crate) fn register_bin(&mut self, name: String, bin: crate::schema::Bin) { self.bins.insert(name, bin); } pub(crate) fn register_bins( &mut self, bins: impl Iterator, ) { self.bins.extend(bins); } pub(crate) fn resolve_bin( &self, bin: crate::schema::Bin, ) -> Result { match bin { crate::schema::Bin::Path(path) => { let bin = crate::schema::Bin::Path(path); Ok(bin) } crate::schema::Bin::Name(name) => { let bin = self.resolve_name(&name); Ok(bin) } crate::schema::Bin::Ignore => Ok(crate::schema::Bin::Ignore), crate::schema::Bin::Error(err) => Err(err), } } pub(crate) fn resolve_name(&self, name: &str) -> crate::schema::Bin { if let Some(path) = self.bins.get(name) { return path.clone(); } if self.fallback { let path = crate::cargo::cargo_bin(name); if path.exists() { return crate::schema::Bin::Path(path); } } crate::schema::Bin::Name(name.to_owned()) } } impl Default for BinRegistry { fn default() -> Self { Self::new() } } trycmd-0.14.20/src/runner.rs000064400000000000000000000773611046102023000137560ustar 00000000000000use std::io::prelude::*; #[cfg(feature = "color")] use anstream::eprintln; #[cfg(feature = "color")] use anstream::panic; #[cfg(feature = "color")] use anstream::stderr; #[cfg(not(feature = "color"))] use std::eprintln; #[cfg(not(feature = "color"))] use std::io::stderr; use rayon::prelude::*; use snapbox::path::FileType; use snapbox::{DataFormat, NormalizeNewlines, NormalizePaths}; #[derive(Debug)] pub(crate) struct Runner { cases: Vec, } impl Runner { pub(crate) fn new() -> Self { Self { cases: Default::default(), } } pub(crate) fn case(&mut self, case: Case) { self.cases.push(case); } pub(crate) fn run( &self, mode: &Mode, bins: &crate::BinRegistry, substitutions: &snapbox::Substitutions, ) { let palette = snapbox::report::Palette::color(); if self.cases.is_empty() { eprintln!("{}", palette.warn("There are no trycmd tests enabled yet")); } else { let failures: Vec<_> = self .cases .par_iter() .flat_map(|c| { let results = c.run(mode, bins, substitutions); let stderr = stderr(); let mut stderr = stderr.lock(); results .into_iter() .filter_map(|s| { snapbox::debug!("Case: {:#?}", s); match s { Ok(status) => { let _ = writeln!( stderr, "{} {} ... {}", palette.hint("Testing"), status.name(), status.spawn.status.summary() ); if !status.is_ok() { // Assuming `status` will print the newline let _ = write!(stderr, "{}", &status); } None } Err(status) => { let _ = writeln!( stderr, "{} {} ... {}", palette.hint("Testing"), status.name(), palette.error("failed"), ); // Assuming `status` will print the newline let _ = write!(stderr, "{}", &status); Some(status) } } }) .collect::>() }) .collect(); if !failures.is_empty() { let stderr = stderr(); let mut stderr = stderr.lock(); let _ = writeln!( stderr, "{}", palette.hint("Update snapshots with `TRYCMD=overwrite`"), ); let _ = writeln!( stderr, "{}", palette.hint("Debug output with `TRYCMD=dump`"), ); panic!("{} of {} tests failed", failures.len(), self.cases.len()); } } } } impl Default for Runner { fn default() -> Self { Self::new() } } #[derive(Debug)] pub(crate) struct Case { pub(crate) path: std::path::PathBuf, pub(crate) expected: Option, pub(crate) timeout: Option, pub(crate) default_bin: Option, pub(crate) env: crate::schema::Env, pub(crate) error: Option, } impl Case { pub(crate) fn with_error(path: std::path::PathBuf, error: crate::Error) -> Self { Self { path, expected: None, timeout: None, default_bin: None, env: Default::default(), error: Some(SpawnStatus::Failure(error)), } } pub(crate) fn run( &self, mode: &Mode, bins: &crate::BinRegistry, substitutions: &snapbox::Substitutions, ) -> Vec> { if self.expected == Some(crate::schema::CommandStatus::Skipped) { let output = Output::sequence(self.path.clone()); assert_eq!(output.spawn.status, SpawnStatus::Skipped); return vec![Ok(output)]; } if let Some(err) = self.error.clone() { let mut output = Output::step(self.path.clone(), "setup".into()); output.spawn.status = err; return vec![Err(output)]; } let mut sequence = match crate::schema::TryCmd::load(&self.path) { Ok(sequence) => sequence, Err(e) => { let output = Output::step(self.path.clone(), "setup".into()); return vec![Err(output.error(e))]; } }; if sequence.steps.is_empty() { let output = Output::sequence(self.path.clone()); assert_eq!(output.spawn.status, SpawnStatus::Skipped); return vec![Ok(output)]; } let fs_context = match fs_context( &self.path, sequence.fs.base.as_deref(), sequence.fs.sandbox(), mode, ) { Ok(fs_context) => fs_context, Err(e) => { let output = Output::step(self.path.clone(), "setup".into()); return vec![Err( output.error(format!("Failed to initialize sandbox: {}", e).into()) )]; } }; let cwd = match fs_context .path() .map(|p| { sequence.fs.rel_cwd().map(|rel| { let p = p.join(rel); snapbox::path::strip_trailing_slash(&p).to_owned() }) }) .transpose() { Ok(cwd) => cwd.or_else(|| std::env::current_dir().ok()), Err(e) => { let output = Output::step(self.path.clone(), "setup".into()); return vec![Err(output.error(e))]; } }; let mut substitutions = substitutions.clone(); if let Some(root) = fs_context.path() { substitutions .insert("[ROOT]", root.display().to_string()) .unwrap(); } if let Some(cwd) = cwd.clone().or_else(|| std::env::current_dir().ok()) { substitutions .insert("[CWD]", cwd.display().to_string()) .unwrap(); } substitutions .insert("[EXE]", std::env::consts::EXE_SUFFIX) .unwrap(); snapbox::debug!("{:?}", substitutions); let mut outputs = Vec::with_capacity(sequence.steps.len()); let mut prior_step_failed = false; for step in &mut sequence.steps { if prior_step_failed { step.expected_status = Some(crate::schema::CommandStatus::Skipped); } let step_status = self.run_step(step, cwd.as_deref(), bins, &substitutions); if fs_context.is_mutable() && step_status.is_err() && *mode == Mode::Fail { prior_step_failed = true; } outputs.push(step_status); } match mode { Mode::Dump(root) => { for output in &mut outputs { let output = match output { Ok(output) => output, Err(output) => output, }; output.stdout = match self.dump_stream(root, output.id.as_deref(), output.stdout.take()) { Ok(stream) => stream, Err(stream) => stream, }; output.stderr = match self.dump_stream(root, output.id.as_deref(), output.stderr.take()) { Ok(stream) => stream, Err(stream) => stream, }; } } Mode::Overwrite => { // `rev()` to ensure we don't mess up our line number info for step_status in outputs.iter_mut().rev() { if let Err(output) = step_status { let res = sequence.overwrite( &self.path, output.id.as_deref(), output.stdout.as_ref().map(|s| &s.content), output.stderr.as_ref().map(|s| &s.content), output.spawn.exit, ); if res.is_ok() { *step_status = Ok(output.clone()); } } } } Mode::Fail => {} } if sequence.fs.sandbox() { let mut ok = true; let mut output = Output::step(self.path.clone(), "teardown".into()); output.fs = match self.validate_fs( fs_context.path().expect("sandbox must be filled"), output.fs, mode, &substitutions, ) { Ok(fs) => fs, Err(fs) => { ok = false; fs } }; if let Err(err) = fs_context.close() { ok = false; output.fs.context.push(FileStatus::Failure( format!("Failed to cleanup sandbox: {}", err).into(), )); } let output = if ok { output.spawn.status = SpawnStatus::Ok; Ok(output) } else { output.spawn.status = SpawnStatus::Failure("Files left in unexpected state".into()); Err(output) }; outputs.push(output); } outputs } #[allow(clippy::result_large_err)] pub(crate) fn run_step( &self, step: &mut crate::schema::Step, cwd: Option<&std::path::Path>, bins: &crate::BinRegistry, substitutions: &snapbox::Substitutions, ) -> Result { let output = if let Some(id) = step.id.clone() { Output::step(self.path.clone(), id) } else { Output::sequence(self.path.clone()) }; let mut bin = step.bin.take(); if bin.is_none() { bin = self.default_bin.clone() } bin = bin .map(|name| bins.resolve_bin(name)) .transpose() .map_err(|e| output.clone().error(e))?; step.bin = bin; if step.timeout.is_none() { step.timeout = self.timeout; } if self.expected.is_some() { step.expected_status = self.expected; } step.env.update(&self.env); if step.expected_status() == crate::schema::CommandStatus::Skipped { assert_eq!(output.spawn.status, SpawnStatus::Skipped); return Ok(output); } match &step.bin { Some(crate::schema::Bin::Path(_)) => {} Some(crate::schema::Bin::Name(_name)) => { // Unhandled by resolve snapbox::debug!("bin={:?} not found", _name); assert_eq!(output.spawn.status, SpawnStatus::Skipped); return Ok(output); } Some(crate::schema::Bin::Error(_)) => {} // Unlike `Name`, this always represents a bug None => {} Some(crate::schema::Bin::Ignore) => { // Unhandled by resolve assert_eq!(output.spawn.status, SpawnStatus::Skipped); return Ok(output); } } let cmd = step.to_command(cwd).map_err(|e| output.clone().error(e))?; let cmd_output = cmd .output() .map_err(|e| output.clone().error(e.to_string().into()))?; let output = output.output(cmd_output); // For Mode::Dump's sake, allow running all let output = self.validate_spawn(output, step.expected_status()); let output = self.validate_streams(output, step, substitutions); if output.is_ok() { Ok(output) } else { Err(output) } } fn validate_spawn(&self, mut output: Output, expected: crate::schema::CommandStatus) -> Output { let status = output.spawn.exit.expect("bale out before now"); match expected { crate::schema::CommandStatus::Success => { if !status.success() { output.spawn.status = SpawnStatus::Expected("success".into()); } } crate::schema::CommandStatus::Failed => { if status.success() || status.code().is_none() { output.spawn.status = SpawnStatus::Expected("failure".into()); } } crate::schema::CommandStatus::Interrupted => { if status.code().is_some() { output.spawn.status = SpawnStatus::Expected("interrupted".into()); } } crate::schema::CommandStatus::Skipped => unreachable!("handled earlier"), crate::schema::CommandStatus::Code(expected_code) => { if Some(expected_code) != status.code() { output.spawn.status = SpawnStatus::Expected(expected_code.to_string()); } } } output } fn validate_streams( &self, mut output: Output, step: &crate::schema::Step, substitutions: &snapbox::Substitutions, ) -> Output { output.stdout = self.validate_stream( output.stdout, step.expected_stdout.as_ref(), step.binary, substitutions, ); output.stderr = self.validate_stream( output.stderr, step.expected_stderr.as_ref(), step.binary, substitutions, ); output } fn validate_stream( &self, stream: Option, expected_content: Option<&crate::Data>, binary: bool, substitutions: &snapbox::Substitutions, ) -> Option { let mut stream = stream?; if !binary { stream = stream.make_text(); if !stream.is_ok() { return Some(stream); } } if let Some(expected_content) = expected_content { stream.content = stream.content.normalize(snapbox::NormalizeMatches::new( substitutions, expected_content, )); if stream.content != *expected_content { stream.status = StreamStatus::Expected(expected_content.clone()); return Some(stream); } } Some(stream) } fn dump_stream( &self, root: &std::path::Path, id: Option<&str>, stream: Option, ) -> Result, Option> { if let Some(stream) = stream { let file_name = match id { Some(id) => { format!( "{}-{}.{}", self.path.file_stem().unwrap().to_string_lossy(), id, stream.stream.as_str(), ) } None => { format!( "{}.{}", self.path.file_stem().unwrap().to_string_lossy(), stream.stream.as_str(), ) } }; let stream_path = root.join(file_name); stream.content.write_to(&stream_path).map_err(|e| { let mut stream = stream.clone(); if stream.is_ok() { stream.status = StreamStatus::Failure(e); } stream })?; Ok(Some(stream)) } else { Ok(None) } } fn validate_fs( &self, actual_root: &std::path::Path, mut fs: Filesystem, mode: &Mode, substitutions: &snapbox::Substitutions, ) -> Result { let mut ok = true; #[cfg(feature = "filesystem")] if let Mode::Dump(_) = mode { // Handled as part of PathFixture } else { let fixture_root = self.path.with_extension("out"); if fixture_root.exists() { for status in snapbox::path::PathDiff::subset_matches_iter( fixture_root, actual_root, substitutions, ) { match status { Ok((expected_path, actual_path)) => { fs.context.push(FileStatus::Ok { actual_path, expected_path, }); } Err(diff) => { let mut is_current_ok = false; if *mode == Mode::Overwrite && diff.overwrite().is_ok() { is_current_ok = true; } fs.context.push(diff.into()); if !is_current_ok { ok = false; } } } } } } if ok { Ok(fs) } else { Err(fs) } } } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Output { path: std::path::PathBuf, id: Option, spawn: Spawn, stdout: Option, stderr: Option, fs: Filesystem, } impl Output { fn sequence(path: std::path::PathBuf) -> Self { Self { path, id: None, spawn: Spawn { exit: None, status: SpawnStatus::Skipped, }, stdout: None, stderr: None, fs: Default::default(), } } fn step(path: std::path::PathBuf, step: String) -> Self { Self { path, id: Some(step), spawn: Default::default(), stdout: None, stderr: None, fs: Default::default(), } } fn output(mut self, output: std::process::Output) -> Self { self.spawn.exit = Some(output.status); assert_eq!(self.spawn.status, SpawnStatus::Skipped); self.spawn.status = SpawnStatus::Ok; self.stdout = Some(Stream { stream: Stdio::Stdout, content: output.stdout.into(), status: StreamStatus::Ok, }); self.stderr = Some(Stream { stream: Stdio::Stderr, content: output.stderr.into(), status: StreamStatus::Ok, }); self } fn error(mut self, msg: crate::Error) -> Self { self.spawn.status = SpawnStatus::Failure(msg); self } fn is_ok(&self) -> bool { self.spawn.is_ok() && self.stdout.as_ref().map(|s| s.is_ok()).unwrap_or(true) && self.stderr.as_ref().map(|s| s.is_ok()).unwrap_or(true) && self.fs.is_ok() } fn name(&self) -> String { self.id .as_deref() .map(|id| format!("{}:{}", self.path.display(), id)) .unwrap_or_else(|| self.path.display().to_string()) } } impl std::fmt::Display for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.spawn.fmt(f)?; if let Some(stdout) = &self.stdout { stdout.fmt(f)?; } if let Some(stderr) = &self.stderr { stderr.fmt(f)?; } self.fs.fmt(f)?; Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] struct Spawn { exit: Option, status: SpawnStatus, } impl Spawn { fn is_ok(&self) -> bool { self.status.is_ok() } } impl Default for Spawn { fn default() -> Self { Self { exit: None, status: SpawnStatus::Skipped, } } } impl std::fmt::Display for Spawn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let palette = snapbox::report::Palette::color(); match &self.status { SpawnStatus::Ok => { if let Some(exit) = self.exit { if exit.success() { writeln!(f, "Exit: {}", palette.info("success"))?; } else if let Some(code) = exit.code() { writeln!(f, "Exit: {}", palette.error(code))?; } else { writeln!(f, "Exit: {}", palette.error("interrupted"))?; } } } SpawnStatus::Skipped => { writeln!(f, "{}", palette.warn("Skipped"))?; } SpawnStatus::Failure(msg) => { writeln!(f, "Failed: {}", palette.error(msg))?; } SpawnStatus::Expected(expected) => { if let Some(exit) = self.exit { if exit.success() { writeln!( f, "Expected {}, was {}", palette.info(expected), palette.error("success") )?; } else { writeln!( f, "Expected {}, was {}", palette.info(expected), palette.error(snapbox::cmd::display_exit_status(exit)) )?; } } } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum SpawnStatus { Ok, Skipped, Failure(crate::Error), Expected(String), } impl SpawnStatus { fn is_ok(&self) -> bool { match self { Self::Ok | Self::Skipped => true, Self::Failure(_) | Self::Expected(_) => false, } } fn summary(&self) -> impl std::fmt::Display { let palette = snapbox::report::Palette::color(); match self { Self::Ok => palette.info("ok"), Self::Skipped => palette.warn("ignored"), Self::Failure(_) | Self::Expected(_) => palette.error("failed"), } } } #[derive(Clone, Debug, PartialEq, Eq)] struct Stream { stream: Stdio, content: crate::Data, status: StreamStatus, } impl Stream { fn make_text(mut self) -> Self { let content = self.content.try_coerce(DataFormat::Text); if content.format() != DataFormat::Text { self.status = StreamStatus::Failure("Unable to convert underlying Data to Text".into()); } self.content = content .normalize(NormalizePaths) .normalize(NormalizeNewlines); self } fn is_ok(&self) -> bool { self.status.is_ok() } } impl std::fmt::Display for Stream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let palette = snapbox::report::Palette::color(); match &self.status { StreamStatus::Ok => { writeln!(f, "{}:", self.stream)?; writeln!(f, "{}", palette.info(&self.content))?; } StreamStatus::Failure(msg) => { writeln!( f, "{} {}:", self.stream, palette.error(format_args!("({})", msg)) )?; writeln!(f, "{}", palette.info(&self.content))?; } StreamStatus::Expected(expected) => { snapbox::report::write_diff( f, expected, &self.content, Some(&self.stream), Some(&self.stream), palette, )?; } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] enum StreamStatus { Ok, Failure(crate::Error), Expected(crate::Data), } impl StreamStatus { fn is_ok(&self) -> bool { match self { Self::Ok => true, Self::Failure(_) | Self::Expected(_) => false, } } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum Stdio { Stdout, Stderr, } impl Stdio { fn as_str(&self) -> &str { match self { Self::Stdout => "stdout", Self::Stderr => "stderr", } } } impl std::fmt::Display for Stdio { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.as_str().fmt(f) } } #[derive(Clone, Default, Debug, PartialEq, Eq)] struct Filesystem { context: Vec, } impl Filesystem { fn is_ok(&self) -> bool { if self.context.is_empty() { true } else { self.context.iter().all(FileStatus::is_ok) } } } impl std::fmt::Display for Filesystem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for status in &self.context { status.fmt(f)?; } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] enum FileStatus { Ok { expected_path: std::path::PathBuf, actual_path: std::path::PathBuf, }, 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 FileStatus { fn is_ok(&self) -> bool { match self { Self::Ok { .. } => true, Self::Failure(_) | Self::TypeMismatch { .. } | Self::LinkMismatch { .. } | Self::ContentMismatch { .. } => false, } } } impl From for FileStatus { fn from(other: snapbox::path::PathDiff) -> Self { match other { snapbox::path::PathDiff::Failure(err) => FileStatus::Failure(err), snapbox::path::PathDiff::TypeMismatch { expected_path, actual_path, expected_type, actual_type, } => FileStatus::TypeMismatch { actual_path, expected_path, actual_type, expected_type, }, snapbox::path::PathDiff::LinkMismatch { expected_path, actual_path, expected_target, actual_target, } => FileStatus::LinkMismatch { actual_path, expected_path, actual_target, expected_target, }, snapbox::path::PathDiff::ContentMismatch { expected_path, actual_path, expected_content, actual_content, } => FileStatus::ContentMismatch { actual_path, expected_path, actual_content, expected_content, }, } } } impl std::fmt::Display for FileStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let palette = snapbox::report::Palette::color(); match &self { Self::Ok { expected_path, actual_path: _actual_path, } => { writeln!( f, "{}: is {}", expected_path.display(), palette.info("good"), )?; } 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, } => { snapbox::report::write_diff( f, expected_content, actual_content, Some(&expected_path.display()), Some(&actual_path.display()), palette, )?; } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum Mode { Fail, Overwrite, Dump(std::path::PathBuf), } impl Mode { pub(crate) fn initialize(&self) -> Result<(), std::io::Error> { match self { Self::Fail => {} Self::Overwrite => {} Self::Dump(root) => { std::fs::create_dir_all(root)?; let gitignore_path = root.join(".gitignore"); std::fs::write(gitignore_path, "*\n")?; } } Ok(()) } } #[cfg_attr(not(feature = "filesystem"), allow(unused_variables))] fn fs_context( path: &std::path::Path, cwd: Option<&std::path::Path>, sandbox: bool, mode: &crate::Mode, ) -> Result { if sandbox { #[cfg(feature = "filesystem")] match mode { crate::Mode::Dump(root) => { let target = root.join(path.with_extension("out").file_name().unwrap()); let mut context = snapbox::path::PathFixture::mutable_at(&target)?; if let Some(cwd) = cwd { context = context.with_template(cwd)?; } Ok(context) } crate::Mode::Fail | crate::Mode::Overwrite => { let mut context = snapbox::path::PathFixture::mutable_temp()?; if let Some(cwd) = cwd { context = context.with_template(cwd)?; } Ok(context) } } #[cfg(not(feature = "filesystem"))] Err("Sandboxing is disabled".into()) } else { Ok(cwd .map(snapbox::path::PathFixture::immutable) .unwrap_or_else(snapbox::path::PathFixture::none)) } } trycmd-0.14.20/src/schema.rs000064400000000000000000001366431046102023000137040ustar 00000000000000//! `cmd.toml` Schema //! //! [`OneShot`] is the top-level item in the `cmd.toml` files. use snapbox::{NormalizeNewlines, NormalizePaths}; use std::collections::BTreeMap; use std::collections::VecDeque; #[derive(Clone, Default, Debug, PartialEq, Eq)] pub(crate) struct TryCmd { pub(crate) steps: Vec, pub(crate) fs: Filesystem, } impl TryCmd { pub(crate) fn load(path: &std::path::Path) -> Result { let mut sequence = if let Some(ext) = path.extension() { if ext == std::ffi::OsStr::new("toml") { let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let one_shot = OneShot::parse_toml(&raw)?; let mut sequence: Self = one_shot.into(); let is_binary = match sequence.steps[0].binary { true => snapbox::DataFormat::Binary, false => snapbox::DataFormat::Text, }; if sequence.steps[0].stdin.is_none() { let stdin_path = path.with_extension("stdin"); let stdin = if stdin_path.exists() { // No `map_text` as we will trust what the user inputted Some(crate::Data::read_from(&stdin_path, Some(is_binary))?) } else { None }; sequence.steps[0].stdin = stdin; } if sequence.steps[0].expected_stdout.is_none() { let stdout_path = path.with_extension("stdout"); let stdout = if stdout_path.exists() { Some( crate::Data::read_from(&stdout_path, Some(is_binary))? .normalize(NormalizePaths) .normalize(NormalizeNewlines), ) } else { None }; sequence.steps[0].expected_stdout = stdout; } if sequence.steps[0].expected_stderr.is_none() { let stderr_path = path.with_extension("stderr"); let stderr = if stderr_path.exists() { Some( crate::Data::read_from(&stderr_path, Some(is_binary))? .normalize(NormalizePaths) .normalize(NormalizeNewlines), ) } else { None }; sequence.steps[0].expected_stderr = stderr; } sequence } else if ext == std::ffi::OsStr::new("trycmd") || ext == std::ffi::OsStr::new("md") { let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let normalized = snapbox::utils::normalize_lines(&raw); Self::parse_trycmd(&normalized)? } else { return Err(format!("Unsupported extension: {}", ext.to_string_lossy()).into()); } } else { return Err("No extension".into()); }; sequence.fs.base = sequence.fs.base.take().map(|base| { path.parent() .unwrap_or_else(|| std::path::Path::new(".")) .join(base) }); sequence.fs.cwd = sequence.fs.cwd.take().map(|cwd| { path.parent() .unwrap_or_else(|| std::path::Path::new(".")) .join(cwd) }); if sequence.fs.base.is_none() { let base_path = path.with_extension("in"); if base_path.exists() { sequence.fs.base = Some(base_path); } else if sequence.fs.cwd.is_some() { sequence.fs.base = sequence.fs.cwd.clone(); } } if sequence.fs.cwd.is_none() { sequence.fs.cwd = sequence.fs.base.clone(); } if sequence.fs.sandbox.is_none() { sequence.fs.sandbox = Some(path.with_extension("out").exists()); } sequence.fs.base = sequence .fs .base .take() .map(|p| snapbox::path::resolve_dir(p).map_err(|e| e.to_string())) .transpose()?; sequence.fs.cwd = sequence .fs .cwd .take() .map(|p| snapbox::path::resolve_dir(p).map_err(|e| e.to_string())) .transpose()?; Ok(sequence) } pub(crate) fn overwrite( &self, path: &std::path::Path, id: Option<&str>, stdout: Option<&crate::Data>, stderr: Option<&crate::Data>, exit: Option, ) -> Result<(), crate::Error> { if let Some(ext) = path.extension() { if ext == std::ffi::OsStr::new("toml") { assert_eq!(id, None); overwrite_toml_output(path, id, stdout, "stdout", "stdout")?; overwrite_toml_output(path, id, stderr, "stderr", "stderr")?; if let Some(status) = exit { let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let overwritten = overwrite_toml_status(status, raw) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; std::fs::write(path, overwritten) .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; } } else if ext == std::ffi::OsStr::new("trycmd") || ext == std::ffi::OsStr::new("md") { if stderr.is_some() && stderr != Some(&crate::Data::new()) { panic!("stderr should have been merged: {:?}", stderr); } if let (Some(id), Some(stdout)) = (id, stdout) { let step = self .steps .iter() .find(|s| s.id.as_deref() == Some(id)) .expect("id is valid"); let mut line_nums = step .expected_stdout_source .clone() .expect("always present for .trycmd"); let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let mut normalized = snapbox::utils::normalize_lines(&raw); overwrite_trycmd_status(exit, step, &mut line_nums, &mut normalized)?; let mut stdout = stdout.render().expect("at least Text"); // Add back trailing newline removed when parsing stdout.push('\n'); replace_lines(&mut normalized, line_nums, &stdout)?; std::fs::write(path, normalized.into_bytes()) .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; } } else { return Err(format!("Unsupported extension: {}", ext.to_string_lossy()).into()); } } else { return Err("No extension".into()); } Ok(()) } fn parse_trycmd(s: &str) -> Result { let mut steps = Vec::new(); let mut lines: VecDeque<_> = snapbox::utils::LinesWithTerminator::new(s) .enumerate() .map(|(i, l)| (i + 1, l)) .collect(); 'outer: loop { let mut fence_pattern = "```".to_owned(); while let Some((_, line)) = lines.pop_front() { let tick_end = line .char_indices() .find_map(|(i, c)| (c != '`').then_some(i)) .unwrap_or(line.len()); if 3 <= tick_end { fence_pattern = line[..tick_end].to_owned(); let raw = line[tick_end..].trim(); if raw.is_empty() { // Assuming a trycmd block break; } else { let mut info = raw.split(','); let lang = info.next().unwrap(); match lang { "trycmd" | "console" => { if info.any(|i| i == "ignore") { snapbox::debug!("ignore from infostring: {:?}", info); } else { break; } } _ => { snapbox::debug!("ignore from lang: {:?}", lang); } } } // Irrelevant block, consume to end while let Some((_, line)) = lines.pop_front() { if line.starts_with(&fence_pattern) { continue 'outer; } } } } 'code: loop { let mut cmdline = Vec::new(); let mut expected_status_source = None; let mut expected_status = Some(CommandStatus::Success); let mut stdout = String::new(); let cmd_start; let mut stdout_start; if let Some((line_num, line)) = lines.pop_front() { if line.starts_with(&fence_pattern) { break; } else if let Some(raw) = line.strip_prefix("$ ") { cmdline.extend(shlex::Shlex::new(raw.trim())); cmd_start = line_num; stdout_start = line_num + 1; } else { return Err( format!("Expected `$` on line {}, got `{}`", line_num, line).into() ); } } else { break 'outer; } while let Some((line_num, line)) = lines.pop_front() { if let Some(raw) = line.strip_prefix("> ") { cmdline.extend(shlex::Shlex::new(raw.trim())); stdout_start = line_num + 1; } else { lines.push_front((line_num, line)); break; } } if let Some((line_num, line)) = lines.pop_front() { if let Some(raw) = line.strip_prefix("? ") { expected_status_source = Some(line_num); expected_status = Some(raw.trim().parse::()?); stdout_start = line_num + 1; } else { lines.push_front((line_num, line)); } } let mut post_stdout_start = stdout_start; let mut block_done = false; while let Some((line_num, line)) = lines.pop_front() { if line.starts_with("$ ") { lines.push_front((line_num, line)); post_stdout_start = line_num; break; } else if line.starts_with(&fence_pattern) { block_done = true; post_stdout_start = line_num; break; } else { stdout.push_str(line); post_stdout_start = line_num + 1; } } if stdout.ends_with('\n') { // Last newline is for formatting purposes so tests can verify cases without a // trailing newline. stdout.pop(); } let mut env = Env::default(); let bin = loop { if cmdline.is_empty() { return Err(format!("No bin specified on line {}", cmd_start).into()); } let next = cmdline.remove(0); if let Some((key, value)) = next.split_once('=') { env.add.insert(key.to_owned(), value.to_owned()); } else { break next; } }; let step = Step { id: Some(cmd_start.to_string()), bin: Some(Bin::Name(bin)), args: cmdline, env, stdin: None, stderr_to_stdout: true, expected_status_source, expected_status, expected_stdout_source: Some(stdout_start..post_stdout_start), expected_stdout: Some(crate::Data::text(stdout)), expected_stderr_source: None, expected_stderr: None, binary: false, timeout: None, }; steps.push(step); if block_done { break 'code; } } } Ok(Self { steps, ..Default::default() }) } } fn overwrite_toml_output( path: &std::path::Path, _id: Option<&str>, output: Option<&crate::Data>, output_ext: &str, output_field: &str, ) -> Result<(), crate::Error> { if let Some(output) = output { let output_path = path.with_extension(output_ext); if output_path.exists() { output.write_to(&output_path)?; } else if let Some(output) = output.render() { let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let mut doc = raw .parse::() .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; if let Some(output_value) = doc.get_mut(output_field) { *output_value = toml_edit::value(output); } std::fs::write(path, doc.to_string()) .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; } else { output.write_to(&output_path)?; let raw = std::fs::read_to_string(path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; let mut doc = raw .parse::() .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; doc[output_field] = toml_edit::Item::None; std::fs::write(path, doc.to_string()) .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; } } Ok(()) } fn overwrite_toml_status( status: std::process::ExitStatus, raw: String, ) -> Result { let mut doc = raw.parse::()?; if let Some(code) = status.code() { if status.success() { match doc.get("status") { Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected))) if expected.value() == "success" => {} Some( toml_edit::Item::Value(toml_edit::Value::InlineTable(_)) | toml_edit::Item::Table(_), ) => { if !matches!( doc["status"].get("code"), Some(toml_edit::Item::Value(toml_edit::Value::Integer(ref expected))) if expected.value() == &0) { // Remove `status` to use the default value (success) doc["status"] = toml_edit::Item::None; } } _ => { // Remove `status` to use the default value (success) doc["status"] = toml_edit::Item::None; } } } else { let code = code as i64; match doc.get("status") { Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected))) => { if expected.value() != "failed" { doc["status"] = toml_edit::value("failed"); } } Some( toml_edit::Item::Value(toml_edit::Value::InlineTable(_)) | toml_edit::Item::Table(_), ) => { if !matches!( doc["status"].get("code"), Some(toml_edit::Item::Value(toml_edit::Value::Integer(ref expected))) if expected.value() == &code) { doc["status"]["code"] = toml_edit::value(code); } } _ => { let mut status = toml_edit::InlineTable::default(); status.set_dotted(true); status.insert("code", code.into()); doc["status"] = toml_edit::value(status); } } } } else if !matches!( doc.get("status"), Some(toml_edit::Item::Value(toml_edit::Value::String(ref expected))) if expected.value() == "interrupted") { doc["status"] = toml_edit::value("interrupted"); } Ok(doc.to_string()) } fn overwrite_trycmd_status( exit: Option, step: &Step, stdout_line_nums: &mut std::ops::Range, normalized: &mut String, ) -> Result<(), snapbox::Error> { let status = match exit { Some(status) => status, _ => { return Ok(()); } }; let formatted_status = if let Some(code) = status.code() { if status.success() { if let (true, Some(line_num)) = ( step.expected_status != Some(CommandStatus::Success), step.expected_status_source, ) { replace_lines(normalized, line_num..(line_num + 1), "")?; *stdout_line_nums = (stdout_line_nums.start - 1)..(stdout_line_nums.end - 1); } None } else { match step.expected_status { Some(CommandStatus::Success | CommandStatus::Interrupted) => { Some(format!("? {code}")) } Some(CommandStatus::Code(expected)) if expected != code => { Some(format!("? {code}")) } _ => None, } } } else { if step.expected_status == Some(CommandStatus::Interrupted) { None } else { Some("? interrupted".into()) } }; if let Some(status) = formatted_status { if let Some(line_num) = step.expected_status_source { replace_lines(normalized, line_num..(line_num + 1), &status)?; } else { let line_num = stdout_line_nums.start; replace_lines(normalized, line_num..line_num, &status)?; *stdout_line_nums = (line_num + 1)..(stdout_line_nums.end + 1); } } Ok(()) } /// Update an inline snapshot fn replace_lines( data: &mut String, line_nums: std::ops::Range, text: &str, ) -> Result<(), crate::Error> { let mut output_lines = String::new(); for (line_num, line) in snapbox::utils::LinesWithTerminator::new(data) .enumerate() .map(|(i, l)| (i + 1, l)) { if line_num == line_nums.start { output_lines.push_str(text); if !text.is_empty() && !text.ends_with('\n') { output_lines.push('\n'); } } if !line_nums.contains(&line_num) { output_lines.push_str(line); } } *data = output_lines; Ok(()) } impl std::str::FromStr for TryCmd { type Err = crate::Error; fn from_str(s: &str) -> Result { Self::parse_trycmd(s) } } impl From for TryCmd { fn from(other: OneShot) -> Self { let OneShot { bin, args, env, stdin, stdout, stderr, stderr_to_stdout, status, binary, timeout, fs, } = other; Self { steps: vec![Step { id: None, bin, args: args.into_vec(), env, stdin: stdin.map(crate::Data::text), stderr_to_stdout, expected_status_source: None, expected_status: status, expected_stdout_source: None, expected_stdout: stdout.map(crate::Data::text), expected_stderr_source: None, expected_stderr: stderr.map(crate::Data::text), binary, timeout, }], fs, } } } #[derive(Clone, Default, Debug, PartialEq, Eq)] pub(crate) struct Step { pub(crate) id: Option, pub(crate) bin: Option, pub(crate) args: Vec, pub(crate) env: Env, pub(crate) stdin: Option, pub(crate) stderr_to_stdout: bool, pub(crate) expected_status_source: Option, pub(crate) expected_status: Option, pub(crate) expected_stdout_source: Option>, pub(crate) expected_stdout: Option, pub(crate) expected_stderr_source: Option>, pub(crate) expected_stderr: Option, pub(crate) binary: bool, pub(crate) timeout: Option, } impl Step { pub(crate) fn to_command( &self, cwd: Option<&std::path::Path>, ) -> Result { let bin = match &self.bin { Some(Bin::Path(path)) => Ok(path.clone()), Some(Bin::Name(name)) => Err(format!("Unknown bin.name = {}", name).into()), Some(Bin::Ignore) => Err("Internal error: tried to run an ignored bin".into()), Some(Bin::Error(err)) => Err(err.clone()), None => Err("No bin specified".into()), }?; if !bin.exists() { return Err(format!("Bin doesn't exist: {}", bin.display()).into()); } let mut cmd = snapbox::cmd::Command::new(bin).args(&self.args); if let Some(cwd) = cwd { cmd = cmd.current_dir(cwd); } if let Some(stdin) = &self.stdin { cmd = cmd.stdin(stdin); } if self.stderr_to_stdout { cmd = cmd.stderr_to_stdout(); } if let Some(timeout) = self.timeout { cmd = cmd.timeout(timeout) } cmd = self.env.apply(cmd); Ok(cmd) } pub(crate) fn expected_status(&self) -> CommandStatus { self.expected_status.unwrap_or_default() } } /// Top-level data in `cmd.toml` files #[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct OneShot { pub(crate) bin: Option, #[serde(default)] pub(crate) args: Args, #[serde(default)] pub(crate) env: Env, #[serde(default)] pub(crate) stdin: Option, #[serde(default)] pub(crate) stdout: Option, #[serde(default)] pub(crate) stderr: Option, #[serde(default)] pub(crate) stderr_to_stdout: bool, pub(crate) status: Option, #[serde(default)] pub(crate) binary: bool, #[serde(default)] #[serde(deserialize_with = "humantime_serde::deserialize")] pub(crate) timeout: Option, #[serde(default)] pub(crate) fs: Filesystem, } impl OneShot { fn parse_toml(s: &str) -> Result { toml_edit::de::from_str(s).map_err(|e| e.to_string().into()) } } #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] pub(crate) enum Args { Joined(JoinedArgs), Split(Vec), } impl Args { fn new() -> Self { Self::Split(Default::default()) } fn as_slice(&self) -> &[String] { match self { Self::Joined(j) => j.inner.as_slice(), Self::Split(v) => v.as_slice(), } } fn into_vec(self) -> Vec { match self { Self::Joined(j) => j.inner, Self::Split(v) => v, } } } impl Default for Args { fn default() -> Self { Self::new() } } impl std::ops::Deref for Args { type Target = [String]; fn deref(&self) -> &Self::Target { self.as_slice() } } #[derive(Clone, Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub(crate) struct JoinedArgs { inner: Vec, } impl JoinedArgs { #[cfg(test)] pub(crate) fn from_vec(inner: Vec) -> Self { JoinedArgs { inner } } #[allow(clippy::inherent_to_string_shadow_display)] fn to_string(&self) -> String { shlex::join(self.inner.iter().map(|s| s.as_str())) } } impl std::str::FromStr for JoinedArgs { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { let inner = shlex::Shlex::new(s).collect(); Ok(Self { inner }) } } impl std::fmt::Display for JoinedArgs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_string().fmt(f) } } impl<'de> serde::de::Deserialize<'de> for JoinedArgs { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; std::str::FromStr::from_str(&s).map_err(serde::de::Error::custom) } } impl serde::ser::Serialize for JoinedArgs { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(&self.to_string()) } } /// Describe the command's filesystem context #[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Filesystem { pub(crate) cwd: Option, /// Sandbox base pub(crate) base: Option, pub(crate) sandbox: Option, } impl Filesystem { pub(crate) fn sandbox(&self) -> bool { self.sandbox.unwrap_or_default() } pub(crate) fn rel_cwd(&self) -> Result<&std::path::Path, crate::Error> { if let (Some(orig_cwd), Some(orig_base)) = (self.cwd.as_deref(), self.base.as_deref()) { let rel_cwd = orig_cwd.strip_prefix(orig_base).map_err(|_| { crate::Error::new(format!( "fs.cwd ({}) must be within fs.base ({})", orig_cwd.display(), orig_base.display() )) })?; Ok(rel_cwd) } else { Ok(std::path::Path::new("")) } } } /// Describe command's environment #[derive(Clone, Default, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Env { #[serde(default)] pub(crate) inherit: Option, #[serde(default)] pub(crate) add: BTreeMap, #[serde(default)] pub(crate) remove: Vec, } impl Env { pub(crate) fn update(&mut self, other: &Self) { if self.inherit.is_none() { self.inherit = other.inherit; } self.add .extend(other.add.iter().map(|(k, v)| (k.clone(), v.clone()))); self.remove.extend(other.remove.iter().cloned()); } pub(crate) fn apply(&self, mut command: snapbox::cmd::Command) -> snapbox::cmd::Command { if !self.inherit() { command = command.env_clear(); } for remove in &self.remove { command = command.env_remove(remove); } command.envs(&self.add) } pub(crate) fn inherit(&self) -> bool { self.inherit.unwrap_or(true) } } /// Target under test #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Bin { Path(std::path::PathBuf), Name(String), Ignore, #[serde(skip)] Error(crate::Error), } impl From for Bin { fn from(other: std::path::PathBuf) -> Self { Self::Path(other) } } impl<'a> From<&'a std::path::PathBuf> for Bin { fn from(other: &'a std::path::PathBuf) -> Self { Self::Path(other.clone()) } } impl<'a> From<&'a std::path::Path> for Bin { fn from(other: &'a std::path::Path) -> Self { Self::Path(other.to_owned()) } } impl From> for Bin where P: Into, E: std::fmt::Display, { fn from(other: Result) -> Self { match other { Ok(path) => path.into(), Err(err) => { let err = crate::Error::new(err.to_string()); Bin::Error(err) } } } } /// Expected status for command #[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[derive(Default)] pub enum CommandStatus { #[default] Success, Failed, Interrupted, Skipped, Code(i32), } impl std::str::FromStr for CommandStatus { type Err = crate::Error; fn from_str(s: &str) -> Result { match s { "success" => Ok(Self::Success), "failed" => Ok(Self::Failed), "interrupted" => Ok(Self::Interrupted), "skipped" => Ok(Self::Skipped), _ => s .parse::() .map(Self::Code) .map_err(|_| crate::Error::new(format!("Expected an exit code, got {}", s))), } } } #[cfg(test)] mod test { use super::*; #[test] fn parse_trycmd_empty() { let expected = TryCmd { steps: vec![], ..Default::default() }; let actual = TryCmd::parse_trycmd("").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_empty_fence() { let expected = TryCmd { steps: vec![], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_command() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(4..4), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_command_line() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), args: vec!["arg1".into(), "arg with space".into()], expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(4..4), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd arg1 'arg with space' ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_multi_line() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), args: vec!["arg1".into(), "arg with space".into()], expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(5..5), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd arg1 > 'arg with space' ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_env() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), env: Env { add: IntoIterator::into_iter([ ("KEY1".into(), "VALUE1".into()), ("KEY2".into(), "VALUE2 with space".into()), ]) .collect(), ..Default::default() }, expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(4..4), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ KEY1=VALUE1 KEY2='VALUE2 with space' cmd ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_status() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), expected_status_source: Some(4), expected_status: Some(CommandStatus::Skipped), stderr_to_stdout: true, expected_stdout_source: Some(5..5), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd ? skipped ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_status_code() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), expected_status_source: Some(4), expected_status: Some(CommandStatus::Code(-1)), stderr_to_stdout: true, expected_stdout_source: Some(5..5), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd ? -1 ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_stdout() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(4..6), expected_stdout: Some(crate::Data::text("Hello World\n")), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd Hello World ```", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_escaped_stdout() { let expected = TryCmd { steps: vec![Step { id: Some("3".into()), bin: Some(Bin::Name("cmd".into())), expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(4..7), expected_stdout: Some(crate::Data::text("```\nHello World\n```")), expected_stderr: None, ..Default::default() }], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ```` $ cmd ``` Hello World ``` ````", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_multi_step() { let expected = TryCmd { steps: vec![ Step { id: Some("3".into()), bin: Some(Bin::Name("cmd1".into())), expected_status_source: Some(4), expected_status: Some(CommandStatus::Code(1)), stderr_to_stdout: true, expected_stdout_source: Some(5..5), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }, Step { id: Some("5".into()), bin: Some(Bin::Name("cmd2".into())), expected_status: Some(CommandStatus::Success), stderr_to_stdout: true, expected_stdout_source: Some(6..6), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }, ], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ cmd1 ? 1 $ cmd2 ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_trycmd_info_string() { let expected = TryCmd { steps: vec![ Step { id: Some("3".into()), bin: Some(Bin::Name("bare-cmd".into())), expected_status_source: Some(4), expected_status: Some(CommandStatus::Code(1)), stderr_to_stdout: true, expected_stdout_source: Some(5..5), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }, Step { id: Some("8".into()), bin: Some(Bin::Name("trycmd-cmd".into())), expected_status_source: Some(9), expected_status: Some(CommandStatus::Code(1)), stderr_to_stdout: true, expected_stdout_source: Some(10..10), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }, Step { id: Some("18".into()), bin: Some(Bin::Name("console-cmd".into())), expected_status_source: Some(19), expected_status: Some(CommandStatus::Code(1)), stderr_to_stdout: true, expected_stdout_source: Some(20..20), expected_stdout: Some(crate::Data::new()), expected_stderr: None, ..Default::default() }, ], ..Default::default() }; let actual = TryCmd::parse_trycmd( " ``` $ bare-cmd ? 1 ``` ```trycmd $ trycmd-cmd ? 1 ``` ```sh $ sh-cmd ? 1 ``` ```console $ console-cmd ? 1 ``` ```ignore $ rust-cmd1 ? 1 ``` ```trycmd,ignore $ rust-cmd1 ? 1 ``` ```rust $ rust-cmd1 ? 1 ``` ", ) .unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_minimal() { let expected = OneShot { ..Default::default() }; let actual = OneShot::parse_toml("").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_minimal_env() { let expected = OneShot { ..Default::default() }; let actual = OneShot::parse_toml("[env]").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_bin_name() { let expected = OneShot { bin: Some(Bin::Name("cmd".into())), ..Default::default() }; let actual = OneShot::parse_toml("bin.name = 'cmd'").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_bin_path() { let expected = OneShot { bin: Some(Bin::Path("/usr/bin/cmd".into())), ..Default::default() }; let actual = OneShot::parse_toml("bin.path = '/usr/bin/cmd'").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_args_split() { let expected = OneShot { args: Args::Split(vec!["arg1".into(), "arg with space".into()]), ..Default::default() }; let actual = OneShot::parse_toml(r#"args = ["arg1", "arg with space"]"#).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_args_joined() { let expected = OneShot { args: Args::Joined(JoinedArgs::from_vec(vec![ "arg1".into(), "arg with space".into(), ])), ..Default::default() }; let actual = OneShot::parse_toml(r#"args = "arg1 'arg with space'""#).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_status_success() { let expected = OneShot { status: Some(CommandStatus::Success), ..Default::default() }; let actual = OneShot::parse_toml("status = 'success'").unwrap(); assert_eq!(expected, actual); } #[test] fn parse_toml_status_code() { let expected = OneShot { status: Some(CommandStatus::Code(42)), ..Default::default() }; let actual = OneShot::parse_toml("status.code = 42").unwrap(); assert_eq!(expected, actual); } #[test] fn replace_lines_same_line_count() { let input = "One\nTwo\nThree"; let line_nums = 2..3; let replacement = "World\n"; let expected = "One\nWorld\nThree"; let mut actual = input.to_owned(); replace_lines(&mut actual, line_nums, replacement).unwrap(); assert_eq!(expected, actual); } #[test] fn replace_lines_grow() { let input = "One\nTwo\nThree"; let line_nums = 2..3; let replacement = "World\nTrees\n"; let expected = "One\nWorld\nTrees\nThree"; let mut actual = input.to_owned(); replace_lines(&mut actual, line_nums, replacement).unwrap(); assert_eq!(expected, actual); } #[test] fn replace_lines_shrink() { let input = "One\nTwo\nThree"; let line_nums = 2..3; let replacement = ""; let expected = "One\nThree"; let mut actual = input.to_owned(); replace_lines(&mut actual, line_nums, replacement).unwrap(); assert_eq!(expected, actual); } #[test] fn replace_lines_no_trailing() { let input = "One\nTwo\nThree"; let line_nums = 2..3; let replacement = "World"; let expected = "One\nWorld\nThree"; let mut actual = input.to_owned(); replace_lines(&mut actual, line_nums, replacement).unwrap(); assert_eq!(expected, actual); } #[test] fn replace_lines_empty_range() { let input = "One\nTwo\nThree"; let line_nums = 2..2; let replacement = "World\n"; let expected = "One\nWorld\nTwo\nThree"; let mut actual = input.to_owned(); replace_lines(&mut actual, line_nums, replacement).unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_toml_status_success() { let expected = r#" bin.name = "cmd" "#; let actual = overwrite_toml_status( exit_code_to_status(0), r#" bin.name = "cmd" status = "failed" "# .into(), ) .unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_toml_status_failed() { let expected = r#" bin.name = "cmd" status.code = 1 "#; let actual = overwrite_toml_status( exit_code_to_status(1), r#" bin.name = "cmd" "# .into(), ) .unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_toml_status_keeps_style() { let expected = r#" bin.name = "cmd" status = { code = 1 } # comment "#; let actual = overwrite_toml_status( exit_code_to_status(1), r#" bin.name = "cmd" status = { code = 2 } # comment "# .into(), ) .unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_trycmd_status_success() { let expected = r#" ``` $ cmd arg foo bar ``` "#; let mut actual = r" ``` $ cmd arg ? failed foo bar ``` " .to_owned(); let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0]; overwrite_trycmd_status( Some(exit_code_to_status(0)), step, &mut step.expected_stdout_source.clone().unwrap(), &mut actual, ) .unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_trycmd_status_failed() { let expected = r#" ``` $ cmd arg ? 1 foo bar ``` "#; let mut actual = r" ``` $ cmd arg ? 2 foo bar ``` " .to_owned(); let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0]; overwrite_trycmd_status( Some(exit_code_to_status(1)), step, &mut step.expected_stdout_source.clone().unwrap(), &mut actual, ) .unwrap(); assert_eq!(expected, actual); } #[test] fn overwrite_trycmd_status_keeps_style() { let expected = r#" ``` $ cmd arg ? success foo bar ``` "#; let mut actual = r" ``` $ cmd arg ? success foo bar ``` " .to_owned(); let step = &TryCmd::parse_trycmd(&actual).unwrap().steps[0]; overwrite_trycmd_status( Some(exit_code_to_status(0)), step, &mut step.expected_stdout_source.clone().unwrap(), &mut actual, ) .unwrap(); assert_eq!(expected, actual); } #[cfg(unix)] fn exit_code_to_status(code: u8) -> std::process::ExitStatus { use std::os::unix::process::ExitStatusExt; std::process::ExitStatus::from_raw((code as i32) << 8) } #[cfg(windows)] fn exit_code_to_status(code: u8) -> std::process::ExitStatus { use std::os::windows::process::ExitStatusExt; std::process::ExitStatus::from_raw(code as u32) } #[test] fn exit_code_to_status_works() { assert_eq!(exit_code_to_status(42).code(), Some(42)); } } trycmd-0.14.20/src/spec.rs000064400000000000000000000112411046102023000133600ustar 00000000000000use std::collections::BTreeMap; #[derive(Debug)] pub(crate) struct RunnerSpec { cases: Vec, include: Option>, default_bin: Option, timeout: Option, env: crate::schema::Env, } impl RunnerSpec { pub(crate) fn new() -> Self { Self { cases: Default::default(), include: None, default_bin: None, timeout: Default::default(), env: Default::default(), } } pub(crate) fn case( &mut self, glob: &std::path::Path, #[cfg_attr(miri, allow(unused_variables))] expected: Option, ) { self.cases.push(CaseSpec { glob: glob.into(), #[cfg(not(miri))] expected, #[cfg(miri)] expected: Some(crate::schema::CommandStatus::Skipped), }); } pub(crate) fn include(&mut self, include: Option>) { self.include = include; } pub(crate) fn default_bin(&mut self, bin: Option) { self.default_bin = bin; } pub(crate) fn timeout(&mut self, time: Option) { self.timeout = time; } pub(crate) fn env(&mut self, key: impl Into, value: impl Into) { self.env.add.insert(key.into(), value.into()); } pub(crate) fn prepare(&mut self) -> crate::Runner { let mut runner = crate::Runner::new(); // Both sort and let the last writer win to allow overriding specific cases within a glob let mut cases: BTreeMap = BTreeMap::new(); for spec in &self.cases { if let Some(glob) = get_glob(&spec.glob) { match ::glob::glob(glob) { Ok(paths) => { for path in paths { match path { Ok(path) => { cases.insert( path.clone(), crate::Case { path, expected: spec.expected, default_bin: self.default_bin.clone(), timeout: self.timeout, env: self.env.clone(), error: None, }, ); } Err(err) => { let path = err.path().to_owned(); let err = crate::Error::new(err.into_error().to_string()); cases.insert(path.clone(), crate::Case::with_error(path, err)); } } } } Err(err) => { let err = crate::Error::new(err.to_string()); cases.insert( spec.glob.clone(), crate::Case::with_error(spec.glob.clone(), err), ); } } } else { let path = spec.glob.as_path(); cases.insert( path.into(), crate::Case { path: path.into(), expected: spec.expected, default_bin: self.default_bin.clone(), timeout: self.timeout, env: self.env.clone(), error: None, }, ); } } for case in cases.into_values() { if self.is_included(&case) { runner.case(case); } } runner } fn is_included(&self, case: &crate::Case) -> bool { if let Some(include) = self.include.as_deref() { include .iter() .any(|i| case.path.to_string_lossy().contains(i)) } else { true } } } impl Default for RunnerSpec { fn default() -> Self { Self::new() } } #[derive(Debug)] struct CaseSpec { glob: std::path::PathBuf, expected: Option, } fn get_glob(path: &std::path::Path) -> Option<&str> { if let Some(utf8) = path.to_str() { if utf8.contains('*') { return Some(utf8); } } None }