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"); + } + } +}