Fix #18: Set Next Statement

This commit is contained in:
Pavel Minaev 2019-02-20 21:06:06 -08:00 committed by Pavel Minaev
parent 19aaa3b178
commit 2bb23bc698
7 changed files with 207 additions and 48 deletions

View file

@ -77,7 +77,8 @@ def log_pydevd_msg(cmdid, seq, args, inbound,
else:
cmdname = '???'
cmd = '{} ({})'.format(cmdid, cmdname)
args = args.replace('\n', '\\n')
if isinstance(args, bytes) or isinstance(args, str):
args = args.replace('\n', '\\n')
msg = '{}{:28} [{:>10}]: |{}|'.format(prefix, cmd, seq, args)
log(msg)
@ -420,18 +421,28 @@ try:
import dis
except ImportError:
def get_code_lines(code):
return None
raise NotImplementedError
else:
def get_code_lines(code):
# First, get all line starts for this code object. This does not include
# bodies of nested class and function definitions, as they have their
# own objects.
for _, lineno in dis.findlinestarts(code):
yield lineno
if not isinstance(code, types.CodeType):
path = code
with open(path) as f:
src = f.read()
code = compile(src, path, 'exec', 0, dont_inherit=True)
return get_code_lines(code)
# For nested class and function definitions, their respective code objects
# are constants referenced by this object.
for const in code.co_consts:
if isinstance(const, types.CodeType) and const.co_filename == code.co_filename:
for lineno in get_code_lines(const):
yield lineno
def iterate():
# First, get all line starts for this code object. This does not include
# bodies of nested class and function definitions, as they have their
# own objects.
for _, lineno in dis.findlinestarts(code):
yield lineno
# For nested class and function definitions, their respective code objects
# are constants referenced by this object.
for const in code.co_consts:
if isinstance(const, types.CodeType) and const.co_filename == code.co_filename:
for lineno in get_code_lines(const):
yield lineno
return iterate()

View file

@ -22,6 +22,8 @@ with vendored('pydevd'):
pydevd_constants.CYTHON_SUPPORTED = False
# We limit representation size in our representation provider when needed.
pydevd_constants.MAXIMUM_VARIABLE_REPRESENTATION_SIZE = 2**32
# We want CMD_SET_NEXT_STATEMENT to have a response indicating success or failure.
pydevd_constants.GOTO_HAS_RESPONSE = True
# Now make sure all the top-level modules and packages in pydevd are

View file

@ -115,6 +115,9 @@ USE_LIB_COPY = SUPPORT_GEVENT and \
INTERACTIVE_MODE_AVAILABLE = sys.platform in ('darwin', 'win32') or os.getenv('DISPLAY') is not None
IS_PYCHARM = False
# If True, CMD_SET_NEXT_STATEMENT and CMD_RUN_TO_LINE commands have responses indicating success or failure.
GOTO_HAS_RESPONSE = IS_PYCHARM
LOAD_VALUES_ASYNC = os.getenv('PYDEVD_LOAD_VALUES_ASYNC', 'False') == 'True'
DEFAULT_VALUE = "__pydevd_value_async"
ASYNC_EVAL_TIMEOUT_SEC = 60

View file

@ -37,7 +37,7 @@ from _pydevd_bundle.pydevd_comm_constants import (CMD_THREAD_SUSPEND, CMD_STEP_I
from _pydevd_bundle.pydevd_constants import (IS_JYTH_LESS25, IS_PYCHARM, get_thread_id, get_current_thread_id,
dict_keys, dict_iter_items, DebugInfoHolder, PYTHON_SUSPEND, STATE_SUSPEND, STATE_RUN, get_frame,
clear_cached_thread_id, INTERACTIVE_MODE_AVAILABLE, SHOW_DEBUG_INFO_ENV, IS_PY34_OR_GREATER, IS_PY2, NULL,
NO_FTRACE)
NO_FTRACE, GOTO_HAS_RESPONSE)
from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init
from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE
from _pydevd_bundle.pydevd_extension_api import DebuggerEventHandler
@ -1181,7 +1181,7 @@ class PyDB(object):
if curr_func_name in ('?', '<module>'):
curr_func_name = ''
if curr_func_name == func_name:
if func_name == '*' or curr_func_name == func_name:
line = next_line
frame.f_trace = self.trace_dispatch
frame.f_lineno = line
@ -1302,7 +1302,7 @@ class PyDB(object):
except ValueError as e:
response_msg = "%s" % e
finally:
if IS_PYCHARM:
if GOTO_HAS_RESPONSE:
seq = info.pydev_message
cmd = self.cmd_factory.make_set_next_stmnt_status_message(seq, stop, response_msg)
self.writer.add_command(cmd)

View file

@ -239,6 +239,9 @@ class RequestFailure(Exception):
def __init__(self, message):
self.message = message
def __hash__(self):
return hash(self.message)
def __eq__(self, other):
if not isinstance(other, RequestFailure):
return NotImplemented

View file

@ -503,7 +503,8 @@ class PydevdSocket(object):
raise EOFError
seq, s = self.make_packet(cmd_id, args)
_util.log_pydevd_msg(cmd_id, seq, args, inbound=False)
os.write(self.pipe_w, s.encode('utf8'))
with self.lock:
os.write(self.pipe_w, s.encode('utf8'))
def pydevd_request(self, loop, cmd_id, args, is_json=False):
'''
@ -520,16 +521,16 @@ class PydevdSocket(object):
seq, s = self.make_packet(cmd_id, args)
_util.log_pydevd_msg(cmd_id, seq, args, inbound=False)
fut = loop.create_future()
with self.lock:
self.requests[seq] = loop, fut
as_bytes = s
if not isinstance(as_bytes, bytes):
as_bytes = as_bytes.encode('utf-8')
if is_json:
os.write(self.pipe_w, ('Content-Length:%s\r\n\r\n' % (len(as_bytes),)).encode('ascii'))
os.write(self.pipe_w, as_bytes)
return fut
@ -1104,6 +1105,7 @@ INITIALIZE_RESPONSE = dict(
supportsSetVariable=True,
supportsValueFormattingOptions=True,
supportTerminateDebuggee=True,
supportsGotoTargetsRequest=False, # https://github.com/Microsoft/ptvsd/issues/1163
exceptionBreakpointFilters=[
{
'filter': 'raised',
@ -1347,6 +1349,8 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
self.frame_map = IDMap()
self.var_map = IDMap()
self.source_map = IDMap()
self.goto_target_map = IDMap()
self.current_goto_request = None
self.enable_source_references = False
self.next_var_ref = 0
self._path_mappings = []
@ -2233,6 +2237,34 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
self.pydevd_notify(pydevd_comm.CMD_STEP_RETURN, tid)
self.send_response(request)
@async_handler
def on_gotoTargets(self, request, args):
path = args['source']['path']
line = args['line']
target_id = self.goto_target_map.to_vscode((path, line), autogen=True)
self.send_response(request, targets=[{
'id': target_id,
'label': '{}:{}'.format(path, line),
'line': line,
}])
@async_handler
def on_goto(self, request, args):
if self.current_goto_request is not None:
self.send_error_response(request, 'Already processing a "goto" request.')
return
vsc_tid = args['threadId']
target_id = args['targetId']
pyd_tid = self.thread_map.to_pydevd(vsc_tid)
path, line = self.goto_target_map.to_pydevd(target_id)
self.current_goto_request = request
self.pydevd_notify(
pydevd_comm.CMD_SET_NEXT_STATEMENT,
'{}\t{}\t*'.format(pyd_tid, line))
def _get_hit_condition_expression(self, hit_condition):
"""Following hit condition values are supported
@ -2293,30 +2325,18 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
path = path.encode(sys.getfilesystemencoding())
path = path_to_unicode(pydevd_file_utils.norm_file_to_server(path))
try:
with open(path) as f:
src = f.read()
except Exception:
pass
else:
try:
code = compile(src, path, 'exec', 0, dont_inherit=True)
except Exception:
pass
else:
try:
lines = sorted(_util.get_code_lines(code))
except Exception:
pass
else:
if lines:
for bp in args['breakpoints']:
line = bp['line']
if line not in lines:
# Adjust to the first preceding valid line.
idx = bisect.bisect_left(lines, line)
if idx > 0:
bp['line'] = lines[idx - 1]
try:
lines = sorted(_util.get_code_lines(path))
except Exception:
pass
else:
for bp in args['breakpoints']:
line = bp['line']
if line not in lines:
# Adjust to the first preceding valid line.
idx = bisect.bisect_left(lines, line)
if idx > 0:
bp['line'] = lines[idx - 1]
_, _, resp_args = yield self.pydevd_request(
pydevd_comm.CMD_SET_BREAK,
@ -2573,7 +2593,30 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
@pydevd_events.handler(pydevd_comm.CMD_THREAD_SUSPEND)
@async_handler
def on_pydevd_thread_suspend(self, seq, args):
pass
xml = self.parse_xml_response(args)
reason = int(xml.thread['stop_reason'])
# Normally, we rely on CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION instead,
# but we only get this one in response to CMD_SET_NEXT_STATEMENT.
if reason == pydevd_comm.CMD_SET_NEXT_STATEMENT:
pyd_tid = xml.thread['id']
vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
self.send_event(
'stopped',
reason='pause',
threadId=vsc_tid,
allThreadsStopped=True)
@pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN)
def on_pydevd_thread_run(self, seq, args):
pyd_tid, reason = args.split('\t', 2)
reason = int(reason)
vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
# Normally, we rely on CMD_THREAD_RESUME_SINGLE_NOTIFICATION instead,
# but we only get this one in response to CMD_SET_NEXT_STATEMENT.
if reason == pydevd_comm.CMD_SET_NEXT_STATEMENT:
self.send_event('continued', threadId=vsc_tid)
@pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION)
@async_handler
@ -2634,10 +2677,6 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
description=exc_desc,
**extra)
@pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN)
def on_pydevd_thread_run(self, seq, args):
pass # Ignore: only send continued on CMD_THREAD_RESUME_SINGLE_NOTIFICATION
@pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_RESUME_SINGLE_NOTIFICATION)
def on_pydevd_thread_resume_single_notification(self, seq, args):
resumed_info = json.loads(args)
@ -2677,3 +2716,16 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor):
@pydevd_events.handler(pydevd_comm.CMD_PROCESS_CREATED)
def on_pydevd_process_create(self, seq, args):
pass
@pydevd_events.handler(pydevd_comm.CMD_SET_NEXT_STATEMENT)
def on_pydevd_set_next_statement(self, seq, args):
goto_request = self.current_goto_request
assert goto_request is not None
self.current_goto_request = None
success, message = args.split('\t', 2)
if success == 'True':
self.send_response(goto_request)
else:
self.send_error_response(goto_request, message)

88
tests/func/test_step.py Normal file
View file

@ -0,0 +1,88 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, print_function, with_statement, unicode_literals
import pytest
from tests.helpers import get_marked_line_numbers, print
from tests.helpers.session import DebugSession
from tests.helpers.timeline import Event
from tests.helpers.pattern import ANY
@pytest.mark.skip(reason='https://github.com/Microsoft/ptvsd/issues/1163')
def test_set_next_statement(pyfile, run_as, start_method):
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import_and_enable_debugger()
def func():
print(1) #@inner1
print(2) #@inner2
print(3) #@outer3
func()
line_numbers = get_marked_line_numbers(code_to_debug)
print(line_numbers)
with DebugSession() as session:
session.initialize(
target=(run_as, code_to_debug),
start_method=start_method,
ignore_unobserved=[Event('continued')],
)
session.set_breakpoints(code_to_debug, [line_numbers['inner1']])
session.start_debugging()
stop = session.wait_for_thread_stopped()
frames = stop.stacktrace.body['stackFrames']
line = frames[0]['line']
assert line == line_numbers['inner1']
targets = session.send_request('gotoTargets', {
'source': {'path': code_to_debug},
'line': line_numbers['outer3'],
}).wait_for_response().body['targets']
assert targets == [{
'id': ANY.num,
'label': ANY.str,
'line': line_numbers['outer3']
}]
outer3_target = targets[0]['id']
with pytest.raises(Exception):
session.send_request('goto', {
'threadId': stop.thread_id,
'targetId': outer3_target,
}).wait_for_response()
targets = session.send_request('gotoTargets', {
'source': {'path': code_to_debug},
'line': line_numbers['inner2'],
}).wait_for_response().body['targets']
assert targets == [{
'id': ANY.num,
'label': ANY.str,
'line': line_numbers['inner2'],
}]
inner2_target = targets[0]['id']
session.send_request('goto', {
'threadId': stop.thread_id,
'targetId': inner2_target,
}).wait_for_response()
session.wait_for_next(Event('continued'))
stop = session.wait_for_thread_stopped()
frames = stop.stacktrace.body['stackFrames']
line = frames[0]['line']
assert line == line_numbers['inner2']
session.send_request('continue').wait_for_response()
session.wait_for_exit()