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:
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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'])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) = {
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue