|
17 | 17 | # [This file includes modifications made by New Vector Limited]
|
18 | 18 | #
|
19 | 19 | #
|
| 20 | +import logging |
20 | 21 | import traceback
|
21 |
| -from typing import Any, Coroutine, Generator, List, NoReturn, Optional, Tuple, TypeVar |
| 22 | +from typing import Any, Coroutine, List, NoReturn, Optional, Tuple, TypeVar |
22 | 23 |
|
23 | 24 | from parameterized import parameterized_class
|
24 | 25 |
|
|
47 | 48 | from tests.server import get_clock
|
48 | 49 | from tests.unittest import TestCase
|
49 | 50 |
|
| 51 | +logger = logging.getLogger(__name__) |
| 52 | + |
50 | 53 | T = TypeVar("T")
|
51 | 54 |
|
52 | 55 |
|
@@ -188,47 +191,78 @@ def canceller(_d: Deferred) -> None:
|
188 | 191 |
|
189 | 192 | self.failureResultOf(timing_out_d, defer.TimeoutError)
|
190 | 193 |
|
191 |
| - def test_logcontext_is_preserved_on_cancellation(self) -> None: |
192 |
| - blocking_was_cancelled = False |
| 194 | + async def test_logcontext_is_preserved_on_cancellation(self) -> None: |
| 195 | + # Sanity check that we start in the sentinel context |
| 196 | + self.assertEqual(current_context(), SENTINEL_CONTEXT) |
193 | 197 |
|
194 |
| - @defer.inlineCallbacks |
195 |
| - def blocking() -> Generator["Deferred[object]", object, None]: |
196 |
| - nonlocal blocking_was_cancelled |
| 198 | + incomplete_deferred_was_cancelled = False |
197 | 199 |
|
198 |
| - non_completing_d: Deferred = Deferred() |
199 |
| - with PreserveLoggingContext(): |
200 |
| - try: |
201 |
| - yield non_completing_d |
202 |
| - except CancelledError: |
203 |
| - blocking_was_cancelled = True |
204 |
| - raise |
| 200 | + def mark_was_cancelled(res: Failure) -> None: |
| 201 | + """ |
| 202 | + A passthrough errback which sets `incomplete_deferred_was_cancelled`. |
205 | 203 |
|
206 |
| - with LoggingContext("one") as context_one: |
207 |
| - # the errbacks should be run in the test logcontext |
208 |
| - def errback(res: Failure, deferred_name: str) -> Failure: |
209 |
| - self.assertIs( |
210 |
| - current_context(), |
211 |
| - context_one, |
212 |
| - "errback %s run in unexpected logcontext %s" |
213 |
| - % (deferred_name, current_context()), |
| 204 | + This means we re-raise any exception and allows further errbacks (in |
| 205 | + `timeout_deferred(...)`) to do their thing. Just trying to be a transparent |
| 206 | + proxy of any exception while doing our internal test book-keeping. |
| 207 | + """ |
| 208 | + nonlocal incomplete_deferred_was_cancelled |
| 209 | + if res.check(CancelledError): |
| 210 | + incomplete_deferred_was_cancelled = True |
| 211 | + else: |
| 212 | + logger.error( |
| 213 | + "Expected incomplete_d to fail with `CancelledError` because our " |
| 214 | + "`timeout_deferred(...)` utility canceled it but saw %s", |
| 215 | + res, |
214 | 216 | )
|
215 |
| - return res |
216 | 217 |
|
217 |
| - original_deferred = blocking() |
218 |
| - original_deferred.addErrback(errback, "orig") |
219 |
| - timing_out_d = timeout_deferred(original_deferred, 1.0, self.clock) |
| 218 | + # Re-raise the exception so that any further errbacks can do their thing as |
| 219 | + # normal |
| 220 | + res.raiseException() |
| 221 | + |
| 222 | + # Create a deferred which we will never complete |
| 223 | + incomplete_d: Deferred = Deferred() |
| 224 | + incomplete_d.addErrback(mark_was_cancelled) |
| 225 | + |
| 226 | + with LoggingContext("one") as context_one: |
| 227 | + timing_out_d = timeout_deferred( |
| 228 | + deferred=incomplete_d, |
| 229 | + timeout=1.0, |
| 230 | + reactor=self.clock, |
| 231 | + ) |
220 | 232 | self.assertNoResult(timing_out_d)
|
221 |
| - self.assertIs(current_context(), SENTINEL_CONTEXT) |
222 |
| - timing_out_d.addErrback(errback, "timingout") |
| 233 | + # We should still be in the logcontext we started in |
| 234 | + self.assertIs(current_context(), context_one) |
223 | 235 |
|
224 |
| - self.clock.pump((1.0,)) |
| 236 | + # Pump the reactor until we trigger the timeout |
| 237 | + # |
| 238 | + # We're manually pumping the reactor (and causing any pending callbacks to |
| 239 | + # be called) so we need to be in the sentinel logcontext to avoid leaking |
| 240 | + # our current logcontext into the reactor (which would then get picked up |
| 241 | + # and associated with the next thing the reactor does). `with |
| 242 | + # PreserveLoggingContext()` will reset the logcontext to the sentinel while |
| 243 | + # we're pumping the reactor in the block and return us back to our current |
| 244 | + # logcontext after the block. |
| 245 | + with PreserveLoggingContext(): |
| 246 | + self.clock.pump( |
| 247 | + # We only need to pump `1.0` (seconds) as we set |
| 248 | + # `timeout_deferred(timeout=1.0)` above |
| 249 | + (1.0,) |
| 250 | + ) |
225 | 251 |
|
| 252 | + # We expect the incomplete deferred to have been cancelled because of the |
| 253 | + # timeout by this point |
226 | 254 | self.assertTrue(
|
227 |
| - blocking_was_cancelled, "non-completing deferred was not cancelled" |
| 255 | + incomplete_deferred_was_cancelled, |
| 256 | + "incomplete deferred was not cancelled", |
228 | 257 | )
|
| 258 | + # We should see the `TimeoutError` (instead of a `CancelledError`) |
229 | 259 | self.failureResultOf(timing_out_d, defer.TimeoutError)
|
| 260 | + # We're still in the same logcontext |
230 | 261 | self.assertIs(current_context(), context_one)
|
231 | 262 |
|
| 263 | + # Back to the sentinel context |
| 264 | + self.assertEqual(current_context(), SENTINEL_CONTEXT) |
| 265 | + |
232 | 266 |
|
233 | 267 | class _TestException(Exception):
|
234 | 268 | pass
|
|
0 commit comments