diff --git a/Cargo.lock b/Cargo.lock index f372b4a5..1bc2a94c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,57 +40,59 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" @@ -100,9 +102,9 @@ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -133,9 +135,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.92" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -163,9 +168,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" dependencies = [ "chrono", "chrono-tz-build", @@ -174,9 +179,9 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" dependencies = [ "parse-zoneinfo", "phf_codegen", @@ -184,18 +189,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.35" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" dependencies = [ "anstream", "anstyle", @@ -231,9 +236,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -283,7 +288,7 @@ dependencies = [ "crossterm_winapi", "mio", "parking_lot", - "rustix 0.38.40", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -301,7 +306,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix 1.0.0", + "rustix 1.0.5", "signal-hook", "signal-hook-mio", "winapi", @@ -318,9 +323,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -330,18 +335,34 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" + [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -349,9 +370,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -363,9 +384,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -374,9 +395,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -450,11 +471,26 @@ dependencies = [ "litrs", ] +[[package]] +name = "dtor" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + [[package]] name = "either" -version = "1.10.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -470,9 +506,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -480,15 +516,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -508,9 +544,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -519,21 +555,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" @@ -554,9 +590,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" @@ -566,16 +602,17 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -601,13 +638,12 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "instability" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac48900be4ab1c0dd6f1c2553d86ef371eda69c52b97ffd22af3e4f0a1771eb8" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" dependencies = [ "darling", "indoc", - "pretty_assertions", "proc-macro2", "quote", "syn", @@ -615,15 +651,21 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -635,16 +677,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -672,15 +715,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litrs" @@ -700,9 +743,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -715,15 +758,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -769,9 +812,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -802,17 +845,17 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "os_display" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" dependencies = [ - "unicode-width 0.1.14", + "unicode-width 0.2.0", ] [[package]] @@ -874,9 +917,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand 0.8.5", @@ -905,9 +948,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] [[package]] name = "pretty_assertions" @@ -935,9 +981,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -953,7 +999,7 @@ dependencies = [ "flate2", "hex", "procfs-core", - "rustix 0.38.40", + "rustix 0.38.44", ] [[package]] @@ -975,6 +1021,7 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", + "ctor", "libc", "phf", "phf_codegen", @@ -1002,18 +1049,25 @@ dependencies = [ "uu_w", "uu_watch", "uucore", + "uutests", "xattr", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -1030,8 +1084,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha", - "rand_core 0.9.0", - "zerocopy 0.8.14", + "rand_core 0.9.3", + "zerocopy 0.8.24", ] [[package]] @@ -1041,7 +1095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1052,12 +1106,11 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", - "zerocopy 0.8.14", + "getrandom 0.3.2", ] [[package]] @@ -1083,20 +1136,20 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -1115,9 +1168,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1141,47 +1194,47 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1200,24 +1253,30 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -1256,9 +1315,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" @@ -1268,9 +1327,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1312,9 +1371,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1341,9 +1400,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", - "rustix 1.0.0", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1360,11 +1419,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 0.38.40", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1422,9 +1481,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -1439,15 +1498,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -1455,9 +1514,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -1496,9 +1555,9 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utmp-classic" @@ -1734,6 +1793,26 @@ version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" +[[package]] +name = "uutests" +version = "0.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f33bc1f552cd82939d3e07867b118ed7ef7bc0fef04b330e1ac69f98593cb22" +dependencies = [ + "ctor", + "glob", + "libc", + "nix", + "pretty_assertions", + "rand 0.9.0", + "regex", + "rlimit", + "tempfile", + "time", + "uucore", + "xattr", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1752,32 +1831,33 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1786,9 +1866,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1796,9 +1876,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1809,9 +1889,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wild" @@ -1840,11 +1923,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1885,15 +1968,6 @@ dependencies = [ "windows-core 0.61.0", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -2166,9 +2240,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -2180,7 +2254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix 1.0.0", + "rustix 1.0.5", ] [[package]] @@ -2201,11 +2275,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.14" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive 0.8.24", ] [[package]] @@ -2221,9 +2295,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7f1e7373..c1fc1dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,8 @@ tempfile = "3.10.1" textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "2.0.4" uucore = "0.0.30" +uutests = "0.0.30" +ctor = "0.4.1" walkdir = "2.5.0" windows = { version = "0.61.1" } windows-sys = { version = "0.59.0", default-features = false } @@ -105,6 +107,8 @@ pretty_assertions = "1.4.0" rand = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } +ctor = { workspace = true } +uutests = { workspace = true } uucore = { workspace = true, features = ["entries", "process", "signals"] } [target.'cfg(unix)'.dev-dependencies] diff --git a/tests/by-util/test_free.rs b/tests/by-util/test_free.rs index 52638356..bac19a18 100644 --- a/tests/by-util/test_free.rs +++ b/tests/by-util/test_free.rs @@ -6,7 +6,9 @@ use pretty_assertions::assert_eq; use regex::Regex; -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; // TODO: make tests combineable (e.g. test --total --human) diff --git a/tests/by-util/test_pgrep.rs b/tests/by-util/test_pgrep.rs index 376eed79..274e1dde 100644 --- a/tests/by-util/test_pgrep.rs +++ b/tests/by-util/test_pgrep.rs @@ -9,9 +9,11 @@ use std::{ process::{Child, Command}, }; -use crate::common::util::TestScenario; #[cfg(target_os = "linux")] use regex::Regex; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[cfg(target_os = "linux")] const SINGLE_PID: &str = "^[1-9][0-9]*"; diff --git a/tests/by-util/test_pidof.rs b/tests/by-util/test_pidof.rs index 44a314db..9c62c0d1 100644 --- a/tests/by-util/test_pidof.rs +++ b/tests/by-util/test_pidof.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_no_args() { diff --git a/tests/by-util/test_pidwait.rs b/tests/by-util/test_pidwait.rs index 6791edab..7838715e 100644 --- a/tests/by-util/test_pidwait.rs +++ b/tests/by-util/test_pidwait.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs index f9686ddd..4828e907 100644 --- a/tests/by-util/test_pkill.rs +++ b/tests/by-util/test_pkill.rs @@ -4,7 +4,11 @@ // file that was distributed with this source code. #[cfg(unix)] -use crate::common::util::TestScenario; +use uutests::new_ucmd; +#[cfg(unix)] +use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util_name; #[cfg(unix)] #[test] diff --git a/tests/by-util/test_pmap.rs b/tests/by-util/test_pmap.rs index 032cb992..e899ae0c 100644 --- a/tests/by-util/test_pmap.rs +++ b/tests/by-util/test_pmap.rs @@ -3,11 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; #[cfg(target_os = "linux")] use regex::Regex; #[cfg(target_os = "linux")] use std::process; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; const NON_EXISTING_PID: &str = "999999"; #[cfg(target_os = "linux")] diff --git a/tests/by-util/test_ps.rs b/tests/by-util/test_ps.rs index ab82612a..32f07980 100644 --- a/tests/by-util/test_ps.rs +++ b/tests/by-util/test_ps.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] #[cfg(target_os = "linux")] diff --git a/tests/by-util/test_pwdx.rs b/tests/by-util/test_pwdx.rs index 83d72700..ac83de77 100644 --- a/tests/by-util/test_pwdx.rs +++ b/tests/by-util/test_pwdx.rs @@ -7,7 +7,9 @@ use std::process; use regex::Regex; -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_no_args() { diff --git a/tests/by-util/test_slabtop.rs b/tests/by-util/test_slabtop.rs index e287375d..670d7599 100644 --- a/tests/by-util/test_slabtop.rs +++ b/tests/by-util/test_slabtop.rs @@ -4,8 +4,11 @@ // file that was distributed with this source code. #[cfg(target_os = "linux")] -use crate::common::util::run_ucmd_as_root; -use crate::common::util::TestScenario; +use uutests::util::run_ucmd_as_root; + +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_snice.rs b/tests/by-util/test_snice.rs index 1cf015ed..3c06c94d 100644 --- a/tests/by-util/test_snice.rs +++ b/tests/by-util/test_snice.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_no_args() { diff --git a/tests/by-util/test_sysctl.rs b/tests/by-util/test_sysctl.rs index c3931bbe..cd060568 100644 --- a/tests/by-util/test_sysctl.rs +++ b/tests/by-util/test_sysctl.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { @@ -12,7 +14,10 @@ fn test_invalid_arg() { #[cfg(target_os = "linux")] mod linux { - use crate::common::util::TestScenario; + + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_get_simple() { @@ -68,7 +73,10 @@ mod linux { #[cfg(not(target_os = "linux"))] mod non_linux { - use crate::common::util::TestScenario; + + use uutests::new_ucmd; + use uutests::util::TestScenario; + use uutests::util_name; #[test] fn test_fails_on_unsupported_platforms() { diff --git a/tests/by-util/test_tload.rs b/tests/by-util/test_tload.rs index 85ca6f40..95934ded 100644 --- a/tests/by-util/test_tload.rs +++ b/tests/by-util/test_tload.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_top.rs b/tests/by-util/test_top.rs index bbf8cdda..4d282091 100644 --- a/tests/by-util/test_top.rs +++ b/tests/by-util/test_top.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_vmstat.rs b/tests/by-util/test_vmstat.rs index 12d4ea73..8bc02447 100644 --- a/tests/by-util/test_vmstat.rs +++ b/tests/by-util/test_vmstat.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_simple() { diff --git a/tests/by-util/test_w.rs b/tests/by-util/test_w.rs index 57cd8c21..e5cc9219 100644 --- a/tests/by-util/test_w.rs +++ b/tests/by-util/test_w.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_watch.rs b/tests/by-util/test_watch.rs index 44926252..8a2238fc 100644 --- a/tests/by-util/test_watch.rs +++ b/tests/by-util/test_watch.rs @@ -3,7 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::common::util::TestScenario; +use uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; // runddl32.exe has no console window, no side effects, // and no arguments are required. diff --git a/tests/common/macros.rs b/tests/common/macros.rs deleted file mode 100644 index e5f23d3d..00000000 --- a/tests/common/macros.rs +++ /dev/null @@ -1,93 +0,0 @@ -// This file is part of the uutils procps package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -/// Platform-independent helper for constructing a `PathBuf` from individual elements -#[macro_export] -macro_rules! path_concat { - ($e:expr, ..$n:expr) => {{ - use std::path::PathBuf; - let n = $n; - let mut pb = PathBuf::new(); - for _ in 0..n { - pb.push($e); - } - pb.to_str().unwrap().to_owned() - }}; - ($($e:expr),*) => {{ - use std::path::PathBuf; - let mut pb = PathBuf::new(); - $( - pb.push($e); - )* - pb.to_str().unwrap().to_owned() - }}; -} - -/// Deduce the name of the test binary from the test filename. -/// -/// e.g.: `tests/by-util/test_cat.rs` -> `cat` -#[macro_export] -macro_rules! util_name { - () => { - module_path!() - .split("_") - .nth(1) - .and_then(|s| s.split("::").next()) - .expect("no test name") - }; -} - -/// Convenience macro for acquiring a [`UCommand`] builder. -/// -/// Returns the following: -/// - a [`UCommand`] builder for invoking the binary to be tested -/// -/// This macro is intended for quick, single-call tests. For more complex tests -/// that require multiple invocations of the tested binary, see [`TestScenario`] -/// -/// [`UCommand`]: crate::tests::common::util::UCommand -/// [`TestScenario]: crate::tests::common::util::TestScenario -#[macro_export] -macro_rules! new_ucmd { - () => { - TestScenario::new(util_name!()).ucmd() - }; -} - -/// Convenience macro for acquiring a [`UCommand`] builder and a test path. -/// -/// Returns a tuple containing the following: -/// - an [`AtPath`] that points to a unique temporary test directory -/// - a [`UCommand`] builder for invoking the binary to be tested -/// -/// This macro is intended for quick, single-call tests. For more complex tests -/// that require multiple invocations of the tested binary, see [`TestScenario`] -/// -/// [`UCommand`]: crate::tests::common::util::UCommand -/// [`AtPath`]: crate::tests::common::util::AtPath -/// [`TestScenario]: crate::tests::common::util::TestScenario -#[macro_export] -macro_rules! at_and_ucmd { - () => {{ - let ts = TestScenario::new(util_name!()); - (ts.fixtures.clone(), ts.ucmd()) - }}; -} - -/// If `common::util::expected_result` returns an error, i.e. the `util` in `$PATH` doesn't -/// include a procps version string or the version is too low, -/// this macro can be used to automatically skip the test and print the reason. -#[macro_export] -macro_rules! unwrap_or_return { - ( $e:expr ) => { - match $e { - Ok(x) => x, - Err(e) => { - println!("test skipped: {}", e); - return; - } - } - }; -} diff --git a/tests/common/mod.rs b/tests/common/mod.rs deleted file mode 100644 index 3e9568ab..00000000 --- a/tests/common/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is part of the uutils procps package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. -#[macro_use] -pub mod macros; -pub mod random; -pub mod util; diff --git a/tests/common/random.rs b/tests/common/random.rs deleted file mode 100644 index 24dec8be..00000000 --- a/tests/common/random.rs +++ /dev/null @@ -1,340 +0,0 @@ -// This file is part of the uutils procps package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use rand::distr::{Distribution, Uniform}; -use rand::{rng, Rng}; - -/// Samples alphanumeric characters `[A-Za-z0-9]` including newline `\n` -/// -/// # Examples -/// -/// ```rust,ignore -/// use rand::{Rng, thread_rng}; -/// -/// let vec = thread_rng() -/// .sample_iter(AlphanumericNewline) -/// .take(10) -/// .collect::>(); -/// println!("Random chars: {}", String::from_utf8(vec).unwrap()); -/// ``` -#[derive(Clone, Copy, Debug)] -pub struct AlphanumericNewline; - -impl AlphanumericNewline { - /// The charset to act upon - const CHARSET: &'static [u8] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\n"; - - /// Generate a random byte from [`Self::CHARSET`] and return it as `u8`. - /// - /// # Arguments - /// - /// * `rng`: A [`rand::Rng`] - /// - /// returns: u8 - fn random(rng: &mut R) -> u8 - where - R: Rng + ?Sized, - { - let idx = rng.random_range(0..Self::CHARSET.len()); - Self::CHARSET[idx] - } -} - -impl Distribution for AlphanumericNewline { - fn sample(&self, rng: &mut R) -> u8 { - Self::random(rng) - } -} - -/// Generate a random string from a [`Distribution`] -/// -/// # Examples -/// -/// ```rust,ignore -/// use crate::common::random::{AlphanumericNewline, RandomString}; -/// use rand::distributions::Alphanumeric; -/// -/// // generates a 100 byte string with characters from AlphanumericNewline -/// let random_string = RandomString::generate(AlphanumericNewline, 100); -/// assert_eq!(100, random_string.len()); -/// -/// // generates a 100 byte string with 10 newline characters not ending with a newline -/// let string = RandomString::generate_with_delimiter(Alphanumeric, b'\n', 10, false, 100); -/// assert_eq!(100, random_string.len()); -/// ``` -pub struct RandomString; - -impl RandomString { - /// Generate a random string from the given [`Distribution`] with the given `length` in bytes. - /// - /// # Arguments - /// - /// * `dist`: A u8 [`Distribution`] - /// * `length`: the length of the resulting string in bytes - /// - /// returns: String - pub fn generate(dist: D, length: usize) -> String - where - D: Distribution, - { - rng() - .sample_iter(dist) - .take(length) - .map(|b| b as char) - .collect() - } - - /// Generate a random string from the [`Distribution`] with the given `length` in bytes. The - /// function takes a `delimiter`, which is randomly distributed in the string, such that exactly - /// `num_delimiter` amount of `delimiter`s occur. If `end_with_delimiter` is set, then the - /// string ends with the delimiter, else the string does not end with the delimiter. - /// - /// # Arguments - /// - /// * `dist`: A `u8` [`Distribution`] - /// * `delimiter`: A `u8` delimiter, which does not need to be included in the `Distribution` - /// * `num_delimiter`: The number of `delimiter`s contained in the resulting string - /// * `end_with_delimiter`: If the string shall end with the given delimiter - /// * `length`: the length of the resulting string in bytes - /// - /// returns: String - /// - /// # Examples - /// - /// ```rust,ignore - /// use crate::common::random::{AlphanumericNewline, RandomString}; - /// - /// // generates a 100 byte string with 10 '\0' byte characters not ending with a '\0' byte - /// let string = RandomString::generate_with_delimiter(AlphanumericNewline, 0, 10, false, 100); - /// assert_eq!(100, random_string.len()); - /// assert_eq!( - /// 10, - /// random_string.as_bytes().iter().filter(|p| **p == 0).count() - /// ); - /// assert!(!random_string.as_bytes().ends_with(&[0])); - /// ``` - pub fn generate_with_delimiter( - dist: D, - delimiter: u8, - num_delimiter: usize, - end_with_delimiter: bool, - length: usize, - ) -> String - where - D: Distribution, - { - if length == 0 { - return String::new(); - } else if length == 1 { - return if num_delimiter > 0 { - String::from(delimiter as char) - } else { - String::from(rng().sample(&dist) as char) - }; - } - - let samples = length - 1; - let mut result: Vec = rng().sample_iter(&dist).take(samples).collect(); - - if num_delimiter == 0 { - result.push(rng().sample(&dist)); - return String::from_utf8(result).unwrap(); - } - - let num_delimiter = if end_with_delimiter { - num_delimiter - 1 - } else { - num_delimiter - }; - - // safe to unwrap because samples is at least 1, thus high > low - let between = Uniform::new(0, samples).unwrap(); - for _ in 0..num_delimiter { - let mut pos = between.sample(&mut rng()); - let turn = pos; - while result[pos] == delimiter { - pos += 1; - if pos >= samples { - pos = 0; - } - if pos == turn { - break; - } - } - result[pos] = delimiter; - } - - if end_with_delimiter { - result.push(delimiter); - } else { - result.push(rng().sample(&dist)); - } - - String::from_utf8(result).unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::distr::Alphanumeric; - - #[test] - fn test_random_string_generate() { - let random_string = RandomString::generate(AlphanumericNewline, 0); - assert_eq!(0, random_string.len()); - - let random_string = RandomString::generate(AlphanumericNewline, 1); - assert_eq!(1, random_string.len()); - - let random_string = RandomString::generate(AlphanumericNewline, 100); - assert_eq!(100, random_string.len()); - } - - #[test] - fn test_random_string_generate_with_delimiter_when_length_is_zero() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, false, 0); - assert_eq!(0, random_string.len()); - } - - #[test] - fn test_random_string_generate_with_delimiter_when_num_delimiter_is_greater_than_length() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, false, 1); - assert_eq!(1, random_string.len()); - assert!(random_string.as_bytes().contains(&0)); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_random_string_generate_with_delimiter_should_end_with_delimiter() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, true, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 2, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, true, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_random_string_generate_with_delimiter_should_not_end_with_delimiter() { - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, false, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 0, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 0, true, 1); - assert_eq!(1, random_string.len()); - assert_eq!( - 0, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 2); - assert_eq!(2, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 1, false, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 1, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 2, false, 3); - assert_eq!(3, random_string.len()); - assert_eq!( - 2, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - } - - #[test] - fn test_generate_with_delimiter_with_greater_length() { - let random_string = - RandomString::generate_with_delimiter(Alphanumeric, 0, 100, false, 1000); - assert_eq!(1000, random_string.len()); - assert_eq!( - 100, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - - let random_string = RandomString::generate_with_delimiter(Alphanumeric, 0, 100, true, 1000); - assert_eq!(1000, random_string.len()); - assert_eq!( - 100, - random_string.as_bytes().iter().filter(|p| **p == 0).count() - ); - assert!(random_string.as_bytes().ends_with(&[0])); - } - - /// Originally used to exclude an error within the `random` module. The two - /// affected tests timed out on windows, but only in the ci. These tests are - /// also the source for the concrete numbers. The timed out tests are - /// `test_tail.rs::test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size` - /// `test_tail.rs::test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size`. - #[test] - fn test_generate_random_strings_when_length_is_around_critical_buffer_sizes() { - let length = 8192 * 3; - let random_string = RandomString::generate(AlphanumericNewline, length); - assert_eq!(length, random_string.len()); - - let length = 8192 * 3 + 1; - let random_string = - RandomString::generate_with_delimiter(Alphanumeric, b'\n', 100, true, length); - assert_eq!(length, random_string.len()); - assert_eq!( - 100, - random_string - .as_bytes() - .iter() - .filter(|p| **p == b'\n') - .count() - ); - assert!(!random_string.as_bytes().ends_with(&[0])); - } -} diff --git a/tests/common/util.rs b/tests/common/util.rs deleted file mode 100644 index a269d87e..00000000 --- a/tests/common/util.rs +++ /dev/null @@ -1,3416 +0,0 @@ -// This file is part of the uutils procps package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized - -#![allow(dead_code, unexpected_cfgs)] - -use pretty_assertions::assert_eq; -#[cfg(any(target_os = "linux", target_os = "android"))] -use rlimit::prlimit; -#[cfg(unix)] -use std::borrow::Cow; -use std::collections::VecDeque; -#[cfg(not(windows))] -use std::ffi::CString; -use std::ffi::{OsStr, OsString}; -use std::fs::{self, hard_link, remove_file, File, OpenOptions}; -use std::io::{self, BufWriter, Read, Result, Write}; -#[cfg(unix)] -use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt}; -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; -#[cfg(windows)] -use std::os::windows::fs::{symlink_dir, symlink_file}; -#[cfg(windows)] -use std::path::MAIN_SEPARATOR_STR; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, ExitStatus, Output, Stdio}; -use std::rc::Rc; -use std::sync::mpsc::{self, RecvTimeoutError}; -use std::thread::{sleep, JoinHandle}; -use std::time::{Duration, Instant}; -use std::{env, hint, thread}; -use tempfile::{Builder, TempDir}; - -static TESTS_DIR: &str = "tests"; -static FIXTURES_DIR: &str = "fixtures"; - -static ALREADY_RUN: &str = " you have already run this UCommand, if you want to run \ - another command in the same test, use TestScenario::new instead of \ - testing();"; -static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly."; - -static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; - -pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_procps"); -pub const PATH: &str = env!("PATH"); - -/// Default environment variables to run the commands with -const DEFAULT_ENV: [(&str, &str); 2] = [("LC_ALL", "C"), ("TZ", "UTC")]; - -/// Test if the program is running under CI -pub fn is_ci() -> bool { - std::env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) -} - -/// Read a test scenario fixture, returning its bytes -fn read_scenario_fixture>(tmpd: &Option>, file_rel_path: S) -> Vec { - let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path(); - AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap()) -} - -/// A command result is the outputs of a command (streams and status code) -/// within a struct which has convenience assertion functions about those outputs -#[derive(Debug, Clone)] -pub struct CmdResult { - /// `bin_path` provided by `TestScenario` or `UCommand` - bin_path: PathBuf, - /// `util_name` provided by `TestScenario` or `UCommand` - util_name: Option, - //tmpd is used for convenience functions for asserts against fixtures - tmpd: Option>, - /// exit status for command (if there is one) - exit_status: Option, - /// captured standard output after running the Command - stdout: Vec, - /// captured standard error after running the Command - stderr: Vec, -} - -impl CmdResult { - pub fn new( - bin_path: S, - util_name: Option, - tmpd: Option>, - exit_status: Option, - stdout: U, - stderr: V, - ) -> Self - where - S: Into, - T: AsRef, - U: Into>, - V: Into>, - { - Self { - bin_path: bin_path.into(), - util_name: util_name.map(|s| s.as_ref().into()), - tmpd, - exit_status, - stdout: stdout.into(), - stderr: stderr.into(), - } - } - - /// Apply a function to `stdout` as bytes and return a new [`CmdResult`] - pub fn stdout_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a [u8]) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - function(&self.stdout), - self.stderr.as_slice(), - ) - } - - /// Apply a function to `stdout` as `&str` and return a new [`CmdResult`] - pub fn stdout_str_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a str) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - function(self.stdout_str()), - self.stderr.as_slice(), - ) - } - - /// Apply a function to `stderr` as bytes and return a new [`CmdResult`] - pub fn stderr_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a [u8]) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - self.stdout.as_slice(), - function(&self.stderr), - ) - } - - /// Apply a function to `stderr` as `&str` and return a new [`CmdResult`] - pub fn stderr_str_apply<'a, F, R>(&'a self, function: F) -> Self - where - F: Fn(&'a str) -> R, - R: Into>, - { - Self::new( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - self.exit_status, - self.stdout.as_slice(), - function(self.stderr_str()), - ) - } - - /// Assert `stdout` as bytes with a predicate function returning a `bool`. - #[track_caller] - pub fn stdout_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a [u8]) -> bool, - { - assert!( - predicate(&self.stdout), - "Predicate for stdout as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", - &self.stdout, - &self.stderr - ); - self - } - - /// Assert `stdout` as `&str` with a predicate function returning a `bool`. - #[track_caller] - pub fn stdout_str_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a str) -> bool, - { - assert!( - predicate(self.stdout_str()), - "Predicate for stdout as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// Assert `stderr` as bytes with a predicate function returning a `bool`. - #[track_caller] - pub fn stderr_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a [u8]) -> bool, - { - assert!( - predicate(&self.stderr), - "Predicate for stderr as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n", - &self.stdout, - &self.stderr - ); - self - } - - /// Assert `stderr` as `&str` with a predicate function returning a `bool`. - #[track_caller] - pub fn stderr_str_check<'a, F>(&'a self, predicate: F) -> &'a Self - where - F: Fn(&'a str) -> bool, - { - assert!( - predicate(self.stderr_str()), - "Predicate for stderr as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// Return the exit status of the child process, if any. - /// - /// Returns None if the child process is still running or hasn't been started. - pub fn try_exit_status(&self) -> Option { - self.exit_status - } - - /// Return the exit status of the child process. - /// - /// # Panics - /// - /// If the child process is still running or hasn't been started. - pub fn exit_status(&self) -> ExitStatus { - self.try_exit_status() - .expect("Program must be run first or has not finished, yet") - } - - /// Return the signal the child process received if any. - /// - /// # Platform specific behavior - /// - /// This method is only available on unix systems. - #[cfg(unix)] - pub fn signal(&self) -> Option { - self.exit_status().signal() - } - - /// Assert that the given signal `value` equals the signal the child process received. - /// - /// See also [`std::os::unix::process::ExitStatusExt::signal`]. - /// - /// # Platform specific behavior - /// - /// This assertion method is only available on unix systems. - #[cfg(unix)] - #[track_caller] - pub fn signal_is(&self, value: i32) -> &Self { - let actual = self.signal().unwrap_or_else(|| { - panic!( - "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", - value, - self.try_exit_status() - .map_or("Not available".to_string(), |e| e.to_string()) - ) - }); - - assert_eq!(actual, value); - self - } - - /// Assert that the given signal `name` equals the signal the child process received. - /// - /// Strings like `SIGINT`, `INT` or a number like `15` are all valid names. See also - /// [`std::os::unix::process::ExitStatusExt::signal`] and - /// [`uucore::signals::signal_by_name_or_value`] - /// - /// # Platform specific behavior - /// - /// This assertion method is only available on unix systems. - #[cfg(unix)] - #[track_caller] - pub fn signal_name_is(&self, name: &str) -> &Self { - use uucore::signals::signal_by_name_or_value; - let expected: i32 = signal_by_name_or_value(name) - .unwrap_or_else(|| panic!("Invalid signal name or value: '{name}'")) - .try_into() - .unwrap(); - - let actual = self.signal().unwrap_or_else(|| { - panic!( - "Expected process to be terminated by the '{}' signal, but exit status is: '{}'", - name, - self.try_exit_status() - .map_or("Not available".to_string(), |e| e.to_string()) - ) - }); - - assert_eq!(actual, expected); - self - } - - /// Returns a reference to the program's standard output as a slice of bytes - pub fn stdout(&self) -> &[u8] { - &self.stdout - } - - /// Returns the program's standard output as a string slice - pub fn stdout_str(&self) -> &str { - std::str::from_utf8(&self.stdout).unwrap() - } - - /// Returns the program's standard output as a string - /// consumes self - pub fn stdout_move_str(self) -> String { - String::from_utf8(self.stdout).unwrap() - } - - /// Returns the program's standard output as a vec of bytes - /// consumes self - pub fn stdout_move_bytes(self) -> Vec { - self.stdout - } - - /// Returns a reference to the program's standard error as a slice of bytes - pub fn stderr(&self) -> &[u8] { - &self.stderr - } - - /// Returns the program's standard error as a string slice - pub fn stderr_str(&self) -> &str { - std::str::from_utf8(&self.stderr).unwrap() - } - - /// Returns the program's standard error as a string - /// consumes self - pub fn stderr_move_str(self) -> String { - String::from_utf8(self.stderr).unwrap() - } - - /// Returns the program's standard error as a vec of bytes - /// consumes self - pub fn stderr_move_bytes(self) -> Vec { - self.stderr - } - - /// Returns the program's exit code - /// Panics if not run or has not finished yet for example when run with `run_no_wait()` - pub fn code(&self) -> i32 { - self.exit_status().code().unwrap() - } - - #[track_caller] - pub fn code_is(&self, expected_code: i32) -> &Self { - assert_eq!(self.code(), expected_code); - self - } - - /// Returns the program's `TempDir` - /// Panics if not present - pub fn tmpd(&self) -> Rc { - match &self.tmpd { - Some(ptr) => ptr.clone(), - None => panic!("Command not associated with a TempDir"), - } - } - - /// Returns whether the program succeeded - pub fn succeeded(&self) -> bool { - self.exit_status.map_or(true, |e| e.success()) - } - - /// asserts that the command resulted in a success (zero) status code - #[track_caller] - pub fn success(&self) -> &Self { - assert!( - self.succeeded(), - "Command was expected to succeed.\nstdout = {}\n stderr = {}", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in a failure (non-zero) status code - #[track_caller] - pub fn failure(&self) -> &Self { - assert!( - !self.succeeded(), - "Command was expected to fail.\nstdout = {}\n stderr = {}", - self.stdout_str(), - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in empty (zero-length) stderr stream output - /// generally, it's better to use `stdout_only()` instead, - /// but you might find yourself using this function if - /// 1. you can not know exactly what stdout will be or - /// 2. you know that stdout will also be empty - #[track_caller] - pub fn no_stderr(&self) -> &Self { - assert!( - self.stderr.is_empty(), - "Expected stderr to be empty, but it's:\n{}", - self.stderr_str() - ); - self - } - - /// asserts that the command resulted in empty (zero-length) stderr stream output - /// unless asserting there was neither stdout or stderr, `stderr_only` is usually a better choice - /// generally, it's better to use `stderr_only()` instead, - /// but you might find yourself using this function if - /// 1. you can not know exactly what stderr will be or - /// 2. you know that stderr will also be empty - #[track_caller] - pub fn no_stdout(&self) -> &Self { - assert!( - self.stdout.is_empty(), - "Expected stdout to be empty, but it's:\n{}", - self.stdout_str() - ); - self - } - - /// Assert that there is output to neither stderr nor stdout. - #[track_caller] - pub fn no_output(&self) -> &Self { - self.no_stdout().no_stderr() - } - - /// asserts that the command resulted in stdout stream output that equals the - /// passed in value, trailing whitespace are kept to force strict comparison (#1235) - /// `stdout_only()` is a better choice unless stderr may or will be non-empty - #[track_caller] - pub fn stdout_is>(&self, msg: T) -> &Self { - assert_eq!(self.stdout_str(), String::from(msg.as_ref())); - self - } - - /// like `stdout_is`, but succeeds if any elements of `expected` matches stdout. - #[track_caller] - pub fn stdout_is_any + std::fmt::Debug>(&self, expected: &[T]) -> &Self { - assert!( - expected.iter().any(|msg| self.stdout_str() == msg.as_ref()), - "stdout was {}\nExpected any of {:#?}", - self.stdout_str(), - expected - ); - self - } - - /// Like `stdout_is` but newlines are normalized to `\n`. - #[track_caller] - pub fn normalized_newlines_stdout_is>(&self, msg: T) -> &Self { - let msg = msg.as_ref().replace("\r\n", "\n"); - assert_eq!(self.stdout_str().replace("\r\n", "\n"), msg); - self - } - - /// asserts that the command resulted in stdout stream output, - /// whose bytes equal those of the passed in slice - #[track_caller] - pub fn stdout_is_bytes>(&self, msg: T) -> &Self { - assert_eq!(self.stdout, msg.as_ref(), - "stdout as bytes wasn't equal to expected bytes. Result as strings:\nstdout ='{:?}'\nexpected='{:?}'", - std::str::from_utf8(&self.stdout), - std::str::from_utf8(msg.as_ref()), - ); - self - } - - /// like `stdout_is()`, but expects the contents of the file at the provided relative path - #[track_caller] - pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is(String::from_utf8(contents).unwrap()) - } - - /// Assert that the bytes of stdout exactly match those of the given file. - /// - /// Contrast this with [`CmdResult::stdout_is_fixture`], which - /// decodes the contents of the file as a UTF-8 [`String`] before - /// comparison with stdout. - /// - /// # Examples - /// - /// Use this method in a unit test like this: - /// - /// ```rust,ignore - /// #[test] - /// fn test_something() { - /// new_ucmd!().succeeds().stdout_is_fixture_bytes("expected.bin"); - /// } - /// ``` - #[track_caller] - pub fn stdout_is_fixture_bytes>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_is_bytes(contents) - } - - /// like `stdout_is_fixture()`, but replaces the data in fixture file based on values provided in `template_vars` - /// command output - #[track_caller] - pub fn stdout_is_templated_fixture>( - &self, - file_rel_path: T, - template_vars: &[(&str, &str)], - ) -> &Self { - let mut contents = - String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); - for kv in template_vars { - contents = contents.replace(kv.0, kv.1); - } - self.stdout_is(contents) - } - - /// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout. - #[track_caller] - pub fn stdout_is_templated_fixture_any>( - &self, - file_rel_path: T, - template_vars: &[Vec<(String, String)>], - ) { - let contents = String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap(); - let possible_values = template_vars.iter().map(|vars| { - let mut contents = contents.clone(); - for kv in vars { - contents = contents.replace(&kv.0, &kv.1); - } - contents - }); - self.stdout_is_any(&possible_values.collect::>()); - } - - /// assert that the command resulted in stderr stream output that equals the - /// passed in value. - /// - /// `stderr_only` is a better choice unless stdout may or will be non-empty - #[track_caller] - pub fn stderr_is>(&self, msg: T) -> &Self { - assert_eq!(self.stderr_str(), msg.as_ref()); - self - } - - /// asserts that the command resulted in stderr stream output, - /// whose bytes equal those of the passed in slice - #[track_caller] - pub fn stderr_is_bytes>(&self, msg: T) -> &Self { - assert_eq!( - &self.stderr, - msg.as_ref(), - "stderr as bytes wasn't equal to expected bytes. Result as strings:\nstderr ='{:?}'\nexpected='{:?}'", - std::str::from_utf8(&self.stderr), - std::str::from_utf8(msg.as_ref()) - ); - self - } - - /// Like `stdout_is_fixture`, but for stderr - #[track_caller] - pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stderr_is(String::from_utf8(contents).unwrap()) - } - - /// asserts that - /// 1. the command resulted in stdout stream output that equals the - /// passed in value - /// 2. the command resulted in empty (zero-length) stderr stream output - #[track_caller] - pub fn stdout_only>(&self, msg: T) -> &Self { - self.no_stderr().stdout_is(msg) - } - - /// asserts that - /// 1. the command resulted in a stdout stream whose bytes - /// equal those of the passed in value - /// 2. the command resulted in an empty stderr stream - #[track_caller] - pub fn stdout_only_bytes>(&self, msg: T) -> &Self { - self.no_stderr().stdout_is_bytes(msg) - } - - /// like `stdout_only()`, but expects the contents of the file at the provided relative path - #[track_caller] - pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.stdout_only_bytes(contents) - } - - /// asserts that - /// 1. the command resulted in stderr stream output that equals the - /// passed in value - /// 2. the command resulted in empty (zero-length) stdout stream output - #[track_caller] - pub fn stderr_only>(&self, msg: T) -> &Self { - self.no_stdout().stderr_is(msg) - } - - /// asserts that - /// 1. the command resulted in a stderr stream whose bytes equal the ones - /// of the passed value - /// 2. the command resulted in an empty stdout stream - #[track_caller] - pub fn stderr_only_bytes>(&self, msg: T) -> &Self { - self.no_stdout().stderr_is_bytes(msg) - } - - #[track_caller] - pub fn fails_silently(&self) -> &Self { - assert!(!self.succeeded()); - assert!(self.stderr.is_empty()); - self - } - - /// asserts that - /// 1. the command resulted in stderr stream output that equals the - /// the following format - /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` - /// This the expected format when a `UUsageError` is returned or when `show_error!` is called - /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` - /// - /// 2. the command resulted in empty (zero-length) stdout stream output - #[track_caller] - pub fn usage_error>(&self, msg: T) -> &Self { - self.stderr_only(format!( - "{0}: {2}\nTry '{1} {0} --help' for more information.\n", - self.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command - self.bin_path.display(), - msg.as_ref() - )) - } - - #[track_caller] - pub fn stdout_contains>(&self, cmp: T) -> &Self { - assert!( - self.stdout_str().contains(cmp.as_ref()), - "'{}' does not contain '{}'", - self.stdout_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stdout_contains_line>(&self, cmp: T) -> &Self { - assert!( - self.stdout_str().lines().any(|line| line == cmp.as_ref()), - "'{}' does not contain line '{}'", - self.stdout_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stderr_contains>(&self, cmp: T) -> &Self { - assert!( - self.stderr_str().contains(cmp.as_ref()), - "'{}' does not contain '{}'", - self.stderr_str(), - cmp.as_ref() - ); - self - } - - #[track_caller] - pub fn stdout_does_not_contain>(&self, cmp: T) -> &Self { - assert!( - !self.stdout_str().contains(cmp.as_ref()), - "'{}' contains '{}' but should not", - self.stdout_str(), - cmp.as_ref(), - ); - self - } - - #[track_caller] - pub fn stderr_does_not_contain>(&self, cmp: T) -> &Self { - assert!(!self.stderr_str().contains(cmp.as_ref())); - self - } - - #[track_caller] - pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self { - assert!( - regex.is_match(self.stdout_str()), - "Stdout does not match regex:\n{}", - self.stdout_str() - ); - self - } - - #[track_caller] - pub fn stderr_matches(&self, regex: ®ex::Regex) -> &Self { - assert!( - regex.is_match(self.stderr_str()), - "Stderr does not match regex:\n{}", - self.stderr_str() - ); - self - } - - #[track_caller] - pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self { - assert!( - !regex.is_match(self.stdout_str()), - "Stdout matches regex:\n{}", - self.stdout_str() - ); - self - } -} - -pub fn log_info, U: AsRef>(msg: T, par: U) { - println!("{}: {}", msg.as_ref(), par.as_ref()); -} - -pub fn recursive_copy(src: &Path, dest: &Path) -> Result<()> { - if fs::metadata(src)?.is_dir() { - for entry in fs::read_dir(src)? { - let entry = entry?; - let mut new_dest = PathBuf::from(dest); - new_dest.push(entry.file_name()); - if fs::metadata(entry.path())?.is_dir() { - fs::create_dir(&new_dest)?; - recursive_copy(&entry.path(), &new_dest)?; - } else { - fs::copy(entry.path(), new_dest)?; - } - } - } - Ok(()) -} - -pub fn get_root_path() -> &'static str { - if cfg!(windows) { - "C:\\" - } else { - "/" - } -} - -/// Compares the extended attributes (xattrs) of two files or directories. -/// -/// # Returns -/// -/// `true` if both paths have the same set of extended attributes, `false` otherwise. -#[cfg(all(unix, not(target_os = "macos")))] -pub fn compare_xattrs>(path1: P, path2: P) -> bool { - let get_sorted_xattrs = |path: P| { - xattr::list(path) - .map(|attrs| { - let mut attrs = attrs.collect::>(); - attrs.sort(); - attrs - }) - .unwrap_or_else(|_| Vec::new()) - }; - - get_sorted_xattrs(path1) == get_sorted_xattrs(path2) -} - -/// Object-oriented path struct that represents and operates on -/// paths relative to the directory it was constructed for. -#[derive(Clone)] -pub struct AtPath { - pub subdir: PathBuf, -} - -impl AtPath { - pub fn new(subdir: &Path) -> Self { - Self { - subdir: PathBuf::from(subdir), - } - } - - pub fn as_string(&self) -> String { - self.subdir.to_str().unwrap().to_owned() - } - - pub fn plus>(&self, name: P) -> PathBuf { - let mut pathbuf = self.subdir.clone(); - pathbuf.push(name); - pathbuf - } - - pub fn plus_as_string>(&self, name: P) -> String { - self.plus(name).display().to_string() - } - - fn minus(&self, name: &str) -> PathBuf { - let prefixed = PathBuf::from(name); - if prefixed.starts_with(&self.subdir) { - let mut unprefixed = PathBuf::new(); - for component in prefixed.components().skip(self.subdir.components().count()) { - unprefixed.push(component.as_os_str().to_str().unwrap()); - } - unprefixed - } else { - prefixed - } - } - - pub fn minus_as_string(&self, name: &str) -> String { - String::from(self.minus(name).to_str().unwrap()) - } - - pub fn set_readonly(&self, name: &str) { - let metadata = fs::metadata(self.plus(name)).unwrap(); - let mut permissions = metadata.permissions(); - permissions.set_readonly(true); - fs::set_permissions(self.plus(name), permissions).unwrap(); - } - - pub fn open(&self, name: &str) -> File { - log_info("open", self.plus_as_string(name)); - File::open(self.plus(name)).unwrap() - } - - pub fn read(&self, name: &str) -> String { - let mut f = self.open(name); - let mut contents = String::new(); - f.read_to_string(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); - contents - } - - pub fn read_bytes(&self, name: &str) -> Vec { - let mut f = self.open(name); - let mut contents = Vec::new(); - f.read_to_end(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); - contents - } - - pub fn write(&self, name: &str, contents: &str) { - log_info("write(default)", self.plus_as_string(name)); - std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); - } - - pub fn write_bytes(&self, name: &str, contents: &[u8]) { - log_info("write(default)", self.plus_as_string(name)); - std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); - } - - pub fn append(&self, name: &str, contents: &str) { - log_info("write(append)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .append(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(append) {name}: {e}")); - } - - pub fn append_bytes(&self, name: &str, contents: &[u8]) { - log_info("write(append)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .append(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents) - .unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}")); - } - - pub fn truncate(&self, name: &str, contents: &str) { - log_info("write(truncate)", self.plus_as_string(name)); - let mut f = OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(self.plus(name)) - .unwrap(); - f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}")); - } - - pub fn rename(&self, source: &str, target: &str) { - let source = self.plus(source); - let target = self.plus(target); - log_info("rename", format!("{source:?} {target:?}")); - std::fs::rename(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}")); - } - - pub fn remove(&self, source: &str) { - let source = self.plus(source); - log_info("remove", format!("{source:?}")); - std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}")); - } - - pub fn copy(&self, source: &str, target: &str) { - let source = self.plus(source); - let target = self.plus(target); - log_info("copy", format!("{source:?} {target:?}")); - std::fs::copy(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}")); - } - - pub fn rmdir(&self, dir: &str) { - log_info("rmdir", self.plus_as_string(dir)); - fs::remove_dir(self.plus(dir)).unwrap(); - } - - pub fn mkdir>(&self, dir: P) { - let dir = dir.as_ref(); - log_info("mkdir", self.plus_as_string(dir)); - fs::create_dir(self.plus(dir)).unwrap(); - } - - pub fn mkdir_all(&self, dir: &str) { - log_info("mkdir_all", self.plus_as_string(dir)); - fs::create_dir_all(self.plus(dir)).unwrap(); - } - - pub fn make_file(&self, name: &str) -> File { - match File::create(self.plus(name)) { - Ok(f) => f, - Err(e) => panic!("{}", e), - } - } - - pub fn touch>(&self, file: P) { - let file = file.as_ref(); - log_info("touch", self.plus_as_string(file)); - File::create(self.plus(file)).unwrap(); - } - - #[cfg(not(windows))] - pub fn mkfifo(&self, fifo: &str) { - let full_path = self.plus_as_string(fifo); - log_info("mkfifo", &full_path); - unsafe { - let fifo_name: CString = CString::new(full_path).expect("CString creation failed."); - libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR); - } - } - - #[cfg(not(windows))] - pub fn is_fifo(&self, fifo: &str) -> bool { - unsafe { - let name = CString::new(self.plus_as_string(fifo)).unwrap(); - let mut stat: libc::stat = std::mem::zeroed(); - if libc::stat(name.as_ptr(), &mut stat) >= 0 { - libc::S_IFIFO & stat.st_mode as libc::mode_t != 0 - } else { - false - } - } - } - - pub fn hard_link(&self, original: &str, link: &str) { - log_info( - "hard_link", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - hard_link(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn symlink_file(&self, original: &str, link: &str) { - log_info( - "symlink", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - symlink_file(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn relative_symlink_file(&self, original: &str, link: &str) { - #[cfg(windows)] - let original = original.replace('/', MAIN_SEPARATOR_STR); - log_info( - "symlink", - format!("{},{}", &original, &self.plus_as_string(link)), - ); - symlink_file(original, self.plus(link)).unwrap(); - } - - pub fn symlink_dir(&self, original: &str, link: &str) { - log_info( - "symlink", - format!( - "{},{}", - self.plus_as_string(original), - self.plus_as_string(link) - ), - ); - symlink_dir(self.plus(original), self.plus(link)).unwrap(); - } - - pub fn relative_symlink_dir(&self, original: &str, link: &str) { - #[cfg(windows)] - let original = original.replace('/', MAIN_SEPARATOR_STR); - log_info( - "symlink", - format!("{},{}", &original, &self.plus_as_string(link)), - ); - symlink_dir(original, self.plus(link)).unwrap(); - } - - pub fn is_symlink(&self, path: &str) -> bool { - log_info("is_symlink", self.plus_as_string(path)); - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m.file_type().is_symlink(), - Err(_) => false, - } - } - - pub fn resolve_link(&self, path: &str) -> String { - log_info("resolve_link", self.plus_as_string(path)); - match fs::read_link(self.plus(path)) { - Ok(p) => self.minus_as_string(p.to_str().unwrap()), - Err(_) => String::new(), - } - } - - pub fn read_symlink(&self, path: &str) -> String { - log_info("read_symlink", self.plus_as_string(path)); - fs::read_link(self.plus(path)) - .unwrap() - .to_str() - .unwrap() - .to_owned() - } - - pub fn symlink_metadata(&self, path: &str) -> fs::Metadata { - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m, - Err(e) => panic!("{}", e), - } - } - - pub fn metadata(&self, path: &str) -> fs::Metadata { - match fs::metadata(self.plus(path)) { - Ok(m) => m, - Err(e) => panic!("{}", e), - } - } - - pub fn file_exists>(&self, path: P) -> bool { - match fs::metadata(self.plus(path)) { - Ok(m) => m.is_file(), - Err(_) => false, - } - } - - /// Decide whether the named symbolic link exists in the test directory. - pub fn symlink_exists(&self, path: &str) -> bool { - match fs::symlink_metadata(self.plus(path)) { - Ok(m) => m.file_type().is_symlink(), - Err(_) => false, - } - } - - pub fn dir_exists(&self, path: &str) -> bool { - match fs::metadata(self.plus(path)) { - Ok(m) => m.is_dir(), - Err(_) => false, - } - } - - pub fn root_dir_resolved(&self) -> String { - log_info("current_directory_resolved", ""); - let s = self - .subdir - .canonicalize() - .unwrap() - .to_str() - .unwrap() - .to_owned(); - - // Due to canonicalize()'s use of GetFinalPathNameByHandleW() on Windows, the resolved path - // starts with '\\?\' to extend the limit of a given path to 32,767 wide characters. - // - // To address this issue, we remove this prepended string if available. - // - // Source: - // http://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-without-prepended - let prefix = "\\\\?\\"; - - if let Some(stripped) = s.strip_prefix(prefix) { - String::from(stripped) - } else { - s - } - } - - /// Set the permissions of the specified file. - /// - /// # Panics - /// - /// This function panics if there is an error loading the metadata - /// or setting the permissions of the file. - #[cfg(not(windows))] - pub fn set_mode(&self, filename: &str, mode: u32) { - let path = self.plus(filename); - let mut perms = std::fs::metadata(&path).unwrap().permissions(); - perms.set_mode(mode); - std::fs::set_permissions(&path, perms).unwrap(); - } -} - -/// An environment for running a single uutils test case, serves three functions: -/// 1. centralizes logic for locating the uutils binary and calling the utility -/// 2. provides a unique temporary directory for the test case -/// 3. copies over fixtures for the utility to the temporary directory -/// -/// Fixtures can be found under `tests/fixtures/$util_name/` -pub struct TestScenario { - pub bin_path: PathBuf, - pub util_name: String, - pub fixtures: AtPath, - tmpd: Rc, -} - -impl TestScenario { - pub fn new(util_name: T) -> Self - where - T: AsRef, - { - let tmpd = Rc::new(TempDir::new().unwrap()); - let ts = Self { - bin_path: PathBuf::from(TESTS_BINARY), - util_name: util_name.as_ref().into(), - fixtures: AtPath::new(tmpd.as_ref().path()), - tmpd, - }; - let mut fixture_path_builder = env::current_dir().unwrap(); - fixture_path_builder.push(TESTS_DIR); - fixture_path_builder.push(FIXTURES_DIR); - fixture_path_builder.push(util_name.as_ref()); - if let Ok(m) = fs::metadata(&fixture_path_builder) { - if m.is_dir() { - recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap(); - } - } - ts - } - - /// Returns builder for invoking the target uutils binary. Paths given are - /// treated relative to the environment's unique temporary test directory. - pub fn ucmd(&self) -> UCommand { - UCommand::from_test_scenario(self) - } - - /// Returns builder for invoking any system command. Paths given are treated - /// relative to the environment's unique temporary test directory. - pub fn cmd>(&self, bin_path: S) -> UCommand { - let mut command = UCommand::new(); - command.bin_path(bin_path); - command.temp_dir(self.tmpd.clone()); - command - } - - /// Returns builder for invoking any uutils command. Paths given are treated - /// relative to the environment's unique temporary test directory. - pub fn ccmd>(&self, util_name: S) -> UCommand { - UCommand::with_util(util_name, self.tmpd.clone()) - } -} - -/// A `UCommand` is a builder wrapping an individual Command that provides several additional features: -/// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command -/// and asserting on the results. -/// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops -/// the test failure can display the exact call which preceded an assertion failure. -/// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory. -/// -/// Per default `UCommand` runs a command given as an argument in a shell, platform independently. -/// It does so with safety in mind, so the working directory is set to an individual temporary -/// directory and the environment variables are cleared per default. -/// -/// The default behavior can be changed with builder methods: -/// * [`UCommand::with_util`]: Run `procps UTIL_NAME` instead of the shell -/// * [`UCommand::from_test_scenario`]: Run `procps UTIL_NAME` instead of the shell in the -/// temporary directory of the [`TestScenario`] -/// * [`UCommand::current_dir`]: Sets the working directory -/// * ... -#[derive(Debug, Default)] -pub struct UCommand { - args: VecDeque, - env_vars: Vec<(OsString, OsString)>, - current_dir: Option, - bin_path: Option, - util_name: Option, - has_run: bool, - ignore_stdin_write_error: bool, - stdin: Option, - stdout: Option, - stderr: Option, - bytes_into_stdin: Option>, - #[cfg(any(target_os = "linux", target_os = "android"))] - limits: Vec<(rlimit::Resource, u64, u64)>, - stderr_to_stdout: bool, - timeout: Option, - tmpd: Option>, // drop last -} - -impl UCommand { - /// Create a new plain [`UCommand`]. - /// - /// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a - /// shell (`sh -c` on unix platforms or `cmd /C` on windows). - /// - /// Per default the environment is cleared and the working directory is set to an individual - /// temporary directory for safety purposes. - pub fn new() -> Self { - Self { - ..Default::default() - } - } - - /// Create a [`UCommand`] for a specific uutils utility. - /// - /// Sets the temporary directory to `tmpd` and the execution binary to the path where - /// `procps` is found. - pub fn with_util(util_name: T, tmpd: Rc) -> Self - where - T: AsRef, - { - let mut ucmd = Self::new(); - ucmd.util_name = Some(util_name.as_ref().into()); - ucmd.bin_path(TESTS_BINARY).temp_dir(tmpd); - ucmd - } - - /// Create a [`UCommand`] from a [`TestScenario`]. - /// - /// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the - /// execution binary is set to `procps`. - pub fn from_test_scenario(scene: &TestScenario) -> Self { - Self::with_util(&scene.util_name, scene.tmpd.clone()) - } - - /// Set the execution binary. - /// - /// Make sure the binary found at this path is executable. It's safest to provide the - /// canonicalized path instead of just the name of the executable, since path resolution is not - /// guaranteed to work on all platforms. - fn bin_path(&mut self, bin_path: T) -> &mut Self - where - T: Into, - { - self.bin_path = Some(bin_path.into()); - self - } - - /// Set the temporary directory. - /// - /// Per default an individual temporary directory is created for every [`UCommand`]. If not - /// specified otherwise with [`UCommand::current_dir`] the working directory is set to this - /// temporary directory. - fn temp_dir(&mut self, temp_dir: Rc) -> &mut Self { - self.tmpd = Some(temp_dir); - self - } - - /// Set the working directory for this [`UCommand`] - /// - /// Per default the working directory is set to the [`UCommands`] temporary directory. - pub fn current_dir(&mut self, current_dir: T) -> &mut Self - where - T: Into, - { - self.current_dir = Some(current_dir.into()); - self - } - - pub fn set_stdin>(&mut self, stdin: T) -> &mut Self { - self.stdin = Some(stdin.into()); - self - } - - pub fn set_stdout>(&mut self, stdout: T) -> &mut Self { - self.stdout = Some(stdout.into()); - self - } - - pub fn set_stderr>(&mut self, stderr: T) -> &mut Self { - self.stderr = Some(stderr.into()); - self - } - - pub fn stderr_to_stdout(&mut self) -> &mut Self { - self.stderr_to_stdout = true; - self - } - - /// Add a parameter to the invocation. Path arguments are treated relative - /// to the test environment directory. - pub fn arg>(&mut self, arg: S) -> &mut Self { - self.args.push_back(arg.as_ref().into()); - self - } - - /// Add multiple parameters to the invocation. Path arguments are treated relative - /// to the test environment directory. - pub fn args>(&mut self, args: &[S]) -> &mut Self { - self.args.extend(args.iter().map(|s| s.as_ref().into())); - self - } - - /// provides standard input to feed in to the command when spawned - pub fn pipe_in>>(&mut self, input: T) -> &mut Self { - assert!( - self.bytes_into_stdin.is_none(), - "{}", - MULTIPLE_STDIN_MEANINGLESS - ); - self.set_stdin(Stdio::piped()); - self.bytes_into_stdin = Some(input.into()); - self - } - - /// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data - pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut Self { - let contents = read_scenario_fixture(&self.tmpd, file_rel_path); - self.pipe_in(contents) - } - - /// Ignores error caused by feeding stdin to the command. - /// This is typically useful to test non-standard workflows - /// like feeding something to a command that does not read it - pub fn ignore_stdin_write_error(&mut self) -> &mut Self { - self.ignore_stdin_write_error = true; - self - } - - pub fn env(&mut self, key: K, val: V) -> &mut Self - where - K: AsRef, - V: AsRef, - { - self.env_vars - .push((key.as_ref().into(), val.as_ref().into())); - self - } - - pub fn envs(&mut self, iter: I) -> &mut Self - where - I: IntoIterator, - K: AsRef, - V: AsRef, - { - for (k, v) in iter { - self.env(k, v); - } - self - } - - #[cfg(any(target_os = "linux", target_os = "android"))] - pub fn limit( - &mut self, - resource: rlimit::Resource, - soft_limit: u64, - hard_limit: u64, - ) -> &mut Self { - self.limits.push((resource, soft_limit, hard_limit)); - self - } - - /// Set the timeout for [`UCommand::run`] and similar methods in [`UCommand`]. - /// - /// After the timeout elapsed these `run` methods (besides [`UCommand::run_no_wait`]) will - /// panic. When [`UCommand::run_no_wait`] is used, this timeout is applied to - /// [`UChild::wait_with_output`] including all other waiting methods in [`UChild`] implicitly - /// using `wait_with_output()` and additionally [`UChild::kill`]. The default timeout of `kill` - /// will be overwritten by this `timeout`. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - self.timeout = Some(timeout); - self - } - - /// Build the `std::process::Command` and apply the defaults on fields which were not specified - /// by the user. - /// - /// These __defaults__ are: - /// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.). - /// This default also requires to set the first argument to `-c` on unix (`/C` on windows) if - /// this argument wasn't specified explicitly by the user. - /// * `util_name`: `None`. If neither `bin_path` nor `util_name` were given the arguments are - /// run in a shell (See `bin_path` above). - /// * `temp_dir`: If `current_dir` was not set, a new temporary directory will be created in - /// which this command will be run and `current_dir` will be set to this `temp_dir`. - /// * `current_dir`: The temporary directory given by `temp_dir`. - /// * `timeout`: `30 seconds` - /// * `stdin`: `Stdio::null()` - /// * `ignore_stdin_write_error`: `false` - /// * `stdout`, `stderr`: If not specified the output will be captured with [`CapturedOutput`] - /// * `stderr_to_stdout`: `false` - /// * `bytes_into_stdin`: `None` - /// * `limits`: `None`. - fn build(&mut self) -> (Command, Option, Option) { - if self.bin_path.is_some() { - if let Some(util_name) = &self.util_name { - self.args.push_front(util_name.into()); - } - } else if let Some(util_name) = &self.util_name { - self.bin_path = Some(PathBuf::from(TESTS_BINARY)); - self.args.push_front(util_name.into()); - // neither `bin_path` nor `util_name` was set so we apply the default to run the arguments - // in a platform specific shell - } else if cfg!(unix) { - #[cfg(target_os = "android")] - let bin_path = PathBuf::from("/system/bin/sh"); - #[cfg(not(target_os = "android"))] - let bin_path = PathBuf::from("/bin/sh"); - - self.bin_path = Some(bin_path); - let c_arg = OsString::from("-c"); - if !self.args.contains(&c_arg) { - self.args.push_front(c_arg); - } - } else { - self.bin_path = Some(PathBuf::from("cmd")); - let c_arg = OsString::from("/C"); - let k_arg = OsString::from("/K"); - if !self - .args - .iter() - .any(|s| s.eq_ignore_ascii_case(&c_arg) || s.eq_ignore_ascii_case(&k_arg)) - { - self.args.push_front(c_arg); - } - }; - - // unwrap is safe here because we have set `self.bin_path` before - let mut command = Command::new(self.bin_path.as_ref().unwrap()); - command.args(&self.args); - - // We use a temporary directory as working directory if not specified otherwise with - // `current_dir()`. If neither `current_dir` nor a temporary directory is available, then we - // create our own. - if let Some(current_dir) = &self.current_dir { - command.current_dir(current_dir); - } else if let Some(temp_dir) = &self.tmpd { - command.current_dir(temp_dir.path()); - } else { - let temp_dir = tempfile::tempdir().unwrap(); - self.current_dir = Some(temp_dir.path().into()); - command.current_dir(temp_dir.path()); - self.tmpd = Some(Rc::new(temp_dir)); - } - - command.env_clear(); - if cfg!(windows) { - // spell-checker:ignore (dll) rsaenh - // %SYSTEMROOT% is required on Windows to initialize crypto provider - // ... and crypto provider is required for std::rand - // From `procmon`: RegQueryValue HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider\Microsoft Strong Cryptographic Provider\Image Path - // SUCCESS Type: REG_SZ, Length: 66, Data: %SystemRoot%\system32\rsaenh.dll" - if let Some(systemroot) = env::var_os("SYSTEMROOT") { - command.env("SYSTEMROOT", systemroot); - } - } else { - // if someone is setting LD_PRELOAD, there's probably a good reason for it - if let Some(ld_preload) = env::var_os("LD_PRELOAD") { - command.env("LD_PRELOAD", ld_preload); - } - } - - command - .envs(DEFAULT_ENV) - .envs(self.env_vars.iter().cloned()); - - if self.timeout.is_none() { - self.timeout = Some(Duration::from_secs(30)); - } - - let mut captured_stdout = None; - let mut captured_stderr = None; - if self.stderr_to_stdout { - let mut output = CapturedOutput::default(); - - command - .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) - .stdout(Stdio::from(output.try_clone().unwrap())) - .stderr(Stdio::from(output.try_clone().unwrap())); - captured_stdout = Some(output); - } else { - let stdout = if self.stdout.is_some() { - self.stdout.take().unwrap() - } else { - let mut stdout = CapturedOutput::default(); - let stdio = Stdio::from(stdout.try_clone().unwrap()); - captured_stdout = Some(stdout); - stdio - }; - - let stderr = if self.stderr.is_some() { - self.stderr.take().unwrap() - } else { - let mut stderr = CapturedOutput::default(); - let stdio = Stdio::from(stderr.try_clone().unwrap()); - captured_stderr = Some(stderr); - stdio - }; - - command - .stdin(self.stdin.take().unwrap_or_else(Stdio::null)) - .stdout(stdout) - .stderr(stderr); - }; - - (command, captured_stdout, captured_stderr) - } - - /// Spawns the command, feeds the stdin if any, and returns the - /// child process immediately. - pub fn run_no_wait(&mut self) -> UChild { - assert!(!self.has_run, "{}", ALREADY_RUN); - self.has_run = true; - - let (mut command, captured_stdout, captured_stderr) = self.build(); - log_info("run", self.to_string()); - - let child = command.spawn().unwrap(); - - #[cfg(any(target_os = "linux", target_os = "android"))] - for &(resource, soft_limit, hard_limit) in &self.limits { - prlimit( - child.id() as i32, - resource, - Some((soft_limit, hard_limit)), - None, - ) - .unwrap(); - } - - let mut child = UChild::from(self, child, captured_stdout, captured_stderr); - - if let Some(input) = self.bytes_into_stdin.take() { - child.pipe_in(input); - } - - child - } - - /// Spawns the command, feeds the stdin if any, waits for the result - /// and returns a command result. - /// It is recommended that you instead use `succeeds()` or `fails()` - pub fn run(&mut self) -> CmdResult { - self.run_no_wait().wait().unwrap() - } - - /// Spawns the command, feeding the passed in stdin, waits for the result - /// and returns a command result. - /// It is recommended that, instead of this, you use a combination of `pipe_in()` - /// with `succeeds()` or `fails()` - pub fn run_piped_stdin>>(&mut self, input: T) -> CmdResult { - self.pipe_in(input).run() - } - - /// Spawns the command, feeds the stdin if any, waits for the result, - /// asserts success, and returns a command result. - #[track_caller] - pub fn succeeds(&mut self) -> CmdResult { - let cmd_result = self.run(); - cmd_result.success(); - cmd_result - } - - /// Spawns the command, feeds the stdin if any, waits for the result, - /// asserts failure, and returns a command result. - #[track_caller] - pub fn fails(&mut self) -> CmdResult { - let cmd_result = self.run(); - cmd_result.failure(); - cmd_result - } - - pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String { - let tmpdir_path = self.tmpd.as_ref().unwrap().path(); - format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap()) - } -} - -impl std::fmt::Display for UCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut comm_string: Vec = vec![self - .bin_path - .as_ref() - .map_or(String::new(), |p| p.display().to_string())]; - comm_string.extend(self.args.iter().map(|s| s.to_string_lossy().to_string())); - f.write_str(&comm_string.join(" ")) - } -} - -/// Stored the captured output in a temporary file. The file is deleted as soon as -/// [`CapturedOutput`] is dropped. -#[derive(Debug)] -struct CapturedOutput { - current_file: File, - output: tempfile::NamedTempFile, // drop last -} - -impl CapturedOutput { - /// Creates a new instance of `CapturedOutput` - fn new(output: tempfile::NamedTempFile) -> Self { - Self { - current_file: output.reopen().unwrap(), - output, - } - } - - /// Try to clone the file pointer. - fn try_clone(&mut self) -> io::Result { - self.output.as_file().try_clone() - } - - /// Return the captured output as [`String`]. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output(&mut self) -> String { - String::from_utf8(self.output_bytes()).unwrap() - } - - /// Return the exact amount of bytes as `String`. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - /// - /// # Important - /// - /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read - fn output_exact(&mut self, size: usize) -> String { - String::from_utf8(self.output_exact_bytes(size)).unwrap() - } - - /// Return the captured output as bytes. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output_bytes(&mut self) -> Vec { - let mut buffer = Vec::::new(); - self.current_file.read_to_end(&mut buffer).unwrap(); - buffer - } - - /// Return all captured output, so far. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - fn output_all_bytes(&mut self) -> Vec { - let mut buffer = Vec::::new(); - let mut file = self.output.reopen().unwrap(); - - file.read_to_end(&mut buffer).unwrap(); - self.current_file = file; - - buffer - } - - /// Return the exact amount of bytes. - /// - /// Subsequent calls to any of the other output methods will operate on the subsequent output. - /// - /// # Important - /// - /// This method blocks indefinitely if the amount of bytes given by `size` cannot be read - fn output_exact_bytes(&mut self, size: usize) -> Vec { - let mut buffer = vec![0; size]; - self.current_file.read_exact(&mut buffer).unwrap(); - buffer - } -} - -impl Default for CapturedOutput { - fn default() -> Self { - let mut retries = 10; - let file = loop { - let file = Builder::new().rand_bytes(10).suffix(".out").tempfile(); - if file.is_ok() || retries <= 0 { - break file.unwrap(); - } - sleep(Duration::from_millis(100)); - retries -= 1; - }; - Self { - current_file: file.reopen().unwrap(), - output: file, - } - } -} - -impl Drop for CapturedOutput { - fn drop(&mut self) { - let _ = remove_file(self.output.path()); - } -} - -#[derive(Debug, Copy, Clone)] -pub enum AssertionMode { - All, - Current, - Exact(usize, usize), -} -pub struct UChildAssertion<'a> { - uchild: &'a mut UChild, -} - -impl<'a> UChildAssertion<'a> { - pub fn new(uchild: &'a mut UChild) -> Self { - Self { uchild } - } - - fn with_output(&mut self, mode: AssertionMode) -> CmdResult { - let exit_status = if self.uchild.is_alive() { - None - } else { - Some(self.uchild.raw.wait().unwrap()) - }; - let (stdout, stderr) = match mode { - AssertionMode::All => ( - self.uchild.stdout_all_bytes(), - self.uchild.stderr_all_bytes(), - ), - AssertionMode::Current => (self.uchild.stdout_bytes(), self.uchild.stderr_bytes()), - AssertionMode::Exact(expected_stdout_size, expected_stderr_size) => ( - self.uchild.stdout_exact_bytes(expected_stdout_size), - self.uchild.stderr_exact_bytes(expected_stderr_size), - ), - }; - CmdResult::new( - self.uchild.bin_path.clone(), - self.uchild.util_name.clone(), - self.uchild.tmpd.clone(), - exit_status, - stdout, - stderr, - ) - } - - // Make assertions of [`CmdResult`] with all output from start of the process until now. - // - // This method runs [`UChild::stdout_all_bytes`] and [`UChild::stderr_all_bytes`] under the - // hood. See there for side effects - pub fn with_all_output(&mut self) -> CmdResult { - self.with_output(AssertionMode::All) - } - - // Make assertions of [`CmdResult`] with the current output. - // - // This method runs [`UChild::stdout_bytes`] and [`UChild::stderr_bytes`] under the hood. See - // there for side effects - pub fn with_current_output(&mut self) -> CmdResult { - self.with_output(AssertionMode::Current) - } - - // Make assertions of [`CmdResult`] with the exact output. - // - // This method runs [`UChild::stdout_exact_bytes`] and [`UChild::stderr_exact_bytes`] under the - // hood. See there for side effects - pub fn with_exact_output( - &mut self, - expected_stdout_size: usize, - expected_stderr_size: usize, - ) -> CmdResult { - self.with_output(AssertionMode::Exact( - expected_stdout_size, - expected_stderr_size, - )) - } - - // Assert that the child process is alive - #[track_caller] - pub fn is_alive(&mut self) -> &mut Self { - match self - .uchild - .raw - .try_wait() - { - Ok(Some(status)) => panic!( - "Assertion failed. Expected '{}' to be running but exited with status={}.\nstdout: {}\nstderr: {}", - uucore::util_name(), - status, - self.uchild.stdout_all(), - self.uchild.stderr_all() - ), - Ok(None) => {} - Err(error) => panic!("Assertion failed with error '{error:?}'"), - } - - self - } - - // Assert that the child process has exited - #[track_caller] - pub fn is_not_alive(&mut self) -> &mut Self { - match self - .uchild - .raw - .try_wait() - { - Ok(None) => panic!( - "Assertion failed. Expected '{}' to be not running but was alive.\nstdout: {}\nstderr: {}", - uucore::util_name(), - self.uchild.stdout_all(), - self.uchild.stderr_all()), - Ok(_) => {}, - Err(error) => panic!("Assertion failed with error '{error:?}'"), - } - - self - } -} - -/// Abstraction for a [`std::process::Child`] to handle the child process. -pub struct UChild { - raw: Child, - bin_path: PathBuf, - util_name: Option, - captured_stdout: Option, - captured_stderr: Option, - ignore_stdin_write_error: bool, - stderr_to_stdout: bool, - join_handle: Option>>, - timeout: Option, - tmpd: Option>, // drop last -} - -impl UChild { - fn from( - ucommand: &UCommand, - child: Child, - captured_stdout: Option, - captured_stderr: Option, - ) -> Self { - Self { - raw: child, - bin_path: ucommand.bin_path.clone().unwrap(), - util_name: ucommand.util_name.clone(), - captured_stdout, - captured_stderr, - ignore_stdin_write_error: ucommand.ignore_stdin_write_error, - stderr_to_stdout: ucommand.stderr_to_stdout, - join_handle: None, - timeout: ucommand.timeout, - tmpd: ucommand.tmpd.clone(), - } - } - - /// Convenience method for `sleep(Duration::from_millis(millis))` - pub fn delay(&mut self, millis: u64) -> &mut Self { - sleep(Duration::from_millis(millis)); - self - } - - /// Return the pid of the child process, similar to [`Child::id`]. - pub fn id(&self) -> u32 { - self.raw.id() - } - - /// Return true if the child process is still alive and false otherwise. - pub fn is_alive(&mut self) -> bool { - self.raw.try_wait().unwrap().is_none() - } - - /// Return true if the child process is exited and false otherwise. - #[allow(clippy::wrong_self_convention)] - pub fn is_not_alive(&mut self) -> bool { - !self.is_alive() - } - - /// Return a [`UChildAssertion`] - pub fn make_assertion(&mut self) -> UChildAssertion { - UChildAssertion::new(self) - } - - /// Convenience function for calling [`UChild::delay`] and then [`UChild::make_assertion`] - pub fn make_assertion_with_delay(&mut self, millis: u64) -> UChildAssertion { - self.delay(millis).make_assertion() - } - - /// Try to kill the child process and wait for it's termination. - /// - /// This method blocks until the child process is killed, but returns an error if `self.timeout` - /// or the default of 60s was reached. If no such error happened, the process resources are - /// released, so there is usually no need to call `wait` or alike on unix systems although it's - /// still possible to do so. - /// - /// # Platform specific behavior - /// - /// On unix systems the child process resources will be released like a call to [`Child::wait`] - /// or alike would do. - /// - /// # Error - /// - /// If [`Child::kill`] returned an error or if the child process could not be terminated within - /// `self.timeout` or the default of 60s. - pub fn try_kill(&mut self) -> io::Result<()> { - let start = Instant::now(); - self.raw.kill()?; - - let timeout = self.timeout.unwrap_or(Duration::from_secs(60)); - // As a side effect, we're cleaning up the killed child process with the implicit call to - // `Child::try_wait` in `self.is_alive`, which reaps the process id on unix systems. We - // always fail with error on timeout if `self.timeout` is set to zero. - while self.is_alive() || timeout == Duration::ZERO { - if start.elapsed() < timeout { - self.delay(10); - } else { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("kill: Timeout of '{}s' reached", timeout.as_secs_f64()), - )); - } - hint::spin_loop(); - } - - Ok(()) - } - - /// Terminate the child process unconditionally and wait for the termination. - /// - /// Ignores any errors happening during [`Child::kill`] (i.e. child process already exited) but - /// still panics on timeout. - /// - /// # Panics - /// If the child process could not be terminated within `self.timeout` or the default of 60s. - pub fn kill(&mut self) -> &mut Self { - self.try_kill() - .or_else(|error| { - // We still throw the error on timeout in the `try_kill` function - if error.kind() == io::ErrorKind::Other { - Err(error) - } else { - Ok(()) - } - }) - .unwrap(); - self - } - - /// Wait for the child process to terminate and return a [`CmdResult`]. - /// - /// See [`UChild::wait_with_output`] for details on timeouts etc. This method can also be run if - /// the child process was killed with [`UChild::kill`]. - /// - /// # Errors - /// - /// Returns the error from the call to [`UChild::wait_with_output`] if any - pub fn wait(self) -> io::Result { - let (bin_path, util_name, tmpd) = ( - self.bin_path.clone(), - self.util_name.clone(), - self.tmpd.clone(), - ); - - #[allow(deprecated)] - let output = self.wait_with_output()?; - - Ok(CmdResult { - bin_path, - util_name, - tmpd, - exit_status: Some(output.status), - stdout: output.stdout, - stderr: output.stderr, - }) - } - - /// Wait for the child process to terminate and return an instance of [`Output`]. - /// - /// If `self.timeout` is reached while waiting, a [`io::ErrorKind::Other`] representing a - /// timeout error is returned. If no errors happened, we join with the thread created by - /// [`UChild::pipe_in`] if any. - /// - /// # Error - /// - /// If `self.timeout` is reached while waiting or [`Child::wait_with_output`] returned an - /// error. - #[deprecated = "Please use wait() -> io::Result instead."] - pub fn wait_with_output(mut self) -> io::Result { - let output = if let Some(timeout) = self.timeout { - let child = self.raw; - - let (sender, receiver) = mpsc::channel(); - let handle = thread::spawn(move || sender.send(child.wait_with_output())); - - match receiver.recv_timeout(timeout) { - Ok(result) => { - // unwraps are safe here because we got a result from the sender and there was no panic - // causing a disconnect. - handle.join().unwrap().unwrap(); - result - } - Err(RecvTimeoutError::Timeout) => Err(io::Error::new( - io::ErrorKind::Other, - format!("wait: Timeout of '{}s' reached", timeout.as_secs_f64()), - )), - Err(RecvTimeoutError::Disconnected) => { - handle.join().expect("Panic caused disconnect").unwrap(); - panic!("Error receiving from waiting thread because of unexpected disconnect"); - } - } - } else { - self.raw.wait_with_output() - }; - - let mut output = output?; - - if let Some(join_handle) = self.join_handle.take() { - join_handle - .join() - .expect("Error joining with the piping stdin thread") - .unwrap(); - }; - - if let Some(stdout) = self.captured_stdout.as_mut() { - output.stdout = stdout.output_bytes(); - } - if let Some(stderr) = self.captured_stderr.as_mut() { - output.stderr = stderr.output_bytes(); - } - - Ok(output) - } - - /// Read, consume and return the output as [`String`] from [`Child`]'s stdout. - /// - /// See also [`UChild::stdout_bytes`] for side effects. - pub fn stdout(&mut self) -> String { - String::from_utf8(self.stdout_bytes()).unwrap() - } - - /// Read and return all child's output in stdout as String. - /// - /// Note, that a subsequent call of any of these functions - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_bytes`] - /// * [`UChild::stdout_exact_bytes`] - /// - /// will operate on the subsequent output of the child process. - pub fn stdout_all(&mut self) -> String { - String::from_utf8(self.stdout_all_bytes()).unwrap() - } - - /// Read, consume and return the output as bytes from [`Child`]'s stdout. - /// - /// Each subsequent call to any of the functions below will operate on the subsequent output of - /// the child process: - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_exact_bytes`] - /// * and the call to itself [`UChild::stdout_bytes`] - pub fn stdout_bytes(&mut self) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_bytes(), - None if self.raw.stdout.is_some() => { - let mut buffer: Vec = vec![]; - let stdout = self.raw.stdout.as_mut().unwrap(); - stdout.read_to_end(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read and return all output from start of the child process until now. - /// - /// Each subsequent call of any of the methods below will operate on the subsequent output of - /// the child process. This method will panic if the output wasn't captured (for example if - /// [`UCommand::set_stdout`] was used). - /// - /// * [`UChild::stdout`] - /// * [`UChild::stdout_bytes`] - /// * [`UChild::stdout_exact_bytes`] - pub fn stdout_all_bytes(&mut self) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_all_bytes(), - None => { - panic!("Usage error: This method cannot be used if the output wasn't captured.") - } - } - } - - /// Read, consume and return the exact amount of bytes from `stdout`. - /// - /// This method may block indefinitely if the `size` amount of bytes exceeds the amount of bytes - /// that can be read. See also [`UChild::stdout_bytes`] for side effects. - pub fn stdout_exact_bytes(&mut self, size: usize) -> Vec { - match self.captured_stdout.as_mut() { - Some(output) => output.output_exact_bytes(size), - None if self.raw.stdout.is_some() => { - let mut buffer = vec![0; size]; - let stdout = self.raw.stdout.as_mut().unwrap(); - stdout.read_exact(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read, consume and return the child's stderr as String. - /// - /// See also [`UChild::stdout_bytes`] for side effects. If stderr is redirected to stdout with - /// [`UCommand::stderr_to_stdout`] then always an empty string will be returned. - pub fn stderr(&mut self) -> String { - String::from_utf8(self.stderr_bytes()).unwrap() - } - - /// Read and return all child's output in stderr as String. - /// - /// Note, that a subsequent call of any of these functions - /// - /// * [`UChild::stderr`] - /// * [`UChild::stderr_bytes`] - /// * [`UChild::stderr_exact_bytes`] - /// - /// will operate on the subsequent output of the child process. If stderr is redirected to - /// stdout with [`UCommand::stderr_to_stdout`] then always an empty string will be returned. - pub fn stderr_all(&mut self) -> String { - String::from_utf8(self.stderr_all_bytes()).unwrap() - } - - /// Read, consume and return the currently available bytes from child's stderr. - /// - /// If stderr is redirected to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes - /// are returned. See also [`UChild::stdout_bytes`] for side effects. - pub fn stderr_bytes(&mut self) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_bytes(), - None if self.raw.stderr.is_some() => { - let mut buffer: Vec = vec![]; - let stderr = self.raw.stderr.as_mut().unwrap(); - stderr.read_to_end(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Read and return all output from start of the child process until now. - /// - /// Each subsequent call of any of the methods below will operate on the subsequent output of - /// the child process. This method will panic if the output wasn't captured (for example if - /// [`UCommand::set_stderr`] was used). If [`UCommand::stderr_to_stdout`] was used always zero - /// bytes are returned. - /// - /// * [`UChild::stderr`] - /// * [`UChild::stderr_bytes`] - /// * [`UChild::stderr_exact_bytes`] - pub fn stderr_all_bytes(&mut self) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_all_bytes(), - None if self.stderr_to_stdout => vec![], - None => { - panic!("Usage error: This method cannot be used if the output wasn't captured.") - } - } - } - - /// Read, consume and return the exact amount of bytes from stderr. - /// - /// If stderr is redirect to stdout with [`UCommand::stderr_to_stdout`] then always zero bytes - /// are returned. - /// - /// # Important - /// This method blocks indefinitely if the `size` amount of bytes cannot be read. - pub fn stderr_exact_bytes(&mut self, size: usize) -> Vec { - match self.captured_stderr.as_mut() { - Some(output) => output.output_exact_bytes(size), - None if self.raw.stderr.is_some() => { - let stderr = self.raw.stderr.as_mut().unwrap(); - let mut buffer = vec![0; size]; - stderr.read_exact(&mut buffer).unwrap(); - buffer - } - None => vec![], - } - } - - /// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks. - /// - /// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the - /// command line and can be used only once or else panics. Note, that [`UCommand::set_stdin`] - /// must be used together with [`Stdio::piped`] or else this method doesn't work as expected. - /// `Stdio::piped` is the current default when using [`UCommand::run_no_wait`]) without calling - /// `set_stdin`. This method stores a [`JoinHandle`] of the thread in which the writing to the - /// child processes' stdin is running. The associated thread is joined with the main process in - /// the methods below when exiting the child process. - /// - /// * [`UChild::wait`] - /// * [`UChild::wait_with_output`] - /// * [`UChild::pipe_in_and_wait`] - /// * [`UChild::pipe_in_and_wait_with_output`] - /// - /// Usually, there's no need to join manually but if needed, the [`UChild::join`] method can be - /// used . - /// - /// [`JoinHandle`]: std::thread::JoinHandle - pub fn pipe_in>>(&mut self, content: T) -> &mut Self { - let ignore_stdin_write_error = self.ignore_stdin_write_error; - let content = content.into(); - let stdin = self - .raw - .stdin - .take() - .expect("Could not pipe into child process. Was it set to Stdio::null()?"); - - let join_handle = thread::spawn(move || { - let mut writer = BufWriter::new(stdin); - - match writer.write_all(&content).and_then(|()| writer.flush()) { - Err(error) if !ignore_stdin_write_error => Err(io::Error::new( - io::ErrorKind::Other, - format!("failed to write to stdin of child: {error}"), - )), - Ok(()) | Err(_) => Ok(()), - } - }); - - self.join_handle = Some(join_handle); - self - } - - /// Call join on the thread created by [`UChild::pipe_in`] and if the thread is still running. - /// - /// This method can be called multiple times but is a noop if already joined. - pub fn join(&mut self) -> &mut Self { - if let Some(join_handle) = self.join_handle.take() { - join_handle - .join() - .expect("Error joining with the piping stdin thread") - .unwrap(); - } - self - } - - /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait`] - pub fn pipe_in_and_wait>>(mut self, content: T) -> CmdResult { - self.pipe_in(content); - self.wait().unwrap() - } - - /// Convenience method for [`UChild::pipe_in`] and then [`UChild::wait_with_output`] - #[deprecated = "Please use pipe_in_and_wait() -> CmdResult instead."] - pub fn pipe_in_and_wait_with_output>>(mut self, content: T) -> Output { - self.pipe_in(content); - - #[allow(deprecated)] - self.wait_with_output().unwrap() - } - - /// Write some bytes to the child process stdin. - /// - /// This function is meant for small data and faking user input like typing a `yes` or `no`. - /// This function blocks until all data is written but can be used multiple times in contrast to - /// [`UChild::pipe_in`]. - /// - /// # Errors - /// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error - pub fn try_write_in>>(&mut self, data: T) -> io::Result<()> { - let stdin = self.raw.stdin.as_mut().unwrap(); - - match stdin.write_all(&data.into()).and_then(|()| stdin.flush()) { - Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new( - io::ErrorKind::Other, - format!("failed to write to stdin of child: {error}"), - )), - Ok(()) | Err(_) => Ok(()), - } - } - - /// Convenience function for [`UChild::try_write_in`] and a following `unwrap`. - pub fn write_in>>(&mut self, data: T) -> &mut Self { - self.try_write_in(data).unwrap(); - self - } - - /// Close the child process stdout. - /// - /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the - /// default if [`UCommand::set_stdout`] wasn't called. - pub fn close_stdout(&mut self) -> &mut Self { - self.raw.stdout.take(); - self - } - - /// Close the child process stderr. - /// - /// Note this will have no effect if the output was captured with [`CapturedOutput`] which is the - /// default if [`UCommand::set_stderr`] wasn't called. - pub fn close_stderr(&mut self) -> &mut Self { - self.raw.stderr.take(); - self - } - - /// Close the child process stdin. - /// - /// Note, this does not have any effect if using the [`UChild::pipe_in`] method. - pub fn close_stdin(&mut self) -> &mut Self { - self.raw.stdin.take(); - self - } -} - -pub fn vec_of_size(n: usize) -> Vec { - let result = vec![b'a'; n]; - assert_eq!(result.len(), n); - result -} - -pub fn whoami() -> String { - // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. - // - // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" - // whoami: cannot find name for user ID 1001 - // id --name: cannot find name for user ID 1001 - // id --name: cannot find name for group ID 116 - // - // However, when running "id" from within "/bin/bash" it looks fine: - // id: "uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),101(systemd-journal)" - // whoami: "runner" - - // Use environment variable to get current user instead of - // invoking `whoami` and fall back to user "nobody" on error. - std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .unwrap_or_else(|e| { - println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead"); - "nobody".to_string() - }) -} - -/// Add prefix 'g' for `util_name` if not on linux -#[cfg(unix)] -pub fn host_name_for(util_name: &str) -> Cow { - // In some environments, e.g. macOS/freebsd, the GNU procps are prefixed with "g" - // to not interfere with the BSD counterparts already in `$PATH`. - #[cfg(not(target_os = "linux"))] - { - // make call to `host_name_for` idempotent - if util_name.starts_with('g') && util_name != "groups" { - util_name.into() - } else { - format!("g{util_name}").into() - } - } - #[cfg(target_os = "linux")] - util_name.into() -} - -// GNU procps version 8.32 is the reference version since it is the latest version and the -// GNU test suite in "procps/.github/workflows/GnuTests.yml" runs against it. -// However, here 8.30 was chosen because right now there's no ubuntu image for the github actions -// CICD available with a higher version than 8.30. -// GNU procps versions from the CICD images for comparison: -// ubuntu-2004: 8.30 (latest) -// ubuntu-1804: 8.28 -// macos-latest: 8.32 -const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `coreutil` in `$PATH` - -const UUTILS_WARNING: &str = "uutils-tests-warning"; -const UUTILS_INFO: &str = "uutils-tests-info"; - -/// Run `util_name --version` and return Ok if the version is >= `version_expected`. -/// Returns an error if -/// * `util_name` cannot run -/// * the version cannot be parsed -/// * the version is too low -/// -/// This is used by `expected_result` to check if the procps version is >= `VERSION_MIN`. -/// It makes sense to use this manually in a test if a feature -/// is tested that was introduced after `VERSION_MIN` -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; -/// -/// #[test] -/// fn test_xyz() { -/// unwrap_or_return!(check_coreutil_version( -/// util_name!(), -/// VERSION_MIN_MULTIPLE_USERS -/// )); -/// // proceed with the test... -/// } -/// ``` -#[cfg(unix)] -pub fn check_coreutil_version( - util_name: &str, - version_expected: &str, -) -> std::result::Result { - // example: - // $ id --version | head -n 1 - // id (GNU procps) 8.32.162-4eda - - let util_name = &host_name_for(util_name); - log_info("run", format!("{util_name} --version")); - let version_check = match Command::new(util_name.as_ref()) - .env("LC_ALL", "C") - .arg("--version") - .output() - { - Ok(s) => s, - Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")), - }; - std::str::from_utf8(&version_check.stdout).unwrap() - .split('\n') - .collect::>() - .first() - .map_or_else( - || Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")), - |s| { - if s.contains(&format!("(GNU procps) {version_expected}")) { - Ok(format!("{UUTILS_INFO}: {s}")) - } else if s.contains("(GNU procps)") { - let version_found = parse_coreutil_version(s); - let version_expected = version_expected.parse::().unwrap_or_default(); - if version_found > version_expected { - Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}")) - } else { - Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) } - } else { - Err(format!("{UUTILS_WARNING}: no procps version string found for reference procps '{util_name} --version'")) - } - }, - ) -} - -// simple heuristic to parse the procps SemVer string, e.g. "id (GNU procps) 8.32.263-0475" -fn parse_coreutil_version(version_string: &str) -> f32 { - version_string - .split_whitespace() - .last() - .unwrap() - .split('.') - .take(2) - .collect::>() - .join(".") - .parse::() - .unwrap_or_default() -} - -/// This runs the GNU procps `util_name` binary in `$PATH` in order to -/// dynamically gather reference values on the system. -/// If the `util_name` in `$PATH` doesn't include a procps version string, -/// or the version is too low, this returns an error and the test should be skipped. -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// #[test] -/// fn test_xyz() { -/// let ts = TestScenario::new(util_name!()); -/// let result = ts.ucmd().run(); -/// let exp_result = unwrap_or_return!(expected_result(&ts, &[])); -/// result -/// .stdout_is(exp_result.stdout_str()) -/// .stderr_is(exp_result.stderr_str()) -/// .code_is(exp_result.code()); -/// } -///``` -#[cfg(unix)] -pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { - let util_name = ts.util_name.as_str(); - println!("{}", check_coreutil_version(util_name, VERSION_MIN)?); - let util_name = host_name_for(util_name); - - let result = ts - .cmd(util_name.as_ref()) - .env("PATH", PATH) - .envs(DEFAULT_ENV) - .args(args) - .run(); - - let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") { - ( - result.stdout_str().to_string(), - result.stderr_str().to_string(), - ) - } else { - // `host_name_for` added prefix, strip 'g' prefix from results: - let from = util_name.to_string() + ":"; - let to = &from[1..]; - ( - result.stdout_str().replace(&from, to), - result.stderr_str().replace(&from, to), - ) - }; - - Ok(CmdResult::new( - ts.bin_path.as_os_str().to_str().unwrap().to_string(), - Some(ts.util_name.clone()), - Some(result.tmpd()), - result.exit_status, - stdout.as_bytes(), - stderr.as_bytes(), - )) -} - -/// This is a convenience wrapper to run a ucmd with root permissions. -/// It can be used to test programs when being root is needed -/// This runs `sudo -E --non-interactive target/debug/procps util_name args` -/// This is primarily designed to run in an environment where whoami is in $path -/// and where non-interactive sudo is possible. -/// To check if i) non-interactive sudo is possible and ii) if sudo works, this runs: -/// `sudo -E --non-interactive whoami` first. -/// -/// This return an `Err()` if run inside CICD because there's no 'sudo'. -/// -/// Example: -/// -/// ```no_run -/// use crate::common::util::*; -/// #[test] -/// fn test_xyz() { -/// let ts = TestScenario::new("whoami"); -/// let expected = "root\n".to_string(); -/// if let Ok(result) = run_ucmd_as_root(&ts, &[]) { -/// result.stdout_is(expected); -/// } else { -/// println!("TEST SKIPPED"); -/// } -/// } -///``` -#[cfg(unix)] -pub fn run_ucmd_as_root( - ts: &TestScenario, - args: &[&str], -) -> std::result::Result { - run_ucmd_as_root_with_stdin_stdout(ts, args, None, None) -} - -#[cfg(unix)] -pub fn run_ucmd_as_root_with_stdin_stdout( - ts: &TestScenario, - args: &[&str], - stdin: Option<&str>, - stdout: Option<&str>, -) -> std::result::Result { - if is_ci() { - Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI")) - } else { - // check if we can run 'sudo' - log_info("run", "sudo -E --non-interactive whoami"); - match Command::new("sudo") - .envs(DEFAULT_ENV) - .args(["-E", "--non-interactive", "whoami"]) - .output() - { - Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => { - // we can run sudo and we're root - // run ucmd as root: - let mut cmd = ts.cmd("sudo"); - cmd.env("PATH", PATH) - .envs(DEFAULT_ENV) - .arg("-E") - .arg("--non-interactive") - .arg(&ts.bin_path) - .arg(&ts.util_name) - .args(args); - if let Some(stdin) = stdin { - cmd.set_stdin(File::open(stdin).unwrap()); - } - if let Some(stdout) = stdout { - cmd.set_stdout(File::open(stdout).unwrap()); - } - Ok(cmd.run()) - } - Ok(output) - if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") => - { - Err("Cannot run non-interactive sudo".to_string()) - } - Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()), - Err(e) => Err(format!("{UUTILS_WARNING}: {e}")), - } - } -} - -/// Sanity checks for test utils -#[cfg(test)] -mod tests { - // spell-checker:ignore (tests) asdfsadfa - use super::*; - - pub fn run_cmd>(cmd: T) -> CmdResult { - UCommand::new().arg(cmd).run() - } - - #[test] - fn test_command_result_when_no_output_with_exit_32() { - let result = run_cmd("exit 32"); - - if cfg!(windows) { - std::assert!(result.bin_path.ends_with("cmd")); - } else { - std::assert!(result.bin_path.ends_with("sh")); - } - - std::assert!(result.util_name.is_none()); - std::assert!(result.tmpd.is_some()); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 32); - result.code_is(32); - assert!(!result.succeeded()); - result.failure(); - result.fails_silently(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); - result.no_output(); - result.no_stderr(); - result.no_stdout(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_32_then_success_panic() { - run_cmd("exit 32").success(); - } - - #[test] - fn test_command_result_when_no_output_with_exit_0() { - let result = run_cmd("exit 0"); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); - result.no_output(); - result.no_stderr(); - result.no_stdout(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_0_then_failure_panics() { - run_cmd("exit 0").failure(); - } - - #[test] - #[should_panic] - fn test_command_result_when_exit_0_then_silent_failure_panics() { - run_cmd("exit 0").fails_silently(); - } - - #[test] - fn test_command_result_when_stdout_with_exit_0() { - #[cfg(windows)] - let (result, vector, string) = ( - run_cmd("echo hello& exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], - "hello\r\n", - ); - #[cfg(not(windows))] - let (result, vector, string) = ( - run_cmd("echo hello; exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\n'], - "hello\n", - ); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stderr.is_empty()); - std::assert_eq!(result.stdout, vector); - result.no_stderr(); - result.stdout_is(string); - result.stdout_is_bytes(&vector); - result.stdout_only(string); - result.stdout_only_bytes(&vector); - } - - #[test] - fn test_command_result_when_stderr_with_exit_0() { - #[cfg(windows)] - let (result, vector, string) = ( - run_cmd("echo hello>&2& exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'], - "hello\r\n", - ); - #[cfg(not(windows))] - let (result, vector, string) = ( - run_cmd("echo hello >&2; exit 0"), - vec![b'h', b'e', b'l', b'l', b'o', b'\n'], - "hello\n", - ); - - assert!(result.exit_status.is_some()); - std::assert_eq!(result.code(), 0); - result.code_is(0); - assert!(result.succeeded()); - result.success(); - assert!(result.stdout.is_empty()); - result.no_stdout(); - std::assert_eq!(result.stderr, vector); - result.stderr_is(string); - result.stderr_is_bytes(&vector); - result.stderr_only(string); - result.stderr_only_bytes(&vector); - } - - #[test] - fn test_std_does_not_contain() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - res.stdout_does_not_contain("unlikely"); - res.stderr_does_not_contain("unlikely"); - } - - #[test] - #[should_panic] - fn test_stdout_does_not_contain_fail() { - #[cfg(windows)] - let res = run_cmd("echo This is a likely error message& exit 0"); - #[cfg(not(windows))] - let res = run_cmd("echo This is a likely error message; exit 0"); - - res.stdout_does_not_contain("likely"); - } - - #[test] - #[should_panic] - fn test_stderr_does_not_contain_fail() { - #[cfg(windows)] - let res = run_cmd("echo This is a likely error message>&2 & exit 0"); - #[cfg(not(windows))] - let res = run_cmd("echo This is a likely error message >&2; exit 0"); - - res.stderr_does_not_contain("likely"); - } - - #[test] - fn test_stdout_matches() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2 ) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let positive = regex::Regex::new(".*likely.*").unwrap(); - let negative = regex::Regex::new(".*unlikely.*").unwrap(); - res.stdout_matches(&positive); - res.stdout_does_not_match(&negative); - } - - #[test] - #[should_panic] - fn test_stdout_matches_fail() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let negative = regex::Regex::new(".*unlikely.*").unwrap(); - res.stdout_matches(&negative); - } - - #[test] - #[should_panic] - fn test_stdout_not_matches_fail() { - #[cfg(windows)] - let res = run_cmd( - "(echo This is a likely error message& echo This is a likely error message>&2) & exit 0", - ); - #[cfg(not(windows))] - let res = run_cmd( - "echo This is a likely error message; echo This is a likely error message >&2; exit 0", - ); - - let positive = regex::Regex::new(".*likely.*").unwrap(); - res.stdout_does_not_match(&positive); - } - - #[cfg(feature = "echo")] - #[test] - fn test_normalized_newlines_stdout_is() { - let ts = TestScenario::new("echo"); - let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); - - res.normalized_newlines_stdout_is("A\r\nB\nC"); - res.normalized_newlines_stdout_is("A\nB\nC"); - res.normalized_newlines_stdout_is("A\nB\r\nC"); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_normalized_newlines_stdout_is_fail() { - let ts = TestScenario::new("echo"); - let res = ts.ucmd().args(&["-ne", "A\r\nB\nC"]).run(); - - res.normalized_newlines_stdout_is("A\r\nB\nC\n"); - } - - #[cfg(feature = "echo")] - #[test] - fn test_cmd_result_stdout_check_and_stdout_str_check() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - - result.stdout_str_check(|stdout| stdout.ends_with("world\n")); - result.stdout_check(|stdout| stdout.get(0..2).unwrap().eq(&[b'H', b'e'])); - result.no_stderr(); - } - - #[cfg(feature = "echo")] - #[test] - fn test_cmd_result_stderr_check_and_stderr_str_check() { - let ts = TestScenario::new("echo"); - let result = run_cmd(format!( - "{} {} Hello world >&2", - ts.bin_path.display(), - ts.util_name - )); - - result.stderr_str_check(|stderr| stderr.ends_with("world\n")); - result.stderr_check(|stderr| stderr.get(0..2).unwrap().eq(&[b'H', b'e'])); - result.no_stdout(); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_str_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stdout_str_check(str::is_empty); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stdout_check(<[u8]>::is_empty); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stderr_str_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stderr_str_check(|s| !s.is_empty()); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stderr_check_when_false_then_panics() { - let result = TestScenario::new("echo").ucmd().arg("Hello world").run(); - result.stderr_check(|s| !s.is_empty()); - } - - #[cfg(feature = "echo")] - #[test] - #[should_panic] - fn test_cmd_result_stdout_check_when_predicate_panics_then_panic() { - let result = TestScenario::new("echo").ucmd().run(); - result.stdout_str_check(|_| panic!("Just testing")); - } - - #[cfg(feature = "echo")] - #[cfg(unix)] - #[test] - fn test_cmd_result_signal_when_normal_exit_then_no_signal() { - let result = TestScenario::new("echo").ucmd().run(); - assert!(result.signal().is_none()); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[test] - #[should_panic = "Program must be run first or has not finished"] - fn test_cmd_result_signal_when_still_running_then_panic() { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - - child - .make_assertion() - .is_alive() - .with_current_output() - .signal(); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[test] - fn test_cmd_result_signal_when_kill_then_signal() { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - - child.kill(); - child - .make_assertion() - .is_not_alive() - .with_current_output() - .signal_is(9) - .signal_name_is("SIGKILL") - .signal_name_is("KILL") - .signal_name_is("9") - .signal() - .expect("Signal was none"); - - let result = child.wait().unwrap(); - result - .signal_is(9) - .signal_name_is("SIGKILL") - .signal_name_is("KILL") - .signal_name_is("9") - .signal() - .expect("Signal was none"); - } - - #[cfg(feature = "sleep")] - #[cfg(unix)] - #[rstest] - #[case::signal_full_name_lower_case("sigkill")] - #[case::signal_short_name_lower_case("kill")] - #[case::signal_only_part_of_name("IGKILL")] // spell-checker: disable-line - #[case::signal_just_sig("SIG")] - #[case::signal_value_too_high("100")] - #[case::signal_value_negative("-1")] - #[should_panic = "Invalid signal name or value"] - fn test_cmd_result_signal_when_invalid_signal_name_then_panic(#[case] signal_name: &str) { - let mut child = TestScenario::new("sleep").ucmd().arg("60").run_no_wait(); - child.kill(); - let result = child.wait().unwrap(); - result.signal_name_is(signal_name); - } - - #[test] - #[cfg(unix)] - fn test_parse_coreutil_version() { - use std::assert_eq; - assert_eq!( - parse_coreutil_version("id (GNU procps) 9.0.123-0123").to_string(), - "9" - ); - assert_eq!( - parse_coreutil_version("id (GNU procps) 8.32.263-0475").to_string(), - "8.32" - ); - assert_eq!( - parse_coreutil_version("id (GNU procps) 8.25.123-0123").to_string(), - "8.25" - ); - assert_eq!( - parse_coreutil_version("id (GNU procps) 9.0").to_string(), - "9" - ); - assert_eq!( - parse_coreutil_version("id (GNU procps) 8.32").to_string(), - "8.32" - ); - assert_eq!( - parse_coreutil_version("id (GNU procps) 8.25").to_string(), - "8.25" - ); - } - - #[test] - #[cfg(unix)] - fn test_check_coreutil_version() { - match check_coreutil_version("id", VERSION_MIN) { - Ok(s) => assert!(s.starts_with("uutils-tests-")), - Err(s) => assert!(s.starts_with("uutils-tests-warning")), - }; - #[cfg(target_os = "linux")] - std::assert_eq!( - check_coreutil_version("no test name", VERSION_MIN), - Err("uutils-tests-warning: 'no test name' \ - No such file or directory (os error 2)" - .to_string()) - ); - } - - #[test] - #[cfg(unix)] - fn test_expected_result() { - let ts = TestScenario::new("id"); - // assert!(expected_result(&ts, &[]).is_ok()); - match expected_result(&ts, &[]) { - Ok(r) => assert!(r.succeeded()), - Err(s) => assert!(s.starts_with("uutils-tests-warning")), - } - let ts = TestScenario::new("no test name"); - assert!(expected_result(&ts, &[]).is_err()); - } - - #[test] - #[cfg(unix)] - fn test_host_name_for() { - #[cfg(target_os = "linux")] - { - std::assert_eq!(host_name_for("id"), "id"); - std::assert_eq!(host_name_for("groups"), "groups"); - std::assert_eq!(host_name_for("who"), "who"); - } - #[cfg(not(target_os = "linux"))] - { - // spell-checker:ignore (strings) ggroups gwho - std::assert_eq!(host_name_for("id"), "gid"); - std::assert_eq!(host_name_for("groups"), "ggroups"); - std::assert_eq!(host_name_for("who"), "gwho"); - std::assert_eq!(host_name_for("gid"), "gid"); - std::assert_eq!(host_name_for("ggroups"), "ggroups"); - std::assert_eq!(host_name_for("gwho"), "gwho"); - } - } - - #[test] - #[cfg(unix)] - #[cfg(feature = "whoami")] - fn test_run_ucmd_as_root() { - if is_ci() { - println!("TEST SKIPPED (cannot run inside CI)"); - } else { - // Skip test if we can't guarantee non-interactive `sudo`, or if we're not "root" - if let Ok(output) = Command::new("sudo") - .env("LC_ALL", "C") - .args(["-E", "--non-interactive", "whoami"]) - .output() - { - if output.status.success() && String::from_utf8_lossy(&output.stdout).eq("root\n") { - let ts = TestScenario::new("whoami"); - std::assert_eq!( - run_ucmd_as_root(&ts, &[]).unwrap().stdout_str().trim(), - "root" - ); - } else { - println!("TEST SKIPPED (we're not root)"); - } - } else { - println!("TEST SKIPPED (cannot run sudo)"); - } - } - } - - // This error was first detected when running tail so tail is used here but - // should fail with any command that takes piped input. - // See also https://github.com/uutils/procps/issues/3895 - #[cfg(feature = "tail")] - #[test] - #[cfg_attr(not(feature = "expensive_tests"), ignore)] - fn test_when_piped_input_then_no_broken_pipe() { - let ts = TestScenario::new("tail"); - for i in 0..10000 { - dbg!(i); - let test_string = "a\nb\n"; - ts.ucmd() - .args(&["-n", "0"]) - .pipe_in(test_string) - .succeeds() - .no_stdout() - .no_stderr(); - } - } - - #[cfg(feature = "echo")] - #[test] - fn test_uchild_when_run_with_a_non_blocking_util() { - let ts = TestScenario::new("echo"); - ts.ucmd() - .arg("hello world") - .run() - .success() - .stdout_only("hello world\n"); - } - - // Test basically that most of the methods of UChild are working - #[cfg(feature = "echo")] - #[test] - fn test_uchild_when_run_no_wait_with_a_non_blocking_util() { - let ts = TestScenario::new("echo"); - let mut child = ts.ucmd().arg("hello world").run_no_wait(); - - // check `child.is_alive()` and `child.delay()` is working - let mut trials = 10; - while child.is_alive() { - assert!( - trials > 0, - "Assertion failed: child process is still alive." - ); - - child.delay(500); - trials -= 1; - } - - assert!(!child.is_alive()); - - // check `child.is_not_alive()` is working - assert!(child.is_not_alive()); - - // check the current output is correct - std::assert_eq!(child.stdout(), "hello world\n"); - assert!(child.stderr().is_empty()); - - // check the current output of echo is empty. We already called `child.stdout()` and `echo` - // exited so there's no additional output after the first call of `child.stdout()` - assert!(child.stdout().is_empty()); - assert!(child.stderr().is_empty()); - - // check that we're still able to access all output of the child process, even after exit - // and call to `child.stdout()` - std::assert_eq!(child.stdout_all(), "hello world\n"); - assert!(child.stderr_all().is_empty()); - - // we should be able to call kill without panics, even if the process already exited - child.make_assertion().is_not_alive(); - child.kill(); - - // we should be able to call wait without panics and apply some assertions - child.wait().unwrap().code_is(0).no_stdout().no_stderr(); - } - - #[cfg(feature = "cat")] - #[test] - fn test_uchild_when_pipe_in() { - let ts = TestScenario::new("cat"); - let mut child = ts.ucmd().set_stdin(Stdio::piped()).run_no_wait(); - child.pipe_in("content"); - child.wait().unwrap().stdout_only("content").success(); - - ts.ucmd().pipe_in("content").run().stdout_is("content"); - } - - #[cfg(feature = "rm")] - #[test] - fn test_uchild_when_run_no_wait_with_a_blocking_command() { - let ts = TestScenario::new("rm"); - let at = &ts.fixtures; - - at.mkdir("a"); - at.touch("a/empty"); - - #[cfg(target_vendor = "apple")] - let delay: u64 = 2000; - #[cfg(not(target_vendor = "apple"))] - let delay: u64 = 1000; - - let yes = if cfg!(windows) { "y\r\n" } else { "y\n" }; - - let mut child = ts - .ucmd() - .set_stdin(Stdio::piped()) - .stderr_to_stdout() - .args(&["-riv", "a"]) - .run_no_wait(); - child - .make_assertion_with_delay(delay) - .is_alive() - .with_current_output() - .stdout_is("rm: descend into directory 'a'? "); - - #[cfg(windows)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a\\empty'? "; - #[cfg(unix)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a/empty'? "; - child.write_in(yes); - child - .make_assertion_with_delay(delay) - .is_alive() - .with_all_output() - .stdout_is(expected); - - #[cfg(windows)] - let expected = "removed 'a\\empty'\nrm: remove directory 'a'? "; - #[cfg(unix)] - let expected = "removed 'a/empty'\nrm: remove directory 'a'? "; - - child - .write_in(yes) - .make_assertion_with_delay(delay) - .is_alive() - .with_exact_output(44, 0) - .stdout_only(expected); - - let expected = "removed directory 'a'\n"; - - child.write_in(yes); - child.wait().unwrap().stdout_only(expected).success(); - } - - #[cfg(feature = "tail")] - #[test] - fn test_uchild_when_run_with_stderr_to_stdout() { - let ts = TestScenario::new("tail"); - let at = &ts.fixtures; - - at.write("data", "file data\n"); - - let expected_stdout = "==> data <==\n\ - file data\n\ - tail: cannot open 'missing' for reading: No such file or directory\n"; - ts.ucmd() - .args(&["data", "missing"]) - .stderr_to_stdout() - .fails() - .stdout_only(expected_stdout); - } - - #[cfg(feature = "cat")] - #[cfg(unix)] - #[test] - fn test_uchild_when_no_capture_reading_from_infinite_source() { - use regex::Regex; - - let ts = TestScenario::new("cat"); - - let expected_stdout = b"\0".repeat(12345); - let mut child = ts - .ucmd() - .set_stdin(Stdio::from(File::open("/dev/zero").unwrap())) - .set_stdout(Stdio::piped()) - .run_no_wait(); - - child - .make_assertion() - .with_exact_output(12345, 0) - .stdout_only_bytes(expected_stdout); - - child - .kill() - .make_assertion() - .with_current_output() - .stdout_matches(&Regex::new("[\0].*").unwrap()) - .no_stderr(); - } - - #[cfg(feature = "sleep")] - #[test] - fn test_uchild_when_wait_and_timeout_is_reached_then_timeout_error() { - let ts = TestScenario::new("sleep"); - let child = ts - .ucmd() - .timeout(Duration::from_secs(1)) - .arg("10.0") - .run_no_wait(); - - match child.wait() { - Err(error) if error.kind() == io::ErrorKind::Other => { - std::assert_eq!(error.to_string(), "wait: Timeout of '1s' reached"); - } - Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), - Ok(_) => panic!("Assertion failed: Expected timeout of `wait`."), - } - } - - #[cfg(feature = "sleep")] - #[rstest] - #[timeout(Duration::from_secs(5))] - fn test_uchild_when_kill_and_timeout_higher_than_kill_time_then_no_panic() { - let ts = TestScenario::new("sleep"); - let mut child = ts - .ucmd() - .timeout(Duration::from_secs(60)) - .arg("20.0") - .run_no_wait(); - - child.kill().make_assertion().is_not_alive(); - } - - #[cfg(feature = "sleep")] - #[test] - fn test_uchild_when_try_kill_and_timeout_is_reached_then_error() { - let ts = TestScenario::new("sleep"); - let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); - - match child.try_kill() { - Err(error) if error.kind() == io::ErrorKind::Other => { - std::assert_eq!(error.to_string(), "kill: Timeout of '0s' reached"); - } - Err(error) => panic!("Assertion failed: Expected error with timeout but was: {error}"), - Ok(()) => panic!("Assertion failed: Expected timeout of `try_kill`."), - } - } - - #[cfg(feature = "sleep")] - #[test] - #[should_panic = "kill: Timeout of '0s' reached"] - fn test_uchild_when_kill_with_timeout_and_timeout_is_reached_then_panic() { - let ts = TestScenario::new("sleep"); - let mut child = ts.ucmd().timeout(Duration::ZERO).arg("10.0").run_no_wait(); - - child.kill(); - panic!("Assertion failed: Expected timeout of `kill`."); - } - - #[cfg(feature = "sleep")] - #[test] - #[should_panic(expected = "wait: Timeout of '1.1s' reached")] - fn test_ucommand_when_run_with_timeout_and_timeout_is_reached_then_panic() { - let ts = TestScenario::new("sleep"); - ts.ucmd() - .timeout(Duration::from_millis(1100)) - .arg("10.0") - .run(); - - panic!("Assertion failed: Expected timeout of `run`.") - } - - #[cfg(feature = "sleep")] - #[rstest] - #[timeout(Duration::from_secs(10))] - fn test_ucommand_when_run_with_timeout_higher_then_execution_time_then_no_panic() { - let ts = TestScenario::new("sleep"); - ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run(); - } - - #[cfg(feature = "echo")] - #[test] - fn test_ucommand_when_default() { - let shell_cmd = format!("{TESTS_BINARY} echo -n hello"); - - let mut command = UCommand::new(); - command.arg(&shell_cmd).succeeds().stdout_is("hello"); - - #[cfg(target_os = "android")] - let (expected_bin, expected_arg) = (PathBuf::from("/system/bin/sh"), OsString::from("-c")); - #[cfg(all(unix, not(target_os = "android")))] - let (expected_bin, expected_arg) = (PathBuf::from("/bin/sh"), OsString::from("-c")); - #[cfg(windows)] - let (expected_bin, expected_arg) = (PathBuf::from("cmd"), OsString::from("/C")); - - std::assert_eq!(&expected_bin, command.bin_path.as_ref().unwrap()); - assert!(command.util_name.is_none()); - std::assert_eq!(command.args, &[expected_arg, OsString::from(&shell_cmd)]); - assert!(command.tmpd.is_some()); - } - - #[cfg(feature = "echo")] - #[test] - fn test_ucommand_with_util() { - let tmpd = tempfile::tempdir().unwrap(); - let mut command = UCommand::with_util("echo", Rc::new(tmpd)); - - command - .args(&["-n", "hello"]) - .succeeds() - .stdout_only("hello"); - - std::assert_eq!( - &PathBuf::from(TESTS_BINARY), - command.bin_path.as_ref().unwrap() - ); - std::assert_eq!("echo", &command.util_name.unwrap()); - std::assert_eq!( - &[ - OsString::from("echo"), - OsString::from("-n"), - OsString::from("hello") - ], - command.args.make_contiguous() - ); - assert!(command.tmpd.is_some()); - } - - #[cfg(all(unix, not(target_os = "macos")))] - #[test] - fn test_compare_xattrs() { - use tempfile::tempdir; - - let temp_dir = tempdir().unwrap(); - let file_path1 = temp_dir.path().join("test_file1.txt"); - let file_path2 = temp_dir.path().join("test_file2.txt"); - - File::create(&file_path1).unwrap(); - File::create(&file_path2).unwrap(); - - let test_attr = "user.test_attr"; - let test_value = b"test value"; - xattr::set(&file_path1, test_attr, test_value).unwrap(); - - assert!(!compare_xattrs(&file_path1, &file_path2)); - - xattr::set(&file_path2, test_attr, test_value).unwrap(); - assert!(compare_xattrs(&file_path1, &file_path2)); - } -} diff --git a/tests/tests.rs b/tests/tests.rs index 620d49f8..6078611f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,8 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#[macro_use] -mod common; +use std::env; + +pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_procps"); + +// Use the ctor attribute to run this function before any tests +#[ctor::ctor] +fn init() { + unsafe { + // Necessary for uutests to be able to find the binary + std::env::set_var("UUTESTS_BINARY_PATH", TESTS_BINARY); + } +} #[cfg(feature = "pwdx")] #[path = "by-util/test_pwdx.rs"]