Repository: microsoft/ptvsd

Issue:
on_setBreakpoints line adjustment should be ported to pydevd



Discussion:

Resolving patch:
diff --git a/src/ptvsd/_util.py b/src/ptvsd/_util.py
--- a/src/ptvsd/_util.py
+++ b/src/ptvsd/_util.py
@@ -7,7 +7,6 @@
 import contextlib
 import threading
 import time
-import types
 import sys
 
 import ptvsd.log
@@ -342,34 +341,3 @@ def _start(self, *args, **kwargs):
 
     def _stop(self):
         raise NotImplementedError
-
-
-try:
-    import dis
-except ImportError:
-    def get_code_lines(code):
-        raise NotImplementedError
-else:
-    def get_code_lines(code):
-        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)
-
-        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()
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
@@ -1,8 +1,10 @@
 from functools import partial
+import bisect
 import itertools
 import json
 import linecache
 import os
+import types
 
 from _pydevd_bundle._debug_adapter import pydevd_base_schema
 from _pydevd_bundle._debug_adapter.pydevd_schema import (SourceBreakpoint, ScopesResponseBody, Scope,
@@ -21,6 +23,36 @@
 from _pydevd_bundle._debug_adapter.pydevd_schema import CompletionsResponseBody
 
 
+try:
+    import dis
+except ImportError:
+    def _get_code_lines(code):
+        raise NotImplementedError
+else:
+    def _get_code_lines(code):
+        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)
+
+        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()
+
 def _convert_rules_to_exclude_filters(rules, filename_to_server, on_error):
     exclude_filters = []
     if not isinstance(rules, list):
@@ -370,12 +402,27 @@ def on_setbreakpoints_request(self, py_db, request):
         :param SetBreakpointsRequest request:
         '''
         arguments = request.arguments  # : :type arguments: SetBreakpointsArguments
+        # TODO: Path is optional here it could be source reference.
         filename = arguments.source.path
         filename = self.api.filename_to_server(filename)
         func_name = 'None'
 
         self.api.remove_all_breakpoints(py_db, filename)
 
+        # Validate breakpoints and adjust their positions.
+        try:
+            lines = sorted(_get_code_lines(filename))
+        except Exception:
+            pass
+        else:
+            for bp in arguments.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]
+
         btype = 'python-line'
         suspend_policy = 'ALL'
 
@@ -408,10 +455,10 @@ def on_setbreakpoints_request(self, py_db, request):
             # Note that the id is made up (the id for pydevd is unique only within a file, so, the
             # line is used for it).
             # Also, the id is currently not used afterwards, so, we don't even keep a mapping.
-            breakpoints_set.append({'id':self._next_breakpoint_id(), 'verified': True, 'line': line})
+            breakpoints_set.append({'id': self._next_breakpoint_id(), 'verified': True, 'line': line})
 
         body = {'breakpoints': breakpoints_set}
-        set_breakpoints_response = pydevd_base_schema.build_response(request, kwargs={'body':body})
+        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_setexceptionbreakpoints_request(self, py_db, request):
diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py
--- a/src/ptvsd/wrapper.py
+++ b/src/ptvsd/wrapper.py
@@ -4,7 +4,6 @@
 
 from __future__ import print_function, absolute_import, unicode_literals
 
-import bisect
 import copy
 import errno
 import json
@@ -1590,27 +1589,6 @@ def on_setBreakpoints(self, request, args):
         pydevd_request = copy.deepcopy(request)
         del pydevd_request['seq']  # A new seq should be created for pydevd.
 
-        # Validate breakpoints and adjust their positions.
-        args = pydevd_request['arguments']
-        if 'breakpoints' in args and 'path' in args['source']:
-            path = args['source']['path']
-            if sys.version_info < (3,) and not isinstance(path, bytes):
-                path = path.encode(sys.getfilesystemencoding())
-            path = path_to_unicode(pydevd_file_utils.norm_file_to_server(path))
-
-        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,
             pydevd_request,


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_adjust_breakpoint.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_adjust_breakpoint.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_adjust_breakpoint.py
@@ -0,0 +1,10 @@
+def Call():
+    b = True
+    while b:        # expected
+        pass        # requested
+        break
+
+
+if __name__ == '__main__':
+    Call()
+    print('TEST SUCEEDED!')
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -1295,6 +1295,37 @@ def test_stack_levels(case_setup):
         writer.finished_ok = True
 
 
+def test_breakpoint_adjustment(case_setup):
+    with case_setup.test_file('_debugger_case_adjust_breakpoint.py') as writer:
+        json_facade = JsonFacade(writer)
+
+        writer.write_set_protocol('http_json')
+        bp_requested = writer.get_line_index_with_content('requested')
+        bp_expected = writer.get_line_index_with_content('expected')
+
+        set_bp_request = json_facade.write_request(
+            pydevd_schema.SetBreakpointsRequest(pydevd_schema.SetBreakpointsArguments(
+                source=pydevd_schema.Source(path=writer.TEST_FILE, sourceReference=0),
+                breakpoints=[pydevd_schema.SourceBreakpoint(bp_requested).to_dict()]))
+        )
+        set_bp_response = json_facade.wait_for_response(set_bp_request)
+        assert set_bp_response.body.breakpoints[0]['line'] == bp_expected
+
+        json_facade.write_make_initial_run()
+
+        hit = writer.wait_for_breakpoint_hit()
+
+        stack_trace_request = json_facade.write_request(
+            pydevd_schema.StackTraceRequest(pydevd_schema.StackTraceArguments(threadId=hit.thread_id)))
+        stack_trace_response = json_facade.wait_for_response(stack_trace_request)
+        stack_frame = next(iter(stack_trace_response.body.stackFrames))
+        assert stack_frame['line'] == bp_expected
+
+        writer.write_run_thread(hit.thread_id)
+
+        writer.finished_ok = True
+
+
 @pytest.mark.skipif(IS_JYTHON, reason='No goto on Jython.')
 def test_goto(case_setup):
     with case_setup.test_file('_debugger_case_set_next_statement.py') as writer: