Skip to content

Commit ecb6922

Browse files
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.
1 parent 58f8adf commit ecb6922

File tree

4 files changed

+176
-61
lines changed

4 files changed

+176
-61
lines changed

Lib/unittest/async_case.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ def _callCleanup(self, function, *args, **kwargs):
7575
self._callMaybeAsync(function, *args, **kwargs)
7676

7777
def _callAsync(self, func, /, *args, **kwargs):
78-
assert self._asyncioTestLoop is not None
78+
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
7979
ret = func(*args, **kwargs)
80-
assert inspect.isawaitable(ret)
80+
assert inspect.isawaitable(ret), f'{func!r} returned non-awaitable'
8181
fut = self._asyncioTestLoop.create_future()
8282
self._asyncioCallsQueue.put_nowait((fut, ret))
8383
return self._asyncioTestLoop.run_until_complete(fut)
8484

8585
def _callMaybeAsync(self, func, /, *args, **kwargs):
86-
assert self._asyncioTestLoop is not None
86+
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
8787
ret = func(*args, **kwargs)
8888
if inspect.isawaitable(ret):
8989
fut = self._asyncioTestLoop.create_future()
@@ -112,7 +112,7 @@ async def _asyncioLoopRunner(self, fut):
112112
fut.set_exception(ex)
113113

114114
def _setupAsyncioLoop(self):
115-
assert self._asyncioTestLoop is None
115+
assert self._asyncioTestLoop is None, 'asyncio test loop already initialized'
116116
loop = asyncio.new_event_loop()
117117
asyncio.set_event_loop(loop)
118118
loop.set_debug(True)
@@ -122,7 +122,7 @@ def _setupAsyncioLoop(self):
122122
loop.run_until_complete(fut)
123123

124124
def _tearDownAsyncioLoop(self):
125-
assert self._asyncioTestLoop is not None
125+
assert self._asyncioTestLoop is not None, 'asyncio test loop is not initialized'
126126
loop = self._asyncioTestLoop
127127
self._asyncioTestLoop = None
128128
self._asyncioCallsQueue.put_nowait(None)
@@ -161,3 +161,12 @@ def run(self, result=None):
161161
return super().run(result)
162162
finally:
163163
self._tearDownAsyncioLoop()
164+
165+
def debug(self):
166+
self._setupAsyncioLoop()
167+
super().debug()
168+
self._tearDownAsyncioLoop()
169+
170+
def __del__(self):
171+
if self._asyncioTestLoop is not None:
172+
self._tearDownAsyncioLoop()

Lib/unittest/case.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -655,12 +655,12 @@ def debug(self):
655655
or getattr(testMethod, '__unittest_skip_why__', ''))
656656
raise SkipTest(skip_why)
657657

658-
self.setUp()
659-
testMethod()
660-
self.tearDown()
658+
self._callSetUp()
659+
self._callTestMethod(testMethod)
660+
self._callTearDown()
661661
while self._cleanups:
662-
function, args, kwargs = self._cleanups.pop(-1)
663-
function(*args, **kwargs)
662+
function, args, kwargs = self._cleanups.pop()
663+
self._callCleanup(function, *args, **kwargs)
664664

665665
def skipTest(self, reason):
666666
"""Skip this test."""

Lib/unittest/test/test_async_case.py

Lines changed: 155 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import asyncio
22
import unittest
3+
from test import support
4+
5+
6+
class MyException(Exception):
7+
pass
38

49

510
def tearDownModule():
611
asyncio.set_event_loop_policy(None)
712

813

914
class TestAsyncCase(unittest.TestCase):
10-
def test_full_cycle(self):
11-
events = []
15+
maxDiff = None
16+
17+
def tearDown(self):
18+
# Ensure that IsolatedAsyncioTestCase instances are destroyed before
19+
# starting a new event loop
20+
support.gc_collect()
1221

22+
def test_full_cycle(self):
1323
class Test(unittest.IsolatedAsyncioTestCase):
1424
def setUp(self):
1525
self.assertEqual(events, [])
@@ -18,12 +28,13 @@ def setUp(self):
1828
async def asyncSetUp(self):
1929
self.assertEqual(events, ['setUp'])
2030
events.append('asyncSetUp')
31+
self.addAsyncCleanup(self.on_cleanup1)
2132

2233
async def test_func(self):
2334
self.assertEqual(events, ['setUp',
2435
'asyncSetUp'])
2536
events.append('test')
26-
self.addAsyncCleanup(self.on_cleanup)
37+
self.addAsyncCleanup(self.on_cleanup2)
2738

