@@ -121,19 +121,25 @@ def pytest_exception_interact(node, call, report):
121121# If a fixture crashes, whether during setup, teardown, or in a background
122122# task at any other point, then we mark the whole test run as "crashed". When
123123# 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.
129130
130131canary = contextvars .ContextVar ("pytest-trio canary" )
131132
132133
133134class TrioTestContext :
134135 def __init__ (self ):
135136 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 ()
137143 self .fixtures_with_errors = set ()
138144 self .fixtures_with_cancel = set ()
139145 self .error_list = []
@@ -145,8 +151,8 @@ def crash(self, fixture, exc):
145151 self .error_list .append (exc )
146152 self .fixtures_with_errors .add (fixture )
147153 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 ()
150156
151157
152158class TrioFixture :
@@ -240,16 +246,17 @@ async def run(self, test_ctx, contextvars_ctx):
240246 return
241247
242248 # 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 )
243253 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.
247256 assert not self .user_done_events
248257 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 )
253260 else :
254261 func_value = self ._func (** resolved_kwargs )
255262 if isinstance (func_value , Coroutine ):
@@ -261,8 +268,17 @@ async def run(self, test_ctx, contextvars_ctx):
261268 else :
262269 # Regular synchronous function
263270 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 )
264276
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.)
266282 self .setup_done .set ()
267283
268284 # Wait for users to be finished
0 commit comments