1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

Merge pull request #4293 from Joining7943/tests-util-refactor-ucommand-add-run-in-shell

`tests/util`: Small Refactor/Fixes of `UCommand` and add method to run a `UCommand` in a shell platform independently
This commit is contained in:
Terts Diepraam 2023-02-21 22:41:20 +01:00 committed by GitHub
commit 6bd42fde6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 406 additions and 248 deletions

4
Cargo.lock generated
View file

@ -1867,9 +1867,9 @@ dependencies = [
[[package]] [[package]]
name = "rlimit" name = "rlimit"
version = "0.8.3" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7278a1ec8bfd4a4e07515c589f5ff7b309a373f987393aef44813d9dcf87aa3" checksum = "f8a29d87a652dc4d43c586328706bb5cdff211f3f39a530f240b53f7221dab8e"
dependencies = [ dependencies = [
"libc", "libc",
] ]

View file

@ -482,7 +482,7 @@ rstest = "0.16.0"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies]
procfs = { version = "0.14", default-features = false } procfs = { version = "0.14", default-features = false }
rlimit = "0.8.3" rlimit = "0.9.1"
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]
nix = { workspace=true, features=["process", "signal", "user"] } nix = { workspace=true, features=["process", "signal", "user"] }

View file

@ -103,7 +103,7 @@ fn test_closes_file_descriptors() {
"alpha.txt", "alpha.txt",
"alpha.txt", "alpha.txt",
]) ])
.with_limit(Resource::NOFILE, 9, 9) .limit(Resource::NOFILE, 9, 9)
.succeeds(); .succeeds();
} }

View file

@ -48,15 +48,12 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) {
let r = ucmd.run(); let r = ucmd.run();
if !r.succeeded() { if !r.succeeded() {
println!("{}", r.stderr_str()); println!("{}", r.stderr_str());
panic!("{:?}: failed", ucmd.raw); panic!("{ucmd}: failed");
} }
let perms = at.metadata(TEST_FILE).permissions().mode(); let perms = at.metadata(TEST_FILE).permissions().mode();
if perms != test.after { if perms != test.after {
panic!( panic!("{}: expected: {:o} got: {:o}", ucmd, test.after, perms);
"{:?}: expected: {:o} got: {:o}",
ucmd.raw, test.after, perms
);
} }
} }

View file

@ -396,7 +396,7 @@ fn test_chown_only_user_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run(); let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -430,7 +430,7 @@ fn test_chown_fail_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run(); let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -487,7 +487,7 @@ fn test_chown_only_group_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-g").run(); let result = scene.cmd("id").keep_env().arg("-g").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -551,14 +551,14 @@ fn test_chown_owner_group_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run(); let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
let user_id = String::from(result.stdout_str().trim()); let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty()); assert!(!user_id.is_empty());
let result = scene.cmd_keepenv("id").arg("-g").run(); let result = scene.cmd("id").keep_env().arg("-g").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -612,14 +612,14 @@ fn test_chown_owner_group_mix() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run(); let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
let user_id = String::from(result.stdout_str().trim()); let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty()); assert!(!user_id.is_empty());
let result = scene.cmd_keepenv("id").arg("-gn").run(); let result = scene.cmd("id").keep_env().arg("-gn").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }

View file

