[Python-checkins] bpo-45238: Fix unittest.IsolatedAsyncioTestCase.debug() (GH-28449)

ambv webhook-mailer at python.org
Wed Sep 22 11:43:31 EDT 2021


https://github.com/python/cpython/commit/ecb6922ff2d56476a6cfb0941ae55aca5e7fae3d
commit: ecb6922ff2d56476a6cfb0941ae55aca5e7fae3d
branch: main
author: Serhiy Storchaka <storchaka at gmail.com>
committer: ambv <lukasz at langa.pl>
date: 2021-09-22T17:43:23+02:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2021-09-18-16-56-33.bpo-45238.Hng_9V.rst
M Lib/unittest/async_case.py
M Lib/unittest/case.py
M Lib/unittest/test/test_async_case.py

diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py
index bfc68a76e84d9..3e864d14d112f 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,12 @@ def run(self, result=None):
             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 908ae07bac79a..f1067652cc87d 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -655,12 +655,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 93ef1997e0c99..3717486b26563 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 @@ 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',
@@ -38,34 +49,48 @@ 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()
-        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 @@ async def on_cleanup(self):
                 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 @@ async def asyncTearDown(self):
             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 @@ async def asyncSetUp(self):
             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 @@ 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'])
 
 
 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 0000000000000..857f315c520bb
--- /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.



More information about the Python-checkins mailing list