2839
async def asyncTearDown(self):
2940
self.assertEqual(events, ['setUp',
@@ -38,34 +49,48 @@ def tearDown(self):
3849
'asyncTearDown'])
3950
events.append('tearDown')
4051

41-
async def on_cleanup(self):
52+
async def on_cleanup1(self):
53+
self.assertEqual(events, ['setUp',
54+
'asyncSetUp',
55+
'test',
56+
'asyncTearDown',
57+
'tearDown',
58+
'cleanup2'])
59+
events.append('cleanup1')
60+
61+
async def on_cleanup2(self):
4262
self.assertEqual(events, ['setUp',
4363
'asyncSetUp',
4464
'test',
4565
'asyncTearDown',
4666
'tearDown'])
47-
events.append('cleanup')
67+
events.append('cleanup2')
4868

69+
events = []
4970
test = Test("test_func")
50-
test.run()
51-
self.assertEqual(events, ['setUp',
52-
'asyncSetUp',
53-
'test',
54-
'asyncTearDown',
55-
'tearDown',
56-
'cleanup'])
71+
result = test.run()
72+
self.assertEqual(result.errors, [])
73+
self.assertEqual(result.failures, [])
74+
expected = ['setUp', 'asyncSetUp', 'test',
75+
'asyncTearDown', 'tearDown', 'cleanup2', 'cleanup1']
76+
self.assertEqual(events, expected)
5777

58-
def test_exception_in_setup(self):
5978
events = []
79+
test = Test("test_func")
80+
test.debug()
81+
self.assertEqual(events, expected)
82+
test.doCleanups()
83+
self.assertEqual(events, expected)
6084

85+
def test_exception_in_setup(self):
6186
class Test(unittest.IsolatedAsyncioTestCase):
6287
async def asyncSetUp(self):
6388
events.append('asyncSetUp')
64-
raise Exception()
89+
self.addAsyncCleanup(self.on_cleanup)
90+
raise MyException()
6591

6692
async def test_func(self):
6793
events.append('test')
68-
self.addAsyncCleanup(self.on_cleanup)
6994

7095
async def asyncTearDown(self):
7196
events.append('asyncTearDown')
@@ -74,98 +99,135 @@ async def on_cleanup(self):
7499
events.append('cleanup')
75100

76101

102+
events = []
77103
test = Test("test_func")
78-
test.run()
79-
self.assertEqual(events, ['asyncSetUp'])
104+
result = test.run()
105+
self.assertEqual(events, ['asyncSetUp', 'cleanup'])
106+
self.assertIs(result.errors[0][0], test)
107+
self.assertIn('MyException', result.errors[0][1])
80108

81-
def test_exception_in_test(self):
82109
events = []
110+
test = Test("test_func")
111+
try:
112+
test.debug()
113+
except MyException:
114+
pass
115+
else:
116+
self.fail('Expected a MyException exception')
117+
self.assertEqual(events, ['asyncSetUp'])
118+
test.doCleanups()
119+
self.assertEqual(events, ['asyncSetUp', 'cleanup'])
83120

121+
def test_exception_in_test(self):
84122
class Test(unittest.IsolatedAsyncioTestCase):
85123
async def asyncSetUp(self):
86124
events.append('asyncSetUp')
87125

88126
async def test_func(self):
89127
events.append('test')
90-
raise Exception()
91128
self.addAsyncCleanup(self.on_cleanup)
129+
raise MyException()
92130

93131
async def asyncTearDown(self):
94132
events.append('asyncTearDown')
95133

96134
async def on_cleanup(self):
97135
events.append('cleanup')
98136

137+
events = []
99138
test = Test("test_func")
100-
test.run()
101-
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown'])
139+
result = test.run()
140+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
141+
self.assertIs(result.errors[0][0], test)
142+
self.assertIn('MyException', result.errors[0][1])
102143

103-
def test_exception_in_test_after_adding_cleanup(self):
104144
events = []
145+
test = Test("test_func")
146+
try:
147+
test.debug()
148+
except MyException:
149+
pass
150+
else:
151+
self.fail('Expected a MyException exception')
152+
self.assertEqual(events, ['asyncSetUp', 'test'])
153+
test.doCleanups()
154+
self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup'])
105155

156+
def test_exception_in_tear_down(self):
106157
class Test(unittest.IsolatedAsyncioTestCase):
107158
async def asyncSetUp(self):
108159
events.append('asyncSetUp')
109160

110161
async def test_func(self):
111162
events.append('test')
112163
self.addAsyncCleanup(self.on_cleanup)
113-
raise Exception()
114164

