Skip to content

Commit 14cd60c

Browse files
committed
fix: prevent PTY wait deadlock and serialize Windows tests
1 parent 79d6100 commit 14cd60c

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

crates/pty_terminal/src/terminal.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ impl ChildHandle {
237237
}
238238
}
239239

240+
fn set_exit_status_from_wait_result(
241+
exit_status: &OnceLock<ExitStatus>,
242+
wait_result: std::io::Result<ExitStatus>,
243+
) {
244+
let status = wait_result.unwrap_or_else(|_| ExitStatus::with_exit_code(1));
245+
let _ = exit_status.set(status);
246+
}
247+
240248
impl Terminal {
241249
/// Spawns a new child process in a headless terminal with the given size and command.
242250
///
@@ -270,10 +278,7 @@ impl Terminal {
270278
let writer = Arc::clone(&writer);
271279
let exit_status = Arc::clone(&exit_status);
272280
move || {
273-
// Wait for child and set exit status
274-
if let Ok(status) = child.wait() {
275-
let _ = exit_status.set(status);
276-
}
281+
set_exit_status_from_wait_result(&exit_status, child.wait());
277282
// Close writer to signal EOF to the reader
278283
*writer.lock().unwrap() = None;
279284
}
@@ -296,3 +301,31 @@ impl Terminal {
296301
})
297302
}
298303
}
304+
305+
#[cfg(test)]
306+
mod tests {
307+
use super::*;
308+
309+
#[test]
310+
fn set_exit_status_from_wait_result_sets_error_fallback() {
311+
let exit_status = OnceLock::new();
312+
set_exit_status_from_wait_result(
313+
&exit_status,
314+
Err(std::io::Error::other("forced wait error for test")),
315+
);
316+
317+
let status = exit_status.wait();
318+
assert!(!status.success());
319+
assert_eq!(status.exit_code(), 1);
320+
}
321+
322+
#[test]
323+
fn set_exit_status_from_wait_result_preserves_child_status() {
324+
let exit_status = OnceLock::new();
325+
set_exit_status_from_wait_result(&exit_status, Ok(ExitStatus::with_exit_code(42)));
326+
327+
let status = exit_status.wait();
328+
assert!(!status.success());
329+
assert_eq!(status.exit_code(), 42);
330+
}
331+
}

crates/pty_terminal/tests/terminal.rs

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

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+
1119
#[test]
1220
#[timeout(5000)]
1321
#[expect(clippy::print_stdout, reason = "subprocess test output")]
1422
fn is_terminal() {
23+
#[cfg(windows)]
24+
let _guard = windows_test_serial_guard();
25+
1526
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
1627
println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal());
1728
}));
@@ -29,6 +40,9 @@ fn is_terminal() {
2940
#[timeout(5000)]
3041
#[expect(clippy::print_stdout, reason = "subprocess test output")]
3142
fn write_basic_echo() {
43+
#[cfg(windows)]
44+
let _guard = windows_test_serial_guard();
45+
3246
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
3347
use std::io::{BufRead, Write, stdin, stdout};
3448
let stdin = stdin();
@@ -58,6 +72,9 @@ fn write_basic_echo() {
5872
#[timeout(5000)]
5973
#[expect(clippy::print_stdout, reason = "subprocess test output")]
6074
fn write_multiple_lines() {
75+
#[cfg(windows)]
76+
let _guard = windows_test_serial_guard();
77+
6178
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
6279
use std::io::{BufRead, Write, stdin, stdout};
6380
let stdin = stdin();
@@ -109,6 +126,9 @@ fn write_multiple_lines() {
109126
#[timeout(5000)]
110127
#[expect(clippy::print_stdout, reason = "subprocess test output")]
111128
fn write_after_exit() {
129+
#[cfg(windows)]
130+
let _guard = windows_test_serial_guard();
131+
112132
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
113133
print!("exiting");
114134
}));
@@ -137,6 +157,9 @@ fn write_after_exit() {
137157
#[timeout(5000)]
138158
#[expect(clippy::print_stdout, reason = "subprocess test output")]
139159
fn write_interactive_prompt() {
160+
#[cfg(windows)]
161+
let _guard = windows_test_serial_guard();
162+
140163
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
141164
use std::io::{Write, stdin, stdout};
142165
let mut stdout = stdout();
@@ -175,6 +198,9 @@ fn write_interactive_prompt() {
175198
#[timeout(5000)]
176199
#[expect(clippy::print_stdout, reason = "subprocess test output")]
177200
fn resize_terminal() {
201+
#[cfg(windows)]
202+
let _guard = windows_test_serial_guard();
203+
178204
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
179205
use std::io::{Write, stdin, stdout};
180206
#[cfg(unix)]
@@ -272,6 +298,9 @@ fn resize_terminal() {
272298
#[timeout(5000)]
273299
#[expect(clippy::print_stdout, reason = "subprocess test output")]
274300
fn send_ctrl_c_interrupts_process() {
301+
#[cfg(windows)]
302+
let _guard = windows_test_serial_guard();
303+
275304
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
276305
use std::io::{Write, stdout};
277306

@@ -338,6 +367,9 @@ fn send_ctrl_c_interrupts_process() {
338367
#[timeout(5000)]
339368
#[expect(clippy::print_stdout, reason = "subprocess test output")]
340369
fn read_to_end_returns_exit_status_success() {
370+
#[cfg(windows)]
371+
let _guard = windows_test_serial_guard();
372+
341373
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
342374
println!("success");
343375
}));
@@ -352,9 +384,11 @@ fn read_to_end_returns_exit_status_success() {
352384
}
353385

354386
#[test]
355-
#[cfg_attr(windows, timeout(15000))]
356-
#[cfg_attr(not(windows), timeout(5000))]
387+
#[timeout(5000)]
357388
fn read_to_end_returns_exit_status_nonzero() {
389+
#[cfg(windows)]
390+
let _guard = windows_test_serial_guard();
391+
358392
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
359393
std::process::exit(42);
360394
}));

0 commit comments

Comments
 (0)