diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 0436254db..5ffc2c36f 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -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) diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index d1275c5f3..639bed2ba 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -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: @@ -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 '' diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index d8f9941f7..b7cab17c2 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -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) @@ -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): ''' diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_large_exception_stack.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_large_exception_stack.py new file mode 100644 index 000000000..ecd0d7188 --- /dev/null +++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_large_exception_stack.py @@ -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!') \ No newline at end of file diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py index a784fd34b..7a4c706c5 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py @@ -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() @@ -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) diff --git a/tests/func/test_exception.py b/tests/func/test_exception.py index 5a1a241c8..2d9e1a15e 100644 --- a/tests/func/test_exception.py +++ b/tests/func/test_exception.py @@ -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']) @@ -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() diff --git a/tests/helpers/session.py b/tests/helpers/session.py index 897326f70..fd454334b 100644 --- a/tests/helpers/session.py +++ b/tests/helpers/session.py @@ -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 @@ -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', []) @@ -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,