|
5 | 5 | use std::ffi::OsStr; |
6 | 6 | use std::process::ExitStatus; |
7 | 7 |
|
8 | | -use uutests::new_ucmd; |
| 8 | +use uutests::{get_tests_binary, new_ucmd}; |
9 | 9 |
|
10 | 10 | #[cfg(unix)] |
11 | 11 | fn check_termination(result: ExitStatus) { |
12 | | - // yes should exit successfully (code 0) when the pipe breaks. |
13 | | - // Rust ignores SIGPIPE by default, so write() returns EPIPE error, |
14 | | - // which is caught and handled gracefully. This matches GNU coreutils behavior. |
15 | | - assert!(result.success(), "yes should exit successfully"); |
| 12 | + // When SIGPIPE is NOT trapped, yes is killed by signal 13 (exit 141) |
| 13 | + // When SIGPIPE IS trapped, yes exits with code 1 |
| 14 | + assert!(!result.success(), "yes should fail on broken pipe"); |
16 | 15 | } |
17 | 16 |
|
18 | 17 | #[cfg(not(unix))] |
@@ -111,3 +110,57 @@ fn test_non_utf8() { |
111 | 110 | &b"\xbf\xff\xee bar\n".repeat(5000), |
112 | 111 | ); |
113 | 112 | } |
| 113 | + |
| 114 | +/// Test SIGPIPE handling in normal pipe scenario |
| 115 | +/// |
| 116 | +/// When SIGPIPE is NOT trapped, `yes` should: |
| 117 | +/// 1. Be killed by SIGPIPE signal (exit code 141 = 128 + 13) |
| 118 | +/// 2. NOT print any error message to stderr |
| 119 | +/// |
| 120 | +/// This test uses a shell command to simulate `yes | head -n 1` |
| 121 | +/// The expected behavior matches GNU yes. |
| 122 | +#[test] |
| 123 | +#[cfg(unix)] |
| 124 | +fn test_normal_pipe_sigpipe() { |
| 125 | + use std::process::Command; |
| 126 | + |
| 127 | + // Run `yes | head -n 1` via shell with pipefail to capture yes's exit code |
| 128 | + // In this scenario, SIGPIPE is not trapped, so yes should be killed by the signal |
| 129 | + let output = Command::new("sh") |
| 130 | + .arg("-c") |
| 131 | + .arg(format!( |
| 132 | + "set -o pipefail; {} yes | head -n 1 > /dev/null", |
| 133 | + get_tests_binary!() |
| 134 | + )) |
| 135 | + .output() |
| 136 | + .expect("Failed to execute yes | head"); |
| 137 | + |
| 138 | + // Extract exit code |
| 139 | + let exit_code = output.status.code(); |
| 140 | + |
| 141 | + // The process should be killed by SIGPIPE (signal 13) |
| 142 | + // Exit code should be 141 (128 + 13) on most Unix systems |
| 143 | + // OR the process was terminated by signal (status.code() returns None) |
| 144 | + if let Some(code) = exit_code { |
| 145 | + println!("Exit code: {code}"); |
| 146 | + println!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); |
| 147 | + |
| 148 | + assert_eq!( |
| 149 | + code, 141, |
| 150 | + "yes should exit with code 141 (killed by SIGPIPE), but got {code}" |
| 151 | + ); |
| 152 | + } else { |
| 153 | + // Process was terminated by signal (which is also acceptable) |
| 154 | + use std::os::unix::process::ExitStatusExt; |
| 155 | + let signal = output.status.signal().unwrap(); |
| 156 | + println!("Terminated by signal: {signal}"); |
| 157 | + // Signal 13 is SIGPIPE |
| 158 | + assert_eq!(signal, 13, "yes should be killed by SIGPIPE (13)"); |
| 159 | + } |
| 160 | + |
| 161 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 162 | + assert!( |
| 163 | + stderr.is_empty(), |
| 164 | + "yes should NOT print error message in normal pipe scenario, but got: {stderr}" |
| 165 | + ); |
| 166 | +} |
0 commit comments