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