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