job_spec.lua (48824B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 local tt = require('test.functional.testterm') 5 6 local clear = n.clear 7 local eq = t.eq 8 local eval = n.eval 9 local exc_exec = n.exc_exec 10 local feed_command = n.feed_command 11 local feed = n.feed 12 local insert = n.insert 13 local neq = t.neq 14 local next_msg = n.next_msg 15 local testprg = n.testprg 16 local ok = t.ok 17 local source = n.source 18 local write_file = t.write_file 19 local mkdir = t.mkdir 20 local rmdir = n.rmdir 21 local assert_alive = n.assert_alive 22 local command = n.command 23 local fn = n.fn 24 local retry = t.retry 25 local api = n.api 26 local NIL = vim.NIL 27 local poke_eventloop = n.poke_eventloop 28 local get_pathsep = n.get_pathsep 29 local pathroot = n.pathroot 30 local exec_lua = n.exec_lua 31 local nvim_set = n.nvim_set 32 local expect_twostreams = n.expect_twostreams 33 local expect_msg_seq = n.expect_msg_seq 34 local pcall_err = t.pcall_err 35 local matches = t.matches 36 local skip = t.skip 37 local is_os = t.is_os 38 39 describe('jobs', function() 40 local channel 41 42 before_each(function() 43 clear() 44 45 channel = api.nvim_get_chan_info(0).id 46 api.nvim_set_var('channel', channel) 47 source([[ 48 function! Normalize(data) abort 49 " Windows: remove ^M and term escape sequences 50 return type([]) == type(a:data) 51 \ ? mapnew(a:data, 'substitute(substitute(v:val, "\r", "", "g"), "\x1b\\%(\\]\\d\\+;.\\{-}\x07\\|\\[.\\{-}[\x40-\x7E]\\)", "", "g")') 52 \ : a:data 53 endfunction 54 function! OnEvent(id, data, event) dict 55 let userdata = get(self, 'user') 56 let data = Normalize(a:data) 57 " If Normalize() made non-empty data empty, doesn't send a notification. 58 if type([]) == type(data) && len(data) == 1 && !empty(a:data[0]) && empty(data[0]) 59 return 60 endif 61 call rpcnotify(g:channel, a:event, userdata, data) 62 endfunction 63 let g:job_opts = { 64 \ 'on_stdout': function('OnEvent'), 65 \ 'on_exit': function('OnEvent'), 66 \ 'user': 0 67 \ } 68 ]]) 69 end) 70 71 it('validation', function() 72 matches( 73 "E475: Invalid argument: job cannot have both 'pty' and 'rpc' options set", 74 pcall_err(command, "call jobstart(['cat', '-'], { 'pty': v:true, 'rpc': v:true })") 75 ) 76 matches( 77 'E475: Invalid argument: expected valid directory', 78 pcall_err(command, "call jobstart(['cat', '-'], { 'cwd': 9313843 })") 79 ) 80 matches( 81 'E475: Invalid argument: expected valid directory', 82 pcall_err(command, "call jobstart(['cat', '-'], { 'cwd': 'bogusssssss/bogus' })") 83 ) 84 matches( 85 "E475: Invalid argument: 'term' must be Boolean", 86 pcall_err(command, "call jobstart(['cat', '-'], { 'term': 'bogus' })") 87 ) 88 matches( 89 "E475: Invalid argument: 'term' must be Boolean", 90 pcall_err(command, "call jobstart(['cat', '-'], { 'term': 1 })") 91 ) 92 command('set modified') 93 matches( 94 vim.pesc('jobstart(...,{term=true}) requires unmodified buffer'), 95 pcall_err(command, "call jobstart(['cat', '-'], { 'term': v:true })") 96 ) 97 98 -- Non-failure cases: 99 command('set nomodified') 100 command("call jobstart(['cat', '-'], { 'term': v:true })") 101 command("call jobstart(['cat', '-'], { 'term': v:false })") 102 end) 103 104 it('jobstart(term=true) accepts width/height (#33904)', function() 105 local buf = api.nvim_create_buf(false, true) 106 exec_lua(function() 107 vim.api.nvim_buf_call(buf, function() 108 vim.fn.jobstart({ 109 vim.v.progpath, 110 '--clean', 111 '--headless', 112 '+lua tty = vim.uv.new_tty(1, false) print(tty:get_winsize()) tty:close()', 113 }, { 114 term = true, 115 width = 11, 116 height = 12, 117 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 118 }) 119 end) 120 end) 121 retry(nil, nil, function() 122 eq({ '11 12' }, api.nvim_buf_get_lines(buf, 0, 1, false)) 123 end) 124 end) 125 126 it('must specify env option as a dict', function() 127 command('let g:job_opts.env = v:true') 128 local _, err = pcall(function() 129 if is_os('win') then 130 command("let j = jobstart('set', g:job_opts)") 131 else 132 command("let j = jobstart('env', g:job_opts)") 133 end 134 end) 135 matches('E475: Invalid argument: env', err) 136 end) 137 138 it('append environment #env', function() 139 command("let $VAR = 'abc'") 140 command("let $TOTO = 'goodbye world'") 141 command("let g:job_opts.env = {'TOTO': 'hello world'}") 142 if is_os('win') then 143 command([[call jobstart('echo %TOTO% %VAR%', g:job_opts)]]) 144 else 145 command([[call jobstart('echo $TOTO $VAR', g:job_opts)]]) 146 end 147 148 expect_msg_seq({ 149 { 'notification', 'stdout', { 0, { 'hello world abc' } } }, 150 { 'notification', 'stdout', { 0, { '', '' } } }, 151 }, { 152 { 'notification', 'stdout', { 0, { 'hello world abc', '' } } }, 153 { 'notification', 'stdout', { 0, { '' } } }, 154 }) 155 end) 156 157 it('append environment with pty #env', function() 158 command("let $VAR = 'abc'") 159 command("let $TOTO = 'goodbye world'") 160 command('let g:job_opts.pty = v:true') 161 command("let g:job_opts.env = {'TOTO': 'hello world'}") 162 if is_os('win') then 163 command([[call jobstart('echo %TOTO% %VAR%', g:job_opts)]]) 164 else 165 command([[call jobstart('echo $TOTO $VAR', g:job_opts)]]) 166 end 167 expect_msg_seq({ 168 { 'notification', 'stdout', { 0, { 'hello world abc' } } }, 169 { 'notification', 'stdout', { 0, { '', '' } } }, 170 }, { 171 { 'notification', 'stdout', { 0, { 'hello world abc', '' } } }, 172 { 'notification', 'stdout', { 0, { '' } } }, 173 }) 174 end) 175 176 it('replace environment #env', function() 177 command("let $VAR = 'abc'") 178 command("let $TOTO = 'goodbye world'") 179 command("let g:job_opts.env = {'TOTO': 'hello world'}") 180 command('let g:job_opts.clear_env = 1') 181 182 -- libuv ensures that certain "required" environment variables are 183 -- preserved if the user doesn't provide them in a custom environment 184 -- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L672 185 -- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L48-L60 186 -- 187 -- Rather than expecting a completely empty environment, ensure that $VAR 188 -- is *not* in the environment but $TOTO is. 189 if is_os('win') then 190 command([[call jobstart('echo %TOTO% %VAR%', g:job_opts)]]) 191 expect_msg_seq({ 192 { 'notification', 'stdout', { 0, { 'hello world %VAR%', '' } } }, 193 }) 194 else 195 command('set shell=/bin/sh') 196 command([[call jobstart('echo $TOTO $VAR', g:job_opts)]]) 197 expect_msg_seq({ 198 { 'notification', 'stdout', { 0, { 'hello world', '' } } }, 199 }) 200 end 201 end) 202 203 it('handles case-insensitively matching #env vars', function() 204 command("let $TOTO = 'abc'") 205 -- Since $Toto is being set in the job, it should take precedence over the 206 -- global $TOTO on Windows 207 command("let g:job_opts = {'env': {'Toto': 'def'}, 'stdout_buffered': v:true}") 208 if is_os('win') then 209 command([[let j = jobstart('set | find /I "toto="', g:job_opts)]]) 210 else 211 command([[let j = jobstart('env | grep -i toto=', g:job_opts)]]) 212 end 213 command('call jobwait([j])') 214 command('let g:output = Normalize(g:job_opts.stdout)') 215 local actual = eval('g:output') 216 local expected 217 if is_os('win') then 218 -- Toto is normalized to TOTO so we can detect duplicates, and because 219 -- Windows doesn't care about case 220 expected = { 'TOTO=def', '' } 221 else 222 expected = { 'TOTO=abc', 'Toto=def', '' } 223 end 224 table.sort(actual) 225 table.sort(expected) 226 eq(expected, actual) 227 end) 228 229 it('uses &shell and &shellcmdflag if passed a string', function() 230 command("let $VAR = 'abc'") 231 if is_os('win') then 232 command("let j = jobstart('echo %VAR%', g:job_opts)") 233 else 234 command("let j = jobstart('echo $VAR', g:job_opts)") 235 end 236 eq({ 'notification', 'stdout', { 0, { 'abc', '' } } }, next_msg()) 237 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 238 eq({ 'notification', 'exit', { 0, 0 } }, next_msg()) 239 end) 240 241 it('changes to given / directory', function() 242 command("let g:job_opts.cwd = '/'") 243 if is_os('win') then 244 command("let j = jobstart('cd', g:job_opts)") 245 else 246 command("let j = jobstart('pwd', g:job_opts)") 247 end 248 eq({ 'notification', 'stdout', { 0, { pathroot(), '' } } }, next_msg()) 249 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 250 eq({ 'notification', 'exit', { 0, 0 } }, next_msg()) 251 end) 252 253 local function test_job_cwd() 254 local dir = eval('resolve(tempname())'):gsub('/', get_pathsep()) 255 mkdir(dir) 256 finally(function() 257 rmdir(dir) 258 end) 259 command("let g:job_opts.cwd = '" .. dir .. "'") 260 if is_os('win') then 261 command("let j = jobstart('cd', g:job_opts)") 262 else 263 command("let j = jobstart('pwd', g:job_opts)") 264 end 265 expect_msg_seq( 266 { 267 { 'notification', 'stdout', { 0, { dir, '' } } }, 268 { 'notification', 'stdout', { 0, { '' } } }, 269 { 'notification', 'exit', { 0, 0 } }, 270 }, 271 -- Alternative sequence: 272 { 273 { 'notification', 'stdout', { 0, { dir } } }, 274 { 'notification', 'stdout', { 0, { '', '' } } }, 275 { 'notification', 'stdout', { 0, { '' } } }, 276 { 'notification', 'exit', { 0, 0 } }, 277 } 278 ) 279 end 280 281 it('changes to given `cwd` directory', function() 282 test_job_cwd() 283 end) 284 285 it('changes to given `cwd` directory with pty', function() 286 command('let g:job_opts.pty = v:true') 287 test_job_cwd() 288 end) 289 290 it('fails to change to invalid `cwd`', function() 291 local dir = eval('resolve(tempname())."-bogus"') 292 local _, err = pcall(function() 293 command("let g:job_opts.cwd = '" .. dir .. "'") 294 if is_os('win') then 295 command("let j = jobstart('cd', g:job_opts)") 296 else 297 command("let j = jobstart('pwd', g:job_opts)") 298 end 299 end) 300 matches('E475: Invalid argument: expected valid directory$', err) 301 end) 302 303 it('error on non-executable `cwd`', function() 304 skip(is_os('win'), 'N/A for Windows') 305 306 local dir = 'Xtest_not_executable_dir' 307 mkdir(dir) 308 finally(function() 309 rmdir(dir) 310 end) 311 fn.setfperm(dir, 'rw-------') 312 313 matches( 314 '^Vim%(call%):E903: Process failed to start: permission denied: .*', 315 pcall_err(command, ("call jobstart(['pwd'], {'cwd': '%s'})"):format(dir)) 316 ) 317 end) 318 319 it('error log and exit status 122 on non-executable `cwd`', function() 320 skip(is_os('win'), 'N/A for Windows') 321 322 local logfile = 'Xchdir_fail_log' 323 clear({ env = { NVIM_LOG_FILE = logfile } }) 324 325 local dir = 'Xtest_not_executable_dir' 326 mkdir(dir) 327 finally(function() 328 rmdir(dir) 329 n.check_close() 330 os.remove(logfile) 331 end) 332 fn.setfperm(dir, 'rw-------') 333 334 n.exec(([[ 335 let s:chan = jobstart(['pwd'], {'cwd': '%s', 'pty': v:true}) 336 let g:status = jobwait([s:chan], 1000)[0] 337 ]]):format(dir)) 338 eq(122, eval('g:status')) 339 t.assert_log(('chdir%%(%s%%) failed: permission denied'):format(dir), logfile, 100) 340 end) 341 342 it('returns 0 when it fails to start', function() 343 eq('', eval('v:errmsg')) 344 feed_command('let g:test_jobid = jobstart([])') 345 eq(0, eval('g:test_jobid')) 346 eq('E474:', string.match(eval('v:errmsg'), 'E%d*:')) 347 end) 348 349 it('returns -1 when target is not executable #5465', function() 350 local function new_job() 351 return eval([[jobstart('')]]) 352 end 353 local executable_jobid = new_job() 354 355 local exe = is_os('win') and './test/functional/fixtures' 356 or './test/functional/fixtures/non_executable.txt' 357 eq( 358 "Vim:E475: Invalid value for argument cmd: '" .. exe .. "' is not executable", 359 pcall_err(eval, "jobstart(['" .. exe .. "'])") 360 ) 361 eq('', eval('v:errmsg')) 362 -- Non-executable job should not increment the job ids. #5465 363 eq(executable_jobid + 1, new_job()) 364 end) 365 366 it('invokes callbacks when the job writes and exits', function() 367 command("let g:job_opts.on_stderr = function('OnEvent')") 368 command([[call jobstart(has('win32') ? 'echo:' : 'echo', g:job_opts)]]) 369 expect_twostreams({ 370 { 'notification', 'stdout', { 0, { '', '' } } }, 371 { 'notification', 'stdout', { 0, { '' } } }, 372 }, { { 'notification', 'stderr', { 0, { '' } } } }) 373 eq({ 'notification', 'exit', { 0, 0 } }, next_msg()) 374 end) 375 376 it('interactive commands', function() 377 command("let j = jobstart(['cat', '-'], g:job_opts)") 378 neq(0, eval('j')) 379 command('call jobsend(j, "abc\\n")') 380 eq({ 'notification', 'stdout', { 0, { 'abc', '' } } }, next_msg()) 381 command('call jobsend(j, "123\\nxyz\\n")') 382 expect_msg_seq( 383 { { 'notification', 'stdout', { 0, { '123', 'xyz', '' } } } }, 384 -- Alternative sequence: 385 { 386 { 'notification', 'stdout', { 0, { '123', '' } } }, 387 { 'notification', 'stdout', { 0, { 'xyz', '' } } }, 388 } 389 ) 390 command('call jobsend(j, [123, "xyz", ""])') 391 expect_msg_seq( 392 { { 'notification', 'stdout', { 0, { '123', 'xyz', '' } } } }, 393 -- Alternative sequence: 394 { 395 { 'notification', 'stdout', { 0, { '123', '' } } }, 396 { 'notification', 'stdout', { 0, { 'xyz', '' } } }, 397 } 398 ) 399 command('call jobstop(j)') 400 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 401 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 402 end) 403 404 it('preserves NULs', function() 405 -- Make a file with NULs in it. 406 local filename = t.tmpname() 407 write_file(filename, 'abc\0def\n') 408 409 command("let j = jobstart(['cat', '" .. filename .. "'], g:job_opts)") 410 eq({ 'notification', 'stdout', { 0, { 'abc\ndef', '' } } }, next_msg()) 411 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 412 eq({ 'notification', 'exit', { 0, 0 } }, next_msg()) 413 os.remove(filename) 414 415 -- jobsend() preserves NULs. 416 command("let j = jobstart(['cat', '-'], g:job_opts)") 417 command([[call jobsend(j, ["123\n456",""])]]) 418 eq({ 'notification', 'stdout', { 0, { '123\n456', '' } } }, next_msg()) 419 command('call jobstop(j)') 420 end) 421 422 it('emits partial lines (does NOT buffer data lacking newlines)', function() 423 command("let j = jobstart(['cat', '-'], g:job_opts)") 424 command('call jobsend(j, "abc\\nxyz")') 425 eq({ 'notification', 'stdout', { 0, { 'abc', 'xyz' } } }, next_msg()) 426 command('call jobstop(j)') 427 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 428 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 429 end) 430 431 it('preserves newlines', function() 432 command("let j = jobstart(['cat', '-'], g:job_opts)") 433 command('call jobsend(j, "a\\n\\nc\\n\\n\\n\\nb\\n\\n")') 434 eq({ 'notification', 'stdout', { 0, { 'a', '', 'c', '', '', '', 'b', '', '' } } }, next_msg()) 435 end) 436 437 it('preserves NULs', function() 438 command("let j = jobstart(['cat', '-'], g:job_opts)") 439 command('call jobsend(j, ["\n123\n", "abc\\nxyz\n", ""])') 440 eq({ 'notification', 'stdout', { 0, { '\n123\n', 'abc\nxyz\n', '' } } }, next_msg()) 441 command('call jobstop(j)') 442 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 443 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 444 end) 445 446 it('avoids sending final newline', function() 447 command("let j = jobstart(['cat', '-'], g:job_opts)") 448 command('call jobsend(j, ["some data", "without\nfinal nl"])') 449 eq({ 'notification', 'stdout', { 0, { 'some data', 'without\nfinal nl' } } }, next_msg()) 450 command('call jobstop(j)') 451 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 452 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 453 end) 454 455 it('closes the job streams with jobclose', function() 456 command("let j = jobstart(['cat', '-'], g:job_opts)") 457 command('call jobclose(j, "stdin")') 458 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 459 eq({ 'notification', 'exit', { 0, 0 } }, next_msg()) 460 end) 461 462 it('disallows jobsend on a job that closed stdin', function() 463 command("let j = jobstart(['cat', '-'], g:job_opts)") 464 command('call jobclose(j, "stdin")') 465 eq( 466 false, 467 pcall(function() 468 command('call jobsend(j, ["some data"])') 469 end) 470 ) 471 472 command("let g:job_opts.stdin = 'null'") 473 command("let j = jobstart(['cat', '-'], g:job_opts)") 474 eq( 475 false, 476 pcall(function() 477 command('call jobsend(j, ["some data"])') 478 end) 479 ) 480 end) 481 482 it('disallows jobsend on a non-existent job', function() 483 eq(false, pcall(eval, "jobsend(-1, 'lol')")) 484 eq(0, eval('jobstop(-1)')) 485 end) 486 487 it('jobstop twice on the stopped or exited job return 0', function() 488 command("let j = jobstart(['cat', '-'], g:job_opts)") 489 neq(0, eval('j')) 490 eq(1, eval('jobstop(j)')) 491 eq(0, eval('jobstop(j)')) 492 end) 493 494 it('will not leak memory if we leave a job running', function() 495 command("call jobstart(['cat', '-'], g:job_opts)") 496 end) 497 498 it('can get the pid value using getpid', function() 499 command("let j = jobstart(['cat', '-'], g:job_opts)") 500 local pid = eval('jobpid(j)') 501 neq(NIL, api.nvim_get_proc(pid)) 502 command('call jobstop(j)') 503 eq({ 'notification', 'stdout', { 0, { '' } } }, next_msg()) 504 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 505 eq(NIL, api.nvim_get_proc(pid)) 506 end) 507 508 it('disposed on Nvim exit', function() 509 -- Start a child process which doesn't die on stdin close. 510 local j = n.fn.jobstart({ n.nvim_prog, '--clean', '--headless' }) 511 local pid = n.fn.jobpid(j) 512 eq('number', type(api.nvim_get_proc(pid).pid)) 513 clear() 514 eq(NIL, api.nvim_get_proc(pid)) 515 end) 516 517 it('can survive Nvim exit with "detach"', function() 518 local j = n.fn.jobstart({ n.nvim_prog, '--clean', '--headless' }, { detach = true }) 519 local pid = n.fn.jobpid(j) 520 eq('number', type(api.nvim_get_proc(pid).pid)) 521 clear() 522 -- Still alive. 523 eq('number', type(api.nvim_get_proc(pid).pid)) 524 -- Clean up after ourselves. 525 eq(0, vim.uv.kill(pid, 'sigkill')) 526 end) 527 528 it('can pass user data to the callback', function() 529 command('let g:job_opts.user = {"n": 5, "s": "str", "l": [1]}') 530 command([[call jobstart('echo foo', g:job_opts)]]) 531 local data = { n = 5, s = 'str', l = { 1 } } 532 expect_msg_seq( 533 { 534 { 'notification', 'stdout', { data, { 'foo', '' } } }, 535 { 'notification', 'stdout', { data, { '' } } }, 536 }, 537 -- Alternative sequence: 538 { 539 { 'notification', 'stdout', { data, { 'foo' } } }, 540 { 'notification', 'stdout', { data, { '', '' } } }, 541 { 'notification', 'stdout', { data, { '' } } }, 542 } 543 ) 544 eq({ 'notification', 'exit', { data, 0 } }, next_msg()) 545 end) 546 547 it('can omit data callbacks', function() 548 command('unlet g:job_opts.on_stdout') 549 command('let g:job_opts.user = 5') 550 command([[call jobstart('echo foo', g:job_opts)]]) 551 eq({ 'notification', 'exit', { 5, 0 } }, next_msg()) 552 end) 553 554 it('can omit exit callback', function() 555 command('unlet g:job_opts.on_exit') 556 command('let g:job_opts.user = 5') 557 command([[call jobstart('echo foo', g:job_opts)]]) 558 expect_msg_seq( 559 { 560 { 'notification', 'stdout', { 5, { 'foo', '' } } }, 561 { 'notification', 'stdout', { 5, { '' } } }, 562 }, 563 -- Alternative sequence: 564 { 565 { 'notification', 'stdout', { 5, { 'foo' } } }, 566 { 'notification', 'stdout', { 5, { '', '' } } }, 567 { 'notification', 'stdout', { 5, { '' } } }, 568 } 569 ) 570 end) 571 572 it('will pass return code with the exit event', function() 573 command('let g:job_opts.user = 5') 574 command("call jobstart('exit 55', g:job_opts)") 575 eq({ 'notification', 'stdout', { 5, { '' } } }, next_msg()) 576 eq({ 'notification', 'exit', { 5, 55 } }, next_msg()) 577 end) 578 579 it('can receive dictionary functions', function() 580 source([[ 581 let g:dict = {'id': 10} 582 function g:dict.on_exit(id, code, event) 583 call rpcnotify(g:channel, a:event, a:code, self.id) 584 endfunction 585 call jobstart('exit 45', g:dict) 586 ]]) 587 eq({ 'notification', 'exit', { 45, 10 } }, next_msg()) 588 end) 589 590 it('can redefine callbacks being used by a job', function() 591 local screen = Screen.new() 592 screen:set_default_attr_ids({ 593 [1] = { bold = true, foreground = Screen.colors.Blue }, 594 }) 595 source([[ 596 function! g:JobHandler(job_id, data, event) 597 endfunction 598 599 let g:callbacks = { 600 \ 'on_stdout': function('g:JobHandler'), 601 \ 'on_stderr': function('g:JobHandler'), 602 \ 'on_exit': function('g:JobHandler') 603 \ } 604 let job = jobstart(['cat', '-'], g:callbacks) 605 ]]) 606 poke_eventloop() 607 source([[ 608 function! g:JobHandler(job_id, data, event) 609 endfunction 610 ]]) 611 612 eq('', eval('v:errmsg')) 613 end) 614 615 it('requires funcrefs for script-local (s:) functions', function() 616 local screen = Screen.new(60, 5) 617 screen:set_default_attr_ids({ 618 [1] = { bold = true, foreground = Screen.colors.Blue1 }, 619 [2] = { foreground = Screen.colors.Grey100, background = Screen.colors.Red }, 620 [3] = { bold = true, foreground = Screen.colors.SeaGreen4 }, 621 }) 622 623 -- Pass job callback names _without_ `function(...)`. 624 source([[ 625 function! s:OnEvent(id, data, event) dict 626 let g:job_result = get(self, 'user') 627 endfunction 628 let s:job = jobstart('echo "foo"', { 629 \ 'on_stdout': 's:OnEvent', 630 \ 'on_stderr': 's:OnEvent', 631 \ 'on_exit': 's:OnEvent', 632 \ }) 633 ]]) 634 635 screen:expect { any = '{2:E120: Using <SID> not in a script context: s:OnEvent}' } 636 end) 637 638 it('does not repeat output with slow output handlers', function() 639 source([[ 640 let d = {'data': []} 641 function! d.on_stdout(job, data, event) dict 642 call add(self.data, Normalize(a:data)) 643 sleep 200m 644 endfunction 645 function! d.on_exit(job, data, event) dict 646 let g:exit_data = copy(self.data) 647 endfunction 648 if has('win32') 649 let cmd = 'for /L %I in (1,1,5) do @(echo %I& ping -n 2 127.0.0.1 > nul)' 650 else 651 let cmd = ['sh', '-c', 'for i in 1 2 3 4 5; do echo $i; sleep 0.1; done'] 652 endif 653 let g:id = jobstart(cmd, d) 654 sleep 1500m 655 call jobwait([g:id]) 656 ]]) 657 658 local expected = { '1', '2', '3', '4', '5', '' } 659 local chunks = eval('d.data') 660 -- check nothing was received after exit, including EOF 661 eq(eval('g:exit_data'), chunks) 662 local received = { '' } 663 for i, chunk in ipairs(chunks) do 664 if i < #chunks then 665 -- if chunks got joined, a spurious [''] callback was not sent 666 neq({ '' }, chunk) 667 else 668 -- but EOF callback is still sent 669 eq({ '' }, chunk) 670 end 671 received[#received] = received[#received] .. chunk[1] 672 for j = 2, #chunk do 673 received[#received + 1] = chunk[j] 674 end 675 end 676 eq(expected, received) 677 end) 678 679 it('does not invoke callbacks recursively', function() 680 source([[ 681 let d = {'data': []} 682 function! d.on_stdout(job, data, event) dict 683 " if callbacks were invoked recursively, this would cause on_stdout 684 " to be invoked recursively and the data reversed on the call stack 685 sleep 200m 686 call add(self.data, Normalize(a:data)) 687 endfunction 688 function! d.on_exit(job, data, event) dict 689 let g:exit_data = copy(self.data) 690 endfunction 691 if has('win32') 692 let cmd = 'for /L %I in (1,1,5) do @(echo %I& ping -n 2 127.0.0.1 > nul)' 693 else 694 let cmd = ['sh', '-c', 'for i in 1 2 3 4 5; do echo $i; sleep 0.1; done'] 695 endif 696 let g:id = jobstart(cmd, d) 697 sleep 1500m 698 call jobwait([g:id]) 699 ]]) 700 701 local expected = { '1', '2', '3', '4', '5', '' } 702 local chunks = eval('d.data') 703 -- check nothing was received after exit, including EOF 704 eq(eval('g:exit_data'), chunks) 705 local received = { '' } 706 for i, chunk in ipairs(chunks) do 707 if i < #chunks then 708 -- if chunks got joined, a spurious [''] callback was not sent 709 neq({ '' }, chunk) 710 else 711 -- but EOF callback is still sent 712 eq({ '' }, chunk) 713 end 714 received[#received] = received[#received] .. chunk[1] 715 for j = 2, #chunk do 716 received[#received + 1] = chunk[j] 717 end 718 end 719 eq(expected, received) 720 end) 721 722 it('jobstart() works with partial functions', function() 723 source([[ 724 function PrintArgs(a1, a2, id, data, event) 725 " Windows: remove ^M 726 let normalized = mapnew(a:data, 'substitute(v:val, "\r", "", "g")') 727 call rpcnotify(g:channel, '1', a:a1, a:a2, normalized, a:event) 728 endfunction 729 let Callback = function('PrintArgs', ["foo", "bar"]) 730 let g:job_opts = {'on_stdout': Callback} 731 call jobstart('echo some text', g:job_opts) 732 ]]) 733 expect_msg_seq( 734 { { 'notification', '1', { 'foo', 'bar', { 'some text', '' }, 'stdout' } } }, 735 -- Alternative sequence: 736 { 737 { 'notification', '1', { 'foo', 'bar', { 'some text' }, 'stdout' } }, 738 { 'notification', '1', { 'foo', 'bar', { '', '' }, 'stdout' } }, 739 } 740 ) 741 end) 742 743 it('jobstart() works with closures', function() 744 source([[ 745 fun! MkFun() 746 let a1 = 'foo' 747 let a2 = 'bar' 748 return {id, data, event -> rpcnotify(g:channel, '1', a1, a2, Normalize(data), event)} 749 endfun 750 let g:job_opts = {'on_stdout': MkFun()} 751 call jobstart('echo some text', g:job_opts) 752 ]]) 753 expect_msg_seq( 754 { { 'notification', '1', { 'foo', 'bar', { 'some text', '' }, 'stdout' } } }, 755 -- Alternative sequence: 756 { 757 { 'notification', '1', { 'foo', 'bar', { 'some text' }, 'stdout' } }, 758 { 'notification', '1', { 'foo', 'bar', { '', '' }, 'stdout' } }, 759 } 760 ) 761 end) 762 763 it('jobstart() works when closure passed directly to `jobstart`', function() 764 source([[ 765 let g:job_opts = {'on_stdout': {id, data, event -> rpcnotify(g:channel, '1', 'foo', 'bar', Normalize(data), event)}} 766 call jobstart('echo some text', g:job_opts) 767 ]]) 768 expect_msg_seq( 769 { { 'notification', '1', { 'foo', 'bar', { 'some text', '' }, 'stdout' } } }, 770 -- Alternative sequence: 771 { 772 { 'notification', '1', { 'foo', 'bar', { 'some text' }, 'stdout' } }, 773 { 'notification', '1', { 'foo', 'bar', { '', '' }, 'stdout' } }, 774 } 775 ) 776 end) 777 778 it('lists passed to callbacks are freed if not stored #25891', function() 779 if not exec_lua('return pcall(require, "ffi")') then 780 pending('N/A: missing LuaJIT FFI') 781 end 782 783 source([[ 784 let g:stdout = '' 785 func AppendStrOnEvent(id, data, event) 786 let g:stdout ..= join(a:data, "\n") 787 endfunc 788 let g:job_opts = {'on_stdout': function('AppendStrOnEvent')} 789 ]]) 790 local job = eval([[jobstart(['cat', '-'], g:job_opts)]]) 791 792 exec_lua(function() 793 local ffi = require('ffi') 794 ffi.cdef([[ 795 typedef struct listvar_S list_T; 796 list_T *gc_first_list; 797 list_T *tv_list_alloc(ptrdiff_t len); 798 void tv_list_free(list_T *const l); 799 ]]) 800 _G.L = ffi.C.tv_list_alloc(1) 801 _G.L_val = ffi.cast('uintptr_t', _G.L) 802 assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) == _G.L_val) 803 end) 804 805 local str_all = '' 806 for _, str in ipairs({ 'LINE1\nLINE2\nLINE3\n', 'LINE4\n', 'LINE5\nLINE6\n' }) do 807 str_all = str_all .. str 808 api.nvim_chan_send(job, str) 809 retry(nil, 1000, function() 810 eq(str_all, api.nvim_get_var('stdout')) 811 end) 812 end 813 814 exec_lua(function() 815 local ffi = require('ffi') 816 assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) == _G.L_val) 817 ffi.C.tv_list_free(_G.L) 818 assert(ffi.cast('uintptr_t', ffi.C.gc_first_list) ~= _G.L_val) 819 end) 820 end) 821 822 it('jobstart() environment: $NVIM, $NVIM_LISTEN_ADDRESS #11009', function() 823 local function get_child_env(envname, env) 824 return exec_lua( 825 [[ 826 local envname, env = ... 827 local join = function(s) return vim.fn.join(s, '') end 828 local stdout = {} 829 local stderr = {} 830 local opt = { 831 env = env, 832 stdout_buffered = true, 833 stderr_buffered = true, 834 on_stderr = function(chan, data, name) stderr = data end, 835 on_stdout = function(chan, data, name) stdout = data end, 836 } 837 local j1 = vim.fn.jobstart({ vim.v.progpath, '-es', '-V1',('+echo "%s="..getenv("%s")'):format(envname, envname), '+qa!' }, opt) 838 vim.fn.jobwait({ j1 }, 10000) 839 return join({ join(stdout), join(stderr) }) 840 ]], 841 envname, 842 env 843 ) 844 end 845 846 local addr = eval('v:servername') 847 ok((addr):len() > 0) 848 -- $NVIM is _not_ defined in the top-level Nvim process. 849 eq('', eval('$NVIM')) 850 -- jobstart() shares its v:servername with the child via $NVIM. 851 eq('NVIM=' .. addr, get_child_env('NVIM')) 852 -- $NVIM_LISTEN_ADDRESS is unset by server_init in the child. 853 eq('NVIM_LISTEN_ADDRESS=v:null', get_child_env('NVIM_LISTEN_ADDRESS')) 854 eq( 855 'NVIM_LISTEN_ADDRESS=v:null', 856 get_child_env('NVIM_LISTEN_ADDRESS', { NVIM_LISTEN_ADDRESS = 'Xtest_jobstart_env' }) 857 ) 858 -- User can explicitly set $NVIM_LOG_FILE, $VIM, $VIMRUNTIME. 859 eq( 860 'NVIM_LOG_FILE=Xtest_jobstart_env', 861 get_child_env('NVIM_LOG_FILE', { NVIM_LOG_FILE = 'Xtest_jobstart_env' }) 862 ) 863 os.remove('Xtest_jobstart_env') 864 end) 865 866 describe('jobwait()', function() 867 before_each(function() 868 if is_os('win') then 869 n.set_shell_powershell() 870 end 871 end) 872 873 it('returns a list of status codes', function() 874 source([[ 875 call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ 876 \ jobstart('Start-Sleep -Milliseconds 100; exit 4'), 877 \ jobstart('Start-Sleep -Milliseconds 300; exit 5'), 878 \ jobstart('Start-Sleep -Milliseconds 500; exit 6'), 879 \ jobstart('Start-Sleep -Milliseconds 700; exit 7') 880 \ ] : [ 881 \ jobstart('sleep 0.10; exit 4'), 882 \ jobstart('sleep 0.110; exit 5'), 883 \ jobstart('sleep 0.210; exit 6'), 884 \ jobstart('sleep 0.310; exit 7') 885 \ ])) 886 ]]) 887 eq({ 'notification', 'wait', { { 4, 5, 6, 7 } } }, next_msg()) 888 end) 889 890 it('will run callbacks while waiting', function() 891 source([[ 892 let g:dict = {} 893 let g:jobs = [] 894 let g:exits = [] 895 function g:dict.on_stdout(id, code, event) abort 896 call add(g:jobs, a:id) 897 endfunction 898 function g:dict.on_exit(id, code, event) abort 899 if a:code != 5 900 throw 'Error!' 901 endif 902 call add(g:exits, a:id) 903 endfunction 904 call jobwait(has('win32') ? [ 905 \ jobstart('Start-Sleep -Milliseconds 100; exit 5', g:dict), 906 \ jobstart('Start-Sleep -Milliseconds 300; exit 5', g:dict), 907 \ jobstart('Start-Sleep -Milliseconds 500; exit 5', g:dict), 908 \ jobstart('Start-Sleep -Milliseconds 700; exit 5', g:dict) 909 \ ] : [ 910 \ jobstart('sleep 0.010; exit 5', g:dict), 911 \ jobstart('sleep 0.030; exit 5', g:dict), 912 \ jobstart('sleep 0.050; exit 5', g:dict), 913 \ jobstart('sleep 0.070; exit 5', g:dict) 914 \ ]) 915 call rpcnotify(g:channel, 'wait', sort(g:jobs), sort(g:exits)) 916 ]]) 917 eq({ 'notification', 'wait', { { 3, 4, 5, 6 }, { 3, 4, 5, 6 } } }, next_msg()) 918 end) 919 920 it('will return status codes in the order of passed ids', function() 921 source([[ 922 call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ 923 \ jobstart('Start-Sleep -Milliseconds 700; exit 4'), 924 \ jobstart('Start-Sleep -Milliseconds 500; exit 5'), 925 \ jobstart('Start-Sleep -Milliseconds 300; exit 6'), 926 \ jobstart('Start-Sleep -Milliseconds 100; exit 7') 927 \ ] : [ 928 \ jobstart('sleep 0.070; exit 4'), 929 \ jobstart('sleep 0.050; exit 5'), 930 \ jobstart('sleep 0.030; exit 6'), 931 \ jobstart('sleep 0.010; exit 7') 932 \ ])) 933 ]]) 934 eq({ 'notification', 'wait', { { 4, 5, 6, 7 } } }, next_msg()) 935 end) 936 937 it('will return -3 for invalid job ids', function() 938 source([[ 939 call rpcnotify(g:channel, 'wait', jobwait([ 940 \ -10, 941 \ jobstart((has('win32') ? 'Start-Sleep -Milliseconds 100' : 'sleep 0.01').'; exit 5'), 942 \ ])) 943 ]]) 944 eq({ 'notification', 'wait', { { -3, 5 } } }, next_msg()) 945 end) 946 947 it('will return -2 when interrupted without timeout', function() 948 feed_command( 949 'call rpcnotify(g:channel, "ready") | ' 950 .. 'call rpcnotify(g:channel, "wait", ' 951 .. 'jobwait([jobstart("' 952 .. (is_os('win') and 'Start-Sleep 10' or 'sleep 10') 953 .. '; exit 55")]))' 954 ) 955 eq({ 'notification', 'ready', {} }, next_msg()) 956 feed('<c-c>') 957 eq({ 'notification', 'wait', { { -2 } } }, next_msg()) 958 end) 959 960 it('will return -2 when interrupted with timeout', function() 961 feed_command( 962 'call rpcnotify(g:channel, "ready") | ' 963 .. 'call rpcnotify(g:channel, "wait", ' 964 .. 'jobwait([jobstart("' 965 .. (is_os('win') and 'Start-Sleep 10' or 'sleep 10') 966 .. '; exit 55")], 10000))' 967 ) 968 eq({ 'notification', 'ready', {} }, next_msg()) 969 feed('<c-c>') 970 eq({ 'notification', 'wait', { { -2 } } }, next_msg()) 971 end) 972 973 it('can be called recursively', function() 974 source([[ 975 let g:opts = {} 976 let g:counter = 0 977 function g:opts.on_stdout(id, msg, _event) 978 if self.state == 0 979 if self.counter < 10 980 call Run() 981 endif 982 let self.state = 1 983 call jobsend(a:id, "line1\n") 984 elseif self.state == 1 985 let self.state = 2 986 call jobsend(a:id, "line2\n") 987 elseif self.state == 2 988 let self.state = 3 989 call jobsend(a:id, "line3\n") 990 elseif self.state == 3 991 let self.state = 4 992 call rpcnotify(g:channel, 'w', printf('job %d closed', self.counter)) 993 call jobclose(a:id, 'stdin') 994 endif 995 endfunction 996 function g:opts.on_exit(...) 997 call rpcnotify(g:channel, 'w', printf('job %d exited', self.counter)) 998 endfunction 999 function Run() 1000 let g:counter += 1 1001 let j = copy(g:opts) 1002 let j.state = 0 1003 let j.counter = g:counter 1004 call jobwait([ 1005 \ jobstart('echo ready; cat -', j), 1006 \ ]) 1007 endfunction 1008 ]]) 1009 feed_command('call Run()') 1010 local r 1011 for i = 10, 1, -1 do 1012 r = next_msg() 1013 eq('job ' .. i .. ' closed', r[3][1]) 1014 r = next_msg() 1015 eq('job ' .. i .. ' exited', r[3][1]) 1016 end 1017 eq(10, api.nvim_eval('g:counter')) 1018 end) 1019 1020 describe('with timeout argument', function() 1021 it('will return -1 if the wait timed out', function() 1022 source([[ 1023 call rpcnotify(g:channel, 'wait', jobwait([ 1024 \ jobstart((has('win32') ? 'Start-Sleep 10' : 'sleep 10').'; exit 5'), 1025 \ ], 100)) 1026 ]]) 1027 eq({ 'notification', 'wait', { { -1 } } }, next_msg()) 1028 end) 1029 1030 it('can pass 0 to check if a job exists', function() 1031 source([[ 1032 call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ 1033 \ jobstart('Start-Sleep -Milliseconds 50; exit 4'), 1034 \ jobstart('Start-Sleep -Milliseconds 300; exit 5'), 1035 \ ] : [ 1036 \ jobstart('sleep 0.05; exit 4'), 1037 \ jobstart('sleep 0.3; exit 5'), 1038 \ ], 0)) 1039 ]]) 1040 eq({ 'notification', 'wait', { { -1, -1 } } }, next_msg()) 1041 end) 1042 end) 1043 1044 it('hides cursor and flushes messages before blocking', function() 1045 local screen = Screen.new(50, 6) 1046 command([[let g:id = jobstart([v:progpath, '--clean', '--headless'])]]) 1047 source([[ 1048 func PrintAndWait() 1049 echon "aaa\nbbb" 1050 call jobwait([g:id], 300) 1051 echon "\nccc" 1052 endfunc 1053 ]]) 1054 feed(':call PrintAndWait()') 1055 screen:expect([[ 1056 | 1057 {1:~ }|*4 1058 :call PrintAndWait()^ | 1059 ]]) 1060 feed('<CR>') 1061 screen:expect { 1062 grid = [[ 1063 | 1064 {1:~ }|*2 1065 {3: }| 1066 aaa | 1067 bbb | 1068 ]], 1069 timeout = 100, 1070 } 1071 screen:expect([[ 1072 | 1073 {3: }| 1074 aaa | 1075 bbb | 1076 ccc | 1077 {6:Press ENTER or type command to continue}^ | 1078 ]]) 1079 feed('<CR>') 1080 fn.jobstop(api.nvim_get_var('id')) 1081 end) 1082 1083 it('does not set UI busy with zero timeout #31712', function() 1084 local screen = Screen.new(50, 6) 1085 command([[let g:id = jobstart(['sleep', '0.3'])]]) 1086 local busy = 0 1087 screen._handle_busy_start = (function(orig) 1088 return function() 1089 orig(screen) 1090 busy = busy + 1 1091 end 1092 end)(screen._handle_busy_start) 1093 source([[ 1094 func PrintAndPoll() 1095 echon "aaa\nbbb" 1096 call jobwait([g:id], 0) 1097 echon "\nccc" 1098 endfunc 1099 ]]) 1100 feed_command('call PrintAndPoll()') 1101 screen:expect([[ 1102 | 1103 {3: }| 1104 aaa | 1105 bbb | 1106 ccc | 1107 {6:Press ENTER or type command to continue}^ | 1108 ]]) 1109 feed('<CR>') 1110 fn.jobstop(api.nvim_get_var('id')) 1111 eq(0, busy) 1112 end) 1113 end) 1114 1115 pending('exit event follows stdout, stderr', function() 1116 command("let g:job_opts.on_stderr = function('OnEvent')") 1117 command("let j = jobstart(['cat', '-'], g:job_opts)") 1118 api.nvim_eval('jobsend(j, "abcdef")') 1119 api.nvim_eval('jobstop(j)') 1120 expect_msg_seq( 1121 { 1122 { 'notification', 'stdout', { 0, { 'abcdef' } } }, 1123 { 'notification', 'stdout', { 0, { '' } } }, 1124 { 'notification', 'stderr', { 0, { '' } } }, 1125 }, 1126 -- Alternative sequence: 1127 { 1128 { 'notification', 'stderr', { 0, { '' } } }, 1129 { 'notification', 'stdout', { 0, { 'abcdef' } } }, 1130 { 'notification', 'stdout', { 0, { '' } } }, 1131 }, 1132 -- Alternative sequence: 1133 { 1134 { 'notification', 'stdout', { 0, { 'abcdef' } } }, 1135 { 'notification', 'stderr', { 0, { '' } } }, 1136 { 'notification', 'stdout', { 0, { '' } } }, 1137 } 1138 ) 1139 eq({ 'notification', 'exit', { 0, 143 } }, next_msg()) 1140 end) 1141 1142 it('does not crash when repeatedly failing to start shell', function() 1143 source([[ 1144 set shell=nosuchshell 1145 func! DoIt() 1146 call jobstart('true') 1147 call jobstart('true') 1148 endfunc 1149 ]]) 1150 -- The crash only triggered if both jobs are cleaned up on the same event 1151 -- loop tick. This is also prevented by try-block, so feed must be used. 1152 feed_command('call DoIt()') 1153 feed('<cr>') -- press RETURN 1154 assert_alive() 1155 end) 1156 1157 it('jobstop() kills entire process tree #6530', function() 1158 -- XXX: Using `nvim` isn't a good test, it reaps its children on exit. 1159 -- local c = 'call jobstart([v:progpath, "-u", "NONE", "-i", "NONE", "--headless"])' 1160 -- local j = eval("jobstart([v:progpath, '-u', 'NONE', '-i', 'NONE', '--headless', '-c', '" 1161 -- ..c.."', '-c', '"..c.."'])") 1162 1163 -- Create child with several descendants. 1164 if is_os('win') then 1165 source([[ 1166 function! s:formatprocs(pid, prefix) 1167 let result = '' 1168 let result .= a:prefix . printf("%-24.24s%6s %12.12s %s\n", 1169 \ s:procs[a:pid]['name'], 1170 \ a:pid, 1171 \ s:procs[a:pid]['Session Name'], 1172 \ s:procs[a:pid]['Session']) 1173 if has_key(s:procs[a:pid], 'children') 1174 for pid in s:procs[a:pid]['children'] 1175 let result .= s:formatprocs(pid, a:prefix . ' ') 1176 endfor 1177 endif 1178 return result 1179 endfunction 1180 1181 function! PsTree() abort 1182 let s:procs = {} 1183 for proc in map( 1184 \ map( 1185 \ systemlist('tasklist /NH'), 1186 \ 'substitute(v:val, "\r", "", "")'), 1187 \ 'split(v:val, "\\s\\+")') 1188 if len(proc) == 6 1189 let s:procs[proc[1]] .. ']]' .. [[= {'name': proc[0], 1190 \ 'Session Name': proc[2], 1191 \ 'Session': proc[3]} 1192 endif 1193 endfor 1194 for pid in keys(s:procs) 1195 let children = nvim_get_proc_children(str2nr(pid)) 1196 if !empty(children) 1197 let s:procs[pid]['children'] = children 1198 for cpid in children 1199 let s:procs[printf('%d', cpid)]['parent'] = str2nr(pid) 1200 endfor 1201 endif 1202 endfor 1203 let result = '' 1204 for pid in sort(keys(s:procs), {i1, i2 -> i1 - i2}) 1205 if !has_key(s:procs[pid], 'parent') 1206 let result .= s:formatprocs(pid, '') 1207 endif 1208 endfor 1209 return result 1210 endfunction 1211 ]]) 1212 end 1213 local sleep_cmd = (is_os('win') and 'ping -n 31 127.0.0.1' or 'sleep 30') 1214 local j = eval("jobstart('" .. sleep_cmd .. ' | ' .. sleep_cmd .. ' | ' .. sleep_cmd .. "')") 1215 local ppid = fn.jobpid(j) 1216 local children 1217 if is_os('win') then 1218 local status, result = pcall(retry, nil, nil, function() 1219 children = api.nvim_get_proc_children(ppid) 1220 -- On Windows conhost.exe may exist, and 1221 -- e.g. vctip.exe might appear. #10783 1222 ok(#children >= 3 and #children <= 5) 1223 end) 1224 if not status then 1225 print('') 1226 print(eval('PsTree()')) 1227 error(result) 1228 end 1229 else 1230 retry(nil, nil, function() 1231 children = api.nvim_get_proc_children(ppid) 1232 eq(3, #children) 1233 end) 1234 end 1235 -- Assert that nvim_get_proc() sees the children. 1236 for _, child_pid in ipairs(children) do 1237 local info = api.nvim_get_proc(child_pid) 1238 -- eq((is_os('win') and 'nvim.exe' or 'nvim'), info.name) 1239 eq(ppid, info.ppid) 1240 end 1241 -- Kill the root of the tree. 1242 eq(1, fn.jobstop(j)) 1243 -- Assert that the children were killed. 1244 retry(nil, nil, function() 1245 for _, child_pid in ipairs(children) do 1246 eq(NIL, api.nvim_get_proc(child_pid)) 1247 end 1248 end) 1249 end) 1250 1251 it('jobstop on same id before stopped', function() 1252 command('let j = jobstart(["cat", "-"], g:job_opts)') 1253 neq(0, eval('j')) 1254 1255 eq({ 1, 0 }, eval('[jobstop(j), jobstop(j)]')) 1256 end) 1257 1258 describe('running tty-test program', function() 1259 if skip(is_os('win')) then 1260 return 1261 end 1262 local function next_chunk() 1263 local rv 1264 while true do 1265 local msg = next_msg() 1266 local data = msg[3][2] 1267 for i = 1, #data do 1268 data[i] = data[i]:gsub('\n', '\000') 1269 end 1270 rv = table.concat(data, '\n') 1271 rv = rv:gsub('\r\n$', ''):gsub('^\r\n', '') 1272 if rv ~= '' then 1273 break 1274 end 1275 end 1276 return rv 1277 end 1278 1279 local j 1280 local function send(str) 1281 -- check no nvim_chan_free double free with pty job (#14198) 1282 api.nvim_chan_send(j, str) 1283 end 1284 1285 before_each(function() 1286 -- Redefine Normalize() so that TTY data is not munged. 1287 source([[ 1288 function! Normalize(data) abort 1289 return a:data 1290 endfunction 1291 ]]) 1292 insert(testprg('tty-test')) 1293 command('let g:job_opts.pty = 1') 1294 command('let exec = [expand("<cfile>:p")]') 1295 command('let j = jobstart(exec, g:job_opts)') 1296 j = eval 'j' 1297 eq('tty ready', next_chunk()) 1298 end) 1299 1300 it('echoing input', function() 1301 send('test') 1302 eq('test', next_chunk()) 1303 end) 1304 1305 it('resizing window', function() 1306 command('call jobresize(j, 40, 10)') 1307 eq('rows: 10, cols: 40', next_chunk()) 1308 command('call jobresize(j, 10, 40)') 1309 eq('rows: 40, cols: 10', next_chunk()) 1310 end) 1311 1312 it('jobclose() sends SIGHUP', function() 1313 command('call jobclose(j)') 1314 local msg = next_msg() 1315 msg = (msg[2] == 'stdout') and next_msg() or msg -- Skip stdout, if any. 1316 eq({ 'notification', 'exit', { 0, 42 } }, msg) 1317 end) 1318 1319 it('jobstart() does not keep ptmx file descriptor open', function() 1320 -- Start another job (using libuv) 1321 command('let g:job_opts.pty = 0') 1322 local other_jobid = eval("jobstart(['cat', '-'], g:job_opts)") 1323 local other_pid = eval('jobpid(' .. other_jobid .. ')') 1324 1325 -- Other job doesn't block first job from receiving SIGHUP on jobclose() 1326 command('call jobclose(j)') 1327 -- Have to wait so that the SIGHUP can be processed by tty-test on time. 1328 -- Can't wait for the next message in case this test fails, if it fails 1329 -- there won't be any more messages, and the test would hang. 1330 vim.uv.sleep(100) 1331 local err = exc_exec('call jobpid(j)') 1332 eq('Vim(call):E900: Invalid channel id', err) 1333 1334 -- cleanup 1335 eq(other_pid, eval('jobpid(' .. other_jobid .. ')')) 1336 command('call jobstop(' .. other_jobid .. ')') 1337 end) 1338 end) 1339 1340 it('does not close the same handle twice on exit #25086', function() 1341 local filename = string.format('%s.lua', t.tmpname()) 1342 write_file( 1343 filename, 1344 [[ 1345 vim.api.nvim_create_autocmd('VimLeavePre', { 1346 callback = function() 1347 local id = vim.fn.jobstart('sleep 0') 1348 vim.fn.jobwait({id}) 1349 end, 1350 }) 1351 ]] 1352 ) 1353 1354 local screen = tt.setup_child_nvim({ 1355 '--cmd', 1356 'set notermguicolors', 1357 '-i', 1358 'NONE', 1359 '-u', 1360 filename, 1361 }) 1362 -- Wait for startup to complete, so that all terminal responses are received. 1363 screen:expect([[ 1364 ^ | 1365 ~ |*3 1366 {2:[No Name] 0,0-1 All}| 1367 | 1368 {5:-- TERMINAL --} | 1369 ]]) 1370 1371 feed(':q<CR>') 1372 if is_os('freebsd') then 1373 screen:expect { any = vim.pesc('[Process exited 0]') } 1374 else 1375 screen:expect([[ 1376 | 1377 [Process exited 0]^ | 1378 |*4 1379 {5:-- TERMINAL --} | 1380 ]]) 1381 end 1382 end) 1383 1384 it('uses real pipes for stdin/stdout #35984', function() 1385 if is_os('win') then 1386 return -- Not applicable for Windows. 1387 end 1388 1389 -- this fails on linux if we used socketpair() for stdin and stdout, 1390 -- which libuv does if you ask to create stdio streams for you 1391 local val = exec_lua(function() 1392 local output 1393 local job = vim.fn.jobstart('wc /dev/stdin > /dev/stdout', { 1394 stdout_buffered = true, 1395 on_stdout = function(_, data, _) 1396 output = data 1397 end, 1398 }) 1399 vim.fn.chansend(job, 'foo\nbar baz\n') 1400 vim.fn.chanclose(job, 'stdin') 1401 vim.fn.jobwait({ job }) 1402 return output 1403 end) 1404 eq(2, #val, val) 1405 eq({ '2', '3', '12', '/dev/stdin' }, vim.split(val[1], '%s+', { trimempty = true })) 1406 eq('', val[2]) 1407 end) 1408 end) 1409 1410 describe('pty process teardown', function() 1411 local screen 1412 before_each(function() 1413 clear() 1414 screen = Screen.new(30, 6) 1415 screen:expect([[ 1416 ^ | 1417 {1:~ }|*4 1418 | 1419 ]]) 1420 end) 1421 1422 it('does not prevent/delay exit. #4798 #4900', function() 1423 skip(fn.executable('sleep') == 0, 'missing "sleep" command') 1424 -- Use a nested nvim (in :term) to test without --headless. 1425 fn.jobstart({ 1426 n.nvim_prog, 1427 '-u', 1428 'NONE', 1429 '-i', 1430 'NONE', 1431 '--cmd', 1432 nvim_set, 1433 -- Use :term again in the _nested_ nvim to get a PTY process. 1434 -- Use `sleep` to simulate a long-running child of the PTY. 1435 '+terminal', 1436 '+!(sleep 300 &)', 1437 '+qa', 1438 }, { 1439 term = true, 1440 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 1441 }) 1442 1443 -- Exiting should terminate all descendants (PTY, its children, ...). 1444 screen:expect([[ 1445 ^ | 1446 [Process exited 0] | 1447 |*4 1448 ]]) 1449 end) 1450 end)