Use unhandled exception line when creating stack. Fixes #814 (#839)

This commit is contained in:
Fabio Zadrozny 2018-09-25 22:17:27 -03:00 committed by Karthik Nadig
parent d03a2b044c
commit 62f03a2760
6 changed files with 86 additions and 25 deletions

View file

@ -769,7 +769,15 @@ class NetCommandFactory:
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 @@ class NetCommandFactory:
# 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 @@ class NetCommandFactory:
stop_reason=None,
message=None,
suspend_type="trace",
frame_to_lineno=None
):
"""
:return tuple(str,str):
@ -860,17 +869,17 @@ class NetCommandFactory:
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

View file

@ -790,10 +790,14 @@ class PyDB:
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 @@ class PyDB:
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 @@ class PyDB:
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:

View file

@ -284,18 +284,6 @@ class DebuggerRunner(object):
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 @@ class AbstractWriterThread(threading.Thread):
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 @@ class AbstractWriterThread(threading.Thread):
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 @@ class AbstractWriterThread(threading.Thread):
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

View file

@ -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')

View file

@ -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:
@ -1739,7 +1766,7 @@ def test_case_get_thread_stack(case_setup):
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'):

View file

@ -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):