Debugger should not stop on exceptions raised in excluded files. Fixes #1268 (#1280)

This commit is contained in:
Fabio Zadrozny 2019-04-02 16:30:13 -03:00 committed by Karthik Nadig
parent 41d189c197
commit eaa480313b
14 changed files with 2736 additions and 2361 deletions

View file

@ -400,5 +400,6 @@ def main(argv=sys.argv):
daemon.exitcode = 1
raise
if __name__ == '__main__':
main(sys.argv)

View file

@ -110,6 +110,9 @@ def stop_on_unhandled_exception(py_db, thread, additional_info, arg):
if exctype is KeyboardInterrupt:
return
if py_db.exclude_exception_by_filter(exception_breakpoint, tb, True):
return
frames = []
user_frame = None

File diff suppressed because it is too large Load diff

View file

@ -290,10 +290,9 @@ cdef class PyDBFrame:
if not eval_result:
return False, frame
if exception_breakpoint.ignore_libraries:
if not main_debugger.is_exception_trace_in_project_scope(trace):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
if main_debugger.exclude_exception_by_filter(exception_breakpoint, trace, False):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
if ignore_exception_trace(trace):
return False, frame

View file

@ -139,10 +139,9 @@ class PyDBFrame:
if not eval_result:
return False, frame
if exception_breakpoint.ignore_libraries:
if not main_debugger.is_exception_trace_in_project_scope(trace):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
if main_debugger.exclude_exception_by_filter(exception_breakpoint, trace, False):
pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
return False, frame
if ignore_exception_trace(trace):
return False, frame

View file

@ -754,17 +754,27 @@ class PyDB(object):
self._apply_filter_cache[cache_key] = False
return False
def is_exception_trace_in_project_scope(self, trace):
if trace is None or not self.in_project_scope(trace.tb_frame.f_code.co_filename):
def exclude_exception_by_filter(self, exception_breakpoint, trace, is_uncaught):
if not exception_breakpoint.ignore_libraries and not self._exclude_filters_enabled:
return False
else:
trace = trace.tb_next
while trace is not None:
if not self.in_project_scope(trace.tb_frame.f_code.co_filename):
return False
trace = trace.tb_next
if trace is None:
return True
# We need to get the place where it was raised if it's an uncaught exception...
if is_uncaught:
while trace.tb_next is not None:
trace = trace.tb_next
ignore_libraries = exception_breakpoint.ignore_libraries
exclude_filters_enabled = self._exclude_filters_enabled
if (ignore_libraries and not self.in_project_scope(trace.tb_frame.f_code.co_filename)) \
or (exclude_filters_enabled and self._exclude_by_filter(trace.tb_frame, trace.tb_frame.f_code.co_filename)):
return True
return False
def set_project_roots(self, project_roots):
self._files_filtering.set_project_roots(project_roots)
self._clear_skip_caches()

View file

@ -0,0 +1,9 @@
import sys
def main():
print('TEST SUCEEDED!')
sys.exit(1)
main()

View file

@ -0,0 +1,14 @@
if __name__ == '__main__':
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from not_my_code import other
def callback2():
raise RuntimeError('TEST SUCEEDED!')
def callback1():
other.call_me_back2(callback2)
other.call_me_back1(callback1) # break here

View file

@ -0,0 +1,14 @@
if __name__ == '__main__':
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from not_my_code import other
def callback2():
other.raise_exception()
def callback1():
other.call_me_back2(callback2)
other.call_me_back1(callback1) # break here

View file

@ -8,3 +8,7 @@ def call_me_back1(callback):
a = 'other'
callback()
return a
def raise_exception():
raise RuntimeError('TEST SUCEEDED')

View file

