From 8e9cac5e66ea169e0e36e7f191b1b3d2863e0388 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 26 Jan 2024 20:18:43 +0100 Subject: [PATCH] create skeleton & pwdx as poc --- Cargo.lock | 768 ++++++++ Cargo.toml | 103 ++ build.rs | 101 ++ src/bin/procps.rs | 224 +++ src/bin/uudoc.rs | 361 ++++ src/uu/pwdx/Cargo.toml | 17 + src/uu/pwdx/pwdx.md | 7 + src/uu/pwdx/src/main.rs | 1 + src/uu/pwdx/src/pwdx.rs | 52 + tests/by-util/test_pwdx.rs | 14 + tests/common/macros.rs | 93 + tests/common/mod.rs | 8 + tests/common/random.rs | 339 ++++ tests/common/util.rs | 3418 ++++++++++++++++++++++++++++++++++++ tests/tests.rs | 22 + 15 files changed, 5528 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/bin/procps.rs create mode 100644 src/bin/uudoc.rs create mode 100644 src/uu/pwdx/Cargo.toml create mode 100644 src/uu/pwdx/pwdx.md create mode 100644 src/uu/pwdx/src/main.rs create mode 100644 src/uu/pwdx/src/pwdx.rs create mode 100644 tests/by-util/test_pwdx.rs create mode 100644 tests/common/macros.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/common/random.rs create mode 100644 tests/common/util.rs create mode 100644 tests/tests.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..ddb27e9e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,768 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size 0.3.0", +] + +[[package]] +name = "clap_complete" +version = "4.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df631ae429f6613fcd3a7c1adbdb65f637271e561b03680adaa6573015dfb106" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clap_mangen" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a7c2b01e5e779c19f46a94bbd398f33ae63b0f78c07108351fb4536845bb7fd" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "os_display" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +dependencies = [ + "bitflags 2.4.2", + "hex", + "lazy_static", + "procfs-core", + "rustix 0.38.30", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.4.2", + "hex", +] + +[[package]] +name = "procps" +version = "0.0.1" +dependencies = [ + "clap", + "clap_complete", + "clap_mangen", + "libc", + "phf", + "phf_codegen", + "pretty_assertions", + "procfs", + "rand", + "regex", + "rlimit", + "tempfile", + "textwrap", + "uu_pwdx", + "uucore", + "xattr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rlimit" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3560f70f30a0f16d11d01ed078a07740fe6b489667abc7c7b029155d9f21c3d8" +dependencies = [ + "libc", +] + +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.27", + "windows-sys 0.48.0", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix 0.38.30", + "windows-sys 0.48.0", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "terminal_size 0.2.6", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uu_pwdx" +version = "0.0.1" +dependencies = [ + "clap", + "uucore", +] + +[[package]] +name = "uucore" +version = "0.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5de2eba1364f6274f35f121eb8671b98ac5fa8fe1271694721e17034e85e8bc" +dependencies = [ + "clap", + "glob", + "libc", + "nix", + "once_cell", + "os_display", + "uucore_procs", + "wild", +] + +[[package]] +name = "uucore_procs" +version = "0.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb9aeeb06d1f15c5b3b51acddddf3436e3e1480902b2a200618ca5dbb24e392" +dependencies = [ + "proc-macro2", + "quote", + "uuhelp_parser", +] + +[[package]] +name = "uuhelp_parser" +version = "0.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d841f8408028085ca65896cdd60b9925d4e407cb69989a64889f2bebbb51147b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wild" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67" +dependencies = [ + "glob", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.13", + "rustix 0.38.30", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..51fab511 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,103 @@ +# procps (uutils) +# * see the repository LICENSE, README, and CONTRIBUTING files for more information + +# spell-checker:ignore (libs) bigdecimal datetime fundu gethostid kqueue libselinux mangen memmap procfs uuhelp + +[package] +name = "procps" +version = "0.0.1" +authors = ["uutils developers"] +license = "MIT" +description = "procps ~ GNU procps (updated); implemented as universal (cross-platform) utils, written in Rust" +default-run = "procps" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps" +readme = "README.md" +keywords = ["procps", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] +rust-version = "1.70.0" +edition = "2021" + +build = "build.rs" + +[features] +default = ["feat_common_core"] + +feat_common_core = [ + "pwdx", +] + +[workspace.dependencies] +uucore = "0.0.24" +clap = { version = "4.4", features = ["wrap_help", "cargo"] } +clap_complete = "4.4" +clap_mangen = "0.2" +regex = "1.10.2" +sysinfo = "0.30" +libc = "0.2.152" +phf = "0.11.2" +phf_codegen = "0.11.2" +textwrap = { version = "0.16.0", features = ["terminal_size"] } +xattr = "1.3.1" +tempfile = "3.9.0" +rand = { version = "0.8", features = ["small_rng"] } + +[dependencies] +clap = { workspace = true } +clap_complete = { workspace = true } +clap_mangen = { workspace = true } +uucore = { workspace = true } +phf = { workspace = true } +textwrap = { workspace = true } + + +# +pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" } + +[dev-dependencies] +pretty_assertions = "1" +regex = { workspace = true } +tempfile = { workspace = true } +libc = { workspace = true } +rand = { workspace = true } +uucore = { workspace = true, features = ["entries", "process", "signals"] } + +[target.'cfg(unix)'.dev-dependencies] +xattr = { workspace = true } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] +procfs = { version = "0.16", default-features = false } +rlimit = "0.10.1" + +[build-dependencies] +phf_codegen = { workspace = true } + + +[[bin]] +name = "procps" +path = "src/bin/procps.rs" + +[[bin]] +name = "uudoc" +path = "src/bin/uudoc.rs" +required-features = ["uudoc"] + +# The default release profile. It contains all optimizations, without +# sacrificing debug info. With this profile (like in the standard +# release profile), the debug info and the stack traces will still be available. +[profile.release] +lto = true + +# A release-like profile that is tuned to be fast, even when being fast +# compromises on binary size. This includes aborting on panic. +[profile.release-fast] +inherits = "release" +panic = "abort" + +# A release-like profile that is as small as possible. +[profile.release-small] +inherits = "release" +opt-level = "z" +panic = "abort" +strip = true diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..bb4e2b53 --- /dev/null +++ b/build.rs @@ -0,0 +1,101 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (vars) krate + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +pub fn main() { + if let Ok(profile) = env::var("PROFILE") { + println!("cargo:rustc-cfg=build={profile:?}"); + } + + const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_"; + const FEATURE_PREFIX: &str = "feat_"; + const OVERRIDE_PREFIX: &str = "uu_"; + + let out_dir = env::var("OUT_DIR").unwrap(); + + let mut crates = Vec::new(); + for (key, val) in env::vars() { + if val == "1" && key.starts_with(ENV_FEATURE_PREFIX) { + let krate = key[ENV_FEATURE_PREFIX.len()..].to_lowercase(); + // Allow this as we have a bunch of info in the comments + #[allow(clippy::match_same_arms)] + match krate.as_ref() { + "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names + "nightly" | "test_unimplemented" => continue, // crate-local custom features + "uudoc" => continue, // is not a utility + "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' + s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets + _ => {} // util feature name + } + crates.push(krate); + } + } + crates.sort(); + + let mut mf = File::create(Path::new(&out_dir).join("uutils_map.rs")).unwrap(); + + mf.write_all( + "type UtilityMap = phf::OrderedMap<&'static str, (fn(T) -> i32, fn() -> Command)>;\n\ + \n\ + #[allow(clippy::too_many_lines)] + fn util_map() -> UtilityMap {\n" + .as_bytes(), + ) + .unwrap(); + + let mut phf_map = phf_codegen::OrderedMap::<&str>::new(); + for krate in &crates { + let map_value = format!("({krate}::uumain, {krate}::uu_app)"); + match krate.as_ref() { + // 'test' is named uu_test to avoid collision with rust core crate 'test'. + // It can also be invoked by name '[' for the '[ expr ] syntax'. + "uu_test" => { + phf_map.entry("test", &map_value); + phf_map.entry("[", &map_value); + } + k if k.starts_with(OVERRIDE_PREFIX) => { + phf_map.entry(&k[OVERRIDE_PREFIX.len()..], &map_value); + } + "false" | "true" => { + phf_map.entry(krate, &format!("(r#{krate}::uumain, r#{krate}::uu_app)")); + } + "hashsum" => { + phf_map.entry(krate, &format!("({krate}::uumain, {krate}::uu_app_custom)")); + + let map_value = format!("({krate}::uumain, {krate}::uu_app_common)"); + let map_value_bits = format!("({krate}::uumain, {krate}::uu_app_bits)"); + let map_value_b3sum = format!("({krate}::uumain, {krate}::uu_app_b3sum)"); + phf_map.entry("md5sum", &map_value); + phf_map.entry("sha1sum", &map_value); + phf_map.entry("sha224sum", &map_value); + phf_map.entry("sha256sum", &map_value); + phf_map.entry("sha384sum", &map_value); + phf_map.entry("sha512sum", &map_value); + phf_map.entry("sha3sum", &map_value_bits); + phf_map.entry("sha3-224sum", &map_value); + phf_map.entry("sha3-256sum", &map_value); + phf_map.entry("sha3-384sum", &map_value); + phf_map.entry("sha3-512sum", &map_value); + phf_map.entry("shake128sum", &map_value_bits); + phf_map.entry("shake256sum", &map_value_bits); + phf_map.entry("b2sum", &map_value); + phf_map.entry("b3sum", &map_value_b3sum); + } + _ => { + phf_map.entry(krate, &map_value); + } + } + } + write!(mf, "{}", phf_map.build()).unwrap(); + mf.write_all(b"\n}\n").unwrap(); + + mf.flush().unwrap(); +} diff --git a/src/bin/procps.rs b/src/bin/procps.rs new file mode 100644 index 00000000..fc2cd16a --- /dev/null +++ b/src/bin/procps.rs @@ -0,0 +1,224 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore manpages mangen + +use clap::{Arg, Command}; +use clap_complete::Shell; +use std::cmp; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process; +use uucore::display::Quotable; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); + +fn usage(utils: &UtilityMap, name: &str) { + println!("{name} {VERSION} (multi-call binary)\n"); + println!("Usage: {name} [function [arguments...]]\n"); + println!("Currently defined functions:\n"); + #[allow(clippy::map_clone)] + let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); + utils.sort_unstable(); + let display_list = utils.join(", "); + let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions + println!( + "{}", + textwrap::indent(&textwrap::fill(&display_list, width), " ") + ); +} + +fn binary_path(args: &mut impl Iterator) -> PathBuf { + match args.next() { + Some(ref s) if !s.is_empty() => PathBuf::from(s), + _ => std::env::current_exe().unwrap(), + } +} + +fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() +} + +#[allow(clippy::cognitive_complexity)] +fn main() { + uucore::panic::mute_sigpipe_panic(); + + let utils = util_map(); + let mut args = uucore::args_os(); + + let binary = binary_path(&mut args); + let binary_as_util = name(&binary).unwrap_or_else(|| { + usage(&utils, ""); + process::exit(0); + }); + + // binary name equals util name? + if let Some(&(uumain, _)) = utils.get(binary_as_util) { + process::exit(uumain((vec![binary.into()].into_iter()).chain(args))); + } + + // binary name equals prefixed util name? + // * prefix/stem may be any string ending in a non-alphanumeric character + let util_name = if let Some(util) = utils.keys().find(|util| { + binary_as_util.ends_with(*util) + && !binary_as_util[..binary_as_util.len() - (*util).len()] + .ends_with(char::is_alphanumeric) + }) { + // prefixed util => replace 0th (aka, executable name) argument + Some(OsString::from(*util)) + } else { + // unmatched binary name => regard as multi-binary container and advance argument list + uucore::set_utility_is_second_arg(); + args.next() + }; + + // 0th argument equals util name? + if let Some(util_os) = util_name { + fn not_found(util: &OsStr) -> ! { + println!("{}: function/utility not found", util.maybe_quote()); + process::exit(1); + } + + let util = match util_os.to_str() { + Some(util) => util, + None => not_found(&util_os), + }; + + if util == "completion" { + gen_completions(args, &utils); + } + + if util == "manpage" { + gen_manpage(args, &utils); + } + + match utils.get(util) { + Some(&(uumain, _)) => { + process::exit(uumain((vec![util_os].into_iter()).chain(args))); + } + None => { + if util == "--help" || util == "-h" { + // see if they want help on a specific util + if let Some(util_os) = args.next() { + let util = match util_os.to_str() { + Some(util) => util, + None => not_found(&util_os), + }; + + match utils.get(util) { + Some(&(uumain, _)) => { + let code = uumain( + (vec![util_os, OsString::from("--help")].into_iter()) + .chain(args), + ); + io::stdout().flush().expect("could not flush stdout"); + process::exit(code); + } + None => not_found(&util_os), + } + } + usage(&utils, binary_as_util); + process::exit(0); + } else { + not_found(&util_os); + } + } + } + } else { + // no arguments provided + usage(&utils, binary_as_util); + process::exit(0); + } +} + +/// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout +fn gen_completions( + args: impl Iterator, + util_map: &UtilityMap, +) -> ! { + let all_utilities: Vec<_> = std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect(); + + let matches = Command::new("completion") + .about("Prints completions to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) + .required(true), + ) + .arg( + Arg::new("shell") + .value_parser(clap::builder::EnumValueParser::::new()) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + let shell = *matches.get_one::("shell").unwrap(); + + let mut command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + util_map.get(utility).unwrap().1() + }; + let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; + + clap_complete::generate(shell, &mut command, bin_name, &mut io::stdout()); + io::stdout().flush().unwrap(); + process::exit(0); +} + +/// Generate the manpage for the utility in the first parameter +fn gen_manpage( + args: impl Iterator, + util_map: &UtilityMap, +) -> ! { + let all_utilities: Vec<_> = std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect(); + + let matches = Command::new("manpage") + .about("Prints manpage to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("manpage")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + + let command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + util_map.get(utility).unwrap().1() + }; + + let man = clap_mangen::Man::new(command); + man.render(&mut io::stdout()) + .expect("Man page generation failed"); + io::stdout().flush().unwrap(); + process::exit(0); +} + +fn gen_coreutils_app(util_map: &UtilityMap) -> Command { + let mut command = Command::new("coreutils"); + for (name, (_, sub_app)) in util_map { + // Recreate a small subcommand with only the relevant info + // (name & short description) + let about = sub_app() + .get_about() + .expect("Could not get the 'about'") + .to_string(); + let sub_app = Command::new(name).about(about); + command = command.subcommand(sub_app); + } + command +} diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs new file mode 100644 index 00000000..77c7a2fc --- /dev/null +++ b/src/bin/uudoc.rs @@ -0,0 +1,361 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore tldr uuhelp + +use clap::Command; +use std::collections::HashMap; +use std::ffi::OsString; +use std::fs::File; +use std::io::{self, Read, Seek, Write}; +use zip::ZipArchive; + +include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); + +fn main() -> io::Result<()> { + let mut tldr_zip = File::open("docs/tldr.zip") + .ok() + .and_then(|f| ZipArchive::new(f).ok()); + + if tldr_zip.is_none() { + println!("Warning: No tldr archive found, so the documentation will not include examples."); + println!("To include examples in the documentation, download the tldr archive and put it in the docs/ folder."); + println!(); + println!(" curl https://tldr.sh/assets/tldr.zip -o docs/tldr.zip"); + println!(); + } + + let utils = util_map::>>(); + match std::fs::create_dir("docs/src/utils/") { + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + x => x, + }?; + + println!("Writing initial info to SUMMARY.md"); + let mut summary = File::create("docs/src/SUMMARY.md")?; + + let _ = write!( + summary, + "# Summary\n\ + \n\ + [Introduction](index.md)\n\ + * [Installation](installation.md)\n\ + * [Build from source](build.md)\n\ + * [Platform support](platforms.md)\n\ + * [Contributing](contributing.md)\n\ + * [GNU test coverage](test_coverage.md)\n\ + * [Extensions](extensions.md)\n\ + \n\ + # Reference\n\ + * [Multi-call binary](multicall.md)\n", + ); + + println!("Gathering utils per platform"); + let utils_per_platform = { + let mut map = HashMap::new(); + for platform in ["unix", "macos", "windows", "unix_android"] { + let platform_utils: Vec = String::from_utf8( + std::process::Command::new("./util/show-utils.sh") + .arg(format!("--features=feat_os_{}", platform)) + .output()? + .stdout, + ) + .unwrap() + .trim() + .split(' ') + .map(ToString::to_string) + .collect(); + map.insert(platform, platform_utils); + } + + // Linux is a special case because it can support selinux + let platform_utils: Vec = String::from_utf8( + std::process::Command::new("./util/show-utils.sh") + .arg("--features=feat_os_unix feat_selinux") + .output()? + .stdout, + ) + .unwrap() + .trim() + .split(' ') + .map(ToString::to_string) + .collect(); + map.insert("linux", platform_utils); + + map + }; + + let mut utils = utils.entries().collect::>(); + utils.sort(); + + println!("Writing util per platform table"); + { + let mut platform_table_file = File::create("docs/src/platform_table.md").unwrap(); + + // sum, cksum, b2sum, etc. are all available on all platforms, but not in the data structure + // otherwise, we check the map for the util name. + let check_supported = |name: &str, platform: &str| { + if name.ends_with("sum") || utils_per_platform[platform].iter().any(|u| u == name) { + "✓" + } else { + " " + } + }; + writeln!( + platform_table_file, + "| util | Linux | macOS | Windows | FreeBSD | Android |\n\ + | ---------------- | ----- | ----- | ------- | ------- | ------- |" + )?; + for (&name, _) in &utils { + if name == "[" { + continue; + } + // The alignment is not necessary, but makes the output a bit more + // pretty when viewed as plain markdown. + writeln!( + platform_table_file, + "| {:<16} | {:<5} | {:<5} | {:<7} | {:<7} | {:<7} |", + format!("**{name}**"), + check_supported(name, "linux"), + check_supported(name, "macos"), + check_supported(name, "windows"), + check_supported(name, "unix"), + check_supported(name, "unix_android"), + )?; + } + } + + println!("Writing to utils"); + for (&name, (_, command)) in utils { + if name == "[" { + continue; + } + let p = format!("docs/src/utils/{}.md", name); + + let markdown = File::open(format!("src/uu/{name}/{name}.md")) + .and_then(|mut f: File| { + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(s) + }) + .ok(); + + if let Ok(f) = File::create(&p) { + MDWriter { + w: Box::new(f), + command: command(), + name, + tldr_zip: &mut tldr_zip, + utils_per_platform: &utils_per_platform, + markdown, + } + .markdown()?; + println!("Wrote to '{}'", p); + } else { + println!("Error writing to {}", p); + } + writeln!(summary, "* [{0}](utils/{0}.md)", name)?; + } + Ok(()) +} + +struct MDWriter<'a, 'b> { + w: Box, + command: Command, + name: &'a str, + tldr_zip: &'b mut Option>, + utils_per_platform: &'b HashMap<&'b str, Vec>, + markdown: Option, +} + +impl<'a, 'b> MDWriter<'a, 'b> { + fn markdown(&mut self) -> io::Result<()> { + write!(self.w, "# {}\n\n", self.name)?; + self.additional()?; + self.usage()?; + self.about()?; + self.options()?; + self.after_help()?; + self.examples() + } + + fn additional(&mut self) -> io::Result<()> { + writeln!(self.w, "
")?; + self.platforms()?; + self.version()?; + writeln!(self.w, "
") + } + + fn platforms(&mut self) -> io::Result<()> { + writeln!(self.w, "
")?; + for (feature, icon) in [ + ("linux", "linux"), + // freebsd is disabled for now because mdbook does not use font-awesome 5 yet. + // ("unix", "freebsd"), + ("macos", "apple"), + ("windows", "windows"), + ] { + if self.name.contains("sum") + || self.utils_per_platform[feature] + .iter() + .any(|u| u == self.name) + { + writeln!(self.w, "", icon)?; + } + } + writeln!(self.w, "
")?; + + Ok(()) + } + + fn version(&mut self) -> io::Result<()> { + writeln!( + self.w, + "
v{}
", + self.command.render_version().split_once(' ').unwrap().1 + ) + } + + fn usage(&mut self) -> io::Result<()> { + if let Some(markdown) = &self.markdown { + let usage = uuhelp_parser::parse_usage(markdown); + let usage = usage.replace("{}", self.name); + + writeln!(self.w, "\n```")?; + writeln!(self.w, "{}", usage)?; + writeln!(self.w, "```") + } else { + Ok(()) + } + } + + fn about(&mut self) -> io::Result<()> { + if let Some(markdown) = &self.markdown { + writeln!(self.w, "{}", uuhelp_parser::parse_about(markdown)) + } else { + Ok(()) + } + } + + fn after_help(&mut self) -> io::Result<()> { + if let Some(markdown) = &self.markdown { + if let Some(after_help) = uuhelp_parser::parse_section("after help", markdown) { + return writeln!(self.w, "\n\n{after_help}"); + } + } + + Ok(()) + } + + fn examples(&mut self) -> io::Result<()> { + if let Some(zip) = self.tldr_zip { + let content = if let Some(f) = + get_zip_content(zip, &format!("pages/common/{}.md", self.name)) + { + f + } else if let Some(f) = get_zip_content(zip, &format!("pages/linux/{}.md", self.name)) { + f + } else { + println!( + "Warning: Could not find tldr examples for page '{}'", + self.name + ); + return Ok(()); + }; + + writeln!(self.w, "## Examples")?; + writeln!(self.w)?; + for line in content.lines().skip_while(|l| !l.starts_with('-')) { + if let Some(l) = line.strip_prefix("- ") { + writeln!(self.w, "{}", l)?; + } else if line.starts_with('`') { + writeln!(self.w, "```shell\n{}\n```", line.trim_matches('`'))?; + } else if line.is_empty() { + writeln!(self.w)?; + } else { + println!("Not sure what to do with this line:"); + println!("{}", line); + } + } + writeln!(self.w)?; + writeln!( + self.w, + "> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)." + )?; + writeln!(self.w, ">")?; + writeln!( + self.w, + "> Please note that, as uutils is a work in progress, some examples might fail." + )?; + } + Ok(()) + } + + fn options(&mut self) -> io::Result<()> { + writeln!(self.w, "

Options

")?; + write!(self.w, "
")?; + for arg in self.command.get_arguments() { + write!(self.w, "
")?; + let mut first = true; + for l in arg.get_long_and_visible_aliases().unwrap_or_default() { + if first { + first = false; + } else { + write!(self.w, ", ")?; + } + write!(self.w, "")?; + write!(self.w, "--{}", l)?; + if let Some(names) = arg.get_value_names() { + write!( + self.w, + "={}", + names + .iter() + .map(|x| format!("<{}>", x)) + .collect::>() + .join(" ") + )?; + } + write!(self.w, "")?; + } + for s in arg.get_short_and_visible_aliases().unwrap_or_default() { + if first { + first = false; + } else { + write!(self.w, ", ")?; + } + write!(self.w, "")?; + write!(self.w, "-{}", s)?; + if let Some(names) = arg.get_value_names() { + write!( + self.w, + " {}", + names + .iter() + .map(|x| format!("<{}>", x)) + .collect::>() + .join(" ") + )?; + } + write!(self.w, "")?; + } + writeln!(self.w, "
")?; + writeln!( + self.w, + "
\n\n{}\n\n
", + arg.get_help() + .unwrap_or_default() + .to_string() + .replace('\n', "
") + )?; + } + writeln!(self.w, "
\n") + } +} + +fn get_zip_content(archive: &mut ZipArchive, name: &str) -> Option { + let mut s = String::new(); + archive.by_name(name).ok()?.read_to_string(&mut s).unwrap(); + Some(s) +} diff --git a/src/uu/pwdx/Cargo.toml b/src/uu/pwdx/Cargo.toml new file mode 100644 index 00000000..1a57651e --- /dev/null +++ b/src/uu/pwdx/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uu_pwdx" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } + +[lib] +path = "src/pwdx.rs" + +[[bin]] +name = "pwdx" +path = "src/main.rs" diff --git a/src/uu/pwdx/pwdx.md b/src/uu/pwdx/pwdx.md new file mode 100644 index 00000000..0efb6d6f --- /dev/null +++ b/src/uu/pwdx/pwdx.md @@ -0,0 +1,7 @@ +# pwdx + +``` +pwdx [options] pid [...] +``` + +Report current working directory of a process \ No newline at end of file diff --git a/src/uu/pwdx/src/main.rs b/src/uu/pwdx/src/main.rs new file mode 100644 index 00000000..ee1a3361 --- /dev/null +++ b/src/uu/pwdx/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_pwdx); diff --git a/src/uu/pwdx/src/pwdx.rs b/src/uu/pwdx/src/pwdx.rs new file mode 100644 index 00000000..b6c4a1bd --- /dev/null +++ b/src/uu/pwdx/src/pwdx.rs @@ -0,0 +1,52 @@ +// 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 clap::Arg; +use clap::{crate_version, Command}; +use std::env; +use std::fs; +use std::path::Path; +use std::process; + +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("pwdx.md"); +const USAGE: &str = help_usage!("pwdx.md"); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + + let matches = uu_app().try_get_matches_from(args)?; + + let pid_str = matches.get_one::("pid").unwrap(); + let pid = pid_str.parse::().unwrap_or_else(|_| { + eprintln!("Invalid PID"); + process::exit(1); + }); + + let cwd_link = format!("/proc/{}/cwd", pid); + + match fs::read_link(Path::new(&cwd_link)) { + Ok(path) => println!("{}: {}", pid, path.display()), + Err(e) => { + eprintln!("pwdx: failed to read link for PID {}: {}", pid, e); + process::exit(1); + } + } + Ok(()) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg(Arg::new("pid") + .value_name("PID") + .help("Process ID") + .required(true) + .index(1)) +} diff --git a/tests/by-util/test_pwdx.rs b/tests/by-util/test_pwdx.rs new file mode 100644 index 00000000..ca96aff5 --- /dev/null +++ b/tests/by-util/test_pwdx.rs @@ -0,0 +1,14 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore (words) symdir somefakedir + +use std::path::PathBuf; + +use crate::common::util::{TestScenario, UCommand}; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} diff --git a/tests/common/macros.rs b/tests/common/macros.rs new file mode 100644 index 00000000..4902ca49 --- /dev/null +++ b/tests/common/macros.rs @@ -0,0 +1,93 @@ +// This file is part of the uutils coreutils 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 coreutils 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 new file mode 100644 index 00000000..05e2b138 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,8 @@ +// This file is part of the uutils coreutils 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 new file mode 100644 index 00000000..42b6eaa7 --- /dev/null +++ b/tests/common/random.rs @@ -0,0 +1,339 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use rand::distributions::{Distribution, Uniform}; +use rand::{thread_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.gen_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, + { + thread_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(thread_rng().sample(&dist) as char) + }; + } + + let samples = length - 1; + let mut result: Vec = thread_rng().sample_iter(&dist).take(samples).collect(); + + if num_delimiter == 0 { + result.push(thread_rng().sample(&dist)); + return String::from_utf8(result).unwrap(); + } + + let num_delimiter = if end_with_delimiter { + num_delimiter - 1 + } else { + num_delimiter + }; + + let between = Uniform::new(0, samples); + for _ in 0..num_delimiter { + let mut pos = between.sample(&mut thread_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(thread_rng().sample(&dist)); + } + + String::from_utf8(result).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::distributions::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 new file mode 100644 index 00000000..b5bd4f55 --- /dev/null +++ b/tests/common/util.rs @@ -0,0 +1,3418 @@ +// 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)] + +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; +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) -> &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) -> &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) -> &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) -> &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() + .write(true) + .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() + .write(true) + .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.to_string()); + 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.to_string()); + 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 new file mode 100644 index 00000000..44e3fdda --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,22 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +#[macro_use] +mod common; + +#[cfg(feature = "lscpu")] +#[path = "by-util/test_lscpu.rs"] +mod test_lscpu; + +#[cfg(feature = "pwdx")] +#[path = "by-util/test_pwdx.rs"] +mod test_pwdx; + +#[cfg(feature = "mountpoint")] +#[path = "by-util/test_mountpoint.rs"] +mod test_mountpoint; + +#[cfg(feature = "renice")] +#[path = "by-util/test_renice.rs"] +mod test_renice;