Show chained exception frames in stack. Fixes #1042

This commit is contained in:
Fabio Zadrozny 2022-09-16 15:49:00 -03:00
parent c300080c62
commit 349ff7337b
8 changed files with 129 additions and 69 deletions

View file

@ -1420,7 +1420,6 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th
try: try:
try: try:
frames_list = dbg.suspended_frames_manager.get_frames_list(thread_id) frames_list = dbg.suspended_frames_manager.get_frames_list(thread_id)
memo = set()
while frames_list is not None and len(frames_list): while frames_list is not None and len(frames_list):
frames = [] frames = []
@ -1483,8 +1482,7 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th
stack_str += frames_list.exc_context_msg stack_str += frames_list.exc_context_msg
stack_str_lst.append(stack_str) stack_str_lst.append(stack_str)
frames_list = create_frames_list_from_exception_cause( frames_list = frames_list.chained_frames_list
frames_list.trace_obj, None, frames_list.exc_type, frames_list.exc_desc, memo)
if frames_list is None or not frames_list: if frames_list is None or not frames_list:
break break

View file

@ -97,6 +97,8 @@ class FramesList(object):
# This is to know whether an exception was extracted from a __cause__ or __context__. # This is to know whether an exception was extracted from a __cause__ or __context__.
self.exc_context_msg = '' self.exc_context_msg = ''
self.chained_frames_list = None
def append(self, frame): def append(self, frame):
self._frames.append(frame) self._frames.append(frame)
@ -128,7 +130,13 @@ class FramesList(object):
lst.append('\n ') lst.append('\n ')
lst.append(repr(frame)) lst.append(repr(frame))
lst.append(',') lst.append(',')
if self.chained_frames_list is not None:
lst.append('\n--- Chained ---\n')
lst.append(str(self.chained_frames_list))
lst.append('\n)') lst.append('\n)')
return ''.join(lst) return ''.join(lst)
__str__ = __repr__ __str__ = __repr__
@ -142,7 +150,8 @@ class _DummyFrameWrapper(object):
self.f_back = f_back self.f_back = f_back
self.f_trace = None self.f_trace = None
original_code = frame.f_code original_code = frame.f_code
self.f_code = FCode(original_code.co_name , original_code.co_filename) name = original_code.co_name
self.f_code = FCode(name, original_code.co_filename)
@property @property
def f_locals(self): def f_locals(self):
@ -152,6 +161,11 @@ class _DummyFrameWrapper(object):
def f_globals(self): def f_globals(self):
return self._base_frame.f_globals return self._base_frame.f_globals
def __str__(self):
return "<_DummyFrameWrapper, file '%s', line %s, %s" % (self.f_code.co_filename, self.f_lineno, self.f_code.co_name)
__repr__ = __str__
_cause_message = ( _cause_message = (
"\nThe above exception was the direct cause " "\nThe above exception was the direct cause "
@ -231,36 +245,6 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce
lst.append((tb.tb_frame, tb.tb_lineno)) lst.append((tb.tb_frame, tb.tb_lineno))
tb = tb.tb_next tb = tb.tb_next
curr = exc_desc
memo = set()
while True:
initial = curr
try:
curr = getattr(initial, '__cause__', None)
except Exception:
curr = None
if curr is None:
try:
curr = getattr(initial, '__context__', None)
except Exception:
curr = None
if curr is None or id(curr) in memo:
break
# The traceback module does this, so, let's play safe here too...
memo.add(id(curr))
tb = getattr(curr, '__traceback__', None)
while tb is not None:
# Note: we don't use the actual tb.tb_frame because if the cause of the exception
# uses the same frame object, the id(frame) would be the same and the frame_id_to_lineno
# would be wrong as the same frame needs to appear with 2 different lines.
lst.append((_DummyFrameWrapper(tb.tb_frame, tb.tb_lineno, None), tb.tb_lineno))
tb = tb.tb_next
frames_list = None frames_list = None
for tb_frame, tb_lineno in reversed(lst): for tb_frame, tb_lineno in reversed(lst):
@ -290,6 +274,18 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce
if len(frames_list) > 0: if len(frames_list) > 0:
frames_list.current_frame = frames_list.last_frame() frames_list.current_frame = frames_list.last_frame()
curr = frames_list
memo = set()
memo.add(id(exc_desc))
while True:
chained = create_frames_list_from_exception_cause(None, None, None, curr.exc_desc, memo)
if chained is None:
break
else:
curr.chained_frames_list = chained
curr = chained
return frames_list return frames_list

View file

@ -230,7 +230,7 @@ class NetCommandFactoryJson(NetCommandFactory):
frames_list = pydevd_frame_utils.create_frames_list_from_frame(topmost_frame) frames_list = pydevd_frame_utils.create_frames_list_from_frame(topmost_frame)
for frame_id, frame, method_name, original_filename, filename_in_utf8, lineno, applied_mapping, show_as_current_frame in self._iter_visible_frames_info( for frame_id, frame, method_name, original_filename, filename_in_utf8, lineno, applied_mapping, show_as_current_frame in self._iter_visible_frames_info(
py_db, frames_list py_db, frames_list, flatten_chained=True
): ):
try: try:

View file

@ -164,8 +164,10 @@ class NetCommandFactory(object):
except: except:
return self.make_error_message(0, get_exception_traceback_str()) return self.make_error_message(0, get_exception_traceback_str())
def _iter_visible_frames_info(self, py_db, frames_list): def _iter_visible_frames_info(self, py_db, frames_list, flatten_chained=False):
assert frames_list.__class__ == FramesList assert frames_list.__class__ == FramesList
is_chained = False
while True:
for frame in frames_list: for frame in frames_list:
show_as_current_frame = frame is frames_list.current_frame show_as_current_frame = frame is frames_list.current_frame
if frame.f_code is None: if frame.f_code is None:
@ -177,6 +179,9 @@ class NetCommandFactory(object):
pydev_log.info('Frame without co_name: %s', frame) pydev_log.info('Frame without co_name: %s', frame)
continue # IronPython sometimes does not have it! continue # IronPython sometimes does not have it!
if is_chained:
method_name = '[Chained Exc: %s] %s' % (frames_list.exc_desc, method_name)
abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame) abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame)
if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE: if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE:
# Skip pydevd files. # Skip pydevd files.
@ -192,6 +197,14 @@ class NetCommandFactory(object):
yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame
if not flatten_chained:
break
frames_list = frames_list.chained_frames_list
if frames_list is None or len(frames_list) == 0:
break
is_chained = True
def make_thread_stack_str(self, py_db, frames_list): def make_thread_stack_str(self, py_db, frames_list):
assert frames_list.__class__ == FramesList assert frames_list.__class__ == FramesList
make_valid_xml_value = pydevd_xml.make_valid_xml_value make_valid_xml_value = pydevd_xml.make_valid_xml_value
@ -200,7 +213,7 @@ class NetCommandFactory(object):
try: try:
for frame_id, frame, method_name, _original_filename, filename_in_utf8, lineno, _applied_mapping, _show_as_current_frame in self._iter_visible_frames_info( for frame_id, frame, method_name, _original_filename, filename_in_utf8, lineno, _applied_mapping, _show_as_current_frame in self._iter_visible_frames_info(
py_db, frames_list py_db, frames_list, flatten_chained=True
): ):
# print("file is ", filename_in_utf8) # print("file is ", filename_in_utf8)

