event_notification_test.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # Copyright (C) 2015 YouCompleteMe contributors
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. from ycm.test_utils import MockVimModule, ExtendedMock
  18. MockVimModule()
  19. import contextlib
  20. import os
  21. from ycm.youcompleteme import YouCompleteMe
  22. from ycmd import user_options_store
  23. from ycmd.responses import UnknownExtraConf
  24. from mock import call, MagicMock, patch
  25. # The default options which are only relevant to the client, not the server and
  26. # thus are not part of default_options.json, but are required for a working
  27. # YouCompleteMe object.
  28. DEFAULT_CLIENT_OPTIONS = {
  29. 'server_log_level': 'info',
  30. 'extra_conf_vim_data': [],
  31. }
  32. def PostVimMessage_Call( message ):
  33. """Return a mock.call object for a call to vimsupport.PostVimMesasge with the
  34. supplied message"""
  35. return call( 'redraw | echohl WarningMsg | echom \''
  36. + message +
  37. '\' | echohl None' )
  38. def PresentDialog_Confirm_Call( message ):
  39. """Return a mock.call object for a call to vimsupport.PresentDialog, as called
  40. why vimsupport.Confirm with the supplied confirmation message"""
  41. return call( message, [ 'Ok', 'Cancel' ] )
  42. @contextlib.contextmanager
  43. def MockArbitraryBuffer( filetype, native_available = True ):
  44. """Used via the with statement, set up mocked versions of the vim module such
  45. that a single buffer is open with an arbitrary name and arbirary contents. Its
  46. filetype is set to the supplied filetype"""
  47. with patch( 'vim.current' ) as vim_current:
  48. def VimEval( value ):
  49. """Local mock of the vim.eval() function, used to ensure we get the
  50. correct behvaiour"""
  51. if value == '&omnifunc':
  52. # The omnicompleter is not required here
  53. return ''
  54. if value == 'getbufvar(0, "&mod")':
  55. # Ensure that we actually send the even to the server
  56. return 1
  57. if value == 'getbufvar(0, "&ft")' or value == '&filetype':
  58. return filetype
  59. raise ValueError( 'Unexpected evaluation' )
  60. # Arbitrary, but valid, cursor position
  61. vim_current.window.cursor = ( 1, 2 )
  62. # Arbitrary, but valid, single buffer open
  63. current_buffer = MagicMock()
  64. current_buffer.number = 0
  65. current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
  66. current_buffer.name = 'TEST_BUFFER'
  67. # The rest just mock up the Vim module so that our single arbitrary buffer
  68. # makes sense to vimsupport module.
  69. with patch( 'vim.buffers', [ current_buffer ] ):
  70. with patch( 'vim.current.buffer', current_buffer ):
  71. with patch( 'vim.eval', side_effect=VimEval ):
  72. yield
  73. @contextlib.contextmanager
  74. def MockEventNotification( response_method, native_filetype_completer = True ):
  75. """Mock out the EventNotification client request object, replacing the
  76. Response handler's JsonFromFuture with the supplied |response_method|.
  77. Additionally mock out YouCompleteMe's FiletypeCompleterExistsForFiletype
  78. method to return the supplied |native_filetype_completer| parameter, rather
  79. than querying the server"""
  80. # We don't want the event to actually be sent to the server, just have it
  81. # return success
  82. with patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync',
  83. return_value = MagicMock( return_value=True ) ):
  84. # We set up a fake a Response (as called by EventNotification.Response)
  85. # which calls the supplied callback method. Generally this callback just
  86. # raises an apropriate exception, otherwise it would have to return a mock
  87. # future object.
  88. #
  89. # Note: JsonFromFuture is actually part of ycm.client.base_request, but we
  90. # must patch where an object is looked up, not where it is defined.
  91. # See https://docs.python.org/dev/library/unittest.mock.html#where-to-patch
  92. # for details.
  93. with patch( 'ycm.client.event_notification.JsonFromFuture',
  94. side_effect = response_method ):
  95. # Filetype available information comes from the server, so rather than
  96. # relying on that request, we mock out the check. The caller decides if
  97. # filetype completion is available
  98. with patch(
  99. 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
  100. return_value = native_filetype_completer ):
  101. yield
  102. class EventNotification_test( object ):
  103. def setUp( self ):
  104. options = dict( user_options_store.DefaultOptions() )
  105. options.update( DEFAULT_CLIENT_OPTIONS )
  106. user_options_store.SetAll( options )
  107. self.server_state = YouCompleteMe( user_options_store.GetAll() )
  108. pass
  109. def tearDown( self ):
  110. if self.server_state:
  111. self.server_state.OnVimLeave()
  112. @patch( 'vim.command', new_callable = ExtendedMock )
  113. def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ):
  114. # This test validates the behaviour of YouCompleteMe.ValidateParseRequest in
  115. # combination with YouCompleteMe.OnFileReadyToParse when the completer
  116. # raises an exception handling FileReadyToParse event notification
  117. ERROR_TEXT = 'Some completer response text'
  118. def ErrorResponse( *args ):
  119. raise RuntimeError( ERROR_TEXT )
  120. with MockArbitraryBuffer( 'javascript' ):
  121. with MockEventNotification( ErrorResponse ):
  122. self.server_state.OnFileReadyToParse()
  123. assert self.server_state.DiagnosticsForCurrentFileReady()
  124. self.server_state.ValidateParseRequest()
  125. # The first call raises a warning
  126. vim_command.assert_has_exact_calls( [
  127. PostVimMessage_Call( ERROR_TEXT ),
  128. ] )
  129. # Subsequent calls don't re-raise the warning
  130. self.server_state.ValidateParseRequest()
  131. vim_command.assert_has_exact_calls( [
  132. PostVimMessage_Call( ERROR_TEXT ),
  133. ] )
  134. # But it does if a subsequent event raises again
  135. self.server_state.OnFileReadyToParse()
  136. assert self.server_state.DiagnosticsForCurrentFileReady()
  137. self.server_state.ValidateParseRequest()
  138. vim_command.assert_has_exact_calls( [
  139. PostVimMessage_Call( ERROR_TEXT ),
  140. PostVimMessage_Call( ERROR_TEXT ),
  141. ] )
  142. @patch( 'vim.command' )
  143. def FileReadyToParse_NonDiagnostic_Error_NonNative_test( self, vim_command ):
  144. with MockArbitraryBuffer( 'javascript' ):
  145. with MockEventNotification( None, False ):
  146. self.server_state.OnFileReadyToParse()
  147. self.server_state.ValidateParseRequest()
  148. vim_command.assert_not_called()
  149. @patch( 'ycm.client.event_notification._LoadExtraConfFile',
  150. new_callable = ExtendedMock )
  151. @patch( 'ycm.client.event_notification._IgnoreExtraConfFile',
  152. new_callable = ExtendedMock )
  153. def FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test(
  154. self,
  155. ignore_extra_conf,
  156. load_extra_conf,
  157. *args ):
  158. # This test validates the behaviour of YouCompleteMe.ValidateParseRequest in
  159. # combination with YouCompleteMe.OnFileReadyToParse when the completer
  160. # raises the (special) UnknownExtraConf exception
  161. FILE_NAME = 'a_file'
  162. MESSAGE = ( 'Found ' + FILE_NAME + '. Load? \n\n(Question can be '
  163. 'turned off with options, see YCM docs)' )
  164. def UnknownExtraConfResponse( *args ):
  165. raise UnknownExtraConf( FILE_NAME )
  166. with MockArbitraryBuffer( 'javascript' ):
  167. with MockEventNotification( UnknownExtraConfResponse ):
  168. # When the user accepts the extra conf, we load it
  169. with patch( 'ycm.vimsupport.PresentDialog',
  170. return_value = 0,
  171. new_callable = ExtendedMock ) as present_dialog:
  172. self.server_state.OnFileReadyToParse()
  173. assert self.server_state.DiagnosticsForCurrentFileReady()
  174. self.server_state.ValidateParseRequest()
  175. present_dialog.assert_has_exact_calls( [
  176. PresentDialog_Confirm_Call( MESSAGE ),
  177. ] )
  178. load_extra_conf.assert_has_exact_calls( [
  179. call( FILE_NAME ),
  180. ] )
  181. # Subsequent calls don't re-raise the warning
  182. self.server_state.ValidateParseRequest()
  183. present_dialog.assert_has_exact_calls( [
  184. PresentDialog_Confirm_Call( MESSAGE )
  185. ] )
  186. load_extra_conf.assert_has_exact_calls( [
  187. call( FILE_NAME ),
  188. ] )
  189. # But it does if a subsequent event raises again
  190. self.server_state.OnFileReadyToParse()
  191. assert self.server_state.DiagnosticsForCurrentFileReady()
  192. self.server_state.ValidateParseRequest()
  193. present_dialog.assert_has_exact_calls( [
  194. PresentDialog_Confirm_Call( MESSAGE ),
  195. PresentDialog_Confirm_Call( MESSAGE ),
  196. ] )
  197. load_extra_conf.assert_has_exact_calls( [
  198. call( FILE_NAME ),
  199. call( FILE_NAME ),
  200. ] )
  201. # When the user rejects the extra conf, we reject it
  202. with patch( 'ycm.vimsupport.PresentDialog',
  203. return_value = 1,
  204. new_callable = ExtendedMock ) as present_dialog:
  205. self.server_state.OnFileReadyToParse()
  206. assert self.server_state.DiagnosticsForCurrentFileReady()
  207. self.server_state.ValidateParseRequest()
  208. present_dialog.assert_has_exact_calls( [
  209. PresentDialog_Confirm_Call( MESSAGE ),
  210. ] )
  211. ignore_extra_conf.assert_has_exact_calls( [
  212. call( FILE_NAME ),
  213. ] )
  214. # Subsequent calls don't re-raise the warning
  215. self.server_state.ValidateParseRequest()
  216. present_dialog.assert_has_exact_calls( [
  217. PresentDialog_Confirm_Call( MESSAGE )
  218. ] )
  219. ignore_extra_conf.assert_has_exact_calls( [
  220. call( FILE_NAME ),
  221. ] )
  222. # But it does if a subsequent event raises again
  223. self.server_state.OnFileReadyToParse()
  224. assert self.server_state.DiagnosticsForCurrentFileReady()
  225. self.server_state.ValidateParseRequest()
  226. present_dialog.assert_has_exact_calls( [
  227. PresentDialog_Confirm_Call( MESSAGE ),
  228. PresentDialog_Confirm_Call( MESSAGE ),
  229. ] )
  230. ignore_extra_conf.assert_has_exact_calls( [
  231. call( FILE_NAME ),
  232. call( FILE_NAME ),
  233. ] )