2019-05-29 09:33:59 +00:00
|
|
|
import asyncio
|
2022-03-14 11:54:13 +00:00
|
|
|
import contextvars
|
2019-05-29 09:33:59 +00:00
|
|
|
import inspect
|
2021-08-19 09:41:04 +00:00
|
|
|
import warnings
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
from .case import TestCase
|
|
|
|
|
|
|
|
|
|
|
|
class IsolatedAsyncioTestCase(TestCase):
|
|
|
|
# Names intentionally have a long prefix
|
|
|
|
# to reduce a chance of clashing with user-defined attributes
|
|
|
|
# from inherited test case
|
|
|
|
#
|
|
|
|
# The class doesn't call loop.run_until_complete(self.setUp()) and family
|
|
|
|
# but uses a different approach:
|
|
|
|
# 1. create a long-running task that reads self.setUp()
|
|
|
|
# awaitable from queue along with a future
|
|
|
|
# 2. await the awaitable object passing in and set the result
|
|
|
|
# into the future object
|
|
|
|
# 3. Outer code puts the awaitable and the future object into a queue
|
|
|
|
# with waiting for the future
|
|
|
|
# The trick is necessary because every run_until_complete() call
|
|
|
|
# creates a new task with embedded ContextVar context.
|
|
|
|
# To share contextvars between setUp(), test and tearDown() we need to execute
|
|
|
|
# them inside the same task.
|
|
|
|
|
|
|
|
# Note: the test case modifies event loop policy if the policy was not instantiated
|
|
|
|
# yet.
|
|
|
|
# asyncio.get_event_loop_policy() creates a default policy on demand but never
|
|
|
|
# returns None
|
|
|
|
# I believe this is not an issue in user level tests but python itself for testing
|
|
|
|
# should reset a policy in every test module
|
|
|
|
# by calling asyncio.set_event_loop_policy(None) in tearDownModule()
|
|
|
|
|
|
|
|
def __init__(self, methodName='runTest'):
|
|
|
|
super().__init__(methodName)
|
|
|
|
self._asyncioTestLoop = None
|
2022-03-14 11:54:13 +00:00
|
|
|
self._asyncioTestContext = contextvars.copy_context()
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
async def asyncSetUp(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
async def asyncTearDown(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def addAsyncCleanup(self, func, /, *args, **kwargs):
|
|
|
|
# A trivial trampoline to addCleanup()
|
|
|
|
# the function exists because it has a different semantics
|
|
|
|
# and signature:
|
|
|
|
# addCleanup() accepts regular functions
|
|
|
|
# but addAsyncCleanup() accepts coroutines
|
|
|
|
#
|
|
|
|
# We intentionally don't add inspect.iscoroutinefunction() check
|
|
|
|
# for func argument because there is no way
|
|
|
|
# to check for async function reliably:
|
2021-10-06 23:13:48 +00:00
|
|
|
# 1. It can be "async def func()" itself
|
2019-05-29 09:33:59 +00:00
|
|
|
# 2. Class can implement "async def __call__()" method
|
|
|
|
# 3. Regular "def func()" that returns awaitable object
|
|
|
|
self.addCleanup(*(func, *args), **kwargs)
|
|
|
|
|
|
|
|
def _callSetUp(self):
|
2022-03-14 11:54:13 +00:00
|
|
|
self._asyncioTestContext.run(self.setUp)
|
2019-05-29 09:33:59 +00:00
|
|
|
self._callAsync(self.asyncSetUp)
|
|
|
|
|
|
|
|
def _callTestMethod(self, method):
|
2021-08-19 09:41:04 +00:00
|
|
|
if self._callMaybeAsync(method) is not None:
|
|
|
|
warnings.warn(f'It is deprecated to return a value!=None from a '
|
2021-08-22 18:32:45 +00:00
|
|
|
f'test case ({method})', DeprecationWarning, stacklevel=4)
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
def _callTearDown(self):
|
|
|
|
self._callAsync(self.asyncTearDown)
|
2022-03-14 11:54:13 +00:00
|
|
|
self._asyncioTestContext.run(self.tearDown)
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
def _callCleanup(self, function, *args, **kwargs):
|
|
|
|
self._callMaybeAsync(function, *args, **kwargs)
|
|
|
|
|
|
|
|
def _callAsync(self, func, /, *args, **kwargs):
|
2021-09-22 15:43:23 +00:00
|
|
|
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
|
2022-03-14 11:54:13 +00:00
|
|
|
assert inspect.iscoroutinefunction(func), f'{func!r} is not an async function'
|
|
|
|
task = self._asyncioTestLoop.create_task(
|
|
|
|
func(*args, **kwargs),
|
|
|
|
context=self._asyncioTestContext,
|
|
|
|
)
|
|
|
|
return self._asyncioTestLoop.run_until_complete(task)
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
def _callMaybeAsync(self, func, /, *args, **kwargs):
|
2021-09-22 15:43:23 +00:00
|
|
|
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
|
2022-03-14 11:54:13 +00:00
|
|
|
if inspect.iscoroutinefunction(func):
|
|
|
|
task = self._asyncioTestLoop.create_task(
|
|
|
|
func(*args, **kwargs),
|
|
|
|
context=self._asyncioTestContext,
|
|
|
|
)
|
|
|
|
return self._asyncioTestLoop.run_until_complete(task)
|
2019-05-29 09:33:59 +00:00
|
|
|
else:
|
2022-03-14 11:54:13 +00:00
|
|
|
return self._asyncioTestContext.run(func, *args, **kwargs)
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
def _setupAsyncioLoop(self):
|
2021-09-22 15:43:23 +00:00
|
|
|
assert self._asyncioTestLoop is None, 'asyncio test loop already initialized'
|
2019-05-29 09:33:59 +00:00
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
loop.set_debug(True)
|
|
|
|
self._asyncioTestLoop = loop
|
|
|
|
|
|
|
|
def _tearDownAsyncioLoop(self):
|
2021-09-22 15:43:23 +00:00
|
|
|
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
|
2019-05-29 09:33:59 +00:00
|
|
|
loop = self._asyncioTestLoop
|
|
|
|
self._asyncioTestLoop = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
# cancel all tasks
|
|
|
|
to_cancel = asyncio.all_tasks(loop)
|
|
|
|
if not to_cancel:
|
|
|
|
return
|
|
|
|
|
|
|
|
for task in to_cancel:
|
|
|
|
task.cancel()
|
|
|
|
|
|
|
|
loop.run_until_complete(
|
2021-08-16 08:21:08 +00:00
|
|
|
asyncio.gather(*to_cancel, return_exceptions=True))
|
2019-05-29 09:33:59 +00:00
|
|
|
|
|
|
|
for task in to_cancel:
|
|
|
|
if task.cancelled():
|
|
|
|
continue
|
|
|
|
if task.exception() is not None:
|
|
|
|
loop.call_exception_handler({
|
|
|
|
'message': 'unhandled exception during test shutdown',
|
|
|
|
'exception': task.exception(),
|
|
|
|
'task': task,
|
|
|
|
})
|
|
|
|
# shutdown asyncgens
|
|
|
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
|
|
finally:
|
|
|
|
asyncio.set_event_loop(None)
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
def run(self, result=None):
|
|
|
|
self._setupAsyncioLoop()
|
|
|
|
try:
|
|
|
|
return super().run(result)
|
|
|
|
finally:
|
|
|
|
self._tearDownAsyncioLoop()
|
2021-09-22 15:43:23 +00:00
|
|
|
|
|
|
|
def debug(self):
|
|
|
|
self._setupAsyncioLoop()
|
|
|
|
super().debug()
|
|
|
|
self._tearDownAsyncioLoop()
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
if self._asyncioTestLoop is not None:
|
|
|
|
self._tearDownAsyncioLoop()
|