Skip to content

Commit 2852e44

Browse files
Copilotjgarzik
andcommitted
Fix tsort implementation: add stdin support, cycle detection, odd token validation, and comprehensive tests
Co-authored-by: jgarzik <[email protected]>
1 parent 23c5633 commit 2852e44

File tree

3 files changed

+228
-11
lines changed

3 files changed

+228
-11
lines changed

text/tests/text-tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod sed;
2424
mod sort;
2525
mod tail;
2626
mod tr;
27+
mod tsort;
2728
mod unexpand;
2829
mod uniq;
2930
mod wc;

text/tests/tsort/mod.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// Copyright (c) 2024 Jeff Garzik
3+
// Copyright (c) 2024 Hemi Labs, Inc.
4+
//
5+
// This file is part of the posixutils-rs project covered under
6+
// the MIT License. For the full license text, please see the LICENSE
7+
// file in the root directory of this project.
8+
// SPDX-License-Identifier: MIT
9+
//
10+
11+
use plib::testing::{run_test, TestPlan};
12+
13+
fn tsort_test(
14+
args: &[&str],
15+
test_data: &str,
16+
expected_output: &str,
17+
expected_exit_code: i32,
18+
expected_err: &str,
19+
) {
20+
let str_args: Vec<String> = args.iter().map(|s| String::from(*s)).collect();
21+
22+
run_test(TestPlan {
23+
cmd: String::from("tsort"),
24+
args: str_args,
25+
stdin_data: String::from(test_data),
26+
expected_out: String::from(expected_output),
27+
expected_err: String::from(expected_err),
28+
expected_exit_code,
29+
});
30+
}
31+
32+
#[test]
33+
fn test_basic() {
34+
tsort_test(&[], "a b\nc d\nb c\n", "a\nb\nc\nd\n", 0, "");
35+
}
36+
37+
#[test]
38+
fn test_simple_chain() {
39+
tsort_test(&[], "a b\nb c\nc d\n", "a\nb\nc\nd\n", 0, "");
40+
}
41+
42+
#[test]
43+
fn test_multiple_dependencies() {
44+
tsort_test(&[], "a b\na c\nb d\nc d\n", "a\nb\nc\nd\n", 0, "");
45+
}
46+
47+
#[test]
48+
fn test_self_loop() {
49+
tsort_test(&[], "a a\n", "a\n", 0, "");
50+
}
51+
52+
#[test]
53+
fn test_empty_input() {
54+
tsort_test(&[], "", "", 0, "");
55+
}
56+
57+
#[test]
58+
fn test_single_pair() {
59+
tsort_test(&[], "a b\n", "a\nb\n", 0, "");
60+
}
61+
62+
#[test]
63+
fn test_whitespace_separated_chain() {
64+
// Single line with chain dependencies: a->b->c->d
65+
tsort_test(&[], "a b b c c d\n", "a\nb\nc\nd\n", 0, "");
66+
}
67+
68+
#[test]
69+
fn test_multiline_tokens() {
70+
// Chain dependencies across lines
71+
tsort_test(&[], "a b\nb c\n", "a\nb\nc\n", 0, "");
72+
}
73+
74+
#[test]
75+
fn test_odd_number_of_tokens() {
76+
tsort_test(
77+
&[],
78+
"a b c\n",
79+
"",
80+
1,
81+
"stdin: input contains an odd number of tokens\n",
82+
);
83+
}
84+
85+
#[test]
86+
fn test_simple_cycle() {
87+
tsort_test(
88+
&[],
89+
"a b\nb a\n",
90+
"a\nb\n",
91+
1,
92+
"stdin: input contains a loop:\nstdin: a\nstdin: b\n",
93+
);
94+
}
95+
96+
#[test]
97+
fn test_three_way_cycle() {
98+
tsort_test(
99+
&[],
100+
"a b\nb c\nc a\n",
101+
"a\nb\nc\n",
102+
1,
103+
"stdin: input contains a loop:\nstdin: a\nstdin: b\nstdin: c\n",
104+
);
105+
}
106+
107+
#[test]
108+
fn test_partial_cycle() {
109+
// d->e has no cycle, a->b->c->a forms a cycle
110+
tsort_test(
111+
&[],
112+
"a b\nb c\nc a\nd e\n",
113+
"d\ne\na\nb\nc\n",
114+
1,
115+
"stdin: input contains a loop:\nstdin: a\nstdin: b\nstdin: c\n",
116+
);
117+
}
118+
119+
#[test]
120+
fn test_complex_graph_chain() {
121+
// Clear chain: d->c->b->a
122+
tsort_test(&[], "d c\nc b\nb a\n", "d\nc\nb\na\n", 0, "");
123+
}
124+
125+
#[test]
126+
fn test_two_independent_items() {
127+
// Single pair
128+
tsort_test(&[], "a b\n", "a\nb\n", 0, "");
129+
}
130+
131+
#[test]
132+
fn test_duplicate_pairs() {
133+
// Same dependency specified multiple times
134+
tsort_test(&[], "a b\na b\nb c\n", "a\nb\nc\n", 0, "");
135+
}
136+
137+
#[test]
138+
fn test_long_string_tokens() {
139+
tsort_test(
140+
&[],
141+
"very_long_token_name another_long_token\n",
142+
"very_long_token_name\nanother_long_token\n",
143+
0,
144+
"",
145+
);
146+
}
147+
148+
#[test]
149+
fn test_numeric_tokens() {
150+
tsort_test(&[], "1 2\n2 3\n3 4\n", "1\n2\n3\n4\n", 0, "");
151+
}
152+
153+
#[test]
154+
fn test_mixed_tokens() {
155+
tsort_test(
156+
&[],
157+
"file1.c file1.o\nfile1.o prog\n",
158+
"file1.c\nfile1.o\nprog\n",
159+
0,
160+
"",
161+
);
162+
}

text/tsort.rs

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::path::PathBuf;
1212

1313
use clap::Parser;
1414
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
15-
use plib::io::input_stream_opt;
15+
use plib::io::input_stream;
1616
use topological_sort::TopologicalSort;
1717

1818
/// tsort - topological sort
@@ -23,12 +23,17 @@ struct Args {
2323
file: Option<PathBuf>,
2424
}
2525

26-
fn tsort_file(pathname: &Option<PathBuf>) -> io::Result<()> {
27-
let file = input_stream_opt(pathname)?;
26+
fn tsort_file(pathname: &Option<PathBuf>) -> io::Result<i32> {
27+
// Handle stdin with "-" or no argument
28+
let file = match pathname {
29+
Some(path) => input_stream(path, true)?,
30+
None => input_stream(&PathBuf::new(), false)?,
31+
};
2832
let mut reader = io::BufReader::new(file);
2933

3034
let mut ts = TopologicalSort::<String>::new();
3135
let mut sv: Vec<String> = Vec::new();
36+
let mut all_items: std::collections::HashSet<String> = std::collections::HashSet::new();
3237

3338
loop {
3439
let mut buffer = String::new();
@@ -41,6 +46,9 @@ fn tsort_file(pathname: &Option<PathBuf>) -> io::Result<()> {
4146
sv.push(String::from(token));
4247

4348
if sv.len() == 2 {
49+
all_items.insert(sv[0].clone());
50+
all_items.insert(sv[1].clone());
51+
4452
if sv[0] == sv[1] {
4553
ts.insert(String::from(&sv[0]));
4654
} else {
@@ -51,11 +59,56 @@ fn tsort_file(pathname: &Option<PathBuf>) -> io::Result<()> {
5159
}
5260
}
5361

54-
for s in ts {
62+
// Check for odd number of tokens
63+
if !sv.is_empty() {
64+
eprintln!(
65+
"{}: input contains an odd number of tokens",
66+
pathname_display(pathname)
67+
);
68+
return Ok(1);
69+
}
70+
71+
// Collect results and check for cycles
72+
let mut sorted_items = Vec::new();
73+
let mut sorted_set = std::collections::HashSet::new();
74+
75+
for s in &mut ts {
76+
sorted_set.insert(s.clone());
77+
sorted_items.push(s);
78+
}
79+
80+
// If there are remaining items after iteration, there's a cycle
81+
if ts.len() > 0 {
82+
eprintln!("{}: input contains a loop:", pathname_display(pathname));
83+
84+
// Find items that weren't sorted (these are in the cycle)
85+
let mut cycle_items: Vec<String> = all_items.difference(&sorted_set).cloned().collect();
86+
cycle_items.sort(); // For consistent output
87+
88+
// Print cycle items
89+
for item in &cycle_items {
90+
eprintln!("{}: {}", pathname_display(pathname), item);
91+
}
92+
93+
// Print the sorted items first
94+
for s in sorted_items {
95+
println!("{}", s);
96+
}
97+
98+
// Then print the cycle items
99+
for item in &cycle_items {
100+
println!("{}", item);
101+
}
102+
103+
return Ok(1);
104+
}
105+
106+
// Print results
107+
for s in sorted_items {
55108
println!("{}", s);
56109
}
57110

58-
Ok(())
111+
Ok(0)
59112
}
60113

61114
fn pathname_display(path: &Option<PathBuf>) -> String {
@@ -72,12 +125,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
72125

73126
let args = Args::parse();
74127

75-
let mut exit_code = 0;
76-
77-
if let Err(e) = tsort_file(&args.file) {
78-
exit_code = 1;
79-
eprintln!("{}: {}", pathname_display(&args.file), e);
80-
}
128+
let exit_code = match tsort_file(&args.file) {
129+
Ok(code) => code,
130+
Err(e) => {
131+
eprintln!("{}: {}", pathname_display(&args.file), e);
132+
1
133+
}
134+
};
81135

82136
std::process::exit(exit_code)
83137
}

0 commit comments

Comments
 (0)