Forráskód Böngészése

Rewrite omnifunc tests

The VimBuffer object now accepts a Python function as its omnifunc.
micbou 7 éve
szülő
commit
db0b9ab335

+ 4 - 4
python/ycm/tests/event_notification_test.py

@@ -361,7 +361,7 @@ def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
   with patch( 'ycm.client.event_notification.EventNotification.'
               'PostDataToHandlerAsync' ) as post_data_to_handler_async:
     with CurrentWorkingDirectory( unicode_dir ):
-      with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ):
+      with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 5 ) ):
         ycm.OnFileReadyToParse()
 
     assert_that(
@@ -370,7 +370,7 @@ def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
       contains(
         has_entries( {
           'filepath': current_buffer_file,
-          'line_num': 6,
+          'line_num': 1,
           'column_num': 6,
           'file_data': has_entries( {
             current_buffer_file: has_entries( {
@@ -416,7 +416,7 @@ def EventNotification_BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test(
               'PostDataToHandlerAsync' ) as post_data_to_handler_async:
     with MockVimBuffers( [ current_buffer, modified_buffer, unmodified_buffer ],
                          current_buffer,
-                         ( 3, 5 ) ):
+                         ( 1, 5 ) ):
       ycm.OnBufferVisit()
 
     assert_that(
@@ -425,7 +425,7 @@ def EventNotification_BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test(
       contains(
         has_entries( {
           'filepath': current_buffer_file,
-          'line_num': 3,
+          'line_num': 1,
           'column_num': 6,
           'file_data': has_entries( {
             current_buffer_file: has_entries( {

+ 513 - 603
python/ycm/tests/omni_completer_test.py

@@ -24,690 +24,600 @@ 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 patch, call
-from nose.tools import eq_
+from hamcrest import assert_that, contains, contains_string, empty, has_entries
 
-from ycm.tests.test_utils import ExpectedFailure, ExtendedMock, MockVimModule
+from ycm.tests.test_utils import ( ExpectedFailure, MockVimBuffers,
+                                   MockVimModule, ToBytesOnPY2, VimBuffer )
 MockVimModule()
 
 from ycm.tests import YouCompleteMeInstance
 
-from ycmd.utils import ToBytes
-from ycmd.request_wrap import RequestWrap
-
-
-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 PY2:
-    return ToBytes( data )
-
-  return data
-
-
-def BuildRequest( line_num, column_num, contents ):
-  # Note: it would be nice to use ycmd.tests.test_utils.BuildRequest directly
-  # here, but we can't import ycmd.tests.test_utils because that in turn imports
-  # ycm_core, which would cause our "ycm_core not imported" test to fail.
-  return {
-    'line_num': line_num,
-    'column_num': column_num,
-    'filepath': '/test',
-    'file_data': {
-      '/test': {
-        'contents': contents,
-        'filetypes': [ 'java' ] # We need a filetype with a trigger, so we just
-                                # use java
-      }
-    }
-  }
-
-
-def BuildRequestWrap( line_num, column_num, contents ):
-  return RequestWrap( BuildRequest( line_num, column_num, contents ) )
-
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_List_test( ycm ):
-  contents = 'test.'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 6,
-                                   contents = contents )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 6, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 5 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'a', 'b', 'cdef' ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_ListFilter_test( ycm ):
-  contents = 'test.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 7,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'t')" ),
-    ] )
-
-    eq_( results, [] )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 6 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': empty(),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_List_test( ycm ):
-  contents = 'test.'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 6,
-                                   contents = contents )
-
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 5 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'a', 'b', 'cdef' ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_ListFilter_test( ycm ):
-  contents = 'test.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 7,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'t')" ),
-    ] )
-
-    # actual result is that the results are not filtered, as we expect the
-    # omniufunc or vim itself to do this filtering
-    eq_( results, omnifunc_result )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 6 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    # Actual result is that the results are not filtered, as we expect the
+    # omnifunc or vim itself to do this filtering.
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'a', 'b', 'cdef' ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
-def OmniCompleter_GetCompletsions_NoCache_UseFindStart_test( ycm ):
-  contents = 'test.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 7,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 0, omnifunc_result ] ) as vim_eval:
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'test.t')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
+def OmniCompleter_GetCompletions_NoCache_UseFindStart_test( ycm ):
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 0
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 6 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    # Actual result is that the results are not filtered, as we expect the
+    # omnifunc or vim itself to do this filtering.
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'a', 'b', 'cdef' ] ),
+        'completion_start_column': 1
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
-def OmniCompleter_GetCompletsions_Cache_UseFindStart_test( ycm ):
-  contents = 'test.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 7,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'a' ),
-                      ToBytesOnPY2( 'b' ),
-                      ToBytesOnPY2( 'cdef' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 0, omnifunc_result ] ) as vim_eval:
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'test.t')" ),
-    ] )
-
+def OmniCompleter_GetCompletions_Cache_UseFindStart_test( ycm ):
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 0
+    return [ 'a', 'b', 'cdef' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 6 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
     # There are no results because the query 'test.t' doesn't match any
-    # candidate (and cache_omnifunc=1, so we FilterAndSortCandidates)
-    eq_( results, [] )
+    # candidate (and cache_omnifunc=1, so we FilterAndSortCandidates).
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': empty(),
+        'completion_start_column': 1
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_Object_test( ycm ):
-  contents = 'test.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 7,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = {
-    'words': [
-      ToBytesOnPY2( 'a' ),
-      ToBytesOnPY2( 'b' ),
-      ToBytesOnPY2( 'CDtEF' )
-    ]
-  }
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'t')" ),
-    ] )
-
-    eq_( results, [ ToBytesOnPY2( 'CDtEF' ) ] )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return { 'words': [ 'a', 'b', 'CDtEF' ] }
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 6 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': [ 'CDtEF' ],
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_ObjectList_test( ycm ):
-  contents = 'test.tt'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 8,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 'tt' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [
-    {
-      'word': ToBytesOnPY2( 'a' ),
-      'abbr': ToBytesOnPY2( 'ABBR'),
-      'menu': ToBytesOnPY2( 'MENU' ),
-      'info': ToBytesOnPY2( 'INFO' ),
-      'kind': ToBytesOnPY2( 'K' )
-    },
-    {
-      'word': ToBytesOnPY2( 'test' ),
-      'abbr': ToBytesOnPY2( 'ABBRTEST'),
-      'menu': ToBytesOnPY2( 'MENUTEST' ),
-      'info': ToBytesOnPY2( 'INFOTEST' ),
-      'kind': ToBytesOnPY2( 'T' )
-    }
-  ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'tt')" ),
-    ] )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [
+      {
+        'word': 'a',
+        'abbr': 'ABBR',
+        'menu': 'MENU',
+        'info': 'INFO',
+        'kind': 'K'
+      },
+      {
+        'word': 'test',
+        'abbr': 'ABBRTEST',
+        'menu': 'MENUTEST',
+        'info': 'INFOTEST',
+        'kind': 'T'
+      }
+    ]
 
