1use console::Style;
7use pretty_print::{
8 print_diff, print_end_with_status, print_or_empty, print_section, print_with_style,
9};
10use rand::RngExt;
11use rand::prelude::IndexedRandom;
12use rustix::io::dup;
13use rustix::io::read;
14use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout};
15use std::env::temp_dir;
16use std::ffi::OsString;
17use std::fs::File;
18use std::io::{self, Seek, SeekFrom, Write, pipe};
19use std::process::{Command, Stdio};
20use std::sync::atomic::Ordering;
21use std::sync::{Once, atomic::AtomicBool};
22use std::thread;
23
24pub mod pretty_print;
25
26#[derive(Debug)]
29pub struct CommandResult {
30 pub stdout: String,
32
33 pub stderr: String,
35
36 pub exit_code: i32,
38}
39
40static CHECK_GNU: Once = Once::new();
41static IS_GNU: AtomicBool = AtomicBool::new(false);
42
43pub fn is_gnu_cmd(cmd_path: &str) -> io::Result<()> {
44 CHECK_GNU.call_once(|| {
45 let version_output = Command::new(cmd_path).arg("--version").output().unwrap();
46
47 println!("version_output {version_output:#?}");
48
49 let version_str = String::from_utf8_lossy(&version_output.stdout).to_string();
50 if version_str.contains("GNU coreutils") {
51 IS_GNU.store(true, Ordering::Relaxed);
52 }
53 });
54
55 if IS_GNU.load(Ordering::Relaxed) {
56 Ok(())
57 } else {
58 panic!("Not the GNU implementation");
59 }
60}
61
62pub fn generate_and_run_uumain<F>(
63 args: &[OsString],
64 uumain_function: F,
65 pipe_input: Option<&str>,
66) -> CommandResult
67where
68 F: FnOnce(std::vec::IntoIter<OsString>) -> i32 + Send + 'static,
69{
70 let original_stdout_fd_owned = dup(io::stdout()).expect("Failed to duplicate STDOUT_FILENO");
72 let original_stderr_fd_owned = dup(io::stderr()).expect("Failed to duplicate STDERR_FILENO");
73
74 println!("Running test {:?}", &args[0..]);
75 let (read_pipe_stdout, write_pipe_stdout) = pipe().expect("Failed to create pipes");
76 let (read_pipe_stderr, write_pipe_stderr) = pipe().expect("Failed to create pipes");
77
78 dup2_stdout(&write_pipe_stdout).expect("Failed to redirect STDOUT_FILENO");
80 dup2_stderr(&write_pipe_stderr).expect("Failed to redirect STDERR_FILENO");
81
82 let original_stdin_fd_owned = if let Some(input_str) = pipe_input {
84 let mut input_file = tempfile::tempfile().unwrap();
86 write!(input_file, "{input_str}").unwrap();
87 input_file.seek(SeekFrom::Start(0)).unwrap();
88
89 let stdin_fd = dup(io::stdin()).expect("Failed to duplicate STDIN");
91
92 dup2_stdin(&input_file).expect("Failed to set up stdin redirection");
94
95 Some(stdin_fd)
96 } else {
97 None
98 };
99
100 let (uumain_exit_status, captured_stdout, captured_stderr) = thread::scope(|s| {
101 let out = s.spawn(|| read_from_fd(read_pipe_stdout));
102 let err = s.spawn(|| read_from_fd(read_pipe_stderr));
103 #[allow(clippy::unnecessary_to_owned)]
104 let status = uumain_function(args.to_owned().into_iter());
106 uucore::error::set_exit_code(0);
109 io::stdout().flush().unwrap();
110 io::stderr().flush().unwrap();
111 drop(write_pipe_stdout);
113 drop(write_pipe_stderr);
114 let _ = dup2_stdout(&original_stdout_fd_owned);
116 let _ = dup2_stderr(&original_stderr_fd_owned);
117 (status, out.join().unwrap(), err.join().unwrap())
118 });
119
120 if let Some(fd) = original_stdin_fd_owned {
122 dup2_stdin(&fd).expect("Failed to restore the original STDIN");
123 }
124
125 CommandResult {
126 stdout: captured_stdout,
127 stderr: captured_stderr
128 .split_once(':')
129 .map(|x| x.1)
130 .unwrap_or("")
131 .trim()
132 .to_string(),
133 exit_code: uumain_exit_status,
134 }
135}
136
137fn read_from_fd(fd: impl std::os::fd::AsFd) -> String {
138 let mut captured_output = Vec::new();
139 let mut read_buffer = [0; 1024];
140
141 loop {
142 match read(&fd, &mut read_buffer) {
143 Ok(0) => break,
144 Ok(bytes_read) => {
145 captured_output.extend_from_slice(&read_buffer[..bytes_read]);
146 }
147 Err(_) => {
148 eprintln!("Failed to read from the pipe");
149 break;
150 }
151 }
152 }
153
154 String::from_utf8_lossy(&captured_output).into_owned()
155}
156
157pub fn run_gnu_cmd(
158 cmd_path: &str,
159 args: &[OsString],
160 check_gnu: bool,
161 pipe_input: Option<&str>,
162) -> Result<CommandResult, CommandResult> {
163 if check_gnu {
164 if let Err(e) = is_gnu_cmd(cmd_path) {
166 return Err(CommandResult {
168 stdout: String::new(),
169 stderr: e.to_string(),
170 exit_code: -1,
171 });
172 }
173 }
174
175 let mut command = Command::new(cmd_path);
176 for arg in args {
177 command.arg(arg);
178 }
179
180 command.env("LC_ALL", "C");
183
184 let output = if let Some(input_str) = pipe_input {
185 command
187 .stdin(Stdio::piped())
188 .stdout(Stdio::piped())
189 .stderr(Stdio::piped());
190
191 let mut child = command.spawn().expect("Failed to execute command");
192 let child_stdin = child.stdin.as_mut().unwrap();
193 child_stdin
194 .write_all(input_str.as_bytes())
195 .expect("Failed to write to stdin");
196
197 match child.wait_with_output() {
198 Ok(output) => output,
199 Err(e) => {
200 return Err(CommandResult {
201 stdout: String::new(),
202 stderr: e.to_string(),
203 exit_code: -1,
204 });
205 }
206 }
207 } else {
208 match command.output() {
210 Ok(output) => output,
211 Err(e) => {
212 return Err(CommandResult {
213 stdout: String::new(),
214 stderr: e.to_string(),
215 exit_code: -1,
216 });
217 }
218 }
219 };
220 let exit_code = output.status.code().unwrap_or(-1);
221 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
223 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
224 let stderr = stderr
225 .split_once(':')
226 .map(|x| x.1)
227 .unwrap_or("")
228 .trim()
229 .to_string();
230
231 if output.status.success() || !check_gnu {
232 Ok(CommandResult {
233 stdout,
234 stderr,
235 exit_code,
236 })
237 } else {
238 Err(CommandResult {
239 stdout,
240 stderr,
241 exit_code,
242 })
243 }
244}
245
246pub fn compare_result(
255 test_type: &str,
256 input: &str,
257 pipe_input: Option<&str>,
258 rust_result: &CommandResult,
259 gnu_result: &CommandResult,
260 fail_on_stderr_diff: bool,
261) {
262 print_section(format!("Compare result for: {test_type} {input}"));
263
264 if let Some(pipe) = pipe_input {
265 println!("Pipe: {pipe}");
266 }
267
268 let mut discrepancies = Vec::new();
269 let mut should_panic = false;
270
271 if rust_result.stdout.trim() != gnu_result.stdout.trim() {
272 discrepancies.push("stdout differs");
273 println!("Rust stdout:");
274 print_or_empty(rust_result.stdout.as_str());
275 println!("GNU stdout:");
276 print_or_empty(gnu_result.stdout.as_ref());
277 print_diff(&rust_result.stdout, &gnu_result.stdout);
278 should_panic = true;
279 }
280
281 if rust_result.stderr.trim() != gnu_result.stderr.trim() {
282 discrepancies.push("stderr differs");
283 println!("Rust stderr:");
284 print_or_empty(rust_result.stderr.as_str());
285 println!("GNU stderr:");
286 print_or_empty(gnu_result.stderr.as_str());
287 print_diff(&rust_result.stderr, &gnu_result.stderr);
288 if fail_on_stderr_diff {
289 should_panic = true;
290 }
291 }
292
293 if rust_result.exit_code != gnu_result.exit_code {
294 discrepancies.push("exit code differs");
295 println!(
296 "Different exit code: (Rust: {}, GNU: {})",
297 rust_result.exit_code, gnu_result.exit_code
298 );
299 should_panic = true;
300 }
301
302 if discrepancies.is_empty() {
303 print_end_with_status("Same behavior", true);
304 } else {
305 print_with_style(
306 format!("Discrepancies detected: {}", discrepancies.join(", ")),
307 Style::new().red(),
308 );
309 if should_panic {
310 print_end_with_status(
311 format!("Test failed and will panic for: {test_type} {input}"),
312 false,
313 );
314 panic!("Test failed for: {test_type} {input}");
315 } else {
316 print_end_with_status(
317 format!("Test completed with discrepancies for: {test_type} {input}"),
318 false,
319 );
320 }
321 }
322 println!();
323}
324
325pub fn generate_random_string(max_length: usize) -> String {
326 let mut rng = rand::rng();
327 let valid_utf8: Vec<char> =
328 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789🔩🪛🪓⚙️🔗🧰"
329 .chars()
330 .collect();
331 let invalid_utf8 = [0xC3, 0x28]; let mut result = String::new();
333
334 for _ in 0..rng.random_range(0..=max_length) {
335 if rng.random_bool(0.9) {
336 let ch = valid_utf8.choose(&mut rng).unwrap();
337 result.push(*ch);
338 } else {
339 let ch = invalid_utf8.choose(&mut rng).unwrap();
340 if let Some(c) = char::from_u32(*ch as u32) {
341 result.push(c);
342 }
343 }
344 }
345
346 result
347}
348
349#[allow(dead_code)]
350pub fn generate_random_file() -> io::Result<String> {
351 let mut rng = rand::rng();
352 let file_name: String = (0..10)
353 .map(|_| rng.random_range(b'a'..=b'z') as char)
354 .collect();
355 let mut file_path = temp_dir();
356 file_path.push(file_name);
357
358 let mut file = File::create(&file_path)?;
359
360 let content_length = rng.random_range(10..1000);
361 let content: String = (0..content_length)
362 .map(|_| rng.random_range(b' '..=b'~') as char)
363 .collect();
364
365 file.write_all(content.as_bytes())?;
366
367 Ok(file_path.to_str().unwrap().to_string())
368}
369
370#[allow(dead_code)]
371pub fn replace_fuzz_binary_name(cmd: &str, result: &mut CommandResult) {
372 let fuzz_bin_name = format!("fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_{cmd}");
373
374 result.stdout = result.stdout.replace(&fuzz_bin_name, cmd);
375 result.stderr = result.stderr.replace(&fuzz_bin_name, cmd);
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use std::ffi::OsString;
382
383 #[test]
384 fn test_command_result_creation() {
385 let result = CommandResult {
386 stdout: "Hello, world!".to_string(),
387 stderr: "".to_string(),
388 exit_code: 0,
389 };
390
391 assert_eq!(result.stdout, "Hello, world!");
392 assert_eq!(result.stderr, "");
393 assert_eq!(result.exit_code, 0);
394 }
395
396 #[test]
397 fn test_generate_random_string() {
398 let result = generate_random_string(10);
399 assert!(result.chars().count() <= 10);
401
402 let empty_result = generate_random_string(0);
404 assert_eq!(empty_result.chars().count(), 0);
405 }
406
407 #[test]
408 fn test_replace_fuzz_binary_name() {
409 let mut result = CommandResult {
410 stdout: "fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_echo: error".to_string(),
411 stderr: "fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_echo failed".to_string(),
412 exit_code: 1,
413 };
414
415 replace_fuzz_binary_name("echo", &mut result);
416
417 assert_eq!(result.stdout, "echo: error");
418 assert_eq!(result.stderr, "echo failed");
419 assert_eq!(result.exit_code, 1);
420 }
421
422 #[test]
423 fn test_run_gnu_cmd_nonexistent() {
424 let args = vec![OsString::from("--version")];
425 let result = run_gnu_cmd("nonexistent_command_12345", &args, false, None);
426
427 assert!(result.is_err());
429 let error_result = result.unwrap_err();
430 assert_ne!(error_result.exit_code, 0);
431 }
432
433 #[test]
434 fn test_run_gnu_cmd_basic() {
435 let args = vec![OsString::from("--version")];
437 let result = run_gnu_cmd("echo", &args, false, None);
438
439 if let Err(e) = result {
442 assert_ne!(e.exit_code, -1); }
445 }
446
447 #[test]
448 fn test_run_gnu_cmd_with_pipe_input() {
449 let args: Vec<OsString> = vec![];
450 let pipe_input = "hello world";
451 let result = run_gnu_cmd("cat", &args, false, Some(pipe_input));
452 if let Ok(cmd_result) = result {
454 assert_eq!(cmd_result.stdout.trim(), "hello world");
455 }
456 }
457
458 #[test]
459 fn test_generate_random_file() {
460 let result = generate_random_file();
461 if let Ok(path) = result {
463 assert!(!path.is_empty());
464 let _ = std::fs::remove_file(&path);
466 }
467 }
468}