mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
This commit is contained in:
parent
41d189c197
commit
eaa480313b
14 changed files with 2736 additions and 2361 deletions
|
|
@ -400,5 +400,6 @@ def main(argv=sys.argv):
|
|||
daemon.exitcode = 1
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print('TEST SUCEEDED!')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
main()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -8,3 +8,7 @@ def call_me_back1(callback):
|
|||
a = 'other'
|
||||
callback()
|
||||
return a
|
||||
|
||||
|
||||
def raise_exception():
|
||||
raise RuntimeError('TEST SUCEEDED')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
195
tests/func/test_exclude_rules.py
Normal file
195
tests/func/test_exclude_rules.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue