From 62f03a27601cca6e2a2ed222f81164eea79ff302 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Tue, 25 Sep 2018 22:17:27 -0300 Subject: [PATCH] Use unhandled exception line when creating stack. Fixes #814 (#839) --- .../pydevd/_pydevd_bundle/pydevd_comm.py | 19 ++++++++---- ptvsd/_vendored/pydevd/pydevd.py | 18 ++++++++++-- .../pydevd/tests_python/debugger_unittest.py | 27 ++++++++--------- ...gger_case_unhandled_exception_get_stack.py | 12 ++++++++ .../pydevd/tests_python/test_debugger.py | 29 ++++++++++++++++++- .../tests_python/test_tracing_on_top_level.py | 6 ++-- 6 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_unhandled_exception_get_stack.py diff --git a/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 5d4eea2a7..2ed1e5890 100644 --- a/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -769,7 +769,15 @@ def make_thread_killed_message(self, id): except: return self.make_error_message(0, get_exception_traceback_str()) - def make_thread_stack_str(self, frame): + def make_thread_stack_str(self, frame, frame_to_lineno=None): + ''' + :param frame_to_lineno: + If available, the line number for the frame will be gotten from this dict, + otherwise frame.f_lineno will be used (needed for unhandled exceptions as + the place where we report may be different from the place where it's raised). + ''' + if frame_to_lineno is None: + frame_to_lineno = {} make_valid_xml_value = pydevd_xml.make_valid_xml_value cmd_text_list = [] append = cmd_text_list.append @@ -801,7 +809,7 @@ def make_thread_stack_str(self, frame): # print("file is ", filename_in_utf8) - lineno = str(curr_frame.f_lineno) + lineno = frame_to_lineno.get(curr_frame, curr_frame.f_lineno) # print("line is ", lineno) # Note: variables are all gotten 'on-demand'. @@ -822,6 +830,7 @@ def make_thread_suspend_str( stop_reason=None, message=None, suspend_type="trace", + frame_to_lineno=None ): """ :return tuple(str,str): @@ -860,17 +869,17 @@ def make_thread_suspend_str( if suspend_type is not None: append(' suspend_type="%s"' % (suspend_type,)) append('>') - thread_stack_str = self.make_thread_stack_str(frame) + thread_stack_str = self.make_thread_stack_str(frame, frame_to_lineno) append(thread_stack_str) append("") return ''.join(cmd_text_list), thread_stack_str - def make_thread_suspend_message(self, thread_id, frame, stop_reason, message, suspend_type): + def make_thread_suspend_message(self, thread_id, frame, stop_reason, message, suspend_type, frame_to_lineno=None): try: thread_suspend_str, thread_stack_str = self.make_thread_suspend_str( - thread_id, frame, stop_reason, message, suspend_type) + thread_id, frame, stop_reason, message, suspend_type, frame_to_lineno=frame_to_lineno) cmd = NetCommand(CMD_THREAD_SUSPEND, 0, thread_suspend_str) cmd.thread_stack_str = thread_stack_str return cmd diff --git a/ptvsd/_vendored/pydevd/pydevd.py b/ptvsd/_vendored/pydevd/pydevd.py index 41fee9dc3..ae00f8367 100644 --- a/ptvsd/_vendored/pydevd/pydevd.py +++ b/ptvsd/_vendored/pydevd/pydevd.py @@ -790,10 +790,14 @@ def cancel_async_evaluation(self, thread_id, frame_id): finally: self._main_lock.release() - def do_wait_suspend(self, thread, frame, event, arg, suspend_type="trace", send_suspend_message=True): #@UnusedVariable + def do_wait_suspend(self, thread, frame, event, arg, suspend_type="trace", send_suspend_message=True, is_unhandled_exception=False): #@UnusedVariable """ busy waits until the thread state changes to RUN it expects thread's state as attributes of the thread. Upon running, processes any outstanding Stepping commands. + + :param is_unhandled_exception: + If True we should use the line of the exception instead of the current line in the frame + as the paused location on the top-level frame (exception info must be passed on 'arg'). """ self.process_internal_commands() thread_stack_str = '' # @UnusedVariable -- this is here so that `make_get_thread_stack_message` @@ -801,7 +805,15 @@ def do_wait_suspend(self, thread, frame, event, arg, suspend_type="trace", send_ if send_suspend_message: message = thread.additional_info.pydev_message - cmd = self.cmd_factory.make_thread_suspend_message(get_thread_id(thread), frame, thread.stop_reason, message, suspend_type) + frame_to_lineno = {} + if is_unhandled_exception: + # arg must be the exception info (tuple(exc_type, exc, traceback)) + tb = arg[2] + while tb is not None: + frame_to_lineno[tb.tb_frame] = tb.tb_lineno + tb = tb.tb_next + cmd = self.cmd_factory.make_thread_suspend_message(get_thread_id(thread), frame, thread.stop_reason, message, suspend_type, frame_to_lineno=frame_to_lineno) + frame_to_lineno.clear() thread_stack_str = cmd.thread_stack_str # @UnusedVariable -- `make_get_thread_stack_message` uses it later. self.writer.add_command(cmd) @@ -926,7 +938,7 @@ def stop_on_unhandled_exception(self, thread, frame, frames_byid, arg): try: add_exception_to_frame(frame, arg) self.set_suspend(thread, CMD_ADD_EXCEPTION_BREAK) - self.do_wait_suspend(thread, frame, 'exception', arg, "trace") + self.do_wait_suspend(thread, frame, 'exception', arg, "trace", is_unhandled_exception=True) except: pydev_log.error("We've got an error while stopping in post-mortem: %s\n" % (arg[0],)) finally: diff --git a/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py b/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py index 41fa3b7a6..312e29a7e 100644 --- a/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py +++ b/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py @@ -284,18 +284,6 @@ def add_command_line_args(self, args): ret = writer.update_command_line_args(ret) # Provide a hook for the writer return args + ret - def check_case_simple(self, target, test_file): - - class WriterThreadCase(AbstractWriterThread): - - TEST_FILE = _get_debugger_test_file(test_file) - - def run(self): - return target(self) - - with self.check_case(WriterThreadCase) as writer: - yield writer - @contextmanager def check_case(self, writer_class): if callable(writer_class): @@ -503,6 +491,16 @@ def get_pydevd_file(self): dirname = os.path.dirname(dirname) return os.path.abspath(os.path.join(dirname, 'pydevd.py')) + def get_line_index_with_content(self, line_content): + ''' + :return the line index which has the given content (1-based). + ''' + with open(self.TEST_FILE, 'r') as stream: + for i_line, line in enumerate(stream): + if line_content in line: + return i_line + 1 + raise AssertionError('Did not find: %s in %s' % (line_content, self.TEST_FILE)) + def get_cwd(self): return os.path.dirname(self.get_pydevd_file()) @@ -524,7 +522,7 @@ def write(self, s): meaning = ID_TO_MEANING.get(re.search(r'\d+', s).group(), '') if meaning: meaning += ': ' - + self.log.append('write: %s%s' % (meaning, s,)) if SHOW_WRITES_AND_READS: @@ -885,6 +883,9 @@ def write_list_threads(self): def wait_for_list_threads(self, seq): return self.wait_for_message(lambda msg:msg.startswith('502\t%s' % (seq,))) + def wait_for_get_thread_stack_message(self): + return self.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_THREAD_STACK,))) + def wait_for_message(self, accept_message, unquote_msg=True, expect_xml=True): import untangle from io import StringIO diff --git a/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_unhandled_exception_get_stack.py b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_unhandled_exception_get_stack.py new file mode 100644 index 000000000..e013693c8 --- /dev/null +++ b/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_unhandled_exception_get_stack.py @@ -0,0 +1,12 @@ +from contextlib import contextmanager + + +@contextmanager +def something(): + yield + + +with something(): + raise ValueError('TEST SUCEEDED') # break line on unhandled exception + print('a') + print('b') diff --git a/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/ptvsd/_vendored/pydevd/tests_python/test_debugger.py index e06c85acb..7a3a660e9 100644 --- a/ptvsd/_vendored/pydevd/tests_python/test_debugger.py +++ b/ptvsd/_vendored/pydevd/tests_python/test_debugger.py @@ -1212,6 +1212,33 @@ def test_case_set_next_statement(case_setup): writer.finished_ok = True +def test_unhandled_exceptions_get_stack(case_setup_unhandled_exceptions): + + with case_setup_unhandled_exceptions.test_file( + '_debugger_case_unhandled_exception_get_stack.py') as writer: + + writer.write_add_exception_breakpoint_with_policy('Exception', "0", "1", "0") + writer.write_make_initial_run() + + hit = writer.wait_for_breakpoint_hit(REASON_UNCAUGHT_EXCEPTION) + writer.write_get_thread_stack(hit.thread_id) + + msg = writer.wait_for_get_thread_stack_message() + files = [frame['file'] for frame in msg.thread.frame] + assert msg.thread['id'] == hit.thread_id + if not files[0].endswith('_debugger_case_unhandled_exception_get_stack.py'): + raise AssertionError('Expected to find _debugger_case_unhandled_exception_get_stack.py in files[0]. Found: %s' % ('\n'.join(files),)) + + assert len(msg.thread.frame) == 0 # No back frames (stopped in main). + assert msg.thread.frame['name'] == '' + assert msg.thread.frame['line'] == str(writer.get_line_index_with_content('break line on unhandled exception')) + + writer.write_run_thread(hit.thread_id) + + writer.log.append('Marking finished ok.') + writer.finished_ok = True + + @pytest.mark.skipif(not IS_CPYTHON, reason='Only for Python.') def test_case_get_next_statement_targets(case_setup): with case_setup.test_file('_debugger_case_get_next_statement_targets.py') as writer: @@ -1739,7 +1766,7 @@ def _ignore_stderr_line(line): for request_thread_id in thread_id_to_name: writer.write_get_thread_stack(request_thread_id) - msg = writer.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_THREAD_STACK,))) + msg = writer.wait_for_get_thread_stack_message() files = [frame['file'] for frame in msg.thread.frame] assert msg.thread['id'] == request_thread_id if not files[0].endswith('_debugger_case_get_thread_stack.py'): diff --git a/ptvsd/_vendored/pydevd/tests_python/test_tracing_on_top_level.py b/ptvsd/_vendored/pydevd/tests_python/test_tracing_on_top_level.py index fb703254e..f4db3e1e3 100644 --- a/ptvsd/_vendored/pydevd/tests_python/test_tracing_on_top_level.py +++ b/ptvsd/_vendored/pydevd/tests_python/test_tracing_on_top_level.py @@ -39,15 +39,15 @@ class DummyPyDb(PyDB): def __init__(self): PyDB.__init__(self, set_as_global=False) - def do_wait_suspend(self, thread, frame, event, arg, suspend_type="trace", send_suspend_message=True): + def do_wait_suspend( + self, thread, frame, event, arg, *args, **kwargs): from _pydevd_bundle.pydevd_constants import STATE_RUN info = thread.additional_info info.pydev_step_cmd = -1 info.pydev_step_stop = None info.pydev_state = STATE_RUN - return PyDB.do_wait_suspend( - self, thread, frame, event, arg, suspend_type=suspend_type, send_suspend_message=send_suspend_message) + return PyDB.do_wait_suspend(self, thread, frame, event, arg, *args, **kwargs) class _TraceTopLevel(object):