@@ -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