Support function breakpoints. Fixes #468

This commit is contained in:
Fabio Zadrozny 2021-07-08 15:31:42 -03:00
parent 623c503b58
commit d7970b80ad
12 changed files with 4602 additions and 3998 deletions

View file

@ -657,6 +657,14 @@ class PyDevdAPI(object):
py_db.on_breakpoints_changed(removed=True)
def set_function_breakpoints(self, py_db, function_breakpoints):
function_breakpoint_name_to_breakpoint = {}
for function_breakpoint in function_breakpoints:
function_breakpoint_name_to_breakpoint[function_breakpoint.func_name] = function_breakpoint
py_db.function_breakpoint_name_to_breakpoint = function_breakpoint_name_to_breakpoint
py_db.on_breakpoints_changed()
def request_exec_or_evaluate(
self, py_db, seq, thread_id, frame_id, expression, is_exec, trim_if_too_big, attr_to_set_result):
py_db.post_method_as_internal_command(

View file

@ -77,6 +77,36 @@ class LineBreakpoint(object):
return ret
class FunctionBreakpoint(object):
def __init__(self, func_name, condition, expression, suspend_policy="NONE", hit_condition=None, is_logpoint=False):
self.condition = condition
self.func_name = func_name
self.expression = expression
self.suspend_policy = suspend_policy
self.hit_condition = hit_condition
self._hit_count = 0
self._hit_condition_lock = threading.Lock()
self.is_logpoint = is_logpoint
@property
def has_condition(self):
return bool(self.condition) or bool(self.hit_condition)
def handle_hit_condition(self, frame):
if not self.hit_condition:
return False
ret = False
with self._hit_condition_lock:
self._hit_count += 1
expr = self.hit_condition.replace('@HIT@', str(self._hit_count))
try:
ret = bool(eval(expr, frame.f_globals, frame.f_locals))
except Exception:
ret = False
return ret
def get_exception_breakpoint(exctype, exceptions):
if not exctype:
exception_full_qname = None

View file

@ -96,6 +96,8 @@ CMD_STEP_INTO_COROUTINE = 206
CMD_LOAD_SOURCE_FROM_FRAME_ID = 207
CMD_SET_FUNCTION_BREAK = 208
CMD_VERSION = 501
CMD_RETURN = 502
CMD_SET_PROTOCOL = 503

File diff suppressed because it is too large Load diff

View file

@ -154,7 +154,7 @@ from _pydevd_bundle.pydevd_constants import (dict_iter_values, IS_PY3K, RETURN_V
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
try:
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
except ImportError:
@ -718,6 +718,7 @@ cdef class PyDBFrame:
stop_frame = info.pydev_step_stop
step_cmd = info.pydev_step_cmd
function_breakpoint_on_call_event = None
if frame.f_code.co_flags & 0xa0: # 0xa0 == CO_GENERATOR = 0x20 | CO_COROUTINE = 0x80
# Dealing with coroutines and generators:
@ -840,6 +841,8 @@ cdef class PyDBFrame:
is_call = True
is_return = False
is_exception_event = False
if frame.f_code.co_firstlineno == frame.f_lineno: # Check line to deal with async/await.
function_breakpoint_on_call_event = main_debugger.function_breakpoint_name_to_breakpoint.get(frame.f_code.co_name)
elif event == 'exception':
is_exception_event = True
@ -909,7 +912,11 @@ cdef class PyDBFrame:
# we will return nothing for the next trace
# also, after we hit a breakpoint and go to some other debugging state, we have to force the set trace anyway,
# so, that's why the additional checks are there.
if not breakpoints_for_file:
if function_breakpoint_on_call_event:
pass # Do nothing here (just keep on going as we can't skip it).
elif not breakpoints_for_file:
if can_skip:
if has_exception_breakpoints:
return self.trace_exception
@ -983,8 +990,16 @@ cdef class PyDBFrame:
breakpoint = None
exist_result = False
stop = False
stop_reason = 111
bp_type = None
if not is_return and info.pydev_state != 2 and breakpoints_for_file is not None and line in breakpoints_for_file:
if function_breakpoint_on_call_event:
breakpoint = function_breakpoint_on_call_event
stop = True
new_frame = frame
stop_reason = CMD_SET_FUNCTION_BREAK
elif not is_return and info.pydev_state != 2 and breakpoints_for_file is not None and line in breakpoints_for_file:
breakpoint = breakpoints_for_file[line]
new_frame = frame
stop = True
@ -1049,7 +1064,7 @@ cdef class PyDBFrame:
if stop:
self.set_suspend(
thread,
111,
stop_reason,
suspend_other_threads=breakpoint and breakpoint.suspend_policy == "ALL",
)

View file

@ -9,7 +9,7 @@ from _pydevd_bundle.pydevd_constants import (dict_iter_values, IS_PY3K, RETURN_V
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, just_raised, remove_exception_from_frame, ignore_exception_trace
from _pydevd_bundle.pydevd_utils import get_clsname_for_code
from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame
from _pydevd_bundle.pydevd_comm_constants import constant_to_str
from _pydevd_bundle.pydevd_comm_constants import constant_to_str, CMD_SET_FUNCTION_BREAK
try:
from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
except ImportError:
@ -585,6 +585,7 @@ class PyDBFrame:
stop_frame = info.pydev_step_stop
step_cmd = info.pydev_step_cmd
function_breakpoint_on_call_event = None
if frame.f_code.co_flags & 0xa0: # 0xa0 == CO_GENERATOR = 0x20 | CO_COROUTINE = 0x80
# Dealing with coroutines and generators:
@ -707,6 +708,8 @@ class PyDBFrame:
is_call = True
is_return = False
is_exception_event = False
if frame.f_code.co_firstlineno == frame.f_lineno: # Check line to deal with async/await.
function_breakpoint_on_call_event = main_debugger.function_breakpoint_name_to_breakpoint.get(frame.f_code.co_name)
elif event == 'exception':
is_exception_event = True
@ -776,7 +779,11 @@ class PyDBFrame:
# we will return nothing for the next trace
# also, after we hit a breakpoint and go to some other debugging state, we have to force the set trace anyway,
# so, that's why the additional checks are there.
if not breakpoints_for_file:
if function_breakpoint_on_call_event:
pass # Do nothing here (just keep on going as we can't skip it).
elif not breakpoints_for_file:
if can_skip:
if has_exception_breakpoints:
return self.trace_exception
@ -850,8 +857,16 @@ class PyDBFrame:
breakpoint = None
exist_result = False
stop = False
stop_reason = CMD_SET_BREAK
bp_type = None
if not is_return and info.pydev_state != STATE_SUSPEND and breakpoints_for_file is not None and line in breakpoints_for_file:
if function_breakpoint_on_call_event:
breakpoint = function_breakpoint_on_call_event
stop = True
new_frame = frame
stop_reason = CMD_SET_FUNCTION_BREAK
elif not is_return and info.pydev_state != STATE_SUSPEND and breakpoints_for_file is not None and line in breakpoints_for_file:
breakpoint = breakpoints_for_file[line]
new_frame = frame
stop = True
@ -916,7 +931,7 @@ class PyDBFrame:
if stop:
self.set_suspend(
thread,
CMD_SET_BREAK,
stop_reason,
suspend_other_threads=breakpoint and breakpoint.suspend_policy == "ALL",
)

View file

@ -14,7 +14,8 @@ from _pydevd_bundle.pydevd_comm_constants import CMD_THREAD_CREATE, CMD_RETURN,
CMD_STEP_RETURN, CMD_STEP_CAUGHT_EXCEPTION, CMD_ADD_EXCEPTION_BREAK, CMD_SET_BREAK, \
CMD_SET_NEXT_STATEMENT, CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION, \
CMD_THREAD_RESUME_SINGLE_NOTIFICATION, CMD_THREAD_KILL, CMD_STOP_ON_START, CMD_INPUT_REQUESTED, \
CMD_EXIT, CMD_STEP_INTO_COROUTINE, CMD_STEP_RETURN_MY_CODE, CMD_SMART_STEP_INTO
CMD_EXIT, CMD_STEP_INTO_COROUTINE, CMD_STEP_RETURN_MY_CODE, CMD_SMART_STEP_INTO, \
CMD_SET_FUNCTION_BREAK
from _pydevd_bundle.pydevd_constants import get_thread_id, dict_values, ForkSafeLock
from _pydevd_bundle.pydevd_net_command import NetCommand, NULL_NET_COMMAND
from _pydevd_bundle.pydevd_net_command_factory_xml import NetCommandFactory
@ -327,6 +328,8 @@ class NetCommandFactoryJson(NetCommandFactory):
stop_reason = 'exception'
elif stop_reason == CMD_SET_BREAK:
stop_reason = 'breakpoint'
elif stop_reason == CMD_SET_FUNCTION_BREAK:
stop_reason = 'function breakpoint'
elif stop_reason == CMD_SET_NEXT_STATEMENT:
stop_reason = 'goto'
else:

View file

@ -16,9 +16,9 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import (
SetVariableResponseBody, SourceBreakpoint, SourceResponseBody,
VariablesResponseBody, SetBreakpointsResponseBody, Response,
Capabilities, PydevdAuthorizeRequest, Request, StepInTargetsResponse, StepInTarget,
StepInTargetsResponseBody)
StepInTargetsResponseBody, SetFunctionBreakpointsResponseBody)
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_breakpoints import get_exception_class
from _pydevd_bundle.pydevd_breakpoints import get_exception_class, FunctionBreakpoint
from _pydevd_bundle.pydevd_comm_constants import (
CMD_PROCESS_EVENT, CMD_RETURN, CMD_SET_NEXT_STATEMENT, CMD_STEP_INTO,
CMD_STEP_INTO_MY_CODE, CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, file_system_encoding,
@ -223,6 +223,7 @@ class PyDevJsonCommandProcessor(object):
supportsSetExpression=True,
supportsTerminateRequest=True,
supportsClipboardContext=True,
supportsFunctionBreakpoints=True,
exceptionBreakpointFilters=[
{'filter': 'raised', 'label': 'Raised Exceptions', 'default': False},
@ -231,7 +232,6 @@ class PyDevJsonCommandProcessor(object):
],
# Not supported.
supportsFunctionBreakpoints=False,
supportsStepBack=False,
supportsRestartFrame=False,
supportsStepInTargetsRequest=True,
@ -445,7 +445,7 @@ class PyDevJsonCommandProcessor(object):
if IS_PY2 and isinstance(w, unicode):
w = w.encode(getfilesystemencoding())
new_watch_dirs.add(pydevd_file_utils.get_path_with_real_case(pydevd_file_utils.absolute_path(w)))
new_watch_dirs.add(pydevd_file_utils.get_path_with_real_case(pydevd_file_utils.absolute_path(w)))
except Exception:
pydev_log.exception('Error adding watch dir: %s', w)
watch_dirs = new_watch_dirs
@ -681,14 +681,14 @@ class PyDevJsonCommandProcessor(object):
response = pydevd_base_schema.build_response(request)
return NetCommand(CMD_RETURN, 0, response, is_json=True)
def on_setbreakpoints_request(self, py_db, request):
'''
:param SetBreakpointsRequest request:
'''
def _verify_launch_or_attach_done(self, request):
if not self._launch_or_attach_request_done:
# Note that to validate the breakpoints we need the launch request to be done already
# (otherwise the filters wouldn't be set for the breakpoint validation).
body = SetBreakpointsResponseBody([])
if request.command == 'setFunctionBreakpoints':
body = SetFunctionBreakpointsResponseBody([])
else:
body = SetBreakpointsResponseBody([])
response = pydevd_base_schema.build_response(
request,
kwargs={
@ -698,6 +698,48 @@ class PyDevJsonCommandProcessor(object):
})
return NetCommand(CMD_RETURN, 0, response, is_json=True)
def on_setfunctionbreakpoints_request(self, py_db, request):
'''
:param SetFunctionBreakpointsRequest request:
'''
response = self._verify_launch_or_attach_done(request)
if response is not None:
return response
arguments = request.arguments # : :type arguments: SetFunctionBreakpointsArguments
function_breakpoints = []
suspend_policy = 'ALL'
# Not currently covered by the DAP.
is_logpoint = False
expression = None
breakpoints_set = []
for bp in arguments.breakpoints:
hit_condition = self._get_hit_condition_expression(bp.get('hitCondition'))
condition = bp.get('condition')
function_breakpoints.append(
FunctionBreakpoint(bp['name'], condition, expression, suspend_policy, hit_condition, is_logpoint))
# Note: always succeeds.
breakpoints_set.append(pydevd_schema.Breakpoint(
verified=True, id=self._next_breakpoint_id()).to_dict())
self.api.set_function_breakpoints(py_db, function_breakpoints)
body = {'breakpoints': breakpoints_set}
set_breakpoints_response = pydevd_base_schema.build_response(request, kwargs={'body': body})
return NetCommand(CMD_RETURN, 0, set_breakpoints_response, is_json=True)
def on_setbreakpoints_request(self, py_db, request):
'''
:param SetBreakpointsRequest request:
'''
response = self._verify_launch_or_attach_done(request)
if response is not None:
return response
arguments = request.arguments # : :type arguments: SetBreakpointsArguments
# TODO: Path is optional here it could be source reference.
filename = self.api.filename_to_str(arguments.source.path)

View file

@ -125,6 +125,7 @@ cdef class ThreadInfo:
cdef class FuncCodeInfo:
cdef public str co_filename
cdef public str co_name
cdef public str canonical_normalized_filename
cdef bint always_skip_code
cdef public bint breakpoint_found
@ -240,6 +241,7 @@ cdef FuncCodeInfo get_func_code_info(ThreadInfo thread_info, PyFrameObject * fra
return func_code_info_obj
cdef str co_filename = <str> code_obj.co_filename
cdef str co_name = <str> code_obj.co_name
cdef dict cache_file_type
cdef tuple cache_file_type_key
@ -247,6 +249,7 @@ cdef FuncCodeInfo get_func_code_info(ThreadInfo thread_info, PyFrameObject * fra
func_code_info.breakpoints_mtime = main_debugger.mtime
func_code_info.co_filename = co_filename
func_code_info.co_name = co_name
if not func_code_info.always_skip_code:
try:
@ -272,6 +275,7 @@ cdef FuncCodeInfo get_func_code_info(ThreadInfo thread_info, PyFrameObject * fra
if main_debugger is not None:
breakpoints: dict = main_debugger.breakpoints.get(func_code_info.canonical_normalized_filename)
function_breakpoint: object = main_debugger.function_breakpoint_name_to_breakpoint.get(func_code_info.co_name)
# print('\n---')
# print(main_debugger.breakpoints)
# print(func_code_info.canonical_normalized_filename)
@ -289,6 +293,11 @@ cdef FuncCodeInfo get_func_code_info(ThreadInfo thread_info, PyFrameObject * fra
cached_code_obj_info.compute_force_stay_in_untraced_mode(breakpoints)
func_code_info.breakpoint_found = breakpoint_found
elif function_breakpoint:
# Go directly into tracing mode
func_code_info.breakpoint_found = True
func_code_info.new_code = None
elif breakpoints:
# if DEBUG:
# print('found breakpoints', code_obj_py.co_name, breakpoints)

View file

@ -530,6 +530,7 @@ class PyDB(object):
# These are the breakpoints meant to be consumed during runtime.
self.breakpoints = {}
self.function_breakpoint_name_to_breakpoint = {}
# Set communication protocol
PyDevdAPI().set_protocol(self, 0, PydevdCustomization.DEFAULT_PROTOCOL)

View file

@ -13,14 +13,15 @@ from _pydevd_bundle._debug_adapter import pydevd_schema, pydevd_base_schema
from _pydevd_bundle._debug_adapter.pydevd_base_schema import from_json
from _pydevd_bundle._debug_adapter.pydevd_schema import (ThreadEvent, ModuleEvent, OutputEvent,
ExceptionOptions, Response, StoppedEvent, ContinuedEvent, ProcessEvent, InitializeRequest,
InitializeRequestArguments, TerminateArguments, TerminateRequest, TerminatedEvent)
InitializeRequestArguments, TerminateArguments, TerminateRequest, TerminatedEvent,
FunctionBreakpoint, SetFunctionBreakpointsRequest, SetFunctionBreakpointsArguments)
from _pydevd_bundle.pydevd_comm_constants import file_system_encoding
from _pydevd_bundle.pydevd_constants import (int_types, IS_64BIT_PROCESS,
PY_VERSION_STR, PY_IMPL_VERSION_STR, PY_IMPL_NAME, IS_PY36_OR_GREATER,
IS_PYPY, GENERATED_LEN_ATTR_NAME, IS_WINDOWS, IS_LINUX, IS_MAC)
from tests_python import debugger_unittest
from tests_python.debug_constants import TEST_CHERRYPY, IS_PY2, TEST_DJANGO, TEST_FLASK, IS_PY26, \
IS_PY27, IS_CPYTHON, TEST_GEVENT, TEST_CYTHON, IS_PY36_OR_GREATER
IS_PY27, IS_CPYTHON, TEST_GEVENT, TEST_CYTHON
from tests_python.debugger_unittest import (IS_JYTHON, IS_APPVEYOR, overrides,
get_free_port, wait_for_condition)
from _pydevd_bundle.pydevd_utils import DAPGrouper
@ -154,6 +155,14 @@ class JsonFacade(object):
assert found_line in line, 'Expect to break at line: %s. Found: %s (file: %s)' % (line, found_line, path)
return json_hit
def write_set_function_breakpoints(
self, function_names):
function_breakpoints = [FunctionBreakpoint(name,) for name in function_names]
arguments = SetFunctionBreakpointsArguments(function_breakpoints)
request = SetFunctionBreakpointsRequest(arguments)
response = self.wait_for_response(self.write_request(request))
assert response.success
def write_set_breakpoints(
self,
lines,
@ -5545,6 +5554,46 @@ def test_step_into_target_genexpr(case_setup):
writer.finished_ok = True
def test_function_breakpoints_basic(case_setup, pyfile):
@pyfile
def module():
def do_something(): # break here
print('TEST SUCEEDED')
if __name__ == '__main__':
do_something()
with case_setup.test_file(module) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(justMyCode=False)
bp = writer.get_line_index_with_content('break here')
json_facade.write_set_function_breakpoints(['do_something'])
json_facade.write_make_initial_run()
hit = json_facade.wait_for_thread_stopped('function breakpoint', line=bp)
json_facade.write_continue()
writer.finished_ok = True
@pytest.mark.skipif(not IS_PY36_OR_GREATER, reason='Python 3.6 onwards required for test.')
def test_function_breakpoints_async(case_setup):
with case_setup.test_file('_debugger_case_stop_async_iteration.py') as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(justMyCode=False)
bp = writer.get_line_index_with_content('async def gen():')
json_facade.write_set_function_breakpoints(['gen'])
json_facade.write_make_initial_run()
hit = json_facade.wait_for_thread_stopped('function breakpoint', line=bp)
json_facade.write_continue()
writer.finished_ok = True
if __name__ == '__main__':
pytest.main(['-k', 'test_case_skipping_filters', '-s'])