channels_spec.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. local t = require('test.testutil')
  2. local n = require('test.functional.testnvim')()
  3. local clear, eq, eval, next_msg, ok, source = n.clear, t.eq, n.eval, n.next_msg, t.ok, n.source
  4. local command, fn, api = n.command, n.fn, n.api
  5. local matches = t.matches
  6. local sleep = vim.uv.sleep
  7. local get_session, set_session = n.get_session, n.set_session
  8. local nvim_prog = n.nvim_prog
  9. local is_os = t.is_os
  10. local retry = t.retry
  11. local expect_twostreams = n.expect_twostreams
  12. local assert_alive = n.assert_alive
  13. local pcall_err = t.pcall_err
  14. local skip = t.skip
  15. describe('channels', function()
  16. local init = [[
  17. function! Normalize(data) abort
  18. " Windows: remove ^M
  19. return type([]) == type(a:data)
  20. \ ? map(a:data, 'substitute(v:val, "\r", "", "g")')
  21. \ : a:data
  22. endfunction
  23. function! OnEvent(id, data, event) dict
  24. call rpcnotify(1, a:event, a:id, a:data)
  25. endfunction
  26. ]]
  27. before_each(function()
  28. clear()
  29. source(init)
  30. end)
  31. pending('can connect to socket', function()
  32. local server = n.new_session(true)
  33. set_session(server)
  34. local address = fn.serverlist()[1]
  35. local client = n.new_session(true)
  36. set_session(client)
  37. source(init)
  38. api.nvim_set_var('address', address)
  39. command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
  40. local id = eval('g:id')
  41. ok(id > 0)
  42. command("call chansend(g:id, msgpackdump([[2,'nvim_set_var',['code',23]]]))")
  43. set_session(server)
  44. retry(nil, 1000, function()
  45. eq(23, api.nvim_get_var('code'))
  46. end)
  47. set_session(client)
  48. command("call chansend(g:id, msgpackdump([[0,0,'nvim_eval',['2+3']]]))")
  49. local res = eval('msgpackdump([[1,0,v:null,5]])')
  50. eq({ '\148\001\n\192\005' }, res)
  51. eq({ 'notification', 'data', { id, res } }, next_msg())
  52. command("call chansend(g:id, msgpackdump([[2,'nvim_command',['quit']]]))")
  53. eq({ 'notification', 'data', { id, { '' } } }, next_msg())
  54. end)
  55. it('dont crash due to garbage in rpc #23781', function()
  56. local client = get_session()
  57. local server = n.new_session(true)
  58. set_session(server)
  59. local address = fn.serverlist()[1]
  60. set_session(client)
  61. api.nvim_set_var('address', address)
  62. command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
  63. local id = eval('g:id')
  64. ok(id > 0)
  65. command("call chansend(g:id, 'F')")
  66. eq({ 'notification', 'data', { id, { '' } } }, next_msg())
  67. set_session(server)
  68. assert_alive()
  69. set_session(client)
  70. command('call chanclose(g:id)')
  71. command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
  72. id = eval('g:id')
  73. ok(id > 0)
  74. command("call chansend(g:id, msgpackdump([[2, 'redraw', 'F']], 'B')[:-4])")
  75. set_session(server)
  76. assert_alive()
  77. set_session(client)
  78. command("call chansend(g:id, 'F')")
  79. eq({ 'notification', 'data', { id, { '' } } }, next_msg())
  80. set_session(server)
  81. assert_alive()
  82. set_session(client)
  83. command('call chanclose(g:id)')
  84. server:close()
  85. end)
  86. it('can use stdio channel', function()
  87. source([[
  88. let g:job_opts = {
  89. \ 'on_stdout': function('OnEvent'),
  90. \ 'on_stderr': function('OnEvent'),
  91. \ 'on_exit': function('OnEvent'),
  92. \ }
  93. ]])
  94. api.nvim_set_var('nvim_prog', nvim_prog)
  95. api.nvim_set_var(
  96. 'code',
  97. [[
  98. function! OnEvent(id, data, event) dict
  99. let text = string([a:id, a:data, a:event])
  100. call chansend(g:x, text)
  101. if a:data == ['']
  102. call chansend(v:stderr, "*dies*")
  103. quit
  104. endif
  105. endfunction
  106. let g:x = stdioopen({'on_stdin':'OnEvent'})
  107. call chansend(x, "hello")
  108. ]]
  109. )
  110. command(
  111. "let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
  112. )
  113. local id = eval('g:id')
  114. ok(id > 0)
  115. eq({ 'notification', 'stdout', { id, { 'hello' } } }, next_msg())
  116. command("call chansend(id, 'howdy')")
  117. eq({ 'notification', 'stdout', { id, { "[1, ['howdy'], 'stdin']" } } }, next_msg())
  118. command('call chansend(id, 0z686f6c61)')
  119. eq({ 'notification', 'stdout', { id, { "[1, ['hola'], 'stdin']" } } }, next_msg())
  120. command("call chanclose(id, 'stdin')")
  121. expect_twostreams({
  122. { 'notification', 'stdout', { id, { "[1, [''], 'stdin']" } } },
  123. { 'notification', 'stdout', { id, { '' } } },
  124. }, {
  125. { 'notification', 'stderr', { id, { '*dies*' } } },
  126. { 'notification', 'stderr', { id, { '' } } },
  127. })
  128. eq({ 'notification', 'exit', { 3, 0 } }, next_msg())
  129. end)
  130. it('can use stdio channel and on_print callback', function()
  131. source([[
  132. let g:job_opts = {
  133. \ 'on_stdout': function('OnEvent'),
  134. \ 'on_stderr': function('OnEvent'),
  135. \ 'on_exit': function('OnEvent'),
  136. \ }
  137. ]])
  138. api.nvim_set_var('nvim_prog', nvim_prog)
  139. api.nvim_set_var(
  140. 'code',
  141. [[
  142. function! OnStdin(id, data, event) dict
  143. echo string([a:id, a:data, a:event])
  144. if a:data == ['']
  145. quit
  146. endif
  147. endfunction
  148. function! OnPrint(text) dict
  149. call chansend(g:x, ['OnPrint:' .. a:text])
  150. endfunction
  151. let g:x = stdioopen({'on_stdin': funcref('OnStdin'), 'on_print':'OnPrint'})
  152. call chansend(x, "hello")
  153. ]]
  154. )
  155. command(
  156. "let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
  157. )
  158. local id = eval('g:id')
  159. ok(id > 0)
  160. eq({ 'notification', 'stdout', { id, { 'hello' } } }, next_msg())
  161. command("call chansend(id, 'howdy')")
  162. eq({ 'notification', 'stdout', { id, { "OnPrint:[1, ['howdy'], 'stdin']" } } }, next_msg())
  163. end)
  164. local function expect_twoline(id, stream, line1, line2, nobr)
  165. local msg = next_msg()
  166. local joined = nobr and { line1 .. line2 } or { line1, line2 }
  167. if not pcall(eq, { 'notification', stream, { id, joined } }, msg) then
  168. local sep = (not nobr) and '' or nil
  169. eq({ 'notification', stream, { id, { line1, sep } } }, msg)
  170. eq({ 'notification', stream, { id, { line2 } } }, next_msg())
  171. end
  172. end
  173. it('can use stdio channel with pty', function()
  174. skip(is_os('win'))
  175. source([[
  176. let g:job_opts = {
  177. \ 'on_stdout': function('OnEvent'),
  178. \ 'on_exit': function('OnEvent'),
  179. \ 'pty': v:true,
  180. \ }
  181. ]])
  182. api.nvim_set_var('nvim_prog', nvim_prog)
  183. api.nvim_set_var(
  184. 'code',
  185. [[
  186. function! OnEvent(id, data, event) dict
  187. let text = string([a:id, a:data, a:event])
  188. call chansend(g:x, text)
  189. endfunction
  190. let g:x = stdioopen({'on_stdin':'OnEvent'})
  191. ]]
  192. )
  193. command(
  194. "let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
  195. )
  196. local id = eval('g:id')
  197. ok(id > 0)
  198. command("call chansend(id, 'TEXT\n')")
  199. expect_twoline(id, 'stdout', 'TEXT\r', "[1, ['TEXT', ''], 'stdin']")
  200. command('call chansend(id, 0z426c6f6273210a)')
  201. expect_twoline(id, 'stdout', 'Blobs!\r', "[1, ['Blobs!', ''], 'stdin']")
  202. command("call chansend(id, 'neovan')")
  203. eq({ 'notification', 'stdout', { id, { 'neovan' } } }, next_msg())
  204. command("call chansend(id, '\127\127im\n')")
  205. expect_twoline(id, 'stdout', '\b \b\b \bim\r', "[1, ['neovim', ''], 'stdin']")
  206. command("call chansend(id, 'incomplet\004')")
  207. local bsdlike = is_os('bsd') or is_os('mac')
  208. local extra = bsdlike and '^D\008\008' or ''
  209. expect_twoline(id, 'stdout', 'incomplet' .. extra, "[1, ['incomplet'], 'stdin']", true)
  210. command("call chansend(id, '\004')")
  211. if bsdlike then
  212. expect_twoline(id, 'stdout', extra, "[1, [''], 'stdin']", true)
  213. else
  214. eq({ 'notification', 'stdout', { id, { "[1, [''], 'stdin']" } } }, next_msg())
  215. end
  216. -- channel is still open
  217. command("call chansend(id, 'hi again!\n')")
  218. eq({ 'notification', 'stdout', { id, { 'hi again!\r', '' } } }, next_msg())
  219. end)
  220. it('stdio channel can use rpc and stderr simultaneously', function()
  221. skip(is_os('win'))
  222. source([[
  223. let g:job_opts = {
  224. \ 'on_stderr': function('OnEvent'),
  225. \ 'on_exit': function('OnEvent'),
  226. \ 'rpc': v:true,
  227. \ }
  228. ]])
  229. api.nvim_set_var('nvim_prog', nvim_prog)
  230. api.nvim_set_var(
  231. 'code',
  232. [[
  233. let id = stdioopen({'rpc':v:true})
  234. call rpcnotify(id,"nvim_call_function", "rpcnotify", [1, "message", "hi there!", id])
  235. call chansend(v:stderr, "trouble!")
  236. ]]
  237. )
  238. command(
  239. "let id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
  240. )
  241. eq({ 'notification', 'message', { 'hi there!', 1 } }, next_msg())
  242. eq({ 'notification', 'stderr', { 3, { 'trouble!' } } }, next_msg())
  243. eq(30, eval("rpcrequest(id, 'nvim_eval', '[chansend(v:stderr, \"math??\"), 5*6][1]')"))
  244. eq({ 'notification', 'stderr', { 3, { 'math??' } } }, next_msg())
  245. local _, err =
  246. pcall(command, "call rpcrequest(id, 'nvim_command', 'call chanclose(v:stderr, \"stdin\")')")
  247. matches('E906: invalid stream for channel', err)
  248. eq(1, eval("rpcrequest(id, 'nvim_eval', 'chanclose(v:stderr, \"stderr\")')"))
  249. eq({ 'notification', 'stderr', { 3, { '' } } }, next_msg())
  250. command("call rpcnotify(id, 'nvim_command', 'quit')")
  251. eq({ 'notification', 'exit', { 3, 0 } }, next_msg())
  252. end)
  253. it('stdio channel works with stdout redirected to file #30509', function()
  254. t.write_file(
  255. 'Xstdio_write.vim',
  256. [[
  257. let chan = stdioopen({})
  258. call chansend(chan, 'foo')
  259. call chansend(chan, 'bar')
  260. qall!
  261. ]]
  262. )
  263. local fd = assert(vim.uv.fs_open('Xstdio_redir', 'w', 420))
  264. local exit_code, exit_signal
  265. local handle = vim.uv.spawn(nvim_prog, {
  266. args = { '-u', 'NONE', '-i', 'NONE', '--headless', '-S', 'Xstdio_write.vim' },
  267. -- Simulate shell redirection: "nvim ... > Xstdio_redir". #30509
  268. stdio = { nil, fd, nil },
  269. }, function(code, signal)
  270. vim.uv.stop()
  271. exit_code, exit_signal = code, signal
  272. end)
  273. finally(function()
  274. handle:close()
  275. vim.uv.fs_close(fd)
  276. os.remove('Xstdio_write.vim')
  277. os.remove('Xstdio_redir')
  278. end)
  279. vim.uv.run('default')
  280. eq({ 0, 0 }, { exit_code, exit_signal })
  281. eq('foobar', t.read_file('Xstdio_redir'))
  282. end)
  283. it('can use buffered output mode', function()
  284. skip(fn.executable('grep') == 0, 'missing "grep" command')
  285. source([[
  286. let g:job_opts = {
  287. \ 'on_stdout': function('OnEvent'),
  288. \ 'on_exit': function('OnEvent'),
  289. \ 'stdout_buffered': v:true,
  290. \ }
  291. ]])
  292. command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
  293. local id = eval('g:id')
  294. command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]])
  295. sleep(10)
  296. command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]])
  297. command("call chanclose(id, 'stdin')")
  298. eq({
  299. 'notification',
  300. 'stdout',
  301. { id, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
  302. }, next_msg())
  303. eq({ 'notification', 'exit', { id, 0 } }, next_msg())
  304. command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
  305. id = eval('g:id')
  306. command([[call chansend(id, "is no number\nnot at all")]])
  307. command("call chanclose(id, 'stdin')")
  308. -- works correctly with no output
  309. eq({ 'notification', 'stdout', { id, { '' } } }, next_msg())
  310. eq({ 'notification', 'exit', { id, 1 } }, next_msg())
  311. end)
  312. it('can use buffered output mode with no stream callback', function()
  313. skip(fn.executable('grep') == 0, 'missing "grep" command')
  314. source([[
  315. function! OnEvent(id, data, event) dict
  316. call rpcnotify(1, a:event, a:id, a:data, self.stdout)
  317. endfunction
  318. let g:job_opts = {
  319. \ 'on_exit': function('OnEvent'),
  320. \ 'stdout_buffered': v:true,
  321. \ }
  322. ]])
  323. command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
  324. local id = eval('g:id')
  325. command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]])
  326. sleep(10)
  327. command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]])
  328. command("call chanclose(id, 'stdin')")
  329. eq({
  330. 'notification',
  331. 'exit',
  332. { id, 0, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
  333. }, next_msg())
  334. -- if dict is reused the new value is not stored,
  335. -- but nvim also does not crash
  336. command("let id = jobstart(['cat'], g:job_opts)")
  337. id = eval('g:id')
  338. command([[call chansend(id, "cat text\n")]])
  339. sleep(10)
  340. command("call chanclose(id, 'stdin')")
  341. -- old value was not overwritten
  342. eq({
  343. 'notification',
  344. 'exit',
  345. { id, 0, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
  346. }, next_msg())
  347. -- and an error was thrown.
  348. eq(
  349. "E5210: dict key 'stdout' already set for buffered stream in channel " .. id,
  350. eval('v:errmsg')
  351. )
  352. -- reset dictionary
  353. source([[
  354. let g:job_opts = {
  355. \ 'on_exit': function('OnEvent'),
  356. \ 'stdout_buffered': v:true,
  357. \ }
  358. ]])
  359. command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
  360. id = eval('g:id')
  361. command([[call chansend(id, "is no number\nnot at all")]])
  362. command("call chanclose(id, 'stdin')")
  363. -- works correctly with no output
  364. eq({ 'notification', 'exit', { id, 1, { '' } } }, next_msg())
  365. end)
  366. end)
  367. describe('loopback', function()
  368. before_each(function()
  369. clear()
  370. command("let chan = sockconnect('pipe', v:servername, {'rpc': v:true})")
  371. end)
  372. it('does not crash when sending raw data', function()
  373. eq(
  374. "Vim(call):Can't send raw data to rpc channel",
  375. pcall_err(command, "call chansend(chan, 'test')")
  376. )
  377. assert_alive()
  378. end)
  379. it('are released when closed', function()
  380. local chans = eval('len(nvim_list_chans())')
  381. command('call chanclose(chan)')
  382. eq(chans - 1, eval('len(nvim_list_chans())'))
  383. end)
  384. end)