Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
99dff95
tests(ls, tee, cat): expand GNU-compat coverage for TIME_STYLE, broke…
naoNao89 Sep 20, 2025
db3e7d3
chore(fmt): rustfmt test_cat.rs after test cleanup
naoNao89 Sep 20, 2025
0b25819
tests(ls): make TIME_STYLE iso recent/older test independent of 'touc…
naoNao89 Sep 21, 2025
3107fe3
chore(spell): rename test temp filename to avoid cspell false positives
naoNao89 Sep 21, 2025
0a5e2b0
ci(spell): add 'timefile' to cspell ignore list in test_ls.rs
naoNao89 Sep 21, 2025
c72abe0
tests: fix cat broken-pipe flake; make stdbuf tests feature-aware
naoNao89 Sep 21, 2025
6ac5031
linux: gate SELinux code to Linux; fix cat broken pipe test
naoNao89 Sep 21, 2025
fd1cfb2
cspell: whitelist errno EPIPE in workspace dictionary
naoNao89 Sep 21, 2025
211eaed
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 21, 2025
30835b7
tests(tee): skip broken-pipe early termination test on FreeBSD to avo…
naoNao89 Sep 22, 2025
1c3dcb9
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 22, 2025
2750da2
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 22, 2025
4ffe334
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 24, 2025
47859f7
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 26, 2025
82646c7
test: address review feedback and harden GNU-compat tests; fix SELinu…
naoNao89 Sep 26, 2025
4e90de7
tests: address review feedback and stabilize GNU-compat coverage for …
naoNao89 Sep 29, 2025
09ded48
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 29, 2025
e8aba43
tests(ls): fix unused variable in test_ls_time_sort_without_long when…
naoNao89 Sep 29, 2025
b34140c
Merge branch 'main' into test/gnu-compat_write-errors_and_ls-time-sty…
naoNao89 Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/cspell.dictionaries/workspace.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ ENOTEMPTY
EOPNOTSUPP
EPERM
EROFS
EPIPE


# * vars/fcntl
F_GETFL
Expand Down
9 changes: 8 additions & 1 deletion src/uu/chcon/src/main.rs
Original file line number Diff line number Diff line change
@@ -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() {}
6 changes: 3 additions & 3 deletions src/uu/ls/src/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1083,11 +1083,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
}
Expand Down Expand Up @@ -3273,7 +3273,7 @@ fn get_security_context(config: &Config, p_buf: &Path, must_dereference: bool) -
}
}
if config.selinux_supported {
#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
{
match selinux::SecurityContext::of_path(p_buf, must_dereference.to_owned(), false) {
Err(_r) => {
Expand Down
9 changes: 8 additions & 1 deletion src/uu/runcon/src/main.rs
Original file line number Diff line number Diff line change
@@ -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() {}
28 changes: 28 additions & 0 deletions tests/by-util/test_cat.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
#[cfg(unix)]
// Verify cat handles broken pipe without hanging or crashing (GNU-compat focus)
#[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::<String>();
let result = new_ucmd!()
.set_stdout(write_end)
.pipe_in(content.as_bytes())
.run();

// Ensure the process exits (no hang) even if platforms differ in exit code/message on SIGPIPE
assert!(result.try_exit_status().is_some(), "process did not exit");
}
}

// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
Expand Down
157 changes: 156 additions & 1 deletion tests/by-util/test_ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -6163,3 +6163,158 @@ 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("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()
.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("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!());
let at = &scene.fixtures;
at.touch("a");
// Ensure distinct modification times
std::thread::sleep(Duration::from_millis(10));
at.touch("b");

// With -u (access time) sorting selected without -l, the first line should be the most recent by time key
// Here we simply check that the order changes between default and -tu sorts
let default_out = scene.ucmd().succeeds();
let tu_out = scene.ucmd().arg("-tu").succeeds();

let def = default_out.stdout_str();
let tu = tu_out.stdout_str();
assert_ne!(
def.lines().next().unwrap_or(""),
tu.lines().next().unwrap_or("")
);
}
24 changes: 24 additions & 0 deletions tests/by-util/test_stdbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
Expand All @@ -24,6 +25,18 @@ fn test_permission() {
.stderr_contains("Permission denied");
}

#[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!()
Expand All @@ -33,6 +46,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.
Expand Down
63 changes: 63 additions & 0 deletions tests/by-util/test_tee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,66 @@ 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);
}

// 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::<String>();
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());
}
Loading