diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 6fd3dadcea3..e1fffb92555 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -128,6 +128,7 @@ ENOSYS ENOTEMPTY EOPNOTSUPP EPERM +EPIPE EROFS # * vars/fcntl diff --git a/src/uu/chcon/src/main.rs b/src/uu/chcon/src/main.rs index d1354d840af..4cfde914e79 100644 --- a/src/uu/chcon/src/main.rs +++ b/src/uu/chcon/src/main.rs @@ -1,2 +1,9 @@ -#![cfg(target_os = "linux")] +// On non-Linux targets, provide a stub main to keep the binary target present +// and the workspace buildable. Using item-level cfg avoids excluding the crate +// entirely (via #![cfg(...)]), which can break tooling and cross builds that +// expect this binary to exist even when it’s a no-op off Linux. +#[cfg(target_os = "linux")] uucore::bin!(uu_chcon); + +#[cfg(not(target_os = "linux"))] +fn main() {} diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e677e3b16a1..6fd16fdedc9 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1085,11 +1085,11 @@ impl Config { time_format_older, context, selinux_supported: { - #[cfg(feature = "selinux")] + #[cfg(all(feature = "selinux", target_os = "linux"))] { uucore::selinux::is_selinux_enabled() } - #[cfg(not(feature = "selinux"))] + #[cfg(not(all(feature = "selinux", target_os = "linux")))] { false } @@ -3309,7 +3309,7 @@ fn get_security_context<'a>( } if config.selinux_supported { - #[cfg(feature = "selinux")] + #[cfg(all(feature = "selinux", target_os = "linux"))] { match selinux::SecurityContext::of_path(path, must_dereference, false) { Err(_r) => { diff --git a/src/uu/runcon/src/main.rs b/src/uu/runcon/src/main.rs index ab4c4b15944..65d8ed66aab 100644 --- a/src/uu/runcon/src/main.rs +++ b/src/uu/runcon/src/main.rs @@ -1,2 +1,9 @@ -#![cfg(target_os = "linux")] +// On non-Linux targets, provide a stub main to keep the binary target present +// and the workspace buildable. Using item-level cfg avoids excluding the crate +// entirely (via #![cfg(...)]), which can break tooling and cross builds that +// expect this binary to exist even when it’s a no-op off Linux. +#[cfg(target_os = "linux")] uucore::bin!(uu_runcon); + +#[cfg(not(target_os = "linux"))] +fn main() {} diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index c809231c7b0..ea3250e0526 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -18,6 +18,32 @@ use uutests::util::TestScenario; use uutests::util::vec_of_size; use uutests::util_name; +#[cfg(unix)] +// Verify cat handles a broken pipe on stdout without hanging or crashing and exits nonzero +#[test] +fn test_cat_broken_pipe_nonzero_and_message() { + use std::fs::File; + use std::os::unix::io::FromRawFd; + use uutests::new_ucmd; + + unsafe { + let mut fds: [libc::c_int; 2] = [0, 0]; + assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "Failed to create pipe"); + // Close the read end to simulate a broken pipe on stdout + let read_end = File::from_raw_fd(fds[0]); + // Explicitly drop the read-end so writers see EPIPE instead of blocking on a full pipe + std::mem::drop(read_end); + let write_end = File::from_raw_fd(fds[1]); + + let content = (0..10000).map(|_| "x").collect::(); + // On Unix, SIGPIPE should lead to a non-zero exit; ensure process exits and fails + new_ucmd!() + .set_stdout(write_end) + .pipe_in(content.as_bytes()) + .fails(); + } +} + #[test] fn test_output_simple() { new_ucmd!() diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index ec37f78b4c5..512ea11ab73 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -395,34 +395,36 @@ fn test_total() { // ... // /dev/loop14 63488 63488 0 100% /snap/core20/1361 // total 258775268 98099712 148220200 40% - - let output = new_ucmd!().arg("--total").succeeds().stdout_str_lossy(); + // Use explicit numeric-only columns to avoid whitespace issues in Filesystem or Mounted on columns + let output = new_ucmd!() + .arg("--output=size,used,avail") + .arg("--total") + .succeeds() + .stdout_str_lossy(); // Skip the header line. let lines: Vec<&str> = output.lines().skip(1).collect(); - // Parse the values from the last row. + // Parse the values from the last row (report totals) let last_line = lines.last().unwrap(); let mut iter = last_line.split_whitespace(); - assert_eq!(iter.next().unwrap(), "total"); - let reported_total_size = iter.next().unwrap().parse().unwrap(); - let reported_total_used = iter.next().unwrap().parse().unwrap(); - let reported_total_avail = iter.next().unwrap().parse().unwrap(); + let reported_total_size: u64 = iter.next().unwrap().parse().unwrap(); + let reported_total_used: u64 = iter.next().unwrap().parse().unwrap(); + let reported_total_avail: u64 = iter.next().unwrap().parse().unwrap(); // Loop over each row except the last, computing the sum of each column. - let mut computed_total_size = 0; - let mut computed_total_used = 0; - let mut computed_total_avail = 0; + let mut computed_total_size: u64 = 0; + let mut computed_total_used: u64 = 0; + let mut computed_total_avail: u64 = 0; let n = lines.len(); for line in &lines[..n - 1] { let mut iter = line.split_whitespace(); - iter.next().unwrap(); computed_total_size += iter.next().unwrap().parse::().unwrap(); computed_total_used += iter.next().unwrap().parse::().unwrap(); computed_total_avail += iter.next().unwrap().parse::().unwrap(); } - // Check that the sum of each column matches the reported value in - // the last row. + // Check that the sum of each column matches the reported value in the last row. assert_eq!(computed_total_size, reported_total_size); assert_eq!(computed_total_used, reported_total_used); assert_eq!(computed_total_avail, reported_total_avail); diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index a9ab7d7df2e..6563391f642 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo +// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo timefile // spell-checker:ignore (words) fakeroot setcap drwxr bcdlps #![allow( clippy::similar_names, @@ -6163,3 +6163,176 @@ fn ls_emoji_alignment() { .stdout_contains("💐") .stdout_contains("漢"); } + +// Additional tests for TIME_STYLE and time sorting compatibility with GNU +#[test] +fn test_ls_time_style_env_full_iso() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("t1"); + + let out = scene + .ucmd() + .env("TZ", "UTC") + .env("TIME_STYLE", "full-iso") + .arg("-l") + .arg("t1") + .succeeds(); + + // Expect an ISO-like timestamp in output (YYYY-MM-DD HH:MM) + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}").unwrap(); + assert!( + re.is_match(out.stdout_str()), + "unexpected timestamp: {}", + out.stdout_str() + ); +} + +#[test] +fn test_ls_time_style_iso_recent_and_older() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + // Recent file (now) + at.touch("recent"); + + // Recent format for --time-style=iso is %m-%d %H:%M + let recent = scene + .ucmd() + .env("TZ", "UTC") + .arg("-l") + .arg("--time-style=iso") + .arg("recent") + .succeeds(); + let re_recent = Regex::new(r"(^|\n).*\d{2}-\d{2} \d{2}:\d{2} ").unwrap(); + assert!( + re_recent.is_match(recent.stdout_str()), + "recent not matched: {}", + recent.stdout_str() + ); + + // Older format appends a full ISO date padded (year present) + // Only run this part when the test binary includes `touch` + #[cfg(feature = "touch")] + { + // Older file: set mtime to 1970-01-01 using uutils touch + scene + .ccmd("touch") + .args(&["-d", "1970-01-01", "older"]) // RFC3339-ish date understood by GNU and uutils touch + .succeeds(); + + let older = scene + .ucmd() + .arg("-l") + .arg("--time-style=iso") + .arg("older") + .succeeds(); + let re_older = Regex::new(r"(^|\n).*\d{4}-\d{2}-\d{2} +").unwrap(); + assert!( + re_older.is_match(older.stdout_str()), + "older not matched: {}", + older.stdout_str() + ); + } +} + +#[test] +fn test_ls_time_style_posix_locale_override() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("p1"); + + // With LC_ALL=POSIX and TIME_STYLE=posix-full-iso, GNU falls back to locale format like "%b %e %H:%M" + let out = scene + .ucmd() + .env("TZ", "UTC") + .env("LC_ALL", "POSIX") + .env("TIME_STYLE", "posix-full-iso") + .arg("-l") + .arg("p1") + .succeeds(); + // Expect month name rather than ISO dashes + let re_locale = Regex::new(r" [A-Z][a-z]{2} +\d{1,2} +\d{2}:\d{2} ").unwrap(); + assert!( + re_locale.is_match(out.stdout_str()), + "locale format not matched: {}", + out.stdout_str() + ); +} + +#[test] +fn test_ls_time_style_precedence_last_wins() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("timefile"); + + // time-style first, full-time last -> expect full-iso-like (seconds) + let out1 = scene + .ucmd() + .arg("--time-style=long-iso") + .arg("--full-time") + .arg("-l") + .arg("timefile") + .succeeds(); + let has_seconds = Regex::new(r"\d{2}:\d{2}:\d{2}") + .unwrap() + .is_match(out1.stdout_str()); + assert!( + has_seconds, + "expected seconds in full-time: {}", + out1.stdout_str() + ); + + // full-time first, time-style last -> expect style override (no seconds for long-iso) + let out2 = scene + .ucmd() + .arg("--full-time") + .arg("--time-style=long-iso") + .arg("-l") + .arg("timefile") + .succeeds(); + let no_seconds = !Regex::new(r"\d{2}:\d{2}:\d{2}") + .unwrap() + .is_match(out2.stdout_str()); + assert!( + no_seconds, + "expected no seconds in long-iso: {}", + out2.stdout_str() + ); +} + +#[test] +fn test_ls_time_sort_without_long() { + let scene = TestScenario::new(util_name!()); + + // Create two files with deterministic, distinct modification times using touch -d + #[cfg(feature = "touch")] + { + scene + .ccmd("touch") + .args(&["-d", "1970-01-01 00:00:00 UTC", "a"]) + .succeeds(); + scene + .ccmd("touch") + .args(&["-d", "1970-01-02 00:00:00 UTC", "b"]) + .succeeds(); + } + #[cfg(not(feature = "touch"))] + { + let at = &scene.fixtures; + at.touch("a"); + // Fallback: sleep long enough to ensure FS timestamp resolution differences + std::thread::sleep(Duration::from_secs(2)); + at.touch("b"); + } + + // Compare default (name order) vs time-sorted (-t) order; they should differ + let default_out = scene.ucmd().succeeds(); + let t_out = scene.ucmd().arg("-t").succeeds(); + + let def = default_out.stdout_str(); + let t = t_out.stdout_str(); + assert_ne!( + def.lines().next().unwrap_or(""), + t.lines().next().unwrap_or("") + ); +} diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 8c3fef5870d..d2421cfbef0 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -15,6 +15,7 @@ fn invalid_input() { new_ucmd!().arg("-/").fails_with_code(125); } +#[cfg(not(feature = "feat_external_libstdbuf"))] #[test] fn test_permission() { new_ucmd!() @@ -24,6 +25,23 @@ fn test_permission() { .stderr_contains("Permission denied"); } +// TODO: Tests below are brittle when feat_external_libstdbuf is enabled and libstdbuf is not installed. +// Align stdbuf with GNU search order to enable deterministic testing without installation: +// 1) search for libstdbuf next to the stdbuf binary, 2) then in LIBSTDBUF_DIR, 3) then system locations. +// After implementing this, rework tests to provide a temporary symlink rather than depending on system state. + +#[cfg(feature = "feat_external_libstdbuf")] +#[test] +fn test_permission_external_missing_lib() { + // When built with external libstdbuf, running stdbuf fails early if lib is not installed + new_ucmd!() + .arg("-o1") + .arg(".") + .fails_with_code(1) + .stderr_contains("External libstdbuf not found"); +} + +#[cfg(not(feature = "feat_external_libstdbuf"))] #[test] fn test_no_such() { new_ucmd!() @@ -33,6 +51,17 @@ fn test_no_such() { .stderr_contains("No such file or directory"); } +#[cfg(feature = "feat_external_libstdbuf")] +#[test] +fn test_no_such_external_missing_lib() { + // With external lib mode and missing installation, stdbuf fails before spawning the command + new_ucmd!() + .arg("-o1") + .arg("no_such") + .fails_with_code(1) + .stderr_contains("External libstdbuf not found"); +} + // Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target // does not provide musl-compiled system utilities (like head), leading to dynamic linker errors // when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. diff --git a/tests/by-util/test_tee.rs b/tests/by-util/test_tee.rs index 10596f02ce8..ba6993371d1 100644 --- a/tests/by-util/test_tee.rs +++ b/tests/by-util/test_tee.rs @@ -623,3 +623,93 @@ mod linux_only { assert!(result.stderr_str().contains("No space left on device")); } } + +// Additional cross-platform tee tests to cover GNU compatibility around --output-error +#[test] +fn test_output_error_flag_without_value_defaults_warn_nopipe() { + // When --output-error is present without an explicit value, it should default to warn-nopipe + // We can't easily simulate a broken pipe across all platforms here, but we can ensure + // the flag is accepted without error and basic tee functionality still works. + let (at, mut ucmd) = at_and_ucmd!(); + let file_out = "tee_output_error_default.txt"; + let content = "abc"; + + let result = ucmd + .arg("--output-error") + .arg(file_out) + .pipe_in(content) + .succeeds(); + + result.stdout_is(content); + assert!(at.file_exists(file_out)); + assert_eq!(at.read(file_out), content); +} +// Unix-only: presence-only --output-error should not crash on broken pipe. +// Current implementation may exit zero; we only assert the process exits to avoid flakiness. +// TODO: When semantics are aligned with GNU warn-nopipe, strengthen assertions here. +#[cfg(all(unix, not(target_os = "freebsd")))] +#[test] +fn test_output_error_presence_only_broken_pipe_unix() { + use std::fs::File; + use std::os::unix::io::FromRawFd; + + unsafe { + let mut fds: [libc::c_int; 2] = [0, 0]; + assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "Failed to create pipe"); + // Close the read end to simulate a broken pipe on stdout + let _read_end = File::from_raw_fd(fds[0]); + let write_end = File::from_raw_fd(fds[1]); + + let content = (0..10_000).map(|_| "x").collect::(); + let result = new_ucmd!() + .arg("--output-error") // presence-only flag + .set_stdout(write_end) + .pipe_in(content.as_bytes()) + .run(); + + // Assert that a status was produced (i.e., process exited) and no crash occurred. + assert!(result.try_exit_status().is_some(), "process did not exit"); + } +} + +// Skip on FreeBSD due to repeated CI hangs in FreeBSD VM (see PR #8684) +#[cfg(all(unix, not(target_os = "freebsd")))] +#[test] +fn test_broken_pipe_early_termination_stdout_only() { + use std::fs::File; + use std::os::unix::io::FromRawFd; + + // Create a broken stdout by creating a pipe and dropping the read end + unsafe { + let mut fds: [libc::c_int; 2] = [0, 0]; + assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "Failed to create pipe"); + // Close the read end immediately to simulate a broken pipe + let _read_end = File::from_raw_fd(fds[0]); + let write_end = File::from_raw_fd(fds[1]); + + let content = (0..10_000).map(|_| "x").collect::(); + let mut proc = new_ucmd!(); + let result = proc + .set_stdout(write_end) + .ignore_stdin_write_error() + .pipe_in(content.as_bytes()) + .run(); + + // GNU tee exits nonzero on broken pipe unless configured otherwise; implementation + // details vary by mode, but we should not panic and should return an exit status. + // Assert that a status was produced (i.e., process exited) and no crash occurred. + assert!(result.try_exit_status().is_some(), "process did not exit"); + } +} + +#[test] +fn test_write_failure_reports_error_and_nonzero_exit() { + // Simulate a file open failure which should be reported via show_error and cause a failure + let (at, mut ucmd) = at_and_ucmd!(); + // Create a directory and try to use it as an output file (open will fail) + at.mkdir("out_dir"); + + let result = ucmd.arg("out_dir").pipe_in("data").fails(); + + assert!(!result.stderr_str().is_empty()); +}