From ecb6922ff2d56476a6cfb0941ae55aca5e7fae3d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 22 Sep 2021 18:43:23 +0300 Subject: bpo-45238: Fix unittest.IsolatedAsyncioTestCase.debug() (GH-28449) It runs now asynchronous methods and callbacks. If it fails, doCleanups() can be called for cleaning up. --- Lib/unittest/async_case.py | 19 +- Lib/unittest/case.py | 10 +- Lib/unittest/test/test_async_case.py | 206 ++++++++++++++++----- .../2021-09-18-16-56-33.bpo-45238.Hng_9V.rst | 2 + 4 files changed, 176 insertions(+), 61 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 bfc68a7..3e864d1 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -75,15 +75,15 @@ class IsolatedAsyncioTestCase(TestCase): 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 @@ class IsolatedAsyncioTestCase(TestCase): 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 @@ class IsolatedAsyncioTestCase(TestCase): 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,12 @@ class IsolatedAsyncioTestCase(TestCase): return super().run(result) finally: self._tearDownAsyncioLoop() + + def debug(self): + self._setupAsyncioLoop() + super().debug() + self._tearDownAsyncioLoop() + + def __del__(self): + if self._asyncioTestLoop is not None: + self._tearDownAsyncioLoop() diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 908ae07..f106765 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -655,12 +655,12 @@ class TestCase(object): 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 93ef199..3717486 100644 --- a/Lib/unittest/test/test_async_case.py +++ b/Lib/unittest/test/test_async_case.py @@ -1,5 +1,10 @@ import asyncio import unittest +from test import support + + +class MyException(Exception): + pass def tearDownModule(): @@ -7,9 +12,14 @@ def tearDownModule(): class TestAsyncCase(unittest.TestCase): - def test_full_cycle(self): - events = [] + 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): self.assertEqual(events, []) @@ -18,12 +28,13 @@ class TestAsyncCase(unittest.TestCase): 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', @@ -38,34 +49,48 @@ class TestAsyncCase(unittest.TestCase): '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() - self.assertEqual(events, ['setUp', - 'asyncSetUp', - 'test', - 'asyncTearDown', - 'tearDown', - 'cleanup']) + result = test.run() + self.assertEqual(result.errors, []) + self.assertEqual(result.failures, []) + expected = ['setUp', 'asyncSetUp', 'test', + 'asyncTearDown', 'tearDown', 'cleanup2', 'cleanup1'] + 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() + 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') @@ -74,21 +99,34 @@ class TestAsyncCase(unittest.TestCase): events.append('cleanup') + events = [] test = Test("test_func") - test.run() - self.assertEqual(events, ['asyncSetUp']) + result = test.run() + self.assertEqual(events, ['asyncSetUp', 'cleanup']) + 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") + try: + test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') + self.assertEqual(events, ['asyncSetUp']) + test.doCleanups() + 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 Exception() self.addAsyncCleanup(self.on_cleanup) + raise MyException() async def asyncTearDown(self): events.append('asyncTearDown') @@ -96,13 +134,26 @@ class TestAsyncCase(unittest.TestCase): async def on_cleanup(self): events.append('cleanup') + events = [] test = Test("test_func") - test.run() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown']) + 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_test_after_adding_cleanup(self): 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']) + def test_exception_in_tear_down(self): class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') @@ -110,62 +161,73 @@ class TestAsyncCase(unittest.TestCase): async def test_func(self): events.append('test') self.addAsyncCleanup(self.on_cleanup) - raise Exception() async def asyncTearDown(self): events.append('asyncTearDown') + 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_down(self): events = [] - - class Test(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - events.append('asyncSetUp') - - async def test_func(self): - events.append('test') - self.addAsyncCleanup(self.on_cleanup) - - async def asyncTearDown(self): - events.append('asyncTearDown') - raise Exception() - - async def on_cleanup(self): - events.append('cleanup') - test = Test("test_func") - test.run() + 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']) - def test_exception_in_tear_clean_up(self): - events = [] - class Test(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): events.append('asyncSetUp') 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 Exception() + 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") - test.run() - self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup']) + result = test.run() + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2', 'cleanup1']) + self.assertIs(result.errors[0][0], test) + self.assertIn('MyException: other error', result.errors[0][1]) + self.assertIn('MyException: some error', result.errors[1][1]) + + events = [] + test = Test("test_func") + try: + test.debug() + except MyException: + pass + else: + self.fail('Expected a MyException exception') + self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2']) + test.doCleanups() + 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 @@ -255,7 +317,49 @@ class TestAsyncCase(unittest.TestCase): 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']) if __name__ == "__main__": 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 0000000..857f315 --- /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. -- cgit v0.12