view src/testdir/test_channel.vim @ 11416:32aed0993813 v8.0.0592

patch 8.0.0592: if a job writes to a buffer screen is not updated commit https://github.com/vim/vim/commit/29ae377ea7039874337bc79ace9ab2b37b9056e5 Author: Bram Moolenaar <Bram@vim.org> Date: Sun Apr 30 19:39:39 2017 +0200 patch 8.0.0592: if a job writes to a buffer screen is not updated Problem: If a job writes to a buffer and the user is typing a command, the screen isn't updated. When a message is displayed the changed buffer may cause it to be cleared. (Ramel Eshed) Solution: Update the screen and then the command line if the screen didn't scroll. Avoid inserting screen lines, as it clears any message. Update the status line when the buffer changed.
author Christian Brabandt <cb@256bit.org>
date Sun, 30 Apr 2017 19:45:03 +0200
parents 1c4ebbae41d2
children 5cd9ba96561d
line wrap: on
line source

" Test for channel functions.

if !has('channel')
  finish
endif

source shared.vim

let s:python = PythonProg()
if s:python == ''
  " Can't run this test without Python.
  finish
endif

" Uncomment the next line to see what happens. Output is in
" src/testdir/channellog.
" call ch_logfile('channellog', 'w')

let s:chopt = {}

" Run "testfunc" after sarting the server and stop the server afterwards.
func s:run_server(testfunc, ...)
  call RunServer('test_channel.py', a:testfunc, a:000)
endfunc

let g:Ch_responseMsg = ''
func Ch_requestHandler(handle, msg)
  let g:Ch_responseHandle = a:handle
  let g:Ch_responseMsg = a:msg
endfunc

func Ch_communicate(port)
  " Avoid dropping messages, since we don't use a callback here.
  let s:chopt.drop = 'never'
  let handle = ch_open('localhost:' . a:port, s:chopt)
  unlet s:chopt.drop
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  if has('job')
    " check that getjob without a job is handled correctly
    call assert_equal('no process', string(ch_getjob(handle)))
  endif
  let dict = ch_info(handle)
  call assert_true(dict.id != 0)
  call assert_equal('open', dict.status)
  call assert_equal(a:port, string(dict.port))
  call assert_equal('open', dict.sock_status)
  call assert_equal('socket', dict.sock_io)

  " Simple string request and reply.
  call assert_equal('got it', ch_evalexpr(handle, 'hello!'))

  " Malformed command should be ignored.
  call assert_equal('ok', ch_evalexpr(handle, 'malformed1'))
  call assert_equal('ok', ch_evalexpr(handle, 'malformed2'))
  call assert_equal('ok', ch_evalexpr(handle, 'malformed3'))

  " split command should work
  call assert_equal('ok', ch_evalexpr(handle, 'split'))
  call WaitFor('exists("g:split")')
  call assert_equal(123, g:split)

  " string with ][ should work
  call assert_equal('this][that', ch_evalexpr(handle, 'echo this][that'))

  " nothing to read now
  call assert_equal(0, ch_canread(handle))

  " sending three messages quickly then reading should work
  for i in range(3)
    call ch_sendexpr(handle, 'echo hello ' . i)
  endfor
  call assert_equal('hello 0', ch_read(handle)[1])
  call assert_equal('hello 1', ch_read(handle)[1])
  call assert_equal('hello 2', ch_read(handle)[1])

  " Request that triggers sending two ex commands.  These will usually be
  " handled before getting the response, but it's not guaranteed, thus wait a
  " tiny bit for the commands to get executed.
  call assert_equal('ok', ch_evalexpr(handle, 'make change'))
  call WaitFor('"added2" == getline("$")')
  call assert_equal('added1', getline(line('$') - 1))
  call assert_equal('added2', getline('$'))

  " Request command "foo bar", which fails silently.
  call assert_equal('ok', ch_evalexpr(handle, 'bad command'))
  call WaitFor('v:errmsg =~ "E492"')
  call assert_match('E492:.*foo bar', v:errmsg)

  call assert_equal('ok', ch_evalexpr(handle, 'do normal', {'timeout': 100}))
  call WaitFor('"added more" == getline("$")')
  call assert_equal('added more', getline('$'))

  " Send a request with a specific handler.
  call ch_sendexpr(handle, 'hello!', {'callback': 'Ch_requestHandler'})
  call WaitFor('exists("g:Ch_responseHandle")')
  if !exists('g:Ch_responseHandle')
    call assert_report('g:Ch_responseHandle was not set')
  else
    call assert_equal(handle, g:Ch_responseHandle)
    unlet g:Ch_responseHandle
  endif
  call assert_equal('got it', g:Ch_responseMsg)

  let g:Ch_responseMsg = ''
  call ch_sendexpr(handle, 'hello!', {'callback': function('Ch_requestHandler')})
  call WaitFor('exists("g:Ch_responseHandle")')
  if !exists('g:Ch_responseHandle')
    call assert_report('g:Ch_responseHandle was not set')
  else
    call assert_equal(handle, g:Ch_responseHandle)
    unlet g:Ch_responseHandle
  endif
  call assert_equal('got it', g:Ch_responseMsg)

  " Using lambda.
  let g:Ch_responseMsg = ''
  call ch_sendexpr(handle, 'hello!', {'callback': {a, b -> Ch_requestHandler(a, b)}})
  call WaitFor('exists("g:Ch_responseHandle")')
  if !exists('g:Ch_responseHandle')
    call assert_report('g:Ch_responseHandle was not set')
  else
    call assert_equal(handle, g:Ch_responseHandle)
    unlet g:Ch_responseHandle
  endif
  call assert_equal('got it', g:Ch_responseMsg)

  " Collect garbage, tests that our handle isn't collected.
  call test_garbagecollect_now()

  " check setting options (without testing the effect)
  call ch_setoptions(handle, {'callback': 's:NotUsed'})
  call ch_setoptions(handle, {'timeout': 1111})
  call ch_setoptions(handle, {'mode': 'json'})
  call assert_fails("call ch_setoptions(handle, {'waittime': 111})", "E475")
  call ch_setoptions(handle, {'callback': ''})
  call ch_setoptions(handle, {'drop': 'never'})
  call ch_setoptions(handle, {'drop': 'auto'})
  call assert_fails("call ch_setoptions(handle, {'drop': 'bad'})", "E475")

  " Send an eval request that works.
  call assert_equal('ok', ch_evalexpr(handle, 'eval-works'))
  sleep 10m
  call assert_equal([-1, 'foo123'], ch_evalexpr(handle, 'eval-result'))

  " Send an eval request with special characters.
  call assert_equal('ok', ch_evalexpr(handle, 'eval-special'))
  sleep 10m
  call assert_equal([-2, "foo\x7f\x10\x01bar"], ch_evalexpr(handle, 'eval-result'))

  " Send an eval request to get a line with special characters.
  call setline(3, "a\nb\<CR>c\x01d\x7fe")
  call assert_equal('ok', ch_evalexpr(handle, 'eval-getline'))
  sleep 10m
  call assert_equal([-3, "a\nb\<CR>c\x01d\x7fe"], ch_evalexpr(handle, 'eval-result'))

  " Send an eval request that fails.
  call assert_equal('ok', ch_evalexpr(handle, 'eval-fails'))
  sleep 10m
  call assert_equal([-4, 'ERROR'], ch_evalexpr(handle, 'eval-result'))

  " Send an eval request that works but can't be encoded.
  call assert_equal('ok', ch_evalexpr(handle, 'eval-error'))
  sleep 10m
  call assert_equal([-5, 'ERROR'], ch_evalexpr(handle, 'eval-result'))

  " Send a bad eval request. There will be no response.
  call assert_equal('ok', ch_evalexpr(handle, 'eval-bad'))
  sleep 10m
  call assert_equal([-5, 'ERROR'], ch_evalexpr(handle, 'eval-result'))

  " Send an expr request
  call assert_equal('ok', ch_evalexpr(handle, 'an expr'))
  call WaitFor('"three" == getline("$")')
  call assert_equal('one', getline(line('$') - 2))
  call assert_equal('two', getline(line('$') - 1))
  call assert_equal('three', getline('$'))

  " Request a redraw, we don't check for the effect.
  call assert_equal('ok', ch_evalexpr(handle, 'redraw'))
  call assert_equal('ok', ch_evalexpr(handle, 'redraw!'))

  call assert_equal('ok', ch_evalexpr(handle, 'empty-request'))

  " Reading while there is nothing available.
  call assert_equal(v:none, ch_read(handle, {'timeout': 0}))
  let start = reltime()
  call assert_equal(v:none, ch_read(handle, {'timeout': 333}))
  let elapsed = reltime(start)
  call assert_true(reltimefloat(elapsed) > 0.3)
  call assert_true(reltimefloat(elapsed) < 0.6)

  " Send without waiting for a response, then wait for a response.
  call ch_sendexpr(handle, 'wait a bit')
  let resp = ch_read(handle)
  call assert_equal(type([]), type(resp))
  call assert_equal(type(11), type(resp[0]))
  call assert_equal('waited', resp[1])

  " make the server quit, can't check if this works, should not hang.
  call ch_sendexpr(handle, '!quit!')