-    eq_( results, [ omnifunc_result[ 1 ] ] )
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.tt' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 7 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': contains( {
+          'word': 'test',
+          'abbr': 'ABBRTEST',
+          'menu': 'MENUTEST',
+          'info': 'INFOTEST',
+          'kind': 'T'
+        } ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_ObjectList_test( ycm ):
-  contents = 'test.tt'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 8,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 'tt' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [
-    {
-      'word': ToBytesOnPY2( 'a' ),
-      'abbr': ToBytesOnPY2( 'ABBR'),
-      'menu': ToBytesOnPY2( 'MENU' ),
-      'info': ToBytesOnPY2( 'INFO' ),
-      'kind': ToBytesOnPY2( 'K' )
-    },
-    {
-      'word': ToBytesOnPY2( 'test' ),
-      'abbr': ToBytesOnPY2( 'ABBRTEST'),
-      'menu': ToBytesOnPY2( 'MENUTEST' ),
-      'info': ToBytesOnPY2( 'INFOTEST' ),
-      'kind': ToBytesOnPY2( 'T' )
-    }
-  ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return [
+      {
+        'word': 'a',
+        'abbr': 'ABBR',
+        'menu': 'MENU',
+        'info': 'INFO',
+        'kind': 'K'
+      },
+      {
+        'word': 'test',
+        'abbr': 'ABBRTEST',
+        'menu': 'MENUTEST',
+        'info': 'INFOTEST',
+        'kind': 'T'
+      }
+    ]
 
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'tt')" ),
-    ] )
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.tt' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
 
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 7 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
     # We don't filter the result - we expect the omnifunc to do that
