Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 58 additions & 26 deletions src/uu/groups/src/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// ============================================================================
// Test suite summary for GNU coreutils 8.32.162-4eda
// ============================================================================
// PASS: tests/misc/groups-dash.sh
// PASS: tests/misc/groups-process-all.sh
// PASS: tests/misc/groups-version.sh

// spell-checker:ignore (ToDO) passwd

Expand All @@ -14,11 +21,15 @@ use uucore::entries::{get_groups_gnu, gid2grp, Locate, Passwd};

use clap::{crate_version, App, Arg};

static ABOUT: &str = "display current group names";
static OPT_USER: &str = "user";
mod options {
pub const USERS: &str = "USERNAME";
}
static ABOUT: &str = "Print group memberships for each USERNAME or, \
if no USERNAME is specified, for\nthe current process \
(which may differ if the groups data‐base has changed).";

fn get_usage() -> String {
format!("{0} [USERNAME]", executable!())
format!("{0} [OPTION]... [USERNAME]...", executable!())
}

pub fn uumain(args: impl uucore::Args) -> i32 {
Expand All @@ -28,36 +39,57 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.version(crate_version!())
.about(ABOUT)
.usage(&usage[..])
.arg(Arg::with_name(OPT_USER))
.arg(
Arg::with_name(options::USERS)
.multiple(true)
.takes_value(true)
.value_name(options::USERS),
)
.get_matches_from(args);

match matches.value_of(OPT_USER) {
None => {
let users: Vec<String> = matches
.values_of(options::USERS)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();

let mut exit_code = 0;

if users.is_empty() {
println!(
"{}",
get_groups_gnu(None)
.unwrap()
.iter()
.map(|&gid| gid2grp(gid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gid);
exit_code = 1;
gid.to_string()
}))
.collect::<Vec<_>>()
.join(" ")
);
return exit_code;
}

for user in users {
if let Ok(p) = Passwd::locate(user.as_str()) {
println!(
"{}",
get_groups_gnu(None)
.unwrap()
"{} : {}",
user,
p.belongs_to()
.iter()
.map(|&g| gid2grp(g).unwrap())
.map(|&gid| gid2grp(gid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gid);
exit_code = 1;
gid.to_string()
}))
.collect::<Vec<_>>()
.join(" ")
);
0
}
Some(user) => {
if let Ok(p) = Passwd::locate(user) {
println!(
"{}",
p.belongs_to()
.iter()
.map(|&g| gid2grp(g).unwrap())
.collect::<Vec<_>>()
.join(" ")
);
0
} else {
crash!(1, "unknown user {}", user);
}
} else {
show_error!("'{}': no such user", user);
exit_code = 1;
}
}
exit_code
}
192 changes: 156 additions & 36 deletions tests/by-util/test_groups.rs
Original file line number Diff line number Diff line change
@@ -1,56 +1,176 @@
use crate::common::util::*;

// spell-checker:ignore (ToDO) coreutil

// These tests run the GNU coreutils `(g)groups` binary in `$PATH` in order to gather reference values.
// If the `(g)groups` in `$PATH` doesn't include a coreutils version string,
// or the version is too low, the test is skipped.

// The reference version is 8.32. Here 8.30 was chosen because right now there's no
// ubuntu image for github action available with a higher version than 8.30.
const VERSION_MIN: &str = "8.30"; // minimum Version for the reference `groups` in $PATH
const VERSION_MIN_MULTIPLE_USERS: &str = "8.31"; // this feature was introduced in GNU's coreutils 8.31
const UUTILS_WARNING: &str = "uutils-tests-warning";
const UUTILS_INFO: &str = "uutils-tests-info";

macro_rules! unwrap_or_return {
( $e:expr ) => {
match $e {
Ok(x) => x,
Err(e) => {
println!("{}: test skipped: {}", UUTILS_INFO, e);
return;
}
}
};
}

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").unwrap_or_else(|e| {
println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e);
"nobody".to_string()
})
}

#[test]
#[cfg(unix)]
fn test_groups() {
if !is_ci() {
new_ucmd!().succeeds().stdout_is(expected_result(&[]));
} else {
// TODO: investigate how this could be tested in CI
// stderr = groups: cannot find name for group ID 116
println!("test skipped:");
}
let result = new_ucmd!().run();
let exp_result = unwrap_or_return!(expected_result(&[]));

result
.stdout_is(exp_result.stdout_str())
.stderr_is(exp_result.stderr_str())
.code_is(exp_result.code());
}

