@@ -49,13 +49,10 @@ struct PseudoTerminalTests {
4949 @Test func `read reports end of input once the child and the local replica are gone`() throws {
5050 let terminal = try PseudoTerminal ( )
5151 let process = try startProcess ( script: " printf 'done \\ n' " , terminal: terminal)
52- terminal. closeReplica ( )
53-
54- let output = drainToEndOfInput ( terminal)
55- process. waitUntilExit ( )
56- terminal. closePrimary ( )
5752
5853 // Terminating at all is the assertion: a leaked replica descriptor would hang this forever.
54+ let output = collectOutput ( of: process, on: terminal)
55+
5956 #expect( output == " done \n " )
6057 }
6158
@@ -96,40 +93,66 @@ struct PseudoTerminalTests {
9693}
9794
9895private extension PseudoTerminalTests {
99- /// Runs `script` with the replica as stdout+stderr and drains the primary to end-of-input.
10096 func runOnPseudoTerminal(
10197 columns: UInt16 = PseudoTerminal . defaultColumns,
10298 rows: UInt16 = PseudoTerminal . defaultRows,
10399 script: String ,
104100 ) throws -> String {
105101 let terminal = try PseudoTerminal ( columns: columns, rows: rows)
106102 let process = try startProcess ( script: script, terminal: terminal)
107- terminal . closeReplica ( )
103+ let output = collectOutput ( of : process , on : terminal )
108104
109- let output = drainToEndOfInput ( terminal)
110- process. waitUntilExit ( )
111- terminal. closePrimary ( )
105+ #expect( process. terminationStatus == 0 , " the child exited \( process. terminationStatus) " )
112106
113107 return output
114108 }
115109
116- /// Ignores idle timeouts; safe because every script under test terminates on its own.
117- func drainToEndOfInput( _ terminal: PseudoTerminal ) -> String {
110+ func collectOutput( of process: Process , on terminal: PseudoTerminal ) -> String {
118111 var data = Data ( )
112+
113+ live: while true {
114+ switch terminal. read ( timeout: . milliseconds( 25 ) ) {
115+ case let . data( chunk) :
116+ data. append ( chunk)
117+ case . timedOut:
118+ if !process. isRunning {
119+ break live
120+ }
121+ case . endOfInput:
122+ Issue . record ( " end of input arrived while this process still held the replica open " )
123+ break live
124+ case let . failed( code) :
125+ Issue . record ( " reading the terminal failed: \( PseudoTerminal . describe ( errno: code) ) " )
126+ break live
127+ }
128+ }
129+ process. waitUntilExit ( )
130+
131+ terminal. closeReplica ( )
132+ drainToEndOfInput ( terminal, into: & data)
133+ terminal. closePrimary ( )
134+
135+ return UTF8StreamDecoder . lossyString ( data)
136+ }
137+
138+ func drainToEndOfInput( _ terminal: PseudoTerminal , into data: inout Data ) {
139+ let deadline = Date ( ) . addingTimeInterval ( 10 )
119140 loop: while true {
120141 switch terminal. read ( ) {
121142 case let . data( chunk) :
122143 data. append ( chunk)
123144 case . timedOut:
124- continue
145+ if Date ( ) > deadline {
146+ Issue . record ( " end of input never arrived: a replica descriptor is still open somewhere " )
147+ break loop
148+ }
125149 case . endOfInput:
126150 break loop
127151 case let . failed( code) :
128152 Issue . record ( " reading the terminal failed: \( PseudoTerminal . describe ( errno: code) ) " )
129153 break loop
130154 }
131155 }
132- return UTF8StreamDecoder . lossyString ( data)
133156 }
134157
135158 /// `Foundation.Process` deliberately, to exercise ``PseudoTerminal`` independently of the runner.
0 commit comments