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