123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608 |
- # Copyright (C) 2011-2018 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 <http://www.gnu.org/licenses/>.
- from __future__ import unicode_literals
- from __future__ import print_function
- from __future__ import division
- from __future__ import absolute_import
- # Not installing aliases from python-future; it's unreliable and slow.
- from builtins import * # noqa
- from future.utils import PY2
- from mock import DEFAULT, MagicMock, patch
- from hamcrest import assert_that, equal_to
- import contextlib
- import functools
- import nose
- import os
- import re
- import sys
- from ycmd.utils import GetCurrentDirectory, ToBytes, ToUnicode
- BUFNR_REGEX = re.compile( '^bufnr\(\'(?P<buffer_filename>.+)\', ([01])\)$' )
- BUFWINNR_REGEX = re.compile( '^bufwinnr\((?P<buffer_number>[0-9]+)\)$' )
- BWIPEOUT_REGEX = re.compile(
- '^(?:silent! )bwipeout!? (?P<buffer_number>[0-9]+)$' )
- GETBUFVAR_REGEX = re.compile(
- '^getbufvar\((?P<buffer_number>[0-9]+), "(?P<option>.+)"\)$' )
- MATCHADD_REGEX = re.compile(
- '^matchadd\(\'(?P<group>.+)\', \'(?P<pattern>.+)\'\)$' )
- MATCHDELETE_REGEX = re.compile( '^matchdelete\((?P<id>\d+)\)$' )
- OMNIFUNC_REGEX_FORMAT = (
- '^{omnifunc_name}\((?P<findstart>[01]),[\'"](?P<base>.*)[\'"]\)$' )
- FNAMEESCAPE_REGEX = re.compile( '^fnameescape\(\'(?P<filepath>.+)\'\)$' )
- SIGN_LIST_REGEX = re.compile(
- "^silent sign place buffer=(?P<bufnr>\d+)$" )
- SIGN_PLACE_REGEX = re.compile(
- '^sign place (?P<id>\d+) name=(?P<name>\w+) line=(?P<line>\d+) '
- 'buffer=(?P<bufnr>\d+)$' )
- SIGN_UNPLACE_REGEX = re.compile(
- '^sign unplace (?P<id>\d+) buffer=(?P<bufnr>\d+)$' )
- REDIR_START_REGEX = re.compile( '^redir => (?P<variable>[\w:]+)$' )
- REDIR_END_REGEX = re.compile( '^redir END$' )
- # One-and only instance of mocked Vim object. The first 'import vim' that is
- # executed binds the vim module to the instance of MagicMock that is created,
- # and subsquent assignments to sys.modules[ 'vim' ] don't retrospectively
- # update them. The result is that while running the tests, we must assign only
- # one instance of MagicMock to sys.modules[ 'vim' ] and always return it.
- #
- # More explanation is available:
- # https://github.com/Valloric/YouCompleteMe/pull/1694
- VIM_MOCK = MagicMock()
- VIM_MATCHES = []
- VIM_SIGNS = []
- REDIR = {
- 'status': False,
- 'variable': '',
- 'output': ''
- }
- @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 ):
- 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 ):
- for vim_buffer in VIM_MOCK.buffers:
- if vim_buffer.number == buffer_number:
- if option == '&mod':
- return vim_buffer.modified
- if option == '&ft':
- return vim_buffer.filetype
- if option == 'changedtick':
- return vim_buffer.changedtick
- if option == '&bh':
- return vim_buffer.bufhidden
- return ''
- return ''
- def _MockVimBufferEval( value ):
- if value == '&omnifunc':
- return VIM_MOCK.current.buffer.omnifunc_name
- if value == '&filetype':
- return VIM_MOCK.current.buffer.filetype
- match = BUFNR_REGEX.search( value )
- if match:
- buffer_filename = match.group( 'buffer_filename' )
- return _MockGetBufferNumber( buffer_filename )
- match = BUFWINNR_REGEX.search( value )
- if match:
- buffer_number = int( match.group( '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 )
- current_buffer = VIM_MOCK.current.buffer
- match = re.search( OMNIFUNC_REGEX_FORMAT.format(
- omnifunc_name = current_buffer.omnifunc_name ),
- value )
- if match:
- findstart = int( match.group( 'findstart' ) )
- base = match.group( 'base' )
- value = current_buffer.omnifunc( findstart, base )
- return value if findstart else ToBytesOnPY2( value )
- 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
- if value == '&hidden':
- return 0
- if value == '&expandtab':
- return 1
- return None
- def _MockVimFunctionsEval( value ):
- if value == 'tempname()':
- return '_TEMP_FILE_'
- if value == 'tagfiles()':
- return [ 'tags' ]
- if value == 'shiftwidth()':
- return 2
- return None
- def _MockVimMatchEval( value ):
- if value == 'getmatches()':
- # Returning a copy, because ClearYcmSyntaxMatches() gets the result of
- # getmatches(), iterates over it and removes elements from VIM_MATCHES.
- return list( VIM_MATCHES )
- match = MATCHADD_REGEX.search( value )
- if match:
- group = match.group( 'group' )
- option = match.group( 'pattern' )
- vim_match = VimMatch( group, option )
- VIM_MATCHES.append( vim_match )
- return vim_match.id
- match = MATCHDELETE_REGEX.search( value )
- if match:
- identity = int( match.group( 'id' ) )
- for index, vim_match in enumerate( VIM_MATCHES ):
- if vim_match.id == identity:
- VIM_MATCHES.pop( index )
- return -1
- return 0
- return None
- # This variable exists to easily mock the 'g:ycm_server_python_interpreter'
- # option in tests.
- server_python_interpreter = ''
- def _MockVimEval( value ):
- if value == 'g:ycm_min_num_of_chars_for_completion':
- return 0
- if value == 'g:ycm_server_python_interpreter':
- return server_python_interpreter
- result = _MockVimFunctionsEval( value )
- if result is not None:
- return result
- result = _MockVimOptionsEval( value )
- if result is not None:
- return result
- result = _MockVimBufferEval( value )
- if result is not None:
- return result
- result = _MockVimMatchEval( value )
- if result is not None:
- return result
- match = FNAMEESCAPE_REGEX.search( value )
- if match:
- return match.group( 'filepath' )
- if value == REDIR[ 'variable' ]:
- return REDIR[ 'output' ]
- raise VimError( 'Unexpected evaluation: {0}'.format( value ) )
- def _MockWipeoutBuffer( buffer_number ):
- buffers = VIM_MOCK.buffers
- for index, buffer in enumerate( buffers ):
- if buffer.number == buffer_number:
- return buffers.pop( index )
- def _MockSignCommand( command ):
- match = SIGN_LIST_REGEX.search( command )
- if match and REDIR[ 'status' ]:
- bufnr = int( match.group( 'bufnr' ) )
- REDIR[ 'output' ] = ( '--- Signs ---\n'
- 'Signs for foo:\n' )
- for sign in VIM_SIGNS:
- if sign.bufnr == bufnr:
- REDIR[ 'output' ] += (
- ' line={0} id={1} name={2}'.format( sign.line,
- sign.id,
- sign.name ) )
- return True
- match = SIGN_PLACE_REGEX.search( command )
- if match:
- VIM_SIGNS.append( VimSign( int( match.group( 'id' ) ),
- int( match.group( 'line' ) ),
- match.group( 'name' ),
- int( match.group( 'bufnr' ) ) ) )
- return True
- match = SIGN_UNPLACE_REGEX.search( command )
- if match:
- sign_id = int( match.group( 'id' ) )
- bufnr = int( match.group( 'bufnr' ) )
- for sign in VIM_SIGNS:
- if sign.id == sign_id and sign.bufnr == bufnr:
- VIM_SIGNS.remove( sign )
- return True
- return False
- def _MockVimCommand( command ):
- match = BWIPEOUT_REGEX.search( command )
- if match:
- return _MockWipeoutBuffer( int( match.group( 1 ) ) )
- match = REDIR_START_REGEX.search( command )
- if match:
- REDIR[ 'status' ] = True
- REDIR[ 'variable' ] = match.group( 'variable' )
- return
- match = REDIR_END_REGEX.search( command )
- if match:
- REDIR[ 'status' ] = False
- return
- if command == 'unlet ' + REDIR[ 'variable' ]:
- REDIR[ 'variable' ] = ''
- return
- result = _MockSignCommand( command )
- if result:
- return
- return DEFAULT
- class VimBuffer( object ):
- """An object that looks like a vim.buffer object:
- - |name| : full path of the buffer with symbolic links resolved;
- - |number| : buffer number;
- - |contents| : list of lines representing the buffer contents;
- - |filetype| : buffer filetype. Empty string if no filetype is set;
- - |modified| : True if the buffer has unsaved changes, False otherwise;
- - |bufhidden|: value of the 'bufhidden' option (see :h bufhidden);
- - |window| : number of the buffer window. None if the buffer is hidden;
- - |omnifunc| : omni completion function used by the buffer. Must be a Python
- function that takes the same arguments and returns the same
- values as a Vim completion function (:h complete-functions).
- Example:
- def Omnifunc( findstart, base ):
- if findstart:
- return 5
- return [ 'a', 'b', 'c' ]"""
- def __init__( self, name,
- number = 1,
- contents = [ '' ],
- filetype = '',
- modified = False,
- bufhidden = '',
- window = None,
- omnifunc = None,
- visual_start = None,
- visual_end = None ):
- self.name = os.path.realpath( name ) if name else ''
- self.number = number
- self.contents = contents
- self.filetype = filetype
- self.modified = modified
- self.bufhidden = bufhidden
- self.window = window
- self.omnifunc = omnifunc
- self.omnifunc_name = omnifunc.__name__ if omnifunc else ''
- self.changedtick = 1
- self.options = {
- 'mod': modified,
- 'bh': bufhidden
- }
- self.visual_start = visual_start
- self.visual_end = visual_end
- def __getitem__( self, index ):
- """Returns the bytes for a given line at index |index|."""
- return self.contents[ index ]
- def __len__( self ):
- return len( self.contents )
- def __setitem__( self, key, value ):
- return self.contents.__setitem__( key, value )
- def GetLines( self ):
- """Returns the contents of the buffer as a list of unicode strings."""
- return [ ToUnicode( x ) for x in self.contents ]
- def mark( self, name ):
- if name == '<':
- return self.visual_start
- if name == '>':
- return self.visual_end
- raise ValueError( 'Unexpected mark: {name}'.format( name = name ) )
- class VimBuffers( object ):
- """An object that looks like a vim.buffers object."""
- def __init__( self, *buffers ):
- """Arguments are VimBuffer objects."""
- self._buffers = list( buffers )
- def __getitem__( self, number ):
- """Emulates vim.buffers[ number ]"""
- for buffer_object in self._buffers:
- if number == buffer_object.number:
- return buffer_object
- raise KeyError( number )
- def __iter__( self ):
- """Emulates for loop on vim.buffers"""
- return iter( self._buffers )
- def pop( self, index ):
- return self._buffers.pop( index )
- class VimMatch( object ):
- def __init__( self, group, pattern ):
- self.id = len( VIM_MATCHES )
- self.group = group
- self.pattern = pattern
- def __eq__( self, other ):
- return self.group == other.group and self.pattern == other.pattern
- def __repr__( self ):
- return "VimMatch( group = '{0}', pattern = '{1}' )".format( self.group,
- self.pattern )
- def __getitem__( self, key ):
- if key == 'group':
- return self.group
- elif key == 'id':
- return self.id
- class VimSign( object ):
- def __init__( self, sign_id, line, name, bufnr ):
- self.id = sign_id
- self.line = line
- self.name = name
- self.bufnr = bufnr
- def __eq__( self, other ):
- return ( self.id == other.id and
- self.line == other.line and
- self.name == other.name and
- self.bufnr == other.bufnr )
- def __repr__( self ):
- return ( "VimSign( id = {0}, line = {1}, "
- "name = '{2}', bufnr = {3} )".format( self.id,
- self.line,
- self.name,
- self.bufnr ) )
- def __getitem__( self, key ):
- if key == 'group':
- return self.group
- elif key == 'id':
- return self.id
- @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.' )
- line = current_buffer.contents[ cursor_position[ 0 ] - 1 ]
- with patch( 'vim.buffers', VimBuffers( *buffers ) ):
- with patch( 'vim.current.buffer', current_buffer ):
- with patch( 'vim.current.window.cursor', cursor_position ):
- with patch( 'vim.current.line', line ):
- yield VIM_MOCK
- 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
- need to add additional mocks to vim module functions, then use 'patch' from
- mock module, to ensure that the state of the vim mock is returned before the
- next test. That is:
- from ycm.tests.test_utils import MockVimModule
- from mock import patch
- # Do this once
- MockVimModule()
- @patch( 'vim.eval', return_value='test' )
- @patch( 'vim.command', side_effect=ValueError )
- def test( vim_command, vim_eval ):
- # use vim.command via vim_command, e.g.:
- vim_command.assert_has_calls( ... )
- Failure to use this approach may lead to unexpected failures in other
- tests."""
- VIM_MOCK.buffers = {}
- VIM_MOCK.command = MagicMock( side_effect = _MockVimCommand )
- VIM_MOCK.eval = MagicMock( side_effect = _MockVimEval )
- VIM_MOCK.error = VimError
- sys.modules[ 'vim' ] = VIM_MOCK
- return VIM_MOCK
- class VimError( Exception ):
- def __init__( self, code ):
- self.code = code
- def __str__( self ):
- return repr( self.code )
- class ExtendedMock( MagicMock ):
- """An extension to the MagicMock class which adds the ability to check that a
- callable is called with a precise set of calls in a precise order.
- Example Usage:
- from ycm.tests.test_utils import ExtendedMock
- @patch( 'test.testing', new_callable = ExtendedMock, ... )
- def my_test( test_testing ):
- ...
- """
- def assert_has_exact_calls( self, calls, any_order = False ):
- self.assert_has_calls( calls, any_order )
- assert_that( self.call_count, equal_to( len( calls ) ) )
- 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
- def ToBytesOnPY2( data ):
- # To test the omnifunc, etc. returning strings, which can be of different
- # types depending on python version, we use ToBytes on PY2 and just the native
- # str on python3. This roughly matches what happens between py2 and py3
- # versions within Vim.
- if not PY2:
- return data
- if isinstance( data, list ):
- return [ ToBytesOnPY2( item ) for item in data ]
- if isinstance( data, dict ):
- for item in data:
- data[ item ] = ToBytesOnPY2( data[ item ] )
- return data
- return ToBytes( data )
|