From df9c8fd9e0dd665c03448a7e983e6aa258cdd2d8 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 28 Aug 2018 18:33:28 -0700 Subject: [PATCH] Adds debug console completion (#772) * Add support for debug console completion * Add test files * Adding tests * Add supportsCompletionsRequest to _requests * Add test for bad request * Fix sorting issue in test * Address comments * Remove unsupported test for completions * Add required argument to completions request in tests * Fix linter issues --- debugger_protocol/messages/_requests.py | 1 + ptvsd/wrapper.py | 45 +++ tests/highlevel/test_messages.py | 18 -- .../test_completions/attach_completions.py | 23 ++ .../test_completions/launch_completions.py | 16 + tests/system_tests/test_completions.py | 294 ++++++++++++++++++ 6 files changed, 379 insertions(+), 18 deletions(-) create mode 100644 tests/resources/system_tests/test_completions/attach_completions.py create mode 100644 tests/resources/system_tests/test_completions/launch_completions.py create mode 100644 tests/system_tests/test_completions.py diff --git a/debugger_protocol/messages/_requests.py b/debugger_protocol/messages/_requests.py index aa0b041ef..27e2279fd 100644 --- a/debugger_protocol/messages/_requests.py +++ b/debugger_protocol/messages/_requests.py @@ -83,6 +83,7 @@ class Capabilities(FieldsNamespace): Field('supportsSetExpression', bool), Field('supportsModulesRequest', bool), Field('supportsDebuggerProperties', bool), + Field('supportsCompletionsRequest', bool), ] diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 4f11e8660..fe1e21b3f 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -54,6 +54,22 @@ # print(s) #ipcjson._TRACE = ipcjson_trace +#completion types. +TYPE_IMPORT = '0' +TYPE_CLASS = '1' +TYPE_FUNCTION = '2' +TYPE_ATTR = '3' +TYPE_BUILTIN = '4' +TYPE_PARAM = '5' +TYPE_LOOK_UP = { + TYPE_IMPORT: 'module', + TYPE_CLASS: 'class', + TYPE_FUNCTION: 'function', + TYPE_ATTR: 'field', + TYPE_BUILTIN: 'keyword', + TYPE_PARAM: 'variable', +} + def NOOP(*args, **kwargs): pass @@ -952,6 +968,7 @@ def _stop_event_loop(self): INITIALIZE_RESPONSE = dict( + supportsCompletionsRequest=True, supportsConditionalBreakpoints=True, supportsConfigurationDoneRequest=True, supportsDebuggerProperties=True, @@ -2222,6 +2239,34 @@ def on_exceptionInfo(self, request, args): 'source': source}, ) + @async_handler + def on_completions(self, request, args): + text = args['text'] + vsc_fid = args.get('frameId', None) + + try: + pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid) + except KeyError: + self.send_error_response(request) + + cmd_args = '{}\t{}\t{}\t{}'.format(pyd_tid, pyd_fid, 'LOCAL', text) + _, _, resp_args = yield self.pydevd_request( + pydevd_comm.CMD_GET_COMPLETIONS, + cmd_args) + + xml = self.parse_xml_response(resp_args) + targets = [] + for item in list(xml.comp): + target = {} + target['label'] = unquote(item['p0']) + try: + target['type'] = TYPE_LOOK_UP[item['p3']] + except KeyError: + pass + targets.append(target) + + self.send_response(request, targets=targets) + # Custom ptvsd message def on_ptvsd_systemInfo(self, request, args): try: diff --git a/tests/highlevel/test_messages.py b/tests/highlevel/test_messages.py index d48ab56bf..cefc4a5f4 100644 --- a/tests/highlevel/test_messages.py +++ b/tests/highlevel/test_messages.py @@ -2299,24 +2299,6 @@ def test_unsupported(self): self.assert_received(self.debugger, []) -class CompletionsTests(NormalRequestTest, unittest.TestCase): - - COMMAND = 'completions' - - def test_unsupported(self): - with self.launched(): - self.send_request( - text='spa', - column=3, - ) - received = self.vsc.received - - self.assert_vsc_received(received, [ - self.expected_failure('Unknown command'), - ]) - self.assert_received(self.debugger, []) - - ################################## # VSC events diff --git a/tests/resources/system_tests/test_completions/attach_completions.py b/tests/resources/system_tests/test_completions/attach_completions.py new file mode 100644 index 000000000..2e3ff8c62 --- /dev/null +++ b/tests/resources/system_tests/test_completions/attach_completions.py @@ -0,0 +1,23 @@ +import sys +import ptvsd + +ptvsd.enable_attach((sys.argv[1], sys.argv[2])) +ptvsd.wait_for_attach() + + +class SomeClass(): + def __init__(self, someVar): + self.some_var = someVar + + def do_someting(self): + someVariable = self.some_var + return someVariable + + +def someFunction(someVar): + someVariable = someVar + return SomeClass(someVariable).do_someting() + + +someFunction('value') +print('done') diff --git a/tests/resources/system_tests/test_completions/launch_completions.py b/tests/resources/system_tests/test_completions/launch_completions.py new file mode 100644 index 000000000..3b1f4c8b5 --- /dev/null +++ b/tests/resources/system_tests/test_completions/launch_completions.py @@ -0,0 +1,16 @@ +class SomeClass(): + def __init__(self, someVar): + self.some_var = someVar + + def do_someting(self): + someVariable = self.some_var + return someVariable + + +def someFunction(someVar): + someVariable = someVar + return SomeClass(someVariable).do_someting() + + +someFunction('value') +print('done') diff --git a/tests/system_tests/test_completions.py b/tests/system_tests/test_completions.py new file mode 100644 index 000000000..f14dc73a7 --- /dev/null +++ b/tests/system_tests/test_completions.py @@ -0,0 +1,294 @@ +import os +import os.path + +from tests.helpers.resource import TestResources +from . import ( + lifecycle_handshake, LifecycleTestsBase, DebugInfo, PORT, +) + + +TEST_FILES = TestResources.from_module(__name__) + + +class CompletionsTests(LifecycleTestsBase): + + def run_test_completions(self, debug_info, bp_filename, bp_line, expected): + pathMappings = [] + # Required to ensure sourceReference = 0 + if (debug_info.starttype == 'attach'): + pathMappings.append({ + 'localRoot': debug_info.cwd, + 'remoteRoot': debug_info.cwd + }) + options = { + 'debugOptions': ['RedirectOutput'], + 'pathMappings': pathMappings + } + breakpoints = [{ + 'source': { + 'path': bp_filename + }, + 'breakpoints': [{ + 'line': bp_line + }] + }] + + with self.start_debugging(debug_info) as dbg: + session = dbg.session + with session.wait_for_event('stopped') as result: + (_, req_launch_attach, _, _, _, _, + ) = lifecycle_handshake(session, debug_info.starttype, + options=options, + breakpoints=breakpoints) + req_launch_attach.wait() + + event = result['msg'] + tid = event.body['threadId'] + + req_stacktrace = session.send_request( + 'stackTrace', + threadId=tid, + ) + req_stacktrace.wait() + frames = req_stacktrace.resp.body['stackFrames'] + frame_id = frames[0]['id'] + + req_completions = session.send_request( + 'completions', + text='some', + frameId=int(frame_id), + column=1 + ) + req_completions.wait(timeout=2.0) + targets = req_completions.resp.body['targets'] + + # make a request with bad frame id + bad_req_completions = session.send_request( + 'completions', + text='some', + frameId=int(1234), + column=1 + ) + bad_req_completions.wait(timeout=2.0) + bad_result = bad_req_completions.resp.success + + session.send_request( + 'continue', + threadId=tid, + ) + + targets.sort(key=lambda t: t['label']) + expected.sort(key=lambda t: t['label']) + self.assertEqual(targets, expected) + self.assertEqual(bad_result, False) + + def run_test_outermost_scope(self, debug_info, filename, line): + self.run_test_completions( + debug_info, + bp_filename=filename, + bp_line=line, + expected=[ + { + 'label': 'SomeClass', + 'type': 'class' + }, + { + 'label': 'someFunction', + 'type': 'function' + } + ] + ) + + def run_test_in_function(self, debug_info, filename, line): + self.run_test_completions( + debug_info, + bp_filename=filename, + bp_line=line, + expected=[ + { + 'label': 'SomeClass', + 'type': 'class' + }, + { + 'label': 'someFunction', + 'type': 'function' + }, + { + 'label': 'someVar', + 'type': 'field' + }, + { + 'label': 'someVariable', + 'type': 'field' + } + ] + ) + + def run_test_in_method(self, debug_info, filename, line): + self.run_test_completions( + debug_info, + bp_filename=filename, + bp_line=line, + expected=[ + { + 'label': 'SomeClass', + 'type': 'class' + }, + { + 'label': 'someFunction', + 'type': 'function' + }, + { + 'label': 'someVariable', + 'type': 'field' + } + ] + ) + + +class LaunchFileTests(CompletionsTests): + def _get_debug_info(self): + filename = TEST_FILES.resolve('launch_completions.py') + cwd = os.path.dirname(filename) + return DebugInfo(filename=filename, cwd=cwd) + + def test_outermost_scope(self): + debug_info = self._get_debug_info() + self.run_test_outermost_scope(debug_info, debug_info.filename, 16) + + def test_in_function(self): + debug_info = self._get_debug_info() + self.run_test_in_function(debug_info, debug_info.filename, 12) + + def test_in_method(self): + debug_info = self._get_debug_info() + self.run_test_in_method(debug_info, debug_info.filename, 7) + + +class LaunchModuleTests(CompletionsTests): + def _get_debug_info(self): + module_name = 'launch_completions' + filename = TEST_FILES.resolve('launch_completions.py') + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + return DebugInfo(modulename=module_name, cwd=cwd, env=env), filename + + def test_outermost_scope(self): + debug_info, filename = self._get_debug_info() + self.run_test_outermost_scope(debug_info, filename, 16) + + def test_in_function(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_function(debug_info, filename, 12) + + def test_in_method(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_method(debug_info, filename, 7) + + +class ServerAttachTests(CompletionsTests): + def _get_debug_info(self): + filename = TEST_FILES.resolve('launch_completions.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + return DebugInfo( + filename=filename, + cwd=cwd, + starttype='attach', + argv=argv, + ), filename + + def test_outermost_scope(self): + debug_info, filename = self._get_debug_info() + self.run_test_outermost_scope(debug_info, filename, 16) + + def test_in_function(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_function(debug_info, filename, 12) + + def test_in_method(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_method(debug_info, filename, 7) + + +class ServerAttachModuleTests(CompletionsTests): + def _get_debug_info(self): + module_name = 'launch_completions' + filename = TEST_FILES.resolve('launch_completions.py') + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + return DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + starttype='attach', + ), filename + + def test_outermost_scope(self): + debug_info, filename = self._get_debug_info() + self.run_test_outermost_scope(debug_info, filename, 16) + + def test_in_function(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_function(debug_info, filename, 12) + + def test_in_method(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_method(debug_info, filename, 7) + + +class PTVSDAttachTests(CompletionsTests): + def _get_debug_info(self): + filename = TEST_FILES.resolve('attach_completions.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + return DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + ), filename + + def test_outermost_scope(self): + debug_info, filename = self._get_debug_info() + self.run_test_outermost_scope(debug_info, filename, 23) + + def test_in_function(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_function(debug_info, filename, 19) + + def test_in_method(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_method(debug_info, filename, 14) + + +class PTVSDAttachModuleTests(CompletionsTests): + def _get_debug_info(self): + filename = TEST_FILES.resolve('attach_completions.py') + module_name = 'attach_completions' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + return DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + attachtype='import', + starttype='attach', + ), filename + + def test_outermost_scope(self): + debug_info, filename = self._get_debug_info() + self.run_test_outermost_scope(debug_info, filename, 23) + + def test_in_function(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_function(debug_info, filename, 19) + + def test_in_method(self): + debug_info, filename = self._get_debug_info() + self.run_test_in_method(debug_info, filename, 14)