123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- local t = require('test.testutil')
- local n = require('test.functional.testnvim')()
- local assert_alive = n.assert_alive
- local clear, source = n.clear, n.source
- local api = n.api
- local insert = n.insert
- local eq, next_msg = t.eq, n.next_msg
- local exc_exec = n.exc_exec
- local exec_lua = n.exec_lua
- local command = n.command
- local eval = n.eval
- describe('Vimscript dictionary notifications', function()
- local channel
- before_each(function()
- clear()
- channel = api.nvim_get_chan_info(0).id
- api.nvim_set_var('channel', channel)
- end)
- -- the same set of tests are applied to top-level dictionaries(g:, b:, w: and
- -- t:) and a dictionary variable, so we generate them in the following
- -- function.
- local function gentests(dict_expr, dict_init)
- local is_g = dict_expr == 'g:'
- local function update(opval, key)
- if not key then
- key = 'watched'
- end
- if opval == '' then
- command(("unlet %s['%s']"):format(dict_expr, key))
- else
- command(("let %s['%s'] %s"):format(dict_expr, key, opval))
- end
- end
- local function update_with_api(opval, key)
- if not key then
- key = 'watched'
- end
- if opval == '' then
- exec_lua(("vim.api.nvim_del_var('%s')"):format(key))
- else
- exec_lua(("vim.api.nvim_set_var('%s', %s)"):format(key, opval))
- end
- end
- local function update_with_vim_g(opval, key)
- if not key then
- key = 'watched'
- end
- if opval == '' then
- exec_lua(('vim.g.%s = nil'):format(key))
- else
- exec_lua(('vim.g.%s %s'):format(key, opval))
- end
- end
- local function verify_echo()
- -- helper to verify that no notifications are sent after certain change
- -- to a dict
- command("call rpcnotify(g:channel, 'echo')")
- eq({ 'notification', 'echo', {} }, next_msg())
- end
- local function verify_value(vals, key)
- if not key then
- key = 'watched'
- end
- eq({ 'notification', 'values', { key, vals } }, next_msg())
- end
- describe(dict_expr .. ' watcher', function()
- if dict_init then
- before_each(function()
- source(dict_init)
- end)
- end
- before_each(function()
- source([[
- function! g:Changed(dict, key, value)
- if a:dict isnot ]] .. dict_expr .. [[ |
- throw 'invalid dict'
- endif
- call rpcnotify(g:channel, 'values', a:key, a:value)
- endfunction
- call dictwatcheradd(]] .. dict_expr .. [[, "watched", "g:Changed")
- call dictwatcheradd(]] .. dict_expr .. [[, "watched2", "g:Changed")
- ]])
- end)
- after_each(function()
- source([[
- call dictwatcherdel(]] .. dict_expr .. [[, "watched", "g:Changed")
- call dictwatcherdel(]] .. dict_expr .. [[, "watched2", "g:Changed")
- ]])
- update('= "test"')
- update('= "test2"', 'watched2')
- update('', 'watched2')
- update('')
- verify_echo()
- if is_g then
- update_with_api('"test"')
- update_with_api('"test2"', 'watched2')
- update_with_api('', 'watched2')
- update_with_api('')
- verify_echo()
- update_with_vim_g('= "test"')
- update_with_vim_g('= "test2"', 'watched2')
- update_with_vim_g('', 'watched2')
- update_with_vim_g('')
- verify_echo()
- end
- end)
- it('is not triggered when unwatched keys are updated', function()
- update('= "noop"', 'unwatched')
- update('.= "noop2"', 'unwatched')
- update('', 'unwatched')
- verify_echo()
- if is_g then
- update_with_api('"noop"', 'unwatched')
- update_with_api('vim.g.unwatched .. "noop2"', 'unwatched')
- update_with_api('', 'unwatched')
- verify_echo()
- update_with_vim_g('= "noop"', 'unwatched')
- update_with_vim_g('= vim.g.unwatched .. "noop2"', 'unwatched')
- update_with_vim_g('', 'unwatched')
- verify_echo()
- end
- end)
- it('is triggered by remove()', function()
- update('= "test"')
- verify_value({ new = 'test' })
- command('call remove(' .. dict_expr .. ', "watched")')
- verify_value({ old = 'test' })
- end)
- if is_g then
- it('is triggered by remove() when updated with nvim_*_var', function()
- update_with_api('"test"')
- verify_value({ new = 'test' })
- command('call remove(' .. dict_expr .. ', "watched")')
- verify_value({ old = 'test' })
- end)
- it('is triggered by remove() when updated with vim.g', function()
- update_with_vim_g('= "test"')
- verify_value({ new = 'test' })
- command('call remove(' .. dict_expr .. ', "watched")')
- verify_value({ old = 'test' })
- end)
- end
- it('is triggered by extend()', function()
- update('= "xtend"')
- verify_value({ new = 'xtend' })
- command([[
- call extend(]] .. dict_expr .. [[, {'watched': 'xtend2', 'watched2': 5, 'watched3': 'a'})
- ]])
- verify_value({ old = 'xtend', new = 'xtend2' })
- verify_value({ new = 5 }, 'watched2')
- update('')
- verify_value({ old = 'xtend2' })
- update('', 'watched2')
- verify_value({ old = 5 }, 'watched2')
- update('', 'watched3')
- verify_echo()
- end)
- it('is triggered with key patterns', function()
- source([[
- call dictwatcheradd(]] .. dict_expr .. [[, "wat*", "g:Changed")
- ]])
- update('= 1')
- verify_value({ new = 1 })
- verify_value({ new = 1 })
- update('= 3', 'watched2')
- verify_value({ new = 3 }, 'watched2')
- verify_value({ new = 3 }, 'watched2')
- verify_echo()
- source([[
- call dictwatcherdel(]] .. dict_expr .. [[, "wat*", "g:Changed")
- ]])
- -- watch every key pattern
- source([[
- call dictwatcheradd(]] .. dict_expr .. [[, "*", "g:Changed")
- ]])
- update('= 3', 'another_key')
- update('= 4', 'another_key')
- update('', 'another_key')
- update('= 2')
- verify_value({ new = 3 }, 'another_key')
- verify_value({ old = 3, new = 4 }, 'another_key')
- verify_value({ old = 4 }, 'another_key')
- verify_value({ old = 1, new = 2 })
- verify_value({ old = 1, new = 2 })
- verify_echo()
- source([[
- call dictwatcherdel(]] .. dict_expr .. [[, "*", "g:Changed")
- ]])
- end)
- it('is triggered for empty keys', function()
- command([[
- call dictwatcheradd(]] .. dict_expr .. [[, "", "g:Changed")
- ]])
- update('= 1', '')
- verify_value({ new = 1 }, '')
- update('= 2', '')
- verify_value({ old = 1, new = 2 }, '')
- command([[
- call dictwatcherdel(]] .. dict_expr .. [[, "", "g:Changed")
- ]])
- end)
- it('is triggered for empty keys when using catch-all *', function()
- command([[
- call dictwatcheradd(]] .. dict_expr .. [[, "*", "g:Changed")
- ]])
- update('= 1', '')
- verify_value({ new = 1 }, '')
- update('= 2', '')
- verify_value({ old = 1, new = 2 }, '')
- command([[
- call dictwatcherdel(]] .. dict_expr .. [[, "*", "g:Changed")
- ]])
- end)
- -- test a sequence of updates of different types to ensure proper memory
- -- management(with ASAN)
- local function test_updates(tests)
- it('test change sequence', function()
- local input, output
- for i = 1, #tests do
- input, output = unpack(tests[i])
- update(input)
- verify_value(output)
- end
- end)
- end
- test_updates({
- { '= 3', { new = 3 } },
- { '= 6', { old = 3, new = 6 } },
- { '+= 3', { old = 6, new = 9 } },
- { '', { old = 9 } },
- })
- test_updates({
- { '= "str"', { new = 'str' } },
- { '= "str2"', { old = 'str', new = 'str2' } },
- { '.= "2str"', { old = 'str2', new = 'str22str' } },
- { '', { old = 'str22str' } },
- })
- test_updates({
- { '= [1, 2]', { new = { 1, 2 } } },
- { '= [1, 2, 3]', { old = { 1, 2 }, new = { 1, 2, 3 } } },
- -- the += will update the list in place, so old and new are the same
- { '+= [4, 5]', { old = { 1, 2, 3, 4, 5 }, new = { 1, 2, 3, 4, 5 } } },
- { '', { old = { 1, 2, 3, 4, 5 } } },
- })
- test_updates({
- { '= {"k": "v"}', { new = { k = 'v' } } },
- { '= {"k1": 2}', { old = { k = 'v' }, new = { k1 = 2 } } },
- { '', { old = { k1 = 2 } } },
- })
- end)
- end
- gentests('g:')
- gentests('b:')
- gentests('w:')
- gentests('t:')
- gentests('g:dict_var', 'let g:dict_var = {}')
- describe('multiple watchers on the same dict/key', function()
- before_each(function()
- source([[
- function! g:Watcher1(dict, key, value)
- call rpcnotify(g:channel, '1', a:key, a:value)
- endfunction
- function! g:Watcher2(dict, key, value)
- call rpcnotify(g:channel, '2', a:key, a:value)
- endfunction
- call dictwatcheradd(g:, "key", "g:Watcher1")
- call dictwatcheradd(g:, "key", "g:Watcher2")
- ]])
- end)
- it('invokes all callbacks when the key is changed', function()
- command('let g:key = "value"')
- eq({ 'notification', '1', { 'key', { new = 'value' } } }, next_msg())
- eq({ 'notification', '2', { 'key', { new = 'value' } } }, next_msg())
- end)
- it('only removes watchers that fully match dict, key and callback', function()
- command('let g:key = "value"')
- eq({ 'notification', '1', { 'key', { new = 'value' } } }, next_msg())
- eq({ 'notification', '2', { 'key', { new = 'value' } } }, next_msg())
- command('call dictwatcherdel(g:, "key", "g:Watcher1")')
- command('let g:key = "v2"')
- eq({ 'notification', '2', { 'key', { old = 'value', new = 'v2' } } }, next_msg())
- end)
- end)
- it('errors out when adding to v:_null_dict', function()
- command([[
- function! g:Watcher1(dict, key, value)
- call rpcnotify(g:channel, '1', a:key, a:value)
- endfunction
- ]])
- eq(
- 'Vim(call):E46: Cannot change read-only variable "dictwatcheradd() argument"',
- exc_exec('call dictwatcheradd(v:_null_dict, "x", "g:Watcher1")')
- )
- end)
- describe('errors', function()
- before_each(function()
- source([[
- function! g:Watcher1(dict, key, value)
- call rpcnotify(g:channel, '1', a:key, a:value)
- endfunction
- function! g:Watcher2(dict, key, value)
- call rpcnotify(g:channel, '2', a:key, a:value)
- endfunction
- ]])
- end)
- -- WARNING: This suite depends on the above tests
- it('fails to remove if no watcher with matching callback is found', function()
- eq(
- "Vim(call):Couldn't find a watcher matching key and callback",
- exc_exec('call dictwatcherdel(g:, "key", "g:Watcher1")')
- )
- end)
- it('fails to remove if no watcher with matching key is found', function()
- eq(
- "Vim(call):Couldn't find a watcher matching key and callback",
- exc_exec('call dictwatcherdel(g:, "invalid_key", "g:Watcher2")')
- )
- end)
- it("does not fail to add/remove if the callback doesn't exist", function()
- command('call dictwatcheradd(g:, "key", "g:InvalidCb")')
- command('call dictwatcherdel(g:, "key", "g:InvalidCb")')
- end)
- it('fails to remove watcher from v:_null_dict', function()
- eq(
- "Vim(call):Couldn't find a watcher matching key and callback",
- exc_exec('call dictwatcherdel(v:_null_dict, "x", "g:Watcher2")')
- )
- end)
- --[[
- [ it("fails to add/remove if the callback doesn't exist", function()
- [ eq("Vim(call):Function g:InvalidCb doesn't exist",
- [ exc_exec('call dictwatcheradd(g:, "key", "g:InvalidCb")'))
- [ eq("Vim(call):Function g:InvalidCb doesn't exist",
- [ exc_exec('call dictwatcherdel(g:, "key", "g:InvalidCb")'))
- [ end)
- ]]
- it('does not fail to replace a watcher function', function()
- source([[
- let g:key = 'v2'
- call dictwatcheradd(g:, "key", "g:Watcher2")
- function! g:ReplaceWatcher2()
- function! g:Watcher2(dict, key, value)
- call rpcnotify(g:channel, '2b', a:key, a:value)
- endfunction
- endfunction
- ]])
- command('call g:ReplaceWatcher2()')
- command('let g:key = "value"')
- eq({ 'notification', '2b', { 'key', { old = 'v2', new = 'value' } } }, next_msg())
- end)
- it('does not crash when freeing a watched dictionary', function()
- source([[
- function! Watcher(dict, key, value)
- echo a:key string(a:value)
- endfunction
- function! MakeWatch()
- let d = {'foo': 'bar'}
- call dictwatcheradd(d, 'foo', function('Watcher'))
- endfunction
- ]])
- command('call MakeWatch()')
- assert_alive()
- end)
- end)
- describe('with lambdas', function()
- it('works correctly', function()
- source([[
- let d = {'foo': 'baz'}
- call dictwatcheradd(d, 'foo', {dict, key, value -> rpcnotify(g:channel, '2', key, value)})
- let d.foo = 'bar'
- ]])
- eq({ 'notification', '2', { 'foo', { old = 'baz', new = 'bar' } } }, next_msg())
- end)
- end)
- it('for b:changedtick', function()
- source([[
- function! OnTickChanged(dict, key, value)
- call rpcnotify(g:channel, 'SendChangeTick', a:key, a:value)
- endfunction
- call dictwatcheradd(b:, 'changedtick', 'OnTickChanged')
- ]])
- insert('t')
- eq({ 'notification', 'SendChangeTick', { 'changedtick', { old = 2, new = 3 } } }, next_msg())
- command([[call dictwatcherdel(b:, 'changedtick', 'OnTickChanged')]])
- insert('t')
- assert_alive()
- end)
- it('does not cause use-after-free when unletting from callback', function()
- source([[
- let g:called = 0
- function W(...) abort
- unlet g:d
- let g:called = 1
- endfunction
- let g:d = {}
- call dictwatcheradd(g:d, '*', function('W'))
- let g:d.foo = 123
- ]])
- eq(1, eval('g:called'))
- end)
- it('does not crash when using dictwatcherdel in callback', function()
- source([[
- let g:d = {}
- function! W1(...)
- " Delete current and following watcher.
- call dictwatcherdel(g:d, '*', function('W1'))
- call dictwatcherdel(g:d, '*', function('W2'))
- try
- call dictwatcherdel({}, 'meh', function('tr'))
- catch
- let g:exc = v:exception
- endtry
- endfunction
- call dictwatcheradd(g:d, '*', function('W1'))
- function! W2(...)
- endfunction
- call dictwatcheradd(g:d, '*', function('W2'))
- let g:d.foo = 23
- ]])
- eq(23, eval('g:d.foo'))
- eq("Vim(call):Couldn't find a watcher matching key and callback", eval('g:exc'))
- end)
- it('does not call watcher added in callback', function()
- source([[
- let g:d = {}
- let g:calls = []
- function! W1(...) abort
- call add(g:calls, 'W1')
- call dictwatcheradd(g:d, '*', function('W2'))
- endfunction
- function! W2(...) abort
- call add(g:calls, 'W2')
- endfunction
- call dictwatcheradd(g:d, '*', function('W1'))
- let g:d.foo = 23
- ]])
- eq(23, eval('g:d.foo'))
- eq({ 'W1' }, eval('g:calls'))
- end)
- it('calls watcher deleted in callback', function()
- source([[
- let g:d = {}
- let g:calls = []
- function! W1(...) abort
- call add(g:calls, "W1")
- call dictwatcherdel(g:d, '*', function('W2'))
- endfunction
- function! W2(...) abort
- call add(g:calls, "W2")
- endfunction
- call dictwatcheradd(g:d, '*', function('W1'))
- call dictwatcheradd(g:d, '*', function('W2'))
- let g:d.foo = 123
- unlet g:d
- let g:d = {}
- call dictwatcheradd(g:d, '*', function('W2'))
- call dictwatcheradd(g:d, '*', function('W1'))
- let g:d.foo = 123
- ]])
- eq(123, eval('g:d.foo'))
- eq({ 'W1', 'W2', 'W2', 'W1' }, eval('g:calls'))
- end)
- end)
|