Skip to content

Commit 695b8f0

Browse files
committed
Catch exceptions in asynchronous tasks when testing
Currently, exceptions in asynchronous tasks are logged by the loop, but do not cause tests to fail. Fix this.
1 parent bdfdd89 commit 695b8f0

File tree

3 files changed

+64
-7
lines changed

3 files changed

+64
-7
lines changed

asyncpg/_testbase/__init__.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import logging
1414
import os
1515
import re
16+
import textwrap
1617
import time
18+
import traceback
1719
import unittest
1820

1921

@@ -109,6 +111,21 @@ def tearDownClass(cls):
109111
cls.loop.close()
110112
asyncio.set_event_loop(None)
111113

114+
def setUp(self):
115+
self.loop.set_exception_handler(self.loop_exception_handler)
116+
self.__unhandled_exceptions = []
117+
118+
def tearDown(self):
119+
if self.__unhandled_exceptions:
120+
formatted = []
121+
122+
for i, context in enumerate(self.__unhandled_exceptions):
123+
formatted.append(self._format_loop_exception(context, i + 1))
124+
125+
self.fail(
126+
'unexpected exceptions in asynchronous code:\n' +
127+
'\n'.join(formatted))
128+
112129
@contextlib.contextmanager
113130
def assertRunUnder(self, delta):
114131
st = time.monotonic()
@@ -146,6 +163,44 @@ def handler(loop, ctx):
146163
finally:
147164
self.loop.set_exception_handler(old_handler)
148165

166+
def loop_exception_handler(self, loop, context):
167+
self.__unhandled_exceptions.append(context)
168+
loop.default_exception_handler(context)
169+
170+
def _format_loop_exception(self, context, n):
171+
message = context.get('message', 'Unhandled exception in event loop')
172+
exception = context.get('exception')
173+
if exception is not None:
174+
exc_info = (type(exception), exception, exception.__traceback__)
175+
else:
176+
exc_info = None
177+
178+
lines = []
179+
for key in sorted(context):
180+
if key in {'message', 'exception'}:
181+
continue
182+
value = context[key]
183+
if key == 'source_traceback':
184+
tb = ''.join(traceback.format_list(value))
185+
value = 'Object created at (most recent call last):\n'
186+
value += tb.rstrip()
187+
else:
188+
try:
189+
value = repr(value)
190+
except Exception as ex:
191+
value = ('Exception in __repr__ {!r}; '
192+
'value type: {!r}'.format(ex, type(value)))
193+
lines.append('[{}]: {}\n\n'.format(key, value))
194+
195+
if exc_info is not None:
196+
lines.append('[exception]:\n')
197+
formatted_exc = textwrap.indent(
198+
''.join(traceback.format_exception(*exc_info)), ' ')
199+
lines.append(formatted_exc)
200+
201+
details = textwrap.indent(''.join(lines), ' ')
202+
return '{:02d}. {}:\n{}\n'.format(n, message, details)
203+
149204

150205
_default_cluster = None
151206

asyncpg/_testbase/fuzzer.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,14 @@ async def handle(self):
187187
loop=self.loop, return_when=asyncio.FIRST_COMPLETED)
188188

189189
finally:
190-
if hasattr(self.loop, 'remove_reader'):
191-
# Asyncio *really* doesn't like when the sockets are
192-
# closed under it.
193-
self.loop.remove_reader(self.client_sock.fileno())
194-
self.loop.remove_writer(self.client_sock.fileno())
195-
self.loop.remove_reader(self.backend_sock.fileno())
196-
self.loop.remove_writer(self.backend_sock.fileno())
190+
# Asyncio fails to properly remove the readers and writers
191+
# when the task doing recv() or send() is cancelled, so
192+
# we must remove the readers and writers manually before
193+
# closing the sockets.
194+
self.loop.remove_reader(self.client_sock.fileno())
195+
self.loop.remove_writer(self.client_sock.fileno())
196+
self.loop.remove_reader(self.backend_sock.fileno())
197+
self.loop.remove_writer(self.backend_sock.fileno())
197198

198199
self.client_sock.close()
199200
self.backend_sock.close()

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[pytest]
22
addopts = --capture=no --assert=plain --strict --tb native
33
testpaths = tests
4+
filterwarnings = default

0 commit comments

Comments
 (0)