@@ -108,12 +108,75 @@ def _child_environ(env):
108108 return environ
109109
110110
111- def _run_in_subprocess ( module , qualname , options , env , timeout ) :
112- """Run module.qualname (a test method or class) in a fresh subprocess.
111+ class _SubprocessTest :
112+ """A test running in a subprocess, started by _start_test() .
113113
114- Return ``(payload, output, returncode)``, where *payload* is the decoded
115- ``{'outcomes': ..., 'durations': ...}`` mapping from the subprocess, or
116- ``None`` if it did not run to completion (crash, import error, ...).
114+ The parent can watch the subprocess (its pid) while the test runs, and
115+ must wait() for it.
116+ """
117+
118+ def __init__ (self , proc , result_path ):
119+ self ._proc = proc
120+ self ._result_path = result_path
121+
122+ @property
123+ def pid (self ):
124+ return self ._proc .pid
125+
126+ def wait (self , timeout = None , tick = None , interval = 1.0 ):
127+ """Wait for the test to finish, calling *tick* every *interval* seconds.
128+
129+ Return ``(payload, output, returncode)``, where *payload* is the
130+ decoded ``{'outcomes': ..., 'durations': ...}`` mapping from the
131+ subprocess, or ``None`` if it did not run to completion (crash,
132+ import error, ...).
133+ """
134+ import marshal
135+ import subprocess
136+ import time
137+ deadline = None if timeout is None else time .monotonic () + timeout
138+ try :
139+ while True :
140+ step = None if deadline is None else max (
141+ 0.0 , deadline - time .monotonic ())
142+ # Wake up for the next tick, unless the timeout comes first.
143+ ticking = tick is not None and (step is None or step > interval )
144+ try :
145+ # communicate(), not wait(): a test writing more than a
146+ # pipe buffer would block. Retrying keeps what it read.
147+ stdout , stderr = self ._proc .communicate (
148+ timeout = interval if ticking else step )
149+ break
150+ except subprocess .TimeoutExpired :
151+ if ticking :
152+ tick ()
153+ continue
154+ # Report the hang rather than leaving the runner stuck.
155+ self ._proc .kill ()
156+ stdout , stderr = self ._proc .communicate ()
157+ raise _SubprocessTestError (
158+ f'test did not complete in a subprocess '
159+ f'within { timeout } seconds'
160+ ) from _remote (_decode (stdout ) + _decode (stderr ))
161+ try :
162+ with open (self ._result_path , 'rb' ) as f :
163+ payload = marshal .load (f )
164+ except (OSError , EOFError , ValueError ):
165+ payload = None
166+ output = _decode (stdout ) + _decode (stderr )
167+ return payload , output , self ._proc .returncode
168+ finally :
169+ try :
170+ os .unlink (self ._result_path )
171+ except OSError :
172+ pass
173+
174+
175+ def _start_test (module , qualname , options = (), env = None ):
176+ """Start module.qualname (a test method or class) in a fresh subprocess.
177+
178+ Return a _SubprocessTest. Its wait() is what removes the temporary file
179+ the subprocess writes its result to.
117180 """
118181 import marshal
119182 import subprocess
@@ -129,26 +192,16 @@ def _run_in_subprocess(module, qualname, options, env, timeout):
129192 cmd = [sys .executable , * options , '-m' , 'test.support.subprocess_runner' ,
130193 module , qualname , result_path ,
131194 marshal .dumps (_child_config ()).hex ()]
132- try :
133- proc = subprocess .run (cmd , capture_output = True ,
134- env = _child_environ (env ), timeout = timeout )
135- except subprocess .TimeoutExpired as exc :
136- # Report the hang rather than leaving the test runner stuck.
137- output = _decode (exc .stdout ) + _decode (exc .stderr )
138- raise _SubprocessTestError (
139- f'test did not complete in a subprocess '
140- f'within { timeout } seconds' ) from _remote (output )
141- try :
142- with open (result_path , 'rb' ) as f :
143- payload = marshal .load (f )
144- except (OSError , EOFError , ValueError ):
145- payload = None
146- finally :
195+ proc = subprocess .Popen (cmd , stdout = subprocess .PIPE ,
196+ stderr = subprocess .PIPE , env = _child_environ (env ))
197+ except BaseException :
147198 try :
148199 os .unlink (result_path )
149200 except OSError :
150201 pass
151- return payload , _decode (proc .stdout ) + _decode (proc .stderr ), proc .returncode
202+ raise
203+ return _SubprocessTest (proc , result_path )
204+
152205
153206
154207def _replay_outcome (test , outcome ):
@@ -200,6 +253,19 @@ def _check_returncode(returncode, output, what):
200253 raise exc from _remote (output )
201254
202255
256+ def _replay_test (test , payload , output , returncode ):
257+ """Reproduce in *test* the result that _SubprocessTest.wait() returned."""
258+ if payload is None :
259+ exc = _SubprocessTestError (
260+ f'test did not complete in a subprocess (exit code { returncode } )' )
261+ raise exc from _remote (output )
262+ # The parent measures the test method's own duration (the real cost of the
263+ # isolated run, subprocess startup included), so nothing to forward here.
264+ # Replay the outcomes first: a failure of the test itself is more useful.
265+ _replay_outcomes (test , payload ['outcomes' ])
266+ _check_returncode (returncode , output , 'test' )
267+
268+
203269def _isolate_method (func , options , env , timeout ):
204270 @functools .wraps (func )
205271 def wrapper (self , / , * args , ** kwargs ):
@@ -209,18 +275,8 @@ def wrapper(self, /, *args, **kwargs):
209275 _check_subprocess_support ()
210276 cls = type (self )
211277 qualname = f'{ cls .__qualname__ } .{ func .__name__ } '
212- payload , output , returncode = _run_in_subprocess (cls .__module__ ,
213- qualname , options ,
214- env , timeout )
215- if payload is None :
216- exc = _SubprocessTestError (
217- f'test did not complete in a subprocess (exit code { returncode } )' )
218- raise exc from _remote (output )
219- # The parent measures this method's own duration (the real cost of the
220- # isolated run, subprocess startup included), so nothing to forward here.
221- # Replay the outcomes first: a failure of the test itself is more useful.
222- _replay_outcomes (self , payload ['outcomes' ])
223- _check_returncode (returncode , output , 'test' )
278+ proc = _start_test (cls .__module__ , qualname , options , env )
279+ _replay_test (self , * proc .wait (timeout ))
224280 return wrapper
225281
226282
@@ -244,9 +300,8 @@ def setUpClass(cls):
244300 _check_subprocess_support ()
245301 # Run the whole class in a single subprocess and stash the outcomes
246302 # for the test methods to replay.
247- payload , output , returncode = _run_in_subprocess (cls .__module__ ,
248- cls .__qualname__ ,
249- options , env , timeout )
303+ proc = _start_test (cls .__module__ , cls .__qualname__ , options , env )
304+ payload , output , returncode = proc .wait (timeout )
250305 if payload is None :
251306 exc = _SubprocessTestError (
252307 f'class did not complete in a subprocess (exit code { returncode } )' )
0 commit comments