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