dict_notifications_spec.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. local t = require('test.testutil')
  2. local n = require('test.functional.testnvim')()
  3. local assert_alive = n.assert_alive
  4. local clear, source = n.clear, n.source
  5. local api = n.api
  6. local insert = n.insert
  7. local eq, next_msg = t.eq, n.next_msg
  8. local exc_exec = n.exc_exec
  9. local exec_lua = n.exec_lua
  10. local command = n.command
  11. local eval = n.eval
  12. describe('Vimscript dictionary notifications', function()
  13. local channel
  14. before_each(function()
  15. clear()
  16. channel = api.nvim_get_chan_info(0).id
  17. api.nvim_set_var('channel', channel)
  18. end)
  19. -- the same set of tests are applied to top-level dictionaries(g:, b:, w: and
  20. -- t:) and a dictionary variable, so we generate them in the following
  21. -- function.
  22. local function gentests(dict_expr, dict_init)
  23. local is_g = dict_expr == 'g:'
  24. local function update(opval, key)
  25. if not key then
  26. key = 'watched'
  27. end
  28. if opval == '' then
  29. command(("unlet %s['%s']"):format(dict_expr, key))
  30. else
  31. command(("let %s['%s'] %s"):format(dict_expr, key, opval))
  32. end
  33. end
  34. local function update_with_api(opval, key)
  35. if not key then
  36. key = 'watched'
  37. end
  38. if opval == '' then
  39. exec_lua(("vim.api.nvim_del_var('%s')"):format(key))
  40. else
  41. exec_lua(("vim.api.nvim_set_var('%s', %s)"):format(key, opval))
  42. end
  43. end
  44. local function update_with_vim_g(opval, key)
  45. if not key then
  46. key = 'watched'
  47. end
  48. if opval == '' then
  49. exec_lua(('vim.g.%s = nil'):format(key))
  50. else
  51. exec_lua(('vim.g.%s %s'):format(key, opval))
  52. end
  53. end
  54. local function verify_echo()
  55. -- helper to verify that no notifications are sent after certain change
  56. -- to a dict
  57. command("call rpcnotify(g:channel, 'echo')")
  58. eq({ 'notification', 'echo', {} }, next_msg())
  59. end
  60. local function verify_value(vals, key)
  61. if not key then
  62. key = 'watched'
  63. end
  64. eq({ 'notification', 'values', { key, vals } }, next_msg())
  65. end
  66. describe(dict_expr .. ' watcher', function()
  67. if dict_init then
  68. before_each(function()
  69. source(dict_init)
  70. end)
  71. end
  72. before_each(function()
  73. source([[
  74. function! g:Changed(dict, key, value)
  75. if a:dict isnot ]] .. dict_expr .. [[ |
  76. throw 'invalid dict'
  77. endif
  78. call rpcnotify(g:channel, 'values', a:key, a:value)
  79. endfunction
  80. call dictwatcheradd(]] .. dict_expr .. [[, "watched", "g:Changed")
  81. call dictwatcheradd(]] .. dict_expr .. [[, "watched2", "g:Changed")
  82. ]])
  83. end)
  84. after_each(function()
  85. source([[
  86. call dictwatcherdel(]] .. dict_expr .. [[, "watched", "g:Changed")
  87. call dictwatcherdel(]] .. dict_expr .. [[, "watched2", "g:Changed")
  88. ]])
  89. update('= "test"')
  90. update('= "test2"', 'watched2')
  91. update('', 'watched2')
  92. update('')
  93. verify_echo()
  94. if is_g then
  95. update_with_api('"test"')
  96. update_with_api('"test2"', 'watched2')
  97. update_with_api('', 'watched2')
  98. update_with_api('')
  99. verify_echo()
  100. update_with_vim_g('= "test"')
  101. update_with_vim_g('= "test2"', 'watched2')
  102. update_with_vim_g('', 'watched2')
  103. update_with_vim_g('')
  104. verify_echo()
  105. end
  106. end)
  107. it('is not triggered when unwatched keys are updated', function()
  108. update('= "noop"', 'unwatched')
  109. update('.= "noop2"', 'unwatched')
  110. update('', 'unwatched')
  111. verify_echo()
  112. if is_g then
  113. update_with_api('"noop"', 'unwatched')
  114. update_with_api('vim.g.unwatched .. "noop2"', 'unwatched')
  115. update_with_api('', 'unwatched')
  116. verify_echo()
  117. update_with_vim_g('= "noop"', 'unwatched')
  118. update_with_vim_g('= vim.g.unwatched .. "noop2"', 'unwatched')
  119. update_with_vim_g('', 'unwatched')
  120. verify_echo()
  121. end
  122. end)
  123. it('is triggered by remove()', function()
  124. update('= "test"')
  125. verify_value({ new = 'test' })
  126. command('call remove(' .. dict_expr .. ', "watched")')
  127. verify_value({ old = 'test' })
  128. end)
  129. if is_g then
  130. it('is triggered by remove() when updated with nvim_*_var', function()
  131. update_with_api('"test"')
  132. verify_value({ new = 'test' })
  133. command('call remove(' .. dict_expr .. ', "watched")')
  134. verify_value({ old = 'test' })
  135. end)
  136. it('is triggered by remove() when updated with vim.g', function()
  137. update_with_vim_g('= "test"')
  138. verify_value({ new = 'test' })
  139. command('call remove(' .. dict_expr .. ', "watched")')
  140. verify_value({ old = 'test' })
  141. end)
  142. end
  143. it('is triggered by extend()', function()
  144. update('= "xtend"')
  145. verify_value({ new = 'xtend' })
  146. command([[
  147. call extend(]] .. dict_expr .. [[, {'watched': 'xtend2', 'watched2': 5, 'watched3': 'a'})
  148. ]])
  149. verify_value({ old = 'xtend', new = 'xtend2' })
  150. verify_value({ new = 5 }, 'watched2')
  151. update('')
  152. verify_value({ old = 'xtend2' })
  153. update('', 'watched2')
  154. verify_value({ old = 5 }, 'watched2')
  155. update('', 'watched3')
  156. verify_echo()
  157. end)
  158. it('is triggered with key patterns', function()
  159. source([[
  160. call dictwatcheradd(]] .. dict_expr .. [[, "wat*", "g:Changed")
  161. ]])
  162. update('= 1')
  163. verify_value({ new = 1 })
  164. verify_value({ new = 1 })
  165. update('= 3', 'watched2')
  166. verify_value({ new = 3 }, 'watched2')
  167. verify_value({ new = 3 }, 'watched2')
  168. verify_echo()
  169. source([[
  170. call dictwatcherdel(]] .. dict_expr .. [[, "wat*", "g:Changed")
  171. ]])
  172. -- watch every key pattern
  173. source([[
  174. call dictwatcheradd(]] .. dict_expr .. [[, "*", "g:Changed")
  175. ]])
  176. update('= 3', 'another_key')
  177. update('= 4', 'another_key')
  178. update('', 'another_key')
  179. update('= 2')
  180. verify_value({ new = 3 }, 'another_key')
  181. verify_value({ old = 3, new = 4 }, 'another_key')
  182. verify_value({ old = 4 }, 'another_key')
  183. verify_value({ old = 1, new = 2 })
  184. verify_value({ old = 1, new = 2 })
  185. verify_echo()
  186. source([[
  187. call dictwatcherdel(]] .. dict_expr .. [[, "*", "g:Changed")
  188. ]])
  189. end)
  190. it('is triggered for empty keys', function()
  191. command([[
  192. call dictwatcheradd(]] .. dict_expr .. [[, "", "g:Changed")
  193. ]])
  194. update('= 1', '')
  195. verify_value({ new = 1 }, '')
  196. update('= 2', '')
  197. verify_value({ old = 1, new = 2 }, '')
  198. command([[
  199. call dictwatcherdel(]] .. dict_expr .. [[, "", "g:Changed")
  200. ]])
  201. end)
  202. it('is triggered for empty keys when using catch-all *', function()
  203. command([[
  204. call dictwatcheradd(]] .. dict_expr .. [[, "*", "g:Changed")
  205. ]])
  206. update('= 1', '')
  207. verify_value({ new = 1 }, '')
  208. update('= 2', '')
  209. verify_value({ old = 1, new = 2 }, '')
  210. command([[
  211. call dictwatcherdel(]] .. dict_expr .. [[, "*", "g:Changed")
  212. ]])
  213. end)
  214. -- test a sequence of updates of different types to ensure proper memory
  215. -- management(with ASAN)
  216. local function test_updates(tests)
  217. it('test change sequence', function()
  218. local input, output
  219. for i = 1, #tests do
  220. input, output = unpack(tests[i])
  221. update(input)
  222. verify_value(output)
  223. end
  224. end)
  225. end
  226. test_updates({
  227. { '= 3', { new = 3 } },
  228. { '= 6', { old = 3, new = 6 } },
  229. { '+= 3', { old = 6, new = 9 } },
  230. { '', { old = 9 } },
  231. })
  232. test_updates({
  233. { '= "str"', { new = 'str' } },
  234. { '= "str2"', { old = 'str', new = 'str2' } },
  235. { '.= "2str"', { old = 'str2', new = 'str22str' } },
  236. { '', { old = 'str22str' } },
  237. })
  238. test_updates({
  239. { '= [1, 2]', { new = { 1, 2 } } },
  240. { '= [1, 2, 3]', { old = { 1, 2 }, new = { 1, 2, 3 } } },
  241. -- the += will update the list in place, so old and new are the same
  242. { '+= [4, 5]', { old = { 1, 2, 3, 4, 5 }, new = { 1, 2, 3, 4, 5 } } },
  243. { '', { old = { 1, 2, 3, 4, 5 } } },
  244. })
  245. test_updates({
  246. { '= {"k": "v"}', { new = { k = 'v' } } },
  247. { '= {"k1": 2}', { old = { k = 'v' }, new = { k1 = 2 } } },
  248. { '', { old = { k1 = 2 } } },
  249. })
  250. end)
  251. end
  252. gentests('g:')
  253. gentests('b:')
  254. gentests('w:')
  255. gentests('t:')
  256. gentests('g:dict_var', 'let g:dict_var = {}')
  257. describe('multiple watchers on the same dict/key', function()
  258. before_each(function()
  259. source([[
  260. function! g:Watcher1(dict, key, value)
  261. call rpcnotify(g:channel, '1', a:key, a:value)
  262. endfunction
  263. function! g:Watcher2(dict, key, value)
  264. call rpcnotify(g:channel, '2', a:key, a:value)
  265. endfunction
  266. call dictwatcheradd(g:, "key", "g:Watcher1")
  267. call dictwatcheradd(g:, "key", "g:Watcher2")
  268. ]])
  269. end)
  270. it('invokes all callbacks when the key is changed', function()
  271. command('let g:key = "value"')
  272. eq({ 'notification', '1', { 'key', { new = 'value' } } }, next_msg())
  273. eq({ 'notification', '2', { 'key', { new = 'value' } } }, next_msg())
  274. end)
  275. it('only removes watchers that fully match dict, key and callback', function()
  276. command('let g:key = "value"')
  277. eq({ 'notification', '1', { 'key', { new = 'value' } } }, next_msg())
  278. eq({ 'notification', '2', { 'key', { new = 'value' } } }, next_msg())
  279. command('call dictwatcherdel(g:, "key", "g:Watcher1")')
  280. command('let g:key = "v2"')
  281. eq({ 'notification', '2', { 'key', { old = 'value', new = 'v2' } } }, next_msg())
  282. end)
  283. end)
  284. it('errors out when adding to v:_null_dict', function()
  285. command([[
  286. function! g:Watcher1(dict, key, value)
  287. call rpcnotify(g:channel, '1', a:key, a:value)
  288. endfunction
  289. ]])
  290. eq(
  291. 'Vim(call):E46: Cannot change read-only variable "dictwatcheradd() argument"',
  292. exc_exec('call dictwatcheradd(v:_null_dict, "x", "g:Watcher1")')
  293. )
  294. end)
  295. describe('errors', function()
  296. before_each(function()
  297. source([[
  298. function! g:Watcher1(dict, key, value)
  299. call rpcnotify(g:channel, '1', a:key, a:value)
  300. endfunction
  301. function! g:Watcher2(dict, key, value)
  302. call rpcnotify(g:channel, '2', a:key, a:value)
  303. endfunction
  304. ]])
  305. end)
  306. -- WARNING: This suite depends on the above tests
  307. it('fails to remove if no watcher with matching callback is found', function()
  308. eq(
  309. "Vim(call):Couldn't find a watcher matching key and callback",
  310. exc_exec('call dictwatcherdel(g:, "key", "g:Watcher1")')
  311. )
  312. end)
  313. it('fails to remove if no watcher with matching key is found', function()
  314. eq(
  315. "Vim(call):Couldn't find a watcher matching key and callback",
  316. exc_exec('call dictwatcherdel(g:, "invalid_key", "g:Watcher2")')
  317. )
  318. end)
  319. it("does not fail to add/remove if the callback doesn't exist", function()
  320. command('call dictwatcheradd(g:, "key", "g:InvalidCb")')
  321. command('call dictwatcherdel(g:, "key", "g:InvalidCb")')
  322. end)
  323. it('fails to remove watcher from v:_null_dict', function()
  324. eq(
  325. "Vim(call):Couldn't find a watcher matching key and callback",
  326. exc_exec('call dictwatcherdel(v:_null_dict, "x", "g:Watcher2")')
  327. )
  328. end)
  329. --[[
  330. [ it("fails to add/remove if the callback doesn't exist", function()
  331. [ eq("Vim(call):Function g:InvalidCb doesn't exist",
  332. [ exc_exec('call dictwatcheradd(g:, "key", "g:InvalidCb")'))
  333. [ eq("Vim(call):Function g:InvalidCb doesn't exist",
  334. [ exc_exec('call dictwatcherdel(g:, "key", "g:InvalidCb")'))
  335. [ end)
  336. ]]
  337. it('does not fail to replace a watcher function', function()
  338. source([[
  339. let g:key = 'v2'
  340. call dictwatcheradd(g:, "key", "g:Watcher2")
  341. function! g:ReplaceWatcher2()
  342. function! g:Watcher2(dict, key, value)
  343. call rpcnotify(g:channel, '2b', a:key, a:value)
  344. endfunction
  345. endfunction
  346. ]])
  347. command('call g:ReplaceWatcher2()')
  348. command('let g:key = "value"')
  349. eq({ 'notification', '2b', { 'key', { old = 'v2', new = 'value' } } }, next_msg())
  350. end)
  351. it('does not crash when freeing a watched dictionary', function()
  352. source([[
  353. function! Watcher(dict, key, value)
  354. echo a:key string(a:value)
  355. endfunction
  356. function! MakeWatch()
  357. let d = {'foo': 'bar'}
  358. call dictwatcheradd(d, 'foo', function('Watcher'))
  359. endfunction
  360. ]])
  361. command('call MakeWatch()')
  362. assert_alive()
  363. end)
  364. end)
  365. describe('with lambdas', function()
  366. it('works correctly', function()
  367. source([[
  368. let d = {'foo': 'baz'}
  369. call dictwatcheradd(d, 'foo', {dict, key, value -> rpcnotify(g:channel, '2', key, value)})
  370. let d.foo = 'bar'
  371. ]])
  372. eq({ 'notification', '2', { 'foo', { old = 'baz', new = 'bar' } } }, next_msg())
  373. end)
  374. end)
  375. it('for b:changedtick', function()
  376. source([[
  377. function! OnTickChanged(dict, key, value)
  378. call rpcnotify(g:channel, 'SendChangeTick', a:key, a:value)
  379. endfunction
  380. call dictwatcheradd(b:, 'changedtick', 'OnTickChanged')
  381. ]])
  382. insert('t')
  383. eq({ 'notification', 'SendChangeTick', { 'changedtick', { old = 2, new = 3 } } }, next_msg())
  384. command([[call dictwatcherdel(b:, 'changedtick', 'OnTickChanged')]])
  385. insert('t')
  386. assert_alive()
  387. end)
  388. it('does not cause use-after-free when unletting from callback', function()
  389. source([[
  390. let g:called = 0
  391. function W(...) abort
  392. unlet g:d
  393. let g:called = 1
  394. endfunction
  395. let g:d = {}
  396. call dictwatcheradd(g:d, '*', function('W'))
  397. let g:d.foo = 123
  398. ]])
  399. eq(1, eval('g:called'))
  400. end)
  401. it('does not crash when using dictwatcherdel in callback', function()
  402. source([[
  403. let g:d = {}
  404. function! W1(...)
  405. " Delete current and following watcher.
  406. call dictwatcherdel(g:d, '*', function('W1'))
  407. call dictwatcherdel(g:d, '*', function('W2'))
  408. try
  409. call dictwatcherdel({}, 'meh', function('tr'))
  410. catch
  411. let g:exc = v:exception
  412. endtry
  413. endfunction
  414. call dictwatcheradd(g:d, '*', function('W1'))
  415. function! W2(...)
  416. endfunction
  417. call dictwatcheradd(g:d, '*', function('W2'))
  418. let g:d.foo = 23
  419. ]])
  420. eq(23, eval('g:d.foo'))
  421. eq("Vim(call):Couldn't find a watcher matching key and callback", eval('g:exc'))
  422. end)
  423. it('does not call watcher added in callback', function()
  424. source([[
  425. let g:d = {}
  426. let g:calls = []
  427. function! W1(...) abort
  428. call add(g:calls, 'W1')
  429. call dictwatcheradd(g:d, '*', function('W2'))
  430. endfunction
  431. function! W2(...) abort
  432. call add(g:calls, 'W2')
  433. endfunction
  434. call dictwatcheradd(g:d, '*', function('W1'))
  435. let g:d.foo = 23
  436. ]])
  437. eq(23, eval('g:d.foo'))
  438. eq({ 'W1' }, eval('g:calls'))
  439. end)
  440. it('calls watcher deleted in callback', function()
  441. source([[
  442. let g:d = {}
  443. let g:calls = []
  444. function! W1(...) abort
  445. call add(g:calls, "W1")
  446. call dictwatcherdel(g:d, '*', function('W2'))
  447. endfunction
  448. function! W2(...) abort
  449. call add(g:calls, "W2")
  450. endfunction
  451. call dictwatcheradd(g:d, '*', function('W1'))
  452. call dictwatcheradd(g:d, '*', function('W2'))
  453. let g:d.foo = 123
  454. unlet g:d
  455. let g:d = {}
  456. call dictwatcheradd(g:d, '*', function('W2'))
  457. call dictwatcheradd(g:d, '*', function('W1'))
  458. let g:d.foo = 123
  459. ]])
  460. eq(123, eval('g:d.foo'))
  461. eq({ 'W1', 'W2', 'W2', 'W1' }, eval('g:calls'))
  462. end)
  463. end)