#![allow(dead_code)]
#![allow(
clippy::too_many_lines,
clippy::should_panic_without_expect,
clippy::missing_errors_doc
)]
use core::str;
#[cfg(unix)]
use libc::mode_t;
#[cfg(unix)]
use nix::pty::OpenptyResult;
#[cfg(unix)]
use nix::sys;
use pretty_assertions::assert_eq;
#[cfg(unix)]
use rlimit::setrlimit;
use std::borrow::Cow;
use std::collections::VecDeque;
#[cfg(not(windows))]
use std::ffi::CString;
use std::ffi::{OsStr, OsString};
use std::fs::{self, File, OpenOptions, hard_link, remove_file};
use std::io::{self, BufWriter, Read, Result, Write};
#[cfg(unix)]
use std::os::fd::OwnedFd;
#[cfg(unix)]
use std::os::unix::fs::{PermissionsExt, symlink as symlink_dir, symlink as symlink_file};
#[cfg(unix)]
use std::os::unix::net::UnixListener;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::fs::{symlink_dir, symlink_file};
#[cfg(windows)]
use std::path::MAIN_SEPARATOR_STR;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Output, Stdio};
use std::rc::Rc;
use std::sync::mpsc::{self, RecvTimeoutError};
use std::thread::{JoinHandle, sleep};
use std::time::{Duration, Instant};
use std::{env, hint, mem, thread};
use tempfile::{Builder, TempDir};
use std::sync::OnceLock;
static TESTS_DIR: &str = "tests";
static FIXTURES_DIR: &str = "fixtures";
static ALREADY_RUN: &str = " you have already run this UCommand, if you want to run \
another command in the same test, use TestScenario::new instead of \
testing();";
static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly.";
static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin";
static END_OF_TRANSMISSION_SEQUENCE: &[u8] = b"\n\x04";
static TESTS_BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
pub fn get_tests_binary() -> &'static str {
TESTS_BINARY_PATH.get_or_init(|| {
if let Ok(path) = env::var("UUTESTS_BINARY_PATH") {
return PathBuf::from(path);
}
panic!("Could not determine coreutils binary path. Please set UUTESTS_BINARY_PATH environment variable");
})
.to_str()
.unwrap()
}
#[macro_export]
macro_rules! get_tests_binary {
() => {
$crate::util::get_tests_binary()
};
}
pub const PATH: &str = env!("PATH");
const DEFAULT_ENV: [(&str, &str); 2] = [("LC_ALL", "C"), ("TZ", "UTC")];
pub fn is_ci() -> bool {
std::env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true"))
}
fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: Option<&Rc<TempDir>>, file_rel_path: S) -> Vec<u8> {
let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path();
AtPath::new(tmpdir_path).read_bytes(file_rel_path.as_ref().to_str().unwrap())
}
#[derive(Debug, Clone)]
pub struct CmdResult {
bin_path: PathBuf,
util_name: Option<String>,
tmpd: Option<Rc<TempDir>>,
exit_status: Option<ExitStatus>,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl CmdResult {
pub fn new<S, T, U, V>(
bin_path: S,
util_name: Option<T>,
tmpd: Option<Rc<TempDir>>,
exit_status: Option<ExitStatus>,
stdout: U,
stderr: V,
) -> Self
where
S: Into<PathBuf>,
T: AsRef<str>,
U: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{
Self {
bin_path: bin_path.into(),
util_name: util_name.map(|s| s.as_ref().into()),
tmpd,
exit_status,
stdout: stdout.into(),
stderr: stderr.into(),
}
}
pub fn stdout_apply<'a, F, R>(&'a self, function: F) -> Self
where
F: Fn(&'a [u8]) -> R,
R: Into<Vec<u8>>,
{
Self::new(
self.bin_path.clone(),
self.util_name.clone(),
self.tmpd.clone(),
self.exit_status,
function(&self.stdout),
self.stderr.as_slice(),
)
}
pub fn stdout_str_apply<'a, F, R>(&'a self, function: F) -> Self
where
F: Fn(&'a str) -> R,
R: Into<Vec<u8>>,
{
Self::new(
self.bin_path.clone(),
self.util_name.clone(),
self.tmpd.clone(),
self.exit_status,
function(self.stdout_str()),
self.stderr.as_slice(),
)
}
pub fn stderr_apply<'a, F, R>(&'a self, function: F) -> Self
where
F: Fn(&'a [u8]) -> R,
R: Into<Vec<u8>>,
{
Self::new(
self.bin_path.clone(),
self.util_name.clone(),
self.tmpd.clone(),
self.exit_status,
self.stdout.as_slice(),
function(&self.stderr),
)
}
pub fn stderr_str_apply<'a, F, R>(&'a self, function: F) -> Self
where
F: Fn(&'a str) -> R,
R: Into<Vec<u8>>,
{
Self::new(
self.bin_path.clone(),
self.util_name.clone(),
self.tmpd.clone(),
self.exit_status,
self.stdout.as_slice(),
function(self.stderr_str()),
)
}
#[track_caller]
pub fn stdout_check<'a, F>(&'a self, predicate: F) -> &'a Self
where
F: Fn(&'a [u8]) -> bool,
{
assert!(
predicate(&self.stdout),
"Predicate for stdout as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n",
self.stdout,
self.stderr
);
self
}
#[track_caller]
pub fn stdout_str_check<'a, F>(&'a self, predicate: F) -> &'a Self
where
F: Fn(&'a str) -> bool,
{
assert!(
predicate(self.stdout_str()),
"Predicate for stdout as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n",
self.stdout_str(),
self.stderr_str()
);
self
}
#[track_caller]
pub fn stderr_check<'a, F>(&'a self, predicate: F) -> &'a Self
where
F: Fn(&'a [u8]) -> bool,
{
assert!(
predicate(&self.stderr),
"Predicate for stderr as `bytes` evaluated to false.\nstdout='{:?}'\nstderr='{:?}'\n",
self.stdout,
self.stderr
);
self
}
#[track_caller]
pub fn stderr_str_check<'a, F>(&'a self, predicate: F) -> &'a Self
where
F: Fn(&'a str) -> bool,
{
assert!(
predicate(self.stderr_str()),
"Predicate for stderr as `str` evaluated to false.\nstdout='{}'\nstderr='{}'\n",
self.stdout_str(),
self.stderr_str()
);
self
}
pub fn try_exit_status(&self) -> Option<ExitStatus> {
self.exit_status
}
pub fn exit_status(&self) -> ExitStatus {
self.try_exit_status()
.expect("Program must be run first or has not finished, yet")
}
#[cfg(unix)]
pub fn signal(&self) -> Option<i32> {
self.exit_status().signal()
}
#[cfg(unix)]
#[track_caller]
pub fn signal_is(&self, value: i32) -> &Self {
let actual = self.signal().unwrap_or_else(|| {
panic!(
"Expected process to be terminated by the '{value}' signal, but exit status is: '{}'",
self.try_exit_status()
.map_or("Not available".to_string(), |e| e.to_string())
)
});
assert_eq!(actual, value);
self
}
#[cfg(unix)]
#[track_caller]
pub fn signal_name_is(&self, name: &str) -> &Self {
use uucore::signals::signal_by_name_or_value;
let expected: i32 = signal_by_name_or_value(name)
.unwrap_or_else(|| panic!("Invalid signal name or value: '{name}'"))
.try_into()
.unwrap();
let actual = self.signal().unwrap_or_else(|| {
panic!(
"Expected process to be terminated by the '{name}' signal, but exit status is: '{}'",
self.try_exit_status()
.map_or("Not available".to_string(), |e| e.to_string())
)
});
assert_eq!(actual, expected);
self
}
pub fn stdout(&self) -> &[u8] {
&self.stdout
}
pub fn stdout_str(&self) -> &str {
std::str::from_utf8(&self.stdout).unwrap()
}
pub fn stdout_str_lossy(self) -> String {
String::from_utf8_lossy(&self.stdout).to_string()
}
pub fn stdout_move_str(self) -> String {
String::from_utf8(self.stdout).unwrap()
}
pub fn stdout_move_bytes(self) -> Vec<u8> {
self.stdout
}
pub fn stderr(&self) -> &[u8] {
&self.stderr
}
pub fn stderr_str(&self) -> &str {
std::str::from_utf8(&self.stderr).unwrap()
}
pub fn stderr_str_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.stderr)
}
pub fn stderr_move_str(self) -> String {
String::from_utf8(self.stderr).unwrap()
}
pub fn stderr_move_bytes(self) -> Vec<u8> {
self.stderr
}
pub fn code(&self) -> i32 {
self.exit_status().code().unwrap()
}
#[track_caller]
pub fn code_is(&self, expected_code: i32) -> &Self {
let fails = self.code() != expected_code;
if fails {
eprintln!(
"stdout:\n{}\nstderr:\n{}",
self.stdout_str(),
self.stderr_str()
);
}
assert_eq!(self.code(), expected_code);
self
}
pub fn tmpd(&self) -> Rc<TempDir> {
match &self.tmpd {
Some(ptr) => ptr.clone(),
None => panic!("Command not associated with a TempDir"),
}
}
pub fn succeeded(&self) -> bool {
self.exit_status.is_none_or(|e| e.success())
}
#[track_caller]
pub fn success(&self) -> &Self {
assert!(
self.succeeded(),
"Command was expected to succeed. code: {}\nstdout = {}\n stderr = {}",
self.code(),
self.stdout_str(),
self.stderr_str()
);
self
}
#[track_caller]
pub fn failure(&self) -> &Self {
assert!(
!self.succeeded(),
"Command was expected to fail.\nstdout = {}\n stderr = {}",
self.stdout_str(),
self.stderr_str()
);
self
}
#[track_caller]
pub fn no_stderr(&self) -> &Self {
assert!(
self.stderr.is_empty(),
"Expected stderr to be empty, but it's:\n{}",
self.stderr_str()
);
self
}
#[track_caller]
pub fn no_stdout(&self) -> &Self {
assert!(
self.stdout.is_empty(),
"Expected stdout to be empty, but it's:\n{}",
self.stdout_str()
);
self
}
#[track_caller]
pub fn no_output(&self) -> &Self {
self.no_stdout().no_stderr()
}
#[track_caller]
pub fn stdout_is<T: AsRef<str>>(&self, msg: T) -> &Self {
assert_eq!(self.stdout_str(), String::from(msg.as_ref()));
self
}
#[track_caller]
pub fn stdout_is_any<T: AsRef<str> + std::fmt::Debug>(&self, expected: &[T]) -> &Self {
assert!(
expected.iter().any(|msg| self.stdout_str() == msg.as_ref()),
"stdout was {}\nExpected any of {expected:#?}",
self.stdout_str(),
);
self
}
#[track_caller]
pub fn normalized_newlines_stdout_is<T: AsRef<str>>(&self, msg: T) -> &Self {
let msg = msg.as_ref().replace("\r\n", "\n");
assert_eq!(self.stdout_str().replace("\r\n", "\n"), msg);
self
}
#[track_caller]
pub fn stdout_is_bytes<T: AsRef<[u8]>>(&self, msg: T) -> &Self {
assert_eq!(
self.stdout,
msg.as_ref(),
"stdout as bytes wasn't equal to expected bytes. Result as strings:\nstdout ='{:?}'\nexpected='{:?}'",
std::str::from_utf8(&self.stdout),
std::str::from_utf8(msg.as_ref()),
);
self
}
#[track_caller]
pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path);
self.stdout_is(String::from_utf8(contents).unwrap())
}
#[track_caller]
pub fn stdout_is_fixture_bytes<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path);
self.stdout_is_bytes(contents)
}
#[track_caller]
pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>(
&self,
file_rel_path: T,
template_vars: &[(&str, &str)],
) -> &Self {
let mut contents =
String::from_utf8(read_scenario_fixture(self.tmpd.as_ref(), file_rel_path)).unwrap();
for kv in template_vars {
contents = contents.replace(kv.0, kv.1);
}
self.stdout_is(contents)
}
#[track_caller]
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.as_ref(), file_rel_path)).unwrap();
let possible_values = template_vars.iter().map(|vars| {
let mut contents = contents.clone();
for kv in vars {
contents = contents.replace(&kv.0, &kv.1);
}
contents
});
self.stdout_is_any(&possible_values.collect::<Vec<_>>());
}
#[track_caller]
pub fn stderr_is<T: AsRef<str>>(&self, msg: T) -> &Self {
assert_eq!(self.stderr_str(), msg.as_ref());
self
}
#[track_caller]
pub fn stderr_is_bytes<T: AsRef<[u8]>>(&self, msg: T) -> &Self {
assert_eq!(
&self.stderr,
msg.as_ref(),
"stderr as bytes wasn't equal to expected bytes. Result as strings:\nstderr ='{:?}'\nexpected='{:?}'",
std::str::from_utf8(&self.stderr),
std::str::from_utf8(msg.as_ref())
);
self
}
#[track_caller]
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path);
self.stderr_is(String::from_utf8(contents).unwrap())
}
#[track_caller]
pub fn stdout_only<T: AsRef<str>>(&self, msg: T) -> &Self {
self.no_stderr().stdout_is(msg)
}
#[track_caller]
pub fn stdout_only_bytes<T: AsRef<[u8]>>(&self, msg: T) -> &Self {
self.no_stderr().stdout_is_bytes(msg)
}
#[track_caller]
pub fn stdout_only_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path);
self.stdout_only_bytes(contents)
}
#[track_caller]
pub fn stderr_only<T: AsRef<str>>(&self, msg: T) -> &Self {
self.no_stdout().stderr_is(msg)
}
#[track_caller]
pub fn stderr_only_bytes<T: AsRef<[u8]>>(&self, msg: T) -> &Self {
self.no_stdout().stderr_is_bytes(msg)
}
#[track_caller]
pub fn fails_silently(&self) -> &Self {
assert!(!self.succeeded());
assert!(
self.stderr.is_empty(),
"Expected stderr to be empty, but it's:\n{}",
self.stderr_str()
);
self
}
#[track_caller]
pub fn usage_error<T: AsRef<str>>(&self, msg: T) -> &Self {
self.stderr_only(format!(
"{0}: {2}\nTry '{1} {0} --help' for more information.\n",
self.util_name.as_ref().unwrap(), self.bin_path.display(),
msg.as_ref()
))
}
#[track_caller]
pub fn stdout_contains<T: AsRef<str>>(&self, cmp: T) -> &Self {
assert!(
self.stdout_str().contains(cmp.as_ref()),
"'{}' does not contain '{}'",
self.stdout_str(),
cmp.as_ref()
);
self
}
#[track_caller]
pub fn stdout_contains_line<T: AsRef<str>>(&self, cmp: T) -> &Self {
assert!(
self.stdout_str().lines().any(|line| line == cmp.as_ref()),
"'{}' does not contain line '{}'",
self.stdout_str(),
cmp.as_ref()
);
self
}
#[track_caller]
pub fn stdout_contains_bytes<T: AsRef<[u8]>>(&self, cmp: T) -> &Self {
assert!(
self.stdout()
.windows(cmp.as_ref().len())
.any(|sub| sub == cmp.as_ref()),
"'{:?}'\ndoes not contain\n'{:?}'",
self.stdout(),
cmp.as_ref()
);
self
}
#[track_caller]
pub fn stderr_contains<T: AsRef<str>>(&self, cmp: T) -> &Self {
assert!(
self.stderr_str().contains(cmp.as_ref()),
"'{}' does not contain '{}'",
self.stderr_str(),
cmp.as_ref()
);
self
}
#[track_caller]
pub fn stderr_contains_bytes<T: AsRef<[u8]>>(&self, cmp: T) -> &Self {
assert!(
self.stderr()
.windows(cmp.as_ref().len())
.any(|sub| sub == cmp.as_ref()),
"'{:?}'\ndoes not contain\n'{:?}'",
self.stderr(),
cmp.as_ref()
);
self
}
#[track_caller]
pub fn stdout_does_not_contain<T: AsRef<str>>(&self, cmp: T) -> &Self {
assert!(
!self.stdout_str().contains(cmp.as_ref()),
"'{}' contains '{}' but should not",
self.stdout_str(),
cmp.as_ref(),
);
self
}
#[track_caller]
pub fn stderr_does_not_contain<T: AsRef<str>>(&self, cmp: T) -> &Self {
assert!(!self.stderr_str().contains(cmp.as_ref()));
self
}
#[track_caller]
pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self {
assert!(
regex.is_match(self.stdout_str()),
"Stdout does not match regex:\n{}",
self.stdout_str()
);
self
}
#[track_caller]
pub fn stderr_matches(&self, regex: ®ex::Regex) -> &Self {
assert!(
regex.is_match(self.stderr_str()),
"Stderr does not match regex:\n{}",
self.stderr_str()
);
self
}
#[track_caller]
pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self {
assert!(
!regex.is_match(self.stdout_str()),
"Stdout matches regex:\n{}",
self.stdout_str()
);
self
}
}
pub fn log_info<T: AsRef<str>, U: AsRef<str>>(msg: T, par: U) {
println!("{}: {}", msg.as_ref(), par.as_ref());
}
pub fn recursive_copy(src: &Path, dest: &Path) -> Result<()> {
if fs::metadata(src)?.is_dir() {
for entry in fs::read_dir(src)? {
let entry = entry?;
let mut new_dest = PathBuf::from(dest);
new_dest.push(entry.file_name());
if fs::metadata(entry.path())?.is_dir() {
fs::create_dir(&new_dest)?;
recursive_copy(&entry.path(), &new_dest)?;
} else {
fs::copy(entry.path(), new_dest)?;
}
}
}
Ok(())
}
pub fn get_root_path() -> &'static str {
if cfg!(windows) { "C:\\" } else { "/" }
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "openbsd"))))]
pub fn compare_xattrs<P: AsRef<std::path::Path>>(path1: P, path2: P) -> bool {
let get_sorted_xattrs = |path: P| {
xattr::list(path)
.map(|attrs| {
let mut attrs = attrs.collect::<Vec<_>>();
attrs.sort();
attrs
})
.unwrap_or_default()
};
get_sorted_xattrs(path1) == get_sorted_xattrs(path2)
}
#[derive(Clone)]
pub struct AtPath {
pub subdir: PathBuf,
}
impl AtPath {
pub fn new(subdir: &Path) -> Self {
Self {
subdir: PathBuf::from(subdir),
}
}
pub fn as_string(&self) -> String {
self.subdir.to_str().unwrap().to_owned()
}
pub fn plus<P: AsRef<Path>>(&self, name: P) -> PathBuf {
let mut pathbuf = self.subdir.clone();
pathbuf.push(name);
pathbuf
}
pub fn plus_as_string<P: AsRef<Path>>(&self, name: P) -> String {
self.plus(name).display().to_string()
}
fn minus(&self, name: &str) -> PathBuf {
let prefixed = PathBuf::from(name);
if prefixed.starts_with(&self.subdir) {
let mut unprefixed = PathBuf::new();
for component in prefixed.components().skip(self.subdir.components().count()) {
unprefixed.push(component.as_os_str().to_str().unwrap());
}
unprefixed
} else {
prefixed
}
}
pub fn minus_as_string(&self, name: &str) -> String {
String::from(self.minus(name).to_str().unwrap())
}
pub fn set_readonly(&self, name: &str) {
let metadata = fs::metadata(self.plus(name)).unwrap();
let mut permissions = metadata.permissions();
permissions.set_readonly(true);
fs::set_permissions(self.plus(name), permissions).unwrap();
}
pub fn open(&self, name: &str) -> File {
log_info("open", self.plus_as_string(name));
File::open(self.plus(name)).unwrap()
}
pub fn read(&self, name: &str) -> String {
let mut f = self.open(name);
let mut contents = String::new();
f.read_to_string(&mut contents)
.unwrap_or_else(|e| panic!("Couldn't read {name}: {e}"));
contents
}
pub fn read_bytes(&self, name: &str) -> Vec<u8> {
let mut f = self.open(name);
let mut contents = Vec::new();
f.read_to_end(&mut contents)
.unwrap_or_else(|e| panic!("Couldn't read {name}: {e}"));
contents
}
pub fn write(&self, name: &str, contents: &str) {
log_info("write(default)", self.plus_as_string(name));
std::fs::write(self.plus(name), contents)
.unwrap_or_else(|e| panic!("Couldn't write {name}: {e}"));
}
pub fn write_bytes(&self, name: &str, contents: &[u8]) {
log_info("write(default)", self.plus_as_string(name));
std::fs::write(self.plus(name), contents)
.unwrap_or_else(|e| panic!("Couldn't write {name}: {e}"));
}
pub fn append(&self, name: impl AsRef<Path>, contents: &str) {
let name = name.as_ref();
log_info("write(append)", self.plus_as_string(name));
let mut f = OpenOptions::new()
.append(true)
.create(true)
.open(self.plus(name))
.unwrap();
f.write_all(contents.as_bytes())
.unwrap_or_else(|e| panic!("Couldn't write(append) {}: {e}", name.display()));
}
pub fn append_bytes(&self, name: &str, contents: &[u8]) {
log_info("write(append)", self.plus_as_string(name));
let mut f = OpenOptions::new()
.append(true)
.create(true)
.open(self.plus(name))
.unwrap();
f.write_all(contents)
.unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}"));
}
pub fn truncate(&self, name: &str, contents: &str) {
log_info("write(truncate)", self.plus_as_string(name));
let mut f = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(self.plus(name))
.unwrap();
f.write_all(contents.as_bytes())
.unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}"));
}
pub fn rename(&self, source: &str, target: &str) {
let source = self.plus(source);
let target = self.plus(target);
log_info("rename", format!("{source:?} {target:?}"));
std::fs::rename(&source, &target)
.unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}"));
}
pub fn remove(&self, source: &str) {
let source = self.plus(source);
log_info("remove", format!("{source:?}"));
std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}"));
}
pub fn copy(&self, source: &str, target: &str) {
let source = self.plus(source);
let target = self.plus(target);
log_info("copy", format!("{source:?} {target:?}"));
std::fs::copy(&source, &target)
.unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}"));
}
pub fn rmdir(&self, dir: &str) {
log_info("rmdir", self.plus_as_string(dir));
fs::remove_dir(self.plus(dir)).unwrap();
}
pub fn mkdir<P: AsRef<Path>>(&self, dir: P) {
let dir = dir.as_ref();
log_info("mkdir", self.plus_as_string(dir));
fs::create_dir(self.plus(dir)).unwrap();
}
pub fn mkdir_all(&self, dir: &str) {
log_info("mkdir_all", self.plus_as_string(dir));
fs::create_dir_all(self.plus(dir)).unwrap();
}
pub fn make_file(&self, name: &str) -> File {
match File::create(self.plus(name)) {
Ok(f) => f,
Err(e) => panic!("{e}"),
}
}
pub fn touch<P: AsRef<Path>>(&self, file: P) {
let file = file.as_ref();
log_info("touch", self.plus_as_string(file));
File::create(self.plus(file)).unwrap();
}
#[cfg(not(windows))]
pub fn mkfifo(&self, fifo: &str) {
let full_path = self.plus_as_string(fifo);
log_info("mkfifo", &full_path);
unsafe {
let fifo_name: CString = CString::new(full_path).expect("CString creation failed.");
libc::mkfifo(fifo_name.as_ptr(), libc::S_IWUSR | libc::S_IRUSR);
}
}
#[cfg(unix)]
pub fn mksocket(&self, socket: &str) {
let full_path = self.plus_as_string(socket);
log_info("mksocket", &full_path);
UnixListener::bind(full_path).expect("Socket file creation failed.");
}
#[cfg(not(windows))]
pub fn is_fifo(&self, fifo: &str) -> bool {
unsafe {
let name = CString::new(self.plus_as_string(fifo)).unwrap();
let mut stat: libc::stat = std::mem::zeroed();
if libc::stat(name.as_ptr(), &mut stat) >= 0 {
libc::S_IFIFO & stat.st_mode as libc::mode_t != 0
} else {
false
}
}
}
#[cfg(not(windows))]
pub fn is_char_device(&self, char_dev: &str) -> bool {
unsafe {
let name = CString::new(self.plus_as_string(char_dev)).unwrap();
let mut stat: libc::stat = std::mem::zeroed();
if libc::stat(name.as_ptr(), &mut stat) >= 0 {
libc::S_IFCHR & stat.st_mode as libc::mode_t != 0
} else {
false
}
}
}
pub fn hard_link(&self, original: &str, link: &str) {
log_info(
"hard_link",
format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
);
hard_link(self.plus(original), self.plus(link)).unwrap();
}
pub fn symlink_file(&self, original: &str, link: &str) {
log_info(
"symlink",
format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
);
symlink_file(self.plus(original), self.plus(link)).unwrap();
}
pub fn relative_symlink_file(&self, original: &str, link: &str) {
#[cfg(windows)]
let original = original.replace('/', MAIN_SEPARATOR_STR);
log_info(
"symlink",
format!("{original},{}", self.plus_as_string(link)),
);
symlink_file(original, self.plus(link)).unwrap();
}
pub fn symlink_dir(&self, original: &str, link: &str) {
log_info(
"symlink",
format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
);
symlink_dir(self.plus(original), self.plus(link)).unwrap();
}
pub fn relative_symlink_dir(&self, original: &str, link: &str) {
#[cfg(windows)]
let original = original.replace('/', MAIN_SEPARATOR_STR);
log_info(
"symlink",
format!("{original},{}", self.plus_as_string(link)),
);
symlink_dir(original, self.plus(link)).unwrap();
}
pub fn is_symlink(&self, path: &str) -> bool {
log_info("is_symlink", self.plus_as_string(path));
match fs::symlink_metadata(self.plus(path)) {
Ok(m) => m.file_type().is_symlink(),
Err(_) => false,
}
}
pub fn resolve_link(&self, path: &str) -> String {
log_info("resolve_link", self.plus_as_string(path));
match fs::read_link(self.plus(path)) {
Ok(p) => self.minus_as_string(p.to_str().unwrap()),
Err(_) => String::new(),
}
}
pub fn read_symlink(&self, path: &str) -> String {
log_info("read_symlink", self.plus_as_string(path));
fs::read_link(self.plus(path))
.unwrap()
.to_str()
.unwrap()
.to_owned()
}
pub fn symlink_metadata(&self, path: &str) -> fs::Metadata {
match fs::symlink_metadata(self.plus(path)) {
Ok(m) => m,
Err(e) => panic!("{e}"),
}
}
pub fn metadata(&self, path: &str) -> fs::Metadata {
match fs::metadata(self.plus(path)) {
Ok(m) => m,
Err(e) => panic!("{e}"),
}
}
pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
match fs::metadata(self.plus(path)) {
Ok(m) => m.is_file(),
Err(_) => false,
}
}
pub fn symlink_exists<P: AsRef<Path>>(&self, path: P) -> bool {
match fs::symlink_metadata(self.plus(path)) {
Ok(m) => m.file_type().is_symlink(),
Err(_) => false,
}
}
pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
match fs::metadata(self.plus(path)) {
Ok(m) => m.is_dir(),
Err(_) => false,
}
}
pub fn root_dir_resolved(&self) -> String {
log_info("current_directory_resolved", "");
let s = self
.subdir
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.to_owned();
let prefix = "\\\\?\\";
if let Some(stripped) = s.strip_prefix(prefix) {
String::from(stripped)
} else {
s
}
}
#[cfg(not(windows))]
pub fn set_mode(&self, filename: &str, mode: u32) {
let path = self.plus(filename);
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(mode);
std::fs::set_permissions(&path, perms).unwrap();
}
}
pub struct TestScenario {
pub bin_path: PathBuf,
pub util_name: String,
pub fixtures: AtPath,
tmpd: Rc<TempDir>,
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
tmp_fs_mountpoint: Option<String>,
}
impl TestScenario {
pub fn new<T>(util_name: T) -> Self
where
T: AsRef<str>,
{
let tmpd = Rc::new(TempDir::new().unwrap());
println!("bin: {:?}", get_tests_binary!());
let ts = Self {
bin_path: PathBuf::from(get_tests_binary!()),
util_name: util_name.as_ref().into(),
fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd,
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
tmp_fs_mountpoint: None,
};
let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR);
fixture_path_builder.push(FIXTURES_DIR);
fixture_path_builder.push(util_name.as_ref());
if let Ok(m) = fs::metadata(&fixture_path_builder) {
if m.is_dir() {
recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap();
}
}
ts
}
pub fn ucmd(&self) -> UCommand {
UCommand::from_test_scenario(self)
}
pub fn cmd<S: Into<PathBuf>>(&self, bin_path: S) -> UCommand {
let mut command = UCommand::new();
command.bin_path(bin_path);
command.temp_dir(self.tmpd.clone());
command
}
pub fn cmd_shell<S: AsRef<OsStr>>(&self, cmd: S) -> UCommand {
let mut command = UCommand::new();
command.arg(cmd);
command.temp_dir(self.tmpd.clone());
command
}
pub fn ccmd<S: AsRef<str>>(&self, util_name: S) -> UCommand {
UCommand::with_util(util_name, self.tmpd.clone())
}
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
pub fn mount_temp_fs(&mut self, mount_point: &str) -> core::result::Result<(), String> {
if self.tmp_fs_mountpoint.is_some() {
return Err("already mounted".to_string());
}
let cmd_result = self
.cmd("mount")
.arg("-t")
.arg("tmpfs")
.arg("-o")
.arg("size=640k") .arg("tmpfs")
.arg(mount_point)
.run();
if !cmd_result.succeeded() {
return Err(format!("mount failed: {}", cmd_result.stderr_str()));
}
self.tmp_fs_mountpoint = Some(mount_point.to_string());
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
pub fn umount_temp_fs(&mut self) {
if let Some(mount_point) = self.tmp_fs_mountpoint.as_ref() {
self.cmd("umount").arg(mount_point).succeeds();
self.tmp_fs_mountpoint = None;
}
}
}
impl Drop for TestScenario {
fn drop(&mut self) {
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
self.umount_temp_fs();
}
}
#[cfg(unix)]
#[derive(Debug, Default)]
pub struct TerminalSimulation {
pub size: Option<libc::winsize>,
pub stdin: bool,
pub stdout: bool,
pub stderr: bool,
}
#[derive(Debug, Default)]
pub struct UCommand {
args: VecDeque<OsString>,
env_vars: Vec<(OsString, OsString)>,
current_dir: Option<PathBuf>,
bin_path: Option<PathBuf>,
util_name: Option<String>,
has_run: bool,
ignore_stdin_write_error: bool,
stdin: Option<Stdio>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
bytes_into_stdin: Option<Vec<u8>>,
#[cfg(unix)]
limits: Vec<(rlimit::Resource, u64, u64)>,
stderr_to_stdout: bool,
timeout: Option<Duration>,
#[cfg(unix)]
terminal_simulation: Option<TerminalSimulation>,
tmpd: Option<Rc<TempDir>>, #[cfg(unix)]
umask: Option<mode_t>,
}
impl UCommand {
pub fn new() -> Self {
Self {
..Default::default()
}
}
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(&*get_tests_binary!()).temp_dir(tmpd);
ucmd
}
pub fn from_test_scenario(scene: &TestScenario) -> Self {
Self::with_util(&scene.util_name, scene.tmpd.clone())
}
fn bin_path<T>(&mut self, bin_path: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.bin_path = Some(bin_path.into());
self
}
fn temp_dir(&mut self, temp_dir: Rc<TempDir>) -> &mut Self {
self.tmpd = Some(temp_dir);
self
}
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 {
self.stdin = Some(stdin.into());
self
}
pub fn set_stdout<T: Into<Stdio>>(&mut self, stdout: T) -> &mut Self {
self.stdout = Some(stdout.into());
self
}
pub fn set_stderr<T: Into<Stdio>>(&mut self, stderr: T) -> &mut Self {
self.stderr = Some(stderr.into());
self
}
pub fn stderr_to_stdout(&mut self) -> &mut Self {
self.stderr_to_stdout = true;
self
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.args.push_back(arg.as_ref().into());
self
}
pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
self.args.extend(args.iter().map(|s| s.as_ref().into()));
self
}
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, input: T) -> &mut Self {
assert!(
self.bytes_into_stdin.is_none(),
"{MULTIPLE_STDIN_MEANINGLESS}",
);
self.set_stdin(Stdio::piped());
self.bytes_into_stdin = Some(input.into());
self
}
pub fn pipe_in_fixture<S: AsRef<OsStr>>(&mut self, file_rel_path: S) -> &mut Self {
let contents = read_scenario_fixture(self.tmpd.as_ref(), file_rel_path);
self.pipe_in(contents)
}
pub fn ignore_stdin_write_error(&mut self) -> &mut Self {
self.ignore_stdin_write_error = true;
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.env_vars
.push((key.as_ref().into(), val.as_ref().into()));
self
}
pub fn envs<I, K, V>(&mut self, iter: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for (k, v) in iter {
self.env(k, v);
}
self
}
#[cfg(unix)]
pub fn limit(
&mut self,
resource: rlimit::Resource,
soft_limit: u64,
hard_limit: u64,
) -> &mut Self {
self.limits.push((resource, soft_limit, hard_limit));
self
}
#[cfg(unix)]
pub fn umask(&mut self, umask: mode_t) -> &mut Self {
self.umask = Some(umask);
self
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = Some(timeout);
self
}
#[cfg(unix)]
pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self {
if enable {
self.terminal_simulation = Some(TerminalSimulation {
stdin: true,
stdout: true,
stderr: true,
..Default::default()
});
} else {
self.terminal_simulation = None;
}
self
}
#[cfg(unix)]
pub fn terminal_sim_stdio(&mut self, config: TerminalSimulation) -> &mut Self {
self.terminal_simulation = Some(config);
self
}
#[cfg(unix)]
fn read_from_pty(pty_fd: std::os::fd::OwnedFd, out: File) {
let read_file = std::fs::File::from(pty_fd);
let mut reader = std::io::BufReader::new(read_file);
let mut writer = std::io::BufWriter::new(out);
let result = std::io::copy(&mut reader, &mut writer);
match result {
Ok(_) => {}
Err(e) if e.raw_os_error().unwrap_or_default() == 5 => {}
Err(e) => {
eprintln!("Unexpected error: {e:?}");
panic!("error forwarding output of pty");
}
}
}
#[cfg(unix)]
fn spawn_reader_thread(
captured_output: Option<CapturedOutput>,
pty_fd_master: OwnedFd,
name: String,
) -> Option<CapturedOutput> {
if let Some(mut captured_output_i) = captured_output {
let fd = captured_output_i.try_clone().unwrap();
let handle = std::thread::Builder::new()
.name(name)
.spawn(move || {
Self::read_from_pty(pty_fd_master, fd);
})
.unwrap();
captured_output_i.reader_thread_handle = Some(handle);
Some(captured_output_i)
} else {
None
}
}
fn build(
&mut self,
) -> (
Command,
Option<CapturedOutput>,
Option<CapturedOutput>,
Option<File>,
) {
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(&*get_tests_binary!()));
self.args.push_front(util_name.into());
} 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);
}
};
let mut command = Command::new(self.bin_path.as_ref().unwrap());
command.args(&self.args);
if let Some(current_dir) = &self.current_dir {
command.current_dir(current_dir);
} else if let Some(temp_dir) = &self.tmpd {
command.current_dir(temp_dir.path());
} else {
let temp_dir = tempfile::tempdir().unwrap();
self.current_dir = Some(temp_dir.path().into());
command.current_dir(temp_dir.path());
self.tmpd = Some(Rc::new(temp_dir));
}
command.env_clear();
if cfg!(windows) {
if let Some(systemroot) = env::var_os("SYSTEMROOT") {
command.env("SYSTEMROOT", systemroot);
}
} else {
if let Some(ld_preload) = env::var_os("LD_PRELOAD") {
command.env("LD_PRELOAD", ld_preload);
}
}
if let Some(ld_preload) = env::var_os("LLVM_PROFILE_FILE") {
command.env("LLVM_PROFILE_FILE", ld_preload);
}
command
.envs(DEFAULT_ENV)
.envs(self.env_vars.iter().cloned());
if self.timeout.is_none() {
self.timeout = Some(Duration::from_secs(30));
}
let mut captured_stdout = None;
let mut captured_stderr = None;
#[cfg(unix)]
let mut stdin_pty: Option<File> = None;
#[cfg(not(unix))]
let stdin_pty: Option<File> = None;
if self.stderr_to_stdout {
let mut output = CapturedOutput::default();
command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(Stdio::from(output.try_clone().unwrap()))
.stderr(Stdio::from(output.try_clone().unwrap()));
captured_stdout = Some(output);
} else {
let stdout = if self.stdout.is_some() {
self.stdout.take().unwrap()
} else {
let mut stdout = CapturedOutput::default();
let stdio = Stdio::from(stdout.try_clone().unwrap());
captured_stdout = Some(stdout);
stdio
};
let stderr = if self.stderr.is_some() {
self.stderr.take().unwrap()
} else {
let mut stderr = CapturedOutput::default();
let stdio = Stdio::from(stderr.try_clone().unwrap());
captured_stderr = Some(stderr);
stdio
};
command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(stdout)
.stderr(stderr);
};
#[cfg(unix)]
if let Some(simulated_terminal) = &self.terminal_simulation {
let terminal_size = simulated_terminal.size.unwrap_or(libc::winsize {
ws_col: 80,
ws_row: 30,
ws_xpixel: 80 * 8,
ws_ypixel: 30 * 10,
});
if simulated_terminal.stdin {
let OpenptyResult {
slave: pi_slave,
master: pi_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
stdin_pty = Some(File::from(pi_master));
command.stdin(pi_slave);
}
if simulated_terminal.stdout {
let OpenptyResult {
slave: po_slave,
master: po_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
captured_stdout = Self::spawn_reader_thread(
captured_stdout,
po_master,
"stdout_reader".to_string(),
);
command.stdout(po_slave);
}
if simulated_terminal.stderr {
let OpenptyResult {
slave: pe_slave,
master: pe_master,
} = nix::pty::openpty(&terminal_size, None).unwrap();
captured_stderr = Self::spawn_reader_thread(
captured_stderr,
pe_master,
"stderr_reader".to_string(),
);
command.stderr(pe_slave);
}
}
#[cfg(unix)]
if !self.limits.is_empty() {
let limits_copy = self.limits.clone();
let closure = move || -> Result<()> {
for &(resource, soft_limit, hard_limit) in &limits_copy {
setrlimit(resource, soft_limit, hard_limit)?;
}
Ok(())
};
unsafe {
command.pre_exec(closure);
}
}
#[cfg(unix)]
if let Some(umask) = self.umask {
unsafe {
command.pre_exec(move || {
libc::umask(umask);
Ok(())
});
}
}
(command, captured_stdout, captured_stderr, stdin_pty)
}
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, stdin_pty) = self.build();
log_info("run", self.to_string());
let child = command.spawn().unwrap();
let mut child = UChild::from(self, child, captured_stdout, captured_stderr, stdin_pty);
if let Some(input) = self.bytes_into_stdin.take() {
child.pipe_in(input);
}
child
}
pub fn run(&mut self) -> CmdResult {
self.run_no_wait().wait().unwrap()
}
pub fn run_piped_stdin<T: Into<Vec<u8>>>(&mut self, input: T) -> CmdResult {
self.pipe_in(input).run()
}
#[track_caller]
pub fn succeeds(&mut self) -> CmdResult {
let cmd_result = self.run();
cmd_result.success();
cmd_result
}
#[track_caller]
pub fn fails(&mut self) -> CmdResult {
let cmd_result = self.run();
cmd_result.failure();
cmd_result
}
#[track_caller]
pub fn fails_with_code(&mut self, expected_code: i32) -> CmdResult {
let cmd_result = self.run();
cmd_result.failure();
cmd_result.code_is(expected_code);
cmd_result
}
pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String {
let tmpdir_path = self.tmpd.as_ref().unwrap().path();
format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap())
}
#[track_caller]
pub fn run_stdout_starts_with(&mut self, expected: &[u8]) -> CmdResult {
let mut child = self.set_stdout(Stdio::piped()).run_no_wait();
let buf = child.stdout_exact_bytes(expected.len());
child.close_stdout();
assert_eq!(buf.as_slice(), expected);
child.wait().unwrap()
}
}
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(" "))
}
}
#[derive(Debug)]
struct CapturedOutput {
current_file: File,
output: tempfile::NamedTempFile, reader_thread_handle: Option<thread::JoinHandle<()>>,
}
impl CapturedOutput {
fn new(output: tempfile::NamedTempFile) -> Self {
Self {
current_file: output.reopen().unwrap(),
output,
reader_thread_handle: None,
}
}
fn try_clone(&mut self) -> io::Result<File> {
self.output.as_file().try_clone()
}
fn output(&mut self) -> String {
String::from_utf8(self.output_bytes()).unwrap()
}
fn output_exact(&mut self, size: usize) -> String {
String::from_utf8(self.output_exact_bytes(size)).unwrap()
}
fn output_bytes(&mut self) -> Vec<u8> {
let mut buffer = Vec::<u8>::new();
self.current_file.read_to_end(&mut buffer).unwrap();
buffer
}
fn output_all_bytes(&mut self) -> Vec<u8> {
let mut buffer = Vec::<u8>::new();
let mut file = self.output.reopen().unwrap();
file.read_to_end(&mut buffer).unwrap();
self.current_file = file;
buffer
}
fn output_exact_bytes(&mut self, size: usize) -> Vec<u8> {
let mut buffer = vec![0; size];
self.current_file.read_exact(&mut buffer).unwrap();
buffer
}
}
impl Default for CapturedOutput {
fn default() -> Self {
let mut retries = 10;
let file = loop {
let file = Builder::new().rand_bytes(10).suffix(".out").tempfile();
if file.is_ok() || retries <= 0 {
break file.unwrap();
}
sleep(Duration::from_millis(100));
retries -= 1;
};
Self {
current_file: file.reopen().unwrap(),
output: file,
reader_thread_handle: None,
}
}
}
impl Drop for CapturedOutput {
fn drop(&mut self) {
let _ = remove_file(self.output.path());
}
}
#[derive(Debug, Copy, Clone)]
pub enum AssertionMode {
All,
Current,
Exact(usize, usize),
}
pub struct UChildAssertion<'a> {
uchild: &'a mut UChild,
}
impl<'a> UChildAssertion<'a> {
pub fn new(uchild: &'a mut UChild) -> Self {
Self { uchild }
}
fn with_output(&mut self, mode: AssertionMode) -> CmdResult {
let exit_status = if self.uchild.is_alive() {
None
} else {
Some(self.uchild.raw.wait().unwrap())
};
let (stdout, stderr) = match mode {
AssertionMode::All => (
self.uchild.stdout_all_bytes(),
self.uchild.stderr_all_bytes(),
),
AssertionMode::Current => (self.uchild.stdout_bytes(), self.uchild.stderr_bytes()),
AssertionMode::Exact(expected_stdout_size, expected_stderr_size) => (
self.uchild.stdout_exact_bytes(expected_stdout_size),
self.uchild.stderr_exact_bytes(expected_stderr_size),
),
};
CmdResult::new(
self.uchild.bin_path.clone(),
self.uchild.util_name.clone(),
self.uchild.tmpd.clone(),
exit_status,
stdout,
stderr,
)
}
pub fn with_all_output(&mut self) -> CmdResult {
self.with_output(AssertionMode::All)
}
pub fn with_current_output(&mut self) -> CmdResult {
self.with_output(AssertionMode::Current)
}
pub fn with_exact_output(
&mut self,
expected_stdout_size: usize,
expected_stderr_size: usize,
) -> CmdResult {
self.with_output(AssertionMode::Exact(
expected_stdout_size,
expected_stderr_size,
))
}
#[track_caller]
pub fn is_alive(&mut self) -> &mut Self {
match self.uchild.raw.try_wait() {
Ok(Some(status)) => panic!(
"Assertion failed. Expected '{}' to be running but exited with status={status}.\nstdout: {}\nstderr: {}",
uucore::util_name(),
self.uchild.stdout_all(),
self.uchild.stderr_all()
),
Ok(None) => {}
Err(error) => panic!("Assertion failed with error '{error:?}'"),
}
self
}
#[track_caller]
pub fn is_not_alive(&mut self) -> &mut Self {
match self.uchild.raw.try_wait() {
Ok(None) => panic!(
"Assertion failed. Expected '{}' to be not running but was alive.\nstdout: {}\nstderr: {}",
uucore::util_name(),
self.uchild.stdout_all(),
self.uchild.stderr_all()
),
Ok(_) => {}
Err(error) => panic!("Assertion failed with error '{error:?}'"),
}
self
}
}
pub struct UChild {
raw: Child,
bin_path: PathBuf,
util_name: Option<String>,
captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>,
stdin_pty: Option<File>,
ignore_stdin_write_error: bool,
stderr_to_stdout: bool,
join_handle: Option<JoinHandle<io::Result<()>>>,
timeout: Option<Duration>,
tmpd: Option<Rc<TempDir>>, }
impl UChild {
fn from(
ucommand: &UCommand,
child: Child,
captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>,
stdin_pty: Option<File>,
) -> Self {
Self {
raw: child,
bin_path: ucommand.bin_path.clone().unwrap(),
util_name: ucommand.util_name.clone(),
captured_stdout,
captured_stderr,
stdin_pty,
ignore_stdin_write_error: ucommand.ignore_stdin_write_error,
stderr_to_stdout: ucommand.stderr_to_stdout,
join_handle: None,
timeout: ucommand.timeout,
tmpd: ucommand.tmpd.clone(),
}
}
pub fn delay(&mut self, millis: u64) -> &mut Self {
sleep(Duration::from_millis(millis));
self
}
pub fn id(&self) -> u32 {
self.raw.id()
}
pub fn is_alive(&mut self) -> bool {
self.raw.try_wait().unwrap().is_none()
}
#[allow(clippy::wrong_self_convention)]
pub fn is_not_alive(&mut self) -> bool {
!self.is_alive()
}
pub fn make_assertion(&mut self) -> UChildAssertion<'_> {
UChildAssertion::new(self)
}
pub fn make_assertion_with_delay(&mut self, millis: u64) -> UChildAssertion<'_> {
self.delay(millis).make_assertion()
}
pub fn try_kill(&mut self) -> io::Result<()> {
let start = Instant::now();
self.raw.kill()?;
let timeout = self.timeout.unwrap_or(Duration::from_secs(60));
while self.is_alive() || timeout == Duration::ZERO {
if start.elapsed() < timeout {
self.delay(10);
} else {
return Err(io::Error::other(format!(
"kill: Timeout of '{}s' reached",
timeout.as_secs_f64()
)));
}
hint::spin_loop();
}
Ok(())
}
pub fn kill(&mut self) -> &mut Self {
self.try_kill()
.or_else(|error| {
if error.kind() == io::ErrorKind::Other {
Err(error)
} else {
Ok(())
}
})
.unwrap();
self
}
#[cfg(unix)]
pub fn try_kill_with_custom_signal(
&mut self,
signal_name: sys::signal::Signal,
) -> io::Result<()> {
let start = Instant::now();
sys::signal::kill(
nix::unistd::Pid::from_raw(self.raw.id().try_into().unwrap()),
signal_name,
)
.unwrap();
let timeout = self.timeout.unwrap_or(Duration::from_secs(60));
while self.is_alive() || timeout == Duration::ZERO {
if start.elapsed() < timeout {
self.delay(10);
} else {
return Err(io::Error::other(format!(
"kill: Timeout of '{}s' reached",
timeout.as_secs_f64()
)));
}
hint::spin_loop();
}
Ok(())
}
#[cfg(unix)]
pub fn kill_with_custom_signal(&mut self, signal_name: sys::signal::Signal) -> &mut Self {
self.try_kill_with_custom_signal(signal_name)
.or_else(|error| {
if error.kind() == io::ErrorKind::Other {
Err(error)
} else {
Ok(())
}
})
.unwrap();
self
}
pub fn wait(self) -> io::Result<CmdResult> {
let (bin_path, util_name, tmpd) = (
self.bin_path.clone(),
self.util_name.clone(),
self.tmpd.clone(),
);
let output = self.wait_with_output()?;
Ok(CmdResult {
bin_path,
util_name,
tmpd,
exit_status: Some(output.status),
stdout: output.stdout,
stderr: output.stderr,
})
}
fn wait_with_output(mut self) -> io::Result<Output> {
self.join(); self.close_stdin();
let output = if let Some(timeout) = self.timeout {
let child = self.raw;
let (sender, receiver) = mpsc::channel();
let handle = thread::Builder::new()
.name("wait_with_output".to_string())
.spawn(move || sender.send(child.wait_with_output()))
.unwrap();
match receiver.recv_timeout(timeout) {
Ok(result) => {
handle.join().unwrap().unwrap();
result
}
Err(RecvTimeoutError::Timeout) => Err(io::Error::other(format!(
"wait: Timeout of '{}s' reached",
timeout.as_secs_f64()
))),
Err(RecvTimeoutError::Disconnected) => {
handle.join().expect("Panic caused disconnect").unwrap();
panic!("Error receiving from waiting thread because of unexpected disconnect");
}
}
} else {
self.raw.wait_with_output()
};
let mut output = output?;
if let Some(join_handle) = self.join_handle.take() {
join_handle
.join()
.expect("Error joining with the piping stdin thread")
.unwrap();
};
if let Some(stdout) = self.captured_stdout.as_mut() {
if let Some(handle) = stdout.reader_thread_handle.take() {
handle.join().unwrap();
}
output.stdout = stdout.output_bytes();
}
if let Some(stderr) = self.captured_stderr.as_mut() {
if let Some(handle) = stderr.reader_thread_handle.take() {
handle.join().unwrap();
}
output.stderr = stderr.output_bytes();
}
Ok(output)
}
pub fn stdout(&mut self) -> String {
String::from_utf8(self.stdout_bytes()).unwrap()
}
pub fn stdout_all(&mut self) -> String {
String::from_utf8(self.stdout_all_bytes()).unwrap()
}
pub fn stdout_bytes(&mut self) -> Vec<u8> {
match self.captured_stdout.as_mut() {
Some(output) => output.output_bytes(),
None if self.raw.stdout.is_some() => {
let mut buffer: Vec<u8> = vec![];
let stdout = self.raw.stdout.as_mut().unwrap();
stdout.read_to_end(&mut buffer).unwrap();
buffer
}
None => vec![],
}
}
pub fn stdout_all_bytes(&mut self) -> Vec<u8> {
match self.captured_stdout.as_mut() {
Some(output) => output.output_all_bytes(),
None => {
panic!("Usage error: This method cannot be used if the output wasn't captured.")
}
}
}
pub fn stdout_exact_bytes(&mut self, size: usize) -> Vec<u8> {
match self.captured_stdout.as_mut() {
Some(output) => output.output_exact_bytes(size),
None if self.raw.stdout.is_some() => {
let mut buffer = vec![0; size];
let stdout = self.raw.stdout.as_mut().unwrap();
stdout.read_exact(&mut buffer).unwrap();
buffer
}
None => vec![],
}
}
pub fn stderr(&mut self) -> String {
String::from_utf8(self.stderr_bytes()).unwrap()
}
pub fn stderr_all(&mut self) -> String {
String::from_utf8(self.stderr_all_bytes()).unwrap()
}
pub fn stderr_bytes(&mut self) -> Vec<u8> {
match self.captured_stderr.as_mut() {
Some(output) => output.output_bytes(),
None if self.raw.stderr.is_some() => {
let mut buffer: Vec<u8> = vec![];
let stderr = self.raw.stderr.as_mut().unwrap();
stderr.read_to_end(&mut buffer).unwrap();
buffer
}
None => vec![],
}
}
pub fn stderr_all_bytes(&mut self) -> Vec<u8> {
match self.captured_stderr.as_mut() {
Some(output) => output.output_all_bytes(),
None if self.stderr_to_stdout => vec![],
None => {
panic!("Usage error: This method cannot be used if the output wasn't captured.")
}
}
}
pub fn stderr_exact_bytes(&mut self, size: usize) -> Vec<u8> {
match self.captured_stderr.as_mut() {
Some(output) => output.output_exact_bytes(size),
None if self.raw.stderr.is_some() => {
let stderr = self.raw.stderr.as_mut().unwrap();
let mut buffer = vec![0; size];
stderr.read_exact(&mut buffer).unwrap();
buffer
}
None => vec![],
}
}
fn access_stdin_as_writer<'a>(&'a mut self) -> Box<dyn Write + Send + 'a> {
if let Some(stdin_fd) = &self.stdin_pty {
Box::new(BufWriter::new(stdin_fd.try_clone().unwrap()))
} else {
let stdin: &mut std::process::ChildStdin = self.raw.stdin.as_mut().unwrap();
Box::new(BufWriter::new(stdin))
}
}
fn take_stdin_as_writer(&mut self) -> Box<dyn Write + Send> {
if let Some(stdin_fd) = mem::take(&mut self.stdin_pty) {
Box::new(BufWriter::new(stdin_fd))
} else {
let stdin = self
.raw
.stdin
.take()
.expect("Could not pipe into child process. Was it set to Stdio::null()?");
Box::new(BufWriter::new(stdin))
}
}
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, content: T) -> &mut Self {
let ignore_stdin_write_error = self.ignore_stdin_write_error;
let mut content: Vec<u8> = content.into();
if self.stdin_pty.is_some() {
content.append(&mut END_OF_TRANSMISSION_SEQUENCE.to_vec());
}
let mut writer = self.take_stdin_as_writer();
let join_handle = std::thread::Builder::new()
.name("pipe_in".to_string())
.spawn(
move || match writer.write_all(&content).and_then(|()| writer.flush()) {
Err(error) if !ignore_stdin_write_error => Err(io::Error::other(format!(
"failed to write to stdin of child: {error}"
))),
Ok(()) | Err(_) => Ok(()),
},
)
.unwrap();
self.join_handle = Some(join_handle);
self
}
pub fn join(&mut self) -> &mut Self {
if let Some(join_handle) = self.join_handle.take() {
join_handle
.join()
.expect("Error joining with the piping stdin thread")
.unwrap();
}
self
}
pub fn pipe_in_and_wait<T: Into<Vec<u8>>>(mut self, content: T) -> CmdResult {
self.pipe_in(content);
self.wait().unwrap()
}
pub fn try_write_in<T: Into<Vec<u8>>>(&mut self, data: T) -> io::Result<()> {
let ignore_stdin_write_error = self.ignore_stdin_write_error;
let mut writer = self.access_stdin_as_writer();
match writer.write_all(&data.into()).and_then(|()| writer.flush()) {
Err(error) if !ignore_stdin_write_error => Err(io::Error::other(format!(
"failed to write to stdin of child: {error}"
))),
Ok(()) | Err(_) => Ok(()),
}
}
pub fn write_in<T: Into<Vec<u8>>>(&mut self, data: T) -> &mut Self {
self.try_write_in(data).unwrap();
self
}
pub fn close_stdout(&mut self) -> &mut Self {
self.raw.stdout.take();
self
}
pub fn close_stderr(&mut self) -> &mut Self {
self.raw.stderr.take();
self
}
pub fn close_stdin(&mut self) -> &mut Self {
self.raw.stdin.take();
if self.stdin_pty.is_some() {
let _ = self.try_write_in(END_OF_TRANSMISSION_SEQUENCE);
self.stdin_pty.take();
}
self
}
}
pub fn vec_of_size(n: usize) -> Vec<u8> {
let result = vec![b'a'; n];
assert_eq!(result.len(), n);
result
}
pub fn whoami() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|e| {
println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead");
"nobody".to_string()
})
}
#[cfg(unix)]
pub fn host_name_for(util_name: &str) -> Cow<'_, str> {
#[cfg(not(target_os = "linux"))]
{
if util_name.starts_with('g') && util_name != "groups" {
util_name.into()
} else {
format!("g{util_name}").into()
}
}
#[cfg(target_os = "linux")]
util_name.into()
}
const VERSION_MIN: &str = "8.30";
const UUTILS_WARNING: &str = "uutils-tests-warning";
const UUTILS_INFO: &str = "uutils-tests-info";
#[cfg(unix)]
pub fn check_coreutil_version(
util_name: &str,
version_expected: &str,
) -> std::result::Result<String, String> {
let util_name = &host_name_for(util_name);
log_info("run", format!("{util_name} --version"));
let version_check = match Command::new(util_name.as_ref())
.env("LC_ALL", "C")
.arg("--version")
.output()
{
Ok(s) => s,
Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")),
};
std::str::from_utf8(&version_check.stdout).unwrap()
.split('\n')
.collect::<Vec<_>>()
.first()
.map_or_else(
|| Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")),
|s| {
if s.contains(&format!("(GNU coreutils) {version_expected}")) {
Ok(format!("{UUTILS_INFO}: {s}"))
} else if s.contains("(GNU coreutils)") {
let version_found = parse_coreutil_version(s);
let version_expected = version_expected.parse::<f32>().unwrap_or_default();
if version_found > version_expected {
Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}"))
} else {
Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) }
} else {
Err(format!("{UUTILS_WARNING}: no coreutils version string found for reference coreutils '{util_name} --version'"))
}
},
)
}
fn parse_coreutil_version(version_string: &str) -> f32 {
version_string
.split_whitespace()
.last()
.unwrap()
.split('.')
.take(2)
.collect::<Vec<_>>()
.join(".")
.parse::<f32>()
.unwrap_or_default()
}
#[cfg(unix)]
pub fn gnu_cmd_result(
ts: &TestScenario,
args: &[&str],
envs: &[(&str, &str)],
) -> std::result::Result<CmdResult, String> {
let util_name = ts.util_name.as_str();
println!("{}", check_coreutil_version(util_name, VERSION_MIN)?);
let util_name = host_name_for(util_name);
let result = ts
.cmd(util_name.as_ref())
.env("PATH", PATH)
.envs(DEFAULT_ENV)
.envs(envs.iter().copied())
.args(args)
.run();
let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") {
(
result.stdout_str().to_string(),
result.stderr_str_lossy().to_string(),
)
} else {
let from = util_name.to_string() + ":";
let to = &from[1..];
(
result.stdout_str().replace(&from, to),
result.stderr_str_lossy().replace(&from, to),
)
};
Ok(CmdResult::new(
ts.bin_path.as_os_str().to_str().unwrap().to_string(),
Some(ts.util_name.clone()),
Some(result.tmpd()),
result.exit_status,
stdout.as_bytes(),
stderr.as_bytes(),
))
}
#[cfg(unix)]
pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<CmdResult, String> {
gnu_cmd_result(ts, args, &[])
}
#[cfg(unix)]
pub fn run_ucmd_as_root(
ts: &TestScenario,
args: &[&str],
) -> std::result::Result<CmdResult, String> {
run_ucmd_as_root_with_stdin_stdout(ts, args, None, None)
}
#[cfg(unix)]
pub fn run_ucmd_as_root_with_stdin_stdout(
ts: &TestScenario,
args: &[&str],
stdin: Option<&str>,
stdout: Option<&str>,
) -> std::result::Result<CmdResult, String> {
if is_ci() {
Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI"))
} else {
log_info("run", "sudo -E --non-interactive whoami");
match Command::new("sudo")
.envs(DEFAULT_ENV)
.args(["-E", "--non-interactive", "whoami"])
.output()
{
Ok(output) if String::from_utf8_lossy(&output.stdout).eq("root\n") => {
let mut cmd = ts.cmd("sudo");
cmd.env("PATH", PATH)
.envs(DEFAULT_ENV)
.arg("-E")
.arg("--non-interactive")
.arg(&ts.bin_path)
.arg(&ts.util_name)
.args(args);
if let Some(stdin) = stdin {
cmd.set_stdin(File::open(stdin).unwrap());
}
if let Some(stdout) = stdout {
cmd.set_stdout(File::open(stdout).unwrap());
}
Ok(cmd.run())
}
Ok(output)
if String::from_utf8_lossy(&output.stderr).eq("sudo: a password is required\n") =>
{
Err("Cannot run non-interactive sudo".to_string())
}
Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()),
Err(e) => Err(format!("{UUTILS_WARNING}: {e}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
#[ctor::ctor]
fn init() {
unsafe {
std::env::set_var("UUTESTS_BINARY_PATH", "");
}
}
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
UCommand::new().arg(cmd).run()
}
#[test]
fn test_command_result_when_no_output_with_exit_32() {
let result = run_cmd("exit 32");
if cfg!(windows) {
std::assert!(result.bin_path.ends_with("cmd"));
} else {
std::assert!(result.bin_path.ends_with("sh"));
}
std::assert!(result.util_name.is_none());
std::assert!(result.tmpd.is_some());
assert!(result.exit_status.is_some());
std::assert_eq!(result.code(), 32);
result.code_is(32);
assert!(!result.succeeded());
result.failure();
result.fails_silently();
assert!(result.stderr.is_empty());
assert!(result.stdout.is_empty());
result.no_output();
result.no_stderr();
result.no_stdout();
}
#[test]
#[should_panic]
fn test_command_result_when_exit_32_then_success_panic() {
run_cmd("exit 32").success();
}
#[test]
fn test_command_result_when_no_output_with_exit_0() {
let result = run_cmd("exit 0");
assert!(result.exit_status.is_some());
std::assert_eq!(result.code(), 0);
result.code_is(0);
assert!(result.succeeded());
result.success();
assert!(result.stderr.is_empty());
assert!(result.stdout.is_empty());
result.no_output();
result.no_stderr();
result.no_stdout();
}
#[test]
#[should_panic]
fn test_command_result_when_exit_0_then_failure_panics() {
run_cmd("exit 0").failure();
}
#[test]
#[should_panic]
fn test_command_result_when_exit_0_then_silent_failure_panics() {
run_cmd("exit 0").fails_silently();
}
#[test]
fn test_command_result_when_stdout_with_exit_0() {
#[cfg(windows)]
let (result, vector, string) = (
run_cmd("echo hello& exit 0"),
vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'],
"hello\r\n",
);
#[cfg(not(windows))]
let (result, vector, string) = (
run_cmd("echo hello; exit 0"),
vec![b'h', b'e', b'l', b'l', b'o', b'\n'],
"hello\n",
);
assert!(result.exit_status.is_some());
std::assert_eq!(result.code(), 0);
result.code_is(0);
assert!(result.succeeded());
result.success();
assert!(result.stderr.is_empty());
std::assert_eq!(result.stdout, vector);
result.no_stderr();
result.stdout_is(string);
result.stdout_is_bytes(&vector);
result.stdout_only(string);
result.stdout_only_bytes(&vector);
}
#[test]
fn test_command_result_when_stderr_with_exit_0() {
#[cfg(windows)]
let (result, vector, string) = (
run_cmd("echo hello>&2& exit 0"),
vec![b'h', b'e', b'l', b'l', b'o', b'\r', b'\n'],
"hello\r\n",
);
#[cfg(not(windows))]
let (result, vector, string) = (
run_cmd("echo hello >&2; exit 0"),
vec![b'h', b'e', b'l', b'l', b'o', b'\n'],
"hello\n",
);
assert!(result.exit_status.is_some());
std::assert_eq!(result.code(), 0);
result.code_is(0);
assert!(result.succeeded());
result.success();
assert!(result.stdout.is_empty());
result.no_stdout();
std::assert_eq!(result.stderr, vector);
result.stderr_is(string);
result.stderr_is_bytes(&vector);
result.stderr_only(string);
result.stderr_only_bytes(&vector);
}
#[test]
fn test_std_does_not_contain() {
#[cfg(windows)]
let res = run_cmd(
"(echo This is a likely error message& echo This is a likely error message>&2) & exit 0",
);
#[cfg(not(windows))]
let res = run_cmd(
"echo This is a likely error message; echo This is a likely error message >&2; exit 0",
);
res.stdout_does_not_contain("unlikely");
res.stderr_does_not_contain("unlikely");
}
#[test]
#[should_panic]
fn test_stdout_does_not_contain_fail() {
#[cfg(windows)]
let res = run_cmd("echo This is a likely error message& exit 0");
#[cfg(not(windows))]
let res = run_cmd("echo This is a likely error message; exit 0");
res.stdout_does_not_contain("likely");
}
#[test]
#[should_panic]
fn test_stderr_does_not_contain_fail() {
#[cfg(windows)]
let res = run_cmd("echo This is a likely error message>&2 & exit 0");
#[cfg(not(windows))]
let res = run_cmd("echo This is a likely error message >&2; exit 0");
res.stderr_does_not_contain("likely");
}
#[test]
fn test_stdout_matches() {
#[cfg(windows)]
let res = run_cmd(
"(echo This is a likely error message& echo This is a likely error message>&2 ) & exit 0",
);
#[cfg(not(windows))]
let res = run_cmd(
"echo This is a likely error message; echo This is a likely error message >&2; exit 0",
);
let positive = regex::Regex::new(".*likely.*").unwrap();
let negative = regex::Regex::new(".*unlikely.*").unwrap();
res.stdout_matches(&positive);
res.stdout_does_not_match(&negative);
}
#[test]
#[should_panic]
fn test_stdout_matches_fail() {
#[cfg(windows)]
let res = run_cmd(
"(echo This is a likely error message& echo This is a likely error message>&2) & exit 0",
);
#[cfg(not(windows))]
let res = run_cmd(
"echo This is a likely error message; echo This is a likely error message >&2; exit 0",
);
let negative = regex::Regex::new(".*unlikely.*").unwrap();
res.stdout_matches(&negative);
}
#[test]
#[should_panic]
fn test_stdout_not_matches_fail() {
#[cfg(windows)]
let res = run_cmd(
"(echo This is a likely error message& echo This is a likely error message>&2) & exit 0",
);
#[cfg(not(windows))]
let res = run_cmd(
"echo This is a likely error message; echo This is a likely error message >&2; exit 0",
);
let positive = regex::Regex::new(".*likely.*").unwrap();
res.stdout_does_not_match(&positive);
}
#[test]
#[cfg(unix)]
fn test_parse_coreutil_version() {
use std::assert_eq;
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 9.0.123-0123").to_string(),
"9"
);
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 8.32.263-0475").to_string(),
"8.32"
);
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 8.25.123-0123").to_string(),
"8.25"
);
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 9.0").to_string(),
"9"
);
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 8.32").to_string(),
"8.32"
);
assert_eq!(
parse_coreutil_version("id (GNU coreutils) 8.25").to_string(),
"8.25"
);
}
#[test]
#[cfg(unix)]
fn test_check_coreutil_version() {
match check_coreutil_version("id", VERSION_MIN) {
Ok(s) => assert!(s.starts_with("uutils-tests-")),
Err(s) => assert!(s.starts_with("uutils-tests-warning")),
};
#[cfg(target_os = "linux")]
std::assert_eq!(
check_coreutil_version("no test name", VERSION_MIN),
Err("uutils-tests-warning: 'no test name' \
No such file or directory (os error 2)"
.to_string())
);
}
#[test]
#[cfg(unix)]
fn test_expected_result() {
let ts = TestScenario::new("id");
match expected_result(&ts, &[]) {
Ok(r) => assert!(r.succeeded()),
Err(s) => assert!(s.starts_with("uutils-tests-warning")),
}
let ts = TestScenario::new("no test name");
assert!(expected_result(&ts, &[]).is_err());
}
#[test]
#[cfg(unix)]
fn test_host_name_for() {
#[cfg(target_os = "linux")]
{
std::assert_eq!(host_name_for("id"), "id");
std::assert_eq!(host_name_for("groups"), "groups");
std::assert_eq!(host_name_for("who"), "who");
}
#[cfg(not(target_os = "linux"))]
{
std::assert_eq!(host_name_for("id"), "gid");
std::assert_eq!(host_name_for("groups"), "ggroups");
std::assert_eq!(host_name_for("who"), "gwho");
std::assert_eq!(host_name_for("gid"), "gid");
std::assert_eq!(host_name_for("ggroups"), "ggroups");
std::assert_eq!(host_name_for("gwho"), "gwho");
}
}
#[test]
#[cfg(unix)]
fn test_run_ucmd_as_root() {
if is_ci() {
println!("TEST SKIPPED (cannot run inside CI)");
} else {
if let Ok(output) = Command::new("sudo")
.env("LC_ALL", "C")
.args(["-E", "--non-interactive", "whoami"])
.output()
{
if output.status.success() && String::from_utf8_lossy(&output.stdout).eq("root\n") {
let ts = TestScenario::new("whoami");
std::assert_eq!(
run_ucmd_as_root(&ts, &[]).unwrap().stdout_str().trim(),
"root"
);
} else {
println!("TEST SKIPPED (we're not root)");
}
} else {
println!("TEST SKIPPED (cannot run sudo)");
}
}
}
#[cfg(all(unix, not(any(target_os = "macos", target_os = "openbsd"))))]
#[test]
fn test_compare_xattrs() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path1 = temp_dir.path().join("test_file1.txt");
let file_path2 = temp_dir.path().join("test_file2.txt");
File::create(&file_path1).unwrap();
File::create(&file_path2).unwrap();
let test_attr = "user.test_attr";
let test_value = b"test value";
xattr::set(&file_path1, test_attr, test_value).unwrap();
assert!(!compare_xattrs(&file_path1, &file_path2));
xattr::set(&file_path2, test_attr, test_value).unwrap();
assert!(compare_xattrs(&file_path1, &file_path2));
}
#[cfg(unix)]
#[test]
fn test_application_of_process_resource_limits_unlimited_file_size() {
let ts = TestScenario::new("util");
ts.cmd("sh")
.args(&["-c", "ulimit -Sf; ulimit -Hf"])
.succeeds()
.no_stderr()
.stdout_is("unlimited\nunlimited\n");
}
#[cfg(unix)]
#[test]
fn test_application_of_process_resource_limits_limited_file_size() {
let unit_size_bytes = if cfg!(target_os = "macos") { 1024 } else { 512 };
let ts = TestScenario::new("util");
ts.cmd("sh")
.args(&["-c", "ulimit -Sf; ulimit -Hf"])
.limit(
rlimit::Resource::FSIZE,
8 * unit_size_bytes,
16 * unit_size_bytes,
)
.succeeds()
.no_stderr()
.stdout_is("8\n16\n");
}
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
#[test]
fn test_altering_umask() {
use uucore::mode::get_umask;
let p_umask = get_umask();
let c_umask = if p_umask == 0o002 { 0o007 } else { 0o002 };
let expected = if cfg!(target_os = "android") {
if p_umask == 0o002 { "007\n" } else { "002\n" }
} else if p_umask == 0o002 {
"0007\n"
} else {
"0002\n"
};
let ts = TestScenario::new("util");
ts.cmd_shell("umask")
.umask(c_umask)
.succeeds()
.stdout_is(expected);
std::assert_eq!(p_umask, get_umask()); }
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
#[test]
fn test_mount_temp_fs() {
let mut scene = TestScenario::new("util");
let at = &scene.fixtures;
if scene.cmd("whoami").run().stdout_str() != "root\n" {
return;
}
at.mkdir("mountpoint");
let mountpoint = at.plus("mountpoint");
scene.mount_temp_fs(mountpoint.to_str().unwrap()).unwrap();
scene
.cmd("df")
.arg("-h")
.arg(mountpoint)
.succeeds()
.stdout_contains("tmpfs");
}
}