Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c81ddd3
test: add comprehensive test coverage for tsort cycle detection and g…
mattsu2020 Nov 15, 2025
3db9f15
refactor(tests): consolidate multi-line string constants to single-li…
mattsu2020 Nov 15, 2025
a3db118
feat(tsort): add hidden warn flag and reverse successor iteration order
mattsu2020 Nov 15, 2025
c75da6f
fix(test): prefix uniq error messages with program name
mattsu2020 Nov 15, 2025
70c7ed4
fix(test): update chroot error assertion to include command prefix
mattsu2020 Nov 15, 2025
a3667f2
fix(test/chroot): update error message assertion to match standardize…
mattsu2020 Nov 15, 2025
507024e
fix: update uniq error messages in tests, removing 'uniq: ' prefix
mattsu2020 Nov 15, 2025
f7b5f67
fix(comm): update test assertion to match actual error message withou…
mattsu2020 Nov 15, 2025
8601226
refactor(clap_localization): replace print_prefixed_error with direct…
mattsu2020 Nov 15, 2025
326509d
refactor(clap_localization): remove prefixed error printing and use d…
mattsu2020 Nov 15, 2025
399464a
test(tests/tsort): ignore test for single input file until error mess…
mattsu2020 Nov 15, 2025
57ab06a
Merge branch 'uutils:main' into tsort_compatibility
mattsu2020 Nov 16, 2025
37cc210
Merge branch 'main' into tsort_compatibility
mattsu2020 Dec 1, 2025
062f993
feat(tsort): reject multiple input arguments with custom error
mattsu2020 Dec 1, 2025
4275d60
refactor: format TSORT_EXTRA_OPERAND_ERROR constant for readability
mattsu2020 Dec 1, 2025
6b420d8
chore: remove tests_tsort.patch from gnu-patches series
mattsu2020 Dec 1, 2025
e288476
fix(tsort): simplify error message construction by removing .into() w…
mattsu2020 Dec 2, 2025
832c734
feat: internationalize error messages in tsort command
mattsu2020 Dec 3, 2025
7d21d9c
fix(tsort): ensure expect message is &str by calling .as_str()
mattsu2020 Dec 3, 2025
7cb4474
Merge branch 'main' into tsort_compatibility
mattsu2020 Dec 23, 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
10 changes: 8 additions & 2 deletions src/uu/tsort/src/tsort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("-")
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 24 additions & 26 deletions src/uucore/src/lib/mods/clap_localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/by-util/test_chroot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<newroot>"));
Expand Down
2 changes: 1 addition & 1 deletion tests/by-util/test_comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FILE2>\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 <FILE2>\n\nUsage: comm [OPTION]... FILE1 FILE2\n\nFor more information, try '--help'.\n");
}

#[test]
Expand Down
93 changes: 92 additions & 1 deletion tests/by-util/test_tsort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down Expand Up @@ -153,3 +153,94 @@ 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 = "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() {
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\nc\nb\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\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\nx\nd\ny\ne\nz\nf\nr\ng\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);
}
12 changes: 6 additions & 6 deletions tests/by-util/test_uniq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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[=<delimit-method>]'\n",
"uniq: error: invalid value 'badoption' for '--all-repeated[=<delimit-method>]'\n",
"\n",
" [possible values: none, prepend, separate]\n",
"\n",
Expand Down Expand Up @@ -1066,7 +1066,7 @@ fn gnu_tests() {
input: "",
stdout: Some(""),
stderr: Some(concat!(
"error: the argument '--group[=<group-method>]' cannot be used with '--count'\n",
"uniq: error: the argument '--group[=<group-method>]' cannot be used with '--count'\n",
"\n",
"For more information, try '--help'.\n"
)),
Expand All @@ -1078,7 +1078,7 @@ fn gnu_tests() {
input: "",
stdout: Some(""),
stderr: Some(concat!(
"error: the argument '--group[=<group-method>]' cannot be used with '--repeated'\n",
"uniq: error: the argument '--group[=<group-method>]' cannot be used with '--repeated'\n",
"\n",
"For more information, try '--help'.\n"
)),
Expand All @@ -1090,7 +1090,7 @@ fn gnu_tests() {
input: "",
stdout: Some(""),
stderr: Some(concat!(
"error: the argument '--group[=<group-method>]' cannot be used with '--unique'\n",
"uniq: error: the argument '--group[=<group-method>]' cannot be used with '--unique'\n",
"\n",
"For more information, try '--help'.\n"
)),
Expand All @@ -1102,7 +1102,7 @@ fn gnu_tests() {
input: "",
stdout: Some(""),
stderr: Some(concat!(
"error: the argument '--group[=<group-method>]' cannot be used with '--all-repeated[=<delimit-method>]'\n",
"uniq: error: the argument '--group[=<group-method>]' cannot be used with '--all-repeated[=<delimit-method>]'\n",
"\n",
"For more information, try '--help'.\n"
)),
Expand All @@ -1114,7 +1114,7 @@ fn gnu_tests() {
input: "",
stdout: Some(""),
stderr: Some(concat!(
"error: invalid value 'badoption' for '--group[=<group-method>]'\n",
"uniq: error: invalid value 'badoption' for '--group[=<group-method>]'\n",
"\n",
" [possible values: separate, prepend, append, both]\n",
"\n",
Expand Down
20 changes: 10 additions & 10 deletions tests/fixtures/tsort/call_graph.expected
Original file line number Diff line number Diff line change
@@ -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
Loading