@ -1545,7 +1545,7 @@ fn test_closes_file_descriptors() {
.arg("--reflink=auto") .arg("--reflink=auto")
.arg("dir_with_10_files/") .arg("dir_with_10_files/")
.arg("dir_with_10_files_new/") .arg("dir_with_10_files_new/")
.with_limit(Resource::NOFILE, limit_fd, limit_fd) .limit(Resource::NOFILE, limit_fd, limit_fd)
.succeeds(); .succeeds();
} }
@ -1692,7 +1692,8 @@ fn test_cp_reflink_always_override() {
.succeeds(); .succeeds();
if !scene if !scene
.cmd_keepenv("env") .cmd("env")
.keep_env()
.args(&["mkfs.btrfs", "--rootdir", ROOTDIR, DISK]) .args(&["mkfs.btrfs", "--rootdir", ROOTDIR, DISK])
.run() .run()
.succeeded() .succeeded()
@ -1704,7 +1705,8 @@ fn test_cp_reflink_always_override() {
scene.fixtures.mkdir(MOUNTPOINT); scene.fixtures.mkdir(MOUNTPOINT);
let mount = scene let mount = scene
.cmd_keepenv("sudo") .cmd("sudo")
.keep_env()
.args(&["-E", "--non-interactive", "mount", DISK, MOUNTPOINT]) .args(&["-E", "--non-interactive", "mount", DISK, MOUNTPOINT])
.run(); .run();
@ -1730,7 +1732,8 @@ fn test_cp_reflink_always_override() {
.succeeds(); .succeeds();
scene scene
.cmd_keepenv("sudo") .cmd("sudo")
.keep_env()
.args(&["-E", "--non-interactive", "umount", MOUNTPOINT]) .args(&["-E", "--non-interactive", "umount", MOUNTPOINT])
.succeeds(); .succeeds();
} }
@ -2524,9 +2527,9 @@ fn test_src_base_dot() {
let at = ts.fixtures.clone(); let at = ts.fixtures.clone();
at.mkdir("x"); at.mkdir("x");
at.mkdir("y"); at.mkdir("y");
let mut ucmd = UCommand::new(ts.bin_path, &Some(ts.util_name), at.plus("y"), true); ts.ucmd()
.current_dir(at.plus("y"))
ucmd.args(&["--verbose", "-r", "../x/.", "."]) .args(&["--verbose", "-r", "../x/.", "."])
.succeeds() .succeeds()
.no_stderr() .no_stderr()
.no_stdout(); .no_stdout();

View file

@ -156,7 +156,8 @@ fn test_unset_variable() {
// This test depends on the HOME variable being pre-defined by the // This test depends on the HOME variable being pre-defined by the
// default shell // default shell
let out = TestScenario::new(util_name!()) let out = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-u") .arg("-u")
.arg("HOME") .arg("HOME")
.succeeds() .succeeds()

View file

@ -425,7 +425,8 @@ fn test_mktemp_tmpdir_one_arg() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let result = scene let result = scene
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--tmpdir") .arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX") .arg("apt-key-gpghome.XXXXXXXXXX")
.succeeds(); .succeeds();
@ -438,7 +439,8 @@ fn test_mktemp_directory_tmpdir() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let result = scene let result = scene
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--directory") .arg("--directory")
.arg("--tmpdir") .arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX") .arg("apt-key-gpghome.XXXXXXXXXX")

View file

@ -20,7 +20,8 @@ fn test_nproc_all_omp() {
assert!(nproc > 0); assert!(nproc > 0);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "60") .env("OMP_NUM_THREADS", "60")
.succeeds(); .succeeds();
@ -28,7 +29,8 @@ fn test_nproc_all_omp() {
assert_eq!(nproc_omp, 60); assert_eq!(nproc_omp, 60);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "1") // Has no effect .env("OMP_NUM_THREADS", "1") // Has no effect
.arg("--all") .arg("--all")
.succeeds(); .succeeds();
@ -37,7 +39,8 @@ fn test_nproc_all_omp() {
// If the parsing fails, returns the number of CPU // If the parsing fails, returns the number of CPU
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "incorrectnumber") // returns the number CPU .env("OMP_NUM_THREADS", "incorrectnumber") // returns the number CPU
.succeeds(); .succeeds();
let nproc_omp: u8 = result.stdout_str().trim().parse().unwrap(); let nproc_omp: u8 = result.stdout_str().trim().parse().unwrap();
@ -51,7 +54,8 @@ fn test_nproc_ignore() {
if nproc_total > 1 { if nproc_total > 1 {
// Ignore all CPU but one // Ignore all CPU but one
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--ignore") .arg("--ignore")
.arg((nproc_total - 1).to_string()) .arg((nproc_total - 1).to_string())
.succeeds(); .succeeds();
@ -59,7 +63,8 @@ fn test_nproc_ignore() {
assert_eq!(nproc, 1); assert_eq!(nproc, 1);
// Ignore all CPU but one with a string // Ignore all CPU but one with a string
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--ignore= 1") .arg("--ignore= 1")
.succeeds(); .succeeds();
let nproc: u8 = result.stdout_str().trim().parse().unwrap(); let nproc: u8 = result.stdout_str().trim().parse().unwrap();
@ -70,7 +75,8 @@ fn test_nproc_ignore() {
#[test] #[test]
fn test_nproc_ignore_all_omp() { fn test_nproc_ignore_all_omp() {
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42") .env("OMP_NUM_THREADS", "42")
.arg("--ignore=40") .arg("--ignore=40")
.succeeds(); .succeeds();
@ -81,7 +87,8 @@ fn test_nproc_ignore_all_omp() {
#[test] #[test]
fn test_nproc_omp_limit() { fn test_nproc_omp_limit() {
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42") .env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "0") .env("OMP_THREAD_LIMIT", "0")
.succeeds(); .succeeds();
@ -89,7 +96,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, 42); assert_eq!(nproc, 42);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42") .env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "2") .env("OMP_THREAD_LIMIT", "2")
.succeeds(); .succeeds();
@ -97,7 +105,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, 2); assert_eq!(nproc, 2);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42") .env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "2bad") .env("OMP_THREAD_LIMIT", "2bad")
.succeeds(); .succeeds();
@ -109,14 +118,16 @@ fn test_nproc_omp_limit() {
assert!(nproc_system > 0); assert!(nproc_system > 0);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_THREAD_LIMIT", "1") .env("OMP_THREAD_LIMIT", "1")
.succeeds(); .succeeds();
let nproc: u8 = result.stdout_str().trim().parse().unwrap(); let nproc: u8 = result.stdout_str().trim().parse().unwrap();
assert_eq!(nproc, 1); assert_eq!(nproc, 1);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "0") .env("OMP_NUM_THREADS", "0")
.env("OMP_THREAD_LIMIT", "") .env("OMP_THREAD_LIMIT", "")
.succeeds(); .succeeds();
@ -124,7 +135,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, nproc_system); assert_eq!(nproc, nproc_system);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "") .env("OMP_NUM_THREADS", "")
.env("OMP_THREAD_LIMIT", "") .env("OMP_THREAD_LIMIT", "")
.succeeds(); .succeeds();
@ -132,7 +144,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, nproc_system); assert_eq!(nproc, nproc_system);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1") .env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "") .env("OMP_THREAD_LIMIT", "")
.succeeds(); .succeeds();
@ -140,7 +153,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc); assert_eq!(2, nproc);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,ignored") .env("OMP_NUM_THREADS", "2,ignored")
.env("OMP_THREAD_LIMIT", "") .env("OMP_THREAD_LIMIT", "")
.succeeds(); .succeeds();
@ -148,7 +162,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc); assert_eq!(2, nproc);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1") .env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "0") .env("OMP_THREAD_LIMIT", "0")
.succeeds(); .succeeds();
@ -156,7 +171,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc); assert_eq!(2, nproc);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1") .env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "1bad") .env("OMP_THREAD_LIMIT", "1bad")
.succeeds(); .succeeds();
@ -164,7 +180,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc); assert_eq!(2, nproc);
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "29,2,1") .env("OMP_NUM_THREADS", "29,2,1")
.env("OMP_THREAD_LIMIT", "1bad") .env("OMP_THREAD_LIMIT", "1bad")
.succeeds(); .succeeds();

View file

@ -8,7 +8,8 @@ fn test_get_all() {
assert_eq!(env::var(key), Ok("VALUE".to_string())); assert_eq!(env::var(key), Ok("VALUE".to_string()));
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.succeeds() .succeeds()
.stdout_contains("HOME=") .stdout_contains("HOME=")
.stdout_contains("KEY=VALUE"); .stdout_contains("KEY=VALUE");
@ -21,7 +22,8 @@ fn test_get_var() {
assert_eq!(env::var(key), Ok("VALUE".to_string())); assert_eq!(env::var(key), Ok("VALUE".to_string()));
let result = TestScenario::new(util_name!()) let result = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("KEY") .arg("KEY")
.succeeds(); .succeeds();

View file

@ -60,7 +60,7 @@ fn symlinked_env() -> Env {
// Note: on Windows this requires admin permissions // Note: on Windows this requires admin permissions
at.symlink_dir("subdir", "symdir"); at.symlink_dir("subdir", "symdir");
let root = PathBuf::from(at.root_dir_resolved()); let root = PathBuf::from(at.root_dir_resolved());
ucmd.raw.current_dir(root.join("symdir")); ucmd.current_dir(root.join("symdir"));
#[cfg(not(windows))] #[cfg(not(windows))]
ucmd.env("PWD", root.join("symdir")); ucmd.env("PWD", root.join("symdir"));
Env { Env {

View file

@ -31,7 +31,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; let buffer_sizes = ["0", "50K", "50k", "1M", "100M"];
for buffer_size in &buffer_sizes { for buffer_size in &buffer_sizes {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-n") .arg("-n")
.arg("-S") .arg("-S")
.arg(buffer_size) .arg(buffer_size)
@ -44,7 +45,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["1000G", "10T"]; let buffer_sizes = ["1000G", "10T"];
for buffer_size in &buffer_sizes { for buffer_size in &buffer_sizes {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-n") .arg("-n")
.arg("-S") .arg("-S")
.arg(buffer_size) .arg(buffer_size)
@ -918,7 +920,8 @@ fn test_compress_merge() {
fn test_compress_fail() { fn test_compress_fail() {
#[cfg(not(windows))] #[cfg(not(windows))]
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&[ .args(&[
"ext_sort.txt", "ext_sort.txt",
"-n", "-n",
@ -934,7 +937,8 @@ fn test_compress_fail() {
// So, don't check the output // So, don't check the output
#[cfg(windows)] #[cfg(windows)]
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&[ .args(&[
"ext_sort.txt", "ext_sort.txt",
"-n", "-n",
@ -949,7 +953,8 @@ fn test_compress_fail() {
#[test] #[test]
fn test_merge_batches() { fn test_merge_batches() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.timeout(Duration::from_secs(120)) .timeout(Duration::from_secs(120))
.args(&["ext_sort.txt", "-n", "-S", "150b"]) .args(&["ext_sort.txt", "-n", "-S", "150b"])
.succeeds() .succeeds()
@ -959,7 +964,8 @@ fn test_merge_batches() {
#[test] #[test]
fn test_merge_batch_size() { fn test_merge_batch_size() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--batch-size=2") .arg("--batch-size=2")
.arg("-m") .arg("-m")
.arg("--unique") .arg("--unique")
@ -1067,7 +1073,8 @@ fn test_output_is_input() {
at.touch("file"); at.touch("file");
at.append("file", input); at.append("file", input);
scene scene
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&["-m", "-u", "-o", "file", "file", "file", "file"]) .args(&["-m", "-u", "-o", "file", "file", "file", "file"])
.succeeds(); .succeeds();
assert_eq!(at.read("file"), input); assert_eq!(at.read("file"), input);

View file

@ -300,19 +300,15 @@ fn test_invalid_utf8_integer_compare() {
let source = [0x66, 0x6f, 0x80, 0x6f]; let source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]); let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!(); new_ucmd!()
cmd.arg("123").arg("-ne"); .args(&[OsStr::new("123"), OsStr::new("-ne"), arg])
cmd.raw.arg(arg); .run()
cmd.run()
.code_is(2) .code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n"); .stderr_is("test: invalid integer $'fo\\x80o'\n");
let mut cmd = new_ucmd!(); new_ucmd!()
cmd.raw.arg(arg); .args(&[arg, OsStr::new("-eq"), OsStr::new("456")])
cmd.arg("-eq").arg("456"); .run()
cmd.run()
.code_is(2) .code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n"); .stderr_is("test: invalid integer $'fo\\x80o'\n");
} }

View file

@ -10,7 +10,8 @@ fn test_invalid_arg() {
#[test] #[test]
fn test_uptime() { fn test_uptime() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.succeeds() .succeeds()
.stdout_contains("load average:") .stdout_contains("load average:")
.stdout_contains(" up "); .stdout_contains(" up ");

View file

@ -22,7 +22,8 @@ fn test_users_check_name() {
// note: clippy::needless_borrow *false positive* // note: clippy::needless_borrow *false positive*
#[allow(clippy::needless_borrow)] #[allow(clippy::needless_borrow)]
let expected = TestScenario::new(&util_name) let expected = TestScenario::new(&util_name)
.cmd_keepenv(util_name) .cmd(util_name)
.keep_env()
.env("LC_ALL", "C") .env("LC_ALL", "C")
.succeeds() .succeeds()
.stdout_move_str(); .stdout_move_str();

View file

@ -3,19 +3,20 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized
#![allow(dead_code)] #![allow(dead_code)]
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", target_os = "android"))]
use rlimit::prlimit; use rlimit::prlimit;
use rstest::rstest; use rstest::rstest;
#[cfg(unix)] #[cfg(unix)]
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::VecDeque;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::ffi::CString; use std::ffi::CString;
use std::ffi::OsStr; use std::ffi::{OsStr, OsString};
use std::fs::{self, hard_link, remove_file, File, OpenOptions}; use std::fs::{self, hard_link, remove_file, File, OpenOptions};
use std::io::{self, BufWriter, Read, Result, Write}; use std::io::{self, BufWriter, Read, Result, Write};
#[cfg(unix)] #[cfg(unix)]
@ -34,7 +35,6 @@ use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, hint, thread}; use std::{env, hint, thread};
use tempfile::{Builder, TempDir}; use tempfile::{Builder, TempDir};
use uucore::Args;
static TESTS_DIR: &str = "tests"; static TESTS_DIR: &str = "tests";
static FIXTURES_DIR: &str = "fixtures"; static FIXTURES_DIR: &str = "fixtures";
@ -46,6 +46,8 @@ static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical
static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin"; 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_coreutils");
/// Test if the program is running under CI /// Test if the program is running under CI
pub fn is_ci() -> bool { pub fn is_ci() -> bool {
std::env::var("CI") std::env::var("CI")
@ -64,7 +66,7 @@ fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: &Option<Rc<TempDir>>, file_rel_p
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CmdResult { pub struct CmdResult {
/// bin_path provided by `TestScenario` or `UCommand` /// bin_path provided by `TestScenario` or `UCommand`
bin_path: String, bin_path: PathBuf,
/// util_name provided by `TestScenario` or `UCommand` /// util_name provided by `TestScenario` or `UCommand`
util_name: Option<String>, util_name: Option<String>,
//tmpd is used for convenience functions for asserts against fixtures //tmpd is used for convenience functions for asserts against fixtures
@ -78,21 +80,23 @@ pub struct CmdResult {
} }
impl CmdResult { impl CmdResult {
pub fn new<T, U>( pub fn new<S, T, U, V>(
bin_path: String, bin_path: S,
util_name: Option<String>, util_name: Option<T>,
tmpd: Option<Rc<TempDir>>, tmpd: Option<Rc<TempDir>>,
exit_status: Option<ExitStatus>, exit_status: Option<ExitStatus>,
stdout: T, stdout: U,
stderr: U, stderr: V,
) -> Self ) -> Self
where where
T: Into<Vec<u8>>, S: Into<PathBuf>,
T: AsRef<str>,
U: Into<Vec<u8>>, U: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{ {
Self { Self {
bin_path, bin_path: bin_path.into(),
util_name, util_name: util_name.map(|s| s.as_ref().into()),
tmpd, tmpd,
exit_status, exit_status,
stdout: stdout.into(), stdout: stdout.into(),
@ -634,7 +638,7 @@ impl CmdResult {
self.stderr_only(format!( self.stderr_only(format!(
"{0}: {2}\nTry '{1} {0} --help' for more information.\n", "{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.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command
self.bin_path, self.bin_path.display(),
msg.as_ref() msg.as_ref()
)) ))
} }
@ -1093,18 +1097,21 @@ pub struct TestScenario {
} }
impl TestScenario { impl TestScenario {
pub fn new(util_name: &str) -> Self { pub fn new<T>(util_name: T) -> Self
where
T: AsRef<str>,
{
let tmpd = Rc::new(TempDir::new().unwrap()); let tmpd = Rc::new(TempDir::new().unwrap());
let ts = Self { let ts = Self {
bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")), bin_path: PathBuf::from(TESTS_BINARY),
util_name: String::from(util_name), util_name: util_name.as_ref().into(),
fixtures: AtPath::new(tmpd.as_ref().path()), fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd, tmpd,
}; };
let mut fixture_path_builder = env::current_dir().unwrap(); let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR); fixture_path_builder.push(TESTS_DIR);
fixture_path_builder.push(FIXTURES_DIR); fixture_path_builder.push(FIXTURES_DIR);
fixture_path_builder.push(util_name); fixture_path_builder.push(util_name.as_ref());
if let Ok(m) = fs::metadata(&fixture_path_builder) { if let Ok(m) = fs::metadata(&fixture_path_builder) {
if m.is_dir() { if m.is_dir() {
recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap(); recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap();
@ -1116,58 +1123,50 @@ impl TestScenario {
/// Returns builder for invoking the target uutils binary. Paths given are /// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory. /// treated relative to the environment's unique temporary test directory.
pub fn ucmd(&self) -> UCommand { pub fn ucmd(&self) -> UCommand {
self.composite_cmd(&self.bin_path, &self.util_name, true) UCommand::from_test_scenario(self)
}
/// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory.
pub fn composite_cmd<S: AsRef<OsStr>, T: AsRef<OsStr>>(
&self,
bin: S,
util_name: T,
env_clear: bool,
) -> UCommand {
UCommand::new_from_tmp(bin, &Some(util_name), self.tmpd.clone(), env_clear)
} }
/// Returns builder for invoking any system command. Paths given are treated /// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory. /// relative to the environment's unique temporary test directory.
pub fn cmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand { pub fn cmd<S: Into<PathBuf>>(&self, bin_path: S) -> UCommand {
UCommand::new_from_tmp::<S, S>(bin, &None, self.tmpd.clone(), true) 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 /// Returns builder for invoking any uutils command. Paths given are treated
/// relative to the environment's unique temporary test directory. /// relative to the environment's unique temporary test directory.
pub fn ccmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand { pub fn ccmd<S: AsRef<str>>(&self, util_name: S) -> UCommand {
self.composite_cmd(&self.bin_path, bin, true) UCommand::with_util(util_name, self.tmpd.clone())
}
// different names are used rather than an argument
// because the need to keep the environment is exceedingly rare.
pub fn ucmd_keepenv(&self) -> UCommand {
self.composite_cmd(&self.bin_path, &self.util_name, false)
}
/// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory.
/// Differs from the builder returned by `cmd` in that `cmd_keepenv` does not call
/// `Command::env_clear` (Clears the entire environment map for the child process.)
pub fn cmd_keepenv<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
UCommand::new_from_tmp::<S, S>(bin, &None, self.tmpd.clone(), false)
} }
} }
/// A `UCommand` is a wrapper around an individual Command that provides several additional features /// 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 /// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command
/// and asserting on the results. /// and asserting on the results.
/// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops /// 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. /// the test failure can display the exact call which preceded an assertion failure.
/// 3. it provides convenience construction arguments to set the Command working directory and/or clear its environment. /// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory.
#[derive(Debug)] ///
/// 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 `coreutils UTIL_NAME` instead of the shell
/// * [`UCommand::from_test_scenario`]: Run `coreutils UTIL_NAME` instead of the shell in the
/// temporary directory of the [`TestScenario`]
/// * [`UCommand::current_dir`]: Sets the working directory
/// * [`UCommand::keep_env`]: Keep environment variables instead of clearing them
/// * ...
#[derive(Debug, Default)]
pub struct UCommand { pub struct UCommand {
pub raw: Command, args: VecDeque<OsString>,
comm_string: String, env_vars: Vec<(OsString, OsString)>,
bin_path: String, current_dir: Option<PathBuf>,
env_clear: bool,
bin_path: Option<PathBuf>,
util_name: Option<String>, util_name: Option<String>,
has_run: bool, has_run: bool,
ignore_stdin_write_error: bool, ignore_stdin_write_error: bool,
@ -1183,72 +1182,80 @@ pub struct UCommand {
} }
impl UCommand { impl UCommand {
pub fn new<T: AsRef<OsStr>, S: AsRef<OsStr>, U: AsRef<OsStr>>( /// Create a new plain [`UCommand`].
bin_path: T, ///
util_name: &Option<S>, /// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a
curdir: U, /// shell (`sh -c` on unix platforms or `cmd /C` on windows).
env_clear: bool, ///
) -> Self { /// Per default the environment is cleared and the working directory is set to an individual
let bin_path = bin_path.as_ref(); /// temporary directory for safety purposes.
let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref); pub fn new() -> Self {
Self {
let mut ucmd = Self { env_clear: true,
tmpd: None, ..Default::default()
has_run: false,
raw: {
let mut cmd = Command::new(bin_path);
cmd.current_dir(curdir.as_ref());
if env_clear {
cmd.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") {
cmd.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") {
cmd.env("LD_PRELOAD", ld_preload);
}
}
}
cmd
},
comm_string: String::from(bin_path.to_str().unwrap()),
bin_path: bin_path.to_str().unwrap().to_string(),
util_name: util_name.map(|un| un.to_str().unwrap().to_string()),
ignore_stdin_write_error: false,
bytes_into_stdin: None,
stdin: None,
stdout: None,
stderr: None,
#[cfg(any(target_os = "linux", target_os = "android"))]
limits: vec![],
stderr_to_stdout: false,
timeout: Some(Duration::from_secs(30)),
};
if let Some(un) = util_name {
ucmd.arg(un);
} }
}
/// Create a [`UCommand`] for a specific uutils utility.
///
/// Sets the temporary directory to `tmpd` and the execution binary to the path where
/// `coreutils` is found.
pub fn with_util<T>(util_name: T, tmpd: Rc<TempDir>) -> Self
where
T: AsRef<str>,
{
let mut ucmd = Self::new();
ucmd.util_name = Some(util_name.as_ref().into());
ucmd.bin_path(TESTS_BINARY).temp_dir(tmpd);
ucmd ucmd
} }
pub fn new_from_tmp<T: AsRef<OsStr>, S: AsRef<OsStr>>( /// Create a [`UCommand`] from a [`TestScenario`].
bin_path: T, ///
util_name: &Option<S>, /// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the
tmpd: Rc<TempDir>, /// execution binary is set to `coreutils`.
env_clear: bool, pub fn from_test_scenario(scene: &TestScenario) -> Self {
) -> Self { Self::with_util(&scene.util_name, scene.tmpd.clone())
let tmpd_path_buf = String::from(tmpd.as_ref().path().to_str().unwrap()); }
let mut ucmd: Self = Self::new(bin_path, util_name, tmpd_path_buf, env_clear);
ucmd.tmpd = Some(tmpd); /// Set the execution binary.
ucmd ///
/// 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<T>(&mut self, bin_path: T) -> &mut Self
where
T: Into<PathBuf>,
{
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<TempDir>) -> &mut Self {
self.tmpd = Some(temp_dir);
self
}
/// Keep the environment variables instead of clearing them before running the command.
pub fn keep_env(&mut self) -> &mut Self {
self.env_clear = false;
self
}
/// Set the working directory for this [`UCommand`]
///
/// Per default the working directory is set to the [`UCommands`] temporary directory.
pub fn current_dir<T>(&mut self, current_dir: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.current_dir = Some(current_dir.into());
self
} }
pub fn set_stdin<T: Into<Stdio>>(&mut self, stdin: T) -> &mut Self { pub fn set_stdin<T: Into<Stdio>>(&mut self, stdin: T) -> &mut Self {
@ -1274,29 +1281,14 @@ impl UCommand {
/// Add a parameter to the invocation. Path arguments are treated relative /// Add a parameter to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
assert!(!self.has_run, "{}", ALREADY_RUN); self.args.push_back(arg.as_ref().into());
self.comm_string.push(' ');
self.comm_string
.push_str(arg.as_ref().to_str().unwrap_or_default());
self.raw.arg(arg.as_ref());
self self
} }
/// Add multiple parameters to the invocation. Path arguments are treated relative /// Add multiple parameters to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self { pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
assert!(!self.has_run, "{}", MULTIPLE_STDIN_MEANINGLESS); self.args.extend(args.iter().map(|s| s.as_ref().into()));
let strings = args
.iter()
.map(|s| s.as_ref().to_os_string())
.collect_ignore();
for s in strings {
self.comm_string.push(' ');
self.comm_string.push_str(&s);
}
self.raw.args(args.as_ref());
self self
} }
@ -1331,13 +1323,13 @@ impl UCommand {
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr>, V: AsRef<OsStr>,
{ {
assert!(!self.has_run, "{}", ALREADY_RUN); self.env_vars
self.raw.env(key, val); .push((key.as_ref().into(), val.as_ref().into()));
self self
} }
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
pub fn with_limit( pub fn limit(
&mut self, &mut self,
resource: rlimit::Resource, resource: rlimit::Resource,
soft_limit: u64, soft_limit: u64,
@ -1359,26 +1351,113 @@ impl UCommand {
self self
} }
/// Spawns the command, feeds the stdin if any, and returns the /// Build the `std::process::Command` and apply the defaults on fields which were not specified
/// child process immediately. /// by the user.
pub fn run_no_wait(&mut self) -> UChild { ///
assert!(!self.has_run, "{}", ALREADY_RUN); /// These __defaults__ are:
self.has_run = true; /// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.).
log_info("run", &self.comm_string); /// 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`
/// * `env_clear`: `true`. (Almost) all environment variables will be cleared.
/// * `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<CapturedOutput>, Option<CapturedOutput>) {
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));
}
if self.env_clear {
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(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_stdout = None;
let mut captured_stderr = None; let mut captured_stderr = None;
let command = if self.stderr_to_stdout { if self.stderr_to_stdout {
let mut output = CapturedOutput::default(); let mut output = CapturedOutput::default();
let command = self command
.raw
.stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(Stdio::from(output.try_clone().unwrap())) .stdout(Stdio::from(output.try_clone().unwrap()))
.stderr(Stdio::from(output.try_clone().unwrap())); .stderr(Stdio::from(output.try_clone().unwrap()));
captured_stdout = Some(output); captured_stdout = Some(output);
command
} else { } else {
let stdout = if self.stdout.is_some() { let stdout = if self.stdout.is_some() {
self.stdout.take().unwrap() self.stdout.take().unwrap()
@ -1398,15 +1477,27 @@ impl UCommand {
stdio stdio
}; };
self.raw command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(stdout) .stdout(stdout)
.stderr(stderr) .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(); let child = command.spawn().unwrap();
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", target_os = "android"))]
for &(resource, soft_limit, hard_limit) in &self.limits { for &(resource, soft_limit, hard_limit) in &self.limits {
prlimit( prlimit(
child.id() as i32, child.id() as i32,
@ -1465,6 +1556,17 @@ impl UCommand {
} }
} }
impl std::fmt::Display for UCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut comm_string: Vec<String> = 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 /// Stored the captured output in a temporary file. The file is deleted as soon as
/// [`CapturedOutput`] is dropped. /// [`CapturedOutput`] is dropped.
#[derive(Debug)] #[derive(Debug)]
@ -1597,14 +1699,14 @@ impl<'a> UChildAssertion<'a> {
self.uchild.stderr_exact_bytes(expected_stderr_size), self.uchild.stderr_exact_bytes(expected_stderr_size),
), ),
}; };
CmdResult { CmdResult::new(
bin_path: self.uchild.bin_path.clone(), self.uchild.bin_path.clone(),
util_name: self.uchild.util_name.clone(), self.uchild.util_name.clone(),
tmpd: self.uchild.tmpd.clone(), self.uchild.tmpd.clone(),
exit_status, exit_status,
stdout, stdout,
stderr, stderr,
} )
} }
// Make assertions of [`CmdResult`] with all output from start of the process until now. // Make assertions of [`CmdResult`] with all output from start of the process until now.
@ -1684,7 +1786,7 @@ impl<'a> UChildAssertion<'a> {
/// Abstraction for a [`std::process::Child`] to handle the child process. /// Abstraction for a [`std::process::Child`] to handle the child process.
pub struct UChild { pub struct UChild {
raw: Child, raw: Child,
bin_path: String, bin_path: PathBuf,
util_name: Option<String>, util_name: Option<String>,
captured_stdout: Option<CapturedOutput>, captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>, captured_stderr: Option<CapturedOutput>,
@ -1704,7 +1806,7 @@ impl UChild {
) -> Self { ) -> Self {
Self { Self {
raw: child, raw: child,
bin_path: ucommand.bin_path.clone(), bin_path: ucommand.bin_path.clone().unwrap(),
util_name: ucommand.util_name.clone(), util_name: ucommand.util_name.clone(),
captured_stdout, captured_stdout,
captured_stderr, captured_stderr,
@ -2335,11 +2437,13 @@ fn parse_coreutil_version(version_string: &str) -> f32 {
///``` ///```
#[cfg(unix)] #[cfg(unix)]
pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<CmdResult, String> { pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<CmdResult, String> {
println!("{}", check_coreutil_version(&ts.util_name, VERSION_MIN)?); let util_name = ts.util_name.as_str();
let util_name = &host_name_for(&ts.util_name); println!("{}", check_coreutil_version(util_name, VERSION_MIN)?);
let util_name = host_name_for(util_name);
let result = ts let result = ts
.cmd_keepenv(util_name.as_ref()) .cmd(util_name.as_ref())
.keep_env()
.env("LC_ALL", "C") .env("LC_ALL", "C")
.args(args) .args(args)
.run(); .run();
@ -2411,7 +2515,8 @@ pub fn run_ucmd_as_root(
// we can run sudo and we're root // we can run sudo and we're root
// run ucmd as root: // run ucmd as root:
Ok(ts Ok(ts
.cmd_keepenv("sudo") .cmd("sudo")
.keep_env()
.env("LC_ALL", "C") .env("LC_ALL", "C")
.arg("-E") .arg("-E")
.arg("--non-interactive") .arg("--non-interactive")
@ -2439,30 +2544,8 @@ mod tests {
// spell-checker:ignore (tests) asdfsadfa // spell-checker:ignore (tests) asdfsadfa
use super::*; use super::*;
#[cfg(unix)]
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult { pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
let mut ucmd = UCommand::new_from_tmp::<&str, String>( UCommand::new().arg(cmd).run()
"sh",
&None,
Rc::new(tempfile::tempdir().unwrap()),
true,
);
ucmd.arg("-c");
ucmd.arg(cmd);
ucmd.run()
}
#[cfg(windows)]
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
let mut ucmd = UCommand::new_from_tmp::<&str, String>(
"cmd",
&None,
Rc::new(tempfile::tempdir().unwrap()),
true,
);
ucmd.arg("/C");
ucmd.arg(cmd);
ucmd.run()
} }
#[test] #[test]
@ -3200,4 +3283,52 @@ mod tests {
let ts = TestScenario::new("sleep"); let ts = TestScenario::new("sleep");
ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run(); 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());
}
} }