##// END OF EJS Templates
py3: remove dead code to make file descriptors non-inheritable...
Manuel Jacob -
r50191:b6b6ae9e default
parent child Browse files
Show More
@@ -1,365 +1,340
1 1 #!/usr/bin/env python
2 2 """
3 3 Tests the buffering behavior of stdio streams in `mercurial.utils.procutil`.
4 4 """
5 5
6 6 import contextlib
7 7 import errno
8 8 import os
9 9 import pickle
10 10 import signal
11 11 import subprocess
12 12 import sys
13 13 import tempfile
14 14 import unittest
15 15
16 16 from mercurial import pycompat, util
17 17
18 18
19 if pycompat.ispy3:
20
21 def set_noninheritable(fd):
22 # On Python 3, file descriptors are non-inheritable by default.
23 pass
24
25
26 else:
27 if pycompat.iswindows:
28 # unused
29 set_noninheritable = None
30 else:
31 import fcntl
32
33 def set_noninheritable(fd):
34 old = fcntl.fcntl(fd, fcntl.F_GETFD)
35 fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
36
37
38 19 TEST_BUFFERING_CHILD_SCRIPT = r'''
39 20 import os
40 21
41 22 from mercurial import dispatch
42 23 from mercurial.utils import procutil
43 24
44 25 dispatch.initstdio()
45 26 procutil.{stream}.write(b'aaa')
46 27 os.write(procutil.{stream}.fileno(), b'[written aaa]')
47 28 procutil.{stream}.write(b'bbb\n')
48 29 os.write(procutil.{stream}.fileno(), b'[written bbb\\n]')
49 30 '''
50 31 UNBUFFERED = b'aaa[written aaa]bbb\n[written bbb\\n]'
51 32 LINE_BUFFERED = b'[written aaa]aaabbb\n[written bbb\\n]'
52 33 FULLY_BUFFERED = b'[written aaa][written bbb\\n]aaabbb\n'
53 34
54 35
55 36 TEST_LARGE_WRITE_CHILD_SCRIPT = r'''
56 37 import os
57 38 import signal
58 39 import sys
59 40
60 41 from mercurial import dispatch
61 42 from mercurial.utils import procutil
62 43
63 44 signal.signal(signal.SIGINT, lambda *x: None)
64 45 dispatch.initstdio()
65 46 write_result = procutil.{stream}.write(b'x' * 1048576)
66 47 with os.fdopen(
67 48 os.open({write_result_fn!r}, os.O_WRONLY | getattr(os, 'O_TEMPORARY', 0)),
68 49 'w',
69 50 ) as write_result_f:
70 51 write_result_f.write(str(write_result))
71 52 '''
72 53
73 54
74 55 TEST_BROKEN_PIPE_CHILD_SCRIPT = r'''
75 56 import os
76 57 import pickle
77 58
78 59 from mercurial import dispatch
79 60 from mercurial.utils import procutil
80 61
81 62 dispatch.initstdio()
82 63 procutil.stdin.read(1) # wait until parent process closed pipe
83 64 try:
84 65 procutil.{stream}.write(b'test')
85 66 procutil.{stream}.flush()
86 67 except EnvironmentError as e:
87 68 with os.fdopen(
88 69 os.open(
89 70 {err_fn!r},
90 71 os.O_WRONLY
91 72 | getattr(os, 'O_BINARY', 0)
92 73 | getattr(os, 'O_TEMPORARY', 0),
93 74 ),
94 75 'wb',
95 76 ) as err_f:
96 77 pickle.dump(e, err_f)
97 78 # Exit early to suppress further broken pipe errors at interpreter shutdown.
98 79 os._exit(0)
99 80 '''
100 81
101 82
102 83 @contextlib.contextmanager
103 84 def _closing(fds):
104 85 try:
105 86 yield
106 87 finally:
107 88 for fd in fds:
108 89 try:
109 90 os.close(fd)
110 91 except EnvironmentError:
111 92 pass
112 93
113 94
114 95 # In the following, we set the FDs non-inheritable mainly to make it possible
115 96 # for tests to close the receiving end of the pipe / PTYs.
116 97
117 98
118 99 @contextlib.contextmanager
119 100 def _devnull():
120 101 devnull = os.open(os.devnull, os.O_WRONLY)
121 102 # We don't have a receiving end, so it's not worth the effort on Python 2
122 103 # on Windows to make the FD non-inheritable.
123 104 with _closing([devnull]):
124 105 yield (None, devnull)
125 106
126 107
127 108 @contextlib.contextmanager
128 109 def _pipes():
129 110 rwpair = os.pipe()
130 # Pipes are already non-inheritable on Windows.
131 if not pycompat.iswindows:
132 set_noninheritable(rwpair[0])
133 set_noninheritable(rwpair[1])
134 111 with _closing(rwpair):
135 112 yield rwpair
136 113
137 114
138 115 @contextlib.contextmanager
139 116 def _ptys():
140 117 if pycompat.iswindows:
141 118 raise unittest.SkipTest("PTYs are not supported on Windows")
142 119 import pty
143 120 import tty
144 121
145 122 rwpair = pty.openpty()
146 set_noninheritable(rwpair[0])
147 set_noninheritable(rwpair[1])
148 123 with _closing(rwpair):
149 124 tty.setraw(rwpair[0])
150 125 yield rwpair
151 126
152 127
153 128 def _readall(fd, buffer_size, initial_buf=None):
154 129 buf = initial_buf or []
155 130 while True:
156 131 try:
157 132 s = os.read(fd, buffer_size)
158 133 except OSError as e:
159 134 if e.errno == errno.EIO:
160 135 # If the child-facing PTY got closed, reading from the
161 136 # parent-facing PTY raises EIO.
162 137 break
163 138 raise
164 139 if not s:
165 140 break
166 141 buf.append(s)
167 142 return b''.join(buf)
168 143
169 144
170 145 class TestStdio(unittest.TestCase):
171 146 def _test(
172 147 self,
173 148 child_script,
174 149 stream,
175 150 rwpair_generator,
176 151 check_output,
177 152 python_args=[],
178 153 post_child_check=None,
179 154 stdin_generator=None,
180 155 ):
181 156 assert stream in ('stdout', 'stderr')
182 157 if stdin_generator is None:
183 158 stdin_generator = open(os.devnull, 'rb')
184 159 with rwpair_generator() as (
185 160 stream_receiver,
186 161 child_stream,
187 162 ), stdin_generator as child_stdin:
188 163 proc = subprocess.Popen(
189 164 [sys.executable] + python_args + ['-c', child_script],
190 165 stdin=child_stdin,
191 166 stdout=child_stream if stream == 'stdout' else None,
192 167 stderr=child_stream if stream == 'stderr' else None,
193 168 )
194 169 try:
195 170 os.close(child_stream)
196 171 if stream_receiver is not None:
197 172 check_output(stream_receiver, proc)
198 173 except: # re-raises
199 174 proc.terminate()
200 175 raise
201 176 finally:
202 177 retcode = proc.wait()
203 178 self.assertEqual(retcode, 0)
204 179 if post_child_check is not None:
205 180 post_child_check()
206 181
207 182 def _test_buffering(
208 183 self, stream, rwpair_generator, expected_output, python_args=[]
209 184 ):
210 185 def check_output(stream_receiver, proc):
211 186 self.assertEqual(_readall(stream_receiver, 1024), expected_output)
212 187
213 188 self._test(
214 189 TEST_BUFFERING_CHILD_SCRIPT.format(stream=stream),
215 190 stream,
216 191 rwpair_generator,
217 192 check_output,
218 193 python_args,
219 194 )
220 195
221 196 def test_buffering_stdout_devnull(self):
222 197 self._test_buffering('stdout', _devnull, None)
223 198
224 199 def test_buffering_stdout_pipes(self):
225 200 self._test_buffering('stdout', _pipes, FULLY_BUFFERED)
226 201
227 202 def test_buffering_stdout_ptys(self):
228 203 self._test_buffering('stdout', _ptys, LINE_BUFFERED)
229 204
230 205 def test_buffering_stdout_devnull_unbuffered(self):
231 206 self._test_buffering('stdout', _devnull, None, python_args=['-u'])
232 207
233 208 def test_buffering_stdout_pipes_unbuffered(self):
234 209 self._test_buffering('stdout', _pipes, UNBUFFERED, python_args=['-u'])
235 210
236 211 def test_buffering_stdout_ptys_unbuffered(self):
237 212 self._test_buffering('stdout', _ptys, UNBUFFERED, python_args=['-u'])
238 213
239 214 if not pycompat.ispy3 and not pycompat.iswindows:
240 215 # On Python 2 on non-Windows, we manually open stdout in line-buffered
241 216 # mode if connected to a TTY. We should check if Python was configured
242 217 # to use unbuffered stdout, but it's hard to do that.
243 218 test_buffering_stdout_ptys_unbuffered = unittest.expectedFailure(
244 219 test_buffering_stdout_ptys_unbuffered
245 220 )
246 221
247 222 def _test_large_write(self, stream, rwpair_generator, python_args=[]):
248 223 if not pycompat.ispy3 and pycompat.isdarwin:
249 224 # Python 2 doesn't always retry on EINTR, but the libc might retry.
250 225 # So far, it was observed only on macOS that EINTR is raised at the
251 226 # Python level. As Python 2 support will be dropped soon-ish, we
252 227 # won't attempt to fix it.
253 228 raise unittest.SkipTest("raises EINTR on macOS")
254 229
255 230 def check_output(stream_receiver, proc):
256 231 if not pycompat.iswindows:
257 232 # On Unix, we can provoke a partial write() by interrupting it
258 233 # by a signal handler as soon as a bit of data was written.
259 234 # We test that write() is called until all data is written.
260 235 buf = [os.read(stream_receiver, 1)]
261 236 proc.send_signal(signal.SIGINT)
262 237 else:
263 238 # On Windows, there doesn't seem to be a way to cause partial
264 239 # writes.
265 240 buf = []
266 241 self.assertEqual(
267 242 _readall(stream_receiver, 131072, buf), b'x' * 1048576
268 243 )
269 244
270 245 def post_child_check():
271 246 write_result_str = write_result_f.read()
272 247 if pycompat.ispy3:
273 248 # On Python 3, we test that the correct number of bytes is
274 249 # claimed to have been written.
275 250 expected_write_result_str = '1048576'
276 251 else:
277 252 # On Python 2, we only check that the large write does not
278 253 # crash.
279 254 expected_write_result_str = 'None'
280 255 self.assertEqual(write_result_str, expected_write_result_str)
281 256
282 257 with tempfile.NamedTemporaryFile('r') as write_result_f:
283 258 self._test(
284 259 TEST_LARGE_WRITE_CHILD_SCRIPT.format(
285 260 stream=stream, write_result_fn=write_result_f.name
286 261 ),
287 262 stream,
288 263 rwpair_generator,
289 264 check_output,
290 265 python_args,
291 266 post_child_check=post_child_check,
292 267 )
293 268
294 269 def test_large_write_stdout_devnull(self):
295 270 self._test_large_write('stdout', _devnull)
296 271
297 272 def test_large_write_stdout_pipes(self):
298 273 self._test_large_write('stdout', _pipes)
299 274
300 275 def test_large_write_stdout_ptys(self):
301 276 self._test_large_write('stdout', _ptys)
302 277
303 278 def test_large_write_stdout_devnull_unbuffered(self):
304 279 self._test_large_write('stdout', _devnull, python_args=['-u'])
305 280
306 281 def test_large_write_stdout_pipes_unbuffered(self):
307 282 self._test_large_write('stdout', _pipes, python_args=['-u'])
308 283
309 284 def test_large_write_stdout_ptys_unbuffered(self):
310 285 self._test_large_write('stdout', _ptys, python_args=['-u'])
311 286
312 287 def test_large_write_stderr_devnull(self):
313 288 self._test_large_write('stderr', _devnull)
314 289
315 290 def test_large_write_stderr_pipes(self):
316 291 self._test_large_write('stderr', _pipes)
317 292
318 293 def test_large_write_stderr_ptys(self):
319 294 self._test_large_write('stderr', _ptys)
320 295
321 296 def test_large_write_stderr_devnull_unbuffered(self):
322 297 self._test_large_write('stderr', _devnull, python_args=['-u'])
323 298
324 299 def test_large_write_stderr_pipes_unbuffered(self):
325 300 self._test_large_write('stderr', _pipes, python_args=['-u'])
326 301
327 302 def test_large_write_stderr_ptys_unbuffered(self):
328 303 self._test_large_write('stderr', _ptys, python_args=['-u'])
329 304
330 305 def _test_broken_pipe(self, stream):
331 306 assert stream in ('stdout', 'stderr')
332 307
333 308 def check_output(stream_receiver, proc):
334 309 os.close(stream_receiver)
335 310 proc.stdin.write(b'x')
336 311 proc.stdin.close()
337 312
338 313 def post_child_check():
339 314 err = pickle.load(err_f)
340 315 self.assertEqual(err.errno, errno.EPIPE)
341 316 self.assertEqual(err.strerror, "Broken pipe")
342 317
343 318 with tempfile.NamedTemporaryFile('rb') as err_f:
344 319 self._test(
345 320 TEST_BROKEN_PIPE_CHILD_SCRIPT.format(
346 321 stream=stream, err_fn=err_f.name
347 322 ),
348 323 stream,
349 324 _pipes,
350 325 check_output,
351 326 post_child_check=post_child_check,
352 327 stdin_generator=util.nullcontextmanager(subprocess.PIPE),
353 328 )
354 329
355 330 def test_broken_pipe_stdout(self):
356 331 self._test_broken_pipe('stdout')
357 332
358 333 def test_broken_pipe_stderr(self):
359 334 self._test_broken_pipe('stderr')
360 335
361 336
362 337 if __name__ == '__main__':
363 338 import silenttestrunner
364 339
365 340 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now