Skip to content

Commit

Permalink
defer: Move more inlineCallbacks tests to correct module
Browse files Browse the repository at this point in the history
  • Loading branch information
p12tic committed May 6, 2024
1 parent 243eaa3 commit 9cba611
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 92 deletions.
96 changes: 95 additions & 1 deletion src/twisted/internet/test/test_inlinecb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"""

import traceback
from typing import Any, Generator, Union
import unittest as pyunit
import weakref
from typing import Any, Generator, List, Set, Union

from twisted.internet import reactor, task
from twisted.internet.defer import (
Expand All @@ -19,6 +21,7 @@
returnValue,
succeed,
)
from twisted.python.compat import _PYPY
from twisted.trial.unittest import SynchronousTestCase, TestCase


Expand Down Expand Up @@ -289,6 +292,42 @@ def _raises():
# 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):
"""
Expand Down Expand Up @@ -1174,3 +1213,58 @@ def test_AsynchronousCancellationStacked(self):

def test_AsynchronousCancellationStackedOnSecondDeferred(self):
self.doAsynchronousCancellation(stacked=True, 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])
91 changes: 0 additions & 91 deletions src/twisted/test/test_defer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3982,97 +3982,6 @@ def test_fromCoroutineRequiresCoroutine(self) -> None:
for thing in thingsThatAreNotCoroutines:
self.assertRaises(defer.NotACoroutineError, Deferred.fromCoroutine, thing)

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)

@defer.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, defer.CancelledError)
self.assertEqual(canceller2Calls, [d2])

@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)

@defer.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())

@pyunit.skipIf(_PYPY, "GC works differently on PyPy.")
def test_coroutineNoCircularReference(self) -> None:
"""
Expand Down

0 comments on commit 9cba611

Please sign in to comment.