|
|
@@ -137,10 +137,20 @@ async fn wait_ready(base_url: &str) -> Result<()> {
|
|
|
}
|
|
|
|
|
|
/// Tee one output pipe to the console and the log file.
|
|
|
+/// Tee one output pipe to the console and the log file.
|
|
|
+///
|
|
|
+/// The read buffer must not be empty: `read()` on a zero-length slice
|
|
|
+/// returns 0 (interpreted as EOF), so an empty `Vec` would silently sever
|
|
|
+/// the pipe — the child then dies on its first stderr write with EPIPE
|
|
|
+/// (silent exit 120 in CPython).
|
|
|
fn pump<R: Read>(mut reader: R, to_console: bool, file: Arc<Mutex<fs::File>>) {
|
|
|
- let mut buf = Vec::new();
|
|
|
+ let mut buf = vec![0u8; 8192];
|
|
|
loop {
|
|
|
- let n = reader.read(&mut buf).expect("reading bench output pipe");
|
|
|
+ let n = match reader.read(&mut buf) {
|
|
|
+ Ok(n) => n,
|
|
|
+ Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
|
+ Err(e) => panic!("reading bench output pipe: {e}"),
|
|
|
+ };
|
|
|
if n == 0 {
|
|
|
break;
|
|
|
}
|
|
|
@@ -150,7 +160,6 @@ fn pump<R: Read>(mut reader: R, to_console: bool, file: Arc<Mutex<fs::File>>) {
|
|
|
if let Ok(mut f) = file.lock() {
|
|
|
f.write_all(&buf[..n]).ok();
|
|
|
}
|
|
|
- buf.clear();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -321,6 +330,38 @@ mod tests {
|
|
|
assert_eq!(civil_date(20_818), (2026, 12, 31));
|
|
|
}
|
|
|
|
|
|
+ /// Regression: the pump must not treat a not-yet-written pipe as EOF.
|
|
|
+ /// `read()` into a zero-length buffer returns 0, which the old pump
|
|
|
+ /// treated as EOF — closing the pipe and killing the child on its first
|
|
|
+ /// stderr write with EPIPE (silent exit 120 in CPython).
|
|
|
+ #[test]
|
|
|
+ fn pump_keeps_pipe_open_until_child_exits() {
|
|
|
+ let tmp =
|
|
|
+ std::env::temp_dir().join(format!("sm121-bench-pump-test-{}", std::process::id()));
|
|
|
+ fs::create_dir_all(&tmp).unwrap();
|
|
|
+ let log = tmp.join("pump.log");
|
|
|
+
|
|
|
+ let mut child = Command::new("sh")
|
|
|
+ .args(["-c", "sleep 0.3; echo late-output >&2"])
|
|
|
+ .stdout(Stdio::piped())
|
|
|
+ .stderr(Stdio::piped())
|
|
|
+ .spawn()
|
|
|
+ .unwrap();
|
|
|
+ let out = child.stdout.take().unwrap();
|
|
|
+ let err = child.stderr.take().unwrap();
|
|
|
+ let f = Arc::new(Mutex::new(fs::File::create(&log).unwrap()));
|
|
|
+ let f1 = f.clone();
|
|
|
+ let t1 = std::thread::spawn(move || pump(out, false, f1));
|
|
|
+ let t2 = std::thread::spawn(move || pump(err, false, f));
|
|
|
+ let status = child.wait().unwrap();
|
|
|
+ t1.join().unwrap();
|
|
|
+ t2.join().unwrap();
|
|
|
+ let body = fs::read_to_string(&log).unwrap();
|
|
|
+ assert_eq!(status.code(), Some(0), "child must not hit EPIPE: {body}");
|
|
|
+ assert!(body.contains("late-output"), "piped stderr lost: {body:?}");
|
|
|
+ let _ = fs::remove_dir_all(&tmp);
|
|
|
+ }
|
|
|
+
|
|
|
#[test]
|
|
|
fn sanitize_replaces_bad_chars() {
|
|
|
assert_eq!(sanitize("arm B: 2x"), "arm-B--2x");
|