Merge branch 'fix_getgrouplist' into id_zero_2351

This commit is contained in:
Jan Scheer
2021-06-16 19:20:34 +02:00
18 changed files with 701 additions and 647 deletions
Generated
+1
View File
@@ -2597,6 +2597,7 @@ version = "0.0.6"
dependencies = [
"clap",
"libc",
"nix 0.20.0",
"uucore",
"uucore_procs",
]
+54
View File
@@ -28,6 +28,7 @@ use std::os::windows::io::AsRawHandle;
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH};
use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::InvalidEncodingHandling;
@@ -56,6 +57,7 @@ mod options {
pub const BLOCK_SIZE_1M: &str = "m";
pub const SEPARATE_DIRS: &str = "S";
pub const SUMMARIZE: &str = "s";
pub const THRESHOLD: &str = "threshold";
pub const SI: &str = "si";
pub const TIME: &str = "time";
pub const TIME_STYLE: &str = "time-style";
@@ -510,6 +512,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::ONE_FILE_SYSTEM)
.help("skip directories on different file systems")
)
.arg(
Arg::with_name(options::THRESHOLD)
.short("t")
.long(options::THRESHOLD)
.alias("th")
.value_name("SIZE")
.number_of_values(1)
.allow_hyphen_values(true)
.help("exclude entries smaller than SIZE if positive, \
or entries greater than SIZE if negative")
)
// .arg(
// Arg::with_name("")
// .short("x")
@@ -586,6 +599,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let block_size = u64::try_from(read_block_size(matches.value_of(options::BLOCK_SIZE))).unwrap();
let threshold = matches.value_of(options::THRESHOLD).map(|s| {
Threshold::from_str(s)
.unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::THRESHOLD)))
});
let multiplier: u64 = if matches.is_present(options::SI) {
1000
} else {
@@ -654,6 +672,11 @@ Try '{} --help' for more information.",
// See: http://linux.die.net/man/2/stat
stat.blocks * 512
};
if threshold.map_or(false, |threshold| threshold.should_exclude(size)) {
continue;
}
if matches.is_present(options::TIME) {
let tm = {
let secs = {
@@ -720,6 +743,37 @@ Try '{} --help' for more information.",
0
}
#[derive(Clone, Copy)]
enum Threshold {
Lower(u64),
Upper(u64),
}
impl FromStr for Threshold {
type Err = ParseSizeError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let offset = if s.starts_with(&['-', '+'][..]) { 1 } else { 0 };
let size = u64::try_from(parse_size(&s[offset..])?).unwrap();
if s.starts_with('-') {
Ok(Threshold::Upper(size))
} else {
Ok(Threshold::Lower(size))
}
}
}
impl Threshold {
fn should_exclude(&self, size: u64) -> bool {
match *self {
Threshold::Upper(threshold) => size > threshold,
Threshold::Lower(threshold) => size < threshold,
}
}
}
fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String {
// NOTE:
// GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection
+10 -19
View File
@@ -129,33 +129,24 @@ fn handle_obsolete(mut args: Vec<String>) -> (Vec<String>, Option<String>) {
}
fn table() {
let mut name_width = 0;
/* Compute the maximum width of a signal name. */
for s in &ALL_SIGNALS {
if s.name.len() > name_width {
name_width = s.name.len()
}
}
let name_width = ALL_SIGNALS.iter().map(|n| n.len()).max().unwrap();
for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
print!("{0: >#2} {1: <#8}", idx + 1, signal.name);
//TODO: obtain max signal width here
print!("{0: >#2} {1: <#2$}", idx, signal, name_width + 2);
if (idx + 1) % 7 == 0 {
println!();
}
}
println!()
}
fn print_signal(signal_name_or_value: &str) {
for signal in &ALL_SIGNALS {
if signal.name == signal_name_or_value
|| (format!("SIG{}", signal.name)) == signal_name_or_value
{
println!("{}", signal.value);
for (value, &signal) in ALL_SIGNALS.iter().enumerate() {
if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value {
println!("{}", value);
exit!(EXIT_OK as i32)
} else if signal_name_or_value == signal.value.to_string() {
println!("{}", signal.name);
} else if signal_name_or_value == value.to_string() {
println!("{}", signal);
exit!(EXIT_OK as i32)
}
}
@@ -165,8 +156,8 @@ fn print_signal(signal_name_or_value: &str) {
fn print_signals() {
let mut pos = 0;
for (idx, signal) in ALL_SIGNALS.iter().enumerate() {
pos += signal.name.len();
print!("{}", signal.name);
pos += signal.len();
print!("{}", signal);
if idx > 0 && pos > 73 {
println!();
pos = 0;
+186 -140
View File
File diff suppressed because it is too large Load Diff
+27 -9
View File
@@ -6,7 +6,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (vars) FiletestOp StrlenOp
// spell-checker:ignore (vars) egid euid FiletestOp StrlenOp
mod parser;
@@ -96,8 +96,10 @@ fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
"-e" => path(&f, PathCondition::Exists),
"-f" => path(&f, PathCondition::Regular),
"-g" => path(&f, PathCondition::GroupIdFlag),
"-G" => path(&f, PathCondition::GroupOwns),
"-h" => path(&f, PathCondition::SymLink),
"-L" => path(&f, PathCondition::SymLink),
"-O" => path(&f, PathCondition::UserOwns),
"-p" => path(&f, PathCondition::Fifo),
"-r" => path(&f, PathCondition::Readable),
"-S" => path(&f, PathCondition::Socket),
@@ -166,7 +168,9 @@ enum PathCondition {
Exists,
Regular,
GroupIdFlag,
GroupOwns,
SymLink,
UserOwns,
Fifo,
Readable,
Socket,
@@ -190,18 +194,28 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
Execute = 0o1,
}
let perm = |metadata: Metadata, p: Permission| {
let geteuid = || {
#[cfg(not(target_os = "redox"))]
let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
let euid = unsafe { libc::geteuid() };
#[cfg(target_os = "redox")]
let (uid, gid) = (
syscall::getuid().unwrap() as u32,
syscall::getgid().unwrap() as u32,
);
let euid = syscall::geteuid().unwrap() as u32;
if uid == metadata.uid() {
euid
};
let getegid = || {
#[cfg(not(target_os = "redox"))]
let egid = unsafe { libc::getegid() };
#[cfg(target_os = "redox")]
let egid = syscall::getegid().unwrap() as u32;
egid
};
let perm = |metadata: Metadata, p: Permission| {
if geteuid() == metadata.uid() {
metadata.mode() & ((p as u32) << 6) != 0
} else if gid == metadata.gid() {
} else if getegid() == metadata.gid() {
metadata.mode() & ((p as u32) << 3) != 0
} else {
metadata.mode() & (p as u32) != 0
@@ -230,7 +244,9 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
PathCondition::Exists => true,
PathCondition::Regular => file_type.is_file(),
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
PathCondition::GroupOwns => metadata.gid() == getegid(),
PathCondition::SymLink => metadata.file_type().is_symlink(),
PathCondition::UserOwns => metadata.uid() == geteuid(),
PathCondition::Fifo => file_type.is_fifo(),
PathCondition::Readable => perm(metadata, Permission::Read),
PathCondition::Socket => file_type.is_socket(),
@@ -257,7 +273,9 @@ fn path(path: &OsStr, condition: PathCondition) -> bool {
PathCondition::Exists => true,
PathCondition::Regular => stat.is_file(),
PathCondition::GroupIdFlag => false,
PathCondition::GroupOwns => unimplemented!(),
PathCondition::SymLink => false,
PathCondition::UserOwns => unimplemented!(),
PathCondition::Fifo => false,
PathCondition::Readable => false, // TODO
PathCondition::Socket => false,
+1
View File
@@ -17,6 +17,7 @@ path = "src/timeout.rs"
[dependencies]
clap = "2.33"
libc = "0.2.42"
nix = "0.20.0"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
+72 -46
View File
@@ -5,7 +5,7 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld
#[macro_use]
extern crate uucore;
@@ -17,13 +17,13 @@ use std::io::ErrorKind;
use std::process::{Command, Stdio};
use std::time::Duration;
use uucore::process::ChildExt;
use uucore::signals::signal_by_name_or_value;
use uucore::signals::{signal_by_name_or_value, signal_name_by_value};
use uucore::InvalidEncodingHandling;
static ABOUT: &str = "Start COMMAND, and kill it if still running after DURATION.";
fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!())
format!("{0} [OPTION] DURATION COMMAND...", executable!())
}
const ERR_EXIT_STATUS: i32 = 125;
@@ -33,22 +33,22 @@ pub mod options {
pub static KILL_AFTER: &str = "kill-after";
pub static SIGNAL: &str = "signal";
pub static PRESERVE_STATUS: &str = "preserve-status";
pub static VERBOSE: &str = "verbose";
// Positional args.
pub static DURATION: &str = "duration";
pub static COMMAND: &str = "command";
pub static ARGS: &str = "args";
}
struct Config {
foreground: bool,
kill_after: Duration,
kill_after: Option<Duration>,
signal: usize,
duration: Duration,
preserve_status: bool,
verbose: bool,
command: String,
command_args: Vec<String>,
command: Vec<String>,
}
impl Config {
@@ -66,23 +66,22 @@ impl Config {
_ => uucore::signals::signal_by_name_or_value("TERM").unwrap(),
};
let kill_after: Duration = match options.value_of(options::KILL_AFTER) {
Some(time) => uucore::parse_time::from_str(time).unwrap(),
None => Duration::new(0, 0),
};
let kill_after = options
.value_of(options::KILL_AFTER)
.map(|time| uucore::parse_time::from_str(time).unwrap());
let duration: Duration =
uucore::parse_time::from_str(options.value_of(options::DURATION).unwrap()).unwrap();
let preserve_status: bool = options.is_present(options::PRESERVE_STATUS);
let foreground = options.is_present(options::FOREGROUND);
let verbose = options.is_present(options::VERBOSE);
let command: String = options.value_of(options::COMMAND).unwrap().to_string();
let command_args: Vec<String> = match options.values_of(options::ARGS) {
Some(values) => values.map(|x| x.to_owned()).collect(),
None => vec![],
};
let command = options
.values_of(options::COMMAND)
.unwrap()
.map(String::from)
.collect::<Vec<_>>();
Config {
foreground,
@@ -91,7 +90,7 @@ impl Config {
duration,
preserve_status,
command,
command_args,
verbose,
}
}
}
@@ -128,6 +127,12 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("specify the signal to be sent on timeout; SIGNAL may be a name like 'HUP' or a number; see 'kill -l' for a list of signals")
.takes_value(true)
)
.arg(
Arg::with_name(options::VERBOSE)
.short("v")
.long(options::VERBOSE)
.help("diagnose to stderr any signal sent upon timeout")
)
.arg(
Arg::with_name(options::DURATION)
.index(1)
@@ -137,9 +142,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Arg::with_name(options::COMMAND)
.index(2)
.required(true)
)
.arg(
Arg::with_name(options::ARGS).multiple(true)
.multiple(true)
)
.setting(AppSettings::TrailingVarArg);
@@ -148,31 +151,42 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let config = Config::from(matches);
timeout(
&config.command,
&config.command_args,
config.duration,
config.signal,
config.kill_after,
config.foreground,
config.preserve_status,
config.verbose,
)
}
/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
fn unblock_sigchld() {
unsafe {
nix::sys::signal::signal(
nix::sys::signal::Signal::SIGCHLD,
nix::sys::signal::SigHandler::SigDfl,
)
.unwrap();
}
}
/// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes.
fn timeout(
cmdname: &str,
args: &[String],
cmd: &[String],
duration: Duration,
signal: usize,
kill_after: Duration,
kill_after: Option<Duration>,
foreground: bool,
preserve_status: bool,
verbose: bool,
) -> i32 {
if !foreground {
unsafe { libc::setpgid(0, 0) };
}
let mut process = match Command::new(cmdname)
.args(args)
let mut process = match Command::new(&cmd[0])
.args(&cmd[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
@@ -190,32 +204,44 @@ fn timeout(
}
}
};
unblock_sigchld();
match process.wait_or_timeout(duration) {
Ok(Some(status)) => status.code().unwrap_or_else(|| status.signal().unwrap()),
Ok(None) => {
if verbose {
show_error!(
"sending signal {} to command '{}'",
signal_name_by_value(signal).unwrap(),
cmd[0]
);
}
return_if_err!(ERR_EXIT_STATUS, process.send_signal(signal));
match process.wait_or_timeout(kill_after) {
Ok(Some(status)) => {
if preserve_status {
status.code().unwrap_or_else(|| status.signal().unwrap())
} else {
124
if let Some(kill_after) = kill_after {
match process.wait_or_timeout(kill_after) {
Ok(Some(status)) => {
if preserve_status {
status.code().unwrap_or_else(|| status.signal().unwrap())
} else {
124
}
}
}
Ok(None) => {
if kill_after == Duration::new(0, 0) {
// XXX: this may not be right
return 124;
Ok(None) => {
if verbose {
show_error!("sending signal KILL to command '{}'", cmd[0]);
}
return_if_err!(
ERR_EXIT_STATUS,
process.send_signal(
uucore::signals::signal_by_name_or_value("KILL").unwrap()
)
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
}
return_if_err!(
ERR_EXIT_STATUS,
process
.send_signal(uucore::signals::signal_by_name_or_value("KILL").unwrap())
);
return_if_err!(ERR_EXIT_STATUS, process.wait());
137
Err(_) => 124,
}
Err(_) => 124,
} else {
124
}
}
Err(_) => {
+9 -1
View File
@@ -166,7 +166,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}
if let Err(e) = File::create(path) {
show_warning!("cannot touch '{}': {}", path, e);
match e.kind() {
std::io::ErrorKind::NotFound => {
show_error!("cannot touch '{}': {}", path, "No such file or directory")
}
std::io::ErrorKind::PermissionDenied => {
show_error!("cannot touch '{}': {}", path, "Permission denied")
}
_ => show_error!("cannot touch '{}': {}", path, e),
}
error_code = 1;
continue;
};
+43 -18
View File
@@ -47,6 +47,9 @@ use std::io::Result as IOResult;
use std::ptr;
extern "C" {
/// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html
/// > The getgrouplist() function scans the group database to obtain
/// > the list of groups that user belongs to.
fn getgrouplist(
name: *const c_char,
gid: gid_t,
@@ -55,6 +58,13 @@ extern "C" {
) -> c_int;
}
/// From: https://man7.org/linux/man-pages/man2/getgroups.2.html
/// > getgroups() returns the supplementary group IDs of the calling
/// > process in list.
/// > If size is zero, list is not modified, but the total number of
/// > supplementary group IDs for the process is returned. This allows
/// > the caller to determine the size of a dynamically allocated list
/// > to be used in a further call to getgroups().
pub fn get_groups() -> IOResult<Vec<gid_t>> {
let ngroups = unsafe { getgroups(0, ptr::null_mut()) };
if ngroups == -1 {
@@ -83,17 +93,17 @@ pub fn get_groups() -> IOResult<Vec<gid_t>> {
/// for `id --groups --real` if `gid` and `egid` are not equal.
///
/// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html
/// As implied by the definition of supplementary groups, the
/// effective group ID may appear in the array returned by
/// getgroups() or it may be returned only by getegid(). Duplication
/// may exist, but the application needs to call getegid() to be sure
/// of getting all of the information. Various implementation
/// variations and administrative sequences cause the set of groups
/// appearing in the result of getgroups() to vary in order and as to
/// whether the effective group ID is included, even when the set of
/// groups is the same (in the mathematical sense of ``set''). (The
/// history of a process and its parents could affect the details of
/// the result.)
/// > As implied by the definition of supplementary groups, the
/// > effective group ID may appear in the array returned by
/// > getgroups() or it may be returned only by getegid(). Duplication
/// > may exist, but the application needs to call getegid() to be sure
/// > of getting all of the information. Various implementation
/// > variations and administrative sequences cause the set of groups
/// > appearing in the result of getgroups() to vary in order and as to
/// > whether the effective group ID is included, even when the set of
/// > groups is the same (in the mathematical sense of ``set''). (The
/// > history of a process and its parents could affect the details of
/// > the result.)
#[cfg(all(unix, feature = "process"))]
pub fn get_groups_gnu(arg_id: Option<u32>) -> IOResult<Vec<gid_t>> {
let groups = get_groups()?;
@@ -187,20 +197,35 @@ impl Passwd {
/// This is a wrapper function for `libc::getgrouplist`.
///
/// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html
/// If the user is a member of more than *ngroups groups, then
/// getgrouplist() returns -1. In this case, the value returned in
/// *ngroups can be used to resize the buffer passed to a further
/// call getgrouplist().
/// > If the number of groups of which user is a member is less than or
/// > equal to *ngroups, then the value *ngroups is returned.
/// > If the user is a member of more than *ngroups groups, then
/// > getgrouplist() returns -1. In this case, the value returned in
/// > *ngroups can be used to resize the buffer passed to a further
/// > call getgrouplist().
///
/// However, on macOS/darwin (and maybe others?) `getgrouplist` does
/// not update `ngroups` if `ngroups` is too small. Therefore, if not
/// updated by `getgrouplist`, `ngroups` needs to be increased in a
/// loop until `getgrouplist` stops returning -1.
pub fn belongs_to(&self) -> Vec<gid_t> {
let mut ngroups: c_int = 8;
let mut ngroups_old: c_int;
let mut groups = Vec::with_capacity(ngroups as usize);
let gid = self.inner.pw_gid;
let name = self.inner.pw_name;
unsafe {
if getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) == -1 {
loop {
ngroups_old = ngroups;
if unsafe { getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) } == -1 {
if ngroups == ngroups_old {
ngroups *= 2;
}
groups.resize(ngroups as usize, 0);
getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups);
} else {
break;
}
}
unsafe {
groups.set_len(ngroups as usize);
}
groups.truncate(ngroups as usize);
+10
View File
@@ -17,18 +17,22 @@ use std::process::ExitStatus as StdExitStatus;
use std::thread;
use std::time::{Duration, Instant};
/// `geteuid()` returns the effective user ID of the calling process.
pub fn geteuid() -> uid_t {
unsafe { libc::geteuid() }
}
/// `getegid()` returns the effective group ID of the calling process.
pub fn getegid() -> gid_t {
unsafe { libc::getegid() }
}
/// `getgid()` returns the real group ID of the calling process.
pub fn getgid() -> gid_t {
unsafe { libc::getgid() }
}
/// `getuid()` returns the real user ID of the calling process.
pub fn getuid() -> uid_t {
unsafe { libc::getuid() }
}
@@ -93,6 +97,7 @@ pub trait ChildExt {
fn send_signal(&mut self, signal: usize) -> io::Result<()>;
/// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
}
@@ -106,6 +111,11 @@ impl ChildExt for Child {
}
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) {
return self
.wait()
.map(|status| Some(ExitStatus::from_std_status(status)));
}
// .try_wait() doesn't drop stdin, so we do it manually
drop(self.stdin.take());
+26 -284
View File
@@ -10,11 +10,6 @@
pub static DEFAULT_SIGNAL: usize = 15;
pub struct Signal<'a> {
pub name: &'a str,
pub value: usize,
}
/*
Linux Programmer's Manual
@@ -29,131 +24,10 @@ Linux Programmer's Manual
*/
#[cfg(target_os = "linux")]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [
Signal {
name: "HUP",
value: 1,
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "BUS",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "USR1",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "USR2",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "STKFLT",
value: 16,
},
Signal {
name: "CHLD",
value: 17,
},
Signal {
name: "CONT",
value: 18,
},
Signal {
name: "STOP",
value: 19,
},
Signal {
name: "TSTP",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "URG",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "POLL",
value: 29,
},
Signal {
name: "PWR",
value: 30,
},
Signal {
name: "SYS",
value: 31,
},
pub static ALL_SIGNALS: [&str; 32] = [
"EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
"USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU",
"URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS",
];
/*
@@ -198,131 +72,10 @@ No Name Default Action Description
*/
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
pub static ALL_SIGNALS: [Signal<'static>; 31] = [
Signal {
name: "HUP",
value: 1,
},
Signal {
name: "INT",
value: 2,
},
Signal {
name: "QUIT",
value: 3,
},
Signal {
name: "ILL",
value: 4,
},
Signal {
name: "TRAP",
value: 5,
},
Signal {
name: "ABRT",
value: 6,
},
Signal {
name: "EMT",
value: 7,
},
Signal {
name: "FPE",
value: 8,
},
Signal {
name: "KILL",
value: 9,
},
Signal {
name: "BUS",
value: 10,
},
Signal {
name: "SEGV",
value: 11,
},
Signal {
name: "SYS",
value: 12,
},
Signal {
name: "PIPE",
value: 13,
},
Signal {
name: "ALRM",
value: 14,
},
Signal {
name: "TERM",
value: 15,
},
Signal {
name: "URG",
value: 16,
},
Signal {
name: "STOP",
value: 17,
},
Signal {
name: "TSTP",
value: 18,
},
Signal {
name: "CONT",
value: 19,
},
Signal {
name: "CHLD",
value: 20,
},
Signal {
name: "TTIN",
value: 21,
},
Signal {
name: "TTOU",
value: 22,
},
Signal {
name: "IO",
value: 23,
},
Signal {
name: "XCPU",
value: 24,
},
Signal {
name: "XFSZ",
value: 25,
},
Signal {
name: "VTALRM",
value: 26,
},
Signal {
name: "PROF",
value: 27,
},
Signal {
name: "WINCH",
value: 28,
},
Signal {
name: "INFO",
value: 29,
},
Signal {
name: "USR1",
value: 30,
},
Signal {
name: "USR2",
value: 31,
},
pub static ALL_SIGNALS: [&str; 32] = [
"EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
"SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
"XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2",
];
pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
@@ -335,56 +88,45 @@ pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
}
let signal_name = signal_name_or_value.trim_start_matches("SIG");
ALL_SIGNALS
.iter()
.find(|s| s.name == signal_name)
.map(|s| s.value)
ALL_SIGNALS.iter().position(|&s| s == signal_name)
}
#[inline(always)]
pub fn is_signal(num: usize) -> bool {
// Named signals start at 1
num <= ALL_SIGNALS.len()
num < ALL_SIGNALS.len()
}
#[test]
fn signals_all_contiguous() {
for (i, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal.value, i + 1);
}
}
#[test]
fn signals_all_are_signal() {
for signal in &ALL_SIGNALS {
assert!(is_signal(signal.value));
}
pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> {
ALL_SIGNALS.get(signal_value).copied()
}
#[test]
fn signal_by_value() {
assert_eq!(signal_by_name_or_value("0"), Some(0));
for signal in &ALL_SIGNALS {
assert_eq!(
signal_by_name_or_value(&signal.value.to_string()),
Some(signal.value)
);
for (value, _signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_by_name_or_value(&value.to_string()), Some(value));
}
}
#[test]
fn signal_by_short_name() {
for signal in &ALL_SIGNALS {
assert_eq!(signal_by_name_or_value(signal.name), Some(signal.value));
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_by_name_or_value(signal), Some(value));
}
}
#[test]
fn signal_by_long_name() {
for signal in &ALL_SIGNALS {
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(
signal_by_name_or_value(&format!("SIG{}", signal.name)),
Some(signal.value)
signal_by_name_or_value(&format!("SIG{}", signal)),
Some(value)
);
}
}
#[test]
fn name() {
for (value, signal) in ALL_SIGNALS.iter().enumerate() {
assert_eq!(signal_name_by_value(value), Some(*signal));
}
}
+37 -13
View File
@@ -80,19 +80,22 @@ fn _du_basics_subdir(s: &str) {
#[test]
fn test_du_invalid_size() {
new_ucmd!()
.arg("--block-size=1fb4t")
.arg("/tmp")
.fails()
.code_is(1)
.stderr_only("du: invalid --block-size argument '1fb4t'");
#[cfg(not(target_pointer_width = "128"))]
new_ucmd!()
.arg("--block-size=1Y")
.arg("/tmp")
.fails()
.code_is(1)
.stderr_only("du: --block-size argument '1Y' too large");
let args = &["block-size", "threshold"];
for s in args {
new_ucmd!()
.arg(format!("--{}=1fb4t", s))
.arg("/tmp")
.fails()
.code_is(1)
.stderr_only(format!("du: invalid --{} argument '1fb4t'", s));
#[cfg(not(target_pointer_width = "128"))]
new_ucmd!()
.arg(format!("--{}=1Y", s))
.arg("/tmp")
.fails()
.code_is(1)
.stderr_only(format!("du: --{} argument '1Y' too large", s));
}
}
#[test]
@@ -351,3 +354,24 @@ fn test_du_one_file_system() {
}
_du_basics_subdir(result.stdout_str());
}
#[test]
fn test_du_threshold() {
let scene = TestScenario::new(util_name!());
let threshold = if cfg!(windows) { "7K" } else { "10K" };
scene
.ucmd()
.arg(format!("--threshold={}", threshold))
.succeeds()
.stdout_contains("links")
.stdout_does_not_contain("deeper");
scene
.ucmd()
.arg(format!("--threshold=-{}", threshold))
.succeeds()
.stdout_does_not_contain("links")
.stdout_contains("deeper");
}
+57 -99
View File
@@ -3,6 +3,7 @@
use crate::common::util::*;
use chrono::offset::Local;
use chrono::DateTime;
use chrono::Duration;
use std::fs::metadata;
fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
@@ -20,8 +21,22 @@ fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
.unwrap_or_default()
}
fn now_time() -> String {
Local::now().format("%b %d %H:%M %Y").to_string()
fn all_minutes(from: DateTime<Local>, to: DateTime<Local>) -> Vec<String> {
const FORMAT: &str = "%b %d %H:%M %Y";
let mut vec = vec![];
let mut current = from;
while current < to {
vec.push(current.format(FORMAT).to_string());
current = current + Duration::minutes(1);
}
vec
}
fn valid_last_modified_template_vars(from: DateTime<Local>) -> Vec<Vec<(String, String)>> {
all_minutes(from, Local::now())
.into_iter()
.map(|time| vec![("{last_modified_time}".to_string(), time)])
.collect()
}
#[test]
@@ -33,10 +48,7 @@ fn test_without_any_options() {
scenario
.args(&[test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -48,10 +60,7 @@ fn test_with_numbering_option_with_number_width() {
scenario
.args(&["-n", "2", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -66,10 +75,7 @@ fn test_with_long_header_option() {
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![
(&"{last_modified_time}".to_string(), &value),
(&"{header}".to_string(), &header.to_string()),
],
&[("{last_modified_time}", &value), ("{header}", header)],
);
new_ucmd!()
@@ -77,10 +83,7 @@ fn test_with_long_header_option() {
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![
(&"{last_modified_time}".to_string(), &value),
(&"{header}".to_string(), &header.to_string()),
],
&[("{last_modified_time}", &value), ("{header}", header)],
);
}
@@ -93,18 +96,12 @@ fn test_with_double_space_option() {
scenario
.args(&["-d", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&["--double-space", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -116,10 +113,7 @@ fn test_with_first_line_number_option() {
scenario
.args(&["-N", "5", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -131,10 +125,7 @@ fn test_with_first_line_number_long_option() {
scenario
.args(&["--first-line-number=5", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -146,10 +137,7 @@ fn test_with_number_option_with_custom_separator_char() {
scenario
.args(&["-nc", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -161,10 +149,7 @@ fn test_with_number_option_with_custom_separator_char_and_width() {
scenario
.args(&["-nc1", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -207,25 +192,19 @@ fn test_with_page_range() {
scenario
.args(&["--pages=15", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&["+15", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&["--pages=15:17", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)],
&[("{last_modified_time}", &value)],
);
new_ucmd!()
@@ -233,7 +212,7 @@ fn test_with_page_range() {
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)],
&[("{last_modified_time}", &value)],
);
}
@@ -246,10 +225,7 @@ fn test_with_no_header_trailer_option() {
scenario
.args(&["-t", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -262,10 +238,7 @@ fn test_with_page_length_option() {
scenario
.args(&["--pages=2:3", "-l", "100", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&["--pages=2:3", "-l", "5", "-n", test_file_path])
@@ -288,14 +261,14 @@ fn test_with_suppress_error_option() {
fn test_with_stdin() {
let expected_file_path = "stdin.log.expected";
let mut scenario = new_ucmd!();
let now = now_time();
let start = Local::now();
scenario
.pipe_in_fixture("stdin.log")
.args(&["--pages=1:2", "-n", "-"])
.run()
.stdout_is_templated_fixture(
.stdout_is_templated_fixture_any(
expected_file_path,
vec![(&"{last_modified_time}".to_string(), &now)],
&valid_last_modified_template_vars(start),
);
}
@@ -308,18 +281,12 @@ fn test_with_column() {
scenario
.args(&["--pages=3:5", "--column=3", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&["--pages=3:5", "-3", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -331,10 +298,7 @@ fn test_with_column_across_option() {
scenario
.args(&["--pages=3:5", "--column=3", "-a", "-n", test_file_path])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -354,10 +318,7 @@ fn test_with_column_across_option_and_column_separator() {
test_file_path,
])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
new_ucmd!()
.args(&[
@@ -371,7 +332,7 @@ fn test_with_column_across_option_and_column_separator() {
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &value)],
&[("{last_modified_time}", &value)],
);
}
@@ -382,25 +343,25 @@ fn test_with_mpr() {
let expected_test_file_path = "mpr.log.expected";
let expected_test_file_path1 = "mpr1.log.expected";
let expected_test_file_path2 = "mpr2.log.expected";
let now = now_time();
let start = Local::now();
new_ucmd!()
.args(&["--pages=1:2", "-m", "-n", test_file_path, test_file_path1])
.succeeds()
.stdout_is_templated_fixture(
.stdout_is_templated_fixture_any(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &now)],
&valid_last_modified_template_vars(start),
);
let now = now_time();
let start = Local::now();
new_ucmd!()
.args(&["--pages=2:4", "-m", "-n", test_file_path, test_file_path1])
.succeeds()
.stdout_is_templated_fixture(
.stdout_is_templated_fixture_any(
expected_test_file_path1,
vec![(&"{last_modified_time}".to_string(), &now)],
&valid_last_modified_template_vars(start),
);
let now = now_time();
let start = Local::now();
new_ucmd!()
.args(&[
"--pages=1:2",
@@ -413,9 +374,9 @@ fn test_with_mpr() {
test_file_path,
])
.succeeds()
.stdout_is_templated_fixture(
.stdout_is_templated_fixture_any(
expected_test_file_path2,
vec![(&"{last_modified_time}".to_string(), &now)],
&valid_last_modified_template_vars(start),
);
}
@@ -452,10 +413,7 @@ fn test_with_offset_space_option() {
test_file_path,
])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
vec![(&"{last_modified_time}".to_string(), &value)],
);
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}
#[test]
@@ -497,9 +455,9 @@ fn test_with_pr_core_utils_tests() {
scenario_with_expected_status.stdout_is_templated_fixture(
test_file_path,
vec![
(&"{last_modified_time}".to_string(), &value),
(&"{file_name}".to_string(), &input_file_path.to_string()),
&[
("{last_modified_time}", &value),
("{file_name}", input_file_path),
],
);
}
@@ -511,12 +469,12 @@ fn test_with_join_lines_option() {
let test_file_2 = "test.log";
let expected_file_path = "joined.log.expected";
let mut scenario = new_ucmd!();
let now = now_time();
let start = Local::now();
scenario
.args(&["+1:2", "-J", "-m", test_file_1, test_file_2])
.run()
.stdout_is_templated_fixture(
.stdout_is_templated_fixture_any(
expected_file_path,
vec![(&"{last_modified_time}".to_string(), &now)],
&valid_last_modified_template_vars(start),
);
}
+20 -16
View File
@@ -762,26 +762,30 @@ fn test_pipe() {
#[test]
fn test_check() {
new_ucmd!()
.arg("-c")
.arg("check_fail.txt")
.fails()
.stdout_is("sort: check_fail.txt:6: disorder: 5\n");
for diagnose_arg in &["-c", "--check", "--check=diagnose-first"] {
new_ucmd!()
.arg(diagnose_arg)
.arg("check_fail.txt")
.fails()
.stdout_is("sort: check_fail.txt:6: disorder: 5\n");
new_ucmd!()
.arg("-c")
.arg("multiple_files.expected")
.succeeds()
.stdout_is("");
new_ucmd!()
.arg(diagnose_arg)
.arg("multiple_files.expected")
.succeeds()
.stdout_is("");
}
}
#[test]
fn test_check_silent() {
new_ucmd!()
.arg("-C")
.arg("check_fail.txt")
.fails()
.stdout_is("");
for silent_arg in &["-C", "--check=silent", "--check=quiet"] {
new_ucmd!()
.arg(silent_arg)
.arg("check_fail.txt")
.fails()
.stdout_is("");
}
}
#[test]
@@ -835,7 +839,7 @@ fn test_nonexistent_file() {
#[test]
fn test_blanks() {
test_helper("blanks", &["-b", "--ignore-blanks"]);
test_helper("blanks", &["-b", "--ignore-leading-blanks"]);
}
#[test]
+47 -1
View File
@@ -8,7 +8,7 @@
// file that was distributed with this source code.
//
// spell-checker:ignore (words) pseudofloat
// spell-checker:ignore (words) egid euid pseudofloat
use crate::common::util::*;
@@ -476,6 +476,52 @@ fn test_nonexistent_file_is_not_symlink() {
.succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_euid() {
new_ucmd!().args(&["-O", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-O", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_euid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-O", "/bin/sh"])
.succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_file_owned_by_egid() {
new_ucmd!().args(&["-G", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_nonexistent_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-G", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_file_not_owned_by_egid() {
new_ucmd!()
.args(&["-f", "/bin/sh", "-a", "!", "-G", "/bin/sh"])
.succeeds();
}
#[test]
fn test_op_precedence_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();
+36
View File
@@ -9,3 +9,39 @@ fn test_subcommand_return_code() {
new_ucmd!().arg("1").arg("false").run().status_code(1);
}
#[test]
fn test_command_with_args() {
new_ucmd!()
.args(&["1700", "echo", "-n", "abcd"])
.succeeds()
.stdout_only("abcd");
}
#[test]
fn test_verbose() {
for &verbose_flag in &["-v", "--verbose"] {
new_ucmd!()
.args(&[verbose_flag, ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal TERM to command 'sleep'");
new_ucmd!()
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"])
.fails()
.stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'");
}
}
#[test]
fn test_zero_timeout() {
new_ucmd!()
.args(&["-v", "0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
new_ucmd!()
.args(&["-v", "0", "-s0", "-k0", "sleep", ".1"])
.succeeds()
.no_stderr()
.no_stdout();
}
+35
View File
@@ -6,6 +6,7 @@ use self::touch::filetime::{self, FileTime};
extern crate time;
use crate::common::util::*;
use std::path::PathBuf;
fn get_file_times(at: &AtPath, path: &str) -> (FileTime, FileTime) {
let m = at.metadata(path);
@@ -466,3 +467,37 @@ fn test_touch_trailing_slash() {
let file = "no-file/";
ucmd.args(&[file]).fails();
}
#[test]
fn test_touch_no_such_file_error_msg() {
let dirname = "nonexistent";
let filename = "file";
let path = PathBuf::from(dirname).join(filename);
let path_str = path.to_str().unwrap();
new_ucmd!().arg(&path).fails().stderr_only(format!(
"touch: cannot touch '{}': No such file or directory",
path_str
));
}
#[test]
#[cfg(unix)]
fn test_touch_permission_denied_error_msg() {
let (at, mut ucmd) = at_and_ucmd!();
let dirname = "dir_with_read_only_access";
let filename = "file";
let path = PathBuf::from(dirname).join(filename);
let path_str = path.to_str().unwrap();
// create dest without write permissions
at.mkdir(dirname);
at.set_readonly(dirname);
let full_path = at.plus_as_string(path_str);
ucmd.arg(&full_path).fails().stderr_only(format!(
"touch: cannot touch '{}': Permission denied",
&full_path
));
}
+30 -1
View File
@@ -223,6 +223,18 @@ impl CmdResult {
self
}
/// like `stdout_is`, but succeeds if any elements of `expected` matches stdout.
pub fn stdout_is_any<T: AsRef<str> + std::fmt::Debug>(&self, expected: Vec<T>) -> &CmdResult {
if !expected.iter().any(|msg| self.stdout_str() == msg.as_ref()) {
panic!(
"stdout was {}\nExpected any of {:#?}",
self.stdout_str(),
expected
)
}
self
}
/// Like `stdout_is` but newlines are normalized to `\n`.
pub fn normalized_newlines_stdout_is<T: AsRef<str>>(&self, msg: T) -> &CmdResult {
let msg = msg.as_ref().replace("\r\n", "\n");
@@ -247,7 +259,7 @@ impl CmdResult {
pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>(
&self,
file_rel_path: T,
template_vars: Vec<(&String, &String)>,
template_vars: &[(&str, &str)],
) -> &CmdResult {
let mut contents =
String::from_utf8(read_scenario_fixture(&self.tmpd, file_rel_path)).unwrap();
@@ -257,6 +269,23 @@ impl CmdResult {
self.stdout_is(contents)
}
/// like `stdout_is_templated_fixture`, but succeeds if any replacement by `template_vars` results in the actual stdout.
pub fn stdout_is_templated_fixture_any<T: AsRef<OsStr>>(
&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.iter() {
contents = contents.replace(&kv.0, &kv.1);
}
contents
});
self.stdout_is_any(possible_values.collect());
}
/// asserts that the command resulted in stderr stream output that equals the
/// passed in value, when both are trimmed of trailing whitespace
/// stderr_only is a better choice unless stdout may or will be non-empty