Skip to content

Commit

Permalink
#11694 Avoid asyncioreactor state leak between tests (#11695)
Browse files Browse the repository at this point in the history
Give each AsyncioSelectorReactor its own event loop when running the tests.
  • Loading branch information
exarkun committed Sep 28, 2022
2 parents 1773b5d + d8fd8c1 commit d66a35a
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 17 deletions.
37 changes: 35 additions & 2 deletions src/twisted/internet/test/reactormixins.py
Expand Up @@ -18,7 +18,7 @@
import os
import signal
import time
from typing import Dict, Optional, Sequence, Type, Union
from typing import TYPE_CHECKING, Dict, Optional, Sequence, Type, Union

from zope.interface import Interface

Expand All @@ -30,6 +30,11 @@
from twisted.trial.unittest import SkipTest, SynchronousTestCase
from twisted.trial.util import DEFAULT_TIMEOUT_DURATION, acquireAttribute

if TYPE_CHECKING:
# Only bring in this name to support the type annotation below. We don't
# really want to import a reactor module this early at runtime.
from twisted.internet import asyncioreactor

# Access private APIs.
try:
from twisted.internet import process as _process
Expand Down Expand Up @@ -153,7 +158,7 @@ class ReactorBuilder:
]
)

_reactors.append("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
_reactors.append("twisted.internet.test.reactormixins.AsyncioSelectorReactor")

if platform.isMacOSX():
_reactors.append("twisted.internet.cfreactor.CFReactor")
Expand Down Expand Up @@ -366,3 +371,31 @@ class testcase(cls, SynchronousTestCase): # type: ignore[valid-type,misc]
testcase.__qualname__ = ".".join(cls.__qualname__.split()[0:-1] + [name])
classes[testcase.__name__] = testcase
return classes


def asyncioSelectorReactor(self: object) -> "asyncioreactor.AsyncioSelectorReactor":
"""
Make a new asyncio reactor associated with a new event loop.
The test suite prefers this constructor because having a new event loop
for each reactor provides better test isolation. The real constructor
prefers to re-use (or create) a global loop because of how this interacts
with other asyncio-based libraries and applications (though maybe it
shouldn't).
@param self: The L{ReactorBuilder} subclass this is being called on. We
don't use this parameter but we get called with it anyway.
"""
from asyncio import new_event_loop, set_event_loop

from twisted.internet import asyncioreactor

loop = new_event_loop()
set_event_loop(loop)

return asyncioreactor.AsyncioSelectorReactor(loop)


# Give it an alias that makes the names of the generated test classes fit the
# pattern.
AsyncioSelectorReactor = asyncioSelectorReactor
16 changes: 2 additions & 14 deletions src/twisted/internet/test/test_threads.py
Expand Up @@ -172,20 +172,8 @@ def test_cleanUpThreadPoolEvenBeforeReactorIsRun(self):
reactor = self.buildReactor()
threadPoolRef = ref(reactor.getThreadPool())
reactor.fireSystemEvent("shutdown")

if reactor.__class__.__name__ == "AsyncioSelectorReactor":
self.assertIsNone(reactor.threadpool)
# ReactorBase.__init__ sets self.crash as a 'shutdown'
# event, which in turn calls stop on the underlying
# asyncio event loop, which in turn sets a _stopping
# attribute on it that's only unset after an iteration of
# the loop. Subsequent tests can only reuse the asyncio
# loop if it's allowed to run and unset that _stopping
# attribute.
self.runReactor(reactor)
else:
gc.collect()
self.assertIsNone(threadPoolRef())
gc.collect()
self.assertIsNone(threadPoolRef())

def test_isInIOThread(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/twisted/internet/test/test_time.py
Expand Up @@ -43,7 +43,7 @@ def eventSource(reactor, event):

else:
raise SkipTest(
"Do not know how to synthesize non-time event to " "stop the test"
"Do not know how to synthesize non-time event to stop the test"
)

# Pick a pretty big delay.
Expand Down
Empty file.

0 comments on commit d66a35a

Please sign in to comment.