Skip to content

Commit 40eb45b

Browse files
committed
fix: preserve PTY wait errors and remove test serialization
1 parent 14cd60c commit 40eb45b

5 files changed

Lines changed: 34 additions & 58 deletions

File tree

crates/pty_terminal/src/terminal.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub struct PtyWriter {
3232
/// A cloneable handle to a child process spawned in a PTY.
3333
pub struct ChildHandle {
3434
child_killer: Box<dyn ChildKiller + Send + Sync>,
35-
exit_status: Arc<OnceLock<ExitStatus>>,
35+
exit_status: Arc<OnceLock<std::io::Result<ExitStatus>>>,
3636
}
3737

3838
impl Clone for ChildHandle {
@@ -221,9 +221,16 @@ impl PtyWriter {
221221

222222
impl ChildHandle {
223223
/// Blocks until the child process has exited and returns its exit status.
224+
///
225+
/// # Errors
226+
///
227+
/// Returns an error if waiting for the child process exit status fails.
224228
#[must_use]
225-
pub fn wait(&self) -> ExitStatus {
226-
self.exit_status.wait().clone()
229+
pub fn wait(&self) -> std::io::Result<ExitStatus> {
230+
match self.exit_status.wait() {
231+
Ok(status) => Ok(status.clone()),
232+
Err(error) => Err(std::io::Error::new(error.kind(), error.to_string())),
233+
}
227234
}
228235

229236
/// Kills the child process.
@@ -238,11 +245,10 @@ impl ChildHandle {
238245
}
239246

240247
fn set_exit_status_from_wait_result(
241-
exit_status: &OnceLock<ExitStatus>,
248+
exit_status: &OnceLock<std::io::Result<ExitStatus>>,
242249
wait_result: std::io::Result<ExitStatus>,
243250
) {
244-
let status = wait_result.unwrap_or_else(|_| ExitStatus::with_exit_code(1));
245-
let _ = exit_status.set(status);
251+
let _ = exit_status.set(wait_result);
246252
}
247253

248254
impl Terminal {
@@ -271,7 +277,7 @@ impl Terminal {
271277
let child_killer = child.clone_killer();
272278
drop(pty_pair.slave); // Critical: drop slave so EOF is signaled when child exits
273279
let master = pty_pair.master;
274-
let exit_status: Arc<OnceLock<ExitStatus>> = Arc::new(OnceLock::new());
280+
let exit_status: Arc<OnceLock<std::io::Result<ExitStatus>>> = Arc::new(OnceLock::new());
275281

276282
// Background thread: wait for child to exit, set exit status, then close writer to trigger EOF
277283
thread::spawn({
@@ -307,16 +313,17 @@ mod tests {
307313
use super::*;
308314

309315
#[test]
310-
fn set_exit_status_from_wait_result_sets_error_fallback() {
316+
fn set_exit_status_from_wait_result_preserves_error() {
311317
let exit_status = OnceLock::new();
312318
set_exit_status_from_wait_result(
313319
&exit_status,
314320
Err(std::io::Error::other("forced wait error for test")),
315321
);
316322

317323
let status = exit_status.wait();
318-
assert!(!status.success());
319-
assert_eq!(status.exit_code(), 1);
324+
assert!(status.is_err());
325+
assert_eq!(status.as_ref().unwrap_err().kind(), std::io::ErrorKind::Other);
326+
assert_eq!(status.as_ref().unwrap_err().to_string(), "forced wait error for test");
320327
}
321328

322329
#[test]
@@ -325,7 +332,7 @@ mod tests {
325332
set_exit_status_from_wait_result(&exit_status, Ok(ExitStatus::with_exit_code(42)));
326333

327334
let status = exit_status.wait();
328-
assert!(!status.success());
329-
assert_eq!(status.exit_code(), 42);
335+
assert!(!status.as_ref().unwrap().success());
336+
assert_eq!(status.as_ref().unwrap().exit_code(), 42);
330337
}
331338
}

crates/pty_terminal/tests/terminal.rs

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#[cfg(windows)]
2-
use std::sync::{LazyLock, Mutex, MutexGuard};
31
use std::{
42
io::{BufRead, BufReader, IsTerminal, Read, Write, stderr, stdin, stdout},
53
time::{Duration, Instant},
@@ -10,19 +8,10 @@ use portable_pty::CommandBuilder;
108
use pty_terminal::{geo::ScreenSize, terminal::Terminal};
119
use subprocess_test::command_for_fn;
1210

13-
#[cfg(windows)]
14-
fn windows_test_serial_guard() -> MutexGuard<'static, ()> {
15-
static WINDOWS_TEST_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
16-
WINDOWS_TEST_MUTEX.lock().unwrap()
17-
}
18-
1911
#[test]
2012
#[timeout(5000)]
2113
#[expect(clippy::print_stdout, reason = "subprocess test output")]
2214
fn is_terminal() {
23-
#[cfg(windows)]
24-
let _guard = windows_test_serial_guard();
25-
2615
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
2716
println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal());
2817
}));
@@ -31,7 +20,7 @@ fn is_terminal() {
3120
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
3221
let mut discard = Vec::new();
3322
pty_reader.read_to_end(&mut discard).unwrap();
34-
let _ = child_handle.wait();
23+
let _ = child_handle.wait().unwrap();
3524
let output = pty_reader.screen_contents();
3625
assert_eq!(output.trim(), "true true true");
3726
}
@@ -40,9 +29,6 @@ fn is_terminal() {
4029
#[timeout(5000)]
4130
#[expect(clippy::print_stdout, reason = "subprocess test output")]
4231
fn write_basic_echo() {
43-
#[cfg(windows)]
44-
let _guard = windows_test_serial_guard();
45-
4632
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
4733
use std::io::{BufRead, Write, stdin, stdout};
4834
let stdin = stdin();
@@ -61,7 +47,7 @@ fn write_basic_echo() {
6147

6248
let mut discard = Vec::new();
6349
pty_reader.read_to_end(&mut discard).unwrap();
64-
let _ = child_handle.wait();
50+
let _ = child_handle.wait().unwrap();
6551

6652
let output = pty_reader.screen_contents();
6753
// PTY echoes the input, so we see "hello world\nhello world"
@@ -72,9 +58,6 @@ fn write_basic_echo() {
7258
#[timeout(5000)]
7359
#[expect(clippy::print_stdout, reason = "subprocess test output")]
7460
fn write_multiple_lines() {
75-
#[cfg(windows)]
76-
let _guard = windows_test_serial_guard();
77-
7861
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
7962
use std::io::{BufRead, Write, stdin, stdout};
8063
let stdin = stdin();
@@ -115,7 +98,7 @@ fn write_multiple_lines() {
11598

11699
let mut discard = Vec::new();
117100
pty_reader.read_to_end(&mut discard).unwrap();
118-
let _ = child_handle.wait();
101+
let _ = child_handle.wait().unwrap();
119102

120103
let output = pty_reader.screen_contents();
121104
// PTY echoes input, then child prints "Echo: {line}\n" for each
@@ -126,9 +109,6 @@ fn write_multiple_lines() {
126109
#[timeout(5000)]
127110
#[expect(clippy::print_stdout, reason = "subprocess test output")]
128111
fn write_after_exit() {
129-
#[cfg(windows)]
130-
let _guard = windows_test_serial_guard();
131-
132112
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
133113
print!("exiting");
134114
}));
@@ -139,7 +119,7 @@ fn write_after_exit() {
139119
// Read all output - this blocks until child exits and EOF is reached
140120
let mut discard = Vec::new();
141121
pty_reader.read_to_end(&mut discard).unwrap();
142-
let _ = child_handle.wait();
122+
let _ = child_handle.wait().unwrap();
143123

144124
// Writer shutdown is done by a background thread after child wait returns.
145125
// Poll briefly for the writer state to flip to closed before asserting write failure.
@@ -157,9 +137,6 @@ fn write_after_exit() {
157137
#[timeout(5000)]
158138
#[expect(clippy::print_stdout, reason = "subprocess test output")]
159139
fn write_interactive_prompt() {
160-
#[cfg(windows)]
161-
let _guard = windows_test_serial_guard();
162-
163140
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
164141
use std::io::{Write, stdin, stdout};
165142
let mut stdout = stdout();
@@ -188,7 +165,7 @@ fn write_interactive_prompt() {
188165

189166
let mut discard = Vec::new();
190167
pty_reader.read_to_end(&mut discard).unwrap();
191-
let _ = child_handle.wait();
168+
let _ = child_handle.wait().unwrap();
192169

193170
let output = pty_reader.screen_contents();
194171
assert_eq!(output.trim(), "Name: Alice\nHello, Alice");
@@ -198,9 +175,6 @@ fn write_interactive_prompt() {
198175
#[timeout(5000)]
199176
#[expect(clippy::print_stdout, reason = "subprocess test output")]
200177
fn resize_terminal() {
201-
#[cfg(windows)]
202-
let _guard = windows_test_serial_guard();
203-
204178
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
205179
use std::io::{Write, stdin, stdout};
206180
#[cfg(unix)]
@@ -298,9 +272,6 @@ fn resize_terminal() {
298272
#[timeout(5000)]
299273
#[expect(clippy::print_stdout, reason = "subprocess test output")]
300274
fn send_ctrl_c_interrupts_process() {
301-
#[cfg(windows)]
302-
let _guard = windows_test_serial_guard();
303-
304275
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
305276
use std::io::{Write, stdout};
306277

@@ -367,9 +338,6 @@ fn send_ctrl_c_interrupts_process() {
367338
#[timeout(5000)]
368339
#[expect(clippy::print_stdout, reason = "subprocess test output")]
369340
fn read_to_end_returns_exit_status_success() {
370-
#[cfg(windows)]
371-
let _guard = windows_test_serial_guard();
372-
373341
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
374342
println!("success");
375343
}));
@@ -378,17 +346,14 @@ fn read_to_end_returns_exit_status_success() {
378346
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
379347
let mut discard = Vec::new();
380348
pty_reader.read_to_end(&mut discard).unwrap();
381-
let status = child_handle.wait();
349+
let status = child_handle.wait().unwrap();
382350
assert!(status.success());
383351
assert_eq!(status.exit_code(), 0);
384352
}
385353

386354
#[test]
387355
#[timeout(5000)]
388356
fn read_to_end_returns_exit_status_nonzero() {
389-
#[cfg(windows)]
390-
let _guard = windows_test_serial_guard();
391-
392357
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
393358
std::process::exit(42);
394359
}));
@@ -397,7 +362,7 @@ fn read_to_end_returns_exit_status_nonzero() {
397362
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
398363
let mut discard = Vec::new();
399364
pty_reader.read_to_end(&mut discard).unwrap();
400-
let status = child_handle.wait();
365+
let status = child_handle.wait().unwrap();
401366
assert!(!status.success());
402367
assert_eq!(status.exit_code(), 42);
403368
}

crates/pty_terminal_test/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,14 @@ impl Reader {
9191

9292
/// Reads all remaining PTY output until the child exits, then returns the exit status.
9393
///
94+
/// # Errors
95+
///
96+
/// Returns an error if waiting for the child process exit status fails.
97+
///
9498
/// # Panics
9599
///
96100
/// Panics if reading from the PTY fails.
97-
pub fn wait_for_exit(&mut self) -> ExitStatus {
101+
pub fn wait_for_exit(&mut self) -> std::io::Result<ExitStatus> {
98102
let mut discard = Vec::new();
99103
self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed");
100104
self.child_handle.wait()

crates/pty_terminal_test/tests/milestone.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn milestone_raw_mode_keystrokes() {
6767
// Write 'q' to quit and wait for the child to exit
6868
writer.write_all(b"q").unwrap();
6969
writer.flush().unwrap();
70-
let status = reader.wait_for_exit();
70+
let status = reader.wait_for_exit().unwrap();
7171
assert!(status.success());
7272
}
7373

@@ -122,6 +122,6 @@ fn milestone_does_not_pollute_screen() {
122122

123123
writer.write_all(b"q").unwrap();
124124
writer.flush().unwrap();
125-
let status = reader.wait_for_exit();
125+
let status = reader.wait_for_exit().unwrap();
126126
assert!(status.success());
127127
}

crates/vite_task_bin/tests/e2e_snapshots/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
391391
}
392392
}
393393

394-
let status = terminal.reader.wait_for_exit();
394+
let status = terminal.reader.wait_for_exit().unwrap();
395395
let screen = terminal.reader.screen_contents();
396396

397397
{

0 commit comments

Comments
 (0)