Skip to content

Commit e16a7cf

Browse files
committed
test(snapshots): bound the PTY spawn with the per-step timeout
The per-step timeout only wrapped the interaction phase (rx.recv_timeout): TestTerminal::spawn (openpty + fork/exec the child into the PTY) ran synchronously before it. On runners where that spawn can block (the Linux analog of the ConPTY blocking that keeps the Windows snapshot job on a GitHub-hosted runner), a wedged spawn was unbounded, and because the parallel execution gate holds the case's lease, it stalled the whole suite (6h job default, no step log). Run the spawn on a helper thread bounded by the same per-step timeout: a wedged spawn now fails that one case (like any other step timeout) and the suite proceeds. Success path is unchanged (verified against the interactive picker cases). The helper thread is abandoned on timeout, which is fine for a dev-only test binary that exits when the run ends.
1 parent 5ebe64e commit e16a7cf

1 file changed

Lines changed: 109 additions & 81 deletions

File tree

  • crates/vite_cli_snapshots/tests/cli_snapshots

crates/vite_cli_snapshots/tests/cli_snapshots/main.rs

Lines changed: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -907,93 +907,121 @@ fn run_case(
907907
let timeout = step.timeout(step_default_timeout);
908908

909909
let (termination_state, raw_output) = if step.tty {
910-
let mut cmd = CommandBuilder::new(&program);
911-
for arg in &argv[1..] {
912-
cmd.arg(arg);
913-
}
914-
cmd.env_clear();
915-
for (k, v) in step_env {
916-
cmd.env(k, v);
917-
}
918-
cmd.cwd(&step_cwd);
919-
920-
let terminal = TestTerminal::spawn(SCREEN_SIZE, cmd).unwrap();
921-
let mut killer = terminal.child_handle.clone();
922-
let interactions = step.interactions.clone();
923-
let formatted_snapshot = step.formatted_snapshot;
924-
let output = Arc::new(Mutex::new(String::new()));
925-
let output_for_thread = Arc::clone(&output);
926-
let (tx, rx) = mpsc::channel();
927-
std::thread::spawn(move || {
928-
let mut terminal = terminal;
929-
930-
for interaction in interactions {
931-
match interaction {
932-
Interaction::ExpectMilestone(expect) => {
933-
output_for_thread.lock().unwrap().push_str(&format!(
934-
"**→ expect-milestone:** `{}`\n\n",
935-
expect.expect_milestone
936-
));
937-
let milestone_screen =
938-
terminal.reader.expect_milestone(&expect.expect_milestone);
939-
let mut output = output_for_thread.lock().unwrap();
940-
push_fenced_block(&mut output, &milestone_screen);
941-
output.push('\n');
942-
}
943-
Interaction::Write(write) => {
944-
output_for_thread
945-
.lock()
946-
.unwrap()
947-
.push_str(&format!("**← write:** `{}`\n\n", write.write));
948-
terminal.writer.write_all(write.write.as_bytes()).unwrap();
949-
terminal.writer.flush().unwrap();
950-
}
951-
Interaction::WriteLine(write_line) => {
952-
output_for_thread.lock().unwrap().push_str(&format!(
953-
"**← write-line:** `{}`\n\n",
954-
write_line.write_line
955-
));
956-
terminal.writer.write_line(write_line.write_line.as_bytes()).unwrap();
957-
}
958-
Interaction::WriteKey(write_key) => {
959-
let key_name = write_key.write_key.as_str();
960-
output_for_thread
961-
.lock()
962-
.unwrap()
963-
.push_str(&format!("**← write-key:** `{key_name}`\n\n"));
964-
terminal.writer.write_all(write_key.write_key.bytes()).unwrap();
965-
terminal.writer.flush().unwrap();
910+
'tty: {
911+
let mut cmd = CommandBuilder::new(&program);
912+
for arg in &argv[1..] {
913+
cmd.arg(arg);
914+
}
915+
cmd.env_clear();
916+
for (k, v) in step_env {
917+
cmd.env(k, v);
918+
}
919+
cmd.cwd(&step_cwd);
920+
921+
// Bound the PTY spawn itself. `TestTerminal::spawn` (openpty plus
922+
// fork/exec of the child into the PTY) can block indefinitely on
923+
// some CI runners, and it runs before the interaction-phase
924+
// `recv_timeout` below could catch it. Run it on a helper thread so
925+
// a wedged spawn becomes a per-step timeout, not a suite-wide hang:
926+
// the helper thread is abandoned, this case fails, and the rest of
927+
// the suite proceeds.
928+
let (spawn_tx, spawn_rx) = mpsc::channel();
929+
std::thread::spawn(move || {
930+
let _ = spawn_tx.send(TestTerminal::spawn(SCREEN_SIZE, cmd));
931+
});
932+
let terminal = match spawn_rx.recv_timeout(timeout) {
933+
Ok(Ok(terminal)) => terminal,
934+
Ok(Err(err)) => panic!("failed to spawn PTY terminal: {err}"),
935+
Err(mpsc::RecvTimeoutError::Timeout) => {
936+
break 'tty (
937+
TerminationState::TimedOut,
938+
"(PTY spawn did not complete)".to_string(),
939+
);
940+
}
941+
Err(mpsc::RecvTimeoutError::Disconnected) => {
942+
panic!("PTY spawn thread panicked");
943+
}
944+
};
945+
let mut killer = terminal.child_handle.clone();
946+
let interactions = step.interactions.clone();
947+
let formatted_snapshot = step.formatted_snapshot;
948+
let output = Arc::new(Mutex::new(String::new()));
949+
let output_for_thread = Arc::clone(&output);
950+
let (tx, rx) = mpsc::channel();
951+
std::thread::spawn(move || {
952+
let mut terminal = terminal;
953+
954+
for interaction in interactions {
955+
match interaction {
956+
Interaction::ExpectMilestone(expect) => {
957+
output_for_thread.lock().unwrap().push_str(&format!(
958+
"**→ expect-milestone:** `{}`\n\n",
959+
expect.expect_milestone
960+
));
961+
let milestone_screen =
962+
terminal.reader.expect_milestone(&expect.expect_milestone);
963+
let mut output = output_for_thread.lock().unwrap();
964+
push_fenced_block(&mut output, &milestone_screen);
965+
output.push('\n');
966+
}
967+
Interaction::Write(write) => {
968+
output_for_thread
969+
.lock()
970+
.unwrap()
971+
.push_str(&format!("**← write:** `{}`\n\n", write.write));
972+
terminal.writer.write_all(write.write.as_bytes()).unwrap();
973+
terminal.writer.flush().unwrap();
974+
}
975+
Interaction::WriteLine(write_line) => {
976+
output_for_thread.lock().unwrap().push_str(&format!(
977+
"**← write-line:** `{}`\n\n",
978+
write_line.write_line
979+
));
980+
terminal
981+
.writer
982+
.write_line(write_line.write_line.as_bytes())
983+
.unwrap();
984+
}
985+
Interaction::WriteKey(write_key) => {
986+
let key_name = write_key.write_key.as_str();
987+
output_for_thread
988+
.lock()
989+
.unwrap()
990+
.push_str(&format!("**← write-key:** `{key_name}`\n\n"));
991+
terminal.writer.write_all(write_key.write_key.bytes()).unwrap();
992+
terminal.writer.flush().unwrap();
993+
}
966994
}
967995
}
968-
}
969996

970-
let status = terminal.reader.wait_for_exit().unwrap();
971-
let screen = if formatted_snapshot {
972-
render_formatted_screen(&terminal.reader.screen_contents_formatted())
973-
} else {
974-
terminal.reader.screen_contents()
975-
};
997+
let status = terminal.reader.wait_for_exit().unwrap();
998+
let screen = if formatted_snapshot {
999+
render_formatted_screen(&terminal.reader.screen_contents_formatted())
1000+
} else {
1001+
terminal.reader.screen_contents()
1002+
};
9761003

977-
{
978-
let mut output = output_for_thread.lock().unwrap();
979-
push_fenced_block(&mut output, &screen);
980-
}
1004+
{
1005+
let mut output = output_for_thread.lock().unwrap();
1006+
push_fenced_block(&mut output, &screen);
1007+
}
9811008

982-
let _ = tx.send(i64::from(status.exit_code()));
983-
});
1009+
let _ = tx.send(i64::from(status.exit_code()));
1010+
});
9841011

985-
match rx.recv_timeout(timeout) {
986-
Ok(exit_code) => {
987-
let output = output.lock().unwrap().clone();
988-
(TerminationState::Exited(exit_code), output)
989-
}
990-
Err(mpsc::RecvTimeoutError::Timeout) => {
991-
let _ = killer.kill();
992-
let output = output.lock().unwrap().clone();
993-
(TerminationState::TimedOut, output)
994-
}
995-
Err(mpsc::RecvTimeoutError::Disconnected) => {
996-
panic!("terminal thread panicked");
1012+
match rx.recv_timeout(timeout) {
1013+
Ok(exit_code) => {
1014+
let output = output.lock().unwrap().clone();
1015+
(TerminationState::Exited(exit_code), output)
1016+
}
1017+
Err(mpsc::RecvTimeoutError::Timeout) => {
1018+
let _ = killer.kill();
1019+
let output = output.lock().unwrap().clone();
1020+
(TerminationState::TimedOut, output)
1021+
}
1022+
Err(mpsc::RecvTimeoutError::Disconnected) => {
1023+
panic!("terminal thread panicked");
1024+
}
9971025
}
9981026
}
9991027
} else {

0 commit comments

Comments
 (0)