-    # based on the query we supplied (Note: that means no fuzzy matching!)
-    eq_( results, omnifunc_result )
+    # based on the query we supplied (Note: that means no fuzzy matching!).
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ {
+          'word': 'a',
+          'abbr': 'ABBR',
+          'menu': 'MENU',
+          'info': 'INFO',
+          'kind': 'K'
+        }, {
+          'word': 'test',
+          'abbr': 'ABBRTEST',
+          'menu': 'MENUTEST',
+          'info': 'INFOTEST',
+          'kind': 'T'
+        } ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_ObjectListObject_test( ycm ):
-  contents = 'test.tt'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 8,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 'tt' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = {
-    'words': [
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return { 'words': [
       {
-        'word': ToBytesOnPY2( 'a' ),
-        'abbr': ToBytesOnPY2( 'ABBR'),
-        'menu': ToBytesOnPY2( 'MENU' ),
-        'info': ToBytesOnPY2( 'INFO' ),
-        'kind': ToBytesOnPY2( 'K' )
+        'word': 'a',
+        'abbr': 'ABBR',
+        'menu': 'MENU',
+        'info': 'INFO',
+        'kind': 'K'
       },
       {
-        'word': ToBytesOnPY2( 'test' ),
-        'abbr': ToBytesOnPY2( 'ABBRTEST'),
-        'menu': ToBytesOnPY2( 'MENUTEST' ),
-        'info': ToBytesOnPY2( 'INFOTEST' ),
-        'kind': ToBytesOnPY2( 'T' )
+        'word': 'test',
+        'abbr': 'ABBRTEST',
+        'menu': 'MENUTEST',
+        'info': 'INFOTEST',
+        'kind': 'T'
       }
-    ]
-  }
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'tt')" ),
-    ] )
-
-    eq_( results, [ omnifunc_result[ 'words' ][ 1 ] ] )
+    ] }
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.tt' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 7 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ {
+          'word': 'test',
+          'abbr': 'ABBRTEST',
+          'menu': 'MENUTEST',
+          'info': 'INFOTEST',
+          'kind': 'T'
+        } ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_ObjectListObject_test( ycm ):
-  contents = 'test.tt'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 8,
-                                   contents = contents )
-
-  eq_( request_data[ 'query' ], 'tt' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = {
-    'words': [
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 5
+    return { 'words': [
       {
-        'word': ToBytesOnPY2( 'a' ),
-        'abbr': ToBytesOnPY2( 'ABBR'),
-        'menu': ToBytesOnPY2( 'MENU' ),
-        'info': ToBytesOnPY2( 'INFO' ),
-        'kind': ToBytesOnPY2( 'K' )
+        'word': 'a',
+        'abbr': 'ABBR',
+        'menu': 'MENU',
+        'info': 'INFO',
+        'kind': 'K'
       },
       {
-        'word': ToBytesOnPY2( 'test' ),
-        'abbr': ToBytesOnPY2( 'ABBRTEST'),
-        'menu': ToBytesOnPY2( 'MENUTEST' ),
-        'info': ToBytesOnPY2( 'INFOTEST' ),
-        'kind': ToBytesOnPY2( 'T' )
+        'word': 'test',
+        'abbr': 'ABBRTEST',
+        'menu': 'MENUTEST',
+        'info': 'INFOTEST',
+        'kind': 'T'
       }
-    ]
-  }
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 5, omnifunc_result ] ) as vim_eval:
+    ] }
 
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'tt')" ),
-    ] )
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ 'test.tt' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
 
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 7 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
     # No FilterAndSortCandidates for cache_omnifunc=0 (we expect the omnifunc
     # to do the filtering?)
-    eq_( results, omnifunc_result[ 'words' ] )
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ {
+          'word': 'a',
+          'abbr': 'ABBR',
+          'menu': 'MENU',
+          'info': 'INFO',
+          'kind': 'K'
+        }, {
+          'word': 'test',
+          'abbr': 'ABBRTEST',
+          'menu': 'MENUTEST',
+          'info': 'INFOTEST',
+          'kind': 'T'
+        } ] ),
+        'completion_start_column': 6
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_List_Unicode_test( ycm ):
-  contents = '†åsty_π.'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 13,
-                                   contents = contents )
-
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( '†est' ),
-                      ToBytesOnPY2( 'å_unicode_identifier' ),
-                      ToBytesOnPY2( 'πππππππ yummy πie' ) ]
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return [ '†est', 'å_unicode_identifier', 'πππππππ yummy πie' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 12 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ '†est',
+                                       'å_unicode_identifier',
+                                       'πππππππ yummy πie' ] ),
+        'completion_start_column': 13
+      } )
+    )
 
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
 
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
-
-
-@YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
+@YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_List_Unicode_test( ycm ):
-  contents = '†åsty_π.'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 13,
-                                   contents = contents )
-
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( '†est' ),
-                      ToBytesOnPY2( 'å_unicode_identifier' ),
-                      ToBytesOnPY2( 'πππππππ yummy πie' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
-
-
-@ExpectedFailure( 'Filtering on unicode is not supported by the server' )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return [ '†est', 'å_unicode_identifier', 'πππππππ yummy πie' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 12 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ '†est',
+                                       'å_unicode_identifier',
+                                       'πππππππ yummy πie' ] ),
+        'completion_start_column': 13
+      } )
+    )
+
+
+@ExpectedFailure( 'Filtering on unicode is not supported by the server',
+                  contains_string( "value for 'completions' was <[]>" ) )
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_List_Filter_Unicode_test( ycm ):
-  contents = '†åsty_π.ππ'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 17,
-                                   contents = contents )
-
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( '†est' ),
-                      ToBytesOnPY2( 'å_unicode_identifier' ),
-                      ToBytesOnPY2( 'πππππππ yummy πie' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'ππ')" ),
-    ] )
-
-    # Fails here: Filtering on unicode is not supported
-    eq_( results, [ omnifunc_result[ 2 ] ] )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return [ '†est', 'å_unicode_identifier', 'πππππππ yummy πie' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.ππ' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 17 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'πππππππ yummy πie' ] ),
+        'completion_start_column': 13
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 0 } )
 def OmniCompleter_GetCompletions_NoCache_List_Filter_Unicode_test( ycm ):
-  contents = '†åsty_π.ππ'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 17,
-                                   contents = contents )
-
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [ ToBytesOnPY2( 'πππππππ yummy πie' ) ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'ππ')" ),
-    ] )
-
-    eq_( results, omnifunc_result )
-
-
-@ExpectedFailure( 'Filtering on unicode is not supported by the server' )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return [ 'πππππππ yummy πie' ]
+
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.ππ' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 17 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ 'πππππππ yummy πie' ] ),
+        'completion_start_column': 13
+      } )
+    )
+
+
+@ExpectedFailure( 'Filtering on unicode is not supported by the server',
+                  contains_string( "value for 'completions' was <[]>" ) )
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_ObjectList_Unicode_test( ycm ):
-  contents = '†åsty_π.ππ'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 17,
-                                   contents = contents )
-
-
-  eq_( request_data[ 'query' ], 'ππ' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
-
-  omnifunc_result = [
-    {
-      'word': ToBytesOnPY2( 'ålpha∫et' ),
-      'abbr': ToBytesOnPY2( 'å∫∫®'),
-      'menu': ToBytesOnPY2( 'µ´~¨á' ),
-      'info': ToBytesOnPY2( '^~fo' ),
-      'kind': ToBytesOnPY2( '˚' )
-    },
-    {
-      'word': ToBytesOnPY2( 'π†´ß†π' ),
-      'abbr': ToBytesOnPY2( 'ÅııÂʉÍÊ'),
-      'menu': ToBytesOnPY2( '˜‰ˆËʉÍÊ' ),
-      'info': ToBytesOnPY2( 'ȈÏØʉÍÊ' ),
-      'kind': ToBytesOnPY2( 'Ê' )
-    }
-  ]
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'ππ')" ),
-    ] )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return [
+      {
+        'word': 'ålpha∫et',
+        'abbr': 'å∫∫®',
+        'menu': 'µ´~¨á',
+        'info': '^~fo',
+        'kind': '˚'
+      },
+      {
+        'word': 'π†´ß†π',
+        'abbr': 'ÅııÂʉÍÊ',
+        'menu': '˜‰ˆËʉÍÊ',
+        'info': 'ȈÏØʉÍÊ',
+        'kind': 'Ê'
+      }
+    ]
 
