Skip to content

Commit 41fb6c5

Browse files
committed
Add Lib/test/test_file_eintr.py from 3.13.7
1 parent e00a95d commit 41fb6c5

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

Lib/test/test_file_eintr.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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+
11+
import os
12+
import select
13+
import signal
14+
import subprocess
15+
import sys
16+
import time
17+
import unittest
18+
from test import support
19+
20+
if 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.
24+
import _io
25+
import _pyio
26+
27+
@unittest.skipUnless(os.name == 'posix', 'tests requires a posix system.')
28+
class 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+
@unittest.expectedFailure # TODO: RUSTPYTHON
156+
def test_readline(self):
157+
"""readline() must handle signals and not lose data."""
158+
self._test_reading(
159+
data_to_write=b'hello, world!',
160+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
161+
read_method_name='readline',
162+
expected=b'hello, world!\n'))
163+
164+
@unittest.expectedFailure # TODO: RUSTPYTHON
165+
def test_readlines(self):
166+
"""readlines() must handle signals and not lose data."""
167+
self._test_reading(
168+
data_to_write=b'hello\nworld!',
169+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
170+
read_method_name='readlines',
171+
expected=[b'hello\n', b'world!\n']))
172+
173+
@unittest.expectedFailure # TODO: RUSTPYTHON
174+
def test_readall(self):
175+
"""readall() must handle signals and not lose data."""
176+
self._test_reading(
177+
data_to_write=b'hello\nworld!',
178+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
179+
read_method_name='readall',
180+
expected=b'hello\nworld!\n'))
181+
# read() is the same thing as readall().
182+
self._test_reading(
183+
data_to_write=b'hello\nworld!',
184+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
185+
read_method_name='read',
186+
expected=b'hello\nworld!\n'))
187+
188+
189+
class CTestFileIOSignalInterrupt(TestFileIOSignalInterrupt, unittest.TestCase):
190+
modname = '_io'
191+
192+
class PyTestFileIOSignalInterrupt(TestFileIOSignalInterrupt, unittest.TestCase):
193+
modname = '_pyio'
194+
195+
196+
class TestBufferedIOSignalInterrupt(TestFileIOSignalInterrupt):
197+
def _generate_infile_setup_code(self):
198+
"""Returns the infile = ... line of code to make a BufferedReader."""
199+
return ('import %s as io ;infile = io.open(sys.stdin.fileno(), "rb") ;'
200+
'assert isinstance(infile, io.BufferedReader)' %
201+
self.modname)
202+
203+
@unittest.expectedFailure # TODO: RUSTPYTHON
204+
def test_readall(self):
205+
"""BufferedReader.read() must handle signals and not lose data."""
206+
self._test_reading(
207+
data_to_write=b'hello\nworld!',
208+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
209+
read_method_name='read',
210+
expected=b'hello\nworld!\n'))
211+
212+
class CTestBufferedIOSignalInterrupt(TestBufferedIOSignalInterrupt, unittest.TestCase):
213+
modname = '_io'
214+
215+
class PyTestBufferedIOSignalInterrupt(TestBufferedIOSignalInterrupt, unittest.TestCase):
216+
modname = '_pyio'
217+
218+
219+
class TestTextIOSignalInterrupt(TestFileIOSignalInterrupt):
220+
def _generate_infile_setup_code(self):
221+
"""Returns the infile = ... line of code to make a TextIOWrapper."""
222+
return ('import %s as io ;'
223+
'infile = io.open(sys.stdin.fileno(), encoding="utf-8", newline=None) ;'
224+
'assert isinstance(infile, io.TextIOWrapper)' %
225+
self.modname)
226+
227+
@unittest.expectedFailure # TODO: RUSTPYTHON
228+
def test_readline(self):
229+
"""readline() must handle signals and not lose data."""
230+
self._test_reading(
231+
data_to_write=b'hello, world!',
232+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
233+
read_method_name='readline',
234+
expected='hello, world!\n'))
235+
236+
@unittest.expectedFailure # TODO: RUSTPYTHON
237+
def test_readlines(self):
238+
"""readlines() must handle signals and not lose data."""
239+
self._test_reading(
240+
data_to_write=b'hello\r\nworld!',
241+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
242+
read_method_name='readlines',
243+
expected=['hello\n', 'world!\n']))
244+
245+
@unittest.expectedFailure # TODO: RUSTPYTHON
246+
def test_readall(self):
247+
"""read() must handle signals and not lose data."""
248+
self._test_reading(
249+
data_to_write=b'hello\nworld!',
250+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
251+
read_method_name='read',
252+
expected="hello\nworld!\n"))
253+
254+
class CTestTextIOSignalInterrupt(TestTextIOSignalInterrupt, unittest.TestCase):
255+
modname = '_io'
256+
257+
class PyTestTextIOSignalInterrupt(TestTextIOSignalInterrupt, unittest.TestCase):
258+
modname = '_pyio'
259+
260+
261+
if __name__ == '__main__':
262+
unittest.main()

0 commit comments

Comments
 (0)