| 1 | """ |
|---|
| 2 | Tests for allmydata.util.deferredutil. |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | from __future__ import annotations |
|---|
| 6 | |
|---|
| 7 | from twisted.trial import unittest |
|---|
| 8 | from twisted.internet import defer, reactor |
|---|
| 9 | from twisted.internet.defer import Deferred |
|---|
| 10 | from twisted.python.failure import Failure |
|---|
| 11 | from hypothesis.strategies import integers |
|---|
| 12 | from hypothesis import given |
|---|
| 13 | |
|---|
| 14 | from allmydata.util import deferredutil |
|---|
| 15 | from allmydata.util.deferredutil import race, MultiFailure |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | class DeferredUtilTests(unittest.TestCase, deferredutil.WaitForDelayedCallsMixin): |
|---|
| 19 | def test_gather_results(self): |
|---|
| 20 | d1 = defer.Deferred() |
|---|
| 21 | d2 = defer.Deferred() |
|---|
| 22 | res = deferredutil.gatherResults([d1, d2]) |
|---|
| 23 | d1.errback(ValueError("BAD")) |
|---|
| 24 | def _callb(res): |
|---|
| 25 | self.fail("Should have errbacked, not resulted in %s" % (res,)) |
|---|
| 26 | def _errb(thef): |
|---|
| 27 | thef.trap(ValueError) |
|---|
| 28 | res.addCallbacks(_callb, _errb) |
|---|
| 29 | return res |
|---|
| 30 | |
|---|
| 31 | def test_success(self): |
|---|
| 32 | d1, d2 = defer.Deferred(), defer.Deferred() |
|---|
| 33 | good = [] |
|---|
| 34 | bad = [] |
|---|
| 35 | dlss = deferredutil.DeferredListShouldSucceed([d1,d2]) |
|---|
| 36 | dlss.addCallbacks(good.append, bad.append) |
|---|
| 37 | d1.callback(1) |
|---|
| 38 | d2.callback(2) |
|---|
| 39 | self.failUnlessEqual(good, [[1,2]]) |
|---|
| 40 | self.failUnlessEqual(bad, []) |
|---|
| 41 | |
|---|
| 42 | def test_failure(self): |
|---|
| 43 | d1, d2 = defer.Deferred(), defer.Deferred() |
|---|
| 44 | good = [] |
|---|
| 45 | bad = [] |
|---|
| 46 | dlss = deferredutil.DeferredListShouldSucceed([d1,d2]) |
|---|
| 47 | dlss.addCallbacks(good.append, bad.append) |
|---|
| 48 | d1.addErrback(lambda _ignore: None) |
|---|
| 49 | d2.addErrback(lambda _ignore: None) |
|---|
| 50 | d1.callback(1) |
|---|
| 51 | d2.errback(ValueError()) |
|---|
| 52 | self.failUnlessEqual(good, []) |
|---|
| 53 | self.failUnlessEqual(len(bad), 1) |
|---|
| 54 | f = bad[0] |
|---|
| 55 | self.failUnless(isinstance(f, Failure)) |
|---|
| 56 | self.failUnless(f.check(ValueError)) |
|---|
| 57 | |
|---|
| 58 | def test_wait_for_delayed_calls(self): |
|---|
| 59 | """ |
|---|
| 60 | This tests that 'wait_for_delayed_calls' does in fact wait for a |
|---|
| 61 | delayed call that is active when the test returns. If it didn't, |
|---|
| 62 | Trial would report an unclean reactor error for this test. |
|---|
| 63 | """ |
|---|
| 64 | def _trigger(): |
|---|
| 65 | #print("trigger") |
|---|
| 66 | pass |
|---|
| 67 | reactor.callLater(0.1, _trigger) |
|---|
| 68 | |
|---|
| 69 | d = defer.succeed(None) |
|---|
| 70 | d.addBoth(self.wait_for_delayed_calls) |
|---|
| 71 | return d |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | class UntilTests(unittest.TestCase): |
|---|
| 75 | """ |
|---|
| 76 | Tests for ``deferredutil.until``. |
|---|
| 77 | """ |
|---|
| 78 | def test_exception(self): |
|---|
| 79 | """ |
|---|
| 80 | If the action raises an exception, the ``Deferred`` returned by ``until`` |
|---|
| 81 | fires with a ``Failure``. |
|---|
| 82 | """ |
|---|
| 83 | self.assertFailure( |
|---|
| 84 | deferredutil.until(lambda: 1/0, lambda: True), |
|---|
| 85 | ZeroDivisionError, |
|---|
| 86 | ) |
|---|
| 87 | |
|---|
| 88 | def test_stops_on_condition(self): |
|---|
| 89 | """ |
|---|
| 90 | The action is called repeatedly until ``condition`` returns ``True``. |
|---|
| 91 | """ |
|---|
| 92 | calls = [] |
|---|
| 93 | def action(): |
|---|
| 94 | calls.append(None) |
|---|
| 95 | |
|---|
| 96 | def condition(): |
|---|
| 97 | return len(calls) == 3 |
|---|
| 98 | |
|---|
| 99 | self.assertIs( |
|---|
| 100 | self.successResultOf( |
|---|
| 101 | deferredutil.until(action, condition), |
|---|
| 102 | ), |
|---|
| 103 | None, |
|---|
| 104 | ) |
|---|
| 105 | self.assertEqual(3, len(calls)) |
|---|
| 106 | |
|---|
| 107 | def test_waits_for_deferred(self): |
|---|
| 108 | """ |
|---|
| 109 | If the action returns a ``Deferred`` then it is called again when the |
|---|
| 110 | ``Deferred`` fires. |
|---|
| 111 | """ |
|---|
| 112 | counter = [0] |
|---|
| 113 | r1 = defer.Deferred() |
|---|
| 114 | r2 = defer.Deferred() |
|---|
| 115 | results = [r1, r2] |
|---|
| 116 | def action(): |
|---|
| 117 | counter[0] += 1 |
|---|
| 118 | return results.pop(0) |
|---|
| 119 | |
|---|
| 120 | def condition(): |
|---|
| 121 | return False |
|---|
| 122 | |
|---|
| 123 | deferredutil.until(action, condition) |
|---|
| 124 | self.assertEqual([1], counter) |
|---|
| 125 | r1.callback(None) |
|---|
| 126 | self.assertEqual([2], counter) |
|---|
| 127 | |
|---|
| 128 | |
|---|
| 129 | class AsyncToDeferred(unittest.TestCase): |
|---|
| 130 | """Tests for ``deferredutil.async_to_deferred.``""" |
|---|
| 131 | |
|---|
| 132 | def test_async_to_deferred_success(self): |
|---|
| 133 | """ |
|---|
| 134 | Normal results from a ``@async_to_deferred``-wrapped function get |
|---|
| 135 | turned into a ``Deferred`` with that value. |
|---|
| 136 | """ |
|---|
| 137 | @deferredutil.async_to_deferred |
|---|
| 138 | async def f(x, y): |
|---|
| 139 | return x + y |
|---|
| 140 | |
|---|
| 141 | result = f(1, y=2) |
|---|
| 142 | self.assertEqual(self.successResultOf(result), 3) |
|---|
| 143 | |
|---|
| 144 | def test_async_to_deferred_exception(self): |
|---|
| 145 | """ |
|---|
| 146 | Exceptions from a ``@async_to_deferred``-wrapped function get |
|---|
| 147 | turned into a ``Deferred`` with that value. |
|---|
| 148 | """ |
|---|
| 149 | @deferredutil.async_to_deferred |
|---|
| 150 | async def f(x, y): |
|---|
| 151 | return x/y |
|---|
| 152 | |
|---|
| 153 | result = f(1, 0) |
|---|
| 154 | self.assertIsInstance(self.failureResultOf(result).value, ZeroDivisionError) |
|---|
| 155 | |
|---|
| 156 | |
|---|
| 157 | |
|---|
| 158 | def _setupRaceState(numDeferreds: int) -> tuple[list[int], list[Deferred[object]]]: |
|---|
| 159 | """ |
|---|
| 160 | Create a list of Deferreds and a corresponding list of integers |
|---|
| 161 | tracking how many times each Deferred has been cancelled. Without |
|---|
| 162 | additional steps the Deferreds will never fire. |
|---|
| 163 | """ |
|---|
| 164 | cancelledState = [0] * numDeferreds |
|---|
| 165 | |
|---|
| 166 | ds: list[Deferred[object]] = [] |
|---|
| 167 | for n in range(numDeferreds): |
|---|
| 168 | |
|---|
| 169 | def cancel(d: Deferred, n: int = n) -> None: |
|---|
| 170 | cancelledState[n] += 1 |
|---|
| 171 | |
|---|
| 172 | ds.append(Deferred(canceller=cancel)) |
|---|
| 173 | |
|---|
| 174 | return cancelledState, ds |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | class RaceTests(unittest.SynchronousTestCase): |
|---|
| 178 | """ |
|---|
| 179 | Tests for L{race}. |
|---|
| 180 | """ |
|---|
| 181 | |
|---|
| 182 | @given( |
|---|
| 183 | beforeWinner=integers(min_value=0, max_value=3), |
|---|
| 184 | afterWinner=integers(min_value=0, max_value=3), |
|---|
| 185 | ) |
|---|
| 186 | def test_success(self, beforeWinner: int, afterWinner: int) -> None: |
|---|
| 187 | """ |
|---|
| 188 | When one of the L{Deferred}s passed to L{race} fires successfully, |
|---|
| 189 | the L{Deferred} return by L{race} fires with the index of that |
|---|
| 190 | L{Deferred} and its result and cancels the rest of the L{Deferred}s. |
|---|
| 191 | @param beforeWinner: A randomly selected number of Deferreds to |
|---|
| 192 | appear before the "winning" Deferred in the list passed in. |
|---|
| 193 | @param beforeWinner: A randomly selected number of Deferreds to |
|---|
| 194 | appear after the "winning" Deferred in the list passed in. |
|---|
| 195 | """ |
|---|
| 196 | cancelledState, ds = _setupRaceState(beforeWinner + 1 + afterWinner) |
|---|
| 197 | |
|---|
| 198 | raceResult = race(ds) |
|---|
| 199 | expected = object() |
|---|
| 200 | ds[beforeWinner].callback(expected) |
|---|
| 201 | |
|---|
| 202 | # The result should be the index and result of the only Deferred that |
|---|
| 203 | # fired. |
|---|
| 204 | self.assertEqual( |
|---|
| 205 | self.successResultOf(raceResult), |
|---|
| 206 | (beforeWinner, expected), |
|---|
| 207 | ) |
|---|
| 208 | # All Deferreds except the winner should have been cancelled once. |
|---|
| 209 | expectedCancelledState = [1] * beforeWinner + [0] + [1] * afterWinner |
|---|
| 210 | self.assertEqual( |
|---|
| 211 | cancelledState, |
|---|
| 212 | expectedCancelledState, |
|---|
| 213 | ) |
|---|
| 214 | |
|---|
| 215 | @given( |
|---|
| 216 | beforeWinner=integers(min_value=0, max_value=3), |
|---|
| 217 | afterWinner=integers(min_value=0, max_value=3), |
|---|
| 218 | ) |
|---|
| 219 | def test_failure(self, beforeWinner: int, afterWinner: int) -> None: |
|---|
| 220 | """ |
|---|
| 221 | When all of the L{Deferred}s passed to L{race} fire with failures, |
|---|
| 222 | the L{Deferred} return by L{race} fires with L{MultiFailure} wrapping |
|---|
| 223 | all of their failures. |
|---|
| 224 | @param beforeWinner: A randomly selected number of Deferreds to |
|---|
| 225 | appear before the "winning" Deferred in the list passed in. |
|---|
| 226 | @param beforeWinner: A randomly selected number of Deferreds to |
|---|
| 227 | appear after the "winning" Deferred in the list passed in. |
|---|
| 228 | """ |
|---|
| 229 | cancelledState, ds = _setupRaceState(beforeWinner + 1 + afterWinner) |
|---|
| 230 | |
|---|
| 231 | failure = Failure(Exception("The test demands failures.")) |
|---|
| 232 | raceResult = race(ds) |
|---|
| 233 | for d in ds: |
|---|
| 234 | d.errback(failure) |
|---|
| 235 | |
|---|
| 236 | actualFailure = self.failureResultOf(raceResult, MultiFailure) |
|---|
| 237 | self.assertEqual( |
|---|
| 238 | actualFailure.value.failures, |
|---|
| 239 | [failure] * len(ds), |
|---|
| 240 | ) |
|---|
| 241 | self.assertEqual( |
|---|
| 242 | cancelledState, |
|---|
| 243 | [0] * len(ds), |
|---|
| 244 | ) |
|---|
| 245 | |
|---|
| 246 | @given( |
|---|
| 247 | beforeWinner=integers(min_value=0, max_value=3), |
|---|
| 248 | afterWinner=integers(min_value=0, max_value=3), |
|---|
| 249 | ) |
|---|
| 250 | def test_resultAfterCancel(self, beforeWinner: int, afterWinner: int) -> None: |
|---|
| 251 | """ |
|---|
| 252 | If one of the Deferreds fires after it was cancelled its result |
|---|
| 253 | goes nowhere. In particular, it does not cause any errors to be |
|---|
| 254 | logged. |
|---|
| 255 | """ |
|---|
| 256 | # Ensure we have a Deferred to win and at least one other Deferred |
|---|
| 257 | # that can ignore cancellation. |
|---|
| 258 | ds: list[Deferred[None]] = [ |
|---|
| 259 | Deferred() for n in range(beforeWinner + 2 + afterWinner) |
|---|
| 260 | ] |
|---|
| 261 | |
|---|
| 262 | raceResult = race(ds) |
|---|
| 263 | ds[beforeWinner].callback(None) |
|---|
| 264 | ds[beforeWinner + 1].callback(None) |
|---|
| 265 | |
|---|
| 266 | self.successResultOf(raceResult) |
|---|
| 267 | self.assertEqual(len(self.flushLoggedErrors()), 0) |
|---|
| 268 | |
|---|
| 269 | def test_resultFromCancel(self) -> None: |
|---|
| 270 | """ |
|---|
| 271 | If one of the input Deferreds has a cancel function that fires it |
|---|
| 272 | with success, nothing bad happens. |
|---|
| 273 | """ |
|---|
| 274 | winner: Deferred[object] = Deferred() |
|---|
| 275 | ds: list[Deferred[object]] = [ |
|---|
| 276 | winner, |
|---|
| 277 | Deferred(canceller=lambda d: d.callback(object())), |
|---|
| 278 | ] |
|---|
| 279 | expected = object() |
|---|
| 280 | raceResult = race(ds) |
|---|
| 281 | winner.callback(expected) |
|---|
| 282 | |
|---|
| 283 | self.assertEqual(self.successResultOf(raceResult), (0, expected)) |
|---|
| 284 | |
|---|
| 285 | @given( |
|---|
| 286 | numDeferreds=integers(min_value=1, max_value=3), |
|---|
| 287 | ) |
|---|
| 288 | def test_cancel(self, numDeferreds: int) -> None: |
|---|
| 289 | """ |
|---|
| 290 | If the result of L{race} is cancelled then all of the L{Deferred}s |
|---|
| 291 | passed in are cancelled. |
|---|
| 292 | """ |
|---|
| 293 | cancelledState, ds = _setupRaceState(numDeferreds) |
|---|
| 294 | |
|---|
| 295 | raceResult = race(ds) |
|---|
| 296 | raceResult.cancel() |
|---|
| 297 | |
|---|
| 298 | self.assertEqual(cancelledState, [1] * numDeferreds) |
|---|
| 299 | self.failureResultOf(raceResult, MultiFailure) |
|---|