event_notification_test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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 __future__ import unicode_literals
  18. from __future__ import print_function
  19. from __future__ import division
  20. from __future__ import absolute_import
  21. from future import standard_library
  22. standard_library.install_aliases()
  23. from builtins import * # noqa
  24. from ycm.test_utils import MockVimModule, ExtendedMock
  25. MockVimModule()
  26. import contextlib
  27. import os
  28. from ycm.youcompleteme import YouCompleteMe
  29. from ycmd import user_options_store
  30. from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range,
  31. UnknownExtraConf )
  32. from mock import call, MagicMock, patch
  33. from nose.tools import eq_, ok_
  34. # The default options which are only relevant to the client, not the server and
  35. # thus are not part of default_options.json, but are required for a working
  36. # YouCompleteMe object.
  37. DEFAULT_CLIENT_OPTIONS = {
  38. 'server_log_level': 'info',
  39. 'extra_conf_vim_data': [],
  40. 'show_diagnostics_ui': 1,
  41. 'enable_diagnostic_signs': 1,
  42. 'enable_diagnostic_highlighting': 0,
  43. 'always_populate_location_list': 0,
  44. }
  45. def PostVimMessage_Call( message ):
  46. """Return a mock.call object for a call to vimsupport.PostVimMesasge with the
  47. supplied message"""
  48. return call( 'redraw | echohl WarningMsg | echom \''
  49. + message +
  50. '\' | echohl None' )
  51. def PresentDialog_Confirm_Call( message ):
  52. """Return a mock.call object for a call to vimsupport.PresentDialog, as called
  53. why vimsupport.Confirm with the supplied confirmation message"""
  54. return call( message, [ 'Ok', 'Cancel' ] )
  55. def PlaceSign_Call( sign_id, line_num, buffer_num, is_error ):
  56. sign_name = 'YcmError' if is_error else 'YcmWarning'
  57. return call( 'sign place {0} line={1} name={2} buffer={3}'
  58. .format( sign_id, line_num, sign_name, buffer_num ) )
  59. def UnplaceSign_Call( sign_id, buffer_num ):
  60. return call( 'try | exec "sign unplace {0} buffer={1}" |'
  61. ' catch /E158/ | endtry'.format( sign_id, buffer_num ) )
  62. @contextlib.contextmanager
  63. def MockArbitraryBuffer( filetype, native_available = True ):
  64. """Used via the with statement, set up mocked versions of the vim module such
  65. that a single buffer is open with an arbitrary name and arbirary contents. Its
  66. filetype is set to the supplied filetype"""
  67. with patch( 'vim.current' ) as vim_current:
  68. def VimEval( value ):
  69. """Local mock of the vim.eval() function, used to ensure we get the
  70. correct behvaiour"""
  71. if value == '&omnifunc':
  72. # The omnicompleter is not required here
  73. return ''
  74. if value == 'getbufvar(0, "&mod")':
  75. # Ensure that we actually send the even to the server
  76. return 1
  77. if value == 'getbufvar(0, "&ft")' or value == '&filetype':
  78. return filetype
  79. if value.startswith( 'bufnr(' ):
  80. return 0
  81. if value.startswith( 'bufwinnr(' ):
  82. return 0
  83. raise ValueError( 'Unexpected evaluation' )
  84. # Arbitrary, but valid, cursor position
  85. vim_current.window.cursor = ( 1, 2 )
  86. # Arbitrary, but valid, single buffer open
  87. current_buffer = MagicMock()
  88. current_buffer.number = 0
  89. current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
  90. current_buffer.name = 'TEST_BUFFER'
  91. current_buffer.window = 0
  92. # The rest just mock up the Vim module so that our single arbitrary buffer
  93. # makes sense to vimsupport module.
  94. with patch( 'vim.buffers', [ current_buffer ] ):
  95. with patch( 'vim.current.buffer', current_buffer ):
  96. with patch( 'vim.eval', side_effect=VimEval ):
  97. yield
  98. @contextlib.contextmanager
  99. def MockEventNotification( response_method, native_filetype_completer = True ):
  100. """Mock out the EventNotification client request object, replacing the
  101. Response handler's JsonFromFuture with the supplied |response_method|.
  102. Additionally mock out YouCompleteMe's FiletypeCompleterExistsForFiletype
  103. method to return the supplied |native_filetype_completer| parameter, rather
  104. than querying the server"""
  105. # We don't want the event to actually be sent to the server, just have it
  106. # return success
  107. with patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync',
  108. return_value = MagicMock( return_value=True ) ):
  109. # We set up a fake a Response (as called by EventNotification.Response)
  110. # which calls the supplied callback method. Generally this callback just
  111. # raises an apropriate exception, otherwise it would have to return a mock
  112. # future object.
  113. #
  114. # Note: JsonFromFuture is actually part of ycm.client.base_request, but we
  115. # must patch where an object is looked up, not where it is defined.
  116. # See https://docs.python.org/dev/library/unittest.mock.html#where-to-patch
  117. # for details.
  118. with patch( 'ycm.client.event_notification.JsonFromFuture',
  119. side_effect = response_method ):
  120. # Filetype available information comes from the server, so rather than
  121. # relying on that request, we mock out the check. The caller decides if
  122. # filetype completion is available
  123. with patch(
  124. 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
  125. return_value = native_filetype_completer ):
  126. yield
  127. class EventNotification_test( object ):
  128. def setUp( self ):
  129. options = dict( user_options_store.DefaultOptions() )
  130. options.update( DEFAULT_CLIENT_OPTIONS )
  131. user_options_store.SetAll( options )
  132. self.server_state = YouCompleteMe( user_options_store.GetAll() )
  133. pass
  134. def tearDown( self ):
  135. if self.server_state:
  136. self.server_state.OnVimLeave()
  137. @patch( 'vim.command', new_callable = ExtendedMock )
  138. def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ):
  139. # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
  140. # in combination with YouCompleteMe.OnFileReadyToParse when the completer
  141. # raises an exception handling FileReadyToParse event notification
  142. ERROR_TEXT = 'Some completer response text'
  143. def ErrorResponse( *args ):
  144. raise RuntimeError( ERROR_TEXT )
  145. with MockArbitraryBuffer( 'javascript' ):
  146. with MockEventNotification( ErrorResponse ):
  147. self.server_state.OnFileReadyToParse()
  148. assert self.server_state.FileParseRequestReady()
  149. self.server_state.HandleFileParseRequest()
  150. # The first call raises a warning
  151. vim_command.assert_has_exact_calls( [
  152. PostVimMessage_Call( ERROR_TEXT ),
  153. ] )
  154. # Subsequent calls don't re-raise the warning
  155. self.server_state.HandleFileParseRequest()
  156. vim_command.assert_has_exact_calls( [
  157. PostVimMessage_Call( ERROR_TEXT ),
  158. ] )
  159. # But it does if a subsequent event raises again
  160. self.server_state.OnFileReadyToParse()
  161. assert self.server_state.FileParseRequestReady()
  162. self.server_state.HandleFileParseRequest()
  163. vim_command.assert_has_exact_calls( [
  164. PostVimMessage_Call( ERROR_TEXT ),
  165. PostVimMessage_Call( ERROR_TEXT ),
  166. ] )
  167. @patch( 'vim.command' )
  168. def FileReadyToParse_NonDiagnostic_Error_NonNative_test( self, vim_command ):
  169. with MockArbitraryBuffer( 'javascript' ):
  170. with MockEventNotification( None, False ):
  171. self.server_state.OnFileReadyToParse()
  172. self.server_state.HandleFileParseRequest()
  173. vim_command.assert_not_called()
  174. @patch( 'ycm.client.event_notification._LoadExtraConfFile',
  175. new_callable = ExtendedMock )
  176. @patch( 'ycm.client.event_notification._IgnoreExtraConfFile',
  177. new_callable = ExtendedMock )
  178. def FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test(
  179. self,
  180. ignore_extra_conf,
  181. load_extra_conf,
  182. *args ):
  183. # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
  184. # in combination with YouCompleteMe.OnFileReadyToParse when the completer
  185. # raises the (special) UnknownExtraConf exception
  186. FILE_NAME = 'a_file'
  187. MESSAGE = ( 'Found ' + FILE_NAME + '. Load? \n\n(Question can be '
  188. 'turned off with options, see YCM docs)' )
  189. def UnknownExtraConfResponse( *args ):
  190. raise UnknownExtraConf( FILE_NAME )
  191. with MockArbitraryBuffer( 'javascript' ):
  192. with MockEventNotification( UnknownExtraConfResponse ):
  193. # When the user accepts the extra conf, we load it
  194. with patch( 'ycm.vimsupport.PresentDialog',
  195. return_value = 0,
  196. new_callable = ExtendedMock ) as present_dialog:
  197. self.server_state.OnFileReadyToParse()
  198. assert self.server_state.FileParseRequestReady()
  199. self.server_state.HandleFileParseRequest()
  200. present_dialog.assert_has_exact_calls( [
  201. PresentDialog_Confirm_Call( MESSAGE ),
  202. ] )
  203. load_extra_conf.assert_has_exact_calls( [
  204. call( FILE_NAME ),
  205. ] )
  206. # Subsequent calls don't re-raise the warning
  207. self.server_state.HandleFileParseRequest()
  208. present_dialog.assert_has_exact_calls( [
  209. PresentDialog_Confirm_Call( MESSAGE )
  210. ] )
  211. load_extra_conf.assert_has_exact_calls( [
  212. call( FILE_NAME ),
  213. ] )
  214. # But it does if a subsequent event raises again
  215. self.server_state.OnFileReadyToParse()
  216. assert self.server_state.FileParseRequestReady()
  217. self.server_state.HandleFileParseRequest()
  218. present_dialog.assert_has_exact_calls( [
  219. PresentDialog_Confirm_Call( MESSAGE ),
  220. PresentDialog_Confirm_Call( MESSAGE ),
  221. ] )
  222. load_extra_conf.assert_has_exact_calls( [
  223. call( FILE_NAME ),
  224. call( FILE_NAME ),
  225. ] )
  226. # When the user rejects the extra conf, we reject it
  227. with patch( 'ycm.vimsupport.PresentDialog',
  228. return_value = 1,
  229. new_callable = ExtendedMock ) as present_dialog:
  230. self.server_state.OnFileReadyToParse()
  231. assert self.server_state.FileParseRequestReady()
  232. self.server_state.HandleFileParseRequest()
  233. present_dialog.assert_has_exact_calls( [
  234. PresentDialog_Confirm_Call( MESSAGE ),
  235. ] )
  236. ignore_extra_conf.assert_has_exact_calls( [
  237. call( FILE_NAME ),
  238. ] )
  239. # Subsequent calls don't re-raise the warning
  240. self.server_state.HandleFileParseRequest()
  241. present_dialog.assert_has_exact_calls( [
  242. PresentDialog_Confirm_Call( MESSAGE )
  243. ] )
  244. ignore_extra_conf.assert_has_exact_calls( [
  245. call( FILE_NAME ),
  246. ] )
  247. # But it does if a subsequent event raises again
  248. self.server_state.OnFileReadyToParse()
  249. assert self.server_state.FileParseRequestReady()
  250. self.server_state.HandleFileParseRequest()
  251. present_dialog.assert_has_exact_calls( [
  252. PresentDialog_Confirm_Call( MESSAGE ),
  253. PresentDialog_Confirm_Call( MESSAGE ),
  254. ] )
  255. ignore_extra_conf.assert_has_exact_calls( [
  256. call( FILE_NAME ),
  257. call( FILE_NAME ),
  258. ] )
  259. def FileReadyToParse_Diagnostic_Error_Native_test( self ):
  260. self._Check_FileReadyToParse_Diagnostic_Error()
  261. self._Check_FileReadyToParse_Diagnostic_Warning()
  262. self._Check_FileReadyToParse_Diagnostic_Clean()
  263. @patch( 'vim.command' )
  264. def _Check_FileReadyToParse_Diagnostic_Error( self, vim_command ):
  265. # Tests Vim sign placement and error/warning count python API
  266. # when one error is returned.
  267. def DiagnosticResponse( *args ):
  268. start = Location( 1, 2, 'TEST_BUFFER' )
  269. end = Location( 1, 4, 'TEST_BUFFER' )
  270. extent = Range( start, end )
  271. diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
  272. return [ BuildDiagnosticData( diagnostic ) ]
  273. with MockArbitraryBuffer( 'cpp' ):
  274. with MockEventNotification( DiagnosticResponse ):
  275. self.server_state.OnFileReadyToParse()
  276. ok_( self.server_state.FileParseRequestReady() )
  277. self.server_state.HandleFileParseRequest()
  278. vim_command.assert_has_calls( [
  279. PlaceSign_Call( 1, 1, 0, True )
  280. ] )
  281. eq_( self.server_state.GetErrorCount(), 1 )
  282. eq_( self.server_state.GetWarningCount(), 0 )
  283. # Consequent calls to HandleFileParseRequest shouldn't mess with
  284. # existing diagnostics, when there is no new parse request.
  285. vim_command.reset_mock()
  286. ok_( not self.server_state.FileParseRequestReady() )
  287. self.server_state.HandleFileParseRequest()
  288. vim_command.assert_not_called()
  289. eq_( self.server_state.GetErrorCount(), 1 )
  290. eq_( self.server_state.GetWarningCount(), 0 )
  291. @patch( 'vim.command' )
  292. def _Check_FileReadyToParse_Diagnostic_Warning( self, vim_command ):
  293. # Tests Vim sign placement/unplacement and error/warning count python API
  294. # when one warning is returned.
  295. # Should be called after _Check_FileReadyToParse_Diagnostic_Error
  296. def DiagnosticResponse( *args ):
  297. start = Location( 2, 2, 'TEST_BUFFER' )
  298. end = Location( 2, 4, 'TEST_BUFFER' )
  299. extent = Range( start, end )
  300. diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
  301. return [ BuildDiagnosticData( diagnostic ) ]
  302. with MockArbitraryBuffer( 'cpp' ):
  303. with MockEventNotification( DiagnosticResponse ):
  304. self.server_state.OnFileReadyToParse()
  305. ok_( self.server_state.FileParseRequestReady() )
  306. self.server_state.HandleFileParseRequest()
  307. vim_command.assert_has_calls( [
  308. PlaceSign_Call( 2, 2, 0, False ),
  309. UnplaceSign_Call( 1, 0 )
  310. ] )
  311. eq_( self.server_state.GetErrorCount(), 0 )
  312. eq_( self.server_state.GetWarningCount(), 1 )
  313. # Consequent calls to HandleFileParseRequest shouldn't mess with
  314. # existing diagnostics, when there is no new parse request.
  315. vim_command.reset_mock()
  316. ok_( not self.server_state.FileParseRequestReady() )
  317. self.server_state.HandleFileParseRequest()
  318. vim_command.assert_not_called()
  319. eq_( self.server_state.GetErrorCount(), 0 )
  320. eq_( self.server_state.GetWarningCount(), 1 )
  321. @patch( 'vim.command' )
  322. def _Check_FileReadyToParse_Diagnostic_Clean( self, vim_command ):
  323. # Tests Vim sign unplacement and error/warning count python API
  324. # when there are no errors/warnings left.
  325. # Should be called after _Check_FileReadyToParse_Diagnostic_Warning
  326. with MockArbitraryBuffer( 'cpp' ):
  327. with MockEventNotification( MagicMock( return_value = [] ) ):
  328. self.server_state.OnFileReadyToParse()
  329. self.server_state.HandleFileParseRequest()
  330. vim_command.assert_has_calls( [
  331. UnplaceSign_Call( 2, 0 )
  332. ] )
  333. eq_( self.server_state.GetErrorCount(), 0 )
  334. eq_( self.server_state.GetWarningCount(), 0 )