view src/testdir/test_autocmd.vim @ 34454:f8fed6c8bb60 v9.1.0143

patch 9.1.0143: [security]: autocmd causes use-after-free in set_curbuf() Commit: https://github.com/vim/vim/commit/55f8bba73be5f9c3a5a4d0d6c5f56e65f2c7d3fc Author: Christian Brabandt <cb@256bit.org> Date: Wed Feb 28 23:32:00 2024 +0100 patch 9.1.0143: [security]: autocmd causes use-after-free in set_curbuf() Problem: [security]: autocmd cause use-after-free in set_curbuf() (kawarimidoll) Solution: check side-effect of BufLeave autocommand, when the number of windows changed, close windows containing buffers that will be wiped, if curbuf changed unexpectedly make sure b_nwindows is decremented otherwise it cannot be wiped set_curbuf() already makes some efforts to ensure the BufLeave autocommands do not cause issues. However there are still 2 issues that are not taken care of: 1) If a BufLeave autocommand opens a new window containing the same buffer as that is going got be closed in close_buffer() a bit later, we suddenly have another window open, containing a free'd buffer. So we must check if the number of windows changed and if it does (and the current buffer is going to be wiped (according to the 'bufhidden' setting), let's immediately close all windows containing the current buffer using close_windows() 2) If a BufLeave autocommand changes our current buffer (displays it in the current window), buf->b_nwindow will be incremented. As part of set_curbuf() we will however enter another buffer soon, which means, the newly created curbuf will have b_nwindows still have set, even so the buffer is no longer displayed in a window. This causes later problems, because it will no longer be possible to wipe such a buffer. So just before entering the final buffer, check if the curbuf changed when calling the BufLeave autocommand and if it does (and curbuf is still valid), decrement curbuf->b_nwindows. Both issues can be verified using the provided test (however the second issue only because such an impacted buffer won't be wiped, causing futher issues in later tests). fixes: #13839 closes: #14104 Signed-off-by: Christian Brabandt <cb@256bit.org>
author Christian Brabandt <cb@256bit.org>
date Wed, 28 Feb 2024 23:45:03 +0100
parents 3b654f4462c5
children 56ba5ea924a7
line wrap: on
line source

" Tests for autocommands

source shared.vim
source check.vim
source term_util.vim
source screendump.vim
import './vim9.vim' as v9

func s:cleanup_buffers() abort
  for bnr in range(1, bufnr('$'))
    if bufloaded(bnr) && bufnr('%') != bnr
      execute 'bd! ' . bnr
    endif
  endfor
endfunc

func Test_vim_did_enter()
  call assert_false(v:vim_did_enter)

  " This script will never reach the main loop, can't check if v:vim_did_enter
  " becomes one.
endfunc

" Test for the CursorHold autocmd
func Test_CursorHold_autocmd()
  CheckRunVimInTerminal
  call writefile(['one', 'two', 'three'], 'XoneTwoThree', 'D')
  let before =<< trim END
    set updatetime=10
    au CursorHold * call writefile([line('.')], 'XCHoutput', 'a')
  END
  call writefile(before, 'XCHinit', 'D')
  let buf = RunVimInTerminal('-S XCHinit XoneTwoThree', {})
  call term_sendkeys(buf, "G")
  call term_wait(buf, 50)
  call term_sendkeys(buf, "gg")
  call term_wait(buf)
  call WaitForAssert({-> assert_equal(['1'], readfile('XCHoutput')[-1:-1])})
  call term_sendkeys(buf, "j")
  call term_wait(buf)
  call WaitForAssert({-> assert_equal(['1', '2'], readfile('XCHoutput')[-2:-1])})
  call term_sendkeys(buf, "j")
  call term_wait(buf)
  call WaitForAssert({-> assert_equal(['1', '2', '3'], readfile('XCHoutput')[-3:-1])})
  call StopVimInTerminal(buf)

  call delete('XCHoutput')
endfunc

if has('timers')

  func ExitInsertMode(id)
    call feedkeys("\<Esc>")
  endfunc

  func Test_cursorhold_insert()
    " depends on timing
    let g:test_is_flaky = 1

    " Need to move the cursor.
    call feedkeys("ggG", "xt")

    let g:triggered = 0
    au CursorHoldI * let g:triggered += 1
    set updatetime=20
    call timer_start(200, 'ExitInsertMode')
    call feedkeys('a', 'x!')
    sleep 30m
    call assert_equal(1, g:triggered)
    unlet g:triggered
    au! CursorHoldI
    set updatetime&
  endfunc

  func Test_cursorhold_insert_with_timer_interrupt()
    CheckFeature job
    " Need to move the cursor.
    call feedkeys("ggG", "xt")

    " Confirm the timer invoked in exit_cb of the job doesn't disturb
    " CursorHoldI event.
    let g:triggered = 0
    au CursorHoldI * let g:triggered += 1
    set updatetime=100
    call job_start(has('win32') ? 'cmd /c echo:' : 'echo',
          \ {'exit_cb': {-> timer_start(200, 'ExitInsertMode')}})
    call feedkeys('a', 'x!')
    call assert_equal(1, g:triggered)
    unlet g:triggered
    au! CursorHoldI
    set updatetime&
  endfunc

  func Test_cursorhold_insert_ctrl_x()
    let g:triggered = 0
    au CursorHoldI * let g:triggered += 1
    set updatetime=20
    call timer_start(100, 'ExitInsertMode')
    " CursorHoldI does not trigger after CTRL-X
    call feedkeys("a\<C-X>", 'x!')
    call assert_equal(0, g:triggered)
    unlet g:triggered
    au! CursorHoldI
    set updatetime&
  endfunc

  func Test_cursorhold_insert_ctrl_g_U()
    au CursorHoldI * :
    set updatetime=20
    new
    call timer_start(100, { -> feedkeys("\<Left>foo\<Esc>", 't') })
    call feedkeys("i()\<C-g>U", 'tx!')
    sleep 200m
    call assert_equal('(foo)', getline(1))
    undo
    call assert_equal('', getline(1))

    bwipe!
    au! CursorHoldI
    set updatetime&
  endfunc

  func Test_OptionSet_modeline()
    call test_override('starting', 1)
    au! OptionSet
    augroup set_tabstop
      au OptionSet tabstop call timer_start(1, {-> execute("echo 'Handler called'", "")})
    augroup END
    call writefile(['vim: set ts=7 sw=5 :', 'something'], 'XoptionsetModeline', 'D')
    set modeline
    let v:errmsg = ''
    call assert_fails('split XoptionsetModeline', 'E12:')
    call assert_equal(7, &ts)
    call assert_equal('', v:errmsg)

    augroup set_tabstop
      au!
    augroup END
    bwipe!
    set ts&
    call test_override('starting', 0)
  endfunc

endif "has('timers')

func Test_bufunload()
  augroup test_bufunload_group
    autocmd!
    autocmd BufUnload * call add(s:li, "bufunload")
    autocmd BufDelete * call add(s:li, "bufdelete")
    autocmd BufWipeout * call add(s:li, "bufwipeout")
  augroup END

  let s:li = []
  new
  setlocal bufhidden=
  bunload
  call assert_equal(["bufunload", "bufdelete"], s:li)

  let s:li = []
  new
  setlocal bufhidden=delete
  bunload
  call assert_equal(["bufunload", "bufdelete"], s:li)

  let s:li = []
  new
  setlocal bufhidden=unload
  bwipeout
  call assert_equal(["bufunload", "bufdelete", "bufwipeout"], s:li)

  au! test_bufunload_group
  augroup! test_bufunload_group
endfunc

" SEGV occurs in older versions.  (At least 7.4.2005 or older)
func Test_autocmd_bufunload_with_tabnext()
  tabedit
  tabfirst

  augroup test_autocmd_bufunload_with_tabnext_group
    autocmd!
    autocmd BufUnload <buffer> tabnext
  augroup END

  quit
  call assert_equal(2, tabpagenr('$'))

  autocmd! test_autocmd_bufunload_with_tabnext_group
  augroup! test_autocmd_bufunload_with_tabnext_group
  tablast
  quit
endfunc

func Test_argdelete_in_next()
  au BufNew,BufEnter,BufLeave,BufWinEnter * argdel
  call assert_fails('next a b', 'E1156:')
  au! BufNew,BufEnter,BufLeave,BufWinEnter *
endfunc

func Test_autocmd_bufwinleave_with_tabfirst()
  tabedit
  augroup sample
    autocmd!
    autocmd BufWinLeave <buffer> tabfirst
  augroup END
  call setline(1, ['a', 'b', 'c'])
  edit! a.txt
  tabclose
endfunc

" SEGV occurs in older versions.  (At least 7.4.2321 or older)
func Test_autocmd_bufunload_avoiding_SEGV_01()
  split aa.txt
  let lastbuf = bufnr('$')

  augroup test_autocmd_bufunload
    autocmd!
    exe 'autocmd BufUnload <buffer> ' . (lastbuf + 1) . 'bwipeout!'
  augroup END

  call assert_fails('edit bb.txt', 'E937:')

  autocmd! test_autocmd_bufunload
  augroup! test_autocmd_bufunload
  bwipe! aa.txt
  bwipe! bb.txt
endfunc

" SEGV occurs in older versions.  (At least 7.4.2321 or older)
func Test_autocmd_bufunload_avoiding_SEGV_02()
  setlocal buftype=nowrite
  let lastbuf = bufnr('$')

  augroup test_autocmd_bufunload
    autocmd!
    exe 'autocmd BufUnload <buffer> ' . (lastbuf + 1) . 'bwipeout!'
  augroup END

  normal! i1
  call assert_fails('edit a.txt', 'E517:')

  autocmd! test_autocmd_bufunload
  augroup! test_autocmd_bufunload
  bwipe! a.txt
endfunc

func Test_autocmd_dummy_wipeout()
  " prepare files
  call writefile([''], 'Xdummywipetest1.txt', 'D')
  call writefile([''], 'Xdummywipetest2.txt', 'D')
  augroup test_bufunload_group
    autocmd!
    autocmd BufUnload * call add(s:li, "bufunload")
    autocmd BufDelete * call add(s:li, "bufdelete")
    autocmd BufWipeout * call add(s:li, "bufwipeout")
  augroup END

  let s:li = []
  split Xdummywipetest1.txt
  silent! vimgrep /notmatched/ Xdummywipetest*
  call assert_equal(["bufunload", "bufwipeout"], s:li)

  bwipeout
  au! test_bufunload_group
  augroup! test_bufunload_group
endfunc

func Test_win_tab_autocmd()
  let g:record = []

  augroup testing
    au WinNewPre * call add(g:record, 'WinNewPre')
    au WinNew * call add(g:record, 'WinNew')
    au WinClosed * call add(g:record, 'WinClosed')
    au WinEnter * call add(g:record, 'WinEnter')
    au WinLeave * call add(g:record, 'WinLeave')
    au TabNew * call add(g:record, 'TabNew')
    au TabClosed * call add(g:record, 'TabClosed')
    au TabEnter * call add(g:record, 'TabEnter')
    au TabLeave * call add(g:record, 'TabLeave')
  augroup END

  split
  tabnew
  close
  close

  call assert_equal([
	\ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
	\ 'WinLeave', 'TabLeave', 'WinNewPre', 'WinNew', 'WinEnter', 'TabNew', 'TabEnter',
	\ 'WinLeave', 'TabLeave', 'WinClosed', 'TabClosed', 'WinEnter', 'TabEnter',
	\ 'WinLeave', 'WinClosed', 'WinEnter'
	\ ], g:record)

  let g:record = []
  tabnew somefile
  tabnext
  bwipe somefile

  call assert_equal([
	\ 'WinLeave', 'TabLeave', 'WinNewPre', 'WinNew', 'WinEnter', 'TabNew', 'TabEnter',
	\ 'WinLeave', 'TabLeave', 'WinEnter', 'TabEnter',
	\ 'WinClosed', 'TabClosed'
	\ ], g:record)

  let g:record = []
  copen
  help
  tabnext
  vnew

  call assert_equal([
	\ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
	\ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter',
	\ 'WinNewPre', 'WinLeave', 'WinNew', 'WinEnter'
	\ ], g:record)

  augroup testing
    au!
  augroup END
  unlet g:record
endfunc

func Test_WinNewPre()
  " Test that the old window layout can be accessed before a new window is created.
  let g:layouts_pre = []
  let g:layouts_post = []
  augroup testing
    au WinNewPre * call add(g:layouts_pre, winlayout())
    au WinNew * call add(g:layouts_post, winlayout())
  augroup END
  split
  call assert_notequal(g:layouts_pre[0], g:layouts_post[0])
  split
  call assert_equal(g:layouts_pre[1], g:layouts_post[0])
  call assert_notequal(g:layouts_pre[1], g:layouts_post[1])
  tabnew
  call assert_notequal(g:layouts_pre[2], g:layouts_post[1])
  call assert_notequal(g:layouts_pre[2], g:layouts_post[2])
  augroup testing
    au!
  augroup END
  unlet g:layouts_pre
  unlet g:layouts_post

  " Test modifying window layout during WinNewPre throws.
  let g:caught = 0
  augroup testing
    au!
    au WinNewPre * split
  augroup END
  try
    vnew
  catch
    let g:caught += 1
  endtry
  augroup testing
    au!
    au WinNewPre * tabnew
  augroup END
  try
    vnew
  catch
    let g:caught += 1
  endtry
  augroup testing
    au!
    au WinNewPre * close
  augroup END
  try
    vnew
  catch
    let g:caught += 1
  endtry
  augroup testing
    au!
    au WinNewPre * tabclose
  augroup END
  try
    vnew
  catch
    let g:caught += 1
  endtry
  call assert_equal(4, g:caught)
  augroup testing
    au!
  augroup END
  unlet g:caught
endfunc

func Test_WinResized()
  CheckRunVimInTerminal

  let lines =<< trim END
    set scrolloff=0
    call setline(1, ['111', '222'])
    vnew
    call setline(1, ['aaa', 'bbb'])
    new
    call setline(1, ['foo', 'bar'])

    let g:resized = 0
    au WinResized * let g:resized += 1

    func WriteResizedEvent()
      call writefile([json_encode(v:event)], 'XresizeEvent')
    endfunc
    au WinResized * call WriteResizedEvent()
  END
  call writefile(lines, 'Xtest_winresized', 'D')
  let buf = RunVimInTerminal('-S Xtest_winresized', {'rows': 10})

  " redraw now to avoid a redraw after the :echo command
  call term_sendkeys(buf, ":redraw!\<CR>")
  call TermWait(buf)

  call term_sendkeys(buf, ":echo g:resized\<CR>")
  call WaitForAssert({-> assert_match('^0$', term_getline(buf, 10))}, 1000)

  " increase window height, two windows will be reported
  call term_sendkeys(buf, "\<C-W>+")
  call TermWait(buf)
  call term_sendkeys(buf, ":echo g:resized\<CR>")
  call WaitForAssert({-> assert_match('^1$', term_getline(buf, 10))}, 1000)

  let event = readfile('XresizeEvent')[0]->json_decode()
  call assert_equal({
        \ 'windows': [1002, 1001],
        \ }, event)

  " increase window width, three windows will be reported
  call term_sendkeys(buf, "\<C-W>>")
  call TermWait(buf)
  call term_sendkeys(buf, ":echo g:resized\<CR>")
  call WaitForAssert({-> assert_match('^2$', term_getline(buf, 10))}, 1000)

  let event = readfile('XresizeEvent')[0]->json_decode()
  call assert_equal({
        \ 'windows': [1002, 1001, 1000],
        \ }, event)

  call delete('XresizeEvent')
  call StopVimInTerminal(buf)
endfunc

