Repository: microsoft/ptvsd

Issue:
Undefined variable error in breakpoint condition on for line
## Environment data

- PTVSD version: 4.1.3
- OS and version: Windows 10
- Python version (& distribution if applicable, e.g. Anaconda): 3.6
- Using VS Code or Visual Studio: VS

## Actual behavior

Undefined variable error, breakpoint never hit
```
Error while evaluating expression: i>1600 
Traceback (most recent call last): 
  File "c:\program files (x86)\microsoft visual studio\2019\preview\common7\ide\extensions\microsoft\python\core\Packages\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_frame.py", line 52, in handle_breakpoint_condition 
    return eval(condition, new_frame.f_globals, new_frame.f_locals) 
  File "<string>", line 1, in <module> 
NameError: name 'i' is not defined 
The program 'python.exe' has exited with code -1 (0xffffffff).
```


## Expected  behavior

Undefined variable error ignored and breakpoint hit once it is defined. That was the behavior in ptvsd 3.x. 

## Steps to reproduce:
1. Write this code:

```
from math import cos, radians 
def make_dot_string(x): 
    rad = radians(x) 
    numspaces = int(20 * cos(radians(x)) + 20) 
    st = ' ' * numspaces + 'o' 
    return st 
def main(): 
    for i in range(0, 1800, 12): 
        s=make_dot_string(i) 
        print(s) 
main()
```

2. set a conditions breakpoint on the `for i in range...` line, hover on red dot and click breakpoint setting, set condition `i>1600`

3. start debugging

Note: of course if you put the breakpoint inside the for loop instead, that works as expected.



Discussion:
A question: should all errors be silenced or just name errors?
@karthiknadig this one needs additional integration work (which was removed because of failing ptvsd tests).
@fabioz with the fix, i don't see `NameError` but the break point does not hit after `i` gets defined.
EDIT: Ignore this. This works as expected, with the fix.

Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -7,6 +7,7 @@
 import contextlib
 import errno
 import io
+import json
 import os
 import platform
 import pydevd_file_utils
@@ -1403,6 +1404,12 @@ def _process_debug_options(self, opts):
         if opts.get('SHOW_RETURN_VALUE', False):
             self.pydevd_request(pydevd_comm.CMD_SHOW_RETURN_VALUES, '1\t1')
 
+        # Print on all but NameError, don't suspend on any.
+        self.pydevd_request(pydevd_comm.CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION, json.dumps(dict(
+            skip_suspend_on_breakpoint_exception=('BaseException',),
+            skip_print_breakpoint_exception=('NameError',),
+        )))
+
         self._apply_code_stepping_settings()
 
     def _is_just_my_code_stepping_enabled(self):
@@ -2525,3 +2532,8 @@ def on_pydevd_cmd_write_to_console2(self, seq, args):
         category = 'stdout' if ctx == '1' else 'stderr'
         content = unquote(xml.io['s'])
         self.send_event('output', category=category, output=content)
+
+    @pydevd_events.handler(pydevd_comm.CMD_GET_BREAKPOINT_EXCEPTION)
+    def on_pydevd_get_breakpoint_exception(self, seq, args):
+        # If pydevd sends exception info from a failed breakpoint condition, just ignore.
+        pass


Test patch:
diff --git a/tests/helpers/pydevd/_messages.py b/tests/helpers/pydevd/_messages.py
--- a/tests/helpers/pydevd/_messages.py
+++ b/tests/helpers/pydevd/_messages.py
@@ -117,3 +117,15 @@ def format_exception_details(self, threadid, exc, *frames):
             info += fmt.format(*frame)
         info += '</thread></xml>'
         return info
+
+    def format_breakpoint_exception(self, threadid, exc_type, stacktrace):
+        info = '<xml>'
+        for filename, line, methodname, methodobj in stacktrace:
+            fn = pydevd_xml.make_valid_xml_value(filename)
+            mn = pydevd_xml.make_valid_xml_value(methodname)
+            obj = pydevd_xml.make_valid_xml_value(methodobj)
+            info += '<frame thread_id = "%s" file="%s" line="%s" name="%s" obj="%s" />' \
+                % (threadid, fn, line, mn, obj)
+        info += "</xml>"
+        return exc_type + '\t' + info
+
diff --git a/tests/highlevel/__init__.py b/tests/highlevel/__init__.py
--- a/tests/highlevel/__init__.py
+++ b/tests/highlevel/__init__.py
@@ -18,6 +18,7 @@
     CMD_SET_PROJECT_ROOTS,
     CMD_GET_THREAD_STACK,
     CMD_GET_EXCEPTION_DETAILS,
+    CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION,
 )
 
 from ptvsd._util import new_hidden_thread
@@ -140,9 +141,10 @@ def __init__(self, fix):
     @contextlib.contextmanager
     def _wait_for_initialized(self):
         with self._fix.wait_for_command(CMD_REDIRECT_OUTPUT):
