diff --git a/.gitmodules b/.gitmodules index c2d83b37a2..147088e4d0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,14 +13,6 @@ [submodule "third_party/requests"] path = third_party/requests_deps/requests url = https://github.com/requests/requests -[submodule "third_party/go/src/github.com/mdempsky/gocode"] - path = third_party/go/src/github.com/mdempsky/gocode - url = https://github.com/mdempsky/gocode - ignore = dirty -[submodule "third_party/go/src/github.com/rogpeppe/godef"] - path = third_party/go/src/github.com/rogpeppe/godef - url = https://github.com/rogpeppe/godef - ignore = dirty [submodule "third_party/jedi"] path = third_party/jedi_deps/jedi url = https://github.com/davidhalter/jedi @@ -48,3 +40,6 @@ [submodule "third_party/idna"] path = third_party/requests_deps/idna url = https://github.com/kjd/idna +[submodule "third_party/go/src/golang.org/x/tools"] + path = third_party/go/src/golang.org/x/tools + url = https://go.googlesource.com/tools diff --git a/build.py b/build.py index 69a06c32aa..2ed6210262 100755 --- a/build.py +++ b/build.py @@ -682,18 +682,11 @@ def EnableGoCompleter( args ): go = FindExecutableOrDie( 'go', 'go is required to build gocode.' ) go_dir = p.join( DIR_OF_THIS_SCRIPT, 'third_party', 'go' ) - os.chdir( p.join( go_dir, 'src', 'github.com', 'mdempsky', 'gocode' ) ) - new_env = os.environ.copy() - new_env[ 'GOPATH' ] = go_dir + os.chdir( p.join( + go_dir, 'src', 'golang.org', 'x', 'tools', 'cmd', 'gopls' ) ) CheckCall( [ go, 'build' ], - env = new_env, - quiet = args.quiet, - status_message = 'Building gocode for go completion' ) - os.chdir( p.join( go_dir, 'src', 'github.com', 'rogpeppe', 'godef' ) ) - CheckCall( [ go, 'build' ], - env = new_env, quiet = args.quiet, - status_message = 'Building godef for go definition' ) + status_message = 'Building gopls for go completion' ) def WriteToolchainVersion( version ): diff --git a/run_tests.py b/run_tests.py index 26ed061bc2..8b5ad81be7 100755 --- a/run_tests.py +++ b/run_tests.py @@ -72,7 +72,8 @@ def RunFlake8(): # - no aliases. SIMPLE_COMPLETERS = [ 'clangd', - 'rust' + 'rust', + 'go', ] # More complex or legacy cases can specify all of: @@ -95,11 +96,6 @@ def RunFlake8(): 'test': [ '--exclude-dir=ycmd/tests/tern' ], 'aliases': [ 'js', 'tern' ] }, - 'go': { - 'build': [ '--go-completer' ], - 'test': [ '--exclude-dir=ycmd/tests/go' ], - 'aliases': [ 'gocode' ] - }, 'typescript': { 'build': [ '--ts-completer' ], 'test': [ '--exclude-dir=ycmd/tests/javascript', diff --git a/third_party/go/src/github.com/mdempsky/gocode b/third_party/go/src/github.com/mdempsky/gocode deleted file mode 160000 index 00e7f5ac29..0000000000 --- a/third_party/go/src/github.com/mdempsky/gocode +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 00e7f5ac290aeb20a3d8d31e737ae560a191a1d5 diff --git a/third_party/go/src/github.com/rogpeppe/godef b/third_party/go/src/github.com/rogpeppe/godef deleted file mode 160000 index b692db1de5..0000000000 --- a/third_party/go/src/github.com/rogpeppe/godef +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b692db1de5229d4248e23c41736b431eb665615d diff --git a/third_party/go/src/golang.org/x/tools b/third_party/go/src/golang.org/x/tools new file mode 160000 index 0000000000..cf84161cff --- /dev/null +++ b/third_party/go/src/golang.org/x/tools @@ -0,0 +1 @@ +Subproject commit cf84161cff3fdeddfd5ab5e686c1e2c17cb5db04 diff --git a/ycmd/completers/go/go_completer.py b/ycmd/completers/go/go_completer.py index 80964a1e8f..d540d80dfe 100644 --- a/ycmd/completers/go/go_completer.py +++ b/ycmd/completers/go/go_completer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2018 ycmd contributors +# Copyright (C) 2019 ycmd contributors # # This file is part of ycmd. # @@ -22,344 +22,96 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -import json import logging import os -import subprocess -import threading from ycmd import responses from ycmd import utils -from ycmd.utils import LOGGER, ToBytes, ToUnicode, ExecutableName -from ycmd.completers.completer import Completer +from ycmd.completers.language_server import ( simple_language_server_completer, + language_server_completer ) -SHELL_ERROR_MESSAGE = ( 'Command {command} failed with code {code} and error ' - '"{error}".' ) -COMPUTE_OFFSET_ERROR_MESSAGE = ( 'Go completer could not compute byte offset ' - 'corresponding to line {line} and column ' - '{column}.' ) -GOCODE_PARSE_ERROR_MESSAGE = 'Gocode returned invalid JSON response.' -GOCODE_NO_COMPLETIONS_MESSAGE = 'No completions found.' -GOCODE_PANIC_MESSAGE = ( 'Gocode panicked trying to find completions, ' - 'you likely have a syntax error.' ) - -GO_DIR = os.path.abspath( - os.path.join( os.path.dirname( __file__ ), '..', '..', '..', 'third_party', - 'go', 'src', 'github.com' ) ) -GO_BINARIES = dict( { - 'gocode': os.path.join( GO_DIR, 'mdempsky', 'gocode', - ExecutableName( 'gocode' ) ), - 'godef': os.path.join( GO_DIR, 'rogpeppe', 'godef', - ExecutableName( 'godef' ) ) -} ) - -LOGFILE_FORMAT = 'gocode_{port}_{std}_' - - -def FindBinary( binary, user_options ): - """Find the path to the Gocode/Godef binary. - - If 'gocode_binary_path' or 'godef_binary_path' - in the options is blank, use the version installed - with YCM, if it exists. - - If the 'gocode_binary_path' or 'godef_binary_path' is - specified, use it as an absolute path. - - If the resolved binary exists, return the path, - otherwise return None.""" - - def _FindPath(): - key = '{0}_binary_path'.format( binary ) - if user_options.get( key ): - return user_options[ key ] - return GO_BINARIES.get( binary ) - - binary_path = _FindPath() - if os.path.isfile( binary_path ): - return binary_path - return None +PATH_TO_GOPLS = os.path.abspath( os.path.join( os.path.dirname( __file__ ), + '..', + '..', + '..', + 'third_party', + 'go', + 'src', + 'golang.org', + 'x', + 'tools', + 'cmd', + 'gopls', + utils.ExecutableName( 'gopls' ) ) ) def ShouldEnableGoCompleter( user_options ): - def _HasBinary( binary ): - binary_path = FindBinary( binary, user_options ) - if not binary_path: - LOGGER.error( '%s binary not found', binary_path ) - return binary_path - - return all( _HasBinary( binary ) for binary in [ 'gocode', 'godef' ] ) + server_exists = os.path.isfile( PATH_TO_GOPLS ) + if server_exists: + return True + utils.LOGGER.info( 'No gopls executable at %s.', PATH_TO_GOPLS ) + return False -class GoCompleter( Completer ): - """Completer for Go using the Gocode daemon for completions: - https://github.com/nsf/gocode - and the Godef binary for jumping to definitions: - https://github.com/Manishearth/godef""" - +class GoCompleter( simple_language_server_completer.SimpleLSPCompleter ): def __init__( self, user_options ): super( GoCompleter, self ).__init__( user_options ) - self._gocode_binary_path = FindBinary( 'gocode', user_options ) - self._gocode_lock = threading.RLock() - self._gocode_handle = None - self._gocode_port = None - self._gocode_host = None - self._gocode_stderr = None - self._gocode_stdout = None - self._godef_binary_path = FindBinary( 'godef', user_options ) - self._keep_logfiles = user_options[ 'server_keep_logfiles' ] + def GetServerName( self ): + return 'gopls' - self._StartServer() - - def SupportedFiletypes( self ): - return [ 'go' ] + def _GetProjectDirectory( self, request_data, extra_conf_dir ): + # Without LSP workspaces support, GOPLS relies on the rootUri to detect a + # project. + # TODO: add support for LSP workspaces to allow users to change project + # without having to restart GOPLS. + for folder in utils.PathsToAllParentFolders( request_data[ 'filepath' ] ): + if os.path.isfile( os.path.join( folder, 'go.mod' ) ): + return folder + return super( GoCompleter, self )._GetProjectDirectory( request_data, + extra_conf_dir ) - def ComputeCandidatesInner( self, request_data ): - filename = request_data[ 'filepath' ] - LOGGER.info( 'Gocode completion request %s', filename ) + def GetCommandLine( self ): + cmdline = [ PATH_TO_GOPLS, '-logfile', self._stderr_file ] + if utils.LOGGER.isEnabledFor( logging.DEBUG ): + cmdline.append( '-rpc.trace' ) + return cmdline - contents = utils.ToBytes( - request_data[ 'file_data' ][ filename ][ 'contents' ] ) - # NOTE: Offsets sent to gocode are byte offsets, so using start_column - # with contents (a bytes instance) above is correct. - offset = _ComputeOffset( contents, - request_data[ 'line_num' ], - request_data[ 'start_column' ] ) + def SupportedFiletypes( self ): + return [ 'go' ] - stdoutdata = self._ExecuteCommand( [ self._gocode_binary_path, - '-sock', 'tcp', - '-addr', self._gocode_host, - '-f=json', 'autocomplete', - filename, str( offset ) ], - contents = contents ) + def GetType( self, request_data ): try: - resultdata = json.loads( ToUnicode( stdoutdata ) ) - except ValueError: - LOGGER.error( GOCODE_PARSE_ERROR_MESSAGE ) - raise RuntimeError( GOCODE_PARSE_ERROR_MESSAGE ) - - if not isinstance( resultdata, list ) or len( resultdata ) != 2: - LOGGER.error( GOCODE_NO_COMPLETIONS_MESSAGE ) - raise RuntimeError( GOCODE_NO_COMPLETIONS_MESSAGE ) - for result in resultdata[ 1 ]: - if result.get( 'class' ) == 'PANIC': - raise RuntimeError( GOCODE_PANIC_MESSAGE ) + result = self.GetHoverResponse( request_data )[ 'value' ] + return responses.BuildDisplayMessageResponse( result ) + except language_server_completer.ResponseFailedException: + raise RuntimeError( 'Unknown type.' ) - return [ responses.BuildCompletionData( - insertion_text = x[ 'name' ], - extra_data = x ) for x in resultdata[ 1 ] ] - - def GetSubcommandsMap( self ): + def GetCustomSubcommands( self ): return { - 'StopServer' : ( lambda self, request_data, args: - self._StopServer() ), - 'RestartServer' : ( lambda self, request_data, args: - self._RestartServer() ), - 'GoTo' : ( lambda self, request_data, args: - self._GoToDefinition( request_data ) ), - 'GoToDefinition' : ( lambda self, request_data, args: - self._GoToDefinition( request_data ) ), - 'GoToDeclaration': ( lambda self, request_data, args: - self._GoToDefinition( request_data ) ), + 'RestartServer': ( + lambda self, request_data, args: self._RestartServer( request_data ) + ), + 'FixIt': ( + lambda self, request_data, args: self.GetCodeActions( request_data, + args ) + ), + 'GetType': ( + # In addition to type information we show declaration. + lambda self, request_data, args: self.GetType( request_data ) + ), } - def _ExecuteCommand( self, command, contents = None ): - """Run a command in a subprocess and communicate with it using the contents - argument. Return the standard output. - - It is used to send a command to the Gocode daemon or execute the Godef - binary.""" - phandle = utils.SafePopen( command, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = subprocess.PIPE ) - - stdoutdata, stderrdata = phandle.communicate( contents ) - - if phandle.returncode: - message = SHELL_ERROR_MESSAGE.format( - command = ' '.join( command ), - code = phandle.returncode, - error = ToUnicode( stderrdata.strip() ) ) - LOGGER.error( message ) - raise RuntimeError( message ) - - return stdoutdata - - - def _StartServer( self ): - """Start the Gocode server.""" - with self._gocode_lock: - LOGGER.info( 'Starting Gocode server' ) - - self._gocode_port = utils.GetUnusedLocalhostPort() - self._gocode_host = '127.0.0.1:{0}'.format( self._gocode_port ) - - command = [ self._gocode_binary_path, - '-s', - '-sock', 'tcp', - '-addr', self._gocode_host ] - - if LOGGER.isEnabledFor( logging.DEBUG ): - command.append( '-debug' ) - - self._gocode_stdout = utils.CreateLogfile( - LOGFILE_FORMAT.format( port = self._gocode_port, std = 'stdout' ) ) - self._gocode_stderr = utils.CreateLogfile( - LOGFILE_FORMAT.format( port = self._gocode_port, std = 'stderr' ) ) - - with utils.OpenForStdHandle( self._gocode_stdout ) as stdout: - with utils.OpenForStdHandle( self._gocode_stderr ) as stderr: - self._gocode_handle = utils.SafePopen( command, - stdout = stdout, - stderr = stderr ) - - - def _StopServer( self ): - """Stop the Gocode server.""" - with self._gocode_lock: - if self._ServerIsRunning(): - LOGGER.info( 'Stopping Gocode server with PID %s', - self._gocode_handle.pid ) - try: - self._ExecuteCommand( [ self._gocode_binary_path, - '-sock', 'tcp', - '-addr', self._gocode_host, - 'close' ] ) - utils.WaitUntilProcessIsTerminated( self._gocode_handle, timeout = 5 ) - LOGGER.info( 'Gocode server stopped' ) - except Exception: - LOGGER.exception( 'Error while stopping Gocode server' ) - - self._CleanUp() - - - def _CleanUp( self ): - self._gocode_handle = None - self._gocode_port = None - self._gocode_host = None - if not self._keep_logfiles: - if self._gocode_stdout: - utils.RemoveIfExists( self._gocode_stdout ) - self._gocode_stdout = None - if self._gocode_stderr: - utils.RemoveIfExists( self._gocode_stderr ) - self._gocode_stderr = None - - - def _RestartServer( self ): - """Restart the Gocode server.""" - with self._gocode_lock: - self._StopServer() - self._StartServer() - - - def _GoToDefinition( self, request_data ): - filename = request_data[ 'filepath' ] - LOGGER.info( 'Godef GoTo request %s', filename ) - - contents = utils.ToBytes( - request_data[ 'file_data' ][ filename ][ 'contents' ] ) - offset = _ComputeOffset( contents, - request_data[ 'line_num' ], - request_data[ 'column_num' ] ) - try: - stdout = self._ExecuteCommand( [ self._godef_binary_path, - '-i', - '-f={}'.format( filename ), - '-json', - '-o={}'.format( offset ) ], - contents = contents ) - # We catch this exception type and not a more specific one because we - # raise it in _ExecuteCommand when the command fails. - except RuntimeError: - LOGGER.exception( 'Failed to jump to definition' ) - raise RuntimeError( 'Can\'t find a definition.' ) - - return self._ConstructGoToFromResponse( stdout ) - - - def _ConstructGoToFromResponse( self, response_str ): - parsed = json.loads( ToUnicode( response_str ) ) - if 'filename' in parsed and 'column' in parsed: - return responses.BuildGoToResponse( parsed[ 'filename' ], - int( parsed[ 'line' ] ), - int( parsed[ 'column' ] ) ) - raise RuntimeError( 'Can\'t jump to definition.' ) - - - def Shutdown( self ): - self._StopServer() - - - def _ServerIsRunning( self ): - """Check if the Gocode server is running (process is up).""" - return utils.ProcessIsRunning( self._gocode_handle ) - - - def ServerIsHealthy( self ): - """Assume the Gocode server is healthy if it's running.""" - return self._ServerIsRunning() - - - def DebugInfo( self, request_data ): - with self._gocode_lock: - gocode_server = responses.DebugInfoServer( - name = 'Gocode', - handle = self._gocode_handle, - executable = self._gocode_binary_path, - address = '127.0.0.1', - port = self._gocode_port, - logfiles = [ self._gocode_stdout, self._gocode_stderr ] ) - - godef_item = responses.DebugInfoItem( key = 'Godef executable', - value = self._godef_binary_path ) - - return responses.BuildDebugInfoResponse( name = 'Go', - servers = [ gocode_server ], - items = [ godef_item ] ) - - - def DetailCandidates( self, request_data, candidates ): - for candidate in candidates: - if 'kind' in candidate: - # This candidate is already detailed - continue - completion = candidate[ 'extra_data' ] - candidate[ 'menu_text' ] = completion[ 'name' ] - candidate[ 'extra_menu_info' ] = completion[ 'type' ] - candidate[ 'kind' ] = completion[ 'class' ] - candidate[ 'detailed_info' ] = ' '.join( [ - completion[ 'name' ], - completion[ 'type' ], - completion[ 'class' ] ] ) - candidate.pop( 'extra_data' ) - return candidates - - -def _ComputeOffset( contents, line, column ): - """Compute the byte offset in the file given the line and column.""" - contents = ToBytes( contents ) - current_line = 1 - current_column = 1 - newline = bytes( b'\n' )[ 0 ] - for i, byte in enumerate( contents ): - if current_line == line and current_column == column: - return i - current_column += 1 - if byte == newline: - current_line += 1 - current_column = 1 - message = COMPUTE_OFFSET_ERROR_MESSAGE.format( line = line, - column = column ) - LOGGER.error( message ) - raise RuntimeError( message ) + def HandleServerCommand( self, request_data, command ): + return language_server_completer.WorkspaceEditToFixIt( + request_data, + command[ 'edit' ], + text = command[ 'title' ] ) diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index eaf630bfaf..a284099ddf 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -1646,10 +1646,13 @@ def GetHoverResponse( self, request_data ): def _GoToRequest( self, request_data, handler ): request_id = self.GetConnection().NextRequestId() - result = self.GetConnection().GetResponse( - request_id, - getattr( lsp, handler )( request_id, request_data ), - REQUEST_TIMEOUT_COMMAND )[ 'result' ] + try: + result = self.GetConnection().GetResponse( + request_id, + getattr( lsp, handler )( request_id, request_data ), + REQUEST_TIMEOUT_COMMAND )[ 'result' ] + except ResponseFailedException: + raise RuntimeError( 'Cannot jump to location' ) if not result: raise RuntimeError( 'Cannot jump to location' ) if not isinstance( result, list ): @@ -1740,8 +1743,11 @@ def WithinRange( diag ): [] ), REQUEST_TIMEOUT_COMMAND ) - response = [ self.HandleServerCommand( request_data, c ) - for c in code_actions[ 'result' ] ] + result = code_actions[ 'result' ] + if result is None: + result = [] + + response = [ self.HandleServerCommand( request_data, c ) for c in result ] # Show a list of actions to the user to select which one to apply. # This is (probably) a more common workflow for "code action". diff --git a/ycmd/tests/go/__init__.py b/ycmd/tests/go/__init__.py index bbcbfce5c6..e02b1d156d 100644 --- a/ycmd/tests/go/__init__.py +++ b/ycmd/tests/go/__init__.py @@ -22,11 +22,17 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa +from pprint import pformat import functools import os - -from ycmd.tests.test_utils import ( ClearCompletionsCache, IsolatedApp, - SetUpApp, StopCompleterServer, +import time + +from ycmd.tests.test_utils import ( BuildRequest, + ClearCompletionsCache, + IgnoreExtraConfOutsideTestsFolder, + IsolatedApp, + SetUpApp, + StopCompleterServer, WaitUntilCompleterServerReady ) shared_app = None @@ -34,7 +40,8 @@ def PathToTestFile( *args ): dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) - return os.path.join( dir_of_current_script, 'testdata', *args ) + # GOPLS doesn't work if any parent directory is named "testdata" + return os.path.join( dir_of_current_script, 'go_module', *args ) def setUpPackage(): @@ -45,7 +52,17 @@ def setUpPackage(): global shared_app shared_app = SetUpApp() - WaitUntilCompleterServerReady( shared_app, 'go' ) + with IgnoreExtraConfOutsideTestsFolder(): + StartGoCompleterServerInDirectory( shared_app, PathToTestFile() ) + + +def StartGoCompleterServerInDirectory( app, directory ): + app.post_json( '/event_notification', + BuildRequest( + filepath = os.path.join( directory, 'goto.go' ), + event_name = 'FileReadyToParse', + filetype = 'go' ) ) + WaitUntilCompleterServerReady( app, 'go' ) def tearDownPackage(): @@ -84,3 +101,59 @@ def Wrapper( *args, **kwargs ): finally: StopCompleterServer( app, 'go' ) return Wrapper + + +class PollForMessagesTimeoutException( Exception ): + pass + + +def PollForMessages( app, request_data, timeout = 30 ): + expiration = time.time() + timeout + while True: + if time.time() > expiration: + raise PollForMessagesTimeoutException( + 'Waited for diagnostics to be ready for {0} seconds, aborting.'.format( + timeout ) ) + + default_args = { + 'filetype' : 'java', + 'line_num' : 1, + 'column_num': 1, + } + args = dict( default_args ) + args.update( request_data ) + + response = app.post_json( '/receive_messages', BuildRequest( **args ) ).json + + print( 'poll response: {0}'.format( pformat( response ) ) ) + + if isinstance( response, bool ): + if not response: + raise RuntimeError( 'The message poll was aborted by the server' ) + elif isinstance( response, list ): + for message in response: + yield message + else: + raise AssertionError( 'Message poll response was wrong type: {0}'.format( + type( response ).__name__ ) ) + + time.sleep( 0.25 ) + + +def WaitForDiagnosticsToBeReady( app, filepath, contents, **kwargs ): + results = None + for tries in range( 0, 60 ): + event_data = BuildRequest( event_name = 'FileReadyToParse', + contents = contents, + filepath = filepath, + filetype = 'go', + **kwargs ) + + results = app.post_json( '/event_notification', event_data ).json + + if results: + break + + time.sleep( 0.5 ) + + return results diff --git a/ycmd/tests/go/debug_info_test.py b/ycmd/tests/go/debug_info_test.py index d627ffcb3f..8f5d54c09c 100644 --- a/ycmd/tests/go/debug_info_test.py +++ b/ycmd/tests/go/debug_info_test.py @@ -22,7 +22,11 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -from hamcrest import assert_that, contains, has_entries, has_entry, instance_of +from hamcrest import ( assert_that, + contains, + has_entries, + has_entry, + instance_of ) from ycmd.tests.go import SharedYcmd from ycmd.tests.test_utils import BuildRequest @@ -36,18 +40,30 @@ def DebugInfo_test( app ): has_entry( 'completer', has_entries( { 'name': 'Go', 'servers': contains( has_entries( { - 'name': 'Gocode', + 'name': 'gopls', 'is_running': instance_of( bool ), - 'executable': instance_of( str ), + 'executable': contains( instance_of( str ), + instance_of( str ), + instance_of( str ), + instance_of( str ) ), + 'address': None, + 'port': None, 'pid': instance_of( int ), - 'address': instance_of( str ), - 'port': instance_of( int ), - 'logfiles': contains( instance_of( str ), - instance_of( str ) ) + 'logfiles': contains( instance_of( str ) ), + 'extras': contains( + has_entries( { + 'key': 'Server State', + 'value': instance_of( str ), + } ), + has_entries( { + 'key': 'Project Directory', + 'value': instance_of( str ), + } ), + has_entries( { + 'key': 'Settings', + 'value': '{}' + } ), + ) } ) ), - 'items': contains( has_entries( { - 'key': 'Godef executable', - 'value': instance_of( str ) - } ) ) } ) ) ) diff --git a/ycmd/tests/go/get_completions_test.py b/ycmd/tests/go/get_completions_test.py index c70eda8e37..426049ec31 100644 --- a/ycmd/tests/go/get_completions_test.py +++ b/ycmd/tests/go/get_completions_test.py @@ -24,7 +24,7 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -from hamcrest import all_of, assert_that, has_item, has_items +from hamcrest import all_of, assert_that, has_items from ycmd.tests.go import PathToTestFile, SharedYcmd from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher @@ -33,7 +33,7 @@ @SharedYcmd def GetCompletions_Basic_test( app ): - filepath = PathToTestFile( 'test.go' ) + filepath = PathToTestFile( 'td', 'test.go' ) completion_data = BuildRequest( filepath = filepath, filetype = 'go', contents = ReadFile( filepath ), @@ -46,50 +46,21 @@ def GetCompletions_Basic_test( app ): assert_that( results, all_of( has_items( - CompletionEntryMatcher( 'Llongfile', 'untyped int' ), - CompletionEntryMatcher( 'Logger', 'struct' ) ) ) ) - completion_data = BuildRequest( filepath = filepath, - filetype = 'go', - contents = ReadFile( filepath ), - force_semantic = True, - line_num = 9, - column_num = 11 ) - - results = app.post_json( '/completions', - completion_data ).json[ 'completions' ] - assert_that( results, - all_of( - has_item( - CompletionEntryMatcher( 'Logger', 'struct' ) ) ) ) - - -@SharedYcmd -def GetCompletions_Unicode_InLine_test( app ): - filepath = PathToTestFile( 'unicode.go' ) - completion_data = BuildRequest( filepath = filepath, - filetype = 'go', - contents = ReadFile( filepath ), - line_num = 7, - column_num = 37 ) - - results = app.post_json( '/completions', - completion_data ).json[ 'completions' ] - assert_that( results, - has_items( CompletionEntryMatcher( u'Printf' ), - CompletionEntryMatcher( u'Fprintf' ), - CompletionEntryMatcher( u'Sprintf' ) ) ) - - -@SharedYcmd -def GetCompletions_Unicode_Identifier_test( app ): - filepath = PathToTestFile( 'unicode.go' ) - completion_data = BuildRequest( filepath = filepath, - filetype = 'go', - contents = ReadFile( filepath ), - line_num = 13, - column_num = 13 ) - - results = app.post_json( '/completions', - completion_data ).json[ 'completions' ] - assert_that( results, - has_items( CompletionEntryMatcher( u'Unicøde' ) ) ) + CompletionEntryMatcher( + 'Llongfile', + 'int', + { + 'detailed_info': 'Llongfile = 8\n\n', + 'menu_text': 'Llongfile = 8', + 'kind': 'Constant', + } + ), + CompletionEntryMatcher( + 'Logger', + 'struct{...}', + { + 'detailed_info': 'Logger\n\n', + 'menu_text': 'Logger', + 'kind': 'Struct', + } + ) ) ) ) diff --git a/ycmd/tests/go/go_completer_test.py b/ycmd/tests/go/go_completer_test.py index b713ccf963..273c137eb5 100644 --- a/ycmd/tests/go/go_completer_test.py +++ b/ycmd/tests/go/go_completer_test.py @@ -1,7 +1,4 @@ -# coding: utf-8 -# -# Copyright (C) 2015 Google Inc. -# 2017 ycmd contributors +# Copyright (C) 2019 ycmd contributors # # This file is part of ycmd. # @@ -25,174 +22,17 @@ # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -from hamcrest import assert_that, calling, raises from mock import patch -from nose.tools import eq_ -import functools -import os +from nose.tools import ok_ -from ycmd.completers.go.go_completer import ( _ComputeOffset, GoCompleter, - GO_BINARIES, FindBinary ) -from ycmd.request_wrap import RequestWrap from ycmd import user_options_store -from ycmd.utils import ReadFile, ToBytes - -TEST_DIR = os.path.dirname( os.path.abspath( __file__ ) ) -DATA_DIR = os.path.join( TEST_DIR, 'testdata' ) -PATH_TO_TEST_FILE = os.path.join( DATA_DIR, 'test2.go' ) -# Use test file as dummy binary -DUMMY_BINARY = PATH_TO_TEST_FILE -PATH_TO_POS121_RES = os.path.join( DATA_DIR, 'gocode_output_offset_121.json' ) -PATH_TO_POS215_RES = os.path.join( DATA_DIR, 'gocode_output_offset_215.json' ) -PATH_TO_POS292_RES = os.path.join( DATA_DIR, 'gocode_output_offset_292.json' ) -# Gocode output when a parsing error causes an internal panic. -PATH_TO_PANIC_OUTPUT_RES = os.path.join( - DATA_DIR, 'gocode_dontpanic_output_offset_10.json' ) - -REQUEST_DATA = { - 'line_num': 1, - 'filepath' : PATH_TO_TEST_FILE, - 'file_data' : { PATH_TO_TEST_FILE : { 'filetypes' : [ 'go' ] } } -} - - -def BuildRequest( line_num, column_num ): - request = REQUEST_DATA.copy() - request[ 'line_num' ] = line_num - request[ 'column_num' ] = column_num - request[ 'file_data' ][ PATH_TO_TEST_FILE ][ 'contents' ] = ReadFile( - PATH_TO_TEST_FILE ) - return RequestWrap( request ) - - -def SetUpGoCompleter( test ): - @functools.wraps( test ) - def Wrapper( *args, **kwargs ): - user_options = user_options_store.DefaultOptions() - user_options[ 'gocode_binary_path' ] = DUMMY_BINARY - with patch( 'ycmd.utils.SafePopen' ): - completer = GoCompleter( user_options ) - return test( completer, *args, **kwargs ) - return Wrapper - - -def FindGoCodeBinary_test(): - user_options = user_options_store.DefaultOptions() - - eq_( GO_BINARIES.get( "gocode" ), FindBinary( "gocode", user_options ) ) - - user_options[ 'gocode_binary_path' ] = DUMMY_BINARY - eq_( DUMMY_BINARY, FindBinary( "gocode", user_options ) ) - - user_options[ 'gocode_binary_path' ] = DATA_DIR - eq_( None, FindBinary( "gocode", user_options ) ) - - -def ComputeOffset_OutOfBoundsOffset_test(): - assert_that( - calling( _ComputeOffset ).with_args( 'test', 2, 1 ), - raises( RuntimeError, 'Go completer could not compute byte offset ' - 'corresponding to line 2 and column 1.' ) ) - - -# Test line-col to offset in the file before any unicode occurrences. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = ReadFile( PATH_TO_POS215_RES ) ) -def ComputeCandidatesInner_BeforeUnicode_test( completer, execute_command ): - # Col 8 corresponds to cursor at log.Pr^int("Line 7 ... - completer.ComputeCandidatesInner( BuildRequest( 7, 8 ) ) - execute_command.assert_called_once_with( - [ DUMMY_BINARY, '-sock', 'tcp', '-addr', completer._gocode_host, - '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '119' ], - contents = ToBytes( ReadFile( PATH_TO_TEST_FILE ) ) ) - - -# Test line-col to offset in the file after a unicode occurrences. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = ReadFile( PATH_TO_POS215_RES ) ) -def ComputeCandidatesInner_AfterUnicode_test( completer, execute_command ): - # Col 9 corresponds to cursor at log.Pri^nt("Line 7 ... - completer.ComputeCandidatesInner( BuildRequest( 9, 9 ) ) - execute_command.assert_called_once_with( - [ DUMMY_BINARY, '-sock', 'tcp', '-addr', completer._gocode_host, - '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '212' ], - contents = ToBytes( ReadFile( PATH_TO_TEST_FILE ) ) ) - - -# Test end to end parsing of completed results. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = ReadFile( PATH_TO_POS292_RES ) ) -def ComputeCandidatesInner_test( completer, execute_command ): - # Col 40 corresponds to cursor at ..., log.Prefi^x ... - candidates = completer.ComputeCandidatesInner( BuildRequest( 10, 40 ) ) - result = completer.DetailCandidates( {}, candidates ) - execute_command.assert_called_once_with( - [ DUMMY_BINARY, '-sock', 'tcp', '-addr', completer._gocode_host, - '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '287' ], - contents = ToBytes( ReadFile( PATH_TO_TEST_FILE ) ) ) - eq_( result, [ { - 'menu_text': u'Prefix', - 'insertion_text': u'Prefix', - 'extra_menu_info': u'func() string', - 'detailed_info': u'Prefix func() string func', - 'kind': u'func' - } ] ) - - -# Test Gocode failure. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = '' ) -def ComputeCandidatesInner_GoCodeFailure_test( completer, *args ): - assert_that( - calling( completer.ComputeCandidatesInner ).with_args( - BuildRequest( 1, 1 ) ), - raises( RuntimeError, 'Gocode returned invalid JSON response.' ) ) - - -# Test JSON parsing failure. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = "{this isn't parseable" ) -def ComputeCandidatesInner_ParseFailure_test( completer, *args ): - assert_that( - calling( completer.ComputeCandidatesInner ).with_args( - BuildRequest( 1, 1 ) ), - raises( RuntimeError, 'Gocode returned invalid JSON response.' ) ) - - -# Test empty results error. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = '[]' ) -def ComputeCandidatesInner_NoResultsFailure_EmptyList_test( completer, *args ): - assert_that( - calling( completer.ComputeCandidatesInner ).with_args( - BuildRequest( 1, 1 ) ), - raises( RuntimeError, 'No completions found.' ) ) +from ycmd.completers.go.hook import GetCompleter -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = 'null\n' ) -def ComputeCandidatesInner_NoResultsFailure_Null_test( completer, *args ): - assert_that( - calling( completer.ComputeCandidatesInner ).with_args( - BuildRequest( 1, 1 ) ), - raises( RuntimeError, 'No completions found.' ) ) +def GetCompleter_GoplsFound_test(): + ok_( GetCompleter( user_options_store.GetAll() ) ) -# Test panic error. -@SetUpGoCompleter -@patch( 'ycmd.completers.go.go_completer.GoCompleter._ExecuteCommand', - return_value = ReadFile( PATH_TO_PANIC_OUTPUT_RES ) ) -def ComputeCandidatesInner_GoCodePanic_test( completer, *args ): - assert_that( - calling( completer.ComputeCandidatesInner ).with_args( - BuildRequest( 1, 1 ) ), - raises( RuntimeError, - 'Gocode panicked trying to find completions, ' - 'you likely have a syntax error.' ) ) +@patch( 'ycmd.completers.go.go_completer.PATH_TO_GOPLS', 'path_does_not_exist' ) +def GetCompleter_GoplsNotFound_test( *args ): + ok_( not GetCompleter( user_options_store.GetAll() ) ) diff --git a/ycmd/tests/go/go_module/go.mod b/ycmd/tests/go/go_module/go.mod new file mode 100644 index 0000000000..341e9cbf05 --- /dev/null +++ b/ycmd/tests/go/go_module/go.mod @@ -0,0 +1,3 @@ +module example.com/owner/module + +go 1.12 diff --git a/ycmd/tests/go/testdata/goto.go b/ycmd/tests/go/go_module/goto.go similarity index 62% rename from ycmd/tests/go/testdata/goto.go rename to ycmd/tests/go/go_module/goto.go index 8aae0ed9a6..ef3c2109a2 100644 --- a/ycmd/tests/go/testdata/goto.go +++ b/ycmd/tests/go/go_module/goto.go @@ -6,4 +6,8 @@ func dummy() { func main() { dummy() //GoTo -} \ No newline at end of file +} + +func foo() { + diagnostics_test +} diff --git a/ycmd/tests/go/go_module/td/test.go b/ycmd/tests/go/go_module/td/test.go new file mode 100644 index 0000000000..eaea06e9d9 --- /dev/null +++ b/ycmd/tests/go/go_module/td/test.go @@ -0,0 +1,10 @@ +// Package td is dummy data for gocode completion test. +package td + +import ( + "log" +) + +func Hello() { + log.Log +} diff --git a/ycmd/tests/go/testdata/unicode.go b/ycmd/tests/go/go_module/unicode/unicode.go similarity index 100% rename from ycmd/tests/go/testdata/unicode.go rename to ycmd/tests/go/go_module/unicode/unicode.go diff --git a/ycmd/tests/go/server_management_test.py b/ycmd/tests/go/server_management_test.py new file mode 100644 index 0000000000..0d911d9c4d --- /dev/null +++ b/ycmd/tests/go/server_management_test.py @@ -0,0 +1,116 @@ +# Copyright (C) 2019 ycmd contributors +# +# This file is part of ycmd. +# +# ycmd is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ycmd is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ycmd. If not, see . + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +# Not installing aliases from python-future; it's unreliable and slow. +from builtins import * # noqa + +from hamcrest import assert_that, contains, has_entry +from mock import patch + +from ycmd.tests.go import ( PathToTestFile, + IsolatedYcmd, + StartGoCompleterServerInDirectory ) +from ycmd.tests.test_utils import ( BuildRequest, + MockProcessTerminationTimingOut, + WaitUntilCompleterServerReady ) + + +def AssertGoCompleterServerIsRunning( app, is_running ): + request_data = BuildRequest( filetype = 'go' ) + assert_that( app.post_json( '/debug_info', request_data ).json, + has_entry( + 'completer', + has_entry( 'servers', contains( + has_entry( 'is_running', is_running ) + ) ) + ) ) + + +@IsolatedYcmd +def ServerManagement_RestartServer_test( app ): + filepath = PathToTestFile( 'goto.go' ) + StartGoCompleterServerInDirectory( app, filepath ) + + AssertGoCompleterServerIsRunning( app, True ) + + app.post_json( + '/run_completer_command', + BuildRequest( + filepath = filepath, + filetype = 'go', + command_arguments = [ 'RestartServer' ], + ), + ) + + WaitUntilCompleterServerReady( app, 'go' ) + + AssertGoCompleterServerIsRunning( app, True ) + + +@IsolatedYcmd +@patch( 'shutil.rmtree', side_effect = OSError ) +@patch( 'ycmd.utils.WaitUntilProcessIsTerminated', + MockProcessTerminationTimingOut ) +def ServerManagement_CloseServer_Unclean_test( app, *args ): + StartGoCompleterServerInDirectory( app, PathToTestFile() ) + + app.post_json( + '/run_completer_command', + BuildRequest( + filetype = 'go', + command_arguments = [ 'StopServer' ] + ) + ) + + request_data = BuildRequest( filetype = 'go' ) + assert_that( app.post_json( '/debug_info', request_data ).json, + has_entry( + 'completer', + has_entry( 'servers', contains( + has_entry( 'is_running', False ) + ) ) + ) ) + + +@IsolatedYcmd +def ServerManagement_StopServerTwice_test( app ): + StartGoCompleterServerInDirectory( app, PathToTestFile() ) + + app.post_json( + '/run_completer_command', + BuildRequest( + filetype = 'go', + command_arguments = [ 'StopServer' ], + ), + ) + + AssertGoCompleterServerIsRunning( app, False ) + + # Stopping a stopped server is a no-op + app.post_json( + '/run_completer_command', + BuildRequest( + filetype = 'go', + command_arguments = [ 'StopServer' ], + ), + ) + + AssertGoCompleterServerIsRunning( app, False ) diff --git a/ycmd/tests/go/subcommands_test.py b/ycmd/tests/go/subcommands_test.py index ec6438d96e..edf3e60da7 100644 --- a/ycmd/tests/go/subcommands_test.py +++ b/ycmd/tests/go/subcommands_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2018 ycmd contributors +# Copyright (C) 2015-2019 ycmd contributors # # This file is part of ycmd. # @@ -15,44 +15,67 @@ # You should have received a copy of the GNU General Public License # along with ycmd. If not, see . +from __future__ import absolute_import from __future__ import unicode_literals from __future__ import print_function from __future__ import division -from __future__ import absolute_import # Not installing aliases from python-future; it's unreliable and slow. from builtins import * # noqa -from hamcrest import assert_that, contains, has_entries, has_entry +from hamcrest import ( assert_that, + contains, + contains_inanyorder, + empty, + equal_to, + has_entries, + has_entry, + matches_regexp ) from mock import patch -from nose.tools import eq_ from pprint import pformat +import os import requests -from ycmd.tests.go import IsolatedYcmd, PathToTestFile, SharedYcmd +from ycmd import handlers +from ycmd.completers.language_server.language_server_completer import ( + ResponseFailedException +) +from ycmd.tests.go import ( PathToTestFile, + SharedYcmd, + StartGoCompleterServerInDirectory ) from ycmd.tests.test_utils import ( BuildRequest, - CombineRequest, + ChunkMatcher, ErrorMatcher, - MockProcessTerminationTimingOut, - WaitUntilCompleterServerReady ) + ExpectedFailure, + LocationMatcher ) from ycmd.utils import ReadFile -@SharedYcmd -def Subcommands_DefinedSubcommands_test( app ): - subcommands_data = BuildRequest( completer_target = 'go' ) - eq_( sorted( [ 'RestartServer', - 'GoTo', - 'GoToDefinition', - 'GoToDeclaration' ] ), - app.post_json( '/defined_subcommands', - subcommands_data ).json ) +RESPONSE_TIMEOUT = 5 + +def RunTest( app, test, contents = None ): + if not contents: + contents = ReadFile( test[ 'request' ][ 'filepath' ] ) -def RunTest( app, test ): - contents = ReadFile( test[ 'request' ][ 'filepath' ] ) + def CombineRequest( request, data ): + kw = request + request.update( data ) + return BuildRequest( **kw ) - # We ignore errors here and check the response code ourself. - # This is to allow testing of requests returning errors. + # Because we aren't testing this command, we *always* ignore errors. This + # is mainly because we (may) want to test scenarios where the completer + # throws an exception and the easiest way to do that is to throw from + # within the FlagsForFile function. + app.post_json( '/event_notification', + CombineRequest( test[ 'request' ], { + 'event_name': 'FileReadyToParse', + 'contents': contents, + 'filetype': 'go', + } ), + expect_errors = True ) + + # We also ignore errors here, but then we check the response code + # ourself. This is to allow testing of requests returning errors. response = app.post_json( '/run_completer_command', CombineRequest( test[ 'request' ], { @@ -65,106 +88,332 @@ def RunTest( app, test ): expect_errors = True ) - print( 'completer response: {0}'.format( pformat( response.json ) ) ) - - eq_( response.status_code, test[ 'expect' ][ 'response' ] ) + print( 'completer response: {}'.format( pformat( response.json ) ) ) + assert_that( response.status_code, + equal_to( test[ 'expect' ][ 'response' ] ) ) assert_that( response.json, test[ 'expect' ][ 'data' ] ) @SharedYcmd -def Subcommands_GoTo_Basic( app, goto_command ): +def RunFixItTest( app, description, filepath, line, col, fixits_for_line ): RunTest( app, { - 'description': goto_command + ' works within file', + 'description': description, 'request': { - 'command': goto_command, - 'line_num': 8, - 'column_num': 8, - 'filepath': PathToTestFile( 'goto.go' ), + 'command': 'FixIt', + 'line_num': line, + 'column_num': col, + 'filepath': filepath, }, 'expect': { 'response': requests.codes.ok, - 'data': has_entries( { - 'filepath': PathToTestFile( 'goto.go' ), - 'line_num': 3, - 'column_num': 6, - } ) + 'data': fixits_for_line, } } ) -def Subcommands_GoTo_Basic_test(): - for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: - yield Subcommands_GoTo_Basic, command +@SharedYcmd +def Subcommands_DefinedSubcommands_test( app ): + subcommands_data = BuildRequest( completer_target = 'go' ) + + assert_that( app.post_json( '/defined_subcommands', subcommands_data ).json, + contains_inanyorder( 'Format', + 'GetType', + 'GoTo', + 'GoToDeclaration', + 'GoToDefinition', + 'GoToType', + 'FixIt', + 'RestartServer' ) ) + + +def Subcommands_ServerNotInitialized_test(): + filepath = PathToTestFile( 'goto.go' ) + + completer = handlers._server_state.GetFiletypeCompleter( [ 'go' ] ) + + @SharedYcmd + @patch.object( completer, '_ServerIsInitialized', return_value = False ) + def Test( app, cmd, arguments, *args ): + RunTest( app, { + 'description': 'Subcommand ' + cmd + ' handles server not ready', + 'request': { + 'command': cmd, + 'line_num': 1, + 'column_num': 1, + 'filepath': filepath, + 'arguments': arguments, + }, + 'expect': { + 'response': requests.codes.internal_server_error, + 'data': ErrorMatcher( RuntimeError, + 'Server is initializing. Please wait.' ), + } + } ) + + yield Test, 'Format', [] + yield Test, 'GetType', [] + yield Test, 'GoTo', [] + yield Test, 'GoToDeclaration', [] + yield Test, 'GoToDefinition', [] + yield Test, 'GoToType', [] + yield Test, 'FixIt', [] @SharedYcmd -def Subcommands_GoTo_Keyword( app, goto_command ): +def Subcommands_Format_WholeFile_test( app ): + # RLS can't execute textDocument/formatting if any file + # under the project root has errors, so we need to use + # a different project just for formatting. + # For further details check https://github.com/go-lang/rls/issues/1397 + project_dir = PathToTestFile() + StartGoCompleterServerInDirectory( app, project_dir ) + + filepath = os.path.join( project_dir, 'goto.go' ) + RunTest( app, { - 'description': goto_command + ' can\'t jump on keyword', + 'description': 'Formatting is applied on the whole file', 'request': { - 'command': goto_command, - 'line_num': 3, - 'column_num': 3, - 'filepath': PathToTestFile( 'goto.go' ), + 'command': 'Format', + 'filepath': filepath, + 'options': { + 'tab_size': 2, + 'insert_spaces': True + } }, 'expect': { - 'response': requests.codes.internal_server_error, - 'data': ErrorMatcher( RuntimeError, 'Can\'t find a definition.' ) + 'response': requests.codes.ok, + 'data': has_entries( { + 'fixits': contains( has_entries( { + 'chunks': contains( + ChunkMatcher( '', + LocationMatcher( filepath, 8, 1 ), + LocationMatcher( filepath, 9, 1 ) ), + ChunkMatcher( '\tdummy() //GoTo\n', + LocationMatcher( filepath, 9, 1 ), + LocationMatcher( filepath, 9, 1 ) ), + ChunkMatcher( '', + LocationMatcher( filepath, 12, 1 ), + LocationMatcher( filepath, 13, 1 ) ), + ChunkMatcher( '\tdiagnostics_test\n', + LocationMatcher( filepath, 13, 1 ), + LocationMatcher( filepath, 13, 1 ) ), + ) + } ) ) + } ) } } ) -def Subcommands_GoTo_Keyword_test(): - for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: - yield Subcommands_GoTo_Keyword, command +@ExpectedFailure( 'rangeFormat is not yet implemented', + matches_regexp( '\nExpected: <200>\n but: was <500>\n' ) ) +@SharedYcmd +def Subcommands_Format_Range_test( app ): + # RLS can't execute textDocument/formatting if any file + # under the project root has errors, so we need to use + # a different project just for formatting. + # For further details check https://github.com/go-lang/rls/issues/1397 + project_dir = PathToTestFile() + StartGoCompleterServerInDirectory( app, project_dir ) + filepath = os.path.join( project_dir, 'goto.go' ) -@SharedYcmd -def Subcommands_GoTo_WindowsNewlines( app, goto_command ): RunTest( app, { - 'description': goto_command + ' works with Windows newlines', + 'description': 'Formatting is applied on some part of the file', 'request': { - 'command': goto_command, - 'line_num': 4, - 'column_num': 7, - 'filepath': PathToTestFile( 'win.go' ), + 'command': 'Format', + 'filepath': filepath, + 'range': { + 'start': { + 'line_num': 7, + 'column_num': 1, + }, + 'end': { + 'line_num': 9, + 'column_num': 2 + } + }, + 'options': { + 'tab_size': 4, + 'insert_spaces': False + } }, 'expect': { 'response': requests.codes.ok, 'data': has_entries( { - 'filepath': PathToTestFile( 'win.go' ), - 'line_num': 2, - 'column_num': 6, + 'fixits': contains( has_entries( { + 'chunks': contains( + ChunkMatcher( 'fn unformatted_function(param: bool) -> bool {\n' + '\treturn param;\n' + '}\n' + '\n' + 'fn \n' + 'main()\n' + ' {\n' + ' unformatted_function( false );\n' + + '}\n', + LocationMatcher( filepath, 1, 1 ), + LocationMatcher( filepath, 9, 1 ) ), + ) + } ) ) } ) } } ) -def Subcommands_GoTo_WindowsNewlines_test(): - for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: - yield Subcommands_GoTo_WindowsNewlines, command +@SharedYcmd +def Subcommands_GetType_UnknownType_test( app ): + RunTest( app, { + 'description': 'GetType on a unknown type raises an error', + 'request': { + 'command': 'GetType', + 'line_num': 2, + 'column_num': 4, + 'filepath': PathToTestFile( 'td', 'test.go' ), + }, + 'expect': { + 'response': requests.codes.internal_server_error, + 'data': ErrorMatcher( RuntimeError, 'Unknown type.' ) + } + } ) + + +@SharedYcmd +def Subcommands_GetType_Function_test( app ): + RunTest( app, { + 'description': 'GetType on a function returns its type', + 'request': { + 'command': 'GetType', + 'line_num': 8, + 'column_num': 6, + 'filepath': PathToTestFile( 'td', 'test.go' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entry( 'message', 'func Hello()' ), + } + } ) + + +@SharedYcmd +def RunGoToTest( app, command, test ): + folder = PathToTestFile() + filepath = PathToTestFile( test[ 'req' ][ 0 ] ) + request = { + 'command': command, + 'line_num': test[ 'req' ][ 1 ], + 'column_num': test[ 'req' ][ 2 ], + 'filepath': filepath, + } + response = test[ 'res' ] -@IsolatedYcmd -@patch( 'ycmd.utils.WaitUntilProcessIsTerminated', - MockProcessTerminationTimingOut ) -def Subcommands_StopServer_Timeout_test( app ): - WaitUntilCompleterServerReady( app, 'go' ) + if isinstance( response, list ): + expect = { + 'response': requests.codes.ok, + 'data': contains( *[ + LocationMatcher( + os.path.join( folder, location[ 0 ] ), + location[ 1 ], + location[ 2 ] + ) for location in response + ] ) + } + elif isinstance( response, tuple ): + expect = { + 'response': requests.codes.ok, + 'data': LocationMatcher( + os.path.join( folder, response[ 0 ] ), + response[ 1 ], + response[ 2 ] + ) + } + else: + expect = { + 'response': requests.codes.internal_server_error, + 'data': ErrorMatcher( RuntimeError, test[ 'res' ] ) + } - app.post_json( - '/run_completer_command', - BuildRequest( - filetype = 'go', - command_arguments = [ 'StopServer' ] - ) - ) + RunTest( app, { + 'request': request, + 'expect' : expect + } ) - request_data = BuildRequest( filetype = 'go' ) - assert_that( app.post_json( '/debug_info', request_data ).json, - has_entry( - 'completer', - has_entry( 'servers', contains( - has_entry( 'is_running', False ) - ) ) - ) ) + +def Subcommands_GoTo_test(): + unicode_go_path = os.path.join( 'unicode', 'unicode.go' ) + tests = [ + # Struct + { 'req': ( unicode_go_path, 13, 5 ), 'res': ( unicode_go_path, 10, 5 ) }, + # Function + { 'req': ( 'goto.go', 8, 5 ), 'res': ( 'goto.go', 3, 6 ) }, + # Keyword + { 'req': ( 'goto.go', 3, 2 ), 'res': 'Cannot jump to location' }, + ] + + for test in tests: + for command in [ 'GoToDeclaration', 'GoToDefinition', 'GoTo' ]: + yield RunGoToTest, command, test + + +def Subcommands_GoToType_test(): + unicode_go_path = os.path.join( 'unicode', 'unicode.go' ) + tests = [ + # Works + { 'req': ( unicode_go_path, 13, 5 ), 'res': ( unicode_go_path, 3, 6 ) }, + # Fails + { 'req': ( unicode_go_path, 11, 7 ), 'res': 'Cannot jump to location' } ] + for test in tests: + yield RunGoToTest, 'GoToType', test + + +def Subcommands_FixIt_NullResponse_test(): + filepath = PathToTestFile( 'td', 'test.go' ) + yield ( RunFixItTest, 'Gopls returned NULL for response[ \'result\' ]', + filepath, 1, 1, has_entry( 'fixits', empty() ) ) + + +@SharedYcmd +def Subcommands_FixIt_ParseError_test( app ): + RunTest( app, { + 'description': 'Parse error leads to ResponseFailedException', + 'request': { + 'command': 'FixIt', + 'line_num': 1, + 'column_num': 1, + 'filepath': PathToTestFile( 'unicode', 'unicode.go' ), + }, + 'expect': { + 'response': requests.codes.internal_server_error, + 'data': ErrorMatcher( ResponseFailedException, + matches_regexp( '^Request failed: \\d' ) ) + } + } ) + + +def Subcommands_FixIt_Simple_test(): + filepath = PathToTestFile( 'goto.go' ) + fixit = has_entries( { + 'fixits': contains( + has_entries( { + 'text': "Organize Imports", + 'chunks': contains( + ChunkMatcher( '', + LocationMatcher( filepath, 8, 1 ), + LocationMatcher( filepath, 9, 1 ) ), + ChunkMatcher( '\tdummy() //GoTo\n', + LocationMatcher( filepath, 9, 1 ), + LocationMatcher( filepath, 9, 1 ) ), + ChunkMatcher( '', + LocationMatcher( filepath, 12, 1 ), + LocationMatcher( filepath, 13, 1 ) ), + ChunkMatcher( '\tdiagnostics_test\n', + LocationMatcher( filepath, 13, 1 ), + LocationMatcher( filepath, 13, 1 ) ), + ), + } ), + ) + } ) + yield ( RunFixItTest, 'Only one fixit returned', + filepath, 1, 1, fixit ) diff --git a/ycmd/tests/go/testdata/dontpanic.go b/ycmd/tests/go/testdata/dontpanic.go deleted file mode 100644 index b270164fdc..0000000000 --- a/ycmd/tests/go/testdata/dontpanic.go +++ /dev/null @@ -1,14 +0,0 @@ -// Binary dontpanic has a syntax error which causes a PANIC in gocode. We -// don't use this in any test, just the related gocode json output, it just -// lives here to document how to make gocode panic. Don't panic, and always -// bring a towel. -package main - -import { // <-- should be (, not { - "fmt" - "net/http" -} - -func main() { - http. -} diff --git a/ycmd/tests/go/testdata/gocode_dontpanic_output_offset_10.json b/ycmd/tests/go/testdata/gocode_dontpanic_output_offset_10.json deleted file mode 100644 index dbd298ee65..0000000000 --- a/ycmd/tests/go/testdata/gocode_dontpanic_output_offset_10.json +++ /dev/null @@ -1 +0,0 @@ -[0, [{"class": "PANIC", "name": "PANIC", "type": "PANIC"}]] \ No newline at end of file diff --git a/ycmd/tests/go/testdata/gocode_output_offset_121.json b/ycmd/tests/go/testdata/gocode_output_offset_121.json deleted file mode 100644 index 90b8c58f7d..0000000000 --- a/ycmd/tests/go/testdata/gocode_output_offset_121.json +++ /dev/null @@ -1 +0,0 @@ -[2, [{"class": "func", "name": "Prefix", "type": "func() string"}, {"class": "func", "name": "Print", "type": "func(v ...interface{})"}, {"class": "func", "name": "Printf", "type": "func(format string, v ...interface{})"}, {"class": "func", "name": "Println", "type": "func(v ...interface{})"}]] \ No newline at end of file diff --git a/ycmd/tests/go/testdata/gocode_output_offset_215.json b/ycmd/tests/go/testdata/gocode_output_offset_215.json deleted file mode 100644 index cda1e08e52..0000000000 --- a/ycmd/tests/go/testdata/gocode_output_offset_215.json +++ /dev/null @@ -1 +0,0 @@ -[3, [{"class": "func", "name": "Print", "type": "func(v ...interface{})"}, {"class": "func", "name": "Printf", "type": "func(format string, v ...interface{})"}, {"class": "func", "name": "Println", "type": "func(v ...interface{})"}]] \ No newline at end of file diff --git a/ycmd/tests/go/testdata/gocode_output_offset_292.json b/ycmd/tests/go/testdata/gocode_output_offset_292.json deleted file mode 100644 index 73a8e5e815..0000000000 --- a/ycmd/tests/go/testdata/gocode_output_offset_292.json +++ /dev/null @@ -1 +0,0 @@ -[5, [{"class": "func", "name": "Prefix", "type": "func() string"}]] \ No newline at end of file diff --git a/ycmd/tests/go/testdata/test.go b/ycmd/tests/go/testdata/test.go deleted file mode 100644 index d68d7e4966..0000000000 --- a/ycmd/tests/go/testdata/test.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package testdata is dummy data for gocode completion test. -package testdata - -import ( - "log" -) - -func Hello() { - log.Logge -} diff --git a/ycmd/tests/go/testdata/test2.go b/ycmd/tests/go/testdata/test2.go deleted file mode 100644 index 03a94690c7..0000000000 --- a/ycmd/tests/go/testdata/test2.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package testdata is a test input for gocode completions. -package testdata - -import "log" - -func LogSomeStuff() { - log.Print("Line 7: Astrid was born on Jan 30") - log.Print("Line 8: pɹɐɥ sı ǝpoɔıun") - log.Print("Line 9: Karl was born on Jan 10") - log.Printf("log prefix: %s", log.Prefix()) -} diff --git a/ycmd/tests/go/testdata/win.go b/ycmd/tests/go/testdata/win.go deleted file mode 100644 index 39727c9558..0000000000 --- a/ycmd/tests/go/testdata/win.go +++ /dev/null @@ -1,5 +0,0 @@ -package main -func foo() {} -func main() { - foo() -}