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