66import termios
77from functools import partial
88from test .support import os_helper , force_not_colorized_test_class
9- from test .support .strace_helper import requires_strace
109import subprocess
11- import shutil
1210import signal
1311import textwrap
1412
@@ -339,38 +337,104 @@ def test_eio_error_handling_in_restore(self, mock_tcgetattr, mock_tcsetattr):
339337 self .fail ("EIO error should have been handled gracefully in restore()" )
340338 raise
341339
342- @requires_strace ()
343- class TestEIOWithStrace (unittest .TestCase ):
344- def _attach_strace (self , pid ):
345- strace = shutil .which ("strace" )
346- cmd = [strace , "-qq" , "-p" , str (pid ), "-e" , "inject=read:error=EIO:when=1" , "-o" , "/dev/null" ]
347- try :
348- p = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
349- try :
350- p .communicate (timeout = 0.15 )
351- except subprocess .TimeoutExpired :
352- return p
353- except (OSError , subprocess .SubprocessError ):
354- pass
355- except (OSError , subprocess .SubprocessError ):
356- pass
357-
358- # Fallback command
359- cmd = [strace , "-qq" , "-p" , str (pid ), "-e" , "fault=read:error=EIO:when=1" , "-o" , "/dev/null" ]
360- p = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
361- try :
362- p .communicate (timeout = 0.15 )
363- except subprocess .TimeoutExpired :
364- pass
365- return p
366-
340+ @unittest .skipUnless (sys .platform == "linux" , "Only valid on Linux" )
367341 def test_repl_eio (self ):
342+ # Use the pty-based approach to simulate EIO error
368343 child_code = textwrap .dedent ("""
369- import signal, sys
370- signal.signal(signal.SIGUSR1, lambda *a: None)
344+ import os, sys, pty, fcntl, termios, signal, time, errno
345+
346+ def handler(sig, f):
347+ pass
348+
349+ def create_eio_condition():
350+ try:
351+ # Try to create a condition that will actually produce EIO
352+ # Method: Use your original script's approach with modifications
353+ master_fd, slave_fd = pty.openpty()
354+ # Fork a child that will manipulate the pty
355+ child_pid = os.fork()
356+ if child_pid == 0:
357+ # Child process
358+ try:
359+ # Set up session and control terminal like your script
360+ os.setsid()
361+ fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
362+ # Get process group
363+ p2_pgid = os.getpgrp()
364+ # Fork grandchild
365+ grandchild_pid = os.fork()
366+ if grandchild_pid == 0:
367+ # Grandchild - set up process group
368+ os.setpgid(0, 0)
369+ # Redirect stdin to slave
370+ os.dup2(slave_fd, 0)
371+ if slave_fd > 2:
372+ os.close(slave_fd)
373+ # Fork great-grandchild for terminal control manipulation
374+ ggc_pid = os.fork()
375+ if ggc_pid == 0:
376+ # Great-grandchild - just exit quickly
377+ sys.exit(0)
378+ else:
379+ # Back to grandchild
380+ try:
381+ os.tcsetpgrp(0, p2_pgid)
382+ except:
383+ pass
384+ sys.exit(0)
385+ else:
386+ # Back to child
387+ try:
388+ os.setpgid(grandchild_pid, grandchild_pid)
389+ except ProcessLookupError:
390+ pass
391+ os.tcsetpgrp(slave_fd, grandchild_pid)
392+ if slave_fd > 2:
393+ os.close(slave_fd)
394+ os.waitpid(grandchild_pid, 0)
395+ # Manipulate terminal control to create EIO condition
396+ os.tcsetpgrp(master_fd, p2_pgid)
397+ # Now try to read from master - this might cause EIO
398+ try:
399+ os.read(master_fd, 1)
400+ except OSError as e:
401+ if e.errno == errno.EIO:
402+ print(f"Setup created EIO condition: {e}", file=sys.stderr)
403+ sys.exit(0)
404+ except Exception as setup_e:
405+ print(f"Setup error: {setup_e}", file=sys.stderr)
406+ sys.exit(1)
407+ else:
408+ # Parent process
409+ os.close(slave_fd)
410+ os.waitpid(child_pid, 0)
411+ # Now replace stdin with master_fd and try to read
412+ os.dup2(master_fd, 0)
413+ os.close(master_fd)
414+ # This should now trigger EIO
415+ result = input()
416+ print(f"Unexpectedly got input: {repr(result)}", file=sys.stderr)
417+ sys.exit(0)
418+ except OSError as e:
419+ if e.errno == errno.EIO:
420+ print(f"Got EIO: {e}", file=sys.stderr)
421+ sys.exit(1)
422+ elif e.errno == errno.ENXIO:
423+ print(f"Got ENXIO (no such device): {e}", file=sys.stderr)
424+ sys.exit(1) # Treat ENXIO as success too
425+ else:
426+ print(f"Got other OSError: errno={e.errno} {e}", file=sys.stderr)
427+ sys.exit(2)
428+ except EOFError as e:
429+ print(f"Got EOFError: {e}", file=sys.stderr)
430+ sys.exit(3)
431+ except Exception as e:
432+ print(f"Got unexpected error: {type(e).__name__}: {e}", file=sys.stderr)
433+ sys.exit(4)
434+ # Set up signal handler for coordination
435+ signal.signal(signal.SIGUSR1, lambda *a: create_eio_condition())
371436 print("READY", flush=True)
372437 signal.pause()
373- input()
374438 """ ).strip ()
375439
376440 proc = subprocess .Popen (
@@ -381,22 +445,11 @@ def test_repl_eio(self):
381445 text = True
382446 )
383447
384- ready = False
385- while not ready :
386- line = proc .stdout .readline ().strip ()
387- if line == "READY" :
388- ready = True
389- break
390- if proc .poll () is not None :
391- self .fail ("Child process exited unexpectedly" )
448+ ready_line = proc .stdout .readline ().strip ()
449+ if ready_line != "READY" or proc .poll () is not None :
450+ self .fail ("Child process failed to start properly" )
392451
393- tracer = self ._attach_strace (proc .pid )
394-
395- try :
396- os .kill (proc .pid , signal .SIGUSR1 )
397- _ , err = proc .communicate (timeout = 5 )
398- finally :
399- if tracer and tracer .poll () is None :
400- tracer .terminate ()
401- self .assertNotEqual (proc .returncode , 0 )
402- self .assertTrue ("Errno 5" in err or "Input/output error" in err or "EOFError" in err , err )
452+ os .kill (proc .pid , signal .SIGUSR1 )
453+ _ , err = proc .communicate (timeout = 5 ) # sleep for pty to settle
454+ self .assertEqual (proc .returncode , 1 , f"Expected EIO error, got return code { proc .returncode } " )
455+ self .assertIn ("Got EIO:" , err , f"Expected EIO error message in stderr: { err } " )
0 commit comments