source: trunk/src/allmydata/test/test_deferredutil.py

Last change on this file was b43150b, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-03-08T21:48:08Z

Add future import.

  • Property mode set to 100644
File size: 9.5 KB
Line 
1"""
2Tests for allmydata.util.deferredutil.
3"""
4
5from __future__ import annotations
6
7from twisted.trial import unittest
8from twisted.internet import defer, reactor
9from twisted.internet.defer import Deferred
10from twisted.python.failure import Failure
11from hypothesis.strategies import integers
12from hypothesis import given
13
14from allmydata.util import deferredutil
15from allmydata.util.deferredutil import race, MultiFailure
16
17
18class 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
74class 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
129class 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
158def _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
177class 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)
Note: See TracBrowser for help on using the repository browser.