From 6b771b5f8c264f4998dc50090880f12d3148c465 Mon Sep 17 00:00:00 2001 From: micbou Date: Tue, 26 Apr 2016 23:40:07 +0200 Subject: [PATCH] Add checks to ycm_core library In addition to the outdated library check, add the following checks: - missing library; - compiled with Python 2 but imported with Python 3; - compiled with Python 3 but imported with Python 2. Exit with a non-zero status code if one of these requirements is not met. Add corresponding tests. Remove check_core_version script. --- README.md | 11 +++ check_core_version.py | 44 ----------- ycmd/__main__.py | 8 +- ycmd/handlers.py | 23 ++---- ycmd/server_utils.py | 97 +++++++++++++++++++---- ycmd/tests/check_core_version_test.py | 31 -------- ycmd/tests/server_utils_test.py | 110 +++++++++++++++++++++++++- 7 files changed, 212 insertions(+), 112 deletions(-) delete mode 100755 check_core_version.py delete mode 100644 ycmd/tests/check_core_version_test.py diff --git a/README.md b/README.md index 4bd3379a39..eb20d816d1 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,17 @@ keep-alive background thread that periodically pings ycmd (just call the You can also turn this off by passing `--idle_suicide_seconds=0`, although that isn't recommended. +### Exit codes + +During startup, ycmd attempts to load the `ycm_core` library and exits with one +of the following status codes if unsuccessful: + +- 3: unexpected error while loading the library; +- 4: the `ycm_core` library is missing; +- 5: the `ycm_core` library is compiled for Python 3 but loaded in Python 2; +- 6: the `ycm_core` library is compiled for Python 2 but loaded in Python 3; +- 7: the version of the `ycm_core` library is outdated. + User-level customization ----------------------- diff --git a/check_core_version.py b/check_core_version.py deleted file mode 100755 index 16399f6640..0000000000 --- a/check_core_version.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2015 Google Inc. -# -# 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 . - -import sys -import os -import ycm_core - -VERSION_FILENAME = 'CORE_VERSION' - - -def DirectoryOfThisScript(): - return os.path.dirname( os.path.abspath( __file__ ) ) - - -def ExpectedCoreVersion(): - return int( open( os.path.join( DirectoryOfThisScript(), - VERSION_FILENAME ) ).read() ) - - -def CompatibleWithCurrentCoreVersion(): - try: - current_core_version = ycm_core.YcmCoreVersion() - except AttributeError: - return False - return ExpectedCoreVersion() == current_core_version - - -if not CompatibleWithCurrentCoreVersion(): - sys.exit( 2 ) -sys.exit( 0 ) diff --git a/ycmd/__main__.py b/ycmd/__main__.py index 411e161ce7..e242904212 100755 --- a/ycmd/__main__.py +++ b/ycmd/__main__.py @@ -25,7 +25,7 @@ import os sys.path.insert( 0, os.path.dirname( os.path.abspath( __file__ ) ) ) -from server_utils import SetUpPythonPath, CompatibleWithCurrentCoreVersion +from server_utils import SetUpPythonPath, CompatibleWithCurrentCore SetUpPythonPath() from future import standard_library @@ -157,9 +157,9 @@ def Main(): YcmCoreSanityCheck() extra_conf_store.CallGlobalExtraConfYcmCorePreloadIfExists() - if not CompatibleWithCurrentCoreVersion(): - # ycm_core.[so|dll|dylib] is too old and needs to be recompiled. - sys.exit( 2 ) + code = CompatibleWithCurrentCore() + if code: + sys.exit( code ) PossiblyDetachFromTerminal() diff --git a/ycmd/handlers.py b/ycmd/handlers.py index 425273ece6..98943b5e61 100644 --- a/ycmd/handlers.py +++ b/ycmd/handlers.py @@ -23,30 +23,17 @@ standard_library.install_aliases() from builtins import * # noqa -from os import path - -try: - import ycm_core -except ImportError as e: - raise RuntimeError( - 'Error importing ycm_core. Are you sure you have placed a ' - 'version 3.2+ libclang.[so|dll|dylib] in folder "{0}"? ' - 'See the Installation Guide in the docs. Full error: {1}'.format( - path.realpath( path.join( path.abspath( __file__ ), '..', '..' ) ), - str( e ) ) ) - import atexit -import logging -import json import bottle import http.client +import json +import logging import traceback from bottle import request -from . import server_state -from ycmd import user_options_store + +import ycm_core +from ycmd import extra_conf_store, hmac_plugin, server_state, user_options_store from ycmd.responses import BuildExceptionResponse, BuildCompletionResponse -from ycmd import hmac_plugin -from ycmd import extra_conf_store from ycmd.request_wrap import RequestWrap from ycmd.bottle_utils import SetResponseHeader from ycmd.completers.completer_utils import FilterAndSortCandidatesWrap diff --git a/ycmd/server_utils.py b/ycmd/server_utils.py index ed08782169..b8166d9bcc 100644 --- a/ycmd/server_utils.py +++ b/ycmd/server_utils.py @@ -22,24 +22,49 @@ # No other imports from `future` because this module is loaded before we have # put our submodules in sys.path -import sys -import os import io +import logging +import os import re +import sys + +CORE_MISSING_ERROR_REGEX = re.compile( "No module named '?ycm_core'?" ) +CORE_PYTHON2_ERROR_REGEX = re.compile( + 'dynamic module does not define (?:init|module export) ' + 'function \(PyInit_ycm_core\)|' + 'Module use of python2[0-9].dll conflicts with this version of Python\.$' ) +CORE_PYTHON3_ERROR_REGEX = re.compile( + 'dynamic module does not define init function \(initycm_core\)|' + 'Module use of python3[0-9].dll conflicts with this version of Python\.$' ) + +CORE_MISSING_MESSAGE = ( + 'ycm_core library not detected; you need to compile it by running the ' + 'build.py script. See the documentation for more details.' ) +CORE_PYTHON2_MESSAGE = ( + 'ycm_core library compiled for Python 2 but loaded in Python 3.' ) +CORE_PYTHON3_MESSAGE = ( + 'ycm_core library compiled for Python 3 but loaded in Python 2.' ) +CORE_OUTDATED_MESSAGE = ( + 'ycm_core library too old; PLEASE RECOMPILE by running the build.py script. ' + 'See the documentation for more details.' ) + +# Status codes returned by the CompatibleWithCurrentCore function. Values 1 and +# 2 are not used because 1 is for general errors and 2 has often a special +# meaning for Unix programs. See +# https://docs.python.org/2/library/sys.html#sys.exit +CORE_COMPATIBLE_STATUS = 0 +CORE_UNEXPECTED_STATUS = 3 +CORE_MISSING_STATUS = 4 +CORE_PYTHON2_STATUS = 5 +CORE_PYTHON3_STATUS = 6 +CORE_OUTDATED_STATUS = 7 VERSION_FILENAME = 'CORE_VERSION' -CORE_NOT_COMPATIBLE_MESSAGE = ( - 'ycmd can\'t run: ycm_core lib too old, PLEASE RECOMPILE' -) DIR_OF_CURRENT_SCRIPT = os.path.dirname( os.path.abspath( __file__ ) ) DIR_PACKAGES_REGEX = re.compile( '(site|dist)-packages$' ) - -def SetUpPythonPath(): - sys.path.insert( 0, os.path.join( DIR_OF_CURRENT_SCRIPT, '..' ) ) - - AddNearestThirdPartyFoldersToSysPath( __file__ ) +_logger = logging.getLogger( __name__ ) def ExpectedCoreVersion(): @@ -48,13 +73,57 @@ def ExpectedCoreVersion(): return int( f.read() ) -def CompatibleWithCurrentCoreVersion(): - import ycm_core +def ImportCore(): + """Imports and returns the ycm_core module. This function exists for easily + mocking this import in tests.""" + import ycm_core as ycm_core + return ycm_core + + +def CompatibleWithCurrentCore(): + """Checks if ycm_core library is compatible and returns with one of the + following status codes: + - CORE_COMPATIBLE_STATUS: ycm_core is compatible; + - CORE_UNEXPECTED_STATUS: unexpected error while loading ycm_core; + - CORE_MISSING_STATUS : ycm_core is missing; + - CORE_PYTHON2_STATUS : ycm_core is compiled with Python 2 but loaded with + Python 3; + - CORE_PYTHON3_STATUS : ycm_core is compiled with Python 3 but loaded with + Python 2; + - CORE_OUTDATED_STATUS : ycm_core version is outdated.""" + try: + ycm_core = ImportCore() + except ImportError as error: + message = str( error ) + if CORE_MISSING_ERROR_REGEX.match( message ): + _logger.exception( CORE_MISSING_MESSAGE ) + return CORE_MISSING_STATUS + if CORE_PYTHON2_ERROR_REGEX.match( message ): + _logger.exception( CORE_PYTHON2_MESSAGE ) + return CORE_PYTHON2_STATUS + if CORE_PYTHON3_ERROR_REGEX.match( message ): + _logger.exception( CORE_PYTHON3_MESSAGE ) + return CORE_PYTHON3_STATUS + _logger.exception( message ) + return CORE_UNEXPECTED_STATUS + try: current_core_version = ycm_core.YcmCoreVersion() except AttributeError: - return False - return ExpectedCoreVersion() == current_core_version + _logger.exception( CORE_OUTDATED_MESSAGE ) + return CORE_OUTDATED_STATUS + + if ExpectedCoreVersion() != current_core_version: + _logger.error( CORE_OUTDATED_MESSAGE ) + return CORE_OUTDATED_STATUS + + return CORE_COMPATIBLE_STATUS + + +def SetUpPythonPath(): + sys.path.insert( 0, os.path.join( DIR_OF_CURRENT_SCRIPT, '..' ) ) + + AddNearestThirdPartyFoldersToSysPath( __file__ ) def AncestorFolders( path ): diff --git a/ycmd/tests/check_core_version_test.py b/ycmd/tests/check_core_version_test.py deleted file mode 100644 index 74347241b4..0000000000 --- a/ycmd/tests/check_core_version_test.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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 __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 - -from ..server_utils import CompatibleWithCurrentCoreVersion -from nose.tools import eq_ - - -def CompatibleWithCurrentCoreVersion_test(): - eq_( CompatibleWithCurrentCoreVersion(), True ) diff --git a/ycmd/tests/server_utils_test.py b/ycmd/tests/server_utils_test.py index a24670dca5..c9c0cb1de4 100644 --- a/ycmd/tests/server_utils_test.py +++ b/ycmd/tests/server_utils_test.py @@ -24,13 +24,14 @@ from builtins import * # noqa from hamcrest import ( assert_that, calling, contains, contains_inanyorder, - raises ) + equal_to, empty, raises ) from mock import patch from nose.tools import ok_ import os.path import sys from ycmd.server_utils import ( AddNearestThirdPartyFoldersToSysPath, + CompatibleWithCurrentCore, PathToNearestThirdPartyFolder ) DIR_OF_THIRD_PARTY = os.path.abspath( @@ -50,6 +51,113 @@ ) +@patch( 'ycmd.server_utils._logger' ) +def RunCompatibleWithCurrentCore( test, logger ): + if 'import_error' in test: + with patch( 'ycmd.server_utils.ImportCore', + side_effect = ImportError( test[ 'import_error' ] ) ): + code = CompatibleWithCurrentCore() + else: + code = CompatibleWithCurrentCore() + + assert_that( code, equal_to( test[ 'return_code' ] ) ) + + if 'message' in test: + assert_that( logger.method_calls[ 0 ][ 1 ][ 0 ], + equal_to( test[ 'message' ] ) ) + else: + assert_that( logger.method_calls, empty() ) + + +def CompatibleWithCurrentCore_Compatible_test(): + RunCompatibleWithCurrentCore( { + 'return_code': 0 + } ) + + +def CompatibleWithCurrentCore_Unexpected_test(): + RunCompatibleWithCurrentCore( { + 'import_error': 'unexpected import error', + 'return_code': 3, + 'message': 'unexpected import error' + } ) + + +def CompatibleWithCurrentCore_Missing_test(): + import_errors = [ + # Raised by Python 2. + 'No module named ycm_core', + # Raised by Python 3. + "No module named 'ycm_core'" + ] + + for error in import_errors: + yield RunCompatibleWithCurrentCore, { + 'import_error': error, + 'return_code': 4, + 'message': 'ycm_core library not detected; you need to compile it by ' + 'running the build.py script. See the documentation for more ' + 'details.' + } + + +def CompatibleWithCurrentCore_Python2_test(): + import_errors = [ + # Raised on Linux and OS X with Python 3.3 and 3.4. + 'dynamic module does not define init function (PyInit_ycm_core).', + # Raised on Linux and OS X with Python 3.5. + 'dynamic module does not define module export function (PyInit_ycm_core).', + # Raised on Windows. + 'Module use of python26.dll conflicts with this version of Python.', + 'Module use of python27.dll conflicts with this version of Python.' + ] + + for error in import_errors: + yield RunCompatibleWithCurrentCore, { + 'import_error': error, + 'return_code': 5, + 'message': 'ycm_core library compiled for Python 2 ' + 'but loaded in Python 3.' + } + + +def CompatibleWithCurrentCore_Python3_test(): + import_errors = [ + # Raised on Linux and OS X. + 'dynamic module does not define init function (initycm_core).', + # Raised on Windows. + 'Module use of python34.dll conflicts with this version of Python.', + 'Module use of python35.dll conflicts with this version of Python.' + ] + + for error in import_errors: + yield RunCompatibleWithCurrentCore, { + 'import_error': error, + 'return_code': 6, + 'message': 'ycm_core library compiled for Python 3 ' + 'but loaded in Python 2.' + } + + +@patch( 'ycm_core.YcmCoreVersion', side_effect = AttributeError() ) +def CompatibleWithCurrentCore_Outdated_NoYcmCoreVersionMethod_test( *args ): + RunCompatibleWithCurrentCore( { + 'return_code': 7, + 'message': 'ycm_core library too old; PLEASE RECOMPILE by running the ' + 'build.py script. See the documentation for more details.' + } ) + + +@patch( 'ycm_core.YcmCoreVersion', return_value = 10 ) +@patch( 'ycmd.server_utils.ExpectedCoreVersion', return_value = 11 ) +def CompatibleWithCurrentCore_Outdated_NoVersionMatch_test( *args ): + RunCompatibleWithCurrentCore( { + 'return_code': 7, + 'message': 'ycm_core library too old; PLEASE RECOMPILE by running the ' + 'build.py script. See the documentation for more details.' + } ) + + def PathToNearestThirdPartyFolder_Success_test(): ok_( PathToNearestThirdPartyFolder( os.path.abspath( __file__ ) ) )