-    # Fails here: Filtering on unicode is not supported
-    eq_( results, [ omnifunc_result[ 1 ] ] )
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.ππ' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 17 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': ToBytesOnPY2( [ {
+          'word': 'π†´ß†π',
+          'abbr': 'ÅııÂʉÍÊ',
+          'menu': '˜‰ˆËʉÍÊ',
+          'info': 'ȈÏØʉÍÊ',
+          'kind': 'Ê'
+        } ] ),
+        'completion_start_column': 13
+      } )
+    )
 
 
 @YouCompleteMeInstance( { 'cache_omnifunc': 1 } )
 def OmniCompleter_GetCompletions_Cache_ObjectListObject_Unicode_test( ycm ):
-  contents = '†åsty_π.t'
-  request_data = BuildRequestWrap( line_num = 1,
-                                   column_num = 14,
-                                   contents = contents )
-
-
-  eq_( request_data[ 'query' ], 't' )
-
-  # Make sure there is an omnifunc set up.
-  with patch( 'vim.eval', return_value = ToBytesOnPY2( 'test_omnifunc' ) ):
-    ycm._omnicomp.OnFileReadyToParse( request_data )
+  def Omnifunc( findstart, base ):
+    if findstart:
+      return 12
+    return {
+      'words': [
+        {
+          'word': 'ålpha∫et',
+          'abbr': 'å∫∫®',
+          'menu': 'µ´~¨á',
+          'info': '^~fo',
+          'kind': '˚'
+        },
+        {
+          'word': 'π†´ß†π',
+          'abbr': 'ÅııÂʉÍÊ',
+          'menu': '˜‰ˆËʉÍÊ',
+          'info': 'ȈÏØʉÍÊ',
+          'kind': 'Ê'
+        },
+        {
+          'word': 'test',
+          'abbr': 'ÅııÂʉÍÊ',
+          'menu': '˜‰ˆËʉÍÊ',
+          'info': 'ȈÏØʉÍÊ',
+          'kind': 'Ê'
+        }
+      ]
+    }
 
