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)