diff --git a/python/ycm/tests/__init__.py b/python/ycm/tests/__init__.py index 6cb7ad54..22417b82 100644 --- a/python/ycm/tests/__init__.py +++ b/python/ycm/tests/__init__.py @@ -27,6 +27,7 @@ from ycm.tests.test_utils import MockVimModule MockVimModule() import functools +import os import requests import time @@ -48,6 +49,11 @@ DEFAULT_CLIENT_OPTIONS = { } +def PathToTestFile( *args ): + dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) ) + return os.path.join( dir_of_current_script, 'testdata', *args ) + + def _MakeUserOptions( custom_options = {} ): options = dict( user_options_store.DefaultOptions() ) options.update( DEFAULT_CLIENT_OPTIONS ) diff --git a/python/ycm/tests/completion_test.py b/python/ycm/tests/completion_test.py new file mode 100644 index 00000000..45b5ef51 --- /dev/null +++ b/python/ycm/tests/completion_test.py @@ -0,0 +1,54 @@ +# coding: utf-8 +# +# Copyright (C) 2016 YouCompleteMe contributors +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe 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. +# +# YouCompleteMe 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 YouCompleteMe. 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 ycm.tests.test_utils import ( CurrentWorkingDirectory, MockVimModule, + MockVimBuffers, VimBuffer ) +MockVimModule() + +from hamcrest import assert_that, empty, has_entries + +from ycm.tests import PathToTestFile, YouCompleteMeInstance + + +@YouCompleteMeInstance() +def CreateCompletionRequest_UnicodeWorkingDirectory_test( ycm ): + unicode_dir = PathToTestFile( 'uni¢𐍈d€' ) + current_buffer = VimBuffer( PathToTestFile( 'uni¢𐍈d€', 'current_buffer' ) ) + + with CurrentWorkingDirectory( unicode_dir ): + with MockVimBuffers( [ current_buffer ], current_buffer, ( 5, 2 ) ): + ycm.CreateCompletionRequest(), + + results = ycm.GetCompletions() + + assert_that( + results, + has_entries( { + 'words': empty(), + 'refresh': 'always' + } ) + ) diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py index e86339f4..d12892eb 100644 --- a/python/ycm/tests/event_notification_test.py +++ b/python/ycm/tests/event_notification_test.py @@ -1,3 +1,5 @@ +# coding: utf-8 +# # Copyright (C) 2015-2016 YouCompleteMe contributors # # This file is part of YouCompleteMe. @@ -23,17 +25,18 @@ from future import standard_library standard_library.install_aliases() from builtins import * # noqa -from ycm.tests.test_utils import ExtendedMock, MockVimModule, VimBuffer +from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock, + MockVimBuffers, MockVimModule, VimBuffer ) MockVimModule() import contextlib import os -from ycm.tests import YouCompleteMeInstance +from ycm.tests import PathToTestFile, YouCompleteMeInstance from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range, UnknownExtraConf, ServerError ) -from hamcrest import assert_that, contains, has_entries +from hamcrest import assert_that, contains, has_entries, has_item from mock import call, MagicMock, patch from nose.tools import eq_, ok_ @@ -56,23 +59,17 @@ def UnplaceSign_Call( sign_id, buffer_num ): @contextlib.contextmanager -def MockArbitraryBuffer( filetype, native_available = True ): - """Used via the with statement, set up mocked versions of the vim module such - that a single buffer is open with an arbitrary name and arbirary contents. Its - filetype is set to the supplied filetype""" +def MockArbitraryBuffer( filetype ): + """Used via the with statement, set up a single buffer with an arbitrary name + and no contents. Its filetype is set to the supplied filetype.""" # Arbitrary, but valid, single buffer open. current_buffer = VimBuffer( os.path.realpath( 'TEST_BUFFER' ), window = 1, filetype = filetype ) - # The rest just mock up the Vim module so that our single arbitrary buffer - # makes sense to vimsupport module. - with patch( 'vim.buffers', [ current_buffer ] ): - with patch( 'vim.current.buffer', current_buffer ): - # Arbitrary but valid cursor position. - with patch( 'vim.current.window.cursor', ( 1, 2 ) ): - yield + with MockVimBuffers( [ current_buffer ], current_buffer ): + yield @contextlib.contextmanager @@ -352,6 +349,44 @@ def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ): eq_( ycm.GetWarningCount(), 0 ) +@patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' ) +@YouCompleteMeInstance( { 'collect_identifiers_from_tags_files': 1 } ) +def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test( + ycm, *args ): + unicode_dir = PathToTestFile( 'uni¢𐍈d€' ) + current_buffer_file = PathToTestFile( 'uni¢𐍈d€', 'current_buffer' ) + current_buffer = VimBuffer( name = current_buffer_file, + contents = [ 'current_buffer_contents' ], + filetype = 'some_filetype' ) + + with patch( 'ycm.client.base_request.BaseRequest.' + 'PostDataToHandlerAsync' ) as post_data_to_handler_async: + with CurrentWorkingDirectory( unicode_dir ): + with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ): + ycm.OnFileReadyToParse() + + assert_that( + # Positional arguments passed to PostDataToHandlerAsync. + post_data_to_handler_async.call_args[ 0 ], + contains( + has_entries( { + 'filepath': current_buffer_file, + 'line_num': 6, + 'column_num': 6, + 'file_data': has_entries( { + current_buffer_file: has_entries( { + 'contents': 'current_buffer_contents\n', + 'filetypes': [ 'some_filetype' ] + } ) + } ), + 'event_name': 'FileReadyToParse', + 'tag_files': has_item( PathToTestFile( 'uni¢𐍈d€', 'tags' ) ) + } ), + 'event_notification' + ) + ) + + @patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' ) @YouCompleteMeInstance() def EventNotification_BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test( @@ -380,12 +415,10 @@ def EventNotification_BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test( with patch( 'ycm.client.base_request.BaseRequest.' 'PostDataToHandlerAsync' ) as post_data_to_handler_async: - with patch( 'vim.buffers', [ current_buffer, - modified_buffer, - unmodified_buffer ] ): - with patch( 'vim.current.buffer', current_buffer ): - with patch( 'vim.current.window.cursor', ( 3, 5 ) ): - ycm.OnBufferVisit() + with MockVimBuffers( [ current_buffer, modified_buffer, unmodified_buffer ], + current_buffer, + ( 3, 5 ) ): + ycm.OnBufferVisit() assert_that( # Positional arguments passed to PostDataToHandlerAsync. @@ -431,9 +464,8 @@ def EventNotification_BufferUnload_BuildRequestForDeletedAndUnsavedBuffers_test( with patch( 'ycm.client.base_request.BaseRequest.' 'PostDataToHandlerAsync' ) as post_data_to_handler_async: - with patch( 'vim.buffers', [ current_buffer, deleted_buffer ] ): - with patch( 'vim.current.buffer', current_buffer ): - ycm.OnBufferUnload( deleted_buffer_file ) + with MockVimBuffers( [ current_buffer, deleted_buffer ], current_buffer ): + ycm.OnBufferUnload( deleted_buffer_file ) assert_that( # Positional arguments passed to PostDataToHandlerAsync. diff --git a/python/ycm/tests/test_utils.py b/python/ycm/tests/test_utils.py index fe07d5e7..d1733dbd 100644 --- a/python/ycm/tests/test_utils.py +++ b/python/ycm/tests/test_utils.py @@ -24,14 +24,16 @@ from future import standard_library standard_library.install_aliases() from builtins import * # noqa -from mock import MagicMock +from mock import MagicMock, patch from hamcrest import assert_that, equal_to +import contextlib +import functools +import nose +import os import re import sys -import nose -import functools -from ycmd.utils import ToUnicode +from ycmd.utils import GetCurrentDirectory, ToUnicode BUFNR_REGEX = re.compile( '^bufnr\(\'(?P.+)\', ([01])\)$' ) @@ -52,21 +54,31 @@ GETBUFVAR_REGEX = re.compile( VIM_MOCK = MagicMock() -def MockGetBufferNumber( buffer_filename ): +@contextlib.contextmanager +def CurrentWorkingDirectory( path ): + old_cwd = GetCurrentDirectory() + os.chdir( path ) + try: + yield + finally: + os.chdir( old_cwd ) + + +def _MockGetBufferNumber( buffer_filename ): for vim_buffer in VIM_MOCK.buffers: if vim_buffer.name == buffer_filename: return vim_buffer.number return -1 -def MockGetBufferWindowNumber( buffer_number ): +def _MockGetBufferWindowNumber( buffer_number ): for vim_buffer in VIM_MOCK.buffers: if vim_buffer.number == buffer_number and vim_buffer.window: return vim_buffer.window return -1 -def MockGetBufferVariable( buffer_number, option ): +def _MockGetBufferVariable( buffer_number, option ): for vim_buffer in VIM_MOCK.buffers: if vim_buffer.number == buffer_number: if option == 'mod': @@ -77,20 +89,7 @@ def MockGetBufferVariable( buffer_number, option ): return '' -def MockVimEval( value ): - if value == 'g:ycm_min_num_of_chars_for_completion': - return 0 - - if value == 'g:ycm_server_python_interpreter': - return '' - - if value == 'tempname()': - return '_TEMP_FILE_' - - if value == '&previewheight': - # Default value from Vim - return 12 - +def _MockVimBufferEval( value ): if value == '&omnifunc': return VIM_MOCK.current.buffer.omnifunc @@ -100,23 +99,66 @@ def MockVimEval( value ): match = BUFNR_REGEX.search( value ) if match: buffer_filename = match.group( 'buffer_filename' ) - return MockGetBufferNumber( buffer_filename ) + return _MockGetBufferNumber( buffer_filename ) match = BUFWINNR_REGEX.search( value ) if match: buffer_number = int( match.group( 'buffer_number' ) ) - return MockGetBufferWindowNumber( buffer_number ) + return _MockGetBufferWindowNumber( buffer_number ) match = GETBUFVAR_REGEX.search( value ) if match: buffer_number = int( match.group( 'buffer_number' ) ) option = match.group( 'option' ) - return MockGetBufferVariable( buffer_number, option ) + return _MockGetBufferVariable( buffer_number, option ) + + return None + + +def _MockVimOptionsEval( value ): + if value == '&previewheight': + return 12 + + if value == '&columns': + return 80 + + if value == '&ruler': + return 0 + + if value == '&showcmd': + return 1 + + return None + + +def _MockVimEval( value ): + if value == 'g:ycm_min_num_of_chars_for_completion': + return 0 + + if value == 'g:ycm_server_python_interpreter': + return '' + + if value == 'tempname()': + return '_TEMP_FILE_' + + if value == 'complete_check()': + return 0 + + if value == 'tagfiles()': + return [ 'tags' ] + + result = _MockVimOptionsEval( value ) + if result is not None: + return result + + result = _MockVimBufferEval( value ) + if result is not None: + return result raise ValueError( 'Unexpected evaluation: {0}'.format( value ) ) -def MockWipeoutBuffer( buffer_number ): +def _MockWipeoutBuffer( buffer_number ): buffers = VIM_MOCK.buffers for index, buffer in enumerate( buffers ): @@ -127,7 +169,7 @@ def MockWipeoutBuffer( buffer_number ): def MockVimCommand( command ): match = BWIPEOUT_REGEX.search( command ) if match: - return MockWipeoutBuffer( int( match.group( 1 ) ) ) + return _MockWipeoutBuffer( int( match.group( 1 ) ) ) raise RuntimeError( 'Unexpected command: ' + command ) @@ -159,7 +201,7 @@ class VimBuffer( object ): def __getitem__( self, index ): - """Return the bytes for a given line at index |index|.""" + """Returns the bytes for a given line at index |index|.""" return self.contents[ index ] @@ -172,10 +214,24 @@ class VimBuffer( object ): def GetLines( self ): - """Return the contents of the buffer as a list of unicode strings.""" + """Returns the contents of the buffer as a list of unicode strings.""" return [ ToUnicode( x ) for x in self.contents ] +@contextlib.contextmanager +def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ) ): + """Simulates the Vim buffers list |buffers| where |current_buffer| is the + buffer displayed in the current window and |cursor_position| is the current + cursor position. All buffers are represented by a VimBuffer object.""" + if current_buffer not in buffers: + raise RuntimeError( 'Current buffer must be part of the buffers list.' ) + + with patch( 'vim.buffers', buffers ): + with patch( 'vim.current.buffer', current_buffer ): + with patch( 'vim.current.window.cursor', cursor_position ): + yield + + def MockVimModule(): """The 'vim' module is something that is only present when running inside the Vim Python interpreter, so we replace it with a MagicMock for tests. If you @@ -199,7 +255,7 @@ def MockVimModule(): tests.""" VIM_MOCK.buffers = {} - VIM_MOCK.eval = MagicMock( side_effect = MockVimEval ) + VIM_MOCK.eval = MagicMock( side_effect = _MockVimEval ) sys.modules[ 'vim' ] = VIM_MOCK return VIM_MOCK diff --git a/python/ycm/tests/testdata/uni¢𐍈d€/tags b/python/ycm/tests/testdata/uni¢𐍈d€/tags new file mode 100644 index 00000000..e69de29b diff --git a/python/ycm/tests/vimsupport_test.py b/python/ycm/tests/vimsupport_test.py index c740c2bf..a0727f04 100644 --- a/python/ycm/tests/vimsupport_test.py +++ b/python/ycm/tests/vimsupport_test.py @@ -25,13 +25,14 @@ from future import standard_library standard_library.install_aliases() from builtins import * # noqa -from ycm.tests.test_utils import ( ExtendedMock, MockVimCommand, VimBuffer, - MockVimModule ) +from ycm.tests import PathToTestFile +from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock, + MockVimCommand, MockVimModule, VimBuffer ) MockVimModule() from ycm import vimsupport from nose.tools import eq_ -from hamcrest import assert_that, calling, raises, none, has_entry +from hamcrest import assert_that, calling, equal_to, has_entry, none, raises from mock import MagicMock, call, patch from ycmd.utils import ToBytes import os @@ -1418,6 +1419,14 @@ def GetUnsavedAndSpecifiedBufferData_EncodedUnicodeCharsInBuffers_test(): has_entry( u'contents', u'abc\nfДa\n' ) ) ) +def GetBufferFilepath_NoBufferName_UnicodeWorkingDirectory_test(): + vim_buffer = VimBuffer( '', number = 42 ) + unicode_dir = PathToTestFile( u'uni¢𐍈d€' ) + with CurrentWorkingDirectory( unicode_dir ): + assert_that( vimsupport.GetBufferFilepath( vim_buffer ), + equal_to( os.path.join( unicode_dir, '42' ) ) ) + + # NOTE: Vim returns byte offsets for columns, not actual character columns. This # makes 'ДД' have 4 columns: column 0, column 2 and column 4. @patch( 'vim.current.line', ToBytes( 'ДДaa' ) ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index c50641e0..4c8564d3 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -1,4 +1,5 @@ -# Copyright (C) 2011, 2012 Google Inc. +# Copyright (C) 2011-2012 Google Inc. +# 2016 YouCompleteMe contributors # # This file is part of YouCompleteMe. # @@ -26,11 +27,11 @@ from builtins import * # noqa from future.utils import iterkeys import vim import os -import tempfile import json import re from collections import defaultdict -from ycmd.utils import ToUnicode, ToBytes, JoinLinesAsUnicode +from ycmd.utils import ( GetCurrentDirectory, JoinLinesAsUnicode, ToBytes, + ToUnicode ) from ycmd import user_options_store BUFFER_COMMAND_MAP = { 'same-buffer' : 'edit', @@ -156,13 +157,8 @@ def GetBufferFilepath( buffer_object ): if buffer_object.name: return buffer_object.name # Buffers that have just been created by a command like :enew don't have any - # buffer name so we use the buffer number for that. Also, os.getcwd() throws - # an exception when the CWD has been deleted so we handle that. - try: - folder_path = os.getcwd() - except OSError: - folder_path = tempfile.gettempdir() - return os.path.join( folder_path, str( buffer_object.number ) ) + # buffer name so we use the buffer number for that. + return os.path.join( GetCurrentDirectory(), str( buffer_object.number ) ) def UnplaceSignInBuffer( buffer_number, sign_id ): diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 461fcb3a..6acfe520 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -1,4 +1,5 @@ -# Copyright (C) 2011, 2012 Google Inc. +# Copyright (C) 2011-2012 Google Inc. +# 2016 YouCompleteMe contributors # # This file is part of YouCompleteMe. # @@ -225,7 +226,7 @@ class YouCompleteMe( object ): self._omnicomp, wrapped_request_data ) return self._latest_completion_request - request_data[ 'working_dir' ] = os.getcwd() + request_data[ 'working_dir' ] = utils.GetCurrentDirectory() self._AddExtraConfDataIfNeeded( request_data ) if force_semantic: @@ -677,12 +678,8 @@ class YouCompleteMe( object ): def _AddTagsFilesIfNeeded( self, extra_data ): def GetTagFiles(): tag_files = vim.eval( 'tagfiles()' ) - # getcwd() throws an exception when the CWD has been deleted. - try: - current_working_directory = os.getcwd() - except OSError: - return [] - return [ os.path.join( current_working_directory, x ) for x in tag_files ] + return [ os.path.join( utils.GetCurrentDirectory(), tag_file ) + for tag_file in tag_files ] if not self._user_options[ 'collect_identifiers_from_tags_files' ]: return diff --git a/third_party/ycmd b/third_party/ycmd index f3232ce5..63c3d992 160000 --- a/third_party/ycmd +++ b/third_party/ycmd @@ -1 +1 @@ -Subproject commit f3232ce5180753822d839c80e9b9ea4e33e89d4c +Subproject commit 63c3d992a2db8d189cd78a25a70c87348726fc52