job_spec.lua 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. local helpers = require('test.functional.helpers')(after_each)
  2. local clear, eq, eval, exc_exec, feed_command, feed, insert, neq, next_msg, nvim,
  3. nvim_dir, ok, source, write_file, mkdir, rmdir = helpers.clear,
  4. helpers.eq, helpers.eval, helpers.exc_exec, helpers.feed_command, helpers.feed,
  5. helpers.insert, helpers.neq, helpers.next_msg, helpers.nvim,
  6. helpers.nvim_dir, helpers.ok, helpers.source,
  7. helpers.write_file, helpers.mkdir, helpers.rmdir
  8. local command = helpers.command
  9. local funcs = helpers.funcs
  10. local retry = helpers.retry
  11. local meths = helpers.meths
  12. local NIL = helpers.NIL
  13. local wait = helpers.wait
  14. local iswin = helpers.iswin
  15. local get_pathsep = helpers.get_pathsep
  16. local pathroot = helpers.pathroot
  17. local nvim_set = helpers.nvim_set
  18. local expect_twostreams = helpers.expect_twostreams
  19. local expect_msg_seq = helpers.expect_msg_seq
  20. local expect_err = helpers.expect_err
  21. local Screen = require('test.functional.ui.screen')
  22. -- Kill process with given pid
  23. local function os_kill(pid)
  24. return os.execute((iswin()
  25. and 'taskkill /f /t /pid '..pid..' > nul'
  26. or 'kill -9 '..pid..' > /dev/null'))
  27. end
  28. describe('jobs', function()
  29. local channel
  30. before_each(function()
  31. clear()
  32. channel = nvim('get_api_info')[1]
  33. nvim('set_var', 'channel', channel)
  34. source([[
  35. function! Normalize(data) abort
  36. " Windows: remove ^M
  37. return type([]) == type(a:data)
  38. \ ? map(a:data, 'substitute(v:val, "\r", "", "g")')
  39. \ : a:data
  40. endfunction
  41. function! OnEvent(id, data, event) dict
  42. let userdata = get(self, 'user')
  43. let data = Normalize(a:data)
  44. call rpcnotify(g:channel, a:event, userdata, data)
  45. endfunction
  46. let g:job_opts = {
  47. \ 'on_stdout': function('OnEvent'),
  48. \ 'on_exit': function('OnEvent'),
  49. \ 'user': 0
  50. \ }
  51. ]])
  52. end)
  53. it('uses &shell and &shellcmdflag if passed a string', function()
  54. nvim('command', "let $VAR = 'abc'")
  55. if iswin() then
  56. nvim('command', "let j = jobstart('echo %VAR%', g:job_opts)")
  57. else
  58. nvim('command', "let j = jobstart('echo $VAR', g:job_opts)")
  59. end
  60. eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg())
  61. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  62. eq({'notification', 'exit', {0, 0}}, next_msg())
  63. end)
  64. it('changes to given / directory', function()
  65. nvim('command', "let g:job_opts.cwd = '/'")
  66. if iswin() then
  67. nvim('command', "let j = jobstart('cd', g:job_opts)")
  68. else
  69. nvim('command', "let j = jobstart('pwd', g:job_opts)")
  70. end
  71. eq({'notification', 'stdout',
  72. {0, {pathroot(), ''}}}, next_msg())
  73. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  74. eq({'notification', 'exit', {0, 0}}, next_msg())
  75. end)
  76. it('changes to given `cwd` directory', function()
  77. local dir = eval("resolve(tempname())"):gsub("/", get_pathsep())
  78. mkdir(dir)
  79. nvim('command', "let g:job_opts.cwd = '" .. dir .. "'")
  80. if iswin() then
  81. nvim('command', "let j = jobstart('cd', g:job_opts)")
  82. else
  83. nvim('command', "let j = jobstart('pwd', g:job_opts)")
  84. end
  85. expect_msg_seq(
  86. { {'notification', 'stdout', {0, {dir, ''} } },
  87. {'notification', 'stdout', {0, {''} } },
  88. {'notification', 'exit', {0, 0} }
  89. },
  90. -- Alternative sequence:
  91. { {'notification', 'stdout', {0, {dir} } },
  92. {'notification', 'stdout', {0, {'', ''} } },
  93. {'notification', 'stdout', {0, {''} } },
  94. {'notification', 'exit', {0, 0} }
  95. }
  96. )
  97. rmdir(dir)
  98. end)
  99. it('fails to change to invalid `cwd`', function()
  100. local dir = eval('resolve(tempname())."-bogus"')
  101. local _, err = pcall(function()
  102. nvim('command', "let g:job_opts.cwd = '" .. dir .. "'")
  103. if iswin() then
  104. nvim('command', "let j = jobstart('cd', g:job_opts)")
  105. else
  106. nvim('command', "let j = jobstart('pwd', g:job_opts)")
  107. end
  108. end)
  109. ok(string.find(err, "E475: Invalid argument: expected valid directory$") ~= nil)
  110. end)
  111. it('produces error when using non-executable `cwd`', function()
  112. if iswin() then return end -- N/A for Windows
  113. local dir = 'Xtest_not_executable_dir'
  114. mkdir(dir)
  115. funcs.setfperm(dir, 'rw-------')
  116. expect_err('E475: Invalid argument: expected valid directory$', nvim,
  117. 'command', "call jobstart('pwd', {'cwd': '" .. dir .. "'})")
  118. rmdir(dir)
  119. end)
  120. it('returns 0 when it fails to start', function()
  121. eq("", eval("v:errmsg"))
  122. feed_command("let g:test_jobid = jobstart([])")
  123. eq(0, eval("g:test_jobid"))
  124. eq("E474:", string.match(eval("v:errmsg"), "E%d*:"))
  125. end)
  126. it('returns -1 when target is not executable #5465', function()
  127. local function new_job()
  128. return eval([[jobstart('')]])
  129. end
  130. local executable_jobid = new_job()
  131. local nonexecutable_jobid = eval("jobstart(['"..(iswin()
  132. and './test/functional/fixtures'
  133. or './test/functional/fixtures/non_executable.txt').."'])")
  134. eq(-1, nonexecutable_jobid)
  135. -- Should _not_ throw an error.
  136. eq("", eval("v:errmsg"))
  137. -- Non-executable job should not increment the job ids. #5465
  138. eq(executable_jobid + 1, new_job())
  139. end)
  140. it('invokes callbacks when the job writes and exits', function()
  141. nvim('command', "let g:job_opts.on_stderr = function('OnEvent')")
  142. nvim('command', [[call jobstart(has('win32') ? 'echo:' : 'echo', g:job_opts)]])
  143. expect_twostreams({{'notification', 'stdout', {0, {'', ''}}},
  144. {'notification', 'stdout', {0, {''}}}},
  145. {{'notification', 'stderr', {0, {''}}}})
  146. eq({'notification', 'exit', {0, 0}}, next_msg())
  147. end)
  148. it('allows interactive commands', function()
  149. nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  150. neq(0, eval('j'))
  151. nvim('command', 'call jobsend(j, "abc\\n")')
  152. eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg())
  153. nvim('command', 'call jobsend(j, "123\\nxyz\\n")')
  154. expect_msg_seq(
  155. { {'notification', 'stdout', {0, {'123', 'xyz', ''}}}
  156. },
  157. -- Alternative sequence:
  158. { {'notification', 'stdout', {0, {'123', ''}}},
  159. {'notification', 'stdout', {0, {'xyz', ''}}}
  160. }
  161. )
  162. nvim('command', 'call jobsend(j, [123, "xyz", ""])')
  163. expect_msg_seq(
  164. { {'notification', 'stdout', {0, {'123', 'xyz', ''}}}
  165. },
  166. -- Alternative sequence:
  167. { {'notification', 'stdout', {0, {'123', ''}}},
  168. {'notification', 'stdout', {0, {'xyz', ''}}}
  169. }
  170. )
  171. nvim('command', "call jobstop(j)")
  172. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  173. eq({'notification', 'exit', {0, 0}}, next_msg())
  174. end)
  175. it('preserves NULs', function()
  176. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  177. -- Make a file with NULs in it.
  178. local filename = helpers.tmpname()
  179. write_file(filename, "abc\0def\n")
  180. nvim('command', "let j = jobstart(['cat', '"..filename.."'], g:job_opts)")
  181. eq({'notification', 'stdout', {0, {'abc\ndef', ''}}}, next_msg())
  182. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  183. eq({'notification', 'exit', {0, 0}}, next_msg())
  184. os.remove(filename)
  185. -- jobsend() preserves NULs.
  186. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  187. nvim('command', [[call jobsend(j, ["123\n456",""])]])
  188. eq({'notification', 'stdout', {0, {'123\n456', ''}}}, next_msg())
  189. nvim('command', "call jobstop(j)")
  190. end)
  191. it("will not buffer data if it doesn't end in newlines", function()
  192. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  193. if os.getenv("TRAVIS") and os.getenv("CC") == "gcc-4.9"
  194. and helpers.os_name() == "osx" then
  195. -- XXX: Hangs Travis macOS since e9061117a5b8f195c3f26a5cb94e18ddd7752d86.
  196. pending("[Hangs on Travis macOS. #5002]", function() end)
  197. return
  198. end
  199. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  200. nvim('command', 'call jobsend(j, "abc\\nxyz")')
  201. eq({'notification', 'stdout', {0, {'abc', 'xyz'}}}, next_msg())
  202. nvim('command', "call jobstop(j)")
  203. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  204. eq({'notification', 'exit', {0, 0}}, next_msg())
  205. end)
  206. it('preserves newlines', function()
  207. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  208. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  209. nvim('command', 'call jobsend(j, "a\\n\\nc\\n\\n\\n\\nb\\n\\n")')
  210. eq({'notification', 'stdout',
  211. {0, {'a', '', 'c', '', '', '', 'b', '', ''}}}, next_msg())
  212. end)
  213. it('preserves NULs', function()
  214. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  215. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  216. nvim('command', 'call jobsend(j, ["\n123\n", "abc\\nxyz\n", ""])')
  217. eq({'notification', 'stdout', {0, {'\n123\n', 'abc\nxyz\n', ''}}},
  218. next_msg())
  219. nvim('command', "call jobstop(j)")
  220. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  221. eq({'notification', 'exit', {0, 0}}, next_msg())
  222. end)
  223. it('avoids sending final newline', function()
  224. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  225. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  226. nvim('command', 'call jobsend(j, ["some data", "without\nfinal nl"])')
  227. eq({'notification', 'stdout', {0, {'some data', 'without\nfinal nl'}}},
  228. next_msg())
  229. nvim('command', "call jobstop(j)")
  230. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  231. eq({'notification', 'exit', {0, 0}}, next_msg())
  232. end)
  233. it('closes the job streams with jobclose', function()
  234. nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  235. nvim('command', 'call jobclose(j, "stdin")')
  236. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  237. eq({'notification', 'exit', {0, iswin() and 1 or 0}}, next_msg())
  238. end)
  239. it("disallows jobsend on a job that closed stdin", function()
  240. nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  241. nvim('command', 'call jobclose(j, "stdin")')
  242. eq(false, pcall(function()
  243. nvim('command', 'call jobsend(j, ["some data"])')
  244. end))
  245. end)
  246. it('disallows jobsend/stop on a non-existent job', function()
  247. eq(false, pcall(eval, "jobsend(-1, 'lol')"))
  248. eq(false, pcall(eval, "jobstop(-1)"))
  249. end)
  250. it('disallows jobstop twice on the same job', function()
  251. nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  252. neq(0, eval('j'))
  253. eq(true, pcall(eval, "jobstop(j)"))
  254. eq(false, pcall(eval, "jobstop(j)"))
  255. end)
  256. it('will not leak memory if we leave a job running', function()
  257. nvim('command', "call jobstart(['cat', '-'], g:job_opts)")
  258. end)
  259. it('can get the pid value using getpid', function()
  260. nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  261. local pid = eval('jobpid(j)')
  262. neq(NIL, meths.get_proc(pid))
  263. nvim('command', 'call jobstop(j)')
  264. eq({'notification', 'stdout', {0, {''}}}, next_msg())
  265. if iswin() then
  266. expect_msg_seq(
  267. -- win64
  268. { {'notification', 'exit', {0, 1}}
  269. },
  270. -- win32
  271. { {'notification', 'exit', {0, 15}}
  272. }
  273. )
  274. else
  275. eq({'notification', 'exit', {0, 0}}, next_msg())
  276. end
  277. eq(NIL, meths.get_proc(pid))
  278. end)
  279. it("do not survive the exit of nvim", function()
  280. -- use sleep, which doesn't die on stdin close
  281. nvim('command', "let g:j = jobstart(has('win32') ? ['ping', '-n', '1001', '127.0.0.1'] : ['sleep', '1000'], g:job_opts)")
  282. local pid = eval('jobpid(g:j)')
  283. neq(NIL, meths.get_proc(pid))
  284. clear()
  285. eq(NIL, meths.get_proc(pid))
  286. end)
  287. it('can survive the exit of nvim with "detach"', function()
  288. nvim('command', 'let g:job_opts.detach = 1')
  289. nvim('command', "let g:j = jobstart(has('win32') ? ['ping', '-n', '1001', '127.0.0.1'] : ['sleep', '1000'], g:job_opts)")
  290. local pid = eval('jobpid(g:j)')
  291. neq(NIL, meths.get_proc(pid))
  292. clear()
  293. neq(NIL, meths.get_proc(pid))
  294. -- clean up after ourselves
  295. eq(0, os_kill(pid))
  296. end)
  297. it('can pass user data to the callback', function()
  298. nvim('command', 'let g:job_opts.user = {"n": 5, "s": "str", "l": [1]}')
  299. nvim('command', [[call jobstart('echo foo', g:job_opts)]])
  300. local data = {n = 5, s = 'str', l = {1}}
  301. expect_msg_seq(
  302. { {'notification', 'stdout', {data, {'foo', ''}}},
  303. {'notification', 'stdout', {data, {''}}},
  304. },
  305. -- Alternative sequence:
  306. { {'notification', 'stdout', {data, {'foo'}}},
  307. {'notification', 'stdout', {data, {'', ''}}},
  308. {'notification', 'stdout', {data, {''}}},
  309. }
  310. )
  311. eq({'notification', 'exit', {data, 0}}, next_msg())
  312. end)
  313. it('can omit data callbacks', function()
  314. nvim('command', 'unlet g:job_opts.on_stdout')
  315. nvim('command', 'let g:job_opts.user = 5')
  316. nvim('command', [[call jobstart('echo foo', g:job_opts)]])
  317. eq({'notification', 'exit', {5, 0}}, next_msg())
  318. end)
  319. it('can omit exit callback', function()
  320. nvim('command', 'unlet g:job_opts.on_exit')
  321. nvim('command', 'let g:job_opts.user = 5')
  322. nvim('command', [[call jobstart('echo foo', g:job_opts)]])
  323. expect_msg_seq(
  324. { {'notification', 'stdout', {5, {'foo', ''} } },
  325. {'notification', 'stdout', {5, {''} } },
  326. },
  327. -- Alternative sequence:
  328. { {'notification', 'stdout', {5, {'foo'} } },
  329. {'notification', 'stdout', {5, {'', ''} } },
  330. {'notification', 'stdout', {5, {''} } },
  331. }
  332. )
  333. end)
  334. it('will pass return code with the exit event', function()
  335. nvim('command', 'let g:job_opts.user = 5')
  336. nvim('command', "call jobstart('exit 55', g:job_opts)")
  337. eq({'notification', 'stdout', {5, {''}}}, next_msg())
  338. eq({'notification', 'exit', {5, 55}}, next_msg())
  339. end)
  340. it('can receive dictionary functions', function()
  341. source([[
  342. let g:dict = {'id': 10}
  343. function g:dict.on_exit(id, code, event)
  344. call rpcnotify(g:channel, a:event, a:code, self.id)
  345. endfunction
  346. call jobstart('exit 45', g:dict)
  347. ]])
  348. eq({'notification', 'exit', {45, 10}}, next_msg())
  349. end)
  350. it('can redefine callbacks being used by a job', function()
  351. local screen = Screen.new()
  352. screen:attach()
  353. screen:set_default_attr_ids({
  354. [1] = {bold=true, foreground=Screen.colors.Blue},
  355. })
  356. source([[
  357. function! g:JobHandler(job_id, data, event)
  358. endfunction
  359. let g:callbacks = {
  360. \ 'on_stdout': function('g:JobHandler'),
  361. \ 'on_stderr': function('g:JobHandler'),
  362. \ 'on_exit': function('g:JobHandler')
  363. \ }
  364. let job = jobstart(['cat', '-'], g:callbacks)
  365. ]])
  366. wait()
  367. source([[
  368. function! g:JobHandler(job_id, data, event)
  369. endfunction
  370. ]])
  371. eq("", eval("v:errmsg"))
  372. end)
  373. it('requires funcrefs for script-local (s:) functions', function()
  374. local screen = Screen.new(60, 5)
  375. screen:attach()
  376. screen:set_default_attr_ids({
  377. [1] = {bold = true, foreground = Screen.colors.Blue1},
  378. [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
  379. [3] = {bold = true, foreground = Screen.colors.SeaGreen4}
  380. })
  381. -- Pass job callback names _without_ `function(...)`.
  382. source([[
  383. function! s:OnEvent(id, data, event) dict
  384. let g:job_result = get(self, 'user')
  385. endfunction
  386. let s:job = jobstart('echo "foo"', {
  387. \ 'on_stdout': 's:OnEvent',
  388. \ 'on_stderr': 's:OnEvent',
  389. \ 'on_exit': 's:OnEvent',
  390. \ })
  391. ]])
  392. screen:expect{any="{2:E120: Using <SID> not in a script context: s:OnEvent}"}
  393. end)
  394. it('does not repeat output with slow output handlers', function()
  395. source([[
  396. let d = {'data': []}
  397. function! d.on_stdout(job, data, event) dict
  398. call add(self.data, Normalize(a:data))
  399. sleep 200m
  400. endfunction
  401. if has('win32')
  402. let cmd = 'for /L %I in (1,1,5) do @(echo %I& ping -n 2 127.0.0.1 > nul)'
  403. else
  404. let cmd = ['sh', '-c', 'for i in $(seq 1 5); do echo $i; sleep 0.1; done']
  405. endif
  406. call jobwait([jobstart(cmd, d)])
  407. ]])
  408. local expected = {'1', '2', '3', '4', '5', ''}
  409. local chunks = eval('d.data')
  410. local received = {''}
  411. for i, chunk in ipairs(chunks) do
  412. if i < #chunks then
  413. -- if chunks got joined, a spurious [''] callback was not sent
  414. neq({''}, chunk)
  415. else
  416. -- but EOF callback is still sent
  417. eq({''}, chunk)
  418. end
  419. received[#received] = received[#received]..chunk[1]
  420. for j = 2, #chunk do
  421. received[#received+1] = chunk[j]
  422. end
  423. end
  424. eq(expected, received)
  425. end)
  426. it('jobstart() works with partial functions', function()
  427. source([[
  428. function PrintArgs(a1, a2, id, data, event)
  429. " Windows: remove ^M
  430. let normalized = map(a:data, 'substitute(v:val, "\r", "", "g")')
  431. call rpcnotify(g:channel, '1', a:a1, a:a2, normalized, a:event)
  432. endfunction
  433. let Callback = function('PrintArgs', ["foo", "bar"])
  434. let g:job_opts = {'on_stdout': Callback}
  435. call jobstart('echo some text', g:job_opts)
  436. ]])
  437. expect_msg_seq(
  438. { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}},
  439. },
  440. -- Alternative sequence:
  441. { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}},
  442. {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}},
  443. }
  444. )
  445. end)
  446. it('jobstart() works with closures', function()
  447. source([[
  448. fun! MkFun()
  449. let a1 = 'foo'
  450. let a2 = 'bar'
  451. return {id, data, event -> rpcnotify(g:channel, '1', a1, a2, Normalize(data), event)}
  452. endfun
  453. let g:job_opts = {'on_stdout': MkFun()}
  454. call jobstart('echo some text', g:job_opts)
  455. ]])
  456. expect_msg_seq(
  457. { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}},
  458. },
  459. -- Alternative sequence:
  460. { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}},
  461. {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}},
  462. }
  463. )
  464. end)
  465. it('jobstart() works when closure passed directly to `jobstart`', function()
  466. source([[
  467. let g:job_opts = {'on_stdout': {id, data, event -> rpcnotify(g:channel, '1', 'foo', 'bar', Normalize(data), event)}}
  468. call jobstart('echo some text', g:job_opts)
  469. ]])
  470. expect_msg_seq(
  471. { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}},
  472. },
  473. -- Alternative sequence:
  474. { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}},
  475. {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}},
  476. }
  477. )
  478. end)
  479. describe('jobwait', function()
  480. before_each(function()
  481. if iswin() then
  482. helpers.set_shell_powershell()
  483. end
  484. end)
  485. it('returns a list of status codes', function()
  486. source([[
  487. call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [
  488. \ jobstart('Start-Sleep -Milliseconds 100; exit 4'),
  489. \ jobstart('Start-Sleep -Milliseconds 300; exit 5'),
  490. \ jobstart('Start-Sleep -Milliseconds 500; exit 6'),
  491. \ jobstart('Start-Sleep -Milliseconds 700; exit 7')
  492. \ ] : [
  493. \ jobstart('sleep 0.10; exit 4'),
  494. \ jobstart('sleep 0.110; exit 5'),
  495. \ jobstart('sleep 0.210; exit 6'),
  496. \ jobstart('sleep 0.310; exit 7')
  497. \ ]))
  498. ]])
  499. eq({'notification', 'wait', {{4, 5, 6, 7}}}, next_msg())
  500. end)
  501. it('will run callbacks while waiting', function()
  502. source([[
  503. let g:dict = {'id': 10}
  504. let g:exits = 0
  505. function g:dict.on_exit(id, code, event)
  506. if a:code != 5
  507. throw 'Error!'
  508. endif
  509. let g:exits += 1
  510. endfunction
  511. call jobwait(has('win32') ? [
  512. \ jobstart('Start-Sleep -Milliseconds 100; exit 5', g:dict),
  513. \ jobstart('Start-Sleep -Milliseconds 300; exit 5', g:dict),
  514. \ jobstart('Start-Sleep -Milliseconds 500; exit 5', g:dict),
  515. \ jobstart('Start-Sleep -Milliseconds 700; exit 5', g:dict)
  516. \ ] : [
  517. \ jobstart('sleep 0.010; exit 5', g:dict),
  518. \ jobstart('sleep 0.030; exit 5', g:dict),
  519. \ jobstart('sleep 0.050; exit 5', g:dict),
  520. \ jobstart('sleep 0.070; exit 5', g:dict)
  521. \ ])
  522. call rpcnotify(g:channel, 'wait', g:exits)
  523. ]])
  524. eq({'notification', 'wait', {4}}, next_msg())
  525. end)
  526. it('will return status codes in the order of passed ids', function()
  527. source([[
  528. call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [
  529. \ jobstart('Start-Sleep -Milliseconds 700; exit 4'),
  530. \ jobstart('Start-Sleep -Milliseconds 500; exit 5'),
  531. \ jobstart('Start-Sleep -Milliseconds 300; exit 6'),
  532. \ jobstart('Start-Sleep -Milliseconds 100; exit 7')
  533. \ ] : [
  534. \ jobstart('sleep 0.070; exit 4'),
  535. \ jobstart('sleep 0.050; exit 5'),
  536. \ jobstart('sleep 0.030; exit 6'),
  537. \ jobstart('sleep 0.010; exit 7')
  538. \ ]))
  539. ]])
  540. eq({'notification', 'wait', {{4, 5, 6, 7}}}, next_msg())
  541. end)
  542. it('will return -3 for invalid job ids', function()
  543. source([[
  544. call rpcnotify(g:channel, 'wait', jobwait([
  545. \ -10,
  546. \ jobstart((has('win32') ? 'Start-Sleep -Milliseconds 100' : 'sleep 0.01').'; exit 5'),
  547. \ ]))
  548. ]])
  549. eq({'notification', 'wait', {{-3, 5}}}, next_msg())
  550. end)
  551. it('will return -2 when interrupted without timeout', function()
  552. feed_command('call rpcnotify(g:channel, "ready") | '..
  553. 'call rpcnotify(g:channel, "wait", '..
  554. 'jobwait([jobstart("'..
  555. (iswin() and 'Start-Sleep 10' or 'sleep 10')..
  556. '; exit 55")]))')
  557. eq({'notification', 'ready', {}}, next_msg())
  558. feed('<c-c>')
  559. eq({'notification', 'wait', {{-2}}}, next_msg())
  560. end)
  561. it('will return -2 when interrupted with timeout', function()
  562. feed_command('call rpcnotify(g:channel, "ready") | '..
  563. 'call rpcnotify(g:channel, "wait", '..
  564. 'jobwait([jobstart("'..
  565. (iswin() and 'Start-Sleep 10' or 'sleep 10')..
  566. '; exit 55")], 10000))')
  567. eq({'notification', 'ready', {}}, next_msg())
  568. feed('<c-c>')
  569. eq({'notification', 'wait', {{-2}}}, next_msg())
  570. end)
  571. it('can be called recursively', function()
  572. if helpers.pending_win32(pending) then return end -- TODO: Need `cat`.
  573. source([[
  574. let g:opts = {}
  575. let g:counter = 0
  576. function g:opts.on_stdout(id, msg, _event)
  577. if self.state == 0
  578. if self.counter < 10
  579. call Run()
  580. endif
  581. let self.state = 1
  582. call jobsend(a:id, "line1\n")
  583. elseif self.state == 1
  584. let self.state = 2
  585. call jobsend(a:id, "line2\n")
  586. elseif self.state == 2
  587. let self.state = 3
  588. call jobsend(a:id, "line3\n")
  589. elseif self.state == 3
  590. let self.state = 4
  591. call rpcnotify(g:channel, 'w', printf('job %d closed', self.counter))
  592. call jobclose(a:id, 'stdin')
  593. endif
  594. endfunction
  595. function g:opts.on_exit(...)
  596. call rpcnotify(g:channel, 'w', printf('job %d exited', self.counter))
  597. endfunction
  598. function Run()
  599. let g:counter += 1
  600. let j = copy(g:opts)
  601. let j.state = 0
  602. let j.counter = g:counter
  603. call jobwait([
  604. \ jobstart('echo ready; cat -', j),
  605. \ ])
  606. endfunction
  607. ]])
  608. feed_command('call Run()')
  609. local r
  610. for i = 10, 1, -1 do
  611. r = next_msg()
  612. eq('job '..i..' closed', r[3][1])
  613. r = next_msg()
  614. eq('job '..i..' exited', r[3][1])
  615. end
  616. eq(10, nvim('eval', 'g:counter'))
  617. end)
  618. describe('with timeout argument', function()
  619. it('will return -1 if the wait timed out', function()
  620. source([[
  621. call rpcnotify(g:channel, 'wait', jobwait([
  622. \ jobstart('exit 4'),
  623. \ jobstart((has('win32') ? 'Start-Sleep 10' : 'sleep 10').'; exit 5'),
  624. \ ], has('win32') ? 6000 : 100))
  625. ]])
  626. eq({'notification', 'wait', {{4, -1}}}, next_msg())
  627. end)
  628. it('can pass 0 to check if a job exists', function()
  629. source([[
  630. call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [
  631. \ jobstart('Start-Sleep -Milliseconds 50; exit 4'),
  632. \ jobstart('Start-Sleep -Milliseconds 300; exit 5'),
  633. \ ] : [
  634. \ jobstart('sleep 0.05; exit 4'),
  635. \ jobstart('sleep 0.3; exit 5'),
  636. \ ], 0))
  637. ]])
  638. eq({'notification', 'wait', {{-1, -1}}}, next_msg())
  639. end)
  640. end)
  641. end)
  642. -- FIXME need to wait until jobsend succeeds before calling jobstop
  643. pending('will only emit the "exit" event after "stdout" and "stderr"', function()
  644. nvim('command', "let g:job_opts.on_stderr = function('s:OnEvent')")
  645. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)")
  646. local jobid = nvim('eval', 'j')
  647. nvim('eval', 'jobsend(j, "abcdef")')
  648. nvim('eval', 'jobstop(j)')
  649. eq({'notification', 'j', {0, {jobid, 'stdout', {'abcdef'}}}}, next_msg())
  650. eq({'notification', 'j', {0, {jobid, 'exit'}}}, next_msg())
  651. end)
  652. it('cannot have both rpc and pty options', function()
  653. command("let g:job_opts.pty = v:true")
  654. command("let g:job_opts.rpc = v:true")
  655. local _, err = pcall(command, "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)")
  656. ok(string.find(err, "E475: Invalid argument: job cannot have both 'pty' and 'rpc' options set") ~= nil)
  657. end)
  658. it('does not crash when repeatedly failing to start shell', function()
  659. source([[
  660. set shell=nosuchshell
  661. func! DoIt()
  662. call jobstart('true')
  663. call jobstart('true')
  664. endfunc
  665. ]])
  666. -- The crash only triggered if both jobs are cleaned up on the same event
  667. -- loop tick. This is also prevented by try-block, so feed must be used.
  668. feed_command("call DoIt()")
  669. feed('<cr>') -- press RETURN
  670. eq(2,eval('1+1'))
  671. end)
  672. it('jobstop() kills entire process tree #6530', function()
  673. -- XXX: Using `nvim` isn't a good test, it reaps its children on exit.
  674. -- local c = 'call jobstart([v:progpath, "-u", "NONE", "-i", "NONE", "--headless"])'
  675. -- local j = eval("jobstart([v:progpath, '-u', 'NONE', '-i', 'NONE', '--headless', '-c', '"
  676. -- ..c.."', '-c', '"..c.."'])")
  677. -- Create child with several descendants.
  678. local sleep_cmd = (iswin()
  679. and 'ping -n 31 127.0.0.1'
  680. or 'sleep 30')
  681. local j = eval("jobstart('"..sleep_cmd..' | '..sleep_cmd..' | '..sleep_cmd.."')")
  682. local ppid = funcs.jobpid(j)
  683. local children
  684. retry(nil, nil, function()
  685. children = meths.get_proc_children(ppid)
  686. eq((iswin() and 4 or 3), #children)
  687. end)
  688. -- Assert that nvim_get_proc() sees the children.
  689. for _, child_pid in ipairs(children) do
  690. local info = meths.get_proc(child_pid)
  691. -- eq((iswin() and 'nvim.exe' or 'nvim'), info.name)
  692. eq(ppid, info.ppid)
  693. end
  694. -- Kill the root of the tree.
  695. funcs.jobstop(j)
  696. -- Assert that the children were killed.
  697. retry(nil, nil, function()
  698. for _, child_pid in ipairs(children) do
  699. eq(NIL, meths.get_proc(child_pid))
  700. end
  701. end)
  702. end)
  703. describe('running tty-test program', function()
  704. if helpers.pending_win32(pending) then return end
  705. local function next_chunk()
  706. local rv
  707. while true do
  708. local msg = next_msg()
  709. local data = msg[3][2]
  710. for i = 1, #data do
  711. data[i] = data[i]:gsub('\n', '\000')
  712. end
  713. rv = table.concat(data, '\n')
  714. rv = rv:gsub('\r\n$', ''):gsub('^\r\n', '')
  715. if rv ~= '' then
  716. break
  717. end
  718. end
  719. return rv
  720. end
  721. local function send(str)
  722. nvim('command', 'call jobsend(j, "'..str..'")')
  723. end
  724. before_each(function()
  725. -- Redefine Normalize() so that TTY data is not munged.
  726. source([[
  727. function! Normalize(data) abort
  728. return a:data
  729. endfunction
  730. ]])
  731. local ext = iswin() and '.exe' or ''
  732. insert(nvim_dir..'/tty-test'..ext) -- Full path to tty-test.
  733. nvim('command', 'let g:job_opts.pty = 1')
  734. nvim('command', 'let exec = [expand("<cfile>:p")]')
  735. nvim('command', "let j = jobstart(exec, g:job_opts)")
  736. eq('tty ready', next_chunk())
  737. end)
  738. it('echoing input', function()
  739. send('test')
  740. eq('test', next_chunk())
  741. end)
  742. it('resizing window', function()
  743. nvim('command', 'call jobresize(j, 40, 10)')
  744. eq('rows: 10, cols: 40', next_chunk())
  745. nvim('command', 'call jobresize(j, 10, 40)')
  746. eq('rows: 40, cols: 10', next_chunk())
  747. end)
  748. it('jobclose() sends SIGHUP', function()
  749. nvim('command', 'call jobclose(j)')
  750. local msg = next_msg()
  751. msg = (msg[2] == 'stdout') and next_msg() or msg -- Skip stdout, if any.
  752. eq({'notification', 'exit', {0, 42}}, msg)
  753. end)
  754. it('jobstart() does not keep ptmx file descriptor open', function()
  755. -- Start another job (using libuv)
  756. command('let g:job_opts.pty = 0')
  757. local other_jobid = eval("jobstart(['cat', '-'], g:job_opts)")
  758. local other_pid = eval('jobpid(' .. other_jobid .. ')')
  759. -- Other job doesn't block first job from recieving SIGHUP on jobclose()
  760. command('call jobclose(j)')
  761. -- Have to wait so that the SIGHUP can be processed by tty-test on time.
  762. -- Can't wait for the next message in case this test fails, if it fails
  763. -- there won't be any more messages, and the test would hang.
  764. helpers.sleep(100)
  765. local err = exc_exec('call jobpid(j)')
  766. eq('Vim(call):E900: Invalid channel id', err)
  767. -- cleanup
  768. eq(other_pid, eval('jobpid(' .. other_jobid .. ')'))
  769. command('call jobstop(' .. other_jobid .. ')')
  770. end)
  771. end)
  772. end)
  773. describe("pty process teardown", function()
  774. local screen
  775. before_each(function()
  776. clear()
  777. screen = Screen.new(30, 6)
  778. screen:attach()
  779. screen:expect([[
  780. ^ |
  781. ~ |
  782. ~ |
  783. ~ |
  784. ~ |
  785. |
  786. ]])
  787. end)
  788. after_each(function()
  789. screen:detach()
  790. end)
  791. it("does not prevent/delay exit. #4798 #4900", function()
  792. if helpers.pending_win32(pending) then return end
  793. -- Use a nested nvim (in :term) to test without --headless.
  794. feed_command(":terminal '"..helpers.nvim_prog
  795. .."' -u NONE -i NONE --cmd '"..nvim_set.."' "
  796. -- Use :term again in the _nested_ nvim to get a PTY process.
  797. -- Use `sleep` to simulate a long-running child of the PTY.
  798. .."+terminal +'!(sleep 300 &)' +qa")
  799. -- Exiting should terminate all descendants (PTY, its children, ...).
  800. screen:expect([[
  801. ^ |
  802. [Process exited 0] |
  803. |
  804. |
  805. |
  806. |
  807. ]])
  808. end)
  809. end)