From b77e62d63bc19b0532a2ce57785d97ce078d7498 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 6 May 2019 20:44:23 -0400 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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__":