From c81ddd3db9ef3f784b3a0c5c094ad61873a92d66 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 19:34:24 +0900 Subject: [PATCH 01/17] test: add comprehensive test coverage for tsort cycle detection and graph topologies - Introduce new test cases for cycle loops in file inputs, including extra nodes and multiple loops - Add tests for POSIX graph examples and linear tree graphs to validate topological sorting - Include tests for error handling on odd token counts and multiple file inputs - Ensures robustness and correctness of tsort implementation across various edge cases and standard scenarios --- tests/by-util/test_tsort.rs | 93 +++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 077fd26b772..dee215361d6 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -153,3 +153,96 @@ fn test_loop_for_iterative_dfs_correctness() { .fails_with_code(1) .stderr_contains("tsort: -: input contains a loop:\ntsort: B\ntsort: C"); } + +const TSORT_LOOP_STDERR: &str = "tsort: f: input contains a loop:\ntsort: s\ntsort: t\n"; +const TSORT_LOOP_STDERR_AC: &str = + "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; +const TSORT_ODD_ERROR: &str = "tsort: -: input contains an odd number of tokens\n"; +const TSORT_UNEXPECTED_ARG_ERROR: &str = + "error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; + +#[test] +fn test_cycle_loop_from_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", "t b\nt s\ns t\n"); + + ucmd.arg("f") + .fails_with_code(1) + .stdout_is("s\nt\nb\n") + .stderr_is(TSORT_LOOP_STDERR); +} + +#[test] +fn test_cycle_loop_with_extra_node_from_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", "t x\nt s\ns t\n"); + + ucmd.arg("f") + .fails_with_code(1) + .stdout_is("s\nt\nx\n") + .stderr_is(TSORT_LOOP_STDERR); +} + +#[test] +fn test_cycle_loop_multiple_loops_from_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", "a a\na b\na c\nc a\nb a\n"); + + ucmd.arg("f") + .fails_with_code(1) + .stdout_is("a\nb\nc\n") + .stderr_is(TSORT_LOOP_STDERR_AC); +} + +#[test] +fn test_posix_graph_examples() { + new_ucmd!() + .pipe_in("a b c c d e\ng g\nf g e f\nh h\n") + .succeeds() + .stdout_only("a\nc\nd\nh\nb\ne\nf\ng\n"); + + new_ucmd!() + .pipe_in("b a\nd c\nz h x h r h\n") + .succeeds() + .stdout_only("b\nd\nr\nx\nz\na\nc\nh\n"); +} + +#[test] +fn test_linear_tree_graphs() { + new_ucmd!() + .pipe_in("a b b c c d d e e f f g\n") + .succeeds() + .stdout_only("a\nb\nc\nd\ne\nf\ng\n"); + + new_ucmd!() + .pipe_in("a b b c c d d e e f f g\nc x x y y z\n") + .succeeds() + .stdout_only("a\nb\nc\nd\nx\ne\ny\nf\nz\ng\n"); + + new_ucmd!() + .pipe_in("a b b c c d d e e f f g\nc x x y y z\nf r r s s t\n") + .succeeds() + .stdout_only("a\nb\nc\nd\nx\ne\ny\nf\nz\ng\nr\ns\nt\n"); +} + +#[test] +fn test_odd_number_of_tokens() { + new_ucmd!() + .pipe_in("a\n") + .fails_with_code(1) + .stdout_is("") + .stderr_is(TSORT_ODD_ERROR); +} + +#[test] +fn test_only_one_input_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f", ""); + at.write("g", ""); + + ucmd.arg("f") + .arg("g") + .fails_with_code(1) + .stdout_is("") + .stderr_is(TSORT_UNEXPECTED_ARG_ERROR); +} From 3db9f15ff4c4e9dde68f27b69fc6b35fde223895 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 19:35:20 +0900 Subject: [PATCH 02/17] refactor(tests): consolidate multi-line string constants to single-line literals in tsort test file - Reformatted TSORT_LOOP_STDERR_AC and TSORT_UNEXPECTED_ARG_ERROR constants for improved code readability and consistency, with no change in string content or functionality. The multi-line format was merged into single lines to align with potential linting rules or style preferences for string literals in tests. This refactoring enhances maintainability without affecting test logic. --- tests/by-util/test_tsort.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index dee215361d6..2e1a622f645 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -155,11 +155,9 @@ fn test_loop_for_iterative_dfs_correctness() { } const TSORT_LOOP_STDERR: &str = "tsort: f: input contains a loop:\ntsort: s\ntsort: t\n"; -const TSORT_LOOP_STDERR_AC: &str = - "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; +const TSORT_LOOP_STDERR_AC: &str = "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; const TSORT_ODD_ERROR: &str = "tsort: -: input contains an odd number of tokens\n"; -const TSORT_UNEXPECTED_ARG_ERROR: &str = - "error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; +const TSORT_UNEXPECTED_ARG_ERROR: &str = "error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; #[test] fn test_cycle_loop_from_file() { From a3db1180431756a6eef4940e2c5c312b044e251d Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 20:32:35 +0900 Subject: [PATCH 03/17] feat(tsort): add hidden warn flag and reverse successor iteration order - Added `ArgAction` import and a new hidden `-w`/`--warn` argument to the tsort command for future warning features - Modified iteration of successor names to use `.into_iter().rev()` in the topological sorting algorithm to process nodes in reverse order, ensuring more stable and predictable output sequence - Refactored Clap error localization in `clap_localization.rs` to use `print_prefixed_error()` method instead of direct `eprintln!` calls, improving consistency in error message formatting across the application --- src/uu/tsort/src/tsort.rs | 10 +++- src/uucore/src/lib/mods/clap_localization.rs | 50 ++++++++++---------- tests/by-util/test_comm.rs | 2 +- tests/by-util/test_tsort.rs | 10 ++-- tests/fixtures/tsort/call_graph.expected | 20 ++++---- 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 4b52e1e4534..850bd2369c8 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. //spell-checker:ignore TAOCP indegree -use clap::{Arg, Command}; +use clap::{Arg, ArgAction, Command}; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; use std::ffi::OsString; @@ -96,6 +96,12 @@ pub fn uu_app() -> Command { .override_usage(format_usage(&translate!("tsort-usage"))) .about(translate!("tsort-about")) .infer_long_args(true) + .arg( + Arg::new("warn") + .short('w') + .action(ArgAction::SetTrue) + .hide(true), + ) .arg( Arg::new(options::FILE) .default_value("-") @@ -190,7 +196,7 @@ impl<'input> Graph<'input> { let v = self.find_next_node(&mut independent_nodes_queue); println!("{v}"); if let Some(node_to_process) = self.nodes.remove(v) { - for successor_name in node_to_process.successor_names { + for successor_name in node_to_process.successor_names.into_iter().rev() { let successor_node = self.nodes.get_mut(successor_name).unwrap(); successor_node.predecessor_count -= 1; if successor_node.predecessor_count == 0 { diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 5a54bf7c302..b62a4eb052c 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -150,14 +150,11 @@ impl<'a> ErrorFormatter<'a> { let error_word = translate!("common-error"); // Print main error - eprintln!( - "{}", - translate!( - "clap-error-unexpected-argument", - "arg" => self.color_mgr.colorize(&arg_str, Color::Yellow), - "error_word" => self.color_mgr.colorize(&error_word, Color::Red) - ) - ); + self.print_prefixed_error(&translate!( + "clap-error-unexpected-argument", + "arg" => self.color_mgr.colorize(&arg_str, Color::Yellow), + "error_word" => self.color_mgr.colorize(&error_word, Color::Red) + )); eprintln!(); // Show suggestion if available @@ -204,12 +201,12 @@ impl<'a> ErrorFormatter<'a> { if value.is_empty() { // Value required but not provided let error_word = translate!("common-error"); - eprintln!( - "{}", - translate!("clap-error-value-required", - "error_word" => self.color_mgr.colorize(&error_word, Color::Red), - "option" => self.color_mgr.colorize(&option, Color::Green)) + let error_line = translate!( + "clap-error-value-required", + "error_word" => self.color_mgr.colorize(&error_word, Color::Red), + "option" => self.color_mgr.colorize(&option, Color::Green) ); + self.print_prefixed_error(&error_line); } else { // Invalid value provided let error_word = translate!("common-error"); @@ -219,13 +216,12 @@ impl<'a> ErrorFormatter<'a> { "value" => self.color_mgr.colorize(&value, Color::Yellow), "option" => self.color_mgr.colorize(&option, Color::Green) ); - // Include validation error if present match err.source() { Some(source) if matches!(err.kind(), ErrorKind::ValueValidation) => { - eprintln!("{error_msg}: {source}"); + self.print_prefixed_error(&format!("{error_msg}: {source}")); } - _ => eprintln!("{error_msg}"), + _ => self.print_prefixed_error(&error_msg), } } @@ -280,13 +276,10 @@ impl<'a> ErrorFormatter<'a> { .starts_with("error: the following required arguments were not provided:") => { let error_word = translate!("common-error"); - eprintln!( - "{}", - translate!( - "clap-error-missing-required-arguments", - "error_word" => self.color_mgr.colorize(&error_word, Color::Red) - ) - ); + self.print_prefixed_error(&translate!( + "clap-error-missing-required-arguments", + "error_word" => self.color_mgr.colorize(&error_word, Color::Red) + )); // Print the missing arguments for line in lines.iter().skip(1) { @@ -345,10 +338,11 @@ impl<'a> ErrorFormatter<'a> { F: FnOnce(), { let error_word = translate!("common-error"); - eprintln!( + let message_line = format!( "{}: {message}", self.color_mgr.colorize(&error_word, Color::Red) ); + self.print_prefixed_error(&message_line); callback(); std::process::exit(exit_code); } @@ -360,12 +354,16 @@ impl<'a> ErrorFormatter<'a> { if let Some(colon_pos) = line.find(':') { let after_colon = &line[colon_pos..]; - eprintln!("{colored_error}{after_colon}"); + self.print_prefixed_error(&format!("{colored_error}{after_colon}")); } else { - eprintln!("{line}"); + self.print_prefixed_error(line); } } + fn print_prefixed_error(&self, message: &str) { + eprintln!("{}: {message}", self.util_name); + } + /// Extract and print clap's built-in tips fn print_clap_tips(&self, err: &Error) { let rendered_str = err.render().to_string(); diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index bf719d7fbd1..a3051ed97d4 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -592,7 +592,7 @@ fn test_comm_arg_error() { .args(&["a"]) .fails() .code_is(1) - .stderr_is("error: the following required arguments were not provided:\n \n\nUsage: comm [OPTION]... FILE1 FILE2\n\nFor more information, try '--help'.\n"); + .stderr_is("comm: error: the following required arguments were not provided:\n \n\nUsage: comm [OPTION]... FILE1 FILE2\n\nFor more information, try '--help'.\n"); } #[test] diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 2e1a622f645..1ceff6c83c6 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -119,7 +119,7 @@ fn test_two_cycles() { new_ucmd!() .pipe_in("a b b c c b b d d b") .fails_with_code(1) - .stdout_is("a\nb\nc\nd\n") + .stdout_is("a\nb\nd\nc\n") .stderr_is("tsort: -: input contains a loop:\ntsort: b\ntsort: c\ntsort: -: input contains a loop:\ntsort: b\ntsort: d\n"); } @@ -157,7 +157,7 @@ fn test_loop_for_iterative_dfs_correctness() { const TSORT_LOOP_STDERR: &str = "tsort: f: input contains a loop:\ntsort: s\ntsort: t\n"; const TSORT_LOOP_STDERR_AC: &str = "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; const TSORT_ODD_ERROR: &str = "tsort: -: input contains an odd number of tokens\n"; -const TSORT_UNEXPECTED_ARG_ERROR: &str = "error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; +const TSORT_UNEXPECTED_ARG_ERROR: &str = "tsort: error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; #[test] fn test_cycle_loop_from_file() { @@ -188,7 +188,7 @@ fn test_cycle_loop_multiple_loops_from_file() { ucmd.arg("f") .fails_with_code(1) - .stdout_is("a\nb\nc\n") + .stdout_is("a\nc\nb\n") .stderr_is(TSORT_LOOP_STDERR_AC); } @@ -215,12 +215,12 @@ fn test_linear_tree_graphs() { new_ucmd!() .pipe_in("a b b c c d d e e f f g\nc x x y y z\n") .succeeds() - .stdout_only("a\nb\nc\nd\nx\ne\ny\nf\nz\ng\n"); + .stdout_only("a\nb\nc\nx\nd\ny\ne\nz\nf\ng\n"); new_ucmd!() .pipe_in("a b b c c d d e e f f g\nc x x y y z\nf r r s s t\n") .succeeds() - .stdout_only("a\nb\nc\nd\nx\ne\ny\nf\nz\ng\nr\ns\nt\n"); + .stdout_only("a\nb\nc\nx\nd\ny\ne\nz\nf\nr\ng\ns\nt\n"); } #[test] diff --git a/tests/fixtures/tsort/call_graph.expected b/tests/fixtures/tsort/call_graph.expected index e33aa72bd04..df1b950f627 100644 --- a/tests/fixtures/tsort/call_graph.expected +++ b/tests/fixtures/tsort/call_graph.expected @@ -1,17 +1,17 @@ main -parse_options -tail_file tail_forever -tail +tail_file +parse_options recheck +tail write_header -tail_lines -tail_bytes pretty_name -start_lines -file_lines -pipe_lines -xlseek -start_bytes +tail_bytes +tail_lines pipe_bytes +start_bytes +xlseek +pipe_lines +file_lines +start_lines dump_remainder From c75da6f79229bffd688031ec06d9b0cb48336d83 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 20:57:57 +0900 Subject: [PATCH 04/17] fix(test): prefix uniq error messages with program name Update expected error outputs in uniq tests to match the new format where error messages are prefixed with "uniq: ". This ensures test cases align with the updated error message formatting in the utility, providing clearer error identification by including the program name at the start of each error message. Changes affect multiple test assertions for invalid options and incompatible argument combinations. --- tests/by-util/test_uniq.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index 6c9107ce527..a6c6c25567c 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -889,7 +889,7 @@ fn gnu_tests() { input: "", // Note: Different from GNU test, but should not matter stdout: Some(""), stderr: Some(concat!( - "error: invalid value 'badoption' for '--all-repeated[=]'\n", + "uniq: error: invalid value 'badoption' for '--all-repeated[=]'\n", "\n", " [possible values: none, prepend, separate]\n", "\n", @@ -1066,7 +1066,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "error: the argument '--group[=]' cannot be used with '--count'\n", + "uniq: error: the argument '--group[=]' cannot be used with '--count'\n", "\n", "For more information, try '--help'.\n" )), @@ -1078,7 +1078,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "error: the argument '--group[=]' cannot be used with '--repeated'\n", + "uniq: error: the argument '--group[=]' cannot be used with '--repeated'\n", "\n", "For more information, try '--help'.\n" )), @@ -1090,7 +1090,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "error: the argument '--group[=]' cannot be used with '--unique'\n", + "uniq: error: the argument '--group[=]' cannot be used with '--unique'\n", "\n", "For more information, try '--help'.\n" )), @@ -1102,7 +1102,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "error: the argument '--group[=]' cannot be used with '--all-repeated[=]'\n", + "uniq: error: the argument '--group[=]' cannot be used with '--all-repeated[=]'\n", "\n", "For more information, try '--help'.\n" )), @@ -1114,7 +1114,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "error: invalid value 'badoption' for '--group[=]'\n", + "uniq: error: invalid value 'badoption' for '--group[=]'\n", "\n", " [possible values: separate, prepend, append, both]\n", "\n", From 70c7ed4cc7dcb2480330d21ac91792e2c2e9d4f2 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 21:20:26 +0900 Subject: [PATCH 05/17] fix(test): update chroot error assertion to include command prefix The expected error message now starts with "chroot: " to match the updated output format in the chroot utility. This ensures the test accurately reflects the command's behavior. --- tests/by-util/test_chroot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 38c3727b1cd..7cf59114340 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -23,7 +23,7 @@ fn test_missing_operand() { assert!( result .stderr_str() - .starts_with("error: the following required arguments were not provided") + .starts_with("chroot: error: the following required arguments were not provided") ); assert!(result.stderr_str().contains("")); From a3667f26da8eccc6193c48f4d773780d9ed8741d Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 22:04:20 +0900 Subject: [PATCH 06/17] fix(test/chroot): update error message assertion to match standardized format Remove "chroot: " prefix from expected error output, aligning the test with the updated stderr format that omits utility name redundancy in error messages. --- tests/by-util/test_chroot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 7cf59114340..38c3727b1cd 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -23,7 +23,7 @@ fn test_missing_operand() { assert!( result .stderr_str() - .starts_with("chroot: error: the following required arguments were not provided") + .starts_with("error: the following required arguments were not provided") ); assert!(result.stderr_str().contains("")); From 507024e23a3d3d027dc19409e10e90dab4eb9c88 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 22:07:01 +0900 Subject: [PATCH 07/17] fix: update uniq error messages in tests, removing 'uniq: ' prefix Remove the 'uniq: ' prefix from expected error messages in test cases to match the updated output format of the uniq utility, ensuring tests pass with the current implementation. This change affects multiple GNU compatibility tests for invalid options and argument conflicts. --- tests/by-util/test_uniq.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/by-util/test_uniq.rs b/tests/by-util/test_uniq.rs index a6c6c25567c..6c9107ce527 100644 --- a/tests/by-util/test_uniq.rs +++ b/tests/by-util/test_uniq.rs @@ -889,7 +889,7 @@ fn gnu_tests() { input: "", // Note: Different from GNU test, but should not matter stdout: Some(""), stderr: Some(concat!( - "uniq: error: invalid value 'badoption' for '--all-repeated[=]'\n", + "error: invalid value 'badoption' for '--all-repeated[=]'\n", "\n", " [possible values: none, prepend, separate]\n", "\n", @@ -1066,7 +1066,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "uniq: error: the argument '--group[=]' cannot be used with '--count'\n", + "error: the argument '--group[=]' cannot be used with '--count'\n", "\n", "For more information, try '--help'.\n" )), @@ -1078,7 +1078,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "uniq: error: the argument '--group[=]' cannot be used with '--repeated'\n", + "error: the argument '--group[=]' cannot be used with '--repeated'\n", "\n", "For more information, try '--help'.\n" )), @@ -1090,7 +1090,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "uniq: error: the argument '--group[=]' cannot be used with '--unique'\n", + "error: the argument '--group[=]' cannot be used with '--unique'\n", "\n", "For more information, try '--help'.\n" )), @@ -1102,7 +1102,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "uniq: error: the argument '--group[=]' cannot be used with '--all-repeated[=]'\n", + "error: the argument '--group[=]' cannot be used with '--all-repeated[=]'\n", "\n", "For more information, try '--help'.\n" )), @@ -1114,7 +1114,7 @@ fn gnu_tests() { input: "", stdout: Some(""), stderr: Some(concat!( - "uniq: error: invalid value 'badoption' for '--group[=]'\n", + "error: invalid value 'badoption' for '--group[=]'\n", "\n", " [possible values: separate, prepend, append, both]\n", "\n", From f7b5f67cf6ab461970bb418717494ce4ce494801 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 22:07:57 +0900 Subject: [PATCH 08/17] fix(comm): update test assertion to match actual error message without 'comm: ' prefix The stderr assertion in test_comm_arg_error was expecting an error message prefixed with "comm: ", but the actual command output does not include this prefix. This update fixes the test to align with the real behavior, ensuring the test passes correctly. --- tests/by-util/test_comm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index a3051ed97d4..bf719d7fbd1 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -592,7 +592,7 @@ fn test_comm_arg_error() { .args(&["a"]) .fails() .code_is(1) - .stderr_is("comm: error: the following required arguments were not provided:\n \n\nUsage: comm [OPTION]... FILE1 FILE2\n\nFor more information, try '--help'.\n"); + .stderr_is("error: the following required arguments were not provided:\n \n\nUsage: comm [OPTION]... FILE1 FILE2\n\nFor more information, try '--help'.\n"); } #[test] From 8601226f8fa8a9810ae7d0a3889807ee54f5a654 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 22:14:34 +0900 Subject: [PATCH 09/17] refactor(clap_localization): replace print_prefixed_error with direct stderr output in ErrorFormatter Replace the call to self.print_prefixed_error with direct eprintln for printing unexpected argument errors, and add an additional blank line for better formatting and readability in error messages. This change aims to simplify the output process and ensure consistent error presentation in the clap localization module. --- src/uucore/src/lib/mods/clap_localization.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index b62a4eb052c..4a21770904f 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -150,11 +150,14 @@ impl<'a> ErrorFormatter<'a> { let error_word = translate!("common-error"); // Print main error - self.print_prefixed_error(&translate!( - "clap-error-unexpected-argument", - "arg" => self.color_mgr.colorize(&arg_str, Color::Yellow), - "error_word" => self.color_mgr.colorize(&error_word, Color::Red) - )); + eprintln!( + "{}", + translate!( + "clap-error-unexpected-argument", + "arg" => self.color_mgr.colorize(&arg_str, Color::Yellow), + "error_word" => self.color_mgr.colorize(&error_word, Color::Red) + ) + ); eprintln!(); // Show suggestion if available From 326509dc7689e0cce2e863b2afa4d7d142d080ed Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 22:54:52 +0900 Subject: [PATCH 10/17] refactor(clap_localization): remove prefixed error printing and use direct eprintln for cleaner output Modified error handling in clap_localization.rs to eliminate the utility name prefix by replacing self.print_prefixed_error calls with direct eprintln! invocations. This simplifies the codebase and changes error message formatting to display clap errors without the preceding util name. Removed the unused print_prefixed_error method. --- src/uucore/src/lib/mods/clap_localization.rs | 36 +++++++++----------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 4a21770904f..b6e906a9052 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -204,12 +204,12 @@ impl<'a> ErrorFormatter<'a> { if value.is_empty() { // Value required but not provided let error_word = translate!("common-error"); - let error_line = translate!( - "clap-error-value-required", - "error_word" => self.color_mgr.colorize(&error_word, Color::Red), - "option" => self.color_mgr.colorize(&option, Color::Green) + eprintln!( + "{}", + translate!("clap-error-value-required", + "error_word" => self.color_mgr.colorize(&error_word, Color::Red), + "option" => self.color_mgr.colorize(&option, Color::Green)) ); - self.print_prefixed_error(&error_line); } else { // Invalid value provided let error_word = translate!("common-error"); @@ -222,9 +222,9 @@ impl<'a> ErrorFormatter<'a> { // Include validation error if present match err.source() { Some(source) if matches!(err.kind(), ErrorKind::ValueValidation) => { - self.print_prefixed_error(&format!("{error_msg}: {source}")); + eprintln!("{error_msg}: {source}"); } - _ => self.print_prefixed_error(&error_msg), + _ => eprintln!("{error_msg}"), } } @@ -279,10 +279,13 @@ impl<'a> ErrorFormatter<'a> { .starts_with("error: the following required arguments were not provided:") => { let error_word = translate!("common-error"); - self.print_prefixed_error(&translate!( - "clap-error-missing-required-arguments", - "error_word" => self.color_mgr.colorize(&error_word, Color::Red) - )); + eprintln!( + "{}", + translate!( + "clap-error-missing-required-arguments", + "error_word" => self.color_mgr.colorize(&error_word, Color::Red) + ) + ); // Print the missing arguments for line in lines.iter().skip(1) { @@ -341,11 +344,10 @@ impl<'a> ErrorFormatter<'a> { F: FnOnce(), { let error_word = translate!("common-error"); - let message_line = format!( + eprintln!( "{}: {message}", self.color_mgr.colorize(&error_word, Color::Red) ); - self.print_prefixed_error(&message_line); callback(); std::process::exit(exit_code); } @@ -357,16 +359,12 @@ impl<'a> ErrorFormatter<'a> { if let Some(colon_pos) = line.find(':') { let after_colon = &line[colon_pos..]; - self.print_prefixed_error(&format!("{colored_error}{after_colon}")); + eprintln!("{colored_error}{after_colon}"); } else { - self.print_prefixed_error(line); + eprintln!("{line}"); } } - fn print_prefixed_error(&self, message: &str) { - eprintln!("{}: {message}", self.util_name); - } - /// Extract and print clap's built-in tips fn print_clap_tips(&self, err: &Error) { let rendered_str = err.render().to_string(); From 399464a959f671353dfdf026dbf842c09281a4b0 Mon Sep 17 00:00:00 2001 From: mattsu Date: Sat, 15 Nov 2025 23:07:32 +0900 Subject: [PATCH 11/17] test(tests/tsort): ignore test for single input file until error message is corrected - Added #[ignore] attribute to test_only_one_input_file to skip it during execution. - Reason: Test likely fails due to an incorrect error message; this prevents false negatives while the message is being fixed in the tsort utility. --- tests/by-util/test_tsort.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 1ceff6c83c6..a28b5774938 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -233,6 +233,7 @@ fn test_odd_number_of_tokens() { } #[test] +#[ignore = "After correcting the error message, execute."] fn test_only_one_input_file() { let (at, mut ucmd) = at_and_ucmd!(); at.write("f", ""); From 062f993abd2e079d25118eff205f8bf8c32950ad Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 1 Dec 2025 21:18:27 +0900 Subject: [PATCH 12/17] feat(tsort): reject multiple input arguments with custom error - Change FILE arg to accept zero or more inputs (appended), defaulting to "-" if none - Add validation to error on more than one input with "extra operand" message - Update test to expect new error format, matching GNU tsort behavior - Unignore test_only_one_input_file after error message correction --- src/uu/tsort/src/tsort.rs | 33 ++++++++++++++++++++++++++------- tests/by-util/test_tsort.rs | 7 +++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 850bd2369c8..2f184e0e974 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -10,7 +10,7 @@ use std::ffi::OsString; use std::path::Path; use thiserror::Error; use uucore::display::Quotable; -use uucore::error::{UError, UResult}; +use uucore::error::{UError, UResult, USimpleError}; use uucore::{format_usage, show}; use uucore::translate; @@ -49,15 +49,33 @@ impl UError for LoopNode<'_> {} pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - let input = matches - .get_one::(options::FILE) - .expect("Value is required by clap"); + let mut inputs: Vec = matches + .get_many::(options::FILE) + .map(|vals| vals.cloned().collect()) + .unwrap_or_default(); + + if inputs.is_empty() { + inputs.push(OsString::from("-")); + } + + if inputs.len() > 1 { + return Err(USimpleError::new( + 1, + format!( + "extra operand {}\nTry 'tsort --help' for more information.", + inputs[1].quote() + ), + ) + .into()); + } + + let input = inputs.into_iter().next().expect("at least one input"); let data = if input == "-" { let stdin = std::io::stdin(); std::io::read_to_string(stdin)? } else { - let path = Path::new(input); + let path = Path::new(&input); if path.is_dir() { return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); } @@ -104,10 +122,11 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::FILE) - .default_value("-") .hide(true) .value_parser(clap::value_parser!(OsString)) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .num_args(0..) + .action(ArgAction::Append), ) } diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index a28b5774938..203e8ef2db6 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -77,7 +77,7 @@ fn test_multiple_arguments() { .arg("call_graph.txt") .arg("invalid_file") .fails() - .stderr_contains("unexpected argument 'invalid_file' found"); + .stderr_contains("extra operand 'invalid_file'"); } #[test] @@ -157,7 +157,7 @@ fn test_loop_for_iterative_dfs_correctness() { const TSORT_LOOP_STDERR: &str = "tsort: f: input contains a loop:\ntsort: s\ntsort: t\n"; const TSORT_LOOP_STDERR_AC: &str = "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; const TSORT_ODD_ERROR: &str = "tsort: -: input contains an odd number of tokens\n"; -const TSORT_UNEXPECTED_ARG_ERROR: &str = "tsort: error: unexpected argument 'g' found\n\nUsage: tsort [OPTIONS] FILE\n\nFor more information, try '--help'.\n"; +const TSORT_EXTRA_OPERAND_ERROR: &str = "tsort: extra operand 'g'\nTry 'tsort --help' for more information.\n"; #[test] fn test_cycle_loop_from_file() { @@ -233,7 +233,6 @@ fn test_odd_number_of_tokens() { } #[test] -#[ignore = "After correcting the error message, execute."] fn test_only_one_input_file() { let (at, mut ucmd) = at_and_ucmd!(); at.write("f", ""); @@ -243,5 +242,5 @@ fn test_only_one_input_file() { .arg("g") .fails_with_code(1) .stdout_is("") - .stderr_is(TSORT_UNEXPECTED_ARG_ERROR); + .stderr_is(TSORT_EXTRA_OPERAND_ERROR); } From 4275d60328a53e8b9915d4f8ad0a1aeac55a6f0d Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 1 Dec 2025 21:18:47 +0900 Subject: [PATCH 13/17] refactor: format TSORT_EXTRA_OPERAND_ERROR constant for readability Split the TSORT_EXTRA_OPERAND_ERROR constant string into multiple lines to improve code formatting and adhere to line length guidelines. --- tests/by-util/test_tsort.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index 203e8ef2db6..64fe97385df 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -157,7 +157,8 @@ fn test_loop_for_iterative_dfs_correctness() { const TSORT_LOOP_STDERR: &str = "tsort: f: input contains a loop:\ntsort: s\ntsort: t\n"; const TSORT_LOOP_STDERR_AC: &str = "tsort: f: input contains a loop:\ntsort: a\ntsort: b\ntsort: f: input contains a loop:\ntsort: a\ntsort: c\n"; const TSORT_ODD_ERROR: &str = "tsort: -: input contains an odd number of tokens\n"; -const TSORT_EXTRA_OPERAND_ERROR: &str = "tsort: extra operand 'g'\nTry 'tsort --help' for more information.\n"; +const TSORT_EXTRA_OPERAND_ERROR: &str = + "tsort: extra operand 'g'\nTry 'tsort --help' for more information.\n"; #[test] fn test_cycle_loop_from_file() { From 6b420d8640aa5c73f1e34eba1b4d31318ae0a924 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 1 Dec 2025 21:49:02 +0900 Subject: [PATCH 14/17] chore: remove tests_tsort.patch from gnu-patches series Removed the tests_tsort.patch entry as it is no longer applied, possibly due to upstream integration or obsolescence, to keep the patch series current and relevant. --- util/gnu-patches/series | 1 - util/gnu-patches/tests_tsort.patch | 17 ----------------- 2 files changed, 18 deletions(-) delete mode 100644 util/gnu-patches/tests_tsort.patch diff --git a/util/gnu-patches/series b/util/gnu-patches/series index 5fb1398cd17..f1a24d0f67e 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -7,7 +7,6 @@ tests_env_env-S.pl.patch tests_invalid_opt.patch tests_ls_no_cap.patch tests_sort_merge.pl.patch -tests_tsort.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff diff --git a/util/gnu-patches/tests_tsort.patch b/util/gnu-patches/tests_tsort.patch deleted file mode 100644 index 1cc1603ee8f..00000000000 --- a/util/gnu-patches/tests_tsort.patch +++ /dev/null @@ -1,17 +0,0 @@ -Index: gnu/tests/misc/tsort.pl -=================================================================== ---- gnu.orig/tests/misc/tsort.pl -+++ gnu/tests/misc/tsort.pl -@@ -54,8 +54,10 @@ my @Tests = - - ['only-one', {IN => {f => ""}}, {IN => {g => ""}}, - {EXIT => 1}, -- {ERR => "tsort: extra operand 'g'\n" -- . "Try 'tsort --help' for more information.\n"}], -+ {ERR => "tsort: error: unexpected argument 'g' found\n\n" -+ . "Usage: tsort [OPTIONS] FILE\n\n" -+ . "For more information, try '--help'.\n" -+ }], - ); - - my $save_temps = $ENV{DEBUG}; From e28847683c6903122f91ed64f4faa5c85d5aaedf Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 2 Dec 2025 20:04:46 +0900 Subject: [PATCH 15/17] fix(tsort): simplify error message construction by removing .into() wrapper Remove unnecessary `.into()` call when creating the extra operand error in uumain, resulting in cleaner, more concise error handling code. This change does not alter the program's functionality but improves code readability and reduces nesting. --- src/uu/tsort/src/tsort.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 2f184e0e974..b4386fe989a 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -65,8 +65,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { "extra operand {}\nTry 'tsort --help' for more information.", inputs[1].quote() ), - ) - .into()); + )); } let input = inputs.into_iter().next().expect("at least one input"); From 832c734a7785ad76f035aa3e995f7ce6976b5e07 Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 3 Dec 2025 17:53:56 +0900 Subject: [PATCH 16/17] feat: internationalize error messages in tsort command Add localized strings for 'extra operand' and 'at least one input' errors in en-US and fr-FR locales. Update code to use translate! macro for consistent error reporting across languages, improving user experience for international users. --- src/uu/tsort/locales/en-US.ftl | 3 +++ src/uu/tsort/locales/fr-FR.ftl | 3 +++ src/uu/tsort/src/tsort.rs | 12 ++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/uu/tsort/locales/en-US.ftl b/src/uu/tsort/locales/en-US.ftl index a4b4218c3f6..2b2f90a1e01 100644 --- a/src/uu/tsort/locales/en-US.ftl +++ b/src/uu/tsort/locales/en-US.ftl @@ -6,3 +6,6 @@ tsort-usage = tsort [OPTIONS] FILE tsort-error-is-dir = read error: Is a directory tsort-error-odd = input contains an odd number of tokens tsort-error-loop = input contains a loop: +tsort-error-extra-operand = extra operand { $operand } + Try '{ $util } --help' for more information. +tsort-error-at-least-one-input = at least one input diff --git a/src/uu/tsort/locales/fr-FR.ftl b/src/uu/tsort/locales/fr-FR.ftl index 18349b978ca..c3594e7a212 100644 --- a/src/uu/tsort/locales/fr-FR.ftl +++ b/src/uu/tsort/locales/fr-FR.ftl @@ -6,3 +6,6 @@ tsort-usage = tsort [OPTIONS] FILE tsort-error-is-dir = erreur de lecture : c'est un répertoire tsort-error-odd = l'entrée contient un nombre impair de jetons tsort-error-loop = l'entrée contient une boucle : +tsort-error-extra-operand = opérande supplémentaire { $operand } + Essayez '{ $util } --help' pour plus d'informations. +tsort-error-at-least-one-input = au moins une entrée diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index b4386fe989a..59ac75c8d53 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -61,14 +61,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if inputs.len() > 1 { return Err(USimpleError::new( 1, - format!( - "extra operand {}\nTry 'tsort --help' for more information.", - inputs[1].quote() + translate!( + "tsort-error-extra-operand", + "operand" => inputs[1].quote(), + "util" => uucore::util_name() ), )); } - let input = inputs.into_iter().next().expect("at least one input"); + let input = inputs + .into_iter() + .next() + .expect(translate!("tsort-error-at-least-one-input")); let data = if input == "-" { let stdin = std::io::stdin(); From 7d21d9cf7f9c802609be455fd2c280997cd7a153 Mon Sep 17 00:00:00 2001 From: mattsu Date: Wed, 3 Dec 2025 17:59:06 +0900 Subject: [PATCH 17/17] fix(tsort): ensure expect message is &str by calling .as_str() The translate! macro returns a String, but expect() requires a &str. Added .as_str() to convert the translated string for correct type usage and fix compilation error. --- src/uu/tsort/src/tsort.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 59ac75c8d53..67d8cca2630 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -72,7 +72,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let input = inputs .into_iter() .next() - .expect(translate!("tsort-error-at-least-one-input")); + .expect(translate!("tsort-error-at-least-one-input").as_str()); let data = if input == "-" { let stdin = std::io::stdin();