View file

@ -1657,13 +1657,13 @@ def test_case_throw_exc_reason_xml(case_setup):
name_and_lines.append((frame['name'], frame['line'])) name_and_lines.append((frame['name'], frame['line']))
assert name_and_lines == [ assert name_and_lines == [
('method2', '2'),
('method', '6'),
('foobar', '16'),
('handle', '10'),
('foobar', '18'),
('foobar', '20'), ('foobar', '20'),
('<module>', '23'), ('<module>', '23'),
('[Chained Exc: another while handling] foobar', '18'),
('[Chained Exc: another while handling] handle', '10'),
('[Chained Exc: TEST SUCEEDED] foobar', '16'),
('[Chained Exc: TEST SUCEEDED] method', '6'),
('[Chained Exc: TEST SUCEEDED] method2', '2'),
] ]
hit = writer.wait_for_breakpoint_hit(REASON_UNCAUGHT_EXCEPTION) hit = writer.wait_for_breakpoint_hit(REASON_UNCAUGHT_EXCEPTION)

View file

@ -751,7 +751,15 @@ def test_case_throw_exc_reason(case_setup):
stack_frames = json_hit.stack_trace_response.body.stackFrames stack_frames = json_hit.stack_trace_response.body.stackFrames
# Note that the additional context doesn't really appear in the stack # Note that the additional context doesn't really appear in the stack
# frames, only in the details. # frames, only in the details.
assert [x['name'] for x in stack_frames] == ['foobar', '<module>'] assert [x['name'] for x in stack_frames] == [
'foobar',
'<module>',
'[Chained Exc: another while handling] foobar',
'[Chained Exc: another while handling] handle',
'[Chained Exc: TEST SUCEEDED] foobar',
'[Chained Exc: TEST SUCEEDED] method',
'[Chained Exc: TEST SUCEEDED] method2',
]
body = exc_info_response.body body = exc_info_response.body
assert body.exceptionId.endswith('RuntimeError') assert body.exceptionId.endswith('RuntimeError')
@ -760,10 +768,16 @@ def test_case_throw_exc_reason(case_setup):
# Check that we have all the lines (including the cause/context) in the stack trace. # Check that we have all the lines (including the cause/context) in the stack trace.
import re import re
lines_and_names = re.findall(r',\sline\s(\d+),\sin\s([\w|<|>]+)', body.details.stackTrace) lines_and_names = re.findall(r',\sline\s(\d+),\sin\s(\[Chained Exception\]\s)?([\w|<|>]+)', body.details.stackTrace)
assert lines_and_names == [ assert lines_and_names == [
('16', 'foobar'), ('6', 'method'), ('2', 'method2'), ('18', 'foobar'), ('10', 'handle'), ('20', 'foobar'), ('23', '<module>') ('16', '', 'foobar'),
] ('6', '', 'method'),
('2', '', 'method2'),
('18', '', 'foobar'),
('10', '', 'handle'),
('20', '', 'foobar'),
('23', '', '<module>'),
], 'Did not find the expected names in:\n%s' % (body.details.stackTrace,)
json_facade.write_continue() json_facade.write_continue()
@ -802,7 +816,12 @@ def test_case_throw_exc_reason_shown(case_setup):
stack_frames = json_hit.stack_trace_response.body.stackFrames stack_frames = json_hit.stack_trace_response.body.stackFrames
# Note that the additional context doesn't really appear in the stack # Note that the additional context doesn't really appear in the stack
# frames, only in the details. # frames, only in the details.
assert [x['name'] for x in stack_frames] == ['method', '<module>'] assert [x['name'] for x in stack_frames] == [
'method',
'<module>',
"[Chained Exc: 'foo'] method",
"[Chained Exc: 'foo'] method2",
]
body = exc_info_response.body body = exc_info_response.body
assert body.exceptionId == 'Exception' assert body.exceptionId == 'Exception'
@ -3323,7 +3342,7 @@ def test_exception_details(case_setup, max_frames):
else: else:
json_facade.write_launch(maxExceptionStackFrames=max_frames) json_facade.write_launch(maxExceptionStackFrames=max_frames)
min_expected_lines = 10 min_expected_lines = 10
max_expected_lines = 21 max_expected_lines = 22
json_facade.write_set_exception_breakpoints(['raised']) json_facade.write_set_exception_breakpoints(['raised'])

