Home | History | Annotate | Download | only in test
      1 # Written to test interrupted system calls interfering with our many buffered
      2 # IO implementations.  http://bugs.python.org/issue12268
      3 #
      4 # This tests the '_io' module.  Similar tests for Python 2.x's older
      5 # default file I/O implementation exist within test_file2k.py.
      6 #
      7 # It was suggested that this code could be merged into test_io and the tests
      8 # made to work using the same method as the existing signal tests in test_io.
      9 # I was unable to get single process tests using alarm or setitimer that way
     10 # to reproduce the EINTR problems.  This process based test suite reproduces
     11 # the problems prior to the issue12268 patch reliably on Linux and OSX.
     12 #  - gregory.p.smith
     13 
     14 import os
     15 import select
     16 import signal
     17 import subprocess
     18 import sys
     19 from test.test_support import run_unittest
     20 import time
     21 import unittest
     22 
     23 # Test import all of the things we're about to try testing up front.
     24 from _io import FileIO
     25 
     26 
     27 @unittest.skipUnless(os.name == 'posix', 'tests requires a posix system.')
     28 class TestFileIOSignalInterrupt(unittest.TestCase):
     29     def setUp(self):
     30         self._process = None
     31 
     32     def tearDown(self):
     33         if self._process and self._process.poll() is None:
     34             try:
     35                 self._process.kill()
     36             except OSError:
     37                 pass
     38 
     39     def _generate_infile_setup_code(self):
     40         """Returns the infile = ... line of code for the reader process.
     41 
     42         subclasseses should override this to test different IO objects.
     43         """
     44         return ('import _io ;'
     45                 'infile = _io.FileIO(sys.stdin.fileno(), "rb")')
     46 
     47     def fail_with_process_info(self, why, stdout=b'', stderr=b'',
     48                                communicate=True):
     49         """A common way to cleanup and fail with useful debug output.
     50 
     51         Kills the process if it is still running, collects remaining output
     52         and fails the test with an error message including the output.
     53 
     54         Args:
     55             why: Text to go after "Error from IO process" in the message.
     56             stdout, stderr: standard output and error from the process so
     57                 far to include in the error message.
     58             communicate: bool, when True we call communicate() on the process
     59                 after killing it to gather additional output.
     60         """
     61         if self._process.poll() is None:
     62             time.sleep(0.1)  # give it time to finish printing the error.
     63             try:
     64                 self._process.terminate()  # Ensure it dies.
     65             except OSError:
     66                 pass
     67         if communicate:
     68             stdout_end, stderr_end = self._process.communicate()
     69             stdout += stdout_end
     70             stderr += stderr_end
     71         self.fail('Error from IO process %s:\nSTDOUT:\n%sSTDERR:\n%s\n' %
     72                   (why, stdout.decode(), stderr.decode()))
     73 
     74     def _test_reading(self, data_to_write, read_and_verify_code):
     75         """Generic buffered read method test harness to validate EINTR behavior.
     76 
     77         Also validates that Python signal handlers are run during the read.
     78 
     79         Args:
     80             data_to_write: String to write to the child process for reading
     81                 before sending it a signal, confirming the signal was handled,
     82                 writing a final newline and closing the infile pipe.
     83             read_and_verify_code: Single "line" of code to read from a file
     84                 object named 'infile' and validate the result.  This will be
     85                 executed as part of a python subprocess fed data_to_write.
     86         """
     87         infile_setup_code = self._generate_infile_setup_code()
     88         # Total pipe IO in this function is smaller than the minimum posix OS
     89         # pipe buffer size of 512 bytes.  No writer should block.
     90         assert len(data_to_write) < 512, 'data_to_write must fit in pipe buf.'
     91 
     92         # Start a subprocess to call our read method while handling a signal.
     93         self._process = subprocess.Popen(
     94                 [sys.executable, '-u', '-c',
     95                  'import io, signal, sys ;'
     96                  'signal.signal(signal.SIGINT, '
     97                                'lambda s, f: sys.stderr.write("$\\n")) ;'
     98                  + infile_setup_code + ' ;' +
     99                  'sys.stderr.write("Worm Sign!\\n") ;'
    100                  + read_and_verify_code + ' ;' +
    101                  'infile.close()'
    102                 ],
    103                 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    104                 stderr=subprocess.PIPE)
    105 
    106         # Wait for the signal handler to be installed.
    107         worm_sign = self._process.stderr.read(len(b'Worm Sign!\n'))
    108         if worm_sign != b'Worm Sign!\n':  # See also, Dune by Frank Herbert.
    109             self.fail_with_process_info('while awaiting a sign',
    110                                         stderr=worm_sign)
    111         self._process.stdin.write(data_to_write)
    112 
    113         signals_sent = 0
    114         rlist = []
    115         # We don't know when the read_and_verify_code in our child is actually
    116         # executing within the read system call we want to interrupt.  This
    117         # loop waits for a bit before sending the first signal to increase
    118         # the likelihood of that.  Implementations without correct EINTR
    119         # and signal handling usually fail this test.
    120         while not rlist:
    121             rlist, _, _ = select.select([self._process.stderr], (), (), 0.05)
    122             self._process.send_signal(signal.SIGINT)
    123             signals_sent += 1
    124             if signals_sent > 200:
    125                 self._process.kill()
    126                 self.fail('reader process failed to handle our signals.')
    127         # This assumes anything unexpected that writes to stderr will also
    128         # write a newline.  That is true of the traceback printing code.
    129         signal_line = self._process.stderr.readline()
    130         if signal_line != b'$\n':
    131             self.fail_with_process_info('while awaiting signal',
    132                                         stderr=signal_line)
    133 
    134         # We append a newline to our input so that a readline call can
    135         # end on its own before the EOF is seen and so that we're testing
    136         # the read call that was interrupted by a signal before the end of
    137         # the data stream has been reached.
    138         stdout, stderr = self._process.communicate(input=b'\n')
    139         if self._process.returncode:
    140             self.fail_with_process_info(
    141                     'exited rc=%d' % self._process.returncode,
    142                     stdout, stderr, communicate=False)
    143         # PASS!
    144 
    145     # String format for the read_and_verify_code used by read methods.
    146     _READING_CODE_TEMPLATE = (
    147             'got = infile.{read_method_name}() ;'
    148             'expected = {expected!r} ;'
    149             'assert got == expected, ('
    150                     '"{read_method_name} returned wrong data.\\n"'
    151                     '"got data %r\\nexpected %r" % (got, expected))'
    152             )
    153 
    154     def test_readline(self):
    155         """readline() must handle signals and not lose data."""
    156         self._test_reading(
    157                 data_to_write=b'hello, world!',
    158                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    159                         read_method_name='readline',
    160                         expected=b'hello, world!\n'))
    161 
    162     def test_readlines(self):
    163         """readlines() must handle signals and not lose data."""
    164         self._test_reading(
    165                 data_to_write=b'hello\nworld!',
    166                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    167                         read_method_name='readlines',
    168                         expected=[b'hello\n', b'world!\n']))
    169 
    170     def test_readall(self):
    171         """readall() must handle signals and not lose data."""
    172         self._test_reading(
    173                 data_to_write=b'hello\nworld!',
    174                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    175                         read_method_name='readall',
    176                         expected=b'hello\nworld!\n'))
    177         # read() is the same thing as readall().
    178         self._test_reading(
    179                 data_to_write=b'hello\nworld!',
    180                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    181                         read_method_name='read',
    182                         expected=b'hello\nworld!\n'))
    183 
    184 
    185 class TestBufferedIOSignalInterrupt(TestFileIOSignalInterrupt):
    186     def _generate_infile_setup_code(self):
    187         """Returns the infile = ... line of code to make a BufferedReader."""
    188         return ('infile = io.open(sys.stdin.fileno(), "rb") ;'
    189                 'import _io ;assert isinstance(infile, _io.BufferedReader)')
    190 
    191     def test_readall(self):
    192         """BufferedReader.read() must handle signals and not lose data."""
    193         self._test_reading(
    194                 data_to_write=b'hello\nworld!',
    195                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    196                         read_method_name='read',
    197                         expected=b'hello\nworld!\n'))
    198 
    199 
    200 class TestTextIOSignalInterrupt(TestFileIOSignalInterrupt):
    201     def _generate_infile_setup_code(self):
    202         """Returns the infile = ... line of code to make a TextIOWrapper."""
    203         return ('infile = io.open(sys.stdin.fileno(), "rt", newline=None) ;'
    204                 'import _io ;assert isinstance(infile, _io.TextIOWrapper)')
    205 
    206     def test_readline(self):
    207         """readline() must handle signals and not lose data."""
    208         self._test_reading(
    209                 data_to_write=b'hello, world!',
    210                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    211                         read_method_name='readline',
    212                         expected='hello, world!\n'))
    213 
    214     def test_readlines(self):
    215         """readlines() must handle signals and not lose data."""
    216         self._test_reading(
    217                 data_to_write=b'hello\r\nworld!',
    218                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    219                         read_method_name='readlines',
    220                         expected=['hello\n', 'world!\n']))
    221 
    222     def test_readall(self):
    223         """read() must handle signals and not lose data."""
    224         self._test_reading(
    225                 data_to_write=b'hello\nworld!',
    226                 read_and_verify_code=self._READING_CODE_TEMPLATE.format(
    227                         read_method_name='read',
    228                         expected="hello\nworld!\n"))
    229 
    230 
    231 def test_main():
    232     test_cases = [
    233             tc for tc in globals().values()
    234             if isinstance(tc, type) and issubclass(tc, unittest.TestCase)]
    235     run_unittest(*test_cases)
    236 
    237 
    238 if __name__ == '__main__':
    239     test_main()
    240