[3.10] bpo-45238: Fix unittest.IsolatedAsyncioTestCase.debug() (GH-28449) (GH-28521)

It runs now asynchronous methods and callbacks.

If it fails, doCleanups() can be called for cleaning up.
(cherry picked from commit ecb6922ff2d56476a6cfb0941ae55aca5e7fae3d)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py
index 86ed704..c48380e 100644
--- a/Lib/unittest/async_case.py
+++ b/Lib/unittest/async_case.py
@@ -72,15 +72,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()
@@ -109,7 +109,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)
@@ -119,7 +119,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)
@@ -158,3 +158,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 b771ce0..607c7ae 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -659,12 +659,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 6e48b9e..e46b99f 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)
+
+        events = []
+        test = Test("test_func")
+        test.debug()
+        self.assertEqual(events, expected)
+        test.doCleanups()
+        self.assertEqual(events, expected)
 
     def test_exception_in_setup(self):
-        events = []
-
         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,35 +99,26 @@ async def on_cleanup(self):
                 events.append('cleanup')
 
 
+        events = []
         test = Test("test_func")
-        test.run()
+        result = test.run()
+        self.assertEqual(events, ['asyncSetUp', 'cleanup'])
+        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.doCleanups()
+        self.assertEqual(events, ['asyncSetUp', 'cleanup'])
 
     def test_exception_in_test(self):
-        events = []
-
-        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)
-
-            async def asyncTearDown(self):
-                events.append('asyncTearDown')
-
-            async def on_cleanup(self):
-                events.append('cleanup')
-
-        test = Test("test_func")
-        test.run()
-        self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown'])
-
-    def test_exception_in_test_after_adding_cleanup(self):
-        events = []
-
         class Test(unittest.IsolatedAsyncioTestCase):
             async def asyncSetUp(self):
                 events.append('asyncSetUp')
@@ -110,7 +126,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 +134,26 @@ 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])
+
+        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):
-        events = []
-
         class Test(unittest.IsolatedAsyncioTestCase):
             async def asyncSetUp(self):
                 events.append('asyncSetUp')
@@ -135,37 +164,70 @@ 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])
 
+        events = []
+        test = Test("test_func")
+        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_cleanups_interleave_order(self):
         events = []
@@ -235,7 +297,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__":