|
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