From 4d99db46e7ef1078acdfc6732c43dab78430c288 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 6 Sep 2021 16:01:18 +0300 Subject: [PATCH 1/3] bpo-45238: Fix unittest.IsolatedAsyncioTestCase.debug() It runs now asynchronous methods and callbacks. If it fails, doCleanups() can be called for cleaning up. --- Lib/unittest/async_case.py | 27 ++++- Lib/unittest/case.py | 10 +- Lib/unittest/test/test_async_case.py | 102 +++++++++++++----- .../2021-09-18-16-56-33.bpo-45238.Hng_9V.rst | 2 + 4 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-09-18-16-56-33.bpo-45238.Hng_9V.rst diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index bfc68a76e84d93..41384e905e3221 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -75,15 +75,15 @@ def _callCleanup(self, function, *args, **kwargs): self._callMaybeAsync(function, *args, **kwargs) def _callAsync(self, func, /, *args, **kwargs): - assert self._asyncioTestLoop is not None + assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized' ret = func(*args, **kwargs) - assert inspect.isawaitable(ret) + assert inspect.isawaitable(ret), f'{func!r} returned non-awaitable' 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 + assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized' ret = func(*args, **kwargs) if inspect.isawaitable(ret): fut = self._asyncioTestLoop.create_future() @@ -112,7 +112,7 @@ async def _asyncioLoopRunner(self, fut): fut.set_exception(ex) def _setupAsyncioLoop(self): - assert self._asyncioTestLoop is None + assert self._asyncioTestLoop is None, 'asyncio test loop already initialized' loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.set_debug(True) @@ -122,7 +122,7 @@ def _setupAsyncioLoop(self): loop.run_until_complete(fut) def _tearDownAsyncioLoop(self): - assert self._asyncioTestLoop is not None + assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized' loop = self._asyncioTestLoop self._asyncioTestLoop = None self._asyncioCallsQueue.put_nowait(None) @@ -161,3 +161,20 @@ def run(self, result=None): return super().run(result) finally: self._tearDownAsyncioLoop() + + def doCleanups(self): + if self._asyncioTestLoop is None: + self._setupAsyncioLoop() + try: + return super().doCleanups() + finally: + self._tearDownAsyncioLoop() + else: + return super().doCleanups() + + def debug(self): + self._setupAsyncioLoop() + try: + super().debug() + finally: + self._tearDownAsyncioLoop() diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 9fbf8524fcca7c..d294f55aa7aab6 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -661,12 +661,12 @@ def debug(self): or getattr(testMethod, '__unittest_skip_why__', '')) raise SkipTest(skip_why) - self.setUp() - testMethod() - self.tearDown() + self._callSetUp() + self._callTestMethod(testMethod) + self._callTearDown() while self._cleanups: - function, args, kwargs = self._cleanups.pop(-1) - function(*args, **kwargs) + function, args, kwargs = self._cleanups.pop() + self._callCleanup(function, *args, **kwargs) def skipTest(self, reason): """Skip this test.""" diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 93ef1997e0c99f..4608614c146e87 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -2,14 +2,18 @@ import unittest +class MyException(Exception): + pass + + def tearDownModule(): asyncio.set_event_loop_policy(None) class TestAsyncCase(unittest.TestCase): - def test_full_cycle(self): - events = [] + maxDiff = None + def test_full_cycle(self): class Test(unittest.IsolatedAsyncioTestCase): def setUp(self): self.assertEqual(events, []) @@ -46,22 +50,25 @@ async def on_cleanup(self): 'tearDown']) events.append('cleanup') + events = [] test = Test("test_func") test.run() - self.assertEqual(events, ['setUp', - 'asyncSetUp', - 'test', - 'asyncTearDown', - 'tearDown', - 'cleanup']) + expected = ['setUp', 'asyncSetUp', 'test', + 'asyncTearDown', 'tearDown', 'cleanup'] + self.assertEqual(events, expected) - def test_exception_in_setup(self): events = [] + test = Test("test_func") + test.debug() + self.assertEqual(events, expected) + test.doCleanups() + self.assertEqual(events, expected) + def test_exception_in_setup(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') - raise Exception() + raise MyException() async def test_func(self): events.append('test') @@ -74,20 +81,29 @@ async def on_cleanup(self): events.append('cleanup') + events = [] test = Test("test_func") - test.run() + result = test.run() self.assertEqual(events, ['asyncSetUp']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException', result.errors[0][1]) - def test_exception_in_test(self): events = [] + test = Test("test_func") + with self.assertRaises(MyException): + test.debug() + self.assertEqual(events, ['asyncSetUp']) + test.doCleanups() + self.assertEqual(events, ['asyncSetUp']) + def test_exception_in_test(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') async def test_func(self): events.append('test') - raise Exception() + raise MyException() self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): @@ -96,13 +112,22 @@ async def asyncTearDown(self): async def on_cleanup(self): events.append('cleanup') + events = [] test = Test("test_func") - test.run() + result = test.run() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException', result.errors[0][1]) - def test_exception_in_test_after_adding_cleanup(self): events = [] + test = Test("test_func") + with self.assertRaises(MyException): + test.debug() + self.assertEqual(events, ['asyncSetUp', 'test']) + test.doCleanups() + self.assertEqual(events, ['asyncSetUp', 'test']) + def test_exception_in_test_after_adding_cleanup(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -110,7 +135,7 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') self.addAsyncCleanup(self.on_cleanup) - raise Exception() + raise MyException() async def asyncTearDown(self): events.append('asyncTearDown') @@ -118,13 +143,22 @@ async def asyncTearDown(self): async def on_cleanup(self): events.append('cleanup') + events = [] test = Test("test_func") - test.run() + result = test.run() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException', result.errors[0][1]) - def test_exception_in_tear_down(self): events = [] + test = Test("test_func") + with self.assertRaises(MyException): + test.debug() + self.assertEqual(events, ['asyncSetUp', 'test']) + test.doCleanups() + self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup']) + def test_exception_in_tear_down(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -135,19 +169,28 @@ async def test_func(self): async def asyncTearDown(self): events.append('asyncTearDown') - raise Exception() + raise MyException() async def on_cleanup(self): events.append('cleanup') + events = [] test = Test("test_func") - test.run() + result = test.run() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException', result.errors[0][1]) - - def test_exception_in_tear_clean_up(self): events = [] + test = Test("test_func") + with self.assertRaises(MyException): + test.debug() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) + test.doCleanups() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + + def test_exception_in_tear_clean_up(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -161,10 +204,21 @@ async def asyncTearDown(self): async def on_cleanup(self): events.append('cleanup') - raise Exception() + raise MyException() + events = [] test = Test("test_func") - test.run() + result = test.run() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException', result.errors[0][1]) + + events = [] + test = Test("test_func") + with self.assertRaises(MyException): + test.debug() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) def test_deprecation_of_return_val_from_test(self): diff --git a/Misc/NEWS.d/next/Library/2021-09-18-16-56-33.bpo-45238.Hng_9V.rst b/Misc/NEWS.d/next/Library/2021-09-18-16-56-33.bpo-45238.Hng_9V.rst new file mode 100644 index 00000000000000..857f315c520bba --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-18-16-56-33.bpo-45238.Hng_9V.rst @@ -0,0 +1,2 @@ +Fix :meth:`unittest.IsolatedAsyncioTestCase.debug`: it runs now asynchronous +methods and callbacks. From 5eb175c09b4902122ec067194c8eb5ec3bdd5016 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 19 Sep 2021 12:55:42 +0300 Subject: [PATCH 2/3] Keep the event loop for doCleanup(). --- Lib/unittest/async_case.py | 18 ++---- Lib/unittest/test/test_async_case.py | 86 ++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 41384e905e3221..3e864d14d112fa 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -162,19 +162,11 @@ def run(self, result=None): finally: self._tearDownAsyncioLoop() - def doCleanups(self): - if self._asyncioTestLoop is None: - self._setupAsyncioLoop() - try: - return super().doCleanups() - finally: - self._tearDownAsyncioLoop() - else: - return super().doCleanups() - def debug(self): self._setupAsyncioLoop() - try: - super().debug() - finally: + super().debug() + self._tearDownAsyncioLoop() + + def __del__(self): + if self._asyncioTestLoop is not None: self._tearDownAsyncioLoop() diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 4608614c146e87..681e058c17737b 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -1,4 +1,5 @@ import asyncio +import gc import unittest @@ -90,11 +91,17 @@ async def on_cleanup(self): events = [] test = Test("test_func") - with self.assertRaises(MyException): + try: test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp']) test.doCleanups() self.assertEqual(events, ['asyncSetUp']) + del test + gc.collect() def test_exception_in_test(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -121,11 +128,17 @@ async def on_cleanup(self): events = [] test = Test("test_func") - with self.assertRaises(MyException): + try: test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp', 'test']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test']) + del test + gc.collect() def test_exception_in_test_after_adding_cleanup(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -152,11 +165,17 @@ async def on_cleanup(self): events = [] test = Test("test_func") - with self.assertRaises(MyException): + try: test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp', 'test']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup']) + del test + gc.collect() def test_exception_in_tear_down(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -183,12 +202,17 @@ async def on_cleanup(self): events = [] test = Test("test_func") - with self.assertRaises(MyException): + try: test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) - + del test + gc.collect() def test_exception_in_tear_clean_up(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -215,11 +239,17 @@ async def on_cleanup(self): events = [] test = Test("test_func") - with self.assertRaises(MyException): + try: test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + del test + gc.collect() def test_deprecation_of_return_val_from_test(self): # Issue 41322 - deprecate return of value!=None from a test @@ -309,7 +339,51 @@ async def coro(): output = test.run() self.assertTrue(cancelled) + def test_debug_cleanup_same_loop(self): + class Test(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + async def coro(): + await asyncio.sleep(0) + fut = asyncio.ensure_future(coro()) + self.addAsyncCleanup(self.cleanup, fut) + events.append('asyncSetUp') + + async def test_func(self): + events.append('test') + raise MyException() + + async def asyncTearDown(self): + events.append('asyncTearDown') + + async def cleanup(self, fut): + try: + # Raises an exception if in different loop + await asyncio.wait([fut]) + events.append('cleanup') + except: + import traceback + traceback.print_exc() + raise + events = [] + test = Test("test_func") + result = test.run() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertIn('MyException', result.errors[0][1]) + + events = [] + test = Test("test_func") + try: + test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') + self.assertEqual(events, ['asyncSetUp', 'test']) + test.doCleanups() + self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup']) + del test + gc.collect() if __name__ == "__main__": From daf807f43fb319ace7cde1affbfd4e318346ea13 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 20 Sep 2021 20:34:05 +0300 Subject: [PATCH 3/3] Refactor tests. --- Lib/unittest/test/test_async_case.py | 104 +++++++++++---------------- 1 file changed, 40 insertions(+), 64 deletions(-) diff --git a/Lib/unittest/test/test_async_case.py b/Lib/unittest/test/test_async_case.py index 681e058c17737b..3717486b26563e 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -1,6 +1,6 @@ import asyncio -import gc import unittest +from test import support class MyException(Exception): @@ -14,6 +14,11 @@ def tearDownModule(): class TestAsyncCase(unittest.TestCase): maxDiff = None + def tearDown(self): + # Ensure that IsolatedAsyncioTestCase instances are destroyed before + # starting a new event loop + support.gc_collect() + def test_full_cycle(self): class Test(unittest.IsolatedAsyncioTestCase): def setUp(self): @@ -23,12 +28,13 @@ def setUp(self): async def asyncSetUp(self): self.assertEqual(events, ['setUp']) events.append('asyncSetUp') + self.addAsyncCleanup(self.on_cleanup1) async def test_func(self): self.assertEqual(events, ['setUp', 'asyncSetUp']) events.append('test') - self.addAsyncCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup2) async def asyncTearDown(self): self.assertEqual(events, ['setUp', @@ -43,19 +49,30 @@ def tearDown(self): 'asyncTearDown']) events.append('tearDown') - async def on_cleanup(self): + async def on_cleanup1(self): + self.assertEqual(events, ['setUp', + 'asyncSetUp', + 'test', + 'asyncTearDown', + 'tearDown', + 'cleanup2']) + events.append('cleanup1') + + async def on_cleanup2(self): self.assertEqual(events, ['setUp', 'asyncSetUp', 'test', 'asyncTearDown', 'tearDown']) - events.append('cleanup') + events.append('cleanup2') events = [] test = Test("test_func") - test.run() + result = test.run() + self.assertEqual(result.errors, []) + self.assertEqual(result.failures, []) expected = ['setUp', 'asyncSetUp', 'test', - 'asyncTearDown', 'tearDown', 'cleanup'] + 'asyncTearDown', 'tearDown', 'cleanup2', 'cleanup1'] self.assertEqual(events, expected) events = [] @@ -69,11 +86,11 @@ def test_exception_in_setup(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') + self.addAsyncCleanup(self.on_cleanup) raise MyException() async def test_func(self): events.append('test') - self.addAsyncCleanup(self.on_cleanup) async def asyncTearDown(self): events.append('asyncTearDown') @@ -85,7 +102,7 @@ async def on_cleanup(self): events = [] test = Test("test_func") result = test.run() - self.assertEqual(events, ['asyncSetUp']) + self.assertEqual(events, ['asyncSetUp', 'cleanup']) self.assertIs(result.errors[0][0], test) self.assertIn('MyException', result.errors[0][1]) @@ -99,48 +116,9 @@ async def on_cleanup(self): self.fail('Expected a MyException exception') self.assertEqual(events, ['asyncSetUp']) test.doCleanups() - self.assertEqual(events, ['asyncSetUp']) - del test - gc.collect() + self.assertEqual(events, ['asyncSetUp', 'cleanup']) def test_exception_in_test(self): - class Test(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - events.append('asyncSetUp') - - async def test_func(self): - events.append('test') - raise MyException() - self.addAsyncCleanup(self.on_cleanup) - - async def asyncTearDown(self): - events.append('asyncTearDown') - - async def on_cleanup(self): - events.append('cleanup') - - events = [] - test = Test("test_func") - result = test.run() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) - self.assertIs(result.errors[0][0], test) - self.assertIn('MyException', result.errors[0][1]) - - events = [] - test = Test("test_func") - try: - test.debug() - except MyException: - pass - else: - self.fail('Expected a MyException exception') - self.assertEqual(events, ['asyncSetUp', 'test']) - test.doCleanups() - self.assertEqual(events, ['asyncSetUp', 'test']) - del test - gc.collect() - - def test_exception_in_test_after_adding_cleanup(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -174,8 +152,6 @@ async def on_cleanup(self): self.assertEqual(events, ['asyncSetUp', 'test']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup']) - del test - gc.collect() def test_exception_in_tear_down(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -211,8 +187,6 @@ async def on_cleanup(self): self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) - del test - gc.collect() def test_exception_in_tear_clean_up(self): class Test(unittest.IsolatedAsyncioTestCase): @@ -221,21 +195,27 @@ async def asyncSetUp(self): async def test_func(self): events.append('test') - self.addAsyncCleanup(self.on_cleanup) + self.addAsyncCleanup(self.on_cleanup1) + self.addAsyncCleanup(self.on_cleanup2) async def asyncTearDown(self): events.append('asyncTearDown') - async def on_cleanup(self): - events.append('cleanup') - raise MyException() + async def on_cleanup1(self): + events.append('cleanup1') + raise MyException('some error') + + async def on_cleanup2(self): + events.append('cleanup2') + raise MyException('other error') events = [] test = Test("test_func") result = test.run() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2', 'cleanup1']) self.assertIs(result.errors[0][0], test) - self.assertIn('MyException', result.errors[0][1]) + self.assertIn('MyException: other error', result.errors[0][1]) + self.assertIn('MyException: some error', result.errors[1][1]) events = [] test = Test("test_func") @@ -245,11 +225,9 @@ async def on_cleanup(self): pass else: self.fail('Expected a MyException exception') - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2']) test.doCleanups() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) - del test - gc.collect() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2', 'cleanup1']) def test_deprecation_of_return_val_from_test(self): # Issue 41322 - deprecate return of value!=None from a test @@ -382,8 +360,6 @@ async def cleanup(self, fut): self.assertEqual(events, ['asyncSetUp', 'test']) test.doCleanups() self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup']) - del test - gc.collect() if __name__ == "__main__":