@@ -445,34 +445,21 @@ class TestUnixConsoleInputHook(TestCase):
445445
446446 def test_input_hook_output_is_cooked (self ):
447447 master_fd , slave_fd = pty .openpty ()
448-
449- # Drain the master continuously: on some platforms (e.g. macOS)
450- # tcsetattr(TCSADRAIN) blocks until the master side is read, so an
451- # undrained pty would deadlock the mode switch.
452- chunks = []
453- reading = True
454-
455- def reader ():
456- while reading :
457- r , _ , _ = select .select ([master_fd ], [], [], 0.1 )
458- if master_fd in r :
459- try :
460- data = os .read (master_fd , 4096 )
461- except OSError :
462- break
463- if not data :
464- break
465- chunks .append (data )
466-
467- reader_thread = threading .Thread (target = reader )
468- reader_thread .start ()
469-
470- def cleanup ():
471- nonlocal reading
472- reading = False
473- reader_thread .join ()
474- os .close (master_fd )
475- self .addCleanup (cleanup )
448+ self .addCleanup (os .close , master_fd )
449+
450+ # tcsetattr(TCSADRAIN) blocks on some platforms (e.g. macOS) while the
451+ # master still holds unread output, so empty it before each mode switch.
452+ def drain ():
453+ out = b""
454+ while select .select ([master_fd ], [], [], 0 )[0 ]:
455+ try :
456+ data = os .read (master_fd , 4096 )
457+ except OSError :
458+ break
459+ if not data :
460+ break
461+ out += data
462+ return out
476463
477464 # Start from a cooked terminal so there are saved flags to restore.
478465 attr = _termios .tcgetattr (slave_fd )
@@ -482,6 +469,7 @@ def cleanup():
482469 console = UnixConsole (slave_fd , slave_fd , term = "xterm" )
483470 console .prepare ()
484471 try :
472+ drain () # discard prepare()'s own setup sequences
485473 # pyrepl's own rendering runs with OPOST cleared.
486474 self .assertFalse (_termios .tcgetattr (slave_fd )[1 ] & _termios .OPOST )
487475
@@ -490,6 +478,7 @@ def cleanup():
490478 def fake_hook ():
491479 observed ["oflag" ] = _termios .tcgetattr (slave_fd )[1 ]
492480 os .write (slave_fd , b"line1\n line2\n " )
481+ observed ["output" ] = drain ()
493482 return 0
494483
495484 with patch ("_pyrepl.unix_console.posix" ) as mock_posix :
@@ -503,24 +492,27 @@ def fake_hook():
503492 self .assertTrue (observed ["oflag" ] & _termios .OPOST )
504493 # ...and raw mode was restored afterwards.
505494 self .assertFalse (_termios .tcgetattr (slave_fd )[1 ] & _termios .OPOST )
495+ # The tty translated the hook's bare '\n' into '\r\n'.
496+ self .assertEqual (observed ["output" ], b"line1\r \n line2\r \n " )
506497 finally :
507- console .restore ()
508- os .close (slave_fd )
509-
510- # The switch back to raw mode already drained the hook's output, so
511- # joining the reader is enough -- no sleep needed.
512- reading = False
513- reader_thread .join ()
514- while select .select ([master_fd ], [], [], 0 )[0 ]:
498+ # restore() writes and only then switches modes, so there is no
499+ # point left to drain from here; keep the master empty elsewhere.
500+ stop = threading .Event ()
501+
502+ def pump ():
503+ while not stop .is_set ():
504+ if select .select ([master_fd ], [], [], 0.05 )[0 ]:
505+ try :
506+ if not os .read (master_fd , 4096 ):
507+ break
508+ except OSError :
509+ break
510+
511+ pump_thread = threading .Thread (target = pump )
512+ pump_thread .start ()
515513 try :
516- extra = os .read (master_fd , 4096 )
517- except OSError :
518- break
519- if not extra :
520- break
521- chunks .append (extra )
522-
523- data = b"" .join (chunks )
524- # The tty translated the hook's bare '\n' into '\r\n'.
525- self .assertIn (b"line1\r \n line2\r \n " , data )
526- self .assertNotIn (b"line1\n line2\n " , data )
514+ console .restore ()
515+ finally :
516+ stop .set ()
517+ pump_thread .join ()
518+ os .close (slave_fd )
0 commit comments