115165
async def asyncTearDown(self):
116166
events.append('asyncTearDown')
167+
raise MyException()
117168

118169
async def on_cleanup(self):
119170
events.append('cleanup')
120171

172+
events = []
121173
test = Test("test_func")
122-
test.run()
174+
result = test.run()
123175
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
176+
self.assertIs(result.errors[0][0], test)
177+
self.assertIn('MyException', result.errors[0][1])
124178

125-
def test_exception_in_tear_down(self):
126179
events = []
127-
128-
class Test(unittest.IsolatedAsyncioTestCase):
129-
async def asyncSetUp(self):
130-
events.append('asyncSetUp')
131-
132-
async def test_func(self):
133-
events.append('test')
134-
self.addAsyncCleanup(self.on_cleanup)
135-
136-
async def asyncTearDown(self):
137-
events.append('asyncTearDown')
138-
raise Exception()
139-
140-
async def on_cleanup(self):
141-
events.append('cleanup')
142-
143180
test = Test("test_func")
144-
test.run()
181+
try:
182+
test.debug()
183+
except MyException:
184+
pass
185+
else:
186+
self.fail('Expected a MyException exception')
187+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown'])
188+
test.doCleanups()
145189
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
146190

147-
148191
def test_exception_in_tear_clean_up(self):
149-
events = []
150-
151192
class Test(unittest.IsolatedAsyncioTestCase):
152193
async def asyncSetUp(self):
153194
events.append('asyncSetUp')
154195

155196
async def test_func(self):
156197
events.append('test')
157-
self.addAsyncCleanup(self.on_cleanup)
198+
self.addAsyncCleanup(self.on_cleanup1)
199+
self.addAsyncCleanup(self.on_cleanup2)
158200

159201
async def asyncTearDown(self):
160202
events.append('asyncTearDown')
161203

162-
async def on_cleanup(self):
163-
events.append('cleanup')
164-
raise Exception()
204+
async def on_cleanup1(self):
205+
events.append('cleanup1')
206+
raise MyException('some error')
207+
208+
async def on_cleanup2(self):
209+
events.append('cleanup2')
210+
raise MyException('other error')
165211

212+
events = []
166213
test = Test("test_func")
167-
test.run()
168-
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
214+
result = test.run()
215+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2', 'cleanup1'])
216+
self.assertIs(result.errors[0][0], test)
217+
self.assertIn('MyException: other error', result.errors[0][1])
218+
self.assertIn('MyException: some error', result.errors[1][1])
219+
220+
events = []
221+
test = Test("test_func")
222+
try:
223+
test.debug()
224+
except MyException:
225+
pass
226+
else:
227+
self.fail('Expected a MyException exception')
228+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2'])
229+
test.doCleanups()
230+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup2', 'cleanup1'])
169231

170232
def test_deprecation_of_return_val_from_test(self):
171233
# Issue 41322 - deprecate return of value!=None from a test
@@ -255,7 +317,49 @@ async def coro():
255317
output = test.run()
256318
self.assertTrue(cancelled)
257319

320+
def test_debug_cleanup_same_loop(self):
321+
class Test(unittest.IsolatedAsyncioTestCase):
322+
async def asyncSetUp(self):
323+
async def coro():
324+
await asyncio.sleep(0)
325+
fut = asyncio.ensure_future(coro())
326+
self.addAsyncCleanup(self.cleanup, fut)
327+
events.append('asyncSetUp')
328+
329+
async def test_func(self):
330+
events.append('test')
331+
raise MyException()
258332

333+
async def asyncTearDown(self):
334+
events.append('asyncTearDown')
335+
336+
async def cleanup(self, fut):
337+
try:
338+
# Raises an exception if in different loop
339+
await asyncio.wait([fut])
340+
events.append('cleanup')
341+
except:
342+
import traceback
343+
traceback.print_exc()
344+
raise
345+
346+
events = []
347+
test = Test("test_func")
348+
result = test.run()
349+
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
350+
self.assertIn('MyException', result.errors[0][1])
351+
352+
events = []
353+
test = Test("test_func")
354+
try:
355+
test.debug()
356+
except MyException:
357+
pass
358+
else:
359+
self.fail('Expected a MyException exception')
360+
self.assertEqual(events, ['asyncSetUp', 'test'])
361+
test.doCleanups()
362+
self.assertEqual(events, ['asyncSetUp', 'test', 'cleanup'])
259363

260364

261365
if __name__ == "__main__":
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :meth:`unittest.IsolatedAsyncioTestCase.debug`: it runs now asynchronous
2+
methods and callbacks.

0 commit comments

Comments
 (0)