" Test for timers

source check.vim
CheckFeature timers

source screendump.vim
source shared.vim
source term_util.vim

func SetUp()
  " The tests here use timers, thus are sensitive to timing.
  let g:test_is_flaky = 1
endfunc

func MyHandler(timer)
  let g:val += 1
endfunc

func MyHandlerWithLists(lists, timer)
  let x = string(a:lists)
endfunc

func Test_timer_oneshot()
  let g:val = 0
  let timer = timer_start(50, 'MyHandler')
  let slept = WaitFor('g:val == 1')
  call assert_equal(1, g:val)
  if has('mac')
    " Mac on Travis can be very slow.
    let limit = 180
  else
    let limit = 100
  endif
  if has('reltime')
    call assert_inrange(49, limit, slept)
  else
    call assert_inrange(20, limit, slept)
  endif
endfunc

func Test_timer_repeat_three()
  let g:val = 0
  let timer = timer_start(50, 'MyHandler', {'repeat': 3})
  let slept = WaitFor('g:val == 3')
  call assert_equal(3, g:val)
  if has('reltime')
    if has('mac')
      " Mac on Travis can be slow.
      call assert_inrange(149, 400, slept)
    else
      call assert_inrange(149, 250, slept)
    endif
  else
    call assert_inrange(80, 200, slept)
  endif
endfunc

func Test_timer_repeat_many()
  let g:val = 0
  let timer = timer_start(50, 'MyHandler', {'repeat': -1})
  sleep 200m
  call timer_stop(timer)
  " Mac on Travis can be slow.
  if has('mac')
    call assert_inrange(1, 5, g:val)
  else
    call assert_inrange(2, 5, g:val)
  endif
endfunc

func Test_timer_with_partial_callback()
  let g:val = 0
  let meow = {'one': 1}
  function meow.bite(...)
    let g:val += self.one
  endfunction

  call timer_start(50, meow.bite)
  let slept = WaitFor('g:val == 1')
  call assert_equal(1, g:val)
  if has('reltime')
    " Mac on Travis can be slow.
    if has('mac')
      call assert_inrange(49, 180, slept)
    else
      call assert_inrange(49, 130, slept)
    endif
  else
    call assert_inrange(20, 100, slept)
  endif
endfunc

func Test_timer_retain_partial()
  call timer_start(50, function('MyHandlerWithLists', [['a']]))
  call test_garbagecollect_now()
  sleep 100m
endfunc

