diff --git a/.gitmodules b/.gitmodules
index 1b9d3f55f4..656ad47514 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,9 +1,6 @@
[submodule "third_party/OmniSharpServer"]
path = third_party/OmniSharpServer
url = https://github.com/nosami/OmniSharpServer
-[submodule "third_party/jedi"]
- path = third_party/jedi
- url = https://github.com/davidhalter/jedi
[submodule "third_party/argparse"]
path = third_party/argparse
url = https://github.com/bewest/argparse
@@ -25,3 +22,6 @@
[submodule "third_party/tern"]
path = third_party/tern
url = https://github.com/ternjs/tern.git
+[submodule "third_party/JediHTTP"]
+ path = third_party/JediHTTP
+ url = https://github.com/vheon/JediHTTP
diff --git a/third_party/JediHTTP b/third_party/JediHTTP
new file mode 160000
index 0000000000..e19e32ca78
--- /dev/null
+++ b/third_party/JediHTTP
@@ -0,0 +1 @@
+Subproject commit e19e32ca78b2ff43287d77391b976e1e254a0012
diff --git a/third_party/jedi b/third_party/jedi
deleted file mode 160000
index 66557903ae..0000000000
--- a/third_party/jedi
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 66557903ae4c1174eb437a8feeeb718e69d5fa3a
diff --git a/ycmd/completers/python/jedi_completer.py b/ycmd/completers/python/jedi_completer.py
index c64296e299..e7e3b7d7c5 100644
--- a/ycmd/completers/python/jedi_completer.py
+++ b/ycmd/completers/python/jedi_completer.py
@@ -21,24 +21,57 @@
from ycmd.utils import ToUtf8IfNeeded
from ycmd.completers.completer import Completer
-from ycmd import responses
+from ycmd import responses, utils
+from jedihttp import hmaclib
-try:
- import jedi
-except ImportError:
- raise ImportError(
- 'Error importing jedi. Make sure the jedi submodule has been checked out. '
- 'In the YouCompleteMe folder, run "git submodule update --init --recursive"')
+import logging
+import urlparse
+import requests
+import threading
+import sys
+import os
+import base64
+
+
+HMAC_SECRET_LENGTH = 16
+PYTHON_EXECUTABLE_PATH = sys.executable
+LOG_FILENAME_FORMAT = os.path.join( utils.PathToTempDir(),
+ u'jedihttp_{port}_{std}.log' )
+PATH_TO_JEDIHTTP = os.path.join( os.path.abspath( os.path.dirname( __file__ ) ),
+ '..', '..', '..',
+ 'third_party', 'JediHTTP', 'jedihttp.py' )
+
+
+class HmacAuth( requests.auth.AuthBase ):
+ def __init__( self, secret ):
+ self._hmac_helper = hmaclib.JediHTTPHmacHelper( secret )
+
+ def __call__( self, req ):
+ self._hmac_helper.SignRequestHeaders( req.headers,
+ req.method,
+ req.path_url,
+ req.body )
+ return req
class JediCompleter( Completer ):
"""
- A Completer that uses the Jedi completion engine.
+ A Completer that uses the Jedi engine HTTP wrapper JediHTTP.
https://jedi.readthedocs.org/en/latest/
+ https://github.com/vheon/JediHTTP
"""
def __init__( self, user_options ):
super( JediCompleter, self ).__init__( user_options )
+ self._server_lock = threading.RLock()
+ self._jedihttp_port = None
+ self._jedihttp_phandle = None
+ self._logger = logging.getLogger( __name__ )
+ self._logfile_stdout = None
+ self._logfile_stderr = None
+ self._keep_logfiles = user_options[ 'server_keep_logfiles' ]
+ self._hmac_secret = ''
+ self._StartServer()
def SupportedFiletypes( self ):
@@ -46,24 +79,126 @@ def SupportedFiletypes( self ):
return [ 'python' ]
- def _GetJediScript( self, request_data ):
- filename = request_data[ 'filepath' ]
- contents = request_data[ 'file_data' ][ filename ][ 'contents' ]
- line = request_data[ 'line_num' ]
- # Jedi expects columns to start at 0, not 1
- column = request_data[ 'column_num' ] - 1
+ def Shutdown( self ):
+ if self.ServerIsRunning():
+ self._StopServer()
+
+
+ def ServerIsReady( self ):
+ """ Check if JediHTTP server is ready. """
+ try:
+ return bool( self._GetResponse( '/ready' ) )
+ except Exception:
+ return False
+
+
+ def ServerIsRunning( self ):
+ """ Check if JediHTTP server is running (up and serving). """
+ with self._server_lock:
+ if not self._jedihttp_phandle or not self._jedihttp_port:
+ return False
+
+ try:
+ return bool( self._GetResponse( '/healthy' ) )
+ except Exception:
+ return False
+
+
+ def RestartServer( self, request_data ):
+ """ Restart the JediHTTP Server. """
+ with self._server_lock:
+ self._StopServer()
+ self._StartServer()
+
+
+ def _StopServer( self ):
+ with self._server_lock:
+ self._logger.info( 'Stopping JediHTTP' )
+ if self._jedihttp_phandle:
+ self._jedihttp_phandle.terminate()
+ self._jedihttp_phandle = None
+ self._jedihttp_port = None
+
+ if not self._keep_logfiles:
+ utils.RemoveIfExists( self._logfile_stdout )
+ utils.RemoveIfExists( self._logfile_stderr )
+
+
+ def _StartServer( self ):
+ with self._server_lock:
+ self._logger.info( 'Starting JediHTTP server' )
+ self._ChoosePort()
+ self._GenerateHmacSecret()
+
+ with hmaclib.TemporaryHmacSecretFile( self._hmac_secret ) as hmac_file:
+ command = [ PYTHON_EXECUTABLE_PATH,
+ PATH_TO_JEDIHTTP,
+ '--port', str( self._jedihttp_port ),
+ '--hmac-file-secret', hmac_file.name ]
+
+ self._logfile_stdout = LOG_FILENAME_FORMAT.format(
+ port = self._jedihttp_port, std = 'stdout' )
+ self._logfile_stderr = LOG_FILENAME_FORMAT.format(
+ port = self._jedihttp_port, std = 'stderr' )
+
+ with open( self._logfile_stderr, 'w' ) as logerr:
+ with open( self._logfile_stdout, 'w' ) as logout:
+ self._jedihttp_phandle = utils.SafePopen( command,
+ stdout = logout,
+ stderr = logerr )
+
+
+ def _ChoosePort( self ):
+ if not self._jedihttp_port:
+ self._jedihttp_port = utils.GetUnusedLocalhostPort()
+ self._logger.info( u'using port {0}'.format( self._jedihttp_port ) )
+
+
+ def _GenerateHmacSecret( self ):
+ self._hmac_secret = base64.b64encode( os.urandom( HMAC_SECRET_LENGTH ) )
+
+
+ def _GetResponse( self, handler, request_data = {} ):
+ """ Handle communication with server """
+ target = urlparse.urljoin( self._ServerLocation(), handler )
+ parameters = self._TranslateRequestForJediHTTP( request_data )
+ response = requests.post( target,
+ json = parameters,
+ auth = HmacAuth( self._hmac_secret ) )
+ response.raise_for_status()
+ return response.json()
- return jedi.Script( contents, line, column, filename )
+
+ def _TranslateRequestForJediHTTP( self, request_data ):
+ if not request_data:
+ return {}
+
+ path = request_data[ 'filepath' ]
+ source = request_data[ 'file_data' ][ path ][ 'contents' ]
+ line = request_data[ 'line_num' ]
+ # JediHTTP as Jedi itself expects columns to start at 0, not 1
+ col = request_data[ 'column_num' ] - 1
+
+ return {
+ 'source': source,
+ 'line': line,
+ 'col': col,
+ 'source_path': path
+ }
+
+
+ def _ServerLocation( self ):
+ return 'http://127.0.0.1:' + str( self._jedihttp_port )
def _GetExtraData( self, completion ):
location = {}
- if completion.module_path:
- location[ 'filepath' ] = ToUtf8IfNeeded( completion.module_path )
- if completion.line:
- location[ 'line_num' ] = completion.line
- if completion.column:
- location[ 'column_num' ] = completion.column + 1
+ if completion[ 'module_path' ]:
+ location[ 'filepath' ] = ToUtf8IfNeeded( completion[ 'module_path' ] )
+ if completion[ 'line' ]:
+ location[ 'line_num' ] = completion[ 'line' ]
+ if completion[ 'column' ]:
+ location[ 'column_num' ] = completion[ 'column' ] + 1
if location:
extra_data = {}
@@ -74,13 +209,24 @@ def _GetExtraData( self, completion ):
def ComputeCandidatesInner( self, request_data ):
- script = self._GetJediScript( request_data )
return [ responses.BuildCompletionData(
- ToUtf8IfNeeded( completion.name ),
- ToUtf8IfNeeded( completion.description ),
- ToUtf8IfNeeded( completion.docstring() ),
+ ToUtf8IfNeeded( completion[ 'name' ] ),
+ ToUtf8IfNeeded( completion[ 'description' ] ),
+ ToUtf8IfNeeded( completion[ 'docstring' ] ),
extra_data = self._GetExtraData( completion ) )
- for completion in script.completions() ]
+ for completion in self._JediCompletions( request_data ) ]
+
+
+ def _JediCompletions( self, request_data ):
+ return self._GetResponse( '/completions', request_data )[ 'completions' ]
+
+
+ def DefinedSubcommands( self ):
+ # We don't want expose this sub-command because is not really needed for
+ # the user but is useful in tests for tearing down the server
+ subcommands = super( JediCompleter, self ).DefinedSubcommands()
+ subcommands.remove( 'StopServer' )
+ return subcommands
def GetSubcommandsMap( self ):
@@ -92,87 +238,103 @@ def GetSubcommandsMap( self ):
'GoTo' : ( lambda self, request_data, args:
self._GoTo( request_data ) ),
'GetDoc' : ( lambda self, request_data, args:
- self._GetDoc( request_data ) )
+ self._GetDoc( request_data ) ),
+ 'StopServer' : ( lambda self, request_data, args:
+ self.Shutdown() ),
+ 'RestartServer' : ( lambda self, request_data, args:
+ self.RestartServer() )
}
def _GoToDefinition( self, request_data ):
- definitions = self._GetDefinitionsList( request_data )
- if definitions:
- return self._BuildGoToResponse( definitions )
- else:
+ definitions = self._GetDefinitionsList( '/gotodefinition', request_data )
+ if not definitions:
raise RuntimeError( 'Can\'t jump to definition.' )
+ return self._BuildGoToResponse( definitions )
def _GoToDeclaration( self, request_data ):
- definitions = self._GetDefinitionsList( request_data, declaration = True )
- if definitions:
- return self._BuildGoToResponse( definitions )
- else:
+ definitions = self._GetDefinitionsList( '/gotoassignment', request_data )
+ if not definitions:
raise RuntimeError( 'Can\'t jump to declaration.' )
+ return self._BuildGoToResponse( definitions )
def _GoTo( self, request_data ):
- definitions = ( self._GetDefinitionsList( request_data ) or
- self._GetDefinitionsList( request_data, declaration = True ) )
- if definitions:
- return self._BuildGoToResponse( definitions )
- else:
+ try:
+ return self._GoToDefinition( request_data )
+ except Exception:
+ pass
+
+ try:
+ return self._GoToDeclaration( request_data )
+ except Exception:
raise RuntimeError( 'Can\'t jump to definition or declaration.' )
def _GetDoc( self, request_data ):
- definitions = self._GetDefinitionsList( request_data )
- if definitions:
+ try:
+ definitions = self._GetDefinitionsList( '/gotodefinition', request_data )
return self._BuildDetailedInfoResponse( definitions )
- else:
+ except Exception:
raise RuntimeError( 'Can\'t find a definition.' )
- def _GetDefinitionsList( self, request_data, declaration = False ):
- definitions = []
- script = self._GetJediScript( request_data )
+ def _GetDefinitionsList( self, handler, request_data ):
try:
- if declaration:
- definitions = script.goto_assignments()
- else:
- definitions = script.goto_definitions()
- except jedi.NotFoundError:
- raise RuntimeError(
- 'Cannot follow nothing. Put your cursor on a valid name.' )
-
- return definitions
+ response = self._GetResponse( handler, request_data )
+ return response[ 'definitions' ]
+ except Exception:
+ raise RuntimeError( 'Cannot follow nothing. Put your cursor on a valid name.' )
def _BuildGoToResponse( self, definition_list ):
if len( definition_list ) == 1:
definition = definition_list[ 0 ]
- if definition.in_builtin_module():
- if definition.is_keyword:
- raise RuntimeError(
- 'Cannot get the definition of Python keywords.' )
+ if definition[ 'in_builtin_module' ]:
+ if definition[ 'is_keyword' ]:
+ raise RuntimeError( 'Cannot get the definition of Python keywords.' )
else:
raise RuntimeError( 'Builtin modules cannot be displayed.' )
else:
- return responses.BuildGoToResponse( definition.module_path,
- definition.line,
- definition.column + 1 )
+ return responses.BuildGoToResponse( definition[ 'module_path' ],
+ definition[ 'line' ],
+ definition[ 'column' ] + 1 )
else:
# multiple definitions
defs = []
for definition in definition_list:
- if definition.in_builtin_module():
+ if definition[ 'in_builtin_module' ]:
defs.append( responses.BuildDescriptionOnlyGoToResponse(
- 'Builtin ' + definition.description ) )
+ 'Builtin ' + definition[ 'description' ] ) )
else:
defs.append(
- responses.BuildGoToResponse( definition.module_path,
- definition.line,
- definition.column + 1,
- definition.description ) )
+ responses.BuildGoToResponse( definition[ 'module_path' ],
+ definition[ 'line' ],
+ definition[ 'column' ] + 1,
+ definition[ 'description' ] ) )
return defs
+
def _BuildDetailedInfoResponse( self, definition_list ):
- docs = [ definition.docstring() for definition in definition_list ]
+ docs = [ definition[ 'docstring' ] for definition in definition_list ]
return responses.BuildDetailedInfoResponse( '\n---\n'.join( docs ) )
+
+ def DebugInfo( self, request_data ):
+ with self._server_lock:
+ if self.ServerIsRunning():
+ return ( 'JediHTTP running at 127.0.0.1:{0}\n'
+ ' stdout log: {1}\n'
+ ' stderr log: {2}' ).format( self._jedihttp_port,
+ self._logfile_stdout,
+ self._logfile_stderr )
+
+ if self._logfile_stdout and self._logfile_stderr:
+ return ( 'JediHTTP is no longer running\n'
+ ' stdout log: {1}\n'
+ ' stderr log: {2}' ).format( self._logfile_stdout,
+ self._logfile_stderr )
+
+ return 'JediHTTP is not running'
+
diff --git a/ycmd/tests/detailed_diagnostics_test.py b/ycmd/tests/detailed_diagnostics_test.py
new file mode 100644
index 0000000000..5ee8194e14
--- /dev/null
+++ b/ycmd/tests/detailed_diagnostics_test.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2016 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 nose.tools import eq_
+from hamcrest import assert_that
+from ..responses import NoDiagnosticSupport, BuildDisplayMessageResponse
+from .handlers_test import Handlers_test
+from .test_utils import DummyCompleter
+from mock import patch
+import httplib
+
+
+class Diagnostics_test( Handlers_test ):
+
+ def DoesntWork_test( self ):
+ with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+ diag_data = self._BuildRequest( contents = "foo = 5",
+ line_num = 2,
+ filetype = 'dummy_filetype' )
+
+ response = self._app.post_json( '/detailed_diagnostic',
+ diag_data,
+ expect_errors = True )
+
+ eq_( response.status_code, httplib.INTERNAL_SERVER_ERROR )
+ assert_that( response.json, self._ErrorMatcher( NoDiagnosticSupport ) )
+
+
+ @patch( 'ycmd.tests.test_utils.DummyCompleter.GetDetailedDiagnostic',
+ return_value = BuildDisplayMessageResponse( "detailed diagnostic" ) )
+ def DoesWork_test( self, *args ):
+ with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+ diag_data = self._BuildRequest( contents = "foo = 5",
+ filetype = 'dummy_filetype' )
+
+ response = self._app.post_json( '/detailed_diagnostic', diag_data )
+ assert_that( response.json, self._MessageMatcher( "detailed diagnostic" ) )
diff --git a/ycmd/tests/get_completions_test.py b/ycmd/tests/get_completions_test.py
index 14ad3f532d..b879b02f96 100644
--- a/ycmd/tests/get_completions_test.py
+++ b/ycmd/tests/get_completions_test.py
@@ -23,6 +23,8 @@
from hamcrest import assert_that, has_items
from .. import handlers
from .handlers_test import Handlers_test
+from ycmd.tests.test_utils import DummyCompleter
+from mock import patch
class GetCompletions_test( Handlers_test ):
@@ -93,15 +95,18 @@ def IdentifierCompleter_WorksForSpecialIdentifierChars_test( self ):
)
- def ForceSemantic_Works_test( self ):
- completion_data = self._BuildRequest( filetype = 'python',
- force_semantic = True )
+ @patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList',
+ return_value = [ 'foo', 'bar', 'qux' ] )
+ def ForceSemantic_Works_test( self, *args ):
+ with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+ completion_data = self._BuildRequest( filetype = 'dummy_filetype',
+ force_semantic = True )
- results = self._app.post_json( '/completions',
- completion_data ).json[ 'completions' ]
- assert_that( results, has_items( self._CompletionEntryMatcher( 'abs' ),
- self._CompletionEntryMatcher( 'open' ),
- self._CompletionEntryMatcher( 'bool' ) ) )
+ results = self._app.post_json( '/completions',
+ completion_data ).json[ 'completions' ]
+ assert_that( results, has_items( self._CompletionEntryMatcher( 'foo' ),
+ self._CompletionEntryMatcher( 'bar' ),
+ self._CompletionEntryMatcher( 'qux' ) ) )
def IdentifierCompleter_SyntaxKeywordsAdded_test( self ):
diff --git a/ycmd/tests/handlers_test.py b/ycmd/tests/handlers_test.py
index 95db86427d..c88dc90a92 100644
--- a/ycmd/tests/handlers_test.py
+++ b/ycmd/tests/handlers_test.py
@@ -22,8 +22,10 @@
from webtest import TestApp
from .. import handlers
from ycmd import user_options_store
-from hamcrest import has_entries, has_entry
+from hamcrest import has_entries, has_entry, contains_string
from test_utils import BuildRequest
+from mock import patch
+import contextlib
import bottle
import os
@@ -40,6 +42,14 @@ def setUp( self ):
self._app = TestApp( handlers.app )
+ @contextlib.contextmanager
+ def PatchCompleter( self, completer, filetype ):
+ user_options = handlers._server_state._user_options
+ with patch.dict( 'ycmd.handlers._server_state._filetype_completers',
+ { filetype: completer( user_options ) } ):
+ yield
+
+
@staticmethod
def _BuildRequest( **kwargs ):
return BuildRequest( **kwargs )
@@ -75,12 +85,19 @@ def _ChangeSpecificOptions( options ):
@staticmethod
- def _ErrorMatcher( cls, msg ):
+ def _ErrorMatcher( cls, msg = None ):
""" Returns a hamcrest matcher for a server exception response """
- return has_entries( {
- 'exception': has_entry( 'TYPE', cls.__name__ ),
- 'message': msg,
- } )
+ entry = { 'exception': has_entry( 'TYPE', cls.__name__ ) }
+
+ if msg:
+ entry.update( { 'message': msg } )
+
+ return has_entries( entry )
+
+
+ @staticmethod
+ def _MessageMatcher( msg ):
+ return has_entry( 'message', contains_string( msg ) )
def _PathToTestFile( self, *args ):
diff --git a/ycmd/tests/misc_handlers_test.py b/ycmd/tests/misc_handlers_test.py
index f88c0faaff..c16e57d55d 100644
--- a/ycmd/tests/misc_handlers_test.py
+++ b/ycmd/tests/misc_handlers_test.py
@@ -20,14 +20,16 @@
from nose.tools import ok_
from .handlers_test import Handlers_test
+from ycmd.tests.test_utils import DummyCompleter
class MiscHandlers_test( Handlers_test ):
def SemanticCompletionAvailable_test( self ):
- request_data = self._BuildRequest( filetype = 'python' )
- ok_( self._app.post_json( '/semantic_completion_available',
- request_data ).json )
+ with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+ request_data = self._BuildRequest( filetype = 'dummy_filetype' )
+ ok_( self._app.post_json( '/semantic_completion_available',
+ request_data ).json )
def EventNotification_AlwaysJsonResponse_test( self ):
diff --git a/ycmd/tests/python/diagnostics_test.py b/ycmd/tests/python/diagnostics_test.py
deleted file mode 100644
index 897881eb2a..0000000000
--- a/ycmd/tests/python/diagnostics_test.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2015 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 nose.tools import eq_
-from hamcrest import assert_that, has_entry
-from ...responses import NoDiagnosticSupport
-from ..handlers_test import Handlers_test
-import httplib
-
-
-class Python_Diagnostics_test( Handlers_test ):
-
- def DoesntWork_test( self ):
- diag_data = self._BuildRequest( contents = "foo = 5",
- line_num = 2,
- filetype = 'python' )
-
- response = self._app.post_json( '/detailed_diagnostic',
- diag_data,
- expect_errors = True )
-
- eq_( response.status_code, httplib.INTERNAL_SERVER_ERROR )
- assert_that( response.json,
- has_entry( 'exception',
- has_entry( 'TYPE',
- NoDiagnosticSupport.__name__ ) ) )
diff --git a/ycmd/tests/python/get_completions_test.py b/ycmd/tests/python/get_completions_test.py
index a5ca2d773e..76773c5ada 100644
--- a/ycmd/tests/python/get_completions_test.py
+++ b/ycmd/tests/python/get_completions_test.py
@@ -51,7 +51,6 @@ def CombineRequest( request, data ):
'contents': contents,
} ) )
-
# We ignore errors here and we check the response code ourself.
# This is to allow testing of requests returning errors.
response = self._app.post_json( '/completions',
diff --git a/ycmd/tests/python/python_handlers_test.py b/ycmd/tests/python/python_handlers_test.py
index 7296c93f0d..f47b1cb173 100644
--- a/ycmd/tests/python/python_handlers_test.py
+++ b/ycmd/tests/python/python_handlers_test.py
@@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with ycmd. If not, see .
+import time
from ..handlers_test import Handlers_test
@@ -24,3 +25,33 @@ class Python_Handlers_test( Handlers_test ):
def __init__( self ):
self._file = __file__
+
+
+ def setUp( self ):
+ super( Python_Handlers_test, self ).setUp()
+ self.WaitUntilJediHTTPServerReady()
+
+
+ def tearDown( self ):
+ self.StopJediHTTPServer()
+
+
+ def WaitUntilJediHTTPServerReady( self ):
+ retries = 10
+
+ while retries > 0:
+ result = self._app.get( '/ready', { 'subserver': 'python' } ).json
+ if result:
+ return
+
+ time.sleep( 0.2 )
+ retries = retries - 1
+
+ raise RuntimeError( "Timeout waiting for JediHTTP" )
+
+
+ def StopJediHTTPServer( self ):
+ request = self._BuildRequest( completer_target = 'filetype_default',
+ command_arguments = [ 'StopServer' ],
+ filetype = 'python' )
+ self._app.post_json( '/run_completer_command', request )
diff --git a/ycmd/tests/python/subcommands_test.py b/ycmd/tests/python/subcommands_test.py
index 5c8a30b448..4df7baea82 100644
--- a/ycmd/tests/python/subcommands_test.py
+++ b/ycmd/tests/python/subcommands_test.py
@@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with ycmd. If not, see .
+from hamcrest import assert_that
from nose.tools import eq_
from python_handlers_test import Python_Handlers_test
import os.path
@@ -24,26 +25,106 @@
class Python_Subcommands_test( Python_Handlers_test ):
- def GoTo_ZeroBasedLineAndColumn_test( self ):
+ def GoTo_Variation_ZeroBasedLineAndColumn_test( self ):
+ tests = [
+ {
+ 'command_arguments': [ 'GoToDefinition' ],
+ 'response': {
+ 'filepath': os.path.abspath( '/foo.py' ),
+ 'line_num': 2,
+ 'column_num': 5
+ }
+ },
+ {
+ 'command_arguments': [ 'GoToDeclaration' ],
+ 'response': {
+ 'filepath': os.path.abspath( '/foo.py' ),
+ 'line_num': 7,
+ 'column_num': 1
+ }
+ }
+ ]
+ for test in tests:
+ yield self._Run_GoTo_Variation_ZeroBasedLineAndColumn, test
+
+
+ def _Run_GoTo_Variation_ZeroBasedLineAndColumn( self, test ):
+ # Example taken directly from jedi docs
+ # http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html#examples
contents = """
-def foo():
- pass
+def my_func():
+ print 'called'
-foo()
+alias = my_func
+my_list = [1, None, alias]
+inception = my_list[2]
+
+inception()
"""
+ goto_data = self._BuildRequest(
+ completer_target = 'filetype_default',
+ command_arguments = test[ 'command_arguments' ],
+ line_num = 9,
+ contents = contents,
+ filetype = 'python',
+ filepath = '/foo.py'
+ )
+
+ eq_( test[ 'response' ],
+ self._app.post_json( '/run_completer_command', goto_data ).json )
+
+
+ def GoToDefinition_NotFound_test( self ):
+ filepath = self._PathToTestFile( 'goto_file5.py' )
+ goto_data = self._BuildRequest( command_arguments = [ 'GoToDefinition' ],
+ line_num = 4,
+ contents = open( filepath ).read(),
+ filetype = 'python',
+ filepath = filepath )
+
+ response = self._app.post_json( '/run_completer_command',
+ goto_data,
+ expect_errors = True ).json
+ assert_that( response,
+ self._ErrorMatcher( RuntimeError, "Can\'t jump to definition." ) )
+
+
+ def GoTo_test( self ):
+ # Tests taken from https://github.com/Valloric/YouCompleteMe/issues/1236
+ tests = [
+ {
+ 'request': { 'filename': 'goto_file1.py', 'line_num': 2 },
+ 'response': {
+ 'filepath': self._PathToTestFile( 'goto_file3.py' ),
+ 'line_num': 1,
+ 'column_num': 5
+ }
+ },
+ {
+ 'request': { 'filename': 'goto_file4.py', 'line_num': 2 },
+ 'response': {
+ 'filepath': self._PathToTestFile( 'goto_file4.py' ),
+ 'line_num': 1,
+ 'column_num': 18
+ }
+ }
+ ]
+ for test in tests:
+ yield self._Run_GoTo, test
+
+
+ def _Run_GoTo( self, test ):
+ filepath = self._PathToTestFile( test[ 'request' ][ 'filename' ] )
goto_data = self._BuildRequest( completer_target = 'filetype_default',
- command_arguments = ['GoToDefinition'],
- line_num = 5,
- contents = contents,
+ command_arguments = [ 'GoTo' ],
+ line_num = test[ 'request' ][ 'line_num' ],
+ contents = open( filepath ).read(),
filetype = 'python',
- filepath = '/foo.py' )
+ filepath = filepath )
- eq_( {
- 'filepath': os.path.abspath( '/foo.py' ),
- 'line_num': 2,
- 'column_num': 5
- }, self._app.post_json( '/run_completer_command', goto_data ).json )
+ eq_( test[ 'response' ],
+ self._app.post_json( '/run_completer_command', goto_data ).json )
def GetDoc_Method_test( self ):
diff --git a/ycmd/tests/python/testdata/goto_file1.py b/ycmd/tests/python/testdata/goto_file1.py
new file mode 100644
index 0000000000..7573ea1e4e
--- /dev/null
+++ b/ycmd/tests/python/testdata/goto_file1.py
@@ -0,0 +1,2 @@
+from goto_file2 import foo
+foo
diff --git a/ycmd/tests/python/testdata/goto_file2.py b/ycmd/tests/python/testdata/goto_file2.py
new file mode 100644
index 0000000000..dd02fd0ced
--- /dev/null
+++ b/ycmd/tests/python/testdata/goto_file2.py
@@ -0,0 +1 @@
+from goto_file3 import bar as foo
diff --git a/ycmd/tests/python/testdata/goto_file3.py b/ycmd/tests/python/testdata/goto_file3.py
new file mode 100644
index 0000000000..5be982ddee
--- /dev/null
+++ b/ycmd/tests/python/testdata/goto_file3.py
@@ -0,0 +1,2 @@
+def bar():
+ pass
diff --git a/ycmd/tests/python/testdata/goto_file4.py b/ycmd/tests/python/testdata/goto_file4.py
new file mode 100644
index 0000000000..eb235e09a4
--- /dev/null
+++ b/ycmd/tests/python/testdata/goto_file4.py
@@ -0,0 +1,2 @@
+from math import sin
+sin
diff --git a/ycmd/tests/python/testdata/goto_file5.py b/ycmd/tests/python/testdata/goto_file5.py
new file mode 100644
index 0000000000..0cf868b903
--- /dev/null
+++ b/ycmd/tests/python/testdata/goto_file5.py
@@ -0,0 +1,4 @@
+class Foo(object):
+ pass
+
+Bar
diff --git a/ycmd/tests/subcommands_test.py b/ycmd/tests/subcommands_test.py
index 8ed3516169..584fcf7e9f 100644
--- a/ycmd/tests/subcommands_test.py
+++ b/ycmd/tests/subcommands_test.py
@@ -20,25 +20,26 @@
from nose.tools import eq_
from .handlers_test import Handlers_test
-
+from ycmd.tests.test_utils import DummyCompleter
+from mock import patch
class Subcommands_test( Handlers_test ):
- def Basic_test( self ):
- subcommands_data = self._BuildRequest( completer_target = 'python' )
+ @patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
+ return_value = { 'A': lambda x: x, 'B': lambda x: x, 'C': lambda x: x } )
+ def Basic_test( self, *args ):
+ with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+ subcommands_data = self._BuildRequest( completer_target = 'dummy_filetype' )
- eq_( [ 'GetDoc',
- 'GoTo',
- 'GoToDeclaration',
- 'GoToDefinition' ],
- self._app.post_json( '/defined_subcommands', subcommands_data ).json )
+ eq_( [ 'A', 'B', 'C' ],
+ self._app.post_json( '/defined_subcommands', subcommands_data ).json )
- def NoExplicitCompleterTargetSpecified_test( self ):
- subcommands_data = self._BuildRequest( filetype = 'python' )
+ @patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
+ return_value = { 'A': lambda x: x, 'B': lambda x: x, 'C': lambda x: x } )
+ def NoExplicitCompleterTargetSpecified_test( self, *args ):
+ with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+ subcommands_data = self._BuildRequest( filetype = 'dummy_filetype' )
- eq_( [ 'GetDoc',
- 'GoTo',
- 'GoToDeclaration',
- 'GoToDefinition' ],
- self._app.post_json( '/defined_subcommands', subcommands_data ).json )
+ eq_( [ 'A', 'B', 'C' ],
+ self._app.post_json( '/defined_subcommands', subcommands_data ).json )
diff --git a/ycmd/tests/test_utils.py b/ycmd/tests/test_utils.py
index 8e063e2551..2c19ad5080 100644
--- a/ycmd/tests/test_utils.py
+++ b/ycmd/tests/test_utils.py
@@ -19,6 +19,10 @@
# along with ycmd. If not, see .
+from ycmd.completers.completer import Completer
+from ycmd.responses import BuildCompletionData
+
+
def BuildRequest( **kwargs ):
filepath = kwargs[ 'filepath' ] if 'filepath' in kwargs else '/foo'
contents = kwargs[ 'contents' ] if 'contents' in kwargs else ''
@@ -47,3 +51,22 @@ def BuildRequest( **kwargs ):
request[ key ] = value
return request
+
+
+class DummyCompleter( Completer ):
+ def __init__( self, user_options ):
+ super( DummyCompleter, self ).__init__( user_options )
+
+
+ def SupportedFiletypes( self ):
+ return []
+
+
+ def ComputeCandidatesInner( self, request_data ):
+ return [ BuildCompletionData( candidate )
+ for candidate in self.CandidatesList() ]
+
+
+ # This method is here for testing purpose, so it can be mocked during tests
+ def CandidatesList( self ):
+ return []