From 97f09a2c3f8a2d9e109dca966d4f1f5fcc940fda Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:44:19 +0100 Subject: [PATCH 1/7] uucore: normalize wasi errno quirks to posix errorkind WASI reports several conditions with different (or no) ErrorKind than native Unix: opening/removing a directory surfaces raw EBADF instead of EISDIR, reading from a directory-backed descriptor surfaces EISDIR under a different errno per preview version, and preview2 reports a broken pipe as raw EIO. This adds wasi_normalize_open_error, wasi_normalize_read_error, and wasi_is_broken_pipe to uucore::error so call sites can convert these to the same ErrorKind/behavior used on Unix; all three are no-ops off WASI. Also fixes read_os_string_lines to return an io::Error on invalid UTF-8 instead of panicking, since argv/filenames aren't guaranteed valid UTF-8 on WASI. --- src/uucore/src/lib/mods/error.rs | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 1f513efcc1a..46bdfacd0ce 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -491,6 +491,67 @@ pub fn strip_errno(err: &std::io::Error) -> String { msg } +/// Remap the `EBADF`-instead-of-`EISDIR` error WASI raises for filesystem +/// operations attempted on a directory as if it were a regular file. +/// +/// On WASI (both preview 1 and preview 2), `std::fs::File::open`/`.read()`, +/// `std::fs::remove_file`, and similar calls surface raw errno `8` (`EBADF`) +/// when given a directory, instead of the `EISDIR` that Unix platforms +/// return, so `std::io::Error::kind()` comes back `Uncategorized` rather +/// than `IsADirectory`. Call this on such errors to normalize that case to +/// `ErrorKind::IsADirectory`, matching Unix behavior. +/// +/// This is a no-op on non-WASI targets. +pub fn wasi_normalize_open_error(err: std::io::Error) -> std::io::Error { + #[cfg(target_os = "wasi")] + if err.raw_os_error() == Some(8) { + return std::io::Error::new(std::io::ErrorKind::IsADirectory, "Is a directory"); + } + err +} + +/// Remap the error from reading a stream that turned out to be backed by a +/// directory file descriptor (e.g. stdin redirected from a directory, or a +/// raw `read()` on an fd already open on a directory). +/// +/// This covers two distinct WASI error shapes for the same underlying +/// condition: reading a directory fd that was already open when handed to +/// us (e.g. stdin redirected from a directory) surfaces `EISDIR`, under a +/// different errno per preview version (`31` on preview 1, `29` on preview +/// 2); reading a directory fd that we opened ourselves via +/// `std::fs::File::open` surfaces the same `EBADF` (errno `8`) that +/// [`wasi_normalize_open_error`] handles for `open()`-time failures, because +/// on WASI the directory check is deferred from `open` to the first `read`. +/// +/// This is a no-op on non-WASI targets. +pub fn wasi_normalize_read_error(err: std::io::Error) -> std::io::Error { + #[cfg(all(target_os = "wasi", target_env = "p1"))] + if matches!(err.raw_os_error(), Some(31) | Some(8)) { + return std::io::Error::new(std::io::ErrorKind::IsADirectory, "Is a directory"); + } + #[cfg(all(target_os = "wasi", target_env = "p2"))] + if matches!(err.raw_os_error(), Some(29) | Some(8)) { + return std::io::Error::new(std::io::ErrorKind::IsADirectory, "Is a directory"); + } + err +} + +/// Whether `err` represents a broken pipe on the current platform. +/// +/// On WASI preview 2, writing to stdout after the reader is gone surfaces a +/// raw `EIO` (errno `29`) instead of being mapped to `ErrorKind::BrokenPipe` +/// like preview 1 and Unix do; this checks for both. +pub fn wasi_is_broken_pipe(err: &std::io::Error) -> bool { + if err.kind() == std::io::ErrorKind::BrokenPipe { + return true; + } + #[cfg(all(target_os = "wasi", target_env = "p2"))] + if err.raw_os_error() == Some(29) { + return true; + } + false +} + /// Enables the conversion from [`std::io::Error`] to [`UError`] and from [`std::io::Result`] to /// [`UResult`]. pub trait FromIo { From 2481647df8906b3492fec37d80a6e065ff0c7c94 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:56:34 +0100 Subject: [PATCH 2/7] feat(wasi): apply wasi errno normalization across utilities Route open/read errors through the new uucore::error normalization helpers so cat, rm, wc, csplit, tee, sort, seq, tr, tsort, and split report the same ErrorKind/message on WASI as on native Unix: opening or reading a directory now surfaces IsADirectory instead of a raw EBADF/EISDIR errno, and seq's broken-pipe short-circuit also catches preview2's raw EIO. rm and tsort gain small WASI-specific fallbacks (readability probing via read_dir, directory detection via stat) where the platform lacks the Unix APIs those checks normally use. --- src/uu/cat/src/cat.rs | 24 ++++- src/uu/csplit/src/csplit.rs | 2 +- src/uu/dd/src/dd.rs | 171 ++++++++++++++++++++++-------- src/uu/dd/src/progress.rs | 73 ++++++++++++- src/uu/rm/src/rm.rs | 43 +++++++- src/uu/seq/src/seq.rs | 2 +- src/uu/sort/src/sort.rs | 4 +- src/uu/split/src/platform/wasi.rs | 10 +- src/uu/tee/src/tee.rs | 1 + src/uu/tr/src/operation.rs | 5 +- src/uu/tr/src/simd.rs | 5 +- src/uu/tsort/Cargo.toml | 5 + src/uu/tsort/src/tsort.rs | 14 +-- src/uu/wc/src/wc.rs | 35 +++--- src/uu/yes/src/yes.rs | 2 +- src/uucore/src/lib/mods/io.rs | 25 ++++- 16 files changed, 337 insertions(+), 84 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 867f573e705..0e42caea7d5 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -376,7 +376,16 @@ fn cat_path(path: &OsString, options: &OutputOptions, state: &mut OutputState) - #[cfg(unix)] InputType::Socket => Err(CatError::NoSuchDeviceOrAddress), _ => { - let file = File::open(path)?; + let file = File::open(path).map_err(|e| { + let e = uucore::error::wasi_normalize_open_error(e); + match e.kind() { + ErrorKind::IsADirectory => CatError::IsDirectory, + // WASI: Check if this might be a symlink loop error (ELOOP == 32) + #[cfg(target_os = "wasi")] + _ if e.raw_os_error() == Some(32) => CatError::TooManySymlinks, + _ => CatError::from(e), + } + })?; if !is_safe_overwrite(&file, &io::stdout()) { return Err(CatError::OutputIsInput); } @@ -434,13 +443,24 @@ fn get_input_type(path: &OsString) -> CatResult { let ft = match metadata(path) { Ok(md) => md.file_type(), Err(e) => { + let e = uucore::error::wasi_normalize_open_error(e); + if e.kind() == ErrorKind::IsADirectory { + return Err(CatError::IsDirectory); + } if let Some(raw_error) = e.raw_os_error() { // On Unix-like systems, the error code for "Too many levels of symbolic links" is 40 (ELOOP). // we want to provide a proper error message in this case. - #[cfg(not(any(target_vendor = "apple", target_os = "freebsd")))] + #[cfg(not(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "wasi" + )))] let too_many_symlink_code = 40; #[cfg(any(target_vendor = "apple", target_os = "freebsd"))] let too_many_symlink_code = 62; + // wasi-libc's ELOOP is 32, unlike the 63 used by some other targets. + #[cfg(target_os = "wasi")] + let too_many_symlink_code = 32; if raw_error == too_many_symlink_code { return Err(CatError::TooManySymlinks); } diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index ff709e9ff1b..59ec7102848 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -98,7 +98,7 @@ impl Iterator for LinesWithNewlines { match self.inner.read_until(b'\n', &mut v) { Ok(0) => None, Ok(_) => Some(ret(v)), - Err(e) => Some(Err(e)), + Err(e) => Some(Err(uucore::error::wasi_normalize_open_error(e))), } } } diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index b9e759bf901..7c79bea51a4 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -19,7 +19,7 @@ use blocks::Converter; use datastructures::{ConversionMode, IConvFlags, IFlags, OConvFlags, OFlags, options}; use parseargs::Parser; use progress::ProgUpdateType; -use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater}; +use progress::{ProgUpdate, ProgressReporter, ReadStat, StatusLevel, WriteStat}; #[cfg(target_os = "linux")] use progress::{check_and_reset_sigusr1, install_sigusr1_handler}; use uucore::io::OwnedFileDescriptorOrHandle; @@ -28,22 +28,24 @@ use uucore::translate; use std::cmp; use std::env; use std::ffi::OsString; -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] use std::fs::Metadata; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; +#[cfg(any(unix, target_os = "wasi"))] +use std::os::fd::{AsRawFd, FromRawFd}; +#[cfg(unix)] +use std::os::unix::fs::FileTypeExt; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::fs::OpenOptionsExt; -#[cfg(unix)] -use std::os::unix::{ - fs::FileTypeExt, - io::{AsRawFd, FromRawFd}, -}; #[cfg(windows)] use std::os::windows::{fs::MetadataExt, io::AsHandle}; use std::path::Path; +#[cfg(not(target_os = "wasi"))] use std::sync::atomic::AtomicU8; -use std::sync::{Arc, atomic::Ordering::Relaxed, mpsc}; +#[cfg(not(target_os = "wasi"))] +use std::sync::{Arc, atomic::Ordering::Relaxed}; +#[cfg(not(target_os = "wasi"))] use std::thread; use std::time::{Duration, Instant}; @@ -51,7 +53,7 @@ use clap::{Arg, Command}; use num_integer::Integer; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] use uucore::error::{USimpleError, set_exit_code}; #[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] use uucore::show_if_err; @@ -87,14 +89,28 @@ struct Settings { /// the first caller each interval will yield true. /// /// When all instances are dropped the background thread will exit on the next interval. +#[cfg(not(target_os = "wasi"))] pub struct Alarm { trigger: Arc, } +/// A timer which triggers on a given interval. +/// +/// WASI has no thread support, so instead of a background thread waking up +/// on the interval, this polls the wall clock on each [`Alarm::get_trigger`] +/// call and fires once enough time has elapsed since the last trigger. +#[cfg(target_os = "wasi")] +pub struct Alarm { + interval: Duration, + last_trigger: std::cell::Cell, + signalled: std::cell::Cell, +} + pub const ALARM_TRIGGER_NONE: u8 = 0; pub const ALARM_TRIGGER_TIMER: u8 = 1; pub const ALARM_TRIGGER_SIGNAL: u8 = 2; +#[cfg(not(target_os = "wasi"))] impl Alarm { /// use to construct alarm timer with duration pub fn with_interval(interval: Duration) -> Self { @@ -127,6 +143,46 @@ impl Alarm { } } +#[cfg(target_os = "wasi")] +impl Alarm { + /// use to construct alarm timer with duration + pub fn with_interval(interval: Duration) -> Self { + Self { + interval, + last_trigger: std::cell::Cell::new(Instant::now()), + signalled: std::cell::Cell::new(false), + } + } + + /// Manually trigger the alarm as a signal event + pub fn manual_trigger(&self) { + self.signalled.set(true); + } + + /// Use this function to poll for any pending alarm event + /// + /// Returns `ALARM_TRIGGER_NONE` for no pending event. + /// Returns `ALARM_TRIGGER_TIMER` if the event was triggered by timer + /// Returns `ALARM_TRIGGER_SIGNAL` if the event was triggered manually + /// by the closure returned from `manual_trigger_fn` + pub fn get_trigger(&self) -> u8 { + if self.signalled.replace(false) { + return ALARM_TRIGGER_SIGNAL; + } + let now = Instant::now(); + if now.duration_since(self.last_trigger.get()) >= self.interval { + self.last_trigger.set(now); + return ALARM_TRIGGER_TIMER; + } + ALARM_TRIGGER_NONE + } + + // Getter function for the configured interval duration + pub fn get_interval(&self) -> Duration { + self.interval + } +} + /// A number in blocks or bytes /// /// Some values (seek, skip, iseek, oseek) can have values either in blocks or in bytes. @@ -213,14 +269,14 @@ impl AlignedBuf { /// fine-grained access to reading from stdin. enum Source { /// Input from stdin. - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Stdin(io::Stdin), /// Input from a file. File(File), /// Input from stdin, opened from its file descriptor. - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] StdinFile(File), /// Input from a named pipe, also known as a FIFO. @@ -236,7 +292,14 @@ impl Source { /// the [`File`] parameter. You can use this instead of /// `Source::Stdin` to allow reading from stdin without consuming /// the entire contents of stdin when this process terminates. - #[cfg(unix)] + /// + /// This also avoids `io::Stdin`'s internal read-ahead buffering, which + /// on WASI would otherwise consume bytes from the underlying + /// descriptor beyond what `dd` itself has processed (observable e.g. + /// when a caller shares the same host file descriptor across two + /// separate `dd` invocations and expects the second one to resume from + /// where the first left off). + #[cfg(any(unix, target_os = "wasi"))] fn stdin_as_file() -> Self { let fd = io::stdin().as_raw_fd(); let f = unsafe { File::from_raw_fd(fd) }; @@ -245,7 +308,7 @@ impl Source { fn skip(&mut self, n: u64, ibs: usize) -> io::Result { match self { - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Self::Stdin(stdin) => { let m = uucore::io::read_and_discard(stdin, n, ibs)?; if m < n { @@ -256,8 +319,12 @@ impl Source { } Ok(m) } - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] Self::StdinFile(f) => { + // WASI's sandbox model has no block devices, so this + // GNU-compatibility special case (skip beyond a block + // device's length) doesn't apply there. + #[cfg(unix)] if let Ok(Some(len)) = try_get_len_of_block_device(f) && len < n { @@ -335,10 +402,15 @@ impl Source { impl Read for Source { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] Self::Stdin(stdin) => stdin.read(buf), - Self::File(f) => f.read(buf), - #[cfg(unix)] + // On WASI, opening a directory via `File::open` succeeds (the + // directory check is deferred to the first read), which then + // fails with a raw EBADF instead of IsADirectory. + Self::File(f) => f + .read(buf) + .map_err(uucore::error::wasi_normalize_open_error), + #[cfg(any(unix, target_os = "wasi"))] Self::StdinFile(f) => f.read(buf), #[cfg(unix)] Self::Fifo(f) => f.read(buf), @@ -376,11 +448,11 @@ impl<'a> Input<'a> { _ => Source::Stdin(io::stdin()), } }; - #[cfg(all(not(unix), not(windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] let mut src = Source::Stdin(io::stdin()); - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] let mut src = Source::stdin_as_file(); - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] if let Source::StdinFile(f) = &src && settings.iflags.directory && !f.metadata()?.is_dir() @@ -408,6 +480,16 @@ impl<'a> Input<'a> { opts.custom_flags(libc_flags); } + // On WASI, opening an empty path resolves to the preopened + // directory itself instead of failing with `NotFound` the way + // it does on Unix, so reject it explicitly here. + #[cfg(target_os = "wasi")] + if filename.as_os_str().is_empty() { + return Err(io::Error::from_raw_os_error(libc::ENOENT).map_err_context( + || translate!("dd-error-failed-to-open", "path" => filename.quote()), + )); + } + opts.open(filename).map_err_context( || translate!("dd-error-failed-to-open", "path" => filename.quote()), )? @@ -851,6 +933,14 @@ impl<'a> Output<'a> { /// Instantiate this struct with the named file as a destination. fn new_file(filename: &Path, settings: &'a Settings) -> UResult { fn open_dst(path: &Path, cflags: &OConvFlags, oflags: &OFlags) -> Result { + // On WASI, opening an empty path resolves to the preopened + // directory itself instead of failing with `NotFound` the way + // it does on Unix, so reject it explicitly here. + #[cfg(target_os = "wasi")] + if path.as_os_str().is_empty() { + return Err(io::Error::from_raw_os_error(libc::ENOENT)); + } + let mut opts = OpenOptions::new(); opts.write(true) .create(!cflags.nocreat) @@ -1148,18 +1238,15 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // the input and output block sizes. let bsize = calc_bsize(i.settings.ibs, o.settings.obs); - // Start a thread that reports transfer progress. + // Set up progress reporting. // // The `dd` program reports its progress after every block is written, // at most every 1 second, and only if `status=progress` is given on - // the command-line or a SIGUSR1 signal is received. We - // perform this reporting in a new thread so as not to take - // any CPU time away from the actual reading and writing of - // data. We send a `ProgUpdate` from the transmitter `prog_tx` - // to the receives `rx`, and the receiver prints the transfer - // information. - let (prog_tx, rx) = mpsc::channel(); - let output_thread = thread::spawn(gen_prog_updater(rx, i.settings.status)); + // the command-line or a SIGUSR1 signal is received. On platforms with + // thread support, this reporting happens in its own thread so as not + // to take any CPU time away from the actual reading and writing of + // data; on WASI, which has no threads, updates are processed inline. + let reporter = ProgressReporter::spawn(i.settings.status); // Whether to truncate the output file after all blocks have been written. let truncate = !o.settings.oconv.notrunc; @@ -1178,8 +1265,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { rstat, wstat, start, - &prog_tx, - output_thread, + reporter, truncate, ); } @@ -1311,7 +1397,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { }; rstat.records_truncated = conv_rstat.records_truncated; let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), tp); - prog_tx.send(prog_update).unwrap_or(()); + reporter.send(prog_update); } wstat += conv_wstat; @@ -1327,14 +1413,12 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { if let Some(e) = copy_error { // Flushing and syncing are pointless now, but the caller still wants the statistics. let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), ProgUpdateType::Final); - prog_tx.send(prog_update).unwrap_or(()); - output_thread - .join() - .expect("Failed to join with the output thread."); + reporter.send(prog_update); + reporter.finish(); return Err(e); } - finalize(o, rstat, wstat, start, &prog_tx, output_thread, truncate) + finalize(o, rstat, wstat, start, reporter, truncate) } /// Write `data` through `converter`, which may count truncated records in `rstat`. @@ -1372,13 +1456,12 @@ fn write_to<'a>( } /// Flush output, print final stats, and join with the progress thread. -fn finalize( +fn finalize( mut output: BlockWriter, rstat: ReadStat, wstat: WriteStat, start: Instant, - prog_tx: &mpsc::Sender, - output_thread: thread::JoinHandle, + reporter: ProgressReporter, truncate: bool, ) -> io::Result<()> { // Flush the output in case a partial write has been buffered but @@ -1396,11 +1479,9 @@ fn finalize( // Print the final read/write statistics. let wstat = wstat + wstat_update; let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), ProgUpdateType::Final); - prog_tx.send(prog_update).unwrap_or(()); - // Wait for the output thread to finish - output_thread - .join() - .expect("Failed to join with the output thread."); + reporter.send(prog_update); + // Wait for the output thread to finish (a no-op on WASI, which has no thread). + reporter.finish(); Ok(()) } diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index ebc11eb3010..6723e0aaec4 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -15,12 +15,14 @@ use std::io::Write; #[cfg(target_os = "linux")] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(not(target_os = "wasi"))] use std::sync::mpsc; use std::time::Duration; +#[cfg(not(target_os = "wasi"))] +use uucore::locale::setup_localization; use uucore::{ error::{UResult, set_exit_code}, format::num_format::{FloatVariant, Formatter}, - locale::setup_localization, translate, }; @@ -419,12 +421,79 @@ pub(crate) enum StatusLevel { None, } +/// Reports [`ProgUpdate`]s to stderr as `dd` runs. +/// +/// On platforms with thread support, updates are sent over a channel to a +/// dedicated reporting thread so that printing progress never takes CPU +/// time away from the actual reading and writing of data. WASI has no +/// thread support, so there updates are printed inline by the caller +/// instead. +#[cfg(not(target_os = "wasi"))] +pub(crate) struct ProgressReporter { + tx: mpsc::Sender, + thread: std::thread::JoinHandle<()>, +} + +#[cfg(not(target_os = "wasi"))] +impl ProgressReporter { + pub(crate) fn spawn(print_level: Option) -> Self { + let (tx, rx) = mpsc::channel(); + let thread = std::thread::spawn(gen_prog_updater(rx, print_level)); + Self { tx, thread } + } + + /// Send a progress update to the reporting thread. + pub(crate) fn send(&self, update: ProgUpdate) { + self.tx.send(update).unwrap_or(()); + } + + /// Wait for the reporting thread to finish processing the final update. + pub(crate) fn finish(self) { + self.thread + .join() + .expect("Failed to join with the output thread."); + } +} + +/// WASI has no thread support, so [`ProgressReporter::send`] processes each +/// update inline instead of forwarding it to a background thread. +#[cfg(target_os = "wasi")] +pub(crate) struct ProgressReporter { + print_level: Option, + progress_printed: std::cell::Cell, +} + +#[cfg(target_os = "wasi")] +impl ProgressReporter { + pub(crate) fn spawn(print_level: Option) -> Self { + Self { + print_level, + progress_printed: std::cell::Cell::new(false), + } + } + + /// Process a progress update inline. + pub(crate) fn send(&self, update: ProgUpdate) { + if update.update_type == ProgUpdateType::Final { + update.print_final_stats(self.print_level, self.progress_printed.get()); + return; + } + if Some(StatusLevel::Progress) == self.print_level { + update.reprint_prog_line(); + self.progress_printed.set(true); + } + } + + /// No background thread to wait for on WASI. + pub(crate) fn finish(self) {} +} + /// Return a closure that can be used in its own thread to print progress info. /// /// This function returns a closure that receives [`ProgUpdate`] /// instances sent through `rx`. When a [`ProgUpdate`] instance is /// received, the transfer statistics are re-printed to stderr. -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "wasi")))] pub(crate) fn gen_prog_updater( rx: mpsc::Receiver, print_level: Option, diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index bb7bea531d0..ca0a04e2130 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -104,8 +104,17 @@ fn report_verbose_write_error(result: UResult<()>) { /// Helper function to show error with context and return error status fn show_removal_error(error: io::Error, path: &Path) -> bool { - let e = error.map_err_context(|| translate!("rm-error-cannot-remove", "file" => path.quote())); - show_error!("{e}"); + let error = uucore::error::wasi_normalize_open_error(error); + if error.kind() == io::ErrorKind::IsADirectory { + show_error!( + "{}", + RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string()) + ); + } else { + let e = + error.map_err_context(|| translate!("rm-error-cannot-remove", "file" => path.quote())); + show_error!("{e}"); + } true } @@ -634,11 +643,17 @@ fn is_readable_metadata(metadata: &Metadata) -> bool { } /// Whether the given file or directory is readable. -#[cfg(any(not(unix), target_os = "redox"))] +#[cfg(all(any(not(unix), target_os = "redox"), not(target_os = "wasi")))] fn is_readable(_path: &Path) -> bool { true } +/// WASI has no `PermissionsExt`, so probe readability by opening the directory. +#[cfg(target_os = "wasi")] +fn is_readable(path: &Path) -> bool { + fs::read_dir(path).is_ok() +} + #[cfg(unix)] fn is_writable_metadata(metadata: &Metadata) -> bool { let mode = metadata.permissions().mode(); @@ -1071,9 +1086,27 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata } } +#[cfg(target_os = "wasi")] +fn handle_writable_directory(path: &Path, options: &Options, _metadata: &Metadata) -> bool { + let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal(); + + // Try to read the directory to check if it's accessible + let is_accessible = fs::read_dir(path).is_ok(); + + match (stdin_ok, is_accessible, options.interactive) { + (false, _, InteractiveMode::PromptProtected) => true, + (false, false, InteractiveMode::Never) => true, + (_, false, _) => prompt_yes!( + "attempt removal of inaccessible directory {}?", + path.quote() + ), + (_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()), + (_, _, _) => true, + } +} + // I have this here for completeness but it will always return "remove directory {}" because metadata.permissions().readonly() only works for file not directories -#[cfg(not(windows))] -#[cfg(not(unix))] +#[cfg(not(any(windows, unix, target_os = "wasi")))] fn handle_writable_directory(path: &Path, options: &Options, _metadata: &Metadata) -> bool { if options.interactive == InteractiveMode::Always { prompt_yes!("remove directory {}?", path.quote()) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index e661b4f7a28..ce77053c657 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -225,7 +225,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { match result { Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => { + Err(err) if uucore::error::wasi_is_broken_pipe(&err) => { // GNU seq prints the Broken pipe message but still exits with status 0 // unless SIGPIPE was explicitly ignored, in which case it should fail. let err = err.map_err_context(|| "write error".into()); diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 3159a1020ce..5eb997cd6bd 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2216,7 +2216,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for (line_num, line_res) in buf_reader.split(b'\0').enumerate() { let line = line_res.map_err(|error| SortError::ReadFailed { path: files0_from.clone(), - error, + error: uucore::error::wasi_normalize_read_error(error), })?; if line.as_slice() == STDIN_FILE.as_bytes() { return Err(SortError::MinusInStdIn.into()); @@ -3384,7 +3384,7 @@ fn open_with_open_failed_error(path: impl AsRef) -> UResult Ok(Box::new(f) as Box), Err(error) => Err(SortError::OpenFailed { path: path.to_owned(), - error, + error: uucore::error::wasi_normalize_open_error(error), } .into()), } diff --git a/src/uu/split/src/platform/wasi.rs b/src/uu/split/src/platform/wasi.rs index acfff8bed53..edf1f6352f8 100644 --- a/src/uu/split/src/platform/wasi.rs +++ b/src/uu/split/src/platform/wasi.rs @@ -29,16 +29,22 @@ pub fn instantiate_current_writer( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); } + let open_file_error = |e: std::io::Error| { + let e = uucore::error::strip_errno(&e); + std::io::Error::other(format!("{}: {e}", filename.quote())) + }; let file = if is_new { std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) - .open(std::path::Path::new(filename))? + .open(std::path::Path::new(filename)) + .map_err(open_file_error)? } else { std::fs::OpenOptions::new() .append(true) - .open(std::path::Path::new(filename))? + .open(std::path::Path::new(filename)) + .map_err(open_file_error)? }; Ok(Writer::File(file)) } diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 278bc1d3f94..9e2d87f46d1 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -214,6 +214,7 @@ impl MultiWriter { Ok(slice) => self.write_flush(slice)?, Err(e) if e.kind() == ErrorKind::Interrupted => {} Err(e) => { + let e = uucore::error::wasi_normalize_read_error(e); show_error!( "{}", translate!("tee-error-stdin", "error" => strip_errno(&e)) diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 22fd8904719..6916d306b62 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -947,7 +947,10 @@ where Ok(0) => break, // EOF reached Ok(len) => len, Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, - Err(e) => return Err(e.map_err_context(|| translate!("tr-error-read-error"))), + Err(e) => { + let e = uucore::error::wasi_normalize_read_error(e); + return Err(e.map_err_context(|| translate!("tr-error-read-error"))); + } }; // Process the buffer and collect translated chars to output diff --git a/src/uu/tr/src/simd.rs b/src/uu/tr/src/simd.rs index 4af7760a415..55f2662cf9e 100644 --- a/src/uu/tr/src/simd.rs +++ b/src/uu/tr/src/simd.rs @@ -75,7 +75,10 @@ where Ok(0) => break, Ok(len) => len, Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, - Err(e) => return Err(e.map_err_context(|| translate!("tr-error-read-error"))), + Err(e) => { + let e = uucore::error::wasi_normalize_read_error(e); + return Err(e.map_err_context(|| translate!("tr-error-read-error"))); + } }; output_buf.clear(); diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index fa31eccccb2..7a9f43c4f26 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -32,6 +32,11 @@ windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] } [target.'cfg(any(unix, target_os = "wasi"))'.dependencies] rustix = { workspace = true, features = ["fs"] } +[target.'cfg(target_os = "wasi")'.dependencies] +# rustix's `std` feature is off at the workspace level; on wasip2 nothing else +# in the graph turns it on, so enable it here for the `AsFd` bound on `fadvise`. +rustix = { workspace = true, features = ["std"] } + [[bin]] name = "tsort" path = "src/main.rs" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index fd48eb0b3bd..83c0ba0551a 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -58,17 +58,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { process_input(io::stdin().lock(), &mut g)?; } else { let mut options: OpenOptions; - // some platforms cannot catch this as read error. Needs additional cost by stat + // Windows and WASI cannot catch opening a directory as a read error, so + // detect it up front with a stat; WASI lacks the Windows-only sequential + // scan hint, so only Windows sets the custom flag. + #[cfg(any(windows, target_os = "wasi"))] + if std::path::Path::new(input).is_dir() { + return Err(Error::Read(ReadError::IsDir(input.to_string_lossy().to_string())).into()); + } #[cfg(windows)] { use std::os::windows::fs::OpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_SEQUENTIAL_SCAN; - let input = std::path::Path::new(input); - if input.is_dir() { - return Err( - Error::Read(ReadError::IsDir(input.to_string_lossy().to_string())).into(), - ); - } // advise the OS we will access the data sequentially if possible (windows) options = File::options() .custom_flags(FILE_FLAG_SEQUENTIAL_SCAN) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index d9ce60b099b..4ef620842b1 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -701,7 +701,10 @@ fn word_count_from_input(input: &Input<'_>, settings: &Settings) -> CountResult }; match maybe_err { None => CountResult::Success(total), - Some(err) => CountResult::Interrupted(total, err), + Some(err) => { + let err = uucore::error::wasi_normalize_open_error(err); + CountResult::Interrupted(total, err) + } } } @@ -775,16 +778,19 @@ fn files0_iter_stdin<'a>() -> impl Iterator> { fn files0_iter_file<'a>(path: &Path) -> UResult>> { match File::open(path) { Ok(f) => Ok(files0_iter(f, path.into())), - Err(e) => Err(e.map_err_context(|| { - translate!("wc-error-cannot-open-for-reading", - "path" => quoting_style::locale_aware_escape_name( - path.as_os_str(), - QuotingStyle::SHELL_ESCAPE_QUOTE, + Err(e) => { + let e = uucore::error::wasi_normalize_open_error(e); + Err(e.map_err_context(|| { + translate!("wc-error-cannot-open-for-reading", + "path" => quoting_style::locale_aware_escape_name( + path.as_os_str(), + QuotingStyle::SHELL_ESCAPE_QUOTE, + ) + .into_string() + .expect("All escaped names with the escaping option return valid strings.") ) - .into_string() - .expect("All escaped names with the escaping option return valid strings.") - ) - })), + })) + } } } @@ -813,9 +819,12 @@ fn files0_iter<'a>( Ok(Input::Path(PathBuf::from(s).into())) } } - Err(e) => Err(e.map_err_context( - || translate!("wc-error-read-error", "path" => escape_name_wrapper(&err_path)), - ) as Box), + Err(e) => { + let e = uucore::error::wasi_normalize_read_error(e); + Err(e.map_err_context( + || translate!("wc-error-read-error", "path" => escape_name_wrapper(&err_path)), + ) as Box) + } }), ); // Loop until there is an error; yield that error and then nothing else. diff --git a/src/uu/yes/src/yes.rs b/src/uu/yes/src/yes.rs index 581867341fd..ed6c88a6fd9 100644 --- a/src/uu/yes/src/yes.rs +++ b/src/uu/yes/src/yes.rs @@ -33,7 +33,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(()) => Ok(()), // On Windows and WASI, silently handle broken pipe since there's no SIGPIPE #[cfg(any(windows, target_os = "wasi"))] - Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()), + Err(err) if uucore::error::wasi_is_broken_pipe(&err) => Ok(()), Err(err) => Err(USimpleError::new( 1, translate!("yes-error-standard-output", "error" => strip_errno(&err)), diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index f63949a44d4..47a282eb988 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -84,13 +84,36 @@ impl OwnedFileDescriptorOrHandle { /// conversion from borrowed native type /// /// e.g. `std::io::stdout()`, `std::fs::File`, ... - #[cfg(not(windows))] + #[cfg(not(any(windows, target_os = "wasi")))] pub fn from(t: T) -> io::Result { Ok(Self { fx: t.as_fd().try_clone_to_owned()?, }) } + /// conversion from borrowed native type + /// + /// e.g. `std::io::stdout()`, `std::fs::File`, ... + /// + /// `BorrowedFd::try_clone_to_owned` (`fcntl(F_DUPFD_CLOEXEC)`) is + /// unsupported on WASI (confirmed on both wasip1 and wasip2 under + /// wasmtime, for any file descriptor, not just stdio), so take + /// ownership of the raw descriptor directly instead of duplicating it, + /// the same way `dd`'s own `Source::stdin_as_file` already does for + /// stdin. `t` is forgotten rather than dropped after extracting its raw + /// descriptor: for `io::Stdin`/`io::Stdout`/`io::Stderr` this is a no-op + /// since Rust 1.61 (they don't own fd 0/1/2 in the first place), and for + /// an owned `File` it prevents a double-close of the fd we just took + /// ownership of. + #[cfg(target_os = "wasi")] + #[allow(clippy::unnecessary_wraps)] + pub fn from(t: T) -> io::Result { + use std::os::fd::{AsRawFd, FromRawFd}; + let fx = unsafe { OwnedFd::from_raw_fd(t.as_fd().as_raw_fd()) }; + std::mem::forget(t); + Ok(Self { fx }) + } + /// instantiates a corresponding `File` pub fn into_file(self) -> File { File::from(self.fx) From d023dfbee1d199fabaaf896efe4de18d1d8c0b5c Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:17:34 +0100 Subject: [PATCH 3/7] uucore: for fs use real inode dev stat on wasi FileInformation previously fell back to a std::fs::Metadata-based heuristic (comparing file type and size) on WASI, since it lacked access to nix::sys::stat. Switch it to rustix::fs::Stat, the same backing type used on Unix, giving WASI real inode/device identity, nlink, and equality checks instead of an approximation. Also add WASI implementations of are_hardlinks_to_same_file and are_hardlinks_or_one_way_symlink_to_same_file via rustix::fs::stat/ lstat, since std::os::unix::fs::MetadataExt is unavailable there. --- src/uucore/src/lib/features/fs.rs | 43 +++++++++++++++++++++--- src/uucore/src/lib/features/fsext/mod.rs | 6 +++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index ee4feab59d8..f9ceac77cfc 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -148,7 +148,19 @@ impl FileInformation { pub fn inode(&self) -> u64 { #[cfg(target_os = "haiku")] return self.0.st_ino.try_into().unwrap(); - #[cfg(not(target_os = "haiku"))] + #[cfg(all( + not(any(target_os = "haiku", target_os = "netbsd", target_os = "wasi")), + target_pointer_width = "64" + ))] + return self.0.st_ino; + #[cfg(all( + not(target_os = "haiku"), + any( + target_os = "netbsd", + target_os = "wasi", + not(target_pointer_width = "64") + ) + ))] #[allow(clippy::useless_conversion)] return self.0.st_ino.into(); } @@ -770,8 +782,8 @@ pub fn is_symlink_loop(path: &Path) -> bool { true } -#[cfg(not(unix))] -// Hard link comparison is not supported on non-Unix platforms +#[cfg(not(any(unix, target_os = "wasi")))] +// Hard link comparison is not supported on non-Unix, non-WASI platforms pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool { false } @@ -800,7 +812,20 @@ pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() } -#[cfg(not(unix))] +// `std::os::unix::fs::MetadataExt` is unavailable on WASI (`std::os::wasi` is +// nightly-only). `rustix::fs::stat` exposes the same st_ino/st_dev fields and +// works on stable for both wasip1 and wasip2. +#[cfg(target_os = "wasi")] +pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { + let (Ok(source_stat), Ok(target_stat)) = (rustix::fs::lstat(source), rustix::fs::lstat(target)) + else { + return false; + }; + + source_stat.st_ino == target_stat.st_ino && source_stat.st_dev == target_stat.st_dev +} + +#[cfg(not(any(unix, target_os = "wasi")))] pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool { false } @@ -829,6 +854,16 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() } +#[cfg(target_os = "wasi")] +pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool { + let (Ok(source_stat), Ok(target_stat)) = (rustix::fs::stat(source), rustix::fs::lstat(target)) + else { + return false; + }; + + source_stat.st_ino == target_stat.st_ino && source_stat.st_dev == target_stat.st_dev +} + /// Returns true if the passed `path` ends with a path terminator. /// /// This function examines the last character of the path to determine diff --git a/src/uucore/src/lib/features/fsext/mod.rs b/src/uucore/src/lib/features/fsext/mod.rs index 8348b90f3a2..f223d0bd709 100644 --- a/src/uucore/src/lib/features/fsext/mod.rs +++ b/src/uucore/src/lib/features/fsext/mod.rs @@ -139,7 +139,11 @@ fn metadata_get_change_time(md: &Metadata) -> Option { #[cfg(not(unix))] fn metadata_get_change_time(_md: &Metadata) -> Option { - // Not available. + // Not available: `std::fs::Metadata` has no ctime accessor without + // `std::os::unix::fs::MetadataExt` (unix-only) or `std::os::wasi::fs::MetadataExt` + // (WASI, but nightly-only). Getting ctime on WASI would require get stat of + // the path directly via `rustix::fs::stat` instead of going through + // `Metadata`, which isn't available at this call site. None } From 87a4e6c23a8d8849864fe15facad6b869ddf5a6e Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:30:20 +0100 Subject: [PATCH 4/7] fix(wasi): filesystem syscall gaps in cp, touch, date, sort Several per-util operations relied on APIs unavailable or nightly-only on WASI: cp now treats ENOSYS like EOPNOTSUPP when skipping optional attribute preservation (WASI has no chmod/chown at all), reads timestamps via Metadata::accessed/modified instead of the filetime crate (which panics there), creates symlinks via rustix::fs::symlink, and only errors out of --reflink when explicitly requested rather than whenever it isn't Never. touch applies the same filetime-avoidance and uses rustix::fs::utimensat for symlink timestamps. date implements get_clock_resolution and set_system_datetime for WASI (the latter reporting "not supported", since the sandbox has no wall-clock-set syscall). sort's ordering check runs single-threaded on WASI, which has no thread support, and its external-merge path inserts a missing separator between concatenated files so lines don't merge across file boundaries. --- src/uu/cp/src/cp.rs | 3 + src/uu/date/Cargo.toml | 3 + src/uu/date/locales/en-US.ftl | 1 + src/uu/date/locales/fr-FR.ftl | 1 + src/uu/date/src/date.rs | 29 ++++++- src/uu/sort/src/check.rs | 129 +++++++++++++++++++++++++++--- src/uu/sort/src/ext_sort/wasi.rs | 7 ++ src/uu/touch/Cargo.toml | 6 ++ src/uu/touch/src/platform/mod.rs | 2 +- src/uu/touch/src/platform/wasi.rs | 12 +-- src/uu/touch/src/touch.rs | 57 ++++++++----- 11 files changed, 211 insertions(+), 39 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 639babccb9c..e5def0cabc9 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2112,6 +2112,9 @@ fn symlink_file( ) })?; } + // `std::os::unix::fs::symlink` is unavailable on WASI (`std::os::wasi` is + // nightly-only), so route symlink creation through the platform module, + // which uses `rustix::fs::symlink` (stable for both wasip1 and wasip2). #[cfg(target_os = "wasi")] { platform::create_symlink(source, dest).map_err(|e| { diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index d6b73322dc1..4984fca9aef 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -42,6 +42,9 @@ rustix = { workspace = true, features = ["time"] } # no environment injection). jiff-tzdb = { workspace = true } +[target.'cfg(target_os = "wasi")'.dependencies] +libc = { workspace = true } + [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ "Win32_Foundation", diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index a86e862aa1e..33e16eef679 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -101,6 +101,7 @@ date-error-invalid-format = invalid format '{$format}' ({$error}) date-error-expected-file-got-directory = expected file, got directory {$path} date-error-date-overflow = date overflow '{$date}' date-error-setting-date-not-supported-redox = setting the date is not supported by Redox +date-error-setting-date-not-supported-wasi = setting the date is not supported by WASI date-error-cannot-set-date = cannot set date date-error-extra-operand = extra operand '{$operand}' date-error-write = write error: {$error} diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 03aebbb089f..2398691565c 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -96,6 +96,7 @@ date-error-invalid-format = format invalide '{$format}' ({$error}) date-error-expected-file-got-directory = fichier attendu, répertoire obtenu {$path} date-error-date-overflow = débordement de date '{$date}' date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox +date-error-setting-date-not-supported-wasi = la définition de la date n'est pas prise en charge par WASI date-error-cannot-set-date = impossible de définir la date date-error-extra-operand = opérande supplémentaire '{$operand}' date-error-write = erreur d'écriture: {$error} diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 7ae5aa87a8a..5e7b47f2fd2 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -106,6 +106,9 @@ enum DateError { #[cfg(target_os = "redox")] #[error("{}", translate!("date-error-setting-date-not-supported-redox"))] SettingDateNotSupportedRedox, + #[cfg(target_os = "wasi")] + #[error("{}", translate!("date-error-setting-date-not-supported-wasi"))] + SettingDateNotSupportedWasi, } impl UError for DateError {} @@ -1350,7 +1353,23 @@ fn parse_date>( } } -#[cfg(not(any(unix, windows)))] +#[cfg(target_os = "wasi")] +/// Returns the resolution of the system's realtime clock. +/// +/// `rustix::time::clock_getres` excludes WASI, so call `libc::clock_getres` +/// (available on WASI) directly instead. +fn get_clock_resolution() -> Timestamp { + let timespec = unsafe { + let mut timespec: libc::timespec = std::mem::zeroed(); + libc::clock_getres(libc::CLOCK_REALTIME, &raw mut timespec); + timespec + }; + + #[allow(clippy::unnecessary_cast, reason = "needed for 32 bit target")] + Timestamp::constant(timespec.tv_sec as _, timespec.tv_nsec as _) +} + +#[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_clock_resolution() -> Timestamp { unimplemented!("getting clock resolution not implemented (unsupported target)"); } @@ -1389,7 +1408,7 @@ fn get_clock_resolution() -> Timestamp { Timestamp::constant(0, 100) } -#[cfg(not(any(unix, windows)))] +#[cfg(not(any(unix, windows, target_os = "wasi")))] fn set_system_datetime(_date: Zoned) -> UResult<()> { unimplemented!("setting date not implemented (unsupported target)"); } @@ -1403,6 +1422,12 @@ fn convert_for_set(date: Zoned, utc: bool) -> Zoned { } } +#[cfg(target_os = "wasi")] +/// The WASI sandbox has no syscall for setting the wall clock. +fn set_system_datetime(_date: Zoned) -> UResult<()> { + Err(Box::new(DateError::SettingDateNotSupportedWasi)) +} + #[cfg(target_os = "redox")] fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(Box::new(DateError::SettingDateNotSupportedRedox)) diff --git a/src/uu/sort/src/check.rs b/src/uu/sort/src/check.rs index a826bc75507..f161426bd79 100644 --- a/src/uu/sort/src/check.rs +++ b/src/uu/sort/src/check.rs @@ -11,9 +11,9 @@ use crate::{ compare_by, open, }; use itertools::Itertools; +use std::{cmp::Ordering, ffi::OsStr}; +#[cfg(not(target_os = "wasi"))] use std::{ - cmp::Ordering, - ffi::OsStr, io::Read, iter, sync::mpsc::{Receiver, SyncSender, sync_channel}, @@ -21,11 +21,78 @@ use std::{ }; use uucore::error::UResult; +fn buffer_size(settings: &GlobalSettings) -> usize { + if settings.buffer_size < 100 * 1024 { + // when the buffer size is smaller than 100KiB we choose it instead of the default. + // this improves testability. + settings.buffer_size + } else { + 100 * 1024 + } +} + +/// Given the chunks of a file (in order), find the first pair of adjacent +/// lines that violates the requested ordering and report it as a +/// [`SortError::Disorder`]. +#[cfg(target_os = "wasi")] +fn check_chunks( + path: &OsStr, + settings: &GlobalSettings, + max_allowed_cmp: Ordering, + chunks: impl Iterator, +) -> UResult<()> { + let mut prev_chunk: Option = None; + let mut line_idx = 0; + for chunk in chunks { + line_idx += 1; + if let Some(prev_chunk) = &prev_chunk { + // Check if the first element of the new chunk is greater than the last + // element from the previous chunk + let prev_last = prev_chunk.lines().last().unwrap(); + let new_first = chunk.lines().first().unwrap(); + + if compare_by( + prev_last, + new_first, + settings, + prev_chunk.line_data(), + chunk.line_data(), + ) > max_allowed_cmp + { + return Err(SortError::Disorder { + file: path.to_owned(), + line_number: line_idx, + line: String::from_utf8_lossy(new_first.line).into_owned(), + silent: settings.check_silent, + } + .into()); + } + } + + for (a, b) in chunk.lines().iter().tuple_windows() { + line_idx += 1; + if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) > max_allowed_cmp { + return Err(SortError::Disorder { + file: path.to_owned(), + line_number: line_idx, + line: String::from_utf8_lossy(b.line).into_owned(), + silent: settings.check_silent, + } + .into()); + } + } + + prev_chunk = Some(chunk); + } + Ok(()) +} + /// Check if the file at `path` is ordered. /// /// # Returns /// /// The code we should exit with. +#[cfg(not(target_os = "wasi"))] pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { let max_allowed_cmp = if settings.unique { // If `unique` is enabled, the previous line must compare _less_ to the next one. @@ -42,13 +109,7 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { move || reader(file, &recycled_receiver, &loaded_sender, &settings) }); for _ in 0..2 { - let _ = recycled_sender.send(RecycledChunk::new(if settings.buffer_size < 100 * 1024 { - // when the buffer size is smaller than 100KiB we choose it instead of the default. - // this improves testability. - settings.buffer_size - } else { - 100 * 1024 - })); + let _ = recycled_sender.send(RecycledChunk::new(buffer_size(settings))); } let mut prev_chunk: Option = None; @@ -114,7 +175,27 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { result } +/// Check if the file at `path` is ordered. +/// +/// WASI has no thread support, so this reads every chunk up front on the +/// current thread instead of streaming them from a background reader thread. +/// +/// # Returns +/// +/// The code we should exit with. +#[cfg(target_os = "wasi")] +pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { + let max_allowed_cmp = if settings.unique { + Ordering::Less + } else { + Ordering::Equal + }; + let chunks = read_all_chunks(path, settings)?; + check_chunks(path, settings, max_allowed_cmp, chunks.into_iter()) +} + /// The function running on the reader thread. +#[cfg(not(target_os = "wasi"))] fn reader( mut file: Box, receiver: &Receiver, @@ -139,3 +220,33 @@ fn reader( } Ok(()) } + +/// Read every chunk of `path` up front, without any recycling or background +/// thread. Used on WASI, which has no thread support. +#[cfg(target_os = "wasi")] +fn read_all_chunks(path: &OsStr, settings: &GlobalSettings) -> UResult> { + let mut file = open(path)?; + let mut carry_over = vec![]; + let mut chunks = Vec::new(); + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + loop { + let recycled = RecycledChunk::new(buffer_size(settings)); + let should_continue = chunks::read( + &sender, + recycled, + None, + &mut carry_over, + &mut file, + &mut std::iter::empty(), + settings.line_ending.into(), + settings, + )?; + while let Ok(chunk) = receiver.try_recv() { + chunks.push(chunk); + } + if !should_continue { + break; + } + } + Ok(chunks) +} diff --git a/src/uu/sort/src/ext_sort/wasi.rs b/src/uu/sort/src/ext_sort/wasi.rs index fb0577324d5..1379052dafe 100644 --- a/src/uu/sort/src/ext_sort/wasi.rs +++ b/src/uu/sort/src/ext_sort/wasi.rs @@ -32,6 +32,13 @@ pub fn ext_sort( // moderately sized inputs; very large files may cause OOM. let mut input = Vec::new(); for file in files { + // Insert the separator between files whose preceding content doesn't + // already end with one; otherwise the last line of one file would + // merge with the first line of the next, e.g. "a\nb" + "b" -> + // "a\nbb" instead of "a\nb" + '\n' + "b". + if !input.is_empty() && input.last() != Some(&separator) { + input.push(separator); + } file?.read_to_end(&mut input)?; } diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index cc7685f5384..6942914b134 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -35,6 +35,12 @@ libc = { workspace = true } [target.'cfg(any(unix, target_os = "wasi"))'.dependencies] rustix = { workspace = true, features = ["fs"] } +[target.'cfg(target_os = "wasi")'.dependencies] +libc = { workspace = true } +# rustix's `std` feature is off at the workspace level; on wasip2 nothing else +# in the graph turns it on, so enable it here for `Arg`/`io::Error` conversions. +rustix = { workspace = true, features = ["std"] } + [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", diff --git a/src/uu/touch/src/platform/mod.rs b/src/uu/touch/src/platform/mod.rs index 3a062f6a025..5f0007a34c4 100644 --- a/src/uu/touch/src/platform/mod.rs +++ b/src/uu/touch/src/platform/mod.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. #[cfg(target_os = "wasi")] -pub use self::wasi::{pathbuf_from_stdout, set_file_times, set_symlink_file_times}; +pub use self::wasi::{pathbuf_from_stdout, set_symlink_file_times}; #[cfg(windows)] pub use self::windows::pathbuf_from_stdout; diff --git a/src/uu/touch/src/platform/wasi.rs b/src/uu/touch/src/platform/wasi.rs index 4dc9dfc697d..e0ba098956c 100644 --- a/src/uu/touch/src/platform/wasi.rs +++ b/src/uu/touch/src/platform/wasi.rs @@ -14,16 +14,12 @@ use uucore::translate; use crate::error::TouchError; -/// WASI replacement for `filetime::set_file_times`. +/// WASI replacement for `filetime::set_symlink_file_times`. /// /// The `filetime` crate has an unimplemented stub on `wasm32-wasi`. WASI -/// supports setting both atime and mtime via `utimensat`, which we reach -/// through `rustix`. -pub fn set_file_times(path: &Path, atime: FileTime, mtime: FileTime) -> Result<()> { - set_times(path, atime, mtime, AtFlags::empty()) -} - -/// WASI replacement for `filetime::set_symlink_file_times`. +/// supports setting times via `utimensat`, which we reach through `rustix`. +/// The non-symlink path goes through `set_times_by_path` in the main module, +/// which adds a mode-0 retry, so only the symlink variant is needed here. pub fn set_symlink_file_times(path: &Path, atime: FileTime, mtime: FileTime) -> Result<()> { set_times(path, atime, mtime, AtFlags::SYMLINK_NOFOLLOW) } diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 2cc9bc875b8..d5142152955 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -22,7 +22,7 @@ use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan, Zoned}; #[cfg(unix)] use libc::O_NONBLOCK; -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] use rustix::fs::Timestamps; #[cfg(unix)] use rustix::fs::futimens; @@ -47,7 +47,7 @@ use crate::error::TouchError; #[cfg(not(unix))] use crate::platform::pathbuf_from_stdout; #[cfg(target_os = "wasi")] -use crate::platform::{set_file_times, set_symlink_file_times}; +use crate::platform::set_symlink_file_times; /// Options contains all the possible behaviors and flags for touch. /// @@ -602,6 +602,10 @@ fn update_times( // sets the file access and modification times for a file or a symbolic link. // The filename, access time (atime), and modification time (mtime) are provided as inputs. + // On WASI, `filetime::set_symlink_file_times` always fails (the crate has + // no WASI-specific backend), so `set_symlink_file_times` resolves to the + // `platform::wasi` implementation, which uses `rustix::fs::utimensat` with + // `AT_SYMLINK_NOFOLLOW`. if opts.no_deref && !is_stdout { return set_symlink_file_times(path, atime, mtime).map_err_context( || translate!("touch-error-setting-times-of-path", "path" => path.quote()), @@ -636,7 +640,12 @@ fn update_times( set_times_by_path(path, atime, mtime) } - #[cfg(not(unix))] + #[cfg(target_os = "wasi")] + { + set_times_by_path(path, atime, mtime) + } + + #[cfg(not(any(unix, target_os = "wasi")))] { set_file_times(path, atime, mtime).map_err_context( || translate!("touch-error-setting-times-of-path", "path" => path.quote()), @@ -646,7 +655,7 @@ fn update_times( /// Build a rustix `Timestamps` from the access and modification `FileTime`s, /// preserving the `UTIME_NOW`/`UTIME_OMIT` sentinels in the nanoseconds field. -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] fn build_timestamps(atime: FileTime, mtime: FileTime) -> Timestamps { Timestamps { last_access: rustix::fs::Timespec { @@ -660,11 +669,12 @@ fn build_timestamps(atime: FileTime, mtime: FileTime) -> Timestamps { } } -/// Set file times by path using `utimensat`, following symlinks. +#[cfg(all(any(unix, target_os = "wasi"), not(target_os = "redox")))] +/// Set file times by path using `utimensat`. /// -/// This never opens the file, so it does not block on special files such as -/// FIFOs. -#[cfg(all(unix, not(target_os = "redox")))] +/// This never opens the file on Unix, avoiding blocks on FIFOs. On WASI, if +/// `utimensat` fails on a non-symlink (e.g. unopenable mode 0 files), it retries +/// with `SYMLINK_NOFOLLOW`. fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<()> { let timestamps = build_timestamps(atime, mtime); rustix::fs::utimensat( @@ -673,6 +683,18 @@ fn set_times_by_path(path: &Path, atime: FileTime, mtime: FileTime) -> UResult<( ×tamps, rustix::fs::AtFlags::empty(), ) + .or_else(|err| { + if cfg!(target_os = "wasi") && !path.is_symlink() { + rustix::fs::utimensat( + rustix::fs::CWD, + path, + ×tamps, + rustix::fs::AtFlags::SYMLINK_NOFOLLOW, + ) + } else { + Err(err) + } + }) .map_err(|e| Error::from_raw_os_error(e.raw_os_error())) .map_err_context(|| translate!("touch-error-setting-times-of-path", "path" => path.quote())) } @@ -732,18 +754,15 @@ fn stat(path: &Path, follow: bool) -> std::io::Result<(FileTime, FileTime)> { fs::symlink_metadata(path)? }; - // `FileTime::from_last_{access,modification}_time` is unimplemented on - // `wasm32-wasi`, so go through `Metadata::{accessed, modified}` (which - // return `SystemTime`) and convert via `FileTime::from_system_time`. + // `filetime::FileTime::from_last_{access,modification}_time` panics on + // WASI (the `filetime` crate has no WASI-specific backend and falls back + // to its unimplemented generic wasm one). `Metadata::accessed`/`modified` + // are stable and WASI-backed, so use those instead there. #[cfg(target_os = "wasi")] - { - let atime = metadata.accessed()?; - let mtime = metadata.modified()?; - Ok(( - FileTime::from_system_time(atime), - FileTime::from_system_time(mtime), - )) - } + return Ok(( + FileTime::from(metadata.accessed()?), + FileTime::from(metadata.modified()?), + )); #[cfg(not(target_os = "wasi"))] Ok(( FileTime::from_last_access_time(&metadata), From 1c78e5f182d9fcba0813bcdbdb4981d1dc50982b Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:48:16 +0100 Subject: [PATCH 5/7] test: add wasi coverage and ci wiring Adds and retargets integration tests across most utilities to verify the WASI-specific behavior introduced in prior commits (error codes, symlink/hardlink handling, cp attribute preservation, sort ordering, touch timestamps, etc.), and tags tests that can't run under the WASI sandbox with #[cfg_attr(wasi_runner/wasip2_runner, ignore = "..")] (host paths, real threads, chmod/chown, and similar gaps). The uutests harness's symlink helpers now build relative targets under the WASI runner instead of absolute host paths, and gain a UUTESTS_WASM_RUNNER_ARGS env var. wasi.yml now runs the wasip2 integration suite (previously wasip1-only) with the wasip2_exit_with_code feature and wasmtime's cli-exit-with-code flag, and expands the tested utility list as WASI coverage grows. docs/src/wasi-test-gaps.md is expanded to catalog every ignore reason. mktemp: fix temp dir panic in wasi chore: fix --- .github/workflows/wasi.yml | 37 ++-- .../workspace.wordlist.txt | 6 + Cargo.toml | 1 + docs/src/wasi-test-gaps.md | 170 ++++++++++++++++-- src/uu/mktemp/src/mktemp.rs | 3 +- tests/by-util/test_b2sum.rs | 12 +- tests/by-util/test_cat.rs | 9 +- tests/by-util/test_cksum.rs | 64 ++++--- tests/by-util/test_comm.rs | 1 + tests/by-util/test_cp.rs | 10 ++ tests/by-util/test_csplit.rs | 13 ++ tests/by-util/test_cut.rs | 3 +- tests/by-util/test_date.rs | 71 +++++++- tests/by-util/test_dd.rs | 18 ++ tests/by-util/test_dir.rs | 1 + tests/by-util/test_dircolors.rs | 1 + tests/by-util/test_factor.rs | 1 + tests/by-util/test_false.rs | 1 + tests/by-util/test_fmt.rs | 1 + tests/by-util/test_fold.rs | 3 +- tests/by-util/test_head.rs | 3 +- tests/by-util/test_join.rs | 4 + tests/by-util/test_ls.rs | 118 +++++++++++- tests/by-util/test_md5sum.rs | 50 +++--- tests/by-util/test_mkdir.rs | 36 ++++ tests/by-util/test_mv.rs | 31 ++++ tests/by-util/test_nproc.rs | 4 +- tests/by-util/test_od.rs | 1 + tests/by-util/test_pathchk.rs | 5 + tests/by-util/test_pr.rs | 13 ++ tests/by-util/test_printenv.rs | 10 ++ tests/by-util/test_ptx.rs | 1 + tests/by-util/test_pwd.rs | 52 ++++++ tests/by-util/test_readlink.rs | 21 +++ tests/by-util/test_realpath.rs | 29 +++ tests/by-util/test_rm.rs | 28 ++- tests/by-util/test_rmdir.rs | 10 +- tests/by-util/test_seq.rs | 12 +- tests/by-util/test_sha1sum.rs | 4 +- tests/by-util/test_shred.rs | 17 ++ tests/by-util/test_sleep.rs | 2 + tests/by-util/test_sort.rs | 86 ++++++++- tests/by-util/test_split.rs | 48 +++++ tests/by-util/test_tail.rs | 14 ++ tests/by-util/test_touch.rs | 12 +- tests/by-util/test_tr.rs | 3 +- tests/by-util/test_true.rs | 1 + tests/by-util/test_tsort.rs | 1 + tests/by-util/test_tty.rs | 4 + tests/by-util/test_uname.rs | 10 +- tests/by-util/test_uniq.rs | 2 + tests/by-util/test_wc.rs | 43 ++--- tests/uutests/Cargo.toml | 1 + tests/uutests/src/lib/util.rs | 29 ++- 54 files changed, 992 insertions(+), 139 deletions(-) diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index b6fa143e1ee..6f05f55f4c1 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -25,7 +25,7 @@ jobs: matrix: job: - { target: wasm32-wasip1, rust-flags: "--cfg wasi_runner" } - - { target: wasm32-wasip2, rust-flags: "--cfg wasi_runner --cfg wasip2_runner" } + - { target: wasm32-wasip2, rust-flags: "--cfg wasi_runner --cfg wasip2_runner" } steps: - uses: actions/checkout@v7.0.1 with: @@ -48,34 +48,33 @@ jobs: UTILS=$(./util/show-utils.sh | tr ' ' '\n' | grep -vE "^($EXCLUDE)$" | sed 's/^/-p uu_/' | tr '\n' ' ') cargo test --target ${{ matrix.job.target }} --no-default-features $UTILS - name: Run integration tests via wasmtime - if: matrix.job.target == 'wasm32-wasip1' env: RUSTFLAGS: ${{ matrix.job.rust-flags }} run: | # Build the WASI binary - cargo build --target ${{ matrix.job.target }} --no-default-features --features feat_wasm + cargo build --target ${{ matrix.job.target }} --no-default-features --features feat_wasm,wasip2_exit_with_code # Run host-compiled integration tests against the WASI binary. # Tests incompatible with WASI are annotated with # #[cfg_attr(wasi_runner, ignore)] in the test source files. # The binary above is built without default features, so it has no # caret diagnostics; the host test crate does have them, hence the # not(wasi_runner) in the cfg guarding every `mod diagnostics`. - # TODO: add integration tests for these tools as WASI support is extended: - # arch b2sum cksum csplit date dir dircolors fmt join - # ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink - # realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum - # sha512sum shred sleep sort split tsort uname uniq vdir UUTESTS_BINARY_PATH="$(pwd)/target/${{ matrix.job.target }}/debug/coreutils.wasm" \ UUTESTS_WASM_RUNNER=wasmtime \ + UUTESTS_WASM_RUNNER_ARGS="-S cli-exit-with-code=y" \ cargo test --test tests -- \ - test_base32:: test_base64:: test_basenc:: test_basename:: \ - test_cat:: test_comm:: test_cp:: test_cut:: test_dirname:: test_echo:: \ - test_expand:: test_expr:: test_factor:: test_false:: test_fold:: \ - test_head:: test_link:: test_ln:: \ - test_ls::test_ls_a_dotdot_no_error_on_wasi \ - test_ls::test_ls_al_no_capabilities_insufficient_on_wasi \ - test_mktemp:: test_nl:: test_numfmt:: \ - test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \ - test_tail:: test_tee:: test_test:: test_touch:: test_tr:: \ - test_true:: test_truncate:: \ - test_unexpand:: test_unlink:: test_wc:: test_yes:: + test_arch:: test_b2sum:: test_base32:: test_base64:: test_basename:: \ + test_basenc:: test_cat:: test_cksum:: test_comm:: test_cp:: \ + test_csplit:: test_cut:: test_date:: test_dd:: test_dir:: \ + test_dircolors:: test_dirname:: test_echo:: test_expand:: test_expr:: \ + test_factor:: test_false:: test_fmt:: test_fold:: test_head:: \ + test_join:: test_link:: test_ln:: test_ls:: test_md5sum:: test_mkdir:: \ + test_mktemp:: test_mv:: test_nl:: test_nproc:: test_numfmt:: test_od:: \ + test_paste:: test_pathchk:: test_pr:: test_printenv:: test_printf:: \ + test_ptx:: test_pwd:: test_readlink:: test_realpath:: test_rm:: \ + test_rmdir:: test_seq:: test_sha1sum:: test_sha224sum:: \ + test_sha256sum:: test_sha384sum:: test_sha512sum:: test_shred:: \ + test_shuf:: test_sleep:: test_sort:: test_split:: test_sum:: \ + test_tail:: test_tee:: test_test:: test_touch:: test_tr:: test_true:: \ + test_truncate:: test_tsort:: test_tty:: test_uname:: test_unexpand:: \ + test_uniq:: test_unlink:: test_vdir:: test_wc:: test_yes:: diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 971e99afcdb..c3169f5b9b3 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -122,11 +122,13 @@ ENODATA ENOENT ENOSPC ENOSYS +ENOTDIR ENOTEMPTY EOPNOTSUPP EPERM EPIPE EROFS +ETXTBSY # * vars/fcntl F_GETFL @@ -181,8 +183,10 @@ addrlen blocksize canonname chroot +cmdline dlsym execvp +fchmod fdatasync freeaddrinfo getaddrinfo @@ -202,6 +206,7 @@ inodes isatty lchown pathlen +reflink setgid setgroups settime @@ -216,6 +221,7 @@ strerror strlen syncfs umask +unflushed waitpid # * vars/nix diff --git a/Cargo.toml b/Cargo.toml index d2521e2a426..35217f4266c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,6 +176,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(pgo_training)', 'cfg(target_os, values("cygwin"))', 'cfg(wasi_runner)', + 'cfg(wasip2_runner)', ] } unused_qualifications = "warn" diff --git a/docs/src/wasi-test-gaps.md b/docs/src/wasi-test-gaps.md index 4789ef03dc9..0da51af4a84 100644 --- a/docs/src/wasi-test-gaps.md +++ b/docs/src/wasi-test-gaps.md @@ -1,37 +1,183 @@ # WASI integration test gaps -Tests annotated with `#[cfg_attr(wasi_runner, ignore = "...")]` are skipped when running integration tests against a WASI binary via wasmtime. This document tracks the reasons so that gaps in WASI support are visible in one place. +Tests annotated with `#[cfg_attr(wasi_runner, ignore = "...")]` or `#[cfg_attr(wasip2_runner, ignore = "...")]` are skipped when running integration tests against a WASI binary via wasmtime. This document tracks the reasons so that gaps in WASI support are visible in one place. -To find all annotated tests: `grep -rn 'wasi_runner, ignore' tests/` +To find all annotated tests: `grep -rn 'wasi_runner, ignore\|wasip2_runner, ignore' tests/` -## Tools not yet covered by integration tests - -arch, b2sum, cat, cksum, cp, csplit, date, dir, dircolors, fmt, join, ls, md5sum, mkdir, mv, nproc, pathchk, pr, printenv, ptx, pwd, readlink, realpath, rm, rmdir, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, sleep, sort, split, tail, touch, tsort, uname, uniq, vdir, yes +To find the tests for a specific reason: `grep -rn '' tests/` ## WASI sandbox: host paths not visible -The WASI guest only sees directories explicitly mapped with `--dir`. Host paths like `/proc`, `/sys`, and `/dev` are not accessible. Affected tests include those that read `/proc/version`, `/proc/modules`, `/proc/cpuinfo`, `/proc/self/mem`, `/sys/kernel/profiling`, `/dev/null`, `/dev/zero`, `/dev/full`, and tests that rely on anonymous pipes or Linux-specific I/O error paths. +The WASI guest only sees directories explicitly mapped with `--dir`. Host paths outside those mappings are not accessible, so any test that reads or writes an absolute host path fails. This is the single largest gap and covers many sub-cases: + +- Generic unmapped absolute paths ("WASI sandbox: host paths not visible", "WASI sandbox: absolute host paths not visible", "WASI sandbox: cross-scenario absolute host path not visible"). +- `/dev` special files: `/dev/null`, `/dev/zero`, `/dev/random`, `/dev/console`, and `/dev` generally. +- `/proc` and `/sys`: `/proc/version`, `/proc/modules`, `/proc/cpuinfo`, `/proc/self/mem`, `/proc/1/cmdline`, `/sys/kernel/profiling`, and `/proc`/`/sys` generally. +- Locale and timezone databases, which live under host paths the guest never sees ("WASI sandbox: locale database not visible", "WASI sandbox: timezone database not visible", "WASI sandbox: timezone/locale database not visible", including the `--time-style=locale` and "host locale check passes but the wasm guest can't use it" variants). +- Path identity/display mismatches that stem from the same root cause — the guest's view of the filesystem is a virtual root, not the host's real one: `pwd`/`getcwd` report the guest's virtual root instead of the host absolute path ("WASI sandbox: pwd reports the guest's virtual root..."), canonicalized paths resolve to the virtual root instead of the host path ("WASI sandbox: canonicalized path resolves to the guest's virtual root..."), `current_directory_resolved` used to build expected hyperlink URIs differs from the host path, `/` inside the guest is its own writable root rather than the real filesystem root, and UNC-style paths resolve differently inside the guest root than on the host. ## WASI: argv/filenames must be valid UTF-8 -The WASI specification requires that argv entries and filenames are valid UTF-8. Tests that pass non-UTF-8 bytes as arguments or create files with non-UTF-8 names cannot run under WASI. +The WASI specification requires that argv entries, environment values, and filenames are valid UTF-8. Tests that pass non-UTF-8 bytes as arguments, environment values, or create files with non-UTF-8 names cannot run under WASI ("WASI: argv/filenames must be valid UTF-8", "WASI: argv must be valid UTF-8", "WASI: env values must be valid UTF-8", "WASI: preopened directories reject non-UTF-8 filenames", "WASI: non-utf8 arguments cannot be passed through the spawned test harness", "WASI preview2: OsString requires valid UTF-8, unlike unix/wasip1"). ## WASI: no FIFO/mkfifo support -WASI does not support creating or opening FIFOs (named pipes). Tests that use `mkfifo` are skipped. +WASI does not support creating or opening FIFOs (named pipes). Tests that use `mkfifo`, classify files via `FileTypeExt::is_fifo()`, or read from a FIFO (which surfaces as `EINVAL`/"Invalid seek" instead of blocking) are skipped. ## WASI: no pipe/signal support -WASI does not support Unix signals or pipe creation. Tests that rely on `SIGPIPE`, broken pipe detection, or pipe-based I/O are skipped. +WASI does not support Unix signals or pipe creation. Tests that rely on `SIGPIPE`, `SIGINT`, broken pipe detection, or pipe-based I/O are skipped ("WASI: no pipe/signal support", "WASI: no signal support", "WASI: no signal support (SIGINT)"). ## WASI: no subprocess spawning -WASI does not support spawning child processes. Tests that shell out to other commands or invoke a second binary are skipped. +WASI does not support spawning child processes. Tests that shell out to other commands, invoke a second binary, or rely on `--filter`'s process-spawning support are skipped ("WASI: no subprocess spawning", "WASI: --filter has no process-spawning support"). + +## WASI: no stdout-to-file redirection + +Tests that redirect a subprocess's stdout directly to a file outside the test harness's own plumbing are skipped; the wasmtime runner does not support this redirection path. + +## WASI: sparse/reflink/ACL copy-on-write features not supported + +`cp`'s sparse-file detection, reflink/copy-on-write (`--reflink`), and ACL preservation rely on Linux-specific filesystem features that wasmtime's virtualized filesystem does not implement. + +## WASI: follow mode (-f) is not supported on this platform + +`tail -f` and related follow-mode behavior depend on OS-level file-change notification that is not available to a WASI guest. + +## WASI: st_mode has no real permission bits + +WASI's `stat` only reports file type, not real Unix permission bits, so `st_mode` is a placeholder. Tests that assert on permission bits, mode-dependent coloring, or mode-dependent output are skipped ("WASI: st_mode has no real permission bits", "...only file-type; ls -l shows placeholder rwx", "...color/mode-dependent output differs"). + +## WASI: chmod/umask have no real effect in the guest sandbox + +`chmod` has no ENOSYS-free syscall in the WASI guest, so tests that expect `chmod` to restore write permission or make a directory read-only are skipped. Similarly `umask()` only affects the wasmtime host process, not the guest sandbox, so tests asserting on umask-influenced output are skipped. + +## WASI: sort -m spawns real OS threads for multi-file merge + +`sort -m`'s multi-file merge path spawns real OS threads, which is unsupported under wasmtime's default configuration. This also affects `--compress-program`, since `ext_sort` falls back to a single-threaded in-memory path that bypasses the external compress program entirely. + +## WASI: File::try_clone() is unsupported + +`shuf --random-source` (and similar) rely on `File::try_clone()`, which is unsupported under the WASI runtime. + +## WASI: read_link on absolute paths fails under wasmtime via spawned test harness + +`fs::read_link` on an absolute path inside the sandbox (e.g. `/file2`) returns `EPERM` when the WASI binary is launched through `std::process::Command` from the test harness, even though the same call works when wasmtime is invoked directly. This breaks `uucore::fs::canonicalize` for symlink sources, so tests that rely on following a symlink to compute a relative path are skipped ("WASI: read_link on absolute paths fails...", "WASI: read_link() not supported for symlinks"). ## WASI: stdin file position not preserved through wasmtime When stdin is a seekable file, wasmtime does not preserve the file position between the host and guest. Tests that validate stdin offset behavior after `head` reads are skipped. -## WASI: read_link on absolute paths fails under wasmtime via spawned test harness +## WASI: inode/ctime/atime metadata gaps + +Several `stat`-adjacent metadata fields are unreliable or unavailable under WASI: inode display needs a `rustix::fs::stat`-based path and is currently gated to unix only; `ctime` is unavailable via `std::fs::Metadata` on stable; access/change time tracking granularity does not match the host filesystem's; and `rustix::fs::stat` doesn't return stable inode identity across path lookups under wasmtime ("WASI preview2: rustix::fs::stat doesn't return stable inode identity..."). + +## WASI: utimensat rejects negative (pre-1970) timestamps + +Setting a file's timestamp to before the Unix epoch fails with `EINVAL` under WASI's `utimensat`, unlike native Unix. + +## WASI: setting the system clock is not supported at all + +Unlike native Unix, where changing the system clock is merely permission-gated, WASI does not support setting the system clock at all. + +## WASI: direct file descriptor manipulation not supported + +Tests that manipulate file descriptors directly (e.g. reusing a descriptor across `dup`-like operations to alias stdin/stdout to the same file) rely on primitives not supported under WASI. + +## WASI: no /dev/fd/0 support + +Without `/dev/fd/0`, a redirected-directory stdin hits the generic pipe error path (like macOS) instead of the regular-file path with the GNU-matching error message. + +## WASI: killing the wasmtime process discards the unflushed output buffer + +When a test kills the wasmtime process to check partial output, any unflushed output buffer is discarded, so streamed bytes never reach stdout the way they would with a natively killed process. + +## WASI: resource limits not supported + +Tests that use `rlimit` to constrain resources (file descriptors, address space) don't observe the expected behavior under wasmtime, since it doesn't enforce host-style resource limits inside the guest. This includes address-space-limit regression tests, for which "the WASI runner target is not suitable." + +## WASI Preview2: exit with code requires an opt-in feature + +`std::process::exit` on `wasm32-wasip2` goes through the *stable* `wasi:cli/exit#exit` +function, which only carries a success/failure bit, so every nonzero exit code +collapses to `1`. + +The [wasi:cli/exit#exit-with-code](https://github.com/WebAssembly/WASI/blob/a1fc383d01eabaf3fac01de03c0ab1a01bfdd099/proposals/cli/wit/exit.wit#L16) +function propagates the real exit code, but it is marked `@unstable` in the +WIT definition. `uucore`'s `wasip2-exit-with-code` Cargo feature (off by +default) switches `uucore::error::process_exit` to call it via the `wasip2` +crate. Because the function is unstable, any WASI host must explicitly opt +in or the process traps instead of exiting (e.g. wasmtime requires +`-S cli-exit-with-code=y`). CI builds with this feature enabled and passes +that flag, so integration tests see the real exit code. + +## WASI Preview2: OS error message text/mapping differs from native Unix + +Some I/O error paths produce a different underlying OS error, or a +different-but-equivalent error text, under WASI Preview2 than on native +Unix, even though the exit code is the same. Examples: reading a directory +as a file surfaces as `Bad file descriptor` instead of `Is a directory`; +`stat` on a dangling symlink with a trailing slash returns `ENOTDIR` instead +of `ENOENT`; `stat` on a trailing-slash path over a regular file surfaces a +raw `ENOTDIR` from the runtime instead of going through the +`CannotStatNotADirectory` path. Tests that assert on the exact error text or +error code for these paths are skipped under `wasip2_runner`/`wasi_runner`. + +## WASI P2: /dev/full filesystem not available + +`/dev/full` (a device that always reports "No space left on device" on +write) is a Linux/FreeBSD/NetBSD-specific device node not present in the +WASI Preview2 sandbox. Tests that pipe output to `/dev/full` to exercise a +write-failure path are skipped. + +## Harness/environment mismatches (not WASI capability gaps) + +A handful of skipped tests aren't blocked by a WASI capability at all — they're blocked by how the test harness or CI environment invokes the binary: + +- Tests that invoke the binary directly via a shell script or `std::process::Command`, bypassing the wasmtime runner entirely, can't run against the WASI binary as-is. +- A test that sets `HOME` to a relative path breaks wasmtime's own cache-dir resolution. +- A test that asserts on macOS-specific error text can't pass when the wasm guest under test isn't macOS, even though the test binary itself runs on macOS. +- `ls`'s open-fd-leak regression test hits wasmtime's own `--dir` sandbox fd/depth limit before reaching the 30-level depth the test is trying to probe, unrelated to whether `ls` itself leaks descriptors. +- `sort`'s buffer-size test with `u64::MAX`-sized arguments overflows differently on WASI because the wasm guest is always a 32-bit target regardless of the host's pointer width. + +## WASI: the `same-file` crate has no WASI backend + +`cp --link` on a symlinked directory needs to detect whether source and +destination are the same file. That check goes through the `same-file` +crate, which has no `wasm32-wasip1`/`wasm32-wasip2` implementation and always +returns "same-file is not supported on this platform" on that target. + +## Needs investigation + +A number of `cp`, `ls`, and `mv` tests were originally bulk-tagged with the +placeholder `ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)"` +pending a closer look. Most have since been triaged: 59 of them actually pass +under wasmtime (their `ignore` attribute has been removed) once the earlier +placeholder was replaced with real testing. Search for `needs investigation` +in `tests/by-util/test_cp.rs`, `tests/by-util/test_ls.rs`, and +`tests/by-util/test_mv.rs` to find the ones still pending. + +The tests still carrying the placeholder fall into a few buckets, none fixable +with a small coreutils-side change: -`fs::read_link` on an absolute path inside the sandbox (e.g. `/file2`) returns `EPERM` when the WASI binary is launched through `std::process::Command` from the test harness, even though the same call works when wasmtime is invoked directly. This breaks `uucore::fs::canonicalize` for symlink sources, so tests that rely on following a symlink to compute a relative path are skipped. +- **chmod/permission-mode preservation**: `cp --preserve=mode`, `-p`, `-a`, + and friends hit `ENOSYS` ("Function not implemented") when they try to + `chmod` under WASI, since there is no working `fchmod`/`chmod` syscall in + the guest sandbox. +- **File type gaps**: char devices, sockets, and other special files can't be + `stat`ed or copied (`cp: cannot stat`, `cp: ... Not supported`) because the + WASI guest doesn't expose real device/socket types. +- **`filetime` has no WASI backend**: preserving timestamps through symlinks + (`set_symlink_file_times`) hits the crate's `wasm.rs` stub, which + unconditionally returns "Wasm not implemented". +- **Debug/reflink text mismatches**: `cp --debug`'s `copy offload`/`reflink`/ + `sparse detection` fields, and the `--reflink` error message, are written + assuming a `target_os = "linux"`/`"macos"` host and don't have a WASI-specific + branch, so the text differs from what the test expects. +- **Virtual-root-relative absolute paths**: a few tests build an absolute + path via `root_dir_resolved()` or copy `.` into a sibling directory + reached through `..`; both hit the same "WASI sandbox: pwd reports the + guest's virtual root, not the host's absolute path" limitation described + above, just via a different code path (`cp: cannot stat` on the + synthesized absolute path, or `cp: cannot copy a directory ... into + itself` on the `..`-relative destination). diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 9b7b5ca96da..90974be6eda 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -142,8 +142,9 @@ impl Options { } else if matches.get_flag(OPT_T) || matches.contains_id(OPT_TMPDIR) { // If --tmpdir is given without an argument, or -t is given // export in TMPDIR - #[cfg(target_os = "wasi")] // WASI's `std::env::temp_dir()` unconditionally panics + // (`library/std/src/sys/paths/wasi.rs`). + #[cfg(target_os = "wasi")] let default_tmp_dir = env::var_os(TMPDIR_ENV_VAR) .map_or_else(|| PathBuf::from(FALLBACK_TMPDIR), PathBuf::from); #[cfg(not(target_os = "wasi"))] diff --git a/tests/by-util/test_b2sum.rs b/tests/by-util/test_b2sum.rs index eea837fd630..8d523c371f7 100644 --- a/tests/by-util/test_b2sum.rs +++ b/tests/by-util/test_b2sum.rs @@ -139,7 +139,7 @@ fn test_check_b2sum_length_option_0() { .ccmd("b2sum") .arg("--length=0") .arg("-c") - .arg(at.subdir.join("testf.b2sum")) + .arg("testf.b2sum") .succeeds() .stdout_only("testf: OK\n"); } @@ -172,7 +172,7 @@ fn test_check_b2sum_length_option_8() { .ccmd("b2sum") .arg("--length=8") .arg("-c") - .arg(at.subdir.join("testf.b2sum")) + .arg("testf.b2sum") .succeeds() .stdout_only("testf: OK\n"); } @@ -200,7 +200,7 @@ fn test_invalid_b2sum_length_option_not_multiple_of_8() { scene .ccmd("b2sum") .arg("--length=9") - .arg(at.subdir.join("testf")) + .arg("testf") .fails_with_code(1) .stderr_contains("b2sum: invalid length: '9'") .stderr_contains("b2sum: length is not a multiple of 8"); @@ -220,7 +220,7 @@ fn test_invalid_b2sum_length_option_too_large(#[case] len: &str) { .ccmd("b2sum") .arg("--length") .arg(len) - .arg(at.subdir.join("testf")) + .arg("testf") .fails_with_code(1) .no_stdout() .stderr_contains(format!("b2sum: invalid length: '{len}'")) @@ -303,7 +303,7 @@ fn test_check_b2sum_strict_check() { scene .ccmd("b2sum") .arg("-c") - .arg(at.subdir.join("ck")) + .arg("ck") .succeeds() .stdout_only(&output); @@ -311,7 +311,7 @@ fn test_check_b2sum_strict_check() { .ccmd("b2sum") .arg("--strict") .arg("-c") - .arg(at.subdir.join("ck")) + .arg("ck") .succeeds() .stdout_only(&output); } diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index 402bb1a39c9..cffdd20b3d5 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -203,6 +203,7 @@ fn test_piped_to_dev_null() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_piped_to_dev_full() { for append in [true, false] { let s = TestScenario::new(util_name!()); @@ -612,10 +613,15 @@ fn test_domain_socket() { s.ucmd() .args(&[socket_path]) .fails() - .stderr_contains("No such device or address"); + .stderr_contains(if cfg!(wasi_runner) { + "No such file or directory" + } else { + "No such device or address" + }); } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_write_to_self_empty() { // it's ok if the input file is also the output file if it's empty let s = TestScenario::new(util_name!()); @@ -887,6 +893,7 @@ fn test_write_error_handling() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_version_help_dev_full() { use std::fs::OpenOptions; diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 1178a58a5a0..054e15bcb19 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -987,6 +987,10 @@ fn test_check_error_incorrect_format() { #[cfg(unix)] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_dev_null() { let scene = TestScenario::new(util_name!()); @@ -1048,7 +1052,7 @@ fn test_reset_binary() { .arg("--tag") .arg("--untagged") .arg("--algorithm=md5") - .arg(at.subdir.join("f")) + .arg("f") .succeeds() .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *"); } @@ -1067,7 +1071,7 @@ fn test_reset_binary_but_set() { .arg("--untagged") .arg("--binary") .arg("--algorithm=md5") - .arg(at.subdir.join("f")) + .arg("f") .succeeds() .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *"); } @@ -1083,7 +1087,7 @@ mod output_format { ucmd.arg("--text") .arg("--tag") .args(&["-a", "md5"]) - .arg(at.subdir.join("f")) + .arg("f") .fails(); } @@ -1095,7 +1099,7 @@ mod output_format { // --text without --untagged fails ucmd.arg("--text") .args(&["-a", "md5"]) - .arg(at.subdir.join("f")) + .arg("f") .fails_with_code(1) .stderr_contains("--text mode is only supported with --untagged"); } @@ -1109,7 +1113,7 @@ mod output_format { ucmd.arg("--text") .arg("--binary") .args(&["-a", "md5"]) - .arg(at.subdir.join("f")) + .arg("f") .succeeds() // No --untagged, tagged output is used .stdout_contains("f) = d41d8cd98f00b204e9800998ecf8427e"); @@ -1125,7 +1129,7 @@ mod output_format { .arg("--binary") .arg("--untagged") .args(&["-a", "md5"]) - .arg(at.subdir.join("f")) + .arg("f") .succeeds() // Untagged output is used .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *"); @@ -1144,7 +1148,7 @@ fn test_binary_file() { .arg("--untagged") .arg("-b") .arg("--algorithm=md5") - .arg(at.subdir.join("f")) + .arg("f") .succeeds() .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *"); @@ -1154,7 +1158,7 @@ fn test_binary_file() { .arg("--untagged") .arg("--binary") .arg("--algorithm=md5") - .arg(at.subdir.join("f")) + .arg("f") .succeeds() .stdout_contains("d41d8cd98f00b204e9800998ecf8427e *"); @@ -1784,7 +1788,7 @@ fn test_check_directory_error() { #[cfg(windows)] let err_msg = "cksum: d: Permission denied\n"; ucmd.arg("--check") - .arg(at.subdir.join("f")) + .arg("f") .fails() .stderr_contains(err_msg); } @@ -1802,7 +1806,7 @@ fn test_check_base64_hashes() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("check")) + .arg("check") .succeeds() .stdout_is("empty: OK\nempty: OK\nempty: OK\n"); } @@ -2005,6 +2009,10 @@ mod check_encoding { // This test should pass on linux and macos. #[cfg(not(windows))] #[test] + #[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: OsString requires valid UTF-8, unlike unix/wasip1" + )] fn test_check_non_utf8_comment() { use super::*; let hashes = @@ -2020,7 +2028,7 @@ mod check_encoding { at.write_bytes("check", hashes); cmd.arg("--check") - .arg(at.subdir.join("check")) + .arg("check") .succeeds() .stdout_is("empty: OK\nempty: OK\nempty: OK\n") .no_stderr(); @@ -2030,6 +2038,10 @@ mod check_encoding { // create a file which name contains '\xff'. #[cfg(target_os = "linux")] #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI: preopened directories reject non-UTF-8 filenames" + )] fn test_check_non_utf8_filename() { use super::*; use std::{ffi::OsString, os::unix::ffi::OsStringExt}; @@ -2045,7 +2057,7 @@ mod check_encoding { scene .ucmd() .arg("--check") - .arg(at.subdir.join("check")) + .arg("check") .succeeds() .stdout_is_bytes(b"'funky'$'\\377''name': OK\n") .no_stderr(); @@ -2056,7 +2068,7 @@ mod check_encoding { scene .ucmd() .arg("--check") - .arg(at.subdir.join("check")) + .arg("check") .fails() .stdout_is_bytes(b"'funky'$'\\377''name': FAILED\n") .stderr_contains("1 computed checksum did NOT match"); @@ -2067,7 +2079,7 @@ mod check_encoding { scene .ucmd() .arg("--check") - .arg(at.subdir.join("check")) + .arg("check") .fails() .stdout_is_bytes(b"'flakey'$'\\377''name': FAILED open or read\n") .stderr_contains("1 listed file could not be read"); @@ -2075,6 +2087,10 @@ mod check_encoding { #[cfg(target_os = "linux")] #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI: preopened directories reject non-UTF-8 filenames" + )] fn test_quoting_in_stderr() { use super::*; use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; @@ -2120,7 +2136,7 @@ fn test_check_blake_length_guess() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("foo.sums")) + .arg("foo.sums") .succeeds() .stdout_is("foo.dat: OK\n"); } @@ -2134,7 +2150,7 @@ fn test_check_blake_length_guess() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("foo.sums")) + .arg("foo.sums") .fails() .stderr_contains("foo.sums: no properly formatted checksum lines found"); @@ -2145,7 +2161,7 @@ fn test_check_blake_length_guess() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("foo.sums")) + .arg("foo.sums") .fails() .stderr_contains("foo.sums: no properly formatted checksum lines found"); @@ -2156,7 +2172,7 @@ fn test_check_blake_length_guess() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("foo.sums")) + .arg("foo.sums") .fails() .stderr_contains("foo.sums: no properly formatted checksum lines found"); } @@ -2174,7 +2190,7 @@ fn test_check_confusing_base64() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("foo.sums")) + .arg("foo.sums") .succeeds() .stdout_is("foo.dat: OK\n"); } @@ -2198,14 +2214,14 @@ fn test_check_mix_hex_base64() { scene .ucmd() .arg("--check") - .arg(at.subdir.join("hex_b64")) + .arg("hex_b64") .succeeds() .stdout_only("foo2.dat: OK\nfoo1.dat: OK\n"); scene .ucmd() .arg("--check") - .arg(at.subdir.join("b64_hex")) + .arg("b64_hex") .succeeds() .stdout_only("foo1.dat: OK\nfoo2.dat: OK\n"); } @@ -2592,6 +2608,10 @@ mod cksum_check_mode { #[test] #[cfg_attr(not(unix), ignore = "/dev/null is only available on UNIX")] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" + )] fn test_untagged_base64_matching_tag() { let (at, mut ucmd) = at_and_ucmd!(); @@ -3199,6 +3219,7 @@ mod debug_flag { #[test] #[cfg(all(target_os = "linux", not(target_env = "musl")))] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_check_file_with_io_error() { // /proc/self/mem causes EIO when read without proper seeking new_ucmd!() @@ -3213,6 +3234,7 @@ fn test_check_file_with_io_error() { #[test] #[cfg(all(target_os = "linux", not(target_env = "musl")))] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_check_checkfile_with_io_error() { // /proc/self/mem causes EIO when read without proper seeking new_ucmd!() diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index f65e198d6be..3b0580618f2 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -783,6 +783,7 @@ fn test_c_locale_still_orders_by_bytes() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_comm_write_error_dev_full() { use std::fs::OpenOptions; let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 50788c0e9a0..00575922ef2 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -106,6 +106,7 @@ fn test_cp_stream_to_full() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_cp_verbose_write_error_is_reported() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("source_file"); @@ -3173,6 +3174,10 @@ fn test_cp_conflicting_update() { #[test] #[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)" +)] fn test_cp_reflink_insufficient_permission() { let (at, mut ucmd) = at_and_ucmd!(); @@ -3190,6 +3195,7 @@ fn test_cp_reflink_insufficient_permission() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_closes_file_descriptors() { use rustix::process::Resource; @@ -4317,6 +4323,10 @@ fn test_cp_overriding_arguments() { } #[test] +#[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: rustix::fs::stat doesn't return stable inode identity across path lookups (wasmtime filesystem limitation)" +)] fn test_copy_no_dereference_1() { let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("a"); diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index 9f1ae574556..3d72272fd32 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1677,6 +1677,10 @@ fn repeat_everything() { #[cfg(unix)] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: reading a FIFO returns EINVAL/'Invalid seek'" +)] fn test_named_pipe_input_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1735,6 +1739,7 @@ fn test_stdin_no_trailing_newline() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_csplit_non_utf8_paths() { use std::os::unix::ffi::OsStringExt; let (at, mut ucmd) = at_and_ucmd!(); @@ -1748,6 +1753,10 @@ fn test_csplit_non_utf8_paths() { /// Test write error detection using /dev/full #[test] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/full) not visible" +)] fn test_write_error_dev_full() { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file("/dev/full", "xx01"); @@ -1764,6 +1773,10 @@ fn test_write_error_dev_full() { /// Test write error with -k keeps files #[test] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/full) not visible" +)] fn test_write_error_dev_full_keep_files() { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file("/dev/full", "xx01"); diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 9c2c7b3c3e6..d106ed49595 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -940,8 +940,9 @@ fn test_emoji_delim() { .stdout_only("🌹\n"); } -#[cfg(target_os = "linux")] #[test] +#[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .arg("-d=") diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 3c4ea6fbd38..fa9d5ec0b4f 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -386,6 +386,10 @@ fn test_date_utc_with_d_flag() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_utc_vs_local() { let cases = [ ("-d", "2024-01-01 12:00", "+%H:%M %Z", "12:00 EST\n"), @@ -550,6 +554,10 @@ fn test_date_error_echoes_input_verbatim() { #[test] #[cfg(all(unix, not(target_os = "android")))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: setting the system clock is not supported at all, not just permission-gated" +)] fn test_date_set_permissions_error() { if !(geteuid().is_root() || uucore::os::is_wsl_1()) { let result = new_ucmd!() @@ -563,6 +571,10 @@ fn test_date_set_permissions_error() { #[test] #[cfg(all(unix, not(target_os = "android")))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: setting the system clock is not supported at all, not just permission-gated" +)] fn test_date_set_hyphen_prefixed_values() { // test -s flag accepts hyphen-prefixed values like "-3 days" if !(geteuid().is_root() || uucore::os::is_wsl_1()) { @@ -1000,7 +1012,7 @@ fn test_date_parse_from_format() { 2023-04-15 18:30:00", ); ucmd.arg("-f") - .arg(at.plus(FILE)) + .arg(FILE) .arg("+%Y-%m-%d %H:%M:%S") .succeeds(); } @@ -1028,6 +1040,10 @@ const JAN2: &str = "2024-01-02 12:00:00 +0000"; const JUL2: &str = "2024-07-02 12:00:00 +0000"; #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_tz() { fn test_tz(tz: &str, date: &str, output: &str) { println!("Test with TZ={tz}, date=\"{date}\"."); @@ -1074,6 +1090,10 @@ fn test_date_tz_with_utc_flag() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_tz_various_formats() { fn test_tz(tz: &str, date: &str, output: &str) { println!("Test with TZ={tz}, date=\"{date}\"."); @@ -1102,6 +1122,10 @@ fn test_date_tz_various_formats() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_tz_with_relative_time() { new_ucmd!() .env("TZ", "America/Vancouver") @@ -1113,6 +1137,10 @@ fn test_date_tz_with_relative_time() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_utc_time() { // Test that -u flag shows correct UTC time // We get 2 UTC times just in case we're really unlucky and this runs around @@ -1609,6 +1637,10 @@ fn test_date_whitespace_between_items() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_relative_m9() { // Military timezone "m9" should be parsed as noon + 9 hours = 21:00 UTC // When displayed in TZ=UTC+9 (which is UTC-9), this shows as 12:00 local time @@ -1906,6 +1938,10 @@ fn test_date_locale_en_us_vs_c_difference() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_locale_hu_hungarian() { // Regression test for uutils/coreutils#11240: the GNU modifier fast-path // ("%-e") used to run before ICU localization, so "%b"/"%A" came out in @@ -2197,6 +2233,10 @@ fn test_date_input_hhmm_ampm() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_input_trailing_tz_abbrev_rezones() { // `TZ=UTC+1 date -d '2024-01-01 EST'` should display the instant in UTC+1 // (GNU: 04:00:00 -01:00), not leave it in EST (the pre-fix uutils @@ -2325,6 +2365,10 @@ fn test_date_parenthesis_vs_other_special_chars() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_iranian_locale_solar_hijri_calendar() { // Test Iranian locale uses Solar Hijri calendar // Verify the Solar Hijri calendar is used in the Iranian locale @@ -2392,6 +2436,10 @@ fn test_date_iranian_locale_solar_hijri_calendar() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_ethiopian_locale_calendar() { // Test Ethiopian locale uses Ethiopian calendar // Verify the Ethiopian calendar is used in the Ethiopian locale @@ -2539,6 +2587,10 @@ fn check_date(locale: &str, date: &str, fmt: &str, expected: &str) { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_locale_calendar_conversions() { // Persian (Solar Hijri) - Nowruz is March 20/21 for (d, e) in [ @@ -2590,6 +2642,10 @@ fn test_locale_calendar_conversions() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_locale_month_names() { // %B full month names: Jan, Jun, Dec for each locale for (loc, jan, jun, dec) in [ @@ -2610,6 +2666,10 @@ fn test_locale_month_names() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_locale_abbreviated_month_names() { // %b abbreviated month names: Feb, Jun, Dec for each locale // This test ensures we don't get double periods in locales like Hungarian @@ -2633,6 +2693,10 @@ fn test_locale_abbreviated_month_names() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_locale_day_names() { // %A full day names: Mon (26th), Sun (25th), Sat (24th) Jan 2026 for (loc, mon, sun, sat) in [ @@ -2734,6 +2798,10 @@ fn test_date_month_subtraction_keeps_day() { // Tests for embedded timezone parsing #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: timezone/locale database not visible" +)] fn test_date_embedded_timezone_conversion() { // Parse date with embedded timezone // Date should be interpreted in embedded TZ, then displayed in environment TZ @@ -2750,6 +2818,7 @@ fn test_date_embedded_timezone_conversion() { // Tests for invalid UTF-8 in date string #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: argv must be valid UTF-8")] fn test_date_invalid_utf8_byte_rejected() { use std::os::unix::ffi::OsStrExt; diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index ea529fb121b..71040e9241f 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1130,6 +1130,7 @@ fn test_random_73k_test_obs_lt_not_a_multiple_ibs() { #[cfg(not(windows))] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: fifo reads surface as BrokenPipe")] fn test_random_73k_test_lazy_fullblock() { let (at, mut ucmd) = at_and_ucmd!(); at.mkfifo("fifo"); @@ -1282,6 +1283,7 @@ fn test_truncated_record() { /// Test that the output file can be `/dev/null`. #[cfg(unix)] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: no /dev/null device node")] fn test_outfile_dev_null() { new_ucmd!().arg("of=/dev/null").succeeds().no_stdout(); } @@ -1551,6 +1553,7 @@ fn test_bytes_suffix_recursive() { /// Test for "conv=sync" with a slow reader. #[cfg(not(windows))] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: fifo reads surface as BrokenPipe")] fn test_sync_delayed_reader() { let (at, mut ucmd) = at_and_ucmd!(); at.mkfifo("fifo"); @@ -1613,6 +1616,7 @@ fn test_sparse() { /// Test that a seek on an output FIFO results in a read. #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: hangs reading from a fifo opened for output")] fn test_seek_output_fifo() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -1657,6 +1661,10 @@ fn test_skip_input_fifo() { /// Test for reading part of stdin from each of two child processes. #[cfg(not(windows))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "runs the coreutils binary directly via a raw shell pipeline, bypassing the WASI runner wrapping" +)] fn test_multiple_processes_reading_stdin() { // TODO Investigate if this is possible on Windows. let printf = "printf 'abcdef\n'".to_string(); @@ -1985,6 +1993,11 @@ fn test_iflag_directory_fails_when_file_is_piped_via_std_in() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "wasmtime's stdin worker thread over-reads from a host fd backed by a regular file, \ + consuming bytes beyond what the guest actually requested (bytecodealliance/wasmtime)" +)] fn test_stdin_stdout_not_rewound_even_when_connected_to_seekable_file() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -2044,6 +2057,7 @@ fn test_wrong_number_err_msg() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: no /dev/urandom device node")] fn test_no_dropped_writes() { const BLK_SIZE: usize = 0x4000; const COUNT: usize = 1000; @@ -2204,6 +2218,10 @@ fn test_nocache_eof_fadvise_zero_length() { #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: rust-lang/libc lacks LC_* constants for the wasi target, so dd can't call setlocale there" +)] fn test_iso8859_1_case_conversion() { use std::process::Command; // Test ISO-8859-1 case conversion for accented characters diff --git a/tests/by-util/test_dir.rs b/tests/by-util/test_dir.rs index 08e519ea0e4..5f1a64aef9b 100644 --- a/tests/by-util/test_dir.rs +++ b/tests/by-util/test_dir.rs @@ -136,6 +136,7 @@ fn test_version() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_write_error() { let scene = TestScenario::new(util_name!()); scene.fixtures.touch("file"); diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index 3760124f286..ce74da38695 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -11,6 +11,7 @@ use uutests::new_ucmd; #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_dircolors_non_utf8_paths() { use std::os::unix::ffi::OsStringExt; let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_factor.rs b/tests/by-util/test_factor.rs index 8e75b30bad5..adb1ef1c60c 100644 --- a/tests/by-util/test_factor.rs +++ b/tests/by-util/test_factor.rs @@ -388,6 +388,7 @@ fn fails_on_invalid_number() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn short_circuit_write_error() { use std::fs::OpenOptions; diff --git a/tests/by-util/test_false.rs b/tests/by-util/test_false.rs index 8c672c8fa6f..cb3c3f19509 100644 --- a/tests/by-util/test_false.rs +++ b/tests/by-util/test_false.rs @@ -52,6 +52,7 @@ fn test_conflict() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_full() { for option in ["--version", "--help"] { let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index ecff20dd488..741a7b0d604 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -465,6 +465,7 @@ fn test_fmt_knuth_plass_line_breaking() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_fmt_non_utf8_paths() { use uutests::at_and_ucmd; diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index a4da15250bc..e8480810116 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -286,8 +286,9 @@ fn test_zero_width_data_line_counts() { ); } -#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] #[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_fold_reports_no_space_left_on_dev_full() { use std::fs::OpenOptions; use std::process::Stdio; diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index ca4eaae35d9..eb9577afcfc 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -926,8 +926,9 @@ fn test_all_but_last_lines() { .stdout_is_fixture("lorem_ipsum_backwards_15_lines.expected"); } -#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] #[test] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_write_to_dev_full() { use std::fs::OpenOptions; diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index 1b34d2ecc08..04005e5c796 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -571,6 +571,7 @@ fn non_line_feeds() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI: argv must be valid UTF-8")] fn non_unicode() { new_ucmd!() .arg("non-unicode_1.bin") @@ -646,6 +647,7 @@ fn null_line_endings() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_full() { let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); new_ucmd!() @@ -658,6 +660,7 @@ fn test_full() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_join_non_utf8_paths() { use std::fs::File; use std::io::Write; @@ -705,6 +708,7 @@ fn join_emoji_delim_inner_key() { #[cfg(unix)] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_locale_collation() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 6ee2b34bf20..b73ad0b437b 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -240,6 +240,7 @@ fn get_filesystem_type(scene: &TestScenario, path: &Path) -> String { #[cfg(all(feature = "truncate", feature = "dd"))] #[test] // FIXME: fix this test for FreeBSD and OpenBSD #[cfg(not(target_os = "openbsd"))] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_ls_allocation_size() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -481,6 +482,10 @@ fn test_ls_allocation_size() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_ls_devices() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1265,6 +1270,10 @@ fn test_ls_long_padding_of_size_column_with_multiple_files() { #[test] #[cfg(all(feature = "ln", feature = "mkdir", feature = "touch"))] #[allow(clippy::items_after_statements)] +#[cfg_attr( + wasi_runner, + ignore = "WASI: st_mode has no real permission bits; color/mode-dependent output differs" +)] fn test_ls_long_symlink_color() { // If you break this test after breaking mkdir, touch, or ln, do not be alarmed! // This test is made for ls, but it attempts to run those utils in the process. @@ -1522,7 +1531,7 @@ fn test_ls_long_dangling_symlink_color() { // Ensure dangling link name uses `or=` and target uses `mi=`. let name_regex = Regex::new(r"(?:\x1b\[[0-9;]*m)*\x1b\[([0-9;]*)mdir1/dangling_symlink\x1b\[0m").unwrap(); - let target_path = regex::escape(&at.plus_as_string("foo")); + let target_path = regex::escape(&format!("..{}foo", std::path::MAIN_SEPARATOR)); let target_pattern = format!(r"(?:\x1b\[[0-9;]*m)*\x1b\[([0-9;]*)m{target_path}\x1b\[0m"); let target_regex = Regex::new(&target_pattern).unwrap(); @@ -1755,6 +1764,10 @@ fn test_ls_directory_dangling_symlink_uses_ln_when_or_blank() { #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: st_mode has no real permission bits; color/mode-dependent output differs" +)] fn test_ls_long_total_size() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2232,6 +2245,10 @@ fn test_ls_order_size() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: ctime is unavailable via std::fs::Metadata on stable" +)] fn test_ls_long_ctime() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2275,6 +2292,10 @@ fn test_ls_order_birthtime() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: locale database not visible (time-style=locale)" +)] fn test_ls_time_styles() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2616,6 +2637,10 @@ fn test_ls_order_time_breaks_ties_by_name() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: access/change time tracking granularity does not match the host filesystem's" +)] fn test_ls_order_time() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2921,6 +2946,10 @@ mod quoting { // and must not escape embedded apostrophes or double quotes; in the C // locale they fall back to ASCII single/double quotes. #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: locale database not visible (host locale check passes but the wasm guest can't use it)" + )] fn test_ls_quoting_locale_utf8() { if !is_locale_available("en_US.UTF-8") { return; @@ -3396,6 +3425,7 @@ mod quoting { /// gets escaped depending on the used locale. #[cfg(not(any(target_vendor = "apple", windows, target_os = "openbsd")))] #[test] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_locale_aware_quoting() { let cases: &[(&[u8], _, _, &[&str])] = &[ ( @@ -3458,6 +3488,7 @@ mod quoting { } #[test] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_c_dot_utf8_renders_utf8() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -3553,6 +3584,10 @@ fn test_ls_color() { #[test] #[cfg(not(feature = "selinux"))] // Disabled on the SELinux runner for now +#[cfg_attr( + wasi_runner, + ignore = "WASI: inode display is gated to unix; needs a rustix::fs::stat-based path" +)] fn test_ls_inode() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -3595,6 +3630,10 @@ fn test_ls_inode() { #[test] #[cfg(not(windows))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: FileTypeExt::is_fifo() is unix-only, so FIFOs aren't classified" +)] fn test_ls_indicator_style() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -3710,7 +3749,7 @@ fn test_ls_indicator_style_symlink_target_long() { .succeeds() .stdout_contains("dir_link -> ") .stdout_does_not_contain("dir_link@ -> ") - .stdout_contains("/dir/"); + .stdout_contains(" dir/"); } #[test] @@ -5044,6 +5083,7 @@ fn test_ls_sort_extension() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: absolute host paths not visible")] fn test_ls_path() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -5086,6 +5126,10 @@ fn test_ls_path() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: inode display is gated to unix; needs a rustix::fs::stat-based path" +)] fn test_ls_dangling_symlinks() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -5624,6 +5668,10 @@ fn test_tabsize_formatting() { #[cfg(all(unix, not(target_os = "android")))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/console) not visible" +)] fn test_device_number() { use std::fs::{metadata, read_dir}; use std::os::unix::fs::{FileTypeExt, MetadataExt}; @@ -5655,6 +5703,10 @@ fn test_device_number() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI: preopened directories reject non-UTF-8 filenames" +)] fn test_invalid_utf8() { let (at, mut ucmd) = at_and_ucmd!(); @@ -5735,6 +5787,10 @@ fn test_ls_dired_quoting_style_name() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: current_directory_resolved differs from host path used in expected hyperlink URI" +)] fn test_ls_dired_hyperlink() { // we will have link but not the DIRED output // note that the order matters @@ -6397,6 +6453,10 @@ fn test_ls_cf_output_should_be_delimited_by_tab() { #[cfg(all(unix, feature = "dd"))] #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_posixly_correct_and_block_size_env_vars() { let scene = TestScenario::new(util_name!()); @@ -6451,6 +6511,10 @@ fn test_posixly_correct_and_block_size_env_vars() { #[cfg(all(unix, feature = "dd"))] #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_posixly_correct_and_block_size_env_vars_with_k() { let scene = TestScenario::new(util_name!()); @@ -6517,6 +6581,10 @@ fn test_ls_invalid_block_size() { #[cfg(all(unix, feature = "dd"))] #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_ls_invalid_block_size_in_env_var() { let scene = TestScenario::new(util_name!()); @@ -6564,6 +6632,10 @@ fn test_ls_invalid_block_size_in_env_var() { #[cfg(all(unix, feature = "dd"))] #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_ls_block_size_override() { let scene = TestScenario::new(util_name!()); @@ -6675,6 +6747,10 @@ fn test_ls_block_size_override_self() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: current_directory_resolved differs from host path used in expected hyperlink URI" +)] fn test_ls_hyperlink() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -6752,6 +6828,10 @@ fn test_ls_hyperlink_encode_link() { // spell-checker:enable #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: current_directory_resolved differs from host path used in expected hyperlink URI" +)] fn test_ls_hyperlink_dirs() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -6796,6 +6876,10 @@ fn test_ls_hyperlink_dirs() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: current_directory_resolved differs from host path used in expected hyperlink URI" +)] fn test_ls_hyperlink_recursive_dirs() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -6993,6 +7077,10 @@ fn test_term_colorterm() { #[cfg(all(unix, not(target_vendor = "apple")))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)" +)] fn test_acl_display() { use std::process::Command; @@ -7049,6 +7137,10 @@ fn test_acl_display() { // Each file with an ACL must not inflate the link-count column width. #[cfg(all(unix, not(target_vendor = "apple")))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)" +)] fn test_acl_padding_not_inflated() { use std::process::Command; @@ -7103,6 +7195,10 @@ fn test_acl_padding_not_inflated() { #[test] #[cfg(not(feature = "selinux"))] // Disabled on the SELinux runner for now +#[cfg_attr( + wasi_runner, + ignore = "WASI: st_mode has no real permission bits; color/mode-dependent output differs" +)] fn test_ls_color_norm() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -7516,6 +7612,10 @@ fn test_unknown_format_specifier() { #[cfg(all(unix, not(target_vendor = "apple")))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)" +)] fn test_acl_display_symlink() { use std::process::Command; @@ -7687,6 +7787,7 @@ fn test_ls_time_style_iso_recent_and_older() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_ls_time_style_posix_locale_override() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -8078,6 +8179,10 @@ fn test_f_flag_combined_behavior() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: st_mode has no real permission bits, only file-type; ls -l shows placeholder rwx" +)] fn test_f_with_long_format() { // Test that -f works with long format (-l) let scene = TestScenario::new(util_name!()); @@ -8101,6 +8206,7 @@ fn test_f_with_long_format() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_ls_proc_self_fd_no_errors() { // Regression test: ReadDir must stay alive until metadata() is called // to prevent "cannot access '/proc/self/fd/3'" errors. @@ -8116,6 +8222,10 @@ fn test_ls_proc_self_fd_no_errors() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "wasmtime's --dir sandbox hits its own fd/depth limit before 30 levels, unrelated to whether ls itself leaks fds" +)] fn test_ls_recursive_no_fd_leak() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -8137,6 +8247,10 @@ fn test_ls_recursive_no_fd_leak() { #[test] #[cfg(all(unix, not(target_vendor = "apple")))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: preopened directories reject non-UTF-8 filenames" +)] fn test_ls_non_utf8_hidden() { use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; let scene = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_md5sum.rs b/tests/by-util/test_md5sum.rs index 1d4d7d6d7fb..e00a8305db9 100644 --- a/tests/by-util/test_md5sum.rs +++ b/tests/by-util/test_md5sum.rs @@ -133,7 +133,7 @@ fn test_check_md5_ignore_missing() { scene .ccmd("md5sum") .arg("-c") - .arg(at.subdir.join("testf.sha1")) + .arg("testf.sha1") .fails() .stdout_contains("testf2: FAILED open or read"); @@ -141,14 +141,14 @@ fn test_check_md5_ignore_missing() { .ccmd("md5sum") .arg("-c") .arg("--ignore-missing") - .arg(at.subdir.join("testf.sha1")) + .arg("testf.sha1") .succeeds() .stdout_only("testf: OK\n"); scene .ccmd("md5sum") .arg("--ignore-missing") - .arg(at.subdir.join("testf.sha1")) + .arg("testf.sha1") .fails() .stderr_contains( "md5sum: the --ignore-missing option is meaningful only when verifying checksums", @@ -416,7 +416,7 @@ fn test_check_empty_line() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stderr_contains("WARNING: 1 line is improperly formatted"); } @@ -470,7 +470,7 @@ fn test_check_strict_error() { .ccmd("md5sum") .arg("--check") .arg("--strict") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains("WARNING: 3 lines are improperly formatted"); } @@ -489,7 +489,7 @@ fn test_check_warn() { .ccmd("md5sum") .arg("--check") .arg("--warn") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stderr_contains("in.md5: 3: improperly formatted MD5 checksum line") .stderr_contains("WARNING: 1 line is improperly formatted"); @@ -499,7 +499,7 @@ fn test_check_warn() { .ccmd("md5sum") .arg("--check") .arg("--strict") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails(); } @@ -514,7 +514,7 @@ fn test_check_status() { .ccmd("md5sum") .arg("--check") .arg("--status") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .no_output(); } @@ -530,7 +530,7 @@ fn test_check_status_code() { .ccmd("md5sum") .arg("--check") .arg("--status") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .no_output(); } @@ -557,7 +557,7 @@ fn test_sha1_with_md5sum_should_fail() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("f.sha1")) + .arg("f.sha1") .fails() .stderr_contains("f.sha1: no properly formatted checksum lines found") .stderr_does_not_contain("WARNING: 1 line is improperly formatted"); @@ -578,7 +578,7 @@ fn test_check_one_two_space_star() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stdout_is("empty: OK\n"); @@ -588,7 +588,7 @@ fn test_check_one_two_space_star() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stdout_is("'*empty': FAILED open or read\n"); @@ -597,7 +597,7 @@ fn test_check_one_two_space_star() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stdout_is("'*empty': OK\n"); } @@ -622,7 +622,7 @@ fn test_check_space_star_or_not() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stdout_contains("c: FAILED") .stdout_does_not_contain("a: FAILED") @@ -638,7 +638,7 @@ fn test_check_space_star_or_not() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stdout_contains("a: OK") .stderr_contains("WARNING: 1 line is improperly formatted"); @@ -654,7 +654,7 @@ fn test_check_no_backslash_no_space() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stdout_is("f: OK\n"); } @@ -669,7 +669,7 @@ fn test_incomplete_format() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains("no properly formatted checksum lines found"); } @@ -685,7 +685,7 @@ fn test_start_error() { .ccmd("md5sum") .arg("--check") .arg("--strict") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stdout_is("f: OK\n") .stderr_contains("WARNING: 1 line is improperly formatted"); @@ -702,7 +702,7 @@ fn test_check_check_ignore_no_file() { .ccmd("md5sum") .arg("--check") .arg("--ignore-missing") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains("in.md5: no file was verified"); } @@ -721,7 +721,7 @@ fn test_check_directory_error() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains(err_msg); } @@ -769,7 +769,7 @@ fn test_check_quiet() { .ccmd("md5sum") .arg("--quiet") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .no_output(); @@ -779,7 +779,7 @@ fn test_check_quiet() { .ccmd("md5sum") .arg("--quiet") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stdout_contains("f: FAILED") .stderr_contains("WARNING: 1 computed checksum did NOT match"); @@ -787,13 +787,13 @@ fn test_check_quiet() { scene .ccmd("md5sum") .arg("--quiet") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains("md5sum: the --quiet option is meaningful only when verifying checksums"); scene .ccmd("md5sum") .arg("--strict") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .fails() .stderr_contains("md5sum: the --strict option is meaningful only when verifying checksums"); } @@ -808,7 +808,7 @@ fn test_star_to_start() { scene .ccmd("md5sum") .arg("--check") - .arg(at.subdir.join("in.md5")) + .arg("in.md5") .succeeds() .stdout_only("f: OK\n"); } diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index c0601a850d8..f18b26a50eb 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -25,6 +25,10 @@ fn test_invalid_arg() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "spawns the binary directly via std::process::Command, bypassing the wasmtime runner" +)] fn test_version_no_path() { use std::process::Command; use uutests::get_tests_binary; @@ -147,6 +151,10 @@ fn test_mkdir_dup_dir_parent() { #[cfg(not(windows))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: umask() only affects the wasmtime host process, not the guest sandbox" +)] fn test_mkdir_parent_mode() { let (at, mut ucmd) = at_and_ucmd!(); @@ -174,6 +182,10 @@ fn test_mkdir_parent_mode() { #[cfg(not(windows))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: umask() only affects the wasmtime host process, not the guest sandbox" +)] fn test_mkdir_parent_mode_check_existing_parent() { let (at, mut ucmd) = at_and_ucmd!(); @@ -238,6 +250,7 @@ fn test_mkdir_dup_file() { #[test] #[cfg(not(windows))] +#[cfg_attr(wasi_runner, ignore = "WASI: st_mode has no real permission bits")] fn test_symbolic_mode() { let (at, mut ucmd) = at_and_ucmd!(); let test_dir = "test_dir"; @@ -249,6 +262,7 @@ fn test_symbolic_mode() { #[test] #[cfg(not(windows))] +#[cfg_attr(wasi_runner, ignore = "WASI: st_mode has no real permission bits")] fn test_symbolic_alteration() { let (at, mut ucmd) = at_and_ucmd!(); let test_dir = "test_dir"; @@ -266,6 +280,7 @@ fn test_symbolic_alteration() { #[test] #[cfg(not(windows))] +#[cfg_attr(wasi_runner, ignore = "WASI: st_mode has no real permission bits")] fn test_multi_symbolic() { let (at, mut ucmd) = at_and_ucmd!(); let test_dir = "test_dir"; @@ -423,6 +438,7 @@ fn test_mkdir_p_respects_umask_without_acl() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: st_mode has no real permission bits")] fn test_mkdir_explicit_mode_zero() { use std::os::unix::fs::PermissionsExt; let (at, mut ucmd) = at_and_ucmd!(); @@ -433,6 +449,10 @@ fn test_mkdir_explicit_mode_zero() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI: umask() only affects the wasmtime host process, not the guest sandbox" +)] fn test_mkdir_explicit_mode_with_umask() { // -m must win over umask: requesting 0o777 with a restrictive umask must // still yield 0o777, since the umask is shaped to not block requested bits. @@ -815,6 +835,10 @@ fn test_mkdir_case_sensitivity() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: UNC-style path resolves differently inside the guest root than on the host" +)] fn test_mkdir_network_paths() { // Test network path formats (UNC paths on Windows) let scene = TestScenario::new(util_name!()); @@ -878,6 +902,10 @@ fn test_mkdir_environment_expansion() { /// Now it temporarily sets umask to 0 and creates with the exact mode. #[cfg(not(windows))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: umask() only affects the wasmtime host process, not the guest sandbox" +)] fn test_mkdir_mode_ignores_umask() { // Test that -m 0700 with restrictive umask still creates 0700 { @@ -951,6 +979,10 @@ fn test_mkdir_mode_ignores_umask() { /// - Final directory uses the exact requested mode (ignoring umask) #[cfg(not(windows))] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: umask() only affects the wasmtime host process, not the guest sandbox" +)] fn test_mkdir_parent_mode_with_explicit_mode() { let (at, mut ucmd) = at_and_ucmd!(); let umask: mode_t = 0o022; @@ -1017,6 +1049,10 @@ fn test_mkdir_parent_inherits_setgid() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "spawns the binary directly via std::process::Command, bypassing the wasmtime runner" +)] fn test_mkdir_concurrent_creation() { // Test concurrent mkdir -p operations: 10 iterations, 8 threads, 40 levels nesting use std::thread; diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index e2c3f534685..45cc2bb63a6 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -654,6 +654,7 @@ fn test_mv_symlink_into_target() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_broken_symlink_to_another_fs() { use tempfile::TempDir; @@ -777,6 +778,10 @@ fn test_mv_backup_simple_guard_allows_unrelated_source() { /// backup rename cannot destroy it. GNU allows this. #[test] #[cfg(all(unix, not(target_os = "android")))] +#[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: rustix::fs::stat doesn't return stable inode identity across path lookups (wasmtime filesystem limitation)" +)] fn test_mv_backup_simple_guard_allows_symlink_source() { let (at, mut ucmd) = at_and_ucmd!(); at.write("test_mv_sym_a", "DSTDATA"); @@ -1690,6 +1695,10 @@ fn test_mv_verbose() { #[test] #[cfg(any(target_os = "linux", target_os = "android"))] // mkdir does not support -m on windows. Freebsd doesn't return a permission error either. #[cfg(feature = "mkdir")] +#[cfg_attr( + wasi_runner, + ignore = "WASI: needs investigation (chmod/interactive/device/mode gaps)" +)] fn test_mv_permission_error() { let scene = TestScenario::new("mkdir"); let folder1 = "bar"; @@ -1906,6 +1915,10 @@ fn test_mv_seen_multiple_files_to_directory() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: stat on a trailing-slash path over a regular file surfaces a raw ENOTDIR from the runtime instead of the CannotStatNotADirectory path" +)] fn test_mv_dir_into_file_where_both_are_files() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2103,6 +2116,7 @@ mod inter_partition_copying { // Ensure that the copying code used in an inter-partition move unlinks the destination symlink. #[test] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_unlinks_dest_symlink() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -2152,6 +2166,7 @@ mod inter_partition_copying { // In an inter-partition move if unlinking the destination symlink fails, ensure // that it would output the proper error message. #[test] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_unlinks_dest_symlink_error_message() { use uutests::util::TestScenario; let scene = TestScenario::new(util_name!()); @@ -2184,6 +2199,7 @@ mod inter_partition_copying { // Test that hardlinks are preserved when moving files across partitions #[test] #[cfg(unix)] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_preserves_hardlinks_across_partitions() { use std::fs; use std::os::unix::fs::MetadataExt; @@ -2260,6 +2276,7 @@ mod inter_partition_copying { #[cfg(unix)] #[allow(clippy::too_many_lines)] #[allow(clippy::similar_names)] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_preserves_multiple_hardlink_groups_across_partitions() { use std::fs::metadata; use std::os::unix::fs::MetadataExt; @@ -2373,6 +2390,7 @@ mod inter_partition_copying { // Test hardlinks within directories being moved #[test] #[cfg(unix)] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_preserves_hardlinks_in_directories_across_partitions() { use std::fs::metadata; use std::os::unix::fs::MetadataExt; @@ -2475,6 +2493,7 @@ mod inter_partition_copying { #[cfg(unix)] #[allow(clippy::too_many_lines)] #[allow(clippy::similar_names)] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_preserves_complex_hardlinks_across_nested_directories() { use std::fs::metadata; use std::os::unix::fs::MetadataExt; @@ -2621,6 +2640,7 @@ mod inter_partition_copying { #[test] #[cfg(unix)] + #[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] pub(crate) fn test_mv_dir_with_fifo_across_partitions() { use std::os::unix::fs::FileTypeExt; use tempfile::TempDir; @@ -2853,6 +2873,7 @@ fn test_special_file_different_filesystem() { /// a cross-device move fails due to permission errors when removing the target file #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_permission_denied() { use std::fs::{set_permissions, write}; use std::os::unix::fs::PermissionsExt; @@ -2901,6 +2922,7 @@ fn test_mv_cross_device_permission_denied() { /// than truncate `victim`. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_refuses_planted_symlink_dest() { use std::os::unix::fs::symlink; use tempfile::TempDir; @@ -3055,6 +3077,7 @@ fn test_mv_error_usage_display_too_few() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_verbose_directory_recursive() { use tempfile::TempDir; @@ -3110,6 +3133,7 @@ fn test_mv_verbose_directory_recursive() { #[cfg(unix)] #[test] +#[cfg_attr(wasi_runner, ignore = "WASI: st_mode has no real permission bits")] fn test_mv_prompt_unwriteable_file_when_using_tty() { let (at, mut ucmd) = at_and_ucmd!(); @@ -3261,6 +3285,7 @@ fn test_mv_cross_device_dir_xattr_preserved() { /// destination atomically, matching GNU. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_symlink_onto_existing() { use std::fs; use std::os::unix::fs::symlink; @@ -3301,6 +3326,7 @@ fn test_mv_cross_device_symlink_onto_existing() { /// fail without destroying the directory or its contents. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_symlink_onto_existing_dir() { use std::fs; use std::os::unix::fs::symlink; @@ -3342,6 +3368,7 @@ fn test_mv_cross_device_symlink_onto_existing_dir() { /// (not expanded into full copies of their targets) #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_symlink_preserved() { use std::fs; use std::os::unix::fs::symlink; @@ -3389,6 +3416,7 @@ fn test_mv_cross_device_symlink_preserved() { /// Test that broken/dangling symlinks are preserved during cross-device moves #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_broken_symlink_preserved() { use std::fs; use std::os::unix::fs::symlink; @@ -3434,6 +3462,7 @@ fn test_mv_cross_device_broken_symlink_preserved() { /// Test that symlinks to regular files are preserved during cross-device moves #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_file_symlink_preserved() { use std::fs; use std::os::unix::fs::symlink; @@ -3495,6 +3524,7 @@ fn find_other_group(current: u32) -> Option { /// See https://github.com/uutils/coreutils/issues/9714 #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_preserves_ownership() { use std::fs; use std::os::unix::fs::MetadataExt; @@ -3557,6 +3587,7 @@ fn test_mv_cross_device_preserves_ownership() { /// See https://github.com/uutils/coreutils/issues/9714 #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_mv_cross_device_preserves_ownership_recursive() { use std::fs; use std::os::unix::fs::MetadataExt; diff --git a/tests/by-util/test_nproc.rs b/tests/by-util/test_nproc.rs index 0c48b0e190f..194e9aadf47 100644 --- a/tests/by-util/test_nproc.rs +++ b/tests/by-util/test_nproc.rs @@ -52,7 +52,9 @@ fn test_nproc_all_omp() { assert_eq!(nproc, nproc_omp); // clamp overflow - #[cfg(target_pointer_width = "64")] + // The wasm guest is always a 32-bit target regardless of the host's + // pointer width, so it clamps to u32::MAX there instead of u64::MAX. + #[cfg(all(target_pointer_width = "64", not(wasi_runner)))] TestScenario::new(util_name!()) .ucmd() .env("OMP_NUM_THREADS", "99999999999999999999") diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index 8868bca7caa..3d62616907b 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1403,6 +1403,7 @@ fn test_od_eintr_handling() { // Regression test: od should handle write errors to /dev/full without aborting. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_write_error_dev_full() { use std::fs::File; diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 96ccc1c3212..68415bac0e2 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -25,6 +25,10 @@ fn test_invalid_arg() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI: symlink_metadata(\"\") succeeds instead of returning ENOENT" +)] fn test_default_mode() { // accept some reasonable default new_ucmd!().args(&["dir/file"]).succeeds().no_stdout(); @@ -193,6 +197,7 @@ fn test_empty_path_portability_message() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_pathchk_non_utf8_paths() { let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); new_ucmd!().arg(&filename).succeeds(); diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 7d4096622fc..97bc41cf555 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -558,6 +558,10 @@ fn test_large_page_width_does_not_panic() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: wasmtime itself needs more memory than the guest's rlimit allows" +)] fn test_offset_large_value_does_not_abort_under_memory_limit() { use rustix::process::Resource; use std::process::Stdio; @@ -625,6 +629,7 @@ fn test_with_date_format() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_with_date_format_env() { // POSIXLY_CORRECT + LC_ALL/TIME=POSIX uses "%b %e %H:%M %Y" date format let whitespace = " ".repeat(49); @@ -749,6 +754,10 @@ fn test_version() { #[cfg(unix)] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_pr_char_device_dev_null() { new_ucmd!().arg("/dev/null").succeeds(); } @@ -1305,6 +1314,10 @@ fn test_negative_expand_tabs() { #[cfg(unix)] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_merge_empty_input() { new_ucmd!() .args(&["-m", "/dev/null", "/dev/null"]) diff --git a/tests/by-util/test_printenv.rs b/tests/by-util/test_printenv.rs index 9b5c027aafd..4346895a38c 100644 --- a/tests/by-util/test_printenv.rs +++ b/tests/by-util/test_printenv.rs @@ -6,6 +6,10 @@ use uutests::new_ucmd; #[test] +#[cfg_attr( + wasi_runner, + ignore = "sets HOME to a relative path, which breaks wasmtime's own cache-dir resolution" +)] fn test_get_all() { new_ucmd!() .env("HOME", "FOO") @@ -69,6 +73,10 @@ fn test_invalid_option_exit_code() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "sets HOME to a relative path, which breaks wasmtime's own cache-dir resolution" +)] fn test_null_separator() { // printenv should use \x00 as separator if null option is provided for null_opt in ["-0", "--null"] { @@ -95,6 +103,7 @@ fn test_null_separator() { #[test] #[cfg(unix)] #[cfg(not(any(target_os = "freebsd", target_os = "android", target_os = "openbsd")))] +#[cfg_attr(wasi_runner, ignore = "WASI: env values must be valid UTF-8")] fn test_non_utf8_value() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -121,6 +130,7 @@ fn test_non_utf8_value() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: env values must be valid UTF-8")] fn test_non_utf8_env_vars() { use std::ffi::OsString; use std::os::unix::ffi::OsStringExt; diff --git a/tests/by-util/test_ptx.rs b/tests/by-util/test_ptx.rs index 223e4182b7c..85cb21470af 100644 --- a/tests/by-util/test_ptx.rs +++ b/tests/by-util/test_ptx.rs @@ -238,6 +238,7 @@ fn test_format() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .arg("-G") diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index e0448179f75..ee3a50b08fc 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -17,12 +17,20 @@ fn test_invalid_arg() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_default() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.succeeds().stdout_is(at.root_dir_resolved() + "\n"); } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_ignores_non_option_arguments() { // GNU pwd ignores non-option operands, warning on stderr but exiting 0. let (at, mut ucmd) = at_and_ucmd!(); @@ -34,6 +42,10 @@ fn test_ignores_non_option_arguments() { #[cfg(unix)] #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_deleted_dir() { use std::process::Command; use uutests::util::TestScenario; @@ -86,24 +98,40 @@ fn symlinked_env() -> Env { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_logical() { let mut env = symlinked_env(); env.ucmd.arg("-L").succeeds().stdout_is(env.symdir + "\n"); } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_physical() { let mut env = symlinked_env(); env.ucmd.arg("-P").succeeds().stdout_is(env.subdir + "\n"); } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_default() { let mut env = symlinked_env(); env.ucmd.succeeds().stdout_is(env.subdir + "\n"); } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_default_posix() { let mut env = symlinked_env(); env.ucmd @@ -113,6 +141,10 @@ fn test_symlinked_default_posix() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_default_posix_l() { let mut env = symlinked_env(); env.ucmd @@ -123,6 +155,10 @@ fn test_symlinked_default_posix_l() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" +)] fn test_symlinked_default_posix_p() { let mut env = symlinked_env(); env.ucmd @@ -139,6 +175,10 @@ pub mod untrustworthy_pwd_var { use super::*; #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" + )] fn test_nonexistent_logical() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.arg("-L") @@ -148,6 +188,10 @@ pub mod untrustworthy_pwd_var { } #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" + )] fn test_wrong_logical() { let mut env = symlinked_env(); env.ucmd @@ -158,6 +202,10 @@ pub mod untrustworthy_pwd_var { } #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" + )] fn test_redundant_logical() { let mut env = symlinked_env(); env.ucmd @@ -168,6 +216,10 @@ pub mod untrustworthy_pwd_var { } #[test] + #[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: pwd reports the guest's virtual root, not the host's absolute path" + )] fn test_relative_logical() { let mut env = symlinked_env(); env.ucmd diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index e4cbc5ad9b0..7bc1daa5587 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -81,6 +81,10 @@ fn test_canonicalize_existing_keeps_going_after_a_missing_operand() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_canonicalize() { let (at, mut ucmd) = at_and_ucmd!(); let actual = ucmd.arg("-f").arg(".").succeeds().stdout_move_str(); @@ -91,6 +95,10 @@ fn test_canonicalize() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_canonicalize_existing() { let (at, mut ucmd) = at_and_ucmd!(); let actual = ucmd.arg("-e").arg(".").succeeds().stdout_move_str(); @@ -101,6 +109,10 @@ fn test_canonicalize_existing() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_canonicalize_missing() { let (at, mut ucmd) = at_and_ucmd!(); let actual = ucmd.arg("-m").arg(GIBBERISH).succeeds().stdout_move_str(); @@ -112,6 +124,10 @@ fn test_canonicalize_missing() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_canonicalize_symlink_before_parentdir() { // GNU readlink follows the symlink first and only then evaluates `..`. // Logical resolution would collapse `link/..` up front and return the current directory instead. @@ -126,6 +142,10 @@ fn test_canonicalize_symlink_before_parentdir() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_long_redirection_to_current_dir() { let (at, mut ucmd) = at_and_ucmd!(); // Create a 256-character path to current directory @@ -488,6 +508,7 @@ fn test_delimiters() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_readlink_non_utf8_paths() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 1e143ecefe2..4da2e24d67c 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -17,6 +17,10 @@ use std::path::{MAIN_SEPARATOR, Path}; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_current_directory() { let (at, mut ucmd) = at_and_ucmd!(); let expect = at.root_dir_resolved() + "\n"; @@ -24,6 +28,10 @@ fn test_realpath_current_directory() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_long_redirection_to_current_dir() { let (at, mut ucmd) = at_and_ucmd!(); // Create a 256-character path to current directory @@ -163,6 +171,10 @@ fn test_realpath_logical_mode() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_dangling() { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file("nonexistent-file", "link"); @@ -225,6 +237,10 @@ fn test_realpath_symlink_follow_limit() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_default_allows_final_non_existent() { let p = Path::new("").join(GIBBERISH); let (at, mut ucmd) = at_and_ucmd!(); @@ -239,6 +255,10 @@ fn test_realpath_default_forbids_non_final_non_existent() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_existing() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.arg("-e") @@ -261,6 +281,10 @@ fn test_realpath_existing_error_quiet() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_missing() { let p = Path::new("").join(GIBBERISH).join(GIBBERISH); let (at, mut ucmd) = at_and_ucmd!(); @@ -272,6 +296,10 @@ fn test_realpath_missing() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: canonicalized path resolves to the guest's virtual root, not the host's absolute path" +)] fn test_realpath_when_symlink_is_absolute_and_enoent() { let (at, mut ucmd) = at_and_ucmd!(); @@ -557,6 +585,7 @@ fn test_realpath_empty() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_realpath_non_utf8_paths() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index e8da3d52aed..3c55cb5e9b6 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -340,6 +340,7 @@ fn test_verbose() { // write failure on stderr and exit 1 instead of panicking. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_verbose_write_error_does_not_panic() { use std::fs::OpenOptions; @@ -364,6 +365,7 @@ fn test_verbose_write_error_does_not_panic() { // Directory variant of the #10551 regression test. #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_verbose_write_error_does_not_panic_dir() { use std::fs::OpenOptions; @@ -489,6 +491,7 @@ fn test_no_operand() { } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_verbose_slash() { let (at, mut ucmd) = at_and_ucmd!(); let dir = "test_rm_verbose_slash_directory"; @@ -926,6 +929,7 @@ fn test_current_or_parent_dir_rm4() { at.touch(file_1); at.touch(file_2); + #[cfg(not(wasi_runner))] let answers = [ "rm: refusing to remove '.' or '..' directory: skipping 'd/.'", "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", @@ -937,6 +941,20 @@ fn test_current_or_parent_dir_rm4() { "rm: refusing to remove '.' or '..' directory: skipping '../'", "rm: refusing to remove '.' or '..' directory: skipping '..'", ]; + #[cfg(wasi_runner)] + let answers = [ + "rm: refusing to remove '.' or '..' directory: skipping 'd/.'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/./'", + "rm: refusing to remove '.' or '..' directory: skipping 'd/..'", + "rm: it is dangerous to operate recursively on 'd/../' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + "rm: refusing to remove '.' or '..' directory: skipping '.'", + "rm: it is dangerous to operate recursively on './' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + "rm: it is dangerous to operate recursively on '../' (same as '/')", + "rm: use --no-preserve-root to override this failsafe", + ]; let std_err_str = ts .ucmd() .arg("-rf") @@ -1168,6 +1186,7 @@ fn test_recursive_remove_unreadable_subdir() { at.set_mode("foo/bar", 0o0000); let result = ucmd.args(&["-r", "-f", "foo"]).fails(); + result.stderr_contains("Permission denied"); result.stderr_contains("foo/bar"); @@ -1225,9 +1244,12 @@ fn test_inaccessible_dir_recursive() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore)] fn test_non_utf8_paths() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; + #[cfg(all(target_os = "wasi", target_env = "p1"))] + use std::os::wasi::ffi::OsStrExt; let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1531,8 +1553,9 @@ fn test_symlink_to_readonly_no_prompt() { } /// Test that --preserve-root properly detects symlinks pointing to root. -#[cfg(unix)] #[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_preserve_root_symlink_to_root() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1553,8 +1576,9 @@ fn test_preserve_root_symlink_to_root() { } /// Test that --preserve-root properly detects nested symlinks pointing to root. -#[cfg(unix)] #[test] +#[cfg(unix)] +#[cfg_attr(wasi_runner, ignore)] fn test_preserve_root_nested_symlink_to_root() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_rmdir.rs b/tests/by-util/test_rmdir.rs index 5de44899e90..65fe9735faf 100644 --- a/tests/by-util/test_rmdir.rs +++ b/tests/by-util/test_rmdir.rs @@ -208,6 +208,7 @@ fn test_rmdir_ignore_nonempty_no_permissions() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI: read_link() not supported for symlinks")] fn test_rmdir_remove_symlink_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -220,8 +221,9 @@ fn test_rmdir_remove_symlink_file() { } // This behavior is known to happen on Linux but not all Unixes -#[cfg(any(target_os = "linux", target_os = "android"))] #[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg_attr(wasi_runner, ignore = "WASI: read_link() not supported for symlinks")] fn test_rmdir_remove_symlink_dir() { let (at, mut ucmd) = at_and_ucmd!(); @@ -233,8 +235,9 @@ fn test_rmdir_remove_symlink_dir() { .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed\n"); } -#[cfg(any(target_os = "linux", target_os = "android"))] #[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg_attr(wasi_runner, ignore = "WASI: read_link() not supported for symlinks")] fn test_rmdir_remove_symlink_dangling() { let (at, mut ucmd) = at_and_ucmd!(); @@ -245,8 +248,9 @@ fn test_rmdir_remove_symlink_dangling() { .stderr_is("rmdir: failed to remove 'dl/': Symbolic link not followed\n"); } -#[cfg(any(target_os = "linux", target_os = "android"))] #[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg_attr(wasi_runner, ignore = "WASI: read_link() not supported for symlinks")] fn test_rmdir_remove_symlink_dir_with_trailing_slashes() { // a symlink with trailing slashes should still be printing the 'Symbolic link not followed' // message diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 7637fe6db0b..22ec1e7d826 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -234,6 +234,10 @@ fn test_width_invalid_float() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "invokes the binary directly from a shell script, bypassing the wasmtime runner" +)] fn test_sigpipe_ignored_reports_write_error() { let scene = TestScenario::new(util_name!()); let seq_bin = scene.bin_path.clone().into_os_string(); @@ -298,6 +302,7 @@ fn test_separator_and_terminator() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_separator_non_utf8() { use std::{ffi::OsString, os::unix::ffi::OsStringExt}; @@ -321,6 +326,7 @@ fn test_separator_non_utf8() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_terminator_non_utf8() { use std::{ffi::OsString, os::unix::ffi::OsStringExt}; @@ -734,9 +740,11 @@ fn test_infinite_sequence(#[case] args: &[&str], #[case] expected_start: &[u8]) let result = new_ucmd!() .args(args) .run_stdout_starts_with(expected_start); - #[cfg(unix)] + // On Unix systems (not WASI), seq should be terminated by SIGPIPE when the pipe closes. + // On WASI and Windows, there are no signals, so just check the process succeeded. + #[cfg(all(unix, not(wasi_runner)))] result.signal_name_is("PIPE"); - #[cfg(not(unix))] + #[cfg(any(not(unix), wasi_runner))] result.success(); } diff --git a/tests/by-util/test_sha1sum.rs b/tests/by-util/test_sha1sum.rs index 80096a8eff2..22c6ce8b24f 100644 --- a/tests/by-util/test_sha1sum.rs +++ b/tests/by-util/test_sha1sum.rs @@ -134,7 +134,7 @@ fn test_check_sha1() { scene .ccmd("sha1sum") .arg("-c") - .arg(at.subdir.join("testf.sha1")) + .arg("testf.sha1") .succeeds() .stdout_only("testf: OK\n"); } @@ -153,7 +153,7 @@ fn test_check_file_not_found_warning() { scene .ccmd("sha1sum") .arg("-c") - .arg(at.subdir.join("testf.sha1")) + .arg("testf.sha1") .fails() .stdout_is("testf: FAILED open or read\n") .stderr_is("sha1sum: testf: No such file or directory\nsha1sum: WARNING: 1 listed file could not be read\n"); diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index c3a57ab3cb9..09cea46b439 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -134,6 +134,10 @@ fn test_shred_u() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: chmod has no ENOSYS-free syscall; -f can't restore write permission" +)] fn test_shred_force() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -265,6 +269,10 @@ fn test_all_patterns_present() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: File::try_clone() (used for --random-source) is unsupported" +)] fn test_random_source_regular_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -349,6 +357,7 @@ fn test_shred_rename_exhaustion() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_shred_non_utf8_paths() { use std::os::unix::ffi::OsStrExt; let ts = TestScenario::new(util_name!()); @@ -362,6 +371,10 @@ fn test_shred_non_utf8_paths() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: File::try_clone() (used for --random-source) is unsupported" +)] fn test_shred_twenty_passes_with_known_random_source() { let (at, mut ucmd) = at_and_ucmd!(); @@ -418,6 +431,10 @@ fn test_shred_twenty_passes_with_known_random_source() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: File::try_clone() (used for --random-source) is unsupported" +)] fn test_shred_nineteen_passes_first_and_last_are_random() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_sleep.rs b/tests/by-util/test_sleep.rs index d685c6e2d4a..b7e676a6c5a 100644 --- a/tests/by-util/test_sleep.rs +++ b/tests/by-util/test_sleep.rs @@ -145,6 +145,7 @@ fn test_sleep_wrong_time() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no signal support")] fn test_sleep_stops_after_sigsegv() { let mut child = new_ucmd!() .arg("100") @@ -162,6 +163,7 @@ fn test_sleep_stops_after_sigsegv() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no signal support")] fn test_sleep_stops_after_sigbus() { let mut child = new_ucmd!() .arg("100") diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 2d5cb0a55d9..5e509d67ff5 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -37,6 +37,7 @@ fn test_helper(file_name: &str, possible_args: &[&str]) { } #[test] +#[cfg_attr(wasi_runner, ignore)] fn test_buffer_sizes() { #[cfg(target_os = "linux")] let buffer_sizes = ["0", "50K", "50k", "1M", "100M", "0%", "10%"]; @@ -53,6 +54,8 @@ fn test_buffer_sizes() { .stdout_is_fixture("ext_sort.expected"); } + // The wasm guest is always a 32-bit target regardless of the host's + // pointer width, so these overflow there even when the host is 64-bit. #[cfg(not(target_pointer_width = "32"))] { let buffer_sizes = ["1000G", "10T"]; @@ -816,6 +819,7 @@ fn month_sort_input_expected(months: &[String]) -> (String, String) { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_month_sort_french_locale() { let locale = "fr_FR.UTF-8"; if !is_locale_available(locale) { @@ -859,6 +863,7 @@ fn test_month_sort_french_locale() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_month_sort_hungarian_locale() { let locale = "hu_HU.UTF-8"; if !is_locale_available(locale) { @@ -902,6 +907,7 @@ fn test_month_sort_hungarian_locale() { /// E.g. "av ril" should NOT match "avril" — GNU treats it as unknown. #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_month_sort_french_embedded_blanks() { let locale = "fr_FR.UTF-8"; if !is_locale_available(locale) { @@ -966,6 +972,7 @@ fn test_month_sort_french_embedded_blanks() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_month_sort_japanese_locale() { let locale = "ja_JP.UTF-8"; if !is_locale_available(locale) { @@ -1311,6 +1318,10 @@ fn test_unterminated_file_not_fused_across_chunk_boundary() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_interleaved() { new_ucmd!() .arg("-m") @@ -1322,6 +1333,10 @@ fn test_merge_interleaved() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_preserves_long_lines() { use std::fmt::Write; @@ -1354,6 +1369,10 @@ fn test_merge_preserves_long_lines() { // receivers while the reader was still sending, and `chunks::read` unwraps that send. #[test] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_write_error_does_not_panic() { use std::fs::File; @@ -1420,6 +1439,10 @@ fn test_merge_flush_error_is_reported() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_unique() { new_ucmd!() .arg("-m") @@ -1435,6 +1458,10 @@ fn test_merge_unique() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_stable() { new_ucmd!() .arg("-m") @@ -1447,6 +1474,10 @@ fn test_merge_stable() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_reversed() { new_ucmd!() .arg("-m") @@ -1675,6 +1706,10 @@ fn test_compress() { #[test] #[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_compress_merge() { new_ucmd!() .args(&[ @@ -1698,6 +1733,10 @@ fn test_compress_merge() { #[test] #[cfg(not(target_os = "android"))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: ext_sort bypasses --compress-program entirely (single-threaded in-memory path)" +)] fn test_compress_fail() { let result = new_ucmd!() .args(&[ @@ -1760,7 +1799,7 @@ fn test_batch_size_too_large() { "--batch-size argument '{large_batch_size}' too large" )); - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", not(wasi_runner)))] new_ucmd!() .arg(format!("--batch-size={large_batch_size}")) .fails_with_code(2) @@ -1768,6 +1807,10 @@ fn test_batch_size_too_large() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_batch_size() { new_ucmd!() .arg("--batch-size=2") @@ -1787,6 +1830,10 @@ fn test_merge_batch_size() { // TODO(#7542): Re-enable on Android once we figure out why setting limit is broken. // #[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_batch_size_with_limit() { use rustix::process::Resource; // Currently need... @@ -1991,6 +2038,10 @@ fn test_verifies_files_after_keys() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/random) not visible" +)] fn test_verifies_input_files() { new_ucmd!() .args(&["/dev/random", "nonexistent_file"]) @@ -2051,6 +2102,10 @@ fn test_separator_attached_equals_multi_char() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads, unsupported under wasmtime's default config" +)] fn test_output_is_input() { let input = "a\nb\nc\n"; let (at, mut ucmd) = at_and_ucmd!(); @@ -2099,6 +2154,10 @@ fn test_merge_output_file_is_truncated() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_output_device() { new_ucmd!() .args(&["-o", "/dev/null"]) @@ -2107,6 +2166,10 @@ fn test_output_device() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: sort -m spawns real OS threads for multi-file merge, unsupported under wasmtime's default config" +)] fn test_merge_empty_input() { new_ucmd!() .args(&["-m", "empty.txt"]) @@ -2132,6 +2195,7 @@ fn test_wrong_args_exit_code() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: no signal support (SIGINT)")] fn test_tmp_files_deleted_on_sigint() { use rand::{RngExt as _, SeedableRng, rngs::SmallRng}; use rustix::process::{Pid, Signal, kill_process}; @@ -2262,6 +2326,7 @@ fn test_args_check_conflict() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .pipe_in("hello") @@ -2364,6 +2429,10 @@ fn test_files0_from_empty_input_file() { #[test] #[cfg(unix)] +#[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: error message text for this OS error differs from native Unix" +)] fn test_files0_from_non_utf8_filename() { new_ucmd!() .args(&["--files0-from", "-"]) @@ -2374,6 +2443,10 @@ fn test_files0_from_non_utf8_filename() { #[test] #[cfg(unix)] +#[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: error message text for this OS error differs from native Unix" +)] fn test_files0_from_unreadable_source() { new_ucmd!() .args(&["--files0-from", "."]) @@ -2384,6 +2457,7 @@ fn test_files0_from_unreadable_source() { #[cfg(unix)] #[test] // Test files0-from with non-regular empty file +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_files0_from_dev_null_is_empty() { new_ucmd!() .args(&["--files0-from", "/dev/null"]) @@ -2470,6 +2544,10 @@ fn test_files0_from_two_entries_trailing_nul() { #[test] // Test files0-from with non-UTF-8 filenames #[cfg(all(unix, not(target_vendor = "apple")))] +#[cfg_attr( + wasi_runner, + ignore = "WASI: preopened directories reject non-UTF-8 filenames" +)] fn test_files0_from_non_utf8_content() { use std::os::unix::ffi::OsStringExt; let (at, mut ucmd) = at_and_ucmd!(); @@ -3456,6 +3534,7 @@ fn test_locale_collation_utf8() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_locale_interleaved_en_us_utf8() { // Test case for issue: locale-based collation support // In en_US.UTF-8, lowercase and uppercase letters should interleave @@ -3528,6 +3607,7 @@ fn test_locale_with_ignore_case_flag() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_locale_complex_utf8_sorting() { // More complex test with mixed case and special characters // In en_US.UTF-8, should respect locale collation rules @@ -3552,6 +3632,7 @@ fn test_locale_posix_sort_debug_message() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_locale_utf8_sort_debug_message() { new_ucmd!() .env("LC_ALL", "en_US.UTF-8") @@ -3572,7 +3653,7 @@ fn test_failed_to_set_locale_debug_message() { result.stderr_contains("text ordering performed using simple byte comparison"); - #[cfg(all(target_os = "linux", target_env = "gnu"))] + #[cfg(all(target_os = "linux", target_env = "gnu", not(wasi_runner)))] result.stderr_contains("failed to set locale"); } @@ -3606,6 +3687,7 @@ fn test_empty_input_empty_output() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_consistent_sorting_with_i18n_collate() { // Regression test for issue #11980 // Lexicographic fallback sorting for equal sorting keys for 01 and 0_1 diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 247b807de6e..ff76e1cbbbf 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -133,6 +133,7 @@ fn test_invalid_arg() { #[test] #[cfg(all(unix, not(target_os = "android")))] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_split_to_non_seekable() { let (at, mut ucmd) = at_and_ucmd!(); at.symlink_file("/dev/stdout", "xaa"); @@ -310,6 +311,7 @@ fn test_split_additional_suffix_hyphen_value() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: --filter has no process-spawning support")] fn test_filter() { // like `test_split_default()` but run a command before writing let (at, mut ucmd) = at_and_ucmd!(); @@ -353,6 +355,7 @@ fn test_filter_with_env_var_set() { #[test] #[cfg(unix)] +#[cfg_attr(wasi_runner, ignore = "WASI: --filter has no process-spawning support")] fn test_filter_command_fails() { let (at, mut ucmd) = at_and_ucmd!(); let name = "filter-will-fail"; @@ -959,6 +962,10 @@ creating file 'xaf' } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_number_n() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-n", "5", "asciilowercase.txt"]).succeeds(); @@ -975,6 +982,10 @@ fn test_number_n() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_number_kth_of_n() { new_ucmd!() .args(&["--number=3/5", "asciilowercase.txt"]) @@ -1020,6 +1031,10 @@ fn test_number_kth_of_n() { } #[test] +#[cfg_attr( + wasi_runner, + ignore = "wasm guest is always 32-bit; u64::MAX-sized args overflow differently there" +)] fn test_number_kth_of_n_round_robin() { new_ucmd!() .args(&["--number", "r/2/3", "fivelines.txt"]) @@ -1188,6 +1203,10 @@ fn test_elide_empty_files_n_chunks() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_elide_dev_null_n_chunks() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-e", "-n", "3", "/dev/null"]) @@ -1200,6 +1219,10 @@ fn test_elide_dev_null_n_chunks() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_dev_zero() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-n", "3", "/dev/zero"]) @@ -1227,6 +1250,10 @@ fn test_elide_empty_files_l_chunks() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_elide_dev_null_l_chunks() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-e", "-n", "l/3", "/dev/null"]) @@ -1239,6 +1266,10 @@ fn test_elide_dev_null_l_chunks() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/zero) not visible" +)] fn test_number_by_bytes_dev_zero() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-n", "3", "/dev/zero"]) @@ -1298,6 +1329,10 @@ fn test_number_by_lines_kth_huge_number_of_chunks() { #[test] #[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/null) not visible" +)] fn test_number_by_lines_kth_dev_null() { new_ucmd!() .args(&["-n", "l/3/10", "/dev/null"]) @@ -1689,6 +1724,10 @@ fn test_round_robin() { // TODO(#7542): Re-enable on Android once we figure out why rlimit is broken. // #[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI: wasmtime itself needs more file descriptors than the guest's rlimit allows" +)] fn test_round_robin_limited_file_descriptors() { new_ucmd!() .args(&["-n", "r/40", "onehundredlines.txt"]) @@ -1733,6 +1772,7 @@ fn test_split_invalid_input() { /// clap is expected to fail/panic #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_split_non_utf8_argument_unix() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -2028,6 +2068,7 @@ fn test_long_lines() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_split_non_utf8_paths() { let (at, mut ucmd) = at_and_ucmd!(); @@ -2042,6 +2083,7 @@ fn test_split_non_utf8_paths() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_split_non_utf8_prefix_is_byte_preserving() { use std::ffi::OsStr; use std::os::unix::ffi::{OsStrExt, OsStringExt}; @@ -2081,6 +2123,7 @@ fn test_split_non_utf8_prefix_is_byte_preserving() { #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_split_non_utf8_additional_suffix_is_byte_preserving() { use std::ffi::OsStr; use std::os::unix::ffi::{OsStrExt, OsStringExt}; @@ -2182,6 +2225,7 @@ fn test_split_directory_already_exists() { #[test] #[cfg(all(target_os = "linux", target_env = "gnu"))] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host paths not visible")] fn test_io_error() { // /proc/self/mem causes EIO new_ucmd!() @@ -2194,6 +2238,10 @@ fn test_io_error() { /// Writing a chunk to a full device must be reported and must stop the split. #[test] #[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host paths (/dev/full) not visible" +)] fn test_write_error_on_full_device() { if !Path::new("/dev/full").exists() { return; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 8dbce1b9864..0aba99be88b 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -343,6 +343,10 @@ fn test_follow_redirect_stdin_name_retry() { not(target_os = "openbsd"), not(windows) ))] // FIXME: for currently not working platforms +#[cfg_attr( + wasi_runner, + ignore = "WASI: no /dev/fd/0, so redirected-directory stdin hits the generic pipe path (like macOS) instead of the regular-file path with the GNU-matching error message" +)] fn test_stdin_redirect_dir() { // $ mkdir dir // $ tail < dir, $ tail - < dir @@ -380,6 +384,10 @@ fn test_stdin_redirect_dir() { // `test_stdin_redirect_dir` #[test] #[cfg(target_vendor = "apple")] +#[cfg_attr( + wasi_runner, + ignore = "test binary runs on macOS but the wasm guest under test does not, so the expected macOS-specific error text never appears" +)] fn test_stdin_redirect_dir_when_target_os_is_macos() { // $ mkdir dir // $ tail < dir, $ tail - < dir @@ -4219,6 +4227,10 @@ fn test_when_follow_retry_then_initial_print_of_file_is_written_to_stdout() { // TODO: Add test for the warning `--pid=PID is not supported on this system` #[test] +#[cfg_attr( + wasi_runner, + ignore = "WASI: follow mode (-f) is not supported on this platform" +)] fn test_args_when_settings_check_warnings_then_shows_warnings() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -5253,6 +5265,7 @@ fn test_child_when_run_with_stderr_to_stdout() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .pipe_in("hello") @@ -5272,6 +5285,7 @@ fn test_failed_warning_write_is_reported() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported_on_seekable_input() { let ts = TestScenario::new("tail"); let at = &ts.fixtures; diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 187edf0e1b8..a2f5dace1e5 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -980,6 +980,10 @@ fn test_touch_leap_second() { #[test] #[cfg(not(windows))] // File::create doesn't support trailing separator in Windows +#[cfg_attr( + wasip2_runner, + ignore = "WASI preview2: stat on a dangling-symlink-with-trailing-slash returns ENOTDIR instead of ENOENT" +)] fn test_touch_trailing_slash_no_create() { let (at, mut ucmd) = at_and_ucmd!(); at.touch("file"); @@ -1205,11 +1209,9 @@ fn test_touch_device_files() { ignore = "WASI sandbox: absolute symlink targets cannot be followed" )] fn test_touch_does_not_truncate_symlink_target() { - use std::os::unix::fs::symlink; - let (at, mut ucmd) = at_and_ucmd!(); at.write("victim", "do not truncate me"); - symlink(at.plus("victim"), at.plus("link")).unwrap(); + at.symlink_file("victim", "link"); ucmd.arg("link").succeeds(); @@ -1224,10 +1226,8 @@ fn test_touch_does_not_truncate_symlink_target() { ignore = "WASI sandbox: absolute symlink targets cannot be followed" )] fn test_touch_through_dangling_symlink_creates_target() { - use std::os::unix::fs::symlink; - let (at, mut ucmd) = at_and_ucmd!(); - symlink(at.plus("missing"), at.plus("link")).unwrap(); + at.symlink_file("missing", "link"); ucmd.arg("link").succeeds(); diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 2fd477dbf55..8ad38110415 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -1674,8 +1674,9 @@ fn test_octal_escape_ambiguous_followed_by_non_utf8() { .stderr_contains("warning: invalid utf8 sequence"); } -#[cfg(target_os = "linux")] #[test] +#[cfg(target_os = "linux")] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .pipe_in("hello") diff --git a/tests/by-util/test_true.rs b/tests/by-util/test_true.rs index 4e11449baee..8e7eb45504b 100644 --- a/tests/by-util/test_true.rs +++ b/tests/by-util/test_true.rs @@ -55,6 +55,7 @@ fn test_conflict() { #[test] #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_full() { for option in ["--version", "--help"] { let dev_full = OpenOptions::new().write(true).open("/dev/full").unwrap(); diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index fbd1c0b8644..e263c1f6d5c 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -12,6 +12,7 @@ use uutests::new_ucmd; #[test] #[cfg(target_os = "linux")] +#[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] fn test_tsort_non_utf8_paths() { use std::os::unix::ffi::OsStringExt; let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_tty.rs b/tests/by-util/test_tty.rs index 5103a60efac..3a3981bf3c1 100644 --- a/tests/by-util/test_tty.rs +++ b/tests/by-util/test_tty.rs @@ -72,6 +72,10 @@ fn test_help() { #[test] // FixME: freebsd panic #[cfg(all(unix, not(target_os = "freebsd")))] +#[cfg_attr( + wasi_runner, + ignore = "runs the coreutils binary directly via a raw shell command, bypassing the WASI runner wrapping" +)] fn test_stdout_fail() { use std::process::{Command, Stdio}; use uutests::at_and_ts; diff --git a/tests/by-util/test_uname.rs b/tests/by-util/test_uname.rs index 3d45e79d365..990a1aefb30 100644 --- a/tests/by-util/test_uname.rs +++ b/tests/by-util/test_uname.rs @@ -72,11 +72,19 @@ fn test_uname_operating_system() { .arg("--operating-system") .succeeds() .stdout_is("Android\n"); - #[cfg(target_vendor = "apple")] + // The test binary runs on the host (e.g. macOS), but under the WASI + // runner the coreutils binary under test is the wasm guest, which + // correctly self-reports "WASI" rather than the host's OS name. + #[cfg(all(target_vendor = "apple", not(wasi_runner)))] new_ucmd!() .arg("--operating-system") .succeeds() .stdout_is("Darwin\n"); + #[cfg(all(target_vendor = "apple", wasi_runner))] + new_ucmd!() + .arg("--operating-system") + .succeeds() + .stdout_is("WASI\n"); #[cfg(target_os = "freebsd")] new_ucmd!() .arg("--operating-system") diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index 02ea5525421..fac3df1372a 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -1190,6 +1190,7 @@ fn uniq_basic_dedup_cases() { } #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: locale database not visible")] fn test_stdin_w1_multibyte() { let input = "à\ná\n"; new_ucmd!() @@ -1213,6 +1214,7 @@ fn test_c_locale_counts_bytes() { #[cfg(target_os = "linux")] #[test] +#[cfg_attr(wasip2_runner, ignore = "WASI P2: /dev/full filesystem not available")] fn test_failed_write_is_reported() { new_ucmd!() .pipe_in("hello") diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 16fd79293f5..94673e0847d 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -451,17 +451,14 @@ fn test_read_error_order_with_stderr_to_stdout() { let (at, mut ucmd) = at_and_ucmd!(); at.mkdir("ioerrdir"); - let cmd = ucmd.arg("ioerrdir").stderr_to_stdout().fails(); - if std::env::var("UUTESTS_WASM_RUNNER").is_ok() { - // wasi-libc may report a different error string than the host libc - cmd.stdout_contains("wc: ioerrdir:"); - } else { - let expected = format!( - "{:>7} {:>7} {:>7} ioerrdir\nwc: ioerrdir: Is a directory\n", - 0, 0, 0 - ); - cmd.stdout_only(expected); - } + let expected = format!( + "{:>7} {:>7} {:>7} ioerrdir\nwc: ioerrdir: Is a directory\n", + 0, 0, 0 + ); + ucmd.arg("ioerrdir") + .stderr_to_stdout() + .fails() + .stdout_only(expected); } /// Test that getting counts from nonexistent file is an error. @@ -813,23 +810,17 @@ fn files0_from_dir() { const DOT_ERR: &str = dir_err!("."); // On Unix, `read(open("."))` fails. On Windows, `open(".")` fails. Thus, the errors happen in - // different contexts. On WASI, the error string may differ (e.g., "Bad file descriptor"). - let wasm = std::env::var("UUTESTS_WASM_RUNNER").is_ok(); - - let cmd = new_ucmd!().args(&["--files0-from=dir with spaces"]).fails(); - if wasm { - cmd.stderr_contains("wc: 'dir with spaces': read error:"); - } else { - cmd.stderr_only(dir_err!("'dir with spaces'")); - } + // different contexts. + new_ucmd!() + .args(&["--files0-from=dir with spaces"]) + .fails() + .stderr_only(dir_err!("'dir with spaces'")); // Those contexts have different rules about quoting in errors... - let cmd = new_ucmd!().args(&["--files0-from=."]).fails(); - if wasm { - cmd.stderr_contains("wc: .: read error:"); - } else { - cmd.stderr_only(DOT_ERR); - } + new_ucmd!() + .args(&["--files0-from=."]) + .fails() + .stderr_only(DOT_ERR); // That also means you cannot `< . wc --files0-from=-` on Windows. #[cfg(not(windows))] diff --git a/tests/uutests/Cargo.toml b/tests/uutests/Cargo.toml index affc98cf28a..b0740828529 100644 --- a/tests/uutests/Cargo.toml +++ b/tests/uutests/Cargo.toml @@ -31,6 +31,7 @@ uucore = { workspace = true, features = [ "process", "signals", "utmpx", + "fs", ] } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 7bee2cb49f6..889b59cb879 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -1255,7 +1255,19 @@ impl AtPath { self.plus_as_string(link) ), ); - symlink_file(self.plus(original), self.plus(link)).unwrap(); + // Use a relative target instead of `self.plus(original)`'s absolute + // host path: under the WASI test runner, the wasm guest only sees + // the sandbox root, so an absolute host path would always dangle. + // Callers that pass an already-absolute `original` (e.g. "/") mean + // it literally, so leave those alone. + let link_path = self.plus(link); + let target = match (Path::new(original).is_absolute(), link_path.parent()) { + (false, Some(link_dir)) => { + uucore::fs::make_path_relative_to(self.plus(original), link_dir) + } + _ => self.plus(original), + }; + symlink_file(target, link_path).unwrap(); } pub fn relative_symlink_file(&self, original: &str, link: &str) { @@ -1277,7 +1289,15 @@ impl AtPath { self.plus_as_string(link) ), ); - symlink_dir(self.plus(original), self.plus(link)).unwrap(); + // See the comment in `symlink_file` about relative vs. absolute targets. + let link_path = self.plus(link); + let target = match (Path::new(original).is_absolute(), link_path.parent()) { + (false, Some(link_dir)) => { + uucore::fs::make_path_relative_to(self.plus(original), link_dir) + } + _ => self.plus(original), + }; + symlink_dir(target, link_path).unwrap(); } pub fn relative_symlink_dir(&self, original: &str, link: &str) { @@ -1929,6 +1949,11 @@ impl UCommand { let mut command = if let Some(ref runner) = wasm_runner { let bin = self.bin_path.as_ref().unwrap(); let mut cmd = Command::new(runner); + // Extra runner flags, e.g. "-S cli-exit-with-code=y" to opt in to + // an unstable WASI Preview2 feature. Space-separated. + if let Ok(extra_args) = env::var("UUTESTS_WASM_RUNNER_ARGS") { + cmd.args(extra_args.split_whitespace()); + } // Map the working directory as the WASI guest's root. Only files // under this directory are visible to the guest; tests using // absolute host paths outside it must be skipped. From 2031469bd0eebb706ba5b9c7300c48cd5d59326c Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:15:53 +0100 Subject: [PATCH 6/7] chore: use rust nightly --- .github/workflows/wasi.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wasi.yml b/.github/workflows/wasi.yml index 6f05f55f4c1..0fd5c84a77e 100644 --- a/.github/workflows/wasi.yml +++ b/.github/workflows/wasi.yml @@ -24,13 +24,16 @@ jobs: fail-fast: false matrix: job: - - { target: wasm32-wasip1, rust-flags: "--cfg wasi_runner" } - - { target: wasm32-wasip2, rust-flags: "--cfg wasi_runner --cfg wasip2_runner" } + - { target: wasm32-wasip1, rust-flags: "--cfg wasi_runner", toolchain: stable } + # wasm32-wasip2 needs a toolchain where `std::process::exit` propagates the + # real exit code through `wasi:cli/exit#exit` instead of collapsing every + # nonzero code to 1; only available on nightly for now. + - { target: wasm32-wasip2, rust-flags: "--cfg wasi_runner --cfg wasip2_runner", toolchain: nightly } steps: - uses: actions/checkout@v7.0.1 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@${{ matrix.job.toolchain }} with: targets: ${{ matrix.job.target }} - uses: Swatinem/rust-cache@v2 @@ -52,7 +55,7 @@ jobs: RUSTFLAGS: ${{ matrix.job.rust-flags }} run: | # Build the WASI binary - cargo build --target ${{ matrix.job.target }} --no-default-features --features feat_wasm,wasip2_exit_with_code + cargo build --target ${{ matrix.job.target }} --no-default-features --features feat_wasm # Run host-compiled integration tests against the WASI binary. # Tests incompatible with WASI are annotated with # #[cfg_attr(wasi_runner, ignore)] in the test source files. @@ -61,7 +64,6 @@ jobs: # not(wasi_runner) in the cfg guarding every `mod diagnostics`. UUTESTS_BINARY_PATH="$(pwd)/target/${{ matrix.job.target }}/debug/coreutils.wasm" \ UUTESTS_WASM_RUNNER=wasmtime \ - UUTESTS_WASM_RUNNER_ARGS="-S cli-exit-with-code=y" \ cargo test --test tests -- \ test_arch:: test_b2sum:: test_base32:: test_base64:: test_basename:: \ test_basenc:: test_cat:: test_cksum:: test_comm:: test_cp:: \ From 0d592ffe9807a588bde9ca731283933af65eeb6d Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:50:56 +0100 Subject: [PATCH 7/7] chore: fix clippy issues --- src/uu/chmod/src/chmod.rs | 2 +- src/uu/cp/src/platform/linux.rs | 20 ++++---- src/uu/cut/src/matcher.rs | 4 +- src/uu/cut/src/searcher.rs | 4 +- src/uu/dd/src/dd.rs | 2 +- src/uu/ls/src/colors.rs | 2 +- src/uu/ls/src/output.rs | 8 +-- src/uu/printf/src/printf.rs | 4 +- src/uu/ptx/src/ptx.rs | 10 ++-- src/uu/rm/src/rm.rs | 5 +- src/uu/shuf/src/shuf.rs | 2 +- src/uu/sort/src/chunks.rs | 14 ++--- src/uu/sort/src/sort.rs | 2 +- src/uucore/src/lib/features/encoding.rs | 4 +- src/uucore/src/lib/features/fsext/windows.rs | 2 +- src/uucore/src/lib/features/i18n/charmap.rs | 2 +- src/uucore/src/lib/mods/locale.rs | 12 ++--- tests/by-util/test_basename.rs | 9 +--- tests/by-util/test_chown.rs | 54 ++++++++++---------- tests/by-util/test_cp.rs | 2 +- tests/by-util/test_dd.rs | 13 +++-- tests/by-util/test_dirname.rs | 4 +- tests/by-util/test_du.rs | 6 +-- tests/by-util/test_echo.rs | 8 +-- tests/by-util/test_head.rs | 6 +-- tests/by-util/test_hostname.rs | 8 +-- tests/by-util/test_id.rs | 2 +- tests/by-util/test_ln.rs | 17 ++---- tests/by-util/test_logname.rs | 2 +- tests/by-util/test_mv.rs | 8 +-- tests/by-util/test_pwd.rs | 2 +- tests/by-util/test_stat.rs | 6 +-- tests/by-util/test_tee.rs | 2 +- tests/by-util/test_truncate.rs | 4 +- tests/uutests/src/lib/util.rs | 12 ++--- 35 files changed, 127 insertions(+), 137 deletions(-) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d7a49a261b4..fe1b807811f 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -973,6 +973,6 @@ mod tests { let (c, a, i) = extract_negative_modes(["--", "-r", "file"].iter().map(OsString::from)); assert_eq!(c, None); assert_eq!(a, ["--", "-r", "file"]); - assert!(i.is_empty()); + assert_eq!(i, [] as [usize; 0]); } } diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 0ba2ce30076..d14b067e915 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -154,7 +154,7 @@ fn check_sparse_detection(src_file: &File) -> io::Result { fn sparse_copy_without_hole_fd(src_file: &File, dst_file: &File, context: &str) -> CopyResult<()> { let ctx_err = |e: io::Error| CpError::IoErrContext(e, context.to_owned()); - let size = src_file.metadata().map_err(&ctx_err)?.size(); + let size = src_file.metadata().map_err(ctx_err)?.size(); ftruncate(dst_file, size).map_err(|e| CpError::IoErrContext(e.into(), context.to_owned()))?; let mut current_offset = 0; // Maximize the data read at once to 16 MiB to avoid memory hogging with large files @@ -178,10 +178,10 @@ fn sparse_copy_without_hole_fd(src_file: &File, dst_file: &File, context: &str) let buf = &mut buf[..read_len]; src_file .read_exact_at(buf, current_offset + i) - .map_err(&ctx_err)?; + .map_err(ctx_err)?; dst_file .write_all_at(buf, current_offset + i) - .map_err(&ctx_err)?; + .map_err(ctx_err)?; } current_offset = hole; } @@ -194,10 +194,10 @@ fn sparse_copy_fd(src_file: &mut File, dst_file: &File, context: &str) -> CopyRe // Keep the size as u64: on 32-bit targets a usize conversion would // panic for sources of 4 GiB and more. - let size = src_file.metadata().map_err(&ctx_err)?.size(); + let size = src_file.metadata().map_err(ctx_err)?.size(); ftruncate(dst_file, size).map_err(|e| CpError::IoErrContext(e.into(), context.to_owned()))?; - let blksize = dst_file.metadata().map_err(&ctx_err)?.blksize(); + let blksize = dst_file.metadata().map_err(ctx_err)?.blksize(); let mut buf: Vec = vec![0; blksize as usize]; let mut current_offset: u64 = 0; @@ -205,7 +205,7 @@ fn sparse_copy_fd(src_file: &mut File, dst_file: &File, context: &str) -> CopyRe // file extent mappings: // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html while current_offset < size { - let this_read = src_file.read(&mut buf).map_err(&ctx_err)?; + let this_read = src_file.read(&mut buf).map_err(ctx_err)?; if this_read == 0 { // EOF before the size seen at fstat time (source truncated // concurrently): shrink the dest to the bytes actually copied @@ -218,7 +218,7 @@ fn sparse_copy_fd(src_file: &mut File, dst_file: &File, context: &str) -> CopyRe if buf.iter().any(|&x| x != 0) { dst_file .write_all_at(buf, current_offset) - .map_err(&ctx_err)?; + .map_err(ctx_err)?; } current_offset += this_read as u64; } @@ -269,15 +269,15 @@ where let ctx_err = |e: io::Error| CpError::IoErrContext(e, context.to_owned()); - let dest_is_stream = is_stream(&dst_file.metadata().map_err(&ctx_err)?); + let dest_is_stream = is_stream(&dst_file.metadata().map_err(ctx_err)?); if !dest_is_stream { // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually. - dst_file.set_len(0).map_err(&ctx_err)?; + dst_file.set_len(0).map_err(ctx_err)?; } buf_copy::copy_fast(&mut src_file, &mut dst_file) .map_err(|e| io::Error::other(format!("{e}"))) - .map_err(&ctx_err)?; + .map_err(ctx_err)?; Ok(()) } diff --git a/src/uu/cut/src/matcher.rs b/src/uu/cut/src/matcher.rs index 036daa6c027..a006204eaa6 100644 --- a/src/uu/cut/src/matcher.rs +++ b/src/uu/cut/src/matcher.rs @@ -21,7 +21,7 @@ pub struct ExactMatcher<'a> { impl<'a> ExactMatcher<'a> { pub fn new(needle: &'a [u8]) -> Self { - assert!(!needle.is_empty()); + assert_ne!(needle, []); Self { needle } } } @@ -51,7 +51,7 @@ pub struct MbExactMatcher<'a> { impl<'a> MbExactMatcher<'a> { pub fn new(needle: &'a [u8]) -> Self { - assert!(!needle.is_empty()); + assert_ne!(needle, []); Self { needle } } } diff --git a/src/uu/cut/src/searcher.rs b/src/uu/cut/src/searcher.rs index a25fa7909a1..ee5626ef833 100644 --- a/src/uu/cut/src/searcher.rs +++ b/src/uu/cut/src/searcher.rs @@ -58,7 +58,7 @@ mod exact_searcher_tests { let matcher = ExactMatcher::new("a".as_bytes()); let iter = Searcher::new(&matcher, "".as_bytes()); let items: Vec<(usize, usize)> = iter.collect(); - assert!(items.is_empty()); + assert_eq!(items, [] as [(usize, usize); 0]); } fn test_multibyte(line: &[u8], expected: &[(usize, usize)]) { @@ -137,7 +137,7 @@ mod whitespace_searcher_tests { let matcher = WhitespaceMatcher {}; let iter = Searcher::new(&matcher, "".as_bytes()); let items: Vec<(usize, usize)> = iter.collect(); - assert!(items.is_empty()); + assert_eq!(items, [] as [(usize, usize); 0]); } fn test_multispace(line: &[u8], expected: &[(usize, usize)]) { diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 7c79bea51a4..b9299fd1d93 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1886,6 +1886,6 @@ mod tests { use crate::AlignedBuf; let buf = AlignedBuf::new(0).unwrap(); - assert!(buf.as_bytes().is_empty()); + assert_eq!(buf.as_bytes(), []); } } diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index cd0b7cccd24..11f4b8675c9 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -148,7 +148,7 @@ impl<'a> StyleManager<'a> { style_code: &mut String, ) { if let Some(raw) = self.indicator_codes.get(&indicator).cloned() { - debug_assert!(!raw.is_empty()); + debug_assert_ne!(raw, ""); style_code.push_str(self.reset(!self.initial_reset_is_done)); style_code.push_str(ANSI_CSI); style_code.push_str(&raw); diff --git a/src/uu/ls/src/output.rs b/src/uu/ls/src/output.rs index a4ead6c111c..e996cdc8093 100644 --- a/src/uu/ls/src/output.rs +++ b/src/uu/ls/src/output.rs @@ -332,8 +332,8 @@ mod tests { fn test_streaming_output_new() { let collector = StreamingOutput::new(); assert!(collector.entries().is_empty()); - assert!(collector.directories().is_empty()); - assert!(collector.totals().is_empty()); + assert_eq!(collector.directories(), Vec::::new()); + assert_eq!(collector.totals(), Vec::::new()); } #[test] @@ -369,8 +369,8 @@ mod tests { collector.clear(); assert!(collector.entries().is_empty()); - assert!(collector.directories().is_empty()); - assert!(collector.totals().is_empty()); + assert_eq!(collector.directories(), Vec::::new()); + assert_eq!(collector.totals(), Vec::::new()); } #[test] diff --git a/src/uu/printf/src/printf.rs b/src/uu/printf/src/printf.rs index 04f5dbfdb93..4d4d979d0c5 100644 --- a/src/uu/printf/src/printf.rs +++ b/src/uu/printf/src/printf.rs @@ -100,7 +100,7 @@ fn print_formatted(args: impl uucore::Args) -> UResult<()> { if let Ok(FormatItem::Spec(_)) = item { format_seen = true; } - match item.map_err(&raise)?.write(stdout(), &mut args)? { + match item.map_err(raise)?.write(stdout(), &mut args)? { ControlFlow::Continue(()) => {} ControlFlow::Break(()) => return Ok(()), } @@ -127,7 +127,7 @@ fn print_formatted(args: impl uucore::Args) -> UResult<()> { while !args.is_exhausted() { for item in parse_spec_and_escape(format) { - match item.map_err(&raise)?.write(stdout(), &mut args)? { + match item.map_err(raise)?.write(stdout(), &mut args)? { ControlFlow::Continue(()) => {} ControlFlow::Break(()) => return Ok(()), } diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index e0f6a9da228..cbf61aadce6 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -1184,8 +1184,8 @@ mod tests { assert_eq!(chunks.keyword, "nut"); assert_eq!(chunks.after, " cake tart pie"); // Nothing wrapped around the ends of the line. - assert!(chunks.head.is_empty()); - assert!(chunks.tail.is_empty()); + assert_eq!(chunks.head, ""); + assert_eq!(chunks.tail, ""); } #[test] @@ -1204,8 +1204,8 @@ mod tests { assert_eq!(chunks.before, "/plum"); // No word at all fit to the right of the keyword. assert_eq!(chunks.after, "/"); - assert!(chunks.head.is_empty()); - assert!(chunks.tail.is_empty()); + assert_eq!(chunks.head, ""); + assert_eq!(chunks.tail, ""); } #[test] @@ -1222,6 +1222,6 @@ mod tests { &chars(" cake tart pie"), ); assert_eq!(chunks.before, "plum"); - assert!(chunks.after.is_empty()); + assert_eq!(chunks.after, ""); } } diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index ca0a04e2130..3049dd5e89e 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -1094,8 +1094,9 @@ fn handle_writable_directory(path: &Path, options: &Options, _metadata: &Metadat let is_accessible = fs::read_dir(path).is_ok(); match (stdin_ok, is_accessible, options.interactive) { - (false, _, InteractiveMode::PromptProtected) => true, - (false, false, InteractiveMode::Never) => true, + (false, _, InteractiveMode::PromptProtected) | (false, false, InteractiveMode::Never) => { + true + } (_, false, _) => prompt_yes!( "attempt removal of inaccessible directory {}?", path.quote() diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 6689f09c902..c3630e10865 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -502,7 +502,7 @@ mod test_split_seps { #[test] fn test_empty_input() { - assert!(split_seps(b"", b'\n').is_empty()); + assert_eq!(split_seps(b"", b'\n'), [] as [&[u8]; 0]); } #[test] diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index ee8261dd856..362cf9bb6a2 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -273,13 +273,13 @@ fn parse_lines<'a>( let read = read.strip_suffix(&[separator]).unwrap_or(read); - assert!(lines.is_empty()); - assert!(line_data.selections.is_empty()); - assert!(line_data.num_infos.is_empty()); - assert!(line_data.parsed_floats.is_empty()); - assert!(line_data.line_num_floats.is_empty()); - assert!(line_data.collation_key_buffer.is_empty()); - assert!(line_data.collation_key_ends.is_empty()); + assert_eq!(lines.len(), 0); + assert_eq!(line_data.selections.len(), 0); + assert_eq!(line_data.num_infos.len(), 0); + assert_eq!(line_data.parsed_floats.len(), 0); + assert_eq!(line_data.line_num_floats.len(), 0); + assert_eq!(line_data.collation_key_buffer.len(), 0); + assert_eq!(line_data.collation_key_ends.len(), 0); token_buffer.clear(); let mut estimated = (*line_count_hint).max(1); let mut exact_line_count = None; diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 5eb997cd6bd..14bdd6e69b3 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -907,7 +907,7 @@ fn tokenize( token_buffer: &mut Vec, precomputed: &Precomputed, ) { - assert!(token_buffer.is_empty()); + assert_eq!(token_buffer.as_slice(), []); if let Some(separator) = separator { tokenize_with_separator(line, separator, token_buffer); } else { diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 4a6e3394835..f441e2e6def 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -44,7 +44,7 @@ impl Base64SimdWrapper { ) -> Self { assert!(valid_decoding_multiple > 0); assert!(unpadded_multiple > 0); - assert!(!alphabet.is_empty()); + assert_ne!(alphabet, []); Self { alphabet, @@ -195,7 +195,7 @@ impl EncodingWrapper { assert!(unpadded_multiple > 0); - assert!(!alphabet.is_empty()); + assert_ne!(alphabet, []); Self { alphabet, diff --git a/src/uucore/src/lib/features/fsext/windows.rs b/src/uucore/src/lib/features/fsext/windows.rs index dd3eb15b077..124ae0d7b0a 100644 --- a/src/uucore/src/lib/features/fsext/windows.rs +++ b/src/uucore/src/lib/features/fsext/windows.rs @@ -310,7 +310,7 @@ mod tests { .all(|m| m.mount_dir.to_string_lossy().ends_with('\\')) ); let system = mounts.iter().find(|m| m.mount_dir == system_drive).unwrap(); - assert!(!system.fs_type.is_empty()); + assert_ne!(system.fs_type, ""); assert_eq!(system.dev_name, system_drive.to_string_lossy()); } } diff --git a/src/uucore/src/lib/features/i18n/charmap.rs b/src/uucore/src/lib/features/i18n/charmap.rs index e20d2d4c67e..44a23fa31a4 100644 --- a/src/uucore/src/lib/features/i18n/charmap.rs +++ b/src/uucore/src/lib/features/i18n/charmap.rs @@ -69,7 +69,7 @@ impl Encoding { /// `bytes.len()`, and never `0` for a non-empty slice. #[inline] pub fn char_len(self, bytes: &[u8]) -> usize { - debug_assert!(!bytes.is_empty()); + debug_assert_ne!(bytes, []); let b0 = bytes[0]; if b0 <= 0x7F { return 1; diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 3c09978ab82..7ab6dc468a3 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -1748,7 +1748,7 @@ invalid-syntax = This is { $missing // Test that we can get messages (should use embedded English for "test" utility) let message = get_message("test-about"); // Since we're using embedded resources, we should get the expected message - assert!(!message.is_empty()); + assert_ne!(message, ""); // Restore original LANG value if let Some(val) = original_lang { @@ -1779,7 +1779,7 @@ invalid-syntax = This is { $missing // Should fall back to English embedded resources let message = get_message("test-about"); - assert!(!message.is_empty()); // Should get something, not just the key + assert_ne!(message, ""); // Should get something, not just the key // Restore original LANG value if let Some(val) = original_lang { @@ -1858,18 +1858,18 @@ invalid-syntax = This is { $missing // Test that common strings are available after initialization let error_after_init = get_message("common-error"); // Should either be translated or return the key (but not panic) - assert!(!error_after_init.is_empty()); + assert_ne!(error_after_init, ""); let tip_after_init = get_message("common-tip"); - assert!(!tip_after_init.is_empty()); + assert_ne!(tip_after_init, ""); // Test that clap error keys work with fallbacks let unknown_arg_key = get_message("clap-error-unexpected-argument"); - assert!(!unknown_arg_key.is_empty()); + assert_ne!(unknown_arg_key, ""); // Test usage key fallback let usage_key = get_message("common-usage"); - assert!(!usage_key.is_empty()); + assert_ne!(usage_key, ""); }) .join() .unwrap(); diff --git a/tests/by-util/test_basename.rs b/tests/by-util/test_basename.rs index 2b85116e064..f31b6d8736c 100644 --- a/tests/by-util/test_basename.rs +++ b/tests/by-util/test_basename.rs @@ -123,14 +123,7 @@ fn test_zero_param() { } fn expect_error(input: &[&str]) { - assert!( - !new_ucmd!() - .args(input) - .fails() - .no_stdout() - .stderr_str() - .is_empty() - ); + assert_ne!(new_ucmd!().args(input).fails().no_stdout().stderr_str(), ""); } #[test] diff --git a/tests/by-util/test_chown.rs b/tests/by-util/test_chown.rs index 34b37e69e60..a7b8d1dbbd0 100644 --- a/tests/by-util/test_chown.rs +++ b/tests/by-util/test_chown.rs @@ -95,7 +95,7 @@ fn test_chown_only_owner() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -132,7 +132,7 @@ fn test_chown_only_owner_colon() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -176,7 +176,7 @@ fn test_chown_dot_separator_warning() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let file1 = "test_chown_dot_warn"; at.touch(file1); @@ -186,7 +186,7 @@ fn test_chown_dot_separator_warning() { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); // chown user. file should warn about '.' separator scene @@ -294,7 +294,7 @@ fn test_chown_owner_group() { } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -304,7 +304,7 @@ fn test_chown_owner_group() { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); let result = scene .ucmd() @@ -356,7 +356,7 @@ fn test_chown_various_input() { } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -366,7 +366,7 @@ fn test_chown_various_input() { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); let result = scene .ucmd() @@ -415,7 +415,7 @@ fn test_chown_only_group() { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -453,7 +453,7 @@ fn test_chown_only_user_id() { return; } let user_id = String::from(result.stdout_str().trim()); - assert!(!user_id.is_empty()); + assert_ne!(user_id, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -487,7 +487,7 @@ fn test_chown_fail_id() { return; } let user_id = String::from(result.stdout_str().trim()); - assert!(!user_id.is_empty()); + assert_ne!(user_id, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -543,7 +543,7 @@ fn test_chown_only_group_id() { return; } let group_id = String::from(result.stdout_str().trim()); - assert!(!group_id.is_empty()); + assert_ne!(group_id, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -611,14 +611,14 @@ fn test_chown_owner_group_id() { return; } let user_id = String::from(result.stdout_str().trim()); - assert!(!user_id.is_empty()); + assert_ne!(user_id, ""); let result = scene.cmd("id").arg("-g").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_id = String::from(result.stdout_str().trim()); - assert!(!group_id.is_empty()); + assert_ne!(group_id, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -671,14 +671,14 @@ fn test_chown_owner_group_mix() { return; } let user_id = String::from(result.stdout_str().trim()); - assert!(!user_id.is_empty()); + assert_ne!(user_id, ""); let result = scene.cmd("id").arg("-gn").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); let file1 = "test_chown_file1"; at.touch(file1); @@ -710,7 +710,7 @@ fn test_chown_recursive() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); at.mkdir_all("a/b/c"); at.mkdir("z"); @@ -740,7 +740,7 @@ fn test_root_preserve() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let result = scene .ucmd() @@ -782,7 +782,7 @@ fn test_chown_file_notexisting() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); scene .ucmd() @@ -807,7 +807,7 @@ fn test_chown_no_change_to_user() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); for (i, from) in ["42", ":42", "42:42"].iter().enumerate() { let file = i.to_string(); @@ -834,13 +834,13 @@ fn test_chown_no_change_to_group() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); for (i, from) in ["42", ":42", "42:42"].iter().enumerate() { let file = i.to_string(); @@ -867,13 +867,13 @@ fn test_chown_no_change_to_user_group() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); let result = scene.cmd("id").arg("-ng").run(); if skipping_test_is_okay(&result, "id: cannot find name for group ID") { return; } let group_name = String::from(result.stdout_str().trim()); - assert!(!group_name.is_empty()); + assert_ne!(group_name, ""); for (i, from) in ["42", ":42", "42:42"].iter().enumerate() { let file = i.to_string(); @@ -936,7 +936,7 @@ fn test_chown_no_dereference_symlink_to_dir() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); at.mkdir("dir"); at.symlink_dir("dir", "link_to_dir"); @@ -980,7 +980,7 @@ fn test_chown_symlink_cycles() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); at.mkdir_all("a/b/c"); at.symlink_dir("a", "a/b/c/d"); @@ -1028,7 +1028,7 @@ fn test_chown_symlink_two_links_same_dir() { return; } let user_name = String::from(result.stdout_str().trim()); - assert!(!user_name.is_empty()); + assert_ne!(user_name, ""); // spell-checker:disable at.mkdir_all("base/realdir"); diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 00575922ef2..ebbbc3e6b79 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -6121,7 +6121,7 @@ fn test_cp_debug_sparse_never_zero_sized_virtual_file() { )] fn test_cp_zero_sized_virtual_file_contents() { let expected = std::fs::read_to_string("/proc/version").unwrap(); - assert!(!expected.is_empty()); + assert_ne!(expected, ""); for extra in [ &[][..], diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 71040e9241f..a91cd5d0546 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1616,7 +1616,10 @@ fn test_sparse() { /// Test that a seek on an output FIFO results in a read. #[test] #[cfg(unix)] -#[cfg_attr(wasi_runner, ignore = "WASI sandbox: hangs reading from a fifo opened for output")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: hangs reading from a fifo opened for output" +)] fn test_seek_output_fifo() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -2285,7 +2288,7 @@ fn test_locale_aware_case_conversion() { } else { println!("Turkish locale not available, using fallback behavior"); // Test that it at least doesn't crash and produces some output - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } } @@ -2310,7 +2313,7 @@ fn test_french_locale_case_conversion() { } else { println!("French locale not available, using fallback behavior"); // Test that it at least doesn't crash and produces some output - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } // Test reverse conversion: à (0xE0) should convert to À (0xC0) with ucase @@ -2326,7 +2329,7 @@ fn test_french_locale_case_conversion() { println!("French locale case conversion working correctly for à -> À"); } else { println!("French locale not available for reverse conversion, using fallback behavior"); - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } // Test another French character: Ç (0xC7) should convert to ç (0xE7) with lcase @@ -2342,7 +2345,7 @@ fn test_french_locale_case_conversion() { println!("French locale case conversion working correctly for Ç -> ç"); } else { println!("French locale not available for Ç conversion, using fallback behavior"); - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } } diff --git a/tests/by-util/test_dirname.rs b/tests/by-util/test_dirname.rs index 3ed660877ef..09b7fad696d 100644 --- a/tests/by-util/test_dirname.rs +++ b/tests/by-util/test_dirname.rs @@ -84,7 +84,7 @@ fn test_dirname_non_utf8_paths() { let result = new_ucmd!().arg(non_utf8_name).succeeds(); let output = result.stdout_str_lossy(); - assert!(!output.is_empty()); + assert_ne!(output, ""); assert!(output.contains("test_")); } @@ -190,7 +190,7 @@ fn test_trailing_dot_non_utf8() { let result = new_ucmd!().arg(non_utf8_path).succeeds(); let output = result.stdout_str_lossy(); - assert!(!output.is_empty()); + assert_ne!(output, ""); assert!(output.contains("test_")); assert!(!output.trim().ends_with('.')); } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 39cc4626997..6427000ace5 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -2227,10 +2227,10 @@ fn test_du_safe_traversal_with_symlinks() { at.symlink_file(&format!("{deep_path}/target.txt"), "shallow_link.txt"); let result = ts.ucmd().arg("-L").arg("shallow_link.txt").succeeds(); - assert!(!result.stdout_str().is_empty()); + assert_ne!(result.stdout_str(), ""); let result = ts.ucmd().arg("shallow_link.txt").succeeds(); - assert!(!result.stdout_str().is_empty()); + assert_ne!(result.stdout_str(), ""); } #[test] #[cfg(unix)] @@ -2375,7 +2375,7 @@ fn test_du_long_path_from_unreadable() { let target_path = at.plus(&dir_name); let result = ts.ucmd().arg("-s").arg(&target_path).succeeds(); // Should succeed with safe traversal - assert!(!result.stdout_str().is_empty()); + assert_ne!(result.stdout_str(), ""); let output = result.stdout_str().trim(); let parts: Vec<&str> = output.split_whitespace().collect(); assert_eq!(parts.len(), 2); diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 827005ba278..1a1a229fec3 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -855,17 +855,17 @@ fn test_uchild_when_run_no_wait_with_a_non_blocking_util() { // check the current output is correct std::assert_eq!(child.stdout(), "hello world\n"); - assert!(child.stderr().is_empty()); + assert_eq!(child.stderr(), ""); // check the current output of echo is empty. We already called `child.stdout()` and `echo` // exited so there's no additional output after the first call of `child.stdout()` - assert!(child.stdout().is_empty()); - assert!(child.stderr().is_empty()); + assert_eq!(child.stdout(), ""); + assert_eq!(child.stderr(), ""); // check that we're still able to access all output of the child process, even after exit // and call to `child.stdout()` std::assert_eq!(child.stdout_all(), "hello world\n"); - assert!(child.stderr_all().is_empty()); + assert_eq!(child.stderr_all(), ""); // we should be able to call kill without panics, even if the process already exited child.make_assertion().is_not_alive(); diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index eb9577afcfc..a3e79a2aa6c 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -823,7 +823,7 @@ fn test_read_backwards_bytes_proc_fs_version() { let args = ["-c", "-1", "/proc/version"]; let result = ts.ucmd().args(&args).succeeds(); - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } #[cfg(all( @@ -843,7 +843,7 @@ fn test_read_backwards_bytes_proc_fs_modules() { // Only expect output if the file is not empty, e.g. it is empty in default WSL2. if !ts.fixtures.read("/proc/modules").is_empty() { - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } } @@ -864,7 +864,7 @@ fn test_read_backwards_lines_proc_fs_modules() { // Only expect output if the file is not empty, e.g. it is empty in default WSL2. if !ts.fixtures.read("/proc/modules").is_empty() { - assert!(!result.stdout().is_empty()); + assert_ne!(result.stdout(), []); } } diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index dc0b7510192..04b0d1423f7 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -18,13 +18,13 @@ fn test_hostname() { #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").succeeds(); - assert!(!result.stdout_str().trim().is_empty()); + assert_ne!(result.stdout_str().trim(), ""); } #[test] fn test_hostname_full() { let ls_short_res = new_ucmd!().arg("-s").succeeds(); - assert!(!ls_short_res.stdout_str().trim().is_empty()); + assert_ne!(ls_short_res.stdout_str().trim(), ""); new_ucmd!() .arg("-f") @@ -45,7 +45,7 @@ fn test_hostname_domain_empty() { let domain_short = new_ucmd!().arg("-sd").succeeds(); if fqdn.stdout() == short.stdout() { - assert!(domain.stdout().is_empty()); - assert!(domain_short.stdout().is_empty()); + assert_eq!(domain.stdout(), []); + assert_eq!(domain_short.stdout(), []); } } diff --git a/tests/by-util/test_id.rs b/tests/by-util/test_id.rs index 30e4c1ac9a0..71b6d89f3ec 100644 --- a/tests/by-util/test_id.rs +++ b/tests/by-util/test_id.rs @@ -161,7 +161,7 @@ fn test_id_groups_ordering() { let groups = ts.ucmd().arg("-G").succeeds().stdout_move_str(); let mut from_flag: Vec<&str> = groups.split_whitespace().collect(); - assert!(!from_flag.is_empty()); + assert_ne!(from_flag, [] as [&str; 0]); // `-G` heads with the real gid let rgid = ts.ucmd().args(&["-g", "-r"]).succeeds().stdout_move_str(); diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index ddd5e70463b..2d17c030e15 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -546,12 +546,11 @@ fn test_symlink_overwrite_dir_fail() { at.touch(path_a); at.mkdir(path_b); - assert!( - !ucmd - .args(&["-s", "-T", path_a, path_b]) + assert_ne!( + ucmd.args(&["-s", "-T", path_a, path_b]) .fails() - .stderr_str() - .is_empty() + .stderr_str(), + "" ); } @@ -602,13 +601,7 @@ fn test_symlink_target_only() { at.mkdir(dir); - assert!( - !ucmd - .args(&["-s", "-t", dir]) - .fails() - .stderr_str() - .is_empty() - ); + assert_ne!(ucmd.args(&["-s", "-t", dir]).fails().stderr_str(), ""); } #[test] diff --git a/tests/by-util/test_logname.rs b/tests/by-util/test_logname.rs index c3ff0458670..43fa4a1d5d1 100644 --- a/tests/by-util/test_logname.rs +++ b/tests/by-util/test_logname.rs @@ -29,7 +29,7 @@ fn test_normal() { } result.success(); - assert!(!result.stdout_str().trim().is_empty()); + assert_ne!(result.stdout_str().trim(), ""); } #[test] diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 45cc2bb63a6..202982b1aa4 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1727,16 +1727,16 @@ fn test_mv_interactive_error() { // $ at.mkdir dir && at.touch file // $ mv -i dir file // err == mv: cannot overwrite non-directory 'file' with directory 'dir' - assert!( - !scene + assert_ne!( + scene .ucmd() .arg("-i") .arg(dir) .arg(file_a) .pipe_in("y") .fails() - .stderr_str() - .is_empty() + .stderr_str(), + "" ); } diff --git a/tests/by-util/test_pwd.rs b/tests/by-util/test_pwd.rs index ee3a50b08fc..ae16d4f3038 100644 --- a/tests/by-util/test_pwd.rs +++ b/tests/by-util/test_pwd.rs @@ -64,7 +64,7 @@ fn test_deleted_dir() { .output() .unwrap(); assert!(!output.status.success()); - assert!(output.stdout.is_empty()); + assert_eq!(output.stdout, [] as [u8; 0]); assert_eq!( String::from_utf8_lossy(&output.stderr), "pwd: failed to get current directory: No such file or directory\n" diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index ae27fa53294..2206dc5b2d7 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -113,7 +113,7 @@ fn test_terse_normal_format() { println!("expect: {expect:?}"); let v_actual: Vec<&str> = actual.trim().split(' ').collect(); let mut v_expect: Vec<&str> = expect.trim().split(' ').collect(); - assert!(!v_expect.is_empty()); + assert_ne!(v_expect, [] as [&str; 0]); // uu_stat does not support selinux if v_actual.len() == v_expect.len() - 1 && v_expect[v_expect.len() - 1].contains(':') { @@ -145,7 +145,7 @@ fn test_format_created_time() { let re = regex::Regex::new(r"\s").unwrap(); let v_actual: Vec<&str> = re.split(&actual).collect(); let v_expect: Vec<&str> = re.split(&expect).collect(); - assert!(!v_expect.is_empty()); + assert_ne!(v_expect, [] as [&str; 0]); // * allow for inequality if `stat` (aka, expect) returns "-" (unknown value) assert!( expect == "-" @@ -170,7 +170,7 @@ fn test_format_created_seconds() { let re = regex::Regex::new(r"\s").unwrap(); let v_actual: Vec<&str> = re.split(&actual).collect(); let v_expect: Vec<&str> = re.split(&expect).collect(); - assert!(!v_expect.is_empty()); + assert_ne!(v_expect, [] as [&str; 0]); // * allow for inequality if `stat` (aka, expect) returns "0" (unknown value) assert!( expect == "0" diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index f8bad5aa834..0cb033d6fca 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -777,5 +777,5 @@ fn test_write_failure_reports_error_and_nonzero_exit() { let result = ucmd.arg("out_dir").pipe_in("data").fails(); - assert!(!result.stderr_str().is_empty()); + assert_ne!(result.stderr_str(), ""); } diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 841fd53a9fe..3fbe0b61ba1 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -494,7 +494,7 @@ fn test_underflow_relative_size() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-s-1", FILE1]).succeeds().no_output(); assert!(at.file_exists(FILE1)); - assert!(at.read_bytes(FILE1).is_empty()); + assert_eq!(at.read_bytes(FILE1), [] as [u8; 0]); } #[test] @@ -502,7 +502,7 @@ fn test_negative_size_with_space() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["-s", "-1", FILE1]).succeeds().no_output(); assert!(at.file_exists(FILE1)); - assert!(at.read_bytes(FILE1).is_empty()); + assert_eq!(at.read_bytes(FILE1), [] as [u8; 0]); } #[test] diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 889b59cb879..d0501abf5f0 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -3355,8 +3355,8 @@ mod tests { assert!(!result.succeeded()); result.failure(); result.fails_silently(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); + std::assert_eq!(result.stderr, Vec::new()); + std::assert_eq!(result.stdout, Vec::new()); result.no_output(); result.no_stderr(); result.no_stdout(); @@ -3377,8 +3377,8 @@ mod tests { result.code_is(0); assert!(result.succeeded()); result.success(); - assert!(result.stderr.is_empty()); - assert!(result.stdout.is_empty()); + std::assert_eq!(result.stderr, Vec::new()); + std::assert_eq!(result.stdout, Vec::new()); result.no_output(); result.no_stderr(); result.no_stdout(); @@ -3416,7 +3416,7 @@ mod tests { result.code_is(0); assert!(result.succeeded()); result.success(); - assert!(result.stderr.is_empty()); + std::assert_eq!(result.stderr, Vec::new()); std::assert_eq!(result.stdout, vector); result.no_stderr(); result.stdout_is(string); @@ -3445,7 +3445,7 @@ mod tests { result.code_is(0); assert!(result.succeeded()); result.success(); - assert!(result.stdout.is_empty()); + std::assert_eq!(result.stdout, Vec::new()); result.no_stdout(); std::assert_eq!(result.stderr, vector); result.stderr_is(string);