@ -3000,6 +3000,116 @@ def step_method(request):
return request.param
def test_sysexit_on_filtered_file(case_setup):
def get_environ(writer):
env = os.environ.copy()
env.update({'PYDEVD_FILTERS': json.dumps({'**/_debugger_case_sysexit.py': True})})
return env
with case_setup.test_file('_debugger_case_sysexit.py', get_environ=get_environ, EXPECTED_RETURNCODE=1) as writer:
writer.write_add_exception_breakpoint_with_policy(
'SystemExit',
notify_on_handled_exceptions=1, # Notify multiple times
notify_on_unhandled_exceptions=1,
ignore_libraries=0
)
writer.write_make_initial_run()
writer.finished_ok = True
@pytest.mark.parametrize("scenario", [
'handled_once',
'handled_multiple',
'unhandled',
])
def test_exception_not_on_filtered_file(case_setup, scenario):
def get_environ(writer):
env = os.environ.copy()
env.update({'PYDEVD_FILTERS': json.dumps({'**/other.py': True})})
return env
def check_test_suceeded_msg(writer, stdout, stderr):
return 'TEST SUCEEDED' in ''.join(stderr)
def additional_output_checks(writer, stdout, stderr):
if 'raise RuntimeError' not in stderr:
raise AssertionError('Expected test to have an unhandled exception.\nstdout:\n%s\n\nstderr:\n%s' % (
stdout, stderr))
with case_setup.test_file(
'my_code/my_code_exception.py',
get_environ=get_environ,
EXPECTED_RETURNCODE='any',
check_test_suceeded_msg=check_test_suceeded_msg,
additional_output_checks=additional_output_checks,
) as writer:
if scenario == 'handled_once':
writer.write_add_exception_breakpoint_with_policy(
'RuntimeError',
notify_on_handled_exceptions=2, # Notify only once
notify_on_unhandled_exceptions=0,
ignore_libraries=0
)
elif scenario == 'handled_multiple':
writer.write_add_exception_breakpoint_with_policy(
'RuntimeError',
notify_on_handled_exceptions=1, # Notify multiple times
notify_on_unhandled_exceptions=0,
ignore_libraries=0
)
elif scenario == 'unhandled':
writer.write_add_exception_breakpoint_with_policy(
'RuntimeError',
notify_on_handled_exceptions=0,
notify_on_unhandled_exceptions=1,
ignore_libraries=0
)
writer.write_make_initial_run()
for _i in range(3 if scenario == 'handled_multiple' else 1):
hit = writer.wait_for_breakpoint_hit(
REASON_UNCAUGHT_EXCEPTION if scenario == 'unhandled' else REASON_CAUGHT_EXCEPTION)
writer.write_run_thread(hit.thread_id)
writer.finished_ok = True
def test_exception_on_filtered_file(case_setup):
def get_environ(writer):
env = os.environ.copy()
env.update({'PYDEVD_FILTERS': json.dumps({'**/other.py': True})})
return env
def check_test_suceeded_msg(writer, stdout, stderr):
return 'TEST SUCEEDED' in ''.join(stderr)
def additional_output_checks(writer, stdout, stderr):
if 'raise RuntimeError' not in stderr:
raise AssertionError('Expected test to have an unhandled exception.\nstdout:\n%s\n\nstderr:\n%s' % (
stdout, stderr))
with case_setup.test_file(
'my_code/my_code_exception_on_other.py',
get_environ=get_environ,
EXPECTED_RETURNCODE='any',
check_test_suceeded_msg=check_test_suceeded_msg,
additional_output_checks=additional_output_checks,
) as writer:
writer.write_add_exception_breakpoint_with_policy(
'RuntimeError',
notify_on_handled_exceptions=2, # Notify only once
notify_on_unhandled_exceptions=1,
ignore_libraries=0
)
writer.write_make_initial_run()
writer.finished_ok = True
@pytest.mark.parametrize("environ", [
{'PYDEVD_FILTER_LIBRARIES': '1'}, # Global setting for step over
{'PYDEVD_FILTERS': json.dumps({'**/other.py': True})}, # specify as json

View file

@ -119,7 +119,7 @@ def test_django_template_exception_no_multiproc(start_method):
link = DJANGO_LINK + 'badtemplate'
web_request = get_web_content(link, {})
hit = session.wait_for_thread_stopped()
hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
assert frames[0] == ANY.dict_with({
'id': ANY.dap_id,
@ -132,6 +132,7 @@ def test_django_template_exception_no_multiproc(start_method):
'column': 1,
})
# Will stop once in the plugin
resp_exception_info = session.send_request(
'exceptionInfo',
arguments={'threadId': hit.thread_id, }
@ -149,6 +150,10 @@ def test_django_template_exception_no_multiproc(start_method):
session.send_request('continue').wait_for_response(freeze=False)
# And a second time when the exception reaches the user code.
hit = session.wait_for_thread_stopped(reason='exception')
session.send_request('continue').wait_for_response(freeze=False)
# ignore response for exception tests
web_request.wait_for_response()

View file

@ -0,0 +1,195 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import print_function, with_statement, absolute_import
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 os.path import os
import pytest
from tests.helpers.pathutils import get_test_root
@pytest.mark.parametrize('scenario', [
'exclude_by_name',
'exclude_by_dir',
])
@pytest.mark.parametrize('exception_type', [
'RuntimeError',
'SysExit'
])
def test_exceptions_and_exclude_rules(pyfile, run_as, start_method, scenario, exception_type):
if exception_type == 'RuntimeError':
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import_and_enable_debugger()
raise RuntimeError('unhandled error') # @raise_line
elif exception_type == 'SysExit':
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import sys
import_and_enable_debugger()
sys.exit(1) # @raise_line
else:
raise AssertionError('Unexpected exception_type: %s' % (exception_type,))
if scenario == 'exclude_by_name':
rules = [{'path': '**/' + os.path.basename(code_to_debug), 'include': False}]
elif scenario == 'exclude_by_dir':
rules = [{'path': os.path.dirname(code_to_debug), 'include': False}]
else:
raise AssertionError('Unexpected scenario: %s' % (scenario,))
with DebugSession() as session:
session.initialize(
target=(run_as, code_to_debug),
start_method=start_method,
ignore_unobserved=[Event('continued')],
rules=rules,
)
# TODO: The process returncode doesn't match the one returned from the DAP.
# See: https://github.com/Microsoft/ptvsd/issues/1278
session.expected_returncode = ANY.int
filters = ['raised', 'uncaught']
session.send_request('setExceptionBreakpoints', {
'filters': filters
}).wait_for_response()
session.start_debugging()
# No exceptions should be seen.
session.wait_for_exit()
@pytest.mark.parametrize('scenario', [
'exclude_code_to_debug',
'exclude_callback_dir',
])
def test_exceptions_and_partial_exclude_rules(pyfile, run_as, start_method, scenario):
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import_and_enable_debugger()
import backchannel
import sys
json = backchannel.read_json()
call_me_back_dir = json['call_me_back_dir']
sys.path.append(call_me_back_dir)
import call_me_back
def call_func():
raise RuntimeError('unhandled error') # @raise_line
call_me_back.call_me_back(call_func) # @call_me_back_line
print('done')
line_numbers = get_marked_line_numbers(code_to_debug)
call_me_back_dir = get_test_root('call_me_back')
if scenario == 'exclude_code_to_debug':
rules = [
{'path': '**/' + os.path.basename(code_to_debug), 'include': False}
]
elif scenario == 'exclude_callback_dir':
rules = [
{'path': call_me_back_dir, 'include': False}
]
else:
raise AssertionError('Unexpected scenario: %s' % (scenario,))
with DebugSession() as session:
session.initialize(
target=(run_as, code_to_debug),
start_method=start_method,
use_backchannel=True,
ignore_unobserved=[Event('continued')],
rules=rules,
)
# TODO: The process returncode doesn't match the one returned from the DAP.
# See: https://github.com/Microsoft/ptvsd/issues/1278
session.expected_returncode = ANY.int
filters = ['raised', 'uncaught']
session.send_request('setExceptionBreakpoints', {
'filters': filters
}).wait_for_response()
session.start_debugging()
session.write_json({'call_me_back_dir': call_me_back_dir})
if scenario == 'exclude_code_to_debug':
# Stop at handled
hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
# We don't stop at the raise line but rather at the callback module which is
# not excluded.
assert len(frames) == 2
assert frames[0] == ANY.dict_with({
'line': 2,
'source': ANY.dict_with({
'path': Path(os.path.join(call_me_back_dir, 'call_me_back.py'))
})
})
assert frames[1] == ANY.dict_with({
'line': line_numbers['call_me_back_line'],
'source': ANY.dict_with({
'path': Path(code_to_debug)
})
})
# 'continue' should terminate the debuggee
session.send_request('continue').wait_for_response(freeze=False)
# Note: does not stop at unhandled exception because raise was in excluded file.
elif scenario == 'exclude_callback_dir':
# Stop at handled raise_line
hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
assert len(frames) == 3
assert frames[0] == ANY.dict_with({
'line': line_numbers['raise_line'],
'source': ANY.dict_with({
'path': Path(code_to_debug)
})
})
session.send_request('continue').wait_for_response()
# Stop at handled call_me_back_line
hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
assert len(frames) == 1
assert frames[0] == ANY.dict_with({
'line': line_numbers['call_me_back_line'],
'source': ANY.dict_with({
'path': Path(code_to_debug)
})
})
session.send_request('continue').wait_for_response()
# Stop at unhandled
hit = session.wait_for_thread_stopped(reason='exception')
frames = hit.stacktrace.body['stackFrames']
assert len(frames) == 3
assert frames[0] == ANY.dict_with({
'line': line_numbers['raise_line'],
'source': ANY.dict_with({
'path': Path(code_to_debug)
})
})
session.send_request('continue').wait_for_response(freeze=False)
else:
raise AssertionError('Unexpected scenario: %s' % (scenario,))
session.wait_for_exit()

View file

@ -26,7 +26,6 @@ from .pattern import ANY
from .printer import wait_for_output
from .timeline import Timeline, Event, Response
PTVSD_PORT = tests.helpers.get_unique_port(5678)
PTVSD_ENABLE_KEY = 'PTVSD_ENABLE_ATTACH'
PTVSD_HOST_KEY = 'PTVSD_TEST_HOST'
@ -53,6 +52,7 @@ class DebugSession(object):
self.multiprocess_port_range = None
self.debug_options = ['RedirectOutput']
self.path_mappings = []
self.rules = []
self.env = os.environ.copy()
self.env['PYTHONPATH'] = os.path.dirname(debuggee.__file__)
self.cwd = None
@ -233,6 +233,7 @@ class DebugSession(object):
self.path_mappings += kwargs.pop('path_mappings', [])
self.debug_options += kwargs.pop('debug_options', [])
self.program_args += kwargs.pop('program_args', [])
self.rules += kwargs.pop('rules', [])
for k, v in kwargs.items():
setattr(self, k, v)
@ -314,7 +315,7 @@ class DebugSession(object):
self.pid = self.process.pid
self.psutil_process = psutil.Process(self.pid)
self.is_running = True
#watchdog.create(self.pid)
# watchdog.create(self.pid)
if not self.skip_capture:
self._capture_output(self.process.stdout, 'OUT')
@ -498,6 +499,7 @@ class DebugSession(object):
self.send_request(request, {
'debugOptions': self.debug_options,
'pathMappings': self.path_mappings,
'rules': self.rules,
}).wait_for_response()
if not self.no_debug:
@ -587,6 +589,7 @@ class DebugSession(object):
return t
def _capture_output(self, pipe, name):
def _output_worker():
while True:
try:
@ -640,6 +643,7 @@ class DebugSession(object):
try:
child_session.ignore_unobserved = self.ignore_unobserved
child_session.debug_options = self.debug_options
child_session.rules = self.rules
child_session.connect()
child_session.handshake()
except:
@ -664,6 +668,7 @@ class DebugSession(object):
ns._setup_session(**kwargs)
ns.ignore_unobserved = self.ignore_unobserved
ns.debug_options = self.debug_options
ns.rules = self.rules
ns.pid = self.pid
ns.process = self.process