Repository: ycm-core/ycmd

Issue:
exit code not correct if importing ycm_core in global config fails
Hi,

I am not sure this is a real bug, but I encountered this while implementing handling of exit code in `emacs-ycmd`.

I had a `import ycm_core` in my global config. If importing fails there the line with `code = CompatibleWithCurrentCore()` in `__main__.py` will never be reached to return the correct exit code and then I just get an exit code 1.



Discussion:
Ugh, that does sound like a bug.


Resolving patch:
diff --git a/ycmd/extra_conf_store.py b/ycmd/extra_conf_store.py
--- a/ycmd/extra_conf_store.py
+++ b/ycmd/extra_conf_store.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2011, 2012 Google Inc.
+# Copyright (C) 2011-2012 Google Inc.
+#               2016      ycmd contributors
 #
 # This file is part of ycmd.
 #
@@ -37,6 +38,8 @@
 from fnmatch import fnmatch
 
 
+_logger = logging.getLogger( __name__ )
+
 # Singleton variables
 _module_for_module_file = {}
 _module_for_module_file_lock = Lock()
@@ -81,22 +84,34 @@ def Shutdown():
 
 
 def _CallGlobalExtraConfMethod( function_name ):
-  logger = _Logger()
   global_ycm_extra_conf = _GlobalYcmExtraConfFileLocation()
   if not ( global_ycm_extra_conf and
            os.path.exists( global_ycm_extra_conf ) ):
-    logger.debug( 'No global extra conf, not calling method ' + function_name )
+    _logger.debug( 'No global extra conf, '
+                   'not calling method {0}'.format( function_name ) )
+    return
+
+  try:
+    module = Load( global_ycm_extra_conf, force = True )
+  except Exception:
+    _logger.exception( 'Error occurred while loading '
+                       'global extra conf {0}'.format( global_ycm_extra_conf ) )
     return
 
-  module = Load( global_ycm_extra_conf, force = True )
   if not module or not hasattr( module, function_name ):
-    logger.debug( 'Global extra conf not loaded or no function ' +
-                  function_name )
+    _logger.debug( 'Global extra conf not loaded or no function ' +
+                   function_name )
     return
 
-  logger.info( 'Calling global extra conf method {0} on conf file {1}'.format(
-      function_name, global_ycm_extra_conf ) )
-  getattr( module, function_name )()
+  try:
+    _logger.info(
+      'Calling global extra conf method {0} '
+      'on conf file {1}'.format( function_name, global_ycm_extra_conf ) )
+    getattr( module, function_name )()
+  except Exception:
+    _logger.exception(
+      'Error occurred while calling global extra conf method {0} '
+      'on conf file {1}'.format( function_name, global_ycm_extra_conf ) )
 
 
 def Disable( module_file ):
@@ -210,7 +225,3 @@ def _RandomName():
 def _GlobalYcmExtraConfFileLocation():
   return os.path.expanduser(
     user_options_store.Value( 'global_ycm_extra_conf' ) )
-
-
-def _Logger():
-  return logging.getLogger( __name__ )