#[test]
#[cfg(unix)]
#[ignore = "fixme: 'groups USERNAME' needs more debugging"]
fn test_groups_username() {
let scene = TestScenario::new(util_name!());
let whoami_result = scene.cmd("whoami").run();
let test_users = [&whoami()[..]];

let username = if whoami_result.succeeded() {
whoami_result.stdout_move_str()
} else if is_ci() {
String::from("docker")
} else {
println!("test skipped:");
let result = new_ucmd!().args(&test_users).run();
let exp_result = unwrap_or_return!(expected_result(&test_users));

result
.stdout_is(exp_result.stdout_str())
.stderr_is(exp_result.stderr_str())
.code_is(exp_result.code());
}

#[test]
#[cfg(unix)]
fn test_groups_username_multiple() {
// TODO: [2021-06; jhscheer] refactor this as `let util_name = host_name_for(util_name!())` when that function is added to 'tests/common'
#[cfg(target_os = "linux")]
let util_name = util_name!();
#[cfg(all(unix, not(target_os = "linux")))]
let util_name = &format!("g{}", util_name!());
let version_check_string = check_coreutil_version(util_name, VERSION_MIN_MULTIPLE_USERS);
if version_check_string.starts_with(UUTILS_WARNING) {
println!("{}\ntest skipped", version_check_string);
return;
};
}
let test_users = ["root", "man", "postfix", "sshd", &whoami()];

let result = new_ucmd!().args(&test_users).run();
let exp_result = unwrap_or_return!(expected_result(&test_users));

// TODO: stdout should be in the form: "username : group1 group2 group3"
result
.stdout_is(exp_result.stdout_str())
.stderr_is(exp_result.stderr_str())
.code_is(exp_result.code());
}

scene
.ucmd()
.arg(&username)
.succeeds()
.stdout_is(expected_result(&[&username]));
fn check_coreutil_version(util_name: &str, version_expected: &str) -> String {
// example:
// $ id --version | head -n 1
// id (GNU coreutils) 8.32.162-4eda
let scene = TestScenario::new(util_name);
let version_check = scene
.cmd_keepenv(&util_name)
.env("LC_ALL", "C")
.arg("--version")
.run();
version_check
.stdout_str()
.split('\n')
.collect::<Vec<_>>()
.get(0)
.map_or_else(
|| format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name),
|s| {
if s.contains(&format!("(GNU coreutils) {}", version_expected)) {
s.to_string()
} else if s.contains("(GNU coreutils)") {
let version_found = s.split_whitespace().last().unwrap()[..4].parse::<f32>().unwrap_or_default();
let version_expected = version_expected.parse::<f32>().unwrap_or_default();
if version_found > version_expected {
format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found)
} else {
format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found) }
} else {
format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name)
}
},
)
}

#[allow(clippy::needless_borrow)]
#[cfg(unix)]
fn expected_result(args: &[&str]) -> String {
// We want to use GNU id. On most linux systems, this is "id", but on
// bsd-like systems (e.g. FreeBSD, MacOS), it is commonly "gid".
#[cfg(any(target_os = "linux"))]
let util_name = "id";
#[cfg(not(target_os = "linux"))]
let util_name = "gid";

TestScenario::new(util_name)
fn expected_result(args: &[&str]) -> Result<CmdResult, String> {
// TODO: [2021-06; jhscheer] refactor this as `let util_name = host_name_for(util_name!())` when that function is added to 'tests/common'
#[cfg(target_os = "linux")]
let util_name = util_name!();
#[cfg(all(unix, not(target_os = "linux")))]
let util_name = &format!("g{}", util_name!());

let version_check_string = check_coreutil_version(util_name, VERSION_MIN);
if version_check_string.starts_with(UUTILS_WARNING) {
return Err(version_check_string);
}
println!("{}", version_check_string);

let scene = TestScenario::new(util_name);
let result = scene
.cmd_keepenv(util_name)
.env("LANGUAGE", "C")
.env("LC_ALL", "C")
.args(args)
.args(&["-Gn"])
.succeeds()
.stdout_move_str()
.run();

let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") {
(
result.stdout_str().to_string(),
result.stderr_str().to_string(),
)
} else {
// 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(
Some(result.tmpd()),
Some(result.code()),
result.succeeded(),
stdout.as_bytes(),
stderr.as_bytes(),
))
}