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()
-}