Test patch:
diff --git a/ycmd/tests/extra_conf_store_test.py b/ycmd/tests/extra_conf_store_test.py
new file mode 100755
--- /dev/null
+++ b/ycmd/tests/extra_conf_store_test.py
@@ -0,0 +1,141 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import inspect
+from mock import patch
+
+from hamcrest import assert_that, calling, equal_to, has_length, none, raises
+from ycmd import extra_conf_store
+from ycmd.responses import UnknownExtraConf
+from ycmd.tests import PathToTestFile
+from ycmd.tests.test_utils import UserOption
+
+
+class ExtraConfStore_test():
+
+  def setUp( self ):
+    extra_conf_store.Reset()
+
+
+  def ModuleForSourceFile_UnknownExtraConf_test( self ):
+    filename = PathToTestFile( 'extra_conf', 'project', 'some_file' )
+    assert_that(
+      calling( extra_conf_store.ModuleForSourceFile ).with_args( filename ),
+      raises( UnknownExtraConf, 'Found .*\.ycm_extra_conf\.py\. Load?' )
+    )
+
+
+  def ModuleForSourceFile_NoConfirmation_test( self ):
+    filename = PathToTestFile( 'extra_conf', 'project', 'some_file' )
+    extra_conf_file = PathToTestFile( 'extra_conf', 'project',
+                                      '.ycm_extra_conf.py' )
+    with UserOption( 'confirm_extra_conf', 0 ):
+      module = extra_conf_store.ModuleForSourceFile( filename )
+      assert_that( inspect.ismodule( module ) )
+      assert_that( inspect.getfile( module ), equal_to( extra_conf_file ) )
+
+
+  def ModuleForSourceFile_Whitelisted_test( self ):
+    filename = PathToTestFile( 'extra_conf', 'project', 'some_file' )
+    extra_conf_file = PathToTestFile( 'extra_conf', 'project',
+                                      '.ycm_extra_conf.py' )
+    with UserOption( 'extra_conf_globlist', [ extra_conf_file ] ):
+      module = extra_conf_store.ModuleForSourceFile( filename )
+      assert_that( inspect.ismodule( module ) )
+      assert_that( inspect.getfile( module ), equal_to( extra_conf_file ) )
+
+
+  def ModuleForSourceFile_Blacklisted_test( self ):
+    filename = PathToTestFile( 'extra_conf', 'project', 'some_file' )
+    extra_conf_file = PathToTestFile( 'extra_conf', 'project',
+                                      '.ycm_extra_conf.py' )
+    with UserOption( 'extra_conf_globlist', [ '!' + extra_conf_file ] ):
+      assert_that( extra_conf_store.ModuleForSourceFile( filename ), none() )
+
+
+  def ModuleForSourceFile_GlobalExtraConf_test( self ):
+    filename = PathToTestFile( 'extra_conf', 'some_file' )
+    extra_conf_file = PathToTestFile( 'extra_conf', 'global_extra_conf.py' )
+    with UserOption( 'global_ycm_extra_conf', extra_conf_file ):
+      module = extra_conf_store.ModuleForSourceFile( filename )
+      assert_that( inspect.ismodule( module ) )
+      assert_that( inspect.getfile( module ), equal_to( extra_conf_file ) )
+
+
+  @patch( 'ycmd.extra_conf_store._logger', autospec = True )
+  def CallGlobalExtraConfMethod_NoGlobalExtraConf_test( self, logger ):
+    with UserOption( 'global_ycm_extra_conf',
+                     PathToTestFile( 'extra_conf', 'no_extra_conf.py' ) ):
+      extra_conf_store._CallGlobalExtraConfMethod( 'SomeMethod' )
+    assert_that( logger.method_calls, has_length( 1 ) )
+    logger.debug.assert_called_with( 'No global extra conf, not calling method '
+                                     'SomeMethod' )
+
+
+  @patch( 'ycmd.extra_conf_store._logger', autospec = True )
+  def CallGlobalExtraConfMethod_NoMethodInGlobalExtraConf_test( self, logger ):
+    with UserOption( 'global_ycm_extra_conf',
+                     PathToTestFile( 'extra_conf', 'global_extra_conf.py' ) ):
+      extra_conf_store._CallGlobalExtraConfMethod( 'MissingMethod' )
+    assert_that( logger.method_calls, has_length( 1 ) )
+    logger.debug.assert_called_with( 'Global extra conf not loaded or '
+                                     'no function MissingMethod' )
+
+
+  @patch( 'ycmd.extra_conf_store._logger', autospec = True )
+  def CallGlobalExtraConfMethod_NoExceptionFromMethod_test( self, logger ):
+    extra_conf_file = PathToTestFile( 'extra_conf', 'global_extra_conf.py' )
+    with UserOption( 'global_ycm_extra_conf', extra_conf_file ):
+      extra_conf_store._CallGlobalExtraConfMethod( 'NoException' )
+    assert_that( logger.method_calls, has_length( 1 ) )
+    logger.info.assert_called_with( 'Calling global extra conf method '
+                                    'NoException on conf file '
+                                    '{0}'.format( extra_conf_file ) )
+
+
+  @patch( 'ycmd.extra_conf_store._logger', autospec = True )
+  def CallGlobalExtraConfMethod_CatchExceptionFromMethod_test( self, logger ):
+    extra_conf_file = PathToTestFile( 'extra_conf', 'global_extra_conf.py' )
+    with UserOption( 'global_ycm_extra_conf', extra_conf_file ):
+      extra_conf_store._CallGlobalExtraConfMethod( 'RaiseException' )
+    assert_that( logger.method_calls, has_length( 2 ) )
+    logger.info.assert_called_with( 'Calling global extra conf method '
+                                    'RaiseException on conf file '
+                                    '{0}'.format( extra_conf_file ) )
+    logger.exception.assert_called_with(
+      'Error occurred while calling global extra conf method RaiseException '
+      'on conf file {0}'.format( extra_conf_file ) )
+
+
+  @patch( 'ycmd.extra_conf_store._logger', autospec = True )
+  def CallGlobalExtraConfMethod_CatchExceptionFromExtraConf_test( self,
+                                                                  logger ):
+    extra_conf_file = PathToTestFile( 'extra_conf', 'erroneous_extra_conf.py' )
+    with UserOption( 'global_ycm_extra_conf', extra_conf_file ):
+      extra_conf_store._CallGlobalExtraConfMethod( 'NoException' )
+    assert_that( logger.method_calls, has_length( 1 ) )
+    logger.exception.assert_called_with( 'Error occurred while '
+                                         'loading global extra conf '
+                                         '{0}'.format( extra_conf_file ) )
diff --git a/ycmd/tests/misc_handlers_test.py b/ycmd/tests/misc_handlers_test.py
--- a/ycmd/tests/misc_handlers_test.py
+++ b/ycmd/tests/misc_handlers_test.py
@@ -86,7 +86,7 @@ def MiscHandlers_FilterAndSortCandidates_Basic_test( app ):
 
 @SharedYcmd
 def MiscHandlers_LoadExtraConfFile_AlwaysJsonResponse_test( app ):
