Repository: microsoft/ptvsd

Issue:
Don't step into loop when pressing F11 with hit count breakpoint
## Environment data

- VS Code version: 1.40.0-insider
- Extension version (available under the Extensions sidebar): 2019.11.47235-dev

## Expected behaviour

It should step into loop when pressing F11 with hit count breakpoint

## Actual behaviour

Cannot step into loop when pressing F11 with hit count breakpoint, It always stop on "for" statement
![image](https://user-images.githubusercontent.com/41426540/68365455-a0da2200-016b-11ea-9ae6-8513edb93836.png)

## Steps to reproduce:
1. Input below code:
```
    for i in range(1,500,20):
          print(i)
```
2. Set "Hit Count" condition breakpoint such as "10" on "print" statement
3. Press F5
4. Press F11

Repro gif:
![stepintoloop](https://user-images.githubusercontent.com/41426540/68365629-32499400-016c-11ea-97d7-dd7fc0fb37cb.gif)



Discussion:
I think it's because there is no way for the tracing code to distinguish whether this line was hit because it was the target of a step, or because of the breakpoint. So it assumes breakpoint, and then when the condition isn't true, auto-resumes.

I'm not sure what we can do here, though. In particular, we can't just fall back to treating it as end of step if condition isn't True - the problem is that there are cases where the two really are different. E.g. suppose it was some custom generator function instead of `range()`, there was a hitcount breakpoint inside of it, and it was F10 (Step Over). In that case, the breakpoint inside the generator would be hit, and the condition evaluate to False. If we then ignore that, and assume that it was the end of the step, then we'd break in the wrong place. 

The fundamental problem here is that we don't try to compute the destination for a step - which would be very complicated, likely requiring runtime bytecode inspection, and even then I'm not sure how that would work with third party native code modules. We instead rely on Python reporting a line number / stack frame change to us, and treat that as the end of the step. This makes it impossible to distinguish from a breakpoint on the same line. And normally that's not an issue, because with regular breakpoints, it doesn't really matter why we stopped on that line, so long as we stopped. But for conditional and hitcount breakpoint, the reason why we ended up on that line does matter.

@fabioz, do you have any ideas?
I'll take a look at it.

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
@@ -666,9 +666,9 @@ def trace_dispatch(self, frame, event, arg):
 
                         if breakpoint.has_condition:
                             if not eval_result:
-                                return self.trace_dispatch
+                                stop = False
                         elif breakpoint.is_logpoint:
-                            return self.trace_dispatch
+                            stop = False
 
                     if is_call and frame.f_code.co_name in ('<module>', '<lambda>'):
                         # If we find a call for a module, it means that the module is being imported/executed for the


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_hit_count.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_hit_count.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_hit_count.py
@@ -0,0 +1,6 @@
+if __name__ == '__main__':
+    x = 0  # before loop line
+    for i in range(10):  # for line
+        print(i)  # print line
+
+    print('TEST SUCEEDED!')
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
@@ -479,6 +479,64 @@ def test_case_json_logpoints(case_setup):
         writer.finished_ok = True
 
 
+def test_case_json_logpoint_and_step(case_setup):
+    with case_setup.test_file('_debugger_case_hit_count.py') as writer:
+        json_facade = JsonFacade(writer)
+
+        json_facade.write_launch()
+        before_loop_line = writer.get_line_index_with_content('before loop line')
+        for_line = writer.get_line_index_with_content('for line')
+        print_line = writer.get_line_index_with_content('print line')
+        json_facade.write_set_breakpoints(
+            [before_loop_line, print_line],
+            line_to_info={
+                print_line: {'log_message': 'var {repr("_a")} is {_a}'}
+        })
+        json_facade.write_make_initial_run()
+
+        json_hit = json_facade.wait_for_thread_stopped(line=before_loop_line)
+
+        json_facade.write_step_in(json_hit.thread_id)
+        json_hit = json_facade.wait_for_thread_stopped('step', line=for_line)
+
+        json_facade.write_step_in(json_hit.thread_id)
+        json_hit = json_facade.wait_for_thread_stopped('step', line=print_line)
+
+        json_facade.write_continue()
+
+        writer.finished_ok = True
+
+
+@pytest.mark.skipif(IS_PY26, reason='Failing on Python 2.6')
+def test_case_json_hit_count_and_step(case_setup):
+    with case_setup.test_file('_debugger_case_hit_count.py') as writer:
+        json_facade = JsonFacade(writer)
+
+        json_facade.write_launch()
+        for_line = writer.get_line_index_with_content('for line')
+        print_line = writer.get_line_index_with_content('print line')
+        json_facade.write_set_breakpoints(
+            [print_line],
+            line_to_info={
+                print_line: {'hit_condition': '5'}
+        })
+        json_facade.write_make_initial_run()
+
+        json_hit = json_facade.wait_for_thread_stopped(line=print_line)
+        i_local_var = json_facade.get_local_var(json_hit.frame_id, 'i')  # : :type i_local_var: pydevd_schema.Variable
+        assert i_local_var.value == '4'
+
+        json_facade.write_step_in(json_hit.thread_id)
+        json_hit = json_facade.wait_for_thread_stopped('step', line=for_line)
+
+        json_facade.write_step_in(json_hit.thread_id)
+        json_hit = json_facade.wait_for_thread_stopped('step', line=print_line)
+
+        json_facade.write_continue()
+
+        writer.finished_ok = True
+
+
 def test_case_process_event(case_setup):
     with case_setup.test_file('_debugger_case_change_breaks.py') as writer:
         json_facade = JsonFacade(writer)