event_notification_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. # coding: utf-8
  2. #
  3. # Copyright (C) 2015-2016 YouCompleteMe contributors
  4. #
  5. # This file is part of YouCompleteMe.
  6. #
  7. # YouCompleteMe is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # YouCompleteMe is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  19. from __future__ import unicode_literals
  20. from __future__ import print_function
  21. from __future__ import division
  22. from __future__ import absolute_import
  23. # Not installing aliases from python-future; it's unreliable and slow.
  24. from builtins import * # noqa
  25. from ycm.tests.test_utils import ( CurrentWorkingDirectory, ExtendedMock,
  26. MockVimBuffers, MockVimModule, VimBuffer )
  27. MockVimModule()
  28. import contextlib
  29. import os
  30. from ycm.tests import PathToTestFile, YouCompleteMeInstance
  31. from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range,
  32. UnknownExtraConf, ServerError )
  33. from hamcrest import assert_that, contains, has_entries, has_item
  34. from mock import call, MagicMock, patch
  35. from nose.tools import eq_, ok_
  36. def PresentDialog_Confirm_Call( message ):
  37. """Return a mock.call object for a call to vimsupport.PresentDialog, as called
  38. why vimsupport.Confirm with the supplied confirmation message"""
  39. return call( message, [ 'Ok', 'Cancel' ] )
  40. def PlaceSign_Call( sign_id, line_num, buffer_num, is_error ):
  41. sign_name = 'YcmError' if is_error else 'YcmWarning'
  42. return call( 'sign place {0} line={1} name={2} buffer={3}'
  43. .format( sign_id, line_num, sign_name, buffer_num ) )
  44. def UnplaceSign_Call( sign_id, buffer_num ):
  45. return call( 'try | exec "sign unplace {0} buffer={1}" |'
  46. ' catch /E158/ | endtry'.format( sign_id, buffer_num ) )
  47. @contextlib.contextmanager
  48. def MockArbitraryBuffer( filetype ):
  49. """Used via the with statement, set up a single buffer with an arbitrary name
  50. and no contents. Its filetype is set to the supplied filetype."""
  51. # Arbitrary, but valid, single buffer open.
  52. current_buffer = VimBuffer( os.path.realpath( 'TEST_BUFFER' ),
  53. window = 1,
  54. filetype = filetype )
  55. with MockVimBuffers( [ current_buffer ], current_buffer ):
  56. yield
  57. @contextlib.contextmanager
  58. def MockEventNotification( response_method, native_filetype_completer = True ):
  59. """Mock out the EventNotification client request object, replacing the
  60. Response handler's JsonFromFuture with the supplied |response_method|.
  61. Additionally mock out YouCompleteMe's FiletypeCompleterExistsForFiletype
  62. method to return the supplied |native_filetype_completer| parameter, rather
  63. than querying the server"""
  64. # We don't want the event to actually be sent to the server, just have it
  65. # return success
  66. with patch( 'ycm.client.base_request.BaseRequest.PostDataToHandlerAsync',
  67. return_value = MagicMock( return_value=True ) ):
  68. # We set up a fake a Response (as called by EventNotification.Response)
  69. # which calls the supplied callback method. Generally this callback just
  70. # raises an apropriate exception, otherwise it would have to return a mock
  71. # future object.
  72. #
  73. # Note: JsonFromFuture is actually part of ycm.client.base_request, but we
  74. # must patch where an object is looked up, not where it is defined.
  75. # See https://docs.python.org/dev/library/unittest.mock.html#where-to-patch
  76. # for details.
  77. with patch( 'ycm.client.event_notification.JsonFromFuture',
  78. side_effect = response_method ):
  79. # Filetype available information comes from the server, so rather than
  80. # relying on that request, we mock out the check. The caller decides if
  81. # filetype completion is available
  82. with patch(
  83. 'ycm.youcompleteme.YouCompleteMe.FiletypeCompleterExistsForFiletype',
  84. return_value = native_filetype_completer ):
  85. yield
  86. @patch( 'ycm.vimsupport.PostVimMessage', new_callable = ExtendedMock )
  87. @YouCompleteMeInstance()
  88. def EventNotification_FileReadyToParse_NonDiagnostic_Error_test(
  89. ycm, post_vim_message ):
  90. # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
  91. # in combination with YouCompleteMe.OnFileReadyToParse when the completer
  92. # raises an exception handling FileReadyToParse event notification
  93. ERROR_TEXT = 'Some completer response text'
  94. def ErrorResponse( *args ):
  95. raise ServerError( ERROR_TEXT )
  96. with MockArbitraryBuffer( 'javascript' ):
  97. with MockEventNotification( ErrorResponse ):
  98. ycm.OnFileReadyToParse()
  99. ok_( ycm.FileParseRequestReady() )
  100. ycm.HandleFileParseRequest()
  101. # The first call raises a warning
  102. post_vim_message.assert_has_exact_calls( [
  103. call( ERROR_TEXT, truncate = True )
  104. ] )
  105. # Subsequent calls don't re-raise the warning
  106. ycm.HandleFileParseRequest()
  107. post_vim_message.assert_has_exact_calls( [
  108. call( ERROR_TEXT, truncate = True )
  109. ] )
  110. # But it does if a subsequent event raises again
  111. ycm.OnFileReadyToParse()
  112. ok_( ycm.FileParseRequestReady() )
  113. ycm.HandleFileParseRequest()
  114. post_vim_message.assert_has_exact_calls( [
  115. call( ERROR_TEXT, truncate = True ),
  116. call( ERROR_TEXT, truncate = True )
  117. ] )
  118. @patch( 'vim.command' )
  119. @YouCompleteMeInstance()
  120. def EventNotification_FileReadyToParse_NonDiagnostic_Error_NonNative_test(
  121. ycm, vim_command ):
  122. with MockArbitraryBuffer( 'javascript' ):
  123. with MockEventNotification( None, False ):
  124. ycm.OnFileReadyToParse()
  125. ycm.HandleFileParseRequest()
  126. vim_command.assert_not_called()
  127. @patch( 'ycm.client.base_request._LoadExtraConfFile',
  128. new_callable = ExtendedMock )
  129. @patch( 'ycm.client.base_request._IgnoreExtraConfFile',
  130. new_callable = ExtendedMock )
  131. @YouCompleteMeInstance()
  132. def EventNotification_FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test(
  133. ycm, ignore_extra_conf, load_extra_conf ):
  134. # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
  135. # in combination with YouCompleteMe.OnFileReadyToParse when the completer
  136. # raises the (special) UnknownExtraConf exception
  137. FILE_NAME = 'a_file'
  138. MESSAGE = ( 'Found ' + FILE_NAME + '. Load? \n\n(Question can be '
  139. 'turned off with options, see YCM docs)' )
  140. def UnknownExtraConfResponse( *args ):
  141. raise UnknownExtraConf( FILE_NAME )
  142. with MockArbitraryBuffer( 'javascript' ):
  143. with MockEventNotification( UnknownExtraConfResponse ):
  144. # When the user accepts the extra conf, we load it
  145. with patch( 'ycm.vimsupport.PresentDialog',
  146. return_value = 0,
  147. new_callable = ExtendedMock ) as present_dialog:
  148. ycm.OnFileReadyToParse()
  149. ok_( ycm.FileParseRequestReady() )
  150. ycm.HandleFileParseRequest()
  151. present_dialog.assert_has_exact_calls( [
  152. PresentDialog_Confirm_Call( MESSAGE ),
  153. ] )
  154. load_extra_conf.assert_has_exact_calls( [
  155. call( FILE_NAME ),
  156. ] )
  157. # Subsequent calls don't re-raise the warning
  158. ycm.HandleFileParseRequest()
  159. present_dialog.assert_has_exact_calls( [
  160. PresentDialog_Confirm_Call( MESSAGE )
  161. ] )
  162. load_extra_conf.assert_has_exact_calls( [
  163. call( FILE_NAME ),
  164. ] )
  165. # But it does if a subsequent event raises again
  166. ycm.OnFileReadyToParse()
  167. ok_( ycm.FileParseRequestReady() )
  168. ycm.HandleFileParseRequest()
  169. present_dialog.assert_has_exact_calls( [
  170. PresentDialog_Confirm_Call( MESSAGE ),
  171. PresentDialog_Confirm_Call( MESSAGE ),
  172. ] )
  173. load_extra_conf.assert_has_exact_calls( [
  174. call( FILE_NAME ),
  175. call( FILE_NAME ),
  176. ] )
  177. # When the user rejects the extra conf, we reject it
  178. with patch( 'ycm.vimsupport.PresentDialog',
  179. return_value = 1,
  180. new_callable = ExtendedMock ) as present_dialog:
  181. ycm.OnFileReadyToParse()
  182. ok_( ycm.FileParseRequestReady() )
  183. ycm.HandleFileParseRequest()
  184. present_dialog.assert_has_exact_calls( [
  185. PresentDialog_Confirm_Call( MESSAGE ),
  186. ] )
  187. ignore_extra_conf.assert_has_exact_calls( [
  188. call( FILE_NAME ),
  189. ] )
  190. # Subsequent calls don't re-raise the warning
  191. ycm.HandleFileParseRequest()
  192. present_dialog.assert_has_exact_calls( [
  193. PresentDialog_Confirm_Call( MESSAGE )
  194. ] )
  195. ignore_extra_conf.assert_has_exact_calls( [
  196. call( FILE_NAME ),
  197. ] )
  198. # But it does if a subsequent event raises again
  199. ycm.OnFileReadyToParse()
  200. ok_( ycm.FileParseRequestReady() )
  201. ycm.HandleFileParseRequest()
  202. present_dialog.assert_has_exact_calls( [
  203. PresentDialog_Confirm_Call( MESSAGE ),
  204. PresentDialog_Confirm_Call( MESSAGE ),
  205. ] )
  206. ignore_extra_conf.assert_has_exact_calls( [
  207. call( FILE_NAME ),
  208. call( FILE_NAME ),
  209. ] )
  210. @YouCompleteMeInstance()
  211. def EventNotification_FileReadyToParse_Diagnostic_Error_Native_test( ycm ):
  212. _Check_FileReadyToParse_Diagnostic_Error( ycm )
  213. _Check_FileReadyToParse_Diagnostic_Warning( ycm )
  214. _Check_FileReadyToParse_Diagnostic_Clean( ycm )
  215. @patch( 'vim.command' )
  216. def _Check_FileReadyToParse_Diagnostic_Error( ycm, vim_command ):
  217. # Tests Vim sign placement and error/warning count python API
  218. # when one error is returned.
  219. def DiagnosticResponse( *args ):
  220. start = Location( 1, 2, 'TEST_BUFFER' )
  221. end = Location( 1, 4, 'TEST_BUFFER' )
  222. extent = Range( start, end )
  223. diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
  224. return [ BuildDiagnosticData( diagnostic ) ]
  225. with MockArbitraryBuffer( 'cpp' ):
  226. with MockEventNotification( DiagnosticResponse ):
  227. ycm.OnFileReadyToParse()
  228. ok_( ycm.FileParseRequestReady() )
  229. ycm.HandleFileParseRequest()
  230. vim_command.assert_has_calls( [
  231. PlaceSign_Call( 1, 1, 1, True )
  232. ] )
  233. eq_( ycm.GetErrorCount(), 1 )
  234. eq_( ycm.GetWarningCount(), 0 )
  235. # Consequent calls to HandleFileParseRequest shouldn't mess with
  236. # existing diagnostics, when there is no new parse request.
  237. vim_command.reset_mock()
  238. ok_( not ycm.FileParseRequestReady() )
  239. ycm.HandleFileParseRequest()
  240. vim_command.assert_not_called()
  241. eq_( ycm.GetErrorCount(), 1 )
  242. eq_( ycm.GetWarningCount(), 0 )
  243. @patch( 'vim.command' )
  244. def _Check_FileReadyToParse_Diagnostic_Warning( ycm, vim_command ):
  245. # Tests Vim sign placement/unplacement and error/warning count python API
  246. # when one warning is returned.
  247. # Should be called after _Check_FileReadyToParse_Diagnostic_Error
  248. def DiagnosticResponse( *args ):
  249. start = Location( 2, 2, 'TEST_BUFFER' )
  250. end = Location( 2, 4, 'TEST_BUFFER' )
  251. extent = Range( start, end )
  252. diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
  253. return [ BuildDiagnosticData( diagnostic ) ]
  254. with MockArbitraryBuffer( 'cpp' ):
  255. with MockEventNotification( DiagnosticResponse ):
  256. ycm.OnFileReadyToParse()
  257. ok_( ycm.FileParseRequestReady() )
  258. ycm.HandleFileParseRequest()
  259. vim_command.assert_has_calls( [
  260. PlaceSign_Call( 2, 2, 1, False ),
  261. UnplaceSign_Call( 1, 1 )
  262. ] )
  263. eq_( ycm.GetErrorCount(), 0 )
  264. eq_( ycm.GetWarningCount(), 1 )
  265. # Consequent calls to HandleFileParseRequest shouldn't mess with
  266. # existing diagnostics, when there is no new parse request.
  267. vim_command.reset_mock()
  268. ok_( not ycm.FileParseRequestReady() )
  269. ycm.HandleFileParseRequest()
  270. vim_command.assert_not_called()
  271. eq_( ycm.GetErrorCount(), 0 )
  272. eq_( ycm.GetWarningCount(), 1 )
  273. @patch( 'vim.command' )
  274. def _Check_FileReadyToParse_Diagnostic_Clean( ycm, vim_command ):
  275. # Tests Vim sign unplacement and error/warning count python API
  276. # when there are no errors/warnings left.
  277. # Should be called after _Check_FileReadyToParse_Diagnostic_Warning
  278. with MockArbitraryBuffer( 'cpp' ):
  279. with MockEventNotification( MagicMock( return_value = [] ) ):
  280. ycm.OnFileReadyToParse()
  281. ycm.HandleFileParseRequest()
  282. vim_command.assert_has_calls( [
  283. UnplaceSign_Call( 2, 1 )
  284. ] )
  285. eq_( ycm.GetErrorCount(), 0 )
  286. eq_( ycm.GetWarningCount(), 0 )
  287. @patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' )
  288. @YouCompleteMeInstance( { 'collect_identifiers_from_tags_files': 1 } )
  289. def EventNotification_FileReadyToParse_TagFiles_UnicodeWorkingDirectory_test(
  290. ycm, *args ):
  291. unicode_dir = PathToTestFile( 'uni¢𐍈d€' )
  292. current_buffer_file = PathToTestFile( 'uni¢𐍈d€', 'current_buffer' )
  293. current_buffer = VimBuffer( name = current_buffer_file,
  294. contents = [ 'current_buffer_contents' ],
  295. filetype = 'some_filetype' )
  296. with patch( 'ycm.client.base_request.BaseRequest.'
  297. 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
  298. with CurrentWorkingDirectory( unicode_dir ):
  299. with MockVimBuffers( [ current_buffer ], current_buffer, ( 6, 5 ) ):
  300. ycm.OnFileReadyToParse()
  301. assert_that(
  302. # Positional arguments passed to PostDataToHandlerAsync.
  303. post_data_to_handler_async.call_args[ 0 ],
  304. contains(
  305. has_entries( {
  306. 'filepath': current_buffer_file,
  307. 'line_num': 6,
  308. 'column_num': 6,
  309. 'file_data': has_entries( {
  310. current_buffer_file: has_entries( {
  311. 'contents': 'current_buffer_contents\n',
  312. 'filetypes': [ 'some_filetype' ]
  313. } )
  314. } ),
  315. 'event_name': 'FileReadyToParse',
  316. 'tag_files': has_item( PathToTestFile( 'uni¢𐍈d€', 'tags' ) )
  317. } ),
  318. 'event_notification'
  319. )
  320. )
  321. @patch( 'ycm.youcompleteme.YouCompleteMe._AddUltiSnipsDataIfNeeded' )
  322. @YouCompleteMeInstance()
  323. def EventNotification_BufferVisit_BuildRequestForCurrentAndUnsavedBuffers_test(
  324. ycm, *args ):
  325. current_buffer_file = os.path.realpath( 'current_buffer' )
  326. current_buffer = VimBuffer( name = current_buffer_file,
  327. number = 1,
  328. contents = [ 'current_buffer_contents' ],
  329. filetype = 'some_filetype',
  330. modified = False )
  331. modified_buffer_file = os.path.realpath( 'modified_buffer' )
  332. modified_buffer = VimBuffer( name = modified_buffer_file,
  333. number = 2,
  334. contents = [ 'modified_buffer_contents' ],
  335. filetype = 'some_filetype',
  336. modified = True )
  337. unmodified_buffer_file = os.path.realpath( 'unmodified_buffer' )
  338. unmodified_buffer = VimBuffer( name = unmodified_buffer_file,
  339. number = 3,
  340. contents = [ 'unmodified_buffer_contents' ],
  341. filetype = 'some_filetype',
  342. modified = False )
  343. with patch( 'ycm.client.base_request.BaseRequest.'
  344. 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
  345. with MockVimBuffers( [ current_buffer, modified_buffer, unmodified_buffer ],
  346. current_buffer,
  347. ( 3, 5 ) ):
  348. ycm.OnBufferVisit()
  349. assert_that(
  350. # Positional arguments passed to PostDataToHandlerAsync.
  351. post_data_to_handler_async.call_args[ 0 ],
  352. contains(
  353. has_entries( {
  354. 'filepath': current_buffer_file,
  355. 'line_num': 3,
  356. 'column_num': 6,
  357. 'file_data': has_entries( {
  358. current_buffer_file: has_entries( {
  359. 'contents': 'current_buffer_contents\n',
  360. 'filetypes': [ 'some_filetype' ]
  361. } ),
  362. modified_buffer_file: has_entries( {
  363. 'contents': 'modified_buffer_contents\n',
  364. 'filetypes': [ 'some_filetype' ]
  365. } )
  366. } ),
  367. 'event_name': 'BufferVisit'
  368. } ),
  369. 'event_notification'
  370. )
  371. )
  372. @YouCompleteMeInstance()
  373. def EventNotification_BufferUnload_BuildRequestForDeletedAndUnsavedBuffers_test(
  374. ycm ):
  375. current_buffer_file = os.path.realpath( 'current_buffer' )
  376. current_buffer = VimBuffer( name = current_buffer_file,
  377. number = 1,
  378. contents = [ 'current_buffer_contents' ],
  379. filetype = 'some_filetype',
  380. modified = True )
  381. deleted_buffer_file = os.path.realpath( 'deleted_buffer' )
  382. deleted_buffer = VimBuffer( name = deleted_buffer_file,
  383. number = 2,
  384. contents = [ 'deleted_buffer_contents' ],
  385. filetype = 'some_filetype',
  386. modified = False )
  387. with patch( 'ycm.client.base_request.BaseRequest.'
  388. 'PostDataToHandlerAsync' ) as post_data_to_handler_async:
  389. with MockVimBuffers( [ current_buffer, deleted_buffer ], current_buffer ):
  390. ycm.OnBufferUnload( deleted_buffer_file )
  391. assert_that(
  392. # Positional arguments passed to PostDataToHandlerAsync.
  393. post_data_to_handler_async.call_args[ 0 ],
  394. contains(
  395. has_entries( {
  396. 'filepath': deleted_buffer_file,
  397. 'line_num': 1,
  398. 'column_num': 1,
  399. 'file_data': has_entries( {
  400. current_buffer_file: has_entries( {
  401. 'contents': 'current_buffer_contents\n',
  402. 'filetypes': [ 'some_filetype' ]
  403. } ),
  404. deleted_buffer_file: has_entries( {
  405. 'contents': 'deleted_buffer_contents\n',
  406. 'filetypes': [ 'some_filetype' ]
  407. } )
  408. } ),
  409. 'event_name': 'BufferUnload'
  410. } ),
  411. 'event_notification'
  412. )
  413. )