-            with self._fix.wait_for_command(CMD_SET_PROJECT_ROOTS):
-                with self._fix.wait_for_command(CMD_RUN):
-                    yield
+            with self._fix.wait_for_command(CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION):
+                with self._fix.wait_for_command(CMD_SET_PROJECT_ROOTS):
+                    with self._fix.wait_for_command(CMD_RUN):
+                        yield
 
     def _initialize(self):
         version = self._fix.fake.VERSION
diff --git a/tests/highlevel/test_lifecycle.py b/tests/highlevel/test_lifecycle.py
--- a/tests/highlevel/test_lifecycle.py
+++ b/tests/highlevel/test_lifecycle.py
@@ -1,3 +1,4 @@
+import json
 import os
 import ptvsd
 import sys
@@ -8,6 +9,7 @@
     CMD_RUN,
     CMD_VERSION,
     CMD_SET_PROJECT_ROOTS,
+    CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION,
 )
 
 from . import (
@@ -112,6 +114,10 @@ def attach(self, expected_os_id, attach_args):
             self.debugger_msgs.new_request(CMD_VERSION,
                                            *['1.1', expected_os_id, 'ID']),
             self.debugger_msgs.new_request(CMD_REDIRECT_OUTPUT),
+            self.debugger_msgs.new_request(CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION, json.dumps(dict(
+                skip_suspend_on_breakpoint_exception=('BaseException',),
+                skip_print_breakpoint_exception=('NameError',),
+            ))),
             self.debugger_msgs.new_request(CMD_SET_PROJECT_ROOTS,
                                            _get_project_dirs()),
             self.debugger_msgs.new_request(CMD_RUN),
@@ -158,7 +164,8 @@ def test_launch(self):
             # configuration
             req_config = self.send_request('configurationDone')
             self.wait_for_pydevd('version', 'redirect-output',
-                                 'run', 'set_project_roots')
+                                 'run', 'suspend_on_breakpoint_exception',
+                                 'set_project_roots')
 
             # Normal ops would go here.
 
@@ -190,6 +197,10 @@ def test_launch(self):
             self.debugger_msgs.new_request(CMD_VERSION,
                                            *['1.1', OS_ID, 'ID']),
             self.debugger_msgs.new_request(CMD_REDIRECT_OUTPUT),
+            self.debugger_msgs.new_request(CMD_SUSPEND_ON_BREAKPOINT_EXCEPTION, json.dumps(dict(
+                skip_suspend_on_breakpoint_exception=('BaseException',),
+                skip_print_breakpoint_exception=('NameError',),
+            ))),
             self.debugger_msgs.new_request(CMD_SET_PROJECT_ROOTS,
                                            _get_project_dirs()),
             self.debugger_msgs.new_request(CMD_RUN),
diff --git a/tests/highlevel/test_messages.py b/tests/highlevel/test_messages.py
--- a/tests/highlevel/test_messages.py
+++ b/tests/highlevel/test_messages.py
@@ -2798,22 +2798,19 @@ class GetExceptionBreakpointEventTests(PyDevdEventTest, unittest.TestCase):
     CMD = CMD_GET_BREAKPOINT_EXCEPTION
     EVENT = None
 
-    def pydevd_payload(self, tid, exc, frame):
-        return self.debugger_msgs.format_exception(tid, exc, frame)
+    def pydevd_payload(self, tid, exc_type, stacktrace):
+        return self.debugger_msgs.format_breakpoint_exception(tid, exc_type, stacktrace)
 
-    def test_unsupported(self):
-        ptid = 10
-        exc = RuntimeError('something went wrong')
-        frame = (2, 'spam', 'abc.py', 10)  # (pfid, func, file, line)
+    def test_basic(self):
+        trace = [('abc.py', 10, 'spam', 'eggs'), ]  # (file, line, func, obj)
         with self.launched():
-            with self.assertRaises(UnsupportedPyDevdCommandError):
-                self.send_event(ptid, exc, frame)
+            self.send_event(10, 'RuntimeError', trace)
             received = self.vsc.received
 
+        # Note: We don't send any events to client. So this should be empty.
         self.assert_vsc_received(received, [])
         self.assert_received(self.debugger, [])
 
-
 class ShowConsoleEventTests(PyDevdEventTest, unittest.TestCase):
 
     CMD = CMD_SHOW_CONSOLE
@@ -2853,3 +2850,20 @@ def test_unsupported(self):
 
         self.assert_vsc_received(received, [])
         self.assert_received(self.debugger, [])
+
+class UnsupportedPyDevdEventTests(PyDevdEventTest, unittest.TestCase):
+
+    CMD = 12345
+    EVENT = None
+
+    def pydevd_paylaod(self, msg):
+        return msg
+
+    def test_unsupported(self):
+        with self.launched():
+            with self.assertRaises(UnsupportedPyDevdCommandError):
+                self.send_event('unsupported')
+            received = self.vsc.received
+
+        self.assert_vsc_received(received, [])
+        self.assert_received(self.debugger, [])