From e5d6c6970b2f7c7b40a65190ed5de5f8dac0af70 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Tue, 24 Aug 2021 15:41:24 +0200 Subject: [PATCH] yes: Cleanup Report errors properly instead of panicking. Replace zero_copy by a simpler specialized private module. Do not assume splices move all data at once. Use the modern uutils machinery. Remove the "latency" feature. The time it takes to prepare the buffer is drowned out by the startup time anyway. yes: Add tests yes: Fix long input test on Windows --- .../workspace.wordlist.txt | 4 +- Cargo.lock | 3 +- src/uu/yes/Cargo.toml | 8 +- src/uu/yes/src/splice.rs | 110 +++++++++++++ src/uu/yes/src/yes.rs | 61 ++++---- src/uucore/Cargo.toml | 4 - src/uucore/src/lib/features.rs | 2 - src/uucore/src/lib/features/zero_copy.rs | 148 ------------------ .../src/lib/features/zero_copy/platform.rs | 21 --- .../features/zero_copy/platform/default.rs | 21 --- .../lib/features/zero_copy/platform/linux.rs | 113 ------------- .../lib/features/zero_copy/platform/unix.rs | 18 --- .../features/zero_copy/platform/windows.rs | 19 --- src/uucore/src/lib/lib.rs | 10 -- tests/by-util/test_yes.rs | 73 ++++++++- 15 files changed, 217 insertions(+), 398 deletions(-) create mode 100644 src/uu/yes/src/splice.rs delete mode 100644 src/uucore/src/lib/features/zero_copy.rs delete mode 100644 src/uucore/src/lib/features/zero_copy/platform.rs delete mode 100644 src/uucore/src/lib/features/zero_copy/platform/default.rs delete mode 100644 src/uucore/src/lib/features/zero_copy/platform/linux.rs delete mode 100644 src/uucore/src/lib/features/zero_copy/platform/unix.rs delete mode 100644 src/uucore/src/lib/features/zero_copy/platform/windows.rs diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 9c4b1c82f..bee22c2d0 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -106,12 +106,14 @@ whoami # * vars/errno errno +EBADF EEXIST +EINVAL ENODATA ENOENT ENOSYS -EPERM EOPNOTSUPP +EPERM # * vars/fcntl F_GETFL diff --git a/Cargo.lock b/Cargo.lock index 1066c02ef..c0d3ceee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3141,6 +3141,7 @@ name = "uu_yes" version = "0.0.7" dependencies = [ "clap", + "nix 0.20.0", "uucore", "uucore_procs", ] @@ -3157,8 +3158,6 @@ dependencies = [ "getopts", "lazy_static", "libc", - "nix 0.19.1", - "platform-info", "termion", "thiserror", "time", diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index ff8465557..b963d4974 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -16,13 +16,11 @@ path = "src/yes.rs" [dependencies] clap = { version = "2.33", features = ["wrap_help"] } -uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["zero-copy"] } +uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -[features] -default = [] -# -latency = [] +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +nix = "0.20.0" [[bin]] name = "yes" diff --git a/src/uu/yes/src/splice.rs b/src/uu/yes/src/splice.rs new file mode 100644 index 000000000..b0573bc9e --- /dev/null +++ b/src/uu/yes/src/splice.rs @@ -0,0 +1,110 @@ +//! On Linux we can use vmsplice() to write data more efficiently. +//! +//! This does not always work. We're not allowed to splice to some targets, +//! and on some systems (notably WSL 1) it isn't supported at all. +//! +//! If we get an error code that suggests splicing isn't supported then we +//! tell that to the caller so it can fall back to a robust naïve method. If +//! we get another kind of error we bubble it up as normal. +//! +//! vmsplice() can only splice into a pipe, so if the output is not a pipe +//! we make our own and use splice() to bridge the gap from the pipe to the +//! output. +//! +//! We assume that an "unsupported" error will only ever happen before any +//! data was successfully written to the output. That way we don't have to +//! make any effort to rescue data from the pipe if splice() fails, we can +//! just fall back and start over from the beginning. + +use std::{ + fs::File, + io, + os::unix::io::{AsRawFd, FromRawFd}, +}; + +use nix::{ + errno::Errno, + fcntl::SpliceFFlags, + libc::S_IFIFO, + sys::{stat::fstat, uio::IoVec}, +}; + +pub(crate) fn splice_data(bytes: &[u8], out: &impl AsRawFd) -> Result<()> { + let is_pipe = fstat(out.as_raw_fd())?.st_mode & S_IFIFO != 0; + + if is_pipe { + loop { + let mut bytes = bytes; + while !bytes.is_empty() { + let len = vmsplice(out, bytes)?; + bytes = &bytes[len..]; + } + } + } else { + let (read, write) = pipe()?; + loop { + let mut bytes = bytes; + while !bytes.is_empty() { + let len = vmsplice(&write, bytes)?; + let mut remaining = len; + while remaining > 0 { + match splice(&read, out, remaining)? { + 0 => panic!("Unexpected end of pipe"), + n => remaining -= n, + }; + } + bytes = &bytes[len..]; + } + } + } +} + +pub(crate) enum Error { + Unsupported, + Io(io::Error), +} + +type Result = std::result::Result; + +impl From for Error { + fn from(error: nix::Error) -> Self { + match error { + nix::Error::Sys(errno) => Error::Io(io::Error::from_raw_os_error(errno as i32)), + _ => Error::Io(io::Error::last_os_error()), + } + } +} + +fn maybe_unsupported(error: nix::Error) -> Error { + match error.as_errno() { + Some(Errno::EINVAL) | Some(Errno::ENOSYS) | Some(Errno::EBADF) => Error::Unsupported, + _ => error.into(), + } +} + +fn splice(source: &impl AsRawFd, target: &impl AsRawFd, len: usize) -> Result { + nix::fcntl::splice( + source.as_raw_fd(), + None, + target.as_raw_fd(), + None, + len, + SpliceFFlags::empty(), + ) + .map_err(maybe_unsupported) +} + +fn vmsplice(target: &impl AsRawFd, bytes: &[u8]) -> Result { + nix::fcntl::vmsplice( + target.as_raw_fd(), + &[IoVec::from_slice(bytes)], + SpliceFFlags::empty(), + ) + .map_err(maybe_unsupported) +} + +fn pipe() -> nix::Result<(File, File)> { + let (read, write) = nix::unistd::pipe()?; + // SAFETY: The file descriptors do not have other owners. + unsafe { Ok((File::from_raw_fd(read), File::from_raw_fd(write))) } +} diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 2c0d43000..03ae4e415 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -7,37 +7,27 @@ /* last synced with: yes (GNU coreutils) 8.13 */ +use std::borrow::Cow; +use std::io::{self, Write}; + #[macro_use] extern crate clap; #[macro_use] extern crate uucore; use clap::{App, Arg}; -use std::borrow::Cow; -use std::io::{self, Write}; -use uucore::zero_copy::ZeroCopyWriter; +use uucore::error::{UResult, USimpleError}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod splice; // it's possible that using a smaller or larger buffer might provide better performance on some // systems, but honestly this is good enough const BUF_SIZE: usize = 16 * 1024; -pub fn uumain(args: impl uucore::Args) -> i32 { - let app = uu_app(); - - let matches = match app.get_matches_from_safe(args) { - Ok(m) => m, - Err(ref e) - if e.kind == clap::ErrorKind::HelpDisplayed - || e.kind == clap::ErrorKind::VersionDisplayed => - { - println!("{}", e); - return 0; - } - Err(f) => { - show_error!("{}", f); - return 1; - } - }; +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().get_matches_from(args); let string = if let Some(values) = matches.values_of("STRING") { let mut result = values.fold(String::new(), |res, s| res + s + " "); @@ -51,16 +41,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut buffer = [0; BUF_SIZE]; let bytes = prepare_buffer(&string, &mut buffer); - exec(bytes); - - 0 + match exec(bytes) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), + Err(err) => Err(USimpleError::new(1, format!("standard output: {}", err))), + } } pub fn uu_app() -> App<'static, 'static> { app_from_crate!().arg(Arg::with_name("STRING").index(1).multiple(true)) } -#[cfg(not(feature = "latency"))] fn prepare_buffer<'a>(input: &'a str, buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8] { if input.len() < BUF_SIZE / 2 { let mut size = 0; @@ -75,16 +66,20 @@ fn prepare_buffer<'a>(input: &'a str, buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8 } } -#[cfg(feature = "latency")] -fn prepare_buffer<'a>(input: &'a str, _buffer: &'a mut [u8; BUF_SIZE]) -> &'a [u8] { - input.as_bytes() -} +pub fn exec(bytes: &[u8]) -> io::Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + #[cfg(any(target_os = "linux", target_os = "android"))] + { + match splice::splice_data(bytes, &stdout) { + Ok(_) => return Ok(()), + Err(splice::Error::Io(err)) => return Err(err), + Err(splice::Error::Unsupported) => (), + } + } -pub fn exec(bytes: &[u8]) { - let mut stdout_raw = io::stdout(); - let mut writer = ZeroCopyWriter::with_default(&mut stdout_raw, |stdout| stdout.lock()); loop { - // TODO: needs to check if pipe fails - writer.write_all(bytes).unwrap(); + stdout.write_all(bytes)?; } } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index c49e0a0f3..6d27ecad4 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -22,9 +22,6 @@ getopts = "<= 0.2.21" wild = "2.0" # * optional thiserror = { version="1.0", optional=true } -lazy_static = { version="1.3", optional=true } -nix = { version="<= 0.19", optional=true } -platform-info = { version="<= 0.1", optional=true } time = { version="<= 0.1.43", optional=true } walkdir = { version="2.3.2", optional=true } # * "problem" dependencies (pinned) @@ -58,4 +55,3 @@ signals = [] utf8 = [] utmpx = ["time", "libc", "dns-lookup"] wide = [] -zero-copy = ["nix", "libc", "lazy_static", "platform-info"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index f90fc7b3d..60be88664 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -8,8 +8,6 @@ pub mod fs; pub mod fsext; #[cfg(feature = "ringbuffer")] pub mod ringbuffer; -#[cfg(feature = "zero-copy")] -pub mod zero_copy; // * (platform-specific) feature-gated modules // ** non-windows diff --git a/src/uucore/src/lib/features/zero_copy.rs b/src/uucore/src/lib/features/zero_copy.rs deleted file mode 100644 index 1eb2c1547..000000000 --- a/src/uucore/src/lib/features/zero_copy.rs +++ /dev/null @@ -1,148 +0,0 @@ -use self::platform::*; - -use std::io::{self, Write}; - -mod platform; - -pub trait AsRawObject { - fn as_raw_object(&self) -> RawObject; -} - -pub trait FromRawObject: Sized { - /// # Safety - /// ToDO ... - unsafe fn from_raw_object(obj: RawObject) -> Option; -} - -// TODO: also make a SpliceWriter that takes an input fd and and output fd and uses splice() to -// transfer data -// TODO: make a TeeWriter or something that takes an input fd and two output fds and uses tee() to -// transfer to both output fds - -enum InnerZeroCopyWriter { - Platform(PlatformZeroCopyWriter), - Standard(T), -} - -impl Write for InnerZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - match self { - InnerZeroCopyWriter::Platform(ref mut writer) => writer.write(buf), - InnerZeroCopyWriter::Standard(ref mut writer) => writer.write(buf), - } - } - - fn flush(&mut self) -> io::Result<()> { - match self { - InnerZeroCopyWriter::Platform(ref mut writer) => writer.flush(), - InnerZeroCopyWriter::Standard(ref mut writer) => writer.flush(), - } - } -} - -pub struct ZeroCopyWriter { - /// This field is never used, but we need it to drop file descriptors - #[allow(dead_code)] - raw_obj_owner: Option, - - inner: InnerZeroCopyWriter, -} - -struct TransformContainer<'a, A: Write + AsRawObject + Sized, B: Write + Sized> { - /// This field is never used and probably could be converted into PhantomData, but might be - /// useful for restructuring later (at the moment it's basically left over from an earlier - /// design) - #[allow(dead_code)] - original: Option<&'a mut A>, - - transformed: Option, -} - -impl<'a, A: Write + AsRawObject + Sized, B: Write + Sized> Write for TransformContainer<'a, A, B> { - fn write(&mut self, bytes: &[u8]) -> io::Result { - self.transformed.as_mut().unwrap().write(bytes) - } - - fn flush(&mut self) -> io::Result<()> { - self.transformed.as_mut().unwrap().flush() - } -} - -impl<'a, A: Write + AsRawObject + Sized, B: Write + Sized> AsRawObject - for TransformContainer<'a, A, B> -{ - fn as_raw_object(&self) -> RawObject { - panic!("Test should never be used") - } -} - -impl ZeroCopyWriter { - pub fn new(writer: T) -> Self { - let raw_obj = writer.as_raw_object(); - match unsafe { PlatformZeroCopyWriter::new(raw_obj) } { - Ok(inner) => ZeroCopyWriter { - raw_obj_owner: Some(writer), - inner: InnerZeroCopyWriter::Platform(inner), - }, - _ => { - // creating the splice writer failed for whatever reason, so just make a default - // writer - ZeroCopyWriter { - raw_obj_owner: None, - inner: InnerZeroCopyWriter::Standard(writer), - } - } - } - } - - pub fn with_default<'a: 'b, 'b, F, W>( - writer: &'a mut T, - func: F, - ) -> ZeroCopyWriter - where - F: Fn(&'a mut T) -> W, - W: Write + Sized + 'b, - { - let raw_obj = writer.as_raw_object(); - match unsafe { PlatformZeroCopyWriter::new(raw_obj) } { - Ok(inner) => ZeroCopyWriter { - raw_obj_owner: Some(TransformContainer { - original: Some(writer), - transformed: None, - }), - inner: InnerZeroCopyWriter::Platform(inner), - }, - _ => { - // XXX: should func actually consume writer and leave it up to the user to save the value? - // maybe provide a default stdin method then? in some cases it would make more sense for the - // value to be consumed - let real_writer = func(writer); - ZeroCopyWriter { - raw_obj_owner: None, - inner: InnerZeroCopyWriter::Standard(TransformContainer { - original: None, - transformed: Some(real_writer), - }), - } - } - } - } - - // XXX: unsure how to get something like this working without allocating, so not providing it - /*pub fn stdout() -> ZeroCopyWriter { - let mut stdout = io::stdout(); - ZeroCopyWriter::with_default(&mut stdout, |stdout| { - stdout.lock() - }) - }*/ -} - -impl Write for ZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform.rs b/src/uucore/src/lib/features/zero_copy/platform.rs deleted file mode 100644 index 67e4354c5..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(any(target_os = "linux", target_os = "android"))] -pub use self::linux::*; -#[cfg(unix)] -pub use self::unix::*; -#[cfg(windows)] -pub use self::windows::*; - -// Add any operating systems we support here -#[cfg(not(any(target_os = "linux", target_os = "android")))] -pub use self::default::*; - -#[cfg(any(target_os = "linux", target_os = "android"))] -mod linux; -#[cfg(unix)] -mod unix; -#[cfg(windows)] -mod windows; - -// Add any operating systems we support here -#[cfg(not(any(target_os = "linux", target_os = "android")))] -mod default; diff --git a/src/uucore/src/lib/features/zero_copy/platform/default.rs b/src/uucore/src/lib/features/zero_copy/platform/default.rs deleted file mode 100644 index 47239a361..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/default.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::features::zero_copy::RawObject; - -use std::io::{self, Write}; - -pub struct PlatformZeroCopyWriter; - -impl PlatformZeroCopyWriter { - pub unsafe fn new(_obj: RawObject) -> Result { - Err(()) - } -} - -impl Write for PlatformZeroCopyWriter { - fn write(&mut self, _bytes: &[u8]) -> io::Result { - panic!("should never occur") - } - - fn flush(&mut self) -> io::Result<()> { - panic!("should never occur") - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/linux.rs b/src/uucore/src/lib/features/zero_copy/platform/linux.rs deleted file mode 100644 index e2bed3061..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/linux.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::io::{self, Write}; -use std::os::unix::io::RawFd; - -use libc::{O_APPEND, S_IFIFO, S_IFREG}; -use nix::errno::Errno; -use nix::fcntl::{fcntl, splice, vmsplice, FcntlArg, SpliceFFlags}; -use nix::sys::stat::{fstat, FileStat}; -use nix::sys::uio::IoVec; -use nix::unistd::pipe; -use platform_info::{PlatformInfo, Uname}; - -use crate::features::zero_copy::{FromRawObject, RawObject}; - -lazy_static::lazy_static! { - static ref IN_WSL: bool = { - let info = PlatformInfo::new().unwrap(); - info.release().contains("Microsoft") - }; -} - -pub struct PlatformZeroCopyWriter { - raw_obj: RawObject, - read_pipe: RawFd, - write_pipe: RawFd, - #[allow(clippy::type_complexity)] - write_fn: fn(&mut PlatformZeroCopyWriter, &[IoVec<&[u8]>], usize) -> io::Result, -} - -impl PlatformZeroCopyWriter { - pub unsafe fn new(raw_obj: RawObject) -> nix::Result { - if *IN_WSL { - // apparently WSL hasn't implemented vmsplice(), causing writes to fail - // thus, we will just say zero-copy doesn't work there rather than working - // around it - return Err(nix::Error::from(Errno::EOPNOTSUPP)); - } - - let stat_info: FileStat = fstat(raw_obj)?; - let access_mode: libc::c_int = fcntl(raw_obj, FcntlArg::F_GETFL)?; - - let is_regular = (stat_info.st_mode & S_IFREG) != 0; - let is_append = (access_mode & O_APPEND) != 0; - let is_fifo = (stat_info.st_mode & S_IFIFO) != 0; - - if is_regular && !is_append { - let (read_pipe, write_pipe) = pipe()?; - - Ok(PlatformZeroCopyWriter { - raw_obj, - read_pipe, - write_pipe, - write_fn: write_regular, - }) - } else if is_fifo { - Ok(PlatformZeroCopyWriter { - raw_obj, - read_pipe: Default::default(), - write_pipe: Default::default(), - write_fn: write_fifo, - }) - } else { - // FIXME: how to error? - Err(nix::Error::from(Errno::UnknownErrno)) - } - } -} - -impl FromRawObject for PlatformZeroCopyWriter { - unsafe fn from_raw_object(obj: RawObject) -> Option { - PlatformZeroCopyWriter::new(obj).ok() - } -} - -impl Write for PlatformZeroCopyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let iovec = &[IoVec::from_slice(buf)]; - - let func = self.write_fn; - func(self, iovec, buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - // XXX: not sure if we need anything else - Ok(()) - } -} - -fn write_regular( - writer: &mut PlatformZeroCopyWriter, - iovec: &[IoVec<&[u8]>], - len: usize, -) -> io::Result { - vmsplice(writer.write_pipe, iovec, SpliceFFlags::empty()) - .and_then(|_| { - splice( - writer.read_pipe, - None, - writer.raw_obj, - None, - len, - SpliceFFlags::empty(), - ) - }) - .map_err(|_| io::Error::last_os_error()) -} - -fn write_fifo( - writer: &mut PlatformZeroCopyWriter, - iovec: &[IoVec<&[u8]>], - _len: usize, -) -> io::Result { - vmsplice(writer.raw_obj, iovec, SpliceFFlags::empty()).map_err(|_| io::Error::last_os_error()) -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/unix.rs b/src/uucore/src/lib/features/zero_copy/platform/unix.rs deleted file mode 100644 index 553549c9b..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/unix.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; - -use crate::features::zero_copy::{AsRawObject, FromRawObject}; - -pub type RawObject = RawFd; - -impl AsRawObject for T { - fn as_raw_object(&self) -> RawObject { - self.as_raw_fd() - } -} - -// FIXME: check if this works right -impl FromRawObject for T { - unsafe fn from_raw_object(obj: RawObject) -> Option { - Some(T::from_raw_fd(obj)) - } -} diff --git a/src/uucore/src/lib/features/zero_copy/platform/windows.rs b/src/uucore/src/lib/features/zero_copy/platform/windows.rs deleted file mode 100644 index 8134bfda3..000000000 --- a/src/uucore/src/lib/features/zero_copy/platform/windows.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::os::windows::io::{AsRawHandle, FromRawHandle, RawHandle}; - -use crate::features::zero_copy::{AsRawObject, FromRawObject}; - -pub type RawObject = RawHandle; - -impl AsRawObject for T { - fn as_raw_object(&self) -> RawObject { - self.as_raw_handle() - } -} - -impl FromRawObject for T { - unsafe fn from_raw_object(obj: RawObject) -> Option { - Some(T::from_raw_handle(obj)) - } -} - -// TODO: see if there's some zero-copy stuff in Windows diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 6acd4e017..a39834ec1 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -3,14 +3,6 @@ // Copyright (C) ~ Alex Lyon // Copyright (C) ~ Roy Ivy III ; MIT license -// * feature-gated external crates -#[cfg(all(feature = "lazy_static", target_os = "linux"))] -extern crate lazy_static; -#[cfg(feature = "nix")] -extern crate nix; -#[cfg(feature = "platform-info")] -extern crate platform_info; - // * feature-gated external crates (re-shared as public internal modules) #[cfg(feature = "libc")] pub extern crate libc; @@ -46,8 +38,6 @@ pub use crate::features::fs; pub use crate::features::fsext; #[cfg(feature = "ringbuffer")] pub use crate::features::ringbuffer; -#[cfg(feature = "zero-copy")] -pub use crate::features::zero_copy; // * (platform-specific) feature-gated modules // ** non-windows diff --git a/tests/by-util/test_yes.rs b/tests/by-util/test_yes.rs index 651491045..7e950e1ea 100644 --- a/tests/by-util/test_yes.rs +++ b/tests/by-util/test_yes.rs @@ -1 +1,72 @@ -// ToDO: add tests +use std::io::Read; + +use crate::common::util::*; + +/// Run `yes`, capture some of the output, close the pipe, and verify it. +fn run(args: &[&str], expected: &[u8]) { + let mut cmd = new_ucmd!(); + let mut child = cmd.args(args).run_no_wait(); + let mut stdout = child.stdout.take().unwrap(); + let mut buf = vec![0; expected.len()]; + stdout.read_exact(&mut buf).unwrap(); + drop(stdout); + assert!(child.wait().unwrap().success()); + assert_eq!(buf.as_slice(), expected); +} + +#[test] +fn test_simple() { + run(&[], b"y\ny\ny\ny\n"); +} + +#[test] +fn test_args() { + run(&["a", "bar", "c"], b"a bar c\na bar c\na ba"); +} + +#[test] +fn test_long_output() { + run(&[], "y\n".repeat(512 * 1024).as_bytes()); +} + +/// Test with an output that seems likely to get mangled in case of incomplete writes. +#[test] +fn test_long_odd_output() { + run(&["abcdef"], "abcdef\n".repeat(1024 * 1024).as_bytes()); +} + +/// Test with an input that doesn't fit in the standard buffer. +#[test] +fn test_long_input() { + #[cfg(not(windows))] + const TIMES: usize = 14000; + // On Windows the command line is limited to 8191 bytes. + // This is not actually enough to fill the buffer, but it's still nice to + // try something long. + #[cfg(windows)] + const TIMES: usize = 500; + let arg = "abcdefg".repeat(TIMES) + "\n"; + let expected_out = arg.repeat(30); + run(&[&arg[..arg.len() - 1]], expected_out.as_bytes()); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +fn test_piped_to_dev_full() { + use std::fs::OpenOptions; + + for &append in &[true, false] { + { + let dev_full = OpenOptions::new() + .write(true) + .append(append) + .open("/dev/full") + .unwrap(); + + new_ucmd!() + .set_stdout(dev_full) + .fails() + .stderr_contains("No space left on device"); + } + } +}