View file

@ -0,0 +1,34 @@
import sys
from _pydevd_bundle.pydevd_constants import EXCEPTION_TYPE_USER_UNHANDLED
def test_create_frames_list_from_traceback():
def method():
raise RuntimeError('first')
def method1():
try:
method()
except Exception as e:
raise RuntimeError('second') from e
def method2():
try:
method1()
except Exception as e:
raise RuntimeError('third') from e
try:
method2()
except Exception as e:
exc_type, exc_desc, trace_obj = sys.exc_info()
frame = sys._getframe()
from _pydevd_bundle.pydevd_frame_utils import create_frames_list_from_traceback
frames_list = create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exception_type=EXCEPTION_TYPE_USER_UNHANDLED)
assert str(frames_list.exc_desc) == 'third'
assert str(frames_list.chained_frames_list.exc_desc) == 'second'
assert str(frames_list.chained_frames_list.chained_frames_list.exc_desc) == 'first'
assert frames_list.chained_frames_list.chained_frames_list.chained_frames_list is None

View file

@ -354,7 +354,7 @@ def test_exception_stack(pyfile, target, run, max_frames):
max_frames, (min_expected_lines, max_expected_lines) = { max_frames, (min_expected_lines, max_expected_lines) = {
"all": (0, (100, 221)), "all": (0, (100, 221)),
"default": (None, (100, 221)), "default": (None, (100, 221)),
10: (10, (10, 21)), 10: (10, (10, 22)),
}[max_frames] }[max_frames]
if max_frames is not None: if max_frames is not None:
session.config["maxExceptionStackFrames"] = max_frames session.config["maxExceptionStackFrames"] = max_frames