neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)