neovim

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

server_requests_spec.lua (15075B)


      1 -- Test server -> client RPC scenarios. Note: unlike `rpcnotify`, to evaluate
      2 -- `rpcrequest` calls we need the client event loop to be running.
      3 local t = require('test.testutil')
      4 local n = require('test.functional.testnvim')()
      5 
      6 local clear, eval = n.clear, n.eval
      7 local eq, neq, run, stop = t.eq, t.neq, n.run, n.stop
      8 local nvim_prog, command, fn = n.nvim_prog, n.command, n.fn
      9 local source, next_msg = n.source, n.next_msg
     10 local ok = t.ok
     11 local api = n.api
     12 local set_session = n.set_session
     13 local pcall_err = t.pcall_err
     14 local assert_alive = n.assert_alive
     15 
     16 describe('server -> client', function()
     17  local cid
     18 
     19  before_each(function()
     20    clear()
     21    cid = api.nvim_get_chan_info(0).id
     22  end)
     23 
     24  it('handles unexpected closed stream while preparing RPC response', function()
     25    source([[
     26      let g:_nvim_args = [v:progpath, '--embed', '--headless', '-n', '-u', 'NONE', '-i', 'NONE', ]
     27      let ch1 = jobstart(g:_nvim_args, {'rpc': v:true})
     28      let child1_ch = rpcrequest(ch1, "nvim_get_chan_info", 0).id
     29      call rpcnotify(ch1, 'nvim_eval', 'rpcrequest('.child1_ch.', "nvim_get_api_info")')
     30 
     31      let ch2 = jobstart(g:_nvim_args, {'rpc': v:true})
     32      let child2_ch = rpcrequest(ch2, "nvim_get_chan_info", 0).id
     33      call rpcnotify(ch2, 'nvim_eval', 'rpcrequest('.child2_ch.', "nvim_get_api_info")')
     34 
     35      call jobstop(ch1)
     36    ]])
     37    assert_alive()
     38  end)
     39 
     40  describe('simple call', function()
     41    it('works', function()
     42      local function on_setup()
     43        eq({ 4, 5, 6 }, eval('rpcrequest(' .. cid .. ', "scall", 1, 2, 3)'))
     44        stop()
     45      end
     46 
     47      local function on_request(method, args)
     48        eq('scall', method)
     49        eq({ 1, 2, 3 }, args)
     50        command('let g:result = [4, 5, 6]')
     51        return eval('g:result')
     52      end
     53      run(on_request, nil, on_setup)
     54    end)
     55  end)
     56 
     57  describe('empty string handling in arrays', function()
     58    -- Because the msgpack encoding for an empty string was interpreted as an
     59    -- error, msgpack arrays with an empty string looked like
     60    -- [..., '', 0, ..., 0] after the conversion, regardless of the array
     61    -- elements following the empty string.
     62    it('works', function()
     63      local function on_setup()
     64        eq({ 1, 2, '', 3, 'asdf' }, eval('rpcrequest(' .. cid .. ', "nstring")'))
     65        stop()
     66      end
     67 
     68      local function on_request()
     69        -- No need to evaluate the args, we are only interested in
     70        -- a response that contains an array with an empty string.
     71        return { 1, 2, '', 3, 'asdf' }
     72      end
     73      run(on_request, nil, on_setup)
     74    end)
     75  end)
     76 
     77  describe('recursive call', function()
     78    it('works', function()
     79      local function on_setup()
     80        api.nvim_set_var('result1', 0)
     81        api.nvim_set_var('result2', 0)
     82        api.nvim_set_var('result3', 0)
     83        api.nvim_set_var('result4', 0)
     84        command('let g:result1 = rpcrequest(' .. cid .. ', "rcall", 2)')
     85        eq(4, api.nvim_get_var('result1'))
     86        eq(8, api.nvim_get_var('result2'))
     87        eq(16, api.nvim_get_var('result3'))
     88        eq(32, api.nvim_get_var('result4'))
     89        stop()
     90      end
     91 
     92      local function on_request(method, args)
     93        eq('rcall', method)
     94        local _n = unpack(args) * 2
     95        if _n <= 16 then
     96          local cmd
     97          if _n == 4 then
     98            cmd = 'let g:result2 = rpcrequest(' .. cid .. ', "rcall", ' .. _n .. ')'
     99          elseif _n == 8 then
    100            cmd = 'let g:result3 = rpcrequest(' .. cid .. ', "rcall", ' .. _n .. ')'
    101          elseif _n == 16 then
    102            cmd = 'let g:result4 = rpcrequest(' .. cid .. ', "rcall", ' .. _n .. ')'
    103          end
    104          command(cmd)
    105        end
    106        return _n
    107      end
    108      run(on_request, nil, on_setup)
    109    end)
    110  end)
    111 
    112  describe('requests and notifications interleaved', function()
    113    it('does not delay notifications during pending request', function()
    114      local received = false
    115      local function on_setup()
    116        eq('retval', fn.rpcrequest(cid, 'doit'))
    117        stop()
    118      end
    119      local function on_request(method)
    120        if method == 'doit' then
    121          fn.rpcnotify(cid, 'headsup')
    122          eq(true, received)
    123          return 'retval'
    124        end
    125      end
    126      local function on_notification(method)
    127        if method == 'headsup' then
    128          received = true
    129        end
    130      end
    131      run(on_request, on_notification, on_setup)
    132    end)
    133 
    134    -- This tests the following scenario:
    135    --
    136    -- server->client [request     ] (1)
    137    -- client->server [request     ] (2) triggered by (1)
    138    -- server->client [notification] (3) triggered by (2)
    139    -- server->client [response    ] (4) response to (2)
    140    -- client->server [request     ] (4) triggered by (3)
    141    -- server->client [request     ] (5) triggered by (4)
    142    -- client->server [response    ] (6) response to (1)
    143    --
    144    -- If the above scenario ever happens, the client connection will be closed
    145    -- because (6) is returned after request (5) is sent, and nvim
    146    -- only deals with one server->client request at a time. (In other words,
    147    -- the client cannot send a response to a request that is not at the top
    148    -- of nvim's request stack).
    149    pending('will close connection if not properly synchronized', function()
    150      local function on_setup()
    151        eq('notified!', eval('rpcrequest(' .. cid .. ', "notify")'))
    152      end
    153 
    154      local function on_request(method)
    155        if method == 'notify' then
    156          eq(1, eval('rpcnotify(' .. cid .. ', "notification")'))
    157          return 'notified!'
    158        elseif method == 'nested' then
    159          -- do some busywork, so the first request will return
    160          -- before this one
    161          for _ = 1, 5 do
    162            assert_alive()
    163          end
    164          eq(1, eval('rpcnotify(' .. cid .. ', "nested_done")'))
    165          return 'done!'
    166        end
    167      end
    168 
    169      local function on_notification(method)
    170        if method == 'notification' then
    171          eq('done!', eval('rpcrequest(' .. cid .. ', "nested")'))
    172        elseif method == 'nested_done' then
    173          ok(false, 'never sent', 'sent')
    174        end
    175      end
    176 
    177      run(on_request, on_notification, on_setup)
    178      -- ignore disconnect failure, otherwise detected by after_each
    179      clear()
    180    end)
    181  end)
    182 
    183  describe('recursive (child) nvim client', function()
    184    before_each(function()
    185      command(
    186        "let vim = rpcstart('"
    187          .. nvim_prog
    188          .. "', ['-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--embed', '--headless'])"
    189      )
    190      neq(0, eval('vim'))
    191    end)
    192 
    193    after_each(function()
    194      command('call rpcstop(vim)')
    195    end)
    196 
    197    it('can send/receive notifications and make requests', function()
    198      command("call rpcnotify(vim, 'vim_set_current_line', 'SOME TEXT')")
    199 
    200      -- Wait for the notification to complete.
    201      command("call rpcrequest(vim, 'vim_eval', '0')")
    202 
    203      eq('SOME TEXT', eval("rpcrequest(vim, 'vim_get_current_line')"))
    204    end)
    205 
    206    it('can communicate buffers, tabpages, and windows', function()
    207      eq({ 1 }, eval("rpcrequest(vim, 'nvim_list_tabpages')"))
    208      -- Window IDs start at 1000 (LOWEST_WIN_ID in window.h)
    209      eq({ 1000 }, eval("rpcrequest(vim, 'nvim_list_wins')"))
    210 
    211      local buf = eval("rpcrequest(vim, 'nvim_list_bufs')")[1]
    212      eq(1, buf)
    213 
    214      eval("rpcnotify(vim, 'buffer_set_line', " .. buf .. ", 0, 'SOME TEXT')")
    215      command("call rpcrequest(vim, 'vim_eval', '0')") -- wait
    216 
    217      eq('SOME TEXT', eval("rpcrequest(vim, 'buffer_get_line', " .. buf .. ', 0)'))
    218 
    219      -- Call get_lines(buf, range [0,0], strict_indexing)
    220      eq({ 'SOME TEXT' }, eval("rpcrequest(vim, 'buffer_get_lines', " .. buf .. ', 0, 1, 1)'))
    221    end)
    222 
    223    it('returns an error if the request failed', function()
    224      eq(
    225        "Vim:Invoking 'does-not-exist' on channel 3:\nInvalid method: does-not-exist",
    226        pcall_err(eval, "rpcrequest(vim, 'does-not-exist')")
    227      )
    228    end)
    229  end)
    230 
    231  describe('jobstart()', function()
    232    local jobid
    233    before_each(function()
    234      local channel = api.nvim_get_chan_info(0).id
    235      api.nvim_set_var('channel', channel)
    236      source([[
    237        function! s:OnEvent(id, data, event)
    238          call rpcnotify(g:channel, a:event, 0, a:data)
    239        endfunction
    240        let g:job_opts = {
    241        \ 'on_stderr': function('s:OnEvent'),
    242        \ 'on_exit': function('s:OnEvent'),
    243        \ 'user': 0,
    244        \ 'rpc': v:true
    245        \ }
    246      ]])
    247      api.nvim_set_var('args', {
    248        nvim_prog,
    249        '-ll',
    250        'test/functional/api/rpc_fixture.lua',
    251        package.path,
    252        package.cpath,
    253      })
    254      jobid = eval('jobstart(g:args, g:job_opts)')
    255      neq(0, jobid)
    256    end)
    257 
    258    after_each(function()
    259      pcall(fn.jobstop, jobid)
    260    end)
    261 
    262    if t.skip(t.is_os('win')) then
    263      return
    264    end
    265 
    266    it('rpc and text stderr can be combined', function()
    267      local status, rv = pcall(fn.rpcrequest, jobid, 'poll')
    268      if not status then
    269        error(string.format('missing nvim Lua module? (%s)', rv))
    270      end
    271      eq('ok', rv)
    272      fn.rpcnotify(jobid, 'ping')
    273      eq({ 'notification', 'pong', {} }, next_msg())
    274      eq('done!', fn.rpcrequest(jobid, 'write_stderr', 'fluff\n'))
    275      eq({ 'notification', 'stderr', { 0, { 'fluff', '' } } }, next_msg())
    276      pcall(fn.rpcrequest, jobid, 'exit')
    277      eq({ 'notification', 'stderr', { 0, { '' } } }, next_msg())
    278      eq({ 'notification', 'exit', { 0, 0 } }, next_msg())
    279    end)
    280  end)
    281 
    282  describe('connecting to another (peer) nvim', function()
    283    local function connect_test(server, mode, address)
    284      local serverpid = fn.getpid()
    285      local client = n.new_session(true)
    286      set_session(client)
    287 
    288      local clientpid = fn.getpid()
    289      neq(serverpid, clientpid)
    290      local id = fn.sockconnect(mode, address, { rpc = true })
    291      ok(id > 0)
    292 
    293      fn.rpcrequest(id, 'nvim_set_current_line', 'hello')
    294      local client_id = fn.rpcrequest(id, 'nvim_get_chan_info', 0).id
    295 
    296      set_session(server)
    297      eq(serverpid, fn.getpid())
    298      eq('hello', api.nvim_get_current_line())
    299 
    300      -- Method calls work both ways.
    301      fn.rpcrequest(client_id, 'nvim_set_current_line', 'howdy!')
    302      eq(id, fn.rpcrequest(client_id, 'nvim_get_chan_info', 0).id)
    303 
    304      set_session(client)
    305      eq(clientpid, fn.getpid())
    306      eq('howdy!', api.nvim_get_current_line())
    307 
    308      -- Sending notification and then closing channel immediately still works.
    309      -- Use a fast API here, as a deferred API call may be aborted by EOF. #13537
    310      n.exec_lua(function()
    311        vim.rpcnotify(id, 'nvim_input', 'ccbye!<Esc>')
    312        vim.fn.chanclose(id)
    313      end)
    314 
    315      set_session(server)
    316      eq(serverpid, fn.getpid())
    317      -- Wait for the notification to be processed.
    318      t.retry(nil, 1000, function()
    319        eq('bye!', api.nvim_get_current_line())
    320      end)
    321 
    322      server:close()
    323      client:close()
    324    end
    325 
    326    it('via named pipe', function()
    327      local server = n.new_session(false)
    328      set_session(server)
    329      local address = fn.serverlist()[1]
    330      local first = string.sub(address, 1, 1)
    331      ok(first == '/' or first == '\\')
    332      connect_test(server, 'pipe', address)
    333    end)
    334 
    335    it('via ipv4 address', function()
    336      local server = n.new_session(false)
    337      set_session(server)
    338      local status, address = pcall(fn.serverstart, '127.0.0.1:')
    339      if not status then
    340        pending('no ipv4 stack')
    341      end
    342      eq('127.0.0.1:', string.sub(address, 1, 10))
    343      connect_test(server, 'tcp', address)
    344    end)
    345 
    346    it('via ipv6 address', function()
    347      local server = n.new_session(false)
    348      set_session(server)
    349      local status, address = pcall(fn.serverstart, '::1:')
    350      if not status then
    351        pending('no ipv6 stack')
    352      end
    353      eq('::1:', string.sub(address, 1, 4))
    354      connect_test(server, 'tcp', address)
    355    end)
    356 
    357    it('via hostname', function()
    358      local server = n.new_session(false)
    359      set_session(server)
    360      local address = fn.serverstart('localhost:')
    361      eq('localhost:', string.sub(address, 1, 10))
    362      connect_test(server, 'tcp', address)
    363    end)
    364 
    365    local function start_server_and_client()
    366      local server = n.new_session(false)
    367      set_session(server)
    368      local address = fn.serverlist()[1]
    369      local client = n.new_session(true)
    370      set_session(client)
    371 
    372      local id = fn.sockconnect('pipe', address, { rpc = true })
    373 
    374      finally(function()
    375        server:close()
    376        client:close()
    377      end)
    378 
    379      return id
    380    end
    381 
    382    it('does not crash on receiving UI events', function()
    383      local id = start_server_and_client()
    384      fn.rpcrequest(id, 'nvim_ui_attach', 80, 24, {})
    385      assert_alive()
    386    end)
    387 
    388    it('does not leak memory with channel closed before response', function()
    389      local id = start_server_and_client()
    390      eq(
    391        ('ch %d was closed by the peer'):format(id),
    392        pcall_err(n.exec_lua, function()
    393          vim.rpcrequest(id, 'nvim_command', 'qall!')
    394        end)
    395      )
    396      eq({}, api.nvim_get_chan_info(id)) -- Channel is closed.
    397    end)
    398 
    399    it('response works with channel closed just after response #24214', function()
    400      local id = start_server_and_client()
    401      eq(
    402        'RESPONSE',
    403        n.exec_lua(function()
    404          local prepare = assert(vim.uv.new_prepare())
    405          -- Block the event loop after writing the request but before polling for I/O
    406          -- so that response and EOF arrive at the same uv_run() call.
    407          prepare:start(function()
    408            vim.uv.sleep(50)
    409            prepare:close()
    410          end)
    411          return vim.rpcrequest(
    412            id,
    413            'nvim_exec_lua',
    414            [[vim.schedule(function() vim.cmd('qall!') end); return 'RESPONSE']],
    415            {}
    416          )
    417        end)
    418      )
    419      t.retry(nil, nil, function()
    420        eq({}, api.nvim_get_chan_info(id)) -- Channel is closed.
    421      end)
    422    end)
    423 
    424    it('via stdio, with many small flushes does not crash #23781', function()
    425      source([[
    426      let chan = jobstart([v:progpath, '--embed', '--headless', '-n', '-u', 'NONE', '-i', 'NONE'], { 'rpc':v:false })
    427      call chansend(chan, 0Z94)
    428      sleep 50m
    429      call chansend(chan, 0Z00)
    430      call chansend(chan, 0Z01)
    431      call chansend(chan, 0ZAC)
    432      call chansend(chan, 0Z6E76696D5F636F6D6D616E64)
    433      call chansend(chan, 0Z91)
    434      call chansend(chan, 0ZA5)
    435      call chansend(chan, 0Z71616C6C21)
    436      let g:statuses = jobwait([chan])
    437      ]])
    438      eq(eval('g:statuses'), { 0 })
    439      assert_alive()
    440    end)
    441  end)
    442 
    443  describe('connecting to its own pipe address', function()
    444    it('does not deadlock', function()
    445      local address = fn.serverlist()[1]
    446      local first = string.sub(address, 1, 1)
    447      ok(first == '/' or first == '\\')
    448      local serverpid = fn.getpid()
    449 
    450      local id = fn.sockconnect('pipe', address, { rpc = true })
    451 
    452      fn.rpcrequest(id, 'nvim_set_current_line', 'hello')
    453      eq('hello', api.nvim_get_current_line())
    454      eq(serverpid, fn.rpcrequest(id, 'nvim_eval', 'getpid()'))
    455 
    456      eq(id, fn.rpcrequest(id, 'nvim_get_chan_info', 0).id)
    457    end)
    458  end)
    459 end)