mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Show chained exception frames in stack. Fixes #1042
This commit is contained in:
parent
c300080c62
commit
349ff7337b
8 changed files with 129 additions and 69 deletions
|
|
@ -1420,7 +1420,6 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th
|
|||
try:
|
||||
try:
|
||||
frames_list = dbg.suspended_frames_manager.get_frames_list(thread_id)
|
||||
memo = set()
|
||||
while frames_list is not None and len(frames_list):
|
||||
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_lst.append(stack_str)
|
||||
|
||||
frames_list = create_frames_list_from_exception_cause(
|
||||
frames_list.trace_obj, None, frames_list.exc_type, frames_list.exc_desc, memo)
|
||||
frames_list = frames_list.chained_frames_list
|
||||
if frames_list is None or not frames_list:
|
||||
break
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,8 @@ class FramesList(object):
|
|||
# This is to know whether an exception was extracted from a __cause__ or __context__.
|
||||
self.exc_context_msg = ''
|
||||
|
||||
self.chained_frames_list = None
|
||||
|
||||
def append(self, frame):
|
||||
self._frames.append(frame)
|
||||
|
||||
|
|
@ -128,7 +130,13 @@ class FramesList(object):
|
|||
lst.append('\n ')
|
||||
lst.append(repr(frame))
|
||||
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)')
|
||||
|
||||
return ''.join(lst)
|
||||
|
||||
__str__ = __repr__
|
||||
|
|
@ -142,7 +150,8 @@ class _DummyFrameWrapper(object):
|
|||
self.f_back = f_back
|
||||
self.f_trace = None
|
||||
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
|
||||
def f_locals(self):
|
||||
|
|
@ -152,6 +161,11 @@ class _DummyFrameWrapper(object):
|
|||
def f_globals(self):
|
||||
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 = (
|
||||
"\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))
|
||||
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
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ class NetCommandFactoryJson(NetCommandFactory):
|
|||
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(
|
||||
py_db, frames_list
|
||||
py_db, frames_list, flatten_chained=True
|
||||
):
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -164,33 +164,46 @@ class NetCommandFactory(object):
|
|||
except:
|
||||
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
|
||||
for frame in frames_list:
|
||||
show_as_current_frame = frame is frames_list.current_frame
|
||||
if frame.f_code is None:
|
||||
pydev_log.info('Frame without f_code: %s', frame)
|
||||
continue # IronPython sometimes does not have it!
|
||||
is_chained = False
|
||||
while True:
|
||||
for frame in frames_list:
|
||||
show_as_current_frame = frame is frames_list.current_frame
|
||||
if frame.f_code is None:
|
||||
pydev_log.info('Frame without f_code: %s', frame)
|
||||
continue # IronPython sometimes does not have it!
|
||||
|
||||
method_name = frame.f_code.co_name # method name (if in method) or ? if global
|
||||
if method_name is None:
|
||||
pydev_log.info('Frame without co_name: %s', frame)
|
||||
continue # IronPython sometimes does not have it!
|
||||
method_name = frame.f_code.co_name # method name (if in method) or ? if global
|
||||
if method_name is None:
|
||||
pydev_log.info('Frame without co_name: %s', frame)
|
||||
continue # IronPython sometimes does not have it!
|
||||
|
||||
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:
|
||||
# Skip pydevd files.
|
||||
frame = frame.f_back
|
||||
continue
|
||||
if is_chained:
|
||||
method_name = '[Chained Exc: %s] %s' % (frames_list.exc_desc, method_name)
|
||||
|
||||
frame_id = id(frame)
|
||||
lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno)
|
||||
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:
|
||||
# Skip pydevd files.
|
||||
frame = frame.f_back
|
||||
continue
|
||||
|
||||
filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno)
|
||||
new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8)
|
||||
applied_mapping = applied_mapping or changed
|
||||
frame_id = id(frame)
|
||||
lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno)
|
||||
|
||||
yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame
|
||||
filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno)
|
||||
new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8)
|
||||
applied_mapping = applied_mapping or changed
|
||||
|
||||
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):
|
||||
assert frames_list.__class__ == FramesList
|
||||
|
|
@ -200,7 +213,7 @@ class NetCommandFactory(object):
|
|||
|
||||
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(
|
||||
py_db, frames_list
|
||||
py_db, frames_list, flatten_chained=True
|
||||
):
|
||||
|
||||
# print("file is ", filename_in_utf8)
|
||||
|
|
|
|||
|
|
@ -1657,13 +1657,13 @@ def test_case_throw_exc_reason_xml(case_setup):
|
|||
name_and_lines.append((frame['name'], frame['line']))
|
||||
|
||||
assert name_and_lines == [
|
||||
('method2', '2'),
|
||||
('method', '6'),
|
||||
('foobar', '16'),
|
||||
('handle', '10'),
|
||||
('foobar', '18'),
|
||||
('foobar', '20'),
|
||||
('<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)
|
||||
|
|
|
|||
|
|
@ -751,7 +751,15 @@ def test_case_throw_exc_reason(case_setup):
|
|||
stack_frames = json_hit.stack_trace_response.body.stackFrames
|
||||
# Note that the additional context doesn't really appear in the stack
|
||||
# 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
|
||||
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.
|
||||
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 == [
|
||||
('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()
|
||||
|
||||
|
|
@ -802,7 +816,12 @@ def test_case_throw_exc_reason_shown(case_setup):
|
|||
stack_frames = json_hit.stack_trace_response.body.stackFrames
|
||||
# Note that the additional context doesn't really appear in the stack
|
||||
# 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
|
||||
assert body.exceptionId == 'Exception'
|
||||
|
|
@ -3323,7 +3342,7 @@ def test_exception_details(case_setup, max_frames):
|
|||
else:
|
||||
json_facade.write_launch(maxExceptionStackFrames=max_frames)
|
||||
min_expected_lines = 10
|
||||
max_expected_lines = 21
|
||||
max_expected_lines = 22
|
||||
|
||||
json_facade.write_set_exception_breakpoints(['raised'])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -354,7 +354,7 @@ def test_exception_stack(pyfile, target, run, max_frames):
|
|||
max_frames, (min_expected_lines, max_expected_lines) = {
|
||||
"all": (0, (100, 221)),
|
||||
"default": (None, (100, 221)),
|
||||
10: (10, (10, 21)),
|
||||
10: (10, (10, 22)),
|
||||
}[max_frames]
|
||||
if max_frames is not None:
|
||||
session.config["maxExceptionStackFrames"] = max_frames
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue