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()
|
---|