@@ -121,19 +121,25 @@ def pytest_exception_interact(node, call, report):
121
121
# If a fixture crashes, whether during setup, teardown, or in a background
122
122
# task at any other point, then we mark the whole test run as "crashed". When
123
123
# a run is "crashed", two things happen: (1) if any fixtures or the test
124
- # itself haven't started yet, then we don't start them. (2) if the test is
125
- # running, we cancel it. That's all. In particular, if a fixture has a
126
- # background crash, we don't propagate that to any other fixtures, we still
127
- # follow the normal teardown sequence, and so on – but since the test is
128
- # cancelled, the teardown sequence should start immediately.
124
+ # itself haven't started yet, then we don't start them, and treat them as if
125
+ # they've already exited. (2) if the test is running, we cancel it. That's
126
+ # all. In particular, if a fixture has a background crash, we don't propagate
127
+ # that to any other fixtures, we still follow the normal teardown sequence,
128
+ # and so on – but since the test is cancelled, the teardown sequence should
129
+ # start immediately.
129
130
130
131
canary = contextvars .ContextVar ("pytest-trio canary" )
131
132
132
133
133
134
class TrioTestContext :
134
135
def __init__ (self ):
135
136
self .crashed = False
136
- self .test_cancel_scope = None
137
+ # This holds cancel scopes for whatever setup steps are currently
138
+ # running -- initially it's the fixtures that are in the middle of
139
+ # evaluating themselves, and then once fixtures are set up it's the
140
+ # test itself. Basically, at any given moment, it's the stuff we need
141
+ # to cancel if we want to start tearing down our fixture DAG.
142
+ self .active_cancel_scopes = set ()
137
143
self .fixtures_with_errors = set ()
138
144
self .fixtures_with_cancel = set ()
139
145
self .error_list = []
@@ -145,8 +151,8 @@ def crash(self, fixture, exc):
145
151
self .error_list .append (exc )
146
152
self .fixtures_with_errors .add (fixture )
147
153
self .crashed = True
148
- if self . test_cancel_scope is not None :
149
- self . test_cancel_scope .cancel ()
154
+ for cscope in self . active_cancel_scopes :
155
+ cscope .cancel ()
150
156
151
157
152
158
class TrioFixture :
@@ -240,16 +246,17 @@ async def run(self, test_ctx, contextvars_ctx):
240
246
return
241
247
242
248
# Run actual fixture setup step
249
+ # If another fixture crashes while we're in the middle of setting
250
+ # up, we want to be cancelled immediately, so we'll save an
251
+ # encompassing cancel scope where self._crash can find it.
252
+ test_ctx .active_cancel_scopes .add (nursery_fixture .cancel_scope )
243
253
if self ._is_test :
244
- # Tests are exactly like fixtures, except that they (1) have
245
- # to be regular async functions, (2) if there's a crash, we
246
- # should cancel them.
254
+ # Tests are exactly like fixtures, except that they to be
255
+ # regular async functions.
247
256
assert not self .user_done_events
248
257
func_value = None
249
- with trio .CancelScope () as cancel_scope :
250
- test_ctx .test_cancel_scope = cancel_scope
251
- assert not test_ctx .crashed
252
- await self ._func (** resolved_kwargs )
258
+ assert not test_ctx .crashed
259
+ await self ._func (** resolved_kwargs )
253
260
else :
254
261
func_value = self ._func (** resolved_kwargs )
255
262
if isinstance (func_value , Coroutine ):
@@ -261,8 +268,17 @@ async def run(self, test_ctx, contextvars_ctx):
261
268
else :
262
269
# Regular synchronous function
263
270
self .fixture_value = func_value
271
+ # Now that we're done setting up, we don't want crashes to cancel
272
+ # us immediately; instead we want them to cancel our downstream
273
+ # dependents, and then eventually let us clean up normally. So
274
+ # remove this from the set of cancel scopes affected by self._crash.
275
+ test_ctx .active_cancel_scopes .remove (nursery_fixture .cancel_scope )
264
276
265
- # Notify our users that self.fixture_value is ready
277
+
278
+ # self.fixture_value is ready, so notify users that they can
279
+ # continue. (Or, maybe we crashed and were cancelled, in which
280
+ # case our users will check test_ctx.crashed and immediately exit,
281
+ # which is fine too.)
266
282
self .setup_done .set ()
267
283
268
284
# Wait for users to be finished
0 commit comments