Skip to content

Commit

Permalink
Use unhandled exception line when creating stack. Fixes microsoft#814
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Sep 25, 2018
1 parent d03a2b0 commit 9f53068
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 25 deletions.
19 changes: 14 additions & 5 deletions ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'.
Expand All @@ -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):
Expand Down Expand Up @@ -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("</thread></xml>")

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
Expand Down
18 changes: 15 additions & 3 deletions ptvsd/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,18 +790,30 @@ 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`
# can retrieve it later.

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)

Expand Down Expand Up @@ -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:
Expand Down
27 changes: 14 additions & 13 deletions ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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())

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
29 changes: 28 additions & 1 deletion ptvsd/_vendored/pydevd/tests_python/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'] == '<module>'
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:
Expand Down Expand Up @@ -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'):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 9f53068

Please sign in to comment.