1- """Run tests in isolated subprocesses (the test.support.isolated decorator).
1+ """Run tests in isolated subprocesses (the test.support.isolation. isolated decorator).
22
33A failure, error or skip that happens in the subprocess is replayed in the
44parent process so that the test runner records it. The original (subprocess)
@@ -78,7 +78,7 @@ class _SubprocessTestError(Exception):
7878def _remote (detail ):
7979 # Wrap the subprocess traceback the way concurrent.futures does, so it is
8080 # clearly delimited when shown as the cause.
81- return _RemoteTraceback ('\n """\n %s """' % detail )
81+ return _RemoteTraceback (f '\n """\n { detail } """' )
8282
8383
8484def _check_subprocess_support ():
@@ -92,9 +92,9 @@ def _check_subprocess_support():
9292def _run_in_subprocess (module , qualname ):
9393 """Run module.qualname (a test method or class) in a fresh subprocess.
9494
95- Return ``(outcomes , output, returncode)``, where *outcomes * is the list of
96- test outcomes decoded from the subprocess, or ``None`` if it did not run to
97- completion (crash, import error, ...).
95+ Return ``(payload , output, returncode)``, where *payload * is the decoded
96+ ``{' outcomes': ..., 'durations': ...}`` mapping from the subprocess, or
97+ ``None`` if it did not run to completion (crash, import error, ...).
9898 """
9999 import json
100100 import subprocess
@@ -110,28 +110,31 @@ def _run_in_subprocess(module, qualname):
110110 proc = subprocess .run (cmd , capture_output = True , text = True , env = env )
111111 try :
112112 with open (result_path , encoding = 'utf-8' ) as f :
113- outcomes = json .load (f )
113+ payload = json .load (f )
114114 except (OSError , ValueError ):
115- outcomes = None
115+ payload = None
116116 finally :
117117 try :
118118 os .unlink (result_path )
119119 except OSError :
120120 pass
121- return outcomes , (proc .stdout or '' ) + (proc .stderr or '' ), proc .returncode
121+ return payload , (proc .stdout or '' ) + (proc .stderr or '' ), proc .returncode
122122
123123
124124def _replay_outcome (test , outcome ):
125125 kind = outcome ['kind' ]
126126 detail = outcome ['detail' ]
127127 if kind == 'skipped' :
128128 test .skipTest (detail ) # the detail is the skip reason, not a traceback
129- elif kind == 'failure' :
130- raise test .failureException ('test failed in the subprocess' ) \
131- from _remote (detail )
129+ elif kind in ('failure' , 'expected_failure' ):
130+ # An expected failure is replayed like a failure: the wrapper carries
131+ # the @expectedFailure marker (copied by functools.wraps), so the parent
132+ # records the raised exception as an expectedFailure, not a failure.
133+ exc = test .failureException ('test failed in the subprocess' )
134+ raise exc from _remote (detail )
132135 else : # 'error'
133- raise _SubprocessTestError ('test failed in the subprocess' ) \
134- from _remote (detail )
136+ exc = _SubprocessTestError ('test failed in the subprocess' )
137+ raise exc from _remote (detail )
135138
136139
137140def _replay_outcomes (test , outcomes ):
@@ -153,8 +156,8 @@ def _raise_fixture_outcome(outcome):
153156 # subprocess in a parent-process fixture, so it applies to every test.
154157 if outcome ['kind' ] == 'skipped' :
155158 raise unittest .SkipTest (outcome ['detail' ])
156- raise _SubprocessTestError ('class failed in the subprocess' ) \
157- from _remote (outcome ['detail' ])
159+ exc = _SubprocessTestError ('class failed in the subprocess' )
160+ raise exc from _remote (outcome ['detail' ])
158161
159162
160163def _isolate_method (func ):
@@ -166,21 +169,27 @@ def wrapper(self, /, *args, **kwargs):
166169 _check_subprocess_support ()
167170 cls = type (self )
168171 qualname = f'{ cls .__qualname__ } .{ func .__name__ } '
169- outcomes , output , returncode = _run_in_subprocess (cls .__module__ ,
170- qualname )
171- if outcomes is None :
172- raise _SubprocessTestError (
173- f'test did not complete in a subprocess (exit code '
174- f'{ returncode } )' ) from _remote (output )
175- _replay_outcomes (self , outcomes )
172+ payload , output , returncode = _run_in_subprocess (cls .__module__ ,
173+ qualname )
174+ if payload is None :
175+ exc = _SubprocessTestError (
176+ f'test did not complete in a subprocess (exit code { returncode } )' )
177+ raise exc from _remote (output )
178+ # The parent measures this method's own duration (the real cost of the
179+ # isolated run, subprocess startup included), so nothing to forward here.
180+ _replay_outcomes (self , payload ['outcomes' ])
176181 return wrapper
177182
178183
179184def _isolate_class (cls ):
185+ # Unwrap to the plain functions: the replacements below call them with the
186+ # runtime cls, so a subclass of an isolated class runs the fixtures bound to
187+ # itself (a bound classmethod would freeze the decoration-time class).
180188 orig_setUpClass = cls .setUpClass .__func__
181189 orig_tearDownClass = cls .tearDownClass .__func__
182190 orig_setUp = cls .setUp
183191 orig_tearDown = cls .tearDown
192+ orig_addDuration = getattr (cls , '_addDuration' , None )
184193
185194 def setUpClass (cls ):
186195 if running_isolated :
@@ -189,26 +198,28 @@ def setUpClass(cls):
189198 _check_subprocess_support ()
190199 # Run the whole class in a single subprocess and stash the outcomes
191200 # for the wrapped test methods to replay.
192- outcomes , output , returncode = _run_in_subprocess (cls .__module__ ,
193- cls .__qualname__ )
194- if outcomes is None :
195- raise _SubprocessTestError (
196- f'class did not complete in a subprocess (exit code '
197- f' { returncode } )' ) from _remote (output )
201+ payload , output , returncode = _run_in_subprocess (cls .__module__ ,
202+ cls .__qualname__ )
203+ if payload is None :
204+ exc = _SubprocessTestError (
205+ f'class did not complete in a subprocess (exit code { returncode } )' )
206+ raise exc from _remote (output )
198207 by_id = {}
199- for outcome in outcomes :
208+ for outcome in payload [ ' outcomes' ] :
200209 if outcome ['fixture' ]:
201210 # A setUpClass()/setUpModule() failure or skip: apply it to the
202211 # whole class by raising it here, in the parent's setUpClass().
203212 _raise_fixture_outcome (outcome )
204213 by_id .setdefault (outcome ['id' ], []).append (outcome )
205214 cls ._isolated_outcomes = by_id
215+ cls ._isolated_durations = dict (payload .get ('durations' , ()))
206216
207217 def tearDownClass (cls ):
208218 if running_isolated :
209219 orig_tearDownClass (cls )
210220 else :
211221 cls ._isolated_outcomes = None
222+ cls ._isolated_durations = None
212223
213224 def setUp (self ):
214225 # In the parent the real test does not run, so neither should setUp().
@@ -219,6 +230,15 @@ def tearDown(self):
219230 if running_isolated :
220231 orig_tearDown (self )
221232
233+ def _addDuration (self , result , elapsed ):
234+ # In the parent, report the per-test duration measured in the subprocess
235+ # rather than the replay time (subprocess startup is paid once, in
236+ # setUpClass).
237+ if not running_isolated :
238+ durations = getattr (type (self ), '_isolated_durations' , None ) or {}
239+ elapsed = durations .get (self .id (), elapsed )
240+ orig_addDuration (self , result , elapsed )
241+
222242 def replay (self ):
223243 by_id = getattr (type (self ), '_isolated_outcomes' , None ) or {}
224244 _replay_outcomes (self , by_id .get (self .id (), []))
@@ -227,6 +247,8 @@ def replay(self):
227247 cls .tearDownClass = classmethod (tearDownClass )
228248 cls .setUp = setUp
229249 cls .tearDown = tearDown
250+ if orig_addDuration is not None :
251+ cls ._addDuration = _addDuration
230252 for name in unittest .TestLoader ().getTestCaseNames (cls ):
231253 method = getattr (cls , name )
232254 @functools .wraps (method )
@@ -254,10 +276,11 @@ def isolated():
254276 shown as the cause of a reported failure or error. Use
255277 :data:`running_isolated` in fixtures to choose what to run in the subprocess.
256278
257- The test is skipped on platforms without subprocess support.
279+ The test is skipped on platforms without subprocess support, since it must
280+ spawn one.
258281 """
259282 def decorator (obj ):
260- if isinstance (obj , type ):
283+ if isinstance (obj , type ) and issubclass ( obj , unittest . TestCase ) :
261284 return _isolate_class (obj )
262285 return _isolate_method (obj )
263286 return decorator
0 commit comments