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