Allow configuring whether the debugger should stop/print errors when there's an exception evaluating a breakpoint condition. Fixes #853 (#876)

* Allow configuring whether the debugger should stop/print errors when there's an exception evaluating a breakpoint condition. Fixes #853

* Temporarily remove CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION configuration (needs test fixes to be applied).

* Fix linting.
This commit is contained in:
Fabio Zadrozny 2018-10-05 17:48:43 -03:00 committed by Karthik Nadig
parent 8fd2c74cd4
commit fcdb060b76
8 changed files with 179 additions and 56 deletions

View file

@ -17,7 +17,7 @@ class ExceptionBreakpoint(object):
notify_on_first_raise_only,
ignore_libraries
):
exctype = _get_class(qname)
exctype = get_exception_class(qname)
self.qname = qname
if exctype is not None:
self.name = exctype.__name__
@ -156,7 +156,7 @@ def stop_on_unhandled_exception(py_db, thread, additional_info, arg):
py_db.stop_on_unhandled_exception(thread, frame, frames_byid, arg)
def _get_class(kls):
def get_exception_class(kls):
if IS_PY24 and "BaseException" == kls:
kls = "Exception"

View file

@ -185,6 +185,8 @@ CMD_STOP_ON_START = 154
# When the debugger is stopped in an exception, this command will provide the details of the current exception (in the current thread).
CMD_GET_EXCEPTION_DETAILS = 155
CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION = 156
CMD_REDIRECT_OUTPUT = 200
CMD_GET_NEXT_STATEMENT_TARGETS = 201
CMD_SET_PROJECT_ROOTS = 202
@ -252,6 +254,7 @@ ID_TO_MEANING = {
'153': 'CMD_THREAD_DUMP_TO_STDERR',
'154': 'CMD_STOP_ON_START',
'155': 'CMD_GET_EXCEPTION_DETAILS',
'156': 'CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION',
'200': 'CMD_REDIRECT_OUTPUT',
'201': 'CMD_GET_NEXT_STATEMENT_TARGETS',

View file

@ -20,6 +20,7 @@ try:
from inspect import CO_GENERATOR
except:
CO_GENERATOR = 0
from _pydevd_bundle.pydevd_constants import IS_PY2
try:
from _pydevd_bundle.pydevd_signature import send_signature_call_trace, send_signature_return_trace
@ -51,34 +52,37 @@ def handle_breakpoint_condition(py_db, info, breakpoint, new_frame):
return False
return eval(condition, new_frame.f_globals, new_frame.f_locals)
except:
if type(condition) != type(''):
if hasattr(condition, 'encode'):
except Exception as e:
if IS_PY2:
# Must be bytes on py2.
if isinstance(condition, unicode):
condition = condition.encode('utf-8')
msg = 'Error while evaluating expression: %s\n' % (condition,)
sys.stderr.write(msg)
traceback.print_exc()
if not py_db.suspend_on_breakpoint_exception:
return False
else:
if not isinstance(e, py_db.skip_print_breakpoint_exception):
sys.stderr.write('Error while evaluating expression: %s\n' % (condition,))
etype, value, tb = sys.exc_info()
traceback.print_exception(etype, value, tb.tb_next)
if not isinstance(e, py_db.skip_suspend_on_breakpoint_exception):
try:
# add exception_type and stacktrace into thread additional info
etype, value, tb = sys.exc_info()
try:
error = ''.join(traceback.format_exception_only(etype, value))
stack = traceback.extract_stack(f=tb.tb_frame.f_back)
error = ''.join(traceback.format_exception_only(etype, value))
stack = traceback.extract_stack(f=tb.tb_frame.f_back)
# On self.set_suspend(thread, CMD_SET_BREAK) this info will be
# sent to the client.
info.conditional_breakpoint_exception = \
('Condition:\n' + condition + '\n\nError:\n' + error, stack)
finally:
etype, value, tb = None, None, None
# On self.set_suspend(thread, CMD_SET_BREAK) this info will be
# sent to the client.
info.conditional_breakpoint_exception = \
('Condition:\n' + condition + '\n\nError:\n' + error, stack)
except:
traceback.print_exc()
return True
return False
finally:
etype, value, tb = None, None, None
def handle_breakpoint_expression(breakpoint, info, new_frame):
@ -619,7 +623,7 @@ class PyDBFrame:
#
# As for lamdba, as it only has a single statement, it's not interesting to trace
# its call and later its line event as they're usually in the same line.
# No need to reset frame.f_trace to keep the same trace function.
return self.trace_dispatch

View file

@ -6,26 +6,27 @@ from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_traceproperty, pydevd_dont_trace, pydevd_utils
import pydevd_tracing
import pydevd_file_utils
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
from _pydevd_bundle.pydevd_comm import CMD_RUN, CMD_VERSION, CMD_LIST_THREADS, CMD_THREAD_KILL, InternalTerminateThread, \
CMD_THREAD_SUSPEND, pydevd_find_thread_by_id, CMD_THREAD_RUN, InternalRunThread, CMD_STEP_INTO, CMD_STEP_OVER, \
CMD_STEP_RETURN, CMD_STEP_INTO_MY_CODE, InternalStepThread, CMD_RUN_TO_LINE, CMD_SET_NEXT_STATEMENT, \
CMD_SMART_STEP_INTO, InternalSetNextStatementThread, CMD_RELOAD_CODE, ReloadCodeCommand, CMD_CHANGE_VARIABLE, \
InternalChangeVariable, CMD_GET_VARIABLE, InternalGetVariable, CMD_GET_ARRAY, InternalGetArray, CMD_GET_COMPLETIONS, \
InternalGetCompletions, CMD_GET_FRAME, InternalGetFrame, CMD_SET_BREAK, file_system_encoding, CMD_REMOVE_BREAK, \
CMD_EVALUATE_EXPRESSION, CMD_EXEC_EXPRESSION, InternalEvaluateExpression, CMD_CONSOLE_EXEC, InternalConsoleExec, \
CMD_SET_PY_EXCEPTION, CMD_GET_FILE_CONTENTS, CMD_SET_PROPERTY_TRACE, CMD_ADD_EXCEPTION_BREAK, \
CMD_REMOVE_EXCEPTION_BREAK, CMD_LOAD_SOURCE, CMD_ADD_DJANGO_EXCEPTION_BREAK, CMD_REMOVE_DJANGO_EXCEPTION_BREAK, \
CMD_EVALUATE_CONSOLE_EXPRESSION, InternalEvaluateConsoleExpression, InternalConsoleGetCompletions, \
CMD_RUN_CUSTOM_OPERATION, InternalRunCustomOperation, CMD_IGNORE_THROWN_EXCEPTION_AT, CMD_ENABLE_DONT_TRACE, \
CMD_SHOW_RETURN_VALUES, ID_TO_MEANING, CMD_GET_DESCRIPTION, InternalGetDescription, InternalLoadFullValue, \
CMD_LOAD_FULL_VALUE, CMD_REDIRECT_OUTPUT, CMD_GET_NEXT_STATEMENT_TARGETS, InternalGetNextStatementTargets, CMD_SET_PROJECT_ROOTS, \
CMD_GET_THREAD_STACK, CMD_THREAD_DUMP_TO_STDERR, CMD_STOP_ON_START, CMD_GET_EXCEPTION_DETAILS, NetCommand,\
CMD_SET_PROTOCOL
from _pydevd_bundle.pydevd_constants import get_thread_id, IS_PY3K, DebugInfoHolder, dict_keys, STATE_RUN, \
NEXT_VALUE_SEPARATOR, IS_WINDOWS
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint, get_exception_class
from _pydevd_bundle.pydevd_comm import (CMD_RUN, CMD_VERSION, CMD_LIST_THREADS, CMD_THREAD_KILL, InternalTerminateThread,
CMD_THREAD_SUSPEND, pydevd_find_thread_by_id, CMD_THREAD_RUN, InternalRunThread, CMD_STEP_INTO, CMD_STEP_OVER,
CMD_STEP_RETURN, CMD_STEP_INTO_MY_CODE, InternalStepThread, CMD_RUN_TO_LINE, CMD_SET_NEXT_STATEMENT,
CMD_SMART_STEP_INTO, InternalSetNextStatementThread, CMD_RELOAD_CODE, ReloadCodeCommand, CMD_CHANGE_VARIABLE,
InternalChangeVariable, CMD_GET_VARIABLE, InternalGetVariable, CMD_GET_ARRAY, InternalGetArray, CMD_GET_COMPLETIONS,
InternalGetCompletions, CMD_GET_FRAME, InternalGetFrame, CMD_SET_BREAK, file_system_encoding, CMD_REMOVE_BREAK,
CMD_EVALUATE_EXPRESSION, CMD_EXEC_EXPRESSION, InternalEvaluateExpression, CMD_CONSOLE_EXEC, InternalConsoleExec,
CMD_SET_PY_EXCEPTION, CMD_GET_FILE_CONTENTS, CMD_SET_PROPERTY_TRACE, CMD_ADD_EXCEPTION_BREAK,
CMD_REMOVE_EXCEPTION_BREAK, CMD_LOAD_SOURCE, CMD_ADD_DJANGO_EXCEPTION_BREAK, CMD_REMOVE_DJANGO_EXCEPTION_BREAK,
CMD_EVALUATE_CONSOLE_EXPRESSION, InternalEvaluateConsoleExpression, InternalConsoleGetCompletions,
CMD_RUN_CUSTOM_OPERATION, InternalRunCustomOperation, CMD_IGNORE_THROWN_EXCEPTION_AT, CMD_ENABLE_DONT_TRACE,
CMD_SHOW_RETURN_VALUES, ID_TO_MEANING, CMD_GET_DESCRIPTION, InternalGetDescription, InternalLoadFullValue,
CMD_LOAD_FULL_VALUE, CMD_REDIRECT_OUTPUT, CMD_GET_NEXT_STATEMENT_TARGETS, InternalGetNextStatementTargets, CMD_SET_PROJECT_ROOTS,
CMD_GET_THREAD_STACK, CMD_THREAD_DUMP_TO_STDERR, CMD_STOP_ON_START, CMD_GET_EXCEPTION_DETAILS, NetCommand,
CMD_SET_PROTOCOL, CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION)
from _pydevd_bundle.pydevd_constants import (get_thread_id, IS_PY3K, DebugInfoHolder, dict_keys, STATE_RUN,
NEXT_VALUE_SEPARATOR, IS_WINDOWS)
from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info
from _pydev_imps._pydev_saved_modules import threading
import json
def process_net_command(py_db, cmd_id, seq, text):
'''Processes a command received from the Java side
@ -819,6 +820,19 @@ def process_net_command(py_db, cmd_id, seq, text):
elif cmd_id == CMD_STOP_ON_START:
py_db.stop_on_start = text.strip() in ('True', 'true', '1')
elif cmd_id == CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION:
# Expected to receive a json string as:
# {
# 'skip_suspend_on_breakpoint_exception': [<exception names where we should suspend>]
# 'skip_print_breakpoint_exception': [<exception names where we should print>]
# }
msg = json.loads(text.strip())
py_db.skip_suspend_on_breakpoint_exception = tuple(
get_exception_class(x) for x in msg.get('skip_suspend_on_breakpoint_exception', ()))
py_db.skip_print_breakpoint_exception = tuple(
get_exception_class(x) for x in msg.get('skip_print_breakpoint_exception', ()))
elif cmd_id == CMD_GET_EXCEPTION_DETAILS:
thread_id = text

View file

@ -256,9 +256,10 @@ class PyDB:
self.skip_on_exceptions_thrown_in_same_context = False
self.ignore_exceptions_thrown_in_lines_with_ignore_exception = True
# Suspend debugger even if breakpoint condition raises an exception
SUSPEND_ON_BREAKPOINT_EXCEPTION = True
self.suspend_on_breakpoint_exception = SUSPEND_ON_BREAKPOINT_EXCEPTION
# Suspend debugger even if breakpoint condition raises an exception.
# May be changed with CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION.
self.skip_suspend_on_breakpoint_exception = () # By default suspend on any Exception.
self.skip_print_breakpoint_exception = () # By default print on any Exception.
# By default user can step into properties getter/setter/deleter methods
self.disable_property_trace = False
@ -711,22 +712,21 @@ class PyDB:
thread.stop_reason = stop_reason
# If conditional breakpoint raises any exception during evaluation send details to Java
if stop_reason == CMD_SET_BREAK and self.suspend_on_breakpoint_exception:
self._send_breakpoint_condition_exception(thread)
if stop_reason == CMD_SET_BREAK and info.conditional_breakpoint_exception is not None:
conditional_breakpoint_exception_tuple = info.conditional_breakpoint_exception
info.conditional_breakpoint_exception = None
self._send_breakpoint_condition_exception(thread, conditional_breakpoint_exception_tuple)
def _send_breakpoint_condition_exception(self, thread):
def _send_breakpoint_condition_exception(self, thread, conditional_breakpoint_exception_tuple):
"""If conditional breakpoint raises an exception during evaluation
send exception details to java
"""
thread_id = get_thread_id(thread)
conditional_breakpoint_exception_tuple = thread.additional_info.conditional_breakpoint_exception
# conditional_breakpoint_exception_tuple - should contain 2 values (exception_type, stacktrace)
if conditional_breakpoint_exception_tuple and len(conditional_breakpoint_exception_tuple) == 2:
exc_type, stacktrace = conditional_breakpoint_exception_tuple
int_cmd = InternalGetBreakpointException(thread_id, exc_type, stacktrace)
# Reset the conditional_breakpoint_exception details to None
thread.additional_info.conditional_breakpoint_exception = None
self.post_internal_command(int_cmd, thread_id)

View file

@ -1,5 +1,6 @@
from collections import namedtuple
from contextlib import contextmanager
import json
try:
from urllib import quote, quote_plus, unquote_plus
except ImportError:
@ -74,6 +75,7 @@ CMD_GET_THREAD_STACK = 152
CMD_THREAD_DUMP_TO_STDERR = 153 # This is mostly for unit-tests to diagnose errors on ci.
CMD_STOP_ON_START = 154
CMD_GET_EXCEPTION_DETAILS = 155
CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION = 156
CMD_REDIRECT_OUTPUT = 200
CMD_GET_NEXT_STATEMENT_TARGETS = 201
@ -736,29 +738,37 @@ class AbstractWriterThread(threading.Thread):
def write_version(self):
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
self.write("501\t%s\t1.0\t%s\tID" % (self.next_seq(), 'WINDOWS' if IS_WINDOWS else 'UNIX'))
self.write("%s\t%s\t1.0\t%s\tID" % (CMD_VERSION, self.next_seq(), 'WINDOWS' if IS_WINDOWS else 'UNIX'))
def get_main_filename(self):
return self.TEST_FILE
def write_add_breakpoint(self, line, func, filename=None, hit_condition=None, is_logpoint=False, suspend_policy=None):
def write_add_breakpoint(self, line, func, filename=None, hit_condition=None, is_logpoint=False, suspend_policy=None, condition=None):
'''
@param line: starts at 1
'''
if filename is None:
filename = self.get_main_filename()
breakpoint_id = self.next_breakpoint_id()
if hit_condition is None and not is_logpoint and suspend_policy is None:
if hit_condition is None and not is_logpoint and suspend_policy is None and condition is None:
# Format kept for backward compatibility tests
self.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\tNone\tNone" % (
CMD_SET_BREAK, self.next_seq(), breakpoint_id, 'python-line', filename, line, func))
else:
# Format: breakpoint_id, type, file, line, func_name, condition, expression, hit_condition, is_logpoint, suspend_policy
self.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\tNone\tNone\t%s\t%s\t%s" % (
CMD_SET_BREAK, self.next_seq(), breakpoint_id, 'python-line', filename, line, func, hit_condition, is_logpoint, suspend_policy))
self.write("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tNone\t%s\t%s\t%s" % (
CMD_SET_BREAK, self.next_seq(), breakpoint_id, 'python-line', filename, line, func, condition, hit_condition, is_logpoint, suspend_policy))
self.log.append('write_add_breakpoint: %s line: %s func: %s' % (breakpoint_id, line, func))
return breakpoint_id
def write_suspend_on_breakpoint_exception(self, skip_suspend_on_breakpoint_exception=('all',), skip_print_breakpoint_exception=('all',)):
self.write("%s\t%s\t%s" % (CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION, self.next_seq(),
json.dumps(dict(
skip_suspend_on_breakpoint_exception=skip_suspend_on_breakpoint_exception,
skip_print_breakpoint_exception=skip_print_breakpoint_exception
))
))
def write_stop_on_start(self, stop=True):
self.write("%s\t%s\t%s" % (CMD_STOP_ON_START, self.next_seq(), stop))

View file

@ -0,0 +1,8 @@
def Call():
for i in range(10): # break here
last_i = i
if __name__ == '__main__':
Call()
print('TEST SUCEEDED!')

View file

@ -16,7 +16,8 @@ from tests_python import debugger_unittest
from tests_python.debugger_unittest import (CMD_SET_PROPERTY_TRACE, REASON_CAUGHT_EXCEPTION,
REASON_UNCAUGHT_EXCEPTION, REASON_STOP_ON_BREAKPOINT, REASON_THREAD_SUSPEND, overrides, CMD_THREAD_CREATE,
CMD_GET_THREAD_STACK, REASON_STEP_INTO_MY_CODE, CMD_GET_EXCEPTION_DETAILS, IS_IRONPYTHON, IS_JYTHON, IS_CPYTHON,
IS_APPVEYOR, wait_for_condition)
IS_APPVEYOR, wait_for_condition, CMD_GET_FRAME, CMD_GET_BREAKPOINT_EXCEPTION,
CMD_THREAD_SUSPEND)
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
try:
from urllib import unquote
@ -127,6 +128,89 @@ def test_case_2(case_setup):
writer.finished_ok = True
@pytest.mark.parametrize(
'skip_suspend_on_breakpoint_exception, skip_print_breakpoint_exception',
(
[['NameError'], []],
[['NameError'], ['NameError']],
[[], []], # Empty means it'll suspend/print in any exception
[[], ['NameError']],
[['ValueError'], ['Exception']],
[['Exception'], ['ValueError']], # ValueError will also suspend/print since we're dealing with a NameError
)
)
def test_case_breakpoint_condition_exc(case_setup, skip_suspend_on_breakpoint_exception, skip_print_breakpoint_exception):
msgs_in_stderr = (
'Error while evaluating expression: i > 5',
"NameError: name 'i' is not defined",
'Traceback (most recent call last):',
'File "<string>", line 1, in <module>',
)
def _ignore_stderr_line(line):
if original_ignore_stderr_line(line):
return True
for msg in msgs_in_stderr:
if msg in line:
return True
return False
def additional_output_checks(stdout, stderr):
original_additional_output_checks(stdout, stderr)
if skip_print_breakpoint_exception in ([], ['ValueError']):
for msg in msgs_in_stderr:
assert msg in stderr
else:
for msg in msgs_in_stderr:
assert msg not in stderr
with case_setup.test_file('_debugger_case_breakpoint_condition_exc.py') as writer:
original_ignore_stderr_line = writer._ignore_stderr_line
writer._ignore_stderr_line = _ignore_stderr_line
original_additional_output_checks = writer.additional_output_checks
writer.additional_output_checks = additional_output_checks
writer.write_suspend_on_breakpoint_exception(skip_suspend_on_breakpoint_exception, skip_print_breakpoint_exception)
breakpoint_id = writer.write_add_breakpoint(
writer.get_line_index_with_content('break here'), 'Call', condition='i > 5')
writer.write_make_initial_run()
if skip_suspend_on_breakpoint_exception in ([], ['ValueError']):
writer.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_BREAKPOINT_EXCEPTION,)))
hit = writer.wait_for_breakpoint_hit()
writer.write_run_thread(hit.thread_id)
if IS_JYTHON:
# Jython will break twice.
if skip_suspend_on_breakpoint_exception in ([], ['ValueError']):
writer.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_BREAKPOINT_EXCEPTION,)))
hit = writer.wait_for_breakpoint_hit()
writer.write_run_thread(hit.thread_id)
hit = writer.wait_for_breakpoint_hit()
thread_id = hit.thread_id
frame_id = hit.frame_id
writer.write_get_frame(thread_id, frame_id)
msg = writer.wait_for_message(lambda msg:msg.startswith('%s\t' % (CMD_GET_FRAME,)))
name_to_value = {}
for var in msg.var:
name_to_value[var['name']] = var['value']
assert name_to_value == {'i': 'int: 6', 'last_i': 'int: 6'}
writer.write_remove_breakpoint(breakpoint_id)
writer.write_run_thread(thread_id)
writer.finished_ok = True
@pytest.mark.skipif(IS_IRONPYTHON, reason='This test fails once in a while due to timing issues on IronPython, so, skipping it.')
def test_case_3(case_setup):
with case_setup.test_file('_debugger_case3.py') as writer:
@ -2182,7 +2266,7 @@ def test_top_level_exceptions_on_attach(case_setup_remote, check_scenario):
def check_test_suceeded_msg(writer, stdout, stderr):
return 'TEST SUCEEDED' in ''.join(stderr)
def additional_output_checks(writer, stdout, stderr):
# Don't call super as we have an expected exception
assert 'ValueError: TEST SUCEEDED' in stderr