-  filepath = PathToTestFile( '.ycm_extra_conf.py' )
+  filepath = PathToTestFile( 'extra_conf', 'project', '.ycm_extra_conf.py' )
   extra_conf_data = BuildRequest( filepath = filepath )
 
   assert_that( app.post_json( '/load_extra_conf_file', extra_conf_data ).json,
@@ -95,7 +95,7 @@ def MiscHandlers_LoadExtraConfFile_AlwaysJsonResponse_test( app ):
 
 @SharedYcmd
 def MiscHandlers_IgnoreExtraConfFile_AlwaysJsonResponse_test( app ):
-  filepath = PathToTestFile( '.ycm_extra_conf.py' )
+  filepath = PathToTestFile( 'extra_conf', 'project', '.ycm_extra_conf.py' )
   extra_conf_data = BuildRequest( filepath = filepath )
 
   assert_that( app.post_json( '/ignore_extra_conf_file', extra_conf_data ).json,
diff --git a/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py b/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py
new file mode 100755
--- /dev/null
+++ b/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py
@@ -0,0 +1 @@
+raise Exception( 'Exception raised' )
diff --git a/ycmd/tests/testdata/extra_conf/global_extra_conf.py b/ycmd/tests/testdata/extra_conf/global_extra_conf.py
new file mode 100755
--- /dev/null
+++ b/ycmd/tests/testdata/extra_conf/global_extra_conf.py
@@ -0,0 +1,6 @@
+def NoException():
+    pass
+
+
+def RaiseException():
+    raise Exception( 'Exception raised' )
diff --git a/ycmd/tests/testdata/.ycm_extra_conf.py b/ycmd/tests/testdata/extra_conf/project/.ycm_extra_conf.py
similarity index 100%
rename from ycmd/tests/testdata/.ycm_extra_conf.py
rename to ycmd/tests/testdata/extra_conf/project/.ycm_extra_conf.py