Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
Add setting to limit number of lines in stack for exception details. F…
Browse files Browse the repository at this point in the history
…ixes #582 (#1309)

* Add maxExcpetionStackFrames setting

* Tests for maxExcpetionStackFrames setting

* Set default as all frames

* Address comments

* Fix merge issue
  • Loading branch information
karthiknadig authored Apr 4, 2019
1 parent 911f9f5 commit 32e4f0e
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 12 deletions.
3 changes: 2 additions & 1 deletion src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,13 @@ def request_stack(self, py_db, seq, thread_id, fmt=None, timeout=.5):
else:
py_db.post_internal_command(internal_get_thread_stack, '*')

def request_exception_info_json(self, py_db, request, thread_id):
def request_exception_info_json(self, py_db, request, thread_id, max_frames):
py_db.post_method_as_internal_command(
thread_id,
internal_get_exception_details_json,
request,
thread_id,
max_frames,
set_additional_thread_info=set_additional_thread_info,
iter_visible_frames_info=py_db.cmd_factory._iter_visible_frames_info)

Expand Down
4 changes: 2 additions & 2 deletions src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,7 @@ def internal_get_description(dbg, seq, thread_id, frame_id, expression):
dbg.writer.add_command(cmd)


def internal_get_exception_details_json(dbg, request, thread_id, set_additional_thread_info=None, iter_visible_frames_info=None):
def internal_get_exception_details_json(dbg, request, thread_id, max_frames, set_additional_thread_info=None, iter_visible_frames_info=None):
''' Fetch exception details
'''
try:
Expand Down Expand Up @@ -1143,7 +1143,7 @@ def internal_get_exception_details_json(dbg, request, thread_id, set_additional_
except:
pass

stack_str = ''.join(traceback.format_list(frames))
stack_str = ''.join(traceback.format_list(frames[-max_frames:]))

# This is an extra bit of data used by Visual Studio
source_path = frames[0][0] if frames else ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ def _set_debug_options(self, py_db, args):
args.get('options'),
args.get('debugOptions'),
)
self._debug_options['args'] = args

debug_stdlib = self._debug_options.get('DEBUG_STDLIB', False)
self.api.set_use_libraries_filter(py_db, not debug_stdlib)

Expand Down Expand Up @@ -491,8 +493,9 @@ def on_exceptioninfo_request(self, py_db, request):
'''
# : :type exception_into_arguments: ExceptionInfoArguments
exception_into_arguments = request.arguments
thread_id = exception_into_arguments.threadId
self.api.request_exception_info_json(py_db, request, thread_id)
thread_id = exception_into_arguments.threadId
max_frames = int(self._debug_options['args'].get('maxExceptionStackFrames', 0))
self.api.request_exception_info_json(py_db, request, thread_id, max_frames)

def on_scopes_request(self, py_db, request):
'''
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def method1(n):
if n <= 0:
raise IndexError('foo')
method2(n-1)

def method2(n):
method1(n-1)

if __name__ == '__main__':
method1(100)
print('TEST SUCEEDED!')
23 changes: 21 additions & 2 deletions src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,11 +1140,28 @@ def test_evaluate(case_setup):
writer.finished_ok = True


def test_exception_details(case_setup):
with case_setup.test_file('_debugger_case_exceptions.py') as writer:
@pytest.mark.parametrize('max_frames', ['default', 'all', 10]) # -1 = default, 0 = all, 10 = 10 frames
def test_exception_details(case_setup, max_frames):
with case_setup.test_file('_debugger_case_large_exception_stack.py') as writer:
json_facade = JsonFacade(writer)

writer.write_set_protocol('http_json')
if max_frames == 'all':
json_facade.write_launch(maxExceptionStackFrames=0)
# trace back compresses repeated text
min_expected_lines = 100
max_expected_lines = 220
elif max_frames == 'default':
json_facade.write_launch()
# default is all frames
# trace back compresses repeated text
min_expected_lines = 100
max_expected_lines = 220
else:
json_facade.write_launch(maxExceptionStackFrames=max_frames)
min_expected_lines = 10
max_expected_lines = 21

json_facade.write_set_exception_breakpoints(['raised'])

json_facade.write_make_initial_run()
Expand All @@ -1157,6 +1174,8 @@ def test_exception_details(case_setup):
assert body.exceptionId.endswith('IndexError')
assert body.description == 'foo'
assert body.details.kwargs['source'] == writer.TEST_FILE
stack_line_count = len(body.details.stackTrace.split('\n'))
assert min_expected_lines <= stack_line_count <= max_expected_lines

json_facade.write_set_exception_breakpoints([]) # Don't stop on reraises.
writer.write_run_thread(hit.thread_id)
Expand Down
75 changes: 74 additions & 1 deletion tests/func/test_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tests.helpers import print, get_marked_line_numbers
from tests.helpers.session import DebugSession
from tests.helpers.timeline import Event
from tests.helpers.pattern import ANY, Path
from tests.helpers.pattern import ANY, Path, Regex


@pytest.mark.parametrize('raised', ['raisedOn', 'raisedOff'])
Expand Down Expand Up @@ -344,3 +344,76 @@ def code_to_debug():
session.send_request('continue').wait_for_response(freeze=False)

session.wait_for_exit()


@pytest.mark.parametrize('max_frames', ['default', 'all', 10])
def test_exception_stack(pyfile, run_as, start_method, max_frames):
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import_and_enable_debugger()
def do_something(n):
if n <= 0:
raise ArithmeticError('bad code') # @unhandled
do_something2(n - 1)

def do_something2(n):
do_something(n-1)

do_something(100)

if max_frames == 'all':
# trace back compresses repeated text
min_expected_lines = 100
max_expected_lines = 220
args = {'maxExceptionStackFrames': 0}
elif max_frames == 'default':
# default is all frames
min_expected_lines = 100
max_expected_lines = 220
args = {}
else:
min_expected_lines = 10
max_expected_lines = 21
args = {'maxExceptionStackFrames': 10}

line_numbers = get_marked_line_numbers(code_to_debug)
with DebugSession() as session:
session.initialize(
target=(run_as, code_to_debug),
start_method=start_method,
ignore_unobserved=[Event('continued')],
expected_returncode=ANY.int,
args=args,
)
session.send_request('setExceptionBreakpoints', {
'filters': ['uncaught']
}).wait_for_response()
session.start_debugging()

hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
assert frames[0]['line'] == line_numbers['unhandled']

resp_exc_info = session.send_request('exceptionInfo', {
'threadId': hit.thread_id
}).wait_for_response()

expected = ANY.dict_with({
'exceptionId': Regex('ArithmeticError'),
'description': 'bad code',
'breakMode': 'unhandled',
'details': ANY.dict_with({
'typeName': Regex('ArithmeticError'),
'message': 'bad code',
'source': Path(code_to_debug),
}),
})
assert resp_exc_info.body == expected
stack_str = resp_exc_info.body['details']['stackTrace']
stack_line_count = len(stack_str.split('\n'))
assert min_expected_lines <= stack_line_count <= max_expected_lines

session.send_request('continue').wait_for_response(freeze=False)

session.wait_for_exit()
10 changes: 6 additions & 4 deletions tests/helpers/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self, start_method='launch', ptvsd_port=None, pid=None):

self.target = ('code', 'print("OK")')
self.start_method = start_method
self.start_method_args = {}
self.no_debug = False
self.ptvsd_port = ptvsd_port or PTVSD_PORT
self.multiprocess = False
Expand Down Expand Up @@ -230,6 +231,7 @@ def _setup_session(self, **kwargs):
] + kwargs.pop('ignore_unobserved', [])

self.env.update(kwargs.pop('env', {}))
self.start_method_args.update(kwargs.pop('args', {}))

self.path_mappings += kwargs.pop('path_mappings', [])
self.debug_options += kwargs.pop('debug_options', [])
Expand Down Expand Up @@ -497,14 +499,14 @@ def handshake(self):
self.wait_for_next(Event('initialized', {}))

request = 'launch' if self.start_method == 'launch' else 'attach'
args = {
self.start_method_args.update({
'debugOptions': self.debug_options,
'pathMappings': self.path_mappings,
'rules': self.rules,
}
})
if self.success_exitcodes is not None:
args['successExitCodes'] = self.success_exitcodes
self.send_request(request, args).wait_for_response()
self.start_method_args['successExitCodes'] = self.success_exitcodes
self.send_request(request, self.start_method_args).wait_for_response()

if not self.no_debug:
# Issue 'threads' so that we get the 'thread' event for the main thread now,
Expand Down

0 comments on commit 32e4f0e

Please sign in to comment.