1681 lines
51 KiB
Python
1681 lines
51 KiB
Python
# -*- test-case-name: twisted.internet.test.test_inlinecb -*-
|
|
# Copyright (c) Twisted Matrix Laboratories.
|
|
# See LICENSE for details.
|
|
|
|
"""
|
|
Tests for L{twisted.internet.inlineCallbacks}.
|
|
"""
|
|
|
|
import traceback
|
|
import unittest as pyunit
|
|
import weakref
|
|
from enum import Enum
|
|
from typing import Any, Generator, List, Set, Union
|
|
|
|
from twisted.internet import reactor, task
|
|
from twisted.internet.defer import (
|
|
CancelledError,
|
|
Deferred,
|
|
Failure,
|
|
fail,
|
|
inlineCallbacks,
|
|
returnValue,
|
|
succeed,
|
|
)
|
|
from twisted.python.compat import _PYPY
|
|
from twisted.trial.unittest import SynchronousTestCase, TestCase
|
|
|
|
|
|
def getValueViaDeferred(value):
|
|
d = Deferred()
|
|
reactor.callLater(0, d.callback, value)
|
|
return d
|
|
|
|
|
|
async def getValueViaCoro(value):
|
|
return await getValueViaDeferred(value)
|
|
|
|
|
|
def getDivisionFailure(msg: Union[str, None] = None) -> Failure:
|
|
"""
|
|
Make a L{Failure} of a divide-by-zero error.
|
|
"""
|
|
try:
|
|
raise ZeroDivisionError(msg)
|
|
except BaseException:
|
|
f = Failure()
|
|
return f
|
|
|
|
|
|
async def getDivisionFailureCoro(msg: Union[str, None] = None) -> None:
|
|
"""
|
|
Make a coroutine that throws a divide-by-zero error.
|
|
"""
|
|
await getValueViaDeferred("value")
|
|
raise ZeroDivisionError(msg)
|
|
|
|
|
|
class TerminalException(Exception):
|
|
"""
|
|
Just a specific exception type for use in inlineCallbacks tests in this
|
|
file.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class BasicTests(TestCase):
|
|
"""
|
|
This test suite tests basic use cases of L{inlineCallbacks}. For more
|
|
complex tests see e.g. StackedInlineCallbacksTests.
|
|
|
|
Note that it is important to directly call addCallbacks and other
|
|
functions exposed as an API, because both L{inlineCallbacks} and
|
|
L{Deferred} may be optimized in ways that are only exercised in particular
|
|
situations.
|
|
"""
|
|
|
|
def testBasics(self):
|
|
"""
|
|
Test that a normal inlineCallbacks works. Tests yielding a
|
|
deferred which callbacks, as well as a deferred errbacks. Also
|
|
ensures returning a final value works.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genBasics():
|
|
x = yield getValueViaDeferred("hi")
|
|
|
|
self.assertEqual(x, "hi")
|
|
|
|
try:
|
|
yield getDivisionFailure("OMG")
|
|
except ZeroDivisionError as e:
|
|
self.assertEqual(str(e), "OMG")
|
|
return "WOOSH"
|
|
|
|
return _genBasics().addCallback(self.assertEqual, "WOOSH")
|
|
|
|
def testBasicsAsync(self):
|
|
"""
|
|
C{inlineCallbacks} can yield a coroutine and catch its exception.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genBasics():
|
|
x = yield getValueViaCoro("hi")
|
|
|
|
self.assertEqual(x, "hi")
|
|
|
|
try:
|
|
yield getDivisionFailureCoro("OMG")
|
|
except ZeroDivisionError as e:
|
|
self.assertEqual(str(e), "OMG")
|
|
returnValue("WOOSH")
|
|
|
|
return _genBasics().addCallback(self.assertEqual, "WOOSH")
|
|
|
|
def testProducesException(self):
|
|
"""
|
|
Ensure that a generator that produces an exception signals
|
|
a Failure condition on result deferred by converting the exception to
|
|
a L{Failure}.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genProduceException():
|
|
yield getValueViaDeferred("hi")
|
|
1 / 0
|
|
|
|
return self.assertFailure(_genProduceException(), ZeroDivisionError)
|
|
|
|
def testNothing(self):
|
|
"""Test that a generator which never yields results in None."""
|
|
|
|
@inlineCallbacks
|
|
def _genNothing():
|
|
if False:
|
|
yield 1 # pragma: no cover
|
|
|
|
return _genNothing().addCallback(self.assertEqual, None)
|
|
|
|
def testHandledTerminalFailure(self):
|
|
"""
|
|
Create a Deferred Generator which yields a Deferred which fails and
|
|
handles the exception which results. Assert that the Deferred
|
|
Generator does not errback its Deferred.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genHandledTerminalFailure():
|
|
try:
|
|
yield fail(TerminalException("Handled Terminal Failure"))
|
|
except TerminalException:
|
|
pass
|
|
|
|
return _genHandledTerminalFailure().addCallback(self.assertEqual, None)
|
|
|
|
def testHandledTerminalAsyncFailure(self):
|
|
"""
|
|
Just like testHandledTerminalFailure, only with a Deferred which fires
|
|
asynchronously with an error.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genHandledTerminalAsyncFailure(d):
|
|
try:
|
|
yield d
|
|
except TerminalException:
|
|
pass
|
|
|
|
d = Deferred()
|
|
deferredGeneratorResultDeferred = _genHandledTerminalAsyncFailure(d)
|
|
d.errback(TerminalException("Handled Terminal Failure"))
|
|
return deferredGeneratorResultDeferred.addCallback(self.assertEqual, None)
|
|
|
|
def testHandledCoroAsyncFailure(self):
|
|
"""
|
|
Just like testHandledCoroAsyncFailure, only with a Deferred which fires
|
|
asynchronously with an error and is wrapped in coroutine.
|
|
"""
|
|
|
|
d = Deferred()
|
|
|
|
async def coro():
|
|
return await d
|
|
|
|
@inlineCallbacks
|
|
def function():
|
|
try:
|
|
yield coro()
|
|
except TerminalException:
|
|
pass
|
|
|
|
d = Deferred()
|
|
inlineResultDeferred = function()
|
|
d.errback(TerminalException("Handled Terminal Failure"))
|
|
return inlineResultDeferred.addCallback(self.assertEqual, None)
|
|
|
|
def testStackUsage(self):
|
|
"""
|
|
Make sure we don't blow the stack when yielding immediately
|
|
available deferreds.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genStackUsage():
|
|
for x in range(5000):
|
|
# Test with yielding a deferred
|
|
yield succeed(1)
|
|
return 0
|
|
|
|
return _genStackUsage().addCallback(self.assertEqual, 0)
|
|
|
|
def testStackUsage2(self):
|
|
"""
|
|
Make sure we don't blow the stack when yielding immediately
|
|
available values.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def _genStackUsage2():
|
|
for x in range(5000):
|
|
# Test with yielding a random value
|
|
yield 1
|
|
return 0
|
|
|
|
return _genStackUsage2().addCallback(self.assertEqual, 0)
|
|
|
|
def testYieldNonDeferred(self):
|
|
"""
|
|
Ensure that yielding a non-deferred passes it back as the
|
|
result of the yield expression.
|
|
|
|
@return: A L{twisted.internet.Deferred}
|
|
@rtype: L{twisted.internet.Deferred}
|
|
"""
|
|
|
|
def _test():
|
|
yield 5
|
|
return 5
|
|
|
|
_test = inlineCallbacks(_test)
|
|
|
|
return _test().addCallback(self.assertEqual, 5)
|
|
|
|
def testReturnNoValue(self):
|
|
"""Ensure a standard python return results in a None result."""
|
|
|
|
def _noReturn():
|
|
yield 5
|
|
return
|
|
|
|
_noReturn = inlineCallbacks(_noReturn)
|
|
|
|
return _noReturn().addCallback(self.assertEqual, None)
|
|
|
|
def testReturnValueDeprecated(self):
|
|
"""C{returnValue} is now deprecated but continues to be available."""
|
|
|
|
@inlineCallbacks
|
|
def _return():
|
|
yield 5
|
|
returnValue(6)
|
|
|
|
d = _return()
|
|
|
|
warnings = self.flushWarnings()
|
|
self.assertEqual(1, len(warnings))
|
|
self.assertIs(DeprecationWarning, warnings[0]["category"])
|
|
self.assertIn(
|
|
"twisted.internet.defer.returnValue was deprecated in Twisted",
|
|
warnings[0]["message"],
|
|
)
|
|
|
|
return d.addCallback(self.assertEqual, 6)
|
|
|
|
def test_nonGeneratorReturn(self):
|
|
"""
|
|
Ensure that C{TypeError} with a message about L{inlineCallbacks} is
|
|
raised when a non-generator returns something other than a generator.
|
|
"""
|
|
|
|
def _noYield():
|
|
return 5
|
|
|
|
_noYield = inlineCallbacks(_noYield)
|
|
|
|
self.assertIn("inlineCallbacks", str(self.assertRaises(TypeError, _noYield)))
|
|
|
|
def test_nonGeneratorReturnValue(self):
|
|
"""
|
|
Ensure that C{TypeError} with a message about L{inlineCallbacks} is
|
|
raised when a non-generator calls L{returnValue}.
|
|
"""
|
|
|
|
def _noYield():
|
|
returnValue(5)
|
|
|
|
_noYield = inlineCallbacks(_noYield)
|
|
|
|
self.assertIn("inlineCallbacks", str(self.assertRaises(TypeError, _noYield)))
|
|
|
|
def test_internalDefGenReturnValueDoesntLeak(self):
|
|
"""
|
|
When one inlineCallbacks calls another, the internal L{_DefGen_Return}
|
|
flow control exception raised by calling L{defer.returnValue} doesn't
|
|
leak into tracebacks captured in the caller.
|
|
"""
|
|
clock = task.Clock()
|
|
|
|
@inlineCallbacks
|
|
def _returns():
|
|
"""
|
|
This is the inner function using returnValue.
|
|
"""
|
|
yield task.deferLater(clock, 0)
|
|
returnValue("actual-value-not-used-for-the-test")
|
|
|
|
@inlineCallbacks
|
|
def _raises():
|
|
try:
|
|
yield _returns()
|
|
raise TerminalException("boom returnValue")
|
|
except TerminalException:
|
|
return traceback.format_exc()
|
|
|
|
d = _raises()
|
|
clock.advance(0)
|
|
tb = self.successResultOf(d)
|
|
|
|
warnings = self.flushWarnings()
|
|
self.assertEqual(1, len(warnings))
|
|
self.assertIs(DeprecationWarning, warnings[0]["category"])
|
|
self.assertIn(
|
|
"twisted.internet.defer.returnValue was deprecated in Twisted",
|
|
warnings[0]["message"],
|
|
)
|
|
|
|
# The internal exception is not in the traceback.
|
|
self.assertNotIn("_DefGen_Return", tb)
|
|
# No other extra exception is in the traceback.
|
|
self.assertNotIn(
|
|
"During handling of the above exception, another exception occurred", tb
|
|
)
|
|
# Our targeted exception is in the traceback
|
|
self.assertIn("test_inlinecb.TerminalException: boom returnValue", tb)
|
|
|
|
def test_internalStopIterationDoesntLeak(self):
|
|
"""
|
|
When one inlineCallbacks calls another, the internal L{StopIteration}
|
|
flow control exception generated when the inner generator returns
|
|
doesn't leak into tracebacks captured in the caller.
|
|
|
|
This is similar to C{test_internalDefGenReturnValueDoesntLeak} but the
|
|
inner function uses the "normal" return statemement rather than the
|
|
C{returnValue} helper.
|
|
"""
|
|
clock = task.Clock()
|
|
|
|
@inlineCallbacks
|
|
def _returns():
|
|
yield task.deferLater(clock, 0)
|
|
return 6
|
|
|
|
@inlineCallbacks
|
|
def _raises():
|
|
try:
|
|
yield _returns()
|
|
raise TerminalException("boom normal return")
|
|
except TerminalException:
|
|
return traceback.format_exc()
|
|
|
|
d = _raises()
|
|
clock.advance(0)
|
|
tb = self.successResultOf(d)
|
|
|
|
# The internal exception is not in the traceback.
|
|
self.assertNotIn("StopIteration", tb)
|
|
# No other extra exception is in the traceback.
|
|
self.assertNotIn(
|
|
"During handling of the above exception, another exception occurred", tb
|
|
)
|
|
# Our targeted exception is in the traceback
|
|
self.assertIn("test_inlinecb.TerminalException: boom normal return", tb)
|
|
|
|
@pyunit.skipIf(_PYPY, "GC works differently on PyPy.")
|
|
def test_inlineCallbacksNoCircularReference(self) -> None:
|
|
"""
|
|
When using L{defer.inlineCallbacks}, after the function exits, it will
|
|
not keep references to the function itself or the arguments.
|
|
|
|
This ensures that the machinery gets deallocated immediately rather than
|
|
waiting for a GC, on CPython.
|
|
|
|
The GC on PyPy works differently (del doesn't immediately deallocate the
|
|
object), so we skip the test.
|
|
"""
|
|
|
|
# Create an object and weak reference to track if its gotten freed.
|
|
obj: Set[Any] = set()
|
|
objWeakRef = weakref.ref(obj)
|
|
|
|
@inlineCallbacks
|
|
def func(a: Any) -> Any:
|
|
yield a
|
|
return a
|
|
|
|
# Run the function
|
|
funcD = func(obj)
|
|
self.assertEqual(obj, self.successResultOf(funcD))
|
|
|
|
funcDWeakRef = weakref.ref(funcD)
|
|
|
|
# Delete the local references to obj and funcD.
|
|
del obj
|
|
del funcD
|
|
|
|
# The object has been freed if the weak reference returns None.
|
|
self.assertIsNone(objWeakRef())
|
|
self.assertIsNone(funcDWeakRef())
|
|
|
|
|
|
class StopIterationReturnTests(TestCase):
|
|
"""
|
|
On Python 3.4 and newer generator functions may use the C{return} statement
|
|
with a value, which is attached to the L{StopIteration} exception that is
|
|
raised.
|
|
|
|
L{inlineCallbacks} will use this value when it fires the C{callback}.
|
|
"""
|
|
|
|
def test_returnWithValue(self):
|
|
"""
|
|
If the C{return} statement has a value it is propagated back to the
|
|
L{Deferred} that the C{inlineCallbacks} function returned.
|
|
"""
|
|
environ = {"inlineCallbacks": inlineCallbacks}
|
|
exec(
|
|
"""
|
|
@inlineCallbacks
|
|
def f(d):
|
|
yield d
|
|
return 14
|
|
""",
|
|
environ,
|
|
)
|
|
d1 = Deferred()
|
|
d2 = environ["f"](d1)
|
|
d1.callback(None)
|
|
self.assertEqual(self.successResultOf(d2), 14)
|
|
|
|
|
|
class StackedInlineCallbacksTests(TestCase):
|
|
"""
|
|
We have an optimization that invokes generators directly when an
|
|
inlineCallbacks-decorated function yields value directly to yield of
|
|
another inlineCallbacks-decorated function.
|
|
"""
|
|
|
|
def runCallbacksOnDeferreds(self, deferredList):
|
|
"""
|
|
Given a list of L{Deferred}, value tuples, invokes each L{Deferred}
|
|
with the corresponding value. Depending on whether value is an
|
|
exception, either callback or errback is called.
|
|
"""
|
|
for d, x in deferredList:
|
|
if isinstance(x, Exception):
|
|
d.errback(x)
|
|
else:
|
|
d.callback(x)
|
|
|
|
def test_nonCalledDeferredSingleYield(self):
|
|
"""
|
|
Tests the case when a chain of L{inlineCallbacks} calls end up
|
|
yielding and blocking on a L{Deferred}.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
x = yield f1(x)
|
|
x += 2
|
|
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
|
|
self.assertEqual(self.successResultOf(res), 8)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f2 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f2 exit", 4),
|
|
("f3 exit", 8),
|
|
],
|
|
)
|
|
|
|
def test_nonCalledDeferredSingleYieldCoro(self):
|
|
"""
|
|
Tests the case when a chain of L{inlineCallbacks} mixed with coroutine
|
|
calls end up yielding and blocking on a L{Deferred}.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
returnValue(x)
|
|
|
|
async def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
x = await f1(x)
|
|
x += 2
|
|
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
returnValue(x)
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
|
|
self.assertEqual(self.successResultOf(res), 8)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f2 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f2 exit", 4),
|
|
("f3 exit", 8),
|
|
],
|
|
)
|
|
|
|
def test_nonCalledDeferredMultipleYields(self):
|
|
"""
|
|
Tests the case when a chain of L{inlineCallbacks} calls end up yielding
|
|
and blocking on a L{Deferred}. In this case the same decorated function
|
|
is yielded multiple times.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
x = yield f1(x)
|
|
x = yield f1(x)
|
|
x = yield f1(x)
|
|
x += 2
|
|
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f2(x)
|
|
x = yield f2(x)
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
for d, x in deferredList:
|
|
d.callback(x)
|
|
|
|
self.assertEqual(self.successResultOf(res), 20)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f2 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f1 enter", 2),
|
|
("f1 exit", 3),
|
|
("f1 enter", 3),
|
|
("f1 exit", 4),
|
|
("f2 exit", 6),
|
|
("f2 enter", 6),
|
|
("f1 enter", 6),
|
|
("f1 exit", 7),
|
|
("f1 enter", 7),
|
|
("f1 exit", 8),
|
|
("f1 enter", 8),
|
|
("f1 exit", 9),
|
|
("f2 exit", 11),
|
|
("f2 enter", 11),
|
|
("f1 enter", 11),
|
|
("f1 exit", 12),
|
|
("f1 enter", 12),
|
|
("f1 exit", 13),
|
|
("f1 enter", 13),
|
|
("f1 exit", 14),
|
|
("f2 exit", 16),
|
|
("f3 exit", 20),
|
|
],
|
|
)
|
|
|
|
def test_nonCalledDeferredMultipleYieldsCoro(self):
|
|
"""
|
|
Tests the case when a chain of L{inlineCallbacks} calls mixed with async
|
|
function calls end up yielding and blocking on a L{Deferred}. In this case
|
|
the same decorated function is yielded multiple times.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
returnValue(x)
|
|
|
|
async def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
x = await f1(x)
|
|
x = await f1(x)
|
|
x = await f1(x)
|
|
x += 2
|
|
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f2(x)
|
|
x = yield f2(x)
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
returnValue(x)
|
|
|
|
res = f3(1)
|
|
for d, x in deferredList:
|
|
d.callback(x)
|
|
|
|
self.assertEqual(self.successResultOf(res), 20)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f2 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f1 enter", 2),
|
|
("f1 exit", 3),
|
|
("f1 enter", 3),
|
|
("f1 exit", 4),
|
|
("f2 exit", 6),
|
|
("f2 enter", 6),
|
|
("f1 enter", 6),
|
|
("f1 exit", 7),
|
|
("f1 enter", 7),
|
|
("f1 exit", 8),
|
|
("f1 enter", 8),
|
|
("f1 exit", 9),
|
|
("f2 exit", 11),
|
|
("f2 enter", 11),
|
|
("f1 enter", 11),
|
|
("f1 exit", 12),
|
|
("f1 enter", 12),
|
|
("f1 exit", 13),
|
|
("f1 enter", 13),
|
|
("f1 exit", 14),
|
|
("f2 exit", 16),
|
|
("f3 exit", 20),
|
|
],
|
|
)
|
|
|
|
def test_intermediateAddCallbacksAndNoWaiting(self):
|
|
"""
|
|
Tests the case when a L{Deferred} produced from L{inlineCallbacks} gets
|
|
a callback added via L{addCallback} and then yielded in a function
|
|
decorated with L{inlineCallbacks}. In this case the initial L{Deferred}
|
|
already has a value.
|
|
"""
|
|
expectations = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
x = yield x
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
x += 2
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
d = f1(x)
|
|
d.addCallback(f2)
|
|
x = yield d
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
self.assertEqual(self.successResultOf(f3(1)), 8)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f2 enter", 2),
|
|
("f2 exit", 4),
|
|
("f3 exit", 8),
|
|
],
|
|
)
|
|
|
|
def test_intermediateAddCallbacksAndWithWaitingFirstYield(self):
|
|
"""
|
|
Tests the case when a L{Deferred} produced from L{inlineCallbacks} gets
|
|
a callback added via L{addCallback} and then yielded in a function
|
|
decorated with L{inlineCallbacks}. In this case the initial L{Deferred}
|
|
does not have a value and blocks entire callback chain.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
x += 2
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
d = f1(x)
|
|
d.addCallback(f2)
|
|
x = yield d
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
|
|
self.assertEqual(self.successResultOf(res), 8)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f2 enter", 2),
|
|
("f2 exit", 4),
|
|
("f3 exit", 8),
|
|
],
|
|
)
|
|
|
|
def test_intermediateAddCallbacksAndWithWaitingSecondYield(self):
|
|
"""
|
|
Tests the case when a L{Deferred} produced from L{inlineCallbacks} gets
|
|
a callback added via L{addCallback} and then yielded in a function
|
|
decorated with L{inlineCallbacks}. In this case the initial L{Deferred}
|
|
does not have a value and blocks the entire callback chain.
|
|
Additionally, a subsequent L{Deferred} blocks the entire callback chain
|
|
again.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
x += 2
|
|
expectations.append(("f2 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f1(x)
|
|
d = f1(x)
|
|
d.addCallback(f2)
|
|
x = yield d
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
|
|
self.assertEqual(self.successResultOf(res), 9)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f1 enter", 2),
|
|
("f1 exit", 3),
|
|
("f2 enter", 3),
|
|
("f2 exit", 5),
|
|
("f3 exit", 9),
|
|
],
|
|
)
|
|
|
|
def test_raisesExceptionFromDeferredWithWaitingFirstCallback(self):
|
|
"""
|
|
Tests the case when a function decorated with L{inlineCallbacks} yields
|
|
a L{Deferred} that results in a failure.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
class MyException(Exception):
|
|
pass
|
|
|
|
@inlineCallbacks
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, MyException()))
|
|
x = yield d
|
|
|
|
# Above line throws an exception. The rest of the function will not
|
|
# be executed, but in case it is (error), it should still work so
|
|
# that assertions at the end of the test work.
|
|
expectations.append(("f2 exit", x)) # pragma: no cover
|
|
return x # pragma: no cover
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
with self.assertRaises(MyException):
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
self.assertEqual(self.successResultOf(res), 5)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f2 enter", 1),
|
|
("f3 exit", 5),
|
|
],
|
|
)
|
|
|
|
def test_raisesExceptionFromDeferredWithWaitingSecondCallback(self):
|
|
"""
|
|
Tests the case when a function decorated with L{inlineCallbacks} blocks
|
|
on a L{Deferred} produced by another function decorated with
|
|
L{inlineCallbacks}. Once that unblocks, a L{Deferred} that results
|
|
in an failure is yielded.
|
|
"""
|
|
expectations = []
|
|
|
|
# list of deferred to invoke with what results
|
|
deferredList = []
|
|
|
|
class MyException(Exception):
|
|
pass
|
|
|
|
@inlineCallbacks
|
|
def f1(x):
|
|
expectations.append(("f1 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, x))
|
|
x = yield d
|
|
x += 1
|
|
|
|
expectations.append(("f1 exit", x))
|
|
return x
|
|
|
|
@inlineCallbacks
|
|
def f2(x):
|
|
expectations.append(("f2 enter", x))
|
|
|
|
d = Deferred()
|
|
deferredList.append((d, MyException()))
|
|
x = yield d
|
|
|
|
# Above line throws an exception. The rest of the function will not
|
|
# be executed, but in case it is (error), it should still work so
|
|
# that assertions at the end of the test work.
|
|
expectations.append(("f2 exit", x)) # pragma: no cover
|
|
return x # pragma: no cover
|
|
|
|
@inlineCallbacks
|
|
def f3(x):
|
|
expectations.append(("f3 enter", x))
|
|
|
|
x = yield f1(x)
|
|
with self.assertRaises(MyException):
|
|
x = yield f2(x)
|
|
x += 4
|
|
|
|
expectations.append(("f3 exit", x))
|
|
return x
|
|
|
|
res = f3(1)
|
|
self.runCallbacksOnDeferreds(deferredList)
|
|
self.assertEqual(self.successResultOf(res), 6)
|
|
self.assertEqual(
|
|
expectations,
|
|
[
|
|
("f3 enter", 1),
|
|
("f1 enter", 1),
|
|
("f1 exit", 2),
|
|
("f2 enter", 2),
|
|
("f3 exit", 6),
|
|
],
|
|
)
|
|
|
|
|
|
class NonLocalExitTests(TestCase):
|
|
"""
|
|
It's possible for L{returnValue} to be (accidentally) invoked at a stack
|
|
level below the L{inlineCallbacks}-decorated function which it is exiting.
|
|
If this happens, L{returnValue} should report useful errors.
|
|
|
|
If L{returnValue} is invoked from a function not decorated by
|
|
L{inlineCallbacks}, it will emit a warning if it causes an
|
|
L{inlineCallbacks} function further up the stack to exit.
|
|
"""
|
|
|
|
def mistakenMethod(self):
|
|
"""
|
|
This method mistakenly invokes L{returnValue}, despite the fact that it
|
|
is not decorated with L{inlineCallbacks}.
|
|
"""
|
|
returnValue(1)
|
|
|
|
def assertMistakenMethodWarning(self, resultList):
|
|
"""
|
|
Flush the current warnings and assert that we have been told that
|
|
C{mistakenMethod} was invoked, and that the result from the Deferred
|
|
that was fired (appended to the given list) is C{mistakenMethod}'s
|
|
result. The warning should indicate that an inlineCallbacks function
|
|
called 'inline' was made to exit.
|
|
"""
|
|
self.assertEqual(resultList, [1])
|
|
warnings = self.flushWarnings(offendingFunctions=[self.mistakenMethod])
|
|
self.assertEqual(len(warnings), 2)
|
|
self.assertEqual(warnings[0]["category"], DeprecationWarning)
|
|
self.assertEqual(warnings[1]["category"], DeprecationWarning)
|
|
self.assertIn(
|
|
"twisted.internet.defer.returnValue was deprecated in Twisted",
|
|
warnings[0]["message"],
|
|
)
|
|
self.assertEqual(
|
|
warnings[1]["message"],
|
|
"returnValue() in 'mistakenMethod' causing 'inline' to exit: "
|
|
"returnValue should only be invoked by functions decorated with "
|
|
"inlineCallbacks",
|
|
)
|
|
|
|
def test_returnValueNonLocalWarning(self):
|
|
"""
|
|
L{returnValue} will emit a non-local exit warning in the simplest case,
|
|
where the offending function is invoked immediately.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def inline():
|
|
self.mistakenMethod()
|
|
returnValue(2)
|
|
yield 0
|
|
|
|
d = inline()
|
|
results = []
|
|
d.addCallback(results.append)
|
|
self.assertMistakenMethodWarning(results)
|
|
|
|
def test_returnValueNonLocalDeferred(self):
|
|
"""
|
|
L{returnValue} will emit a non-local warning in the case where the
|
|
L{inlineCallbacks}-decorated function has already yielded a Deferred
|
|
and therefore moved its generator function along.
|
|
"""
|
|
cause = Deferred()
|
|
|
|
@inlineCallbacks
|
|
def inline():
|
|
yield cause
|
|
self.mistakenMethod()
|
|
returnValue(2)
|
|
|
|
effect = inline()
|
|
results = []
|
|
effect.addCallback(results.append)
|
|
self.assertEqual(results, [])
|
|
cause.callback(1)
|
|
self.assertMistakenMethodWarning(results)
|
|
|
|
|
|
class ForwardTraceBackTests(SynchronousTestCase):
|
|
def test_forwardTracebacks(self):
|
|
"""
|
|
Chained inlineCallbacks are forwarding the traceback information
|
|
from generator to generator.
|
|
|
|
A first simple test with a couple of inline callbacks.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def erroring():
|
|
yield "forcing generator"
|
|
raise Exception("Error Marker")
|
|
|
|
@inlineCallbacks
|
|
def calling():
|
|
yield erroring()
|
|
|
|
d = calling()
|
|
f = self.failureResultOf(d)
|
|
tb = f.getTraceback()
|
|
self.assertIn("in erroring", tb)
|
|
self.assertIn("in calling", tb)
|
|
self.assertIn("Error Marker", tb)
|
|
|
|
def test_forwardTracebacksCoro(self):
|
|
"""
|
|
Chained inlineCallback and coroutine are forwarding the traceback
|
|
information from coroutine to generator.
|
|
"""
|
|
|
|
async def erroring():
|
|
await getValueViaDeferred("value")
|
|
raise Exception("Error Marker")
|
|
|
|
@inlineCallbacks
|
|
def calling():
|
|
yield erroring()
|
|
|
|
d = calling()
|
|
|
|
@d.addErrback
|
|
def check(f):
|
|
tb = f.getTraceback()
|
|
self.assertIn("in erroring", tb)
|
|
self.assertIn("in calling", tb)
|
|
self.assertIn("Error Marker", tb)
|
|
|
|
return d
|
|
|
|
def test_forwardLotsOfTracebacks(self):
|
|
"""
|
|
Several Chained inlineCallbacks gives information about all generators.
|
|
|
|
A wider test with a 4 chained inline callbacks.
|
|
|
|
Application stack-trace should be reported, and implementation details
|
|
like "throwExceptionIntoGenerator" symbols are omitted from the stack.
|
|
|
|
Note that the previous test is testing the simple case, and this one is
|
|
testing the deep recursion case.
|
|
|
|
That case needs specific code in failure.py to accomodate to stack
|
|
breakage introduced by throwExceptionIntoGenerator.
|
|
|
|
Hence we keep the two tests in order to sort out which code we
|
|
might have regression in.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def erroring():
|
|
yield "forcing generator"
|
|
raise Exception("Error Marker")
|
|
|
|
@inlineCallbacks
|
|
def calling3():
|
|
yield erroring()
|
|
|
|
@inlineCallbacks
|
|
def calling2():
|
|
yield calling3()
|
|
|
|
@inlineCallbacks
|
|
def calling():
|
|
yield calling2()
|
|
|
|
d = calling()
|
|
f = self.failureResultOf(d)
|
|
tb = f.getTraceback()
|
|
self.assertIn("in erroring", tb)
|
|
self.assertIn("in calling", tb)
|
|
self.assertIn("in calling2", tb)
|
|
self.assertIn("in calling3", tb)
|
|
self.assertIn("Error Marker", tb)
|
|
self.assertIn("in erroring", f.getTraceback())
|
|
|
|
def test_forwardLotsOfTracebacksCoro(self):
|
|
"""
|
|
Several chained inlineCallbacks mixed with coroutines gives information
|
|
about all generators.
|
|
|
|
A wider test with a 4 chained inline callbacks.
|
|
"""
|
|
|
|
@inlineCallbacks
|
|
def erroring():
|
|
yield "forcing generator"
|
|
raise Exception("Error Marker")
|
|
|
|
async def calling3():
|
|
await erroring()
|
|
|
|
@inlineCallbacks
|
|
def calling2():
|
|
yield calling3()
|
|
|
|
@inlineCallbacks
|
|
def calling():
|
|
yield calling2()
|
|
|
|
d = calling()
|
|
f = self.failureResultOf(d)
|
|
tb = f.getTraceback()
|
|
self.assertIn("in erroring", tb)
|
|
self.assertIn("in calling", tb)
|
|
self.assertIn("in calling2", tb)
|
|
self.assertIn("in calling3", tb)
|
|
self.assertIn("Error Marker", tb)
|
|
self.assertIn("in erroring", f.getTraceback())
|
|
|
|
def test_reraiseTracebacksFromDeferred(self) -> None:
|
|
"""
|
|
L{defer.inlineCallbacks} that receives tracebacks from a regular Deferred and
|
|
re-raise tracebacks into their deferred should not lose their tracebacks.
|
|
"""
|
|
f = getDivisionFailure("msg")
|
|
d: Deferred[None] = Deferred()
|
|
try:
|
|
f.raiseException()
|
|
except BaseException:
|
|
d.errback()
|
|
|
|
def ic(d: object) -> Generator[Any, Any, None]:
|
|
"""
|
|
This is never called.
|
|
It is only used as the decorated function.
|
|
The resulting function is never called in this test.
|
|
This is used to make sure that if we wrap
|
|
an already failed deferred, inlineCallbacks
|
|
will not add any extra traceback frames.
|
|
"""
|
|
yield d # pragma: no cover
|
|
|
|
inlineCallbacks(ic)
|
|
newFailure = self.failureResultOf(d)
|
|
tb = traceback.extract_tb(newFailure.getTracebackObject())
|
|
|
|
self.assertEqual(len(tb), 3)
|
|
self.assertIn("test_inlinecb", tb[2][0])
|
|
self.assertEqual("getDivisionFailure", tb[2][2])
|
|
self.assertEqual("raise ZeroDivisionError(msg)", tb[2][3])
|
|
|
|
self.assertIn("test_inlinecb", tb[0][0])
|
|
self.assertEqual("test_reraiseTracebacksFromDeferred", tb[0][2])
|
|
self.assertEqual("f.raiseException()", tb[0][3])
|
|
|
|
|
|
class UntranslatedError(Exception):
|
|
"""
|
|
Untranslated exception type when testing an exception translation.
|
|
"""
|
|
|
|
|
|
class TranslatedError(Exception):
|
|
"""
|
|
Translated exception type when testing an exception translation.
|
|
"""
|
|
|
|
|
|
class DontFail(Exception):
|
|
"""
|
|
Sample exception type.
|
|
"""
|
|
|
|
def __init__(self, actual):
|
|
Exception.__init__(self)
|
|
self.actualValue = actual
|
|
|
|
|
|
class CancellationTestsStackedType(Enum):
|
|
NOT_STACKED = 0
|
|
STACKED_INLINECB = 1
|
|
STACKED_CORO = 2
|
|
|
|
|
|
class CancellationTests(SynchronousTestCase):
|
|
"""
|
|
Tests for cancellation of L{Deferred}s returned by L{inlineCallbacks}.
|
|
For each of these tests, let:
|
|
- C{G} be a generator decorated with C{inlineCallbacks}
|
|
- C{D} be a L{Deferred} returned by C{G}
|
|
- C{C} be a L{Deferred} awaited by C{G} with C{yield}
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""
|
|
Set up the list of outstanding L{Deferred}s.
|
|
"""
|
|
self.deferredsOutstanding = []
|
|
|
|
def tearDown(self):
|
|
"""
|
|
If any L{Deferred}s are still outstanding, fire them.
|
|
"""
|
|
while self.deferredsOutstanding:
|
|
self.deferredGotten()
|
|
|
|
@inlineCallbacks
|
|
def stackedInlineCB(self, getChildDeferred):
|
|
x = yield getChildDeferred()
|
|
return x
|
|
|
|
async def stackedCoro(self, getChildDeferred):
|
|
return await getChildDeferred()
|
|
|
|
@inlineCallbacks
|
|
def sampleInlineCB(self, stackType, getChildDeferred=None, firstDeferred=None):
|
|
"""
|
|
Generator for testing cascade cancelling cases.
|
|
|
|
@param getChildDeferred: Some callable returning L{Deferred} that we
|
|
awaiting (with C{yield})
|
|
"""
|
|
if getChildDeferred is None:
|
|
getChildDeferred = self.getDeferred
|
|
try:
|
|
if stackType == CancellationTestsStackedType.NOT_STACKED:
|
|
x = yield getChildDeferred()
|
|
else:
|
|
if firstDeferred:
|
|
yield firstDeferred
|
|
if stackType == CancellationTestsStackedType.STACKED_INLINECB:
|
|
x = yield self.stackedCoro(getChildDeferred)
|
|
else:
|
|
x = yield self.stackedInlineCB(getChildDeferred)
|
|
except UntranslatedError:
|
|
raise TranslatedError()
|
|
except DontFail as df:
|
|
x = df.actualValue - 2
|
|
return x + 1
|
|
|
|
def getDeferred(self):
|
|
"""
|
|
A sample function that returns a L{Deferred} that can be fired on
|
|
demand, by L{CancellationTests.deferredGotten}.
|
|
|
|
@return: L{Deferred} that can be fired on demand.
|
|
"""
|
|
self.deferredsOutstanding.append(Deferred())
|
|
return self.deferredsOutstanding[-1]
|
|
|
|
def deferredGotten(self, result=None):
|
|
"""
|
|
Fire the L{Deferred} returned from the least-recent call to
|
|
L{CancellationTests.getDeferred}.
|
|
|
|
@param result: result object to be used when firing the L{Deferred}.
|
|
"""
|
|
self.deferredsOutstanding.pop(0).callback(result)
|
|
|
|
def doCascadeCancellingOnCancel(self, stackType, cancelOnSecondDeferred=False):
|
|
"""
|
|
When C{D} cancelled, C{C} will be immediately cancelled too.
|
|
|
|
@param stackType: defines test stacking scenario
|
|
|
|
@param cancelOnSecondDeferred: if True, tests cancellation on the
|
|
second yield in inlineCallbacks
|
|
"""
|
|
childResultHolder = ["FAILURE"]
|
|
|
|
def getChildDeferred():
|
|
d = Deferred()
|
|
|
|
def _eb(result):
|
|
childResultHolder[0] = result.check(CancelledError)
|
|
return result
|
|
|
|
d.addErrback(_eb)
|
|
return d
|
|
|
|
firstDeferred = None
|
|
if cancelOnSecondDeferred:
|
|
firstDeferred = Deferred()
|
|
d = self.sampleInlineCB(
|
|
getChildDeferred=getChildDeferred,
|
|
stackType=stackType,
|
|
firstDeferred=firstDeferred,
|
|
)
|
|
d.addErrback(lambda result: None)
|
|
if firstDeferred:
|
|
firstDeferred.callback(1)
|
|
d.cancel()
|
|
self.assertEqual(
|
|
childResultHolder[0],
|
|
CancelledError,
|
|
"no cascade cancelling occurs",
|
|
)
|
|
|
|
def test_CascadeCancellingOnCancelNotStacked(self):
|
|
self.doCascadeCancellingOnCancel(CancellationTestsStackedType.NOT_STACKED)
|
|
|
|
def test_CascadeCancellingOnCancelStackedInlineCb(self):
|
|
self.doCascadeCancellingOnCancel(CancellationTestsStackedType.STACKED_INLINECB)
|
|
|
|
def test_CascadeCancellingOnCancelStackedInlineCbOnSecondDeferred(self):
|
|
self.doCascadeCancellingOnCancel(
|
|
CancellationTestsStackedType.STACKED_INLINECB, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_CascadeCancellingOnCancelStackedCoro(self):
|
|
self.doCascadeCancellingOnCancel(CancellationTestsStackedType.STACKED_CORO)
|
|
|
|
def test_CascadeCancellingOnCancelStackedCoroOnSecondDeferred(self):
|
|
self.doCascadeCancellingOnCancel(
|
|
CancellationTestsStackedType.STACKED_CORO, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def doErrbackCancelledErrorOnCancel(self, stackType, cancelOnSecondDeferred=False):
|
|
"""
|
|
When C{D} cancelled, CancelledError from C{C} will be errbacked
|
|
through C{D}.
|
|
|
|
@param stackType: defines test stacking scenario
|
|
|
|
@param cancelOnSecondDeferred: if True, tests cancellation on the
|
|
second yield in inlineCallbacks
|
|
"""
|
|
|
|
firstDeferred = None
|
|
if cancelOnSecondDeferred:
|
|
firstDeferred = Deferred()
|
|
d = self.sampleInlineCB(stackType=stackType, firstDeferred=firstDeferred)
|
|
if firstDeferred:
|
|
firstDeferred.callback(1)
|
|
d.cancel()
|
|
self.assertRaises(CancelledError, self.failureResultOf(d).raiseException)
|
|
|
|
def test_ErrbackCancelledErrorOnCancel(self):
|
|
self.doErrbackCancelledErrorOnCancel(CancellationTestsStackedType.NOT_STACKED)
|
|
|
|
def test_ErrbackCancelledErrorOnCancelStackedInlineCb(self):
|
|
self.doErrbackCancelledErrorOnCancel(
|
|
CancellationTestsStackedType.STACKED_INLINECB
|
|
)
|
|
|
|
def test_ErrbackCancelledErrorOnCancelStackedInlineCbOnSecondDeferred(self):
|
|
self.doErrbackCancelledErrorOnCancel(
|
|
CancellationTestsStackedType.STACKED_INLINECB, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_ErrbackCancelledErrorOnCancelStackedCoro(self):
|
|
self.doErrbackCancelledErrorOnCancel(CancellationTestsStackedType.STACKED_CORO)
|
|
|
|
def test_ErrbackCancelledErrorOnCancelStackedCoroOnSecondDeferred(self):
|
|
self.doErrbackCancelledErrorOnCancel(
|
|
CancellationTestsStackedType.STACKED_CORO, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def doErrorToErrorTranslation(self, stackType, cancelOnSecondDeferred=False):
|
|
"""
|
|
When C{D} is cancelled, and C raises a particular type of error, C{G}
|
|
may catch that error at the point of yielding and translate it into
|
|
a different error which may be received by application code.
|
|
"""
|
|
|
|
def cancel(it):
|
|
it.errback(UntranslatedError())
|
|
|
|
firstDeferred = None
|
|
if cancelOnSecondDeferred:
|
|
firstDeferred = Deferred()
|
|
a = Deferred(cancel)
|
|
d = self.sampleInlineCB(
|
|
getChildDeferred=lambda: a, stackType=stackType, firstDeferred=firstDeferred
|
|
)
|
|
if firstDeferred:
|
|
firstDeferred.callback(1)
|
|
d.cancel()
|
|
self.assertRaises(TranslatedError, self.failureResultOf(d).raiseException)
|
|
|
|
def test_ErrorToErrorTranslation(self):
|
|
self.doErrorToErrorTranslation(CancellationTestsStackedType.NOT_STACKED)
|
|
|
|
def test_ErrorToErrorTranslationStackedInlineCb(self):
|
|
self.doErrorToErrorTranslation(CancellationTestsStackedType.STACKED_INLINECB)
|
|
|
|
def test_ErrorToErrorTranslationStackedInlineCbOnSecondDeferred(self):
|
|
self.doErrorToErrorTranslation(
|
|
CancellationTestsStackedType.STACKED_INLINECB, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_ErrorToErrorTranslationStackedCoro(self):
|
|
self.doErrorToErrorTranslation(CancellationTestsStackedType.STACKED_CORO)
|
|
|
|
def test_ErrorToErrorTranslationStackedCoroOnSecondDeferred(self):
|
|
self.doErrorToErrorTranslation(
|
|
CancellationTestsStackedType.STACKED_CORO, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def doErrorToSuccessTranslation(self, stackType, cancelOnSecondDeferred=False):
|
|
"""
|
|
When C{D} is cancelled, and C{C} raises a particular type of error,
|
|
C{G} may catch that error at the point of yielding and translate it
|
|
into a result value which may be received by application code.
|
|
"""
|
|
|
|
def cancel(it):
|
|
it.errback(DontFail(4321))
|
|
|
|
firstDeferred = None
|
|
if cancelOnSecondDeferred:
|
|
firstDeferred = Deferred()
|
|
a = Deferred(cancel)
|
|
d = self.sampleInlineCB(
|
|
getChildDeferred=lambda: a, stackType=stackType, firstDeferred=firstDeferred
|
|
)
|
|
results = []
|
|
d.addCallback(results.append)
|
|
if firstDeferred:
|
|
firstDeferred.callback(1)
|
|
d.cancel()
|
|
self.assertEquals(results, [4320])
|
|
|
|
def test_ErrorToSuccessTranslation(self):
|
|
self.doErrorToSuccessTranslation(CancellationTestsStackedType.NOT_STACKED)
|
|
|
|
def test_ErrorToSuccessTranslationStackedInlineCb(self):
|
|
self.doErrorToSuccessTranslation(CancellationTestsStackedType.STACKED_INLINECB)
|
|
|
|
def test_ErrorToSuccessTranslationStackedInlineCbOnSecondDeferred(self):
|
|
self.doErrorToSuccessTranslation(
|
|
CancellationTestsStackedType.STACKED_INLINECB, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_ErrorToSuccessTranslationStackedCoro(self):
|
|
self.doErrorToSuccessTranslation(CancellationTestsStackedType.STACKED_CORO)
|
|
|
|
def test_ErrorToSuccessTranslationStackedCoroOnSecondDeferred(self):
|
|
self.doErrorToSuccessTranslation(
|
|
CancellationTestsStackedType.STACKED_CORO, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def doAsynchronousCancellation(self, stackType, cancelOnSecondDeferred=False):
|
|
"""
|
|
When C{D} is cancelled, it won't reach the callbacks added to it by
|
|
application code until C{C} reaches the point in its callback chain
|
|
where C{G} awaits it. Otherwise, application code won't be able to
|
|
track resource usage that C{D} may be using.
|
|
"""
|
|
moreDeferred = Deferred()
|
|
|
|
def deferMeMore(result):
|
|
result.trap(CancelledError)
|
|
return moreDeferred
|
|
|
|
def deferMe():
|
|
d = Deferred()
|
|
d.addErrback(deferMeMore)
|
|
return d
|
|
|
|
firstDeferred = None
|
|
if cancelOnSecondDeferred:
|
|
firstDeferred = Deferred()
|
|
d = self.sampleInlineCB(
|
|
getChildDeferred=deferMe, stackType=stackType, firstDeferred=firstDeferred
|
|
)
|
|
if firstDeferred:
|
|
firstDeferred.callback(1)
|
|
d.cancel()
|
|
self.assertNoResult(d)
|
|
moreDeferred.callback(6543)
|
|
self.assertEqual(self.successResultOf(d), 6544)
|
|
|
|
def test_AsynchronousCancellation(self):
|
|
self.doAsynchronousCancellation(CancellationTestsStackedType.NOT_STACKED)
|
|
|
|
def test_AsynchronousCancellationStackedInlineCb(self):
|
|
self.doAsynchronousCancellation(CancellationTestsStackedType.STACKED_INLINECB)
|
|
|
|
def test_AsynchronousCancellationStackedInlineCbOnSecondDeferred(self):
|
|
self.doAsynchronousCancellation(
|
|
CancellationTestsStackedType.STACKED_INLINECB, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_AsynchronousCancellationStackedCoro(self):
|
|
self.doAsynchronousCancellation(CancellationTestsStackedType.STACKED_CORO)
|
|
|
|
def test_AsynchronousCancellationStackedCoroOnSecondDeferred(self):
|
|
self.doAsynchronousCancellation(
|
|
CancellationTestsStackedType.STACKED_CORO, cancelOnSecondDeferred=True
|
|
)
|
|
|
|
def test_inlineCallbacksCancelCaptured(self) -> None:
|
|
"""
|
|
Cancelling an L{defer.inlineCallbacks} correctly handles the function
|
|
catching the L{defer.CancelledError}.
|
|
|
|
The desired behavior is:
|
|
1. If the function is waiting on an inner deferred, that inner
|
|
deferred is cancelled, and a L{defer.CancelledError} is raised
|
|
within the function.
|
|
2. If the function catches that exception, execution continues, and
|
|
the deferred returned by the function is not resolved.
|
|
3. Cancelling the deferred again cancels any deferred the function
|
|
is waiting on, and the exception is raised.
|
|
"""
|
|
canceller1Calls: List[Deferred[object]] = []
|
|
canceller2Calls: List[Deferred[object]] = []
|
|
d1: Deferred[object] = Deferred(canceller1Calls.append)
|
|
d2: Deferred[object] = Deferred(canceller2Calls.append)
|
|
|
|
@inlineCallbacks
|
|
def testFunc() -> Generator[Deferred[object], object, None]:
|
|
try:
|
|
yield d1
|
|
except Exception:
|
|
pass
|
|
|
|
yield d2
|
|
|
|
# Call the function, and ensure that none of the deferreds have
|
|
# completed or been cancelled yet.
|
|
funcD = testFunc()
|
|
|
|
self.assertNoResult(d1)
|
|
self.assertNoResult(d2)
|
|
self.assertNoResult(funcD)
|
|
self.assertEqual(canceller1Calls, [])
|
|
self.assertEqual(canceller1Calls, [])
|
|
|
|
# Cancel the deferred returned by the function, and check that the first
|
|
# inner deferred has been cancelled, but the returned deferred has not
|
|
# completed (as the function catches the raised exception).
|
|
funcD.cancel()
|
|
|
|
self.assertEqual(canceller1Calls, [d1])
|
|
self.assertEqual(canceller2Calls, [])
|
|
self.assertNoResult(funcD)
|
|
|
|
# Cancel the returned deferred again, this time the returned deferred
|
|
# should have a failure result, as the function did not catch the cancel
|
|
# exception raised by `d2`.
|
|
funcD.cancel()
|
|
failure = self.failureResultOf(funcD)
|
|
self.assertEqual(failure.type, CancelledError)
|
|
self.assertEqual(canceller2Calls, [d2])
|