// spell-checker:ignore (words) agroupthatdoesntexist auserthatdoesntexist cuuser groupname notexisting passgrp use crate::common::util::{is_ci, run_ucmd_as_root, CmdResult, TestScenario}; #[cfg(any(target_os = "linux", target_os = "android"))] use rust_users::get_effective_uid; // Apparently some CI environments have configuration issues, e.g. with 'whoami' and 'id'. // If we are running inside the CI and "needle" is in "stderr" skipping this test is // considered okay. If we are not inside the CI this calls assert!(result.success). // // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" // // stderr: "whoami: cannot find name for user ID 1001" // TODO: Maybe `adduser --uid 1001 username` can put things right? // // stderr: "id: cannot find name for group ID 116" // stderr: "thread 'main' panicked at 'called `Result::unwrap()` on an `Err` // value: Custom { kind: NotFound, error: "No such id: 1001" }', // /project/src/uucore/src/lib/features/perms.rs:176:44" // fn skipping_test_is_okay(result: &CmdResult, needle: &str) -> bool { if !result.succeeded() { println!("result.stdout = {}", result.stdout_str()); println!("result.stderr = {}", result.stderr_str()); if is_ci() && result.stderr_str().contains(needle) { println!("test skipped:"); return true; } else { result.success(); } } false } #[cfg(test)] mod test_passgrp { use chown::entries::{gid2grp, grp2gid, uid2usr, usr2uid}; #[test] fn test_usr2uid() { assert_eq!(0, usr2uid("root").unwrap()); assert!(usr2uid("88_888_888").is_err()); assert!(usr2uid("auserthatdoesntexist").is_err()); } #[test] fn test_grp2gid() { if cfg!(target_os = "linux") || cfg!(target_os = "android") || cfg!(target_os = "windows") { assert_eq!(0, grp2gid("root").unwrap()); } else { assert_eq!(0, grp2gid("wheel").unwrap()); } assert!(grp2gid("88_888_888").is_err()); assert!(grp2gid("agroupthatdoesntexist").is_err()); } #[test] fn test_uid2usr() { assert_eq!("root", uid2usr(0).unwrap()); assert!(uid2usr(88_888_888).is_err()); } #[test] fn test_gid2grp() { if cfg!(target_os = "linux") || cfg!(target_os = "android") || cfg!(target_os = "windows") { assert_eq!("root", gid2grp(0).unwrap()); } else { assert_eq!("wheel", gid2grp(0).unwrap()); } assert!(gid2grp(88_888_888).is_err()); } } #[test] fn test_invalid_option() { new_ucmd!().arg("-w").arg("-q").arg("/").fails(); } #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } #[test] fn test_chown_only_owner() { // test chown username file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); // since only superuser can change owner, we have to change from ourself to ourself let result = scene .ucmd() .arg(user_name) .arg("--verbose") .arg(file1) .run(); result.stderr_contains("retained as"); // try to change to another existing user, e.g. 'root' scene .ucmd() .arg("root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] fn test_chown_only_owner_colon() { // test chown username: file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); scene .ucmd() .arg(format!("{user_name}:")) .arg("--verbose") .arg(file1) .succeeds() .stderr_contains("retained as"); scene .ucmd() .arg(format!("{user_name}.")) .arg("--verbose") .arg(file1) .succeeds() .stderr_contains("retained as"); scene .ucmd() .arg("root:") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] fn test_chown_only_colon() { // test chown : file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let file1 = "test_chown_file1"; at.touch(file1); // expected: // $ chown -v : file.txt 2>out_err ; echo $? ; cat out_err // ownership of 'file.txt' retained // 0 let result = scene.ucmd().arg(":").arg("--verbose").arg(file1).run(); if skipping_test_is_okay(&result, "No such id") { return; } result.stderr_contains("retained as"); // TODO: verbose is not printed to stderr in GNU chown // test chown : file.txt // expected: // $ chown -v :: file.txt 2>out_err ; echo $? ; cat out_err // 1 // chown: invalid group: '::' scene .ucmd() .arg("::") .arg("--verbose") .arg(file1) .fails() .stderr_contains("invalid group: '::'"); scene .ucmd() .arg("..") .arg("--verbose") .arg(file1) .fails() .stderr_contains("invalid group: '..'"); } #[test] fn test_chown_failed_stdout() { // test chown root file.txt // TODO: implement once output "failed to change" to stdout is fixed // expected: // $ chown -v root file.txt 2>out_err ; echo $? ; cat out_err // failed to change ownership of 'file.txt' from jhs to root // 1 // chown: changing ownership of 'file.txt': Operation not permitted } #[test] // FixME: Fails on freebsd because of chown: invalid group: 'root:root' #[cfg(not(target_os = "freebsd"))] fn test_chown_owner_group() { // test chown username:group file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene.cmd("id").arg("-gn").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let result = scene .ucmd() .arg(format!("{user_name}:{group_name}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "chown: invalid group:") { return; } result.stderr_contains("retained as"); scene .ucmd() .arg("root:root:root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("invalid group"); scene .ucmd() .arg("root.root.root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("invalid group"); // TODO: on macos group name is not recognized correctly: "chown: invalid group: 'root:root' #[cfg(any(windows, all(unix, not(target_os = "macos"))))] scene .ucmd() .arg("root:root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] // FixME: Fails on freebsd because of chown: invalid group: 'root:root' #[cfg(not(target_os = "freebsd"))] fn test_chown_various_input() { // test chown username:group file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene.cmd("id").arg("-gn").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let result = scene .ucmd() .arg(format!("{user_name}:{group_name}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "chown: invalid group:") { return; } result.stderr_contains("retained as"); // check that username.groupname is understood let result = scene .ucmd() .arg(format!("{user_name}.{group_name}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "chown: invalid group:") { return; } result.stderr_contains("retained as"); // Fails as user.name doesn't exist in the CI // but it is valid scene .ucmd() .arg(format!("{}:{}", "user.name", "groupname")) .arg("--verbose") .arg(file1) .fails() .stderr_contains("chown: invalid user: 'user.name:groupname'"); } #[test] // FixME: on macos & freebsd group name is not recognized correctly: "chown: invalid group: ':groupname' #[cfg(any( windows, all(unix, not(any(target_os = "macos", target_os = "freebsd"))) ))] fn test_chown_only_group() { // test chown :group file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene .ucmd() .arg(format!(":{user_name}")) .arg("--verbose") .arg(file1) .run(); if is_ci() && result.stderr_str().contains("Operation not permitted") { // With ubuntu with old Rust in the CI, we can get an error return; } if is_ci() && result.stderr_str().contains("chown: invalid group:") { // With mac into the CI, we can get this answer return; } result.stderr_contains("retained as"); result.success(); scene .ucmd() .arg(":root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] fn test_chown_only_user_id() { // test chown 1111 file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("id").arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene.ucmd().arg(user_id).arg("--verbose").arg(file1).run(); if skipping_test_is_okay(&result, "invalid user") { // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" // stderr: "chown: invalid user: '1001' return; } result.stderr_contains("retained as"); scene .ucmd() .arg("0") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] fn test_chown_fail_id() { // test chown 1111. file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("id").arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); scene .ucmd() .arg(format!("{user_id}:")) .arg(file1) .fails() .stderr_contains("invalid spec"); scene .ucmd() .arg(format!("{user_id}.")) .arg(file1) .fails() .stderr_contains("invalid spec"); } /// Test for setting the owner to a user ID for a user that does not exist. /// /// For example: /// /// $ touch f && chown 12345 f /// /// succeeds with exit status 0 and outputs nothing. The owner of the /// file is set to 12345, even though no user with that ID exists. /// /// This test must be run as root, because only the root user can /// transfer ownership of a file. #[test] fn test_chown_only_user_id_nonexistent_user() { let ts = TestScenario::new(util_name!()); let at = ts.fixtures.clone(); at.touch("f"); if let Ok(result) = run_ucmd_as_root(&ts, &["12345", "f"]) { result.success().no_stdout().no_stderr(); } else { print!("Test skipped; requires root user"); } } #[test] // FixME: stderr = chown: ownership of 'test_chown_file1' retained as cuuser:wheel #[cfg(not(target_os = "freebsd"))] fn test_chown_only_group_id() { // test chown :1111 file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("id").arg("-g").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_id = String::from(result.stdout_str().trim()); assert!(!group_id.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene .ucmd() .arg(format!(":{group_id}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "chown: invalid group:") { // With mac into the CI, we can get this answer return; } result.stderr_contains("retained as"); // Apparently on CI "macos-latest, x86_64-apple-darwin, feat_os_macos" // the process has the rights to change from runner:staff to runner:wheel #[cfg(any(windows, all(unix, not(target_os = "macos"))))] scene .ucmd() .arg(":0") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } /// Test for setting the group to a group ID for a group that does not exist. /// /// For example: /// /// $ touch f && chown :12345 f /// /// succeeds with exit status 0 and outputs nothing. The group of the /// file is set to 12345, even though no group with that ID exists. /// /// This test must be run as root, because only the root user can /// transfer ownership of a file. #[test] fn test_chown_only_group_id_nonexistent_group() { let ts = TestScenario::new(util_name!()); let at = ts.fixtures.clone(); at.touch("f"); if let Ok(result) = run_ucmd_as_root(&ts, &[":12345", "f"]) { result.success().no_stdout().no_stderr(); } else { print!("Test skipped; requires root user"); } } #[test] fn test_chown_owner_group_id() { // test chown 1111:1111 file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("id").arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); let result = scene.cmd("id").arg("-g").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_id = String::from(result.stdout_str().trim()); assert!(!group_id.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene .ucmd() .arg(format!("{user_id}:{group_id}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "invalid user") { // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" // stderr: "chown: invalid user: '1001:116' return; } result.stderr_contains("retained as"); let result = scene .ucmd() .arg(format!("{user_id}.{group_id}")) .arg("--verbose") .arg(file1) .run(); if skipping_test_is_okay(&result, "invalid user") { // From the Logs: "Build (ubuntu-18.04, x86_64-unknown-linux-gnu, feat_os_unix, use-cross)" // stderr: "chown: invalid user: '1001.116' return; } result.stderr_contains("retained as"); scene .ucmd() .arg("0:0") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] // FixME: Fails on freebsd because of chown: invalid group: '0:root' #[cfg(not(target_os = "freebsd"))] fn test_chown_owner_group_mix() { // test chown 1111:group file.txt let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("id").arg("-u").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let user_id = String::from(result.stdout_str().trim()); assert!(!user_id.is_empty()); let result = scene.cmd("id").arg("-gn").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file1 = "test_chown_file1"; at.touch(file1); let result = scene .ucmd() .arg(format!("{user_id}:{group_name}")) .arg("--verbose") .arg(file1) .run(); result.stderr_contains("retained as"); // TODO: on macos group name is not recognized correctly: "chown: invalid group: '0:root' #[cfg(any(windows, all(unix, not(target_os = "macos"))))] scene .ucmd() .arg("0:root") .arg("--verbose") .arg(file1) .fails() .stderr_contains("failed to change"); } #[test] fn test_chown_recursive() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); at.mkdir_all("a/b/c"); at.mkdir("z"); at.touch(at.plus_as_string("a/a")); at.touch(at.plus_as_string("a/b/b")); at.touch(at.plus_as_string("a/b/c/c")); at.touch(at.plus_as_string("z/y")); let result = scene .ucmd() .arg("-R") .arg("--verbose") .arg(user_name) .arg("a") .arg("z") .run(); result.stderr_contains("ownership of 'a/a' retained as"); result.stderr_contains("ownership of 'z/y' retained as"); } #[test] fn test_root_preserve() { let scene = TestScenario::new(util_name!()); let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene .ucmd() .arg("--preserve-root") .arg("-R") .arg(user_name) .arg("/") .fails(); result.stderr_contains("chown: it is dangerous to operate recursively"); } #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn test_big_p() { if get_effective_uid() != 0 { new_ucmd!() .arg("-RP") .arg("bin") .arg("/proc/self/cwd") .fails() .stderr_contains( // linux fails with "Operation not permitted (os error 1)" // because of insufficient permissions, // android fails with "Permission denied (os error 13)" // because it can't resolve /proc (even though it can resolve /proc/self/) "chown: changing ownership of '/proc/self/cwd': ", ); } } #[test] fn test_chown_file_notexisting() { // test chown username not_existing let scene = TestScenario::new(util_name!()); let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); scene .ucmd() .arg(&user_name) .arg("--verbose") .arg("not_existing") .fails() .stdout_contains(format!( "failed to change ownership of 'not_existing' to {user_name}" )); // TODO: uncomment once message changed from "cannot dereference" to "cannot access" // result.stderr_contains("cannot access 'not_existing': No such file or directory"); } #[test] fn test_chown_no_change_to_user_from_user() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42") .arg("43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); } #[test] fn test_chown_no_change_to_user_from_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=:42") .arg("43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); } #[test] fn test_chown_no_change_to_user_from_user_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42:42") .arg("43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {user_name}\n")); } #[test] fn test_chown_no_change_to_group_from_user() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42") .arg(":43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); } #[test] fn test_chown_no_change_to_group_from_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=:42") .arg(":43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); } #[test] fn test_chown_no_change_to_group_from_user_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42:42") .arg(":43") .arg(file) .succeeds() .stdout_only(format!("ownership of '{file}' retained as {group_name}\n")); } #[test] fn test_chown_no_change_to_user_group_from_user() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42") .arg("43:43") .arg(file) .succeeds() .stdout_only(format!( "ownership of '{file}' retained as {user_name}:{group_name}\n" )); } #[test] fn test_chown_no_change_to_user_group_from_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=:42") .arg("43:43") .arg(file) .succeeds() .stdout_only(format!( "ownership of '{file}' retained as {user_name}:{group_name}\n" )); } #[test] fn test_chown_no_change_to_user_group_from_user_group() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let result = scene.cmd("whoami").run(); if skipping_test_is_okay(&result, "whoami: cannot find name for user ID") { return; } let user_name = String::from(result.stdout_str().trim()); assert!(!user_name.is_empty()); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); assert!(!group_name.is_empty()); let file = "f"; at.touch(file); scene .ucmd() .arg("-v") .arg("--from=42:42") .arg("43:43") .arg(file) .succeeds() .stdout_only(format!( "ownership of '{file}' retained as {user_name}:{group_name}\n" )); }