endfunc

func Test_communicate()
  call ch_log('Test_communicate()')
  call s:run_server('Ch_communicate')
endfunc

" Test that we can open two channels.
func Ch_two_channels(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  call assert_equal(v:t_channel, type(handle))
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif

  call assert_equal('got it', ch_evalexpr(handle, 'hello!'))

  let newhandle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(newhandle) == "fail"
    call assert_report("Can't open second channel")
    return
  endif
  call assert_equal('got it', ch_evalexpr(newhandle, 'hello!'))
  call assert_equal('got it', ch_evalexpr(handle, 'hello!'))

  call ch_close(handle)
  call assert_equal('got it', ch_evalexpr(newhandle, 'hello!'))

  call ch_close(newhandle)
endfunc

func Test_two_channels()
  call ch_log('Test_two_channels()')
  call s:run_server('Ch_two_channels')
endfunc

" Test that a server crash is handled gracefully.
func Ch_server_crash(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif

  call ch_evalexpr(handle, '!crash!')

  sleep 10m
endfunc

func Test_server_crash()
  call ch_log('Test_server_crash()')
  call s:run_server('Ch_server_crash')
endfunc

"""""""""

func Ch_handler(chan, msg)
  call ch_log('Ch_handler()')
  unlet g:Ch_reply
  let g:Ch_reply = a:msg
endfunc

func Ch_channel_handler(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif

  " Test that it works while waiting on a numbered message.
  call assert_equal('ok', ch_evalexpr(handle, 'call me'))
  call WaitFor('"we called you" == g:Ch_reply')
  call assert_equal('we called you', g:Ch_reply)

  " Test that it works while not waiting on a numbered message.
  call ch_sendexpr(handle, 'call me again')
  call WaitFor('"we did call you" == g:Ch_reply')
  call assert_equal('we did call you', g:Ch_reply)
endfunc

func Test_channel_handler()
  call ch_log('Test_channel_handler()')
  let g:Ch_reply = ""
  let s:chopt.callback = 'Ch_handler'
  call s:run_server('Ch_channel_handler')
  let g:Ch_reply = ""
  let s:chopt.callback = function('Ch_handler')
  call s:run_server('Ch_channel_handler')
  unlet s:chopt.callback
endfunc

"""""""""

let g:Ch_reply = ''
func Ch_zeroHandler(chan, msg)
  unlet g:Ch_reply
  let g:Ch_reply = a:msg
endfunc

let g:Ch_zero_reply = ''
func Ch_oneHandler(chan, msg)
  unlet g:Ch_zero_reply
  let g:Ch_zero_reply = a:msg
endfunc

func Ch_channel_zero(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif

  " Check that eval works.
  call assert_equal('got it', ch_evalexpr(handle, 'hello!'))

  " Check that eval works if a zero id message is sent back.
  let g:Ch_reply = ''
  call assert_equal('sent zero', ch_evalexpr(handle, 'send zero'))
  if s:has_handler
    call WaitFor('"zero index" == g:Ch_reply')
    call assert_equal('zero index', g:Ch_reply)
  else
    sleep 20m
    call assert_equal('', g:Ch_reply)
  endif

  " Check that handler works if a zero id message is sent back.
  let g:Ch_reply = ''
  let g:Ch_zero_reply = ''
  call ch_sendexpr(handle, 'send zero', {'callback': 'Ch_oneHandler'})
  call WaitFor('"sent zero" == g:Ch_zero_reply')
  if s:has_handler
    call assert_equal('zero index', g:Ch_reply)
  else
    call assert_equal('', g:Ch_reply)
  endif
  call assert_equal('sent zero', g:Ch_zero_reply)
endfunc

func Test_zero_reply()
  call ch_log('Test_zero_reply()')
  " Run with channel handler
  let s:has_handler = 1
  let s:chopt.callback = 'Ch_zeroHandler'
  call s:run_server('Ch_channel_zero')
  unlet s:chopt.callback

  " Run without channel handler
  let s:has_handler = 0
  call s:run_server('Ch_channel_zero')
endfunc

"""""""""

let g:Ch_reply1 = ""
func Ch_handleRaw1(chan, msg)
  unlet g:Ch_reply1
  let g:Ch_reply1 = a:msg
endfunc

let g:Ch_reply2 = ""
func Ch_handleRaw2(chan, msg)
  unlet g:Ch_reply2
  let g:Ch_reply2 = a:msg
endfunc

let g:Ch_reply3 = ""
func Ch_handleRaw3(chan, msg)
  unlet g:Ch_reply3
  let g:Ch_reply3 = a:msg
endfunc

func Ch_raw_one_time_callback(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  call ch_setoptions(handle, {'mode': 'raw'})

  " The messages are sent raw, we do our own JSON strings here.
  call ch_sendraw(handle, "[1, \"hello!\"]\n", {'callback': 'Ch_handleRaw1'})
  call WaitFor('g:Ch_reply1 != ""')
  call assert_equal("[1, \"got it\"]", g:Ch_reply1)
  call ch_sendraw(handle, "[2, \"echo something\"]\n", {'callback': 'Ch_handleRaw2'})
  call ch_sendraw(handle, "[3, \"wait a bit\"]\n", {'callback': 'Ch_handleRaw3'})
  call WaitFor('g:Ch_reply2 != ""')
  call assert_equal("[2, \"something\"]", g:Ch_reply2)
  " wait for the 200 msec delayed reply
  call WaitFor('g:Ch_reply3 != ""')
  call assert_equal("[3, \"waited\"]", g:Ch_reply3)
endfunc

func Test_raw_one_time_callback()
  call ch_log('Test_raw_one_time_callback()')
  call s:run_server('Ch_raw_one_time_callback')
endfunc

"""""""""

" Test that trying to connect to a non-existing port fails quickly.
func Test_connect_waittime()
  call ch_log('Test_connect_waittime()')
  let start = reltime()
  let handle = ch_open('localhost:9876', s:chopt)
  if ch_status(handle) != "fail"
    " Oops, port does exists.
    call ch_close(handle)
  else
    let elapsed = reltime(start)
    call assert_true(reltimefloat(elapsed) < 1.0)
  endif

  " We intend to use a socket that doesn't exist and wait for half a second
  " before giving up.  If the socket does exist it can fail in various ways.
  " Check for "Connection reset by peer" to avoid flakyness.
  let start = reltime()
  try
    let handle = ch_open('localhost:9867', {'waittime': 500})
    if ch_status(handle) != "fail"
      " Oops, port does exists.
      call ch_close(handle)
    else
      " Failed connection should wait about 500 msec.  Can be longer if the
      " computer is busy with other things.
      let elapsed = reltime(start)
      call assert_true(reltimefloat(elapsed) > 0.3)
      call assert_true(reltimefloat(elapsed) < 1.5)
    endif
  catch
    if v:exception !~ 'Connection reset by peer'
      call assert_report("Caught exception: " . v:exception)
    endif
  endtry
endfunc

"""""""""

func Test_raw_pipe()
  if !has('job')
    return
  endif
  call ch_log('Test_raw_pipe()')
  " Add a dummy close callback to avoid that messages are dropped when calling
  " ch_canread().
  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'mode': 'raw', 'drop': 'never'})
  call assert_equal(v:t_job, type(job))
  call assert_equal("run", job_status(job))

  call assert_equal("open", ch_status(job))
  call assert_equal("open", ch_status(job), {"part": "out"})
  call assert_equal("open", ch_status(job), {"part": "err"})
  call assert_fails('call ch_status(job, {"in_mode": "raw"})', 'E475:')
  call assert_fails('call ch_status(job, {"part": "in"})', 'E475:')

  let dict = ch_info(job)
  call assert_true(dict.id != 0)
  call assert_equal('open', dict.status)
  call assert_equal('open', dict.out_status)
  call assert_equal('RAW', dict.out_mode)
  call assert_equal('pipe', dict.out_io)
  call assert_equal('open', dict.err_status)
  call assert_equal('RAW', dict.err_mode)
  call assert_equal('pipe', dict.err_io)

  try
    " For a change use the job where a channel is expected.
    call ch_sendraw(job, "echo something\n")
    let msg = ch_readraw(job)
    call assert_equal("something\n", substitute(msg, "\r", "", 'g'))

    call ch_sendraw(job, "double this\n")
    let g:handle = job_getchannel(job)
    call WaitFor('ch_canread(g:handle)')
    unlet g:handle
    let msg = ch_readraw(job)
    call assert_equal("this\nAND this\n", substitute(msg, "\r", "", 'g'))

    let g:Ch_reply = ""
    call ch_sendraw(job, "double this\n", {'callback': 'Ch_handler'})
    call WaitFor('"" != g:Ch_reply')
    call assert_equal("this\nAND this\n", substitute(g:Ch_reply, "\r", "", 'g'))

    let reply = ch_evalraw(job, "quit\n", {'timeout': 100})
    call assert_equal("Goodbye!\n", substitute(reply, "\r", "", 'g'))
  finally
    call job_stop(job)
  endtry

  let g:Ch_job = job
  call WaitFor('"dead" == job_status(g:Ch_job)')
  let info = job_info(job)
  call assert_equal("dead", info.status)
  call assert_equal("term", info.stoponexit)
endfunc

func Test_nl_pipe()
  if !has('job')
    return
  endif
  call ch_log('Test_nl_pipe()')
  let job = job_start([s:python, "test_channel_pipe.py"])
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo something\n")
    call assert_equal("something", ch_readraw(handle))

    call ch_sendraw(handle, "echoerr wrong\n")
    call assert_equal("wrong", ch_readraw(handle, {'part': 'err'}))

    call ch_sendraw(handle, "double this\n")
    call assert_equal("this", ch_readraw(handle))
    call assert_equal("AND this", ch_readraw(handle))

    call ch_sendraw(handle, "split this line\n")
    call assert_equal("this linethis linethis line", ch_readraw(handle))

    let reply = ch_evalraw(handle, "quit\n")
    call assert_equal("Goodbye!", reply)
  finally
    call job_stop(job)
  endtry
endfunc

func Test_nl_err_to_out_pipe()
  if !has('job')
    return
  endif
  call ch_logfile('Xlog')
  call ch_log('Test_nl_err_to_out_pipe()')
  let job = job_start(s:python . " test_channel_pipe.py", {'err_io': 'out'})
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo something\n")
    call assert_equal("something", ch_readraw(handle))

    call ch_sendraw(handle, "echoerr wrong\n")
    call assert_equal("wrong", ch_readraw(handle))
  finally
    call job_stop(job)
    call ch_logfile('')
    let loglines = readfile('Xlog')
    call assert_true(len(loglines) > 10)
    let found_test = 0
    let found_send = 0
    let found_recv = 0
    let found_stop = 0
    for l in loglines
      if l =~ 'Test_nl_err_to_out_pipe'
	let found_test = 1
      endif
      if l =~ 'SEND on.*echo something'
	let found_send = 1
      endif
      if l =~ 'RECV on.*something'
	let found_recv = 1
      endif
      if l =~ 'Stopping job with'
	let found_stop = 1
      endif
    endfor
    call assert_equal(1, found_test)
    call assert_equal(1, found_send)
    call assert_equal(1, found_recv)
    call assert_equal(1, found_stop)
    " On MS-Windows need to sleep for a moment to be able to delete the file.
    sleep 10m
    call delete('Xlog')
  endtry
endfunc

func Stop_g_job()
  call job_stop(g:job)
  if has('win32')
    " On MS-Windows the server must close the file handle before we are able
    " to delete the file.
    call WaitFor('job_status(g:job) == "dead"')
    sleep 10m
  endif
endfunc

func Test_nl_read_file()
  if !has('job')
    return
  endif
  call ch_log('Test_nl_read_file()')
  call writefile(['echo something', 'echoerr wrong', 'double this'], 'Xinput')
  let g:job = job_start(s:python . " test_channel_pipe.py",
	\ {'in_io': 'file', 'in_name': 'Xinput'})
  call assert_equal("run", job_status(g:job))
  try
    let handle = job_getchannel(g:job)
    call assert_equal("something", ch_readraw(handle))
    call assert_equal("wrong", ch_readraw(handle, {'part': 'err'}))
    call assert_equal("this", ch_readraw(handle))
    call assert_equal("AND this", ch_readraw(handle))
  finally
    call Stop_g_job()
    call delete('Xinput')
  endtry
endfunc

func Test_nl_write_out_file()
  if !has('job')
    return
  endif
  call ch_log('Test_nl_write_out_file()')
  let g:job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_io': 'file', 'out_name': 'Xoutput'})
  call assert_equal("run", job_status(g:job))
  try
    let handle = job_getchannel(g:job)
    call ch_sendraw(handle, "echo line one\n")
    call ch_sendraw(handle, "echo line two\n")
    call ch_sendraw(handle, "double this\n")
    call WaitFor('len(readfile("Xoutput")) > 2')
    call assert_equal(['line one', 'line two', 'this', 'AND this'], readfile('Xoutput'))
  finally
    call Stop_g_job()
    call delete('Xoutput')
  endtry
endfunc

func Test_nl_write_err_file()
  if !has('job')
    return
  endif
  call ch_log('Test_nl_write_err_file()')
  let g:job = job_start(s:python . " test_channel_pipe.py",
	\ {'err_io': 'file', 'err_name': 'Xoutput'})
  call assert_equal("run", job_status(g:job))
  try
    let handle = job_getchannel(g:job)
    call ch_sendraw(handle, "echoerr line one\n")
    call ch_sendraw(handle, "echoerr line two\n")
    call ch_sendraw(handle, "doubleerr this\n")
    call WaitFor('len(readfile("Xoutput")) > 2')
    call assert_equal(['line one', 'line two', 'this', 'AND this'], readfile('Xoutput'))
  finally
    call Stop_g_job()
    call delete('Xoutput')
  endtry
endfunc

func Test_nl_write_both_file()
  if !has('job')
    return
  endif
  call ch_log('Test_nl_write_both_file()')
  let g:job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_io': 'file', 'out_name': 'Xoutput', 'err_io': 'out'})
  call assert_equal("run", job_status(g:job))
  try
    let handle = job_getchannel(g:job)
    call ch_sendraw(handle, "echoerr line one\n")
    call ch_sendraw(handle, "echo line two\n")
    call ch_sendraw(handle, "double this\n")
    call ch_sendraw(handle, "doubleerr that\n")
    call WaitFor('len(readfile("Xoutput")) > 5')
    call assert_equal(['line one', 'line two', 'this', 'AND this', 'that', 'AND that'], readfile('Xoutput'))
  finally
    call Stop_g_job()
    call delete('Xoutput')
  endtry
endfunc

func BufCloseCb(ch)
  let g:Ch_bufClosed = 'yes'
endfunc

func Run_test_pipe_to_buffer(use_name, nomod, do_msg)
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_to_buffer()')
  let g:Ch_bufClosed = 'no'
  let options = {'out_io': 'buffer', 'close_cb': 'BufCloseCb'}
  let expected = ['', 'line one', 'line two', 'this', 'AND this', 'Goodbye!']
  if a:use_name
    let options['out_name'] = 'pipe-output'
    if a:do_msg
      let expected[0] = 'Reading from channel output...'
    else
      let options['out_msg'] = 0
      call remove(expected, 0)
    endif
  else
    sp pipe-output
    let options['out_buf'] = bufnr('%')
    quit
    call remove(expected, 0)
  endif
  if a:nomod
    let options['out_modifiable'] = 0
  endif
  let job = job_start(s:python . " test_channel_pipe.py", options)
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo line one\n")
    call ch_sendraw(handle, "echo line two\n")
    call ch_sendraw(handle, "double this\n")
    call ch_sendraw(handle, "quit\n")
    sp pipe-output
    call WaitFor('line("$") >= 6 && g:Ch_bufClosed == "yes"')
    call assert_equal(expected, getline(1, '$'))
    if a:nomod
      call assert_equal(0, &modifiable)
    else
      call assert_equal(1, &modifiable)
    endif
    call assert_equal('yes', g:Ch_bufClosed)
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_to_buffer_name()
  call Run_test_pipe_to_buffer(1, 0, 1)
endfunc

func Test_pipe_to_buffer_nr()
  call Run_test_pipe_to_buffer(0, 0, 1)
endfunc

func Test_pipe_to_buffer_name_nomod()
  call Run_test_pipe_to_buffer(1, 1, 1)
endfunc

func Test_pipe_to_buffer_name_nomsg()
  call Run_test_pipe_to_buffer(1, 0, 1)
endfunc

func Run_test_pipe_err_to_buffer(use_name, nomod, do_msg)
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_err_to_buffer()')
  let options = {'err_io': 'buffer'}
  let expected = ['', 'line one', 'line two', 'this', 'AND this']
  if a:use_name
    let options['err_name'] = 'pipe-err'
    if a:do_msg
      let expected[0] = 'Reading from channel error...'
    else
      let options['err_msg'] = 0
      call remove(expected, 0)
    endif
  else
    sp pipe-err
    let options['err_buf'] = bufnr('%')
    quit
    call remove(expected, 0)
  endif
  if a:nomod
    let options['err_modifiable'] = 0
  endif
  let job = job_start(s:python . " test_channel_pipe.py", options)
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echoerr line one\n")
    call ch_sendraw(handle, "echoerr line two\n")
    call ch_sendraw(handle, "doubleerr this\n")
    call ch_sendraw(handle, "quit\n")
    sp pipe-err
    call WaitFor('line("$") >= 5')
    call assert_equal(expected, getline(1, '$'))
    if a:nomod
      call assert_equal(0, &modifiable)
    else
      call assert_equal(1, &modifiable)
    endif
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_err_to_buffer_name()
  call Run_test_pipe_err_to_buffer(1, 0, 1)
endfunc
  
func Test_pipe_err_to_buffer_nr()
  call Run_test_pipe_err_to_buffer(0, 0, 1)
endfunc
  
func Test_pipe_err_to_buffer_name_nomod()
  call Run_test_pipe_err_to_buffer(1, 1, 1)
endfunc
  
func Test_pipe_err_to_buffer_name_nomsg()
  call Run_test_pipe_err_to_buffer(1, 0, 0)
endfunc
  
func Test_pipe_both_to_buffer()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_both_to_buffer()')
  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_io': 'buffer', 'out_name': 'pipe-err', 'err_io': 'out'})
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo line one\n")
    call ch_sendraw(handle, "echoerr line two\n")
    call ch_sendraw(handle, "double this\n")
    call ch_sendraw(handle, "doubleerr that\n")
    call ch_sendraw(handle, "quit\n")
    sp pipe-err
    call WaitFor('line("$") >= 7')
    call assert_equal(['Reading from channel output...', 'line one', 'line two', 'this', 'AND this', 'that', 'AND that', 'Goodbye!'], getline(1, '$'))
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Run_test_pipe_from_buffer(use_name)
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_from_buffer()')

  sp pipe-input
  call setline(1, ['echo one', 'echo two', 'echo three'])
  let options = {'in_io': 'buffer', 'block_write': 1}
  if a:use_name
    let options['in_name'] = 'pipe-input'
  else
    let options['in_buf'] = bufnr('%')
  endif

  let job = job_start(s:python . " test_channel_pipe.py", options)
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call assert_equal('one', ch_read(handle))
    call assert_equal('two', ch_read(handle))
    call assert_equal('three', ch_read(handle))
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_from_buffer_name()
  call Run_test_pipe_from_buffer(1)
endfunc

func Test_pipe_from_buffer_nr()
  call Run_test_pipe_from_buffer(0)
endfunc

func Run_pipe_through_sort(all, use_buffer)
  if !executable('sort') || !has('job')
    return
  endif
  let options = {'out_io': 'buffer', 'out_name': 'sortout'}
  if a:use_buffer
    split sortin
    call setline(1, ['ccc', 'aaa', 'ddd', 'bbb', 'eee'])
    let options.in_io = 'buffer'
    let options.in_name = 'sortin'
  endif
  if !a:all
    let options.in_top = 2
    let options.in_bot = 4
  endif
  let g:job = job_start('sort', options)
  call assert_equal("run", job_status(g:job))

  if !a:use_buffer
    call ch_sendraw(g:job, "ccc\naaa\nddd\nbbb\neee\n")
    call ch_close_in(g:job)
  endif

  call WaitFor('job_status(g:job) == "dead"')
  call assert_equal("dead", job_status(g:job))

  sp sortout
  call WaitFor('line("$") > 3')
  call assert_equal('Reading from channel output...', getline(1))
  if a:all
    call assert_equal(['aaa', 'bbb', 'ccc', 'ddd', 'eee'], getline(2, 6))
  else
    call assert_equal(['aaa', 'bbb', 'ddd'], getline(2, 4))
  endif

  call job_stop(g:job)
  unlet g:job
  if a:use_buffer
    bwipe! sortin
  endif
  bwipe! sortout
endfunc

func Test_pipe_through_sort_all()
  call ch_log('Test_pipe_through_sort_all()')
  call Run_pipe_through_sort(1, 1)
endfunc

func Test_pipe_through_sort_some()
  call ch_log('Test_pipe_through_sort_some()')
  call Run_pipe_through_sort(0, 1)
endfunc

func Test_pipe_through_sort_feed()
  call ch_log('Test_pipe_through_sort_feed()')
  call Run_pipe_through_sort(1, 0)
endfunc

func Test_pipe_to_nameless_buffer()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_to_nameless_buffer()')
  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_io': 'buffer'})
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo line one\n")
    call ch_sendraw(handle, "echo line two\n")
    exe ch_getbufnr(handle, "out") . 'sbuf'
    call WaitFor('line("$") >= 3')
    call assert_equal(['Reading from channel output...', 'line one', 'line two'], getline(1, '$'))
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_to_buffer_json()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_to_buffer_json()')
  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_io': 'buffer', 'out_mode': 'json'})
  call assert_equal("run", job_status(job))
  try
    let handle = job_getchannel(job)
    call ch_sendraw(handle, "echo [0, \"hello\"]\n")
    call ch_sendraw(handle, "echo [-2, 12.34]\n")
    exe ch_getbufnr(handle, "out") . 'sbuf'
    call WaitFor('line("$") >= 3')
    call assert_equal(['Reading from channel output...', '[0,"hello"]', '[-2,12.34]'], getline(1, '$'))
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

" Wait a little while for the last line, minus "offset", to equal "line".
func s:wait_for_last_line(line, offset)
  for i in range(100)
    if getline(line('$') - a:offset) == a:line
      break
    endif
    sleep 10m
  endfor
endfunc

func Test_pipe_io_two_buffers()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_io_two_buffers()')

  " Create two buffers, one to read from and one to write to.
  split pipe-output
  set buftype=nofile
  split pipe-input
  set buftype=nofile

  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'in_io': 'buffer', 'in_name': 'pipe-input', 'in_top': 0,
	\  'out_io': 'buffer', 'out_name': 'pipe-output',
	\  'block_write': 1})
  call assert_equal("run", job_status(job))
  try
    exe "normal Gaecho hello\<CR>"
    exe bufwinnr('pipe-output') . "wincmd w"
    call s:wait_for_last_line('hello', 0)
    call assert_equal('hello', getline('$'))

    exe bufwinnr('pipe-input') . "wincmd w"
    exe "normal Gadouble this\<CR>"
    exe bufwinnr('pipe-output') . "wincmd w"
    call s:wait_for_last_line('AND this', 0)
    call assert_equal('this', getline(line('$') - 1))
    call assert_equal('AND this', getline('$'))

    bwipe!
    exe bufwinnr('pipe-input') . "wincmd w"
    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_io_one_buffer()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_io_one_buffer()')

  " Create one buffer to read from and to write to.
  split pipe-io
  set buftype=nofile

  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'in_io': 'buffer', 'in_name': 'pipe-io', 'in_top': 0,
	\  'out_io': 'buffer', 'out_name': 'pipe-io',
	\  'block_write': 1})
  call assert_equal("run", job_status(job))
  try
    exe "normal Goecho hello\<CR>"
    call s:wait_for_last_line('hello', 1)
    call assert_equal('hello', getline(line('$') - 1))

    exe "normal Gadouble this\<CR>"
    call s:wait_for_last_line('AND this', 1)
    call assert_equal('this', getline(line('$') - 2))
    call assert_equal('AND this', getline(line('$') - 1))

    bwipe!
  finally
    call job_stop(job)
  endtry
endfunc

func Test_pipe_null()
  if !has('job')
    return
  endif
  call ch_log('Test_pipe_null()')

  " We cannot check that no I/O works, we only check that the job starts
  " properly.
  let job = job_start(s:python . " test_channel_pipe.py something",
	\ {'in_io': 'null'})
  call assert_equal("run", job_status(job))
  try
    call assert_equal('something', ch_read(job))
  finally
    call job_stop(job)
  endtry

  let job = job_start(s:python . " test_channel_pipe.py err-out",
	\ {'out_io': 'null'})
  call assert_equal("run", job_status(job))
  try
    call assert_equal('err-out', ch_read(job, {"part": "err"}))
  finally
    call job_stop(job)
  endtry

  let job = job_start(s:python . " test_channel_pipe.py something",
	\ {'err_io': 'null'})
  call assert_equal("run", job_status(job))
  try
    call assert_equal('something', ch_read(job))
  finally
    call job_stop(job)
  endtry

  let job = job_start(s:python . " test_channel_pipe.py something",
	\ {'out_io': 'null', 'err_io': 'out'})
  call assert_equal("run", job_status(job))
  call job_stop(job)

  let job = job_start(s:python . " test_channel_pipe.py something",
	\ {'in_io': 'null', 'out_io': 'null', 'err_io': 'null'})
  call assert_equal("run", job_status(job))
  call assert_equal('channel fail', string(job_getchannel(job)))
  call assert_equal('fail', ch_status(job))
  call job_stop(job)
endfunc

func Test_pipe_to_buffer_raw()
  if !has('job')
    return
  endif
  call ch_log('Test_raw_pipe_to_buffer()')
  let options = {'out_mode': 'raw', 'out_io': 'buffer', 'out_name': 'testout'}
  split testout
  let job = job_start([s:python, '-c', 
        \ 'import sys; [sys.stdout.write(".") and sys.stdout.flush() for _ in range(10000)]'], options)
  call assert_equal("run", job_status(job))
  call WaitFor('len(join(getline(2,line("$")),"") >= 10000')
  try
    for line in getline(2, '$')
      let line = substitute(line, '^\.*', '', '')
      call assert_equal('', line)
    endfor
  finally
    call job_stop(job)
    bwipe!
  endtry
endfunc

func Test_reuse_channel()
  if !has('job')
    return
  endif
  call ch_log('Test_reuse_channel()')

  let job = job_start(s:python . " test_channel_pipe.py")
  call assert_equal("run", job_status(job))
  let handle = job_getchannel(job)
  try
    call ch_sendraw(handle, "echo something\n")
    call assert_equal("something", ch_readraw(handle))
  finally
    call job_stop(job)
  endtry

  let job = job_start(s:python . " test_channel_pipe.py", {'channel': handle})
  call assert_equal("run", job_status(job))
  let handle = job_getchannel(job)
  try
    call ch_sendraw(handle, "echo again\n")
    call assert_equal("again", ch_readraw(handle))
  finally
    call job_stop(job)
  endtry
endfunc

func Test_out_cb()
  if !has('job')
    return
  endif
  call ch_log('Test_out_cb()')

  let dict = {'thisis': 'dict: '}
  func dict.outHandler(chan, msg) dict
    if type(a:msg) == v:t_string
      let g:Ch_outmsg = self.thisis . a:msg
    else
      let g:Ch_outobj = a:msg
    endif
  endfunc
  func dict.errHandler(chan, msg) dict
    let g:Ch_errmsg = self.thisis . a:msg
  endfunc
  let job = job_start(s:python . " test_channel_pipe.py",
	\ {'out_cb': dict.outHandler,
	\ 'out_mode': 'json',
	\ 'err_cb': dict.errHandler,
	\ 'err_mode': 'json'})
  call assert_equal("run", job_status(job))
  try
    let g:Ch_outmsg = ''
    let g:Ch_errmsg = ''
    call ch_sendraw(job, "echo [0, \"hello\"]\n")
    call ch_sendraw(job, "echoerr [0, \"there\"]\n")
    call WaitFor('g:Ch_outmsg != ""')
    call assert_equal("dict: hello", g:Ch_outmsg)
    call WaitFor('g:Ch_errmsg != ""')
    call assert_equal("dict: there", g:Ch_errmsg)

    " Receive a json object split in pieces
    unlet! g:Ch_outobj
    call ch_sendraw(job, "echosplit [0, {\"one\": 1,| \"tw|o\": 2, \"three\": 3|}]\n")
    call WaitFor('exists("g:Ch_outobj")')
    call assert_equal({'one': 1, 'two': 2, 'three': 3}, g:Ch_outobj)
  finally
    call job_stop(job)
  endtry
endfunc

func Test_out_close_cb()
  if !has('job')
    return
  endif
  call ch_log('Test_out_close_cb()')

  let s:counter = 1
  let g:Ch_msg1 = ''
  let g:Ch_closemsg = 0
  func! OutHandler(chan, msg)
    if s:counter == 1
      let g:Ch_msg1 = a:msg
    endif
    let s:counter += 1
  endfunc
  func! CloseHandler(chan)
    let g:Ch_closemsg = s:counter
    let s:counter += 1
  endfunc
  let job = job_start(s:python . " test_channel_pipe.py quit now",
	\ {'out_cb': 'OutHandler',
	\ 'close_cb': 'CloseHandler'})
  call assert_equal("run", job_status(job))
  try
    call WaitFor('g:Ch_closemsg != 0 && g:Ch_msg1 != ""')
    call assert_equal('quit', g:Ch_msg1)
    call assert_equal(2, g:Ch_closemsg)
  finally
    call job_stop(job)
    delfunc OutHandler
    delfunc CloseHandler
  endtry
endfunc

func Test_read_in_close_cb()
  if !has('job')
    return
  endif
  call ch_log('Test_read_in_close_cb()')

  let g:Ch_received = ''
  func! CloseHandler(chan)
    let g:Ch_received = ch_read(a:chan)
  endfunc
  let job = job_start(s:python . " test_channel_pipe.py quit now",
	\ {'close_cb': 'CloseHandler'})
  call assert_equal("run", job_status(job))
  try
    call WaitFor('g:Ch_received != ""')
    call assert_equal('quit', g:Ch_received)
  finally
    call job_stop(job)
    delfunc CloseHandler
  endtry
endfunc

func Test_out_cb_lambda()
  if !has('job')
    return
  endif
  call ch_log('Test_out_cb_lambda()')

  let job = job_start(s:python . " test_channel_pipe.py",
  \ {'out_cb': {ch, msg -> execute("let g:Ch_outmsg = 'lambda: ' . msg")},
  \ 'out_mode': 'json',
  \ 'err_cb': {ch, msg -> execute(":let g:Ch_errmsg = 'lambda: ' . msg")},
  \ 'err_mode': 'json'})
  call assert_equal("run", job_status(job))
  try
    let g:Ch_outmsg = ''
    let g:Ch_errmsg = ''
    call ch_sendraw(job, "echo [0, \"hello\"]\n")
    call ch_sendraw(job, "echoerr [0, \"there\"]\n")
    call WaitFor('g:Ch_outmsg != ""')
    call assert_equal("lambda: hello", g:Ch_outmsg)
    call WaitFor('g:Ch_errmsg != ""')
    call assert_equal("lambda: there", g:Ch_errmsg)
  finally
    call job_stop(job)
  endtry
endfunc

func Test_close_and_exit_cb()
  if !has('job')
    return
  endif
  call ch_log('Test_close_and_exit_cb')

  let dict = {'ret': {}}
  func dict.close_cb(ch) dict
    let self.ret['close_cb'] = job_status(ch_getjob(a:ch))
  endfunc
  func dict.exit_cb(job, status) dict
    let self.ret['exit_cb'] = job_status(a:job)
  endfunc

  let g:job = job_start('echo', {
        \ 'close_cb': dict.close_cb,
        \ 'exit_cb': dict.exit_cb,
        \ })
  call assert_equal('run', job_status(g:job))
  unlet g:job
  call WaitFor('len(dict.ret) >= 2')
  call assert_equal(2, len(dict.ret))
  call assert_match('^\%(dead\|run\)', dict.ret['close_cb'])
  call assert_equal('dead', dict.ret['exit_cb'])
endfunc

""""""""""

let g:Ch_unletResponse = ''
func s:UnletHandler(handle, msg)
  let g:Ch_unletResponse = a:msg
  unlet s:channelfd
endfunc

" Test that "unlet handle" in a handler doesn't crash Vim.
func Ch_unlet_handle(port)
  let s:channelfd = ch_open('localhost:' . a:port, s:chopt)
  call ch_sendexpr(s:channelfd, "test", {'callback': function('s:UnletHandler')})
  call WaitFor('"what?" == g:Ch_unletResponse')
  call assert_equal('what?', g:Ch_unletResponse)
endfunc

func Test_unlet_handle()
  call ch_log('Test_unlet_handle()')
  call s:run_server('Ch_unlet_handle')
endfunc

""""""""""

let g:Ch_unletResponse = ''
func Ch_CloseHandler(handle, msg)
  let g:Ch_unletResponse = a:msg
  call ch_close(s:channelfd)
endfunc

" Test that "unlet handle" in a handler doesn't crash Vim.
func Ch_close_handle(port)
  let s:channelfd = ch_open('localhost:' . a:port, s:chopt)
  call ch_sendexpr(s:channelfd, "test", {'callback': function('Ch_CloseHandler')})
  call WaitFor('"what?" == g:Ch_unletResponse')
  call assert_equal('what?', g:Ch_unletResponse)
endfunc

func Test_close_handle()
  call ch_log('Test_close_handle()')
  call s:run_server('Ch_close_handle')
endfunc

""""""""""

func Test_open_fail()
  call ch_log('Test_open_fail()')
  silent! let ch = ch_open("noserver")
  echo ch
  let d = ch
endfunc

""""""""""

func Ch_open_delay(port)
  " Wait up to a second for the port to open.
  let s:chopt.waittime = 1000
  let channel = ch_open('localhost:' . a:port, s:chopt)
  unlet s:chopt.waittime
  if ch_status(channel) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  call assert_equal('got it', ch_evalexpr(channel, 'hello!'))
  call ch_close(channel)
endfunc

func Test_open_delay()
  call ch_log('Test_open_delay()')
  " The server will wait half a second before creating the port.
  call s:run_server('Ch_open_delay', 'delay')
endfunc

"""""""""

function MyFunction(a,b,c)
  let g:Ch_call_ret = [a:a, a:b, a:c]
endfunc

function Ch_test_call(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif

  let g:Ch_call_ret = []
  call assert_equal('ok', ch_evalexpr(handle, 'call-func'))
  call WaitFor('len(g:Ch_call_ret) > 0')
  call assert_equal([1, 2, 3], g:Ch_call_ret)
endfunc

func Test_call()
  call ch_log('Test_call()')
  call s:run_server('Ch_test_call')
endfunc

"""""""""

let g:Ch_job_exit_ret = 'not yet'
function MyExitCb(job, status)
  let g:Ch_job_exit_ret = 'done'
endfunc

function Ch_test_exit_callback(port)
  call job_setoptions(g:currentJob, {'exit_cb': 'MyExitCb'})
  let g:Ch_exit_job = g:currentJob
  call assert_equal('MyExitCb', job_info(g:currentJob)['exit_cb'])
endfunc

func Test_exit_callback()
  if has('job')
    call ch_log('Test_exit_callback()')
    call s:run_server('Ch_test_exit_callback')

    " wait up to a second for the job to exit
    for i in range(100)
      if g:Ch_job_exit_ret == 'done'
	break
      endif
      sleep 10m
      " calling job_status() triggers the callback
      call job_status(g:Ch_exit_job)
    endfor

    call assert_equal('done', g:Ch_job_exit_ret)
    call assert_equal('dead', job_info(g:Ch_exit_job).status)
    unlet g:Ch_exit_job
  endif
endfunc

function MyExitTimeCb(job, status)
  if job_info(a:job).process == g:exit_cb_val.process
    let g:exit_cb_val.end = reltime(g:exit_cb_val.start)
  endif
  call Resume()
endfunction

func Test_exit_callback_interval()
  if !has('job')
    return
  endif

  let g:exit_cb_val = {'start': reltime(), 'end': 0, 'process': 0}
  let job = job_start([s:python, '-c', 'import time;time.sleep(0.5)'], {'exit_cb': 'MyExitTimeCb'})
  let g:exit_cb_val.process = job_info(job).process
  call WaitFor('type(g:exit_cb_val.end) != v:t_number || g:exit_cb_val.end != 0')
  let elapsed = reltimefloat(g:exit_cb_val.end)
  call assert_true(elapsed > 0.5)
  call assert_true(elapsed < 1.0)

  " case: unreferenced job, using timer
  if !has('timers')
    return
  endif

  let g:exit_cb_val = {'start': reltime(), 'end': 0, 'process': 0}
  let g:job = job_start([s:python, '-c', 'import time;time.sleep(0.5)'], {'exit_cb': 'MyExitTimeCb'})
  let g:exit_cb_val.process = job_info(g:job).process
  unlet g:job
  call Standby(1000)
  if type(g:exit_cb_val.end) != v:t_number || g:exit_cb_val.end != 0
    let elapsed = reltimefloat(g:exit_cb_val.end)
  else
    let elapsed = 1.0
  endif
  call assert_true(elapsed > 0.5)
  call assert_true(elapsed < 1.0)
endfunc

"""""""""

let g:Ch_close_ret = 'alive'
function MyCloseCb(ch)
  let g:Ch_close_ret = 'closed'
endfunc

function Ch_test_close_callback(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  call ch_setoptions(handle, {'close_cb': 'MyCloseCb'})

  call assert_equal('', ch_evalexpr(handle, 'close me'))
  call WaitFor('"closed" == g:Ch_close_ret')
  call assert_equal('closed', g:Ch_close_ret)
endfunc

func Test_close_callback()
  call ch_log('Test_close_callback()')
  call s:run_server('Ch_test_close_callback')
endfunc

function Ch_test_close_partial(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  let g:Ch_d = {}
  func g:Ch_d.closeCb(ch) dict
    let self.close_ret = 'closed'
  endfunc
  call ch_setoptions(handle, {'close_cb': g:Ch_d.closeCb})

  call assert_equal('', ch_evalexpr(handle, 'close me'))
  call WaitFor('"closed" == g:Ch_d.close_ret')
  call assert_equal('closed', g:Ch_d.close_ret)
  unlet g:Ch_d
endfunc

func Test_close_partial()
  call ch_log('Test_close_partial()')
  call s:run_server('Ch_test_close_partial')
endfunc

func Test_job_start_invalid()
  call assert_fails('call job_start($x)', 'E474:')
  call assert_fails('call job_start("")', 'E474:')
endfunc

func Test_job_stop_immediately()
  if !has('job')
    return
  endif

  let job = job_start([s:python, '-c', 'import time;time.sleep(10)'])
  try
    call job_stop(job)
    call WaitFor('"dead" == job_status(job)')
    call assert_equal('dead', job_status(job))
  finally
    call job_stop(job, 'kill')
  endtry
endfunc

" This was leaking memory.
func Test_partial_in_channel_cycle()
  let d = {}
  let d.a = function('string', [d])
  try
    let d.b = ch_open('nowhere:123', {'close_cb': d.a})
  catch
    call assert_exception('E901:')
  endtry
  unlet d
endfunc

func Test_using_freed_memory()
  let g:a = job_start(['ls'])
  sleep 10m
  call test_garbagecollect_now()
endfunc

func Test_collapse_buffers()
  if !executable('cat') || !has('job')
    return
  endif
  sp test_channel.vim
  let g:linecount = line('$')
  close
  split testout
  1,$delete
  call job_start('cat test_channel.vim', {'out_io': 'buffer', 'out_name': 'testout'})
  call WaitFor('line("$") > g:linecount')
  call assert_inrange(g:linecount, g:linecount + 1, line('$'))
  bwipe!
endfunc

func Test_raw_passes_nul()
  if !executable('cat') || !has('job')
    return
  endif

  " Test lines from the job containing NUL are stored correctly in a buffer.
  new
  call setline(1, ["asdf\nasdf", "xxx\n", "\nyyy"])
  w! Xtestread
  bwipe!
  split testout
  1,$delete
  call job_start('cat Xtestread', {'out_io': 'buffer', 'out_name': 'testout'})
  call WaitFor('line("$") > 2')
  call assert_equal("asdf\nasdf", getline(1))
  call assert_equal("xxx\n", getline(2))
  call assert_equal("\nyyy", getline(3))

  call delete('Xtestread')
  bwipe!

  " Test lines from a buffer with NUL bytes are written correctly to the job.
  new mybuffer
  call setline(1, ["asdf\nasdf", "xxx\n", "\nyyy"])
  let g:Ch_job = job_start('cat', {'in_io': 'buffer', 'in_name': 'mybuffer', 'out_io': 'file', 'out_name': 'Xtestwrite'})
  call WaitFor('"dead" == job_status(g:Ch_job)')
  bwipe!
  split Xtestwrite
  call assert_equal("asdf\nasdf", getline(1))
  call assert_equal("xxx\n", getline(2))
  call assert_equal("\nyyy", getline(3))

  call delete('Xtestwrite')
  bwipe!
endfunc

func MyLineCountCb(ch, msg)
  let g:linecount += 1
endfunc

func Test_read_nonl_line()
  if !has('job')
    return
  endif

  let g:linecount = 0
  if has('win32')
    " workaround: 'shellescape' does improper escaping double quotes
    let arg = 'import sys;sys.stdout.write(\"1\n2\n3\")'
  else
    let arg = 'import sys;sys.stdout.write("1\n2\n3")'
  endif
  call job_start([s:python, '-c', arg], {'callback': 'MyLineCountCb'})
  call WaitFor('3 <= g:linecount')
  call assert_equal(3, g:linecount)
endfunc

func Test_read_from_terminated_job()
  if !has('job')
    return
  endif

  let g:linecount = 0
  if has('win32')
    " workaround: 'shellescape' does improper escaping double quotes 
    let arg = 'import os,sys;os.close(1);sys.stderr.write(\"test\n\")'
  else
    let arg = 'import os,sys;os.close(1);sys.stderr.write("test\n")'
  endif
  call job_start([s:python, '-c', arg], {'callback': 'MyLineCountCb'})
  call WaitFor('1 <= g:linecount')
  call assert_equal(1, g:linecount)
endfunc

function Ch_test_close_lambda(port)
  let handle = ch_open('localhost:' . a:port, s:chopt)
  if ch_status(handle) == "fail"
    call assert_report("Can't open channel")
    return
  endif
  let g:Ch_close_ret = ''
  call ch_setoptions(handle, {'close_cb': {ch -> execute("let g:Ch_close_ret = 'closed'")}})

  call assert_equal('', ch_evalexpr(handle, 'close me'))
  call WaitFor('"closed" == g:Ch_close_ret')
  call assert_equal('closed', g:Ch_close_ret)
endfunc

func Test_close_lambda()
  call ch_log('Test_close_lambda()')
  call s:run_server('Ch_test_close_lambda')
endfunc