diff options
author | Chris Jerdonek <chris.jerdonek@gmail.com> | 2020-05-18 05:47:31 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-18 05:47:31 (GMT) |
commit | da742ba826721da84140abc785856d4ccc2d787f (patch) | |
tree | 1a6f9db52fe93edf9946620d0e2312e97c6f16a0 /Lib/test/test_asyncio | |
parent | d17f3d8315a3a775ab0807fc80acf92b1bd682f8 (diff) | |
download | cpython-da742ba826721da84140abc785856d4ccc2d787f.zip cpython-da742ba826721da84140abc785856d4ccc2d787f.tar.gz cpython-da742ba826721da84140abc785856d4ccc2d787f.tar.bz2 |
bpo-31033: Improve the traceback for cancelled asyncio tasks (GH-19951)
When an asyncio.Task is cancelled, the exception traceback now
starts with where the task was first interrupted. Previously,
the traceback only had "depth one."
Diffstat (limited to 'Lib/test/test_asyncio')
-rw-r--r-- | Lib/test/test_asyncio/test_tasks.py | 136 |
1 files changed, 114 insertions, 22 deletions
diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 65bee52..63968e2 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -10,6 +10,7 @@ import random import re import sys import textwrap +import traceback import types import unittest import weakref @@ -57,6 +58,22 @@ def format_coroutine(qualname, state, src, source_traceback, generator=False): return 'coro=<%s() %s at %s>' % (qualname, state, src) +def get_innermost_context(exc): + """ + Return information about the innermost exception context in the chain. + """ + depth = 0 + while True: + context = exc.__context__ + if context is None: + break + + exc = context + depth += 1 + + return (type(exc), exc.args, depth) + + class Dummy: def __repr__(self): @@ -111,9 +128,10 @@ class BaseTaskTests: self.assertEqual(t._cancel_message, None) t.cancel('my message') + self.assertEqual(t._cancel_message, 'my message') + with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(t) - self.assertEqual(t._cancel_message, 'my message') def test_task_cancel_message_setter(self): async def coro(): @@ -123,10 +141,8 @@ class BaseTaskTests: t._cancel_message = 'my new message' self.assertEqual(t._cancel_message, 'my new message') - # Also check that the value is used for cancel(). with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(t) - self.assertEqual(t._cancel_message, 'my new message') def test_task_del_collect(self): class Evil: @@ -548,8 +564,8 @@ class BaseTaskTests: def test_cancel_with_message_then_future_result(self): # Test Future.result() after calling cancel() with a message. cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), @@ -573,13 +589,17 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, expected_args) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, expected_args, 2)) def test_cancel_with_message_then_future_exception(self): # Test Future.exception() after calling cancel() with a message. cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), @@ -603,7 +623,11 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, expected_args) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, expected_args, 2)) def test_cancel_with_message_before_starting_task(self): loop = asyncio.new_event_loop() @@ -623,7 +647,11 @@ class BaseTaskTests: with self.assertRaises(asyncio.CancelledError) as cm: loop.run_until_complete(task) exc = cm.exception - self.assertEqual(exc.args, ('my message',)) + self.assertEqual(exc.args, ()) + + actual = get_innermost_context(exc) + self.assertEqual(actual, + (asyncio.CancelledError, ('my message',), 2)) def test_cancel_yield(self): with self.assertWarns(DeprecationWarning): @@ -805,6 +833,66 @@ class BaseTaskTests: self.assertTrue(nested_task.cancelled()) self.assertTrue(fut.cancelled()) + def assert_text_contains(self, text, substr): + if substr not in text: + raise RuntimeError(f'text {substr!r} not found in:\n>>>{text}<<<') + + def test_cancel_traceback_for_future_result(self): + # When calling Future.result() on a cancelled task, check that the + # line of code that was interrupted is included in the traceback. + loop = asyncio.new_event_loop() + self.set_event_loop(loop) + + async def nested(): + # This will get cancelled immediately. + await asyncio.sleep(10) + + async def coro(): + task = self.new_task(loop, nested()) + await asyncio.sleep(0) + task.cancel() + await task # search target + + task = self.new_task(loop, coro()) + try: + loop.run_until_complete(task) + except asyncio.CancelledError: + tb = traceback.format_exc() + self.assert_text_contains(tb, "await asyncio.sleep(10)") + # The intermediate await should also be included. + self.assert_text_contains(tb, "await task # search target") + else: + self.fail('CancelledError did not occur') + + def test_cancel_traceback_for_future_exception(self): + # When calling Future.exception() on a cancelled task, check that the + # line of code that was interrupted is included in the traceback. + loop = asyncio.new_event_loop() + self.set_event_loop(loop) + + async def nested(): + # This will get cancelled immediately. + await asyncio.sleep(10) + + async def coro(): + task = self.new_task(loop, nested()) + await asyncio.sleep(0) + task.cancel() + done, pending = await asyncio.wait([task]) + task.exception() # search target + + task = self.new_task(loop, coro()) + try: + loop.run_until_complete(task) + except asyncio.CancelledError: + tb = traceback.format_exc() + self.assert_text_contains(tb, "await asyncio.sleep(10)") + # The intermediate await should also be included. + self.assert_text_contains(tb, + "task.exception() # search target") + else: + self.fail('CancelledError did not occur') + def test_stop_while_run_in_complete(self): def gen(): @@ -2391,15 +2479,14 @@ class BaseTaskTests: def test_cancel_gather_2(self): cases = [ - ((), ('',)), - ((None,), ('',)), + ((), ()), + ((None,), ()), (('my message',), ('my message',)), # Non-string values should roundtrip. ((5,), (5,)), ] for cancel_args, expected_args in cases: with self.subTest(cancel_args=cancel_args): - loop = asyncio.new_event_loop() self.addCleanup(loop.close) @@ -2417,15 +2504,20 @@ class BaseTaskTests: qwe = self.new_task(loop, test()) await asyncio.sleep(0.2) qwe.cancel(*cancel_args) - try: - await qwe - except asyncio.CancelledError as exc: - self.assertEqual(exc.args, expected_args) - else: - self.fail('gather did not propagate the cancellation ' - 'request') - - loop.run_until_complete(main()) + await qwe + + try: + loop.run_until_complete(main()) + except asyncio.CancelledError as exc: + self.assertEqual(exc.args, ()) + exc_type, exc_args, depth = get_innermost_context(exc) + self.assertEqual((exc_type, exc_args), + (asyncio.CancelledError, expected_args)) + # The exact traceback seems to vary in CI. + self.assertIn(depth, (2, 3)) + else: + self.fail('gather did not propagate the cancellation ' + 'request') def test_exception_traceback(self): # See http://bugs.python.org/issue28843 |