From b77e62d63bc19b0532a2ce57785d97ce078d7498 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 6 May 2019 20:44:23 -0400 Subject: [PATCH 01/14] Test on --- Lib/unittest/__init__.py | 3 +- Lib/unittest/async_case.py | 118 +++++++++++++++++++++++++++ Lib/unittest/case.py | 15 +++- Lib/unittest/test/test_async_case.py | 13 +++ 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 Lib/unittest/async_case.py create mode 100644 Lib/unittest/test/test_async_case.py diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py index 5ff1bf37b16965..0c2ac0d911401f 100644 --- a/Lib/unittest/__init__.py +++ b/Lib/unittest/__init__.py @@ -44,7 +44,7 @@ def testMultiply(self): SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ -__all__ = ['TestResult', 'TestCase', 'TestSuite', +__all__ = '[TestResult', 'TestCase', 'AsyncioTestCase', 'TestSuite', 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'expectedFailure', 'TextTestResult', 'installHandler', @@ -57,6 +57,7 @@ def testMultiply(self): __unittest = True from .result import TestResult +from .async_case import AsyncioTestCase from .case import (addModuleCleanup, TestCase, FunctionTestCase, SkipTest, skip, skipIf, skipUnless, expectedFailure) from .suite import BaseTestSuite, TestSuite diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py new file mode 100644 index 00000000000000..2dd51edc8084e0 --- /dev/null +++ b/Lib/unittest/async_case.py @@ -0,0 +1,118 @@ +import asyncio +import inspect + +from .case import TestCase + + + +class AsyncioTestCase(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. + + def __init__(self, methodName='runTest'): + super().__init__(methodName) + self._asyncioTestLoop = None + self._asyncioCallsQueue = None + + def _callSetUp(self): + self._callMaybeAsync(self.setUp) + + def _callTearDown(self): + self._callMaybeAsync(self.tearDown) + + def _callCleanup(self, function, *args, **kwargs): + self._callMaybeAsync(function, *args, **kwargs) + + def _callMaybeAsync(self, func, *args, **kwargs): + assert self._asyncioTestLoop is not None + ret = func(*args, **kwargs) + if inspect.isawaitable(ret): + fut = self._asyncioTestLoop.create_future() + self._asyncioCallsQueue.put_nowait(fut, ret) + return self._asyncioTestLoop.run_until_complete(fut) + else: + return ret + + async def _asyncioLoopRunner(self): + queue = self._asyncioCallsQueue + while True: + query = await queue.get() + queue.task_done() + if query is None: + return + fut, awaitable = query + try: + ret = await awaitable + if not fut.cancelled(): + fut.set_result(ret) + except asyncio.CancelledError: + raise + except Exception as ex: + if not fut.cancelled(): + fut.set_exception(ex) + + def _setupAsyncioLoop(self): + if self._asyncioTestLoop is not None: + return + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + self._asyncioTestLoop = loop + self._asyncioCallsQueue = asyncio.Queue(loop=loop) + self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner()) + + def _tearDownAsyncioLoop(self): + if self._asyncioTestLoop is None: + return + loop = self._asyncioTestLoop + self._asyncioTestLoop = None + self._asyncioCallsQueue.put_nowait(None) + loop.run_until_complete(self._asyncioCallsQueue.join()) + + 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( + asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)) + + 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() diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 8ff2546fc207cc..1e104e2a5fb76f 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -642,6 +642,12 @@ def _addUnexpectedSuccess(self, result): else: addUnexpectedSuccess(self) + def _callSetUp(self): + self.setUp() + + def _callTearDown(self): + self.tearDown() + def run(self, result=None): orig_result = result if result is None: @@ -673,14 +679,14 @@ def run(self, result=None): self._outcome = outcome with outcome.testPartExecutor(self): - self.setUp() + self._callSetUp() if outcome.success: outcome.expecting_failure = expecting_failure with outcome.testPartExecutor(self, isTest=True): testMethod() outcome.expecting_failure = False with outcome.testPartExecutor(self): - self.tearDown() + self._callTearDown() self.doCleanups() for test, reason in outcome.skipped: @@ -711,6 +717,9 @@ def run(self, result=None): # clear the outcome, no more needed self._outcome = None + def _callCleanup(self, function, *args, **kwargs): + function(*args, **kwargs) + def doCleanups(self): """Execute all cleanup functions. Normally called for you after tearDown.""" @@ -718,7 +727,7 @@ def doCleanups(self): while self._cleanups: function, args, kwargs = self._cleanups.pop() with outcome.testPartExecutor(self): - function(*args, **kwargs) + self._callCleanup(function, *args, **kwargs) # return this for backwards compatibility # even though we no longer use it internally diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py new file mode 100644 index 00000000000000..06e8f2f536787e --- /dev/null +++ b/Lib/unittest/test/test_async_case.py @@ -0,0 +1,13 @@ +from unittest import AsyncioTestCase + + +class TestAsyncCase(AsyncioTestCase): + async def setUp(self): + self._setup_called = 1 + + async def test_func(self): + assert self._setup_called == 1 + self._setup_called = 2 + + async def tearDown(self): + assert self._setup_called == 2 From de89fda86528693fef9b941db30573d67b7b4849 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 7 May 2019 15:54:38 -0400 Subject: [PATCH 02/14] Work on --- Lib/unittest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py index 0c2ac0d911401f..0727e54609461b 100644 --- a/Lib/unittest/__init__.py +++ b/Lib/unittest/__init__.py @@ -44,7 +44,7 @@ def testMultiply(self): SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ -__all__ = '[TestResult', 'TestCase', 'AsyncioTestCase', 'TestSuite', +__all__ = ['TestResult', 'TestCase', 'AsyncioTestCase', 'TestSuite', 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'expectedFailure', 'TextTestResult', 'installHandler', From 9eef7236cafc33a2a809c928c4c0493c45b4da92 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 11:57:09 +0300 Subject: [PATCH 03/14] Fix test method call --- Lib/unittest/async_case.py | 3 +++ Lib/unittest/case.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 2dd51edc8084e0..85842c5da9b6d0 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -31,6 +31,9 @@ def __init__(self, methodName='runTest'): def _callSetUp(self): self._callMaybeAsync(self.setUp) + def _callTestMethod(self, method): + self._callMaybeAsync(method) + def _callTearDown(self): self._callMaybeAsync(self.tearDown) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 5845b1f021130f..f569946acac457 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -648,6 +648,9 @@ def _addUnexpectedSuccess(self, result): def _callSetUp(self): self.setUp() + def _callTestMethod(self, method): + method() + def _callTearDown(self): self.tearDown() @@ -686,7 +689,7 @@ def run(self, result=None): if outcome.success: outcome.expecting_failure = expecting_failure with outcome.testPartExecutor(self, isTest=True): - testMethod() + self._callTestMethod(testMethod) outcome.expecting_failure = False with outcome.testPartExecutor(self): self._callTearDown() From 43091a015a970b64927d213663e8d326333a9ca8 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 12:17:46 +0300 Subject: [PATCH 04/14] Code cleanup --- Lib/unittest/case.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index f569946acac457..ed09923b6c1350 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -654,6 +654,9 @@ def _callTestMethod(self, method): def _callTearDown(self): self.tearDown() + def _callCleanup(self, function, *args, **kwargs): + function(*args, **kwargs) + def run(self, result=None): orig_result = result if result is None: @@ -723,9 +726,6 @@ def run(self, result=None): # clear the outcome, no more needed self._outcome = None - def _callCleanup(self, function, *args, **kwargs): - function(*args, **kwargs) - def doCleanups(self): """Execute all cleanup functions. Normally called for you after tearDown.""" From d8ad4d7e38be97131ea484878db93be043f2346d Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 12:33:15 +0300 Subject: [PATCH 05/14] Fix test by using positional-arguments only --- Lib/unittest/case.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index ed09923b6c1350..7b1e86941315e8 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -654,7 +654,7 @@ def _callTestMethod(self, method): def _callTearDown(self): self.tearDown() - def _callCleanup(self, function, *args, **kwargs): + def _callCleanup(self, function, /, *args, **kwargs): function(*args, **kwargs) def run(self, result=None): From 85df7070af27fbdc7b1009482b337424e20146b6 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 13:26:23 +0300 Subject: [PATCH 06/14] Make basic test passing --- Lib/unittest/async_case.py | 182 ++++++++++++++------------- Lib/unittest/test/test_async_case.py | 40 ++++-- 2 files changed, 125 insertions(+), 97 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 85842c5da9b6d0..a84e6187642805 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -23,99 +23,105 @@ class AsyncioTestCase(TestCase): # 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 self._asyncioCallsQueue = None - def _callSetUp(self): - self._callMaybeAsync(self.setUp) - - def _callTestMethod(self, method): - self._callMaybeAsync(method) - - def _callTearDown(self): - self._callMaybeAsync(self.tearDown) - - def _callCleanup(self, function, *args, **kwargs): - self._callMaybeAsync(function, *args, **kwargs) - - def _callMaybeAsync(self, func, *args, **kwargs): - assert self._asyncioTestLoop is not None - ret = func(*args, **kwargs) - if inspect.isawaitable(ret): - fut = self._asyncioTestLoop.create_future() - self._asyncioCallsQueue.put_nowait(fut, ret) - return self._asyncioTestLoop.run_until_complete(fut) - else: - return ret - - async def _asyncioLoopRunner(self): - queue = self._asyncioCallsQueue - while True: - query = await queue.get() - queue.task_done() - if query is None: - return - fut, awaitable = query - try: - ret = await awaitable - if not fut.cancelled(): - fut.set_result(ret) - except asyncio.CancelledError: - raise - except Exception as ex: - if not fut.cancelled(): - fut.set_exception(ex) - - def _setupAsyncioLoop(self): - if self._asyncioTestLoop is not None: + def _callSetUp(self): + self._callMaybeAsync(self.setUp) + + def _callTestMethod(self, method): + self._callMaybeAsync(method) + + def _callTearDown(self): + self._callMaybeAsync(self.tearDown) + + def _callCleanup(self, function, *args, **kwargs): + self._callMaybeAsync(function, *args, **kwargs) + + def _callMaybeAsync(self, func, /, *args, **kwargs): + assert self._asyncioTestLoop is not None + ret = func(*args, **kwargs) + if inspect.isawaitable(ret): + fut = self._asyncioTestLoop.create_future() + self._asyncioCallsQueue.put_nowait((fut, ret)) + return self._asyncioTestLoop.run_until_complete(fut) + else: + return ret + + async def _asyncioLoopRunner(self): + queue = self._asyncioCallsQueue + while True: + query = await queue.get() + queue.task_done() + if query is None: return - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.set_debug(True) - self._asyncioTestLoop = loop - self._asyncioCallsQueue = asyncio.Queue(loop=loop) - self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner()) - - def _tearDownAsyncioLoop(self): - if self._asyncioTestLoop is None: + fut, awaitable = query + try: + ret = await awaitable + if not fut.cancelled(): + fut.set_result(ret) + except asyncio.CancelledError: + raise + except Exception as ex: + if not fut.cancelled(): + fut.set_exception(ex) + + def _setupAsyncioLoop(self): + assert self._asyncioTestLoop is None + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + self._asyncioTestLoop = loop + self._asyncioCallsQueue = asyncio.Queue(loop=loop) + self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner()) + + def _tearDownAsyncioLoop(self): + assert self._asyncioTestLoop is not None + loop = self._asyncioTestLoop + self._asyncioTestLoop = None + self._asyncioCallsQueue.put_nowait(None) + loop.run_until_complete(self._asyncioCallsQueue.join()) + + try: + # cancel all tasks + to_cancel = asyncio.all_tasks(loop) + if not to_cancel: return - loop = self._asyncioTestLoop - self._asyncioTestLoop = None - self._asyncioCallsQueue.put_nowait(None) - loop.run_until_complete(self._asyncioCallsQueue.join()) - 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( - asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)) - - 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() + for task in to_cancel: + task.cancel() + + loop.run_until_complete( + asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)) + + 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() diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 06e8f2f536787e..4df444196c845b 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -1,13 +1,35 @@ -from unittest import AsyncioTestCase +import asyncio +import unittest -class TestAsyncCase(AsyncioTestCase): - async def setUp(self): - self._setup_called = 1 +def tearDownModule(): + asyncio.set_event_loop_policy(None) - async def test_func(self): - assert self._setup_called == 1 - self._setup_called = 2 - async def tearDown(self): - assert self._setup_called == 2 +class TestAsyncCase(unittest.TestCase): + def test_basic(self): + calls = 0 + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + nonlocal calls + self.assertEqual(calls, 0) + calls = 1 + + async def test_func(self): + nonlocal calls + self.assertEqual(calls, 1) + calls = 2 + + async def tearDown(self): + nonlocal calls + self.assertEqual(calls, 2) + calls = 3 + + test = Test("test_func") + test.run() + self.assertEqual(calls, 3) + + +if __name__ == "__main__": + unittest.main() From 78d885f71d5752051b1e18fd75fb8ad1ed9fd3c2 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 13:27:58 +0300 Subject: [PATCH 07/14] Test addCleanup --- Lib/unittest/test/test_async_case.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 4df444196c845b..ef4f1247b2fe37 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -20,15 +20,21 @@ async def test_func(self): nonlocal calls self.assertEqual(calls, 1) calls = 2 + self.addCleanup(self.on_cleanup) async def tearDown(self): nonlocal calls self.assertEqual(calls, 2) calls = 3 + async def on_cleanup(self): + nonlocal calls + self.assertEqual(calls, 3) + calls = 4 + test = Test("test_func") test.run() - self.assertEqual(calls, 3) + self.assertEqual(calls, 4) if __name__ == "__main__": From 58cb1c86aba8ab299daa338ca10c7939a3990397 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 13:45:15 +0300 Subject: [PATCH 08/14] Make test_support happy --- Lib/test/test_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index cb664bab17109d..8f0746aed8299a 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -403,7 +403,7 @@ def test_check__all__(self): ("unittest.result", "unittest.case", "unittest.suite", "unittest.loader", "unittest.main", "unittest.runner", - "unittest.signals"), + "unittest.signals", "unittest.async_case"), extra=extra, blacklist=blacklist) From 085304384ac97dac5935c144045303503929b18d Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 16 May 2019 16:22:46 +0300 Subject: [PATCH 09/14] More tests --- Lib/unittest/test/test_async_case.py | 138 ++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 14 deletions(-) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index ef4f1247b2fe37..943090cac02d4b 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -8,33 +8,143 @@ def tearDownModule(): class TestAsyncCase(unittest.TestCase): def test_basic(self): - calls = 0 + events = [] class Test(unittest.AsyncioTestCase): async def setUp(self): - nonlocal calls - self.assertEqual(calls, 0) - calls = 1 + self.assertEqual(events, []) + events.append('setUp') async def test_func(self): - nonlocal calls - self.assertEqual(calls, 1) - calls = 2 + self.assertEqual(events, ['setUp']) + events.append('test') self.addCleanup(self.on_cleanup) async def tearDown(self): - nonlocal calls - self.assertEqual(calls, 2) - calls = 3 + self.assertEqual(events, ['setUp', 'test']) + events.append('tearDown') async def on_cleanup(self): - nonlocal calls - self.assertEqual(calls, 3) - calls = 4 + self.assertEqual(events, ['setUp', 'test', 'tearDown']) + events.append('cleanup') test = Test("test_func") test.run() - self.assertEqual(calls, 4) + self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + + + def test_exception_in_setup(self): + events = [] + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + events.append('setUp') + raise Exception() + + async def test_func(self): + events.append('test') + self.addCleanup(self.on_cleanup) + + async def tearDown(self): + events.append('tearDown') + + async def on_cleanup(self): + events.append('cleanup') + + + test = Test("test_func") + test.run() + self.assertEqual(events, ['setUp']) + + + def test_exception_in_test(self): + events = [] + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + events.append('setUp') + + async def test_func(self): + events.append('test') + raise Exception() + self.addCleanup(self.on_cleanup) + + async def tearDown(self): + events.append('tearDown') + + async def on_cleanup(self): + events.append('cleanup') + + test = Test("test_func") + test.run() + self.assertEqual(events, ['setUp', 'test', 'tearDown']) + + def test_exception_in_test_after_adding_cleanup(self): + events = [] + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + events.append('setUp') + + async def test_func(self): + events.append('test') + self.addCleanup(self.on_cleanup) + raise Exception() + + async def tearDown(self): + events.append('tearDown') + + async def on_cleanup(self): + events.append('cleanup') + + test = Test("test_func") + test.run() + self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + + def test_exception_in_tear_down(self): + events = [] + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + events.append('setUp') + + async def test_func(self): + events.append('test') + self.addCleanup(self.on_cleanup) + + async def tearDown(self): + events.append('tearDown') + raise Exception() + + async def on_cleanup(self): + events.append('cleanup') + + test = Test("test_func") + test.run() + self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + + + def test_exception_in_tear_clean_up(self): + events = [] + + class Test(unittest.AsyncioTestCase): + async def setUp(self): + events.append('setUp') + + async def test_func(self): + events.append('test') + self.addCleanup(self.on_cleanup) + + async def tearDown(self): + events.append('tearDown') + + async def on_cleanup(self): + events.append('cleanup') + raise Exception() + + test = Test("test_func") + test.run() + self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) if __name__ == "__main__": From 03480c42788baafa47720316e661a33038dccf03 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 17 May 2019 15:00:39 +0300 Subject: [PATCH 10/14] Prefix async setup/teardown methods --- Lib/unittest/async_case.py | 20 ++++++++- Lib/unittest/test/test_async_case.py | 66 ++++++++++++++-------------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index a84e6187642805..e84ca781ff8c49 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -36,18 +36,34 @@ def __init__(self, methodName='runTest'): self._asyncioTestLoop = None self._asyncioCallsQueue = None + async def asyncSetUp(self): + pass + + async def asyncTearDown(self): + pass + def _callSetUp(self): - self._callMaybeAsync(self.setUp) + self.setUp() + self._callAsync(self.asyncSetUp) def _callTestMethod(self, method): self._callMaybeAsync(method) def _callTearDown(self): - self._callMaybeAsync(self.tearDown) + self._callAsync(self.asyncTearDown) + self.tearDown() def _callCleanup(self, function, *args, **kwargs): self._callMaybeAsync(function, *args, **kwargs) + def _callAsync(self, func, /, *args, **kwargs): + assert self._asyncioTestLoop is not None + ret = func(*args, **kwargs) + assert inspect.isawaitable(ret) + fut = self._asyncioTestLoop.create_future() + self._asyncioCallsQueue.put_nowait((fut, ret)) + return self._asyncioTestLoop.run_until_complete(fut) + def _callMaybeAsync(self, func, /, *args, **kwargs): assert self._asyncioTestLoop is not None ret = func(*args, **kwargs) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 943090cac02d4b..1bb6de6025c1e2 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -11,42 +11,42 @@ def test_basic(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): + async def asyncSetUp(self): self.assertEqual(events, []) - events.append('setUp') + events.append('asyncSetUp') async def test_func(self): - self.assertEqual(events, ['setUp']) + self.assertEqual(events, ['asyncSetUp']) events.append('test') self.addCleanup(self.on_cleanup) - async def tearDown(self): - self.assertEqual(events, ['setUp', 'test']) - events.append('tearDown') + async def asyncTearDown(self): + self.assertEqual(events, ['asyncSetUp', 'test']) + events.append('asyncTearDown') async def on_cleanup(self): - self.assertEqual(events, ['setUp', 'test', 'tearDown']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) events.append('cleanup') test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) def test_exception_in_setup(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): - events.append('setUp') + async def asyncSetUp(self): + events.append('asyncSetUp') raise Exception() async def test_func(self): events.append('test') self.addCleanup(self.on_cleanup) - async def tearDown(self): - events.append('tearDown') + async def asyncTearDown(self): + events.append('asyncTearDown') async def on_cleanup(self): events.append('cleanup') @@ -54,66 +54,66 @@ async def on_cleanup(self): test = Test("test_func") test.run() - self.assertEqual(events, ['setUp']) + self.assertEqual(events, ['asyncSetUp']) def test_exception_in_test(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): - events.append('setUp') + async def asyncSetUp(self): + events.append('asyncSetUp') async def test_func(self): events.append('test') raise Exception() self.addCleanup(self.on_cleanup) - async def tearDown(self): - events.append('tearDown') + async def asyncTearDown(self): + events.append('asyncTearDown') async def on_cleanup(self): events.append('cleanup') test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', 'test', 'tearDown']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) def test_exception_in_test_after_adding_cleanup(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): - events.append('setUp') + async def asyncSetUp(self): + events.append('asyncSetUp') async def test_func(self): events.append('test') self.addCleanup(self.on_cleanup) raise Exception() - async def tearDown(self): - events.append('tearDown') + async def asyncTearDown(self): + events.append('asyncTearDown') async def on_cleanup(self): events.append('cleanup') test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) def test_exception_in_tear_down(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): - events.append('setUp') + async def asyncSetUp(self): + events.append('asyncSetUp') async def test_func(self): events.append('test') self.addCleanup(self.on_cleanup) - async def tearDown(self): - events.append('tearDown') + async def asyncTearDown(self): + events.append('asyncTearDown') raise Exception() async def on_cleanup(self): @@ -121,22 +121,22 @@ async def on_cleanup(self): test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) def test_exception_in_tear_clean_up(self): events = [] class Test(unittest.AsyncioTestCase): - async def setUp(self): - events.append('setUp') + async def asyncSetUp(self): + events.append('asyncSetUp') async def test_func(self): events.append('test') self.addCleanup(self.on_cleanup) - async def tearDown(self): - events.append('tearDown') + async def asyncTearDown(self): + events.append('asyncTearDown') async def on_cleanup(self): events.append('cleanup') @@ -144,7 +144,7 @@ async def on_cleanup(self): test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', 'test', 'tearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) if __name__ == "__main__": From 8b99f79073cd33ba1bed37417c087a6f16e3b472 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 20 May 2019 14:45:21 +0300 Subject: [PATCH 11/14] Check for setUp/tearDown calls explicitly --- Lib/unittest/test/test_async_case.py | 35 +++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 1bb6de6025c1e2..38b4e57b5ed39b 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -7,30 +7,53 @@ def tearDownModule(): class TestAsyncCase(unittest.TestCase): - def test_basic(self): + def test_full_cycle(self): events = [] class Test(unittest.AsyncioTestCase): - async def asyncSetUp(self): + def setUp(self): self.assertEqual(events, []) + events.append('setUp') + + async def asyncSetUp(self): + self.assertEqual(events, ['setUp']) events.append('asyncSetUp') async def test_func(self): - self.assertEqual(events, ['asyncSetUp']) + self.assertEqual(events, ['setUp', + 'asyncSetUp']) events.append('test') self.addCleanup(self.on_cleanup) async def asyncTearDown(self): - self.assertEqual(events, ['asyncSetUp', 'test']) + self.assertEqual(events, ['setUp', + 'asyncSetUp', + 'test']) events.append('asyncTearDown') + def tearDown(self): + self.assertEqual(events, ['setUp', + 'asyncSetUp', + 'test', + 'asyncTearDown']) + events.append('tearDown') + async def on_cleanup(self): - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) + self.assertEqual(events, ['setUp', + 'asyncSetUp', + 'test', + 'asyncTearDown', + 'tearDown']) events.append('cleanup') test = Test("test_func") test.run() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertEqual(events, ['setUp', + 'asyncSetUp', + 'test', + 'asyncTearDown', + 'tearDown', + 'cleanup']) def test_exception_in_setup(self): From 80e1898865472fe7666abf56ea575cfee5817ad2 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 20 May 2019 14:48:12 +0300 Subject: [PATCH 12/14] Add NEWS --- .../NEWS.d/next/Library/2019-05-20-14-47-55.bpo-32972.LoeUNh.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-20-14-47-55.bpo-32972.LoeUNh.rst diff --git a/Misc/NEWS.d/next/Library/2019-05-20-14-47-55.bpo-32972.LoeUNh.rst b/Misc/NEWS.d/next/Library/2019-05-20-14-47-55.bpo-32972.LoeUNh.rst new file mode 100644 index 00000000000000..c8c47cd7763554 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-20-14-47-55.bpo-32972.LoeUNh.rst @@ -0,0 +1 @@ +Implement ``unittest.AsyncTestCase`` to help testing asyncio-based code. From 7cedbf0c3a35c195e5171e41c1ccc38f6bf3e79e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 21 May 2019 21:36:58 +0300 Subject: [PATCH 13/14] Rename --- Lib/unittest/__init__.py | 4 ++-- Lib/unittest/async_case.py | 2 +- Lib/unittest/test/test_async_case.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py index 0727e54609461b..ace3a6fb1dd971 100644 --- a/Lib/unittest/__init__.py +++ b/Lib/unittest/__init__.py @@ -44,7 +44,7 @@ def testMultiply(self): SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ -__all__ = ['TestResult', 'TestCase', 'AsyncioTestCase', 'TestSuite', +__all__ = ['TestResult', 'TestCase', 'IsolatedAsyncioTestCase', 'TestSuite', 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'expectedFailure', 'TextTestResult', 'installHandler', @@ -57,7 +57,7 @@ def testMultiply(self): __unittest = True from .result import TestResult -from .async_case import AsyncioTestCase +from .async_case import IsolatedAsyncioTestCase from .case import (addModuleCleanup, TestCase, FunctionTestCase, SkipTest, skip, skipIf, skipUnless, expectedFailure) from .suite import BaseTestSuite, TestSuite diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index e84ca781ff8c49..0f77396b003677 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -5,7 +5,7 @@ -class AsyncioTestCase(TestCase): +class IsolatedAsyncioTestCase(TestCase): # Names intentionally have a long prefix # to reduce a chance of clashing with user-defined attributes # from inherited test case diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 38b4e57b5ed39b..811d3410752d6c 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -10,7 +10,7 @@ class TestAsyncCase(unittest.TestCase): def test_full_cycle(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): def setUp(self): self.assertEqual(events, []) events.append('setUp') @@ -59,7 +59,7 @@ async def on_cleanup(self): def test_exception_in_setup(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') raise Exception() @@ -83,7 +83,7 @@ async def on_cleanup(self): def test_exception_in_test(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -105,7 +105,7 @@ async def on_cleanup(self): def test_exception_in_test_after_adding_cleanup(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -127,7 +127,7 @@ async def on_cleanup(self): def test_exception_in_tear_down(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -150,7 +150,7 @@ async def on_cleanup(self): def test_exception_in_tear_clean_up(self): events = [] - class Test(unittest.AsyncioTestCase): + class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') From c1ac3b5ce32b00517c3e810f0e28675f538b2f8e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 28 May 2019 21:49:42 +0300 Subject: [PATCH 14/14] Support addAsyncCleanup() --- Lib/unittest/async_case.py | 15 +++++++++++ Lib/unittest/test/test_async_case.py | 37 ++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 0f77396b003677..a3c8bfb9eca758 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -42,6 +42,21 @@ async def asyncSetUp(self): 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: + # 1. It can be "async def func()" iself + # 2. Class can implement "async def __call__()" method + # 3. Regular "def func()" that returns awaitable object + self.addCleanup(*(func, *args), **kwargs) + def _callSetUp(self): self.setUp() self._callAsync(self.asyncSetUp) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 811d3410752d6c..2db441da202a01 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -23,7 +23,7 @@ async def test_func(self): self.assertEqual(events, ['setUp', 'asyncSetUp']) events.append('test') - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): self.assertEqual(events, ['setUp', @@ -55,7 +55,6 @@ async def on_cleanup(self): 'tearDown', 'cleanup']) - def test_exception_in_setup(self): events = [] @@ -66,7 +65,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): events.append('asyncTearDown') @@ -79,7 +78,6 @@ async def on_cleanup(self): test.run() self.assertEqual(events, ['asyncSetUp']) - def test_exception_in_test(self): events = [] @@ -90,7 +88,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') raise Exception() - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): events.append('asyncTearDown') @@ -111,7 +109,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) raise Exception() async def asyncTearDown(self): @@ -133,7 +131,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): events.append('asyncTearDown') @@ -156,7 +154,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') - self.addCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): events.append('asyncTearDown') @@ -169,6 +167,29 @@ async def on_cleanup(self): test.run() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + def test_cleanups_interleave_order(self): + events = [] + + class Test(unittest.IsolatedAsyncioTestCase): + async def test_func(self): + self.addAsyncCleanup(self.on_sync_cleanup, 1) + self.addAsyncCleanup(self.on_async_cleanup, 2) + self.addAsyncCleanup(self.on_sync_cleanup, 3) + self.addAsyncCleanup(self.on_async_cleanup, 4) + + async def on_sync_cleanup(self, val): + events.append(f'sync_cleanup {val}') + + async def on_async_cleanup(self, val): + events.append(f'async_cleanup {val}') + + test = Test("test_func") + test.run() + self.assertEqual(events, ['async_cleanup 4', + 'sync_cleanup 3', + 'async_cleanup 2', + 'sync_cleanup 1']) + if __name__ == "__main__": unittest.main()