diff -Nru ycmd-0+20160327+gitc3e6904/appveyor.yml ycmd-0+20161219+git486b809/appveyor.yml --- ycmd-0+20160327+gitc3e6904/appveyor.yml 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/appveyor.yml 2016-12-20 08:50:19.000000000 +0000 @@ -5,6 +5,7 @@ - auto environment: USE_CLANG_COMPLETER: true + COVERAGE: true matrix: # We only test MSVC 11 and 12 with Python 3.5 on 64 bits. - msvc: 11 @@ -27,6 +28,8 @@ - ci\appveyor\appveyor_install.bat build_script: - python run_tests.py --msvc %MSVC% +after_build: + - codecov # Disable automatic tests test: off cache: diff -Nru ycmd-0+20160327+gitc3e6904/build.py ycmd-0+20161219+git486b809/build.py --- ycmd-0+20160327+gitc3e6904/build.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/build.py 2016-12-20 08:50:19.000000000 +0000 @@ -7,12 +7,18 @@ from __future__ import division from __future__ import absolute_import +from distutils import sysconfig +from shutil import rmtree +from tempfile import mkdtemp +import errno +import multiprocessing import os -import subprocess import os.path as p -import sys +import platform +import re import shlex -import errno +import subprocess +import sys PY_MAJOR, PY_MINOR = sys.version_info[ 0 : 2 ] if not ( ( PY_MAJOR == 2 and PY_MINOR >= 6 ) or @@ -27,17 +33,42 @@ for folder in os.listdir( DIR_OF_THIRD_PARTY ): abs_folder_path = p.join( DIR_OF_THIRD_PARTY, folder ) if p.isdir( abs_folder_path ) and not os.listdir( abs_folder_path ): - sys.exit( 'Some folders in ' + DIR_OF_THIRD_PARTY + ' are empty; ' - 'you probably forgot to run:' - '\n\tgit submodule update --init --recursive\n\n' ) + sys.exit( + 'ERROR: some folders in {0} are empty; you probably forgot to run:\n' + '\tgit submodule update --init --recursive\n'.format( DIR_OF_THIRD_PARTY ) + ) sys.path.insert( 1, p.abspath( p.join( DIR_OF_THIRD_PARTY, 'argparse' ) ) ) -from tempfile import mkdtemp -from shutil import rmtree -import platform import argparse -import multiprocessing + +NO_DYNAMIC_PYTHON_ERROR = ( + 'ERROR: found static Python library ({library}) but a dynamic one is ' + 'required. You must use a Python compiled with the {flag} flag. ' + 'If using pyenv, you need to run the command:\n' + ' export PYTHON_CONFIGURE_OPTS="{flag}"\n' + 'before installing a Python version.' ) +NO_PYTHON_LIBRARY_ERROR = 'ERROR: unable to find an appropriate Python library.' + +# Regular expressions used to find static and dynamic Python libraries. +# Notes: +# - Python 3 library name may have an 'm' suffix on Unix platforms, for +# instance libpython3.3m.so; +# - the linker name (the soname without the version) does not always +# exist so we look for the versioned names too; +# - on Windows, the .lib extension is used instead of the .dll one. See +# http://xenophilia.org/winvunix.html to understand why. +STATIC_PYTHON_LIBRARY_REGEX = '^libpython{major}\.{minor}m?\.a$' +DYNAMIC_PYTHON_LIBRARY_REGEX = """ + ^(?: + # Linux, BSD + libpython{major}\.{minor}m?\.so(\.\d+)*| + # OS X + libpython{major}\.{minor}m?\.dylib| + # Windows + python{major}{minor}\.lib + )$ +""" def OnMac(): @@ -99,103 +130,90 @@ def CheckDeps(): if not PathToFirstExistingExecutable( [ 'cmake' ] ): - sys.exit( 'Please install CMake and retry.') + sys.exit( 'ERROR: please install CMake and retry.') + + +def CheckCall( args, **kwargs ): + exit_message = kwargs.get( 'exit_message', None ) + kwargs.pop( 'exit_message', None ) + try: + subprocess.check_call( args, **kwargs ) + except subprocess.CalledProcessError as error: + if exit_message: + sys.exit( exit_message ) + sys.exit( error.returncode ) + + +def GetPossiblePythonLibraryDirectories(): + library_dir = p.dirname( sysconfig.get_python_lib( standard_lib = True ) ) + if OnWindows(): + return [ p.join( library_dir, 'libs' ) ] + # On pyenv, there is no Python dynamic library in the directory returned by + # the LIBPL variable. Such library is located in the parent folder of the + # standard Python library modules. + return [ sysconfig.get_config_var( 'LIBPL' ), library_dir ] + + +def FindPythonLibraries(): + include_dir = sysconfig.get_python_inc() + library_dirs = GetPossiblePythonLibraryDirectories() + # Since ycmd is compiled as a dynamic library, we can't link it to a Python + # static library. If we try, the following error will occur on Mac: + # + # Fatal Python error: PyThreadState_Get: no current thread + # + # while the error happens during linking on Linux and looks something like: + # + # relocation R_X86_64_32 against `a local symbol' can not be used when + # making a shared object; recompile with -fPIC + # + # On Windows, the Python library is always a dynamic one (an import library to + # be exact). To obtain a dynamic library on other platforms, Python must be + # compiled with the --enable-shared flag on Linux or the --enable-framework + # flag on Mac. + # + # So we proceed like this: + # - look for a dynamic library and return its path; + # - if a static library is found instead, raise an error with instructions + # on how to build Python as a dynamic library. + # - if no libraries are found, raise a generic error. + dynamic_name = re.compile( DYNAMIC_PYTHON_LIBRARY_REGEX.format( + major = PY_MAJOR, minor = PY_MINOR ), re.X ) + static_name = re.compile( STATIC_PYTHON_LIBRARY_REGEX.format( + major = PY_MAJOR, minor = PY_MINOR ), re.X ) + static_libraries = [] + + for library_dir in library_dirs: + # Files are sorted so that we found the non-versioned Python library before + # the versioned one. + for filename in sorted( os.listdir( library_dir ) ): + if dynamic_name.match( filename ): + return p.join( library_dir, filename ), include_dir + + if static_name.match( filename ): + static_libraries.append( p.join( library_dir, filename ) ) + + if static_libraries and not OnWindows(): + dynamic_flag = ( '--enable-framework' if OnMac() else + '--enable-shared' ) + sys.exit( NO_DYNAMIC_PYTHON_ERROR.format( library = static_libraries[ 0 ], + flag = dynamic_flag ) ) -# Shamelessly stolen from https://gist.github.com/edufelipe/1027906 -def CheckOutput( *popen_args, **kwargs ): - """Run command with arguments and return its output as a byte string. - Backported from Python 2.7.""" - - process = subprocess.Popen( stdout=subprocess.PIPE, *popen_args, **kwargs ) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - command = kwargs.get( 'args' ) - if command is None: - command = popen_args[ 0 ] - error = subprocess.CalledProcessError( retcode, command ) - error.output = output - raise error - return output + sys.exit( NO_PYTHON_LIBRARY_ERROR ) def CustomPythonCmakeArgs(): # The CMake 'FindPythonLibs' Module does not work properly. # So we are forced to do its job for it. + print( 'Searching Python {major}.{minor} libraries...'.format( + major = PY_MAJOR, minor = PY_MINOR ) ) - print( 'Searching for python libraries...' ) - - python_prefix = CheckOutput( [ - 'python-config', - '--prefix' - ] ).strip().decode( 'utf8' ) - - if p.isfile( p.join( python_prefix, '/Python' ) ): - python_library = p.join( python_prefix, '/Python' ) - python_include = p.join( python_prefix, '/Headers' ) - print( 'Using OSX-style libs from {0}'.format( python_prefix ) ) - else: - major_minor = CheckOutput( [ - 'python', - '-c', - 'import sys;i=sys.version_info;print( "%d.%d" % (i[0], i[1]) )' - ] ).strip().decode( 'utf8' ) - which_python = 'python' + major_minor - - # Python 3 has an 'm' suffix, for instance libpython3.3m.a - if major_minor.startswith( '3' ): - which_python += 'm' - - lib_python = '{0}/lib/lib{1}'.format( python_prefix, which_python ).strip() - - print( 'Searching for python with prefix: {0} and lib {1}:'.format( - python_prefix, which_python ) ) - - # On MacOS, ycmd does not work with statically linked python library. - # It typically manifests with the following error when there is a - # self-compiled python without --enable-framework (or, technically - # --enable-shared): - # - # Fatal Python error: PyThreadState_Get: no current thread - # - # The most likely explanation for this is that both the ycm_core.so and the - # python binary include copies of libpython.a (or whatever included - # objects). When the python interpreter starts it initializes only the - # globals within its copy, so when ycm_core.so's copy starts executing, it - # points at its own copy which is uninitialized. - # - # Some platforms' dynamic linkers (ld.so) are able to resolve this when - # loading shared libraries at runtime[citation needed], but OSX seemingly - # cannot. - # - # So we do 2 things special on OS X: - # - look for a .dylib first - # - if we find a .a, raise an error. - - if p.isfile( '{0}.dylib'.format( lib_python ) ): - python_library = '{0}.dylib'.format( lib_python ) - elif p.isfile( '/usr/lib/lib{0}.dylib'.format( which_python ) ): - # For no clear reason, python2.6 only exists in /usr/lib on OS X and - # not in the python prefix location - python_library = '/usr/lib/lib{0}.dylib'.format( which_python ) - elif p.isfile( '{0}.a'.format( lib_python ) ): - if OnMac(): - sys.exit( 'ERROR: You must use a python compiled with ' - '--enable-shared or --enable-framework (and thus a {0}.dylib ' - 'library) on OS X'.format( lib_python ) ) - - python_library = '{0}.a'.format( lib_python ) - # This check is for CYGWIN - elif p.isfile( '{0}.dll.a'.format( lib_python ) ): - python_library = '{0}.dll.a'.format( lib_python ) - else: - sys.exit( 'ERROR: Unable to find an appropriate python library' ) + python_library, python_include = FindPythonLibraries() - python_include = '{0}/include/{1}'.format( python_prefix, which_python ) + print( 'Found Python library: {0}'.format( python_library ) ) + print( 'Found Python headers folder: {0}'.format( python_include ) ) - print( 'Using PYTHON_LIBRARY={0} PYTHON_INCLUDE_DIR={1}'.format( - python_library, python_include ) ) return [ '-DPYTHON_LIBRARY={0}'.format( python_library ), '-DPYTHON_INCLUDE_DIR={0}'.format( python_include ) @@ -211,8 +229,7 @@ else: generator = 'Visual Studio 11' - if ( not args.arch and platform.architecture()[ 0 ] == '64bit' - or args.arch == 64 ): + if platform.architecture()[ 0 ] == '64bit': generator = generator + ' Win64' return generator @@ -240,9 +257,6 @@ parser.add_argument( '--msvc', type = int, choices = [ 11, 12, 14 ], default = 14, help = 'Choose the Microsoft Visual ' 'Studio version (default: %(default)s).' ) - parser.add_argument( '--arch', type = int, choices = [ 32, 64 ], - help = 'Force architecture to 32 or 64 bits on ' - 'Windows (default: python interpreter architecture).' ), parser.add_argument( '--tern-completer', action = 'store_true', help = 'Enable tern javascript completer' ), @@ -250,14 +264,31 @@ action = 'store_true', help = 'Enable all supported completers', dest = 'all_completers' ) + parser.add_argument( '--enable-coverage', + action = 'store_true', + help = 'For developers: Enable gcov coverage for the ' + 'c++ module' ) + parser.add_argument( '--enable-debug', + action = 'store_true', + help = 'For developers: build ycm_core library with ' + 'debug symbols' ) + parser.add_argument( '--build-dir', + help = 'For developers: perform the build in the ' + 'specified directory, and do not delete the ' + 'build output. This is useful for incremental ' + 'builds, and required for coverage data' ) args = parser.parse_args() + if args.enable_coverage: + # We always want a debug build when running with coverage enabled + args.enable_debug = True + if ( args.system_libclang and not args.clang_completer and not args.all_completers ): - sys.exit( "You can't pass --system-libclang without also passing " - "--clang-completer or --all as well." ) + sys.exit( 'ERROR: you can\'t pass --system-libclang without also passing ' + '--clang-completer or --all as well.' ) return args @@ -272,6 +303,13 @@ if parsed_args.system_boost: cmake_args.append( '-DUSE_SYSTEM_BOOST=ON' ) + if parsed_args.enable_debug: + cmake_args.append( '-DCMAKE_BUILD_TYPE=Debug' ) + + # coverage is not supported for c++ on MSVC + if not OnWindows() and parsed_args.enable_coverage: + cmake_args.append( '-DCMAKE_CXX_FLAGS=-coverage' ) + use_python2 = 'ON' if PY_MAJOR == 2 else 'OFF' cmake_args.append( '-DUSE_PYTHON2=' + use_python2 ) @@ -294,7 +332,7 @@ else: new_env[ 'LD_LIBRARY_PATH' ] = DIR_OF_THIS_SCRIPT - subprocess.check_call( p.join( tests_dir, 'ycm_core_tests' ), env = new_env ) + CheckCall( p.join( tests_dir, 'ycm_core_tests' ), env = new_env ) # On Windows, if the ycmd library is in use while building it, a LNK1104 @@ -318,54 +356,78 @@ def BuildYcmdLib( args ): - build_dir = mkdtemp( prefix = 'ycm_build.' ) + if args.build_dir: + build_dir = os.path.abspath( args.build_dir ) + + if os.path.exists( build_dir ): + print( 'The supplied build directory ' + build_dir + ' exists, ' + 'deleting it.' ) + rmtree( build_dir, ignore_errors = OnTravisOrAppVeyor() ) + + os.makedirs( build_dir ) + else: + build_dir = mkdtemp( prefix = 'ycm_build_' ) try: full_cmake_args = [ '-G', GetGenerator( args ) ] - if OnMac(): - full_cmake_args.extend( CustomPythonCmakeArgs() ) + full_cmake_args.extend( CustomPythonCmakeArgs() ) full_cmake_args.extend( GetCmakeArgs( args ) ) full_cmake_args.append( p.join( DIR_OF_THIS_SCRIPT, 'cpp' ) ) os.chdir( build_dir ) - subprocess.check_call( [ 'cmake' ] + full_cmake_args ) + + exit_message = ( + 'ERROR: the build failed.\n\n' + 'NOTE: it is *highly* unlikely that this is a bug but rather\n' + 'that this is a problem with the configuration of your system\n' + 'or a missing dependency. Please carefully read CONTRIBUTING.md\n' + 'and if you\'re sure that it is a bug, please raise an issue on the\n' + 'issue tracker, including the entire output of this script\n' + 'and the invocation line used to run it.' ) + + CheckCall( [ 'cmake' ] + full_cmake_args, exit_message = exit_message ) build_target = ( 'ycm_core' if 'YCM_TESTRUN' not in os.environ else 'ycm_core_tests' ) build_command = [ 'cmake', '--build', '.', '--target', build_target ] if OnWindows(): - build_command.extend( [ '--config', 'Release' ] ) + config = 'Debug' if args.enable_debug else 'Release' + build_command.extend( [ '--config', config ] ) else: build_command.extend( [ '--', '-j', str( NumCores() ) ] ) - subprocess.check_call( build_command ) + CheckCall( build_command, exit_message = exit_message ) if 'YCM_TESTRUN' in os.environ: RunYcmdTests( build_dir ) finally: os.chdir( DIR_OF_THIS_SCRIPT ) - rmtree( build_dir, ignore_errors = OnTravisOrAppVeyor() ) + + if args.build_dir: + print( 'The build files are in: ' + build_dir ) + else: + rmtree( build_dir, ignore_errors = OnTravisOrAppVeyor() ) def BuildOmniSharp(): build_command = PathToFirstExistingExecutable( [ 'msbuild', 'msbuild.exe', 'xbuild' ] ) if not build_command: - sys.exit( 'msbuild or xbuild is required to build Omnisharp' ) + sys.exit( 'ERROR: msbuild or xbuild is required to build Omnisharp.' ) os.chdir( p.join( DIR_OF_THIS_SCRIPT, 'third_party', 'OmniSharpServer' ) ) - subprocess.check_call( [ build_command, '/property:Configuration=Release' ] ) + CheckCall( [ build_command, '/property:Configuration=Release' ] ) def BuildGoCode(): if not FindExecutable( 'go' ): - sys.exit( 'go is required to build gocode' ) + sys.exit( 'ERROR: go is required to build gocode.' ) os.chdir( p.join( DIR_OF_THIS_SCRIPT, 'third_party', 'gocode' ) ) - subprocess.check_call( [ 'go', 'build' ] ) + CheckCall( [ 'go', 'build' ] ) os.chdir( p.join( DIR_OF_THIS_SCRIPT, 'third_party', 'godef' ) ) - subprocess.check_call( [ 'go', 'build' ] ) + CheckCall( [ 'go', 'build' ] ) def BuildRacerd(): @@ -373,7 +435,7 @@ Build racerd. This requires a reasonably new version of rustc/cargo. """ if not FindExecutable( 'cargo' ): - sys.exit( 'cargo is required for the rust completer' ) + sys.exit( 'ERROR: cargo is required for the Rust completer.' ) os.chdir( p.join( DIR_OF_THIRD_PARTY, 'racerd' ) ) args = [ 'cargo', 'build' ] @@ -381,7 +443,7 @@ # racerd 2.5x slower and we don't care about the speed of the produced racerd. if not OnTravisOrAppVeyor(): args.append( '--release' ) - subprocess.check_call( args ) + CheckCall( args ) def SetUpTern(): @@ -389,7 +451,7 @@ for exe in [ 'node', 'npm' ]: path = FindExecutable( exe ) if not path: - sys.exit( '"' + exe + '" is required to set up ternjs' ) + sys.exit( 'ERROR: {0} is required to set up ternjs.'.format( exe ) ) else: paths[ exe ] = path @@ -400,7 +462,7 @@ # node_modules of the Tern runtime. We also want to be able to install our # own plugins to improve the user experience for all users. # - # This is not possible if we use a git submodle for Tern and simply run 'npm + # This is not possible if we use a git submodule for Tern and simply run 'npm # install' within the submodule source directory, as subsequent 'npm install # tern-my-plugin' will (heinously) install another (arbitrary) version of Tern # within the Tern source tree (e.g. third_party/tern/node_modules/tern. The @@ -411,20 +473,14 @@ # So instead, we have a package.json within our "Tern runtime" directory # (third_party/tern_runtime) that defines the packages that we require, # including Tern and any plugins which we require as standard. - TERN_RUNTIME_DIR = os.path.join( DIR_OF_THIS_SCRIPT, - 'third_party', - 'tern_runtime' ) - try: - os.makedirs( TERN_RUNTIME_DIR ) - except Exception: - # os.makedirs throws if the dir already exists, it also throws if the - # permissions prevent creating the directory. There's no way to know the - # difference, so we just let the call to os.chdir below throw if this fails - # to create the target directory. - pass + os.chdir( p.join( DIR_OF_THIS_SCRIPT, 'third_party', 'tern_runtime' ) ) + CheckCall( [ paths[ 'npm' ], 'install', '--production' ] ) - os.chdir( TERN_RUNTIME_DIR ) - subprocess.check_call( [ paths[ 'npm' ], 'install', '--production' ] ) + +def WritePythonUsedDuringBuild(): + path = p.join( DIR_OF_THIS_SCRIPT, 'PYTHON_USED_DURING_BUILDING' ) + with open( path, 'w' ) as f: + f.write( sys.executable ) def Main(): @@ -440,6 +496,8 @@ SetUpTern() if args.racer_completer or args.all_completers: BuildRacerd() + WritePythonUsedDuringBuild() + if __name__ == '__main__': Main() diff -Nru ycmd-0+20160327+gitc3e6904/check_core_version.py ycmd-0+20161219+git486b809/check_core_version.py --- ycmd-0+20160327+gitc3e6904/check_core_version.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/check_core_version.py 1970-01-01 00:00:00.000000000 +0000 @@ -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 -Nru ycmd-0+20160327+gitc3e6904/ci/appveyor/appveyor_install.bat ycmd-0+20161219+git486b809/ci/appveyor/appveyor_install.bat --- ycmd-0+20160327+gitc3e6904/ci/appveyor/appveyor_install.bat 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ci/appveyor/appveyor_install.bat 2016-12-20 08:50:19.000000000 +0000 @@ -26,24 +26,13 @@ set PATH=%python_path%;%python_path%\Scripts;%PATH% python --version -:: Manually setting PYTHONHOME for python 2.7.11 fix the following error when -:: running core tests: "ImportError: No module named site" -:: TODO: check if this is still needed when python 2.7.12 is released. -if %python% == 27 ( - set PYTHONHOME=%python_path% -) - -:: When using Python 3 on AppVeyor, CMake will always pick the 64 bits -:: libraries. We specifically tell CMake the right path to the libraries -:: according to the architecture. -if %python% == 35 ( - set EXTRA_CMAKE_ARGS="-DPYTHON_LIBRARY=%python_path%\libs\python%python%.lib" -) appveyor DownloadFile https://bootstrap.pypa.io/get-pip.py python get-pip.py pip install -r test_requirements.txt if %errorlevel% neq 0 exit /b %errorlevel% +pip install codecov +if %errorlevel% neq 0 exit /b %errorlevel% :: :: Typescript configuration @@ -58,8 +47,8 @@ :: Rust configuration :: -appveyor DownloadFile https://static.rust-lang.org/dist/rust-1.6.0-x86_64-pc-windows-msvc.exe -rust-1.6.0-x86_64-pc-windows-msvc.exe /VERYSILENT /NORESTART /DIR="C:\Program Files\Rust" +appveyor DownloadFile https://static.rust-lang.org/dist/rust-1.8.0-x86_64-pc-windows-msvc.exe +rust-1.8.0-x86_64-pc-windows-msvc.exe /VERYSILENT /NORESTART /DIR="C:\Program Files\Rust" set PATH=C:\Program Files\Rust\bin;%PATH% rustc -Vv diff -Nru ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.linux.sh ycmd-0+20161219+git486b809/ci/travis/travis_install.linux.sh --- ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.linux.sh 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ci/travis/travis_install.linux.sh 2016-12-20 08:50:19.000000000 +0000 @@ -1,19 +1,19 @@ # Linux-specific installation # We can't use sudo, so we have to approximate the behaviour of the following: -# $ sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-3.7 100 +# $ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-6 90 mkdir ${HOME}/bin -ln -s /usr/bin/clang++-3.7 ${HOME}/bin/clang++ -ln -s /usr/bin/clang-3.7 ${HOME}/bin/clang - -ln -s /usr/bin/clang++-3.7 ${HOME}/bin/c++ -ln -s /usr/bin/clang-3.7 ${HOME}/bin/cc - -# These shouldn't be necessary, but just in case. -ln -s /usr/bin/clang++-3.7 ${HOME}/bin/g++ -ln -s /usr/bin/clang-3.7 ${HOME}/bin/gcc +ln -s /usr/bin/g++-4.8 ${HOME}/bin/c++ +ln -s /usr/bin/gcc-4.8 ${HOME}/bin/cc +ln -s /usr/bin/gcov-4.8 ${HOME}/bin/gcov export PATH=${HOME}/bin:${PATH} +# In order to work with ycmd, python *must* be built as a shared library. This +# is set via the PYTHON_CONFIGURE_OPTS option. +export PYTHON_CONFIGURE_OPTS="--enable-shared" + +# Pre-installed Node.js is too old. Install latest Node.js v4 LTS. +nvm install 4 diff -Nru ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.osx.sh ycmd-0+20161219+git486b809/ci/travis/travis_install.osx.sh --- ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.osx.sh 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ci/travis/travis_install.osx.sh 2016-12-20 08:50:19.000000000 +0000 @@ -15,13 +15,13 @@ pkg-config openssl" -# Install node, go, ninja, pyenv and dependencies +# Install node, go, ninja, pyenv and dependencies. for pkg in $REQUIREMENTS; do - # Install package, or upgrade it if it is already installed + # Install package, or upgrade it if it is already installed. brew install $pkg || brew outdated $pkg || brew upgrade $pkg done # In order to work with ycmd, python *must* be built as a shared library. The # most compatible way to do this on OS X is with --enable-framework. This is -# set via the PYTHON_CONFIGURE_OPTS option +# set via the PYTHON_CONFIGURE_OPTS option. export PYTHON_CONFIGURE_OPTS="--enable-framework" diff -Nru ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.sh ycmd-0+20161219+git486b809/ci/travis/travis_install.sh --- ycmd-0+20160327+gitc3e6904/ci/travis/travis_install.sh 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ci/travis/travis_install.sh 2016-12-20 08:50:19.000000000 +0000 @@ -1,6 +1,7 @@ #!/bin/bash -set -ev +# Exit immediately if a command returns a non-zero status. +set -e #################### # OS-specific setup @@ -15,23 +16,26 @@ # pyenv setup ############# -# DON'T exit if error -set +e -git clone https://github.com/yyuu/pyenv.git ~/.pyenv +export PYENV_ROOT="${HOME}/.pyenv" + +if [ ! -d "${PYENV_ROOT}/.git" ]; then + git clone https://github.com/yyuu/pyenv.git ${PYENV_ROOT} +fi +pushd ${PYENV_ROOT} git fetch --tags git checkout v20160202 -# Exit if error -set -e +popd -export PYENV_ROOT="$HOME/.pyenv" -export PATH="$PYENV_ROOT/bin:$PATH" +export PATH="${PYENV_ROOT}/bin:${PATH}" eval "$(pyenv init -)" if [ "${YCMD_PYTHON_VERSION}" == "2.6" ]; then PYENV_VERSION="2.6.6" elif [ "${YCMD_PYTHON_VERSION}" == "2.7" ]; then - PYENV_VERSION="2.7.6" + # We need a recent enough version of Python 2.7 on OS X or an error occurs + # when installing the psutil dependency for our tests. + PYENV_VERSION="2.7.8" else PYENV_VERSION="3.3.6" fi @@ -54,13 +58,11 @@ pip install -U pip wheel setuptools pip install -r test_requirements.txt -npm install -g typescript - -# We run coverage tests only on a single build, where COVERAGE=true -if [ x"${COVERAGE}" = x"true" ]; then - pip install coveralls -fi +# Enable coverage for Python subprocesses. See: +# http://coverage.readthedocs.org/en/coverage-4.0.3/subprocess.html +echo -e "import coverage\ncoverage.process_startup()" > \ + ${PYENV_ROOT}/versions/${PYENV_VERSION}/lib/python${YCMD_PYTHON_VERSION}/site-packages/sitecustomize.py ############ # rust setup @@ -78,7 +80,10 @@ multirust update stable multirust default stable -# The build infrastructure prints a lot of spam after this script runs, so make -# sure to disable printing, and failing on non-zero exit code after this script -# finishes -set +ev +############### +# Node.js setup +############### + +npm install -g typescript + +set +e diff -Nru ycmd-0+20160327+gitc3e6904/codecov.yml ycmd-0+20161219+git486b809/codecov.yml --- ycmd-0+20160327+gitc3e6904/codecov.yml 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/codecov.yml 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,27 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: 70...100 + + status: + # Learn more at https://codecov.io/docs#yaml_default_commit_status + project: true + patch: true + changes: true + + # We don't want statistics for the tests themselves and certainly not for the + # boost libraries. Note that while we exclude the gcov data for these patterns + # in the codecov call (codecov --gcov-glob ...), the fact that our code + # references these areas also means we need to tell codecov itself to ignore + # them from the stats. + ignore: + - .*/tests/.* + - .*/BoostParts/.* + +comment: + layout: "header, diff, changes, uncovered" + behavior: default # update if exists else create new diff -Nru ycmd-0+20160327+gitc3e6904/CORE_VERSION ycmd-0+20161219+git486b809/CORE_VERSION --- ycmd-0+20160327+gitc3e6904/CORE_VERSION 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/CORE_VERSION 2016-12-20 08:50:19.000000000 +0000 @@ -1 +1 @@ -20 +25 diff -Nru ycmd-0+20160327+gitc3e6904/.coveragerc ycmd-0+20161219+git486b809/.coveragerc --- ycmd-0+20160327+gitc3e6904/.coveragerc 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/.coveragerc 2016-12-20 08:50:19.000000000 +0000 @@ -1,4 +1,14 @@ [report] omit = + */third_party/requests/* */tests/* */__init__.py + +[run] +parallel = True +source = + ycmd + +[paths] +source = + ycmd/ diff -Nru ycmd-0+20160327+gitc3e6904/cpp/CMakeLists.txt ycmd-0+20161219+git486b809/cpp/CMakeLists.txt --- ycmd-0+20160327+gitc3e6904/cpp/CMakeLists.txt 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/CMakeLists.txt 2016-12-20 08:50:19.000000000 +0000 @@ -15,18 +15,12 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -if ( DEFINED ENV{TRAVIS} ) - # We lie to Travis CI about the min CMake version we need. For what we use - # Travis for, the old version of CMake that it has is good enough. - cmake_minimum_required( VERSION 2.8 ) +if ( APPLE ) + # OSX requires CMake >= 2.8.12, see YCM issue #1439 + cmake_minimum_required( VERSION 2.8.12 ) else() - if ( APPLE ) - # OSX requires CMake >= 2.8.12, see YCM issue #1439 - cmake_minimum_required( VERSION 2.8.12 ) - else() - # CMake 2.8.11 is the latest available version on RHEL/CentOS 7 - cmake_minimum_required( VERSION 2.8.11 ) - endif() + # CMake 2.8.11 is the latest available version on RHEL/CentOS 7 + cmake_minimum_required( VERSION 2.8.11 ) endif() project( YouCompleteMe ) diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/Candidate.cpp ycmd-0+20161219+git486b809/cpp/ycm/Candidate.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/Candidate.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/Candidate.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -18,28 +18,21 @@ #include "standard.h" #include "Candidate.h" #include "Result.h" -#include + #include +#include +#include using boost::algorithm::all; using boost::algorithm::is_lower; +using boost::algorithm::is_print; namespace YouCompleteMe { -namespace { - -LetterNode *FirstUppercaseNode( const std::list< LetterNode *> &list ) { - LetterNode *node = NULL; - foreach( LetterNode * current_node, list ) { - if ( current_node->LetterIsUppercase() ) { - node = current_node; - break; - } - } - return node; +bool IsPrintable( const std::string &text ) { + return all( text, is_print( std::locale::classic() ) ); } -} // unnamed namespace std::string GetWordBoundaryChars( const std::string &text ) { std::string result; @@ -66,8 +59,12 @@ Bitset LetterBitsetFromString( const std::string &text ) { Bitset letter_bitset; + foreach ( char letter, text ) { - letter_bitset.set( IndexForChar( letter ) ); + int letter_index = IndexForLetter( letter ); + + if ( IsInAsciiRange( letter_index ) ) + letter_bitset.set( letter_index ); } return letter_bitset; @@ -90,26 +87,28 @@ int index_sum = 0; foreach ( char letter, query ) { - const std::list< LetterNode *> *list = node->NodeListForLetter( letter ); + const NearestLetterNodeIndices *nearest = + node->NearestLetterNodesForLetter( letter ); - if ( !list ) + if ( !nearest ) return Result( false ); - if ( case_sensitive ) { - // When the query letter is uppercase, then we force an uppercase match - // but when the query letter is lowercase, then it can match both an - // uppercase and a lowercase letter. This is by design and it's much - // better than forcing lowercase letter matches. - node = IsUppercase( letter ) ? - FirstUppercaseNode( *list ) : - list->front(); - - if ( !node ) - return Result( false ); + // When the query letter is uppercase, then we force an uppercase match + // but when the query letter is lowercase, then it can match both an + // uppercase and a lowercase letter. This is by design and it's much + // better than forcing lowercase letter matches. + node = NULL; + if ( case_sensitive && IsUppercase( letter ) ) { + if ( nearest->indexOfFirstUppercaseOccurrence >= 0 ) + node = ( *root_node_ )[ nearest->indexOfFirstUppercaseOccurrence ]; } else { - node = list->front(); + if ( nearest->indexOfFirstOccurrence >= 0 ) + node = ( *root_node_ )[ nearest->indexOfFirstOccurrence ]; } + if ( !node ) + return Result( false ); + index_sum += node->Index(); } diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/Candidate.h ycmd-0+20161219+git486b809/cpp/ycm/Candidate.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/Candidate.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/Candidate.h 2016-12-20 08:50:19.000000000 +0000 @@ -31,6 +31,10 @@ class Result; +// Returns true if text contains only printable characters: ASCII characters in +// the range 32-126. +bool IsPrintable( const std::string &text ); + typedef std::bitset< NUM_LETTERS > Bitset; YCM_DLL_EXPORT Bitset LetterBitsetFromString( const std::string &text ); diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/CandidateRepository.cpp ycmd-0+20161219+git486b809/cpp/ycm/CandidateRepository.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/CandidateRepository.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/CandidateRepository.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -22,7 +22,6 @@ #include #include -#include #ifdef USE_CLANG_COMPLETER # include "ClangCompleter/CompletionData.h" @@ -30,9 +29,6 @@ namespace YouCompleteMe { -using boost::all; -using boost::is_print; - namespace { // We set a reasonable max limit to prevent issues with huge candidate strings @@ -133,7 +129,7 @@ const std::string &CandidateRepository::ValidatedCandidateText( const std::string &candidate_text ) { if ( candidate_text.size() <= MAX_CANDIDATE_SIZE && - all( candidate_text, is_print( std::locale::classic() ) ) ) + IsPrintable( candidate_text ) ) return candidate_text; return empty_; diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/ClangHelpers.cpp ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/ClangHelpers.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/ClangHelpers.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/ClangHelpers.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -22,65 +22,110 @@ #include "UnsavedFile.h" #include "Location.h" #include "Range.h" +#include "PythonSupport.h" #include +#include using boost::unordered_map; namespace YouCompleteMe { namespace { -// NOTE: The passed in pointer should never be NULL! -std::string FullDiagnosticText( CXDiagnostic cxdiagnostic ) { - std::string full_text = CXStringToString( - clang_formatDiagnostic( - cxdiagnostic, +DiagnosticKind DiagnosticSeverityToType( CXDiagnosticSeverity severity ) { + switch ( severity ) { + case CXDiagnostic_Ignored: + case CXDiagnostic_Note: + return INFORMATION; + + case CXDiagnostic_Warning: + return WARNING; + + case CXDiagnostic_Error: + case CXDiagnostic_Fatal: + default: + return ERROR; + } +} + +FixIt BuildFixIt( const std::string& text, + CXDiagnostic diagnostic ) { + FixIt fixit; + + uint num_chunks = clang_getDiagnosticNumFixIts( diagnostic ); + if ( !num_chunks ) + return fixit; + + fixit.chunks.reserve( num_chunks ); + fixit.location = Location( clang_getDiagnosticLocation( diagnostic ) ); + fixit.text = text; + + for ( uint idx = 0; idx < num_chunks; ++idx ) { + FixItChunk chunk; + CXSourceRange sourceRange; + chunk.replacement_text = CXStringToString( + clang_getDiagnosticFixIt( diagnostic, + idx, + &sourceRange ) ); + + chunk.range = sourceRange; + fixit.chunks.push_back( chunk ); + } + + return fixit; +} + +/// This method generates a FixIt object for the supplied diagnostic, and any +/// child diagnostics (recursively), should a FixIt be available and appends +/// them to fixits. +/// Similarly it populates full_diagnostic_text with a concatenation of the +/// diagnostic text for the supplied diagnostic and each child diagnostic +/// (recursively). +/// Warning: This method is re-entrant (recursive). +void BuildFullDiagnosticDataFromChildren( + std::string& full_diagnostic_text, + std::vector< FixIt >& fixits, + CXDiagnostic diagnostic ) { + + std::string diag_text = CXStringToString( clang_formatDiagnostic( + diagnostic, clang_defaultDiagnosticDisplayOptions() ) ); + full_diagnostic_text.append( diag_text ); + + // Populate any fixit attached to this diagnostic. + FixIt fixit = BuildFixIt( diag_text, diagnostic ); + if ( fixit.chunks.size() > 0 ) + fixits.push_back( fixit ); + // Note: clang docs say that a CXDiagnosticSet retrieved with // clang_getChildDiagnostics do NOT need to be released with // clang_diposeDiagnosticSet - CXDiagnosticSet diag_set = clang_getChildDiagnostics( cxdiagnostic ); + CXDiagnosticSet diag_set = clang_getChildDiagnostics( diagnostic ); if ( !diag_set ) - return full_text; + return; uint num_child_diagnostics = clang_getNumDiagnosticsInSet( diag_set ); if ( !num_child_diagnostics ) - return full_text; + return; for ( uint i = 0; i < num_child_diagnostics; ++i ) { - CXDiagnostic diagnostic = clang_getDiagnosticInSet( diag_set, i ); + CXDiagnostic child_diag = clang_getDiagnosticInSet( diag_set, i ); - if ( !diagnostic ) + if( !child_diag ) continue; - full_text.append( "\n" ); - full_text.append( FullDiagnosticText( diagnostic ) ); - } - - return full_text; -} - + full_diagnostic_text.append( "\n" ); -DiagnosticKind DiagnosticSeverityToType( CXDiagnosticSeverity severity ) { - switch ( severity ) { - case CXDiagnostic_Ignored: - case CXDiagnostic_Note: - return INFORMATION; - - case CXDiagnostic_Warning: - return WARNING; - - case CXDiagnostic_Error: - case CXDiagnostic_Fatal: - default: - return ERROR; + // recurse + BuildFullDiagnosticDataFromChildren( full_diagnostic_text, + fixits, + child_diag ); } } - // Returns true when the provided completion string is available to the user; // unavailable completion strings refer to entities that are private/protected, // deprecated etc. @@ -224,27 +269,10 @@ diagnostic.ranges_ = GetRanges( diagnostic_wrap ); diagnostic.text_ = CXStringToString( clang_getDiagnosticSpelling( diagnostic_wrap.get() ) ); - diagnostic.long_formatted_text_ = FullDiagnosticText( diagnostic_wrap.get() ); - - uint num_fixits = clang_getDiagnosticNumFixIts( diagnostic_wrap.get() ); - - // If there are any fixits supplied by libclang, cache them in the diagnostic - // object. - diagnostic.fixits_.reserve( num_fixits ); - - for ( uint fixit_idx = 0; fixit_idx < num_fixits; ++fixit_idx ) { - FixItChunk chunk; - CXSourceRange sourceRange; - - chunk.replacement_text = CXStringToString( - clang_getDiagnosticFixIt( diagnostic_wrap.get(), - fixit_idx, - &sourceRange ) ); - chunk.range = sourceRange; - - diagnostic.fixits_.push_back( chunk ); - } + BuildFullDiagnosticDataFromChildren( diagnostic.long_formatted_text_, + diagnostic.fixits_, + diagnostic_wrap.get() ); return diagnostic; } diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/Diagnostic.h ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/Diagnostic.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/Diagnostic.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/Diagnostic.h 2016-12-20 08:50:19.000000000 +0000 @@ -58,6 +58,11 @@ Location location; + /// This is the text of the diagnostic. This is useful when there are + /// multiple diagnostics offering different fixit options. The text is + /// displayed to the user, allowing them choose which diagnostic to apply. + std::string text; + bool operator==( const FixIt &other ) const { return chunks == other.chunks && location == other.location; @@ -84,8 +89,11 @@ std::string long_formatted_text_; - /// The (cached) changes required to fix this diagnostic - std::vector< FixItChunk > fixits_; + /// The (cached) changes required to fix this diagnostic. + /// Note: when there are child diagnostics, there may be multiple possible + /// FixIts for the main reported diagnostic. These are typically notes, + /// offering alternative ways to fix the error. + std::vector< FixIt > fixits_; }; } // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/TranslationUnit.cpp ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/TranslationUnit.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/ClangCompleter/TranslationUnit.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/ClangCompleter/TranslationUnit.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -36,10 +36,12 @@ namespace { unsigned EditingOptions() { + // See cpp/llvm/include/clang-c/Index.h file for detail on these options. return CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_Incomplete | CXTranslationUnit_IncludeBriefCommentsInCodeCompletion | CXTranslationUnit_CreatePreambleOnFirstParse | + CXTranslationUnit_KeepGoing | clang_defaultEditingTranslationUnitOptions(); } @@ -423,19 +425,12 @@ { unique_lock< mutex > lock( diagnostics_mutex_ ); - for ( std::vector< Diagnostic >::const_iterator it - = latest_diagnostics_.begin(); - it != latest_diagnostics_.end(); - ++it ) { - - // Find all fixits for the supplied line - if ( it->fixits_.size() > 0 && - it->location_.line_number_ == static_cast( line ) ) { - FixIt fixit; - fixit.chunks = it->fixits_; - fixit.location = it->location_; - - fixits.push_back( fixit ); + foreach( const Diagnostic& diagnostic, latest_diagnostics_ ) { + // Find all diagnostics for the supplied line which have FixIts attached + if ( diagnostic.location_.line_number_ == static_cast< uint >( line ) ) { + fixits.insert( fixits.end(), + diagnostic.fixits_.begin(), + diagnostic.fixits_.end() ); } } } diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/CMakeLists.txt ycmd-0+20161219+git486b809/cpp/ycm/CMakeLists.txt --- ycmd-0+20160327+gitc3e6904/cpp/ycm/CMakeLists.txt 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/CMakeLists.txt 2016-12-20 08:50:19.000000000 +0000 @@ -30,39 +30,54 @@ NOT PATH_TO_LLVM_ROOT AND NOT EXTERNAL_LIBCLANG_PATH ) - set( CLANG_VERSION "3.8.0" ) + set( CLANG_VERSION "3.9.0" ) if ( APPLE ) set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin" ) set( CLANG_SHA256 - "e5a961e04b0e1738bbb5b824886a34932dc13b0af699d1fe16519d814d7b776f" ) + "bd689aaeafa19832016d5a4917405a73b21bc5281846c0cb816e9545cf99e82b" ) set( CLANG_FILENAME "${CLANG_DIRNAME}.tar.xz" ) elseif ( WIN32 ) if( 64_BIT_PLATFORM ) set( CLANG_DIRNAME "LLVM-${CLANG_VERSION}-win64" ) set( CLANG_SHA256 - "f9c3147157db32beab025314db9b82c600e182d82994880c1d844c70e29d76ef" ) + "3e5b53a79266d3f7f1d5cb4d94283fe2bc61f9689e55f39e3939364f4076b0c9" ) else() set( CLANG_DIRNAME "LLVM-${CLANG_VERSION}-win32" ) set( CLANG_SHA256 - "ebdb056ebc6efd57c0643733a099920d1d5760a4d570580243e6c63f4b52920f" ) + "b4eaa1fa9872e2c76268f32fc597148cfa14c57b7d13e1edd9b9b6496cdf7de8" ) endif() set( CLANG_FILENAME "${CLANG_DIRNAME}.exe" ) + elseif ( ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD" ) + if ( 64_BIT_PLATFORM ) + set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-amd64-unknown-freebsd10" ) + set( CLANG_SHA256 + "df35eaeabec7a474b3c3737c798a0c817795ebbd12952c665c52b2f98abcb6de" ) + set( CLANG_FILENAME "${CLANG_DIRNAME}.tar.xz" ) + else() + set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-i386-unknown-freebsd10" ) + set( CLANG_SHA256 + "16c612019ece5ba1f846740e77c7c1a39b813acb5bcfbfe9f8af32d7c28caa60" ) + set( CLANG_FILENAME "${CLANG_DIRNAME}.tar.xz" ) + endif() else() + # Among the prebuilt binaries available upstream, only the Ubuntu 14.04 + # and openSUSE 13.2 ones are working on Ubuntu 14.04 (and 12.04 which is + # required for Travis builds). We choose openSUSE over Ubuntu 14.04 because + # it does not have the terminfo library (libtinfo) dependency, which is not + # needed for our use case and a source of problems on some distributions + # (see YCM issues #778 and #2259), and is available for 32-bit and 64-bit + # architectures. if ( 64_BIT_PLATFORM ) - # We MUST support the latest Ubuntu LTS release! - # At the time of writing, that's 14.04. Switch to next LTS ~6 months after - # it comes out. - set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-x86_64-linux-gnu-ubuntu-14.04" ) + set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-x86_64-opensuse13.2" ) set( CLANG_SHA256 - "3120c3055ea78bbbb6848510a2af70c68538b990cb0545bac8dad01df8ff69d7" ) + "9153b473dc37d2e21a260231e90e43b97aba1dff5d27f14c947815a90fbdc8d7" ) set( CLANG_FILENAME "${CLANG_DIRNAME}.tar.xz" ) else() - # Clang 3.3 is the last version with pre-built x86 binaries upstream. - # That's too old now. - message( FATAL_ERROR "No pre-built Clang ${CLANG_VERSION} binaries for 32 bit linux. " - "You'll have to compile Clang ${CLANG_VERSION} from source. " - "See the YCM docs for details on how to use a user-compiled libclang." ) + set( CLANG_DIRNAME "clang+llvm-${CLANG_VERSION}-i586-opensuse13.2" ) + set( CLANG_SHA256 + "92309be874810fb5ca3f9037bb400783be89fa6bc96ed141b04297f59e389692" ) + set( CLANG_FILENAME "${CLANG_DIRNAME}.tar.xz" ) endif() endif() @@ -193,7 +208,13 @@ endif() if ( USE_SYSTEM_BOOST ) - find_package( Boost REQUIRED COMPONENTS python filesystem system regex thread ) + set( Boost_COMPONENTS filesystem system regex thread ) + if( USE_PYTHON2 ) + list( APPEND Boost_COMPONENTS python ) + else() + list( APPEND Boost_COMPONENTS python3 ) + endif() + find_package( Boost REQUIRED COMPONENTS ${Boost_COMPONENTS} ) else() set( Boost_INCLUDE_DIR ${BoostParts_SOURCE_DIR} ) set( Boost_LIBRARIES BoostParts ) @@ -276,10 +297,15 @@ else() # For Macs, we do things differently; look further in this file. if ( NOT APPLE ) - # Setting this to true makes sure that libraries we build will have our rpath - # set even without having to do "make install" + # Setting this to true makes sure that libraries we build will have our + # rpath set even without having to do "make install" set( CMAKE_BUILD_WITH_INSTALL_RPATH TRUE ) set( CMAKE_INSTALL_RPATH "\$ORIGIN" ) + # Add directories from all libraries outside the build tree to the rpath. + # This makes the dynamic linker able to find non system libraries that + # our libraries require, in particular the Python one (from pyenv for + # instance). + set( CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE ) endif() endif() @@ -308,6 +334,12 @@ ${SERVER_SOURCES} ) +if ( USE_CLANG_COMPLETER AND NOT LIBCLANG_TARGET ) + message( FATAL_ERROR "Using Clang completer, but no libclang found. " + "Try setting EXTERNAL_LIBCLANG_PATH or revise " + "your configuration" ) +endif() + target_link_libraries( ${PROJECT_NAME} ${Boost_LIBRARIES} ${PYTHON_LIBRARIES} diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/CustomAssert.cpp ycmd-0+20161219+git486b809/cpp/ycm/CustomAssert.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/CustomAssert.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/CustomAssert.cpp 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -/* -* Copyright (c) 2008, Power of Two Games LLC -* 2013, Google Inc. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions are met: -* * Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* * Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in the -* documentation and/or other materials provided with the distribution. -* * Neither the name of Power of Two Games LLC nor the -* names of its contributors may be used to endorse or promote products -* derived from this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY POWER OF TWO GAMES LLC ``AS IS'' AND ANY -* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL POWER OF TWO GAMES LLC BE LIABLE FOR ANY -* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -#include "CustomAssert.h" -#include -#include - -namespace assert_ns -{ - -namespace -{ - -Assert::FailBehavior DefaultHandler(const char* condition, - const char* msg, - const char* file, - const int line) -{ - std::printf("%s(%d): Assert Failure: ", file, line); - - if (condition != NULL) - std::printf("'%s' ", condition); - - if (msg != NULL) - std::printf("%s", msg); - - std::printf("\n"); - - return Assert::Halt; -} - -Assert::Handler& GetAssertHandlerInstance() -{ - static Assert::Handler s_handler = &DefaultHandler; - return s_handler; -} - -} - -Assert::Handler Assert::GetHandler() -{ - return GetAssertHandlerInstance(); -} - -void Assert::SetHandler(Assert::Handler newHandler) -{ - GetAssertHandlerInstance() = newHandler; -} - -Assert::FailBehavior Assert::ReportFailure(const char* condition, - const char* file, - const int line, - const char* msg, ...) -{ - const char* message = NULL; - if (msg != NULL) - { - char messageBuffer[1024]; - { - va_list args; - va_start(args, msg); - -#if defined(_MSC_VER) - vsnprintf_s(messageBuffer, 1024, 1024, msg, args); -#else - vsnprintf(messageBuffer, 1024, msg, args); -#endif - - va_end(args); - } - - message = messageBuffer; - } - - return GetAssertHandlerInstance()(condition, message, file, line); -} - -} diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/CustomAssert.h ycmd-0+20161219+git486b809/cpp/ycm/CustomAssert.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/CustomAssert.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/CustomAssert.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,112 +0,0 @@ -/* -* Copyright (c) 2008, Power of Two Games LLC -* 2013, Google Inc. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions are met: -* * Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* * Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in the -* documentation and/or other materials provided with the distribution. -* * Neither the name of Power of Two Games LLC nor the -* names of its contributors may be used to endorse or promote products -* derived from this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY POWER OF TWO GAMES LLC ``AS IS'' AND ANY -* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL POWER OF TWO GAMES LLC BE LIABLE FOR ANY -* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -#ifndef CUSTOM_ASSERT_H -#define CUSTOM_ASSERT_H - -namespace assert_ns { namespace Assert -{ - -enum FailBehavior -{ - Halt, - Continue, -}; - -typedef FailBehavior (*Handler)(const char* condition, - const char* msg, - const char* file, - int line); - -Handler GetHandler(); -void SetHandler(Handler newHandler); - -FailBehavior ReportFailure(const char* condition, - const char* file, - int line, - const char* msg, ...); -}} - -#if defined(_MSC_VER) -# define X_HALT() __debugbreak() -#elif defined(__GNUC__) || defined(__clang__) -# define X_HALT() __builtin_trap() -#else -# define X_HALT() exit(__LINE__) -#endif - -#define X_UNUSED(x) do { (void)sizeof(x); } while(0) - -#ifndef NDEBUG - #define X_ASSERT(cond) \ - do \ - { \ - if (!(cond)) \ - { \ - if (assert_ns::Assert::ReportFailure(#cond, __FILE__, __LINE__, 0) == \ - assert_ns::Assert::Halt) \ - X_HALT(); \ - } \ - } while(0) - - #define X_ASSERT_MSG(cond, msg, ...) \ - do \ - { \ - if (!(cond)) \ - { \ - if (assert_ns::Assert::ReportFailure(#cond, __FILE__, __LINE__, (msg), __VA_ARGS__) == \ - assert_ns::Assert::Halt) \ - X_HALT(); \ - } \ - } while(0) - - #define X_ASSERT_FAIL(msg, ...) \ - do \ - { \ - if (assert_ns::Assert::ReportFailure(0, __FILE__, __LINE__, (msg), __VA_ARGS__) == \ - assert_ns::Assert::Halt) \ - X_HALT(); \ - } while(0) - - #define X_VERIFY(cond) X_ASSERT(cond) - #define X_VERIFY_MSG(cond, msg, ...) X_ASSERT_MSG(cond, msg, ##__VA_ARGS__) -#else - #define X_ASSERT(condition) \ - do { X_UNUSED(condition); } while(0) - #define X_ASSERT_MSG(condition, msg, ...) \ - do { X_UNUSED(condition); X_UNUSED(msg); } while(0) - #define X_ASSERT_FAIL(msg, ...) \ - do { X_UNUSED(msg); } while(0) - #define X_VERIFY(cond) (void)(cond) - #define X_VERIFY_MSG(cond, msg, ...) \ - do { (void)(cond); X_UNUSED(msg); } while(0) -#endif - -#define X_STATIC_ASSERT(x) \ - typedef char StaticAssert[(x) ? 1 : -1]; - -#endif // CUSTOM_ASSERT_H diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/IdentifierCompleter.cpp ycmd-0+20161219+git486b809/cpp/ycm/IdentifierCompleter.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/IdentifierCompleter.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/IdentifierCompleter.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -86,6 +86,10 @@ const std::string &query, const std::string &filetype ) const { ReleaseGil unlock; + + if ( !IsPrintable( query ) ) + return std::vector< std::string >(); + std::vector< Result > results; identifier_database_.ResultsForQueryAndType( query, filetype, results ); diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/IdentifierUtils.cpp ycmd-0+20161219+git486b809/cpp/ycm/IdentifierUtils.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/IdentifierUtils.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/IdentifierUtils.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -76,6 +76,7 @@ ( "COBOL" , "cobol" ) ( "DosBatch" , "dosbatch" ) ( "Eiffel" , "eiffel" ) + ( "Elixir" , "elixir" ) ( "Erlang" , "erlang" ) ( "Fortran" , "fortran" ) ( "Go" , "go" ) diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNode.cpp ycmd-0+20161219+git486b809/cpp/ycm/LetterNode.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNode.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/LetterNode.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -22,33 +22,39 @@ namespace YouCompleteMe { LetterNode::LetterNode( char letter, int index ) - : is_uppercase_( IsUppercase( letter ) ), - index_( index ) { + : index_( index ), + is_uppercase_( IsUppercase( letter ) ) { } -// TODO: this class needs tests LetterNode::LetterNode( const std::string &text ) - : is_uppercase_( false ), - index_( -1 ) { - letternode_per_text_index_.resize( text.size() ); + : index_( -1 ), + is_uppercase_( false ) { + + letternode_per_text_index_.reserve( text.size() ); for ( uint i = 0; i < text.size(); ++i ) { - char letter = text[ i ]; - LetterNode *node = new LetterNode( letter, i ); - letters_[ letter ].push_back( node ); - letternode_per_text_index_[ i ] = boost::shared_ptr< LetterNode >( node ); + letternode_per_text_index_.push_back( LetterNode( text[ i ], i ) ); + SetNodeIndexForLetterIfNearest( text[ i ], i ); } - for ( int i = static_cast< int >( letternode_per_text_index_.size() ) - 1; - i >= 0; --i ) { - LetterNode *node_to_add = letternode_per_text_index_[ i ].get(); - - for ( int j = i - 1; j >= 0; --j ) { - letternode_per_text_index_[ j ]->PrependNodeForLetter( text[ i ], - node_to_add ); + for ( size_t i = 0; i < text.size(); ++i ) { + for ( size_t j = i + 1; j < text.size(); ++j ) { + letternode_per_text_index_[ i ].SetNodeIndexForLetterIfNearest( text[ j ], + j ); } } } +void LetterNode::SetNodeIndexForLetterIfNearest( char letter, short index ) { + NearestLetterNodeIndices& currentLetterNodeIndices = letters_[ letter ]; + if ( IsUppercase( letter ) ) { + if ( currentLetterNodeIndices.indexOfFirstUppercaseOccurrence == -1 ) + currentLetterNodeIndices.indexOfFirstUppercaseOccurrence = index; + } + + if ( currentLetterNodeIndices.indexOfFirstOccurrence == -1 ) + currentLetterNodeIndices.indexOfFirstOccurrence = index; +} + } // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNode.h ycmd-0+20161219+git486b809/cpp/ycm/LetterNode.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNode.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/LetterNode.h 2016-12-20 08:50:19.000000000 +0000 @@ -18,10 +18,12 @@ #ifndef LETTERNODE_H_EIZ6JVWC #define LETTERNODE_H_EIZ6JVWC +#include "DLLDefines.h" #include "LetterNodeListMap.h" #include #include +#include #include #include @@ -30,37 +32,37 @@ namespace YouCompleteMe { -class LetterNode : boost::noncopyable { +class LetterNode { public: LetterNode( char letter, int index ); - // this is for root nodes - explicit LetterNode( const std::string &text ); + YCM_DLL_EXPORT explicit LetterNode( const std::string &text ); inline bool LetterIsUppercase() const { return is_uppercase_; } + inline const NearestLetterNodeIndices *NearestLetterNodesForLetter( + char letter ) { - inline const std::list< LetterNode * > *NodeListForLetter( char letter ) { return letters_.ListPointerAt( letter ); } - - inline void PrependNodeForLetter( char letter, LetterNode *node ) { - letters_[ letter ].push_front( node ); - } + void SetNodeIndexForLetterIfNearest( char letter, short index ); inline int Index() const { return index_; } -private: + inline LetterNode *operator[]( int index ) { + return &letternode_per_text_index_[ index ]; + } +private: LetterNodeListMap letters_; - std::vector< boost::shared_ptr< LetterNode > > letternode_per_text_index_; - bool is_uppercase_; + std::vector letternode_per_text_index_; int index_; + bool is_uppercase_; }; } // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNodeListMap.cpp ycmd-0+20161219+git486b809/cpp/ycm/LetterNodeListMap.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNodeListMap.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/LetterNodeListMap.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -26,7 +26,12 @@ } -int IndexForChar( char letter ) { +bool IsInAsciiRange( int index ) { + return 0 <= index && index < NUM_LETTERS; +} + + +int IndexForLetter( char letter ) { if ( IsUppercase( letter ) ) return letter + ( 'a' - 'A' ); @@ -35,45 +40,30 @@ LetterNodeListMap::LetterNodeListMap() { - std::fill( letters_.begin(), - letters_.end(), - static_cast< std::list< LetterNode * >* >( NULL ) ); -} - - -LetterNodeListMap::~LetterNodeListMap() { - for ( uint i = 0; i < letters_.size(); ++i ) { - delete letters_[ i ]; - } } -bool LetterNodeListMap::HasLetter( char letter ) { - int letter_index = IndexForChar( letter ); - std::list< LetterNode * > *list = letters_[ letter_index ]; - return list; +LetterNodeListMap::LetterNodeListMap( const LetterNodeListMap &other ) { + if ( other.letters_ ) + letters_.reset( new NearestLetterNodeArray( *other.letters_ ) ); } -std::list< LetterNode * > &LetterNodeListMap::operator[] ( char letter ) { - int letter_index = IndexForChar( letter ); - std::list< LetterNode * > *list = letters_[ letter_index ]; +NearestLetterNodeIndices &LetterNodeListMap::operator[] ( char letter ) { + if ( !letters_ ) + letters_.reset( new NearestLetterNodeArray() ); - if ( list ) - return *list; + int letter_index = IndexForLetter( letter ); - letters_[ letter_index ] = new std::list< LetterNode * >(); - return *letters_[ letter_index ]; + return letters_->at( letter_index ); } -std::list< LetterNode * > *LetterNodeListMap::ListPointerAt( char letter ) { - return letters_[ IndexForChar( letter ) ]; -} - +NearestLetterNodeIndices *LetterNodeListMap::ListPointerAt( char letter ) { + if ( !letters_ ) + return NULL; -bool LetterNodeListMap::HasLetter( char letter ) const { - return letters_[ IndexForChar( letter ) ] != NULL; + return &letters_->at( IndexForLetter( letter ) ); } } // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNodeListMap.h ycmd-0+20161219+git486b809/cpp/ycm/LetterNodeListMap.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/LetterNodeListMap.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/LetterNodeListMap.h 2016-12-20 08:50:19.000000000 +0000 @@ -20,7 +20,8 @@ #include "DLLDefines.h" -#include +#include +#include #include #include @@ -30,24 +31,51 @@ class LetterNode; -YCM_DLL_EXPORT int IndexForChar( char letter ); YCM_DLL_EXPORT bool IsUppercase( char letter ); +bool IsInAsciiRange( int index ); +YCM_DLL_EXPORT int IndexForLetter( char letter ); -class LetterNodeListMap : boost::noncopyable { -public: - LetterNodeListMap(); - YCM_DLL_EXPORT ~LetterNodeListMap(); +/* + * This struct is used as part of the LetterNodeListMap structure. + * Every LetterNode represents 1 position in a string, and contains + * one LetterNodeListMap. The LetterNodeListMap records the first + * occurrence of all ascii characters after the current LetterNode + * in the original string. For each character, the + * LetterNodeListMap contains one instance of NearestLetterNodeIndices + * + * The struct records the position in the original string of the character + * after the current LetterNode, both the first occurrence overall and the + * first uppercase occurrence. If the letter (or uppercase version) + * doesn't occur, it records -1, indicating it isn't present. + * + * The indices can be used to retrieve the corresponding LetterNode from + * the root LetterNode, as it contains a vector of LetterNodes, one per + * position in the original string. + */ +struct NearestLetterNodeIndices { + NearestLetterNodeIndices() + : indexOfFirstOccurrence( -1 ), + indexOfFirstUppercaseOccurrence( -1 ) + {} - bool HasLetter( char letter ); + short indexOfFirstOccurrence; + short indexOfFirstUppercaseOccurrence; +}; - std::list< LetterNode * > &operator[] ( char letter ); +class LetterNodeListMap { +public: + LetterNodeListMap(); + LetterNodeListMap( const LetterNodeListMap &other ); - std::list< LetterNode * > *ListPointerAt( char letter ); + NearestLetterNodeIndices &operator[] ( char letter ); - bool HasLetter( char letter ) const; + YCM_DLL_EXPORT NearestLetterNodeIndices *ListPointerAt( char letter ); private: - boost::array< std::list< LetterNode * >*, NUM_LETTERS > letters_; + typedef boost::array + NearestLetterNodeArray; + + boost::movelib::unique_ptr< NearestLetterNodeArray > letters_; }; } // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/PythonSupport.cpp ycmd-0+20161219+git486b809/cpp/ycm/PythonSupport.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/PythonSupport.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/PythonSupport.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -68,9 +68,11 @@ const std::string &query ) { pylist filtered_candidates; - if ( query.empty() ) { + if ( query.empty() ) return candidates; - } + + if ( !IsPrintable( query ) ) + return boost::python::list(); int num_candidates = len( candidates ); std::vector< const Candidate * > repository_candidates = diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/standard.h ycmd-0+20161219+git486b809/cpp/ycm/standard.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/standard.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/standard.h 2016-12-20 08:50:19.000000000 +0000 @@ -16,7 +16,6 @@ // along with ycmd. If not, see . #include -#include "CustomAssert.h" // We're most definitely not going to use // it as BOOST_FOREACH. diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IdentifierCompleter_test.cpp ycmd-0+20161219+git486b809/cpp/ycm/tests/IdentifierCompleter_test.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IdentifierCompleter_test.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/tests/IdentifierCompleter_test.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -22,6 +22,7 @@ #include "TestUtils.h" using ::testing::ElementsAre; +using ::testing::IsEmpty; using ::testing::WhenSorted; namespace YouCompleteMe { @@ -33,7 +34,7 @@ EXPECT_THAT( IdentifierCompleter( StringVector( "foobar" ) ).CandidatesForQuery( "" ), - ElementsAre() ); + IsEmpty() ); } TEST( IdentifierCompleterTest, NoDuplicatesReturned ) { @@ -268,6 +269,22 @@ } +TEST( IdentifierCompleterTest, EmptyCandidatesForUnicode ) { + EXPECT_THAT( IdentifierCompleter( + StringVector( + "uni¢𐍈d€" ) ).CandidatesForQuery( "¢" ), + IsEmpty() ); +} + + +TEST( IdentifierCompleterTest, EmptyCandidatesForNonPrintable ) { + EXPECT_THAT( IdentifierCompleter( + StringVector( + "\x01\x1f\x7f" ) ).CandidatesForQuery( "\x1f" ), + IsEmpty() ); +} + + TEST( IdentifierCompleterTest, TagsEndToEndWorks ) { IdentifierCompleter completer; std::vector< std::string > tag_files; diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IndexForChar_test.cpp ycmd-0+20161219+git486b809/cpp/ycm/tests/IndexForChar_test.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IndexForChar_test.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/tests/IndexForChar_test.cpp 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -// Copyright (C) 2011, 2012 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 . - -#include -#include "LetterNodeListMap.h" - -namespace YouCompleteMe { - -TEST( IndexForCharTest, Basic ) { - EXPECT_EQ( static_cast( 'a' ), IndexForChar( 'a' ) ); - EXPECT_EQ( static_cast( 'a' ), IndexForChar( 'A' ) ); - EXPECT_EQ( static_cast( 'z' ), IndexForChar( 'z' ) ); - EXPECT_EQ( static_cast( 'z' ), IndexForChar( 'Z' ) ); - - EXPECT_EQ( static_cast( '[' ), IndexForChar( '[' ) ); - EXPECT_EQ( static_cast( ' ' ), IndexForChar( ' ' ) ); - EXPECT_EQ( static_cast( '~' ), IndexForChar( '~' ) ); -} - -} // namespace YouCompleteMe - diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IndexForLetter_test.cpp ycmd-0+20161219+git486b809/cpp/ycm/tests/IndexForLetter_test.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/IndexForLetter_test.cpp 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/tests/IndexForLetter_test.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,35 @@ +// Copyright (C) 2011, 2012 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 . + +#include +#include "LetterNodeListMap.h" + +namespace YouCompleteMe { + +TEST( IndexForLetterTest, Basic ) { + EXPECT_EQ( static_cast( 'a' ), IndexForLetter( 'a' ) ); + EXPECT_EQ( static_cast( 'a' ), IndexForLetter( 'A' ) ); + EXPECT_EQ( static_cast( 'z' ), IndexForLetter( 'z' ) ); + EXPECT_EQ( static_cast( 'z' ), IndexForLetter( 'Z' ) ); + + EXPECT_EQ( static_cast( '[' ), IndexForLetter( '[' ) ); + EXPECT_EQ( static_cast( ' ' ), IndexForLetter( ' ' ) ); + EXPECT_EQ( static_cast( '~' ), IndexForLetter( '~' ) ); +} + +} // namespace YouCompleteMe + diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/LetterBitsetFromString_test.cpp ycmd-0+20161219+git486b809/cpp/ycm/tests/LetterBitsetFromString_test.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/LetterBitsetFromString_test.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/tests/LetterBitsetFromString_test.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -22,15 +22,46 @@ TEST( LetterBitsetFromStringTest, Basic ) { Bitset expected; - expected.set( IndexForChar( 'a' ) ); - expected.set( IndexForChar( 'o' ) ); - expected.set( IndexForChar( 'c' ) ); - expected.set( IndexForChar( 'f' ) ); - expected.set( IndexForChar( 'b' ) ); + expected.set( IndexForLetter( 'a' ) ); + expected.set( IndexForLetter( 'o' ) ); + expected.set( IndexForLetter( 'c' ) ); + expected.set( IndexForLetter( 'f' ) ); + expected.set( IndexForLetter( 'b' ) ); std::string text = "abcfoof"; EXPECT_EQ( expected, LetterBitsetFromString( text ) ); } -} // namespace YouCompleteMe +TEST( LetterBitsetFromStringTest, Boundaries ) { + Bitset expected; + // While the null character (0) is the lower bound, we cannot check it + // because it is used to terminate a string. + expected.set( IndexForLetter( 1 ) ); + expected.set( IndexForLetter( 127 ) ); + + // \x01 is the start of heading character. + // \x7f (127) is the delete character. + // \x80 (-128) and \xff (-1) are out of ASCII characters range and are + // ignored. + std::string text = "\x01\x7f\x80\xff"; + EXPECT_EQ( expected, LetterBitsetFromString( text ) ); +} + + +TEST( LetterBitsetFromStringTest, IgnoreNonAsciiCharacters ) { + Bitset expected; + expected.set( IndexForLetter( 'u' ) ); + expected.set( IndexForLetter( 'n' ) ); + expected.set( IndexForLetter( 'i' ) ); + expected.set( IndexForLetter( 'd' ) ); + + // UTF-8 characters representation: + // ¢: \xc2\xa2 + // €: \xe2\x82\xac + // 𐍈: \xf0\x90\x8d\x88 + std::string text = "uni¢𐍈d€"; + EXPECT_EQ( expected, LetterBitsetFromString( text ) ); +} + +} // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/LetterNode_test.cpp ycmd-0+20161219+git486b809/cpp/ycm/tests/LetterNode_test.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/tests/LetterNode_test.cpp 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/tests/LetterNode_test.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,83 @@ +// 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 . + +#include +#include +#include "LetterNode.h" + +namespace YouCompleteMe { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::IsNull; +using ::testing::Property; +using ::testing::StrEq; + +TEST( LetterNodeTest, AsciiText ) { + LetterNode root_node( "ascIi_texT" ); + EXPECT_THAT( root_node, + AllOf( Property( &LetterNode::Index, -1 ), + Property( &LetterNode::LetterIsUppercase, false ) ) ); + + const NearestLetterNodeIndices *nearest_nodes = + root_node.NearestLetterNodesForLetter( 'i' ); + + EXPECT_THAT( root_node[ nearest_nodes->indexOfFirstOccurrence ], + AllOf( Property( &LetterNode::Index, 3 ), + Property( &LetterNode::LetterIsUppercase, true ) ) ); + EXPECT_THAT( root_node[ nearest_nodes->indexOfFirstUppercaseOccurrence ], + AllOf( Property( &LetterNode::Index, 3 ), + Property( &LetterNode::LetterIsUppercase, true ) ) ); + + LetterNode *node = root_node[ nearest_nodes->indexOfFirstOccurrence ]; + + nearest_nodes = node->NearestLetterNodesForLetter( 'i' ); + EXPECT_THAT( root_node[ nearest_nodes->indexOfFirstOccurrence ], + AllOf( Property( &LetterNode::Index, 4 ), + Property( &LetterNode::LetterIsUppercase, false ) ) ); + EXPECT_EQ( nearest_nodes->indexOfFirstUppercaseOccurrence, -1 ); + + + nearest_nodes = node->NearestLetterNodesForLetter( 't' ); + EXPECT_THAT( root_node[ nearest_nodes->indexOfFirstOccurrence ], + AllOf( Property( &LetterNode::Index, 6 ), + Property( &LetterNode::LetterIsUppercase, false ) ) ); + EXPECT_THAT( root_node[ nearest_nodes->indexOfFirstUppercaseOccurrence ], + AllOf( Property( &LetterNode::Index, 9 ), + Property( &LetterNode::LetterIsUppercase, true ) ) ); + + nearest_nodes = node->NearestLetterNodesForLetter( 'c' ); + EXPECT_EQ( nearest_nodes->indexOfFirstOccurrence, -1 ); + EXPECT_EQ( nearest_nodes->indexOfFirstUppercaseOccurrence, -1 ); +} + + +TEST( LetterNodeTest, ThrowOnNonAsciiCharacters ) { + // UTF-8 characters representation: + // ¢: \xc2\xa2 + // €: \xe2\x82\xac + // 𐍈: \xf0\x90\x8d\x88 + ASSERT_THROW( LetterNode root_node( "uni¢𐍈d€" ), std::out_of_range ); + + try { + LetterNode root_node( "uni¢𐍈d€" ); + } catch ( std::out_of_range error ) { + EXPECT_THAT( error.what(), StrEq( "array<>: index out of range" ) ); + } +} + +} // namespace YouCompleteMe diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/Utils.h ycmd-0+20161219+git486b809/cpp/ycm/Utils.h --- ycmd-0+20160327+gitc3e6904/cpp/ycm/Utils.h 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/Utils.h 2016-12-20 08:50:19.000000000 +0000 @@ -23,6 +23,7 @@ #include #include #include + namespace fs = boost::filesystem; namespace YouCompleteMe { diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/ycm_core.cpp ycmd-0+20161219+git486b809/cpp/ycm/ycm_core.cpp --- ycmd-0+20160327+gitc3e6904/cpp/ycm/ycm_core.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/ycm_core.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -159,7 +159,8 @@ class_< FixIt >( "FixIt" ) .def_readonly( "chunks", &FixIt::chunks ) - .def_readonly( "location", &FixIt::location ); + .def_readonly( "location", &FixIt::location ) + .def_readonly( "text", &FixIt::text ); class_< std::vector< FixIt > >( "FixItVector" ) .def( vector_indexing_suite< std::vector< FixIt > >() ); diff -Nru ycmd-0+20160327+gitc3e6904/cpp/ycm/.ycm_extra_conf.py ycmd-0+20161219+git486b809/cpp/ycm/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/cpp/ycm/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/cpp/ycm/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -182,7 +182,4 @@ relative_to = DirectoryOfThisScript() final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) - return { - 'flags': final_flags, - 'do_cache': True - } + return { 'flags': final_flags } diff -Nru ycmd-0+20160327+gitc3e6904/debian/changelog ycmd-0+20161219+git486b809/debian/changelog --- ycmd-0+20160327+gitc3e6904/debian/changelog 2016-11-01 16:56:22.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/changelog 2017-01-26 09:57:23.000000000 +0000 @@ -1,20 +1,34 @@ -ycmd (0+20160327+gitc3e6904-1build3) zesty; urgency=high +ycmd (0+20161219+git486b809-1) unstable; urgency=low - * No change rebuild against boost1.62. + * New upstream release. (Closes: #850144) + * Bump Standards-Version to 3.9.8 (no changes). + * Remove third party dependency frozendict from package. + * Refresh patches for new upstream release. + * Use updated path for tsserver. (Closes: #841563) + * Replace google-mock dependency with google-test and use correct + directory for gmock target. (Closes: #848727) + * Bumb libclang dependency version to 3.9. + * Update 01-python-patch.patch to: + - Fix a test function in JediHTTP to use upper case letter. + - Remove follow_imports parameter usage from jedi.Script.goto_assignments. + This parameter is only available in the latest revision of jedi and not + available in Debian. + * Remove third_party/frozendict from package + * Add python-frozendict and python-psutil to Build-Depends. + * Add 08-bottle.patch to make ycmd compatible with python-bottle. + * Improve override_dh_auto_clean. + * Remove installation of check_core_version.py. + * Remove gmock and gtest files from package. + * Replace clang includes symbolic link with correct include directory. + (Closes: #825905) + * Remove fix permissions from override_dh_install and only make + /usr/lib/ycmd/ycmd/__main__.py executable. + * Enable hardening. + * Remove unused files from debian/copyright. + * Remove architecture detection for go tests (go tests are excluded + completely), and reorganize EXCLUDE_TEST_PARAMS in debian/rules. - -- Dimitri John Ledkov Tue, 01 Nov 2016 16:56:22 +0000 - -ycmd (0+20160327+gitc3e6904-1build2) yakkety; urgency=medium - - * No-change rebuild for boost soname change. - - -- Matthias Klose Thu, 04 Aug 2016 08:37:22 +0000 - -ycmd (0+20160327+gitc3e6904-1build1) yakkety; urgency=medium - - * No-change rebuild for boost soname change. - - -- Matthias Klose Sat, 23 Apr 2016 18:54:07 +0000 + -- Onur Aslan Thu, 26 Jan 2017 12:57:23 +0300 ycmd (0+20160327+gitc3e6904-1) unstable; urgency=low diff -Nru ycmd-0+20160327+gitc3e6904/debian/control ycmd-0+20161219+git486b809/debian/control --- ycmd-0+20160327+gitc3e6904/debian/control 2016-11-01 16:56:22.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/control 2017-01-09 20:44:04.000000000 +0000 @@ -1,8 +1,7 @@ Source: ycmd Section: devel Priority: optional -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Onur Aslan +Maintainer: Onur Aslan Build-Depends: debhelper (>= 9), cmake, dh-python, @@ -10,11 +9,13 @@ python-bottle, python-dev, python-future, + python-frozendict, python-hamcrest, python-jedi, python-mock, python-nose, python-pep8, + python-psutil, python-requests, python-setuptools, python-waitress, @@ -26,8 +27,8 @@ python3-waitress, python3-bottle, python3-requests, - google-mock, - libclang-3.8-dev, + googletest, + libclang-3.9-dev, libboost-python-dev, libboost-system-dev, libboost-filesystem-dev, @@ -35,7 +36,7 @@ libgtest-dev, node-typescript X-Python-Version: >= 2.7 -Standards-Version: 3.9.7 +Standards-Version: 3.9.8 Homepage: https://github.com/Valloric/ycmd Vcs-Git: https://anonscm.debian.org/git/collab-maint/ycmd.git Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/ycmd.git @@ -47,6 +48,7 @@ ${python:Depends}, python-bottle, python-future, + python-frozendict, python-jedi, python-requests (>= 2.2.1), python-waitress diff -Nru ycmd-0+20160327+gitc3e6904/debian/copyright ycmd-0+20161219+git486b809/debian/copyright --- ycmd-0+20160327+gitc3e6904/debian/copyright 2016-03-30 09:41:11.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/copyright 2017-01-03 17:57:25.000000000 +0000 @@ -34,15 +34,6 @@ its respective authors unless otherwise stated. License: Unlicense -Files: cpp/ycm/CustomAssert.cpp cpp/ycm/CustomAssert.h -Copyright: 2008, Power of Two Games LLC - 2010, Google Inc. -License: BSD-3-clause - -Files: third_party/frozendict/* -Copyright: 2012, Santiago Lezica -License: Expat - Files: third_party/JediHTTP/* Copyright: 2016, Andrea Cedraro License: Apache-2.0 @@ -94,50 +85,5 @@ . For more information, please refer to -License: BSD-3-clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - . - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - . - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - . - * Neither the name of Power of Two Games LLC nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY POWER OF TWO GAMES LLC ``AS IS'' AND ANY - EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL POWER OF TWO GAMES LLC BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -License: Expat - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - License: Apache-2.0 See /usr/share/common-licenses/Apache-2.0. diff -Nru ycmd-0+20160327+gitc3e6904/debian/install ycmd-0+20161219+git486b809/debian/install --- ycmd-0+20160327+gitc3e6904/debian/install 2016-03-31 16:16:26.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/install 2017-01-03 17:57:25.000000000 +0000 @@ -1,8 +1,6 @@ ycmd/* usr/lib/ycmd/ycmd/ CORE_VERSION usr/lib/ycmd/ -check_core_version.py usr/lib/ycmd/ cpp/ycm/.ycm_extra_conf.py usr/lib/ycmd/ ycm_*.so usr/lib/ycmd/ -third_party/frozendict/frozendict usr/lib/ycmd/ third_party/JediHTTP/jedihttp usr/lib/ycmd/third_party/JediHTTP third_party/JediHTTP/jedihttp.py usr/lib/ycmd/third_party/JediHTTP diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/00-build-system.patch ycmd-0+20161219+git486b809/debian/patches/00-build-system.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/00-build-system.patch 2016-04-09 07:53:07.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/00-build-system.patch 2017-01-03 17:57:25.000000000 +0000 @@ -1,59 +1,36 @@ Description: Use Debian's llvm and gmock paths. Also removes unused librt and libboost-thread links. ---- a/cpp/ycm/tests/CMakeLists.txt -+++ b/cpp/ycm/tests/CMakeLists.txt -@@ -30,8 +30,7 @@ - - if ( USE_SYSTEM_GMOCK ) - set( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}" ) -- find_package( GTest REQUIRED ) -- find_package( GMock REQUIRED ) -+ add_subdirectory( /usr/src/gmock gmock ) - else() - if ( WIN32 ) - # Override BUILD_SHARED_LIBS option in gmock and gtest CMakeLists -@@ -108,8 +107,7 @@ - target_link_libraries( ${PROJECT_NAME} - ${Boost_LIBRARIES} - ycm_core -- ${GTEST_LIBRARIES} -- ${GMOCK_LIBRARIES} ) -+ gmock ) - - if ( NOT CMAKE_GENERATOR_IS_XCODE ) - # The test executable expects a "testdata" dir in its working directory. Why? --- a/cpp/ycm/CMakeLists.txt +++ b/cpp/ycm/CMakeLists.txt -@@ -164,7 +164,7 @@ +@@ -179,7 +179,7 @@ if ( PATH_TO_LLVM_ROOT ) set( CLANG_INCLUDES_DIR "${PATH_TO_LLVM_ROOT}/include" ) else() - set( CLANG_INCLUDES_DIR "${CMAKE_SOURCE_DIR}/llvm/include" ) -+ file (GLOB CLANG_INCLUDES_DIR SYS_LLVM_INCLUDE_PATHS /usr/lib/llvm-3.8/include) ++ file (GLOB CLANG_INCLUDES_DIR SYS_LLVM_INCLUDE_PATHS /usr/lib/llvm-3.9/include) endif() if ( NOT IS_ABSOLUTE "${CLANG_INCLUDES_DIR}" ) -@@ -193,7 +193,7 @@ +@@ -208,7 +208,7 @@ endif() if ( USE_SYSTEM_BOOST ) -- find_package( Boost REQUIRED COMPONENTS python filesystem system regex thread ) -+ find_package( Boost REQUIRED COMPONENTS python filesystem system regex ) - else() - set( Boost_INCLUDE_DIR ${BoostParts_SOURCE_DIR} ) - set( Boost_LIBRARIES BoostParts ) -@@ -263,7 +263,8 @@ +- set( Boost_COMPONENTS filesystem system regex thread ) ++ set( Boost_COMPONENTS filesystem system regex ) + if( USE_PYTHON2 ) + list( APPEND Boost_COMPONENTS python ) + else() +@@ -284,7 +284,7 @@ # On Debian-based systems only a symlink to libclang.so.1 is created find_library( TEMP NAMES - clang -+ clang-3.8 + clang-3.9 libclang.so.1 PATHS ${ENV_LIB_PATHS} -@@ -297,10 +298,6 @@ +@@ -323,10 +323,6 @@ set( CMAKE_INSTALL_RPATH "${EXTRA_RPATH}:${CMAKE_INSTALL_RPATH}" ) endif() @@ -64,3 +41,25 @@ ############################################################################# +--- a/cpp/ycm/tests/CMakeLists.txt ++++ b/cpp/ycm/tests/CMakeLists.txt +@@ -30,8 +30,7 @@ + + if ( USE_SYSTEM_GMOCK ) + set( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}" ) +- find_package( GTest REQUIRED ) +- find_package( GMock REQUIRED ) ++ add_subdirectory( /usr/src/googletest/googlemock gmock ) + else() + if ( WIN32 ) + # Override BUILD_SHARED_LIBS option in gmock and gtest CMakeLists +@@ -108,8 +107,7 @@ + target_link_libraries( ${PROJECT_NAME} + ${Boost_LIBRARIES} + ycm_core +- ${GTEST_LIBRARIES} +- ${GMOCK_LIBRARIES} ) ++ gmock ) + + if ( NOT CMAKE_GENERATOR_IS_XCODE ) + # The test executable expects a "testdata" dir in its working directory. Why? diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/01-python-path.patch ycmd-0+20161219+git486b809/debian/patches/01-python-path.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/01-python-path.patch 2016-03-30 09:41:11.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/01-python-path.patch 2017-01-03 17:57:25.000000000 +0000 @@ -1,6 +1,13 @@ -Description: Script is trying to add third_party directory to sys.path. - This package doesn't contain third_party modules, all third_party modules - available as Debian packages and this package already have them in Depends. +Description: This patch is fixing some issue in JediHTTP + * JediHTTP is trying to add third_party directory to sys.path. + This package doesn't contain third_party modules, all third_party modules + available as Debian packages and this package already have them in Depends. + * JediHTTP is also not compatible with jedi 0.9.0 and + requires a more recent revision of jedi which is not available in Debian. + * One of the test is checking stdout from JediHTTP process but it is + sometimes returning an uppercase letter. This patch is fixing this test + case. + --- a/third_party/JediHTTP/jedihttp/utils.py +++ b/third_party/JediHTTP/jedihttp/utils.py @@ -17,3 +24,25 @@ - folder ) ) ) + # System packages used in Debian + pass +--- a/third_party/JediHTTP/jedihttp/handlers.py ++++ b/third_party/JediHTTP/jedihttp/handlers.py +@@ -88,7 +88,7 @@ + follow_imports = ( 'follow_imports' in request_json and + request_json[ 'follow_imports' ] ) + script = _GetJediScript( request_json ) +- response = _FormatDefinitions( script.goto_assignments( follow_imports ) ) ++ response = _FormatDefinitions( script.goto_assignments( ) ) + return _JsonResponse( response ) + + +--- a/third_party/JediHTTP/jedihttp/tests/end_to_end_test.py ++++ b/third_party/JediHTTP/jedihttp/tests/end_to_end_test.py +@@ -45,7 +45,7 @@ + + def wait_for_jedihttp_to_start( jedihttp ): + line = jedihttp.stdout.readline().decode( 'utf8' ) +- good_start = line.startswith( 'serving on' ) ++ good_start = line.lower().startswith( 'serving on' ) + reason = jedihttp.stdout.read().decode( 'utf8' ) if not good_start else '' + return good_start, reason + diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/03-gocode-path.patch ycmd-0+20161219+git486b809/debian/patches/03-gocode-path.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/03-gocode-path.patch 2016-03-30 09:41:11.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/03-gocode-path.patch 2017-01-03 17:57:25.000000000 +0000 @@ -5,7 +5,7 @@ --- a/ycmd/completers/go/go_completer.py +++ b/ycmd/completers/go/go_completer.py -@@ -30,7 +30,7 @@ +@@ -31,7 +31,7 @@ from ycmd import responses from ycmd import utils @@ -13,10 +13,10 @@ +from ycmd.utils import ToBytes, ToUnicode from ycmd.completers.completer import Completer - BINARY_NOT_FOUND_MESSAGE = ( '{0} binary not found. Did you build it? ' + -@@ -45,12 +45,8 @@ - os.path.abspath( os.path.dirname( __file__ ) ), - '..', '..', '..', 'third_party' ) + BINARY_NOT_FOUND_MESSAGE = ( '{0} binary not found. Did you build it? ' +@@ -51,12 +51,8 @@ + DIR_OF_THIRD_PARTY = os.path.abspath( + os.path.join( os.path.dirname( __file__ ), '..', '..', '..', 'third_party' ) ) GO_BINARIES = dict( { - 'gocode': os.path.join( DIR_OF_THIRD_PARTY, - 'gocode', @@ -28,4 +28,4 @@ + 'godef': '/usr/bin/godef' } ) - _logger = logging.getLogger( __name__ ) + LOGFILE_FORMAT = 'gocode_{port}_{std}_' diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/04-tsserver-path.patch ycmd-0+20161219+git486b809/debian/patches/04-tsserver-path.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/04-tsserver-path.patch 2016-04-09 07:53:07.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/04-tsserver-path.patch 2017-01-03 17:57:25.000000000 +0000 @@ -2,23 +2,21 @@ --- a/ycmd/completers/typescript/typescript_completer.py +++ b/ycmd/completers/typescript/typescript_completer.py -@@ -94,7 +94,9 @@ - # the tsserver process' stdin - self._writelock = Lock() +@@ -46,7 +46,7 @@ + MAX_DETAILED_COMPLETIONS = 100 + RESPONSE_TIMEOUT_SECONDS = 10 -- binarypath = utils.PathToFirstExistingExecutable( [ 'tsserver' ] ) -+ binarypath = utils.PathToFirstExistingExecutable( -+ [ '/usr/lib/nodejs/typescript/tsserver.js' ] -+ ) - if not binarypath: - _logger.error( BINARY_NOT_FOUND_MESSAGE ) - raise RuntimeError( BINARY_NOT_FOUND_MESSAGE ) -@@ -122,7 +124,7 @@ - # https://github.com/Microsoft/TypeScript/issues/3403 - # TODO: remove this option when the issue is fixed. - # We also need to redirect the error stream to the output one on Windows. -- self._tsserver_handle = utils.SafePopen( binarypath, -+ self._tsserver_handle = utils.SafePopen( [ 'nodejs', binarypath ], - stdout = subprocess.PIPE, - stdin = subprocess.PIPE, - stderr = subprocess.STDOUT, +-PATH_TO_TSSERVER = utils.FindExecutable( 'tsserver' ) ++PATH_TO_TSSERVER = "/usr/lib/nodejs/typescript/lib/tsserver.js" + + LOGFILE_FORMAT = 'tsserver_' + +@@ -160,7 +160,7 @@ + _logger.info( 'TSServer log file: {0}'.format( self._logfile ) ) + + # We need to redirect the error stream to the output one on Windows. +- self._tsserver_handle = utils.SafePopen( PATH_TO_TSSERVER, ++ self._tsserver_handle = utils.SafePopen( [ "nodejs", PATH_TO_TSSERVER ], + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/05-tern-support.patch ycmd-0+20161219+git486b809/debian/patches/05-tern-support.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/05-tern-support.patch 2016-04-09 07:53:07.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/05-tern-support.patch 2017-01-03 17:57:25.000000000 +0000 @@ -3,23 +3,29 @@ --- a/ycmd/completers/javascript/tern_completer.py +++ b/ycmd/completers/javascript/tern_completer.py -@@ -50,7 +50,7 @@ - 'bin', - 'tern' ) ) +@@ -36,20 +36,12 @@ + + _logger = logging.getLogger( __name__ ) + +-PATH_TO_TERN_BINARY = os.path.abspath( +- os.path.join( +- os.path.dirname( __file__ ), +- '..', +- '..', +- '..', +- 'third_party', +- 'tern_runtime', +- 'node_modules', +- 'tern', +- 'bin', +- 'tern' ) ) ++PATH_TO_TERN_BINARY = os.path.join( ++ os.path.expanduser('~'), ++ 'node_modules', ++ 'tern') -PATH_TO_NODE = utils.PathToFirstExistingExecutable( [ 'node' ] ) +PATH_TO_NODE = utils.PathToFirstExistingExecutable( [ 'nodejs' ] ) # host name/address on which the tern server should listen # note: we use 127.0.0.1 rather than localhost because on some platforms -@@ -71,7 +71,9 @@ - - _logger.info( 'Using node binary from: ' + PATH_TO_NODE ) - -- installed = os.path.exists( PATH_TO_TERNJS_BINARY ) -+ installed = os.path.join(os.path.expanduser('~'), -+ 'node_modules', -+ 'tern') - - if not installed: - _logger.info( 'Not using Tern completer: not installed at ' + diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/06-omnisharp-path.patch ycmd-0+20161219+git486b809/debian/patches/06-omnisharp-path.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/06-omnisharp-path.patch 2016-04-09 07:53:07.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/06-omnisharp-path.patch 2017-01-03 17:57:25.000000000 +0000 @@ -10,33 +10,11 @@ '"./install.py --omnisharp-completer".' ) INVALID_FILE_MESSAGE = 'File is invalid.' NO_DIAGNOSTIC_MESSAGE = 'No diagnostic for current line!' --PATH_TO_OMNISHARP_BINARY = os.path.join( -- os.path.abspath( os.path.dirname( __file__ ) ), -- '..', '..', '..', 'third_party', 'OmniSharpServer', -- 'OmniSharp', 'bin', 'Release', 'OmniSharp.exe' ) +-PATH_TO_OMNISHARP_BINARY = os.path.abspath( +- os.path.join( os.path.dirname( __file__ ), '..', '..', '..', +- 'third_party', 'OmniSharpServer', 'OmniSharp', +- 'bin', 'Release', 'OmniSharp.exe' ) ) +PATH_TO_OMNISHARP_BINARY = '/usr/local/bin/OmniSharp.exe' + LOGFILE_FORMAT = 'omnisharp_{port}_{sln}_{std}_' - class CsharpCompleter( Completer ): ---- a/ycmd/tests/cs/__init__.py -+++ b/ycmd/tests/cs/__init__.py -@@ -62,7 +62,7 @@ - - # If running on Travis CI, keep trying forever. Travis will kill the worker - # after 10 mins if nothing happens. -- while retries > 0 or OnTravis(): -+ while retries > 0: - result = app.get( '/ready', { 'subserver': 'cs' } ).json - if result: - success = True ---- a/ycmd/tests/cs/get_completions_test.py -+++ b/ycmd/tests/cs/get_completions_test.py -@@ -388,8 +388,6 @@ - # The test passes if we caught an exception when trying to start it, - # so raise one if it managed to start - if not exception_caught: -- WaitUntilOmniSharpServerReady( app, filepath ) -- StopOmniSharpServer( app, filepath ) - raise Exception( 'The Omnisharp server started, despite us not being ' - 'able to find a suitable solution file to feed it. Did ' - 'you fiddle with the solution finding code in ' diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/07-shebang.patch ycmd-0+20161219+git486b809/debian/patches/07-shebang.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/07-shebang.patch 2016-03-31 16:16:26.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/07-shebang.patch 2017-01-03 17:57:25.000000000 +0000 @@ -1,12 +1,5 @@ Description: Adds missing script headers ---- a/check_core_version.py -+++ b/check_core_version.py -@@ -1,3 +1,4 @@ -+#!/usr/bin/env python2 - # Copyright (C) 2015 Google Inc. - # - # This file is part of ycmd. --- a/ycmd/__main__.py +++ b/ycmd/__main__.py @@ -1,3 +1,4 @@ diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/08-bottle.patch ycmd-0+20161219+git486b809/debian/patches/08-bottle.patch --- ycmd-0+20160327+gitc3e6904/debian/patches/08-bottle.patch 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/08-bottle.patch 2017-01-03 17:57:25.000000000 +0000 @@ -0,0 +1,26 @@ +Description: Makes ycmd compatible with latest bottle + ycmd comes with it's own bottle module but this module is not included + in Debian package. This patch is making ycmd to compatible with latest + bottle. +Forwarded: https://github.com/Valloric/ycmd/pull/673 + +--- a/ycmd/bottle_utils.py ++++ b/ycmd/bottle_utils.py +@@ -24,7 +24,7 @@ + from builtins import * # noqa + + from future.utils import PY2 +-from ycmd.utils import ToBytes, ToUnicode ++from ycmd.utils import ToUnicode + import bottle + + +@@ -39,5 +39,6 @@ + # making life easier for codebases that work across versions, thus preventing + # tracebacks in the depths of WSGI server frameworks. + def SetResponseHeader( name, value ): +- name = ToBytes( name ) if PY2 else ToUnicode( name ) +- bottle.response.set_header( name, ToUnicode( value ) ) ++ name = name.encode( 'utf8' ) if PY2 else ToUnicode( name ) ++ value = value.encode( 'utf8' ) if PY2 else ToUnicode( value ) ++ bottle.response.set_header( name, value ) diff -Nru ycmd-0+20160327+gitc3e6904/debian/patches/series ycmd-0+20161219+git486b809/debian/patches/series --- ycmd-0+20160327+gitc3e6904/debian/patches/series 2016-03-31 16:16:26.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/patches/series 2017-01-03 17:57:25.000000000 +0000 @@ -6,3 +6,4 @@ 05-tern-support.patch 06-omnisharp-path.patch 07-shebang.patch +08-bottle.patch diff -Nru ycmd-0+20160327+gitc3e6904/debian/repack.local ycmd-0+20161219+git486b809/debian/repack.local --- ycmd-0+20160327+gitc3e6904/debian/repack.local 2016-03-30 09:41:11.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/repack.local 2017-01-03 17:57:25.000000000 +0000 @@ -11,5 +11,4 @@ rm .gitmodules rm .travis.yml -rm third_party/frozendict/.git* rm third_party/JediHTTP/.git* diff -Nru ycmd-0+20160327+gitc3e6904/debian/rules ycmd-0+20161219+git486b809/debian/rules --- ycmd-0+20160327+gitc3e6904/debian/rules 2016-03-31 16:16:26.000000000 +0000 +++ ycmd-0+20161219+git486b809/debian/rules 2017-01-26 09:57:23.000000000 +0000 @@ -1,6 +1,10 @@ #!/usr/bin/make -f # -*- makefile -*- +export DEB_BUILD_MAINT_OPTIONS=hardening=+all +DPKG_EXPORT_BUILDFLAGS = 1 +include /usr/share/dpkg/buildflags.mk + BUILD_DIR = $(CURDIR)/ycm_build GIT_URL = https://github.com/Valloric/ycmd.git DEB_SOURCE = $(shell dpkg-parsechangelog | grep Source: | sed -e 's/Source: //') @@ -10,24 +14,22 @@ DEB_INSTALL_DIR = $(CURDIR)/debian/$(DEB_SOURCE) -# Skip CsCompleter (OmniSharp), javascript (tern) and rust tests. -# This package doesn't support them yet. -# We are also skipping UnknownExtraConfException test. -# If ycmd package is installed in your system or -# /usr/lib/ycmd/ycm_extra_conf.py is present, UnknownExtraConfException is -# failing due to global_ycm_extra_conf defined in -# 02-generic-ycm-extra-conf-py.patch -EXCLUDE_TEST_PARAMS = --exclude='^cs$$' --exclude='UnknownExtraConfException' --exclude='javascript' --exclude='rust' --exclude='go' - -# This package is providing extra golang completion feature if golang and -# gocode packages are installed. *GoCodeCompleter* tests also require golang -# and gocode packages. gocode and golang is not available in every -# architecture. Skip GoCodeCompleter tests if DEB_BUILD_ARCH is not supported -# by golang -GO_DEP_ARCH = $(shell dpkg-architecture -qDEB_BUILD_ARCH | egrep -x "(amd64|armel|armhf|i386)" > /dev/null; echo $$?) -ifneq ($(GO_DEP_ARCH),0) - EXCLUDE_TEST_PARAMS += --exclude='.*GoCodeCompleter.*' -endif +# Skip CsCompleter (OmniSharp), javascript (tern), go and rust tests. +# This package doesn't support them out-of-the-box yet. +EXCLUDE_TEST_PARAMS = --exclude='^cs$$' --exclude='javascript' --exclude='rust' --exclude='go' + + +# AddNearestThirdPartyFoldersToSysPath tests are using third_party/python-future +# directory to test. This directory is removed from ycmd +# package and ycmd is using python-future package. This tests are failing +# because of this reason and needs to be exluded from Debian package. +EXCLUDE_TEST_PARAMS += --exclude='AddNearestThirdPartyFoldersToSysPath_' + + +EXCLUDE_TEST_PARAMS += --exclude='GetCompletions_UnicodeInLineFilter_test' # TODO: Need to investigate +EXCLUDE_TEST_PARAMS += --exclude='FromHandlerWithSubservers_test' # Requires OmniSharp +EXCLUDE_TEST_PARAMS += --exclude='FromWatchdogWithSubservers_test' # Requires OmniSharp + %: dh $@ --with python2 --sourcedirectory=$(CURDIR)/cpp --builddirectory=$(BUILD_DIR) --parallel @@ -39,8 +41,8 @@ ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS))) cd $(BUILD_DIR)/ycm/tests && ./ycm_core_tests # Test JediHTTP - cd third_party/JediHTTP && HOME=$(CURDIR) nosetests -v --with-id -d --no-byte-compile - cd third_party/JediHTTP && HOME=$(CURDIR) nosetests3 -v --with-id -d --no-byte-compile + HOME=$(CURDIR) nosetests -w third_party/JediHTTP -v --with-id -d --no-byte-compile --exclude=test_good_gotoassignment_follow_imports + HOME=$(CURDIR) nosetests3 -w third_party/JediHTTP -v --with-id -d --no-byte-compile --exclude=test_good_gotoassignment_follow_imports # Some tests requires UTF-8 compatible locale HOME=$(CURDIR) LC_ALL=C.UTF-8 ./run_tests.py --skip-build --no-byte-compile $(EXCLUDE_TEST_PARAMS) endif @@ -50,7 +52,8 @@ $(RM) -rfv .noseids third_party/JediHTTP/.noseids .cache $(RM) -rfv *.so* $(RM) -rfv $(BUILD_DIR) - find third_party/JediHTTP -name '*.pyc' -exec $(RM) -v {} \; + $(RM) -rfv Microsoft # typescript tests generating a Microsoft directory + find -name '*.pyc' -exec $(RM) -v {} \; # remove all cached python files override_dh_install: dh_install @@ -64,26 +67,22 @@ $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/general/tests \ $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/go/tests \ $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/tests \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/third_party/JediHTTP/jedihttp/tests + $(DEB_INSTALL_DIR)/usr/lib/ycmd/third_party/JediHTTP/jedihttp/tests \ + $(DEB_INSTALL_DIR)/usr/include \ + $(DEB_INSTALL_DIR)/usr/lib/*.a # Fix permissions - chmod 644 $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/typescript/typescript_completer.py \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/all/identifier_completer.py \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/cpp/clang_completer.py \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/check_core_version.py \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/go/go_completer.py \ - $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/completers/cs/cs_completer.py + chmod 755 $(DEB_INSTALL_DIR)/usr/lib/ycmd/ycmd/__main__.py override_dh_link: $(eval YCMD_CLANG_VER=$(shell objdump -x ycm_core.so | grep -oP 'libclang-\K(\d+\.\d+)')) echo "misc:Clang-Ver=$(YCMD_CLANG_VER)" >> debian/ycmd.substvars - dh_link -pycmd usr/lib/clang/$(YCMD_CLANG_VER)/include usr/lib/ycmd/clang_includes + dh_link -pycmd usr/lib/clang/$(YCMD_CLANG_VER)/include usr/lib/ycmd/clang_includes/include get-orig-source: TMPDIR=`mktemp -d -t`; \ git clone $(GIT_URL) $$TMPDIR; \ cd $$TMPDIR; \ git checkout $(DEB_UPSTREAM_COMMIT); \ - git submodule update --init third_party/frozendict; \ git submodule update --init third_party/JediHTTP; \ tar -czvf $(CURDIR)/ycmd.orig.tar.gz .; \ cd $(CURDIR); \ diff -Nru ycmd-0+20160327+gitc3e6904/DEV_SETUP.md ycmd-0+20161219+git486b809/DEV_SETUP.md --- ycmd-0+20160327+gitc3e6904/DEV_SETUP.md 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/DEV_SETUP.md 2016-12-20 08:50:19.000000000 +0000 @@ -10,7 +10,7 @@ 1. Install [Vagrant][]. 2. `cd` into the folder where you checked out ycmd. 3. `$ vagrant up && vagrant ssh`. This will take a while because the VM is being - built and set up. Only needs to happen once though. + built and set up. Only needs to happen once though. 4. You are now in the VM. Run the tests with `$ ./run_tests.py`. 5. Hack away. When done, exit the ssh connection with `exit`. 6. `$ vagrant suspend` so that you can quickly get back to hacking later. @@ -24,4 +24,122 @@ If you ever feel like you've screwed up the VM, just kill it with `vagrant destroy` and then run `vagrant up` again to get to a clean state. +# Debugging the Python layer + +There are a number of Python debuggers. Presented here are just a couple of +options known to work for certain developers on the ycmd project. + +The options presented are: + +- Using [`ipdb`][ipdb] (this is known not to work well on OS X). +- Using [pyclewn][] and attaching to the running Python process. + +## Using `ipdb` + +1. If you're not using vagrant, install `ipdb` (`pip install ipdb`). +2. At the point in the code you want to break, add the following lines: + +```python +import ipdb; ipdb.set_trace() +``` + +3. Run the tests without `flake8`, e.g. + +```sh +./run_tests.py --skip-build --no-flake8 ycmd/tests/get_completions_test.py +``` + +4. The test breaks at your code breakpoint and offers a command interface to + debug. + +See the `ipdb` docs for more info. + +## Using `pyclewn` in Vim + +The procedure is similar to using `ipdb` but you attach to the suspended process +and use Vim as a graphical debugger: + +1. Install [pyclewna][pyclewn-install] + +2. At the point you want to break, add the following lines: + +```python +import clewn.vim as clewn; clewn.pdb() +``` + +3. Run the tests without `flake8`, e.g. + +```sh +./run_tests.py --skip-build --no-flake8 ycmd/tests/get_completions_test.py +``` + +4. The tests will pause at the breakpoint. Now within Vim attach the debugger + with `:Pyclewn pdb`. Hope that it works. It can be a bit flaky. + +See the pyclewn docs for more info. + +# Debugging the C++ layer (C++ Python library) + +If you want to debug the c++ code using gdb (or your favourite graphical +debugger, e.g. [pyclewn][] in Vim), there are a few things you need to do: + +1. Ensure your Python is built with debug enabled. In the vagrant system that's + as simple as: + +```sh + vagrant up + vagrant ssh + export OPT='-g' # Ensure Python binary has debugging info + export PYTHON_CONFIGURE_OPTS='--enable-shared --with-pydebug' + pyenv install 2.7.11 # or whatever version +``` + + On OS X, you need a working debugger. You can either use `lldb` + which comes with XCode or `brew install gdb`. Note: If you use `gdb` from + homebrew, then you need to sign the binary otherwise you can't debug + anything. See later steps for a link. + +2. Build ycm_core library with debugging information (and link against debug + Python): + +```sh + pyenv shell 2.7.11 + ./build.py --all --enable-debug +``` + +3. Enable debugging in the OS. On Linux (Ubuntu at least, which is what all of + our tests are run on), you must set the following sysctl parameter (you can + make it permanent if you like by adding it to `/etc/sysctl.conf` or via any + other appropriate mechanism): + +```sh + sudo sysctl kernel.yama.ptrace_scope=0 +``` + + On OS X it is more fiddly: + - The binary must be signed. See + https://sourceware.org/gdb/wiki/BuildingOnDarwin + - You *can not* debug system Python. Again: you *must* use a Python that is + *not* the one provided by Apple. Use pyenv. That is the rule. + Don't argue. + + Don't ask why. It's for security. + +3. Here you have choices: either use a Python debugger to break the tests, or + manually use Vim to simulate the scenario you want to debug. In any case, you + will need the PID of the running Python process hosting ycmd to attach to. + Getting this is left as an exercise, but one approach is to simply + install vim with `apt-get install vim` and to get a copy of YouCompleteMe + into `$HOME/.vim/bundle` and symlink `/vargant` as + `$HOME/.vim/bundle/third_party/ycmd`. Anyway, once you have the PID you can + simply attach to the Python process, for example: + + - `:YcmDebugInfo` to get the pid + - `gdb: attach ` + - `break YouCompleteMe::FilterAndSortCandidates` + + [vagrant]: https://www.vagrantup.com/ +[pyclewn]: http://pyclewn.sourceforge.net +[pyclewn-install]: http://pyclewn.sourceforge.net/install.html +[ipdb]: https://pypi.python.org/pypi/ipdb diff -Nru ycmd-0+20160327+gitc3e6904/README.md ycmd-0+20161219+git486b809/README.md --- ycmd-0+20160327+gitc3e6904/README.md 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/README.md 2016-12-20 08:50:19.000000000 +0000 @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/Valloric/ycmd.svg?branch=master)](https://travis-ci.org/Valloric/ycmd) [![Build status](https://ci.appveyor.com/api/projects/status/6fetp5xwb0kkuv2w/branch/master?svg=true)](https://ci.appveyor.com/project/Valloric/ycmd) -[![Coverage Status](https://coveralls.io/repos/Valloric/ycmd/badge.svg?branch=master&service=github)](https://coveralls.io/github/Valloric/ycmd?branch=master) +[![Coverage Status](https://codecov.io/gh/Valloric/ycmd/branch/master/graph/badge.svg)](https://codecov.io/gh/Valloric/ycmd) ycmd is a server that provides APIs for code-completion and other code-comprehension use-cases like semantic GoTo commands (and others). For @@ -24,6 +24,7 @@ - [you-complete-me][atom-you-complete-me]: Atom client. - [YcmdCompletion][sublime-ycmd]: Sublime client - [kak-ycmd][]: Kakoune client. +- [you-complete-me][vscode-you-complete-me]: VSCode client. Feel free to send a pull request adding a link to your client here if you've built one. @@ -153,6 +154,17 @@ 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 return 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 ----------------------- @@ -175,6 +187,66 @@ information on it can also be found in the [corresponding section of YCM's _User Guide_][extra-conf-doc]. +### `.ycm_extra_conf.py` specification + +The `.ycm_extra_conf.py` module must define the following methods: + +#### `FlagsForFile( filename, **kwargs )` + +Required for c-family language support. + +This method is called by the c-family completer to get the +compiler flags to use when compiling the file with absolute path `filename`. +The following additional arguments are optionally supplied depending on user +configuration: + +- `client_data`: any additional data supplied by the client application. + See the [YouCompleteMe documentation][extra-conf-vim-data-doc] for an + example. + +The return value must be one of the following: + +- `None` meaning no flags are known for this file, or + +- a dictionary containing the following items: + + - `flags`: (mandatory) a list of compiler flags. + + - `do_cache`: (optional) a boolean indicating whether or not the result of + this call (i.e. the list of flags) should be cached for this file name. + Defaults to `True`. If unsure, the default is almost always correct. + + - `flags_ready`: (optional) a boolean indicating that the flags should be + used. Defaults to `True`. If unsure, the default is almost always correct. + +A minimal example which simply returns a list of flags is: + +```python +def FlagsForFile( filename, **kwargs ): + return { + 'flags': [ '-x', 'c++' ] + } +``` + +### Global extra conf file specification + +The global extra module must expose the same functions as the +`.ycm_extra_conf.py` module with the following additions: + +#### `YcmCorePreLoad()` + +Optional. + +This method, if defined, is called by the server prior to importing the c++ +python plugin. It is not usually required and its use is for advanced users +only. + +#### `Shutdown()` + +Optional. + +Called prior to the server exiting cleanly. It is not usually required and its +use is for advanced users only. Backwards compatibility ----------------------- @@ -249,4 +321,5 @@ [ccoc]: https://github.com/Valloric/ycmd/blob/master/CODE_OF_CONDUCT.md [dev-setup]: https://github.com/Valloric/ycmd/blob/master/DEV_SETUP.md [test-setup]: https://github.com/Valloric/ycmd/blob/master/TESTS.md - +[extra-conf-vim-data-doc]: https://github.com/Valloric/YouCompleteMe#the-gycm_extra_conf_vim_data-option +[vscode-you-complete-me]: https://marketplace.visualstudio.com/items?itemName=RichardHe.you-complete-me diff -Nru ycmd-0+20160327+gitc3e6904/run_tests.py ycmd-0+20161219+git486b809/run_tests.py --- ycmd-0+20160327+gitc3e6904/run_tests.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/run_tests.py 2016-12-20 08:50:19.000000000 +0000 @@ -120,13 +120,13 @@ parser.add_argument( '--msvc', type = int, choices = [ 11, 12, 14 ], help = 'Choose the Microsoft Visual ' 'Studio version. (default: 14).' ) - parser.add_argument( '--arch', type = int, choices = [ 32, 64 ], - help = 'Force architecture to 32 or 64 bits on ' - 'Windows (default: python interpreter architecture).' ) parser.add_argument( '--coverage', action = 'store_true', help = 'Enable coverage report (requires coverage pkg)' ) parser.add_argument( '--no-flake8', action = 'store_true', help = 'Disable flake8 run.' ) + parser.add_argument( '--dump-path', action = 'store_true', + help = 'Dump the PYTHONPATH required to run tests ' + 'manually, then exit.' ) parsed_args, nosetests_args = parser.parse_known_args() @@ -179,8 +179,12 @@ if args.msvc: build_cmd.extend( [ '--msvc', str( args.msvc ) ] ) - if args.arch: - build_cmd.extend( [ '--arch', str( args.arch ) ] ) + if args.coverage: + # In order to generate coverage data for C++, we use gcov. This requires + # some files generated when building (*.gcno), so we store the build + # output in a known directory, which is then used by the CI infrastructure + # to generate the c++ coverage information. + build_cmd.extend( [ '--enable-coverage', '--build-dir', '.build' ] ) subprocess.check_call( build_cmd ) @@ -195,7 +199,9 @@ nosetests_args.extend( COMPLETERS[ key ][ 'test' ] ) if parsed_args.coverage: - nosetests_args += [ '--with-coverage', '--cover-package=ycmd', + nosetests_args += [ '--with-coverage', + '--cover-erase', + '--cover-package=ycmd', '--cover-html' ] if extra_nosetests_args: @@ -208,6 +214,9 @@ def Main(): parsed_args, nosetests_args = ParseArguments() + if parsed_args.dump_path: + print( os.environ[ 'PYTHONPATH' ] ) + sys.exit() print( 'Running tests on Python', platform.python_version() ) if not parsed_args.no_flake8: RunFlake8() diff -Nru ycmd-0+20160327+gitc3e6904/test_requirements.txt ycmd-0+20161219+git486b809/test_requirements.txt --- ycmd-0+20160327+gitc3e6904/test_requirements.txt 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/test_requirements.txt 2016-12-20 08:50:19.000000000 +0000 @@ -1,10 +1,14 @@ -flake8>=2.5.2 -mock>=1.3.0 -nose>=1.3.7 -PyHamcrest>=1.8.5 -WebTest>=2.0.20 -ordereddict>=1.1 -nose-exclude>=0.4.1 -unittest2>=1.1.0 +# Flake8 3.x dropped support of Python 2.6 and 3.3 +flake8 < 3.0.0; python_version == '2.6' or python_version == '3.3' +flake8 >= 3.0.0; python_version == '2.7' or python_version > '3.3' +mock >= 1.3.0 +nose >= 1.3.7 +PyHamcrest >= 1.8.5 +WebTest >= 2.0.20 +ordereddict >= 1.1 +nose-exclude >= 0.4.1 +unittest2 >= 1.1.0 +psutil >= 3.3.0 # This needs to be kept in sync with submodule checkout in third_party -future==0.15.2 +future == 0.15.2 +coverage >= 4.2 diff -Nru ycmd-0+20160327+gitc3e6904/TESTS.md ycmd-0+20161219+git486b809/TESTS.md --- ycmd-0+20160327+gitc3e6904/TESTS.md 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/TESTS.md 2016-12-20 08:50:19.000000000 +0000 @@ -62,7 +62,7 @@ (default: 14). Windows only. * `--arch`: Force architecture to 32 or 64 bits on Windows (default: python interpreter architecture). Windows Only. -* `--coverage`: Output test coverage report +* `--coverage`: Generate code coverage data Remaining arguments are passed to "nosetests" directly. This means that you can run a specific script or a specific test as follows: @@ -81,15 +81,22 @@ ## Coverage testing -There's a coverage module for Python which works nicely with `nosetests`. This +We can generate coverage data for both the C++ layer and the Python layer. The +CI system will pass this coverage data to codecov.io where you can view coverage +after pushing a branch. + +C++ coverage testing is available only on Linux/Mac and uses gcov. +Stricly speaking, we use the `-coverage` option to your compiler, which in the +case of GNU and LLVM compilers, generate gcov-compatible data. + +For Python, there's a coverage module which works nicely with `nosetests`. This is very useful for highlighting areas of your code which are not covered by the automated integration tests. Run it like this: ``` -$ pip install coverage -$ ./run_tests.py --coverage --cover-html +$ ./run_tests.py --coverage ``` This will print a summary and generate HTML output in `./cover` diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/frozendict/__init__.py ycmd-0+20161219+git486b809/third_party/frozendict/frozendict/__init__.py --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/frozendict/__init__.py 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/frozendict/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -import collections, operator - -class frozendict(collections.Mapping): - - def __init__(self, *args, **kwargs): - self.__dict = dict(*args, **kwargs) - self.__hash = None - - def __getitem__(self, key): - return self.__dict[key] - - def copy(self, **add_or_replace): - return frozendict(self, **add_or_replace) - - def __iter__(self): - return iter(self.__dict) - - def __len__(self): - return len(self.__dict) - - def __repr__(self): - return '' % repr(self.__dict) - - def __hash__(self): - if self.__hash is None: - self.__hash = reduce(operator.xor, map(hash, self.iteritems()), 0) - - return self.__hash diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/LICENSE.txt ycmd-0+20161219+git486b809/third_party/frozendict/LICENSE.txt --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/LICENSE.txt 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/LICENSE.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -Copyright (c) 2012 Santiago Lezica - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/MANIFEST.in ycmd-0+20161219+git486b809/third_party/frozendict/MANIFEST.in --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/MANIFEST.in 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -include *.txt diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/README.md ycmd-0+20161219+git486b809/third_party/frozendict/README.md --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/README.md 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/README.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -frozendict -========== - -`frozendict` is an immutable wrapper around dictionaries that implements the -complete mapping interface. It can be used as a drop-in replacement for -dictionaries where immutability is desired. - -Of course, this is `python`, and you can still poke around the object's -internals if you want. - -The `frozendict` constructor mimics `dict`, and all of the expected -interfaces (`iter`, `len`, `repr`, `hash`, `getitem`) are provided. -Note that a `frozendict` does not guarantee the immutability of its values, so -the utility of `hash` method is restricted by usage. - -The only difference is that the `copy()` method of `frozendict` takes -variable keyword arguments, which will be present as key/value pairs in the new, -immutable copy. - -Example shell usage: - - from frozendict import frozendict - - fd = frozendict({ 'hello': 'World' }) - - print fd - # - - print fd['hello'] - # 'World' - - print fd.copy(another='key/value') - # diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/README.txt ycmd-0+20161219+git486b809/third_party/frozendict/README.txt --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/README.txt 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/README.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -========== -frozendict -========== - -``frozendict`` is an immutable wrapper around dictionaries that implements the -complete mapping interface. It can be used as a drop-in replacement for -dictionaries where immutability is desired. - -Of course, this is ``python``, and you can still poke around the object's -internals if you want. - -The ``frozendict`` constructor mimics ``dict``, and all of the expected -interfaces (``iter``, ``len``, ``repr``, ``hash``, ``getitem``) are provided. -Note that a ``frozendict`` does not guarantee the immutability of its values, so -the utility of ``hash`` method is restricted by usage. - -The only difference is that the ``copy()`` method of ``frozendict`` takes -variable keyword arguments, which will be present as key/value pairs in the new, -immutable copy. - -Example shell usage:: - - from frozendict import frozendict - - fd = frozendict({ 'hello': 'World' }) - - print fd - # - - print fd['hello'] - # 'World' - - print fd.copy(another='key/value') - # diff -Nru ycmd-0+20160327+gitc3e6904/third_party/frozendict/setup.py ycmd-0+20161219+git486b809/third_party/frozendict/setup.py --- ycmd-0+20160327+gitc3e6904/third_party/frozendict/setup.py 2016-04-01 09:03:27.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/frozendict/setup.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -from distutils.core import setup - -setup( - name = 'frozendict', - version = '0.3', - url = 'https://github.com/slezica/python-frozendict', - - author = 'Santiago Lezica', - author_email = 'slezica89@gmail.com', - - packages = ['frozendict'], - license = 'MIT License', - - description = 'An immutable dictionary', - long_description = open('README.txt').read() -) diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/appveyor.yml ycmd-0+20161219+git486b809/third_party/JediHTTP/appveyor.yml --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/appveyor.yml 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/appveyor.yml 2016-12-20 08:50:21.000000000 +0000 @@ -46,6 +46,15 @@ PYTHON_VERSION: "3.3.x" PYTHON_ARCH: "64" TOXENV: "py33" + + - PYTHON: "C:\\Python33-x64" + PYTHON_VERSION: "3.3.x" + PYTHON_ARCH: "64" + TOXENV: "flake8" + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.x" + PYTHON_ARCH: "64" + TOXENV: "flake8" install: - git submodule update --init --recursive diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/handlers.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/handlers.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/handlers.py 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/handlers.py 2016-12-20 08:50:21.000000000 +0000 @@ -21,6 +21,7 @@ import bottle from jedihttp import hmaclib from bottle import response, request, Bottle +from threading import Lock try: import httplib @@ -35,6 +36,9 @@ logger = logging.getLogger( __name__ ) app = Bottle( __name__ ) +# Jedi is not thread safe. +jedi_lock = Lock() + @app.post( '/healthy' ) def healthy(): @@ -51,50 +55,63 @@ @app.post( '/completions' ) def completions(): logger.debug( 'received /completions request' ) - script = _GetJediScript( request.json ) - return _JsonResponse( { + with jedi_lock: + script = _GetJediScript( request.json ) + response = { 'completions': [ { - 'name': completion.name, - 'description': completion.description, - 'docstring': completion.docstring(), 'module_path': completion.module_path, + 'name': completion.name, 'line': completion.line, - 'column': completion.column + 'column': completion.column, + 'docstring': completion.docstring(), + 'description': completion.description, + 'type': completion.type } for completion in script.completions() ] - } ) + } + return _JsonResponse( response ) @app.post( '/gotodefinition' ) def gotodefinition(): logger.debug( 'received /gotodefinition request' ) - script = _GetJediScript( request.json ) - return _JsonResponse( _FormatDefinitions( script.goto_definitions() ) ) + with jedi_lock: + script = _GetJediScript( request.json ) + response = _FormatDefinitions( script.goto_definitions() ) + return _JsonResponse( response ) @app.post( '/gotoassignment' ) def gotoassignments(): logger.debug( 'received /gotoassignment request' ) - script = _GetJediScript( request.json ) - return _JsonResponse( _FormatDefinitions( script.goto_assignments() ) ) + with jedi_lock: + request_json = request.json + follow_imports = ( 'follow_imports' in request_json and + request_json[ 'follow_imports' ] ) + script = _GetJediScript( request_json ) + response = _FormatDefinitions( script.goto_assignments( follow_imports ) ) + return _JsonResponse( response ) @app.post( '/usages' ) def usages(): logger.debug( 'received /usages request' ) - script = _GetJediScript( request.json ) - return _JsonResponse( _FormatDefinitions( script.usages() ) ) + with jedi_lock: + script = _GetJediScript( request.json ) + response = _FormatDefinitions( script.usages() ) + return _JsonResponse( response ) def _FormatDefinitions( definitions ): return { 'definitions': [ { 'module_path': definition.module_path, + 'name': definition.name, + 'in_builtin_module': definition.in_builtin_module(), 'line': definition.line, 'column': definition.column, - 'in_builtin_module': definition.in_builtin_module(), - 'is_keyword': definition.is_keyword, + 'docstring': definition.docstring(), 'description': definition.description, - 'docstring': definition.docstring() + 'is_keyword': definition.is_keyword } for definition in definitions ] } diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/imported.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/imported.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/imported.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/imported.py 2016-12-20 08:50:21.000000000 +0000 @@ -0,0 +1,2 @@ +def imported_function(): + pass diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/importer.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/importer.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/importer.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/fixtures/follow_imports/importer.py 2016-12-20 08:50:21.000000000 +0000 @@ -0,0 +1,3 @@ +from imported import imported_function + +imported_function() diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/handlers_test.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/handlers_test.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/handlers_test.py 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/handlers_test.py 2016-12-20 08:50:21.000000000 +0000 @@ -82,23 +82,25 @@ assert_that( definitions, has_length( 2 ) ) assert_that( definitions, has_items( { - 'description': 'def f', - 'line': 1, + 'module_path': filepath, + 'name': 'f', 'in_builtin_module': False, + 'line': 1, 'column': 4, - 'is_keyword': False, - 'module_path': filepath, 'docstring': 'f()\n\nModule method docs\nAre ' - 'dedented, like you might expect' + 'dedented, like you might expect', + 'description': 'def f', + 'is_keyword': False, }, { - 'description': 'class C', - 'line': 6, + 'module_path': filepath, + 'name': 'C', 'in_builtin_module': False, + 'line': 6, 'column': 6, - 'is_keyword': False, - 'module_path': filepath, - 'docstring': 'Class Documentation' + 'docstring': 'Class Documentation', + 'description': 'class C', + 'is_keyword': False } ) ) @@ -143,13 +145,78 @@ assert_that( definitions, has_length( 1 ) ) assert_that( definitions, has_item( { - 'in_builtin_module': False, - 'is_keyword': False, 'module_path': filepath, - 'column': 0, + 'name': 'inception', + 'in_builtin_module': False, 'line': 18, + 'column': 0, + 'docstring': '', 'description': 'inception = _list[ 2 ]', - 'docstring': '' + 'is_keyword': False + } ) ) + + +def test_good_gotoassignment_do_not_follow_imports(): + app = TestApp( handlers.app ) + filepath = fixture_filepath( 'follow_imports', 'importer.py' ) + request_data = { + 'source': open( filepath ).read(), + 'line': 3, + 'col': 9, + 'source_path': filepath + } + expected_definition = { + 'module_path': filepath, + 'name': 'imported_function', + 'in_builtin_module': False, + 'line': 1, + 'column': 21, + 'docstring': '', + 'description': 'from imported ' + 'import imported_function', + 'is_keyword': False + } + + definitions = app.post_json( '/gotoassignment', + request_data ).json[ 'definitions' ] + + assert_that( definitions, has_length( 1 ) ) + assert_that( definitions, has_item( expected_definition ) ) + + request_data[ 'follow_imports' ] = False + + definitions = app.post_json( '/gotoassignment', + request_data ).json[ 'definitions' ] + + assert_that( definitions, has_length( 1 ) ) + assert_that( definitions, has_item( expected_definition ) ) + + +def test_good_gotoassignment_follow_imports(): + app = TestApp( handlers.app ) + importer_filepath = fixture_filepath( 'follow_imports', 'importer.py' ) + imported_filepath = fixture_filepath( 'follow_imports', 'imported.py' ) + request_data = { + 'source': open( importer_filepath ).read(), + 'line': 3, + 'col': 9, + 'source_path': importer_filepath, + 'follow_imports': True + } + + definitions = app.post_json( '/gotoassignment', + request_data ).json[ 'definitions' ] + + assert_that( definitions, has_length( 1 ) ) + assert_that( definitions, has_item( { + 'module_path': imported_filepath, + 'name': 'imported_function', + 'in_builtin_module': False, + 'line': 1, + 'column': 4, + 'docstring': 'imported_function()\n\n', + 'description': 'def imported_function', + 'is_keyword': False } ) ) @@ -169,40 +236,44 @@ assert_that( definitions, has_length( 4 ) ) assert_that( definitions, has_items( { - 'description': 'def f', - 'in_builtin_module': False, - 'is_keyword': False, 'module_path': filepath, - 'column': 4, + 'name': 'f', + 'in_builtin_module': False, 'line': 1, - 'docstring': 'f()\n\nModule method docs\nAre dedented, like you might expect' + 'column': 4, + 'docstring': 'f()\n\nModule method docs\nAre dedented, like you might expect', + 'description': 'def f', + 'is_keyword': False }, { - 'description': 'a = f()', - 'in_builtin_module': False, - 'is_keyword': False, 'module_path': filepath, - 'column': 4, + 'name': 'f', + 'in_builtin_module': False, 'line': 6, - 'docstring': '' + 'column': 4, + 'description': 'a = f()', + 'docstring': '', + 'is_keyword': False }, { - 'description': 'b = f()', - 'in_builtin_module': False, - 'is_keyword': False, 'module_path': filepath, - 'column': 4, + 'name': 'f', + 'in_builtin_module': False, 'line': 7, - 'docstring': '' + 'column': 4, + 'description': 'b = f()', + 'docstring': '', + 'is_keyword': False }, { - 'description': 'c = f()', - 'in_builtin_module': False, - 'is_keyword': False, 'module_path': filepath, - 'column': 4, + 'name': 'f', + 'in_builtin_module': False, 'line': 8, - 'docstring': '' + 'column': 4, + 'description': 'c = f()', + 'docstring': '', + 'is_keyword': False } ) ) diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/utils.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/utils.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp/tests/utils.py 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp/tests/utils.py 2016-12-20 08:50:21.000000000 +0000 @@ -69,9 +69,9 @@ return decorate -def fixture_filepath( filename ): +def fixture_filepath( *components ): dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) - return os.path.join( dir_of_current_script, 'fixtures', filename ) + return os.path.join( dir_of_current_script, 'fixtures', *components ) # Creation flag to disable creating a console window on Windows. See diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp.py ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp.py --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/jedihttp.py 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/jedihttp.py 2016-12-20 08:50:21.000000000 +0000 @@ -14,9 +14,10 @@ from jedihttp import utils utils.AddVendorFolderToSysPath() -import sys -import os +import logging import json +import os +import sys from base64 import b64decode from argparse import ArgumentParser from waitress import serve @@ -30,11 +31,25 @@ help = 'server host' ) parser.add_argument( '--port', type = int, default = 0, help = 'server port' ) + parser.add_argument( '--log', type = str, default = 'info', + choices = [ 'debug', 'info', 'warning', + 'error', 'critical' ], + help = 'log level' ) parser.add_argument( '--hmac-file-secret', type = str, help = 'file containing hmac secret' ) return parser.parse_args() +def SetUpLogging( log_level ): + numeric_level = getattr( logging, log_level.upper(), None ) + if not isinstance( numeric_level, int ): + raise ValueError( 'Invalid log level: {0}'.format( log_level ) ) + + # Has to be called before any call to logging.getLogger(). + logging.basicConfig( format = '%(asctime)s - %(levelname)s - %(message)s', + level = numeric_level ) + + def GetSecretFromTempFile( tfile ): key = 'hmac_secret' with open( tfile ) as hmac_file: @@ -53,6 +68,8 @@ def Main(): args = ParseArgs() + SetUpLogging( args.log ) + if args.hmac_file_secret: hmac_secret = GetSecretFromTempFile( args.hmac_file_secret ) handlers.app.config[ 'jedihttp.hmac_secret' ] = b64decode( hmac_secret ) diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/README.md ycmd-0+20161219+git486b809/third_party/JediHTTP/README.md --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/README.md 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/README.md 2016-12-20 08:50:21.000000000 +0000 @@ -30,6 +30,12 @@ Listen on PORT. If not specified, will use any available port. +#### `--log` LEVEL + +Set logging level to LEVEL. Available levels, from most verbose to least +verbose, are: `debug`, `info`, `warning`, `error`, and `critical`. Default value is +`info`. + #### `--hmac-secret-file` PATH PATH is the path of a JSON file containing a key named `hmac_secret`. Its value @@ -123,7 +129,8 @@ "source": "def f():\n pass", "line": 1, "col": 0, - "path": "/home/user/code/src/file.py" + "path": "/home/user/code/src/file.py", + "follow_imports": true, // Optional (default is false) } ``` diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/tox.ini ycmd-0+20161219+git486b809/third_party/JediHTTP/tox.ini --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/tox.ini 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/tox.ini 2016-12-20 08:50:21.000000000 +0000 @@ -1,13 +1,16 @@ [tox] -envlist = py26, py27, py33 +envlist = py26, py27, py33, flake8 skipsdist = True [testenv] deps = -r{toxinidir}/test_requirements.txt commands = nosetests -v - flake8 --select=F,C9 --max-complexity=10 --exclude=fixtures jedihttp tests [testenv:py26] deps = {[testenv]deps} unittest2 ordereddict +[testenv:flake8] +deps = {[testenv]deps} +commands = + flake8 --select=F,C9 --max-complexity=10 --exclude=fixtures jedihttp tests diff -Nru ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/.travis.yml ycmd-0+20161219+git486b809/third_party/JediHTTP/.travis.yml --- ycmd-0+20160327+gitc3e6904/third_party/JediHTTP/.travis.yml 2016-04-01 09:03:28.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/JediHTTP/.travis.yml 2016-12-20 08:50:21.000000000 +0000 @@ -20,6 +20,11 @@ - TOXENV=py27 - CROSS_PYTHON_TESTS=true + - python: 2.7 + env: TOXENV=flake8 + - python: 3.3 + env: TOXENV=flake8 + - language: generic os: osx osx_image: xcode7 @@ -44,6 +49,15 @@ os: osx osx_image: xcode7 env: TOXENV=py33 + + - language: generic + os: osx + osx_image: xcode7 + env: TOXENV=flake8 + - language: generic + os: osx + osx_image: xcode7 + env: TOXENV=flake8 before_install: git submodule update --init --recursive install: ./travis/install.sh script: ./travis/run.sh diff -Nru ycmd-0+20160327+gitc3e6904/third_party/tern_runtime/package.json ycmd-0+20161219+git486b809/third_party/tern_runtime/package.json --- ycmd-0+20160327+gitc3e6904/third_party/tern_runtime/package.json 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/third_party/tern_runtime/package.json 2016-12-20 08:50:19.000000000 +0000 @@ -1,6 +1,6 @@ { "description": "ycmd tern runtime area with required tern version and plugins", "dependencies": { - "tern": "0.17.0" + "tern": "0.20.0" } } diff -Nru ycmd-0+20160327+gitc3e6904/update_boost.py ycmd-0+20161219+git486b809/update_boost.py --- ycmd-0+20160327+gitc3e6904/update_boost.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/update_boost.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,210 @@ +#!/usr/bin/env python + +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals +from __future__ import absolute_import + +import os +import platform +import re +import subprocess +import sys +import tarfile +from tempfile import mkdtemp +from shutil import rmtree +from distutils.dir_util import copy_tree + +DIR_OF_THIS_SCRIPT = os.path.dirname( os.path.abspath( __file__ ) ) +DIR_OF_THIRD_PARTY = os.path.join( DIR_OF_THIS_SCRIPT, 'third_party' ) + +sys.path.insert( + 1, os.path.abspath( os.path.join( DIR_OF_THIRD_PARTY, 'argparse' ) ) ) +sys.path.insert( + 1, os.path.abspath( os.path.join( DIR_OF_THIRD_PARTY, 'requests' ) ) ) + +import argparse +import requests + +CHUNK_SIZE = 1024 * 1024 # 1 MB + +BOOST_VERSION_REGEX = re.compile( 'Version (\d+\.\d+\.\d+)' ) +BOOST_URL = ( 'https://sourceforge.net/projects/boost/files/boost/' + '{version}/{archive}/download' ) +BOOST_NAME = 'boost_{version_}' +BOOST_ARCHIVE = BOOST_NAME + '.tar.bz2' +BOOST_PARTS = [ + 'boost/utility.hpp', + 'boost/python.hpp', + 'boost/bind.hpp', + 'boost/lambda/lambda.hpp', + 'boost/exception/all.hpp', + 'boost/tuple/tuple_io.hpp', + 'boost/tuple/tuple_comparison.hpp', + 'boost/regex.hpp', + 'boost/foreach.hpp', + 'boost/smart_ptr.hpp', + 'boost/algorithm/string_regex.hpp', + 'boost/thread.hpp', + 'boost/unordered_map.hpp', + 'boost/unordered_set.hpp', + 'boost/format.hpp', + 'boost/ptr_container/ptr_container.hpp', + 'boost/filesystem.hpp', + 'boost/filesystem/fstream.hpp', + 'boost/utility.hpp', + 'boost/algorithm/cxx11/any_of.hpp', + 'atomic', + 'lockfree', + 'assign', + 'system' +] +BOOST_LIBS_FOLDERS_TO_REMOVE = [ + 'assign', + 'mpi', + 'config', + 'lockfree', + 'doc', + 'test', + 'examples', + 'build' +] +BOOST_LIBS_FILES_TO_REMOVE = [ + # Extracted with Boost 1.61.0 and breaks the build on Windows. + 'xml_woarchive.cpp' +] +BOOST_LIBS_EXTENSIONS_TO_KEEP = [ + '.hpp', + '.cpp', + '.ipp', + '.inl' +] + + +def OnWindows(): + return platform.system() == 'Windows' + + +def Download( url, dest ): + print( 'Downloading {0}.'.format( os.path.basename( dest ) ) ) + r = requests.get( url, stream = True ) + with open( dest, 'wb') as f: + for chunk in r.iter_content( chunk_size = CHUNK_SIZE ): + if chunk: + f.write( chunk ) + r.close() + + +def Extract( path, folder = os.curdir ): + print( 'Extracting {0}.'.format( os.path.basename( path ) ) ) + with tarfile.open( path ) as f: + f.extractall( folder ) + + +def GetLatestBoostVersion(): + download_page = requests.get( 'http://www.boost.org/users/download/' ) + version_match = BOOST_VERSION_REGEX.search( download_page.text ) + if not version_match: + return None + return version_match.group( 1 ) + + +def GetBoostName( version ): + return BOOST_NAME.format( version_ = version.replace( '.', '_' ) ) + + +def GetBoostArchiveName( version ): + return BOOST_ARCHIVE.format( version_ = version.replace( '.', '_' ) ) + + +def GetBoostArchiveUrl( version ): + return BOOST_URL.format( version = version, + archive = GetBoostArchiveName( version ) ) + + +def DownloadBoostLibrary( version, folder ): + archive_path = os.path.join( folder, GetBoostArchiveName( version ) ) + Download( GetBoostArchiveUrl( version ), archive_path ) + + +def CleanBoostParts( boost_libs_dir ): + for root, dirs, files in os.walk( boost_libs_dir ): + for directory in dirs: + if directory in BOOST_LIBS_FOLDERS_TO_REMOVE: + rmtree( os.path.join( root, directory ) ) + for filename in files: + extension = os.path.splitext( filename )[ 1 ] + if ( filename in BOOST_LIBS_FILES_TO_REMOVE or + extension not in BOOST_LIBS_EXTENSIONS_TO_KEEP ): + os.remove( os.path.join( root, filename ) ) + + +def ExtractBoostParts( args ): + print( 'Updating Boost to version {0}.'.format( args.version ) ) + boost_dir = mkdtemp( prefix = 'boost.' ) + + try: + os.chdir( boost_dir ) + + DownloadBoostLibrary( args.version, os.curdir ) + Extract( os.path.join( os.curdir, GetBoostArchiveName( args.version ) ), + os.curdir ) + + os.chdir( os.path.join( os.curdir, GetBoostName( args.version ) ) ) + + bootstrap = os.path.join( os.curdir, + 'bootstrap' + ( '.bat' if OnWindows() else + '.sh' ) ) + subprocess.call( [ bootstrap ] ) + subprocess.call( [ os.path.join( os.curdir, 'b2' ), + os.path.join( 'tools', 'bcp' ) ] ) + boost_parts_dir = os.path.join( os.curdir, 'boost_parts' ) + os.mkdir( boost_parts_dir ) + subprocess.call( [ os.path.join( os.curdir, 'dist', 'bin', 'bcp' ) ] + + BOOST_PARTS + + [ boost_parts_dir ] ) + + CleanBoostParts( os.path.join( boost_parts_dir, 'libs' ) ) + + dest_libs_dir = os.path.join( DIR_OF_THIS_SCRIPT, 'cpp', 'BoostParts', + 'libs' ) + dest_boost_dir = os.path.join( DIR_OF_THIS_SCRIPT, 'cpp', 'BoostParts', + 'boost' ) + if os.path.exists( dest_libs_dir ): + rmtree( dest_libs_dir ) + if os.path.exists( dest_boost_dir ): + rmtree( dest_boost_dir ) + copy_tree( os.path.join( boost_parts_dir, 'libs' ), dest_libs_dir ) + copy_tree( os.path.join( boost_parts_dir, 'boost' ), dest_boost_dir ) + finally: + os.chdir( DIR_OF_THIS_SCRIPT ) + rmtree( boost_dir ) + + +def ParseArguments(): + parser = argparse.ArgumentParser( + description = 'Update Boost parts to the latest Boost version ' + 'or the specified one.' ) + parser.add_argument( '--version', + help = 'Set Boost version. ' + 'Default to latest version.' ) + + args = parser.parse_args() + + if not args.version: + latest_version = GetLatestBoostVersion() + if not latest_version: + sys.exit( 'No latest version found. Set Boost version with ' + 'the --version option.' ) + args.version = latest_version + + return args + + +def Main(): + args = ParseArguments() + ExtractBoostParts( args ) + + +if __name__ == '__main__': + Main() diff -Nru ycmd-0+20160327+gitc3e6904/update_boost.sh ycmd-0+20161219+git486b809/update_boost.sh --- ycmd-0+20160327+gitc3e6904/update_boost.sh 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/update_boost.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -# This script is used to update cpp/BoostParts to the latest boost version -# Give it the full path to the boost_1_XX_X folder - -# Exit if error -set -e - -if [ -z "$1" ]; then - echo "Usage: $0 /path/to/boost_1_XX_X" - exit 0 -fi - -pushd "$1" - -./bootstrap.sh -./b2 tools/bcp - -boost_part_dir=`mktemp -d -t boost_parts.XXXXXX` - -dist/bin/bcp boost/utility.hpp boost/python.hpp boost/bind.hpp boost/lambda/lambda.hpp boost/exception/all.hpp boost/tuple/tuple_io.hpp boost/tuple/tuple_comparison.hpp boost/regex.hpp boost/foreach.hpp boost/smart_ptr.hpp boost/algorithm/string_regex.hpp boost/thread.hpp boost/unordered_map.hpp boost/unordered_set.hpp boost/format.hpp boost/ptr_container/ptr_container.hpp boost/filesystem.hpp boost/filesystem/fstream.hpp boost/utility.hpp boost/algorithm/cxx11/any_of.hpp atomic lockfree assign system $boost_part_dir - -pushd "${boost_part_dir}" - -# DON'T exit if error -set +e - -find libs \( -name assign -o -name mpi -o -name config -o -name lockfree \) -exec rm -rf '{}' \; -find libs \( -name doc -o -name test -o -name examples -o -name build \) -exec rm -rf '{}' \; -find libs -not \( -name "*.hpp" -o -name "*.cpp" -o -name "*.ipp" -o -name "*.inl" \) -type f -delete - -# Breaks the build for some reason and doesn't seem necessary at all. -rm libs/serialization/src/shared_ptr_helper.cpp - -# Exit if error -set -e - -popd -popd - -rm -rf cpp/BoostParts/libs -rm -rf cpp/BoostParts/boost - -cp -R "${boost_part_dir}/libs" cpp/BoostParts/libs -cp -R "${boost_part_dir}/boost" cpp/BoostParts/boost - -rm -rf "${boost_part_dir}" diff -Nru ycmd-0+20160327+gitc3e6904/vagrant_bootstrap.sh ycmd-0+20161219+git486b809/vagrant_bootstrap.sh --- ycmd-0+20160327+gitc3e6904/vagrant_bootstrap.sh 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/vagrant_bootstrap.sh 2016-12-20 08:50:19.000000000 +0000 @@ -13,6 +13,9 @@ # For pyenv Python building export CFLAGS='-O2' +# In order to work with ycmd, python *must* be built as a shared library. This +# is set via the PYTHON_CONFIGURE_OPTS option. +export PYTHON_CONFIGURE_OPTS='--enable-shared' ####################### # APT-GET INSTALL diff -Nru ycmd-0+20160327+gitc3e6904/Vagrantfile ycmd-0+20161219+git486b809/Vagrantfile --- ycmd-0+20160327+gitc3e6904/Vagrantfile 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/Vagrantfile 2016-12-20 08:50:19.000000000 +0000 @@ -19,4 +19,14 @@ v.memory = 3072 v.cpus = 2 end + + config.vm.provider "parallels" do |v, override| + override.vm.box = 'parallels/ubuntu-14.04' + + v.memory = 3072 + v.cpus = 4 + + v.linked_clone = true + v.update_guest_tools = true + end end diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/all/identifier_completer.py ycmd-0+20161219+git486b809/ycmd/completers/all/identifier_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/all/identifier_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/all/identifier_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -29,7 +29,7 @@ from collections import defaultdict from ycmd.completers.general_completer import GeneralCompleter from ycmd import identifier_utils -from ycmd.utils import ToCppStringCompatible +from ycmd.utils import ToCppStringCompatible, SplitLines from ycmd import responses SYNTAX_FILENAME = 'YCM_PLACEHOLDER_FOR_SYNTAX' @@ -54,7 +54,7 @@ completions = self._completer.CandidatesForQueryAndType( ToCppStringCompatible( _SanitizeQuery( request_data[ 'query' ] ) ), - ToCppStringCompatible( request_data[ 'filetypes' ][ 0 ] ) ) + ToCppStringCompatible( request_data[ 'first_filetype' ] ) ) completions = completions[ : self._max_candidates ] completions = _RemoveSmallCandidates( @@ -68,11 +68,8 @@ return [ ConvertCompletionData( x ) for x in completions ] - def AddIdentifier( self, identifier, request_data ): - try: - filetype = request_data[ 'filetypes' ][ 0 ] - except KeyError: - filetype = None + def _AddIdentifier( self, identifier, request_data ): + filetype = request_data[ 'first_filetype' ] filepath = request_data[ 'filepath' ] if not filetype or not filepath or not identifier: @@ -87,34 +84,27 @@ ToCppStringCompatible( filepath ) ) - def AddPreviousIdentifier( self, request_data ): - self.AddIdentifier( + def _AddPreviousIdentifier( self, request_data ): + self._AddIdentifier( _PreviousIdentifier( self.user_options[ 'min_num_of_chars_for_completion' ], request_data ), request_data ) - def AddIdentifierUnderCursor( self, request_data ): - cursor_identifier = _GetCursorIdentifier( request_data ) - if not cursor_identifier: - return - - self.AddIdentifier( cursor_identifier, request_data ) + def _AddIdentifierUnderCursor( self, request_data ): + self._AddIdentifier( _GetCursorIdentifier( request_data ), request_data ) - def AddBufferIdentifiers( self, request_data ): - try: - filetype = request_data[ 'filetypes' ][ 0 ] - except KeyError: - filetype = None + def _AddBufferIdentifiers( self, request_data ): + filetype = request_data[ 'first_filetype' ] filepath = request_data[ 'filepath' ] - collect_from_comments_and_strings = bool( self.user_options[ - 'collect_identifiers_from_comments_and_strings' ] ) if not filetype or not filepath: return + collect_from_comments_and_strings = bool( self.user_options[ + 'collect_identifiers_from_comments_and_strings' ] ) text = request_data[ 'file_data' ][ filepath ][ 'contents' ] self._logger.info( 'Adding buffer identifiers for file: %s', filepath ) self._completer.ClearForFileAndAddIdentifiersToDatabase( @@ -125,8 +115,7 @@ ToCppStringCompatible( filepath ) ) - def AddIdentifiersFromTagFiles( self, tag_files ): - absolute_paths_to_tag_files = ycm_core.StringVector() + def _FilterUnchangedTagFiles( self, tag_files ): for tag_file in tag_files: try: current_mtime = os.path.getmtime( tag_file ) @@ -140,6 +129,12 @@ continue self._tags_file_last_mtime[ tag_file ] = current_mtime + yield tag_file + + + def _AddIdentifiersFromTagFiles( self, tag_files ): + absolute_paths_to_tag_files = ycm_core.StringVector() + for tag_file in self._FilterUnchangedTagFiles( tag_files ): absolute_paths_to_tag_files.append( ToCppStringCompatible( tag_file ) ) if not absolute_paths_to_tag_files: @@ -149,39 +144,39 @@ absolute_paths_to_tag_files ) - def AddIdentifiersFromSyntax( self, keyword_list, filetypes ): + def _AddIdentifiersFromSyntax( self, keyword_list, filetype ): keyword_vector = ycm_core.StringVector() for keyword in keyword_list: keyword_vector.append( ToCppStringCompatible( keyword ) ) - filepath = SYNTAX_FILENAME + filetypes[ 0 ] + filepath = SYNTAX_FILENAME + filetype self._completer.AddIdentifiersToDatabase( keyword_vector, - ToCppStringCompatible( filetypes[ 0 ] ), + ToCppStringCompatible( filetype ), ToCppStringCompatible( filepath ) ) def OnFileReadyToParse( self, request_data ): - self.AddBufferIdentifiers( request_data ) + self._AddBufferIdentifiers( request_data ) if 'tag_files' in request_data: - self.AddIdentifiersFromTagFiles( request_data[ 'tag_files' ] ) + self._AddIdentifiersFromTagFiles( request_data[ 'tag_files' ] ) if 'syntax_keywords' in request_data: - self.AddIdentifiersFromSyntax( request_data[ 'syntax_keywords' ], - request_data[ 'filetypes' ] ) + self._AddIdentifiersFromSyntax( request_data[ 'syntax_keywords' ], + request_data[ 'first_filetype' ] ) def OnInsertLeave( self, request_data ): - self.AddIdentifierUnderCursor( request_data ) + self._AddIdentifierUnderCursor( request_data ) def OnCurrentIdentifierFinished( self, request_data ): - self.AddPreviousIdentifier( request_data ) + self._AddPreviousIdentifier( request_data ) # This looks for the previous identifier and returns it; this might mean looking # at last identifier on the previous line if a new line has just been created. def _PreviousIdentifier( min_num_candidate_size_chars, request_data ): - def PreviousIdentifierOnLine( line, column ): + def PreviousIdentifierOnLine( line, column, filetype ): nearest_ident = '' for match in identifier_utils.IdentifierRegexForFiletype( filetype ).finditer( line ): @@ -190,24 +185,28 @@ return nearest_ident line_num = request_data[ 'line_num' ] - 1 - column_num = request_data[ 'column_num' ] - 1 + column_num = request_data[ 'column_codepoint' ] - 1 filepath = request_data[ 'filepath' ] - try: - filetype = request_data[ 'filetypes' ][ 0 ] - except KeyError: - filetype = None contents_per_line = ( - request_data[ 'file_data' ][ filepath ][ 'contents' ].split( '\n' ) ) + SplitLines( request_data[ 'file_data' ][ filepath ][ 'contents' ] ) ) - ident = PreviousIdentifierOnLine( contents_per_line[ line_num ], column_num ) + filetype = request_data[ 'first_filetype' ] + ident = PreviousIdentifierOnLine( contents_per_line[ line_num ], + column_num, + filetype ) if ident: if len( ident ) < min_num_candidate_size_chars: return '' return ident - prev_line = contents_per_line[ line_num - 1 ] - ident = PreviousIdentifierOnLine( prev_line, len( prev_line ) ) + line_num = line_num - 1 + + if line_num < 0: + return '' + + prev_line = contents_per_line[ line_num ] + ident = PreviousIdentifierOnLine( prev_line, len( prev_line ), filetype ) if len( ident ) < min_num_candidate_size_chars: return '' return ident @@ -221,13 +220,10 @@ def _GetCursorIdentifier( request_data ): - try: - filetype = request_data[ 'filetypes' ][ 0 ] - except KeyError: - filetype = None - return identifier_utils.IdentifierAtIndex( request_data[ 'line_value' ], - request_data[ 'column_num' ] - 1, - filetype ) + return identifier_utils.IdentifierAtIndex( + request_data[ 'line_value' ], + request_data[ 'column_codepoint' ] - 1, + request_data[ 'first_filetype' ] ) def _IdentifiersFromBuffer( text, diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/completer.py ycmd-0+20161219+git486b809/ycmd/completers/completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -40,10 +40,56 @@ Completer. The following are functions that the Vim part of YCM will be calling on your Completer: + *Important note about unicode and byte offsets* + + Useful background: http://utf8everywhere.org + + Internally, all Python strings are unicode string objects, unless otherwise + converted to 'bytes' using ToBytes. In particular, the line_value and + file_data.contents entries in the request_data are unicode strings. + + However, offsets in the API (such as column_num and start_column) are *byte* + offsets into a utf-8 encoded version of the contents of the line or buffer. + Therefore it is *never* safe to perform 'character' arithmetic + (such as '-1' to get the previous 'character') using these byte offsets, and + they cannot *ever* be used to index into line_value or buffer contents + unicode strings. + + It is therefore important to ensure that you use the right type of offsets + for the right type of calculation: + - use codepoint offsets and a unicode string for 'character' calculations + - use byte offsets and utf-8 encoded bytes for all other manipulations + + ycmd provides the following ways of accessing the source data and offsets: + + For working with utf-8 encoded bytes: + - request_data[ 'line_bytes' ] - the line as utf-8 encoded bytes. + - request_data[ 'start_column' ] and request_data[ 'column_num' ]. + + For working with 'character' manipulations (unicode strings and codepoint + offsets): + - request_data[ 'line_value' ] - the line as a unicode string. + - request_data[ 'start_codepoint' ] and request_data[ 'column_codepoint' ]. + + For converting between the two: + - utils.ToBytes + - utils.ByteOffsetToCodepointOffset + - utils.ToUnicode + - utils.CodepointOffsetToByteOffset + + Note: The above use of codepoints for 'character' manipulations is not + strictly correct. There are unicode 'characters' which consume multiple + codepoints. However, it is currently considered viable to use a single + codepoint = a single character until such a time as we improve support for + unicode identifiers. The purpose of the above rule is to prevent crashes and + random encoding exceptions, not to fully support unicode identifiers. + + *END: Important note about unicode and byte offsets* + ShouldUseNow() is called with the start column of where a potential completion string should start and the current line (string) the cursor is on. For instance, if the user's input is 'foo.bar' and the cursor is on the 'r' in - 'bar', start_column will be the 1-based index of 'b' in the line. Your + 'bar', start_column will be the 1-based byte index of 'b' in the line. Your implementation of ShouldUseNow() should return True if your semantic completer should be used and False otherwise. @@ -101,8 +147,8 @@ custom cleanup logic on server shutdown. If your completer uses an external server process, then it can be useful to - implement the ServerIsReady member function to handle the /ready request. This - is very useful for the test suite.""" + implement the ServerIsHealthy member function to handle the /healthy request. + This is very useful for the test suite.""" def __init__( self, user_options ): self.user_options = user_options @@ -146,16 +192,19 @@ if not self.prepared_triggers: return False current_line = request_data[ 'line_value' ] - start_column = request_data[ 'start_column' ] - 1 - column_num = request_data[ 'column_num' ] - 1 + start_codepoint = request_data[ 'start_codepoint' ] - 1 + column_codepoint = request_data[ 'column_codepoint' ] - 1 filetype = self._CurrentFiletype( request_data[ 'filetypes' ] ) return self.prepared_triggers.MatchesForFiletype( - current_line, start_column, column_num, filetype ) + current_line, start_codepoint, column_codepoint, filetype ) def QueryLengthAboveMinThreshold( self, request_data ): - query_length = request_data[ 'column_num' ] - request_data[ 'start_column' ] + # Note: calculation in 'characters' not bytes. + query_length = ( request_data[ 'column_codepoint' ] - + request_data[ 'start_codepoint' ] ) + return query_length >= self.min_num_chars @@ -192,11 +241,18 @@ def ComputeCandidatesInner( self, request_data ): - pass + pass # pragma: no cover def DefinedSubcommands( self ): - return sorted( self.GetSubcommandsMap().keys() ) + subcommands = sorted( self.GetSubcommandsMap().keys() ) + try: + # We don't want expose this subcommand because it is not really needed + # for the user but it is useful in tests for tearing down the server + subcommands.remove( 'StopServer' ) + except ValueError: + pass + return subcommands def GetSubcommandsMap( self ): @@ -251,19 +307,19 @@ def OnFileReadyToParse( self, request_data ): - pass + pass # pragma: no cover def OnBufferVisit( self, request_data ): - pass + pass # pragma: no cover def OnBufferUnload( self, request_data ): - pass + pass # pragma: no cover def OnInsertLeave( self, request_data ): - pass + pass # pragma: no cover def OnUserCommand( self, arguments, request_data ): @@ -281,7 +337,7 @@ def OnCurrentIdentifierFinished( self, request_data ): - pass + pass # pragma: no cover def GetDiagnosticsForCurrentFile( self, request_data ): @@ -312,16 +368,23 @@ def Shutdown( self ): - pass + pass # pragma: no cover def ServerIsReady( self ): - """Called by the /ready handler to check if the underlying completion + return self.ServerIsHealthy() + + + def ServerIsHealthy( self ): + """Called by the /healthy handler to check if the underlying completion server is started and ready to receive requests. Returns bool.""" return True class CompletionsCache( object ): + """Completions for a particular request. Importantly, columns are byte + offsets, not unicode codepoints.""" + def __init__( self ): self._access_lock = threading.Lock() self.Invalidate() @@ -335,6 +398,7 @@ self._completions = None + # start_column is a byte offset. def Update( self, line_num, start_column, completion_type, completions ): with self._access_lock: self._line_num = line_num @@ -343,6 +407,7 @@ self._completions = completions + # start_column is a byte offset. def GetCompletionsIfCacheValid( self, line_num, start_column, completion_type ): with self._access_lock: @@ -352,6 +417,7 @@ return self._completions + # start_column is a byte offset. def _CacheValidNoLock( self, line_num, start_column, completion_type ): return ( line_num == self._line_num and start_column == self._start_column and diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/completer_utils.py ycmd-0+20161219+git486b809/ycmd/completers/completer_utils.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/completer_utils.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/completer_utils.py 2016-12-20 08:50:19.000000000 +0000 @@ -28,8 +28,9 @@ # We don't want ycm_core inside Vim. import os import re +import copy from collections import defaultdict -from ycmd.utils import ToCppStringCompatible, ToUnicode +from ycmd.utils import ToCppStringCompatible, ToUnicode, ReadFile class PreparedTriggers( object ): @@ -46,23 +47,29 @@ self._filetype_to_prepared_triggers = final_triggers - def MatchingTriggerForFiletype( self, current_line, start_column, column_num, + def MatchingTriggerForFiletype( self, + current_line, + start_codepoint, + column_codepoint, filetype ): try: triggers = self._filetype_to_prepared_triggers[ filetype ] except KeyError: return None return _MatchingSemanticTrigger( current_line, - start_column, - column_num, + start_codepoint, + column_codepoint, triggers ) - def MatchesForFiletype( self, current_line, start_column, column_num, + def MatchesForFiletype( self, + current_line, + start_codepoint, + column_codepoint, filetype ): return self.MatchingTriggerForFiletype( current_line, - start_column, - column_num, + start_codepoint, + column_codepoint, filetype ) is not None @@ -92,44 +99,53 @@ return final_dict -def _RegexTriggerMatches( trigger, line_value, start_column, column_num ): +# start_codepoint and column_codepoint are codepoint offsets in the unicode +# string line_value. +def _RegexTriggerMatches( trigger, + line_value, + start_codepoint, + column_codepoint ): for match in trigger.finditer( line_value ): - # By definition of 'start_column', we know that the character just before - # 'start_column' is not an identifier character but all characters - # between 'start_column' and 'column_num' are. This means that if our - # trigger ends with an identifier character, its tail must match between - # 'start_column' and 'column_num', 'start_column' excluded. But if it - # doesn't, its tail must match exactly at 'start_column'. Both cases are - # mutually exclusive hence the following condition. - if start_column <= match.end() and match.end() <= column_num: + # By definition of 'start_codepoint', we know that the character just before + # 'start_codepoint' is not an identifier character but all characters + # between 'start_codepoint' and 'column_codepoint' are. This means that if + # our trigger ends with an identifier character, its tail must match between + # 'start_codepoint' and 'column_codepoint', 'start_codepoint' excluded. But + # if it doesn't, its tail must match exactly at 'start_codepoint'. Both + # cases are mutually exclusive hence the following condition. + if start_codepoint <= match.end() and match.end() <= column_codepoint: return True return False -# start_column and column_num are 0-based -def _MatchingSemanticTrigger( line_value, start_column, column_num, +# start_codepoint and column_codepoint are 0-based and are codepoint offsets +# into the unicode string line_value. +def _MatchingSemanticTrigger( line_value, start_codepoint, column_codepoint, trigger_list ): - if start_column < 0 or column_num < 0: + if start_codepoint < 0 or column_codepoint < 0: return None line_length = len( line_value ) - if not line_length or start_column > line_length: + if not line_length or start_codepoint > line_length: return None # Ignore characters after user's caret column - line_value = line_value[ :column_num ] + line_value = line_value[ : column_codepoint ] for trigger in trigger_list: - if _RegexTriggerMatches( trigger, line_value, start_column, column_num ): + if _RegexTriggerMatches( trigger, + line_value, + start_codepoint, + column_codepoint ): return trigger return None -def _MatchesSemanticTrigger( line_value, start_column, column_num, +def _MatchesSemanticTrigger( line_value, start_codepoint, column_codepoint, trigger_list ): return _MatchingSemanticTrigger( line_value, - start_column, - column_num, + start_codepoint, + column_codepoint, trigger_list ) is not None @@ -155,9 +171,73 @@ def FilterAndSortCandidatesWrap( candidates, sort_property, query ): from ycm_core import FilterAndSortCandidates - return FilterAndSortCandidates( candidates, - ToCppStringCompatible( sort_property ), - ToCppStringCompatible( query ) ) + + # The c++ interface we use only understands the (*native*) 'str' type (i.e. + # not the 'str' type from python-future. If we pass it a 'unicode' or + # 'bytes' instance then various things blow up, such as converting to + # std::string. Therefore all strings passed into the c++ API must pass through + # ToCppStringCompatible (or more strictly all strings which the C++ code + # needs to use and convert. In this case, just the insertion text property) + + # FIXME: This is actually quite inefficient in an area which is used + # constantly and the key performance critical part of the system. There is + # code in the C++ layer (see PythonSupport.cpp:GetUtf8String) which attempts + # to work around this limitation. Unfortunately it has issues which cause the + # above problems, and we work around it by converting here in the python + # layer until we can come up with a better solution in the C++ layer. + + # Note: we must deep copy candidates because we do not want to clobber the + # data that is passed in. It is actually used directly by the cache, so if + # we change the data pointed to by the elements of candidates, then this will + # be reflected in a subsequent response from the cache. This is particularly + # important for those candidates which are *not* returned after the filter, as + # they are not converted back to unicode. + cpp_compatible_candidates = _ConvertCandidatesToCppCompatible( + copy.deepcopy( candidates ), + sort_property ) + + # However, the reset of the python layer expects all the candidates properties + # to be some form of unicode string - a python-future str() instance. + # So we need to convert the insertion text property back to a unicode string + # before returning it. + filtered_candidates = FilterAndSortCandidates( + cpp_compatible_candidates, + ToCppStringCompatible( sort_property ), + ToCppStringCompatible( query ) ) + + return _ConvertCandidatesToPythonCompatible( filtered_candidates, + sort_property ) + + +def _ConvertCandidatesToCppCompatible( candidates, sort_property ): + """Convert the candidates to the format expected by the C++ layer.""" + return _ConvertCandidates( candidates, sort_property, ToCppStringCompatible ) + + +def _ConvertCandidatesToPythonCompatible( candidates, sort_property ): + """Convert the candidates to the format expected by the python layer.""" + return _ConvertCandidates( candidates, sort_property, ToUnicode ) + + +def _ConvertCandidates( candidates, sort_property, converter ): + """Apply the conversion function |converter| to the logical insertion text + field within the candidates in the candidate list |candidates|. The + |sort_property| is required to determine the format of |candidates|. + + The conversion function should take a single argument (the string) and return + the converted string. It should be one of ycmd.utils.ToUnicode or + ycmd.utils.ToCppStringCompatible. + + Typically this method is not called directly, rather it is used via + _ConvertCandidatesToCppCompatible and _ConvertCandidatesToPythonCompatible.""" + + if sort_property: + for candidate in candidates: + candidate[ sort_property ] = converter( candidate[ sort_property ] ) + return candidates + + return [ converter( c ) for c in candidates ] + TRIGGER_REGEX_PREFIX = 're!' @@ -216,3 +296,16 @@ if close_char_pos != -1: include_value = line[ match.end() : close_char_pos ] return include_value, quoted_include + + +def GetFileContents( request_data, filename ): + """Returns the contents of the absolute path |filename| as a unicode + string. If the file contents exist in |request_data| (i.e. it is open and + potentially modified/dirty in the user's editor), then it is returned, + otherwise the file is read from disk (assuming a UTF-8 encoding) and its + contents returned.""" + file_data = request_data[ 'file_data' ] + if filename in file_data: + return ToUnicode( file_data[ filename ][ 'contents' ] ) + + return ToUnicode( ReadFile( filename ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/cpp/clang_completer.py ycmd-0+20161219+git486b809/ycmd/completers/cpp/clang_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/cpp/clang_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/cpp/clang_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -31,11 +31,12 @@ import textwrap from ycmd import responses from ycmd import extra_conf_store -from ycmd.utils import ToCppStringCompatible +from ycmd.utils import ToCppStringCompatible, ToUnicode from ycmd.completers.completer import Completer from ycmd.completers.completer_utils import GetIncludeStatementValue from ycmd.completers.cpp.flags import Flags, PrepareFlagsForClang from ycmd.completers.cpp.ephemeral_values_set import EphemeralValuesSet +from ycmd.responses import NoExtraConfDetected, UnknownExtraConf import xml.etree.ElementTree @@ -130,6 +131,10 @@ self._ClearCompilationFlagCache() ), 'GetType' : ( lambda self, request_data, args: self._GetSemanticInfo( request_data, func = 'GetTypeAtLocation' ) ), + 'GetTypeImprecise' : ( lambda self, request_data, args: + self._GetSemanticInfo( request_data, + func = 'GetTypeAtLocation', + reparse = False ) ), 'GetParent' : ( lambda self, request_data, args: self._GetSemanticInfo( request_data, func = 'GetEnclosingFunctionAtLocation' ) ), @@ -140,7 +145,7 @@ reparse = True, func = 'GetDocsForLocationInFile', response_builder = _BuildGetDocResponse ) ), - 'GetDocQuick' : ( lambda self, request_data, args: + 'GetDocImprecise' : ( lambda self, request_data, args: self._GetSemanticInfo( request_data, reparse = False, func = 'GetDocsForLocationInFile', @@ -333,7 +338,7 @@ def OnBufferUnload( self, request_data ): self._completer.DeleteCachesForFile( - ToCppStringCompatible( request_data[ 'unloaded_buffer' ] ) ) + ToCppStringCompatible( request_data[ 'filepath' ] ) ) def GetDetailedDiagnostic( self, request_data ): @@ -351,6 +356,9 @@ closest_diagnostic = None distance_to_closest_diagnostic = 999 + # FIXME: all of these calculations are currently working with byte + # offsets, which are technically incorrect. We should be working with + # codepoint offsets, as we want the nearest character-wise diagnostic for diagnostic in diagnostics: distance = abs( current_column - diagnostic.location_.column_number_ ) if distance < distance_to_closest_diagnostic: @@ -363,13 +371,24 @@ def DebugInfo( self, request_data ): filename = request_data[ 'filepath' ] - if not filename: - return '' - flags = self._FlagsForRequest( request_data ) or [] - source = extra_conf_store.ModuleFileForSourceFile( filename ) - return 'Flags for {0} loaded from {1}:\n{2}'.format( filename, - source, - list( flags ) ) + try: + extra_conf = extra_conf_store.ModuleFileForSourceFile( filename ) + flags = self._FlagsForRequest( request_data ) or [] + except NoExtraConfDetected: + return ( 'C-family completer debug information:\n' + ' No configuration file found' ) + except UnknownExtraConf as error: + return ( 'C-family completer debug information:\n' + ' Configuration file found but not loaded\n' + ' Configuration path: {0}'.format( + error.extra_conf_file ) ) + if not extra_conf: + return ( 'C-family completer debug information:\n' + ' No configuration file found' ) + return ( 'C-family completer debug information:\n' + ' Configuration file found and loaded\n' + ' Configuration path: {0}\n' + ' Flags: {1}'.format( extra_conf, list( flags ) ) ) def _FlagsForRequest( self, request_data ): @@ -459,7 +478,7 @@ return textwrap.dedent( '\n'.join( [ re.sub( STRIP_TRAILING_COMMENT, '', re.sub( STRIP_LEADING_COMMENT, '', line ) ) - for line in comment.splitlines() ] ) ) + for line in ToUnicode( comment ).splitlines() ] ) ) def _BuildGetDocResponse( doc_data ): @@ -485,11 +504,11 @@ return responses.BuildDetailedInfoResponse( '{0}\n{1}\nType: {2}\nName: {3}\n---\n{4}'.format( - declaration.text if declaration is not None else "", - doc_data.brief_comment, - doc_data.canonical_type, - doc_data.display_name, - _FormatRawComment( doc_data.raw_comment ) ) ) + ToUnicode( declaration.text ) if declaration is not None else "", + ToUnicode( doc_data.brief_comment ), + ToUnicode( doc_data.canonical_type ), + ToUnicode( doc_data.display_name ), + ToUnicode( _FormatRawComment( doc_data.raw_comment ) ) ) ) def _GetAbsolutePath( include_file_name, include_paths ): diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/cpp/flags.py ycmd-0+20161219+git486b809/ycmd/completers/cpp/flags.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/cpp/flags.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/cpp/flags.py 2016-12-20 08:50:19.000000000 +0000 @@ -39,11 +39,19 @@ # We need to remove --fcolor-diagnostics because it will cause shell escape # sequences to show up in editors, which is bad. See Valloric/YouCompleteMe#1421 -STATE_FLAGS_TO_SKIP = set(['-c', '-MP', '--fcolor-diagnostics']) +STATE_FLAGS_TO_SKIP = set( [ '-c', + '-MP', + '-MD', + '-MMD', + '--fcolor-diagnostics' ] ) # The -M* flags spec: # https://gcc.gnu.org/onlinedocs/gcc-4.9.0/gcc/Preprocessor-Options.html -FILE_FLAGS_TO_SKIP = set(['-MD', '-MMD', '-MF', '-MT', '-MQ', '-o']) +FILE_FLAGS_TO_SKIP = set( [ '-MF', + '-MT', + '-MQ', + '-o', + '--serialize-diagnostics' ] ) # Use a regex to correctly detect c++/c language for both versioned and # non-versioned compiler executable names suffixes @@ -91,9 +99,12 @@ if add_extra_clang_flags: flags += self.extra_clang_flags - sanitized_flags = PrepareFlagsForClang( flags, filename ) - if results[ 'do_cache' ]: + sanitized_flags = PrepareFlagsForClang( flags, + filename, + add_extra_clang_flags ) + + if results.get( 'do_cache', True ): self.flags_for_file[ filename ] = sanitized_flags return sanitized_flags @@ -164,10 +175,12 @@ return module.FlagsForFile( filename ) -def PrepareFlagsForClang( flags, filename ): - flags = _CompilerToLanguageFlag( flags ) +def PrepareFlagsForClang( flags, filename, add_extra_clang_flags = True ): + flags = _AddLanguageFlagWhenAppropriate( flags ) flags = _RemoveXclangFlags( flags ) flags = _RemoveUnusedFlags( flags, filename ) + if add_extra_clang_flags: + flags = _EnableTypoCorrection( flags ) flags = _SanitizeFlags( flags ) return flags @@ -227,22 +240,24 @@ return flags[ :-1 ] -def _CompilerToLanguageFlag( flags ): - """When flags come from the compile_commands.json file, the flag preceding - the first flag starting with a dash is usually the path to the compiler that - should be invoked. We want to replace it with a corresponding language flag. - E.g., -x c for gcc and -x c++ for g++.""" +def _AddLanguageFlagWhenAppropriate( flags ): + """When flags come from the compile_commands.json file, the flag preceding the + first flag starting with a dash is usually the path to the compiler that + should be invoked. Since LibClang does not deduce the language from the + compiler name, we explicitely set the language to C++ if the compiler is a C++ + one (g++, clang++, etc.). Otherwise, we let LibClang guess the language from + the file extension. This handles the case where the .h extension is used for + C++ headers.""" flags = _RemoveFlagsPrecedingCompiler( flags ) - # First flag is now the compiler path or a flag starting with a dash - if flags[ 0 ].startswith( '-' ): - return flags - - language = ( 'c++' if CPP_COMPILER_REGEX.search( flags[ 0 ] ) else - 'c' ) + # First flag is now the compiler path or a flag starting with a dash. + first_flag = flags[ 0 ] - return flags[ :1 ] + [ '-x', language ] + flags[ 1: ] + if ( not first_flag.startswith( '-' ) and + CPP_COMPILER_REGEX.search( first_flag ) ): + return [ first_flag, '-x', 'c++' ] + flags[ 1: ] + return flags def _RemoveUnusedFlags( flags, filename ): @@ -266,9 +281,11 @@ previous_flag_is_include = False previous_flag_starts_with_dash = False current_flag_starts_with_dash = False + for flag in flags: previous_flag_starts_with_dash = current_flag_starts_with_dash current_flag_starts_with_dash = flag.startswith( '-' ) + if skip_next: skip_next = False continue @@ -354,6 +371,7 @@ return [] + MAC_INCLUDE_PATHS = [] if OnMac(): @@ -362,13 +380,20 @@ # a Mac because if we don't, libclang would fail to find etc. This # should be fixed upstream in libclang, but until it does, we need to help # users out. - # See Valloric/YouCompleteMe#303 for details. + # See the following for details: + # - Valloric/YouCompleteMe#303 + # - Valloric/YouCompleteMe#2268 MAC_INCLUDE_PATHS = ( _PathsForAllMacToolchains( 'usr/include/c++/v1' ) + [ '/usr/local/include' ] + _PathsForAllMacToolchains( 'usr/include' ) + [ '/usr/include', '/System/Library/Frameworks', '/Library/Frameworks' ] + - _LatestMacClangIncludes() + _LatestMacClangIncludes() + + # We include the MacOS platform SDK because some meaningful parts of the + # standard library are located there. If users are compiling for (say) + # iPhone.platform, etc. they should appear earlier in the include path. + [ '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/' + 'Developer/SDKs/MacOSX.sdk/usr/include' ] ) @@ -377,9 +402,9 @@ if OnMac(): for path in MAC_INCLUDE_PATHS: flags.extend( [ '-isystem', path ] ) - # On Windows, parsing of templates is delayed until instantation time. - # This makes GetType and GetParent commands not returning the expected - # result when the cursor is in templates. + # On Windows, parsing of templates is delayed until instantiation time. + # This makes GetType and GetParent commands fail to return the expected + # result when the cursor is in a template. # Using the -fno-delayed-template-parsing flag disables this behavior. # See # http://clang.llvm.org/extra/PassByValueTransform.html#note-about-delayed-template-parsing # noqa @@ -391,6 +416,24 @@ return flags +def _EnableTypoCorrection( flags ): + """Adds the -fspell-checking flag if the -fno-spell-checking flag is not + present""" + + # "Typo correction" (aka spell checking) in clang allows it to produce + # hints (in the form of fix-its) in the case of certain diagnostics. A common + # example is "no type named 'strng' in namespace 'std'; Did you mean + # 'string'? (FixIt)". This is enabled by default in the clang driver (i.e. the + # 'clang' binary), but is not when using libclang (as we do). It's a useful + # enough feature that we just always turn it on unless the user explicitly + # turned it off in their flags (with -fno-spell-checking). + if '-fno-spell-checking' in flags: + return flags + + flags.append( '-fspell-checking' ) + return flags + + def _SpecialClangIncludes(): libclang_dir = os.path.dirname( ycm_core.__file__ ) path_to_includes = os.path.join( libclang_dir, 'clang_includes' ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/cs/cs_completer.py ycmd-0+20161219+git486b809/ycmd/completers/cs/cs_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/cs/cs_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/cs/cs_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,12 +27,12 @@ from collections import defaultdict import os -import time import re from ycmd.completers.completer import Completer -from ycmd.utils import ForceSemanticCompletion +from ycmd.utils import ForceSemanticCompletion, CodepointOffsetToByteOffset from ycmd import responses from ycmd import utils +from ycmd.completers.completer_utils import GetFileContents import requests import urllib.parse import logging @@ -44,10 +44,11 @@ '"./install.py --omnisharp-completer".' ) INVALID_FILE_MESSAGE = 'File is invalid.' NO_DIAGNOSTIC_MESSAGE = 'No diagnostic for current line!' -PATH_TO_OMNISHARP_BINARY = os.path.join( - os.path.abspath( os.path.dirname( __file__ ) ), - '..', '..', '..', 'third_party', 'OmniSharpServer', - 'OmniSharp', 'bin', 'Release', 'OmniSharp.exe' ) +PATH_TO_OMNISHARP_BINARY = os.path.abspath( + os.path.join( os.path.dirname( __file__ ), '..', '..', '..', + 'third_party', 'OmniSharpServer', 'OmniSharp', + 'bin', 'Release', 'OmniSharp.exe' ) ) +LOGFILE_FORMAT = 'omnisharp_{port}_{sln}_{std}_' class CsharpCompleter( Completer ): @@ -71,10 +72,9 @@ def Shutdown( self ): - if ( self.user_options[ 'auto_stop_csharp_server' ] ): + if self.user_options[ 'auto_stop_csharp_server' ]: for solutioncompleter in itervalues( self._completer_per_solution ): - if solutioncompleter.ServerIsRunning(): - solutioncompleter._StopServer() + solutioncompleter._StopServer() def SupportedFiletypes( self ): @@ -133,10 +133,6 @@ def GetSubcommandsMap( self ): return { - 'StartServer' : ( lambda self, request_data, args: - self._SolutionSubcommand( request_data, - method = '_StartServer', - no_request_data = True ) ), 'StopServer' : ( lambda self, request_data, args: self._SolutionSubcommand( request_data, method = '_StopServer', @@ -182,19 +178,7 @@ method = '_FixIt' ) ), 'GetDoc' : ( lambda self, request_data, args: self._SolutionSubcommand( request_data, - method = '_GetDoc' ) ), - 'ServerIsRunning' : ( lambda self, request_data, args: - self._SolutionSubcommand( request_data, - method = 'ServerIsRunning', - no_request_data = True ) ), - 'ServerIsHealthy' : ( lambda self, request_data, args: - self._SolutionSubcommand( request_data, - method = 'ServerIsHealthy', - no_request_data = True ) ), - 'ServerIsReady' : ( lambda self, request_data, args: - self._SolutionSubcommand( request_data, - method = 'ServerIsReady', - no_request_data = True ) ), + method = '_GetDoc' ) ) } @@ -212,7 +196,7 @@ # Only start the server associated to this solution if the option to # automatically start one is set and no server process is already running. if ( self.user_options[ 'auto_start_csharp_server' ] - and not solutioncompleter.ServerIsRunning() ): + and not solutioncompleter._ServerIsRunning() ): solutioncompleter._StartServer() return @@ -223,7 +207,7 @@ errors = solutioncompleter.CodeCheck( request_data ) - diagnostics = [ self._QuickFixToDiagnostic( x ) for x in + diagnostics = [ self._QuickFixToDiagnostic( request_data, x ) for x in errors[ "QuickFixes" ] ] self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) @@ -232,18 +216,26 @@ diagnostics[ : self._max_diagnostics_to_display ] ] - def _QuickFixToDiagnostic( self, quick_fix ): + def _QuickFixToDiagnostic( self, request_data, quick_fix ): filename = quick_fix[ "FileName" ] - - location = responses.Location( quick_fix[ "Line" ], - quick_fix[ "Column" ], - filename ) - location_range = responses.Range( location, location ) + # NOTE: end of diagnostic range returned by the OmniSharp server is not + # included. + location = _BuildLocation( request_data, + filename, + quick_fix[ 'Line' ], + quick_fix[ 'Column' ] ) + location_end = _BuildLocation( request_data, + filename, + quick_fix[ 'EndLine' ], + quick_fix[ 'EndColumn' ] ) + if not location_end: + location_end = location + location_extent = responses.Range( location, location_end ) return responses.Diagnostic( list(), location, - location_range, - quick_fix[ "Text" ], - quick_fix[ "LogLevel" ].upper() ) + location_extent, + quick_fix[ 'Text' ], + quick_fix[ 'LogLevel' ].upper() ) def GetDetailedDiagnostic( self, request_data ): @@ -261,6 +253,9 @@ closest_diagnostic = None distance_to_closest_diagnostic = 999 + # FIXME: all of these calculations are currently working with byte + # offsets, which are technically incorrect. We should be working with + # codepoint offsets, as we want the nearest character-wise diagnostic for diagnostic in diagnostics: distance = abs( current_column - diagnostic.location_.column_number_ ) if distance < distance_to_closest_diagnostic: @@ -272,15 +267,50 @@ def DebugInfo( self, request_data ): - solutioncompleter = self._GetSolutionCompleter( request_data ) - if solutioncompleter.ServerIsRunning(): - return ( 'OmniSharp Server running at: {0}\n' - 'OmniSharp logfiles:\n{1}\n{2}' ).format( - solutioncompleter._ServerLocation(), - solutioncompleter._filename_stdout, - solutioncompleter._filename_stderr ) - else: - return 'OmniSharp Server is not running' + try: + completer = self._GetSolutionCompleter( request_data ) + except RuntimeError: + return ( + 'C# completer debug information:\n' + ' OmniSharp not running\n' + ' OmniSharp executable: {0}\n' + ' OmniSharp solution: not found'.format( PATH_TO_OMNISHARP_BINARY ) ) + + with completer._server_state_lock: + if completer._ServerIsRunning(): + return ( + 'C# completer debug information:\n' + ' OmniSharp running at: {0}\n' + ' OmniSharp process ID: {1}\n' + ' OmniSharp executable: {2}\n' + ' OmniSharp logfiles:\n' + ' {3}\n' + ' {4}\n' + ' OmniSharp solution: {5}'.format( completer._ServerLocation(), + completer._omnisharp_phandle.pid, + PATH_TO_OMNISHARP_BINARY, + completer._filename_stdout, + completer._filename_stderr, + completer._solution_path ) ) + + if completer._filename_stdout and completer._filename_stderr: + return ( + 'C# completer debug information:\n' + ' OmniSharp no longer running\n' + ' OmniSharp executable: {0}\n' + ' OmniSharp logfiles:\n' + ' {1}\n' + ' {2}\n' + ' OmniSharp solution: {3}'.format( PATH_TO_OMNISHARP_BINARY, + completer._filename_stdout, + completer._filename_stderr, + completer._solution_path ) ) + + return ( 'C# completer debug information:\n' + ' OmniSharp is not running\n' + ' OmniSharp executable: {0}\n' + ' OmniSharp solution: {1}'.format( PATH_TO_OMNISHARP_BINARY, + completer._solution_path ) ) def ServerIsHealthy( self ): @@ -296,7 +326,7 @@ def _CheckAllRunning( self, action ): solutioncompleters = itervalues( self._completer_per_solution ) return all( action( completer ) for completer in solutioncompleters - if completer.ServerIsRunning() ) + if completer._ServerIsRunning() ) def _GetSolutionFile( self, filepath ): @@ -305,7 +335,7 @@ # to be confirmed path_to_solutionfile = solutiondetection.FindSolutionPath( filepath ) if not path_to_solutionfile: - raise RuntimeError( 'Autodetection of solution file failed. \n' ) + raise RuntimeError( 'Autodetection of solution file failed.' ) self._solution_for_file[ filepath ] = path_to_solutionfile return self._solution_for_file[ filepath ] @@ -337,7 +367,7 @@ """ Start the OmniSharp server if not already running. Use a lock to avoid starting the server multiple times for the same solution. """ with self._server_state_lock: - if self.ServerIsRunning(): + if self._ServerIsRunning(): return self._logger.info( 'Starting OmniSharp server' ) @@ -360,14 +390,15 @@ if utils.OnCygwin(): command.extend( [ '--client-path-mode', 'Cygwin' ] ) - filename_format = os.path.join( utils.PathToCreatedTempDir(), - u'omnisharp_{port}_{sln}_{std}.log' ) - solutionfile = os.path.basename( path_to_solutionfile ) - self._filename_stdout = filename_format.format( - port = self._omnisharp_port, sln = solutionfile, std = 'stdout' ) - self._filename_stderr = filename_format.format( - port = self._omnisharp_port, sln = solutionfile, std = 'stderr' ) + self._filename_stdout = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._omnisharp_port, + sln = solutionfile, + std = 'stdout' ) ) + self._filename_stderr = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._omnisharp_port, + sln = solutionfile, + std = 'stderr' ) ) with utils.OpenForStdHandle( self._filename_stderr ) as fstderr: with utils.OpenForStdHandle( self._filename_stdout ) as fstdout: @@ -380,43 +411,30 @@ def _StopServer( self ): """ Stop the OmniSharp server using a lock. """ with self._server_state_lock: - if not self.ServerIsRunning(): - return - - self._logger.info( 'Stopping OmniSharp server' ) - - self._TryToStopServer() - - # Kill it if it's still up - if self.ServerIsRunning(): - self._logger.info( 'Killing OmniSharp server' ) - self._omnisharp_phandle.kill() - - self._CleanupAfterServerStop() + if self._ServerIsRunning(): + self._logger.info( 'Stopping OmniSharp server with PID {0}'.format( + self._omnisharp_phandle.pid ) ) + self._GetResponse( '/stopserver' ) + try: + utils.WaitUntilProcessIsTerminated( self._omnisharp_phandle, + timeout = 5 ) + self._logger.info( 'OmniSharp server stopped' ) + except RuntimeError: + self._logger.exception( 'Error while stopping OmniSharp server' ) - self._logger.info( 'Stopped OmniSharp server' ) + self._CleanUp() - def _TryToStopServer( self ): - for _ in range( 5 ): - try: - self._GetResponse( '/stopserver', timeout = .1 ) - except: - pass - for _ in range( 10 ): - if not self.ServerIsRunning(): - return - time.sleep( .1 ) - - - def _CleanupAfterServerStop( self ): + def _CleanUp( self ): self._omnisharp_port = None self._omnisharp_phandle = None - if ( not self._keep_logfiles ): + if not self._keep_logfiles: if self._filename_stdout: - os.unlink( self._filename_stdout ) + utils.RemoveIfExists( self._filename_stdout ) + self._filename_stdout = None if self._filename_stderr: - os.unlink( self._filename_stderr ) + utils.RemoveIfExists( self._filename_stderr ) + self._filename_stderr = None def _RestartServer( self ): @@ -451,9 +469,11 @@ definition = self._GetResponse( '/gotodefinition', self._DefaultParameters( request_data ) ) if definition[ 'FileName' ] is not None: - return responses.BuildGoToResponse( definition[ 'FileName' ], - definition[ 'Line' ], - definition[ 'Column' ] ) + return responses.BuildGoToResponseFromLocation( + _BuildLocation( request_data, + definition[ 'FileName' ], + definition[ 'Line' ], + definition[ 'Column' ] ) ) else: raise RuntimeError( 'Can\'t jump to definition' ) @@ -466,14 +486,18 @@ if implementation[ 'QuickFixes' ]: if len( implementation[ 'QuickFixes' ] ) == 1: - return responses.BuildGoToResponse( + return responses.BuildGoToResponseFromLocation( + _BuildLocation( + request_data, implementation[ 'QuickFixes' ][ 0 ][ 'FileName' ], implementation[ 'QuickFixes' ][ 0 ][ 'Line' ], - implementation[ 'QuickFixes' ][ 0 ][ 'Column' ] ) + implementation[ 'QuickFixes' ][ 0 ][ 'Column' ] ) ) else: - return [ responses.BuildGoToResponse( x[ 'FileName' ], - x[ 'Line' ], - x[ 'Column' ] ) + return [ responses.BuildGoToResponseFromLocation( + _BuildLocation( request_data, + x[ 'FileName' ], + x[ 'Line' ], + x[ 'Column' ] ) ) for x in implementation[ 'QuickFixes' ] ] else: if ( fallback_to_declaration ): @@ -499,9 +523,11 @@ result = self._GetResponse( '/fixcodeissue', request ) replacement_text = result[ "Text" ] - location = responses.Location( request_data['line_num'], - request_data['column_num'], - request_data['filepath'] ) + # Note: column_num is already a byte offset so we don't need to use + # _BuildLocation. + location = responses.Location( request_data[ 'line_num' ], + request_data[ 'column_num' ], + request_data[ 'filepath' ] ) fixits = [ responses.FixIt( location, _BuildChunks( request_data, replacement_text ) ) ] @@ -525,7 +551,8 @@ """ Some very common request parameters """ parameters = {} parameters[ 'line' ] = request_data[ 'line_num' ] - parameters[ 'column' ] = request_data[ 'column_num' ] + parameters[ 'column' ] = request_data[ 'column_codepoint' ] + filepath = request_data[ 'filepath' ] parameters[ 'buffer' ] = ( request_data[ 'file_data' ][ filepath ][ 'contents' ] ) @@ -533,14 +560,14 @@ return parameters - def ServerIsRunning( self ): + def _ServerIsRunning( self ): """ Check if our OmniSharp server is running (process is up).""" return utils.ProcessIsRunning( self._omnisharp_phandle ) def ServerIsHealthy( self ): """ Check if our OmniSharp server is healthy (up and serving).""" - if not self.ServerIsRunning(): + if not self._ServerIsRunning(): return False try: @@ -551,7 +578,7 @@ def ServerIsReady( self ): """ Check if our OmniSharp server is ready (loaded solution file).""" - if not self.ServerIsRunning(): + if not self._ServerIsRunning(): return False try: @@ -631,6 +658,9 @@ ( start_line, start_column ) = _IndexToLineColumn( old_buffer, start_index ) ( end_line, end_column ) = _IndexToLineColumn( old_buffer, old_length - end_index ) + + # No need for _BuildLocation, because _IndexToLineColumn already converted + # start_column and end_column to byte offsets for us. start = responses.Location( start_line, start_column, filepath ) end = responses.Location( end_line, end_column, filepath ) return [ responses.FixItChunk( replacement_text, @@ -651,11 +681,24 @@ # Adapted from http://stackoverflow.com/a/24495900 def _IndexToLineColumn( text, index ): - """Get (line_number, col) of `index` in `string`.""" + """Get 1-based (line_number, col) of `index` in `string`, where string is a + unicode string and col is a byte offset.""" lines = text.splitlines( True ) curr_pos = 0 for linenum, line in enumerate( lines ): if curr_pos + len( line ) > index: - return linenum + 1, index - curr_pos + 1 + return ( linenum + 1, + CodepointOffsetToByteOffset( line, index - curr_pos + 1 ) ) curr_pos += len( line ) assert False + + +def _BuildLocation( request_data, filename, line_num, column_num ): + if line_num <= 0 or column_num <= 0: + return None + contents = utils.SplitLines( GetFileContents( request_data, filename ) ) + line_value = contents[ line_num - 1 ] + return responses.Location( + line_num, + CodepointOffsetToByteOffset( line_value, column_num ), + filename ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/general/filename_completer.py ycmd-0+20161219+git486b809/ycmd/completers/general/filename_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/general/filename_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/general/filename_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -33,7 +33,7 @@ GetIncludeStatementValue ) from ycmd.completers.cpp.clang_completer import InCFamilyFile from ycmd.completers.cpp.flags import Flags -from ycmd.utils import ToUnicode, OnWindows +from ycmd.utils import GetCurrentDirectory, OnWindows, ToUnicode from ycmd import responses EXTRA_INFO_MAP = { 1 : '[File]', 2 : '[Dir]', 3 : '[File&Dir]' } @@ -79,20 +79,26 @@ def ShouldCompleteIncludeStatement( self, request_data ): - start_column = request_data[ 'start_column' ] - 1 + start_codepoint = request_data[ 'start_codepoint' ] - 1 current_line = request_data[ 'line_value' ] filepath = request_data[ 'filepath' ] filetypes = request_data[ 'file_data' ][ filepath ][ 'filetypes' ] return ( InCFamilyFile( filetypes ) and - AtIncludeStatementStart( current_line[ :start_column ] ) ) + AtIncludeStatementStart( current_line[ : start_codepoint ] ) ) def ShouldUseNowInner( self, request_data ): - start_column = request_data[ 'start_column' ] - 1 current_line = request_data[ 'line_value' ] - return ( start_column and - ( current_line[ start_column - 1 ] in self._triggers or - self.ShouldCompleteIncludeStatement( request_data ) ) ) + start_codepoint = request_data[ 'start_codepoint' ] + + # inspect the previous 'character' from the start column to find the trigger + # note: 1-based still. we subtract 1 when indexing into current_line + trigger_codepoint = start_codepoint - 1 + + return ( + trigger_codepoint > 0 and + ( current_line[ trigger_codepoint - 1 ] in self._triggers or + self.ShouldCompleteIncludeStatement( request_data ) ) ) def SupportedFiletypes( self ): @@ -101,10 +107,10 @@ def ComputeCandidatesInner( self, request_data ): current_line = request_data[ 'line_value' ] - start_column = request_data[ 'start_column' ] - 1 + start_codepoint = request_data[ 'start_codepoint' ] - 1 filepath = request_data[ 'filepath' ] filetypes = request_data[ 'file_data' ][ filepath ][ 'filetypes' ] - line = current_line[ :start_column ] + line = current_line[ : start_codepoint ] if InCFamilyFile( filetypes ): path_dir, quoted_include = ( @@ -178,7 +184,7 @@ if working_dir: return os.path.join( working_dir, path_dir ) else: - return os.path.join( os.getcwd(), path_dir ) + return os.path.join( GetCurrentDirectory(), path_dir ) else: # Return paths relative to the file return os.path.join( os.path.join( os.path.dirname( filepath ) ), diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/go/go_completer.py ycmd-0+20161219+git486b809/ycmd/completers/go/go_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/go/go_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/go/go_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,23 +27,29 @@ import logging import os import subprocess +import threading from ycmd import responses from ycmd import utils from ycmd.utils import ToBytes, ToUnicode, ExecutableName from ycmd.completers.completer import Completer -BINARY_NOT_FOUND_MESSAGE = ( '{0} binary not found. Did you build it? ' + - 'You can do so by running ' + +BINARY_NOT_FOUND_MESSAGE = ( '{0} binary not found. Did you build it? ' + 'You can do so by running ' '"./install.py --gocode-completer".' ) -SHELL_ERROR_MESSAGE = '{0} shell call failed.' -PARSE_ERROR_MESSAGE = 'Gocode returned invalid JSON response.' -NO_COMPLETIONS_MESSAGE = 'Gocode returned empty JSON response.' -GOCODE_PANIC_MESSAGE = ( 'Gocode panicked trying to find completions, ' + +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.' ) -DIR_OF_THIRD_PARTY = os.path.join( - os.path.abspath( os.path.dirname( __file__ ) ), - '..', '..', '..', 'third_party' ) + +DIR_OF_THIRD_PARTY = os.path.abspath( + os.path.join( os.path.dirname( __file__ ), '..', '..', '..', 'third_party' ) ) GO_BINARIES = dict( { 'gocode': os.path.join( DIR_OF_THIRD_PARTY, 'gocode', @@ -53,11 +59,13 @@ ExecutableName( 'godef' ) ) } ) +LOGFILE_FORMAT = 'gocode_{port}_{std}_' + _logger = logging.getLogger( __name__ ) def FindBinary( binary, user_options ): - """ Find the path to the gocode/godef binary. + """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 @@ -67,7 +75,7 @@ specified, use it as an absolute path. If the resolved binary exists, return the path, - otherwise return None. """ + otherwise return None.""" def _FindPath(): key = '{0}_binary_path'.format( binary ) @@ -92,12 +100,26 @@ 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""" def __init__( self, user_options ): super( GoCompleter, self ).__init__( user_options ) - self._popener = utils.SafePopen # Overridden in test. - self._binary_gocode = FindBinary( 'gocode', user_options ) - self._binary_godef = FindBinary( 'godef', 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_address = 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' ] + + self._StartServer() def SupportedFiletypes( self ): @@ -106,109 +128,169 @@ def ComputeCandidatesInner( self, request_data ): filename = request_data[ 'filepath' ] - _logger.info( "gocode completion request %s" % filename ) - if not filename: - return + _logger.info( 'Gocode completion request {0}'.format( filename ) ) contents = utils.ToBytes( request_data[ 'file_data' ][ filename ][ 'contents' ] ) - offset = _ComputeOffset( contents, request_data[ 'line_num' ], - request_data[ 'column_num' ] ) - stdoutdata = self._ExecuteBinary( self._binary_gocode, - '-f=json', 'autocomplete', - filename, - str( offset ), - contents = 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' ] ) + + stdoutdata = self._ExecuteCommand( [ self._gocode_binary_path, + '-sock', 'tcp', + '-addr', self._gocode_address, + '-f=json', 'autocomplete', + filename, str( offset ) ], + contents = contents ) try: resultdata = json.loads( ToUnicode( stdoutdata ) ) except ValueError: - _logger.error( PARSE_ERROR_MESSAGE ) - raise RuntimeError( PARSE_ERROR_MESSAGE ) + _logger.error( GOCODE_PARSE_ERROR_MESSAGE ) + raise RuntimeError( GOCODE_PARSE_ERROR_MESSAGE ) if len( resultdata ) != 2: - _logger.error( NO_COMPLETIONS_MESSAGE ) - raise RuntimeError( NO_COMPLETIONS_MESSAGE ) + _logger.error( GOCODE_NO_COMPLETIONS_MESSAGE ) + raise RuntimeError( GOCODE_NO_COMPLETIONS_MESSAGE ) for result in resultdata[ 1 ]: - if result.get( 'class' ) == "PANIC": + if result.get( 'class' ) == 'PANIC': raise RuntimeError( GOCODE_PANIC_MESSAGE ) - return [ _ConvertCompletionData( x ) for x in resultdata[1] ] + return [ _ConvertCompletionData( x ) for x in resultdata[ 1 ] ] def GetSubcommandsMap( self ): return { - 'StartServer': ( lambda self, request_data, args: self._StartServer() ), - 'StopServer': ( lambda self, request_data, args: self._StopServer() ), - 'GoTo' : ( lambda self, request_data, args: - self._GoToDefinition( request_data ) ), + '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: + 'GoToDeclaration': ( lambda self, request_data, args: self._GoToDefinition( request_data ) ), } - def OnFileReadyToParse( self, request_data ): - self._StartServer() - + 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 ) - def Shutdown( self ): - self._StopServer() + return stdoutdata def _StartServer( self ): - """ Start the GoCode server """ - self._ExecuteBinary( self._binary_gocode ) + """Start the Gocode server.""" + with self._gocode_lock: + _logger.info( 'Starting Gocode server' ) + + self._gocode_port = utils.GetUnusedLocalhostPort() + self._gocode_address = '127.0.0.1:{0}'.format( self._gocode_port ) + + command = [ self._gocode_binary_path, + '-s', + '-sock', 'tcp', + '-addr', self._gocode_address ] + + 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 """ - _logger.info( 'Stopping GoCode server' ) - self._ExecuteBinary( self._binary_gocode, 'close' ) - - - def _ExecuteBinary( self, binary, *args, **kwargs): - """ Execute the GoCode/GoDef binary with given arguments. Use the contents - argument to send data to GoCode. Return the standard output. """ - popen_handle = self._popener( - [ binary ] + list( args ), stdin = subprocess.PIPE, - stdout = subprocess.PIPE, stderr = subprocess.PIPE ) - contents = kwargs[ 'contents' ] if 'contents' in kwargs else None - stdoutdata, stderrdata = popen_handle.communicate( contents ) - - if popen_handle.returncode: - binary_str = 'Godef' if binary == self._binary_godef else 'Gocode' - - _logger.error( SHELL_ERROR_MESSAGE.format( binary_str ) + - " code %i stderr: %s", - popen_handle.returncode, stderrdata) - raise RuntimeError( SHELL_ERROR_MESSAGE.format( binary_str ) ) - - return stdoutdata + """Stop the Gocode server.""" + with self._gocode_lock: + if self._ServerIsRunning(): + _logger.info( 'Stopping Gocode server with PID {0}'.format( + self._gocode_handle.pid ) ) + self._ExecuteCommand( [ self._gocode_binary_path, + '-sock', 'tcp', + '-addr', self._gocode_address, + 'close' ] ) + try: + utils.WaitUntilProcessIsTerminated( self._gocode_handle, timeout = 5 ) + _logger.info( 'Gocode server stopped' ) + except RuntimeError: + _logger.exception( 'Error while stopping Gocode server' ) + + self._CleanUp() + + + def _CleanUp( self ): + self._gocode_handle = None + self._gocode_port = None + self._gocode_address = 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 {0}'.format( filename ) ) + + contents = utils.ToBytes( + request_data[ 'file_data' ][ filename ][ 'contents' ] ) + offset = _ComputeOffset( contents, + request_data[ 'line_num' ], + request_data[ 'column_num' ] ) try: - filename = request_data[ 'filepath' ] - _logger.info( "godef GoTo request %s" % filename ) - if not filename: - return - contents = utils.ToBytes( - request_data[ 'file_data' ][ filename ][ 'contents' ] ) - offset = _ComputeOffset( contents, request_data[ 'line_num' ], - request_data[ 'column_num' ] ) - stdout = self._ExecuteBinary( self._binary_godef, - "-i", - "-f=%s" % filename, - '-json', - "-o=%s" % offset, - contents = contents ) - return self._ConstructGoToFromResponse( stdout ) - except Exception as e: - _logger.exception( e ) - raise RuntimeError( 'Can\'t jump to definition.' ) + stdout = self._ExecuteCommand( [ self._godef_binary_path, + '-i', + '-f={0}'.format( filename ), + '-json', + '-o={0}'.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 as error: + _logger.exception( error ) + raise RuntimeError( 'Can\'t find a definition.' ) + + return self._ConstructGoToFromResponse( stdout ) def _ConstructGoToFromResponse( self, response_str ): @@ -220,24 +302,91 @@ raise RuntimeError( 'Can\'t jump to definition.' ) -# Compute the byte offset in the file given the line and column. -# TODO(ekfriis): If this is slow, consider moving this to C++ ycm_core, -# perhaps in RequestWrap. -def _ComputeOffset( contents, line, col ): + 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 ): + """Check if the Gocode server is healthy (up and serving).""" + if not self._ServerIsRunning(): + return False + + try: + self._ExecuteCommand( [ self._gocode_binary_path, + '-sock', 'tcp', + '-addr', self._gocode_address, + 'status' ] ) + return True + # We catch this exception type and not a more specific one because we + # raise it in _ExecuteCommand when the command fails. + except RuntimeError as error: + _logger.exception( error ) + return False + + + def ServerIsReady( self ): + """Check if the Gocode server is ready. Same as the healthy status.""" + return self.ServerIsHealthy() + + + def DebugInfo( self, request_data ): + with self._gocode_lock: + if self._ServerIsRunning(): + return ( 'Go completer debug information:\n' + ' Gocode running at: http://{0}\n' + ' Gocode process ID: {1}\n' + ' Gocode executable: {2}\n' + ' Gocode logfiles:\n' + ' {3}\n' + ' {4}\n' + ' Godef executable: {5}'.format( self._gocode_address, + self._gocode_handle.pid, + self._gocode_binary_path, + self._gocode_stdout, + self._gocode_stderr, + self._godef_binary_path ) ) + + if self._gocode_stdout and self._gocode_stderr: + return ( 'Go completer debug information:\n' + ' Gocode no longer running\n' + ' Gocode executable: {0}\n' + ' Gocode logfiles:\n' + ' {1}\n' + ' {2}\n' + ' Godef executable: {3}'.format( self._gocode_binary_path, + self._gocode_stdout, + self._gocode_stderr, + self._godef_binary_path ) ) + + return ( 'Go completer debug information:\n' + ' Gocode is not running\n' + ' Gocode executable: {0}\n' + ' Godef executable: {1}'.format( self._gocode_binary_path, + self._godef_binary_path ) ) + + +def _ComputeOffset( contents, line, column ): + """Compute the byte offset in the file given the line and column.""" contents = ToBytes( contents ) - curline = 1 - curcol = 1 + current_line = 1 + current_column = 1 newline = bytes( b'\n' )[ 0 ] for i, byte in enumerate( contents ): - if curline == line and curcol == col: + if current_line == line and current_column == column: return i - curcol += 1 + current_column += 1 if byte == newline: - curline += 1 - curcol = 1 - _logger.error( 'GoCode completer - could not compute byte offset ' + - 'corresponding to L%i C%i', line, col ) - return -1 + current_line += 1 + current_column = 1 + message = COMPUTE_OFFSET_ERROR_MESSAGE.format( line = line, + column = column ) + _logger.error( message ) + raise RuntimeError( message ) def _ConvertCompletionData( completion_data ): diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/javascript/tern_completer.py ycmd-0+20161219+git486b809/ycmd/completers/javascript/tern_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/javascript/tern_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/javascript/tern_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -24,20 +24,19 @@ from future import standard_library standard_library.install_aliases() -import http.client import logging import os import requests import threading -import traceback from subprocess import PIPE from ycmd import utils, responses from ycmd.completers.completer import Completer +from ycmd.completers.completer_utils import GetFileContents _logger = logging.getLogger( __name__ ) -PATH_TO_TERNJS_BINARY = os.path.abspath( +PATH_TO_TERN_BINARY = os.path.abspath( os.path.join( os.path.dirname( __file__ ), '..', @@ -58,6 +57,8 @@ # address. (ahem: Windows) SERVER_HOST = '127.0.0.1' +LOGFILE_FORMAT = 'tern_{port}_{std}_' + def ShouldEnableTernCompleter(): """Returns whether or not the tern completer is 'installed'. That is whether @@ -71,11 +72,11 @@ _logger.info( 'Using node binary from: ' + PATH_TO_NODE ) - installed = os.path.exists( PATH_TO_TERNJS_BINARY ) + installed = os.path.exists( PATH_TO_TERN_BINARY ) if not installed: _logger.info( 'Not using Tern completer: not installed at ' + - PATH_TO_TERNJS_BINARY ) + PATH_TO_TERN_BINARY ) return False return True @@ -89,10 +90,14 @@ def FindTernProjectFile( starting_directory ): + """Finds the path to either a Tern project file or the user's global Tern + configuration file. If found, a tuple is returned containing the path and a + boolean indicating if the path is to a .tern-project file. If not found, + returns (None, False).""" for folder in utils.PathsToAllParentFolders( starting_directory ): tern_project = os.path.join( folder, '.tern-project' ) if os.path.exists( tern_project ): - return tern_project + return ( tern_project, True ) # As described here: http://ternjs.net/doc/manual.html#server a global # .tern-config file is also supported for the Tern server. This can provide @@ -102,9 +107,9 @@ # to be anything other than annoying. tern_config = os.path.expanduser( '~/.tern-config' ) if GlobalConfigExists( tern_config ): - return tern_config + return ( tern_config, False ) - return None + return ( None, False ) class TernCompleter( Completer ): @@ -118,15 +123,22 @@ self._server_keep_logfiles = user_options[ 'server_keep_logfiles' ] # Used to ensure that starting/stopping of the server is synchronised - self._server_state_mutex = threading.Lock() + self._server_state_mutex = threading.RLock() self._do_tern_project_check = False - with self._server_state_mutex: - self._server_stdout = None - self._server_stderr = None - self._Reset() - self._StartServerNoLock() + # Used to determine the absolute path of files returned by the tern server. + # When a .tern_project file exists, paths are returned relative to it. + # Otherwise, they are returned relative to the working directory of the tern + # server. + self._server_paths_relative_to = None + + self._server_handle = None + self._server_port = None + self._server_stdout = None + self._server_stderr = None + + self._StartServer() def _WarnIfMissingTernProject( self ): @@ -144,17 +156,28 @@ if self._ServerIsRunning() and self._do_tern_project_check: self._do_tern_project_check = False - tern_project = FindTernProjectFile( os.getcwd() ) + current_dir = utils.GetCurrentDirectory() + ( tern_project, is_project ) = FindTernProjectFile( current_dir ) if not tern_project: - _logger.warning( 'No .tern-project file detected: ' + os.getcwd() ) + _logger.warning( 'No .tern-project file detected: ' + current_dir ) raise RuntimeError( 'Warning: Unable to detect a .tern-project file ' - 'in the hierarchy before ' + os.getcwd() + + 'in the hierarchy before ' + current_dir + ' and no global .tern-config file was found. ' 'This is required for accurate JavaScript ' 'completion. Please see the User Guide for ' 'details.' ) else: - _logger.info( 'Detected .tern-project file at: ' + tern_project ) + _logger.info( 'Detected Tern configuration file at: ' + tern_project ) + + # Paths are relative to the project file if it exists, otherwise they + # are relative to the working directory of Tern server (which is the + # same as the working directory of ycmd). + self._server_paths_relative_to = ( + os.path.dirname( tern_project ) if is_project else current_dir ) + + _logger.info( 'Tern paths are relative to: ' + + self._server_paths_relative_to ) + def _GetServerAddress( self ): @@ -176,6 +199,7 @@ } completions = self._GetResponse( query, + request_data[ 'start_codepoint' ], request_data ).get( 'completions', [] ) def BuildDoc( completion ): @@ -207,8 +231,8 @@ def GetSubcommandsMap( self ): return { - 'StartServer': ( lambda self, request_data, args: - self._StartServer() ), + 'RestartServer': ( lambda self, request_data, args: + self._RestartServer() ), 'StopServer': ( lambda self, request_data, args: self._StopServer() ), 'GoToDefinition': ( lambda self, request_data, args: @@ -231,28 +255,33 @@ def DebugInfo( self, request_data ): - if self._server_handle is None: - # server is not running because we haven't tried to start it. - return ' * Tern server is not running' - - if not self._ServerIsRunning(): - # The handle is set, but the process isn't running. This means either it - # crashed or we failed to start it. - return ( ' * Tern server is not running (crashed)' - + '\n * Server stdout: ' - + self._server_stdout - + '\n * Server stderr: ' - + self._server_stderr ) - - # Server is up and running. - return ( ' * Tern server is running on port: ' - + str( self._server_port ) - + ' with PID: ' - + str( self._server_handle.pid ) - + '\n * Server stdout: ' - + self._server_stdout - + '\n * Server stderr: ' - + self._server_stderr ) + with self._server_state_mutex: + if self._ServerIsRunning(): + return ( 'JavaScript completer debug information:\n' + ' Tern running at: {0}\n' + ' Tern process ID: {1}\n' + ' Tern executable: {2}\n' + ' Tern logfiles:\n' + ' {3}\n' + ' {4}'.format( self._GetServerAddress(), + self._server_handle.pid, + PATH_TO_TERN_BINARY, + self._server_stdout, + self._server_stderr ) ) + + if self._server_stdout and self._server_stderr: + return ( 'JavaScript completer debug information:\n' + ' Tern no longer running\n' + ' Tern executable: {0}\n' + ' Tern logfiles:\n' + ' {1}\n' + ' {2}\n'.format( PATH_TO_TERN_BINARY, + self._server_stdout, + self._server_stderr ) ) + + return ( 'JavaScript completer debug information:\n' + ' Tern is not running\n' + ' Tern executable: {0}'.format( PATH_TO_TERN_BINARY ) ) def Shutdown( self ): @@ -260,33 +289,18 @@ self._StopServer() - def ServerIsReady( self, request_data = {} ): + def ServerIsHealthy( self, request_data = {} ): if not self._ServerIsRunning(): return False try: target = self._GetServerAddress() + '/ping' response = requests.get( target ) - return response.status_code == http.client.OK + return response.status_code == requests.codes.ok except requests.ConnectionError: return False - def _Reset( self ): - """Callers must hold self._server_state_mutex""" - - if not self._server_keep_logfiles: - if self._server_stdout: - utils.RemoveIfExists( self._server_stdout ) - if self._server_stderr: - utils.RemoveIfExists( self._server_stderr ) - - self._server_handle = None - self._server_port = 0 - self._server_stdout = None - self._server_stderr = None - - def _PostRequest( self, request, request_data ): """Send a raw request with the supplied request block, and return the server's response. If the server is not running, it is started. @@ -319,13 +333,13 @@ response = requests.post( self._GetServerAddress(), json = full_request ) - if response.status_code != http.client.OK: + if response.status_code != requests.codes.ok: raise RuntimeError( response.text ) return response.json() - def _GetResponse( self, query, request_data ): + def _GetResponse( self, query, codepoint, request_data ): """Send a standard file/line request with the supplied query block, and return the server's response. If the server is not running, it is started. @@ -333,12 +347,16 @@ just updating file data in which case _PostRequest should be used directly. The query block should contain the type and any parameters. The files, - position, etc. are added automatically.""" + position, etc. are added automatically. + + NOTE: the |codepoint| parameter is usually the current cursor position, + though it should be the "completion start column" codepoint for completion + requests.""" def MakeTernLocation( request_data ): return { 'line': request_data[ 'line_num' ] - 1, - 'ch': request_data[ 'start_column' ] - 1 + 'ch': codepoint - 1 } full_query = { @@ -351,101 +369,112 @@ return self._PostRequest( { 'query': full_query }, request_data ) - def _StartServer( self ): - if not self._ServerIsRunning(): - with self._server_state_mutex: - self._StartServerNoLock() + def _ServerPathToAbsolute( self, path ): + """Given a path returned from the tern server, return it as an absolute + path. + In particular, if the path is a relative path, return an absolute path + assuming that it is relative to the location of the .tern-project file.""" + if os.path.isabs( path ): + return path - def _StartServerNoLock( self ): - """Start the server, under the lock. + return os.path.join( self._server_paths_relative_to, path ) - Callers must hold self._server_state_mutex""" - if self._ServerIsRunning(): - return + # TODO: this function is way too long. Consider refactoring it. + def _StartServer( self ): + with self._server_state_mutex: + if self._ServerIsRunning(): + return - _logger.info( 'Starting Tern.js server...' ) + _logger.info( 'Starting Tern server...' ) - self._server_port = utils.GetUnusedLocalhostPort() + self._server_port = utils.GetUnusedLocalhostPort() - if _logger.isEnabledFor( logging.DEBUG ): - extra_args = [ '--verbose' ] - else: - extra_args = [] + if _logger.isEnabledFor( logging.DEBUG ): + extra_args = [ '--verbose' ] + else: + extra_args = [] - command = [ PATH_TO_NODE, - PATH_TO_TERNJS_BINARY, - '--port', - str( self._server_port ), - '--host', - SERVER_HOST, - '--persistent', - '--no-port-file' ] + extra_args + command = [ PATH_TO_NODE, + PATH_TO_TERN_BINARY, + '--port', + str( self._server_port ), + '--host', + SERVER_HOST, + '--persistent', + '--no-port-file' ] + extra_args + + _logger.debug( 'Starting tern with the following command: ' + + ' '.join( command ) ) + + try: + self._server_stdout = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._server_port, std = 'stdout' ) ) + + self._server_stderr = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._server_port, std = 'stderr' ) ) + + # We need to open a pipe to stdin or the Tern server is killed. + # See https://github.com/ternjs/tern/issues/740#issuecomment-203979749 + # For unknown reasons, this is only needed on Windows and for Python + # 3.4+ on other platforms. + with utils.OpenForStdHandle( self._server_stdout ) as stdout: + with utils.OpenForStdHandle( self._server_stderr ) as stderr: + self._server_handle = utils.SafePopen( command, + stdin = PIPE, + stdout = stdout, + stderr = stderr ) + except Exception: + _logger.exception( 'Unable to start Tern server' ) + self._CleanUp() + + if self._server_port and self._ServerIsRunning(): + _logger.info( 'Tern Server started with pid: ' + + str( self._server_handle.pid ) + + ' listening on port ' + + str( self._server_port ) ) + _logger.info( 'Tern Server log files are: ' + + self._server_stdout + + ' and ' + + self._server_stderr ) - _logger.debug( 'Starting tern with the following command: ' - + ' '.join( command ) ) + self._do_tern_project_check = True + else: + _logger.warning( 'Tern server did not start successfully' ) - try: - logfile_format = os.path.join( utils.PathToCreatedTempDir(), - u'tern_{port}_{std}.log' ) - self._server_stdout = logfile_format.format( - port = self._server_port, - std = 'stdout' ) - - self._server_stderr = logfile_format.format( - port = self._server_port, - std = 'stderr' ) - - # On Windows, we need to open a pipe to stdin to prevent Tern crashing - # with following error: "Implement me. Unknown stdin file type!" - with utils.OpenForStdHandle( self._server_stdout ) as stdout: - with utils.OpenForStdHandle( self._server_stderr ) as stderr: - self._server_handle = utils.SafePopen( command, - stdin_windows = PIPE, - stdout = stdout, - stderr = stderr ) - except Exception: - _logger.warning( 'Unable to start Tern.js server: ' - + traceback.format_exc() ) - self._Reset() - - if self._server_port > 0 and self._ServerIsRunning(): - _logger.info( 'Tern.js Server started with pid: ' + - str( self._server_handle.pid ) + - ' listening on port ' + - str( self._server_port ) ) - _logger.info( 'Tern.js Server log files are: ' + - self._server_stdout + - ' and ' + - self._server_stderr ) - - self._do_tern_project_check = True - else: - _logger.warning( 'Tern.js server did not start successfully' ) + def _RestartServer( self ): + with self._server_state_mutex: + self._StopServer() + self._StartServer() def _StopServer( self ): with self._server_state_mutex: - self._StopServerNoLock() - + if self._ServerIsRunning(): + _logger.info( 'Stopping Tern server with PID {0}'.format( + self._server_handle.pid ) ) + self._server_handle.terminate() + try: + utils.WaitUntilProcessIsTerminated( self._server_handle, + timeout = 5 ) + _logger.info( 'Tern server stopped' ) + except RuntimeError: + _logger.exception( 'Error while stopping Tern server' ) - def _StopServerNoLock( self ): - """Stop the server, under the lock. + self._CleanUp() - Callers must hold self._server_state_mutex""" - if self._ServerIsRunning(): - _logger.info( 'Stopping Tern.js server with PID ' - + str( self._server_handle.pid ) - + '...' ) - self._server_handle.terminate() - self._server_handle.wait() - - _logger.info( 'Tern.js server terminated.' ) - - self._Reset() + def _CleanUp( self ): + utils.CloseStandardStreams( self._server_handle ) + self._server_handle = None + self._server_port = None + if not self._server_keep_logfiles: + utils.RemoveIfExists( self._server_stdout ) + self._server_stdout = None + utils.RemoveIfExists( self._server_stderr ) + self._server_stderr = None def _ServerIsRunning( self ): @@ -457,7 +486,9 @@ 'type': 'type', } - response = self._GetResponse( query, request_data ) + response = self._GetResponse( query, + request_data[ 'column_codepoint' ], + request_data ) return responses.BuildDisplayMessageResponse( response[ 'type' ] ) @@ -473,7 +504,9 @@ 'types': True } - response = self._GetResponse( query, request_data ) + response = self._GetResponse( query, + request_data[ 'column_codepoint' ], + request_data ) doc_string = 'Name: {name}\nType: {type}\n\n{doc}'.format( name = response.get( 'name', 'Unknown' ), @@ -488,13 +521,17 @@ 'type': 'definition', } - response = self._GetResponse( query, request_data ) - - return responses.BuildGoToResponse( - response[ 'file' ], - response[ 'start' ][ 'line' ] + 1, - response[ 'start' ][ 'ch' ] + 1 - ) + response = self._GetResponse( query, + request_data[ 'column_codepoint' ], + request_data ) + + filepath = self._ServerPathToAbsolute( response[ 'file' ] ) + return responses.BuildGoToResponseFromLocation( + _BuildLocation( + utils.SplitLines( GetFileContents( request_data, filepath ) ), + filepath, + response[ 'start' ][ 'line' ], + response[ 'start' ][ 'ch' ] ) ) def _GoToReferences( self, request_data ): @@ -502,12 +539,20 @@ 'type': 'refs', } - response = self._GetResponse( query, request_data ) + response = self._GetResponse( query, + request_data[ 'column_codepoint' ], + request_data ) + + def BuildRefResponse( ref ): + filepath = self._ServerPathToAbsolute( ref[ 'file' ] ) + return responses.BuildGoToResponseFromLocation( + _BuildLocation( + utils.SplitLines( GetFileContents( request_data, filepath ) ), + filepath, + ref[ 'start' ][ 'line' ], + ref[ 'start' ][ 'ch' ] ) ) - return [ responses.BuildGoToResponse( ref[ 'file' ], - ref[ 'start' ][ 'line' ] + 1, - ref[ 'start' ][ 'ch' ] + 1 ) - for ref in response[ 'refs' ] ] + return [ BuildRefResponse( ref ) for ref in response[ 'refs' ] ] def _Rename( self, request_data, args ): @@ -520,19 +565,21 @@ 'newName': args[ 0 ], } - response = self._GetResponse( query, request_data ) + response = self._GetResponse( query, + request_data[ 'column_codepoint' ], + request_data ) # Tern response format: # 'changes': [ # { - # 'file' + # 'file' (potentially relative path) # 'start' { # 'line' - # 'ch' + # 'ch' (codepoint offset) # } # 'end' { # 'line' - # 'ch' + # 'ch' (codepoint offset) # } # 'text' # } @@ -548,13 +595,13 @@ # 'range' (Range) { # 'start_' (Location): { # 'line_number_', - # 'column_number_', - # 'filename_' + # 'column_number_', (byte offset) + # 'filename_' (note: absolute path!) # }, # 'end_' (Location): { # 'line_number_', - # 'column_number_', - # 'filename_' + # 'column_number_', (byte offset) + # 'filename_' (note: absolute path!) # } # } # } @@ -562,26 +609,33 @@ # 'location' (Location) { # 'line_number_', # 'column_number_', - # 'filename_' + # 'filename_' (note: absolute path!) # } # # ] # } - def BuildRange( filename, start, end ): + + def BuildRange( file_contents, filename, start, end ): return responses.Range( - responses.Location( start[ 'line' ] + 1, - start[ 'ch' ] + 1, - filename ), - responses.Location( end[ 'line' ] + 1, - end[ 'ch' ] + 1, - filename ) ) + _BuildLocation( file_contents, + filename, + start[ 'line' ], + start[ 'ch' ] ), + _BuildLocation( file_contents, + filename, + end[ 'line' ], + end[ 'ch' ] ) ) def BuildFixItChunk( change ): + filepath = self._ServerPathToAbsolute( change[ 'file' ] ) + file_contents = utils.SplitLines( + GetFileContents( request_data, filepath ) ) return responses.FixItChunk( change[ 'text' ], - BuildRange( os.path.abspath( change[ 'file'] ), + BuildRange( file_contents, + filepath, change[ 'start' ], change[ 'end' ] ) ) @@ -595,3 +649,13 @@ request_data[ 'column_num' ], request_data[ 'filepath' ] ), [ BuildFixItChunk( x ) for x in response[ 'changes' ] ] ) ] ) + + +def _BuildLocation( file_contents, filename, line, ch ): + # tern returns codepoint offsets, but we need byte offsets, so we must + # convert + return responses.Location( + line = line + 1, + column = utils.CodepointOffsetToByteOffset( file_contents[ line ], + ch + 1 ), + filename = os.path.realpath( filename ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/python/jedi_completer.py ycmd-0+20161219+git486b809/ycmd/completers/python/jedi_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/python/jedi_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/python/jedi_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -45,11 +45,10 @@ JEDIHTTP_HMAC_HEADER = 'x-jedihttp-hmac' BINARY_NOT_FOUND_MESSAGE = ( 'The specified python interpreter {0} ' + 'was not found. Did you specify it correctly?' ) -LOG_FILENAME_FORMAT = os.path.join( utils.PathToCreatedTempDir(), - u'jedihttp_{port}_{std}.log' ) -PATH_TO_JEDIHTTP = os.path.join( os.path.abspath( os.path.dirname( __file__ ) ), - '..', '..', '..', - 'third_party', 'JediHTTP', 'jedihttp.py' ) +LOGFILE_FORMAT = 'jedihttp_{port}_{std}_' +PATH_TO_JEDIHTTP = os.path.abspath( + os.path.join( os.path.dirname( __file__ ), '..', '..', '..', + 'third_party', 'JediHTTP', 'jedihttp.py' ) ) class JediCompleter( Completer ): @@ -77,16 +76,12 @@ def _UpdatePythonBinary( self, binary ): if binary: - if not self._CheckBinaryExists( binary ): + resolved_binary = utils.FindExecutable( binary ) + if not resolved_binary: msg = BINARY_NOT_FOUND_MESSAGE.format( binary ) self._logger.error( msg ) raise RuntimeError( msg ) - self._python_binary_path = binary - - - def _CheckBinaryExists( self, binary ): - """This method is here to help testing""" - return os.path.isfile( binary ) + self._python_binary_path = resolved_binary def SupportedFiletypes( self ): @@ -95,15 +90,14 @@ def Shutdown( self ): - if self.ServerIsRunning(): - self._StopServer() + self._StopServer() - def ServerIsReady( self ): + def ServerIsHealthy( self ): """ Check if JediHTTP is alive AND ready to serve requests. """ - if not self.ServerIsRunning(): + if not self._ServerIsRunning(): self._logger.debug( 'JediHTTP not running.' ) return False try: @@ -113,10 +107,10 @@ return False - def ServerIsRunning( self ): + def _ServerIsRunning( self ): """ Check if JediHTTP is alive. That doesn't necessarily mean it's ready to - serve requests; that's checked by ServerIsReady. + serve requests; that's checked by ServerIsHealthy. """ with self._server_lock: return ( bool( self._jedihttp_port ) and @@ -134,15 +128,28 @@ def _StopServer( self ): with self._server_lock: - self._logger.info( 'Stopping JediHTTP' ) - if self._jedihttp_phandle: + if self._ServerIsRunning(): + self._logger.info( 'Stopping JediHTTP server with PID {0}'.format( + self._jedihttp_phandle.pid ) ) self._jedihttp_phandle.terminate() - self._jedihttp_phandle = None - self._jedihttp_port = None + try: + utils.WaitUntilProcessIsTerminated( self._jedihttp_phandle, + timeout = 5 ) + self._logger.info( 'JediHTTP server stopped' ) + except RuntimeError: + self._logger.exception( 'Error while stopping JediHTTP server' ) + + self._CleanUp() - if not self._keep_logfiles: - utils.RemoveIfExists( self._logfile_stdout ) - utils.RemoveIfExists( self._logfile_stderr ) + + def _CleanUp( self ): + self._jedihttp_phandle = None + self._jedihttp_port = None + if not self._keep_logfiles: + utils.RemoveIfExists( self._logfile_stdout ) + self._logfile_stdout = None + utils.RemoveIfExists( self._logfile_stderr ) + self._logfile_stderr = None def _StartServer( self ): @@ -162,15 +169,16 @@ command = [ self._python_binary_path, PATH_TO_JEDIHTTP, '--port', str( self._jedihttp_port ), + '--log', self._GetLoggingLevel(), '--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' ) + self._logfile_stdout = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._jedihttp_port, std = 'stdout' ) ) + self._logfile_stderr = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = self._jedihttp_port, std = 'stderr' ) ) - with utils.OpenForStdHandle( self._logfile_stderr ) as logerr: - with utils.OpenForStdHandle( self._logfile_stdout ) as logout: + with utils.OpenForStdHandle( self._logfile_stdout ) as logout: + with utils.OpenForStdHandle( self._logfile_stderr ) as logerr: self._jedihttp_phandle = utils.SafePopen( command, stdout = logout, stderr = logerr ) @@ -180,6 +188,13 @@ return os.urandom( HMAC_SECRET_LENGTH ) + def _GetLoggingLevel( self ): + # Tests are run with the NOTSET logging level but JediHTTP only accepts the + # predefined levels above (DEBUG, INFO, WARNING, etc.). + log_level = max( self._logger.getEffectiveLevel(), logging.DEBUG ) + return logging.getLevelName( log_level ).lower() + + def _GetResponse( self, handler, request_data = {} ): """POST JSON data to JediHTTP server and return JSON response.""" handler = ToBytes( handler ) @@ -218,8 +233,9 @@ 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 + # JediHTTP (as Jedi itself) expects columns to start at 0, not 1, and for + # them to be unicode codepoint offsets. + col = request_data[ 'start_codepoint' ] - 1 return { 'source': source, @@ -260,14 +276,6 @@ 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 ): return { 'GoToDefinition' : ( lambda self, request_data, args: @@ -378,20 +386,38 @@ def DebugInfo( self, request_data ): - with self._server_lock: - if self.ServerIsRunning(): - return ( 'JediHTTP running at 127.0.0.1:{0}\n' - ' python binary: {1}\n' - ' stdout log: {2}\n' - ' stderr log: {3}' ).format( self._jedihttp_port, - self._python_binary_path, - 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' + with self._server_lock: + if self._ServerIsRunning(): + return ( 'Python completer debug information:\n' + ' JediHTTP running at: http://127.0.0.1:{0}\n' + ' JediHTTP process ID: {1}\n' + ' JediHTTP executable: {2}\n' + ' JediHTTP logfiles:\n' + ' {3}\n' + ' {4}\n' + ' Python interpreter: {5}'.format( + self._jedihttp_port, + self._jedihttp_phandle.pid, + PATH_TO_JEDIHTTP, + self._logfile_stdout, + self._logfile_stderr, + self._python_binary_path ) ) + + if self._logfile_stdout and self._logfile_stderr: + return ( 'Python completer debug information:\n' + ' JediHTTP no longer running\n' + ' JediHTTP executable: {0}\n' + ' JediHTTP logfiles:\n' + ' {1}\n' + ' {2}\n' + ' Python interpreter: {3}'.format( + PATH_TO_JEDIHTTP, + self._logfile_stdout, + self._logfile_stderr, + self._python_binary_path ) ) + + return ( 'Python completer debug information:\n' + ' JediHTTP is not running\n' + ' JediHTTP executable: {0}\n' + ' Python interpreter: {1}'.format( PATH_TO_JEDIHTTP, + self._python_binary_path ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/rust/rust_completer.py ycmd-0+20161219+git486b809/ycmd/completers/rust/rust_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/rust/rust_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/rust/rust_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -31,7 +31,6 @@ import logging import urllib.parse import requests -import http.client import json import tempfile import base64 @@ -43,9 +42,8 @@ _logger = logging.getLogger( __name__ ) -DIR_OF_THIS_SCRIPT = p.dirname( p.abspath( __file__ ) ) -DIR_OF_THIRD_PARTY = p.join( DIR_OF_THIS_SCRIPT, '..', '..', '..', - 'third_party' ) +DIR_OF_THIRD_PARTY = p.abspath( + p.join( p.dirname( __file__ ), '..', '..', '..', 'third_party' ) ) RACERD_BINARY_NAME = 'racerd' + ( '.exe' if utils.OnWindows() else '' ) RACERD_BINARY_RELEASE = p.join( DIR_OF_THIRD_PARTY, 'racerd', 'target', @@ -56,14 +54,18 @@ RACERD_HMAC_HEADER = 'x-racerd-hmac' HMAC_SECRET_LENGTH = 16 -BINARY_NOT_FOUND_MESSAGE = ( 'racerd binary not found. Did you build it? ' + - 'You can do so by running ' + - '"./build.py --racer-completer".' ) +BINARY_NOT_FOUND_MESSAGE = ( + 'racerd binary not found. Did you build it? ' + 'You can do so by running "./build.py --racer-completer".' ) +NON_EXISTING_RUST_SOURCES_PATH_MESSAGE = ( + 'Rust sources path does not exist. Check the value of the rust_src_path ' + 'option or the RUST_SRC_PATH environment variable.' ) ERROR_FROM_RACERD_MESSAGE = ( 'Received error from racerd while retrieving completions. You did not ' 'set the rust_src_path option, which is probably causing this issue. ' - 'See YCM docs for details.' -) + 'See YCM docs for details.' ) + +LOGFILE_FORMAT = 'racerd_{port}_{std}_' def FindRacerdBinary( user_options ): @@ -101,7 +103,7 @@ def __init__( self, user_options ): super( RustCompleter, self ).__init__( user_options ) - self._racerd = FindRacerdBinary( user_options ) + self._racerd_binary = FindRacerdBinary( user_options ) self._racerd_host = None self._server_state_lock = threading.RLock() self._keep_logfiles = user_options[ 'server_keep_logfiles' ] @@ -111,8 +113,11 @@ if not self._rust_source_path: _logger.warning( 'No path provided for the rustc source. Please set the ' 'rust_src_path option' ) + elif not p.isdir( self._rust_source_path ): + _logger.error( NON_EXISTING_RUST_SOURCES_PATH_MESSAGE ) + raise RuntimeError( NON_EXISTING_RUST_SOURCES_PATH_MESSAGE ) - if not self._racerd: + if not self._racerd_binary: _logger.error( BINARY_NOT_FOUND_MESSAGE ) raise RuntimeError( BINARY_NOT_FOUND_MESSAGE ) @@ -172,7 +177,7 @@ response.raise_for_status() - if response.status_code is http.client.NO_CONTENT: + if response.status_code == requests.codes.no_content: return None return response.json() @@ -265,7 +270,7 @@ # racerd will delete the secret_file after it's done reading it with tempfile.NamedTemporaryFile( delete = False ) as secret_file: secret_file.write( self._hmac_secret ) - args = [ self._racerd, 'serve', + args = [ self._racerd_binary, 'serve', '--port', str( port ), '-l', '--secret-file', secret_file.name ] @@ -277,13 +282,10 @@ if self._rust_source_path: args.extend( [ '--rust-src-path', self._rust_source_path ] ) - filename_format = p.join( utils.PathToCreatedTempDir(), - 'racerd_{port}_{std}.log' ) - - self._server_stdout = filename_format.format( port = port, - std = 'stdout' ) - self._server_stderr = filename_format.format( port = port, - std = 'stderr' ) + self._server_stdout = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = port, std = 'stdout' ) ) + self._server_stderr = utils.CreateLogfile( + LOGFILE_FORMAT.format( port = port, std = 'stderr' ) ) with utils.OpenForStdHandle( self._server_stderr ) as fstderr: with utils.OpenForStdHandle( self._server_stdout ) as fstdout: @@ -293,26 +295,26 @@ env = env ) self._racerd_host = 'http://127.0.0.1:{0}'.format( port ) - if not self.ServerIsRunning(): + if not self._ServerIsRunning(): raise RuntimeError( 'Failed to start racerd!' ) _logger.info( 'Racerd started on: ' + self._racerd_host ) - def ServerIsRunning( self ): + def _ServerIsRunning( self ): """ Check if racerd is alive. That doesn't necessarily mean it's ready to serve - requests; that's checked by ServerIsReady. + requests; that's checked by ServerIsHealthy. """ with self._server_state_lock: return ( bool( self._racerd_host ) and ProcessIsRunning( self._racerd_phandle ) ) - def ServerIsReady( self ): + def ServerIsHealthy( self ): """ Check if racerd is alive AND ready to serve requests. """ - if not self.ServerIsRunning(): + if not self._ServerIsRunning(): _logger.debug( 'Racerd not running.' ) return False try: @@ -329,28 +331,36 @@ def _StopServer( self ): with self._server_state_lock: if self._racerd_phandle: + _logger.info( 'Stopping Racerd with PID {0}'.format( + self._racerd_phandle.pid ) ) self._racerd_phandle.terminate() - self._racerd_phandle.wait() - self._racerd_phandle = None - self._racerd_host = None - - if not self._keep_logfiles: - # Remove stdout log - if self._server_stdout and p.exists( self._server_stdout ): - os.unlink( self._server_stdout ) - self._server_stdout = None - - # Remove stderr log - if self._server_stderr and p.exists( self._server_stderr ): - os.unlink( self._server_stderr ) - self._server_stderr = None + try: + utils.WaitUntilProcessIsTerminated( self._racerd_phandle, + timeout = 5 ) + _logger.info( 'Racerd stopped' ) + except RuntimeError: + _logger.exception( 'Error while stopping Racerd' ) + + self._CleanUp() + + + def _CleanUp( self ): + self._racerd_phandle = None + self._racerd_host = None + if not self._keep_logfiles: + if self._server_stdout: + utils.RemoveIfExists( self._server_stdout ) + self._server_stdout = None + if self._server_stderr: + utils.RemoveIfExists( self._server_stderr ) + self._server_stderr = None def _RestartServer( self ): _logger.debug( 'RustCompleter restarting racerd' ) with self._server_state_lock: - if self.ServerIsRunning(): + if self._ServerIsRunning(): self._StopServer() self._StartServer() @@ -369,6 +379,8 @@ self._StopServer() ), 'RestartServer' : ( lambda self, request_data, args: self._RestartServer() ), + 'GetDoc' : ( lambda self, request_data, args: + self._GetDoc( request_data ) ), } @@ -384,6 +396,17 @@ raise RuntimeError( 'Can\'t jump to definition.' ) + def _GetDoc( self, request_data ): + try: + definition = self._GetResponse( '/find_definition', + request_data ) + + docs = [ definition[ 'context' ], definition[ 'docs' ] ] + return responses.BuildDetailedInfoResponse( '\n---\n'.join( docs ) ) + except Exception as e: + _logger.exception( e ) + raise RuntimeError( 'Can\'t lookup docs.' ) + def Shutdown( self ): self._StopServer() @@ -394,22 +417,35 @@ def DebugInfo( self, request_data ): with self._server_state_lock: - if self.ServerIsRunning(): - return ( 'racerd\n' - ' listening at: {0}\n' - ' racerd path: {1}\n' - ' stdout log: {2}\n' - ' stderr log: {3}').format( self._racerd_host, - self._racerd, - self._server_stdout, - self._server_stderr ) + if self._ServerIsRunning(): + return ( 'Rust completer debug information:\n' + ' Racerd running at: {0}\n' + ' Racerd process ID: {1}\n' + ' Racerd executable: {2}\n' + ' Racerd logfiles:\n' + ' {3}\n' + ' {4}\n' + ' Rust sources: {5}'.format( self._racerd_host, + self._racerd_phandle.pid, + self._racerd_binary, + self._server_stdout, + self._server_stderr, + self._rust_source_path ) ) if self._server_stdout and self._server_stderr: - return ( 'racerd is no longer running\n', - ' racerd path: {0}\n' - ' stdout log: {1}\n' - ' stderr log: {2}').format( self._racerd, - self._server_stdout, - self._server_stderr ) - - return 'racerd is not running' + return ( 'Rust completer debug information:\n' + ' Racerd no longer running\n' + ' Racerd executable: {0}\n' + ' Racerd logfiles:\n' + ' {1}\n' + ' {2}\n' + ' Rust sources: {3}'.format( self._racerd_binary, + self._server_stdout, + self._server_stderr, + self._rust_source_path ) ) + + return ( 'Rust completer debug information:\n' + ' Racerd is not running\n' + ' Racerd executable: {0}\n' + ' Rust sources: {1}'.format( self._racerd_binary, + self._rust_source_path ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/typescript/hook.py ycmd-0+20161219+git486b809/ycmd/completers/typescript/hook.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/typescript/hook.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/typescript/hook.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,8 +23,12 @@ standard_library.install_aliases() from builtins import * # noqa -from ycmd.completers.typescript.typescript_completer import TypeScriptCompleter +from ycmd.completers.typescript.typescript_completer import ( + ShouldEnableTypescriptCompleter, TypeScriptCompleter ) def GetCompleter( user_options ): + if not ShouldEnableTypescriptCompleter(): + return None + return TypeScriptCompleter( user_options ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/completers/typescript/typescript_completer.py ycmd-0+20161219+git486b809/ycmd/completers/typescript/typescript_completer.py --- ycmd-0+20160327+gitc3e6904/ycmd/completers/typescript/typescript_completer.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/completers/typescript/typescript_completer.py 2016-12-20 08:50:19.000000000 +0000 @@ -30,22 +30,26 @@ import re import subprocess import itertools +import threading -from threading import Thread -from threading import Event -from threading import Lock from tempfile import NamedTemporaryFile from ycmd import responses from ycmd import utils from ycmd.completers.completer import Completer +from ycmd.completers.completer_utils import GetFileContents -BINARY_NOT_FOUND_MESSAGE = ( 'tsserver not found. ' - 'TypeScript 1.5 or higher is required' ) +BINARY_NOT_FOUND_MESSAGE = ( 'TSServer not found. ' + 'TypeScript 1.5 or higher is required.' ) +SERVER_NOT_RUNNING_MESSAGE = 'TSServer is not running.' MAX_DETAILED_COMPLETIONS = 100 RESPONSE_TIMEOUT_SECONDS = 10 +PATH_TO_TSSERVER = utils.FindExecutable( 'tsserver' ) + +LOGFILE_FORMAT = 'tsserver_' + _logger = logging.getLogger( __name__ ) @@ -55,7 +59,7 @@ """ def __init__( self, timeout = RESPONSE_TIMEOUT_SECONDS ): - self._event = Event() + self._event = threading.Event() self._message = None self._timeout = timeout @@ -76,6 +80,16 @@ return self._message[ 'body' ] +def ShouldEnableTypescriptCompleter(): + if not PATH_TO_TSSERVER: + _logger.error( BINARY_NOT_FOUND_MESSAGE ) + return False + + _logger.info( 'Using TSServer located at {0}'.format( PATH_TO_TSSERVER ) ) + + return True + + class TypeScriptCompleter( Completer ): """ Completer for TypeScript. @@ -90,44 +104,32 @@ def __init__( self, user_options ): super( TypeScriptCompleter, self ).__init__( user_options ) + self._logfile = None + + self._tsserver_handle = None + # Used to prevent threads from concurrently writing to # the tsserver process' stdin - self._writelock = Lock() - - binarypath = utils.PathToFirstExistingExecutable( [ 'tsserver' ] ) - if not binarypath: - _logger.error( BINARY_NOT_FOUND_MESSAGE ) - raise RuntimeError( BINARY_NOT_FOUND_MESSAGE ) - - self._logfile = _LogFileName() - tsserver_log = '-file {path} -level {level}'.format( path = self._logfile, - level = _LogLevel() ) - # TSServer get the configuration for the log file through the environment - # variable 'TSS_LOG'. This seems to be undocumented but looking at the - # source code it seems like this is the way: - # https://github.com/Microsoft/TypeScript/blob/8a93b489454fdcbdf544edef05f73a913449be1d/src/server/server.ts#L136 - self._environ = os.environ.copy() - utils.SetEnviron( self._environ, 'TSS_LOG', tsserver_log ) + self._write_lock = threading.Lock() # Each request sent to tsserver must have a sequence id. # Responses contain the id sent in the corresponding request. self._sequenceid = itertools.count() # Used to prevent threads from concurrently accessing the sequence counter - self._sequenceid_lock = Lock() + self._sequenceid_lock = threading.Lock() + + self._server_lock = threading.RLock() + + # Used to read response only if TSServer is running. + self._tsserver_is_running = threading.Event() + + # Start a thread to read response from TSServer. + self._thread = threading.Thread( target = self._ReaderLoop, args = () ) + self._thread.daemon = True + self._thread.start() - # TSServer ignores the fact that newlines are two characters on Windows - # (\r\n) instead of one on other platforms (\n), so we use the - # universal_newlines option to convert those newlines to \n. See the issue - # https://github.com/Microsoft/TypeScript/issues/3403 - # TODO: remove this option when the issue is fixed. - # We also need to redirect the error stream to the output one on Windows. - self._tsserver_handle = utils.SafePopen( binarypath, - stdout = subprocess.PIPE, - stdin = subprocess.PIPE, - stderr = subprocess.STDOUT, - env = self._environ, - universal_newlines = True ) + self._StartServer() # Used to map sequence id's to their corresponding DeferredResponse # objects. The reader loop uses this to hand out responses. @@ -135,16 +137,38 @@ # Used to prevent threads from concurrently reading and writing to # the pending response dictionary - self._pendinglock = Lock() - - # Start a thread to read response from TSServer. - self._thread = Thread( target = self._ReaderLoop, args = () ) - self._thread.daemon = True - self._thread.start() + self._pending_lock = threading.Lock() _logger.info( 'Enabling typescript completion' ) + def _StartServer( self ): + with self._server_lock: + if self._ServerIsRunning(): + return + + self._logfile = utils.CreateLogfile( LOGFILE_FORMAT ) + tsserver_log = '-file {path} -level {level}'.format( path = self._logfile, + level = _LogLevel() ) + # TSServer gets the configuration for the log file through the + # environment variable 'TSS_LOG'. This seems to be undocumented but + # looking at the source code it seems like this is the way: + # https://github.com/Microsoft/TypeScript/blob/8a93b489454fdcbdf544edef05f73a913449be1d/src/server/server.ts#L136 + environ = os.environ.copy() + utils.SetEnviron( environ, 'TSS_LOG', tsserver_log ) + + _logger.info( 'TSServer log file: {0}'.format( self._logfile ) ) + + # We need to redirect the error stream to the output one on Windows. + self._tsserver_handle = utils.SafePopen( PATH_TO_TSSERVER, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + env = environ ) + + self._tsserver_is_running.set() + + def _ReaderLoop( self ): """ Read responses from TSServer and use them to resolve @@ -152,26 +176,30 @@ """ while True: + self._tsserver_is_running.wait() + try: message = self._ReadMessage() - - # We ignore events for now since we don't have a use for them. - msgtype = message[ 'type' ] - if msgtype == 'event': - eventname = message[ 'event' ] - _logger.info( 'Recieved {0} event from tsserver'.format( eventname ) ) - continue - if msgtype != 'response': - _logger.error( 'Unsuported message type {0}'.format( msgtype ) ) - continue - - seq = message[ 'request_seq' ] - with self._pendinglock: - if seq in self._pending: - self._pending[ seq ].resolve( message ) - del self._pending[ seq ] - except Exception as e: - _logger.error( 'ReaderLoop error: {0}'.format( str( e ) ) ) + except RuntimeError: + _logger.exception( SERVER_NOT_RUNNING_MESSAGE ) + self._tsserver_is_running.clear() + continue + + # We ignore events for now since we don't have a use for them. + msgtype = message[ 'type' ] + if msgtype == 'event': + eventname = message[ 'event' ] + _logger.info( 'Received {0} event from tsserver'.format( eventname ) ) + continue + if msgtype != 'response': + _logger.error( 'Unsupported message type {0}'.format( msgtype ) ) + continue + + seq = message[ 'request_seq' ] + with self._pending_lock: + if seq in self._pending: + self._pending[ seq ].resolve( message ) + del self._pending[ seq ] def _ReadMessage( self ): @@ -184,7 +212,7 @@ headerline = self._tsserver_handle.stdout.readline().strip() if not headerline: break - key, value = headerline.split( ':', 1 ) + key, value = utils.ToUnicode( headerline ).split( ':', 1 ) headers[ key.strip() ] = value.strip() # The response message is a JSON object which comes back on one line. @@ -193,7 +221,14 @@ if 'Content-Length' not in headers: raise RuntimeError( "Missing 'Content-Length' header" ) contentlength = int( headers[ 'Content-Length' ] ) - return json.loads( self._tsserver_handle.stdout.read( contentlength ) ) + # TSServer adds a newline at the end of the response message and counts it + # as one character (\n) towards the content length. However, newlines are + # two characters on Windows (\r\n), so we need to take care of that. See + # issue https://github.com/Microsoft/TypeScript/issues/3403 + content = self._tsserver_handle.stdout.read( contentlength ) + if utils.OnWindows() and content.endswith( b'\r' ): + content += self._tsserver_handle.stdout.read( 1 ) + return json.loads( utils.ToUnicode( content ) ) def _BuildRequest( self, command, arguments = None ): @@ -211,6 +246,20 @@ return request + def _WriteRequest( self, request ): + """Write a request to TSServer stdin.""" + + serialized_request = utils.ToBytes( json.dumps( request ) + '\n' ) + with self._write_lock: + try: + self._tsserver_handle.stdin.write( serialized_request ) + self._tsserver_handle.stdin.flush() + # IOError is an alias of OSError in Python 3. + except ( AttributeError, IOError ): + _logger.exception( SERVER_NOT_RUNNING_MESSAGE ) + raise RuntimeError( SERVER_NOT_RUNNING_MESSAGE ) + + def _SendCommand( self, command, arguments = None ): """ Send a request message to TSServer but don't wait for the response. @@ -219,10 +268,7 @@ """ request = self._BuildRequest( command, arguments ) - with self._writelock: - self._tsserver_handle.stdin.write( json.dumps( request ) ) - self._tsserver_handle.stdin.write( "\n" ) - self._tsserver_handle.stdin.flush() + self._WriteRequest( request ) def _SendRequest( self, command, arguments = None ): @@ -233,13 +279,10 @@ request = self._BuildRequest( command, arguments ) deferred = DeferredResponse() - with self._pendinglock: + with self._pending_lock: seq = request[ 'seq' ] self._pending[ seq ] = deferred - with self._writelock: - self._tsserver_handle.stdin.write( json.dumps( request ) ) - self._tsserver_handle.stdin.write( "\n" ) - self._tsserver_handle.stdin.flush() + self._WriteRequest( request ) return deferred.result() @@ -258,7 +301,16 @@ 'file': filename, 'tmpfile': tmpfile.name } ) - os.unlink( tmpfile.name ) + utils.RemoveIfExists( tmpfile.name ) + + + def _ServerIsRunning( self ): + with self._server_lock: + return utils.ProcessIsRunning( self._tsserver_handle ) + + + def ServerIsHealthy( self ): + return self._ServerIsRunning() def SupportedFiletypes( self ): @@ -270,7 +322,7 @@ entries = self._SendRequest( 'completions', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ] + 'offset': request_data[ 'start_codepoint' ] } ) # A less detailed version of the completion data is returned @@ -288,7 +340,7 @@ detailed_entries = self._SendRequest( 'completionEntryDetails', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ], + 'offset': request_data[ 'start_codepoint' ], 'entryNames': names } ) return [ _ConvertDetailedCompletionData( e, namelength ) @@ -297,10 +349,16 @@ def GetSubcommandsMap( self ): return { + 'RestartServer' : ( lambda self, request_data, args: + self._RestartServer( request_data ) ), + 'StopServer' : ( lambda self, request_data, args: + self._StopServer() ), 'GoToDefinition' : ( lambda self, request_data, args: self._GoToDefinition( request_data ) ), 'GoToReferences' : ( lambda self, request_data, args: self._GoToReferences( request_data ) ), + 'GoToType' : ( lambda self, request_data, args: + self._GoToType( request_data ) ), 'GetType' : ( lambda self, request_data, args: self._GetType( request_data ) ), 'GetDoc' : ( lambda self, request_data, args: @@ -330,15 +388,16 @@ filespans = self._SendRequest( 'definition', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ] + 'offset': request_data[ 'column_codepoint' ] } ) span = filespans[ 0 ] - return responses.BuildGoToResponse( - filepath = span[ 'file' ], - line_num = span[ 'start' ][ 'line' ], - column_num = span[ 'start' ][ 'offset' ] - ) + return responses.BuildGoToResponseFromLocation( + _BuildLocation( utils.SplitLines( GetFileContents( request_data, + span[ 'file' ] ) ), + span[ 'file' ], + span[ 'start' ][ 'line' ], + span[ 'start' ][ 'offset' ] ) ) except RuntimeError: raise RuntimeError( 'Could not find definition' ) @@ -348,14 +407,37 @@ response = self._SendRequest( 'references', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ] + 'offset': request_data[ 'column_codepoint' ] } ) - return [ responses.BuildGoToResponse( - filepath = ref[ 'file' ], - line_num = ref[ 'start' ][ 'line' ], - column_num = ref[ 'start' ][ 'offset' ], - description = ref[ 'lineText' ] - ) for ref in response[ 'refs' ] ] + return [ + responses.BuildGoToResponseFromLocation( + _BuildLocation( utils.SplitLines( GetFileContents( request_data, + ref[ 'file' ] ) ), + ref[ 'file' ], + ref[ 'start' ][ 'line' ], + ref[ 'start' ][ 'offset' ] ), + ref[ 'lineText' ] ) + for ref in response[ 'refs' ] + ] + + + def _GoToType( self, request_data ): + self._Reload( request_data ) + try: + filespans = self._SendRequest( 'typeDefinition', { + 'file': request_data[ 'filepath' ], + 'line': request_data[ 'line_num' ], + 'offset': request_data[ 'column_num' ] + } ) + + span = filespans[ 0 ] + return responses.BuildGoToResponse( + filepath = span[ 'file' ], + line_num = span[ 'start' ][ 'line' ], + column_num = span[ 'start' ][ 'offset' ] + ) + except RuntimeError: + raise RuntimeError( 'Could not find type definition' ) def _GetType( self, request_data ): @@ -363,7 +445,7 @@ info = self._SendRequest( 'quickinfo', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ] + 'offset': request_data[ 'column_codepoint' ] } ) return responses.BuildDisplayMessageResponse( info[ 'displayString' ] ) @@ -373,7 +455,7 @@ info = self._SendRequest( 'quickinfo', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ] + 'offset': request_data[ 'column_codepoint' ] } ) message = '{0}\n\n{1}'.format( info[ 'displayString' ], @@ -391,7 +473,7 @@ response = self._SendRequest( 'rename', { 'file': request_data[ 'filepath' ], 'line': request_data[ 'line_num' ], - 'offset': request_data[ 'column_num' ], + 'offset': request_data[ 'column_codepoint' ], 'findInComments': False, 'findInStrings': False, } ) @@ -432,30 +514,76 @@ chunks = [] for file_replacement in response[ 'locs' ]: - chunks.extend( _BuildFixItChunksForFile( new_name, file_replacement ) ) + chunks.extend( _BuildFixItChunksForFile( request_data, + new_name, + file_replacement ) ) return responses.BuildFixItResponse( [ responses.FixIt( location, chunks ) ] ) - def Shutdown( self ): - self._SendCommand( 'exit' ) + def _RestartServer( self, request_data ): + with self._server_lock: + self._StopServer() + self._StartServer() + # This is needed because after we restart the TSServer it would lose all + # the information about the files we were working on. This means that the + # newly started TSServer will know nothing about the buffer we're working + # on after restarting the server. So if we restart the server and right + # after that ask for completion in the buffer, the server will timeout. + # So we notify the server that we're working on the current buffer. + self.OnBufferVisit( request_data ) + + + def _StopServer( self ): + with self._server_lock: + if self._ServerIsRunning(): + _logger.info( 'Stopping TSServer with PID {0}'.format( + self._tsserver_handle.pid ) ) + self._SendCommand( 'exit' ) + try: + utils.WaitUntilProcessIsTerminated( self._tsserver_handle, + timeout = 5 ) + _logger.info( 'TSServer stopped' ) + except RuntimeError: + _logger.exception( 'Error while stopping TSServer' ) + + self._CleanUp() + + + def _CleanUp( self ): + utils.CloseStandardStreams( self._tsserver_handle ) + self._tsserver_handle = None if not self.user_options[ 'server_keep_logfiles' ]: - os.unlink( self._logfile ) + utils.RemoveIfExists( self._logfile ) self._logfile = None - def DebugInfo( self, request_data ): - return ( 'TSServer logfile:\n {0}' ).format( self._logfile ) + def Shutdown( self ): + self._StopServer() -def _LogFileName(): - with NamedTemporaryFile( dir = utils.PathToCreatedTempDir(), - prefix = 'tsserver_', - suffix = '.log', - delete = False ) as logfile: - return logfile.name + def DebugInfo( self, request_data ): + with self._server_lock: + if self._ServerIsRunning(): + return ( 'TypeScript completer debug information:\n' + ' TSServer running\n' + ' TSServer process ID: {0}\n' + ' TSServer executable: {1}\n' + ' TSServer logfile: {2}'.format( self._tsserver_handle.pid, + PATH_TO_TSSERVER, + self._logfile ) ) + if self._logfile: + return ( 'TypeScript completer debug information:\n' + ' TSServer no longer running\n' + ' TSServer executable: {0}\n' + ' TSServer logfile: {1}'.format( PATH_TO_TSSERVER, + self._logfile ) ) + + return ( 'TypeScript completer debug information:\n' + ' TSServer is not running\n' + ' TSServer executable: {0}'.format( PATH_TO_TSSERVER ) ) def _LogLevel(): @@ -486,22 +614,25 @@ ) -def _BuildFixItChunkForRange( new_name, file_name, source_range ): +def _BuildFixItChunkForRange( new_name, + file_contents, + file_name, + source_range ): """ returns list FixItChunk for a tsserver source range """ return responses.FixItChunk( new_name, responses.Range( - start = responses.Location( - source_range[ 'start' ][ 'line' ], - source_range[ 'start' ][ 'offset' ], - file_name ), - end = responses.Location( - source_range[ 'end' ][ 'line' ], - source_range[ 'end' ][ 'offset' ], - file_name ) ) ) + start = _BuildLocation( file_contents, + file_name, + source_range[ 'start' ][ 'line' ], + source_range[ 'start' ][ 'offset' ] ), + end = _BuildLocation( file_contents, + file_name, + source_range[ 'end' ][ 'line' ], + source_range[ 'end' ][ 'offset' ] ) ) ) -def _BuildFixItChunksForFile( new_name, file_replacement ): +def _BuildFixItChunksForFile( request_data, new_name, file_replacement ): """ returns a list of FixItChunk for each replacement range for the supplied file""" @@ -509,7 +640,16 @@ # whereas all other paths in Python are of the C:\\blah\\blah form. We use # normpath to have python do the conversion for us. file_path = os.path.normpath( file_replacement[ 'file' ] ) - _logger.debug( 'Converted {0} to {1}'.format( file_replacement[ 'file' ], - file_path ) ) - return [ _BuildFixItChunkForRange( new_name, file_path, r ) + file_contents = utils.SplitLines( GetFileContents( request_data, file_path ) ) + return [ _BuildFixItChunkForRange( new_name, file_contents, file_path, r ) for r in file_replacement[ 'locs' ] ] + + +def _BuildLocation( file_contents, filename, line, offset ): + return responses.Location( + line = line, + # tsserver returns codepoint offsets, but we need byte offsets, so we must + # convert + column = utils.CodepointOffsetToByteOffset( file_contents[ line - 1 ], + offset ), + filename = filename ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/default_settings.json ycmd-0+20161219+git486b809/ycmd/default_settings.json --- ycmd-0+20160327+gitc3e6904/ycmd/default_settings.json 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/default_settings.json 2016-12-20 08:50:19.000000000 +0000 @@ -25,6 +25,7 @@ "qf": 1, "notes": 1, "markdown": 1, + "netrw": 1, "unite": 1, "text": 1, "vimwiki": 1, diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/extra_conf_store.py ycmd-0+20161219+git486b809/ycmd/extra_conf_store.py --- ycmd-0+20160327+gitc3e6904/ycmd/extra_conf_store.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/extra_conf_store.py 2016-12-20 08:50:19.000000000 +0000 @@ -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 _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 ): @@ -132,21 +147,34 @@ if not module_file: return None - if not force: - with _module_for_module_file_lock: - if module_file in _module_for_module_file: - return _module_for_module_file[ module_file ] - - if not _ShouldLoad( module_file ): - Disable( module_file ) - return None + with _module_for_module_file_lock: + if module_file in _module_for_module_file: + return _module_for_module_file[ module_file ] + + if not force and not _ShouldLoad( module_file ): + Disable( module_file ) + return None # This has to be here because a long time ago, the ycm_extra_conf.py files # used to import clang_helpers.py from the cpp folder. This is not needed # anymore, but there are a lot of old ycm_extra_conf.py files that we don't # want to break. sys.path.insert( 0, _PathToCppCompleterFolder() ) - module = LoadPythonSource( _RandomName(), module_file ) + + # By default, the Python interpreter compiles source files into bytecode to + # load them faster next time they are run. These *.pyc files are generated + # along the source files prior to Python 3.2 or in a __pycache__ folder for + # newer versions. We disable the generation of these files when loading + # ycm_extra_conf.py files as users do not want them inside their projects. + # The drawback is negligible since ycm_extra_conf.py files are generally small + # files thus really fast to compile and only loaded once by editing session. + old_dont_write_bytecode = sys.dont_write_bytecode + sys.dont_write_bytecode = True + try: + module = LoadPythonSource( _RandomName(), module_file ) + finally: + sys.dont_write_bytecode = old_dont_write_bytecode + del sys.path[ 0 ] with _module_for_module_file_lock: @@ -196,7 +224,3 @@ def _GlobalYcmExtraConfFileLocation(): return os.path.expanduser( user_options_store.Value( 'global_ycm_extra_conf' ) ) - - -def _Logger(): - return logging.getLogger( __name__ ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/handlers.py ycmd-0+20161219+git486b809/ycmd/handlers.py --- ycmd-0+20160327+gitc3e6904/ycmd/handlers.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/handlers.py 2016-12-20 08:50:19.000000000 +0000 @@ -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 time import traceback from bottle import request -from . import server_state -from ycmd import user_options_store +from threading import Thread + +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 @@ -54,12 +41,13 @@ # num bytes for the request body buffer; request.json only works if the request # size is less than this -bottle.Request.MEMFILE_MAX = 1000 * 1024 +bottle.Request.MEMFILE_MAX = 10 * 1024 * 1024 _server_state = None _hmac_secret = bytes() _logger = logging.getLogger( __name__ ) app = bottle.Bottle() +wsgi_server = None @app.post( '/event_notification' ) @@ -99,7 +87,7 @@ _logger.info( 'Received completion request' ) request_data = RequestWrap( request.json ) ( do_filetype_completion, forced_filetype_completion ) = ( - _server_state.ShouldUseFiletypeCompleter(request_data ) ) + _server_state.ShouldUseFiletypeCompleter( request_data ) ) _logger.debug( 'Using filetype completion: %s', do_filetype_completion ) errors = None @@ -202,6 +190,8 @@ request_data = RequestWrap( request.json, validate = False ) extra_conf_store.Load( request_data[ 'filepath' ], force = True ) + return _JsonResponse( True ) + @app.post( '/ignore_extra_conf_file' ) def IgnoreExtraConfFile(): @@ -209,6 +199,8 @@ request_data = RequestWrap( request.json, validate = False ) extra_conf_store.Disable( request_data[ 'filepath' ] ) + return _JsonResponse( True ) + @app.post( '/debug_info' ) def DebugInfo(): @@ -225,7 +217,7 @@ request_data = RequestWrap( request.json ) try: output.append( - _GetCompleterForRequestData( request_data ).DebugInfo( request_data) ) + _GetCompleterForRequestData( request_data ).DebugInfo( request_data ) ) except Exception: _logger.debug( 'Exception in debug info request: ' + traceback.format_exc() ) @@ -233,8 +225,15 @@ return _JsonResponse( '\n'.join( output ) ) +@app.post( '/shutdown' ) +def Shutdown(): + _logger.info( 'Received shutdown request' ) + ServerShutdown() + + return _JsonResponse( True ) + + # The type of the param is Bottle.HTTPError -@app.error( http.client.INTERNAL_SERVER_ERROR ) def ErrorHandler( httperror ): body = _JsonResponse( BuildExceptionResponse( httperror.exception, httperror.traceback ) ) @@ -242,6 +241,10 @@ return body +# For every error Bottle encounters it will use this as the default handler +app.default_error_handler = ErrorHandler + + def _JsonResponse( data ): SetResponseHeader( 'Content-Type', 'application/json' ) return json.dumps( data, default = _UniversalSerialize ) @@ -267,9 +270,19 @@ return _server_state.GetFiletypeCompleter( [ completer_target ] ) -@atexit.register def ServerShutdown(): - _logger.info( 'Server shutting down' ) + def Terminator(): + if wsgi_server: + wsgi_server.Shutdown() + + # Use a separate thread to let the server send the response before shutting + # down. + terminator = Thread( target = Terminator ) + terminator.daemon = True + terminator.start() + + +def ServerCleanup(): if _server_state: _server_state.Shutdown() extra_conf_store.Shutdown() @@ -298,3 +311,19 @@ user_options_store.LoadDefaults() _server_state = server_state.ServerState( user_options_store.GetAll() ) extra_conf_store.Reset() + + +def KeepSubserversAlive( check_interval_seconds ): + def Keepalive( check_interval_seconds ): + while True: + time.sleep( check_interval_seconds ) + + _logger.debug( 'Keeping subservers alive' ) + loaded_completers = _server_state.GetLoadedFiletypeCompleters() + for completer in loaded_completers: + completer.ServerIsHealthy() + + keepalive = Thread( target = Keepalive, + args = ( check_interval_seconds, ) ) + keepalive.daemon = True + keepalive.start() diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/hmac_plugin.py ycmd-0+20161219+git486b809/ycmd/hmac_plugin.py --- ycmd-0+20160327+gitc3e6904/ycmd/hmac_plugin.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/hmac_plugin.py 2016-12-20 08:50:19.000000000 +0000 @@ -24,7 +24,7 @@ from builtins import * # noqa import logging -import http.client +import requests from urllib.parse import urlparse from base64 import b64decode, b64encode from bottle import request, abort @@ -59,7 +59,7 @@ def wrapper( *args, **kwargs ): if not HostHeaderCorrect( request ): self._logger.info( 'Dropping request with bad Host header.' ) - abort( http.client.UNAUTHORIZED, + abort( requests.codes.unauthorized, 'Unauthorized, received bad Host header.' ) return @@ -67,7 +67,7 @@ if not RequestAuthenticated( request.method, request.path, body, self._hmac_secret ): self._logger.info( 'Dropping request with bad HMAC.' ) - abort( http.client.UNAUTHORIZED, 'Unauthorized, received bad HMAC.' ) + abort( requests.codes.unauthorized, 'Unauthorized, received bad HMAC.' ) return body = callback( *args, **kwargs ) SetHmacHeader( body, self._hmac_secret ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/identifier_utils.py ycmd-0+20161219+git486b809/ycmd/identifier_utils.py --- ycmd-0+20160327+gitc3e6904/ycmd/identifier_utils.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/identifier_utils.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2014 Google Inc. # # This file is part of ycmd. @@ -50,9 +52,24 @@ # 3. the escaped double quote inside the string r'(?""" self.location = location self.chunks = chunks + self.text = text class FixItChunk( object ): @@ -183,11 +195,11 @@ """Source code location for a diagnostic or FixIt (aka Refactor).""" def __init__ ( self, line, column, filename ): - """Line is 1-based line, column is 1-based column, filename is absolute - path of the file""" + """Line is 1-based line, column is 1-based column byte offset, filename is + absolute path of the file""" self.line_number_ = line self.column_number_ = column - self.filename_ = filename + self.filename_ = os.path.realpath( filename ) def BuildDiagnosticData( diagnostic ): @@ -221,6 +233,7 @@ return { 'location': BuildLocationData( fixit.location ), 'chunks' : [ BuildFixitChunkData( x ) for x in fixit.chunks ], + 'text': fixit.text, } return { diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/server_state.py ycmd-0+20161219+git486b809/ycmd/server_state.py --- ycmd-0+20160327+gitc3e6904/ycmd/server_state.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/server_state.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,6 +26,7 @@ import os import threading import logging +from future.utils import itervalues from ycmd.utils import ForceSemanticCompletion, LoadPythonSource from ycmd.completers.general.general_completer_store import ( GeneralCompleterStore ) @@ -89,6 +90,12 @@ current_filetypes ) ) + def GetLoadedFiletypeCompleters( self ): + with self._filetype_completers_lock: + return set( [ completer for completer in + itervalues( self._filetype_completers ) if completer ] ) + + def FiletypeCompletionAvailable( self, filetypes ): try: self.GetFiletypeCompleter( filetypes ) @@ -111,7 +118,6 @@ - should_use_completer_now: if True, the semantic engine should be used - was_semantic_completion_forced: if True, the user requested "forced" semantic completion - was_semantic_completion_forced is always False if should_use_completer_now is False """ @@ -122,8 +128,10 @@ return ( True, True ) else: # was not forced. check the conditions for triggering - return ( self.GetFiletypeCompleter( filetypes ).ShouldUseNow( - request_data ), False ) + return ( + self.GetFiletypeCompleter( filetypes ).ShouldUseNow( request_data ), + False + ) # don't use semantic, ignore whether or not the user requested forced # completion diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/server_utils.py ycmd-0+20161219+git486b809/ycmd/server_utils.py --- ycmd-0+20160327+gitc3e6904/ycmd/server_utils.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/server_utils.py 2016-12-20 08:50:19.000000000 +0000 @@ -22,22 +22,57 @@ # 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.' ) + +# Exit statuses returned by the CompatibleWithCurrentCore function: +# - 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. +# 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(): @@ -46,13 +81,49 @@ 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 an exit + status.""" + 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 ): @@ -73,6 +144,22 @@ return None +def IsStandardLibraryFolder( path ): + return os.path.isfile( os.path.join( path, 'os.py' ) ) + + +def IsVirtualEnvLibraryFolder( path ): + return os.path.isfile( os.path.join( path, 'orig-prefix.txt' ) ) + + +def GetStandardLibraryIndexInSysPath(): + for path in sys.path: + if ( IsStandardLibraryFolder( path ) and + not IsVirtualEnvLibraryFolder( path ) ): + return sys.path.index( path ) + raise RuntimeError( 'Could not find standard library path in Python path.' ) + + def AddNearestThirdPartyFoldersToSysPath( filepath ): path_to_third_party = PathToNearestThirdPartyFolder( filepath ) if not path_to_third_party: @@ -86,11 +173,16 @@ # under its 'src' folder, but SOME of its modules are only meant to be # accessible under py2, not py3. This is because these modules (like # `queue`) are implementations of modules present in the py3 standard - # library. So to work around issues, we place the python-future last on - # sys.path so that they can be overriden by the standard library. + # library. Furthermore, we need to be sure that they are not overridden by + # already installed packages (for example, the 'builtins' module from + # 'pies2overrides' or a different version of 'python-future'). To work + # around these issues, we place the python-future just after the Python + # standard library so that its modules can be overridden by standard + # modules but not by installed packages. if folder == 'python-future': folder = os.path.join( folder, 'src' ) - sys.path.append( os.path.realpath( os.path.join( path_to_third_party, + sys.path.insert( GetStandardLibraryIndexInSysPath() + 1, + os.path.realpath( os.path.join( path_to_third_party, folder ) ) ) continue sys.path.insert( 0, os.path.realpath( os.path.join( path_to_third_party, diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/check_core_version_test.py ycmd-0+20161219+git486b809/ycmd/tests/check_core_version_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/check_core_version_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/check_core_version_test.py 1970-01-01 00:00:00.000000000 +0000 @@ -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 -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/clang/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,70 @@ +# 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 __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 hamcrest import assert_that, contains_string, matches_regexp + +from ycmd.tests.clang import IsolatedYcmd, PathToTestFile, SharedYcmd +from ycmd.tests.test_utils import BuildRequest + + +@SharedYcmd +def DebugInfo_ExtraConfLoaded_test( app ): + app.post_json( '/load_extra_conf_file', + { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } ) + request_data = BuildRequest( filepath = PathToTestFile( 'basic.cpp' ), + filetype = 'cpp' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'C-family completer debug information:\n' + ' Configuration file found and loaded\n' + ' Configuration path: .+\n' + ' Flags: .+' ) ) + + +@SharedYcmd +def DebugInfo_NoExtraConfFound_test( app ): + request_data = BuildRequest( filetype = 'cpp' ) + # First time, an exception is raised when no .ycm_extra_conf.py file is found. + assert_that( + app.post_json( '/debug_info', request_data ).json, + contains_string( 'C-family completer debug information:\n' + ' No configuration file found' ) ) + # Second time, None is returned as the .ycm_extra_conf.py path. + assert_that( + app.post_json( '/debug_info', request_data ).json, + contains_string( 'C-family completer debug information:\n' + ' No configuration file found' ) ) + + +@IsolatedYcmd +def DebugInfo_ExtraConfFoundButNotLoaded_test( app ): + request_data = BuildRequest( filepath = PathToTestFile( 'basic.cpp' ), + filetype = 'cpp' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( + 'C-family completer debug information:\n' + ' Configuration file found but not loaded\n' + ' Configuration path: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/diagnostics_test.py ycmd-0+20161219+git486b809/ycmd/tests/clang/diagnostics_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/diagnostics_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/diagnostics_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -221,3 +221,32 @@ 'fixit_available': False } ), ) ) + + +@IsolatedYcmd +def Diagnostics_MultipleMissingIncludes_test( app ): + contents = ReadFile( PathToTestFile( 'multiple_missing_includes.cc' ) ) + + event_data = BuildRequest( contents = contents, + event_name = 'FileReadyToParse', + filetype = 'cpp', + compilation_flags = [ '-x', 'c++' ] ) + + response = app.post_json( '/event_notification', event_data ).json + + pprint( response ) + + assert_that( response, has_items( + has_entries( { + 'kind': equal_to( 'ERROR' ), + 'location': has_entries( { 'line_num': 1, 'column_num': 10 } ), + 'text': equal_to( "'first_missing_include' file not found" ), + 'fixit_available': False + } ), + has_entries( { + 'kind': equal_to( 'ERROR' ), + 'location': has_entries( { 'line_num': 2, 'column_num': 10 } ), + 'text': equal_to( "'second_missing_include' file not found" ), + 'fixit_available': False + } ), + ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/flags_test.py ycmd-0+20161219+git486b809/ycmd/tests/clang/flags_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/flags_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/flags_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -28,12 +28,13 @@ from mock import patch, Mock from ycmd.tests.test_utils import MacOnly +from hamcrest import assert_that, contains + @patch( 'ycmd.extra_conf_store.ModuleForSourceFile', return_value = Mock() ) def FlagsForFile_BadNonUnicodeFlagsAreAlsoRemoved_test( *args ): fake_flags = { - 'flags': [ bytes( b'-c' ), '-c', bytes( b'-foo' ), '-bar' ], - 'do_cache': True + 'flags': [ bytes( b'-c' ), '-c', bytes( b'-foo' ), '-bar' ] } with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', @@ -43,6 +44,63 @@ eq_( list( flags_list ), [ '-foo', '-bar' ] ) +@patch( 'ycmd.extra_conf_store.ModuleForSourceFile', return_value = Mock() ) +def FlagsForFile_FlagsCachedByDefault_test( *args ): + flags_object = flags.Flags() + + results = { 'flags': [ '-x', 'c' ] } + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c' ) ) + + results[ 'flags' ] = [ '-x', 'c++' ] + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c' ) ) + + +@patch( 'ycmd.extra_conf_store.ModuleForSourceFile', return_value = Mock() ) +def FlagsForFile_FlagsNotCachedWhenDoCacheIsFalse_test( *args ): + flags_object = flags.Flags() + + results = { + 'flags': [ '-x', 'c' ], + 'do_cache': False + } + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c' ) ) + + results[ 'flags' ] = [ '-x', 'c++' ] + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c++' ) ) + + +@patch( 'ycmd.extra_conf_store.ModuleForSourceFile', return_value = Mock() ) +def FlagsForFile_FlagsCachedWhenDoCacheIsTrue_test( *args ): + flags_object = flags.Flags() + + results = { + 'flags': [ '-x', 'c' ], + 'do_cache': True + } + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c' ) ) + + results[ 'flags' ] = [ '-x', 'c++' ] + with patch( 'ycmd.completers.cpp.flags._CallExtraConfFlagsForFile', + return_value = results ): + flags_list = flags_object.FlagsForFile( '/foo', False ) + assert_that( flags_list, contains( '-x', 'c' ) ) + + def SanitizeFlags_Passthrough_test(): eq_( [ '-foo', '-bar' ], list( flags._SanitizeFlags( [ '-foo', '-bar' ] ) ) ) @@ -162,6 +220,46 @@ filename ) ) +def RemoveUnusedFlags_Depfiles_test(): + full_flags = [ + '/bin/clang', + '-x', 'objective-c', + '-arch', 'armv7', + '-MMD', + '-MT', 'dependencies', + '-MF', 'file', + '--serialize-diagnostics', 'diagnostics' + ] + + expected = [ + '/bin/clang', + '-x', 'objective-c', + '-arch', 'armv7', + ] + + assert_that( flags._RemoveUnusedFlags( full_flags, 'test.m' ), + contains( *expected ) ) + + +def EnableTypoCorrection_Empty_test(): + eq_( flags._EnableTypoCorrection( [] ), [ '-fspell-checking' ] ) + + +def EnableTypoCorrection_Trivial_test(): + eq_( flags._EnableTypoCorrection( [ '-x', 'c++' ] ), + [ '-x', 'c++', '-fspell-checking' ] ) + + +def EnableTypoCorrection_Reciprocal_test(): + eq_( flags._EnableTypoCorrection( [ '-fno-spell-checking' ] ), + [ '-fno-spell-checking' ] ) + + +def EnableTypoCorrection_ReciprocalOthers_test(): + eq_( flags._EnableTypoCorrection( [ '-x', 'c++', '-fno-spell-checking' ] ), + [ '-x', 'c++', '-fno-spell-checking' ] ) + + def RemoveUnusedFlags_RemoveFilenameWithoutPrecedingInclude_test(): def tester( flag ): expected = [ 'clang', flag, '/foo/bar', '-isystem/zoo/goo' ] @@ -202,12 +300,12 @@ flags._RemoveXclangFlags( expected + to_remove + expected ) ) -def CompilerToLanguageFlag_Passthrough_test(): +def AddLanguageFlagWhenAppropriate_Passthrough_test(): eq_( [ '-foo', '-bar' ], - flags._CompilerToLanguageFlag( [ '-foo', '-bar' ] ) ) + flags._AddLanguageFlagWhenAppropriate( [ '-foo', '-bar' ] ) ) -def _ReplaceCompilerTester( compiler, language ): +def _AddLanguageFlagWhenAppropriateTester( compiler, language_flag = [] ): to_removes = [ [], [ '/usr/bin/ccache' ], @@ -216,19 +314,20 @@ expected = [ '-foo', '-bar' ] for to_remove in to_removes: - eq_( [ compiler, '-x', language ] + expected, - flags._CompilerToLanguageFlag( to_remove + [ compiler ] + expected ) ) + eq_( [ compiler ] + language_flag + expected, + flags._AddLanguageFlagWhenAppropriate( to_remove + [ compiler ] + + expected ) ) -def CompilerToLanguageFlag_ReplaceCCompiler_test(): +def AddLanguageFlagWhenAppropriate_CCompiler_test(): compilers = [ 'cc', 'gcc', 'clang', '/usr/bin/cc', '/some/other/path', 'some_command' ] for compiler in compilers: - yield _ReplaceCompilerTester, compiler, 'c' + yield _AddLanguageFlagWhenAppropriateTester, compiler -def CompilerToLanguageFlag_ReplaceCppCompiler_test(): +def AddLanguageFlagWhenAppropriate_CppCompiler_test(): compilers = [ 'c++', 'g++', 'clang++', '/usr/bin/c++', '/some/other/path++', 'some_command++', 'c++-5', 'g++-5.1', 'clang++-3.7.3', '/usr/bin/c++-5', @@ -237,7 +336,7 @@ '/some/other/path++-4.9.31', 'some_command++-5.10' ] for compiler in compilers: - yield _ReplaceCompilerTester, compiler, 'c++' + yield _AddLanguageFlagWhenAppropriateTester, compiler, [ '-x', 'c++' ] def ExtraClangFlags_test(): diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/clang/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2015 ycmd contributors # # This file is part of ycmd. @@ -23,16 +25,18 @@ standard_library.install_aliases() from builtins import * # noqa +import json +import requests from nose.tools import eq_ from hamcrest import ( assert_that, contains, contains_inanyorder, empty, - has_item, has_items, has_entry, has_entries ) -import http.client + has_item, has_items, has_entry, has_entries, + contains_string ) from ycmd.completers.cpp.clang_completer import NO_COMPLETIONS_MESSAGE from ycmd.responses import UnknownExtraConf, NoExtraConfDetected from ycmd.tests.clang import IsolatedYcmd, PathToTestFile, SharedYcmd from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher, - ErrorMatcher, UserOption ) + ErrorMatcher, UserOption, ExpectedFailure ) from ycmd.utils import ReadFile NO_COMPLETIONS_ERROR = ErrorMatcher( RuntimeError, NO_COMPLETIONS_MESSAGE ) @@ -42,22 +46,28 @@ """ Method to run a simple completion test and verify the result - Note: uses the .ycm_extra_conf from general_fallback/ which: + Note: by default uses the .ycm_extra_conf from general_fallback/ which: - supports cpp, c and objc - requires extra_conf_data containing 'filetype&' = the filetype - this should be sufficient for many standard test cases + This should be sufficient for many standard test cases. If not, specify + a path (as a list of path items) in 'extra_conf' member of |test|. test is a dictionary containing: 'request': kwargs for BuildRequest 'expect': { - 'response': server response code (e.g. httplib.OK) + 'response': server response code (e.g. requests.codes.ok) 'data': matcher for the server response json } + 'extra_conf': [ optional list of path items to extra conf file ] """ + + extra_conf = ( test[ 'extra_conf' ] if 'extra_conf' in test + else [ 'general_fallback', + '.ycm_extra_conf.py' ] ) + app.post_json( '/load_extra_conf_file', { - 'filepath': PathToTestFile( 'general_fallback', - '.ycm_extra_conf.py' ) } ) + 'filepath': PathToTestFile( *extra_conf ) } ) contents = ReadFile( test[ 'request' ][ 'filepath' ] ) @@ -88,6 +98,9 @@ eq_( response.status_code, test[ 'expect' ][ 'response' ] ) + print( 'Completer response: {0}'.format( json.dumps( + response.json, indent = 2 ) ) ) + assert_that( response.json, test[ 'expect' ][ 'data' ] ) @@ -105,7 +118,7 @@ 'force_semantic': True, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'DO_SOMETHING_TO', 'void' ), @@ -131,7 +144,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': empty(), 'errors': has_item( NO_COMPLETIONS_ERROR ), @@ -156,7 +169,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': empty(), 'errors': has_item( NO_COMPLETIONS_ERROR ), @@ -179,7 +192,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': has_item( CompletionEntryMatcher( 'a_parameter', '[ID]' ) ), @@ -204,7 +217,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'a_parameter', '[ID]' ), @@ -230,7 +243,7 @@ 'force_semantic': True, }, 'expect': { - 'response': http.client.INTERNAL_SERVER_ERROR, + 'response': requests.codes.internal_server_error, 'data': NO_COMPLETIONS_ERROR, }, } ) @@ -255,7 +268,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains_inanyorder( # do_ is an identifier because it is already in the file when we @@ -270,7 +283,7 @@ } ) -@SharedYcmd +@IsolatedYcmd def GetCompletions_WorksWithExplicitFlags_test( app ): app.post_json( '/ignore_extra_conf_file', @@ -304,7 +317,7 @@ eq_( 7, response_data[ 'completion_start_column' ] ) -@SharedYcmd +@IsolatedYcmd def GetCompletions_NoCompletionsWhenAutoTriggerOff_test( app ): with UserOption( 'auto_trigger', False ): app.post_json( @@ -350,7 +363,7 @@ completion_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, has_entry( 'exception', has_entry( 'TYPE', UnknownExtraConf.__name__ ) ) ) @@ -363,7 +376,7 @@ completion_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, has_entry( 'exception', has_entry( 'TYPE', @@ -409,7 +422,7 @@ response = app.post_json( '/completions', completion_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, has_entry( 'exception', @@ -493,3 +506,60 @@ has_item( CompletionEntryMatcher( 'include.hpp', extra_menu_info = '[File]' ) ) ) + + +@SharedYcmd +def GetCompletions_UnicodeInLine_test( app ): + RunTest( app, { + 'description': 'member completion with a unicode identifier', + 'extra_conf': [ '.ycm_extra_conf.py' ], + 'request': { + 'filetype' : 'cpp', + 'filepath' : PathToTestFile( 'unicode.cc' ), + 'line_num' : 9, + 'column_num': 8, + 'extra_conf_data': { '&filetype': 'cpp' }, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completion_start_column': 8, + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'member_with_å_unicøde', 'int' ), + CompletionEntryMatcher( '~MyStruct', 'void' ), + CompletionEntryMatcher( 'operator=', 'MyStruct &' ), + CompletionEntryMatcher( 'MyStruct::', '' ), + ), + 'errors': empty(), + } ) + }, + } ) + + +@ExpectedFailure( 'Filtering and sorting does not work when the candidate ' + 'contains non-ASCII characters. This is due to the way ' + 'the filtering and sorting code works.', + contains_string( "value for 'completions' no item matches" ) ) +@SharedYcmd +def GetCompletions_UnicodeInLineFilter_test( app ): + RunTest( app, { + 'description': 'member completion with a unicode identifier', + 'extra_conf': [ '.ycm_extra_conf.py' ], + 'request': { + 'filetype' : 'cpp', + 'filepath' : PathToTestFile( 'unicode.cc' ), + 'line_num' : 9, + 'column_num': 10, + 'extra_conf_data': { '&filetype': 'cpp' }, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completion_start_column': 8, + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'member_with_å_unicøde', 'int' ), + ), + 'errors': empty(), + } ) + }, + } ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/clang/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,7 +27,7 @@ import os from ycmd import handlers -from ycmd.tests.test_utils import SetUpApp +from ycmd.tests.test_utils import ClearCompletionsCache, SetUpApp shared_app = None @@ -56,6 +56,7 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/clang/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2015 ycmd contributors # # This file is part of ycmd. @@ -23,21 +25,43 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import ( assert_that, calling, contains, equal_to, - has_entries, raises ) +from hamcrest import ( assert_that, calling, contains, contains_string, + equal_to, has_entries, raises ) from nose.tools import eq_ from pprint import pprint from webtest import AppError -import http.client +import requests import os.path from ycmd.completers.cpp.clang_completer import NO_DOCUMENTATION_MESSAGE from ycmd.tests.clang import PathToTestFile, SharedYcmd -from ycmd.tests.test_utils import BuildRequest, ErrorMatcher +from ycmd.tests.test_utils import ( BuildRequest, + ErrorMatcher, + ChunkMatcher, + LineColMatcher ) from ycmd.utils import ReadFile @SharedYcmd +def Subcommands_DefinedSubcommands_test( app ): + subcommands_data = BuildRequest( completer_target = 'cpp' ) + eq_( sorted( [ 'ClearCompilationFlagCache', + 'FixIt', + 'GetDoc', + 'GetDocImprecise', + 'GetParent', + 'GetType', + 'GetTypeImprecise', + 'GoTo', + 'GoToDeclaration', + 'GoToDefinition', + 'GoToImprecise', + 'GoToInclude' ] ), + app.post_json( '/defined_subcommands', + subcommands_data ).json ) + + +@SharedYcmd def Subcommands_GoTo_ZeroBasedLineAndColumn_test( app ): contents = ReadFile( PathToTestFile( 'GoTo_Clang_ZeroBasedLineAndColumn_test.cc' ) ) @@ -87,25 +111,29 @@ goto_data = BuildRequest( **request ) - eq_( response, - app.post_json( '/run_completer_command', goto_data ).json ) + actual_response = app.post_json( '/run_completer_command', goto_data ).json + pprint( actual_response ) + eq_( response, actual_response ) def Subcommands_GoTo_all_test(): # GoToDeclaration tests = [ # Local::x -> declaration of x - { 'request': [23, 21], 'response': [ 4, 9] }, + { 'request': [ 23, 21 ], 'response': [ 4, 9 ] }, # Local::in_line -> declaration of Local::in_line - { 'request': [24, 26], 'response': [ 6, 10] }, + { 'request': [ 24, 26 ], 'response': [ 6, 10 ] }, # Local -> declaration of Local - { 'request': [24, 16], 'response': [ 2, 11] }, + { 'request': [ 24, 16 ], 'response': [ 2, 11 ] }, # Local::out_of_line -> declaration of Local::out_of_line - { 'request': [25, 27], 'response': [11, 10] }, + { 'request': [ 25, 27 ], 'response': [ 11, 10 ] }, # GoToDeclaration on definition of out_of_line moves to declaration - { 'request': [14, 13], 'response': [11, 10] }, + { 'request': [ 14, 13 ], 'response': [ 11, 10] }, # main -> declaration of main - { 'request': [21, 7], 'response': [19, 5] }, + { 'request': [ 21, 7 ], 'response': [ 19, 5] }, + # Another_Unicøde + { 'request': [ 36, 25 ], 'response': [ 32, 54] }, + { 'request': [ 38, 3 ], 'response': [ 36, 28] }, ] for test in tests: @@ -121,17 +149,19 @@ # tests = [ # Local::x -> declaration of x - { 'request': [23, 21], 'response': [ 4, 9] }, + { 'request': [ 23, 21 ], 'response': [ 4, 9] }, # Local::in_line -> declaration of Local::in_line - { 'request': [24, 26], 'response': [ 6, 10] }, + { 'request': [ 24, 26 ], 'response': [ 6, 10 ] }, # Local -> declaration of Local - { 'request': [24, 16], 'response': [ 2, 11] }, + { 'request': [ 24, 16 ], 'response': [ 2, 11 ] }, # sic: Local::out_of_line -> definition of Local::out_of_line - { 'request': [25, 27], 'response': [14, 13] }, # sic + { 'request': [ 25, 27 ], 'response': [ 14, 13 ] }, # sic # sic: GoToDeclaration on definition of out_of_line moves to itself - { 'request': [14, 13], 'response': [14, 13] }, # sic + { 'request': [ 14, 13 ], 'response': [ 14, 13 ] }, # sic # main -> definition of main (not declaration) - { 'request': [21, 7], 'response': [21, 5] }, # sic + { 'request': [ 21, 7 ], 'response': [ 21, 5] }, # sic + # Unicøde + { 'request': [ 34, 8 ], 'response': [ 32, 26 ] }, ] for test in tests: @@ -158,6 +188,8 @@ { 'request': [14, 13], 'response': [14, 13] }, # sic # main -> definition of main (not declaration) { 'request': [21, 7], 'response': [21, 5] }, # sic + # Another_Unicøde + { 'request': [ 36, 25 ], 'response': [ 32, 54] }, ] for test in tests: @@ -214,8 +246,9 @@ 'column_num' : 1, } - eq_( response, - app.post_json( '/run_completer_command', goto_data ).json ) + actual_response = app.post_json( '/run_completer_command', goto_data ).json + pprint( actual_response ) + eq_( response, actual_response ) def Subcommands_GoToInclude_test(): @@ -286,8 +319,9 @@ request_data = BuildRequest( **request ) - eq_( { 'message': expected }, - app.post_json( '/run_completer_command', request_data ).json ) + response = app.post_json( '/run_completer_command', request_data ).json + pprint( response ) + eq_( { 'message': expected }, response ) def Subcommands_GetType_test(): @@ -359,6 +393,9 @@ [{'line_num': 32, 'column_num': 19}, 'int'], [{'line_num': 33, 'column_num': 13}, 'Foo * => Foo *'], # sic [{'line_num': 33, 'column_num': 20}, 'int'], + + # Unicode + [{'line_num': 47, 'column_num': 13}, 'Unicøde *'], ] for test in tests: @@ -367,6 +404,14 @@ test, [ 'GetType' ] ) + # For every practical scenario, GetTypeImprecise is the same as GetType (it + # just skips the reparse) + for test in tests: + yield ( RunGetSemanticTest, + 'GetType_Clang_test.cc', + test, + [ 'GetTypeImprecise' ] ) + def Subcommands_GetParent_test(): tests = [ @@ -661,11 +706,80 @@ } ) ) +def FixIt_Check_unicode_Ins( results ): + assert_that( results, has_entries( { + 'fixits': contains( has_entries( { + 'chunks': contains( + has_entries( { + 'replacement_text': equal_to(';'), + 'range': has_entries( { + 'start': has_entries( { 'line_num': 21, 'column_num': 39 } ), + 'end' : has_entries( { 'line_num': 21, 'column_num': 39 } ), + } ), + } ) + ), + 'location': has_entries( { 'line_num': 21, 'column_num': 39 } ) + } ) ) + } ) ) + + +def FixIt_Check_cpp11_Note( results ): + assert_that( results, has_entries( { + 'fixits': contains( + # First note: put parens around it + has_entries( { + 'text': contains_string( 'parentheses around the assignment' ), + 'chunks': contains( + ChunkMatcher( '(', + LineColMatcher( 59, 8 ), + LineColMatcher( 59, 8 ) ), + ChunkMatcher( ')', + LineColMatcher( 61, 12 ), + LineColMatcher( 61, 12 ) ) + ), + 'location': LineColMatcher( 60, 8 ), + } ), + + # Second note: change to == + has_entries( { + 'text': contains_string( '==' ), + 'chunks': contains( + ChunkMatcher( '==', + LineColMatcher( 60, 8 ), + LineColMatcher( 60, 9 ) ) + ), + 'location': LineColMatcher( 60, 8 ), + } ) + ) + } ) ) + + +def FixIt_Check_cpp11_SpellCheck( results ): + assert_that( results, has_entries( { + 'fixits': contains( + # Change to SpellingIsNotMyStrongPoint + has_entries( { + 'text': contains_string( "did you mean 'SpellingIsNotMyStrongPoint'" ), + 'chunks': contains( + ChunkMatcher( 'SpellingIsNotMyStrongPoint', + LineColMatcher( 72, 9 ), + LineColMatcher( 72, 35 ) ) + ), + 'location': LineColMatcher( 72, 9 ), + } ) ) + } ) ) + + def Subcommands_FixIt_all_test(): cfile = 'FixIt_Clang_cpp11.cpp' mfile = 'FixIt_Clang_objc.m' + ufile = 'unicode.cc' tests = [ + # L + # i C + # n o + # e l Lang File, Checker [ 16, 0, 'cpp11', cfile, FixIt_Check_cpp11_Ins ], [ 16, 1, 'cpp11', cfile, FixIt_Check_cpp11_Ins ], [ 16, 10, 'cpp11', cfile, FixIt_Check_cpp11_Ins ], @@ -689,6 +803,15 @@ [ 54, 51, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ], [ 54, 52, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ], [ 54, 53, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ], + + # unicode in line for fixit + [ 21, 16, 'cpp11', ufile, FixIt_Check_unicode_Ins ], + + # FixIt attached to a "child" diagnostic (i.e. a Note) + [ 60, 1, 'cpp11', cfile, FixIt_Check_cpp11_Note ], + + # FixIt due to forced spell checking + [ 72, 9, 'cpp11', cfile, FixIt_Check_cpp11_SpellCheck ], ] for test in tests: @@ -805,7 +928,7 @@ event_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) ) @@ -829,7 +952,7 @@ event_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) ) @@ -837,7 +960,7 @@ # Following tests repeat the tests above, but without re-parsing the file @SharedYcmd -def Subcommands_GetDocQuick_Variable_test( app ): +def Subcommands_GetDocImprecise_Variable_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -854,7 +977,7 @@ line_num = 70, column_num = 24, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data ).json @@ -874,7 +997,7 @@ @SharedYcmd -def Subcommands_GetDocQuick_Method_test( app ): +def Subcommands_GetDocImprecise_Method_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -893,7 +1016,7 @@ line_num = 22, column_num = 13, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data ).json @@ -917,7 +1040,7 @@ @SharedYcmd -def Subcommands_GetDocQuick_Namespace_test( app ): +def Subcommands_GetDocImprecise_Namespace_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -936,7 +1059,7 @@ line_num = 65, column_num = 14, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data ).json @@ -954,7 +1077,7 @@ @SharedYcmd -def Subcommands_GetDocQuick_Undocumented_test( app ): +def Subcommands_GetDocImprecise_Undocumented_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -973,21 +1096,21 @@ line_num = 81, column_num = 17, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) ) @SharedYcmd -def Subcommands_GetDocQuick_NoCursor_test( app ): +def Subcommands_GetDocImprecise_NoCursor_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -1006,21 +1129,21 @@ line_num = 1, column_num = 1, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) ) @SharedYcmd -def Subcommands_GetDocQuick_NoReadyToParse_test( app ): +def Subcommands_GetDocImprecise_NoReadyToParse_test( app ): filepath = PathToTestFile( 'GetDoc_Clang.cc' ) contents = ReadFile( filepath ) @@ -1030,7 +1153,7 @@ line_num = 11, column_num = 18, contents = contents, - command_arguments = [ 'GetDocQuick' ], + command_arguments = [ 'GetDocImprecise' ], completer_target = 'filetype_default' ) response = app.post_json( '/run_completer_command', event_data ).json @@ -1044,3 +1167,33 @@ --- This is a method which is only pretend global @param test Set this to true. Do it.""" } ) + + +@SharedYcmd +def Subcommands_GetDoc_Unicode_test( app ): + filepath = PathToTestFile( 'unicode.cc' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'cpp', + compilation_flags = [ '-x', 'c++' ], + line_num = 21, + column_num = 16, + contents = contents, + command_arguments = [ 'GetDoc' ], + completer_target = 'filetype_default' ) + + response = app.post_json( '/run_completer_command', event_data ).json + + pprint( response ) + + eq_( response, { + 'detailed_info': """\ +int member_with_å_unicøde +This method has unicøde in it +Type: int +Name: member_with_å_unicøde +--- + +This method has unicøde in it +""" } ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/client_data/.ycm_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/client_data/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/client_data/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/client_data/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,5 +1,2 @@ def FlagsForFile( filename, **kwargs ): - return { - 'flags': kwargs['client_data']['flags'], - 'do_cache': True - } + return { 'flags': kwargs[ 'client_data' ][ 'flags' ] } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/FixIt_Clang_cpp11.cpp ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/FixIt_Clang_cpp11.cpp --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/FixIt_Clang_cpp11.cpp 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/FixIt_Clang_cpp11.cpp 2016-12-20 08:50:19.000000000 +0000 @@ -53,3 +53,21 @@ namespace test_fixit_multiple { class foo { ~bar() { } }; class bar { ~bar(); }; ~bar::bar() { } } + +void z() { + bool x; + if ( x + = + true + ) { + + } +} + +namespace Typo { + struct SpellingIsNotMyStrongPoint; +} + +void typo() { + Typo::SpellingIsNotMyStringPiont *p; +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/general_fallback/.ycm_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/general_fallback/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/general_fallback/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/general_fallback/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -30,7 +30,4 @@ if 'throw' in client_data: raise ValueError( client_data['throw'] ) - return { - 'flags': opts, - 'do_cache': True - } + return { 'flags': opts } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/GetType_Clang_test.cc ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/GetType_Clang_test.cc --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/GetType_Clang_test.cc 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/GetType_Clang_test.cc 2016-12-20 08:50:19.000000000 +0000 @@ -43,5 +43,8 @@ int ry = rFoo.y; int px = pFoo->x; + struct Unicøde; + Unicøde *ø; + return 0; } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/GoTo_all_Clang_test.cc ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/GoTo_all_Clang_test.cc --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/GoTo_all_Clang_test.cc 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/GoTo_all_Clang_test.cc 2016-12-20 08:50:19.000000000 +0000 @@ -26,3 +26,14 @@ return 0; } + +void unicode() +{ + /* †est ê */ struct Unicøde { int u; }; struct Another_Unicøde; + + Unicøde *ç; + + ç->u; Another_Unicøde *u; + + u; +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/multiple_missing_includes.cc ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/multiple_missing_includes.cc --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/multiple_missing_includes.cc 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/multiple_missing_includes.cc 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,2 @@ +#include "first_missing_include" +#include "second_missing_include" diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/noflags/.ycm_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/noflags/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/noflags/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/noflags/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,6 +1,2 @@ def FlagsForFile( filename ): - return { - 'flags': [], - 'do_cache': True - } - + return { 'flags': [] } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/test-include/.ycm_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/test-include/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/test-include/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/test-include/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -3,8 +3,5 @@ def FlagsForFile( filename, **kwargs ): d = os.path.dirname( filename ) - return { - 'flags': [ '-iquote', os.path.join( d, 'quote' ), - '-I', os.path.join( d, 'system' ) ], - 'do_cache': True - } + return { 'flags': [ '-iquote', os.path.join( d, 'quote' ), + '-I', os.path.join( d, 'system' ) ] } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/unicode.cc ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/unicode.cc --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/unicode.cc 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/unicode.cc 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,22 @@ + +int main( int argc, char ** argv ) +{ + int id_with_å_unicode_chars = 10; + struct MyStruct { + int member_with_å_unicøde; + } myå; + + myå.w +} + +int another_main() +{ + struct MyStruct { + /** + * This method has unicøde in it + */ + int member_with_å_unicøde; + } myå; + + int a = myå.member_with_å_unicøde +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/.ycm_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/.ycm_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/clang/testdata/.ycm_extra_conf.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/clang/testdata/.ycm_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,5 +1,2 @@ def FlagsForFile( filename ): - return { - 'flags': ['-x', 'c++', '-I', '.'], - 'do_cache': True - } + return { 'flags': ['-x', 'c++', '-I', '.'] } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/client_test.py ycmd-0+20161219+git486b809/ycmd/tests/client_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/client_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/client_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,276 @@ +# 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 __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 future.utils import native + +from base64 import b64decode, b64encode +from hamcrest import assert_that, empty, equal_to, is_in +from tempfile import NamedTemporaryFile +import functools +import json +import os +import psutil +import re +import requests +import subprocess +import sys +import time +import urllib.parse + +from ycmd.hmac_utils import CreateHmac, CreateRequestHmac, SecureBytesEqual +from ycmd.tests import PathToTestFile +from ycmd.tests.test_utils import BuildRequest +from ycmd.user_options_store import DefaultOptions +from ycmd.utils import ( CloseStandardStreams, CreateLogfile, + GetUnusedLocalhostPort, ReadFile, RemoveIfExists, + SafePopen, SetEnviron, ToBytes, ToUnicode ) + +HEADERS = { 'content-type': 'application/json' } +HMAC_HEADER = 'x-ycm-hmac' +HMAC_SECRET_LENGTH = 16 +DIR_OF_THIS_SCRIPT = os.path.dirname( os.path.abspath( __file__ ) ) +PATH_TO_YCMD = os.path.join( DIR_OF_THIS_SCRIPT, '..' ) +LOGFILE_FORMAT = 'server_{port}_{std}_' + + +class Client_test( object ): + + def __init__( self ): + self._location = None + self._port = None + self._hmac_secret = None + self._servers = [] + self._logfiles = [] + self._options_dict = DefaultOptions() + self._popen_handle = None + + + def setUp( self ): + self._hmac_secret = os.urandom( HMAC_SECRET_LENGTH ) + self._options_dict[ 'hmac_secret' ] = ToUnicode( + b64encode( self._hmac_secret ) ) + + + def tearDown( self ): + for server in self._servers: + if server.is_running(): + server.terminate() + CloseStandardStreams( self._popen_handle ) + for logfile in self._logfiles: + RemoveIfExists( logfile ) + + + def Start( self, idle_suicide_seconds = 60, + check_interval_seconds = 60 * 10 ): + # The temp options file is deleted by ycmd during startup + with NamedTemporaryFile( mode = 'w+', delete = False ) as options_file: + json.dump( self._options_dict, options_file ) + options_file.flush() + self._port = GetUnusedLocalhostPort() + self._location = 'http://127.0.0.1:' + str( self._port ) + + # Define environment variable to enable subprocesses coverage. See: + # http://coverage.readthedocs.org/en/coverage-4.0.3/subprocess.html + env = os.environ.copy() + SetEnviron( env, 'COVERAGE_PROCESS_START', '.coveragerc' ) + + ycmd_args = [ + sys.executable, + PATH_TO_YCMD, + '--port={0}'.format( self._port ), + '--options_file={0}'.format( options_file.name ), + '--log=debug', + '--idle_suicide_seconds={0}'.format( idle_suicide_seconds ), + '--check_interval_seconds={0}'.format( check_interval_seconds ), + ] + + stdout = CreateLogfile( + LOGFILE_FORMAT.format( port = self._port, std = 'stdout' ) ) + stderr = CreateLogfile( + LOGFILE_FORMAT.format( port = self._port, std = 'stderr' ) ) + self._logfiles.extend( [ stdout, stderr ] ) + ycmd_args.append( '--stdout={0}'.format( stdout ) ) + ycmd_args.append( '--stderr={0}'.format( stderr ) ) + + self._popen_handle = SafePopen( ycmd_args, + stdin_windows = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + env = env ) + self._servers.append( psutil.Process( self._popen_handle.pid ) ) + + self._WaitUntilReady() + extra_conf = PathToTestFile( 'client', '.ycm_extra_conf.py' ) + self.PostRequest( 'load_extra_conf_file', { 'filepath': extra_conf } ) + + + def _IsReady( self, filetype = None ): + params = { 'subserver': filetype } if filetype else None + response = self.GetRequest( 'ready', params ) + response.raise_for_status() + return response.json() + + + def _WaitUntilReady( self, filetype = None, timeout = 5 ): + expiration = time.time() + timeout + while True: + try: + if time.time() > expiration: + server = ( 'the {0} subserver'.format( filetype ) if filetype else + 'ycmd' ) + raise RuntimeError( 'Waited for {0} to be ready for {1} seconds, ' + 'aborting.'.format( server, timeout ) ) + + if self._IsReady( filetype ): + return + except requests.exceptions.ConnectionError: + pass + finally: + time.sleep( 0.1 ) + + + def StartSubserverForFiletype( self, filetype ): + filepath = PathToTestFile( 'client', 'some_file' ) + # Calling the BufferVisit event before the FileReadyToParse one is needed + # for the TypeScript completer. + self.PostRequest( 'event_notification', + BuildRequest( filepath = filepath, + filetype = filetype, + event_name = 'BufferVisit' ) ) + self.PostRequest( 'event_notification', + BuildRequest( filepath = filepath, + filetype = filetype, + event_name = 'FileReadyToParse' ) ) + + self._WaitUntilReady( filetype ) + + response = self.PostRequest( + 'debug_info', + BuildRequest( filepath = filepath, + filetype = filetype ) + ).json() + + pid_match = re.search( 'process ID: (\d+)', response ) + if not pid_match: + raise RuntimeError( 'Cannot find PID in debug informations for {0} ' + 'filetype.'.format( filetype ) ) + subserver_pid = int( pid_match.group( 1 ) ) + self._servers.append( psutil.Process( subserver_pid ) ) + + logfiles = re.findall( '(\S+\.log)', response ) + if not logfiles: + raise RuntimeError( 'Cannot find logfiles in debug informations for {0} ' + 'filetype.'.format( filetype ) ) + self._logfiles.extend( logfiles ) + + + def AssertServersAreRunning( self ): + for server in self._servers: + assert_that( server.is_running(), equal_to( True ) ) + + + def AssertServersShutDown( self, timeout = 5 ): + _, alive_procs = psutil.wait_procs( self._servers, timeout = timeout ) + assert_that( alive_procs, empty() ) + + + def AssertLogfilesAreRemoved( self ): + existing_logfiles = [] + for logfile in self._logfiles: + if os.path.isfile( logfile ): + existing_logfiles.append( logfile ) + assert_that( existing_logfiles, empty() ) + + + def GetRequest( self, handler, params = None ): + return self._Request( 'GET', handler, params = params ) + + + def PostRequest( self, handler, data = None ): + return self._Request( 'POST', handler, data = data ) + + + def _ToUtf8Json( self, data ): + return ToBytes( json.dumps( data ) if data else None ) + + + def _Request( self, method, handler, data = None, params = None ): + request_uri = self._BuildUri( handler ) + data = self._ToUtf8Json( data ) + headers = self._ExtraHeaders( method, + request_uri, + data ) + response = requests.request( method, + request_uri, + headers = headers, + data = data, + params = params ) + return response + + + def _BuildUri( self, handler ): + return native( ToBytes( urllib.parse.urljoin( self._location, handler ) ) ) + + + def _ExtraHeaders( self, method, request_uri, request_body = None ): + if not request_body: + request_body = bytes( b'' ) + headers = dict( HEADERS ) + headers[ HMAC_HEADER ] = b64encode( + CreateRequestHmac( ToBytes( method ), + ToBytes( urllib.parse.urlparse( request_uri ).path ), + request_body, + self._hmac_secret ) ) + return headers + + + def AssertResponse( self, response ): + assert_that( response.status_code, equal_to( requests.codes.ok ) ) + assert_that( HMAC_HEADER, is_in( response.headers ) ) + assert_that( + self._ContentHmacValid( response ), + equal_to( True ) + ) + + + def _ContentHmacValid( self, response ): + our_hmac = CreateHmac( response.content, self._hmac_secret ) + their_hmac = ToBytes( b64decode( response.headers[ HMAC_HEADER ] ) ) + return SecureBytesEqual( our_hmac, their_hmac ) + + + @staticmethod + def CaptureLogfiles( test ): + @functools.wraps( test ) + def Wrapper( self, *args ): + try: + test( self, *args ) + finally: + for logfile in self._logfiles: + if os.path.isfile( logfile ): + sys.stdout.write( 'Logfile {0}:\n\n'.format( logfile ) ) + sys.stdout.write( ReadFile( logfile ) ) + sys.stdout.write( '\n' ) + + return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/completer_test.py ycmd-0+20161219+git486b809/ycmd/tests/completer_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/completer_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/completer_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,4 +1,5 @@ # Copyright (C) 2016 ycmd contributors. +# encoding: utf-8 # # This file is part of ycmd. # @@ -15,9 +16,19 @@ # You should have received a copy of the GNU General Public License # along with ycmd. If not, see . -from ycmd.tests.test_utils import DummyCompleter +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 ycmd.tests.test_utils import DummyCompleter, ExpectedFailure from ycmd.user_options_store import DefaultOptions +from mock import patch from nose.tools import eq_ +from hamcrest import contains_string def _FilterAndSortCandidates_Match( candidates, query, expected_matches ): @@ -48,3 +59,18 @@ _FilterAndSortCandidates_Match( [ { 'insertion_text': 'password' } ], 'p', [ { 'insertion_text': 'password' } ] ) + + +@ExpectedFailure( 'Filtering does not support unicode characters', + contains_string( '[]' ) ) +def FilterAndSortCandidates_Unicode_test(): + _FilterAndSortCandidates_Match( [ { 'insertion_text': 'ø' } ], + 'ø', + [ { 'insertion_text': 'ø' } ] ) + + +@patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap', + return_value = { 'Foo': '', 'StopServer': '' } ) +def DefinedSubcommands_RemoveStopServerSubcommand_test( subcommands_map ): + completer = DummyCompleter( DefaultOptions() ) + eq_( completer.DefinedSubcommands(), [ 'Foo' ] ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/cs/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,119 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.cs import IsolatedYcmd, PathToTestFile, SharedYcmd +from ycmd.tests.test_utils import ( BuildRequest, StopCompleterServer, + UserOption, WaitUntilCompleterServerReady ) +from ycmd.utils import ReadFile + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + filepath = PathToTestFile( 'testy', 'Program.cs' ) + contents = ReadFile( filepath ) + event_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + WaitUntilCompleterServerReady( app, 'cs' ) + + request_data = BuildRequest( filepath = filepath, + filetype = 'cs' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'C# completer debug information:\n' + ' OmniSharp running at: http://localhost:\d+\n' + ' OmniSharp process ID: \d+\n' + ' OmniSharp executable: .+\n' + ' OmniSharp logfiles:\n' + ' .+\n' + ' .+\n' + ' OmniSharp solution: .+' ) ) + + +@SharedYcmd +def DebugInfo_ServerIsNotRunning_NoSolution_test( app ): + request_data = BuildRequest( filetype = 'cs' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'C# completer debug information:\n' + ' OmniSharp not running\n' + ' OmniSharp executable: .+\n' + ' OmniSharp solution: not found' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + filepath = PathToTestFile( 'testy', 'Program.cs' ) + contents = ReadFile( filepath ) + event_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + WaitUntilCompleterServerReady( app, 'cs' ) + + StopCompleterServer( app, 'cs', filepath ) + request_data = BuildRequest( filepath = filepath, + filetype = 'cs' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'C# completer debug information:\n' + ' OmniSharp no longer running\n' + ' OmniSharp executable: .+\n' + ' OmniSharp logfiles:\n' + ' .+\n' + ' .+\n' + ' OmniSharp solution: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + filepath = PathToTestFile( 'testy', 'Program.cs' ) + contents = ReadFile( filepath ) + event_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + WaitUntilCompleterServerReady( app, 'cs' ) + + StopCompleterServer( app, 'cs', filepath ) + request_data = BuildRequest( filepath = filepath, + filetype = 'cs' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'C# completer debug information:\n' + ' OmniSharp is not running\n' + ' OmniSharp executable: .+\n' + ' OmniSharp solution: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/diagnostics_test.py ycmd-0+20161219+git486b809/ycmd/tests/cs/diagnostics_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/diagnostics_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/diagnostics_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -96,6 +96,44 @@ @SharedYcmd +def Diagnostics_WithRange_test( app ): + filepath = PathToTestFile( 'testy', 'DiagnosticRange.cs' ) + with WrapOmniSharpServer( app, filepath ): + contents = ReadFile( filepath ) + + results = {} + for _ in ( 0, 1 ): # First call always returns blank for some reason + event_data = BuildRequest( filepath = filepath, + event_name = 'FileReadyToParse', + filetype = 'cs', + contents = contents ) + + results = app.post_json( '/event_notification', event_data ).json + + assert_that( results, + contains( + has_entries( { + 'kind': equal_to( 'WARNING' ), + 'text': contains_string( + "Name should have prefix '_'" ), + 'location': has_entries( { + 'line_num': 3, + 'column_num': 16 + } ), + 'location_extent': has_entries( { + 'start': has_entries( { + 'line_num': 3, + 'column_num': 16, + } ), + 'end': has_entries( { + 'line_num': 3, + 'column_num': 25, + } ), + } ) + } ) ) ) + + +@SharedYcmd def Diagnostics_MultipleSolution_test( app ): filepaths = [ PathToTestFile( 'testy', 'Program.cs' ), PathToTestFile( 'testy-multiple-solutions', diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/cs/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,8 +25,8 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import ( assert_that, empty, greater_than, has_item, has_items, - has_entries ) +from hamcrest import ( assert_that, calling, empty, greater_than, has_item, + has_items, has_entries, raises ) from nose.tools import eq_ from webtest import AppError @@ -54,6 +54,28 @@ @SharedYcmd +def GetCompletions_Unicode_test( app ): + filepath = PathToTestFile( 'testy', 'Unicode.cs' ) + with WrapOmniSharpServer( app, filepath ): + contents = ReadFile( filepath ) + + completion_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + line_num = 43, + column_num = 26 ) + response_data = app.post_json( '/completions', completion_data ).json + assert_that( response_data[ 'completions' ], + has_items( + CompletionEntryMatcher( 'DoATest()' ), + CompletionEntryMatcher( 'an_int' ), + CompletionEntryMatcher( 'a_unicøde' ), + CompletionEntryMatcher( 'øøø' ) ) ) + + eq_( 26, response_data[ 'completion_start_column' ] ) + + +@SharedYcmd def GetCompletions_MultipleSolution_test( app ): filepaths = [ PathToTestFile( 'testy', 'Program.cs' ), PathToTestFile( 'testy-multiple-solutions', @@ -378,20 +400,10 @@ contents = contents, event_name = 'FileReadyToParse' ) - exception_caught = False - try: - app.post_json( '/event_notification', event_data ) - except AppError as e: - if 'Autodetection of solution file failed' in str( e ): - exception_caught = True - - # The test passes if we caught an exception when trying to start it, - # so raise one if it managed to start - if not exception_caught: - WaitUntilOmniSharpServerReady( app, filepath ) - StopOmniSharpServer( app, filepath ) - raise Exception( 'The Omnisharp server started, despite us not being ' - 'able to find a suitable solution file to feed it. Did ' - 'you fiddle with the solution finding code in ' - 'cs_completer.py? Hopefully you\'ve enhanced it: you ' - 'need to update this test then :)' ) + assert_that( + calling( app.post_json ).with_args( '/event_notification', event_data ), + raises( AppError, 'Autodetection of solution file failed' ), + "The Omnisharp server started, despite us not being able to find a " + "suitable solution file to feed it. Did you fiddle with the solution " + "finding code in cs_completer.py? Hopefully you've enhanced it: you need " + "to update this test then :)" ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/cs/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,10 +26,11 @@ from contextlib import contextmanager import functools import os -import time from ycmd import handlers -from ycmd.tests.test_utils import BuildRequest, SetUpApp +from ycmd.tests.test_utils import ( ClearCompletionsCache, SetUpApp, + StartCompleterServer, StopCompleterServer, + WaitUntilCompleterServerReady ) shared_app = None shared_filepaths = [] @@ -40,47 +41,6 @@ return os.path.join( dir_of_current_script, 'testdata', *args ) -def StartOmniSharpServer( app, filepath ): - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ "StartServer" ], - filepath = filepath, - filetype = 'cs' ) ) - - -def StopOmniSharpServer( app, filepath ): - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'StopServer' ], - filepath = filepath, - filetype = 'cs' ) ) - - -def WaitUntilOmniSharpServerReady( app, filepath ): - retries = 100 - success = False - - # If running on Travis CI, keep trying forever. Travis will kill the worker - # after 10 mins if nothing happens. - while retries > 0 or OnTravis(): - result = app.get( '/ready', { 'subserver': 'cs' } ).json - if result: - success = True - break - request = BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'ServerIsRunning' ], - filepath = filepath, - filetype = 'cs' ) - result = app.post_json( '/run_completer_command', request ).json - if not result: - raise RuntimeError( "OmniSharp failed during startup." ) - time.sleep( 0.2 ) - retries = retries - 1 - - if not success: - raise RuntimeError( "Timeout waiting for OmniSharpServer" ) - - def setUpPackage(): """Initializes the ycmd server as a WebTest application that will be shared by all tests using the SharedYcmd decorator in this package. Additional @@ -100,7 +60,7 @@ global shared_app, shared_filepaths for filepath in shared_filepaths: - StopOmniSharpServer( shared_app, filepath ) + StopCompleterServer( shared_app, 'cs', filepath ) @contextmanager @@ -108,9 +68,9 @@ global shared_filepaths if filepath not in shared_filepaths: - StartOmniSharpServer( app, filepath ) + StartCompleterServer( app, 'cs', filepath ) shared_filepaths.append( filepath ) - WaitUntilOmniSharpServerReady( app, filepath ) + WaitUntilCompleterServerReady( app, 'cs' ) yield @@ -123,6 +83,7 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/cs/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,15 +23,22 @@ standard_library.install_aliases() from builtins import * # noqa + from nose.tools import eq_, ok_ from webtest import AppError +from hamcrest import assert_that, has_entries, contains +import pprint import re import os.path from ycmd.tests.cs import ( IsolatedYcmd, PathToTestFile, SharedYcmd, - StopOmniSharpServer, WaitUntilOmniSharpServerReady, WrapOmniSharpServer ) -from ycmd.tests.test_utils import BuildRequest, UserOption +from ycmd.tests.test_utils import ( BuildRequest, + ChunkMatcher, + LocationMatcher, + StopCompleterServer, + UserOption, + WaitUntilCompleterServerReady ) from ycmd.utils import ReadFile @@ -57,6 +64,27 @@ @SharedYcmd +def Subcommands_GoTo_Unicode_test( app ): + filepath = PathToTestFile( 'testy', 'Unicode.cs' ) + with WrapOmniSharpServer( app, filepath ): + contents = ReadFile( filepath ) + + goto_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GoTo' ], + line_num = 45, + column_num = 43, + contents = contents, + filetype = 'cs', + filepath = filepath ) + + eq_( { + 'filepath': PathToTestFile( 'testy', 'Unicode.cs' ), + 'line_num': 30, + 'column_num': 37 + }, app.post_json( '/run_completer_command', goto_data ).json ) + + +@SharedYcmd def Subcommands_GoToImplementation_Basic_test( app ): filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' ) with WrapOmniSharpServer( app, filepath ): @@ -207,6 +235,33 @@ @SharedYcmd +def Subcommands_GetToImplementation_Unicode_test( app ): + filepath = PathToTestFile( 'testy', 'Unicode.cs' ) + with WrapOmniSharpServer( app, filepath ): + contents = ReadFile( filepath ) + + goto_data = BuildRequest( + completer_target = 'filetype_default', + command_arguments = [ 'GoToImplementation' ], + line_num = 48, + column_num = 44, + contents = contents, + filetype = 'cs', + filepath = filepath + ) + + eq_( [ { + 'filepath': PathToTestFile( 'testy', 'Unicode.cs' ), + 'line_num': 49, + 'column_num': 54 + }, { + 'filepath': PathToTestFile( 'testy', 'Unicode.cs' ), + 'line_num': 50, + 'column_num': 50 + } ], app.post_json( '/run_completer_command', goto_data ).json ) + + +@SharedYcmd def Subcommands_GetType_EmptyMessage_test( app ): filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' ) with WrapOmniSharpServer( app, filepath ): @@ -343,8 +398,12 @@ }, app.post_json( '/run_completer_command', getdoc_data ).json ) -def RunFixItTest( app, line, column, expected_result ): - filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) +def RunFixItTest( app, + line, + column, + result_matcher, + filepath = [ 'testy', 'FixItTestCase.cs' ] ): + filepath = PathToTestFile( *filepath ) with WrapOmniSharpServer( app, filepath ): contents = ReadFile( filepath ) @@ -356,179 +415,95 @@ filetype = 'cs', filepath = filepath ) - eq_( expected_result, - app.post_json( '/run_completer_command', fixit_data ).json ) + response = app.post_json( '/run_completer_command', fixit_data ).json + + pprint.pprint( response ) + + assert_that( response, result_matcher ) @SharedYcmd def Subcommands_FixIt_RemoveSingleLine_test( app ): filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) - RunFixItTest( app, 11, 1, { - u'fixits': [ - { - u'location': { - u'line_num': 11, - u'column_num': 1, - u'filepath': filepath - }, - u'chunks': [ - { - u'replacement_text': '', - u'range': { - u'start': { - u'line_num': 10, - u'column_num': 20, - u'filepath': filepath - }, - u'end': { - u'line_num': 11, - u'column_num': 30, - u'filepath': filepath - }, - } - } - ] - } - ] - } ) + RunFixItTest( app, 11, 1, has_entries( { + 'fixits': contains( has_entries( { + 'location': LocationMatcher( filepath, 11, 1 ), + 'chunks': contains( ChunkMatcher( '', + LocationMatcher( filepath, 10, 20 ), + LocationMatcher( filepath, 11, 30 ) ) ) + } ) ) + } ) ) @SharedYcmd def Subcommands_FixIt_MultipleLines_test( app ): filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) - RunFixItTest( app, 19, 1, { - u'fixits': [ - { - u'location': { - u'line_num': 19, - u'column_num': 1, - u'filepath': filepath - }, - u'chunks': [ - { - u'replacement_text': "return On", - u'range': { - u'start': { - u'line_num': 20, - u'column_num': 13, - u'filepath': filepath - }, - u'end': { - u'line_num': 21, - u'column_num': 35, - u'filepath': filepath - }, - } - } - ] - } - ] - } ) + RunFixItTest( app, 19, 1, has_entries( { + 'fixits': contains( has_entries ( { + 'location': LocationMatcher( filepath, 19, 1 ), + 'chunks': contains( ChunkMatcher( 'return On', + LocationMatcher( filepath, 20, 13 ), + LocationMatcher( filepath, 21, 35 ) ) ) + } ) ) + } ) ) @SharedYcmd def Subcommands_FixIt_SpanFileEdge_test( app ): filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) - RunFixItTest( app, 1, 1, { - u'fixits': [ - { - u'location': { - u'line_num': 1, - u'column_num': 1, - u'filepath': filepath - }, - u'chunks': [ - { - u'replacement_text': 'System', - u'range': { - u'start': { - u'line_num': 1, - u'column_num': 7, - u'filepath': filepath - }, - u'end': { - u'line_num': 3, - u'column_num': 18, - u'filepath': filepath - }, - } - } - ] - } - ] - } ) + RunFixItTest( app, 1, 1, has_entries( { + 'fixits': contains( has_entries ( { + 'location': LocationMatcher( filepath, 1, 1 ), + 'chunks': contains( ChunkMatcher( 'System', + LocationMatcher( filepath, 1, 7 ), + LocationMatcher( filepath, 3, 18 ) ) ) + } ) ) + } ) ) @SharedYcmd def Subcommands_FixIt_AddTextInLine_test( app ): filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) - RunFixItTest( app, 9, 1, { - u'fixits': [ - { - u'location': { - u'line_num': 9, - u'column_num': 1, - u'filepath': filepath - }, - u'chunks': [ - { - u'replacement_text': ', StringComparison.Ordinal', - u'range': { - u'start': { - u'line_num': 9, - u'column_num': 29, - u'filepath': filepath - }, - u'end': { - u'line_num': 9, - u'column_num': 29, - u'filepath': filepath - }, - } - } - ] - } - ] - } ) + RunFixItTest( app, 9, 1, has_entries( { + 'fixits': contains( has_entries ( { + 'location': LocationMatcher( filepath, 9, 1 ), + 'chunks': contains( ChunkMatcher( ', StringComparison.Ordinal', + LocationMatcher( filepath, 9, 29 ), + LocationMatcher( filepath, 9, 29 ) ) ) + } ) ) + } ) ) @SharedYcmd def Subcommands_FixIt_ReplaceTextInLine_test( app ): filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' ) - RunFixItTest( app, 10, 1, { - u'fixits': [ - { - u'location': { - u'line_num': 10, - u'column_num': 1, - u'filepath': filepath - }, - u'chunks': [ - { - u'replacement_text': 'const int', - u'range': { - u'start': { - u'line_num': 10, - u'column_num': 13, - u'filepath': filepath - }, - u'end': { - u'line_num': 10, - u'column_num': 16, - u'filepath': filepath - }, - } - } - ] - } - ] - } ) + RunFixItTest( app, 10, 1, has_entries( { + 'fixits': contains( has_entries ( { + 'location': LocationMatcher( filepath, 10, 1 ), + 'chunks': contains( ChunkMatcher( 'const int', + LocationMatcher( filepath, 10, 13 ), + LocationMatcher( filepath, 10, 16 ) ) ) + } ) ) + } ) ) + + +@SharedYcmd +def Subcommands_FixIt_Unicode_test( app ): + filepath = PathToTestFile( 'testy', 'Unicode.cs' ) + RunFixItTest( app, 30, 54, has_entries( { + 'fixits': contains( has_entries ( { + 'location': LocationMatcher( filepath, 30, 54 ), + 'chunks': contains( ChunkMatcher( ' readonly', + LocationMatcher( filepath, 30, 44 ), + LocationMatcher( filepath, 30, 44 ) ) ) + } ) ) + } ), filepath = [ 'testy', 'Unicode.cs' ] ) @IsolatedYcmd def Subcommands_StopServer_NoErrorIfNotStarted_test( app ): filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' ) - StopOmniSharpServer( app, filepath ) + StopCompleterServer( app, 'cs', filepath ) # Success = no raise @@ -543,15 +518,15 @@ event_name = 'FileReadyToParse' ) app.post_json( '/event_notification', event_data ) - WaitUntilOmniSharpServerReady( app, filepath ) + WaitUntilCompleterServerReady( app, 'cs' ) event_data = BuildRequest( filetype = 'cs', filepath = filepath ) debuginfo = app.post_json( '/debug_info', event_data ).json - log_files_match = re.search( "^OmniSharp logfiles:\n(.*)\n(.*)", - debuginfo, - re.MULTILINE ) + log_files_match = re.search( '^ OmniSharp logfiles:\n' + ' (.*)\n' + ' (.*)', debuginfo, re.MULTILINE ) stdout_logfiles_location = log_files_match.group( 1 ) stderr_logfiles_location = log_files_match.group( 2 ) @@ -561,7 +536,7 @@ ok_( os.path.exists( stderr_logfiles_location ), "Logfile should exist at {0}".format( stderr_logfiles_location ) ) finally: - StopOmniSharpServer( app, filepath ) + StopCompleterServer( app, 'cs', filepath ) if keeping_log_files: ok_( os.path.exists( stdout_logfiles_location ), diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/DiagnosticRange.cs ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/DiagnosticRange.cs --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/DiagnosticRange.cs 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/DiagnosticRange.cs 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,4 @@ +public class Class +{ + public int attribute; +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/GetDocTestCase.cs ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/GetDocTestCase.cs --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/GetDocTestCase.cs 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/GetDocTestCase.cs 2016-12-20 08:50:19.000000000 +0000 @@ -32,5 +32,5 @@ GetDocTestCase tc; tc.DoATest(); } - } + } } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/Unicode.cs ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/Unicode.cs --- ycmd-0+20160327+gitc3e6904/ycmd/tests/cs/testdata/testy/Unicode.cs 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/cs/testdata/testy/Unicode.cs 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,52 @@ +/** + * testy is a namespace for testing + */ +namespace testy { + /** + * Tests the GetDoc subcommands + */ + public class Unicøde { + /** + * Constructor + */ + public Unicøde () { + this.an_int = 1; + } + + /** + * Very important methød. + * + * With multiple lines of commentary + * And Format- + * -ting + */ + public int DoATest() { + return an_int; + } + + /// an integer, or something + private int an_int; + + private int øøø; private Unicøde a_unicøde; + + private static void DoSomething( Unicøde unicode ) + { + + } + + /// Use this for testing + private static void DoTesting() { + Unicøde tc; + tc.DoATest(); + + Unicøde øø; + øø. + + DoSomething( a_unicøde ); + } + + interface Bøøm { void Båm(); } + public class BøømBøøm : Bøøm { public void Båm() { } } + public class BåmBåm : Bøøm { public void Båm() { } } + } +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/diagnostics_test.py ycmd-0+20161219+git486b809/ycmd/tests/diagnostics_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/diagnostics_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/diagnostics_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,7 +26,7 @@ from hamcrest import assert_that from mock import patch from nose.tools import eq_ -import http.client +import requests from ycmd.responses import NoDiagnosticSupport, BuildDisplayMessageResponse from ycmd.tests import SharedYcmd @@ -45,7 +45,7 @@ diag_data, expect_errors = True ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( NoDiagnosticSupport ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/extra_conf_store_test.py ycmd-0+20161219+git486b809/ycmd/tests/extra_conf_store_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/extra_conf_store_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/extra_conf_store_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,168 @@ +# 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 __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, + same_instance ) +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 ) ) + + + def Load_DoNotReloadExtraConf_NoForce_test( self ): + extra_conf_file = PathToTestFile( 'extra_conf', 'project', + '.ycm_extra_conf.py' ) + with patch( 'ycmd.extra_conf_store._ShouldLoad', return_value = True ): + module = extra_conf_store.Load( extra_conf_file ) + assert_that( inspect.ismodule( module ) ) + assert_that( inspect.getfile( module ), equal_to( extra_conf_file ) ) + assert_that( + extra_conf_store.Load( extra_conf_file ), + same_instance( module ) + ) + + + def Load_DoNotReloadExtraConf_ForceEqualsTrue_test( self ): + extra_conf_file = PathToTestFile( 'extra_conf', 'project', + '.ycm_extra_conf.py' ) + with patch( 'ycmd.extra_conf_store._ShouldLoad', return_value = True ): + module = extra_conf_store.Load( extra_conf_file ) + assert_that( inspect.ismodule( module ) ) + assert_that( inspect.getfile( module ), equal_to( extra_conf_file ) ) + assert_that( + extra_conf_store.Load( extra_conf_file, force = True ), + same_instance( module ) + ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/filename_completer_test.py ycmd-0+20161219+git486b809/ycmd/tests/filename_completer_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/filename_completer_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/filename_completer_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,16 +26,19 @@ from builtins import * # noqa import os -from nose.tools import eq_ +from hamcrest import assert_that, contains_inanyorder +from nose.tools import eq_, ok_ from ycmd.completers.general.filename_completer import FilenameCompleter from ycmd.request_wrap import RequestWrap from ycmd import user_options_store +from ycmd.tests.test_utils import CurrentWorkingDirectory, UserOption +from ycmd.utils import GetCurrentDirectory, ToBytes TEST_DIR = os.path.dirname( os.path.abspath( __file__ ) ) DATA_DIR = os.path.join( TEST_DIR, - "testdata", - "filename_completer", - "inner_dir" ) + 'testdata', + 'filename_completer', + 'inner_dir' ) PATH_TO_TEST_FILE = os.path.join( DATA_DIR, "test.cpp" ) REQUEST_DATA = { @@ -45,9 +48,18 @@ } -def _CompletionResultsForLine( filename_completer, contents, extra_data=None ): +def _CompletionResultsForLine( filename_completer, + contents, + extra_data = None, + column_num = None ): request = REQUEST_DATA.copy() - request[ 'column_num' ] = len( contents ) + 1 + + # Strictly, column numbers are *byte* offsets, not character offsets. If + # the contents of the file contain unicode characters, then we should manually + # supply the correct byte offset. + column_num = len( contents ) + 1 if not column_num else column_num + + request[ 'column_num' ] = column_num request[ 'file_data' ][ PATH_TO_TEST_FILE ][ 'contents' ] = contents if extra_data: request.update( extra_data ) @@ -58,6 +70,26 @@ for c in candidates ] +def _ShouldUseNowForLine( filename_completer, + contents, + extra_data = None, + column_num = None ): + request = REQUEST_DATA.copy() + + # Strictly, column numbers are *byte* offsets, not character offsets. If + # the contents of the file contain unicode characters, then we should manually + # supply the correct byte offset. + column_num = len( contents ) + 1 if not column_num else column_num + + request[ 'column_num' ] = column_num + request[ 'file_data' ][ PATH_TO_TEST_FILE ][ 'contents' ] = contents + if extra_data: + request.update( extra_data ) + + request = RequestWrap( request ) + return filename_completer.ShouldUseNow( request ) + + class FilenameCompleter_test( object ): def setUp( self ): self._filename_completer = FilenameCompleter( @@ -71,8 +103,16 @@ ] - def _CompletionResultsForLine( self, contents ): - return _CompletionResultsForLine( self._filename_completer, contents ) + def _CompletionResultsForLine( self, contents, column_num=None ): + return _CompletionResultsForLine( self._filename_completer, + contents, + column_num = column_num ) + + + def _ShouldUseNowForLine( self, contents, column_num=None ): + return _ShouldUseNowForLine( self._filename_completer, + contents, + column_num = column_num ) def QuotedIncludeCompletion_test( self ): @@ -166,7 +206,7 @@ os.environ.pop( 'YCMTESTDIR' ) - eq_( [ ('inner_dir', '[Dir]') ], data ) + eq_( [ ( 'inner_dir', '[Dir]' ), ( '∂†∫', '[Dir]' ) ], data ) def EnvVar_AtStart_Dir_Partial_test( self ): @@ -175,7 +215,7 @@ 'set x = $ycm_test_dir/testdata/filename_completer/inn' ) ) os.environ.pop( 'ycm_test_dir' ) - eq_( [ ('inner_dir', '[Dir]') ], data ) + eq_( [ ( 'inner_dir', '[Dir]' ), ( '∂†∫', '[Dir]' ) ], data ) def EnvVar_InMiddle_File_test( self ): @@ -214,7 +254,7 @@ 'set x = ' + TEST_DIR + '/${YCM_TEST_td}ata/filename_completer/' ) ) os.environ.pop( 'YCM_TEST_td' ) - eq_( [ ('inner_dir', '[Dir]') ], data ) + eq_( [ ( 'inner_dir', '[Dir]' ), ( '∂†∫', '[Dir]' ) ], data ) def EnvVar_InMiddle_Dir_Partial_test( self ): @@ -223,7 +263,7 @@ 'set x = ' + TEST_DIR + '/tes${YCM_TEST_td}/filename_completer/' ) ) os.environ.pop( 'YCM_TEST_td' ) - eq_( [ ('inner_dir', '[Dir]') ], data ) + eq_( [ ( 'inner_dir', '[Dir]' ), ( '∂†∫', '[Dir]' ) ], data ) def EnvVar_Undefined_test( self ): @@ -241,7 +281,7 @@ + '/testdata/filename_completer${YCM_empty_var}/' ) ) os.environ.pop( 'YCM_empty_var' ) - eq_( [ ('inner_dir', '[Dir]') ], data ) + eq_( [ ( 'inner_dir', '[Dir]' ), ( '∂†∫', '[Dir]' ) ], data ) def EnvVar_Undefined_Garbage_test( self ): @@ -271,78 +311,95 @@ eq_( [ ], data ) -def WorkingDir_Use_File_Path_test(): - assert os.getcwd() != DATA_DIR, ( "Please run this test from a different " - "directory" ) - - options = user_options_store.DefaultOptions() - options.update( { - 'filepath_completion_use_working_dir': 0 - } ) - completer = FilenameCompleter( options ) - - data = sorted( _CompletionResultsForLine( completer, 'ls ./include/' ) ) - eq_( [ - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ), - ], data ) - - -def WorkingDir_Use_ycmd_WD_test(): - # Store the working directory so we can return to it - wd = os.getcwd() - - test_dir = os.path.join( DATA_DIR, 'include' ) - assert wd != test_dir, "Please run this test from a different directory" - - try: - options = user_options_store.DefaultOptions() - options.update( { - 'filepath_completion_use_working_dir': 1 - } ) + def Unicode_In_Line_Works_test( self ): + eq_( True, self._ShouldUseNowForLine( + contents = "var x = /†/testing", + # The † character is 3 bytes in UTF-8 + column_num = 15 ) ) + eq_( [ ], self._CompletionResultsForLine( + contents = "var x = /†/testing", + # The † character is 3 bytes in UTF-8 + column_num = 15 ) ) + + + def Unicode_Paths_test( self ): + contents = "test " + DATA_DIR + "/../∂" + # The column number is the first byte of the ∂ character (1-based ) + column_num = ( len( ToBytes( "test" ) ) + + len( ToBytes( DATA_DIR ) ) + + len( ToBytes( '/../' ) ) + + 1 + # 0-based offset of ∂ + 1 ) # Make it 1-based + eq_( True, self._ShouldUseNowForLine( contents, column_num = column_num ) ) + assert_that( self._CompletionResultsForLine( contents, + column_num = column_num ), + contains_inanyorder( ( 'inner_dir', '[Dir]' ), + ( '∂†∫', '[Dir]' ) ) ) + + +def WorkingDir_UseFilePath_test(): + ok_( GetCurrentDirectory() != DATA_DIR, ( 'Please run this test from a ' + 'different directory' ) ) + with UserOption( 'filepath_completion_use_working_dir', 0 ) as options: completer = FilenameCompleter( options ) - # Change current directory to DATA_DIR/include (path to which we expect - # results to be relative) - os.chdir( test_dir ) - - # We don't supply working_dir in the request, so the current working - # directory is used. - data = sorted( _CompletionResultsForLine( completer, 'ls ./' ) ) + data = sorted( _CompletionResultsForLine( completer, 'ls ./include/' ) ) eq_( [ - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ), - ], data ) + ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) + ], data ) + + +def WorkingDir_UseServerWorkingDirectory_test(): + test_dir = os.path.join( DATA_DIR, 'include' ) + with CurrentWorkingDirectory( test_dir ) as old_current_dir: + ok_( old_current_dir != test_dir, ( 'Please run this test from a different ' + 'directory' ) ) + + with UserOption( 'filepath_completion_use_working_dir', 1 ) as options: + completer = FilenameCompleter( options ) + + # We don't supply working_dir in the request, so the current working + # directory is used. + data = sorted( _CompletionResultsForLine( completer, 'ls ./' ) ) + eq_( [ + ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) + ], data ) - finally: - os.chdir( wd ) +def WorkingDir_UseServerWorkingDirectory_Unicode_test(): + test_dir = os.path.join( TEST_DIR, 'testdata', 'filename_completer', '∂†∫' ) + with CurrentWorkingDirectory( test_dir ) as old_current_dir: + ok_( old_current_dir != test_dir, ( 'Please run this test from a different ' + 'directory' ) ) + + with UserOption( 'filepath_completion_use_working_dir', 1 ) as options: + completer = FilenameCompleter( options ) + + # We don't supply working_dir in the request, so the current working + # directory is used. + data = sorted( _CompletionResultsForLine( completer, 'ls ./' ) ) + eq_( [ + ( '†es†.txt', '[File]' ) + ], data ) -def WorkingDir_Use_Client_WD_test(): - # Store the working directory so we can return to it - wd = os.getcwd() +def WorkingDir_UseClientWorkingDirectory_test(): test_dir = os.path.join( DATA_DIR, 'include' ) - assert wd != test_dir, "Please run this test from a different directory" - - try: - options = user_options_store.DefaultOptions() - options.update( { - 'filepath_completion_use_working_dir': 1 - } ) + ok_( GetCurrentDirectory() != test_dir, ( 'Please run this test from a ' + 'different directory' ) ) + with UserOption( 'filepath_completion_use_working_dir', 1 ) as options: completer = FilenameCompleter( options ) - # We supply working_dir in the request, so we expect results to be relative - # to the supplied path + # We supply working_dir in the request, so we expect results to be + # relative to the supplied path. data = sorted( _CompletionResultsForLine( completer, 'ls ./', { - 'working_dir': os.path.join( DATA_DIR, 'include' ) + 'working_dir': test_dir } ) ) eq_( [ - ( 'Qt', '[Dir]' ), - ( 'QtGui', '[Dir]' ), - ], data ) - - finally: - os.chdir( wd ) + ( 'Qt', '[Dir]' ), + ( 'QtGui', '[Dir]' ) + ], data ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2013 Google Inc. # 2015 ycmd contributors # @@ -24,14 +26,15 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import assert_that, equal_to, has_items +from hamcrest import ( assert_that, equal_to, has_items, + contains_string, contains_inanyorder ) from mock import patch from nose.tools import eq_ -from ycmd.tests import SharedYcmd +from ycmd.tests import SharedYcmd, PathToTestFile from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher, DummyCompleter, PatchCompleter, - UserOption ) + UserOption, ExpectedFailure ) @SharedYcmd @@ -70,6 +73,22 @@ @SharedYcmd +def GetCompletions_IdentifierCompleter_FilterShortCandidates_test( app ): + with UserOption( 'min_num_identifier_candidate_chars', 4 ): + event_data = BuildRequest( contents = 'foo foogoo gooo', + event_name = 'FileReadyToParse' ) + app.post_json( '/event_notification', event_data ) + + completion_data = BuildRequest( contents = 'oo', column_num = 3 ) + response = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + + assert_that( response, + contains_inanyorder( CompletionEntryMatcher( 'foogoo' ), + CompletionEntryMatcher( 'gooo' ) ) ) + + +@SharedYcmd def GetCompletions_IdentifierCompleter_StartColumn_AfterWord_test( app ): completion_data = BuildRequest( contents = 'oo foo foogoo ba', column_num = 11 ) @@ -106,6 +125,60 @@ @SharedYcmd +def GetCompletions_IdentifierCompleter_Unicode_InLine_test( app ): + contents = """ + This is some text cøntaining unicøde + """ + + event_data = BuildRequest( contents = contents, + filetype = 'css', + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + + # query is 'tx' + completion_data = BuildRequest( contents = 'tx ' + contents, + filetype = 'css', + column_num = 3 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + + assert_that( + results, + has_items( CompletionEntryMatcher( 'text', '[ID]' ) ) + ) + + +@ExpectedFailure( 'The identifier completer does not support ' + 'unicode characters', + contains_string( '[]' ) ) +@SharedYcmd +def GetCompletions_IdentifierCompleter_UnicodeQuery_InLine_test( app ): + contents = """ + This is some text cøntaining unicøde + """ + + event_data = BuildRequest( contents = contents, + filetype = 'css', + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + + # query is 'cø' + completion_data = BuildRequest( contents = 'cø ' + contents, + filetype = 'css', + column_num = 4 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + + assert_that( + results, + has_items( CompletionEntryMatcher( 'cøntaining', '[ID]' ), + CompletionEntryMatcher( 'unicøde', '[ID]' ) ) + ) + + +@SharedYcmd @patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList', return_value = [ 'foo', 'bar', 'qux' ] ) def GetCompletions_ForceSemantic_Works_test( app, *args ): @@ -138,6 +211,50 @@ @SharedYcmd +def GetCompletions_IdentifierCompleter_TagsAdded_test( app ): + event_data = BuildRequest( event_name = 'FileReadyToParse', + tag_files = [ PathToTestFile( 'basic.tags' ) ] ) + app.post_json( '/event_notification', event_data ) + + completion_data = BuildRequest( contents = 'oo', + column_num = 3, + filetype = 'cpp' ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'foosy' ), + CompletionEntryMatcher( 'fooaaa' ) ) ) + + +@SharedYcmd +def GetCompletions_IdentifierCompleter_JustFinishedIdentifier_test( app ): + event_data = BuildRequest( event_name = 'CurrentIdentifierFinished', + column_num = 4, + contents = 'foo' ) + app.post_json( '/event_notification', event_data ) + + completion_data = BuildRequest( contents = 'oo', column_num = 3 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'foo' ) ) ) + + +@SharedYcmd +def GetCompletions_IdentifierCompleter_IdentifierUnderCursor_test( app ): + event_data = BuildRequest( event_name = 'InsertLeave', + column_num = 2, + contents = 'foo' ) + app.post_json( '/event_notification', event_data ) + + completion_data = BuildRequest( contents = 'oo', column_num = 3 ) + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'foo' ) ) ) + + +@SharedYcmd def GetCompletions_UltiSnipsCompleter_Works_test( app ): event_data = BuildRequest( event_name = 'BufferVisit', @@ -352,3 +469,53 @@ # We ask for candidates twice because of cache invalidation: # completion types are different between requests. assert_that( candidates_list.call_count, equal_to( 2 ) ) + + +@SharedYcmd +@patch( 'ycmd.tests.test_utils.DummyCompleter.ShouldUseNowInner', + return_value = True ) +@patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList', + return_value = [ 'aba', 'cbc' ] ) +def GetCompletions_FilterThenReturnFromCache_test( app, + candidates_list, + *args ): + + with PatchCompleter( DummyCompleter, 'dummy_filetype' ): + # First, fill the cache with an empty query + completion_data = BuildRequest( filetype = 'dummy_filetype', + contents = 'objectA.', + line_num = 1, + column_num = 9 ) + + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'aba' ), + CompletionEntryMatcher( 'cbc' ) ) ) + + # Now, filter them. This causes them to be converted to bytes and back + completion_data = BuildRequest( filetype = 'dummy_filetype', + contents = 'objectA.c', + line_num = 1, + column_num = 10 ) + + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'cbc' ) ) ) + + # Finally, request the original (unfiltered) set again. Ensure that we get + # proper results (not some bytes objects) + # First, fill the cache with an empty query + completion_data = BuildRequest( filetype = 'dummy_filetype', + contents = 'objectA.', + line_num = 1, + column_num = 9 ) + + results = app.post_json( '/completions', + completion_data ).json[ 'completions' ] + assert_that( results, + has_items( CompletionEntryMatcher( 'aba' ), + CompletionEntryMatcher( 'cbc' ) ) ) + + assert_that( candidates_list.call_count, equal_to( 1 ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/go/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,73 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.go import IsolatedYcmd, SharedYcmd +from ycmd.tests.test_utils import BuildRequest, StopCompleterServer, UserOption + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + request_data = BuildRequest( filetype = 'go' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Go completer debug information:\n' + ' Gocode running at: http://127.0.0.1:\d+\n' + ' Gocode process ID: \d+\n' + ' Gocode executable: .+\n' + ' Gocode logfiles:\n' + ' .+\n' + ' .+\n' + ' Godef executable: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + StopCompleterServer( app, 'go' ) + request_data = BuildRequest( filetype = 'go' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Go completer debug information:\n' + ' Gocode no longer running\n' + ' Gocode executable: .+\n' + ' Gocode logfiles:\n' + ' .+\n' + ' .+\n' + ' Godef executable: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + StopCompleterServer( app, 'go' ) + request_data = BuildRequest( filetype = 'go' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Go completer debug information:\n' + ' Gocode is not running\n' + ' Gocode executable: .+\n' + ' Godef executable: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/go/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2015 ycmd contributors # # This file is part of ycmd. @@ -23,7 +25,7 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import assert_that, has_item +from hamcrest import assert_that, has_item, has_items from ycmd.tests.go import PathToTestFile, SharedYcmd from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher @@ -44,3 +46,35 @@ completion_data ).json[ 'completions' ] assert_that( results, has_item( CompletionEntryMatcher( u'Logger' ) ) ) + + +@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' ) ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/go_completer_test.py ycmd-0+20161219+git486b809/ycmd/tests/go/go_completer_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/go_completer_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/go_completer_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,12 +25,17 @@ standard_library.install_aliases() 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 eq_, raises -from ycmd.completers.go.go_completer import GoCompleter, GO_BINARIES, FindBinary + +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 +from ycmd.utils import ReadFile, ToBytes TEST_DIR = os.path.dirname( os.path.abspath( __file__ ) ) DATA_DIR = os.path.join( TEST_DIR, 'testdata' ) @@ -51,133 +56,132 @@ } -class GoCompleter_test( object ): - def setUp( self ): - user_options = user_options_store.DefaultOptions() - user_options[ 'gocode_binary_path' ] = DUMMY_BINARY - self._completer = GoCompleter( user_options ) - - - def _BuildRequest( self, line_num, column_num ): - request = REQUEST_DATA.copy() - request[ 'column_num' ] = column_num - request[ 'line_num' ] = line_num - request[ 'file_data' ][ PATH_TO_TEST_FILE ][ 'contents' ] = ReadFile( - PATH_TO_TEST_FILE ) - return RequestWrap( request ) +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 FindGoCodeBinary_test( self ): +def SetUpGoCompleter( test ): + @functools.wraps( test ) + def Wrapper( *args, **kwargs ): 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 ) ) - - - # Test line-col to offset in the file before any unicode occurrences. - def ComputeCandidatesInnerOffsetBeforeUnicode_test( self ): - mock = MockPopen( returncode = 0, - stdout = ReadFile( PATH_TO_POS121_RES ), - stderr = '' ) - self._completer._popener = mock - # Col 8 corresponds to cursor at log.Pr^int("Line 7 ... - self._completer.ComputeCandidatesInner( self._BuildRequest( 7, 8 ) ) - eq_( mock.cmd, [ - DUMMY_BINARY, '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '121' ] ) - - - # Test line-col to offset in the file after a unicode occurrences. - def ComputeCandidatesInnerAfterUnicode_test( self ): - mock = MockPopen( returncode = 0, - stdout = ReadFile( PATH_TO_POS215_RES ), - stderr = '' ) - self._completer._popener = mock - # Col 9 corresponds to cursor at log.Pri^nt("Line 7 ... - self._completer.ComputeCandidatesInner(self._BuildRequest(9, 9)) - eq_( mock.cmd, [ - DUMMY_BINARY, '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '215' ] ) - - - # Test end to end parsing of completed results. - def ComputeCandidatesInner_test( self ): - mock = MockPopen( returncode = 0, - stdout = ReadFile( PATH_TO_POS292_RES ), - stderr = '' ) - self._completer._popener = mock - # Col 40 corresponds to cursor at ..., log.Prefi^x ... - result = self._completer.ComputeCandidatesInner( - self._BuildRequest( 10, 40 ) ) - eq_( mock.cmd, [ - DUMMY_BINARY, '-f=json', 'autocomplete', PATH_TO_TEST_FILE, '292' ] ) - 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. - @raises( RuntimeError ) - def ComputeCandidatesInnerGoCodeFailure_test( self ): - mock = MockPopen( returncode = 1, stdout = '', stderr = '' ) - self._completer._popener = mock - self._completer.ComputeCandidatesInner( self._BuildRequest( 1, 1 ) ) - - # Test JSON parsing failure. - @raises( RuntimeError ) - def ComputeCandidatesInnerParseFailure_test( self ): - mock = MockPopen( returncode = 0, - stdout = "{this isn't parseable", - stderr = '' ) - self._completer._popener = mock - self._completer.ComputeCandidatesInner( self._BuildRequest( 1, 1 ) ) - - # Test empty results error (different than no results). - @raises( RuntimeError ) - def ComputeCandidatesInnerNoResultsFailure_test( self ): - mock = MockPopen( returncode = 0, stdout = '[]', stderr = '' ) - self._completer._popener = mock - self._completer.ComputeCandidatesInner( self._BuildRequest( 1, 1 ) ) - - # Test empty results error (different than no results). - @raises( RuntimeError ) - def ComputeCandidatesGoCodePanic_test( self ): - mock = MockPopen( returncode = 0, - stdout = ReadFile( PATH_TO_PANIC_OUTPUT_RES ), - stderr = '' ) - self._completer._popener = mock - self._completer.ComputeCandidatesInner( self._BuildRequest( 1, 1 ) ) - - -class MockSubprocess( object ): - def __init__( self, returncode, stdout, stderr ): - self.returncode = returncode - self.stdout = stdout - self.stderr = stderr - - - def communicate( self, stdin ): - self.stdin = stdin - return ( self.stdout, self.stderr ) - - - -class MockPopen( object ): - def __init__( self, returncode = None, stdout = None, stderr = None ): - self._returncode = returncode - self._stdout = stdout - self._stderr = stderr - # cmd will be populated when a subprocess is created. - self.cmd = None - - - def __call__( self, cmd, stdout = None, stderr = None, stdin = None ): - self.cmd = cmd - return MockSubprocess( self._returncode, self._stdout, self._stderr ) + 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_address, + '-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_address, + '-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 ... + result = completer.ComputeCandidatesInner( BuildRequest( 10, 40 ) ) + execute_command.assert_called_once_with( + [ DUMMY_BINARY, '-sock', 'tcp', '-addr', completer._gocode_address, + '-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_test( completer, *args ): + assert_that( + calling( completer.ComputeCandidatesInner ).with_args( + BuildRequest( 1, 1 ) ), + raises( RuntimeError, 'No completions found.' ) ) + + +# 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.' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/go/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,7 +26,10 @@ import functools import os -from ycmd.tests.test_utils import BuildRequest, SetUpApp +from ycmd import handlers +from ycmd.tests.test_utils import ( ClearCompletionsCache, SetUpApp, + StopCompleterServer, + WaitUntilCompleterServerReady ) shared_app = None @@ -36,13 +39,6 @@ return os.path.join( dir_of_current_script, 'testdata', *args ) -def StopGoCodeServer( app ): - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'StopServer' ], - filetype = 'go' ) ) - - def setUpPackage(): """Initializes the ycmd server as a WebTest application that will be shared by all tests using the SharedYcmd decorator in this package. Additional @@ -51,12 +47,13 @@ global shared_app shared_app = SetUpApp() + WaitUntilCompleterServerReady( shared_app, 'go' ) def tearDownPackage(): global shared_app - StopGoCodeServer( shared_app ) + StopCompleterServer( shared_app, 'go' ) def SharedYcmd( test ): @@ -68,5 +65,26 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper + + +def IsolatedYcmd( test ): + """Defines a decorator to be attached to tests of this package. This decorator + passes a unique ycmd application as a parameter. It should be used on tests + that change the server state in a irreversible way (ex: a semantic subserver + is stopped or restarted) or expect a clean state (ex: no semantic subserver + started, no .ycm_extra_conf.py loaded, etc). + + Do NOT attach it to test generators but directly to the yielded tests.""" + @functools.wraps( test ) + def Wrapper( *args, **kwargs ): + old_server_state = handlers._server_state + app = SetUpApp() + try: + test( app, *args, **kwargs ) + finally: + StopCompleterServer( app, 'go' ) + handlers._server_state = old_server_state + return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/go/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,60 +23,125 @@ standard_library.install_aliases() from builtins import * # noqa +from hamcrest import assert_that, has_entries from nose.tools import eq_ +from pprint import pformat +import requests from ycmd.tests.go import PathToTestFile, SharedYcmd -from ycmd.tests.test_utils import BuildRequest +from ycmd.tests.test_utils import BuildRequest, ErrorMatcher from ycmd.utils import ReadFile @SharedYcmd -def RunGoToTest( app, params ): - filepath = PathToTestFile( 'goto.go' ) - contents = ReadFile( filepath ) - - command = params[ 'command' ] - goto_data = BuildRequest( completer_target = 'filetype_default', - command_arguments = [ command ], - line_num = 8, - column_num = 8, - contents = contents, - filetype = 'go', - filepath = filepath ) - - results = app.post_json( '/run_completer_command', - goto_data ) - - eq_( { - 'line_num': 3, 'column_num': 6, 'filepath': filepath - }, results.json ) - - filepath = PathToTestFile( 'win.go' ) - contents = ReadFile( filepath ) - - command = params[ 'command' ] - goto_data = BuildRequest( completer_target = 'filetype_default', - command_arguments = [ command ], - line_num = 4, - column_num = 7, - contents = contents, - filetype = 'go', - filepath = filepath ) - - results = app.post_json( '/run_completer_command', - goto_data ) - - eq_( { - 'line_num': 2, 'column_num': 6, 'filepath': filepath - }, results.json ) - - -def Subcommands_GoTo_all_test(): - tests = [ - { 'command': 'GoTo' }, - { 'command': 'GoToDefinition' }, - { 'command': 'GoToDeclaration' } - ] +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 ) + + +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. + response = app.post_json( + '/run_completer_command', + CombineRequest( test[ 'request' ], { + 'completer_target': 'filetype_default', + 'contents': contents, + 'filetype': 'go', + 'command_arguments': ( [ test[ 'request' ][ 'command' ] ] + + test[ 'request' ].get( 'arguments', [] ) ) + } ), + expect_errors = True + ) - for test in tests: - yield RunGoToTest, test + print( 'completer response: {0}'.format( pformat( response.json ) ) ) + + eq_( response.status_code, test[ 'expect' ][ 'response' ] ) + + assert_that( response.json, test[ 'expect' ][ 'data' ] ) + + +@SharedYcmd +def Subcommands_GoTo_Basic( app, goto_command ): + RunTest( app, { + 'description': goto_command + ' works within file', + 'request': { + 'command': goto_command, + 'line_num': 8, + 'column_num': 8, + 'filepath': PathToTestFile( 'goto.go' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'filepath': PathToTestFile( 'goto.go' ), + 'line_num': 3, + 'column_num': 6, + } ) + } + } ) + + +def Subcommands_GoTo_Basic_test(): + for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: + yield Subcommands_GoTo_Basic, command + + +@SharedYcmd +def Subcommands_GoTo_Keyword( app, goto_command ): + RunTest( app, { + 'description': goto_command + ' can\'t jump on keyword', + 'request': { + 'command': goto_command, + 'line_num': 3, + 'column_num': 3, + 'filepath': PathToTestFile( 'goto.go' ), + }, + 'expect': { + 'response': requests.codes.internal_server_error, + 'data': ErrorMatcher( RuntimeError, 'Can\'t find a definition.' ) + } + } ) + + +def Subcommands_GoTo_Keyword_test(): + for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: + yield Subcommands_GoTo_Keyword, command + + +@SharedYcmd +def Subcommands_GoTo_WindowsNewlines( app, goto_command ): + RunTest( app, { + 'description': goto_command + ' works with Windows newlines', + 'request': { + 'command': goto_command, + 'line_num': 4, + 'column_num': 7, + 'filepath': PathToTestFile( 'win.go' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'filepath': PathToTestFile( 'win.go' ), + 'line_num': 2, + 'column_num': 6, + } ) + } + } ) + + +def Subcommands_GoTo_WindowsNewlines_test(): + for command in [ 'GoTo', 'GoToDefinition', 'GoToDeclaration' ]: + yield Subcommands_GoTo_WindowsNewlines, command diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/go/testdata/unicode.go ycmd-0+20161219+git486b809/ycmd/tests/go/testdata/unicode.go --- ycmd-0+20160327+gitc3e6904/ycmd/tests/go/testdata/unicode.go 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/go/testdata/unicode.go 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,14 @@ +package unicode +import ( "fmt" ) +type Test struct { + Unicøde int +} +func main() { + const test = `unicøde`; fmt.prf + const test2 = `†es†`; fmt.erro + + å_test := Test{ + Unicøde: 10, + } + å_test. +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/hmac_utils_test.py ycmd-0+20161219+git486b809/ycmd/tests/hmac_utils_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/hmac_utils_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/hmac_utils_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -24,11 +24,19 @@ from builtins import * # noqa from binascii import hexlify -from nose.tools import eq_, ok_, raises +from nose.tools import eq_, ok_ +from hamcrest import raises, assert_that, calling from ycmd import hmac_utils as hu from ycmd.tests.test_utils import Py2Only +def CreateHmac_ArgsNotBytes_test(): + assert_that( calling( hu.CreateHmac ).with_args( u'foo', bytes( b'foo' ) ), + raises( TypeError, '.*content*' ) ) + assert_that( calling( hu.CreateHmac ).with_args( bytes( b'foo' ), u'foo' ), + raises( TypeError, '.*hmac_secret*' ) ) + + def CreateHmac_WithBytes_test(): # Test vectors from Wikipedia (HMAC_SHA256): https://goo.gl/cvX0Tn eq_( hexlify( hu.CreateHmac( @@ -48,6 +56,28 @@ 'ef4d59a14946175997479dbc2d1a3cd8' ) +def CreateRequestHmac_ArgsNotBytes_test(): + assert_that( + calling( hu.CreateRequestHmac ).with_args( + u'foo', bytes( b'foo' ), bytes( b'foo' ), bytes( b'foo' ) ), + raises( TypeError, '.*method*' ) ) + + assert_that( + calling( hu.CreateRequestHmac ).with_args( + bytes( b'foo' ), u'foo', bytes( b'foo' ), bytes( b'foo' ) ), + raises( TypeError, '.*path*' ) ) + + assert_that( + calling( hu.CreateRequestHmac ).with_args( + bytes( b'foo' ), bytes( b'foo' ), u'foo', bytes( b'foo' ) ), + raises( TypeError, '.*body*' ) ) + + assert_that( + calling( hu.CreateRequestHmac ).with_args( + bytes( b'foo' ), bytes( b'foo' ), bytes( b'foo' ), u'foo' ), + raises( TypeError, '.*hmac_secret*' ) ) + + def CreateRequestHmac_WithBytes_test(): eq_( hexlify( hu.CreateRequestHmac( bytes( b'GET' ), @@ -71,6 +101,7 @@ def SecureBytesEqual_Basic_test(): ok_( hu.SecureBytesEqual( bytes( b'foo' ), bytes( b'foo' ) ) ) + ok_( not hu.SecureBytesEqual( bytes( b'foo' ), bytes( b'fo' ) ) ) ok_( not hu.SecureBytesEqual( bytes( b'foo' ), bytes( b'goo' ) ) ) @@ -78,12 +109,12 @@ ok_( hu.SecureBytesEqual( bytes(), bytes() ) ) -@raises( TypeError ) def SecureBytesEqual_ExceptionOnUnicode_test(): - ok_( hu.SecureBytesEqual( u'foo', u'foo' ) ) + assert_that( calling( hu.SecureBytesEqual ).with_args( u'foo', u'foo' ), + raises( TypeError, '.*inputs must be bytes.*' ) ) @Py2Only -@raises( TypeError ) def SecureBytesEqual_ExceptionOnPy2Str_test(): - ok_( hu.SecureBytesEqual( 'foo', 'foo' ) ) + assert_that( calling( hu.SecureBytesEqual ).with_args( 'foo', 'foo' ), + raises( TypeError, '.*inputs must be bytes.*' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/identifier_completer_test.py ycmd-0+20161219+git486b809/ycmd/tests/identifier_completer_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/identifier_completer_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/identifier_completer_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,9 +23,13 @@ standard_library.install_aliases() from builtins import * # noqa +import os from nose.tools import eq_ +from ycmd.user_options_store import DefaultOptions from ycmd.completers.all import identifier_completer as ic +from ycmd.completers.all.identifier_completer import IdentifierCompleter from ycmd.request_wrap import RequestWrap +from ycmd.tests import PathToTestFile from ycmd.tests.test_utils import BuildRequest @@ -84,8 +88,16 @@ eq_( 'foo', ic._PreviousIdentifier( 2, BuildRequestWrap( 'foo', 4 ) ) ) -def PreviousIdentifier_ColumnInMiddleStillWholeIdent_test(): - eq_( 'foobar', ic._PreviousIdentifier( 2, BuildRequestWrap( 'foobar', 4 ) ) ) +def PreviousIdentifier_WholeIdentShouldBeBeforeColumn_test(): + eq_( '', + ic._PreviousIdentifier( 2, BuildRequestWrap( 'foobar', + column_num = 4 ) ) ) + + +def PreviousIdentifier_DoNotWrap_test(): + eq_( '', + ic._PreviousIdentifier( 2, BuildRequestWrap( 'foobar\n bar', + column_num = 4 ) ) ) def PreviousIdentifier_IgnoreForwardIdents_test(): @@ -132,3 +144,39 @@ ic._PreviousIdentifier( 2, BuildRequestWrap( 'foo **;()\n ', column_num = 3, line_num = 2 ) ) ) + + +def PreviousIdentifier_NoGoodIdentFound_test(): + eq_( '', + ic._PreviousIdentifier( 5, BuildRequestWrap( 'foo\n ', + column_num = 2, + line_num = 2 ) ) ) + + +def FilterUnchangedTagFiles_NoFiles_test(): + ident_completer = IdentifierCompleter( DefaultOptions() ) + eq_( [], list( ident_completer._FilterUnchangedTagFiles( [] ) ) ) + + +def FilterUnchangedTagFiles_SkipBadFiles_test(): + ident_completer = IdentifierCompleter( DefaultOptions() ) + eq_( [], + list( ident_completer._FilterUnchangedTagFiles( [ '/some/tags' ] ) ) ) + + +def FilterUnchangedTagFiles_KeepGoodFiles_test(): + ident_completer = IdentifierCompleter( DefaultOptions() ) + tag_file = PathToTestFile( 'basic.tags' ) + eq_( [ tag_file ], + list( ident_completer._FilterUnchangedTagFiles( [ tag_file ] ) ) ) + + +def FilterUnchangedTagFiles_SkipUnchangesFiles_test(): + ident_completer = IdentifierCompleter( DefaultOptions() ) + + # simulate an already open tags file that didn't change in the meantime. + tag_file = PathToTestFile( 'basic.tags' ) + ident_completer._tags_file_last_mtime[ tag_file ] = os.path.getmtime( + tag_file ) + + eq_( [], list( ident_completer._FilterUnchangedTagFiles( [ tag_file ] ) ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/identifier_utils_test.py ycmd-0+20161219+git486b809/ycmd/tests/identifier_utils_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/identifier_utils_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/identifier_utils_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -137,10 +137,21 @@ has_item( 'goo' ) ) -def IsIdentifier_generic_test(): +def IsIdentifier_Default_test(): ok_( iu.IsIdentifier( 'foo' ) ) ok_( iu.IsIdentifier( 'foo129' ) ) ok_( iu.IsIdentifier( 'f12' ) ) + ok_( iu.IsIdentifier( 'f12' ) ) + + ok_( iu.IsIdentifier( '_foo' ) ) + ok_( iu.IsIdentifier( '_foo129' ) ) + ok_( iu.IsIdentifier( '_f12' ) ) + ok_( iu.IsIdentifier( '_f12' ) ) + + ok_( iu.IsIdentifier( 'uniçode' ) ) + ok_( iu.IsIdentifier( 'uç' ) ) + ok_( iu.IsIdentifier( 'ç' ) ) + ok_( iu.IsIdentifier( 'çode' ) ) ok_( not iu.IsIdentifier( '1foo129' ) ) ok_( not iu.IsIdentifier( '-foo' ) ) @@ -150,6 +161,24 @@ ok_( not iu.IsIdentifier( '' ) ) +def IsIdentifier_JavaScript_test(): + ok_( iu.IsIdentifier( '_føo1', 'javascript' ) ) + ok_( iu.IsIdentifier( 'fø_o1', 'javascript' ) ) + ok_( iu.IsIdentifier( '$føo1', 'javascript' ) ) + ok_( iu.IsIdentifier( 'fø$o1', 'javascript' ) ) + + ok_( not iu.IsIdentifier( '1føo', 'javascript' ) ) + + +def IsIdentifier_TypeScript_test(): + ok_( iu.IsIdentifier( '_føo1', 'typescript' ) ) + ok_( iu.IsIdentifier( 'fø_o1', 'typescript' ) ) + ok_( iu.IsIdentifier( '$føo1', 'typescript' ) ) + ok_( iu.IsIdentifier( 'fø$o1', 'typescript' ) ) + + ok_( not iu.IsIdentifier( '1føo', 'typescript' ) ) + + def IsIdentifier_Css_test(): ok_( iu.IsIdentifier( 'foo' , 'css' ) ) ok_( iu.IsIdentifier( 'a1' , 'css' ) ) @@ -164,6 +193,7 @@ ok_( not iu.IsIdentifier( '-3' , 'css' ) ) ok_( not iu.IsIdentifier( '3' , 'css' ) ) ok_( not iu.IsIdentifier( 'a' , 'css' ) ) + ok_( not iu.IsIdentifier( '' , 'css' ) ) def IsIdentifier_R_test(): @@ -185,6 +215,7 @@ ok_( not iu.IsIdentifier( '123', 'r' ) ) ok_( not iu.IsIdentifier( '_1a', 'r' ) ) ok_( not iu.IsIdentifier( '_a' , 'r' ) ) + ok_( not iu.IsIdentifier( '' , 'r' ) ) def IsIdentifier_Clojure_test(): @@ -210,6 +241,21 @@ ok_( not iu.IsIdentifier( '9' , 'clojure' ) ) ok_( not iu.IsIdentifier( 'a/b/c', 'clojure' ) ) ok_( not iu.IsIdentifier( '(a)' , 'clojure' ) ) + ok_( not iu.IsIdentifier( '' , 'clojure' ) ) + + +def IsIdentifier_Elisp_test(): + # elisp is using the clojure regexes, so we're testing this more lightly + ok_( iu.IsIdentifier( 'foo' , 'elisp' ) ) + ok_( iu.IsIdentifier( 'f9' , 'elisp' ) ) + ok_( iu.IsIdentifier( 'a.b.c', 'elisp' ) ) + ok_( iu.IsIdentifier( 'a/c' , 'elisp' ) ) + + ok_( not iu.IsIdentifier( '9f' , 'elisp' ) ) + ok_( not iu.IsIdentifier( '9' , 'elisp' ) ) + ok_( not iu.IsIdentifier( 'a/b/c', 'elisp' ) ) + ok_( not iu.IsIdentifier( '(a)' , 'elisp' ) ) + ok_( not iu.IsIdentifier( '' , 'elisp' ) ) def IsIdentifier_Haskell_test(): @@ -223,6 +269,19 @@ ok_( not iu.IsIdentifier( "'x", 'haskell' ) ) ok_( not iu.IsIdentifier( "9x", 'haskell' ) ) ok_( not iu.IsIdentifier( "9" , 'haskell' ) ) + ok_( not iu.IsIdentifier( '' , 'haskell' ) ) + + +def IsIdentifier_Tex_test(): + ok_( iu.IsIdentifier( 'foo', 'tex' ) ) + ok_( iu.IsIdentifier( 'fig:foo', 'tex' ) ) + ok_( iu.IsIdentifier( 'fig:foo-bar', 'tex' ) ) + ok_( iu.IsIdentifier( 'sec:summary', 'tex' ) ) + ok_( iu.IsIdentifier( 'eq:bar_foo', 'tex' ) ) + + ok_( not iu.IsIdentifier( '\section', 'tex' ) ) + ok_( not iu.IsIdentifier( 'some8', 'tex' ) ) + ok_( not iu.IsIdentifier( '' , 'tex' ) ) def IsIdentifier_Perl6_test(): @@ -249,6 +308,7 @@ ok_( not iu.IsIdentifier( "x+" , 'perl6' ) ) ok_( not iu.IsIdentifier( "9x" , 'perl6' ) ) ok_( not iu.IsIdentifier( "9" , 'perl6' ) ) + ok_( not iu.IsIdentifier( '' , 'perl6' ) ) def StartOfLongestIdentifierEndingAtIndex_Simple_test(): diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,7 +26,7 @@ import functools import os -from ycmd.tests.test_utils import SetUpApp +from ycmd.tests.test_utils import ClearCompletionsCache, SetUpApp shared_app = None @@ -55,5 +55,6 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/javascript/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,70 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.javascript import IsolatedYcmd, SharedYcmd +from ycmd.tests.test_utils import BuildRequest, StopCompleterServer, UserOption + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + request_data = BuildRequest( filetype = 'javascript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'JavaScript completer debug information:\n' + ' Tern running at: http://127.0.0.1:\d+\n' + ' Tern process ID: \d+\n' + ' Tern executable: .+\n' + ' Tern logfiles:\n' + ' .+\n' + ' .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + StopCompleterServer( app, 'javascript' ) + request_data = BuildRequest( filetype = 'javascript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'JavaScript completer debug information:\n' + ' Tern no longer running\n' + ' Tern executable: .+\n' + ' Tern logfiles:\n' + ' .+\n' + ' .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + StopCompleterServer( app, 'javascript' ) + request_data = BuildRequest( filetype = 'javascript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'JavaScript completer debug information:\n' + ' Tern is not running\n' + ' Tern executable: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/event_notification_test.py ycmd-0+20161219+git486b809/ycmd/tests/javascript/event_notification_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/event_notification_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/event_notification_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,17 +27,19 @@ from mock import patch from nose.tools import eq_ from pprint import pformat -import http.client +import requests import os -from ycmd.tests.test_utils import BuildRequest, ErrorMatcher -from ycmd.tests.javascript import ( IsolatedYcmd, PathToTestFile, - WaitUntilTernServerReady ) -from ycmd.utils import ReadFile +from ycmd.tests.test_utils import ( BuildRequest, ErrorMatcher, + WaitUntilCompleterServerReady ) +from ycmd.tests.javascript import IsolatedYcmd, PathToTestFile +from ycmd.utils import GetCurrentDirectory, ReadFile @IsolatedYcmd def EventNotification_OnFileReadyToParse_ProjectFile_cwd_test( app ): + WaitUntilCompleterServerReady( app, 'javascript' ) + contents = ReadFile( PathToTestFile( 'simple_test.js' ) ) response = app.post_json( '/event_notification', @@ -47,12 +49,14 @@ filetype = 'javascript' ), expect_errors = True) - eq_( response.status_code, http.client.OK ) + eq_( response.status_code, requests.codes.ok ) assert_that( response.json, empty() ) @IsolatedYcmd def EventNotification_OnFileReadyToParse_ProjectFile_parentdir_test( app ): + WaitUntilCompleterServerReady( app, 'javascript' ) + os.chdir( PathToTestFile( 'lamelib' ) ) contents = ReadFile( PathToTestFile( 'simple_test.js' ) ) @@ -63,7 +67,7 @@ filetype = 'javascript' ), expect_errors = True) - eq_( response.status_code, http.client.OK ) + eq_( response.status_code, requests.codes.ok ) assert_that( response.json, empty() ) @@ -71,6 +75,8 @@ @patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists', return_value = False ) def EventNotification_OnFileReadyToParse_NoProjectFile_test( app, *args ): + WaitUntilCompleterServerReady( app, 'javascript' ) + # We raise an error if we can't detect a .tern-project file. # We only do this on the first OnFileReadyToParse event after a # server startup. @@ -86,13 +92,13 @@ print( 'event response: {0}'.format( pformat( response.json ) ) ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( RuntimeError, 'Warning: Unable to detect a .tern-project file ' - 'in the hierarchy before ' + os.getcwd() + + 'in the hierarchy before ' + GetCurrentDirectory() + ' and no global .tern-config file was found. ' 'This is required for accurate JavaScript ' 'completion. Please see the User Guide for ' @@ -110,23 +116,18 @@ print( 'event response: {0}'.format( pformat( response.json ) ) ) - eq_( response.status_code, http.client.OK ) + eq_( response.status_code, requests.codes.ok ) assert_that( response.json, empty() ) # Restart the server and check that it raises it again app.post_json( '/run_completer_command', - BuildRequest( command_arguments = [ 'StopServer' ], - filetype = 'javascript', - contents = contents, - completer_target = 'filetype_default' ) ) - app.post_json( '/run_completer_command', - BuildRequest( command_arguments = [ 'StartServer' ], + BuildRequest( command_arguments = [ 'RestartServer' ], filetype = 'javascript', contents = contents, completer_target = 'filetype_default' ) ) - WaitUntilTernServerReady( app ) + WaitUntilCompleterServerReady( app, 'javascript' ) response = app.post_json( '/event_notification', BuildRequest( event_name = 'FileReadyToParse', @@ -136,13 +137,13 @@ print( 'event response: {0}'.format( pformat( response.json ) ) ) - eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR ) + eq_( response.status_code, requests.codes.internal_server_error ) assert_that( response.json, ErrorMatcher( RuntimeError, 'Warning: Unable to detect a .tern-project file ' - 'in the hierarchy before ' + os.getcwd() + + 'in the hierarchy before ' + GetCurrentDirectory() + ' and no global .tern-config file was found. ' 'This is required for accurate JavaScript ' 'completion. Please see the User Guide for ' @@ -154,6 +155,8 @@ @patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists', return_value = True ) def EventNotification_OnFileReadyToParse_UseGlobalConfig_test( app, *args ): + WaitUntilCompleterServerReady( app, 'javascript' ) + os.chdir( PathToTestFile( '..' ) ) contents = ReadFile( PathToTestFile( 'simple_test.js' ) ) @@ -165,4 +168,4 @@ print( 'event response: {0}'.format( pformat( response.json ) ) ) - eq_( response.status_code, http.client.OK ) + eq_( response.status_code, requests.codes.ok ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/javascript/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,7 +27,7 @@ has_entries ) from nose.tools import eq_ from pprint import pformat -import http.client +import requests from ycmd.tests.javascript import PathToTestFile, SharedYcmd from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher @@ -101,7 +101,7 @@ 'column_num': 43, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains_inanyorder( CompletionEntryMatcher( 'a_simple_function', @@ -135,7 +135,7 @@ 'column_num': 45, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'basic_type', 'number' ), @@ -159,7 +159,7 @@ 'column_num': 15, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains_inanyorder( CompletionEntryMatcher( 'mine_bitcoin', @@ -195,7 +195,7 @@ 'column_num': 17, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'mine_bitcoin', @@ -219,7 +219,7 @@ 'column_num': 17, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'get_number', 'number' ), @@ -254,7 +254,7 @@ }, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains_inanyorder( CompletionEntryMatcher( 'big_endian_node', 'number' ), @@ -286,7 +286,7 @@ 'column_num': 15, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains_inanyorder( CompletionEntryMatcher( @@ -409,3 +409,75 @@ 'errors': empty(), } ) ) + + +@SharedYcmd +def GetCompletions_Unicode_AfterLine_test( app ): + RunTest( app, { + 'description': 'completions work with unicode chars in the file', + 'request': { + 'filetype' : 'javascript', + 'filepath' : PathToTestFile( 'unicode.js' ), + 'line_num' : 1, + 'column_num': 16, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'charAt', 'fn(i: number) -> string' ), + CompletionEntryMatcher( 'charCodeAt', 'fn(i: number) -> number' ), + ), + 'completion_start_column': 13, + 'errors': empty(), + } ) + }, + } ) + + +@SharedYcmd +def GetCompletions_Unicode_InLine_test( app ): + RunTest( app, { + 'description': 'completions work with unicode chars in the file', + 'request': { + 'filetype' : 'javascript', + 'filepath' : PathToTestFile( 'unicode.js' ), + 'line_num' : 2, + 'column_num': 18, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'charAt', 'fn(i: number) -> string' ), + CompletionEntryMatcher( 'charCodeAt', 'fn(i: number) -> number' ), + ), + 'completion_start_column': 15, + 'errors': empty(), + } ) + }, + } ) + + +@SharedYcmd +def GetCompletions_Unicode_InFile_test( app ): + RunTest( app, { + 'description': 'completions work with unicode chars in the file', + 'request': { + 'filetype' : 'javascript', + 'filepath' : PathToTestFile( 'unicode.js' ), + 'line_num' : 3, + 'column_num': 16, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'charAt', 'fn(i: number) -> string' ), + CompletionEntryMatcher( 'charCodeAt', 'fn(i: number) -> number' ), + ), + 'completion_start_column': 13, + 'errors': empty(), + } ) + }, + } ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/javascript/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,10 +25,14 @@ import functools import os -import time from ycmd import handlers -from ycmd.tests.test_utils import BuildRequest, SetUpApp +from ycmd.tests.test_utils import ( ClearCompletionsCache, + CurrentWorkingDirectory, + SetUpApp, + StopCompleterServer, + WaitUntilCompleterServerReady ) +from ycmd.utils import GetCurrentDirectory shared_app = None shared_current_dir = None @@ -39,36 +43,6 @@ return os.path.join( dir_of_current_script, 'testdata', *args ) -def WaitUntilTernServerReady( app ): - app.post_json( '/run_completer_command', BuildRequest( - command_arguments = [ 'StartServer' ], - completer_target = 'filetype_default', - filetype = 'javascript', - filepath = '/foo.js', - contents = '', - line_num = '1' - ) ) - - retries = 100 - while retries > 0: - result = app.get( '/ready', { 'subserver': 'javascript' } ).json - if result: - return - - time.sleep( 0.2 ) - retries = retries - 1 - - raise RuntimeError( 'Timeout waiting for Tern.js server to be ready' ) - - -def StopTernServer( app ): - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'StopServer' ], - filetype = 'javascript' ), - expect_errors = True ) - - def setUpPackage(): """Initializes the ycmd server as a WebTest application that will be shared by all tests using the SharedYcmd decorator in this package. Additional @@ -77,9 +51,9 @@ global shared_app, shared_current_dir shared_app = SetUpApp() - shared_current_dir = os.getcwd() + shared_current_dir = GetCurrentDirectory() os.chdir( PathToTestFile() ) - WaitUntilTernServerReady( shared_app ) + WaitUntilCompleterServerReady( shared_app, 'javascript' ) def tearDownPackage(): @@ -87,7 +61,7 @@ executed once after running all the tests in the package.""" global shared_app, shared_current_dir - StopTernServer( shared_app ) + StopCompleterServer( shared_app, 'javascript' ) os.chdir( shared_current_dir ) @@ -100,6 +74,7 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper @@ -115,15 +90,11 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): old_server_state = handlers._server_state - old_current_dir = os.getcwd() - + app = SetUpApp() try: - os.chdir( PathToTestFile() ) - app = SetUpApp() - WaitUntilTernServerReady( app ) - test( app, *args, **kwargs ) - StopTernServer( app ) + with CurrentWorkingDirectory( PathToTestFile() ): + test( app, *args, **kwargs ) finally: - os.chdir( old_current_dir ) + StopCompleterServer( app, 'javascript' ) handlers._server_state = old_server_state return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/javascript/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,4 +1,5 @@ # Copyright (C) 2015 ycmd contributors +# encoding: utf-8 # # This file is part of ycmd. # @@ -26,13 +27,14 @@ from hamcrest import assert_that, contains, contains_inanyorder, has_entries from nose.tools import eq_ from pprint import pformat -import http.client +import requests from ycmd.tests.javascript import IsolatedYcmd, PathToTestFile, SharedYcmd from ycmd.tests.test_utils import ( BuildRequest, ChunkMatcher, ErrorMatcher, - LocationMatcher ) + LocationMatcher, + WaitUntilCompleterServerReady ) from ycmd.utils import ReadFile @@ -44,16 +46,16 @@ 'GoTo', 'GetDoc', 'GetType', - 'StartServer', - 'StopServer', 'GoToReferences', - 'RefactorRename' ] ), + 'RefactorRename', + 'RestartServer' ] ), app.post_json( '/defined_subcommands', subcommands_data ).json ) -def RunTest( app, test ): - contents = ReadFile( test[ 'request' ][ 'filepath' ] ) +def RunTest( app, test, contents = None ): + if not contents: + contents = ReadFile( test[ 'request' ][ 'filepath' ] ) def CombineRequest( request, data ): kw = request @@ -104,7 +106,7 @@ 'filepath': PathToTestFile( 'simple_test.js' ), }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'filepath': PathToTestFile( 'simple_test.js' ), 'line_num': 1, @@ -115,6 +117,27 @@ @SharedYcmd +def Subcommands_GoToDefinition_Unicode_test( app ): + RunTest( app, { + 'description': 'GoToDefinition works within file with unicode', + 'request': { + 'command': 'GoToDefinition', + 'line_num': 11, + 'column_num': 12, + 'filepath': PathToTestFile( 'unicode.js' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'filepath': PathToTestFile( 'unicode.js' ), + 'line_num': 6, + 'column_num': 26, + } ) + } + } ) + + +@SharedYcmd def Subcommands_GoTo_test( app ): RunTest( app, { 'description': 'GoTo works the same as GoToDefinition within file', @@ -125,7 +148,7 @@ 'filepath': PathToTestFile( 'simple_test.js' ), }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'filepath': PathToTestFile( 'simple_test.js' ), 'line_num': 1, @@ -135,6 +158,31 @@ } ) +@IsolatedYcmd +def Subcommands_GoTo_RelativePath_test( app ): + WaitUntilCompleterServerReady( app, 'javascript' ) + RunTest( + app, + { + 'description': 'GoTo works when the buffer differs from the file on disk', + 'request': { + 'command': 'GoTo', + 'line_num': 43, + 'column_num': 25, + 'filepath': PathToTestFile( 'simple_test.js' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'filepath': PathToTestFile( 'simple_test.js' ), + 'line_num': 31, + 'column_num': 5, + } ) + } + }, + contents = ReadFile( PathToTestFile( 'simple_test.modified.js' ) ) ) + + @SharedYcmd def Subcommands_GetDoc_test( app ): RunTest( app, { @@ -146,7 +194,7 @@ 'filepath': PathToTestFile( 'coollib', 'cool_object.js' ), }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'detailed_info': ( 'Name: mine_bitcoin\n' @@ -170,7 +218,7 @@ 'filepath': PathToTestFile( 'coollib', 'cool_object.js' ), }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'message': 'number' } ) @@ -189,7 +237,7 @@ 'filepath': PathToTestFile( 'coollib', 'cool_object.js' ), }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': contains_inanyorder( has_entries( { 'filepath': PathToTestFile( 'coollib', 'cool_object.js' ), @@ -207,6 +255,39 @@ @SharedYcmd +def Subcommands_GoToReferences_Unicode_test( app ): + RunTest( app, { + 'description': 'GoToReferences works within file with unicode chars', + 'request': { + 'command': 'GoToReferences', + 'line_num': 11, + 'column_num': 5, + 'filepath': PathToTestFile( 'unicode.js' ), + }, + 'expect': { + 'response': requests.codes.ok, + 'data': contains_inanyorder( + has_entries( { + 'filepath': PathToTestFile( 'unicode.js' ), + 'line_num': 5, + 'column_num': 5, + } ), + has_entries( { + 'filepath': PathToTestFile( 'unicode.js' ), + 'line_num': 9, + 'column_num': 1, + } ), + has_entries( { + 'filepath': PathToTestFile( 'unicode.js' ), + 'line_num': 11, + 'column_num': 1, + } ) + ) + } + } ) + + +@SharedYcmd def Subcommands_GetDocWithNoItendifier_test( app ): RunTest( app, { 'description': 'GetDoc works when no identifier', @@ -217,7 +298,7 @@ 'column_num': 1, }, 'expect': { - 'response': http.client.INTERNAL_SERVER_ERROR, + 'response': requests.codes.internal_server_error, 'data': ErrorMatcher( RuntimeError, 'TernError: No type found ' 'at the given position.' ), } @@ -237,7 +318,7 @@ 'column_num': 32, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries ( { 'fixits': contains( has_entries( { 'chunks': contains( @@ -285,7 +366,7 @@ 'column_num': 14, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries ( { 'fixits': contains( has_entries( { 'chunks': contains( @@ -317,6 +398,8 @@ # an extra file into tern's project memory) @IsolatedYcmd def Subcommands_RefactorRename_MultipleFiles_OnFileReadyToParse_test( app ): + WaitUntilCompleterServerReady( app, 'javascript' ) + file1 = PathToTestFile( 'file1.js' ) file2 = PathToTestFile( 'file2.js' ) file3 = PathToTestFile( 'file3.js' ) @@ -346,7 +429,7 @@ 'column_num': 14, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'fixits': contains( has_entries( { 'chunks': contains( @@ -389,9 +472,43 @@ 'filepath': PathToTestFile( 'coollib', 'cool_object.js' ), }, 'expect': { - 'response': http.client.INTERNAL_SERVER_ERROR, + 'response': requests.codes.internal_server_error, 'data': ErrorMatcher( ValueError, 'Please specify a new name to rename it to.\n' 'Usage: RefactorRename ' ), } } ) + + +@SharedYcmd +def Subcommands_RefactorRename_Unicode_test( app ): + filepath = PathToTestFile( 'unicode.js' ) + RunTest( app, { + 'description': 'RefactorRename works with unicode identifiers', + 'request': { + 'command': 'RefactorRename', + 'arguments': [ '†es†' ], + 'filepath': filepath, + 'line_num': 11, + 'column_num': 3, + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries ( { + 'fixits': contains( has_entries( { + 'chunks': contains( + ChunkMatcher( '†es†', + LocationMatcher( filepath, 5, 5 ), + LocationMatcher( filepath, 5, 13 ) ), + ChunkMatcher( '†es†', + LocationMatcher( filepath, 9, 1 ), + LocationMatcher( filepath, 9, 9 ) ), + ChunkMatcher( '†es†', + LocationMatcher( filepath, 11, 1 ), + LocationMatcher( filepath, 11, 9 ) ) + ), + 'location': LocationMatcher( filepath, 11, 3 ) + } ) ) + } ) + } + } ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/testdata/simple_test.modified.js ycmd-0+20161219+git486b809/ycmd/tests/javascript/testdata/simple_test.modified.js --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/testdata/simple_test.modified.js 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/testdata/simple_test.modified.js 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +var simple_test_obect = { + 'a_simple_function': function( param ) { + return 'yes'; + }, + + 'basic_type': 100, + + 'object': { + 'basic_type': 'test_string' + } +}; + +var simple_assignment = simple_test_obect. +var query_assignment = simple_test_obect.typ +var query_assignment = simple_test_obect.asf + +function blah( simple_test_obect ) { + simple_test_obect.a_simple_function; +} + +blah( simple_test_obect ); simple_test_obect = null; diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/testdata/unicode.js ycmd-0+20161219+git486b809/ycmd/tests/javascript/testdata/unicode.js --- ycmd-0+20160327+gitc3e6904/ycmd/tests/javascript/testdata/unicode.js 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/javascript/testdata/unicode.js 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,11 @@ +var x = 't'.cha +var y = '†'.cha +var x = 't'.cha + +var øbjecø = { + /* unicøde comment */ 'ålpha': '∫eta' +}; + +øbjecø.a + +øbjecø.ålpha diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/misc_handlers_test.py ycmd-0+20161219+git486b809/ycmd/tests/misc_handlers_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/misc_handlers_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/misc_handlers_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -24,10 +24,10 @@ standard_library.install_aliases() from builtins import * # noqa -from nose.tools import ok_ -from hamcrest import assert_that, contains +from hamcrest import assert_that, contains, empty, equal_to, has_entries +import requests -from ycmd.tests import SharedYcmd +from ycmd.tests import PathToTestFile, SharedYcmd from ycmd.tests.test_utils import BuildRequest, DummyCompleter, PatchCompleter @@ -35,7 +35,9 @@ def MiscHandlers_SemanticCompletionAvailable_test( app ): with PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ): request_data = BuildRequest( filetype = 'dummy_filetype' ) - ok_( app.post_json( '/semantic_completion_available', request_data ).json ) + assert_that( app.post_json( '/semantic_completion_available', + request_data ).json, + equal_to( True ) ) @SharedYcmd @@ -43,7 +45,26 @@ event_data = BuildRequest( contents = 'foo foogoo ba', event_name = 'FileReadyToParse' ) - app.post_json( '/event_notification', event_data ).json + assert_that( app.post_json( '/event_notification', event_data ).json, + empty() ) + + +@SharedYcmd +def MiscHandlers_EventNotification_ReturnJsonOnBigFileError_test( app ): + # We generate a content greater than Bottle.MEMFILE_MAX, which is set to 10MB. + contents = "foo " * 5000000 + event_data = BuildRequest( contents = contents, + event_name = 'FileReadyToParse' ) + + response = app.post_json( '/event_notification', + event_data, + expect_errors = True ) + assert_that( response.status_code, + equal_to( requests.codes.request_entity_too_large ) ) + assert_that( response.json, + has_entries( { 'traceback': None, + 'message': 'None', + 'exception': None } ) ) @SharedYcmd @@ -61,3 +82,21 @@ response_data = app.post_json( '/filter_and_sort_candidates', data ).json assert_that( response_data, contains( candidate2, candidate3 ) ) + + +@SharedYcmd +def MiscHandlers_LoadExtraConfFile_AlwaysJsonResponse_test( app ): + 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, + equal_to( True ) ) + + +@SharedYcmd +def MiscHandlers_IgnoreExtraConfFile_AlwaysJsonResponse_test( app ): + 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, + equal_to( True ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/python/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/python/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/python/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/python/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,73 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.python import IsolatedYcmd, SharedYcmd +from ycmd.tests.test_utils import BuildRequest, StopCompleterServer, UserOption + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + request_data = BuildRequest( filetype = 'python' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Python completer debug information:\n' + ' JediHTTP running at: http://127.0.0.1:\d+\n' + ' JediHTTP process ID: \d+\n' + ' JediHTTP executable: .+\n' + ' JediHTTP logfiles:\n' + ' .+\n' + ' .+\n' + ' Python interpreter: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + StopCompleterServer( app, 'python' ) + request_data = BuildRequest( filetype = 'python' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Python completer debug information:\n' + ' JediHTTP no longer running\n' + ' JediHTTP executable: .+\n' + ' JediHTTP logfiles:\n' + ' .+\n' + ' .+\n' + ' Python interpreter: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + StopCompleterServer( app, 'python' ) + request_data = BuildRequest( filetype = 'python' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Python completer debug information:\n' + ' JediHTTP is not running\n' + ' JediHTTP executable: .+\n' + ' Python interpreter: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/python/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/python/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/python/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/python/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -28,11 +28,12 @@ from nose.tools import eq_ from hamcrest import ( assert_that, has_item, has_items, has_entry, has_entries, contains, empty, contains_string ) +import requests + from ycmd.utils import ReadFile from ycmd.tests.python import PathToTestFile, SharedYcmd from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher, CompletionLocationMatcher ) -import http.client @SharedYcmd @@ -127,7 +128,7 @@ 'force_semantic': False, }, 'expect': { - 'response': http.client.OK, + 'response': requests.codes.ok, 'data': has_entries( { 'completions': contains( CompletionEntryMatcher( 'a_parameter', '[ID]' ), @@ -135,5 +136,27 @@ ), 'errors': empty(), } ) + }, + } ) + + +@SharedYcmd +def GetCompletions_Unicode_InLine_test( app ): + RunTest( app, { + 'description': 'return completions for strings with multi-byte chars', + 'request': { + 'filetype' : 'python', + 'filepath' : PathToTestFile( 'unicode.py' ), + 'line_num' : 7, + 'column_num': 14 + }, + 'expect': { + 'response': requests.codes.ok, + 'data': has_entries( { + 'completions': contains( + CompletionEntryMatcher( 'center', 'function: builtins.str.center' ) + ), + 'errors': empty(), + } ) }, } ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/python/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/python/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/python/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/python/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,10 +25,11 @@ import functools import os -import time from ycmd import handlers -from ycmd.tests.test_utils import BuildRequest, SetUpApp +from ycmd.tests.test_utils import ( ClearCompletionsCache, SetUpApp, + StopCompleterServer, + WaitUntilCompleterServerReady ) shared_app = None @@ -38,30 +39,6 @@ return os.path.join( dir_of_current_script, 'testdata', *args ) -def WaitUntilJediHTTPServerReady( app ): - retries = 100 - - while retries > 0: - result = app.get( '/ready', { 'subserver': 'python' } ).json - if result: - return - - time.sleep( 0.2 ) - retries = retries - 1 - - raise RuntimeError( "Timeout waiting for JediHTTP" ) - - -def StopJediHTTPServer( app ): - # We don't actually start a JediHTTP server on every test, so we just - # ignore errors when stopping the server - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'StopServer' ], - filetype = 'python' ), - expect_errors = True ) - - def setUpPackage(): """Initializes the ycmd server as a WebTest application that will be shared by all tests using the SharedYcmd decorator in this package. Additional @@ -70,7 +47,7 @@ global shared_app shared_app = SetUpApp() - WaitUntilJediHTTPServerReady( shared_app ) + WaitUntilCompleterServerReady( shared_app, 'python' ) def tearDownPackage(): @@ -78,7 +55,7 @@ executed once after running all the tests in the package.""" global shared_app - StopJediHTTPServer( shared_app ) + StopCompleterServer( shared_app, 'python' ) def SharedYcmd( test ): @@ -90,6 +67,7 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper @@ -105,9 +83,10 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): old_server_state = handlers._server_state - + app = SetUpApp() try: - test( SetUpApp(), *args, **kwargs ) + test( app, *args, **kwargs ) finally: + StopCompleterServer( app, 'python' ) handlers._server_state = old_server_state return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/python/testdata/unicode.py ycmd-0+20161219+git486b809/ycmd/tests/python/testdata/unicode.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/python/testdata/unicode.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/python/testdata/unicode.py 2016-12-20 08:50:19.000000000 +0000 @@ -3,3 +3,5 @@ pass Fo + +x = '†'.cen diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/python/user_defined_python_test.py ycmd-0+20161219+git486b809/ycmd/tests/python/user_defined_python_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/python/user_defined_python_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/python/user_defined_python_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -86,8 +86,7 @@ @IsolatedYcmd @patch( 'ycmd.utils.SafePopen' ) -@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.' - '_CheckBinaryExists', return_value = False ) +@patch( 'ycmd.utils.FindExecutable', return_value = None ) def UserDefinedPython_WhenNonExistentPythonIsGiven_ReturnAnError_test( app, *args ): python = '/non/existing/path/python' @@ -103,8 +102,7 @@ @IsolatedYcmd @patch( 'ycmd.utils.SafePopen' ) -@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.' - '_CheckBinaryExists', return_value = True ) +@patch( 'ycmd.utils.FindExecutable', side_effect = lambda x: x ) def UserDefinedPython_WhenExistingPythonIsGiven_ThatIsUsed_test( app, *args ): python = '/existing/python' with UserOption( 'python_binary_path', python ): @@ -114,8 +112,7 @@ @IsolatedYcmd @patch( 'ycmd.utils.SafePopen' ) -@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.' - '_CheckBinaryExists', return_value = True ) +@patch( 'ycmd.utils.FindExecutable', side_effect = lambda x: x ) def UserDefinedPython_RestartServerWithoutArguments_WillReuseTheLastPython_test( app, *args ): request = BuildRequest( filetype = 'python', @@ -126,8 +123,7 @@ @IsolatedYcmd @patch( 'ycmd.utils.SafePopen' ) -@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.' - '_CheckBinaryExists', return_value = True ) +@patch( 'ycmd.utils.FindExecutable', side_effect = lambda x: x ) def UserDefinedPython_RestartServerWithArgument_WillUseTheSpecifiedPython_test( app, *args ): python = '/existing/python' @@ -139,8 +135,7 @@ @IsolatedYcmd @patch( 'ycmd.utils.SafePopen' ) -@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.' - '_CheckBinaryExists', return_value = False ) +@patch( 'ycmd.utils.FindExecutable', return_value = None ) def UserDefinedPython_RestartServerWithNonExistingPythonArgument_test( app, *args ): python = '/non/existing/python' diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/request_wrap_test.py ycmd-0+20161219+git486b809/ycmd/tests/request_wrap_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/request_wrap_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/request_wrap_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,6 +25,8 @@ standard_library.install_aliases() from builtins import * # noqa +from ycmd.utils import ToBytes + from nose.tools import eq_ from ..request_wrap import RequestWrap @@ -101,6 +103,55 @@ contents = 'fäö.bär') )[ 'start_column' ] ) +def StartColumn_UnicodeNotIdentifier_test(): + contents = "var x = '†es†ing'." + + # † not considered an identifier character + + for i in range( 13, 15 ): + print( ToBytes( contents )[ i - 1 : i ] ) + eq_( 13, + RequestWrap( PrepareJson( column_num = i, + contents = contents ) )[ 'start_column' ] ) + + eq_( 13, + RequestWrap( PrepareJson( column_num = 15, + contents = contents ) )[ 'start_column' ] ) + + for i in range( 18, 20 ): + print( ToBytes( contents )[ i - 1 : i ] ) + eq_( 18, + RequestWrap( PrepareJson( column_num = i, + contents = contents ) )[ 'start_column' ] ) + + +def StartColumn_QueryIsUnicode_test(): + contents = "var x = ålpha.alphå" + eq_( 16, + RequestWrap( PrepareJson( column_num = 16, + contents = contents ) )[ 'start_column' ] ) + eq_( 16, + RequestWrap( PrepareJson( column_num = 19, + contents = contents ) )[ 'start_column' ] ) + + +def StartColumn_QueryStartsWithUnicode_test(): + contents = "var x = ålpha.ålpha" + eq_( 16, + RequestWrap( PrepareJson( column_num = 16, + contents = contents ) )[ 'start_column' ] ) + eq_( 16, + RequestWrap( PrepareJson( column_num = 19, + contents = contents ) )[ 'start_column' ] ) + + +def StartColumn_ThreeByteUnicode_test(): + contents = "var x = '†'." + eq_( 15, + RequestWrap( PrepareJson( column_num = 15, + contents = contents ) )[ 'start_column' ] ) + + def StartColumn_Paren_test(): eq_( 5, RequestWrap( PrepareJson( column_num = 8, @@ -166,3 +217,15 @@ eq_( '', RequestWrap( PrepareJson( column_num = 8, contents = 'foo ') )[ 'query' ] ) + + +def Query_UnicodeSinglecharInclusive_test(): + eq_( 'ø', + RequestWrap( PrepareJson( column_num = 7, + contents = 'abc.ø' ) )[ 'query' ] ) + + +def Query_UnicodeSinglecharExclusive_test(): + eq_( '', + RequestWrap( PrepareJson( column_num = 5, + contents = 'abc.ø' ) )[ 'query' ] ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/rust/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/rust/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,73 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.rust import IsolatedYcmd, SharedYcmd +from ycmd.tests.test_utils import BuildRequest, StopCompleterServer, UserOption + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + request_data = BuildRequest( filetype = 'rust' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Rust completer debug information:\n' + ' Racerd running at: http://127.0.0.1:\d+\n' + ' Racerd process ID: \d+\n' + ' Racerd executable: .+\n' + ' Racerd logfiles:\n' + ' .+\n' + ' .+\n' + ' Rust sources: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + StopCompleterServer( app, 'rust' ) + request_data = BuildRequest( filetype = 'rust' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Rust completer debug information:\n' + ' Racerd no longer running\n' + ' Racerd executable: .+\n' + ' Racerd logfiles:\n' + ' .+\n' + ' .+\n' + ' Rust sources: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + StopCompleterServer( app, 'rust' ) + request_data = BuildRequest( filetype = 'rust' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'Rust completer debug information:\n' + ' Racerd is not running\n' + ' Racerd executable: .+\n' + ' Rust sources: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/rust/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/rust/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,11 +23,18 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import assert_that, has_entry, has_items, contains_string +from hamcrest import assert_that, empty, has_entry, has_items +from nose.tools import eq_ +from mock import patch +from ycmd.completers.rust.rust_completer import ( + ERROR_FROM_RACERD_MESSAGE, NON_EXISTING_RUST_SOURCES_PATH_MESSAGE ) from ycmd.tests.rust import IsolatedYcmd, PathToTestFile, SharedYcmd -from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher +from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher, + ErrorMatcher, UserOption, + WaitUntilCompleterServerReady ) from ycmd.utils import ReadFile +import requests @SharedYcmd @@ -55,6 +62,8 @@ @IsolatedYcmd def GetCompletions_WhenStandardLibraryCompletionFails_MentionRustSrcPath_test( app ): + WaitUntilCompleterServerReady( app, 'rust' ) + filepath = PathToTestFile( 'std_completions.rs' ) contents = ReadFile( filepath ) @@ -69,5 +78,44 @@ completion_data, expect_errors = True ).json assert_that( response, - has_entry( 'message', - contains_string( 'rust_src_path' ) ) ) + ErrorMatcher( RuntimeError, ERROR_FROM_RACERD_MESSAGE ) ) + + +@IsolatedYcmd +def GetCompletions_NonExistingRustSrcPathFromUserOption_test( app ): + with UserOption( 'rust_src_path', '/non/existing/rust/src/path' ): + response = app.get( '/ready', + { 'subserver': 'rust' }, + expect_errors = True ).json + assert_that( response, + ErrorMatcher( RuntimeError, + NON_EXISTING_RUST_SOURCES_PATH_MESSAGE ) ) + + +@IsolatedYcmd +@patch.dict( 'os.environ', { 'RUST_SRC_PATH': '/non/existing/rust/src/path' } ) +def GetCompletions_NonExistingRustSrcPathFromEnvironmentVariable_test( app ): + response = app.get( '/ready', + { 'subserver': 'rust' }, + expect_errors = True ).json + assert_that( response, + ErrorMatcher( RuntimeError, + NON_EXISTING_RUST_SOURCES_PATH_MESSAGE ) ) + + +@SharedYcmd +def GetCompletions_NoCompletionsFound_test( app ): + filepath = PathToTestFile( 'test.rs' ) + contents = ReadFile( filepath ) + + completion_data = BuildRequest( filepath = filepath, + filetype = 'rust', + contents = contents, + force_semantic = True, + line_num = 4, + column_num = 1 ) + + response = app.post_json( '/completions', completion_data ) + + eq_( response.status_code, requests.codes.ok ) + assert_that( response.json, has_entry( 'completions', empty() ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/rust/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/rust/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -25,10 +25,11 @@ import functools import os -import time -from ycmd.tests.test_utils import BuildRequest, SetUpApp from ycmd import handlers +from ycmd.tests.test_utils import ( ClearCompletionsCache, SetUpApp, + StopCompleterServer, + WaitUntilCompleterServerReady ) shared_app = None @@ -38,28 +39,6 @@ return os.path.join( dir_of_current_script, 'testdata', *args ) -def WaitUntilRacerdServerReady( app ): - retries = 100 - - while retries > 0: - result = app.get( '/ready', { 'subserver': 'rust' } ).json - if result: - return - - time.sleep( 0.2 ) - retries = retries - 1 - - raise RuntimeError( "Timeout waiting for JediHTTP" ) - - -def StopRacerdServer( app ): - app.post_json( '/run_completer_command', - BuildRequest( completer_target = 'filetype_default', - command_arguments = [ 'StopServer' ], - filetype = 'rust' ), - expect_errors = True ) - - def setUpPackage(): """Initializes the ycmd server as a WebTest application that will be shared by all tests using the SharedYcmd decorator in this package. Additional @@ -68,8 +47,7 @@ global shared_app shared_app = SetUpApp() - - WaitUntilRacerdServerReady( shared_app ) + WaitUntilCompleterServerReady( shared_app, 'rust' ) def tearDownPackage(): @@ -77,7 +55,7 @@ executed once after running all the tests in the package.""" global shared_app - StopRacerdServer( shared_app ) + StopCompleterServer( shared_app, 'rust' ) def SharedYcmd( test ): @@ -89,6 +67,7 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper @@ -104,12 +83,10 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): old_server_state = handlers._server_state - + app = SetUpApp() try: - app = SetUpApp() - WaitUntilRacerdServerReady( app ) test( app, *args, **kwargs ) - StopRacerdServer( app ) finally: + StopCompleterServer( app, 'rust' ) handlers._server_state = old_server_state return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/rust/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/rust/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -61,3 +61,47 @@ for test in tests: yield RunGoToTest, test + + +@SharedYcmd +def Subcommands_GetDoc_Method_test( app ): + filepath = PathToTestFile( 'docs.rs' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'rust', + line_num = 7, + column_num = 9, + contents = contents, + command_arguments = [ 'GetDoc' ], + completer_target = 'filetype_default' ) + + response = app.post_json( '/run_completer_command', event_data ).json + + eq_( response, { + 'detailed_info': 'pub fn fun()\n---\n' + 'some docs on a function' + } ) + + +@SharedYcmd +def Subcommands_GetDoc_Fail_Method_test( app ): + filepath = PathToTestFile( 'docs.rs' ) + contents = ReadFile( filepath ) + + # no docs exist for this function + event_data = BuildRequest( filepath = filepath, + filetype = 'rust', + line_num = 8, + column_num = 9, + contents = contents, + command_arguments = [ 'GetDoc' ], + completer_target = 'filetype_default' ) + + response = app.post_json( + '/run_completer_command', + event_data, + expect_errors=True ).json + + eq_( response[ 'exception' ][ 'TYPE' ], 'RuntimeError' ) + eq_( response[ 'message' ], 'Can\'t lookup docs.' ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/testdata/docs.rs ycmd-0+20161219+git486b809/ycmd/tests/rust/testdata/docs.rs --- ycmd-0+20160327+gitc3e6904/ycmd/tests/rust/testdata/docs.rs 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/rust/testdata/docs.rs 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,9 @@ +mod t { + /// some docs on a function + pub fn fun() { } +} + +pub fn main() { + t::fun(); + t::dne(); +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/server_utils_test.py ycmd-0+20161219+git486b809/ycmd/tests/server_utils_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/server_utils_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/server_utils_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,11 +23,136 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import raises, assert_that, calling +from hamcrest import ( assert_that, calling, contains, contains_inanyorder, + empty, equal_to, has_length, raises ) +from mock import patch from nose.tools import ok_ -from ycmd.server_utils import ( PathToNearestThirdPartyFolder, - AddNearestThirdPartyFoldersToSysPath ) import os.path +import sys + +from ycmd.server_utils import ( AddNearestThirdPartyFoldersToSysPath, + CompatibleWithCurrentCore, + PathToNearestThirdPartyFolder ) +from ycmd.tests import PathToTestFile + +DIR_OF_THIRD_PARTY = os.path.abspath( + os.path.join( os.path.dirname( __file__ ), '..', '..', 'third_party' ) ) +THIRD_PARTY_FOLDERS = ( + os.path.join( DIR_OF_THIRD_PARTY, 'argparse' ), + os.path.join( DIR_OF_THIRD_PARTY, 'bottle' ), + os.path.join( DIR_OF_THIRD_PARTY, 'frozendict' ), + os.path.join( DIR_OF_THIRD_PARTY, 'godef' ), + os.path.join( DIR_OF_THIRD_PARTY, 'gocode' ), + os.path.join( DIR_OF_THIRD_PARTY, 'JediHTTP' ), + os.path.join( DIR_OF_THIRD_PARTY, 'OmniSharpServer' ), + os.path.join( DIR_OF_THIRD_PARTY, 'racerd' ), + os.path.join( DIR_OF_THIRD_PARTY, 'requests' ), + os.path.join( DIR_OF_THIRD_PARTY, 'tern_runtime' ), + os.path.join( DIR_OF_THIRD_PARTY, 'waitress' ) +) + + +@patch( 'ycmd.server_utils._logger', autospec = True ) +def RunCompatibleWithCurrentCoreImportException( test, logger ): + with patch( 'ycmd.server_utils.ImportCore', + side_effect = ImportError( test[ 'exception_message' ] ) ): + assert_that( CompatibleWithCurrentCore(), + equal_to( test[ 'exit_status' ] ) ) + + assert_that( logger.method_calls, has_length( 1 ) ) + logger.exception.assert_called_with( test[ 'logged_message' ] ) + + +@patch( 'ycmd.server_utils._logger', autospec = True ) +def CompatibleWithCurrentCore_Compatible_test( logger ): + assert_that( CompatibleWithCurrentCore(), equal_to( 0 ) ) + assert_that( logger.method_calls, empty() ) + + +def CompatibleWithCurrentCore_Unexpected_test(): + RunCompatibleWithCurrentCoreImportException( { + 'exception_message': 'unexpected import exception', + 'exit_status': 3, + 'logged_message': 'unexpected import exception' + } ) + + +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 RunCompatibleWithCurrentCoreImportException, { + 'exception_message': error, + 'exit_status': 4, + 'logged_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_exception_messages = [ + # 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 message in import_exception_messages: + yield RunCompatibleWithCurrentCoreImportException, { + 'exception_message': message, + 'exit_status': 5, + 'logged_message': 'ycm_core library compiled for Python 2 ' + 'but loaded in Python 3.' + } + + +def CompatibleWithCurrentCore_Python3_test(): + import_exception_messages = [ + # 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 message in import_exception_messages: + yield RunCompatibleWithCurrentCoreImportException, { + 'exception_message': message, + 'exit_status': 6, + 'logged_message': 'ycm_core library compiled for Python 3 ' + 'but loaded in Python 2.' + } + + +@patch( 'ycm_core.YcmCoreVersion', side_effect = AttributeError() ) +@patch( 'ycmd.server_utils._logger', autospec = True ) +def CompatibleWithCurrentCore_Outdated_NoYcmCoreVersionMethod_test( logger, + *args ): + assert_that( CompatibleWithCurrentCore(), equal_to( 7 ) ) + assert_that( logger.method_calls, has_length( 1 ) ) + logger.exception.assert_called_with( + '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 ) +@patch( 'ycmd.server_utils._logger', autospec = True ) +def CompatibleWithCurrentCore_Outdated_NoVersionMatch_test( logger, *args ): + assert_that( CompatibleWithCurrentCore(), equal_to( 7 ) ) + assert_that( logger.method_calls, has_length( 1 ) ) + logger.error.assert_called_with( + 'ycm_core library too old; PLEASE RECOMPILE by running the build.py ' + 'script. See the documentation for more details.' ) def PathToNearestThirdPartyFolder_Success_test(): @@ -43,3 +168,52 @@ calling( AddNearestThirdPartyFoldersToSysPath ).with_args( os.path.expanduser( '~' ) ), raises( RuntimeError, '.*third_party folder.*' ) ) + + +@patch( 'sys.path', [ + PathToTestFile( 'python-future', 'some', 'path' ), + PathToTestFile( 'python-future', 'standard_library' ), + PathToTestFile( 'python-future', 'standard_library', 'site-packages' ), + PathToTestFile( 'python-future', 'another', 'path' ) ] ) +def AddNearestThirdPartyFoldersToSysPath_FutureAfterStandardLibrary_test( + *args ): + AddNearestThirdPartyFoldersToSysPath( __file__ ) + assert_that( sys.path[ : len( THIRD_PARTY_FOLDERS ) ], contains_inanyorder( + *THIRD_PARTY_FOLDERS + ) ) + assert_that( sys.path[ len( THIRD_PARTY_FOLDERS ) : ], contains( + PathToTestFile( 'python-future', 'some', 'path' ), + PathToTestFile( 'python-future', 'standard_library' ), + os.path.join( DIR_OF_THIRD_PARTY, 'python-future', 'src' ), + PathToTestFile( 'python-future', 'standard_library', 'site-packages' ), + PathToTestFile( 'python-future', 'another', 'path' ) + ) ) + + +@patch( 'sys.path', [ + PathToTestFile( 'python-future', 'some', 'path' ), + PathToTestFile( 'python-future', 'another', 'path' ) ] ) +def AddNearestThirdPartyFoldersToSysPath_ErrorIfNoStandardLibrary_test( *args ): + assert_that( + calling( AddNearestThirdPartyFoldersToSysPath ).with_args( __file__ ), + raises( RuntimeError, + 'Could not find standard library path in Python path.' ) ) + + +@patch( 'sys.path', [ + PathToTestFile( 'python-future', 'some', 'path' ), + PathToTestFile( 'python-future', 'virtualenv_library' ), + PathToTestFile( 'python-future', 'standard_library' ), + PathToTestFile( 'python-future', 'another', 'path' ) ] ) +def AddNearestThirdPartyFoldersToSysPath_IgnoreVirtualEnvLibrary_test( *args ): + AddNearestThirdPartyFoldersToSysPath( __file__ ) + assert_that( sys.path[ : len( THIRD_PARTY_FOLDERS ) ], contains_inanyorder( + *THIRD_PARTY_FOLDERS + ) ) + assert_that( sys.path[ len( THIRD_PARTY_FOLDERS ) : ], contains( + PathToTestFile( 'python-future', 'some', 'path' ), + PathToTestFile( 'python-future', 'virtualenv_library' ), + PathToTestFile( 'python-future', 'standard_library' ), + os.path.join( DIR_OF_THIRD_PARTY, 'python-future', 'src' ), + PathToTestFile( 'python-future', 'another', 'path' ) + ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/shutdown_test.py ycmd-0+20161219+git486b809/ycmd/tests/shutdown_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/shutdown_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/shutdown_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,90 @@ +# 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 __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 hamcrest import assert_that, equal_to + +from ycmd.tests.client_test import Client_test + + +class Shutdown_test( Client_test ): + + @Client_test.CaptureLogfiles + def FromHandlerWithoutSubserver_test( self ): + self.Start() + self.AssertServersAreRunning() + + response = self.PostRequest( 'shutdown' ) + self.AssertResponse( response ) + assert_that( response.json(), equal_to( True ) ) + self.AssertServersShutDown( timeout = 5 ) + self.AssertLogfilesAreRemoved() + + + @Client_test.CaptureLogfiles + def FromHandlerWithSubservers_test( self ): + self.Start() + + filetypes = [ 'cs', + 'go', + 'javascript', + 'python', + 'typescript', + 'rust' ] + for filetype in filetypes: + self.StartSubserverForFiletype( filetype ) + self.AssertServersAreRunning() + + response = self.PostRequest( 'shutdown' ) + self.AssertResponse( response ) + assert_that( response.json(), equal_to( True ) ) + self.AssertServersShutDown( timeout = 5 ) + self.AssertLogfilesAreRemoved() + + + @Client_test.CaptureLogfiles + def FromWatchdogWithoutSubserver_test( self ): + self.Start( idle_suicide_seconds = 2, check_interval_seconds = 1 ) + self.AssertServersAreRunning() + + self.AssertServersShutDown( timeout = 5 ) + self.AssertLogfilesAreRemoved() + + + @Client_test.CaptureLogfiles + def FromWatchdogWithSubservers_test( self ): + self.Start( idle_suicide_seconds = 5, check_interval_seconds = 1 ) + + filetypes = [ 'cs', + 'go', + 'javascript', + 'python', + 'typescript', + 'rust' ] + for filetype in filetypes: + self.StartSubserverForFiletype( filetype ) + self.AssertServersAreRunning() + + self.AssertServersShutDown( timeout = 15 ) + self.AssertLogfilesAreRemoved() diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/basic.tags ycmd-0+20161219+git486b809/ycmd/tests/testdata/basic.tags --- ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/basic.tags 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/testdata/basic.tags 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,10 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 2 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ +!_TAG_PROGRAM_NAME Exuberant Ctags // +!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ +!_TAG_PROGRAM_VERSION 5.8 // +i1 foo junky;'junklanguage:C++ +i1 bar junky;'junklanguage:C++ +foosy foo junky;"'junk language:C++ zanzibar +fooaaa bar junky;"'junk language:C++ zanzibar diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/testdata/extra_conf/erroneous_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1 @@ +raise Exception( 'Exception raised' ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/extra_conf/global_extra_conf.py ycmd-0+20161219+git486b809/ycmd/tests/testdata/extra_conf/global_extra_conf.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/testdata/extra_conf/global_extra_conf.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/testdata/extra_conf/global_extra_conf.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,6 @@ +def NoException(): + pass + + +def RaiseException(): + raise Exception( 'Exception raised' ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/test_utils.py ycmd-0+20161219+git486b809/ycmd/tests/test_utils.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/test_utils.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/test_utils.py 2016-12-20 08:50:19.000000000 +0000 @@ -27,16 +27,22 @@ from builtins import * # noqa from future.utils import PY2 -from hamcrest import contains_string, has_entry, has_entries +from hamcrest import contains_string, has_entry, has_entries, assert_that from mock import patch from webtest import TestApp import bottle import contextlib +import nose +import functools +import os +import tempfile +import time +import stat from ycmd import handlers, user_options_store from ycmd.completers.completer import Completer from ycmd.responses import BuildCompletionData -from ycmd.utils import OnMac, OnWindows +from ycmd.utils import GetCurrentDirectory, OnMac, OnWindows, ToUnicode import ycm_core try: @@ -50,6 +56,7 @@ ClangOnly = skipIf( not ycm_core.HasClangSupport(), 'Only when Clang support available' ) MacOnly = skipIf( not OnMac(), 'Mac only' ) +UnixOnly = skipIf( OnWindows(), 'Unix only' ) def BuildRequest( **kwargs ): @@ -134,6 +141,13 @@ } ) +def LineColMatcher( line, col ): + return has_entries( { + 'line_num': line, + 'column_num': col + } ) + + @contextlib.contextmanager def PatchCompleter( completer, filetype ): user_options = handlers._server_state._user_options @@ -149,17 +163,82 @@ user_options = current_options.copy() user_options.update( { key: value } ) handlers.UpdateUserOptions( user_options ) - yield + yield user_options finally: handlers.UpdateUserOptions( current_options ) +@contextlib.contextmanager +def CurrentWorkingDirectory( path ): + old_cwd = GetCurrentDirectory() + os.chdir( path ) + try: + yield old_cwd + finally: + os.chdir( old_cwd ) + + +# The "exe" suffix is needed on Windows and not harmful on other platforms. +@contextlib.contextmanager +def TemporaryExecutable( extension = '.exe' ): + with tempfile.NamedTemporaryFile( prefix = 'Temp', + suffix = extension ) as executable: + os.chmod( executable.name, stat.S_IXUSR ) + yield executable.name + + def SetUpApp(): bottle.debug( True ) handlers.SetServerStateToDefaults() return TestApp( handlers.app ) +def StartCompleterServer( app, filetype, filepath = '/foo' ): + app.post_json( '/run_completer_command', + BuildRequest( command_arguments = [ 'RestartServer' ], + filetype = filetype, + filepath = filepath ) ) + + +def StopCompleterServer( app, filetype, filepath = '/foo' ): + app.post_json( '/run_completer_command', + BuildRequest( command_arguments = [ 'StopServer' ], + filetype = filetype, + filepath = filepath ), + expect_errors = True ) + + +def WaitUntilCompleterServerReady( app, filetype ): + retries = 100 + + while retries > 0: + result = app.get( '/ready', { 'subserver': filetype } ).json + if result: + return + + time.sleep( 0.2 ) + retries = retries - 1 + + raise RuntimeError( + 'Timeout waiting for "{0}" filetype completer'.format( filetype ) ) + + + +def ClearCompletionsCache(): + """Invalidates cached completions for completers stored in the server state: + filetype completers and general completers (identifier, filename, and + ultisnips completers). + + This function is used when sharing the application between tests so that + no completions are cached by previous tests.""" + server_state = handlers._server_state + for completer in server_state.GetLoadedFiletypeCompleters(): + completer._completions_cache.Invalidate() + general_completer = server_state.GetGeneralCompleter() + for completer in general_completer._all_completers: + completer._completions_cache.Invalidate() + + class DummyCompleter( Completer ): def __init__( self, user_options ): super( DummyCompleter, self ).__init__( user_options ) @@ -176,3 +255,47 @@ # This method is here for testing purpose, so it can be mocked during tests def CandidatesList( self ): return [] + + +def ExpectedFailure( reason, *exception_matchers ): + """Defines a decorator to be attached to tests. This decorator + marks the test as being known to fail, e.g. where documenting or exercising + known incorrect behaviour. + + The parameters are: + - |reason| a textual description of the reason for the known issue. This + is used for the skip reason + - |exception_matchers| additional arguments are hamcrest matchers to apply + to the exception thrown. If the matchers don't match, then the + test is marked as error, with the original exception. + + If the test fails (for the correct reason), then it is marked as skipped. + If it fails for any other reason, it is marked as failed. + If the test passes, then it is also marked as failed.""" + def decorator( test ): + @functools.wraps( test ) + def Wrapper( *args, **kwargs ): + try: + test( *args, **kwargs ) + except Exception as test_exception: + # Ensure that we failed for the right reason + test_exception_message = ToUnicode( test_exception ) + try: + for matcher in exception_matchers: + assert_that( test_exception_message, matcher ) + except AssertionError: + # Failed for the wrong reason! + import traceback + print( 'Test failed for the wrong reason: ' + traceback.format_exc() ) + # Real failure reason is the *original* exception, we're only trapping + # and ignoring the exception that is expected. + raise test_exception + + # Failed for the right reason + raise nose.SkipTest( reason ) + else: + raise AssertionError( 'Test was expected to fail: {0}'.format( + reason ) ) + return Wrapper + + return decorator diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/debug_info_test.py ycmd-0+20161219+git486b809/ycmd/tests/typescript/debug_info_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/debug_info_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/debug_info_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,66 @@ +# 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 __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 hamcrest import assert_that, matches_regexp + +from ycmd.tests.typescript import IsolatedYcmd, SharedYcmd, StopCompleterServer +from ycmd.tests.test_utils import BuildRequest, UserOption + + +@SharedYcmd +def DebugInfo_ServerIsRunning_test( app ): + request_data = BuildRequest( filetype = 'typescript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'TypeScript completer debug information:\n' + ' TSServer running\n' + ' TSServer process ID: \d+\n' + ' TSServer executable: .+\n' + ' TSServer logfile: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesExist_test( app ): + with UserOption( 'server_keep_logfiles', True ): + StopCompleterServer( app, filetype = 'typescript' ) + request_data = BuildRequest( filetype = 'typescript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'TypeScript completer debug information:\n' + ' TSServer no longer running\n' + ' TSServer executable: .+\n' + ' TSServer logfile: .+' ) ) + + +@IsolatedYcmd +def DebugInfo_ServerIsNotRunning_LogfilesDoNotExist_test( app ): + with UserOption( 'server_keep_logfiles', False ): + StopCompleterServer( app, filetype = 'typescript' ) + request_data = BuildRequest( filetype = 'typescript' ) + assert_that( + app.post_json( '/debug_info', request_data ).json, + matches_regexp( 'TypeScript completer debug information:\n' + ' TSServer is not running\n' + ' TSServer executable: .+' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/event_notification_test.py ycmd-0+20161219+git486b809/ycmd/tests/typescript/event_notification_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/event_notification_test.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/event_notification_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,116 @@ +# 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 __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +from future import standard_library +standard_library.install_aliases() +from builtins import * # noqa + +from hamcrest import assert_that, contains, has_entries + +from ycmd.tests.typescript import IsolatedYcmd, PathToTestFile +from ycmd.tests.test_utils import ( BuildRequest, ClearCompletionsCache, + CompletionEntryMatcher ) +from ycmd.utils import ReadFile + + +@IsolatedYcmd +def EventNotification_OnBufferUnload_CloseFile_test( app ): + # Open main.ts file in a buffer. + main_filepath = PathToTestFile( 'buffer_unload', 'main.ts' ) + main_contents = ReadFile( main_filepath ) + + event_data = BuildRequest( filepath = main_filepath, + filetype = 'typescript', + contents = main_contents, + event_name = 'BufferVisit' ) + app.post_json( '/event_notification', event_data ) + + # Complete in main.ts buffer an object defined in imported.ts. + completion_data = BuildRequest( filepath = main_filepath, + filetype = 'typescript', + contents = main_contents, + line_num = 3, + column_num = 10 ) + response = app.post_json( '/completions', completion_data ) + assert_that( response.json, has_entries( { + 'completions': contains( CompletionEntryMatcher( 'method' ) ) } ) ) + # FIXME: we should not have to clear the cache. + ClearCompletionsCache() + + # Open imported.ts file in another buffer. + imported_filepath = PathToTestFile( 'buffer_unload', 'imported.ts' ) + imported_contents = ReadFile( imported_filepath ) + + event_data = BuildRequest( filepath = imported_filepath, + filetype = 'typescript', + contents = imported_contents, + event_name = 'BufferVisit' ) + app.post_json( '/event_notification', event_data ) + + # Modify imported.ts buffer without writing the changes to disk. + modified_imported_contents = imported_contents.replace( 'method', + 'modified_method' ) + + # FIXME: TypeScript completer should not rely on the FileReadyToParse events + # to synchronize the contents of dirty buffers but use instead the file_data + # field of the request. + event_data = BuildRequest( filepath = imported_filepath, + filetype = 'typescript', + contents = modified_imported_contents, + event_name = 'FileReadyToParse' ) + app.post_json( '/event_notification', event_data ) + + # Complete at same location in main.ts buffer. + imported_data = { + imported_filepath: { + 'filetypes': [ 'typescript' ], + 'contents': modified_imported_contents + } + } + completion_data = BuildRequest( filepath = main_filepath, + filetype = 'typescript', + contents = main_contents, + line_num = 3, + column_num = 10, + file_data = imported_data ) + response = app.post_json( '/completions', completion_data ) + assert_that( response.json, has_entries( { + 'completions': contains( CompletionEntryMatcher( 'modified_method' ) ) } ) + ) + # FIXME: we should not have to clear the cache. + ClearCompletionsCache() + + # Unload imported.ts buffer. + event_data = BuildRequest( filepath = imported_filepath, + filetype = 'typescript', + contents = imported_contents, + event_name = 'BufferUnload' ) + app.post_json( '/event_notification', event_data ) + + # Complete at same location in main.ts buffer. + completion_data = BuildRequest( filepath = main_filepath, + filetype = 'typescript', + contents = main_contents, + line_num = 3, + column_num = 10 ) + response = app.post_json( '/completions', completion_data ) + assert_that( response.json, has_entries( { + 'completions': contains( CompletionEntryMatcher( 'method' ) ) } ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/get_completions_test.py ycmd-0+20161219+git486b809/ycmd/tests/typescript/get_completions_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/get_completions_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/get_completions_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -23,11 +23,15 @@ standard_library.install_aliases() from builtins import * # noqa -from hamcrest import assert_that, contains_inanyorder, has_entries +from hamcrest import ( all_of, any_of, assert_that, calling, + contains_inanyorder, has_entries, has_item, is_not, + raises ) from mock import patch +from webtest import AppError -from ycmd.tests.typescript import PathToTestFile, SharedYcmd -from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher +from ycmd.tests.typescript import IsolatedYcmd, PathToTestFile, SharedYcmd +from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher, + StopCompleterServer ) from ycmd.utils import ReadFile @@ -81,11 +85,88 @@ RunTest( app, { 'expect': { 'data': has_entries( { - 'completions': contains_inanyorder( - CompletionEntryMatcher( 'methodA' ), - CompletionEntryMatcher( 'methodB' ), - CompletionEntryMatcher( 'methodC' ) + 'completions': all_of( + contains_inanyorder( + CompletionEntryMatcher( 'methodA' ), + CompletionEntryMatcher( 'methodB' ), + CompletionEntryMatcher( 'methodC' ), + ), + is_not( any_of( + has_item( + CompletionEntryMatcher( 'methodA', extra_params = { + 'menu_text': 'methodA (method) Foo.methodA(): void' } ) ), + has_item( + CompletionEntryMatcher( 'methodB', extra_params = { + 'menu_text': 'methodB (method) Foo.methodB(): void' } ) ), + has_item( + CompletionEntryMatcher( 'methodC', extra_params = { + 'menu_text': ( 'methodC (method) Foo.methodC(a: ' + '{ foo: string; bar: number; }): void' ) } ) ) + ) ) ) } ) } } ) + + +@SharedYcmd +def GetCompletions_AfterRestart_test( app ): + filepath = PathToTestFile( 'test.ts' ) + + app.post_json( '/run_completer_command', + BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'RestartServer' ], + filetype = 'typescript', + filepath = filepath ) ) + + completion_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = ReadFile( filepath ), + force_semantic = True, + line_num = 17, + column_num = 6 ) + + response = app.post_json( '/completions', completion_data ) + assert_that( response.json, has_entries( { + 'completions': contains_inanyorder( + CompletionEntryMatcher( 'methodA', extra_params = { + 'menu_text': 'methodA (method) Foo.methodA(): void' } ), + CompletionEntryMatcher( 'methodB', extra_params = { + 'menu_text': 'methodB (method) Foo.methodB(): void' } ), + CompletionEntryMatcher( 'methodC', extra_params = { + 'menu_text': ( 'methodC (method) Foo.methodC(a: ' + '{ foo: string; bar: number; }): void' ) } ), + ) + } ) ) + + +@IsolatedYcmd +def GetCompletions_ServerIsNotRunning_test( app ): + StopCompleterServer( app, filetype = 'typescript' ) + + filepath = PathToTestFile( 'test.ts' ) + contents = ReadFile( filepath ) + + # Check that sending a request to TSServer (the response is ignored) raises + # the proper exception. + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + assert_that( + calling( app.post_json ).with_args( '/event_notification', event_data ), + raises( AppError, 'TSServer is not running.' ) ) + + # Check that sending a command to TSServer (the response is processed) raises + # the proper exception. + completion_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + force_semantic = True, + line_num = 17, + column_num = 6 ) + + assert_that( + calling( app.post_json ).with_args( '/completions', completion_data ), + raises( AppError, 'TSServer is not running.' ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/__init__.py ycmd-0+20161219+git486b809/ycmd/tests/typescript/__init__.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/__init__.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/__init__.py 2016-12-20 08:50:19.000000000 +0000 @@ -26,7 +26,10 @@ import functools import os -from ycmd.tests.test_utils import SetUpApp +from ycmd import handlers +from ycmd.tests.test_utils import ( ClearCompletionsCache, SetUpApp, + StopCompleterServer, + WaitUntilCompleterServerReady ) shared_app = None @@ -44,6 +47,13 @@ global shared_app shared_app = SetUpApp() + WaitUntilCompleterServerReady( shared_app, 'typescript' ) + + +def tearDownPackage(): + global shared_app + + StopCompleterServer( shared_app, 'typescript' ) def SharedYcmd( test ): @@ -55,5 +65,26 @@ @functools.wraps( test ) def Wrapper( *args, **kwargs ): + ClearCompletionsCache() return test( shared_app, *args, **kwargs ) return Wrapper + + +def IsolatedYcmd( test ): + """Defines a decorator to be attached to tests of this package. This decorator + passes a unique ycmd application as a parameter. It should be used on tests + that change the server state in a irreversible way (ex: a semantic subserver + is stopped or restarted) or expect a clean state (ex: no semantic subserver + started, no .ycm_extra_conf.py loaded, etc). + + Do NOT attach it to test generators but directly to the yielded tests.""" + @functools.wraps( test ) + def Wrapper( *args, **kwargs ): + old_server_state = handlers._server_state + app = SetUpApp() + try: + test( app, *args, **kwargs ) + finally: + StopCompleterServer( app, 'typescript' ) + handlers._server_state = old_server_state + return Wrapper diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/subcommands_test.py ycmd-0+20161219+git486b809/ycmd/tests/typescript/subcommands_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/subcommands_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/subcommands_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2015 ycmd contributors # # This file is part of ycmd. @@ -23,10 +25,11 @@ standard_library.install_aliases() from builtins import * # noqa +import pprint + from hamcrest import ( assert_that, contains, contains_inanyorder, - has_items, has_entries ) from ycmd.tests.typescript import PathToTestFile, SharedYcmd @@ -147,8 +150,37 @@ @SharedYcmd +def Subcommands_GetDoc_Class_Unicode_test( app ): + filepath = PathToTestFile( 'unicode.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + gettype_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GetDoc' ], + line_num = 35, + column_num = 12, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + response = app.post_json( '/run_completer_command', gettype_data ).json + assert_that( response, + has_entries( { + 'detailed_info': 'class Båøz\n\n' + 'Test unicøde st††††', + } ) ) + + +@SharedYcmd def Subcommands_GoToReferences_test( app ): filepath = PathToTestFile( 'test.ts' ) + file3 = PathToTestFile( 'file3.ts' ) contents = ReadFile( filepath ) event_data = BuildRequest( filepath = filepath, @@ -166,14 +198,77 @@ filetype = 'typescript', filepath = filepath ) - expected = has_items( + expected = contains_inanyorder( has_entries( { 'description': 'var bar = new Bar();', 'line_num' : 33, - 'column_num' : 5 } ), + 'column_num' : 5, + 'filepath' : filepath } ), has_entries( { 'description': 'bar.testMethod();', 'line_num' : 34, - 'column_num' : 1 } ) ) + 'column_num' : 1, + 'filepath' : filepath } ), + has_entries( { 'description': 'bar.nonExistingMethod();', + 'line_num' : 35, + 'column_num' : 1, + 'filepath' : filepath } ), + has_entries( { 'description': 'var bar = new Bar();', + 'line_num' : 1, + 'column_num' : 5, + 'filepath' : file3 } ), + has_entries( { 'description': 'bar.testMethod();', + 'line_num' : 2, + 'column_num' : 1, + 'filepath' : file3 } ) + ) + actual = app.post_json( '/run_completer_command', references_data ).json + + pprint.pprint( actual ) + + assert_that( actual, expected ) + + +@SharedYcmd +def Subcommands_GoToReferences_Unicode_test( app ): + filepath = PathToTestFile( 'unicode.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + references_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GoToReferences' ], + line_num = 14, + column_num = 3, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + expected = contains_inanyorder( + has_entries( { 'description': ' å: number;', + 'line_num' : 14, + 'column_num' : 3, + 'filepath' : filepath } ), + has_entries( { 'description': 'var baz = new Bår(); baz.å;', + 'line_num' : 20, + 'column_num' : 27, + 'filepath' : filepath } ), + has_entries( { 'description': 'baz.å;', + 'line_num' : 23, + 'column_num' : 5, + 'filepath' : filepath } ), + has_entries( { 'description': 'føø_long_long.å;', + 'line_num' : 27, + 'column_num' : 17, + 'filepath' : filepath } ) + ) actual = app.post_json( '/run_completer_command', references_data ).json + + pprint.pprint( actual ) + assert_that( actual, expected ) @@ -207,6 +302,35 @@ @SharedYcmd +def Subcommands_GoTo_Unicode_test( app ): + filepath = PathToTestFile( 'unicode.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + goto_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GoToDefinition' ], + line_num = 28, + column_num = 19, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + response = app.post_json( '/run_completer_command', goto_data ).json + assert_that( response, + has_entries( { + 'filepath': filepath, + 'line_num': 15, + 'column_num': 3, + } ) ) + + +@SharedYcmd def Subcommands_GoTo_Fail_test( app ): filepath = PathToTestFile( 'test.ts' ) contents = ReadFile( filepath ) @@ -234,6 +358,62 @@ @SharedYcmd +def Subcommands_GoToType_test( app ): + filepath = PathToTestFile( 'test.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + goto_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GoToType' ], + line_num = 14, + column_num = 6, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + response = app.post_json( '/run_completer_command', goto_data ).json + assert_that( response, + has_entries( { + 'filepath': filepath, + 'line_num': 2, + 'column_num': 1, + } ) ) + + +@SharedYcmd +def Subcommands_GoToType_fail_test( app ): + filepath = PathToTestFile( 'test.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + goto_data = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'GoToType' ], + line_num = 39, + column_num = 8, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + response = app.post_json( '/run_completer_command', + goto_data, + expect_errors = True ).json + assert_that( response, + ErrorMatcher( RuntimeError, 'Could not find type definition' ) ) + + +@SharedYcmd def Subcommands_RefactorRename_Missing_test( app ): filepath = PathToTestFile( 'test.ts' ) contents = ReadFile( filepath ) @@ -317,7 +497,7 @@ response = app.post_json( '/run_completer_command', request ).json - print( str( response ) ) + pprint.pprint( response, indent = 2 ) assert_that( response, has_entries ( { 'fixits': contains( has_entries( { @@ -364,7 +544,7 @@ response = app.post_json( '/run_completer_command', request ).json - print( str( response ) ) + pprint.pprint( response, indent = 2 ) assert_that( response, has_entries ( { 'fixits': contains( has_entries( { @@ -393,3 +573,53 @@ 'location': LocationMatcher( filepath, 25, 9 ) } ) ) } ) ) + + +@SharedYcmd +def Subcommands_RefactorRename_SimpleUnicode_test( app ): + filepath = PathToTestFile( 'unicode.ts' ) + contents = ReadFile( filepath ) + + event_data = BuildRequest( filepath = filepath, + filetype = 'typescript', + contents = contents, + event_name = 'BufferVisit' ) + + app.post_json( '/event_notification', event_data ) + + request = BuildRequest( completer_target = 'filetype_default', + command_arguments = [ 'RefactorRename', 'ø' ], + line_num = 14, + column_num = 3, + contents = contents, + filetype = 'typescript', + filepath = filepath ) + + response = app.post_json( '/run_completer_command', + request ).json + + pprint.pprint( response, indent = 2 ) + + assert_that( response, has_entries ( { + 'fixits': contains( has_entries( { + 'chunks': contains_inanyorder( + ChunkMatcher( + 'ø', + LocationMatcher( filepath, 14, 3 ), + LocationMatcher( filepath, 14, 5 ) ), + ChunkMatcher( + 'ø', + LocationMatcher( filepath, 20, 27 ), + LocationMatcher( filepath, 20, 29 ) ), + ChunkMatcher( + 'ø', + LocationMatcher( filepath, 23, 5 ), + LocationMatcher( filepath, 23, 7 ) ), + ChunkMatcher( + 'ø', + LocationMatcher( filepath, 27, 17), + LocationMatcher( filepath, 27, 19 ) ), + ), + 'location': LocationMatcher( filepath, 14, 3 ) + } ) ) + } ) ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/buffer_unload/imported.ts ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/buffer_unload/imported.ts --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/buffer_unload/imported.ts 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/buffer_unload/imported.ts 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,4 @@ +export class Imported { + method() { + } +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/buffer_unload/main.ts ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/buffer_unload/main.ts --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/buffer_unload/main.ts 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/buffer_unload/main.ts 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,3 @@ +import { Imported } from "./imported"; +let imported = new Imported(); +imported. diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/tsconfig.json ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/tsconfig.json --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/tsconfig.json 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/tsconfig.json 2016-12-20 08:50:19.000000000 +0000 @@ -5,6 +5,7 @@ "files": [ "test.ts", "file2.ts", - "file3.ts" + "file3.ts", + "unicode.ts" ] } diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/unicode.ts ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/unicode.ts --- ycmd-0+20160327+gitc3e6904/ycmd/tests/typescript/testdata/unicode.ts 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/typescript/testdata/unicode.ts 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,38 @@ + +class Bår { + methodA( + a: { + foo: string; + bar: number; + } + ) {} + + methød() { + return '†est'.cha + } + + å: number; + åbc: number; + abc: number; + a: number; +} + +var baz = new Bår(); baz.å; +baz.abc; +baz.a; +baz.å; + +var føø_long_long = new Bår(); +føø_long_long.methød(); +føø_long_long.å; +føø_long_long.åbc; +føø_long_long.abc; + + +/** + * Test unicøde st†††† + */ +class Båøz +{ + +} diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/tests/utils_test.py ycmd-0+20161219+git486b809/ycmd/tests/utils_test.py --- ycmd-0+20160327+gitc3e6904/ycmd/tests/utils_test.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/tests/utils_test.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2016 ycmd contributors. # # This file is part of ycmd. @@ -25,13 +27,15 @@ import os import subprocess -from shutil import rmtree +import tempfile import ycm_core from future.utils import native from mock import patch, call -from nose.tools import eq_, ok_ +from nose.tools import eq_, ok_, raises from ycmd import utils -from ycmd.tests.test_utils import Py2Only, Py3Only, WindowsOnly +from ycmd.tests.test_utils import ( Py2Only, Py3Only, WindowsOnly, UnixOnly, + CurrentWorkingDirectory, + TemporaryExecutable ) from ycmd.tests import PathToTestFile # NOTE: isinstance() vs type() is carefully used in this test file. Before @@ -157,6 +161,57 @@ @Py2Only +def JoinLinesAsUnicode_Py2Bytes_test(): + value = utils.JoinLinesAsUnicode( [ bytes( 'abc' ), bytes( 'xyz' ) ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +@Py2Only +def JoinLinesAsUnicode_Py2Str_test(): + value = utils.JoinLinesAsUnicode( [ 'abc', 'xyz' ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +@Py2Only +def JoinLinesAsUnicode_Py2FutureStr_test(): + value = utils.JoinLinesAsUnicode( [ str( 'abc' ), str( 'xyz' ) ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +@Py2Only +def JoinLinesAsUnicode_Py2Unicode_test(): + value = utils.JoinLinesAsUnicode( [ u'abc', u'xyz' ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +def JoinLinesAsUnicode_Bytes_test(): + value = utils.JoinLinesAsUnicode( [ bytes( b'abc' ), bytes( b'xyz' ) ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +def JoinLinesAsUnicode_Str_test(): + value = utils.JoinLinesAsUnicode( [ u'abc', u'xyz' ] ) + eq_( value, u'abc\nxyz' ) + ok_( isinstance( value, str ) ) + + +def JoinLinesAsUnicode_EmptyList_test(): + value = utils.JoinLinesAsUnicode( [ ] ) + eq_( value, u'' ) + ok_( isinstance( value, str ) ) + + +@raises( ValueError ) +def JoinLinesAsUnicode_BadInput_test(): + utils.JoinLinesAsUnicode( [ 42 ] ) + + +@Py2Only def ToCppStringCompatible_Py2Str_test(): value = utils.ToCppStringCompatible( 'abc' ) eq_( value, 'abc' ) @@ -233,26 +288,6 @@ eq_( vector[ 0 ], '123' ) -def PathToCreatedTempDir_DirDoesntExist_test(): - tempdir = PathToTestFile( 'tempdir' ) - rmtree( tempdir, ignore_errors = True ) - - try: - eq_( utils.PathToCreatedTempDir( tempdir ), tempdir ) - finally: - rmtree( tempdir, ignore_errors = True ) - - -def PathToCreatedTempDir_DirDoesExist_test(): - tempdir = PathToTestFile( 'tempdir' ) - os.makedirs( tempdir ) - - try: - eq_( utils.PathToCreatedTempDir( tempdir ), tempdir ) - finally: - rmtree( tempdir, ignore_errors = True ) - - def RemoveIfExists_Exists_test(): tempfile = PathToTestFile( 'remove-if-exists' ) open( tempfile, 'a' ).close() @@ -279,24 +314,24 @@ ok_( not utils.PathToFirstExistingExecutable( [ 'ycmd-foobar' ] ) ) -@patch( 'os.environ', { 'TRAVIS': 1 } ) -def OnTravis_IsOnTravis_test(): - ok_( utils.OnTravis() ) - - -@patch( 'os.environ', {} ) -def OnTravis_IsNotOnTravis_test(): - ok_( not utils.OnTravis() ) +@UnixOnly +@patch( 'subprocess.Popen' ) +def SafePopen_RemoveStdinWindows_test( *args ): + utils.SafePopen( [ 'foo' ], stdin_windows = 'bar' ) + eq_( subprocess.Popen.call_args, call( [ 'foo' ] ) ) -@patch( 'ycmd.utils.OnWindows', return_value = False ) +@WindowsOnly @patch( 'subprocess.Popen' ) -def SafePopen_RemovesStdinWindows_test( *args ): +def SafePopen_ReplaceStdinWindowsPIPEOnWindows_test( *args ): utils.SafePopen( [ 'foo' ], stdin_windows = subprocess.PIPE ) - eq_( subprocess.Popen.call_args, call( [ 'foo' ] ) ) + eq_( subprocess.Popen.call_args, + call( [ 'foo' ], + stdin = subprocess.PIPE, + creationflags = utils.CREATE_NO_WINDOW ) ) -@patch( 'ycmd.utils.OnWindows', return_value = True ) +@WindowsOnly @patch( 'ycmd.utils.GetShortPathName', side_effect = lambda x: x ) @patch( 'subprocess.Popen' ) def SafePopen_WindowsPath_test( *args ): @@ -313,21 +348,21 @@ os.remove( tempfile ) -@patch( 'ycmd.utils.OnWindows', return_value = False ) +@UnixOnly def ConvertArgsToShortPath_PassthroughOnUnix_test( *args ): eq_( 'foo', utils.ConvertArgsToShortPath( 'foo' ) ) eq_( [ 'foo' ], utils.ConvertArgsToShortPath( [ 'foo' ] ) ) -@patch( 'ycmd.utils.OnWindows', return_value = False ) -def SetEnviron_UnicodeNotOnWindows_test( *args ): +@UnixOnly +def SetEnviron_UnicodeOnUnix_test( *args ): env = {} utils.SetEnviron( env, u'key', u'value' ) eq_( env, { u'key': u'value' } ) @Py2Only -@patch( 'ycmd.utils.OnWindows', return_value = True ) +@WindowsOnly def SetEnviron_UnicodeOnWindows_test( *args ): env = {} utils.SetEnviron( env, u'key', u'value' ) @@ -375,3 +410,151 @@ print( 'foo', file = f ) finally: os.remove( temp ) + + +def CodepointOffsetToByteOffset_test(): + # Tuples of ( ( unicode_line_value, codepoint_offset ), expected_result ). + tests = [ + # Simple ascii strings. + ( ( 'test', 1 ), 1 ), + ( ( 'test', 4 ), 4 ), + ( ( 'test', 5 ), 5 ), + + # Unicode char at beginning. + ( ( '†est', 1 ), 1 ), + ( ( '†est', 2 ), 4 ), + ( ( '†est', 4 ), 6 ), + ( ( '†est', 5 ), 7 ), + + # Unicode char at end. + ( ( 'tes†', 1 ), 1 ), + ( ( 'tes†', 2 ), 2 ), + ( ( 'tes†', 4 ), 4 ), + ( ( 'tes†', 5 ), 7 ), + + # Unicode char in middle. + ( ( 'tes†ing', 1 ), 1 ), + ( ( 'tes†ing', 2 ), 2 ), + ( ( 'tes†ing', 4 ), 4 ), + ( ( 'tes†ing', 5 ), 7 ), + ( ( 'tes†ing', 7 ), 9 ), + ( ( 'tes†ing', 8 ), 10 ), + + # Converts bytes to Unicode. + ( ( utils.ToBytes( '†est' ), 2 ), 4 ) + ] + + for test in tests: + yield lambda: eq_( utils.CodepointOffsetToByteOffset( *test[ 0 ] ), + test[ 1 ] ) + + +def ByteOffsetToCodepointOffset_test(): + # Tuples of ( ( unicode_line_value, byte_offset ), expected_result ). + tests = [ + # Simple ascii strings. + ( ( 'test', 1 ), 1 ), + ( ( 'test', 4 ), 4 ), + ( ( 'test', 5 ), 5 ), + + # Unicode char at beginning. + ( ( '†est', 1 ), 1 ), + ( ( '†est', 4 ), 2 ), + ( ( '†est', 6 ), 4 ), + ( ( '†est', 7 ), 5 ), + + # Unicode char at end. + ( ( 'tes†', 1 ), 1 ), + ( ( 'tes†', 2 ), 2 ), + ( ( 'tes†', 4 ), 4 ), + ( ( 'tes†', 7 ), 5 ), + + # Unicode char in middle. + ( ( 'tes†ing', 1 ), 1 ), + ( ( 'tes†ing', 2 ), 2 ), + ( ( 'tes†ing', 4 ), 4 ), + ( ( 'tes†ing', 7 ), 5 ), + ( ( 'tes†ing', 9 ), 7 ), + ( ( 'tes†ing', 10 ), 8 ), + ] + + for test in tests: + yield lambda: eq_( utils.ByteOffsetToCodepointOffset( *test[ 0 ] ), + test[ 1 ] ) + + +def SplitLines_test(): + # Tuples of ( input, expected_output ) for utils.SplitLines. + tests = [ + ( '', [ '' ] ), + ( ' ', [ ' ' ] ), + ( '\n', [ '', '' ] ), + ( ' \n', [ ' ', '' ] ), + ( ' \n ', [ ' ', ' ' ] ), + ( 'test\n', [ 'test', '' ] ), + ( '\r', [ '', '' ] ), + ( '\r ', [ '', ' ' ] ), + ( 'test\r', [ 'test', '' ] ), + ( '\n\r', [ '', '', '' ] ), + ( '\r\n', [ '', '' ] ), + ( '\r\n\n', [ '', '', '' ] ), + # Other behaviors are just the behavior of splitlines, so just a couple of + # tests to prove that we don't mangle it. + ( 'test\ntesting', [ 'test', 'testing' ] ), + ( '\ntesting', [ '', 'testing' ] ), + ] + + for test in tests: + yield lambda: eq_( utils.SplitLines( test[ 0 ] ), test[ 1 ] ) + + +def FindExecutable_AbsolutePath_test(): + with TemporaryExecutable() as executable: + eq_( executable, utils.FindExecutable( executable ) ) + + +def FindExecutable_RelativePath_test(): + with TemporaryExecutable() as executable: + dirname, exename = os.path.split( executable ) + relative_executable = os.path.join( '.', exename ) + with CurrentWorkingDirectory( dirname ): + eq_( relative_executable, utils.FindExecutable( relative_executable ) ) + + +@patch.dict( 'os.environ', { 'PATH': tempfile.gettempdir() } ) +def FindExecutable_ExecutableNameInPath_test(): + with TemporaryExecutable() as executable: + dirname, exename = os.path.split( executable ) + eq_( executable, utils.FindExecutable( exename ) ) + + +def FindExecutable_ReturnNoneIfFileIsNotExecutable_test(): + with tempfile.NamedTemporaryFile() as non_executable: + eq_( None, utils.FindExecutable( non_executable.name ) ) + + +@WindowsOnly +def FindExecutable_CurrentDirectory_test(): + with TemporaryExecutable() as executable: + dirname, exename = os.path.split( executable ) + with CurrentWorkingDirectory( dirname ): + eq_( executable, utils.FindExecutable( exename ) ) + + +@WindowsOnly +@patch.dict( 'os.environ', { 'PATHEXT': '.xyz' } ) +def FindExecutable_AdditionalPathExt_test(): + with TemporaryExecutable( extension = '.xyz' ) as executable: + eq_( executable, utils.FindExecutable( executable ) ) + + +@Py2Only +def GetCurrentDirectory_Py2NoCurrentDirectory_test(): + with patch( 'os.getcwdu', side_effect = OSError ): + eq_( utils.GetCurrentDirectory(), tempfile.gettempdir() ) + + +@Py3Only +def GetCurrentDirectory_Py3NoCurrentDirectory_test(): + with patch( 'os.getcwd', side_effect = FileNotFoundError ): # noqa + eq_( utils.GetCurrentDirectory(), tempfile.gettempdir() ) diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/utils.py ycmd-0+20161219+git486b809/ycmd/utils.py --- ycmd-0+20160327+gitc3e6904/ycmd/utils.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/utils.py 2016-12-20 08:50:19.000000000 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Copyright (C) 2011, 2012 Google Inc. # # This file is part of ycmd. @@ -24,28 +26,19 @@ from builtins import * # noqa from future.utils import PY2, native -import tempfile import os -import sys -import signal import socket -import stat import subprocess +import sys +import tempfile +import time # Creation flag to disable creating a console window on Windows. See # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx CREATE_NO_WINDOW = 0x08000000 -# Executable extensions used on Windows -WIN_EXECUTABLE_EXTS = [ '.exe', '.bat', '.cmd' ] -# Don't use this! Call PathToCreatedTempDir() instead. This exists for the sake -# of tests. -RAW_PATH_TO_TEMP_DIR = os.path.join( tempfile.gettempdir(), 'ycm_temp' ) - -# Readable, writable and executable by everyone. -ACCESSIBLE_TO_ALL_MASK = ( stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH | - stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP ) +EXECUTABLE_FILE_MASK = os.F_OK | os.X_OK # Python 3 complains on the common open(path).read() idiom because the file @@ -63,7 +56,19 @@ # (we're replacing sys.stdout!) with an `str` object on py2 will cause # tracebacks because text mode insists on unicode objects. (Don't forget, # `open` is actually `io.open` because of future builtins.) - return open( filepath, 'wb' if PY2 else 'w' ) + # Since this function is used for logging purposes, we don't want the output + # to be delayed. This means no buffering for binary mode and line buffering + # for text mode. See https://docs.python.org/2/library/io.html#io.open + if PY2: + return open( filepath, mode = 'wb', buffering = 0 ) + return open( filepath, mode = 'w', buffering = 1 ) + + +def CreateLogfile( prefix = '' ): + with tempfile.NamedTemporaryFile( prefix = prefix, + suffix = '.log', + delete = False ) as logfile: + return logfile.name # Given an object, returns a str object that's utf-8 encoded. This is meant to @@ -90,6 +95,22 @@ return str( value ) +# When lines is an iterable of all strings or all bytes, equivalent to +# '\n'.join( ToUnicode( lines ) ) +# but faster on large inputs. +def JoinLinesAsUnicode( lines ): + try: + first = next( iter( lines ) ) + except StopIteration: + return str() + + if isinstance( first, str ): + return ToUnicode( '\n'.join( lines ) ) + if isinstance( first, bytes ): + return ToUnicode( b'\n'.join( lines ) ) + raise ValueError( 'lines must contain either strings or bytes.' ) + + # Consistently returns the new bytes() type from python-future. Assumes incoming # strings are either UTF-8 or unicode (which is converted to UTF-8). def ToBytes( value ): @@ -129,22 +150,34 @@ return ToBytes( str( value ) ) -def PathToCreatedTempDir( tempdir = RAW_PATH_TO_TEMP_DIR ): - try: - os.makedirs( tempdir ) - # Needed to support multiple users working on the same machine; - # see issue 606. - MakeFolderAccessibleToAll( tempdir ) - except OSError: - # Folder already exists, skip folder creation. - pass - return tempdir - - -def MakeFolderAccessibleToAll( path_to_folder ): - current_stat = os.stat( path_to_folder ) - flags = current_stat.st_mode | ACCESSIBLE_TO_ALL_MASK - os.chmod( path_to_folder, flags ) +def ByteOffsetToCodepointOffset( line_value, byte_offset ): + """The API calls for byte offsets into the UTF-8 encoded version of the + buffer. However, ycmd internally uses unicode strings. This means that + when we need to walk 'characters' within the buffer, such as when checking + for semantic triggers and similar, we must use codepoint offets, rather than + byte offsets. + + This method converts the |byte_offset|, which is a utf-8 byte offset, into + a codepoint offset in the unicode string |line_value|.""" + + byte_line_value = ToBytes( line_value ) + return len( ToUnicode( byte_line_value[ : byte_offset - 1 ] ) ) + 1 + + +def CodepointOffsetToByteOffset( unicode_line_value, codepoint_offset ): + """The API calls for byte offsets into the UTF-8 encoded version of the + buffer. However, ycmd internally uses unicode strings. This means that + when we need to walk 'characters' within the buffer, such as when checking + for semantic triggers and similar, we must use codepoint offets, rather than + byte offsets. + + This method converts the |codepoint_offset| which is a unicode codepoint + offset into an byte offset into the utf-8 encoded bytes version of + |unicode_line_value|.""" + + # Should be a no-op, but in case someone passes a bytes instance. + unicode_line_value = ToUnicode( unicode_line_value ) + return len( ToBytes( unicode_line_value[ : codepoint_offset - 1 ] ) ) + 1 def GetUnusedLocalhostPort(): @@ -171,27 +204,56 @@ return None -# On Windows, distutils.spawn.find_executable only works for .exe files -# but .bat and .cmd files are also executables, so we use our own -# implementation. +def _GetWindowsExecutable( filename ): + def _GetPossibleWindowsExecutable( filename ): + pathext = [ ext.lower() for ext in + os.environ.get( 'PATHEXT', '' ).split( os.pathsep ) ] + base, extension = os.path.splitext( filename ) + if extension.lower() in pathext: + return [ filename ] + else: + return [ base + ext for ext in pathext ] + + for exe in _GetPossibleWindowsExecutable( filename ): + if os.path.isfile( exe ): + return exe + return None + + +# Check that a given file can be accessed as an executable file, so controlling +# the access mask on Unix and if has a valid extension on Windows. It returns +# the path to the executable or None if no executable was found. +def GetExecutable( filename ): + if OnWindows(): + return _GetWindowsExecutable( filename ) + + if ( os.path.isfile( filename ) + and os.access( filename, EXECUTABLE_FILE_MASK ) ): + return filename + return None + + +# Adapted from https://hg.python.org/cpython/file/3.5/Lib/shutil.py#l1081 +# to be backward compatible with Python2 and more consistent to our codebase. def FindExecutable( executable ): - paths = os.environ[ 'PATH' ].split( os.pathsep ) - base, extension = os.path.splitext( executable ) + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname( executable ): + return GetExecutable( executable ) - if OnWindows() and extension.lower() not in WIN_EXECUTABLE_EXTS: - extensions = WIN_EXECUTABLE_EXTS - else: - extensions = [''] + paths = os.environ[ 'PATH' ].split( os.pathsep ) - for extension in extensions: - executable_name = executable + extension - if not os.path.isfile( executable_name ): - for path in paths: - executable_path = os.path.join(path, executable_name ) - if os.path.isfile( executable_path ): - return executable_path - else: - return executable_name + if OnWindows(): + # The current directory takes precedence on Windows. + curdir = os.path.abspath( os.curdir ) + if curdir not in paths: + paths.insert( 0, curdir ) + + for path in paths: + exe = GetExecutable( os.path.join( path, executable ) ) + if exe: + return exe return None @@ -211,26 +273,27 @@ return sys.platform == 'darwin' -def OnTravis(): - return 'TRAVIS' in os.environ - - def ProcessIsRunning( handle ): return handle is not None and handle.poll() is None -# From here: http://stackoverflow.com/a/8536476/1672783 -def TerminateProcess( pid ): - if OnWindows(): - import ctypes - PROCESS_TERMINATE = 1 - handle = ctypes.windll.kernel32.OpenProcess( PROCESS_TERMINATE, - False, - pid ) - ctypes.windll.kernel32.TerminateProcess( handle, -1 ) - ctypes.windll.kernel32.CloseHandle( handle ) - else: - os.kill( pid, signal.SIGTERM ) +def WaitUntilProcessIsTerminated( handle, timeout = 5 ): + expiration = time.time() + timeout + while True: + if time.time() > expiration: + raise RuntimeError( 'Waited process to terminate for {0} seconds, ' + 'aborting.'.format( timeout ) ) + if not ProcessIsRunning( handle ): + return + time.sleep( 0.1 ) + + +def CloseStandardStreams( handle ): + if not handle: + return + for stream in [ handle.stdin, handle.stdout, handle.stderr ]: + if stream: + stream.close() def PathsToAllParentFolders( path ): @@ -327,3 +390,60 @@ else: import importlib return importlib.machinery.SourceFileLoader( name, pathname ).load_module() + + +def SplitLines( contents ): + """Return a list of each of the lines in the unicode string |contents|. + Behaviour is equivalent to str.splitlines with the following exceptions: + - empty strings are returned as [ '' ] + - a trailing newline is not ignored (i.e. SplitLines( '\n' ) + returns [ '', '' ], not [ '' ]""" + + # We often want to get a list representation of a buffer such that we can + # index all of the 'lines' within it. Python provides str.splitlines for this + # purpose, but its documented behaviors for empty strings and strings ending + # with a newline character are not compatible with this. As a result, we write + # our own wrapper to provide a splitlines implementation which returns the + # actual list of indexable lines in a buffer, where a line may have 0 + # characters. + # + # NOTE: str.split( '\n' ) actually gives this behaviour, except it does not + # work when running on a unix-like system and reading a file with Windows line + # endings. + + # ''.splitlines() returns [], but we want [ '' ] + if contents == '': + return [ '' ] + + lines = contents.splitlines() + + # '\n'.splitlines() returns [ '' ]. We want [ '', '' ]. + # '\n\n\n'.splitlines() returns [ '', '', '' ]. We want [ '', '', '', '' ]. + # + # So we re-instate the empty entry at the end if the original string ends + # with a newline. Universal newlines recognise the following as + # line-terminators: + # - '\n' + # - '\r\n' + # - '\r' + # + # Importantly, notice that \r\n also ends with \n + # + if contents.endswith( '\r' ) or contents.endswith( '\n' ): + lines.append( '' ) + + return lines + + +def GetCurrentDirectory(): + """Returns the current directory as an unicode object. If the current + directory does not exist anymore, returns the temporary folder instead.""" + try: + if PY2: + return os.getcwdu() + return os.getcwd() + # os.getcwdu throws an OSError exception when the current directory has been + # deleted while os.getcwd throws a FileNotFoundError, which is a subclass of + # OSError. + except OSError: + return tempfile.gettempdir() diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/watchdog_plugin.py ycmd-0+20161219+git486b809/ycmd/watchdog_plugin.py --- ycmd-0+20160327+gitc3e6904/ycmd/watchdog_plugin.py 2016-04-01 09:03:25.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/watchdog_plugin.py 2016-12-20 08:50:19.000000000 +0000 @@ -24,10 +24,12 @@ from builtins import * # noqa import time -import os import copy -from ycmd import utils +import logging from threading import Thread, Lock +from ycmd.handlers import ServerShutdown + +_logger = logging.getLogger( __name__ ) # This class implements the Bottle plugin API: @@ -47,7 +49,7 @@ def __init__( self, idle_suicide_seconds, - check_interval_seconds = 60 * 10 ): + check_interval_seconds ): self._check_interval_seconds = check_interval_seconds self._idle_suicide_seconds = idle_suicide_seconds @@ -95,7 +97,8 @@ # wait interval to contact us before we die. if (self._TimeSinceLastRequest() > self._idle_suicide_seconds and self._TimeSinceLastWakeup() < 2 * self._check_interval_seconds): - utils.TerminateProcess( os.getpid() ) + _logger.info( 'Shutting down server due to inactivity' ) + ServerShutdown() self._UpdateLastWakeupTime() diff -Nru ycmd-0+20160327+gitc3e6904/ycmd/wsgi_server.py ycmd-0+20161219+git486b809/ycmd/wsgi_server.py --- ycmd-0+20160327+gitc3e6904/ycmd/wsgi_server.py 1970-01-01 00:00:00.000000000 +0000 +++ ycmd-0+20161219+git486b809/ycmd/wsgi_server.py 2016-12-20 08:50:19.000000000 +0000 @@ -0,0 +1,67 @@ +# 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 __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 future.utils import listvalues +from waitress.server import TcpWSGIServer +import select + + +class StoppableWSGIServer( TcpWSGIServer ): + """StoppableWSGIServer is a subclass of the TcpWSGIServer Waitress server + with a shutdown method. It is based on StopableWSGIServer class from webtest: + https://github.com/Pylons/webtest/blob/master/webtest/http.py""" + + shutdown_requested = False + + def Run( self ): + """Wrapper of TcpWSGIServer run method. It prevents a traceback from + asyncore.""" + + # Message for compatibility with clients who expect the output from + # waitress.serve here + print( 'serving on http://{0}:{1}'.format( + self.effective_host, + self.effective_port ) ) + + try: + self.run() + except select.error: + if not self.shutdown_requested: + raise + + + def Shutdown( self ): + """Properly shutdown the server.""" + self.shutdown_requested = True + # Shutdown waitress threads. + self.task_dispatcher.shutdown() + # Close asyncore channels. + # We don't use itervalues here because _map is modified while looping + # through it. + # NOTE: _map is an attribute from the asyncore.dispatcher class, which is a + # base class of TcpWSGIServer. This may change in future versions of + # waitress so extra care should be taken when updating waitress. + for channel in listvalues( self._map ): + channel.close()