Repository: microsoft/ptvsd

Issue:
Exception info should use CMD_GET_EXCEPTION_DETAILS



Discussion:

Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -272,16 +272,6 @@ def vscode_ids(self):
         return ids
 
 
-class ExceptionInfo(object):
-    # TODO: docstring
-
-    def __init__(self, name, description, stack, source):
-        self.name = name
-        self.description = description
-        self.stack = stack
-        self.source = source
-
-
 class PydevdSocket(object):
     """A dummy socket-like object for communicating with pydevd.
 
@@ -453,7 +443,7 @@ def _find_exception(self, name):
             return name
 
         for ex_name in self.exceptions.keys():
-            # ExceptionInfo.name can be in repr form
+            # exception name can be in repr form
             # here we attempt to find the exception as it
             # is saved in the dictionary
             if ex_name in name:
@@ -2145,72 +2135,71 @@ def on_setExceptionBreakpoints(self, request, args):
         if request is not None:
             self.send_response(request)
 
+    def _parse_exception_details(self, exc_xml, include_stack=True):
+        exc_name = None
+        exc_desc = None
+        exc_source = None
+        exc_stack = None
+        try:
+            xml = self.parse_xml_response(exc_xml)
+            re_name = r"[\'\"](.*)[\'\"]"
+            exc_type = xml.thread['exc_type']
+            exc_desc = xml.thread['exc_desc']
+            try:
+                exc_name = re.findall(re_name, exc_type)[0]
+            except IndexError:
+                exc_name = exc_type
+
+            if include_stack:
+                xframes = list(xml.thread.frame)
+                frame_data = []
+                for f in xframes:
+                    file_path = unquote(f['file'])
+                    if not self.internals_filter.is_internal_path(file_path) \
+                       and self._should_debug(file_path):
+                        line_no = int(f['line'])
+                        func_name = unquote(f['name'])
+                        if _util.is_py34():
+                            # NOTE: In 3.4.* format_list requires the text
+                            # to be passed in the tuple list.
+                            line_text = _util.get_line_for_traceback(file_path,
+                                                                     line_no)
+                            frame_data.append((file_path, line_no,
+                                               func_name, line_text))
+                        else:
+                            frame_data.append((file_path, line_no,
+                                               func_name, None))
+
+                exc_stack = ''.join(traceback.format_list(frame_data))
+                exc_source = unquote(xframes[0]['file'])
+                if self.internals_filter.is_internal_path(exc_source) or \
+                    not self._should_debug(exc_source):
+                    exc_source = None
+        except Exception:
+            exc_name = 'BaseException'
+            exc_desc = 'exception: no description'
+
+        return exc_name, exc_desc, exc_source, exc_stack
+
     @async_handler
     def on_exceptionInfo(self, request, args):
         # TODO: docstring
         pyd_tid = self.thread_map.to_pydevd(args['threadId'])
 
-        # For exception cases both raise and uncaught, pydevd adds a
-        # __exception__ object to the top most frame. Extracting the
-        # exception name and description from that frame gives accurate
-        # exception information. Get exception info from frame
-        try:
-            cmd = pydevd_comm.CMD_GET_THREAD_STACK
-            _, _, resp_args = yield self.pydevd_request(cmd, pyd_tid)
-            xml = self.parse_xml_response(resp_args)
-            xframes = list(xml.thread.frame)
-
-            xframe = xframes[0]
-            pyd_fid = xframe['id']
-
-            cmdargs = '{}\t{}\tFRAME\t__exception__'.format(pyd_tid, pyd_fid)
-            cmdid = pydevd_comm.CMD_GET_VARIABLE
-            _, _, resp_args = yield self.pydevd_request(cmdid, cmdargs)
-            xml = self.parse_xml_response(resp_args)
-
-            name = unquote(xml.var[1]['type'])
-            description = unquote(xml.var[1]['value'])
-
-            frame_data = []
-            for f in xframes:
-                file_path = unquote(f['file'])
-                if not self.internals_filter.is_internal_path(file_path) and \
-                    self._should_debug(file_path):
-                    line_no = int(f['line'])
-                    func_name = unquote(f['name'])
-                    if _util.is_py34():
-                        # NOTE: In 3.4.* format_list requires the text
-                        # to be passed in the tuple list.
-                        line_text = _util.get_line_for_traceback(file_path,
-                                                                 line_no)
-                        frame_data.append((file_path, line_no,
-                                           func_name, line_text))
-                    else:
-                        frame_data.append((file_path, line_no,
-                                           func_name, None))
-
-            stack = ''.join(traceback.format_list(frame_data))
-
-            source = unquote(xframe['file'])
-            if self.internals_filter.is_internal_path(source) or \
-                not self._should_debug(source):
-                source = None
-        except Exception:
-            name = 'BaseException'
-            description = 'exception: no description'
-            stack = None
-            source = None
+        cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS
+        _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid)
+        name, description, source, stack  = \
+            self._parse_exception_details(resp_args)
 
-        exc = ExceptionInfo(name, description, stack, source)
         self.send_response(
             request,
-            exceptionId=exc.name,
-            description=exc.description,
-            breakMode=self.exceptions_mgr.get_break_mode(exc.name),
-            details={'typeName': exc.name,
-                     'message': exc.description,
-                     'stackTrace': exc.stack,
-                     'source': exc.source},
+            exceptionId=name,
+            description=description,
+            breakMode=self.exceptions_mgr.get_break_mode(name),
+            details={'typeName': name,
+                     'message': description,
+                     'stackTrace': stack,
+                     'source': source},
         )
 
     # Custom ptvsd message
@@ -2366,8 +2355,8 @@ def on_pydevd_thread_suspend(self, seq, args):
         autogen = self.start_reason == 'attach'
         vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=autogen)
 
-        description = None
-        text = None
+        exc_desc = None
+        exc_name = None
         if reason in STEP_REASONS:
             reason = 'step'
         elif reason in EXCEPTION_REASONS:
@@ -2378,26 +2367,17 @@ def on_pydevd_thread_suspend(self, seq, args):
             reason = 'pause'
 
         if reason == 'exception':
-            try:
-                pyd_fid = xframe['id']
-                cmdargs = '{}\t{}\tFRAME\t__exception__'.format(pyd_tid,
-                                                                pyd_fid)
-                cmdid = pydevd_comm.CMD_GET_VARIABLE
-                _, _, resp_args = yield self.pydevd_request(cmdid, cmdargs)
-                xml = self.parse_xml_response(resp_args)
-
-                text = unquote(xml.var[1]['type'])
-                description = unquote(xml.var[1]['value'])
-            except Exception:
-                text = 'BaseException'
-                description = 'exception: no description'
+            cmdid = pydevd_comm.CMD_GET_EXCEPTION_DETAILS
+            _, _, resp_args = yield self.pydevd_request(cmdid, pyd_tid)
+            exc_name, exc_desc, _, _  = \
+                self._parse_exception_details(resp_args, include_stack=False)
 
         self.send_event(
             'stopped',
             reason=reason,
             threadId=vsc_tid,
-            text=text,
-            description=description)
+            text=exc_name,
+            description=exc_desc)
 
     @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN)
     def on_pydevd_thread_run(self, seq, args):


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
@@ -101,3 +101,19 @@ def format_exception(self, threadid, exc, frame):
                 frame,
             ),
         )
+
+    def format_exception_details(self, threadid, exc, *frames):
+        name = pydevd_xml.make_valid_xml_value(str(type(exc)))
+        if hasattr(exc, 'args') and len(exc.args) > 0:
+            desc = str(exc.args[0])
+        else:
+            desc = str(exc)
+        desc = pydevd_xml.make_valid_xml_value(desc)
+        info = '<xml>'
+        info += '<thread id="{}" exc_type="{}" exc_desc="{}" >'.format(
+            threadid, name, desc)
+        fmt = '<frame id="{}" name="{}" file="{}" line="{}" />'
+        for frame in frames:  # (fid, func, filename, line)
+            info += fmt.format(*frame)
+        info += '</thread></xml>'
+        return info
diff --git a/tests/highlevel/__init__.py b/tests/highlevel/__init__.py
--- a/tests/highlevel/__init__.py
+++ b/tests/highlevel/__init__.py
@@ -15,9 +15,9 @@
     CMD_STEP_CAUGHT_EXCEPTION,
     CMD_SEND_CURR_EXCEPTION_TRACE,
     CMD_THREAD_CREATE,
-    CMD_GET_VARIABLE,
     CMD_SET_PROJECT_ROOTS,
     CMD_GET_THREAD_STACK,
+    CMD_GET_EXCEPTION_DETAILS,
 )
 
 from ptvsd._util import new_hidden_thread
@@ -503,12 +503,11 @@ def _exception(self, thread, exc, reason, stack):
         self.send_suspend_event(thread, reason, *stack)
         #self.set_exception_var_response(exc)
 
-    def set_exception_var_response(self, exc):
+    def set_exception_var_response(self, threadid, exc, *frames):
         self.set_response(
-            CMD_GET_VARIABLE,
-            self.msgs.format_variables(
-                ('???', '???'),
-                ('???', exc),
+            CMD_GET_EXCEPTION_DETAILS,
+            self.msgs.format_exception_details(
+                threadid, exc, *frames
             ),
         )
 
@@ -824,7 +823,7 @@ def suspend(self, thread, reason, *stack):
         with self.wait_for_event('stopped'):
             if isinstance(reason, Exception):
                 exc = reason
-                self._pydevd.set_exception_var_response(exc)
+                self._pydevd.set_exception_var_response(thread.id, exc, *stack)
                 self._pydevd.send_caught_exception_events(thread, exc, *stack)
             else:
                 self._pydevd.send_suspend_event(thread, reason, *stack)
diff --git a/tests/highlevel/test_live_pydevd.py b/tests/highlevel/test_live_pydevd.py
--- a/tests/highlevel/test_live_pydevd.py
+++ b/tests/highlevel/test_live_pydevd.py
@@ -391,16 +391,12 @@ def test_exception_breakpoints(self):
                 got.append(req['arguments'])
             self.assertNotEqual(req['command'], 'setBreakpoints')
         self.assertEqual(got, config['excbreakpoints'])
-        if sys.version_info >= (3, 7):
-            description = "MyError('ka-boom')"
-        else:
-            description = "MyError('ka-boom',)"
         self.assert_contains(received, [
             self.new_event('stopped', **dict(
                  reason='exception',
                  threadId=tid,
-                 text='MyError',
-                 description=description)),
+                 text='__main__.MyError',
+                 description='ka-boom')),
             self.new_response(req_continue_last, allThreadsContinued=True),
             self.new_event('continued', **dict(threadId=tid, )),
         ])
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
@@ -32,6 +32,7 @@
     CMD_WRITE_TO_CONSOLE,
     CMD_STEP_INTO_MY_CODE,
     CMD_GET_THREAD_STACK,
+    CMD_GET_EXCEPTION_DETAILS,
 )
 
 from . import RunningTest
@@ -2113,11 +2114,12 @@ class ExceptionInfoTests(NormalRequestTest, unittest.TestCase):
     #       ],
     #   ),
 
-    PYDEVD_CMD = CMD_GET_THREAD_STACK
+    PYDEVD_CMD = CMD_GET_EXCEPTION_DETAILS
 
     def pydevd_payload(self, threadid, *args):
-        if self.PYDEVD_CMD == CMD_GET_THREAD_STACK:
-            return self.debugger_msgs.format_frames(threadid, 'pause', *args)
+        if self.PYDEVD_CMD == CMD_GET_EXCEPTION_DETAILS:
+            return self.debugger_msgs.format_exception_details(
+                threadid, args[0], *args[1:])
         else:
             return self.debugger_msgs.format_variables(*args)
 
@@ -2128,28 +2130,19 @@ def test_active_exception(self):
         with self.launched():
             with self.hidden():
                 tid, thread = self.error('x', exc, frame)
-            self.set_debugger_response(thread.id, frame)
-            self.PYDEVD_CMD = CMD_GET_VARIABLE
-            self.set_debugger_response(
-                thread.id,
-                ('type', exc.__class__.__name__),
-                ('value', exc))
+            self.set_debugger_response(thread.id, exc, frame)
             self.send_request(
                 threadId=tid,
             )
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info < (3, 7):
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_response(
                 exceptionId='RuntimeError',
-                description=excstr,
+                description='something went wrong',
                 breakMode='unhandled',
                 details=dict(
-                    message=excstr,
+                    message='something went wrong',
                     typeName='RuntimeError',
                     source=__file__,
                     stackTrace='\n'.join([
@@ -2163,11 +2156,7 @@ def test_active_exception(self):
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_THREAD_STACK,
-                str(thread.id)),
-            self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                '{}\t2\tFRAME\t__exception__'.format(thread.id)),
+                CMD_GET_EXCEPTION_DETAILS, str(thread.id)),
         ])
 
     def test_no_exception(self):
@@ -2177,11 +2166,7 @@ def test_no_exception(self):
         with self.launched():
             with self.hidden():
                 tid, thread = self.error('x', exc, frame)
-            self.set_debugger_response(thread.id, frame)
-            self.PYDEVD_CMD = CMD_GET_VARIABLE
-            # Don't provide exception info to simulate no exception
-            self.set_debugger_response(
-                thread.id)
+            self.set_debugger_response(thread.id, exc, frame)
             self.send_request(
                 threadId=tid,
             )
@@ -2189,24 +2174,25 @@ def test_no_exception(self):
 
         self.assert_vsc_received(received, [
             self.expected_response(
-                exceptionId='BaseException',
-                description='exception: no description',
+                exceptionId='RuntimeError',
+                description='something went wrong',
                 breakMode='unhandled',
                 details=dict(
-                    typeName='BaseException',
-                    message='exception: no description',
-                    stackTrace=None,
-                    source=None
+                    typeName='RuntimeError',
+                    message='something went wrong',
+                    source=__file__,
+                    stackTrace='\n'.join([
+                        '  File "{}", line {}, in fail'.format(__file__,
+                                                               lineno),
+                        '    raise RuntimeError(msg)',
+                        '',
+                    ]),
                 ),
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_THREAD_STACK,
-                str(thread.id)),
-            self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                '{}\t2\tFRAME\t__exception__'.format(thread.id)),
+                CMD_GET_EXCEPTION_DETAILS, str(thread.id)),
         ])
 
 
@@ -2666,31 +2652,26 @@ def test_caught_exception(self):
             with self.hidden():
                 _, thread = self.set_thread('x')
             self.set_debugger_response(
-                CMD_GET_VARIABLE,
-                self.debugger_msgs.format_variables(
-                    ('???', '???'),
-                    ('???', exc),
+                CMD_GET_EXCEPTION_DETAILS,
+                self.debugger_msgs.format_exception_details(
+                    thread.id, exc
                 ),
             )
             tid = self.send_event(thread.id, CMD_STEP_CAUGHT_EXCEPTION)
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info[1] < 7:
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_event(
                 reason='exception',
                 threadId=tid,
                 text='RuntimeError',
-                description=excstr,
+                description='something went wrong',
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                str(thread.id), '2', 'FRAME', '__exception__'),
+                CMD_GET_EXCEPTION_DETAILS,
+                str(thread.id)),
         ])
 
     def test_exception_break(self):
@@ -2699,31 +2680,26 @@ def test_exception_break(self):
             with self.hidden():
                 _, thread = self.set_thread('x')
             self.set_debugger_response(
-                CMD_GET_VARIABLE,
-                self.debugger_msgs.format_variables(
-                    ('???', '???'),
-                    ('???', exc),
+                CMD_GET_EXCEPTION_DETAILS,
+                self.debugger_msgs.format_exception_details(
+                    thread.id, exc
                 ),
             )
             tid = self.send_event(thread.id, CMD_ADD_EXCEPTION_BREAK)
             received = self.vsc.received
 
-        # TODO: Is this the right str?
-        excstr = "RuntimeError('something went wrong')"
-        if sys.version_info[1] < 7:
-            excstr = excstr[:-1] + ',)'
         self.assert_vsc_received(received, [
             self.expected_event(
                 reason='exception',
                 threadId=tid,
                 text='RuntimeError',
-                description=excstr,
+                description='something went wrong',
             ),
         ])
         self.assert_received(self.debugger, [
             self.debugger_msgs.new_request(
-                CMD_GET_VARIABLE,
-                str(thread.id), '2', 'FRAME', '__exception__'),
+                CMD_GET_EXCEPTION_DETAILS,
+                str(thread.id)),
         ])
 
     def test_suspend(self):
diff --git a/tests/system_tests/test_exceptions.py b/tests/system_tests/test_exceptions.py
--- a/tests/system_tests/test_exceptions.py
+++ b/tests/system_tests/test_exceptions.py
@@ -77,8 +77,7 @@ def run_test_breaking_into_handled_exceptions(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -129,8 +128,7 @@ def run_test_breaking_into_unhandled_exceptions(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -191,8 +189,7 @@ def run_test_breaking_into_raised_exceptions_only(self, debug_info,
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -247,8 +244,7 @@ def run_test_breaking_into_raised_and_unhandled_exceptions(
 
             Awaitable.wait_all(req_launch_attach, stopped)
             self.assertEqual(stopped.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped.event.body['description'])
+            self.assertEqual(stopped.event.body['description'], 'Hello')
 
             thread_id = stopped.event.body['threadId']
             req_exc_info = dbg.session.send_request(
@@ -267,8 +263,8 @@ def run_test_breaking_into_raised_and_unhandled_exceptions(
 
             # Second hit on uncaught exception
             self.assertEqual(stopped2.event.body['text'], 'ArithmeticError')
-            self.assertIn("ArithmeticError('Hello'",
-                          stopped2.event.body['description'])
+            self.assertEqual(stopped2.event.body['description'], 'Hello')
+
             req_exc_info2 = dbg.session.send_request(
                 'exceptionInfo',
                 threadId=thread_id,