func Test_timer_info()
  let id = timer_start(1000, 'MyHandler')
  let info = id->timer_info()
  call assert_equal(id, info[0]['id'])
  call assert_equal(1000, info[0]['time'])
  call assert_true(info[0]['remaining'] > 500)
  call assert_true(info[0]['remaining'] <= 1000)
  call assert_equal(1, info[0]['repeat'])
  call assert_equal("function('MyHandler')", string(info[0]['callback']))

  let found = 0
  for info in timer_info()
    if info['id'] == id
      let found += 1
    endif
  endfor
  call assert_equal(1, found)

  call timer_stop(id)
  call assert_equal([], timer_info(id))

  call assert_fails('call timer_info("abc")', 'E1210:')

  " check repeat count inside the callback
  let g:timer_repeat = []
  let tid = timer_start(10, {tid -> execute("call add(g:timer_repeat, timer_info(tid)[0].repeat)")}, #{repeat: 3})
  call WaitForAssert({-> assert_equal([2, 1, 0], g:timer_repeat)})
  unlet g:timer_repeat
endfunc

func Test_timer_stopall()
  let id1 = timer_start(1000, 'MyHandler')
  let id2 = timer_start(2000, 'MyHandler')
  let info = timer_info()
  " count one for the TestTimeout() timer
  call assert_equal(3, len(info))

  call timer_stopall()
  let info = timer_info()
  call assert_equal(0, len(info))
endfunc

def Test_timer_stopall_with_popup()
  # Create a popup that times out after ten seconds.
  # Another timer will fire in half a second and close it early after stopping
  # all timers.
  var pop = popup_create('Popup', {time: 10000})
  var tmr = timer_start(500, (_) => {
    timer_stopall()
    popup_clear()
  })
  sleep 1
  assert_equal([], timer_info(tmr))
  assert_equal([], popup_list())
enddef

func Test_timer_paused()
  let g:val = 0

  let id = timer_start(50, 'MyHandler')
  let info = timer_info(id)
  call assert_equal(0, info[0]['paused'])

  eval id->timer_pause(1)
  let info = timer_info(id)
  call assert_equal(1, info[0]['paused'])
  sleep 100m
  call assert_equal(0, g:val)

  call timer_pause(id, 0)
  let info = timer_info(id)
  call assert_equal(0, info[0]['paused'])

  let slept = WaitFor('g:val == 1')
  call assert_equal(1, g:val)
  if has('reltime')
    if has('mac')
      " The travis Mac machines appear to be very busy.
      call assert_inrange(0, 90, slept)
    else
      call assert_inrange(0, 30, slept)
    endif
  else
    call assert_inrange(0, 10, slept)
  endif

  call assert_fails('call timer_pause("abc", 1)', 'E39:')
endfunc

func StopMyself(timer)
  let g:called += 1
  if g:called == 2
    call timer_stop(a:timer)
  endif
endfunc

func Test_timer_delete_myself()
  let g:called = 0
  let t = timer_start(10, 'StopMyself', {'repeat': -1})
  call WaitForAssert({-> assert_equal(2, g:called)})
  call assert_equal(2, g:called)
  call assert_equal([], timer_info(t))
endfunc

func StopTimer1(timer)
  let g:timer2 = 10->timer_start('StopTimer2')
  " avoid maxfuncdepth error
  call timer_pause(g:timer1, 1)
  sleep 20m
endfunc

func StopTimer2(timer)
  call timer_stop(g:timer1)
endfunc

func Test_timer_stop_in_callback()
  call assert_equal(1, len(timer_info()))
  let g:timer1 = timer_start(10, 'StopTimer1')
  let slept = 0
  for i in range(10)
    if len(timer_info()) == 1
      break
    endif
    sleep 10m
    let slept += 10
  endfor
  if slept == 100
    call assert_equal(1, len(timer_info()))
  else
    " This should take only 30 msec, but on Mac it's often longer
    call assert_inrange(0, 50, slept)
  endif
endfunc

func StopTimerAll(timer)
  call timer_stopall()
endfunc

func Test_timer_stop_all_in_callback()
  " One timer is for TestTimeout()
  call assert_equal(1, len(timer_info()))
  call timer_start(10, 'StopTimerAll')
  call assert_equal(2, len(timer_info()))
  let slept = 0
  for i in range(10)
    if len(timer_info()) == 0
      break
    endif
    sleep 10m
    let slept += 10
  endfor
  if slept == 100
    call assert_equal(0, len(timer_info()))
  else
    call assert_inrange(0, 30, slept)
  endif
endfunc

func FeedkeysCb(timer)
  call feedkeys("hello\<CR>", 'nt')
endfunc

func InputCb(timer)
  call timer_start(10, 'FeedkeysCb')
  let g:val = input('?')
  call Resume()
endfunc

func Test_timer_input_in_timer()
  let g:val = ''
  call timer_start(10, 'InputCb')
  call Standby(1000)
  call assert_equal('hello', g:val)
endfunc

func FuncWithError(timer)
  let g:call_count += 1
  if g:call_count == 4
    return
  endif
  doesnotexist
endfunc

func Test_timer_errors()
  let g:call_count = 0
  let timer = timer_start(10, 'FuncWithError', {'repeat': -1})
  " Timer will be stopped after failing 3 out of 3 times.
  call WaitForAssert({-> assert_equal(3, g:call_count)})
  sleep 50m
  call assert_equal(3, g:call_count)

  call assert_fails('call timer_start(100, "MyHandler", "abc")', 'E1206:')
  call assert_fails('call timer_start(100, [])', 'E921:')
  call assert_fails('call timer_stop("abc")', 'E1210:')
endfunc

func FuncWithCaughtError(timer)
  let g:call_count += 1
  try
    doesnotexist
  catch
    " nop
  endtry
endfunc

func Test_timer_catch_error()
  let g:call_count = 0
  let timer = timer_start(10, 'FuncWithCaughtError', {'repeat': 4})
  " Timer will not be stopped.
  call WaitForAssert({-> assert_equal(4, g:call_count)})
  sleep 50m
  call assert_equal(4, g:call_count)
endfunc

func FeedAndPeek(timer)
  call test_feedinput('a')
  call getchar(1)
endfunc

func Interrupt(timer)
  eval "\<C-C>"->test_feedinput()
endfunc

func Test_timer_peek_and_get_char()
  if !has('unix') && !has('gui_running')
    throw 'Skipped: cannot feed low-level input'
  endif

  call timer_start(0, 'FeedAndPeek')
  let intr = timer_start(100, 'Interrupt')
  let c = getchar()
  call assert_equal(char2nr('a'), c)
  eval intr->timer_stop()
endfunc

func Test_timer_getchar_zero()
  if has('win32') && !has('gui_running')
    throw 'Skipped: cannot feed low-level input'
  endif
  CheckFunction reltimefloat

  " Measure the elapsed time to avoid a hang when it fails.
  let start = reltime()
  let id = timer_start(20, {-> feedkeys('x', 'L')})
  let c = 0
  while c == 0 && reltimefloat(reltime(start)) < 0.2
    let c = getchar(0)
    sleep 10m
  endwhile
  call assert_equal('x', nr2char(c))
  call timer_stop(id)
endfunc

func Test_timer_ex_mode()
  " Function with an empty line.
  func Foo(...)

  endfunc
  let timer = timer_start(40, function('g:Foo'), {'repeat':-1})
  " This used to throw error E749.
  exe "normal Qsleep 100m\rvi\r"
  call timer_stop(timer)
endfunc

func Test_timer_restore_count()
  CheckRunVimInTerminal
  " Check that v:count is saved and restored, not changed by a timer.
  call writefile([
        \ 'nnoremap <expr><silent> L v:count ? v:count . "l" : "l"',
        \ 'func Doit(id)',
        \ '  normal 3j',
        \ 'endfunc',
        \ 'call timer_start(100, "Doit")',
	\ ], 'Xtrcscript', 'D')
  call writefile([
        \ '1-1234',
        \ '2-1234',
        \ '3-1234',
	\ ], 'Xtrctext', 'D')
  let buf = RunVimInTerminal('-S Xtrcscript Xtrctext', {})

  " Wait for the timer to move the cursor to the third line.
  call WaitForAssert({-> assert_equal(3, term_getcursor(buf)[0])})
  call assert_equal(1, term_getcursor(buf)[1])
  " Now check that v:count has not been set to 3
  call term_sendkeys(buf, 'L')
  call WaitForAssert({-> assert_equal(2, term_getcursor(buf)[1])})

  call StopVimInTerminal(buf)
endfunc

" Test that the garbage collector isn't triggered if a timer callback invokes
" vgetc().
func Test_nocatch_timer_garbage_collect()
  " FIXME: why does this fail only on MacOS M1?
  try
    CheckNotMacM1
  catch /Skipped/
    let g:skipped_reason = v:exception
    return
  endtry

  " 'uptimetime. must be bigger than the timer timeout
  set ut=200
  call test_garbagecollect_soon()
  call test_override('no_wait_return', 0)
  func CauseAnError(id)
    " This will show an error and wait for Enter.
    let a = {'foo', 'bar'}
  endfunc
  func FeedChar(id)
    call feedkeys(":\<CR>", 't')
  endfunc
  call timer_start(300, 'FeedChar')
  call timer_start(100, 'CauseAnError')
  let x = getchar()   " wait for error in timer
  let x = getchar(0)  " read any remaining chars
  let x = getchar(0)

  set ut&
  call test_override('no_wait_return', 1)
  delfunc CauseAnError
  delfunc FeedChar
endfunc

func Test_timer_error_in_timer_callback()
  if !has('terminal') || (has('win32') && has('gui_running'))
    throw 'Skipped: cannot run Vim in a terminal window'
  endif

  let lines =<< trim [CODE]
  func Func(timer)
    " fail to create list
    let x = [
  endfunc
  set updatetime=50
  call timer_start(1, 'Func')
  [CODE]
  call writefile(lines, 'Xtest.vim', 'D')

  let buf = term_start(GetVimCommandCleanTerm() .. ' -S Xtest.vim', {'term_rows': 8})
  let job = term_getjob(buf)
  call WaitForAssert({-> assert_notequal('', term_getline(buf, 8))})

  " GC must not run during timer callback, which can make Vim crash.
  call TermWait(buf, 50)
  call term_sendkeys(buf, "\<CR>")
  call TermWait(buf, 50)
  call assert_equal('run', job_status(job))

  call term_sendkeys(buf, ":qall!\<CR>")
  call WaitFor({-> job_status(job) ==# 'dead'})
  if has('unix')
    call assert_equal('', job_info(job).termsig)
  endif

  exe buf .. 'bwipe!'
endfunc

" Test for garbage collection when a timer is still running
func Test_timer_garbage_collect()
  let timer = timer_start(1000, function('MyHandler'), {'repeat' : 10})
  call test_garbagecollect_now()
  let l = timer_info(timer)
  call assert_equal(function('MyHandler'), l[0].callback)
  call timer_stop(timer)
endfunc

func Test_timer_invalid_callback()
  call assert_fails('call timer_start(0, "0")', 'E921:')
endfunc

func Test_timer_changing_function_list()
  CheckRunVimInTerminal

  " Create a large number of functions.  Should get the "more" prompt.
  " The typing "G" triggers the timer, which changes the function table.
  let lines =<< trim END
    for func in map(range(1,99), "'Func' .. v:val")
      exe "func " .. func .. "()"
      endfunc
    endfor
    au CmdlineLeave : call timer_start(0, {-> 0})
  END
  call writefile(lines, 'XTest_timerchange', 'D')
  let buf = RunVimInTerminal('-S XTest_timerchange', #{rows: 10})
  call term_sendkeys(buf, ":fu\<CR>")
  call WaitForAssert({-> assert_match('-- More --', term_getline(buf, 10))})
  call term_sendkeys(buf, "G")
  call WaitForAssert({-> assert_match('E454:', term_getline(buf, 9))})
  call term_sendkeys(buf, "\<Esc>")

  call StopVimInTerminal(buf)
endfunc

func Test_timer_outputting_message()
  CheckRunVimInTerminal

  let lines =<< trim END
    vim9script
    setline(1, 'some text')
    set showcmd ut=2000 cmdheight=1
    timer_start(0, (_) => {
            echon repeat('x', &columns - 11)
        })
  END
  call writefile(lines, 'XTest_timermessage', 'D')
  let buf = RunVimInTerminal('-S XTest_timermessage', #{rows: 6})
  call term_sendkeys(buf, "l")
  call term_wait(buf)
  " should not get a hit-enter prompt
  call WaitForAssert({-> assert_match('xxxxxxxxxxx', term_getline(buf, 6))})

  call StopVimInTerminal(buf)
endfunc

func Test_timer_using_win_execute_undo_sync()
  " FIXME: why does this fail only on MacOS M1?
  CheckNotMacM1

  let bufnr1 = bufnr()
  new
  let g:bufnr2 = bufnr()
  let g:winid = win_getid()
  exe "buffer " .. bufnr1
  wincmd w
  call setline(1, ['test'])
  autocmd InsertEnter * call timer_start(100, { -> win_execute(g:winid, 'buffer ' .. g:bufnr2) })
  call timer_start(200, { -> feedkeys("\<CR>bbbb\<Esc>") })
  call feedkeys("Oaaaa", 'x!t')
  " will hang here until the second timer fires
  call assert_equal(['aaaa', 'bbbb', 'test'], getline(1, '$'))
  undo
  call assert_equal(['test'], getline(1, '$'))

  bwipe!
  bwipe!
  unlet g:winid
  unlet g:bufnr2
  au! InsertEnter
endfunc


" vim: shiftwidth=2 sts=2 expandtab