-  omnifunc_result = {
-    'words': [
-      {
-        'word': ToBytesOnPY2( 'ålpha∫et' ),
-        'abbr': ToBytesOnPY2( 'å∫∫®'),
-        'menu': ToBytesOnPY2( 'µ´~¨á' ),
-        'info': ToBytesOnPY2( '^~fo' ),
-        'kind': ToBytesOnPY2( '˚' )
-      },
-      {
-        'word': ToBytesOnPY2( 'π†´ß†π' ),
-        'abbr': ToBytesOnPY2( 'ÅııÂʉÍÊ'),
-        'menu': ToBytesOnPY2( '˜‰ˆËʉÍÊ' ),
-        'info': ToBytesOnPY2( 'ȈÏØʉÍÊ' ),
-        'kind': ToBytesOnPY2( 'Ê' )
-      },
-      {
-        'word': ToBytesOnPY2( 'test' ),
-        'abbr': ToBytesOnPY2( 'ÅııÂʉÍÊ'),
-        'menu': ToBytesOnPY2( '˜‰ˆËʉÍÊ' ),
-        'info': ToBytesOnPY2( 'ȈÏØʉÍÊ' ),
-        'kind': ToBytesOnPY2( 'Ê' )
-      }
-    ]
-  }
-
-  # And get the completions
-  with patch( 'vim.eval',
-              new_callable = ExtendedMock,
-              side_effect = [ 12, omnifunc_result ] ) as vim_eval:
-
-    results = ycm._omnicomp.ComputeCandidates( request_data )
-
-    vim_eval.assert_has_exact_calls( [
-      call( 'test_omnifunc(1,"")' ),
-      call( "test_omnifunc(0,'t')" ),
-    ] )
-
-    # Note: the filtered results are all unicode objects (not bytes) because
-    # they are passed through the FilterAndSortCandidates machinery
-    # (via the server)
-    eq_( results, [ {
-      'word': 'test',
-      'abbr': 'ÅııÂʉÍÊ',
-      'menu': '˜‰ˆËʉÍÊ',
-      'info': 'ȈÏØʉÍÊ',
-      'kind': 'Ê'
-    } ] )
+  current_buffer = VimBuffer( 'buffer',
+                              contents = [ '†åsty_π.t' ],
+                              filetype = 'java',
+                              omnifunc = Omnifunc )
+
+  with MockVimBuffers( [ current_buffer ], current_buffer, ( 1, 13 ) ):
+    # Make sure there is an omnifunc set up.
+    ycm.OnFileReadyToParse()
+    ycm.SendCompletionRequest()
+    assert_that(
+      ycm.GetCompletionResponse(),
+      has_entries( {
+        'completions': contains( {
+          'word': 'test',
+          'abbr': 'ÅııÂʉÍÊ',
+          'menu': '˜‰ˆËʉÍÊ',
+          'info': 'ȈÏØʉÍÊ',
+          'kind': 'Ê'
+        } ),
+        'completion_start_column': 13
+      } )
+    )

+ 48 - 6
python/ycm/tests/test_utils.py

@@ -23,6 +23,7 @@ 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 MagicMock, patch
 from hamcrest import assert_that, equal_to
 import contextlib
@@ -32,7 +33,7 @@ import os
 import re
 import sys
 
-from ycmd.utils import GetCurrentDirectory, ToUnicode
+from ycmd.utils import GetCurrentDirectory, ToBytes, ToUnicode
 
 
 BUFNR_REGEX = re.compile( '^bufnr\(\'(?P<buffer_filename>.+)\', ([01])\)$' )
@@ -44,6 +45,8 @@ GETBUFVAR_REGEX = re.compile(
 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>.*)[\'"]\)$' )
 
 # 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,
@@ -99,7 +102,7 @@ def _MockGetBufferVariable( buffer_number, option ):
 
 def _MockVimBufferEval( value ):
   if value == '&omnifunc':
-    return VIM_MOCK.current.buffer.omnifunc
+    return VIM_MOCK.current.buffer.omnifunc_name
 
   if value == '&filetype':
     return VIM_MOCK.current.buffer.filetype
@@ -120,6 +123,16 @@ def _MockVimBufferEval( value ):
     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
 
 
@@ -221,16 +234,24 @@ class VimBuffer( object ):
    - |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."""
+   - |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 = [],
+                      contents = [ '' ],
                       filetype = '',
                       modified = False,
                       bufhidden = '',
                       window = None,
-                      omnifunc = '' ):
+                      omnifunc = None ):
     self.name = os.path.realpath( name ) if name else ''
     self.number = number
     self.contents = contents
@@ -239,6 +260,7 @@ class VimBuffer( object ):
     self.bufhidden = bufhidden
     self.window = window
     self.omnifunc = omnifunc
+    self.omnifunc_name = omnifunc.__name__ if omnifunc else ''
     self.changedtick = 1
 
 
@@ -292,10 +314,13 @@ def MockVimBuffers( buffers, current_buffer, cursor_position = ( 1, 1 ) ):
   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', buffers ):
     with patch( 'vim.current.buffer', current_buffer ):
       with patch( 'vim.current.window.cursor', cursor_position ):
-        yield VIM_MOCK
+        with patch( 'vim.current.line', line ):
+          yield VIM_MOCK
 
 
 def MockVimModule():
@@ -395,3 +420,20 @@ def ExpectedFailure( reason, *exception_matchers ):
     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 )