mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 01:34:25 +00:00 
			
		
		
		
	Co-authored-by: bfredl <bjorn.linse@gmail.com> Co-authored-by: Christian Clason <c.clason@uni-graz.at>
		
			
				
	
	
		
			514 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			514 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
local helpers = require('test.functional.helpers')(after_each)
 | 
						|
local assert_alive = helpers.assert_alive
 | 
						|
local clear, nvim, source = helpers.clear, helpers.nvim, helpers.source
 | 
						|
local insert = helpers.insert
 | 
						|
local eq, next_msg = helpers.eq, helpers.next_msg
 | 
						|
local exc_exec = helpers.exc_exec
 | 
						|
local exec_lua = helpers.exec_lua
 | 
						|
local command = helpers.command
 | 
						|
local eval = helpers.eval
 | 
						|
 | 
						|
 | 
						|
describe('VimL dictionary notifications', function()
 | 
						|
  local channel
 | 
						|
 | 
						|
  before_each(function()
 | 
						|
    clear()
 | 
						|
    channel = nvim('get_api_info')[1]
 | 
						|
    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
 | 
						|
      nvim('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'})
 | 
						|
        nvim('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'})
 | 
						|
          nvim('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'})
 | 
						|
          nvim('command', 'call remove('..dict_expr..', "watched")')
 | 
						|
          verify_value({old = 'test'})
 | 
						|
        end)
 | 
						|
      end
 | 
						|
 | 
						|
      it('is triggered by extend()', function()
 | 
						|
        update('= "xtend"')
 | 
						|
        verify_value({new = 'xtend'})
 | 
						|
        nvim('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()
 | 
						|
      nvim('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()
 | 
						|
      nvim('command', 'let g:key = "value"')
 | 
						|
      eq({'notification', '1', {'key', {new = 'value'}}}, next_msg())
 | 
						|
      eq({'notification', '2', {'key', {new = 'value'}}}, next_msg())
 | 
						|
      nvim('command', 'call dictwatcherdel(g:, "key", "g:Watcher1")')
 | 
						|
      nvim('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)
 |