diff --git a/.github/workflows/GNU.yml b/.github/workflows/GNU.yml index 35efccbe5..a68f0a083 100644 --- a/.github/workflows/GNU.yml +++ b/.github/workflows/GNU.yml @@ -80,6 +80,9 @@ jobs: -e '/tests\/misc\/help-version-getopt.sh/ D' \ Makefile + # printf doesn't limit the values used in its arg, so this produced ~2GB of output + sed -i '/INT_OFLOW/ D' tests/misc/printf.sh + # Use the system coreutils where the test fails due to error in a util that is not the one being tested sed -i 's|stat|/usr/bin/stat|' tests/chgrp/basic.sh tests/cp/existing-perm-dir.sh tests/touch/60-seconds.sh tests/misc/sort-compress-proc.sh sed -i 's|ls -|/usr/bin/ls -|' tests/chgrp/posix-H.sh tests/chown/deref.sh tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh tests/du/8gb.sh diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 7d56a7485..e507c5acd 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -22,6 +22,12 @@ use std::io::{self, Read, Write}; use thiserror::Error; use uucore::fs::is_stdin_interactive; +/// Linux splice support +#[cfg(any(target_os = "linux", target_os = "android"))] +mod splice; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::unix::io::{AsRawFd, RawFd}; + /// Unix domain socket support #[cfg(unix)] use std::net::Shutdown; @@ -30,14 +36,6 @@ use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use unix_socket::UnixStream; -/// Linux splice support -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::fcntl::{splice, SpliceFFlags}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use nix::unistd::pipe; -#[cfg(any(target_os = "linux", target_os = "android"))] -use std::os::unix::io::{AsRawFd, RawFd}; - static NAME: &str = "cat"; static VERSION: &str = env!("CARGO_PKG_VERSION"); static SYNTAX: &str = "[OPTION]... [FILE]..."; @@ -395,7 +393,7 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { { // If we're on Linux or Android, try to use the splice() system call // for faster writing. If it works, we're done. - if !write_fast_using_splice(handle, stdout_lock.as_raw_fd())? { + if !splice::write_fast_using_splice(handle, stdout_lock.as_raw_fd())? { return Ok(()); } } @@ -411,75 +409,6 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { Ok(()) } -/// This function is called from `write_fast()` on Linux and Android. The -/// function `splice()` is used to move data between two file descriptors -/// without copying between kernel- and userspace. This results in a large -/// speedup. -/// -/// The `bool` in the result value indicates if we need to fall back to normal -/// copying or not. False means we don't have to. -#[cfg(any(target_os = "linux", target_os = "android"))] -#[inline] -fn write_fast_using_splice(handle: &mut InputHandle, writer: RawFd) -> CatResult { - const BUF_SIZE: usize = 1024 * 16; - - let (pipe_rd, pipe_wr) = pipe()?; - - // We only fall back if splice fails on the first call. - match splice( - handle.file_descriptor, - None, - pipe_wr, - None, - BUF_SIZE, - SpliceFFlags::empty(), - ) { - Ok(n) => { - if n == 0 { - return Ok(false); - } - splice_exact(pipe_rd, writer, n)?; - } - Err(_) => { - return Ok(true); - } - } - - loop { - let n = splice( - handle.file_descriptor, - None, - pipe_wr, - None, - BUF_SIZE, - SpliceFFlags::empty(), - )?; - if n == 0 { - // We read 0 bytes from the input, - // which means we're done copying. - break; - } - splice_exact(pipe_rd, writer, n)?; - } - - Ok(false) -} - -/// Splice wrapper which handles short writes -#[cfg(any(target_os = "linux", target_os = "android"))] -#[inline] -fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { - let mut left = num_bytes; - loop { - let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; - left -= written; - if left == 0 { - break; - } - } - Ok(()) -} - /// Outputs file contents to stdout in a line-by-line fashion, /// propagating any errors that might occur. fn write_lines( diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs new file mode 100644 index 000000000..ccc625467 --- /dev/null +++ b/src/uu/cat/src/splice.rs @@ -0,0 +1,91 @@ +use super::{CatResult, InputHandle}; + +use nix::fcntl::{splice, SpliceFFlags}; +use nix::unistd::{self, pipe}; +use std::io::Read; +use std::os::unix::io::RawFd; + +const BUF_SIZE: usize = 1024 * 16; + +/// This function is called from `write_fast()` on Linux and Android. The +/// function `splice()` is used to move data between two file descriptors +/// without copying between kernel- and userspace. This results in a large +/// speedup. +/// +/// The `bool` in the result value indicates if we need to fall back to normal +/// copying or not. False means we don't have to. +#[inline] +pub(super) fn write_fast_using_splice( + handle: &mut InputHandle, + write_fd: RawFd, +) -> CatResult { + let (pipe_rd, pipe_wr) = match pipe() { + Ok(r) => r, + Err(_) => { + // It is very rare that creating a pipe fails, but it can happen. + return Ok(true); + } + }; + + loop { + match splice( + handle.file_descriptor, + None, + pipe_wr, + None, + BUF_SIZE, + SpliceFFlags::empty(), + ) { + Ok(n) => { + if n == 0 { + return Ok(false); + } + if splice_exact(pipe_rd, write_fd, n).is_err() { + // If the first splice manages to copy to the intermediate + // pipe, but the second splice to stdout fails for some reason + // we can recover by copying the data that we have from the + // intermediate pipe to stdout using normal read/write. Then + // we tell the caller to fall back. + copy_exact(pipe_rd, write_fd, n)?; + return Ok(true); + } + } + Err(_) => { + return Ok(true); + } + } + } +} + +/// Splice wrapper which handles short writes. +#[inline] +fn splice_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { + let mut left = num_bytes; + loop { + let written = splice(read_fd, None, write_fd, None, left, SpliceFFlags::empty())?; + left -= written; + if left == 0 { + break; + } + } + Ok(()) +} + +/// Caller must ensure that `num_bytes <= BUF_SIZE`, otherwise this function +/// will panic. The way we use this function in `write_fast_using_splice` +/// above is safe because `splice` is set to write at most `BUF_SIZE` to the +/// pipe. +#[inline] +fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<()> { + let mut left = num_bytes; + let mut buf = [0; BUF_SIZE]; + loop { + let read = unistd::read(read_fd, &mut buf[..left])?; + let written = unistd::write(write_fd, &mut buf[..read])?; + left -= written; + if left == 0 { + break; + } + } + Ok(()) +} diff --git a/src/uu/ls/BENCHMARKING.md b/src/uu/ls/BENCHMARKING.md new file mode 100644 index 000000000..84a0c3d84 --- /dev/null +++ b/src/uu/ls/BENCHMARKING.md @@ -0,0 +1,34 @@ +# Benchmarking ls + +ls majorly involves fetching a lot of details (depending upon what details are requested, eg. time/date, inode details, etc) for each path using system calls. Ideally, any system call should be done only once for each of the paths - not adhering to this principle leads to a lot of system call overhead multiplying and bubbling up, especially for recursive ls, therefore it is important to always benchmark multiple scenarios. +This is an overwiew over what was benchmarked, and if you make changes to `ls`, you are encouraged to check +how performance was affected for the workloads listed below. Feel free to add other workloads to the +list that we should improve / make sure not to regress. + +Run `cargo build --release` before benchmarking after you make a change! + +## Simple recursive ls + +- Get a large tree, for example linux kernel source tree. +- Benchmark simple recursive ls with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -R tree > /dev/null"`. + +## Recursive ls with all and long options + +- Same tree as above +- Benchmark recursive ls with -al -R options with hyperfine: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"`. + +## Comparing with GNU ls + +Hyperfine accepts multiple commands to run and will compare them. To compare performance with GNU ls +duplicate the string you passed to hyperfine but remove the `target/release/coreutils` bit from it. + +Example: `hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null"` becomes +`hyperfine --warmup 2 "target/release/coreutils ls -al -R tree > /dev/null" "ls -al -R tree > /dev/null"` +(This assumes GNU ls is installed as `ls`) + +This can also be used to compare with version of ls built before your changes to ensure your change does not regress this + +## Checking system call count + +- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems. +- Example: `strace -c target/release/coreutils ls -al -R tree` diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 514539809..e0aa3ec4b 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1038,12 +1038,43 @@ pub fn uumain(args: impl uucore::Args) -> i32 { list(locs, Config::from(matches)) } +/// Represents a Path along with it's associated data +/// Any data that will be reused several times makes sense to be added to this structure +/// Caching data here helps eliminate redundant syscalls to fetch same information +struct PathData { + // Result got from symlink_metadata() or metadata() based on config + md: std::io::Result, + // String formed from get_lossy_string() for the path + lossy_string: String, + // Name of the file - will be empty for . or .. + file_name: String, + // PathBuf that all above data corresponds to + p_buf: PathBuf, +} + +impl PathData { + fn new(p_buf: PathBuf, config: &Config, command_line: bool) -> Self { + let md = get_metadata(&p_buf, config, command_line); + let lossy_string = p_buf.to_string_lossy().into_owned(); + let name = p_buf + .file_name() + .map_or(String::new(), |s| s.to_string_lossy().into_owned()); + Self { + md, + lossy_string, + file_name: name, + p_buf, + } + } +} + fn list(locs: Vec, config: Config) -> i32 { let number_of_locs = locs.len(); - let mut files = Vec::::new(); - let mut dirs = Vec::::new(); + let mut files = Vec::::new(); + let mut dirs = Vec::::new(); let mut has_failed = false; + for loc in locs { let p = PathBuf::from(&loc); if !p.exists() { @@ -1054,36 +1085,28 @@ fn list(locs: Vec, config: Config) -> i32 { continue; } - let show_dir_contents = if !config.directory { - match config.dereference { - Dereference::None => { - if let Ok(md) = p.symlink_metadata() { - md.is_dir() - } else { - show_error!("'{}': {}", &loc, "No such file or directory"); - has_failed = true; - continue; - } - } - _ => p.is_dir(), - } + let path_data = PathData::new(p, &config, true); + + let show_dir_contents = if let Ok(md) = path_data.md.as_ref() { + !config.directory && md.is_dir() } else { + has_failed = true; false }; if show_dir_contents { - dirs.push(p); + dirs.push(path_data); } else { - files.push(p); + files.push(path_data); } } sort_entries(&mut files, &config); - display_items(&files, None, &config, true); + display_items(&files, None, &config); sort_entries(&mut dirs, &config); for dir in dirs { if number_of_locs > 1 { - println!("\n{}:", dir.to_string_lossy()); + println!("\n{}:", dir.lossy_string); } enter_directory(&dir, &config); } @@ -1094,22 +1117,22 @@ fn list(locs: Vec, config: Config) -> i32 { } } -fn sort_entries(entries: &mut Vec, config: &Config) { +fn sort_entries(entries: &mut Vec, config: &Config) { match config.sort { Sort::Time => entries.sort_by_key(|k| { Reverse( - get_metadata(k, false) + k.md.as_ref() .ok() .and_then(|md| get_system_time(&md, config)) .unwrap_or(UNIX_EPOCH), ) }), Sort::Size => { - entries.sort_by_key(|k| Reverse(get_metadata(k, false).map(|md| md.len()).unwrap_or(0))) + entries.sort_by_key(|k| Reverse(k.md.as_ref().map(|md| md.len()).unwrap_or(0))) } // The default sort in GNU ls is case insensitive - Sort::Name => entries.sort_by_key(|k| k.to_string_lossy().to_lowercase()), - Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(a, b)), + Sort::Name => entries.sort_by_key(|k| k.file_name.to_lowercase()), + Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)), Sort::None => {} } @@ -1143,32 +1166,57 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { true } -fn enter_directory(dir: &Path, config: &Config) { - let mut entries: Vec<_> = safe_unwrap!(fs::read_dir(dir).and_then(Iterator::collect)); - - entries.retain(|e| should_display(e, config)); - - let mut entries: Vec<_> = entries.iter().map(DirEntry::path).collect(); - sort_entries(&mut entries, config); - - if config.files == Files::All { - let mut display_entries = entries.clone(); - display_entries.insert(0, dir.join("..")); - display_entries.insert(0, dir.join(".")); - display_items(&display_entries, Some(dir), config, false); +fn enter_directory(dir: &PathData, config: &Config) { + let mut entries: Vec<_> = if config.files == Files::All { + vec![ + PathData::new(dir.p_buf.join("."), config, false), + PathData::new(dir.p_buf.join(".."), config, false), + ] } else { - display_items(&entries, Some(dir), config, false); - } + vec![] + }; + + let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf)) + .map(|res| safe_unwrap!(res)) + .filter(|e| should_display(e, config)) + .map(|e| PathData::new(DirEntry::path(&e), config, false)) + .collect(); + + sort_entries(&mut temp, config); + + entries.append(&mut temp); + + display_items(&entries, Some(&dir.p_buf), config); if config.recursive { - for e in entries.iter().filter(|p| p.is_dir()) { - println!("\n{}:", e.to_string_lossy()); + for e in entries + .iter() + .skip(if config.files == Files::All { 2 } else { 0 }) + .filter(|p| p.md.as_ref().map(|md| md.is_dir()).unwrap_or(false)) + { + println!("\n{}:", e.lossy_string); enter_directory(&e, config); } } } -fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { +fn get_metadata(entry: &Path, config: &Config, command_line: bool) -> std::io::Result { + let dereference = match &config.dereference { + Dereference::All => true, + Dereference::Args => command_line, + Dereference::DirArgs => { + if command_line { + if let Ok(md) = entry.metadata() { + md.is_dir() + } else { + false + } + } else { + false + } + } + Dereference::None => false, + }; if dereference { entry.metadata().or_else(|_| entry.symlink_metadata()) } else { @@ -1176,8 +1224,8 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result { } } -fn display_dir_entry_size(entry: &Path, config: &Config) -> (usize, usize) { - if let Ok(md) = get_metadata(entry, false) { +fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) { + if let Ok(md) = entry.md.as_ref() { ( display_symlink_count(&md).len(), display_file_size(&md, config).len(), @@ -1191,7 +1239,7 @@ fn pad_left(string: String, count: usize) -> String { format!("{:>width$}", string, width = count) } -fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, command_line: bool) { +fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) { if config.format == Format::Long { let (mut max_links, mut max_size) = (1, 1); for item in items { @@ -1200,18 +1248,18 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config, comma max_size = size.max(max_size); } for item in items { - display_item_long(item, strip, max_links, max_size, config, command_line); + display_item_long(item, strip, max_links, max_size, config); } } else { let names = items.iter().filter_map(|i| { - let md = get_metadata(i, false); + let md = i.md.as_ref(); match md { Err(e) => { - let filename = get_file_name(i, strip); + let filename = get_file_name(&i.p_buf, strip); show_error!("'{}': {}", filename, e); None } - Ok(md) => Some(display_file_name(&i, strip, &md, config)), + Ok(md) => Some(display_file_name(&i.p_buf, strip, &md, config)), } }); @@ -1271,33 +1319,15 @@ fn display_grid(names: impl Iterator, width: u16, direction: Direct use uucore::fs::display_permissions; fn display_item_long( - item: &Path, + item: &PathData, strip: Option<&Path>, max_links: usize, max_size: usize, config: &Config, - command_line: bool, ) { - let dereference = match &config.dereference { - Dereference::All => true, - Dereference::Args => command_line, - Dereference::DirArgs => { - if command_line { - if let Ok(md) = item.metadata() { - md.is_dir() - } else { - false - } - } else { - false - } - } - Dereference::None => false, - }; - - let md = match get_metadata(item, dereference) { + let md = match &item.md { Err(e) => { - let filename = get_file_name(&item, strip); + let filename = get_file_name(&item.p_buf, strip); show_error!("{}: {}", filename, e); return; } @@ -1336,7 +1366,7 @@ fn display_item_long( " {} {} {}", pad_left(display_file_size(&md, config), max_size), display_date(&md, config), - display_file_name(&item, strip, &md, config).contents, + display_file_name(&item.p_buf, strip, &md, config).contents, ); } @@ -1348,14 +1378,50 @@ fn get_inode(metadata: &Metadata) -> String { // Currently getpwuid is `linux` target only. If it's broken out into // a posix-compliant attribute this can be updated... #[cfg(unix)] +use std::sync::Mutex; +#[cfg(unix)] use uucore::entries; +#[cfg(unix)] +fn cached_uid2usr(uid: u32) -> String { + lazy_static! { + static ref UID_CACHE: Mutex> = Mutex::new(HashMap::new()); + } + + let mut uid_cache = UID_CACHE.lock().unwrap(); + match uid_cache.get(&uid) { + Some(usr) => usr.clone(), + None => { + let usr = entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()); + uid_cache.insert(uid, usr.clone()); + usr + } + } +} + #[cfg(unix)] fn display_uname(metadata: &Metadata, config: &Config) -> String { if config.long.numeric_uid_gid { metadata.uid().to_string() } else { - entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string()) + cached_uid2usr(metadata.uid()) + } +} + +#[cfg(unix)] +fn cached_gid2grp(gid: u32) -> String { + lazy_static! { + static ref GID_CACHE: Mutex> = Mutex::new(HashMap::new()); + } + + let mut gid_cache = GID_CACHE.lock().unwrap(); + match gid_cache.get(&gid) { + Some(grp) => grp.clone(), + None => { + let grp = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()); + gid_cache.insert(gid, grp.clone()); + grp + } } } @@ -1364,7 +1430,7 @@ fn display_group(metadata: &Metadata, config: &Config) -> String { if config.long.numeric_uid_gid { metadata.gid().to_string() } else { - entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string()) + cached_gid2grp(metadata.gid()) } } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 389269395..9f7ebdd37 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -1,4 +1,7 @@ use crate::common::util::*; +#[cfg(unix)] +use std::fs::OpenOptions; +#[cfg(unix)] use std::io::Read; #[test] @@ -54,7 +57,6 @@ fn test_no_options_big_input() { #[test] #[cfg(unix)] fn test_fifo_symlink() { - use std::fs::OpenOptions; use std::io::Write; use std::thread; @@ -85,6 +87,74 @@ fn test_fifo_symlink() { thread.join().unwrap(); } +#[test] +#[cfg(unix)] +fn test_piped_to_regular_file() { + use std::fs::read_to_string; + + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + let file_path = s.fixtures.plus("file.txt"); + + { + let file = OpenOptions::new() + .create_new(true) + .write(true) + .append(append) + .open(&file_path) + .unwrap(); + + s.ucmd() + .set_stdout(file) + .pipe_in_fixture("alpha.txt") + .succeeds(); + } + let contents = read_to_string(&file_path).unwrap(); + assert_eq!(contents, "abcde\nfghij\nklmno\npqrst\nuvwxyz\n"); + } +} + +#[test] +#[cfg(unix)] +fn test_piped_to_dev_null() { + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + { + let dev_null = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/null") + .unwrap(); + + s.ucmd() + .set_stdout(dev_null) + .pipe_in_fixture("alpha.txt") + .succeeds(); + } + } +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_piped_to_dev_full() { + for &append in &[true, false] { + let s = TestScenario::new(util_name!()); + { + let dev_full = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/full") + .unwrap(); + + s.ucmd() + .set_stdout(dev_full) + .pipe_in_fixture("alpha.txt") + .fails() + .stderr_contains(&"No space left on device".to_owned()); + } + } +} + #[test] fn test_directory() { let s = TestScenario::new(util_name!()); @@ -330,22 +400,29 @@ fn test_domain_socket() { use std::thread; use tempdir::TempDir; use unix_socket::UnixListener; + use std::sync::{Barrier, Arc}; let dir = TempDir::new("unix_socket").expect("failed to create dir"); let socket_path = dir.path().join("sock"); let listener = UnixListener::bind(&socket_path).expect("failed to create socket"); + // use a barrier to ensure we don't run cat before the listener is setup + let barrier = Arc::new(Barrier::new(2)); + let barrier2 = Arc::clone(&barrier); + let thread = thread::spawn(move || { let mut stream = listener.accept().expect("failed to accept connection").0; + barrier2.wait(); stream .write_all(b"a\tb") .expect("failed to write test data"); }); - new_ucmd!() - .args(&[socket_path]) - .succeeds() - .stdout_only("a\tb"); + let child = new_ucmd!().args(&[socket_path]).run_no_wait(); + barrier.wait(); + let stdout = &child.wait_with_output().unwrap().stdout.clone(); + let output = String::from_utf8_lossy(&stdout); + assert_eq!("a\tb", output); thread.join().unwrap(); } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 718e1db1c..ee7ab29f5 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -109,6 +109,14 @@ fn test_ls_width() { .arg("-w=bad") .fails() .stderr_contains("invalid line width"); + + for option in &["-w 1a", "-w=1a", "--width=1a", "--width 1a"] { + scene + .ucmd() + .args(&option.split(" ").collect::>()) + .fails() + .stderr_only("ls: error: invalid line width: ‘1a’"); + } } #[test] diff --git a/tests/common/util.rs b/tests/common/util.rs index 55e121737..95a30d47e 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -696,8 +696,11 @@ pub struct UCommand { comm_string: String, tmpd: Option>, has_run: bool, - stdin: Option>, ignore_stdin_write_error: bool, + stdin: Option, + stdout: Option, + stderr: Option, + bytes_into_stdin: Option>, } impl UCommand { @@ -726,8 +729,11 @@ impl UCommand { cmd }, comm_string: String::from(arg.as_ref().to_str().unwrap()), - stdin: None, ignore_stdin_write_error: false, + bytes_into_stdin: None, + stdin: None, + stdout: None, + stderr: None, } } @@ -738,6 +744,21 @@ impl UCommand { ucmd } + pub fn set_stdin>(&mut self, stdin: T) -> &mut UCommand { + self.stdin = Some(stdin.into()); + self + } + + pub fn set_stdout>(&mut self, stdout: T) -> &mut UCommand { + self.stdout = Some(stdout.into()); + self + } + + pub fn set_stderr>(&mut self, stderr: T) -> &mut UCommand { + self.stderr = Some(stderr.into()); + self + } + /// Add a parameter to the invocation. Path arguments are treated relative /// to the test environment directory. pub fn arg>(&mut self, arg: S) -> &mut UCommand { @@ -767,10 +788,10 @@ impl UCommand { /// provides stdinput to feed in to the command when spawned pub fn pipe_in>>(&mut self, input: T) -> &mut UCommand { - if self.stdin.is_some() { + if self.bytes_into_stdin.is_some() { panic!("{}", MULTIPLE_STDIN_MEANINGLESS); } - self.stdin = Some(input.into()); + self.bytes_into_stdin = Some(input.into()); self } @@ -784,7 +805,7 @@ impl UCommand { /// This is typically useful to test non-standard workflows /// like feeding something to a command that does not read it pub fn ignore_stdin_write_error(&mut self) -> &mut UCommand { - if self.stdin.is_none() { + if self.bytes_into_stdin.is_none() { panic!("{}", NO_STDIN_MEANINGLESS); } self.ignore_stdin_write_error = true; @@ -813,13 +834,13 @@ impl UCommand { log_info("run", &self.comm_string); let mut child = self .raw - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdin(self.stdin.take().unwrap_or_else(|| Stdio::piped())) + .stdout(self.stdout.take().unwrap_or_else(|| Stdio::piped())) + .stderr(self.stderr.take().unwrap_or_else(|| Stdio::piped())) .spawn() .unwrap(); - if let Some(ref input) = self.stdin { + if let Some(ref input) = self.bytes_into_stdin { let write_result = child .stdin .take()