neovim

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

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)