func Test_WinScrolled()
  CheckRunVimInTerminal

  let lines =<< trim END
    set nowrap scrolloff=0
    for ii in range(1, 18)
      call setline(ii, repeat(nr2char(96 + ii), ii * 2))
    endfor
    let win_id = win_getid()
    let g:matched = v:false
    func WriteScrollEvent()
      call writefile([json_encode(v:event)], 'XscrollEvent')
    endfunc
    execute 'au WinScrolled' win_id 'let g:matched = v:true'
    let g:scrolled = 0
    au WinScrolled * let g:scrolled += 1
    au WinScrolled * let g:amatch = str2nr(expand('<amatch>'))
    au WinScrolled * let g:afile = str2nr(expand('<afile>'))
    au WinScrolled * call WriteScrollEvent()
  END
  call writefile(lines, 'Xtest_winscrolled', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled', {'rows': 6})

  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^0 ', term_getline(buf, 6))}, 1000)

  " Scroll left/right in Normal mode.
  call term_sendkeys(buf, "zlzh:echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^2 ', term_getline(buf, 6))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 1, 'topline': 0, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': -1, 'topline': 0, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  " Scroll up/down in Normal mode.
  call term_sendkeys(buf, "\<c-e>\<c-y>:echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^4 ', term_getline(buf, 6))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': -1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  " Scroll up/down in Insert mode.
  call term_sendkeys(buf, "Mi\<c-x>\<c-e>\<Esc>i\<c-x>\<c-y>\<Esc>")
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^6 ', term_getline(buf, 6))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': -1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  " Scroll the window horizontally to focus the last letter of the third line
  " containing only six characters. Moving to the previous and shorter lines
  " should trigger another autocommand as Vim has to make them visible.
  call term_sendkeys(buf, "5zl2k")
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^8 ', term_getline(buf, 6))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 5, 'topline': 0, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': -5, 'topline': 0, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  " Ensure the command was triggered for the specified window ID.
  call term_sendkeys(buf, ":echo g:matched\<CR>")
  call WaitForAssert({-> assert_match('^v:true ', term_getline(buf, 6))}, 1000)

  " Ensure the expansion of <amatch> and <afile> matches the window ID.
  call term_sendkeys(buf, ":echo g:amatch == win_id && g:afile == win_id\<CR>")
  call WaitForAssert({-> assert_match('^v:true ', term_getline(buf, 6))}, 1000)

  call delete('XscrollEvent')
  call StopVimInTerminal(buf)
endfunc

func Test_WinScrolled_mouse()
  CheckRunVimInTerminal

  let lines =<< trim END
    set nowrap scrolloff=0
    set mouse=a term=xterm ttymouse=sgr mousetime=200 clipboard=
    call setline(1, ['foo']->repeat(32))
    split
    let g:scrolled = 0
    au WinScrolled * let g:scrolled += 1
  END
  call writefile(lines, 'Xtest_winscrolled_mouse', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_mouse', {'rows': 10})

  " With the upper split focused, send a scroll-down event to the unfocused one.
  call test_setmouse(7, 1)
  call term_sendkeys(buf, "\<ScrollWheelDown>")
  call TermWait(buf)
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^1', term_getline(buf, 10))}, 1000)

  " Again, but this time while we're in insert mode.
  call term_sendkeys(buf, "i\<ScrollWheelDown>\<Esc>")
  call TermWait(buf)
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^2', term_getline(buf, 10))}, 1000)

  call StopVimInTerminal(buf)
endfunc

func Test_WinScrolled_close_curwin()
  CheckRunVimInTerminal

  let lines =<< trim END
    set nowrap scrolloff=0
    call setline(1, ['aaa', 'bbb'])
    vsplit
    au WinScrolled * close
    au VimLeave * call writefile(['123456'], 'Xtestout')
  END
  call writefile(lines, 'Xtest_winscrolled_close_curwin', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_close_curwin', {'rows': 6})

  " This was using freed memory
  call term_sendkeys(buf, "\<C-E>")
  call TermWait(buf)
  call StopVimInTerminal(buf)

  " check the startup script finished to the end
  call assert_equal(['123456'], readfile('Xtestout'))
  call delete('Xtestout')
endfunc

func Test_WinScrolled_once_only()
  CheckRunVimInTerminal

  let lines =<< trim END
      set cmdheight=2
      call setline(1, ['aaa', 'bbb'])
      let trigger_count = 0
      func ShowInfo(id)
        echo g:trigger_count g:winid winlayout()
      endfunc

      vsplit
      split
      " use a timer to show the info after a redraw
      au WinScrolled * let trigger_count += 1 | let winid = expand('<amatch>') | call timer_start(100, 'ShowInfo')
      wincmd j
      wincmd l
  END
  call writefile(lines, 'Xtest_winscrolled_once', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_once', #{rows: 10, cols: 60, statusoff: 2})

  call term_sendkeys(buf, "\<C-E>")
  call VerifyScreenDump(buf, 'Test_winscrolled_once_only_1', {})

  call StopVimInTerminal(buf)
endfunc

" Check that WinScrolled is not triggered immediately when defined and there
" are split windows.
func Test_WinScrolled_not_when_defined()
  CheckRunVimInTerminal

  let lines =<< trim END
      call setline(1, ['aaa', 'bbb'])
      echo 'nothing happened'
      func ShowTriggered(id)
        echo 'triggered'
      endfunc
  END
  call writefile(lines, 'Xtest_winscrolled_not', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_not', #{rows: 10, cols: 60, statusoff: 2})
  call term_sendkeys(buf, ":split\<CR>")
  call TermWait(buf)
  " use a timer to show the message after redrawing
  call term_sendkeys(buf, ":au WinScrolled * call timer_start(100, 'ShowTriggered')\<CR>")
  call VerifyScreenDump(buf, 'Test_winscrolled_not_when_defined_1', {})

  call term_sendkeys(buf, "\<C-E>")
  call VerifyScreenDump(buf, 'Test_winscrolled_not_when_defined_2', {})

  call StopVimInTerminal(buf)
endfunc

func Test_WinScrolled_long_wrapped()
  CheckRunVimInTerminal

  let lines =<< trim END
    set scrolloff=0
    let height = winheight(0)
    let width = winwidth(0)
    let g:scrolled = 0
    au WinScrolled * let g:scrolled += 1
    call setline(1, repeat('foo', height * width))
    call cursor(1, height * width)
  END
  call writefile(lines, 'Xtest_winscrolled_long_wrapped', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_long_wrapped', {'rows': 6})

  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^0 ', term_getline(buf, 6))}, 1000)

  call term_sendkeys(buf, 'gj')
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^1 ', term_getline(buf, 6))}, 1000)

  call term_sendkeys(buf, '0')
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^2 ', term_getline(buf, 6))}, 1000)

  call term_sendkeys(buf, '$')
  call term_sendkeys(buf, ":echo g:scrolled\<CR>")
  call WaitForAssert({-> assert_match('^3 ', term_getline(buf, 6))}, 1000)

  call StopVimInTerminal(buf)
endfunc

func Test_WinScrolled_diff()
  CheckRunVimInTerminal

  let lines =<< trim END
    set diffopt+=foldcolumn:0
    call setline(1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'])
    vnew
    call setline(1, ['d', 'e', 'f', 'g', 'h', 'i'])
    windo diffthis
    func WriteScrollEvent()
      call writefile([json_encode(v:event)], 'XscrollEvent')
    endfunc
    au WinScrolled * call WriteScrollEvent()
  END
  call writefile(lines, 'Xtest_winscrolled_diff', 'D')
  let buf = RunVimInTerminal('-S Xtest_winscrolled_diff', {'rows': 8})

  call term_sendkeys(buf, "\<C-E>")
  call WaitForAssert({-> assert_match('^d', term_getline(buf, 3))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 1, 'topfill': 1, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': 1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1001': {'leftcol': 0, 'topline': 0, 'topfill': -1, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  call term_sendkeys(buf, "2\<C-E>")
  call WaitForAssert({-> assert_match('^f', term_getline(buf, 3))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 2, 'topfill': 2, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': 2, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1001': {'leftcol': 0, 'topline': 0, 'topfill': -2, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  call term_sendkeys(buf, "\<C-E>")
  call WaitForAssert({-> assert_match('^g', term_getline(buf, 3))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 2, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': 1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1001': {'leftcol': 0, 'topline': 1, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  call term_sendkeys(buf, "2\<C-Y>")
  call WaitForAssert({-> assert_match('^e', term_getline(buf, 3))}, 1000)

  let event = readfile('XscrollEvent')[0]->json_decode()
  call assert_equal({
        \ 'all': {'leftcol': 0, 'topline': 3, 'topfill': 1, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1000': {'leftcol': 0, 'topline': -2, 'topfill': 0, 'width': 0, 'height': 0, 'skipcol': 0},
        \ '1001': {'leftcol': 0, 'topline': -1, 'topfill': 1, 'width': 0, 'height': 0, 'skipcol': 0}
        \ }, event)

  call StopVimInTerminal(buf)
  call delete('XscrollEvent')
endfunc

func Test_WinClosed()
  " Test that the pattern is matched against the closed window's ID, and both
  " <amatch> and <afile> are set to it.
  new
  let winid = win_getid()
  let g:matched = v:false
  augroup test-WinClosed
    autocmd!
    execute 'autocmd WinClosed' winid 'let g:matched = v:true'
    autocmd WinClosed * let g:amatch = str2nr(expand('<amatch>'))
    autocmd WinClosed * let g:afile = str2nr(expand('<afile>'))
  augroup END
  close
  call assert_true(g:matched)
  call assert_equal(winid, g:amatch)
  call assert_equal(winid, g:afile)

  " Test that WinClosed is non-recursive.
  new
  new
  call assert_equal(3, winnr('$'))
  let g:triggered = 0
  augroup test-WinClosed
    autocmd!
    autocmd WinClosed * let g:triggered += 1
    autocmd WinClosed * 2 wincmd c
  augroup END
  close
  call assert_equal(1, winnr('$'))
  call assert_equal(1, g:triggered)

  autocmd! test-WinClosed
  augroup! test-WinClosed
  unlet g:matched
  unlet g:amatch
  unlet g:afile
  unlet g:triggered
endfunc

func Test_WinClosed_throws()
  vnew
  let bnr = bufnr()
  call assert_equal(1, bufloaded(bnr))
  augroup test-WinClosed
    autocmd WinClosed * throw 'foo'
  augroup END
  try
    close
  catch /.*/
  endtry
  call assert_equal(0, bufloaded(bnr))

  autocmd! test-WinClosed
  augroup! test-WinClosed
endfunc

func Test_WinClosed_throws_with_tabs()
  tabnew
  let bnr = bufnr()
  call assert_equal(1, bufloaded(bnr))
  augroup test-WinClosed
    autocmd WinClosed * throw 'foo'
  augroup END
  try
    close
  catch /.*/
  endtry
  call assert_equal(0, bufloaded(bnr))

  autocmd! test-WinClosed
  augroup! test-WinClosed
endfunc

" This used to trigger WinClosed twice for the same window, and the window's
" buffer was NULL in the second autocommand.
func Test_WinClosed_switch_tab()
  edit Xa
  split Xb
  split Xc
  tab split
  new
  augroup test-WinClosed
    autocmd WinClosed * tabprev | bwipe!
  augroup END
  close
  " Check that the tabline has been fully removed
  call assert_equal([1, 1], win_screenpos(0))

  autocmd! test-WinClosed
  augroup! test-WinClosed
  %bwipe!
endfunc

func s:AddAnAutocmd()
  augroup vimBarTest
    au BufReadCmd * echo 'hello'
  augroup END
  call assert_equal(3, len(split(execute('au vimBarTest'), "\n")))
endfunc

func Test_early_bar()
  " test that a bar is recognized before the {event}
  call s:AddAnAutocmd()
  augroup vimBarTest | au! | let done = 77 | augroup END
  call assert_equal(1, len(split(execute('au vimBarTest'), "\n")))
  call assert_equal(77, done)

  call s:AddAnAutocmd()
  augroup vimBarTest| au!| let done = 88 | augroup END
  call assert_equal(1, len(split(execute('au vimBarTest'), "\n")))
  call assert_equal(88, done)

  " test that a bar is recognized after the {event}
  call s:AddAnAutocmd()
  augroup vimBarTest| au!BufReadCmd| let done = 99 | augroup END
  call assert_equal(1, len(split(execute('au vimBarTest'), "\n")))
  call assert_equal(99, done)

  " test that a bar is recognized after the {group}
  call s:AddAnAutocmd()
  au! vimBarTest|echo 'hello'
  call assert_equal(1, len(split(execute('au vimBarTest'), "\n")))
endfunc

func RemoveGroup()
  autocmd! StartOK
  augroup! StartOK
endfunc

func Test_augroup_warning()
  augroup TheWarning
    au VimEnter * echo 'entering'
  augroup END
  call assert_match("TheWarning.*VimEnter", execute('au VimEnter'))
  redir => res
  augroup! TheWarning
  redir END
  call assert_match("W19:", res)
  call assert_match("-Deleted-.*VimEnter", execute('au VimEnter'))

  " check "Another" does not take the pace of the deleted entry
  augroup Another
  augroup END
  call assert_match("-Deleted-.*VimEnter", execute('au VimEnter'))
  augroup! Another

  " no warning for postpone aucmd delete
  augroup StartOK
    au VimEnter * call RemoveGroup()
  augroup END
  call assert_match("StartOK.*VimEnter", execute('au VimEnter'))
  redir => res
  doautocmd VimEnter
  redir END
  call assert_notmatch("W19:", res)
  au! VimEnter

  call assert_fails('augroup!', 'E471:')
endfunc

func Test_BufReadCmdHelp()
  " This used to cause access to free memory
  au BufReadCmd * e +h
  help

  au! BufReadCmd
endfunc

func Test_BufReadCmdHelpJump()
  " This used to cause access to free memory
  au BufReadCmd * e +h{
  " } to fix highlighting
  call assert_fails('help', 'E434:')

  au! BufReadCmd
endfunc

" BufReadCmd is triggered for a "nofile" buffer. Check all values.
func Test_BufReadCmdNofile()
  for val in ['nofile',
            \ 'nowrite',
            \ 'acwrite',
            \ 'quickfix',
            \ 'help',
            \ 'terminal',
            \ 'prompt',
            \ 'popup',
            \ ]
    new somefile
    exe 'set buftype=' .. val
    au BufReadCmd somefile call setline(1, 'triggered')
    edit
    call assert_equal('triggered', getline(1))

    au! BufReadCmd
    bwipe!
  endfor
endfunc

func Test_augroup_deleted()
  " This caused a crash before E936 was introduced
  augroup x
    call assert_fails('augroup! x', 'E936:')
    au VimEnter * echo
  augroup end
  augroup! x
  call assert_match("-Deleted-.*VimEnter", execute('au VimEnter'))
  au! VimEnter
endfunc

" Tests for autocommands on :close command.
" This used to be in test13.
func Test_three_windows()
  " Clean up buffers, because in some cases this function fails.
  call s:cleanup_buffers()

  " Write three files and open them, each in a window.
  " Then go to next window, with autocommand that deletes the previous one.
  " Do this twice, writing the file.
  e! Xtestje1
  call setline(1, 'testje1')
  w
  sp Xtestje2
  call setline(1, 'testje2')
  w
  sp Xtestje3
  call setline(1, 'testje3')
  w
  wincmd w
  au WinLeave Xtestje2 bwipe
  wincmd w
  call assert_equal('Xtestje1', expand('%'))

  au WinLeave Xtestje1 bwipe Xtestje3
  close
  call assert_equal('Xtestje1', expand('%'))

  " Test deleting the buffer on a Unload event.  If this goes wrong there
  " will be the ATTENTION prompt.
  e Xtestje1
  au!
  au! BufUnload Xtestje1 bwipe
  call assert_fails('e Xtestje3', 'E937:')
  call assert_equal('Xtestje3', expand('%'))

  e Xtestje2
  sp Xtestje1
  call assert_fails('e', 'E937:')
  call assert_equal('Xtestje1', expand('%'))

  " Test changing buffers in a BufWipeout autocommand.  If this goes wrong
  " there are ml_line errors and/or a Crash.
  au!
  only
  e Xanother
  e Xtestje1
  bwipe Xtestje2
  bwipe Xtestje3
  au BufWipeout Xtestje1 buf Xtestje1
  bwipe
  call assert_equal('Xanother', expand('%'))

  only
  help
  wincmd w
  1quit
  call assert_equal('Xanother', expand('%'))

  au!
  enew
  call delete('Xtestje1')
  call delete('Xtestje2')
  call delete('Xtestje3')
endfunc

func Test_BufEnter()
  au! BufEnter
  au Bufenter * let val = val . '+'
  let g:val = ''
  split NewFile
  call assert_equal('+', g:val)
  bwipe!
  call assert_equal('++', g:val)

  " Also get BufEnter when editing a directory
  call mkdir('Xbufenterdir', 'D')
  split Xbufenterdir
  call assert_equal('+++', g:val)

  " On MS-Windows we can't edit the directory, make sure we wipe the right
  " buffer.
  bwipe! Xbufenterdir
  au! BufEnter

  " Editing a "nofile" buffer doesn't read the file but does trigger BufEnter
  " for historic reasons.  Also test other 'buftype' values.
  for val in ['nofile',
            \ 'nowrite',
            \ 'acwrite',
            \ 'quickfix',
            \ 'help',
            \ 'terminal',
            \ 'prompt',
            \ 'popup',
            \ ]
    new somefile
    exe 'set buftype=' .. val
    au BufEnter somefile call setline(1, 'some text')
    edit
    call assert_equal('some text', getline(1))
    bwipe!
    au! BufEnter
  endfor

  new
  new
  autocmd BufEnter * ++once close
  call assert_fails('close', 'E1312:')

  au! BufEnter
  only
endfunc

" Closing a window might cause an endless loop
" E814 for older Vims
func Test_autocmd_bufwipe_in_SessLoadPost()
  edit Xtest
  tabnew
  file Xsomething
  set noswapfile
  mksession!

  let content =<< trim [CODE]
    call test_override('ui_delay', 10)
    set nocp noswapfile
    let v:swapchoice = "e"
    augroup test_autocmd_sessionload
    autocmd!
    autocmd SessionLoadPost * exe bufnr("Xsomething") . "bw!"
    augroup END

    func WriteErrors()
      call writefile([execute("messages")], "XerrorsBwipe")
    endfunc
    au VimLeave * call WriteErrors()
  [CODE]

  call writefile(content, 'Xvimrc', 'D')
  call system(GetVimCommand('Xvimrc') .. ' --not-a-term --noplugins -S Session.vim -c cq')
  sleep 100m
  let errors = join(readfile('XerrorsBwipe'))
  call assert_match('E814:', errors)

  set swapfile
  for file in ['Session.vim', 'XerrorsBwipe']
    call delete(file)
  endfor
endfunc

" Using :blast and :ball for many events caused a crash, because b_nwindows was
" not incremented correctly.
func Test_autocmd_blast_badd()
  let content =<< trim [CODE]
      au BufNew,BufAdd,BufWinEnter,BufEnter,BufLeave,BufWinLeave,BufUnload,VimEnter foo* blast
      edit foo1
      au BufNew,BufAdd,BufWinEnter,BufEnter,BufLeave,BufWinLeave,BufUnload,VimEnter foo* ball
      edit foo2
      call writefile(['OK'], 'XerrorsBlast')
      qall
  [CODE]

  call writefile(content, 'XblastBall', 'D')
  call system(GetVimCommand() .. ' --clean -S XblastBall')
  sleep 100m
  call assert_match('OK', readfile('XerrorsBlast')->join())

  call delete('XerrorsBlast')
endfunc

" SEGV occurs in older versions.
func Test_autocmd_bufwipe_in_SessLoadPost2()
  tabnew
  set noswapfile
  mksession!

  let content =<< trim [CODE]
    set nocp noswapfile
    function! DeleteInactiveBufs()
      tabfirst
      let tabblist = []
      for i in range(1, tabpagenr(''$''))
        call extend(tabblist, tabpagebuflist(i))
      endfor
      for b in range(1, bufnr(''$''))
        if bufexists(b) && buflisted(b) && (index(tabblist, b) == -1 || bufname(b) =~# ''^$'')
          exec ''bwipeout '' . b
        endif
      endfor
      echomsg "SessionLoadPost DONE"
    endfunction
    au SessionLoadPost * call DeleteInactiveBufs()

    func WriteErrors()
      call writefile([execute("messages")], "XerrorsPost")
    endfunc
    au VimLeave * call WriteErrors()
  [CODE]

  call writefile(content, 'Xvimrc', 'D')
  call system(GetVimCommand('Xvimrc') .. ' --not-a-term --noplugins -S Session.vim -c cq')
  sleep 100m
  let errors = join(readfile('XerrorsPost'))
  " This probably only ever matches on unix.
  call assert_notmatch('Caught deadly signal SEGV', errors)
  call assert_match('SessionLoadPost DONE', errors)

  set swapfile
  for file in ['Session.vim', 'XerrorsPost']
    call delete(file)
  endfor
endfunc

func Test_empty_doau()
  doau \|
endfunc

func s:AutoCommandOptionSet(match)
  let template = "Option: <%s>, OldVal: <%s>, OldValLocal: <%s>, OldValGlobal: <%s>, NewVal: <%s>, Scope: <%s>, Command: <%s>\n"
  let item     = remove(g:options, 0)
  let expected = printf(template, item[0], item[1], item[2], item[3], item[4], item[5], item[6])
  let actual   = printf(template, a:match, v:option_old, v:option_oldlocal, v:option_oldglobal, v:option_new, v:option_type, v:option_command)
  let g:opt    = [expected, actual]
  "call assert_equal(expected, actual)
endfunc

func Test_OptionSet()
  CheckOption autochdir

  badd test_autocmd.vim

  call test_override('starting', 1)
  set nocp
  au OptionSet * :call s:AutoCommandOptionSet(expand("<amatch>"))

  " 1: Setting number option"
  let g:options = [['number', 0, 0, 0, 1, 'global', 'set']]
  set nu
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 2: Setting local number option"
  let g:options = [['number', 1, 1, '', 0, 'local', 'setlocal']]
  setlocal nonu
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 3: Setting global number option"
  let g:options = [['number', 1, '', 1, 0, 'global', 'setglobal']]
  setglobal nonu
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 4: Setting local autoindent option"
  let g:options = [['autoindent', 0, 0, '', 1, 'local', 'setlocal']]
  setlocal ai
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 5: Setting global autoindent option"
  let g:options = [['autoindent', 0, '', 0, 1, 'global', 'setglobal']]
  setglobal ai
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 6: Setting global autoindent option"
  let g:options = [['autoindent', 1, 1, 1, 0, 'global', 'set']]
  set ai!
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 6a: Setting global autoindent option"
  let g:options = [['autoindent', 1, 1, 0, 0, 'global', 'set']]
  noa setlocal ai
  noa setglobal noai
  set ai!
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " Should not print anything, use :noa
  " 7: don't trigger OptionSet"
  let g:options = [['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid']]
  noa set nonu
  call assert_equal([['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid']], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 8: Setting several global list and number option"
  let g:options = [['list', 0, 0, 0, 1, 'global', 'set'], ['number', 0, 0, 0, 1, 'global', 'set']]
  set list nu
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 9: don't trigger OptionSet"
  let g:options = [['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid'], ['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid']]
  noa set nolist nonu
  call assert_equal([['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid'], ['invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid', 'invalid']], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 10: Setting global acd"
  let g:options = [['autochdir', 0, 0, '', 1, 'local', 'setlocal']]
  setlocal acd
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 11: Setting global autoread (also sets local value)"
  let g:options = [['autoread', 0, 0, 0, 1, 'global', 'set']]
  set ar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 12: Setting local autoread"
  let g:options = [['autoread', 1, 1, '', 1, 'local', 'setlocal']]
  setlocal ar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 13: Setting global autoread"
  let g:options = [['autoread', 1, '', 1, 0, 'global', 'setglobal']]
  setglobal invar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 14: Setting option backspace through :let"
  let g:options = [['backspace', '', '', '', 'eol,indent,start', 'global', 'set']]
  let &bs = "eol,indent,start"
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 15: Setting option backspace through setbufvar()"
  let g:options = [['backup', 0, 0, '', 1, 'local', 'setlocal']]
  " try twice, first time, shouldn't trigger because option name is invalid,
  " second time, it should trigger
  let bnum = bufnr('%')
  call assert_fails("call setbufvar(bnum, '&l:bk', 1)", 'E355:')
  " should trigger, use correct option name
  call setbufvar(bnum, '&backup', 1)
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 16: Setting number option using setwinvar"
  let g:options = [['number', 0, 0, '', 1, 'local', 'setlocal']]
  call setwinvar(0, '&number', 1)
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 17: Setting key option, shouldn't trigger"
  let g:options = [['key', 'invalid', 'invalid1', 'invalid2', 'invalid3', 'invalid4', 'invalid5']]
  setlocal key=blah
  setlocal key=
  call assert_equal([['key', 'invalid', 'invalid1', 'invalid2', 'invalid3', 'invalid4', 'invalid5']], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 18a: Setting string global option"
  let oldval = &backupext
  let g:options = [['backupext', oldval, oldval, oldval, 'foo', 'global', 'set']]
  set backupext=foo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 18b: Resetting string global option"
  let g:options = [['backupext', 'foo', 'foo', 'foo', oldval, 'global', 'set']]
  set backupext&
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 18c: Setting global string global option"
  let g:options = [['backupext', oldval, '', oldval, 'bar', 'global', 'setglobal']]
  setglobal backupext=bar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 18d: Setting local string global option"
  " As this is a global option this sets the global value even though
  " :setlocal is used!
  noa set backupext& " Reset global and local value (without triggering autocmd)
  let g:options = [['backupext', oldval, oldval, '', 'baz', 'local', 'setlocal']]
  setlocal backupext=baz
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 18e: Setting again string global option"
  noa setglobal backupext=ext_global " Reset global and local value (without triggering autocmd)
  noa setlocal backupext=ext_local " Sets the global(!) value!
  let g:options = [['backupext', 'ext_local', 'ext_local', 'ext_local', 'fuu', 'global', 'set']]
  set backupext=fuu
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 19a: Setting string global-local (to buffer) option"
  let oldval = &tags
  let g:options = [['tags', oldval, oldval, oldval, 'tagpath', 'global', 'set']]
  set tags=tagpath
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 19b: Resetting string global-local (to buffer) option"
  let g:options = [['tags', 'tagpath', 'tagpath', 'tagpath', oldval, 'global', 'set']]
  set tags&
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 19c: Setting global string global-local (to buffer) option "
  let g:options = [['tags', oldval, '', oldval, 'tagpath1', 'global', 'setglobal']]
  setglobal tags=tagpath1
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 19d: Setting local string global-local (to buffer) option"
  let g:options = [['tags', 'tagpath1', 'tagpath1', '', 'tagpath2', 'local', 'setlocal']]
  setlocal tags=tagpath2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 19e: Setting again string global-local (to buffer) option"
  " Note: v:option_old is the old global value for global-local string options
  " but the old local value for all other kinds of options.
  noa setglobal tags=tag_global " Reset global and local value (without triggering autocmd)
  noa setlocal tags=tag_local
  let g:options = [['tags', 'tag_global', 'tag_local', 'tag_global', 'tagpath', 'global', 'set']]
  set tags=tagpath
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 19f: Setting string global-local (to buffer) option to an empty string"
  " Note: v:option_old is the old global value for global-local string options
  " but the old local value for all other kinds of options.
  noa set tags=tag_global " Reset global and local value (without triggering autocmd)
  noa setlocal tags= " empty string
  let g:options = [['tags', 'tag_global', '', 'tag_global', 'tagpath', 'global', 'set']]
  set tags=tagpath
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 20a: Setting string local (to buffer) option"
  let oldval = &spelllang
  let g:options = [['spelllang', oldval, oldval, oldval, 'elvish,klingon', 'global', 'set']]
  set spelllang=elvish,klingon
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 20b: Resetting string local (to buffer) option"
  let g:options = [['spelllang', 'elvish,klingon', 'elvish,klingon', 'elvish,klingon', oldval, 'global', 'set']]
  set spelllang&
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 20c: Setting global string local (to buffer) option"
  let g:options = [['spelllang', oldval, '', oldval, 'elvish', 'global', 'setglobal']]
  setglobal spelllang=elvish
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 20d: Setting local string local (to buffer) option"
  noa set spelllang& " Reset global and local value (without triggering autocmd)
  let g:options = [['spelllang', oldval, oldval, '', 'klingon', 'local', 'setlocal']]
  setlocal spelllang=klingon
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 20e: Setting again string local (to buffer) option"
  " Note: v:option_old is the old global value for global-local string options
  " but the old local value for all other kinds of options.
  noa setglobal spelllang=spellglobal " Reset global and local value (without triggering autocmd)
  noa setlocal spelllang=spelllocal
  let g:options = [['spelllang', 'spelllocal', 'spelllocal', 'spellglobal', 'foo', 'global', 'set']]
  set spelllang=foo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 21a: Setting string global-local (to window) option"
  let oldval = &statusline
  let g:options = [['statusline', oldval, oldval, oldval, 'foo', 'global', 'set']]
  set statusline=foo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 21b: Resetting string global-local (to window) option"
  " Note: v:option_old is the old global value for global-local string options
  " but the old local value for all other kinds of options.
  let g:options = [['statusline', 'foo', 'foo', 'foo', oldval, 'global', 'set']]
  set statusline&
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 21c: Setting global string global-local (to window) option"
  let g:options = [['statusline', oldval, '', oldval, 'bar', 'global', 'setglobal']]
  setglobal statusline=bar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 21d: Setting local string global-local (to window) option"
  noa set statusline& " Reset global and local value (without triggering autocmd)
  let g:options = [['statusline', oldval, oldval, '', 'baz', 'local', 'setlocal']]
  setlocal statusline=baz
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 21e: Setting again string global-local (to window) option"
  " Note: v:option_old is the old global value for global-local string options
  " but the old local value for all other kinds of options.
  noa setglobal statusline=bar " Reset global and local value (without triggering autocmd)
  noa setlocal statusline=baz
  let g:options = [['statusline', 'bar', 'baz', 'bar', 'foo', 'global', 'set']]
  set statusline=foo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 22a: Setting string local (to window) option"
  let oldval = &foldignore
  let g:options = [['foldignore', oldval, oldval, oldval, 'fo', 'global', 'set']]
  set foldignore=fo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 22b: Resetting string local (to window) option"
  let g:options = [['foldignore', 'fo', 'fo', 'fo', oldval, 'global', 'set']]
  set foldignore&
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 22c: Setting global string local (to window) option"
  let g:options = [['foldignore', oldval, '', oldval, 'bar', 'global', 'setglobal']]
  setglobal foldignore=bar
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 22d: Setting local string local (to window) option"
  noa set foldignore& " Reset global and local value (without triggering autocmd)
  let g:options = [['foldignore', oldval, oldval, '', 'baz', 'local', 'setlocal']]
  setlocal foldignore=baz
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 22e: Setting again string local (to window) option"
  noa setglobal foldignore=glob " Reset global and local value (without triggering autocmd)
  noa setlocal foldignore=loc
  let g:options = [['foldignore', 'loc', 'loc', 'glob', 'fo', 'global', 'set']]
  set foldignore=fo
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 23a: Setting global number global option"
  noa setglobal cmdheight=8 " Reset global and local value (without triggering autocmd)
  noa setlocal cmdheight=1 " Sets the global(!) value!
  let g:options = [['cmdheight', '1', '', '1', '2', 'global', 'setglobal']]
  setglobal cmdheight=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 23b: Setting local number global option"
  noa setglobal cmdheight=8 " Reset global and local value (without triggering autocmd)
  noa setlocal cmdheight=1 " Sets the global(!) value!
  let g:options = [['cmdheight', '1', '1', '', '2', 'local', 'setlocal']]
  setlocal cmdheight=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 23c: Setting again number global option"
  noa setglobal cmdheight=8 " Reset global and local value (without triggering autocmd)
  noa setlocal cmdheight=1 " Sets the global(!) value!
  let g:options = [['cmdheight', '1', '1', '1', '2', 'global', 'set']]
  set cmdheight=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 23d: Setting again number global option"
  noa set cmdheight=8 " Reset global and local value (without triggering autocmd)
  let g:options = [['cmdheight', '8', '8', '8', '2', 'global', 'set']]
  set cmdheight=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 24a: Setting global number global-local (to buffer) option"
  noa setglobal undolevels=8 " Reset global and local value (without triggering autocmd)
  noa setlocal undolevels=1
  let g:options = [['undolevels', '8', '', '8', '2', 'global', 'setglobal']]
  setglobal undolevels=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 24b: Setting local number global-local (to buffer) option"
  noa setglobal undolevels=8 " Reset global and local value (without triggering autocmd)
  noa setlocal undolevels=1
  let g:options = [['undolevels', '1', '1', '', '2', 'local', 'setlocal']]
  setlocal undolevels=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 24c: Setting again number global-local (to buffer) option"
  noa setglobal undolevels=8 " Reset global and local value (without triggering autocmd)
  noa setlocal undolevels=1
  let g:options = [['undolevels', '1', '1', '8', '2', 'global', 'set']]
  set undolevels=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 24d: Setting again global number global-local (to buffer) option"
  noa set undolevels=8 " Reset global and local value (without triggering autocmd)
  let g:options = [['undolevels', '8', '8', '8', '2', 'global', 'set']]
  set undolevels=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 25a: Setting global number local (to buffer) option"
  noa setglobal wrapmargin=8 " Reset global and local value (without triggering autocmd)
  noa setlocal wrapmargin=1
  let g:options = [['wrapmargin', '8', '', '8', '2', 'global', 'setglobal']]
  setglobal wrapmargin=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 25b: Setting local number local (to buffer) option"
  noa setglobal wrapmargin=8 " Reset global and local value (without triggering autocmd)
  noa setlocal wrapmargin=1
  let g:options = [['wrapmargin', '1', '1', '', '2', 'local', 'setlocal']]
  setlocal wrapmargin=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 25c: Setting again number local (to buffer) option"
  noa setglobal wrapmargin=8 " Reset global and local value (without triggering autocmd)
  noa setlocal wrapmargin=1
  let g:options = [['wrapmargin', '1', '1', '8', '2', 'global', 'set']]
  set wrapmargin=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 25d: Setting again global number local (to buffer) option"
  noa set wrapmargin=8 " Reset global and local value (without triggering autocmd)
  let g:options = [['wrapmargin', '8', '8', '8', '2', 'global', 'set']]
  set wrapmargin=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 26: Setting number global-local (to window) option.
  " Such option does currently not exist.


  " 27a: Setting global number local (to window) option"
  noa setglobal foldcolumn=8 " Reset global and local value (without triggering autocmd)
  noa setlocal foldcolumn=1
  let g:options = [['foldcolumn', '8', '', '8', '2', 'global', 'setglobal']]
  setglobal foldcolumn=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 27b: Setting local number local (to window) option"
  noa setglobal foldcolumn=8 " Reset global and local value (without triggering autocmd)
  noa setlocal foldcolumn=1
  let g:options = [['foldcolumn', '1', '1', '', '2', 'local', 'setlocal']]
  setlocal foldcolumn=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 27c: Setting again number local (to window) option"
  noa setglobal foldcolumn=8 " Reset global and local value (without triggering autocmd)
  noa setlocal foldcolumn=1
  let g:options = [['foldcolumn', '1', '1', '8', '2', 'global', 'set']]
  set foldcolumn=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 27d: Setting again global number local (to window) option"
  noa set foldcolumn=8 " Reset global and local value (without triggering autocmd)
  let g:options = [['foldcolumn', '8', '8', '8', '2', 'global', 'set']]
  set foldcolumn=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 28a: Setting global boolean global option"
  noa setglobal nowrapscan " Reset global and local value (without triggering autocmd)
  noa setlocal wrapscan " Sets the global(!) value!
  let g:options = [['wrapscan', '1', '', '1', '0', 'global', 'setglobal']]
  setglobal nowrapscan
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 28b: Setting local boolean global option"
  noa setglobal nowrapscan " Reset global and local value (without triggering autocmd)
  noa setlocal wrapscan " Sets the global(!) value!
  let g:options = [['wrapscan', '1', '1', '', '0', 'local', 'setlocal']]
  setlocal nowrapscan
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 28c: Setting again boolean global option"
  noa setglobal nowrapscan " Reset global and local value (without triggering autocmd)
  noa setlocal wrapscan " Sets the global(!) value!
  let g:options = [['wrapscan', '1', '1', '1', '0', 'global', 'set']]
  set nowrapscan
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 28d: Setting again global boolean global option"
  noa set nowrapscan " Reset global and local value (without triggering autocmd)
  let g:options = [['wrapscan', '0', '0', '0', '1', 'global', 'set']]
  set wrapscan
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 29a: Setting global boolean global-local (to buffer) option"
  noa setglobal noautoread " Reset global and local value (without triggering autocmd)
  noa setlocal autoread
  let g:options = [['autoread', '0', '', '0', '1', 'global', 'setglobal']]
  setglobal autoread
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 29b: Setting local boolean global-local (to buffer) option"
  noa setglobal noautoread " Reset global and local value (without triggering autocmd)
  noa setlocal autoread
  let g:options = [['autoread', '1', '1', '', '0', 'local', 'setlocal']]
  setlocal noautoread
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 29c: Setting again boolean global-local (to buffer) option"
  noa setglobal noautoread " Reset global and local value (without triggering autocmd)
  noa setlocal autoread
  let g:options = [['autoread', '1', '1', '0', '1', 'global', 'set']]
  set autoread
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 29d: Setting again global boolean global-local (to buffer) option"
  noa set noautoread " Reset global and local value (without triggering autocmd)
  let g:options = [['autoread', '0', '0', '0', '1', 'global', 'set']]
  set autoread
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 30a: Setting global boolean local (to buffer) option"
  noa setglobal nocindent " Reset global and local value (without triggering autocmd)
  noa setlocal cindent
  let g:options = [['cindent', '0', '', '0', '1', 'global', 'setglobal']]
  setglobal cindent
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 30b: Setting local boolean local (to buffer) option"
  noa setglobal nocindent " Reset global and local value (without triggering autocmd)
  noa setlocal cindent
  let g:options = [['cindent', '1', '1', '', '0', 'local', 'setlocal']]
  setlocal nocindent
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 30c: Setting again boolean local (to buffer) option"
  noa setglobal nocindent " Reset global and local value (without triggering autocmd)
  noa setlocal cindent
  let g:options = [['cindent', '1', '1', '0', '1', 'global', 'set']]
  set cindent
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 30d: Setting again global boolean local (to buffer) option"
  noa set nocindent " Reset global and local value (without triggering autocmd)
  let g:options = [['cindent', '0', '0', '0', '1', 'global', 'set']]
  set cindent
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 31: Setting boolean global-local (to window) option
  " Currently no such option exists.


  " 32a: Setting global boolean local (to window) option"
  noa setglobal nocursorcolumn " Reset global and local value (without triggering autocmd)
  noa setlocal cursorcolumn
  let g:options = [['cursorcolumn', '0', '', '0', '1', 'global', 'setglobal']]
  setglobal cursorcolumn
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 32b: Setting local boolean local (to window) option"
  noa setglobal nocursorcolumn " Reset global and local value (without triggering autocmd)
  noa setlocal cursorcolumn
  let g:options = [['cursorcolumn', '1', '1', '', '0', 'local', 'setlocal']]
  setlocal nocursorcolumn
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 32c: Setting again boolean local (to window) option"
  noa setglobal nocursorcolumn " Reset global and local value (without triggering autocmd)
  noa setlocal cursorcolumn
  let g:options = [['cursorcolumn', '1', '1', '0', '1', 'global', 'set']]
  set cursorcolumn
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])

  " 32d: Setting again global boolean local (to window) option"
  noa set nocursorcolumn " Reset global and local value (without triggering autocmd)
  let g:options = [['cursorcolumn', '0', '0', '0', '1', 'global', 'set']]
  set cursorcolumn
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " 33: Test autocommands when an option value is converted internally.
  noa set backspace=1 " Reset global and local value (without triggering autocmd)
  let g:options = [['backspace', 'indent,eol', 'indent,eol', 'indent,eol', '2', 'global', 'set']]
  set backspace=2
  call assert_equal([], g:options)
  call assert_equal(g:opt[0], g:opt[1])


  " Cleanup
  au! OptionSet
  " set tags&
  for opt in ['nu', 'ai', 'acd', 'ar', 'bs', 'backup', 'cul', 'cp', 'backupext', 'tags', 'spelllang', 'statusline', 'foldignore', 'cmdheight', 'undolevels', 'wrapmargin', 'foldcolumn', 'wrapscan', 'autoread', 'cindent', 'cursorcolumn']
    exe printf(":set %s&vim", opt)
  endfor
  call test_override('starting', 0)
  delfunc! AutoCommandOptionSet
endfunc

func Test_OptionSet_diffmode()
  call test_override('starting', 1)
  " 18: Changing an option when entering diff mode
  new
  au OptionSet diff :let &l:cul = v:option_new

  call setline(1, ['buffer 1', 'line2', 'line3', 'line4'])
  call assert_equal(0, &l:cul)
  diffthis
  call assert_equal(1, &l:cul)

  vnew
  call setline(1, ['buffer 2', 'line 2', 'line 3', 'line4'])
  call assert_equal(0, &l:cul)
  diffthis
  call assert_equal(1, &l:cul)

  diffoff
  call assert_equal(0, &l:cul)
  call assert_equal(1, getwinvar(2, '&l:cul'))
  bw!

  call assert_equal(1, &l:cul)
  diffoff!
  call assert_equal(0, &l:cul)
  call assert_equal(0, getwinvar(1, '&l:cul'))
  bw!

  " Cleanup
  au! OptionSet
  call test_override('starting', 0)
endfunc

func Test_OptionSet_diffmode_close()
  call test_override('starting', 1)
  " 19: Try to close the current window when entering diff mode
  " should not segfault
  new
  au OptionSet diff close

  call setline(1, ['buffer 1', 'line2', 'line3', 'line4'])
  call assert_fails(':diffthis', 'E788:')
  call assert_equal(1, &diff)
  vnew
  call setline(1, ['buffer 2', 'line 2', 'line 3', 'line4'])
  call assert_fails(':diffthis', 'E788:')
  call assert_equal(1, &diff)
  set diffopt-=closeoff
  bw!
  call assert_fails(':diffoff!', 'E788:')
  bw!

  " Cleanup
  au! OptionSet
  call test_override('starting', 0)
  "delfunc! AutoCommandOptionSet
endfunc

" Test for Bufleave autocommand that deletes the buffer we are about to edit.
func Test_BufleaveWithDelete()
  new | edit XbufLeave1

  augroup test_bufleavewithdelete
      autocmd!
      autocmd BufLeave XbufLeave1 bwipe XbufLeave2
  augroup END

  call assert_fails('edit XbufLeave2', 'E143:')
  call assert_equal('XbufLeave1', bufname('%'))

  autocmd! test_bufleavewithdelete BufLeave XbufLeave1
  augroup! test_bufleavewithdelete

  new
  bwipe! XbufLeave1
endfunc

" Test for autocommand that changes the buffer list, when doing ":ball".
func Test_Acmd_BufAll()
  enew!
  %bwipe!
  call writefile(['Test file Xxx1'], 'Xxx1', 'D')
  call writefile(['Test file Xxx2'], 'Xxx2', 'D')
  call writefile(['Test file Xxx3'], 'Xxx3', 'D')

  " Add three files to the buffer list
  split Xxx1
  close
  split Xxx2
  close
  split Xxx3
  close

  " Wipe the buffer when the buffer is opened
  au BufReadPost Xxx2 bwipe

  call append(0, 'Test file Xxx4')
  ball

  call assert_equal(2, winnr('$'))
  call assert_equal('Xxx1', bufname(winbufnr(winnr('$'))))
  wincmd t

  au! BufReadPost
  %bwipe!
  enew! | only
endfunc

" Test for autocommand that changes current buffer on BufEnter event.
" Check if modelines are interpreted for the correct buffer.
func Test_Acmd_BufEnter()
  %bwipe!
  call writefile(['start of test file Xxx1',
	      \ "\<Tab>this is a test",
	      \ 'end of test file Xxx1'], 'Xxx1', 'D')
  call writefile(['start of test file Xxx2',
	      \ 'vim: set noai :',
	      \ "\<Tab>this is a test",
	      \ 'end of test file Xxx2'], 'Xxx2', 'D')

  au BufEnter Xxx2 brew
  set ai modeline modelines=3
  edit Xxx1
  " edit Xxx2, autocmd will do :brew
  edit Xxx2
  exe "normal G?this is a\<CR>"
  " Append text with autoindent to this file
  normal othis should be auto-indented
  call assert_equal("\<Tab>this should be auto-indented", getline('.'))
  call assert_equal(3, line('.'))
  " Remove autocmd and edit Xxx2 again
  au! BufEnter Xxx2
  buf! Xxx2
  exe "normal G?this is a\<CR>"
  " append text without autoindent to Xxx
  normal othis should be in column 1
  call assert_equal("this should be in column 1", getline('.'))
  call assert_equal(4, line('.'))

  %bwipe!
  set ai&vim modeline&vim modelines&vim
endfunc

" Test for issue #57
" do not move cursor on <c-o> when autoindent is set
func Test_ai_CTRL_O()
  enew!
  set ai
  let save_fo = &fo
  set fo+=r
  exe "normal o# abcdef\<Esc>2hi\<CR>\<C-O>d0\<Esc>"
  exe "normal o# abcdef\<Esc>2hi\<C-O>d0\<Esc>"
  call assert_equal(['# abc', 'def', 'def'], getline(2, 4))

  set ai&vim
  let &fo = save_fo
  enew!
endfunc

" Test for autocommand that deletes the current buffer on BufLeave event.
" Also test deleting the last buffer, should give a new, empty buffer.
func Test_BufLeave_Wipe()
  %bwipe!
  let content = ['start of test file Xxx',
	      \ 'this is a test',
	      \ 'end of test file Xxx']
  call writefile(content, 'Xxx1', 'D')
  call writefile(content, 'Xxx2', 'D')

  au BufLeave Xxx2 bwipe
  edit Xxx1
  split Xxx2
  " delete buffer Xxx2, we should be back to Xxx1
  bwipe
  call assert_equal('Xxx1', bufname('%'))
  call assert_equal(1, winnr('$'))

  " Create an alternate buffer
  %write! test.out
  call assert_equal('test.out', bufname('#'))
  " delete alternate buffer
  bwipe test.out
  call assert_equal('Xxx1', bufname('%'))
  call assert_equal('', bufname('#'))

  au BufLeave Xxx1 bwipe
  " delete current buffer, get an empty one
  bwipe!
  call assert_equal(1, line('$'))
  call assert_equal('', bufname('%'))
  let g:bufinfo = getbufinfo()
  call assert_equal(1, len(g:bufinfo))

  call delete('test.out')
  %bwipe
  au! BufLeave

  " check that bufinfo doesn't contain a pointer to freed memory
  call test_garbagecollect_now()
endfunc

func Test_QuitPre()
  edit Xfoo
  let winid = win_getid(winnr())
  split Xbar
  au! QuitPre * let g:afile = expand('<afile>')
  " Close the other window, <afile> should be correct.
  exe win_id2win(winid) . 'q'
  call assert_equal('Xfoo', g:afile)

  unlet g:afile
  bwipe Xfoo
  bwipe Xbar
endfunc

func Test_Cmdline()
  au! CmdlineChanged : let g:text = getcmdline()
  let g:text = 0
  call feedkeys(":echom 'hello'\<CR>", 'xt')
  call assert_equal("echom 'hello'", g:text)
  au! CmdlineChanged

  au! CmdlineChanged : let g:entered = expand('<afile>')
  let g:entered = 0
  call feedkeys(":echom 'hello'\<CR>", 'xt')
  call assert_equal(':', g:entered)
  au! CmdlineChanged

  autocmd CmdlineChanged : let g:log += [getcmdline()]

  let g:log = []
  cnoremap <F1> <Cmd>call setcmdline('ls')<CR>
  call feedkeys(":\<F1>", 'xt')
  call assert_equal(['ls'], g:log)
  cunmap <F1>

  let g:log = []
  call feedkeys(":sign \<Tab>\<Tab>\<C-N>\<C-P>\<S-Tab>\<S-Tab>\<Esc>", 'xt')
  call assert_equal([
        \ 's',
        \ 'si',
        \ 'sig',
        \ 'sign',
        \ 'sign ',
        \ 'sign define',
        \ 'sign jump',
        \ 'sign list',
        \ 'sign jump',
        \ 'sign define',
        \ 'sign ',
        \ ], g:log)
  let g:log = []
  set wildmenu wildoptions+=pum
  call feedkeys(":sign \<S-Tab>\<PageUp>\<kPageUp>\<kPageDown>\<PageDown>\<Esc>", 'xt')
  call assert_equal([
        \ 's',
        \ 'si',
        \ 'sig',
        \ 'sign',
        \ 'sign ',
        \ 'sign unplace',
        \ 'sign jump',
        \ 'sign define',
        \ 'sign undefine',
        \ 'sign unplace',
        \ ], g:log)
  set wildmenu& wildoptions&

  let g:log = []
  let @r = 'abc'
  call feedkeys(":0\<C-R>r1\<C-R>\<C-O>r2\<C-R>\<C-R>r3\<Esc>", 'xt')
  call assert_equal([
        \ '0',
        \ '0a',
        \ '0ab',
        \ '0abc',
        \ '0abc1',
        \ '0abc1abc',
        \ '0abc1abc2',
        \ '0abc1abc2abc',
        \ '0abc1abc2abc3',
        \ ], g:log)

  unlet g:log
  au! CmdlineChanged

  au! CmdlineEnter : let g:entered = expand('<afile>')
  au! CmdlineLeave : let g:left = expand('<afile>')
  let g:entered = 0
  let g:left = 0
  call feedkeys(":echo 'hello'\<CR>", 'xt')
  call assert_equal(':', g:entered)
  call assert_equal(':', g:left)
  au! CmdlineEnter
  au! CmdlineLeave

  let save_shellslash = &shellslash
  set noshellslash
  au! CmdlineEnter / let g:entered = expand('<afile>')
  au! CmdlineLeave / let g:left = expand('<afile>')
  let g:entered = 0
  let g:left = 0
  new
  call setline(1, 'hello')
  call feedkeys("/hello\<CR>", 'xt')
  call assert_equal('/', g:entered)
  call assert_equal('/', g:left)
  bwipe!
  au! CmdlineEnter
  au! CmdlineLeave
  let &shellslash = save_shellslash
endfunc

" Test for BufWritePre autocommand that deletes or unloads the buffer.
func Test_BufWritePre()
  %bwipe
  au BufWritePre Xxx1 bunload
  au BufWritePre Xxx2 bwipe

  call writefile(['start of Xxx1', 'test', 'end of Xxx1'], 'Xxx1', 'D')
  call writefile(['start of Xxx2', 'test', 'end of Xxx2'], 'Xxx2', 'D')

  edit Xtest
  e! Xxx2
  bdel Xtest
  e Xxx1
  " write it, will unload it and give an error msg
  call assert_fails('w', 'E203:')
  call assert_equal('Xxx2', bufname('%'))
  edit Xtest
  e! Xxx2
  bwipe Xtest
  " write it, will delete the buffer and give an error msg
  call assert_fails('w', 'E203:')
  call assert_equal('Xxx1', bufname('%'))
  au! BufWritePre
endfunc

" Test for BufUnload autocommand that unloads all the other buffers
func Test_bufunload_all()
  let g:test_is_flaky = 1
  call writefile(['Test file Xxx1'], 'Xxx1', 'D')
  call writefile(['Test file Xxx2'], 'Xxx2', 'D')

  let content =<< trim [CODE]
    func UnloadAllBufs()
      let i = 1
      while i <= bufnr('$')
        if i != bufnr('%') && bufloaded(i)
          exe  i . 'bunload'
        endif
        let i += 1
      endwhile
    endfunc
    au BufUnload * call UnloadAllBufs()
    au VimLeave * call writefile(['Test Finished'], 'Xout')
    edit Xxx1
    split Xxx2
    q
  [CODE]

  call writefile(content, 'Xbunloadtest', 'D')

  call delete('Xout')
  call system(GetVimCommandClean() .. ' -N --not-a-term -S Xbunloadtest')
  call assert_true(filereadable('Xout'))

  call delete('Xout')
endfunc

" Some tests for buffer-local autocommands
func Test_buflocal_autocmd()
  let g:bname = ''
  edit xx
  au BufLeave <buffer> let g:bname = expand("%")
  " here, autocommand for xx should trigger.
  " but autocommand shall not apply to buffer named <buffer>.
  edit somefile
  call assert_equal('xx', g:bname)
  let g:bname = ''
  " here, autocommand shall be auto-deleted
  bwipe xx
  " autocmd should not trigger
  edit xx
  call assert_equal('', g:bname)
  " autocmd should not trigger
  edit somefile
  call assert_equal('', g:bname)
  enew
  unlet g:bname
endfunc

" Test for "*Cmd" autocommands
func Test_Cmd_Autocmds()
  call writefile(['start of Xxx', "\tabc2", 'end of Xxx'], 'Xxx', 'D')

  enew!
  au BufReadCmd XtestA 0r Xxx|$del
  edit XtestA			" will read text of Xxd instead
  call assert_equal('start of Xxx', getline(1))

  au BufWriteCmd XtestA call append(line("$"), "write")
  write				" will append a line to the file
  call assert_equal('write', getline('$'))
  call assert_fails('read XtestA', 'E484:')	" should not read anything
  call assert_equal('write', getline(4))

  " now we have:
  " 1	start of Xxx
  " 2		abc2
  " 3	end of Xxx
  " 4	write

  au FileReadCmd XtestB '[r Xxx
  2r XtestB			" will read Xxx below line 2 instead
  call assert_equal('start of Xxx', getline(3))

  " now we have:
  " 1	start of Xxx
  " 2		abc2
  " 3	start of Xxx
  " 4		abc2
  " 5	end of Xxx
  " 6	end of Xxx
  " 7	write

  au FileWriteCmd XtestC '[,']copy $
  normal 4GA1
  4,5w XtestC			" will copy lines 4 and 5 to the end
  call assert_equal("\tabc21", getline(8))
  call assert_fails('r XtestC', 'E484:')	" should not read anything
  call assert_equal("end of Xxx", getline(9))

  " now we have:
  " 1	start of Xxx
  " 2		abc2
  " 3	start of Xxx
  " 4		abc21
  " 5	end of Xxx
  " 6	end of Xxx
  " 7	write
  " 8		abc21
  " 9	end of Xxx

  let g:lines = []
  au FileAppendCmd XtestD call extend(g:lines, getline(line("'["), line("']")))
  w >>XtestD			" will add lines to 'lines'
  call assert_equal(9, len(g:lines))
  call assert_fails('$r XtestD', 'E484:')	" should not read anything
  call assert_equal(9, line('$'))
  call assert_equal('end of Xxx', getline('$'))

  au BufReadCmd XtestE 0r Xxx|$del
  sp XtestE			" split window with test.out
  call assert_equal('end of Xxx', getline(3))

  let g:lines = []
  exe "normal 2Goasdf\<Esc>\<C-W>\<C-W>"
  au BufWriteCmd XtestE call extend(g:lines, getline(0, '$'))
  wall				" will write other window to 'lines'
  call assert_equal(4, len(g:lines), g:lines)
  call assert_equal('asdf', g:lines[2])

  au! BufReadCmd
  au! BufWriteCmd
  au! FileReadCmd
  au! FileWriteCmd
  au! FileAppendCmd
  %bwipe!
  enew!
endfunc

func s:ReadFile()
  setl noswapfile nomodified
  let filename = resolve(expand("<afile>:p"))
  execute 'read' fnameescape(filename)
  1d_
  exe 'file' fnameescape(filename)
  setl buftype=acwrite
endfunc

func s:WriteFile()
  let filename = resolve(expand("<afile>:p"))
  setl buftype=
  noautocmd execute 'write' fnameescape(filename)
  setl buftype=acwrite
  setl nomodified
endfunc

func Test_BufReadCmd()
  autocmd BufReadCmd *.test call s:ReadFile()
  autocmd BufWriteCmd *.test call s:WriteFile()

  call writefile(['one', 'two', 'three'], 'Xcmd.test', 'D')
  edit Xcmd.test
  call assert_match('Xcmd.test" line 1 of 3', execute('file'))
  normal! Gofour
  write
  call assert_equal(['one', 'two', 'three', 'four'], readfile('Xcmd.test'))

  bwipe!
  au! BufReadCmd
  au! BufWriteCmd
endfunc

func Test_BufWriteCmd()
  autocmd BufWriteCmd Xbufwritecmd let g:written = 1
  new
  file Xbufwritecmd
  set buftype=acwrite
  call mkdir('Xbufwritecmd', 'D')
  write
  " BufWriteCmd should be triggered even if a directory has the same name
  call assert_equal(1, g:written)
  unlet g:written
  au! BufWriteCmd
  bwipe!
endfunc

func SetChangeMarks(start, end)
  exe a:start .. 'mark ['
  exe a:end .. 'mark ]'
endfunc

" Verify the effects of autocmds on '[ and ']
func Test_change_mark_in_autocmds()
  edit! Xtest
  call feedkeys("ia\<CR>b\<CR>c\<CR>d\<C-g>u\<Esc>", 'xtn')

  call SetChangeMarks(2, 3)
  write
  call assert_equal([1, 4], [line("'["), line("']")])

  call SetChangeMarks(2, 3)
  au BufWritePre * call assert_equal([1, 4], [line("'["), line("']")])
  write
  au! BufWritePre

  if has('unix')
    write XtestFilter
    write >> XtestFilter

    call SetChangeMarks(2, 3)
    " Marks are set to the entire range of the write
    au FilterWritePre * call assert_equal([1, 4], [line("'["), line("']")])
    " '[ is adjusted to just before the line that will receive the filtered
    " data
    au FilterReadPre * call assert_equal([4, 4], [line("'["), line("']")])
    " The filtered data is read into the buffer, and the source lines are
    " still present, so the range is after the source lines
    au FilterReadPost * call assert_equal([5, 12], [line("'["), line("']")])
    %!cat XtestFilter
    " After the filtered data is read, the original lines are deleted
    call assert_equal([1, 8], [line("'["), line("']")])
    au! FilterWritePre,FilterReadPre,FilterReadPost
    undo

    call SetChangeMarks(1, 4)
    au FilterWritePre * call assert_equal([2, 3], [line("'["), line("']")])
    au FilterReadPre * call assert_equal([3, 3], [line("'["), line("']")])
    au FilterReadPost * call assert_equal([4, 11], [line("'["), line("']")])
    2,3!cat XtestFilter
    call assert_equal([2, 9], [line("'["), line("']")])
    au! FilterWritePre,FilterReadPre,FilterReadPost
    undo

    call delete('XtestFilter')
  endif

  call SetChangeMarks(1, 4)
  au FileWritePre * call assert_equal([2, 3], [line("'["), line("']")])
  2,3write Xtest2
  au! FileWritePre

  call SetChangeMarks(2, 3)
  au FileAppendPre * call assert_equal([1, 4], [line("'["), line("']")])
  write >> Xtest2
  au! FileAppendPre

  call SetChangeMarks(1, 4)
  au FileAppendPre * call assert_equal([2, 3], [line("'["), line("']")])
  2,3write >> Xtest2
  au! FileAppendPre

  call SetChangeMarks(1, 1)
  au FileReadPre * call assert_equal([3, 1], [line("'["), line("']")])
  au FileReadPost * call assert_equal([4, 11], [line("'["), line("']")])
  3read Xtest2
  au! FileReadPre,FileReadPost
  undo

  call SetChangeMarks(4, 4)
  " When the line is 0, it's adjusted to 1
  au FileReadPre * call assert_equal([1, 4], [line("'["), line("']")])
  au FileReadPost * call assert_equal([1, 8], [line("'["), line("']")])
  0read Xtest2
  au! FileReadPre,FileReadPost
  undo

  call SetChangeMarks(4, 4)
  " When the line is 0, it's adjusted to 1
  au FileReadPre * call assert_equal([1, 4], [line("'["), line("']")])
  au FileReadPost * call assert_equal([2, 9], [line("'["), line("']")])
  1read Xtest2
  au! FileReadPre,FileReadPost
  undo

  bwipe!
  call delete('Xtest')
  call delete('Xtest2')
endfunc

func Test_Filter_noshelltemp()
  CheckExecutable cat

  enew!
  call setline(1, ['a', 'b', 'c', 'd'])

  let shelltemp = &shelltemp
  set shelltemp

  let g:filter_au = 0
  au FilterWritePre * let g:filter_au += 1
  au FilterReadPre * let g:filter_au += 1
  au FilterReadPost * let g:filter_au += 1
  %!cat
  call assert_equal(3, g:filter_au)

  if has('filterpipe')
    set noshelltemp

    let g:filter_au = 0
    au FilterWritePre * let g:filter_au += 1
    au FilterReadPre * let g:filter_au += 1
    au FilterReadPost * let g:filter_au += 1
    %!cat
    call assert_equal(0, g:filter_au)
  endif

  au! FilterWritePre,FilterReadPre,FilterReadPost
  let &shelltemp = shelltemp
  bwipe!
endfunc

func Test_TextYankPost()
  enew!
  call setline(1, ['foo'])

  let g:event = []
  au TextYankPost * let g:event = copy(v:event)

  call assert_equal({}, v:event)
  call assert_fails('let v:event = {}', 'E46:')
  call assert_fails('let v:event.mykey = 0', 'E742:')

  norm "ayiw
  call assert_equal(
        \ #{regcontents: ['foo'], regname: 'a', operator: 'y',
        \   regtype: 'v', visual: v:false, inclusive: v:true},
        \ g:event)
  norm y_
  call assert_equal(
        \ #{regcontents: ['foo'], regname: '',  operator: 'y', regtype: 'V',
        \   visual: v:false, inclusive: v:false},
        \ g:event)
  norm Vy
  call assert_equal(
        \ #{regcontents: ['foo'], regname: '',  operator: 'y', regtype: 'V',
        \   visual: v:true, inclusive: v:true},
        \ g:event)
  call feedkeys("\<C-V>y", 'x')
  call assert_equal(
        \ #{regcontents: ['f'], regname: '',  operator: 'y', regtype: "\x161",
        \   visual: v:true, inclusive: v:true},
        \ g:event)
  norm "xciwbar
  call assert_equal(
        \ #{regcontents: ['foo'], regname: 'x', operator: 'c', regtype: 'v',
        \   visual: v:false, inclusive: v:true},
        \ g:event)
  norm "bdiw
  call assert_equal(
        \ #{regcontents: ['bar'], regname: 'b', operator: 'd', regtype: 'v',
        \   visual: v:false, inclusive: v:true},
        \ g:event)

  call setline(1, 'foobar')
  " exclusive motion
  norm $"ay0
  call assert_equal(
        \ #{regcontents: ['fooba'], regname: 'a', operator: 'y', regtype: 'v',
        \   visual: v:false, inclusive: v:false},
        \ g:event)
  " inclusive motion
  norm 0"ay$
  call assert_equal(
        \ #{regcontents: ['foobar'], regname: 'a', operator: 'y', regtype: 'v',
        \   visual: v:false, inclusive: v:true},
        \ g:event)

  call assert_equal({}, v:event)

  if has('clipboard_working') && !has('gui_running')
    " Test that when the visual selection is automatically copied to clipboard
    " register a TextYankPost is emitted
    call setline(1, ['foobar'])

    let @* = ''
    set clipboard=autoselect
    exe "norm! ggviw\<Esc>"
    call assert_equal(
          \ #{regcontents: ['foobar'], regname: '*', operator: 'y',
          \   regtype: 'v', visual: v:true, inclusive: v:false},
          \ g:event)

    let @+ = ''
    set clipboard=autoselectplus
    exe "norm! ggviw\<Esc>"
    call assert_equal(
          \ #{regcontents: ['foobar'], regname: '+', operator: 'y',
          \   regtype: 'v', visual: v:true, inclusive: v:false},
          \ g:event)

    set clipboard&vim
  endif

  au! TextYankPost
  unlet g:event
  bwipe!
endfunc

func Test_autocommand_all_events()
  call assert_fails('au * * bwipe', 'E1155:')
  call assert_fails('au * x bwipe', 'E1155:')
  call assert_fails('au! * x bwipe', 'E1155:')
endfunc

func Test_autocmd_user()
  au User MyEvent let s:res = [expand("<afile>"), expand("<amatch>")]
  doautocmd User MyEvent
  call assert_equal(['MyEvent', 'MyEvent'], s:res)
  au! User
  unlet s:res
endfunc

func Test_autocmd_user_clear_group()
  CheckRunVimInTerminal

  let lines =<< trim END
    autocmd! User
    for i in range(1, 999)
      exe 'autocmd User ' .. 'Foo' .. i .. ' bar'
    endfor
    au CmdlineLeave : call timer_start(0, {-> execute('autocmd! User')})
  END
  call writefile(lines, 'XautoUser', 'D')
  let buf = RunVimInTerminal('-S XautoUser', {'rows': 10})

  " this was using freed memory
  call term_sendkeys(buf, ":autocmd User\<CR>")
  call TermWait(buf, 50)
  call term_sendkeys(buf, "G")

  call StopVimInTerminal(buf)
endfunc

func Test_autocmd_CmdlineLeave_unlet()
  CheckRunVimInTerminal

  let lines =<< trim END
      for i in range(1, 999)
        exe 'let g:var' .. i '=' i
      endfor
      au CmdlineLeave : call timer_start(0, {-> execute('unlet g:var990')})
  END
  call writefile(lines, 'XleaveUnlet', 'D')
  let buf = RunVimInTerminal('-S XleaveUnlet', {'rows': 10})

  " this was using freed memory
  call term_sendkeys(buf, ":let g:\<CR>")
  call TermWait(buf, 50)
  call term_sendkeys(buf, "G")
  call TermWait(buf, 50)
  call term_sendkeys(buf, "\<CR>")  " for the hit-enter prompt

  call StopVimInTerminal(buf)
endfunc

function s:Before_test_dirchanged()
  augroup test_dirchanged
    autocmd!
  augroup END
  let s:li = []
  let s:dir_this = getcwd()
  let s:dir_foo = s:dir_this . '/Xfoo'
  call mkdir(s:dir_foo)
  let s:dir_bar = s:dir_this . '/Xbar'
  call mkdir(s:dir_bar)
endfunc

function s:After_test_dirchanged()
  call chdir(s:dir_this)
  call delete(s:dir_foo, 'd')
  call delete(s:dir_bar, 'd')
  augroup test_dirchanged
    autocmd!
  augroup END
endfunc

function Test_dirchanged_global()
  call s:Before_test_dirchanged()
  autocmd test_dirchanged DirChangedPre global call add(s:li, expand("<amatch>") .. " pre cd " .. v:event.directory)
  autocmd test_dirchanged DirChanged global call add(s:li, "cd:")
  autocmd test_dirchanged DirChanged global call add(s:li, expand("<afile>"))
  call chdir(s:dir_foo)
  let expected = ["global pre cd " .. s:dir_foo, "cd:", s:dir_foo]
  call assert_equal(expected, s:li)
  call chdir(s:dir_foo)
  call assert_equal(expected, s:li)
  exe 'lcd ' .. fnameescape(s:dir_bar)
  call assert_equal(expected, s:li)

  exe 'cd ' .. s:dir_foo
  exe 'cd ' .. s:dir_bar
  autocmd! test_dirchanged DirChanged global let g:result = expand("<afile>")
  cd -
  call assert_equal(s:dir_foo, substitute(g:result, '\\', '/', 'g'))

  call s:After_test_dirchanged()
endfunc

function Test_dirchanged_local()
  call s:Before_test_dirchanged()
  autocmd test_dirchanged DirChanged window call add(s:li, "lcd:")
  autocmd test_dirchanged DirChanged window call add(s:li, expand("<afile>"))
  call chdir(s:dir_foo)
  call assert_equal([], s:li)
  exe 'lcd ' .. fnameescape(s:dir_bar)
  call assert_equal(["lcd:", s:dir_bar], s:li)
  exe 'lcd ' .. fnameescape(s:dir_bar)
  call assert_equal(["lcd:", s:dir_bar], s:li)
  call s:After_test_dirchanged()
endfunc

function Test_dirchanged_auto()
  CheckOption autochdir
  call s:Before_test_dirchanged()
  call test_autochdir()
  autocmd test_dirchanged DirChangedPre auto call add(s:li, "pre cd " .. v:event.directory)
  autocmd test_dirchanged DirChanged auto call add(s:li, "auto:")
  autocmd test_dirchanged DirChanged auto call add(s:li, expand("<afile>"))
  set acd
  cd ..
  call assert_equal([], s:li)
  exe 'edit ' . s:dir_foo . '/Xautofile'
  call assert_equal(s:dir_foo, getcwd())
  let expected = ["pre cd " .. s:dir_foo, "auto:", s:dir_foo]
  call assert_equal(expected, s:li)
  set noacd
  bwipe!
  call s:After_test_dirchanged()
endfunc

" Test TextChangedI and TextChangedP
func Test_ChangedP()
  new
  call setline(1, ['foo', 'bar', 'foobar'])
  call test_override("char_avail", 1)
  set complete=. completeopt=menuone

  func! TextChangedAutocmd(char)
    let g:autocmd .= a:char
  endfunc

  " TextChanged will not be triggered, only check that it isn't.
  au! TextChanged <buffer> :call TextChangedAutocmd('N')
  au! TextChangedI <buffer> :call TextChangedAutocmd('I')
  au! TextChangedP <buffer> :call TextChangedAutocmd('P')

  call cursor(3, 1)
  let g:autocmd = ''
  call feedkeys("o\<esc>", 'tnix')
  call assert_equal('I', g:autocmd)

  let g:autocmd = ''
  call feedkeys("Sf", 'tnix')
  call assert_equal('II', g:autocmd)

  let g:autocmd = ''
  call feedkeys("Sf\<C-N>", 'tnix')
  call assert_equal('IIP', g:autocmd)

  let g:autocmd = ''
  call feedkeys("Sf\<C-N>\<C-N>", 'tnix')
  call assert_equal('IIPP', g:autocmd)

  let g:autocmd = ''
  call feedkeys("Sf\<C-N>\<C-N>\<C-N>", 'tnix')
  call assert_equal('IIPPP', g:autocmd)

  let g:autocmd = ''
  call feedkeys("Sf\<C-N>\<C-N>\<C-N>\<C-N>", 'tnix')
  call assert_equal('IIPPPP', g:autocmd)

  call assert_equal(['foo', 'bar', 'foobar', 'foo'], getline(1, '$'))
  " TODO: how should it handle completeopt=noinsert,noselect?

  " CleanUp
  call test_override("char_avail", 0)
  au! TextChanged
  au! TextChangedI
  au! TextChangedP
  delfu TextChangedAutocmd
  unlet! g:autocmd
  set complete&vim completeopt&vim

  bw!
endfunc

let g:setline_handled = v:false
func SetLineOne()
  if !g:setline_handled
    call setline(1, "(x)")
    let g:setline_handled = v:true
  endif
endfunc

func Test_TextChangedI_with_setline()
  new
  call test_override('char_avail', 1)
  autocmd TextChangedI <buffer> call SetLineOne()
  call feedkeys("i(\<CR>\<Esc>", 'tx')
  call assert_equal('(', getline(1))
  call assert_equal('x)', getline(2))
  undo
  call assert_equal('', getline(1))
  call assert_equal('', getline(2))

  call test_override('char_avail', 0)
  bwipe!
endfunc

func Test_TextChanged_with_norm()
  " For unknown reason this fails on MS-Windows
  CheckNotMSWindows
  CheckFeature terminal
  let buf = term_start([GetVimProg(), '--clean', '-c', 'set noswapfile'], {'term_rows': 3})
  call assert_equal('running', term_getstatus(buf))
  call term_sendkeys(buf, ":let g:a=0\<cr>")
  call term_wait(buf, 50)
  call term_sendkeys(buf, ":au! TextChanged * :let g:a+=1\<cr>")
  call term_wait(buf, 50)
  call term_sendkeys(buf, ":norm! ia\<cr>")
  call term_wait(buf, 50)
  call term_sendkeys(buf, ":echo g:a\<cr>")
  call term_wait(buf, 50)
  call WaitForAssert({-> assert_match('^1.*$', term_getline(buf, 3))})
  bwipe!
endfunc

func Test_Changed_FirstTime()
  CheckFeature terminal
  CheckNotGui
  " Starting a terminal to run Vim is always considered flaky.
  let g:test_is_flaky = 1

  " Prepare file for TextChanged event.
  call writefile([''], 'Xchanged.txt', 'D')
  let buf = term_start([GetVimProg(), '--clean', '-c', 'set noswapfile'], {'term_rows': 3})
  call assert_equal('running', term_getstatus(buf))
  " Wait for the ruler (in the status line) to be shown.
  " In ConPTY, there is additional character which is drawn up to the width of
  " the screen.
  if has('conpty')
    call WaitForAssert({-> assert_match('\<All.*$', term_getline(buf, 3))})
  else
    call WaitForAssert({-> assert_match('\<All$', term_getline(buf, 3))})
  endif
  " It's only adding autocmd, so that no event occurs.
  call term_sendkeys(buf, ":au! TextChanged <buffer> call writefile(['No'], 'Xchanged.txt')\<cr>")
  call term_sendkeys(buf, "\<C-\\>\<C-N>:qa!\<cr>")
  call WaitForAssert({-> assert_equal('finished', term_getstatus(buf))})
  call assert_equal([''], readfile('Xchanged.txt'))

  " clean up
  bwipe!
endfunc

func Test_autocmd_nested()
  let g:did_nested = 0
  augroup Testing
    au WinNew * edit somefile
    au BufNew * let g:did_nested = 1
  augroup END
  split
  call assert_equal(0, g:did_nested)
  close
  bwipe! somefile

  " old nested argument still works
  augroup Testing
    au!
    au WinNew * nested edit somefile
    au BufNew * let g:did_nested = 1
  augroup END
  split
  call assert_equal(1, g:did_nested)
  close
  bwipe! somefile

  " New ++nested argument works
  augroup Testing
    au!
    au WinNew * ++nested edit somefile
    au BufNew * let g:did_nested = 1
  augroup END
  split
  call assert_equal(1, g:did_nested)
  close
  bwipe! somefile

  " nested without ++ does not work in Vim9 script
  call assert_fails('vim9cmd au WinNew * nested echo fails', 'E1078:')

  augroup Testing
    au!
  augroup END

  call assert_fails('au WinNew * ++nested ++nested echo bad', 'E983:')
  call assert_fails('au WinNew * nested nested echo bad', 'E983:')
endfunc

func Test_autocmd_nested_cursor_invalid()
  set laststatus=0
  copen
  cclose
  call setline(1, ['foo', 'bar', 'baz'])
  3
  augroup nested_inv
    autocmd User foo ++nested copen
    autocmd BufAdd * let &laststatus = 2 - &laststatus
  augroup END
  doautocmd User foo

  augroup nested_inv
    au!
  augroup END
  set laststatus&
  cclose
  bwipe!
endfunc

func Test_autocmd_nested_keeps_cursor_pos()
  enew
  call setline(1, 'foo')
  autocmd User foo ++nested normal! $a
  autocmd InsertLeave * :
  doautocmd User foo
  call assert_equal([0, 1, 3, 0], getpos('.'))

  bwipe!
endfunc

func Test_autocmd_nested_switch_window()
  " run this in a separate Vim so that SafeState works
  CheckRunVimInTerminal

  let lines =<< trim END
      vim9script
      ['()']->writefile('Xautofile')
      autocmd VimEnter * ++nested edit Xautofile | split
      autocmd BufReadPost * autocmd SafeState * ++once foldclosed('.')
      autocmd WinEnter * matchadd('ErrorMsg', 'pat')
  END
  call writefile(lines, 'Xautoscript', 'D')
  let buf = RunVimInTerminal('-S Xautoscript', {'rows': 10})
  call VerifyScreenDump(buf, 'Test_autocmd_nested_switch', {})

  call StopVimInTerminal(buf)
  call delete('Xautofile')
endfunc

func Test_autocmd_once()
  " Without ++once WinNew triggers twice
  let g:did_split = 0
  augroup Testing
    au WinNew * let g:did_split += 1
  augroup END
  split
  split
  call assert_equal(2, g:did_split)
  call assert_true(exists('#WinNew'))
  close
  close

  " With ++once WinNew triggers once
  let g:did_split = 0
  augroup Testing
    au!
    au WinNew * ++once let g:did_split += 1
  augroup END
  split
  split
  call assert_equal(1, g:did_split)
  call assert_false(exists('#WinNew'))
  close
  close

  call assert_fails('au WinNew * ++once ++once echo bad', 'E983:')
endfunc

func Test_autocmd_bufreadpre()
  new
  let b:bufreadpre = 1
  call append(0, range(1000))
  w! XAutocmdBufReadPre.txt
  autocmd BufReadPre <buffer> :let b:bufreadpre += 1
  norm! 500gg
  sp
  norm! 1000gg
  wincmd p
  let g:wsv1 = winsaveview()
  wincmd p
  let g:wsv2 = winsaveview()
  " triggers BufReadPre, should not move the cursor in either window
  " The topline may change one line in a large window.
  edit
  call assert_inrange(g:wsv2.topline - 1, g:wsv2.topline + 1, winsaveview().topline)
  call assert_equal(g:wsv2.lnum, winsaveview().lnum)
  call assert_equal(2, b:bufreadpre)
  wincmd p
  call assert_equal(g:wsv1.topline, winsaveview().topline)
  call assert_equal(g:wsv1.lnum, winsaveview().lnum)
  call assert_equal(2, b:bufreadpre)
  " Now set the cursor position in an BufReadPre autocommand
  " (even though the position will be invalid, this should make Vim reset the
  " cursor position in the other window.
  wincmd p
  set cpo+=g
  " won't do anything, but try to set the cursor on an invalid lnum
  autocmd BufReadPre <buffer> :norm! 70gg
  " triggers BufReadPre, should not move the cursor in either window
  e
  call assert_equal(1, winsaveview().topline)
  call assert_equal(1, winsaveview().lnum)
  call assert_equal(3, b:bufreadpre)
  wincmd p
  call assert_equal(g:wsv1.topline, winsaveview().topline)
  call assert_equal(g:wsv1.lnum, winsaveview().lnum)
  call assert_equal(3, b:bufreadpre)
  close
  close
  call delete('XAutocmdBufReadPre.txt')
  set cpo-=g
endfunc

" FileChangedShell tested in test_filechanged.vim

" Tests for the following autocommands:
" - FileWritePre	writing a compressed file
" - FileReadPost	reading a compressed file
" - BufNewFile		reading a file template
" - BufReadPre		decompressing the file to be read
" - FilterReadPre	substituting characters in the temp file
" - FilterReadPost	substituting characters after filtering
" - FileReadPre		set options for decompression
" - FileReadPost	decompress the file
func Test_ReadWrite_Autocmds()
  " Run this test only on Unix-like systems and if gzip is available
  CheckUnix
  CheckExecutable gzip

  " Make $GZIP empty, "-v" would cause trouble.
  let $GZIP = ""

  " Use a FileChangedShell autocommand to avoid a prompt for 'Xtestfile.gz'
  " being modified outside of Vim (noticed on Solaris).
  au FileChangedShell * echo 'caught FileChangedShell'

  " Test for the FileReadPost, FileWritePre and FileWritePost autocmds
  augroup Test1
    au!
    au FileWritePre    *.gz   '[,']!gzip
    au FileWritePost   *.gz   undo
    au FileReadPost    *.gz   '[,']!gzip -d
  augroup END

  new
  set bin
  call append(0, [
	      \ 'line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ])
  1,9write! Xtestfile.gz
  enew! | close

  new
  " Read and decompress the testfile
  0read Xtestfile.gz
  call assert_equal([
	      \ 'line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ], getline(1, 9))
  enew! | close

  augroup Test1
    au!
  augroup END

  " Test for the FileAppendPre and FileAppendPost autocmds
  augroup Test2
    au!
    au BufNewFile      *.c    read Xtest.c
    au FileAppendPre   *.out  '[,']s/new/NEW/
    au FileAppendPost  *.out  !cat Xtest.c >> test.out
  augroup END

  call writefile(['/*', ' * Here is a new .c file', ' */'], 'Xtest.c', 'D')
  new foo.c			" should load Xtest.c
  call assert_equal(['/*', ' * Here is a new .c file', ' */'], getline(2, 4))
  w! >> test.out		" append it to the output file

  let contents = readfile('test.out')
  call assert_equal(' * Here is a NEW .c file', contents[2])
  call assert_equal(' * Here is a new .c file', contents[5])

  call delete('test.out')
  enew! | close
  augroup Test2
    au!
  augroup END

  " Test for the BufReadPre and BufReadPost autocmds
  augroup Test3
    au!
    " setup autocommands to decompress before reading and re-compress
    " afterwards
    au BufReadPre  *.gz  exe '!gzip -d ' . shellescape(expand("<afile>"))
    au BufReadPre  *.gz  call rename(expand("<afile>:r"), expand("<afile>"))
    au BufReadPost *.gz  call rename(expand("<afile>"), expand("<afile>:r"))
    au BufReadPost *.gz  exe '!gzip ' . shellescape(expand("<afile>:r"))
  augroup END

  e! Xtestfile.gz		" Edit compressed file
  call assert_equal([
	      \ 'line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ], getline(1, 9))

  w! >> test.out		" Append it to the output file

  augroup Test3
    au!
  augroup END

  " Test for the FilterReadPre and FilterReadPost autocmds.
  set shelltemp			" need temp files here
  augroup Test4
    au!
    au FilterReadPre   *.out  call rename(expand("<afile>"), expand("<afile>") . ".t")
    au FilterReadPre   *.out  exe 'silent !sed s/e/E/ ' . shellescape(expand("<afile>")) . ".t >" . shellescape(expand("<afile>"))
    au FilterReadPre   *.out  exe 'silent !rm ' . shellescape(expand("<afile>")) . '.t'
    au FilterReadPost  *.out  '[,']s/x/X/g
  augroup END

  e! test.out			" Edit the output file
  1,$!cat
  call assert_equal([
	      \ 'linE 2	AbcdefghijklmnopqrstuvwXyz',
	      \ 'linE 3	XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
	      \ 'linE 4	AbcdefghijklmnopqrstuvwXyz',
	      \ 'linE 5	XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
	      \ 'linE 6	AbcdefghijklmnopqrstuvwXyz',
	      \ 'linE 7	XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
	      \ 'linE 8	AbcdefghijklmnopqrstuvwXyz',
	      \ 'linE 9	XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
	      \ 'linE 10 AbcdefghijklmnopqrstuvwXyz'
	      \ ], getline(1, 9))
  call assert_equal([
	      \ 'line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ], readfile('test.out'))

  augroup Test4
    au!
  augroup END
  set shelltemp&vim

  " Test for the FileReadPre and FileReadPost autocmds.
  augroup Test5
    au!
    au FileReadPre *.gz exe 'silent !gzip -d ' . shellescape(expand("<afile>"))
    au FileReadPre *.gz call rename(expand("<afile>:r"), expand("<afile>"))
    au FileReadPost *.gz '[,']s/l/L/
  augroup END

  new
  0r Xtestfile.gz		" Read compressed file
  call assert_equal([
	      \ 'Line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'Line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'Line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'Line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'Line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'Line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'Line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'Line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'Line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ], getline(1, 9))
  call assert_equal([
	      \ 'line 2	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 3	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 4	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 5	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 6	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 7	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 8	Abcdefghijklmnopqrstuvwxyz',
	      \ 'line 9	xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
	      \ 'line 10 Abcdefghijklmnopqrstuvwxyz'
	      \ ], readfile('Xtestfile.gz'))

  augroup Test5
    au!
  augroup END

  au! FileChangedShell
  call delete('Xtestfile.gz')
  call delete('test.out')
endfunc

func Test_throw_in_BufWritePre()
  new
  call setline(1, ['one', 'two', 'three'])
  call assert_false(filereadable('Xthefile'))
  augroup throwing
    au BufWritePre X* throw 'do not write'
  augroup END
  try
    w Xthefile
  catch
    let caught = 1
  endtry
  call assert_equal(1, caught)
  call assert_false(filereadable('Xthefile'))

  bwipe!
  au! throwing
endfunc

func Test_autocmd_in_try_block()
  call mkdir('Xintrydir', 'R')
  au BufEnter * let g:fname = expand('%')
  try
    edit Xintrydir/
  endtry
  call assert_match('Xintrydir', g:fname)

  unlet g:fname
  au! BufEnter
endfunc

func Test_autocmd_SafeState()
  CheckRunVimInTerminal

  let lines =<< trim END
	let g:safe = 0
	let g:again = ''
	au SafeState * let g:safe += 1
	au SafeStateAgain * let g:again ..= 'x'
	func CallTimer()
	  call timer_start(10, {id -> execute('let g:again ..= "t"')})
	endfunc
  END
  call writefile(lines, 'XSafeState', 'D')
  let buf = RunVimInTerminal('-S XSafeState', #{rows: 6})

  " Sometimes we loop to handle a K_IGNORE, SafeState may be triggered once or
  " more often.
  call term_sendkeys(buf, ":echo g:safe\<CR>")
  call WaitForAssert({-> assert_match('^\d ', term_getline(buf, 6))}, 1000)

  " SafeStateAgain should be invoked at least three times
  call term_sendkeys(buf, ":echo g:again\<CR>")
  call WaitForAssert({-> assert_match('^xxx', term_getline(buf, 6))}, 1000)

  call term_sendkeys(buf, ":let g:again = ''\<CR>:call CallTimer()\<CR>")
  call TermWait(buf, 50)
  call term_sendkeys(buf, ":\<CR>")
  call TermWait(buf, 50)
  call term_sendkeys(buf, ":echo g:again\<CR>")
  call WaitForAssert({-> assert_match('xtx', term_getline(buf, 6))}, 1000)

  call StopVimInTerminal(buf)
endfunc

func Test_autocmd_CmdWinEnter()
  CheckRunVimInTerminal

  let lines =<< trim END
    augroup vimHints | au! | augroup END
    let b:dummy_var = 'This is a dummy'
    autocmd CmdWinEnter * quit
    let winnr = winnr('$')
  END
  let filename = 'XCmdWinEnter'
  call writefile(lines, filename)
  let buf = RunVimInTerminal('-S '.filename, #{rows: 6})

  call term_sendkeys(buf, "q:")
  call TermWait(buf)
  call term_sendkeys(buf, ":echo b:dummy_var\<cr>")
  call WaitForAssert({-> assert_match('^This is a dummy', term_getline(buf, 6))}, 2000)
  call term_sendkeys(buf, ":echo &buftype\<cr>")
  call WaitForAssert({-> assert_notmatch('^nofile', term_getline(buf, 6))}, 1000)
  call term_sendkeys(buf, ":echo winnr\<cr>")
  call WaitForAssert({-> assert_match('^1', term_getline(buf, 6))}, 1000)

  " clean up
  call StopVimInTerminal(buf)
  call delete(filename)
endfunc

func Test_autocmd_was_using_freed_memory()
  CheckFeature quickfix

  pedit xx
  n x
  augroup winenter
    au WinEnter * if winnr('$') > 2 | quit | endif
  augroup END
  split

  augroup winenter
    au! WinEnter
  augroup END

  bwipe xx
  bwipe x
  pclose
endfunc

func Test_BufWrite_lockmarks()
  let g:test_is_flaky = 1
  edit! Xtest
  call setline(1, ['a', 'b', 'c', 'd'])

  " :lockmarks preserves the marks
  call SetChangeMarks(2, 3)
  lockmarks write
  call assert_equal([2, 3], [line("'["), line("']")])

  " *WritePre autocmds get the correct line range, but lockmarks preserves the
  " original values for the user
  augroup lockmarks
    au!
    au BufWritePre,FilterWritePre * call assert_equal([1, 4], [line("'["), line("']")])
    au FileWritePre * call assert_equal([3, 4], [line("'["), line("']")])
  augroup END

  lockmarks write
  call assert_equal([2, 3], [line("'["), line("']")])

  if executable('cat')
    lockmarks %!cat
    call assert_equal([2, 3], [line("'["), line("']")])
  endif

  lockmarks 3,4write Xtest2
  call assert_equal([2, 3], [line("'["), line("']")])

  au! lockmarks
  augroup! lockmarks
  call delete('Xtest')
  call delete('Xtest2')
endfunc

func Test_FileType_spell()
  if !isdirectory('/tmp')
    throw "Skipped: requires /tmp directory"
  endif

  " this was crashing with an invalid free()
  setglobal spellfile=/tmp/en.utf-8.add
  augroup crash
    autocmd!
    autocmd BufNewFile,BufReadPost crashfile setf somefiletype
    autocmd BufNewFile,BufReadPost crashfile set ft=anotherfiletype
    autocmd FileType anotherfiletype setlocal spell
  augroup END
  func! NoCrash() abort
    edit /tmp/crashfile
  endfunc
  call NoCrash()

  au! crash
  setglobal spellfile=
endfunc

" this was wiping out the current buffer and using freed memory
func Test_SpellFileMissing_bwipe()
  next 0
  au SpellFileMissing 0 bwipe
  call assert_fails('set spell spelllang=0', 'E937:')

  au! SpellFileMissing
  set nospell spelllang=en
  bwipe
endfunc

" Test closing a window or editing another buffer from a FileChangedRO handler
" in a readonly buffer
func Test_FileChangedRO_winclose()
  call test_override('ui_delay', 10)

  augroup FileChangedROTest
    au!
    autocmd FileChangedRO * quit
  augroup END
  new
  set readonly
  call assert_fails('normal i', 'E788:')
  close
  augroup! FileChangedROTest

  augroup FileChangedROTest
    au!
    autocmd FileChangedRO * edit Xrofile
  augroup END
  new
  set readonly
  call assert_fails('normal i', 'E788:')
  close
  augroup! FileChangedROTest
  call test_override('ALL', 0)
endfunc

func LogACmd()
  call add(g:logged, line('$'))
endfunc

func Test_TermChanged()
  CheckNotGui

  enew!
  tabnew
  call setline(1, ['a', 'b', 'c', 'd'])
  $
  au TermChanged * call LogACmd()
  let g:logged = []
  let term_save = &term
  set term=xterm
  call assert_equal([1, 4], g:logged)

  au! TermChanged
  let &term = term_save
  bwipe!
endfunc

" Test for FileReadCmd autocmd
func Test_autocmd_FileReadCmd()
  func ReadFileCmd()
    call append(line('$'), "v:cmdarg = " .. v:cmdarg)
  endfunc
  augroup FileReadCmdTest
    au!
    au FileReadCmd Xtest call ReadFileCmd()
  augroup END

  new
  read ++bin Xtest
  read ++nobin Xtest
  read ++edit Xtest
  read ++bad=keep Xtest
  read ++bad=drop Xtest
  read ++bad=- Xtest
  read ++ff=unix Xtest
  read ++ff=dos Xtest
  read ++ff=mac Xtest
  read ++enc=utf-8 Xtest

  call assert_equal(['',
        \ 'v:cmdarg =  ++bin',
        \ 'v:cmdarg =  ++nobin',
        \ 'v:cmdarg =  ++edit',
        \ 'v:cmdarg =  ++bad=keep',
        \ 'v:cmdarg =  ++bad=drop',
        \ 'v:cmdarg =  ++bad=-',
        \ 'v:cmdarg =  ++ff=unix',
        \ 'v:cmdarg =  ++ff=dos',
        \ 'v:cmdarg =  ++ff=mac',
        \ 'v:cmdarg =  ++enc=utf-8'], getline(1, '$'))

  bwipe!
  augroup FileReadCmdTest
    au!
  augroup END
  delfunc ReadFileCmd
endfunc

" Test for passing invalid arguments to autocmd
func Test_autocmd_invalid_args()
  " Additional character after * for event
  call assert_fails('autocmd *a Xinvfile set ff=unix', 'E215:')
  augroup Test
  augroup END
  " Invalid autocmd event
  call assert_fails('autocmd Bufabc Xinvfile set ft=vim', 'E216:')
  " Invalid autocmd event in a autocmd group
  call assert_fails('autocmd Test Bufabc Xinvfile set ft=vim', 'E216:')
  augroup! Test
  " Execute all autocmds
  call assert_fails('doautocmd * BufEnter', 'E217:')
  call assert_fails('augroup! x1a2b3', 'E367:')
  call assert_fails('autocmd BufNew <buffer=999> pwd', 'E680:')
  call assert_fails('autocmd BufNew \) set ff=unix', 'E55:')
endfunc

" Test for deep nesting of autocmds
func Test_autocmd_deep_nesting()
  autocmd BufEnter Xdeepfile doautocmd BufEnter Xdeepfile
  call assert_fails('doautocmd BufEnter Xdeepfile', 'E218:')
  autocmd! BufEnter Xdeepfile
endfunc

" Tests for SigUSR1 autocmd event, which is only available on posix systems.
func Test_autocmd_sigusr1()
  CheckUnix
  " FIXME: should this work on MacOS M1?
  CheckNotMacM1
  CheckExecutable /bin/kill

  let g:sigusr1_passed = 0
  au SigUSR1 * let g:sigusr1_passed = 1
  call system('/bin/kill -s usr1 ' . getpid())
  call WaitForAssert({-> assert_true(g:sigusr1_passed)})

  au! SigUSR1
  unlet g:sigusr1_passed
endfunc

" Test for BufReadPre autocmd deleting the file
func Test_BufReadPre_delfile()
  augroup TestAuCmd
    au!
    autocmd BufReadPre XbufreadPre call delete('XbufreadPre')
  augroup END
  call writefile([], 'XbufreadPre', 'D')
  call assert_fails('new XbufreadPre', 'E200:')
  call assert_equal('XbufreadPre', @%)
  call assert_equal(1, &readonly)

  augroup TestAuCmd
    au!
  augroup END
  close!
endfunc

" Test for BufReadPre autocmd changing the current buffer
func Test_BufReadPre_changebuf()
  augroup TestAuCmd
    au!
    autocmd BufReadPre Xchangebuf edit Xsomeotherfile
  augroup END
  call writefile([], 'Xchangebuf', 'D')
  call assert_fails('new Xchangebuf', 'E201:')
  call assert_equal('Xsomeotherfile', @%)
  call assert_equal(1, &readonly)

  augroup TestAuCmd
    au!
  augroup END
  close!
endfunc

" Test for BufWipeouti autocmd changing the current buffer when reading a file
" in an empty buffer with 'f' flag in 'cpo'
func Test_BufDelete_changebuf()
  new
  augroup TestAuCmd
    au!
    autocmd BufWipeout * let bufnr = bufadd('somefile') | exe "b " .. bufnr
  augroup END
  let save_cpo = &cpo
  set cpo+=f
  call assert_fails('r Xchangebuf', ['E812:', 'E484:'])
  call assert_equal('somefile', @%)
  let &cpo = save_cpo
  augroup TestAuCmd
    au!
  augroup END
  close!
endfunc

" Test for the temporary internal window used to execute autocmds
func Test_autocmd_window()
  %bw!
  edit one.txt
  tabnew two.txt
  vnew three.txt
  tabnew four.txt
  tabprevious
  let g:blist = []
  augroup aucmd_win_test1
    au!
    au BufEnter * call add(g:blist, [expand('<afile>'),
          \ win_gettype(bufwinnr(expand('<afile>')))])
  augroup END

  doautoall BufEnter
  call assert_equal([
        \ ['one.txt', 'autocmd'],
        \ ['two.txt', ''],
        \ ['four.txt', 'autocmd'],
        \ ['three.txt', ''],
        \ ], g:blist)

  augroup aucmd_win_test1
    au!
  augroup END
  augroup! aucmd_win_test1
  %bw!
endfunc

" Test for trying to close the temporary window used for executing an autocmd
func Test_close_autocmd_window()
  %bw!
  edit one.txt
  tabnew two.txt
  augroup aucmd_win_test2
    au!
    au BufEnter * if expand('<afile>') == 'one.txt' | 1close | endif
  augroup END

  call assert_fails('doautoall BufEnter', 'E813:')

  augroup aucmd_win_test2
    au!
  augroup END
  augroup! aucmd_win_test2
  %bwipe!
endfunc

" Test for trying to close the tab that has the temporary window for exeucing
" an autocmd.
func Test_close_autocmd_tab()
  edit one.txt
  tabnew two.txt
   augroup aucmd_win_test
    au!
    au BufEnter * if expand('<afile>') == 'one.txt' | tabfirst | tabonly | endif
  augroup END

  call assert_fails('doautoall BufEnter', 'E813:')

  tabonly
  augroup aucmd_win_test
    au!
  augroup END
  augroup! aucmd_win_test
  %bwipe!
endfunc

func Test_Visual_doautoall_redraw()
  call setline(1, ['a', 'b'])
  new
  wincmd p
  call feedkeys("G\<C-V>", 'txn')
  autocmd User Explode ++once redraw
  doautoall User Explode
  %bwipe!
endfunc

" This was using freed memory.
func Test_BufNew_arglocal()
  arglocal
  au BufNew * arglocal
  call assert_fails('drop xx', 'E1156:')

  au! BufNew
endfunc

func Test_autocmd_closes_window()
  au BufNew,BufWinLeave * e %e
  file yyy
  au BufNew,BufWinLeave * ball
  n xxx

  %bwipe
  au! BufNew
  au! BufWinLeave
endfunc

func Test_autocmd_quit_psearch()
  sn aa bb
  augroup aucmd_win_test
    au!
    au BufEnter,BufLeave,BufNew,WinEnter,WinLeave,WinNew * if winnr('$') > 1 | q | endif
  augroup END
  ps /

  augroup aucmd_win_test
    au!
  augroup END
  new
  pclose
endfunc

" Fuzzer found some strange combination that caused a crash.
func Test_autocmd_normal_mess()
  " For unknown reason this hangs on MS-Windows
  CheckNotMSWindows

  augroup aucmd_normal_test
    au BufLeave,BufWinLeave,BufHidden,BufUnload,BufDelete,BufWipeout * norm 7q/qc
  augroup END
  call assert_fails('o4', 'E1159')
  silent! H
  call assert_fails('e xx', 'E1159')
  normal G

  augroup aucmd_normal_test
    au!
  augroup END
endfunc

func Test_autocmd_closing_cmdwin()
  " For unknown reason this hangs on MS-Windows
  CheckNotMSWindows

  au BufWinLeave * nested q
  call assert_fails("norm 7q?\n", 'E855:')

  au! BufWinLeave
  new
  only
endfunc

func Test_autocmd_vimgrep()
  augroup aucmd_vimgrep
    au QuickfixCmdPre,BufNew,BufReadCmd * sb
    au QuickfixCmdPre,BufNew,BufReadCmd * q9
  augroup END
  call assert_fails('lv ?a? foo', 'E926:')

  augroup aucmd_vimgrep
    au!
  augroup END
endfunc

func Test_autocmd_with_block()
  augroup block_testing
    au BufReadPost *.xml {
            setlocal matchpairs+=<:>
            /<start
          }
    au CursorHold * {
        autocmd BufReadPre * ++once echo 'one' | echo 'two'
        g:gotSafeState = 77
      }
  augroup END

  let expected = "\n--- Autocommands ---\nblock_testing  BufRead\n    *.xml     {^@            setlocal matchpairs+=<:>^@            /<start^@          }"
  call assert_equal(expected, execute('au BufReadPost *.xml'))

  doautocmd CursorHold
  call assert_equal(77, g:gotSafeState)
  unlet g:gotSafeState

  augroup block_testing
    au!
    autocmd CursorHold * {
      if true
        # comment
        && true

        && true
        g:done = 'yes'
      endif
      }
  augroup END
  doautocmd CursorHold
  call assert_equal('yes', g:done)

  unlet g:done
  augroup block_testing
    au!
  augroup END
endfunc

" Test TextChangedI and TextChanged
func Test_Changed_ChangedI()
  new
  call test_override("char_avail", 1)
  let [g:autocmd_i, g:autocmd_n] = ['','']

  func! TextChangedAutocmdI(char)
    let g:autocmd_{tolower(a:char)} = a:char .. b:changedtick
  endfunc

  augroup Test_TextChanged
    au!
    au TextChanged  <buffer> :call TextChangedAutocmdI('N')
    au TextChangedI <buffer> :call TextChangedAutocmdI('I')
  augroup END

  call feedkeys("ifoo\<esc>", 'tnix')
  " TODO: Test test does not seem to trigger TextChanged autocommand, this
  " requires running Vim in a terminal window.
  " call assert_equal('N3', g:autocmd_n)
  call assert_equal('I3', g:autocmd_i)

  call feedkeys("yyp", 'tnix')
  " TODO: Test test does not seem to trigger TextChanged autocommand.
  " call assert_equal('N4', g:autocmd_n)
  call assert_equal('I3', g:autocmd_i)

  " TextChangedI should only trigger if change was done in Insert mode
  let g:autocmd_i = ''
  call feedkeys("yypi\<esc>", 'tnix')
  call assert_equal('', g:autocmd_i)

  " TextChanged should only trigger if change was done in Normal mode
  let g:autocmd_n = ''
  call feedkeys("ibar\<esc>", 'tnix')
  call assert_equal('', g:autocmd_n)

  " If change is a mix of Normal and Insert modes, TextChangedI should trigger
  func s:validate_mixed_textchangedi(keys)
    call feedkeys("ifoo\<esc>", 'tnix')
    let g:autocmd_i = ''
    let g:autocmd_n = ''
    call feedkeys(a:keys, 'tnix')
    call assert_notequal('', g:autocmd_i)
    call assert_equal('', g:autocmd_n)
  endfunc

  call s:validate_mixed_textchangedi("o\<esc>")
  call s:validate_mixed_textchangedi("O\<esc>")
  call s:validate_mixed_textchangedi("ciw\<esc>")
  call s:validate_mixed_textchangedi("cc\<esc>")
  call s:validate_mixed_textchangedi("C\<esc>")
  call s:validate_mixed_textchangedi("s\<esc>")
  call s:validate_mixed_textchangedi("S\<esc>")


  " CleanUp
  call test_override("char_avail", 0)
  au! TextChanged  <buffer>
  au! TextChangedI <buffer>
  augroup! Test_TextChanged
  delfu TextChangedAutocmdI
  unlet! g:autocmd_i g:autocmd_n

  bw!
endfunc

func Test_closing_autocmd_window()
  let lines =<< trim END
      edit Xa.txt
      tabnew Xb.txt
      autocmd BufEnter Xa.txt unhide 1
      doautoall BufEnter
  END
  call v9.CheckScriptFailure(lines, 'E814:')
  au! BufEnter
  bwipe Xa.txt
  bwipe Xb.txt
endfunc

func Test_switch_window_in_autocmd_window()
  edit Xa.txt
  tabnew Xb.txt
  autocmd BufEnter Xa.txt wincmd w
  doautoall BufEnter
  au! BufEnter
  bwipe Xa.txt
  call assert_false(bufexists('Xa.txt'))
  bwipe Xb.txt
  call assert_false(bufexists('Xb.txt'))
endfunc

func Test_bufwipeout_changes_window()
  " This should not crash, but we don't have any expectations about what
  " happens, changing window in BufWipeout has unpredictable results.
  tabedit
  let g:window_id = win_getid()
  topleft new
  setlocal bufhidden=wipe
  autocmd BufWipeout <buffer> call win_gotoid(g:window_id)
  tabprevious
  +tabclose

  unlet g:window_id
  au! BufWipeout
  %bwipe!
endfunc

func Test_v_event_readonly()
  autocmd CompleteChanged * let v:event.width = 0
  call assert_fails("normal! i\<C-X>\<C-V>", 'E46:')
  au! CompleteChanged

  autocmd DirChangedPre * let v:event.directory = ''
  call assert_fails('cd .', 'E46:')
  au! DirChangedPre

  autocmd ModeChanged * let v:event.new_mode = ''
  call assert_fails('normal! cc', 'E46:')
  au! ModeChanged

  autocmd TextYankPost * let v:event.operator = ''
  call assert_fails('normal! yy', 'E46:')
  au! TextYankPost
endfunc

" Test for ModeChanged pattern
func Test_mode_changes()
  let g:index = 0
  let g:mode_seq = ['n', 'i', 'n', 'v', 'V', 'i', 'ix', 'i', 'ic', 'i', 'n', 'no', 'noV', 'n', 'V', 'v', 's', 'n']
  func! TestMode()
    call assert_equal(g:mode_seq[g:index], get(v:event, "old_mode"))
    call assert_equal(g:mode_seq[g:index + 1], get(v:event, "new_mode"))
    call assert_equal(mode(1), get(v:event, "new_mode"))
    let g:index += 1
  endfunc

  au ModeChanged * :call TestMode()
  let g:n_to_any = 0
  au ModeChanged n:* let g:n_to_any += 1
  call feedkeys("i\<esc>vVca\<CR>\<C-X>\<C-L>\<esc>ggdV\<MouseMove>G", 'tnix')

  let g:V_to_v = 0
  au ModeChanged V:v let g:V_to_v += 1
  call feedkeys("Vv\<C-G>\<esc>", 'tnix')
  call assert_equal(len(filter(g:mode_seq[1:], {idx, val -> val == 'n'})), g:n_to_any)
  call assert_equal(1, g:V_to_v)
  call assert_equal(len(g:mode_seq) - 1, g:index)

  let g:n_to_i = 0
  au ModeChanged n:i let g:n_to_i += 1
  let g:n_to_niI = 0
  au ModeChanged i:niI let g:n_to_niI += 1
  let g:niI_to_i = 0
  au ModeChanged niI:i let g:niI_to_i += 1
  let g:nany_to_i = 0
  au ModeChanged n*:i let g:nany_to_i += 1
  let g:i_to_n = 0
  au ModeChanged i:n let g:i_to_n += 1
  let g:nori_to_any = 0
  au ModeChanged [ni]:* let g:nori_to_any += 1
  let g:i_to_any = 0
  au ModeChanged i:* let g:i_to_any += 1
  let g:index = 0
  let g:mode_seq = ['n', 'i', 'niI', 'i', 'n']
  call feedkeys("a\<C-O>l\<esc>", 'tnix')
  call assert_equal(len(g:mode_seq) - 1, g:index)
  call assert_equal(1, g:n_to_i)
  call assert_equal(1, g:n_to_niI)
  call assert_equal(1, g:niI_to_i)
  call assert_equal(2, g:nany_to_i)
  call assert_equal(1, g:i_to_n)
  call assert_equal(2, g:i_to_any)
  call assert_equal(3, g:nori_to_any)

  if has('terminal')
    let g:mode_seq += ['c', 'n', 't', 'nt', 'c', 'nt', 'n']
    call feedkeys(":term\<CR>\<C-W>N:bd!\<CR>", 'tnix')
    call assert_equal(len(g:mode_seq) - 1, g:index)
    call assert_equal(1, g:n_to_i)
    call assert_equal(1, g:n_to_niI)
    call assert_equal(1, g:niI_to_i)
    call assert_equal(2, g:nany_to_i)
    call assert_equal(1, g:i_to_n)
    call assert_equal(2, g:i_to_any)
    call assert_equal(5, g:nori_to_any)
  endif

  let g:n_to_c = 0
  au ModeChanged n:c let g:n_to_c += 1
  let g:c_to_n = 0
  au ModeChanged c:n let g:c_to_n += 1
  let g:mode_seq += ['c', 'n', 'c', 'n']
  call feedkeys("q:\<C-C>\<Esc>", 'tnix')
  call assert_equal(len(g:mode_seq) - 1, g:index)
  call assert_equal(2, g:n_to_c)
  call assert_equal(2, g:c_to_n)

  let g:n_to_v = 0
  au ModeChanged n:v let g:n_to_v += 1
  let g:v_to_n = 0
  au ModeChanged v:n let g:v_to_n += 1
  let g:mode_seq += ['v', 'n']
  call feedkeys("v\<C-C>", 'tnix')
  call assert_equal(len(g:mode_seq) - 1, g:index)
  call assert_equal(1, g:n_to_v)
  call assert_equal(1, g:v_to_n)

  let g:mode_seq += ['c', 'cr', 'c', 'cr', 'n']
  call feedkeys(":\<Insert>\<Insert>\<Insert>\<CR>", 'tnix')
  call assert_equal(len(g:mode_seq) - 1, g:index)

  au! ModeChanged
  delfunc TestMode
  unlet! g:mode_seq
  unlet! g:index
  unlet! g:n_to_any
  unlet! g:V_to_v
  unlet! g:n_to_i
  unlet! g:n_to_niI
  unlet! g:niI_to_i
  unlet! g:nany_to_i
  unlet! g:i_to_n
  unlet! g:nori_to_any
  unlet! g:i_to_any
  unlet! g:n_to_c
  unlet! g:c_to_n
  unlet! g:n_to_v
  unlet! g:v_to_n
endfunc

func Test_recursive_ModeChanged()
  au! ModeChanged * norm 0u
  sil! norm 
  au! ModeChanged
endfunc

func Test_ModeChanged_starts_visual()
  " This was triggering ModeChanged before setting VIsual, causing a crash.
  au! ModeChanged * norm 0u
  sil! norm 

  au! ModeChanged
endfunc

func Test_noname_autocmd()
  augroup test_noname_autocmd_group
    autocmd!
    autocmd BufEnter * call add(s:li, ["BufEnter", expand("<afile>")])
    autocmd BufDelete * call add(s:li, ["BufDelete", expand("<afile>")])
    autocmd BufLeave * call add(s:li, ["BufLeave", expand("<afile>")])
    autocmd BufUnload * call add(s:li, ["BufUnload", expand("<afile>")])
    autocmd BufWipeout * call add(s:li, ["BufWipeout", expand("<afile>")])
  augroup END

  let s:li = []
  edit foo
  call assert_equal([['BufUnload', ''], ['BufDelete', ''], ['BufWipeout', ''], ['BufEnter', 'foo']], s:li)

  au! test_noname_autocmd_group
  augroup! test_noname_autocmd_group
endfunc

" Test for the autocmd_get() function
func Test_autocmd_get()
  augroup TestAutoCmdFns
    au!
    autocmd BufAdd *.vim echo "bufadd-vim"
    autocmd BufAdd *.py echo "bufadd-py"
    autocmd BufHidden *.vim echo "bufhidden"
  augroup END
  augroup TestAutoCmdFns2
    autocmd BufAdd *.vim echo "bufadd-vim-2"
    autocmd BufRead *.a1b2c3 echo "bufadd-vim-2"
  augroup END

  let l = autocmd_get()
  call assert_true(l->len() > 0)

  " Test for getting all the autocmds in a group
  let expected = [
        \ #{cmd: 'echo "bufadd-vim"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false, once: v:false,
        \  event: 'BufAdd'},
        \ #{cmd: 'echo "bufadd-py"', group: 'TestAutoCmdFns',
        \  pattern: '*.py', nested: v:false, once: v:false,
        \  event: 'BufAdd'},
        \ #{cmd: 'echo "bufhidden"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false,
        \  once: v:false, event: 'BufHidden'}]
  call assert_equal(expected, autocmd_get(#{group: 'TestAutoCmdFns'}))

  " Test for getting autocmds for all the patterns in a group
  call assert_equal(expected, autocmd_get(#{group: 'TestAutoCmdFns',
        \ event: '*'}))

  " Test for getting autocmds for an event in a group
  let expected = [
        \ #{cmd: 'echo "bufadd-vim"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false, once: v:false,
        \  event: 'BufAdd'},
        \ #{cmd: 'echo "bufadd-py"', group: 'TestAutoCmdFns',
        \  pattern: '*.py', nested: v:false, once: v:false,
        \  event: 'BufAdd'}]
  call assert_equal(expected, autocmd_get(#{group: 'TestAutoCmdFns',
        \ event: 'BufAdd'}))

  " Test for getting the autocmds for all the events in a group for particular
  " pattern
  call assert_equal([{'cmd': 'echo "bufadd-py"', 'group': 'TestAutoCmdFns',
        \ 'pattern': '*.py', 'nested': v:false, 'once': v:false,
        \ 'event': 'BufAdd'}],
        \ autocmd_get(#{group: 'TestAutoCmdFns', event: '*', pattern: '*.py'}))

  " Test for getting the autocmds for an events in a group for particular
  " pattern
  let l = autocmd_get(#{group: 'TestAutoCmdFns', event: 'BufAdd',
        \ pattern: '*.vim'})
  call assert_equal([
        \ #{cmd: 'echo "bufadd-vim"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false, once: v:false,
        \  event: 'BufAdd'}], l)

  " Test for getting the autocmds for a pattern in a group
  let l = autocmd_get(#{group: 'TestAutoCmdFns', pattern: '*.vim'})
  call assert_equal([
        \ #{cmd: 'echo "bufadd-vim"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false, once: v:false,
        \  event: 'BufAdd'},
        \ #{cmd: 'echo "bufhidden"', group: 'TestAutoCmdFns',
        \  pattern: '*.vim', nested: v:false,
        \  once: v:false, event: 'BufHidden'}], l)

  " Test for getting the autocmds for a pattern in all the groups
  let l = autocmd_get(#{pattern: '*.a1b2c3'})
  call assert_equal([{'cmd': 'echo "bufadd-vim-2"', 'group': 'TestAutoCmdFns2',
        \ 'pattern': '*.a1b2c3', 'nested': v:false, 'once': v:false,
        \ 'event': 'BufRead'}], l)

  " Test for getting autocmds for a pattern without any autocmds
  call assert_equal([], autocmd_get(#{group: 'TestAutoCmdFns',
        \ pattern: '*.abc'}))
  call assert_equal([], autocmd_get(#{group: 'TestAutoCmdFns',
        \ event: 'BufAdd', pattern: '*.abc'}))
  call assert_equal([], autocmd_get(#{group: 'TestAutoCmdFns',
        \ event: 'BufWipeout'}))
  call assert_fails("call autocmd_get(#{group: 'abc', event: 'BufAdd'})",
        \ 'E367:')
  let cmd = "echo autocmd_get(#{group: 'TestAutoCmdFns', event: 'abc'})"
  call assert_fails(cmd, 'E216:')
  call assert_fails("call autocmd_get(#{group: 'abc'})", 'E367:')
  call assert_fails("echo autocmd_get(#{event: 'abc'})", 'E216:')

  augroup TestAutoCmdFns
    au!
  augroup END
  call assert_equal([], autocmd_get(#{group: 'TestAutoCmdFns'}))

  " Test for nested and once autocmds
  augroup TestAutoCmdFns
    au!
    autocmd VimSuspend * ++nested echo "suspend"
    autocmd VimResume * ++once echo "resume"
  augroup END

  let expected = [
        \ {'cmd': 'echo "suspend"', 'group': 'TestAutoCmdFns', 'pattern': '*',
        \ 'nested': v:true, 'once': v:false, 'event': 'VimSuspend'},
        \ {'cmd': 'echo "resume"', 'group': 'TestAutoCmdFns', 'pattern': '*',
        \  'nested': v:false, 'once': v:true, 'event': 'VimResume'}]
  call assert_equal(expected, autocmd_get(#{group: 'TestAutoCmdFns'}))

  " Test for buffer-local autocmd
  augroup TestAutoCmdFns
    au!
    autocmd TextYankPost <buffer> echo "textyankpost"
  augroup END

  let expected = [
        \ {'cmd': 'echo "textyankpost"', 'group': 'TestAutoCmdFns',
        \  'pattern': '<buffer=' .. bufnr() .. '>', 'nested': v:false,
        \  'once': v:false, 'bufnr': bufnr(), 'event': 'TextYankPost'}]
  call assert_equal(expected, autocmd_get(#{group: 'TestAutoCmdFns'}))

  augroup TestAutoCmdFns
    au!
  augroup END
  augroup! TestAutoCmdFns
  augroup TestAutoCmdFns2
    au!
  augroup END
  augroup! TestAutoCmdFns2

  call assert_fails("echo autocmd_get(#{group: []})", 'E730:')
  call assert_fails("echo autocmd_get(#{event: {}})", 'E731:')
  call assert_fails("echo autocmd_get([])", 'E1206:')
endfunc

" Test for the autocmd_add() function
func Test_autocmd_add()
  " Define a single autocmd in a group
  call autocmd_add([#{group: 'TestAcSet', event: 'BufAdd', pattern: '*.sh',
        \ cmd: 'echo "bufadd"', once: v:true, nested: v:true}])
  call assert_equal([#{cmd: 'echo "bufadd"', group: 'TestAcSet',
        \ pattern: '*.sh', nested: v:true, once: v:true,
        \ event: 'BufAdd'}], autocmd_get(#{group: 'TestAcSet'}))

  " Define two autocmds in the same group
  call autocmd_delete([#{group: 'TestAcSet'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufAdd', pattern: '*.sh',
        \ cmd: 'echo "bufadd"'},
        \ #{group: 'TestAcSet', event: 'BufEnter', pattern: '*.sh',
        \   cmd: 'echo "bufenter"'}])
  call assert_equal([
        \ #{cmd: 'echo "bufadd"', group: 'TestAcSet', pattern: '*.sh',
        \   nested: v:false, once: v:false, event: 'BufAdd'},
        \ #{cmd: 'echo "bufenter"', group: 'TestAcSet', pattern: '*.sh',
        \   nested: v:false, once: v:false, event: 'BufEnter'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " Define a buffer-local autocmd
  call autocmd_delete([#{group: 'TestAcSet'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'CursorHold',
        \ bufnr: bufnr(), cmd: 'echo "cursorhold"'}])
  call assert_equal([
        \ #{cmd: 'echo "cursorhold"', group: 'TestAcSet',
        \   pattern: '<buffer=' .. bufnr() .. '>', nested: v:false,
        \   once: v:false, bufnr: bufnr(), event: 'CursorHold'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " Use an invalid buffer number
  call autocmd_delete([#{group: 'TestAcSet'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufEnter',
        \ bufnr: -1, cmd: 'echo "bufenter"'}])
  let l = [#{group: 'TestAcSet', event: 'BufAdd', bufnr: 9999,
        \ cmd: 'echo "bufadd"'}]
  call assert_fails("echo autocmd_add(l)", 'E680:')
  let l = [#{group: 'TestAcSet', event: 'BufAdd', bufnr: 9999,
        \ pattern: '*.py', cmd: 'echo "bufadd"'}]
  call assert_fails("echo autocmd_add(l)", 'E680:')
  let l = [#{group: 'TestAcSet', event: 'BufAdd', bufnr: 9999,
        \ pattern: ['*.py', '*.c'], cmd: 'echo "bufadd"'}]
  call assert_fails("echo autocmd_add(l)", 'E680:')
  let l = [#{group: 'TestAcSet', event: 'BufRead', bufnr: [],
        \ cmd: 'echo "bufread"'}]
  call assert_fails("echo autocmd_add(l)", 'E745:')
  call assert_equal([], autocmd_get(#{group: 'TestAcSet'}))

  " Add two commands to the same group, event and pattern
  call autocmd_delete([#{group: 'TestAcSet'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufUnload',
        \ pattern: 'abc', cmd: 'echo "cmd1"'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufUnload',
        \ pattern: 'abc', cmd: 'echo "cmd2"'}])
  call assert_equal([
        \ #{cmd: 'echo "cmd1"', group: 'TestAcSet', pattern: 'abc',
        \   nested: v:false,  once: v:false, event: 'BufUnload'},
        \ #{cmd: 'echo "cmd2"', group: 'TestAcSet', pattern: 'abc',
        \   nested: v:false,  once: v:false, event: 'BufUnload'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " When adding a new autocmd, if the autocmd 'group' is not specified, then
  " the current autocmd group should be used.
  call autocmd_delete([#{group: 'TestAcSet'}])
  augroup TestAcSet
    call autocmd_add([#{event: 'BufHidden', pattern: 'abc', cmd: 'echo "abc"'}])
  augroup END
  call assert_equal([
        \ #{cmd: 'echo "abc"', group: 'TestAcSet', pattern: 'abc',
        \   nested: v:false,  once: v:false, event: 'BufHidden'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " Test for replacing a cmd for an event in a group
  call autocmd_delete([#{group: 'TestAcSet'}])
  call autocmd_add([#{replace: v:true, group: 'TestAcSet', event: 'BufEnter',
        \ pattern: '*.py', cmd: 'echo "bufenter"'}])
  call autocmd_add([#{replace: v:true, group: 'TestAcSet', event: 'BufEnter',
        \ pattern: '*.py', cmd: 'echo "bufenter"'}])
  call assert_equal([
        \ #{cmd: 'echo "bufenter"', group: 'TestAcSet', pattern: '*.py',
        \   nested: v:false,  once: v:false, event: 'BufEnter'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " Test for adding a command for an unsupported autocmd event
  let l = [#{group: 'TestAcSet', event: 'abc', pattern: '*.sh',
        \ cmd: 'echo "bufadd"'}]
  call assert_fails('call autocmd_add(l)', 'E216:')

  " Test for using a list of events and patterns
  call autocmd_delete([#{group: 'TestAcSet'}])
  let l = [#{group: 'TestAcSet', event: ['BufEnter', 'BufLeave'],
        \ pattern: ['*.py', '*.sh'], cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  call assert_equal([
        \ #{cmd: 'echo "bufcmds"', group: 'TestAcSet', pattern: '*.py',
        \   nested: v:false,  once: v:false, event: 'BufEnter'},
        \ #{cmd: 'echo "bufcmds"', group: 'TestAcSet', pattern: '*.sh',
        \   nested: v:false,  once: v:false, event: 'BufEnter'},
        \ #{cmd: 'echo "bufcmds"', group: 'TestAcSet', pattern: '*.py',
        \   nested: v:false,  once: v:false, event: 'BufLeave'},
        \ #{cmd: 'echo "bufcmds"', group: 'TestAcSet', pattern: '*.sh',
        \   nested: v:false,  once: v:false, event: 'BufLeave'}],
        \   autocmd_get(#{group: 'TestAcSet'}))

  " Test for invalid values for 'event' item
  call autocmd_delete([#{group: 'TestAcSet'}])
  let l = [#{group: 'TestAcSet', event: test_null_string(),
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: test_null_list(),
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E714:')
  let l = [#{group: 'TestAcSet', event: {},
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E777:')
  let l = [#{group: 'TestAcSet', event: [{}],
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: [test_null_string()],
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: 'BufEnter,BufLeave',
        \ pattern: '*.py', cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E216:')
  let l = [#{group: 'TestAcSet', event: [],
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  let l = [#{group: 'TestAcSet', event: [""],
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E216:')
  let l = [#{group: 'TestAcSet', event: "",
        \ pattern: "*.py", cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  call assert_equal([], autocmd_get(#{group: 'TestAcSet'}))

  " Test for invalid values for 'pattern' item
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: test_null_string(), cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: test_null_list(), cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E714:')
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: {}, cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E777:')
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: [{}], cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: [test_null_string()], cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E928:')
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: [], cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: [""], cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  let l = [#{group: 'TestAcSet', event: "BufEnter",
        \ pattern: "", cmd: 'echo "bufcmds"'}]
  call autocmd_add(l)
  call assert_equal([], autocmd_get(#{group: 'TestAcSet'}))

  let l = [#{group: 'TestAcSet', event: 'BufEnter,abc,BufLeave',
        \ pattern: '*.py', cmd: 'echo "bufcmds"'}]
  call assert_fails('call autocmd_add(l)', 'E216:')

  call assert_fails("call autocmd_add({})", 'E1211:')
  call assert_equal(v:false,  autocmd_add(test_null_list()))
  call assert_true(autocmd_add([[]]))
  call assert_true(autocmd_add([test_null_dict()]))

  augroup TestAcSet
    au!
  augroup END

  call autocmd_add([#{group: 'TestAcSet'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufAdd'}])
  call autocmd_add([#{group: 'TestAcSet', pat: '*.sh'}])
  call autocmd_add([#{group: 'TestAcSet', cmd: 'echo "a"'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufAdd', pat: '*.sh'}])
  call autocmd_add([#{group: 'TestAcSet', event: 'BufAdd', cmd: 'echo "a"'}])
  call autocmd_add([#{group: 'TestAcSet', pat: '*.sh', cmd: 'echo "a"'}])
  call assert_equal([], autocmd_get(#{group: 'TestAcSet'}))

  augroup! TestAcSet
endfunc

" Test for deleting autocmd events and groups
func Test_autocmd_delete()
  " Delete an event in an autocmd group
  augroup TestAcSet
    au!
    au BufAdd *.sh echo "bufadd"
    au BufEnter *.sh echo "bufenter"
  augroup END
  call autocmd_delete([#{group: 'TestAcSet', event: 'BufAdd'}])
  call assert_equal([#{cmd: 'echo "bufenter"', group: 'TestAcSet',
        \ pattern: '*.sh', nested: v:false, once: v:false,
        \ event: 'BufEnter'}], autocmd_get(#{group: 'TestAcSet'}))

  " Delete all the events in an autocmd group
  augroup TestAcSet
    au BufAdd *.sh echo "bufadd"
  augroup END
  call autocmd_delete([#{group: 'TestAcSet', event: '*'}])
  call assert_equal([], autocmd_get(#{group: 'TestAcSet'}))

  " Delete a non-existing autocmd group
  call assert_fails("call autocmd_delete([#{group: 'abc'}])", 'E367:')
  " Delete a non-existing autocmd event
  let l = [#{group: 'TestAcSet', event: 'abc'}]
  call assert_fails("call autocmd_delete(l)", 'E216:')
  " Delete a non-existing autocmd pattern
  let l = [#{group: 'TestAcSet', event: 'BufAdd', pat: 'abc'}]
  call assert_true(autocmd_delete(l))
  " Delete an autocmd for a non-existing buffer
  let l = [#{event: '*', bufnr: 9999, cmd: 'echo "x"'}]
  call assert_fails('call autocmd_delete(l)', 'E680:')

  " Delete an autocmd group
  augroup TestAcSet
    au!
    au BufAdd *.sh echo "bufadd"
    au BufEnter *.sh echo "bufenter"
  augroup END
  call autocmd_delete([#{group: 'TestAcSet'}])
  call assert_fails("call autocmd_get(#{group: 'TestAcSet'})", 'E367:')

  call assert_true(autocmd_delete([[]]))
  call assert_true(autocmd_delete([test_null_dict()]))
endfunc

func Test_autocmd_split_dummy()
  " Autocommand trying to split a window containing a dummy buffer.
  auto BufReadPre * exe "sbuf " .. expand("<abuf>")
  " Avoid the "W11" prompt
  au FileChangedShell * let v:fcs_choice = 'reload'
  func Xautocmd_changelist()
    cal writefile(['Xtestfile2:4:4'], 'Xerr')
    edit Xerr
    lex 'Xtestfile2:4:4'
  endfunc
  call Xautocmd_changelist()
  " Should get E86, but it doesn't always happen (timing?)
  silent! call Xautocmd_changelist()

  au! BufReadPre
  au! FileChangedShell
  delfunc Xautocmd_changelist
  bwipe! Xerr
  call delete('Xerr')
endfunc

" This was crashing because there was only one window to execute autocommands
" in.
func Test_autocmd_nested_setbufvar()
  CheckFeature python3

  set hidden
  edit Xaaa
  edit Xbbb
  call setline(1, 'bar')
  enew
  au BufWriteCmd Xbbb ++nested call setbufvar('Xaaa', '&ft', 'foo') | bw! Xaaa
  au FileType foo call py3eval('vim.current.buffer.options["cindent"]')
  wall

  au! BufWriteCmd
  au! FileType foo
  set nohidden
  call delete('Xaaa')
  call delete('Xbbb')
  %bwipe!
endfunc

func SetupVimTest_shm()
  let g:bwe = []
  let g:brp = []
  set shortmess+=F
  messages clear

  let dirname='XVimTestSHM'
  call mkdir(dirname, 'R')
  call writefile(['test'], dirname .. '/1')
  call writefile(['test'], dirname .. '/2')
  call writefile(['test'], dirname .. '/3')

  augroup test
      autocmd!
      autocmd BufWinEnter * call add(g:bwe, $'BufWinEnter: {expand('<amatch>')}')
      autocmd BufReadPost * call add(g:brp,  $'BufReadPost: {expand('<amatch>')}')
  augroup END

  call setqflist([
        \  {'filename': dirname .. '/1', 'lnum': 1, 'col': 1, 'text': 'test', 'vcol': 0},
        \  {'filename': dirname .. '/2', 'lnum': 1, 'col': 1, 'text': 'test', 'vcol': 0},
        \  {'filename': dirname .. '/3', 'lnum': 1, 'col': 1, 'text': 'test', 'vcol': 0}
        \  ])
  cdo! substitute/test/TEST

  " clean up
  noa enew!
  set shortmess&vim
  augroup test
      autocmd!
  augroup END
  augroup! test
endfunc

func Test_autocmd_shortmess()
  CheckNotMSWindows

  call SetupVimTest_shm()
  let output = execute(':mess')->split('\n')

  let info = copy(output)->filter({idx, val -> val =~# '\d of 3'} )
  let bytes = copy(output)->filter({idx, val -> val =~# 'bytes'} )

  " We test the following here:
  " BufReadPost should have been triggered 3 times, once per file
  " BufWinEnter should have been triggered 3 times, once per file
  " FileInfoMessage should have been shown 3 times, regardless of shm option
  " "(x of 3)" message from :cnext has been shown 3 times

  call assert_equal(3, g:brp->len())
  call assert_equal(3, g:bwe->len())
  call assert_equal(3, info->len())
  call assert_equal(3, bytes->len())

  delfunc SetupVimTest_shm
endfunc

func Test_autocmd_invalidates_undo_on_textchanged()
  CheckRunVimInTerminal
  let script =<< trim END
    set hidden
    " create quickfix list (at least 2 lines to move line)
    vimgrep /u/j %

    " enter quickfix window
    cwindow

    " set modifiable
    setlocal modifiable

    " set autocmd to clear quickfix list

    autocmd! TextChanged <buffer> call setqflist([])
    " move line
    move+1
  END
  call writefile(script, 'XTest_autocmd_invalidates_undo_on_textchanged', 'D')
  let buf = RunVimInTerminal('XTest_autocmd_invalidates_undo_on_textchanged', {'rows': 20})
  call term_sendkeys(buf, ":so %\<cr>")
  call term_sendkeys(buf, "G")
  call WaitForAssert({-> assert_match('^XTest_autocmd_invalidates_undo_on_textchanged\s*$', term_getline(buf, 20))}, 1000)

  call StopVimInTerminal(buf)
endfunc

func Test_autocmd_creates_new_buffer_on_bufleave()
  e a.txt
  e b.txt
  setlocal bufhidden=wipe
  autocmd BufLeave <buffer> diffsplit c.txt
  bn
  call assert_equal(1, winnr('$'))
  call assert_equal('a.txt', bufname('%'))
  bw a.txt
  bw c.txt
endfunc

" vim: shiftwidth=2 sts=2 expandtab