patch 9.1.1525: tests: testdir/ is a bit messy

Problem:  tests: testdir is a bit messy
Solution: move test scripts into testdir/util/ directory

src/testdir/ has become a dumping ground mixing test cases with utility
functions. Let's fix this by moving all utility functions into the
testdir/util/ directory

Also a few related changes had to be done:
- Update Filelist
- update README.txt and mention the new directory layout
- fix shadowbuild by linking the util directory into the shadow dir

closes: #17677

Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/testdir/util/amiga.vim b/src/testdir/util/amiga.vim
new file mode 100644
index 0000000..2837fe5
--- /dev/null
+++ b/src/testdir/util/amiga.vim
@@ -0,0 +1,6 @@
+" Settings for test script execution
+set shell=csh
+map! /tmp t:
+cmap !rm !Delete all
+
+source util/setup.vim
diff --git a/src/testdir/util/check.vim b/src/testdir/util/check.vim
new file mode 100644
index 0000000..aa8eceb
--- /dev/null
+++ b/src/testdir/util/check.vim
@@ -0,0 +1,334 @@
+source util/shared.vim
+source util/term_util.vim
+
+" uses line-continuation
+let s:cpo_save = &cpo
+set cpo&vim
+
+command -nargs=1 MissingFeature throw 'Skipped: ' .. <args> .. ' feature missing'
+
+" Command to check for the presence of a feature.
+command -nargs=1 CheckFeature call CheckFeature(<f-args>)
+func CheckFeature(name)
+  if !has(a:name, 1)
+    throw 'Checking for non-existent feature ' .. a:name
+  endif
+  if !has(a:name)
+    MissingFeature a:name
+  endif
+endfunc
+
+" Command to check for the absence of a feature.
+command -nargs=1 CheckNotFeature call CheckNotFeature(<f-args>)
+func CheckNotFeature(name)
+  if !has(a:name, 1)
+    throw 'Checking for non-existent feature ' .. a:name
+  endif
+  if has(a:name)
+    throw 'Skipped: ' .. a:name .. ' feature present'
+  endif
+endfunc
+
+" Command to check for the presence of a working option.
+command -nargs=1 CheckOption call CheckOption(<f-args>)
+func CheckOption(name)
+  if !exists('&' .. a:name)
+    throw 'Checking for non-existent option ' .. a:name
+  endif
+  if !exists('+' .. a:name)
+    throw 'Skipped: ' .. a:name .. ' option not supported'
+  endif
+endfunc
+
+" Command to check for the presence of a built-in function.
+command -nargs=1 CheckFunction call CheckFunction(<f-args>)
+func CheckFunction(name)
+  if !exists('?' .. a:name)
+    throw 'Checking for non-existent function ' .. a:name
+  endif
+  if !exists('*' .. a:name)
+    throw 'Skipped: ' .. a:name .. ' function missing'
+  endif
+endfunc
+
+" Command to check for the presence of an Ex command
+command -nargs=1 CheckCommand call CheckCommand(<f-args>)
+func CheckCommand(name)
+  if !exists(':' .. a:name)
+    throw 'Skipped: ' .. a:name .. ' command not supported'
+  endif
+endfunc
+
+" Command to check for the presence of a shell command
+command -nargs=1 CheckExecutable call CheckExecutable(<f-args>)
+func CheckExecutable(name)
+  if !executable(a:name)
+    throw 'Skipped: ' .. a:name .. ' program not executable'
+  endif
+endfunc
+
+" Command to check for the presence of python.  Argument should have been
+" obtained with PythonProg()
+func CheckPython(name)
+  if a:name == ''
+    throw 'Skipped: python command not available'
+  endif
+endfunc
+
+" Command to check for running on MS-Windows
+command CheckMSWindows call CheckMSWindows()
+func CheckMSWindows()
+  if !has('win32')
+    throw 'Skipped: only works on MS-Windows'
+  endif
+endfunc
+
+" Command to check for NOT running on MS-Windows
+command CheckNotMSWindows call CheckNotMSWindows()
+func CheckNotMSWindows()
+  if has('win32')
+    throw 'Skipped: does not work on MS-Windows'
+  endif
+endfunc
+
+" Command to check for running on Unix
+command CheckUnix call CheckUnix()
+func CheckUnix()
+  if !has('unix')
+    throw 'Skipped: only works on Unix'
+  endif
+endfunc
+
+" Command to check for running on Linux
+command CheckLinux call CheckLinux()
+func CheckLinux()
+  if !has('linux')
+    throw 'Skipped: only works on Linux'
+  endif
+endfunc
+
+" Command to check for not running on a BSD system.
+command CheckNotBSD call CheckNotBSD()
+func CheckNotBSD()
+  if has('bsd')
+    throw 'Skipped: does not work on BSD'
+  endif
+endfunc
+
+" Command to check for not running on a MacOS
+command CheckNotMac call CheckNotMac()
+func CheckNotMac()
+  if has('mac')
+    throw 'Skipped: does not work on MacOS'
+  endif
+endfunc
+
+" Command to check for not running on a MacOS M1 system.
+command CheckNotMacM1 call CheckNotMacM1()
+func CheckNotMacM1()
+  if has('mac') && system('uname -a') =~ '\<arm64\>'
+    throw 'Skipped: does not work on MacOS M1'
+  endif
+endfunc
+
+func SetupWindowSizeToForVisualDumps()
+  " The dumps used as reference in these tests were created with a terminal
+  " width of 75 columns. The vim window that uses the remainder of the GUI
+  " window width must be at least 3 columns. In theory this means we need the
+  " GUI shell to provide 78+ columns. However the GTK3 resize logic is flaky,
+  " sometimes resulting in X11 Configure events that are narrower than
+  " expected by a number of pixels equal to 2 column widths. Therefore
+  " setting 80 columns ensures that the GUI shell can still provide 78+
+  " columns. This is very likely papering over a GTK3 resize bug but one that
+  " has existed for a very long time. Establishing this workaround is meant to
+  " get the GTK3 code working under CI so that we can focus on removing this
+  " over the long term.
+  if &columns != 80
+    set columns=80
+  endif
+  " Without resetting lines, some GTK3 resize events can carry over between
+  " tests, which invalidate assumptions in the scrollbar offset calculations.
+  if &lines != 25
+    set lines=25
+  endif
+endfunc
+
+" Command to check that making screendumps is supported.
+" Caller must source util/screendump.vim
+command CheckScreendump call CheckScreendump()
+func CheckScreendump()
+  let g:check_screendump_called = v:true
+  if !CanRunVimInTerminal()
+    throw 'Skipped: cannot make screendumps'
+  endif
+  if has('gui_running')
+    call SetupWindowSizeToForVisualDumps()
+  endif
+endfunc
+
+" Command to check that we can Run Vim in a terminal window
+command CheckRunVimInTerminal call CheckRunVimInTerminal()
+func CheckRunVimInTerminal()
+  if !CanRunVimInTerminal()
+    throw 'Skipped: cannot run Vim in a terminal window'
+  endif
+endfunc
+
+" Command to check that we can run the GUI
+command CheckCanRunGui call CheckCanRunGui()
+func CheckCanRunGui()
+  if !has('gui') || ($DISPLAY == "" && !has('gui_running'))
+    throw 'Skipped: cannot start the GUI'
+  endif
+endfunc
+
+" Command to Check for an environment variable
+command -nargs=1 CheckEnv call CheckEnv(<f-args>)
+func CheckEnv(name)
+  if empty(eval('$' .. a:name))
+    throw 'Skipped: Environment variable ' .. a:name .. ' is not set'
+  endif
+endfunc
+
+" Command to Check for pure X11 (no Wayland)
+command -nargs=0 CheckX11 call CheckX11()
+func CheckX11()
+  if !empty($WAYLAND_DISPLAY) || empty($DISPLAY)
+    throw 'Skipped: not pure X11 environment'
+  endif
+endfunc
+
+" Command to check that we are using the GUI
+command CheckGui call CheckGui()
+func CheckGui()
+  if !has('gui_running')
+    throw 'Skipped: only works in the GUI'
+  endif
+endfunc
+
+" Command to check that not currently using the GUI
+command CheckNotGui call CheckNotGui()
+func CheckNotGui()
+  if has('gui_running')
+    throw 'Skipped: only works in the terminal'
+  endif
+endfunc
+
+" Command to check that test is not running as root
+command CheckNotRoot call CheckNotRoot()
+func CheckNotRoot()
+  if IsRoot()
+    throw 'Skipped: cannot run test as root'
+  endif
+endfunc
+
+" Command to check that the current language is English
+command CheckEnglish call CheckEnglish()
+func CheckEnglish()
+  if v:lang != "C" && v:lang !~ '^[Ee]n'
+      throw 'Skipped: only works in English language environment'
+  endif
+endfunc
+
+" Command to check that loopback device has IPv6 address
+command CheckIPv6 call CheckIPv6()
+func CheckIPv6()
+  if !has('ipv6')
+    throw 'Skipped: cannot use IPv6 networking'
+  endif
+  if !exists('s:ipv6_loopback')
+    let s:ipv6_loopback = s:CheckIPv6Loopback()
+  endif
+  if !s:ipv6_loopback
+    throw 'Skipped: no IPv6 address for loopback device'
+  endif
+endfunc
+
+func s:CheckIPv6Loopback()
+  if has('win32')
+    return system('netsh interface ipv6 show interface') =~? '\<Loopback\>'
+  elseif filereadable('/proc/net/if_inet6')
+    return (match(readfile('/proc/net/if_inet6'), '\slo$') >= 0)
+  elseif executable('ifconfig')
+    for dev in ['lo0', 'lo', 'loop']
+      " NOTE: On SunOS, need specify address family 'inet6' to get IPv6 info.
+      if system('ifconfig ' .. dev .. ' inet6 2>/dev/null') =~? '\<inet6\>'
+            \ || system('ifconfig ' .. dev .. ' 2>/dev/null') =~? '\<inet6\>'
+        return v:true
+      endif
+    endfor
+  else
+    " TODO: How to check it in other platforms?
+  endif
+  return v:false
+endfunc
+
+" Command to check for not running under ASAN
+command CheckNotAsan call CheckNotAsan()
+func CheckNotAsan()
+  if execute('version') =~# '-fsanitize=[a-z,]*\<address\>'
+    throw 'Skipped: does not work with ASAN'
+  endif
+endfunc
+
+" Command to check for not running under valgrind
+command CheckNotValgrind call CheckNotValgrind()
+func CheckNotValgrind()
+  if RunningWithValgrind()
+    throw 'Skipped: does not work well with valgrind'
+  endif
+endfunc
+
+" Command to check for X11 based GUI
+command CheckX11BasedGui call CheckX11BasedGui()
+func CheckX11BasedGui()
+  if !g:x11_based_gui
+    throw 'Skipped: requires X11 based GUI'
+  endif
+endfunc
+
+" Command to check that there are two clipboards
+command CheckTwoClipboards call CheckTwoClipboards()
+func CheckTwoClipboards()
+  " avoid changing the clipboard here, only X11 supports both
+  if !has('X11')
+    throw 'Skipped: requires two clipboards'
+  endif
+endfunc
+
+" Command to check for satisfying any of the conditions.
+" e.g. CheckAnyOf Feature:bsd Feature:sun Linux
+command -nargs=+ CheckAnyOf call CheckAnyOf(<f-args>)
+func CheckAnyOf(...)
+  let excp = []
+  for arg in a:000
+    try
+      exe 'Check' .. substitute(arg, ':', ' ', '')
+      return
+    catch /^Skipped:/
+      let excp += [substitute(v:exception, '^Skipped:\s*', '', '')]
+    endtry
+  endfor
+  throw 'Skipped: ' .. join(excp, '; ')
+endfunc
+
+" Command to check for satisfying all of the conditions.
+" e.g. CheckAllOf Unix Gui Option:ballooneval
+command -nargs=+ CheckAllOf call CheckAllOf(<f-args>)
+func CheckAllOf(...)
+  for arg in a:000
+    exe 'Check' .. substitute(arg, ':', ' ', '')
+  endfor
+endfunc
+
+" Check if running under Github Actions
+command CheckGithubActions call CheckGithubActions()
+func CheckGithubActions()
+  if expand('$GITHUB_ACTIONS') ==# 'true'
+    throw "Skipped: FIXME: this test doesn't work on Github Actions CI"
+  endif
+endfunc
+
+let &cpo = s:cpo_save
+unlet s:cpo_save
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/util/color_ramp.vim b/src/testdir/util/color_ramp.vim
new file mode 100644
index 0000000..8eed8f9
--- /dev/null
+++ b/src/testdir/util/color_ramp.vim
@@ -0,0 +1,85 @@
+" Script to generate a file that shows al 256 xterm colors
+
+new
+let lnum = 1
+
+" | in original color pair to see white background.
+let trail_bar = "\033[m|"
+
+" ANSI colors
+call setline(lnum, 'ANSI background')
+let lnum += 1
+
+let s = ''
+for nr in range(0, 7)
+  let s .= "\033[4" . nr . "m    "
+endfor
+for nr in range(8, 15)
+  let s .= "\033[10" . (nr - 8) . "m    "
+endfor
+let s .= trail_bar
+
+call setline(lnum, s)
+let lnum += 1
+
+" ANSI text colors
+call setline(lnum, 'ANSI text')
+let lnum += 1
+
+let s = ''
+for nr in range(0, 7)
+  let s .= "\033[0;3" . nr . "mxxxx"
+endfor
+for nr in range(8, 15)
+  let s .= "\033[0;9" . (nr - 8) . "mxxxx"
+endfor
+let s .= trail_bar
+
+call setline(lnum, s)
+let lnum += 1
+
+" ANSI with bold text
+call setline(lnum, 'ANSI bold text')
+let lnum += 1
+
+let s = ''
+for nr in range(0, 7)
+  let s .= "\033[1;3" . nr . "mxxxx"
+endfor
+for nr in range(8, 15)
+  let s .= "\033[1;9" . (nr - 8) . "mxxxx"
+endfor
+let s .= trail_bar
+
+call setline(lnum, s)
+let lnum += 1
+
+" 6 x 6 x 6 color cube
+call setline(lnum, 'color cube')
+let lnum += 1
+
+for high in range(0, 5)
+  let s = ''
+  for low in range(0, 35)
+    let nr = low + high * 36
+    let s .= "\033[48;5;" . (nr + 16) . "m  "
+  endfor
+  let s .= trail_bar
+  call setline(lnum + high, s)
+endfor
+let lnum += 6
+
+" 24 shades of grey
+call setline(lnum, 'grey ramp')
+let lnum += 1
+
+let s = ''
+for nr in range(0, 23)
+    let s .= "\033[48;5;" . (nr + 232) . "m   "
+endfor
+let s .= trail_bar
+call setline(lnum, s)
+
+set binary
+write! <sfile>:h/color_ramp.txt
+quit
diff --git a/src/testdir/util/dos.vim b/src/testdir/util/dos.vim
new file mode 100644
index 0000000..3134d34
--- /dev/null
+++ b/src/testdir/util/dos.vim
@@ -0,0 +1,9 @@
+" Settings for test script execution
+" Always use "COMMAND.COM", don't use the value of "$SHELL".
+set shell=c:\COMMAND.COM shellquote= shellxquote= shellcmdflag=/c shellredir=>
+" This is used only when the +eval feature is available.
+if executable("cmd.exe")
+   set shell=cmd.exe shellcmdflag=/D\ /c
+endif
+
+source util/setup.vim
diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim
new file mode 100644
index 0000000..1e0f39c
--- /dev/null
+++ b/src/testdir/util/gen_opt_test.vim
@@ -0,0 +1,517 @@
+" Script to generate src/testdir/opt_test.vim from src/optiondefs.h and
+" runtime/doc/options.txt
+
+set cpo&vim
+
+" Only do this when build with the +eval feature.
+if 1
+
+try
+
+set nomore
+
+const K_KENTER = -16715
+
+" Get global-local options.
+" "key" is full-name of the option.
+" "value" is the local value to switch back to the global value.
+b options.txt
+call cursor(1, 1)
+let global_locals = {}
+while search("^'[^']*'.*\\n.*|global-local", 'W')
+  let fullname = getline('.')->matchstr("^'\\zs[^']*")
+  let global_locals[fullname] = ''
+endwhile
+call extend(global_locals, #{
+      \ scrolloff: -1,
+      \ sidescrolloff: -1,
+      \ undolevels: -123456,
+      \})
+
+" Get local-noglobal options.
+" "key" is full-name of the option.
+" "value" is no used.
+b options.txt
+call cursor(1, 1)
+let local_noglobals = {}
+while search("^'[^']*'.*\\n.*|local-noglobal", 'W')
+  let fullname = getline('.')->matchstr("^'\\zs[^']*")
+  let local_noglobals[fullname] = v:true
+endwhile
+
+" Options to skip `setglobal` tests.
+" "key" is full-name of the option.
+" "value" is the reason.
+let skip_setglobal_reasons = #{
+      \ iminsert: 'The global value is always overwritten by the local value',
+      \ imsearch: 'The global value is always overwritten by the local value',
+      \}
+
+" Script header.
+" The test values contains multibyte characters.
+let script = [
+      \ '" DO NOT EDIT: Generated with util/gen_opt_test.vim',
+      \ '" Used by test_options_all.vim.',
+      \ '',
+      \ 'scriptencoding utf-8',
+      \ ]
+
+b optiondefs.h
+const end = search('#define p_term', 'nw')
+
+" font name that works everywhere (hopefully)
+let fontname = has('win32') ? 'fixedsys' : 'fixed'
+
+" Two lists with values: values that work and values that fail.
+" When not listed, "othernum" or "otherstring" is used.
+" When both lists are empty, skip tests for the option.
+" For boolean options, if non-empty a fixed test will be run, otherwise skipped.
+let test_values = {
+      "\ boolean options
+      \ 'termguicolors': [
+      \		has('vtp') && !has('vcon') && !has('gui_running') ? [] : [1],
+      \		[]],
+      \
+      "\ number options
+      \ 'chistory': [[1, 2, 10, 50], [1000, -1]],
+      \ 'cmdheight': [[1, 2, 10], [-1, 0]],
+      \ 'cmdwinheight': [[1, 2, 10], [-1, 0]],
+      \ 'columns': [[12, 80, 10000], [-1, 0, 10]],
+      \ 'conceallevel': [[0, 1, 2, 3], [-1, 4, 99]],
+      \ 'foldcolumn': [[0, 1, 4, 12], [-1, 13, 999]],
+      \ 'helpheight': [[0, 10, 100], [-1]],
+      \ 'history': [[0, 1, 100, 10000], [-1, 10001]],
+      \ 'iminsert': [[0, 1, 2], [-1, 3, 999]],
+      \ 'imsearch': [[-1, 0, 1, 2], [-2, 3, 999]],
+      \ 'imstyle': [[0, 1], [-1, 2, 999]],
+      \ 'lhistory': [[1, 2, 10, 50], [1000, -1]],
+      \ 'lines': [[2, 24, 1000], [-1, 0, 1]],
+      \ 'linespace': [[-1, 0, 2, 4, 999], ['']],
+      \ 'numberwidth': [[1, 4, 8, 10, 11, 20], [-1, 0, 21]],
+      \ 'regexpengine': [[0, 1, 2], [-1, 3, 999]],
+      \ 'report': [[0, 1, 2, 9999], [-1]],
+      \ 'scroll': [[0, 1, 2, 15], [-1, 999]],
+      \ 'scrolljump': [[-100, -1, 0, 1, 2, 15], [-101, 999]],
+      \ 'scrolloff': [[0, 1, 8, 999], [-1]],
+      \ 'shiftwidth': [[0, 1, 8, 999], [-1]],
+      \ 'showtabpanel': [[0, 1, 2], []],
+      \ 'sidescroll': [[0, 1, 8, 999], [-1]],
+      \ 'sidescrolloff': [[0, 1, 8, 999], [-1]],
+      \ 'tabstop': [[1, 4, 8, 12, 9999], [-1, 0, 10000]],
+      \ 'termwinscroll': [[1, 100, 99999], [-1, 0]],
+      \ 'textwidth': [[0, 1, 8, 99], [-1]],
+      \ 'timeoutlen': [[0, 8, 99999], [-1]],
+      \ 'titlelen': [[0, 1, 8, 9999], [-1]],
+      \ 'updatecount': [[0, 1, 8, 9999], [-1]],
+      \ 'updatetime': [[0, 1, 8, 9999], [-1]],
+      \ 'verbose': [[-1, 0, 1, 8, 9999], ['']],
+      \ 'wildchar': [[-1, 0, 100, 'x', '^Y', '^@', '<Esc>', '<t_xx>', '<', '^'],
+      \		['', 'xxx', '<xxx>', '<t_xxx>', '<Esc', '<t_xx', '<C-C>',
+      \		'<NL>', '<CR>', K_KENTER]],
+      \ 'wildcharm': [[-1, 0, 100, 'x', '^Y', '^@', '<Esc>', '<', '^'],
+      \		['', 'xxx', '<xxx>', '<t_xxx>', '<Esc', '<t_xx', '<C-C>',
+      \		'<NL>', '<CR>', K_KENTER]],
+      \ 'winheight': [[1, 10, 999], [-1, 0]],
+      \ 'winminheight': [[0, 1], [-1]],
+      \ 'winminwidth': [[0, 1, 10], [-1]],
+      \ 'winwidth': [[1, 10, 999], [-1, 0]],
+      \ 'wltimeoutlen': [[1, 10, 999],[-1]],
+      \
+      "\ string options
+      \ 'ambiwidth': [['', 'single', 'double'], ['xxx']],
+      \ 'background': [['', 'light', 'dark'], ['xxx']],
+      \ 'backspace': [[0, 1, 2, 3, '', 'indent', 'eol', 'start', 'nostop',
+      \		'eol,start', 'indent,eol,nostop'],
+      \		[-1, 4, 'xxx']],
+      \ 'backupcopy': [['yes', 'no', 'auto'], ['', 'xxx', 'yes,no']],
+      \ 'backupext': [['xxx'], [&patchmode, '*']],
+      \ 'belloff': [['', 'all', 'backspace', 'cursor', 'complete', 'copy',
+      \		'ctrlg', 'error', 'esc', 'ex', 'hangul', 'insertmode', 'lang',
+      \		'mess', 'showmatch', 'operator', 'register', 'shell', 'spell',
+      \		'term', 'wildmode', 'copy,error,shell'],
+      \		['xxx']],
+      \ 'breakindentopt': [['', 'min:3', 'shift:4', 'shift:-2', 'sbr', 'list:5',
+      \		'list:-1', 'column:10', 'column:-5', 'min:1,sbr,shift:2'],
+      \		['xxx', 'min', 'min:x', 'min:-1', 'shift:x', 'sbr:1', 'list:x',
+      \		'column:x']],
+      \ 'browsedir': [['', 'last', 'buffer', 'current', './Xdir\ with\ space'],
+      \		['xxx']],
+      \ 'bufhidden': [['', 'hide', 'unload', 'delete', 'wipe'],
+      \		['xxx', 'hide,wipe']],
+      \ 'buftype': [['', 'nofile', 'nowrite', 'acwrite', 'quickfix', 'help',
+      \		'terminal', 'prompt', 'popup'],
+      \		['xxx', 'help,nofile']],
+      \ 'casemap': [['', 'internal', 'keepascii', 'internal,keepascii'],
+      \		['xxx']],
+      \ 'cedit': [['', '^Y', '^@', '<Esc>', '<t_xx>'],
+      \		['xxx', 'f', '<xxx>', '<t_xxx>', '<Esc', '<t_xx']],
+      \ 'clipboard': [['', 'unnamed', 'unnamedplus', 'autoselect',
+      \		'autoselectplus', 'autoselectml', 'html', 'exclude:vimdisplay',
+      \		'autoselect,unnamed', 'unnamed,exclude:.*'],
+      \		['xxx', 'exclude:\\ze*', 'exclude:\\%(']],
+      \ 'clipmethod': [['wayland', 'x11', 'wayland,x11', ''],['xxx', '--', 'wayland,,', ',x11']],
+      \ 'colorcolumn': [['', '8', '+2', '1,+1,+3'], ['xxx', '-a', '1,', '1;']],
+      \ 'comments': [['', 'b:#', 'b:#,:%'], ['xxx', '-']],
+      \ 'commentstring': [['', '/*\ %s\ */'], ['xxx']],
+      \ 'complete': [['', '.', 'w', 'b', 'u', 'U', 'i', 'd', ']', 't',
+      \		'k', 'kspell', 'k/tmp/dir\\\ with\\\ space/*',
+      \		's', 's/tmp/dir\\\ with\\\ space/*',
+      \		'w,b,k/tmp/dir\\\ with\\\ space/*,s'],
+      \		['xxx']],
+      \ 'concealcursor': [['', 'n', 'v', 'i', 'c', 'nvic'], ['xxx']],
+      \ 'completeopt': [['', 'menu', 'menuone', 'longest', 'preview', 'popup',
+      \		'popuphidden', 'noinsert', 'noselect', 'fuzzy', "preinsert", 'menu,longest'],
+      \		['xxx', 'menu,,,longest,']],
+      \ 'completefuzzycollect': [['', 'keyword', 'files', 'whole_line',
+      \		'keyword,whole_line', 'files,whole_line', 'keyword,files,whole_line'],
+      \		['xxx', 'keyword,,,whole_line,']],
+      \ 'completeitemalign': [['abbr,kind,menu', 'menu,abbr,kind'],
+      \		['', 'xxx', 'abbr', 'abbr,menu', 'abbr,menu,kind,abbr',
+      \		'abbr1234,kind,menu']],
+      \ 'completepopup': [['', 'height:13', 'width:20', 'highlight:That',
+      \		'align:item', 'align:menu', 'border:on', 'border:off',
+      \		'width:10,height:234,highlight:Mine'],
+      \		['xxx', 'xxx:99', 'height:yes', 'width:no', 'align:xxx',
+      \		'border:maybe', 'border:1', 'border:']],
+      \ 'completeslash': [['', 'slash', 'backslash'], ['xxx']],
+      \ 'cryptmethod': [['', 'zip'], ['xxx']],
+      \ 'cscopequickfix': [['', 's-', 'g-', 'd-', 'c-', 't-', 'e-', 'f-', 'i-',
+      \		'a-', 's-,c+,e0'],
+      \		['xxx', 's,g,d']],
+      \ 'cursorlineopt': [['both', 'line', 'number', 'screenline',
+      \		'line,number'],
+      \		['', 'xxx', 'line,screenline']],
+      \ 'debug': [['', 'msg', 'throw', 'beep'], ['xxx']],
+      \ 'diffopt': [['', 'filler', 'context:0', 'context:999', 'iblank',
+      \		'icase', 'iwhite', 'iwhiteall', 'horizontal', 'vertical',
+      \		'closeoff', 'hiddenoff', 'foldcolumn:0', 'foldcolumn:12',
+      \		'followwrap', 'internal', 'indent-heuristic', 'algorithm:myers',
+      \		'icase,iwhite', 'algorithm:minimal', 'algorithm:patience',
+      \		'algorithm:histogram', 'inline:none', 'inline:simple',
+      \		'inline:char', 'inline:word', 'inline:char,inline:word', 'linematch:5'],
+      \		['xxx', 'foldcolumn:', 'foldcolumn:x', 'foldcolumn:xxx',
+      \		'linematch:', 'linematch:x', 'linematch:xxx', 'algorithm:',
+      \		'algorithm:xxx', 'context:', 'context:x', 'context:xxx',
+      \		'inline:xxx']],
+      \ 'display': [['', 'lastline', 'truncate', 'uhex', 'lastline,uhex'],
+      \		['xxx']],
+      \ 'eadirection': [['', 'both', 'ver', 'hor'], ['xxx', 'ver,hor']],
+      \ 'encoding': [['latin1'], ['xxx', '']],
+      \ 'eventignore': [['', 'WinEnter', 'WinLeave,winenter', 'all,WinEnter', 'all,-WinLeave'],
+      \		['xxx']],
+      \ 'eventignorewin': [['', 'WinEnter', 'WinLeave,winenter', 'all,WinEnter', 'all,-WinLeave'],
+      \		['xxx', 'WinNew']],
+      \ 'fileencoding': [['', 'latin1', 'xxx'], []],
+      \ 'fileformat': [['', 'dos', 'unix', 'mac'], ['xxx']],
+      \ 'fileformats': [['', 'dos', 'dos,unix'], ['xxx']],
+      \ 'fillchars': [['', 'stl:x', 'stlnc:x', 'vert:x', 'fold:x', 'foldopen:x',
+      \		'foldclose:x', 'foldsep:x', 'diff:x', 'eob:x', 'lastline:x',
+      \		'trunc:_', 'trunc:_,eob:x,trunc:_',
+      \		'stl:\ ,vert:\|,fold:\\,trunc:…,diff:x'],
+      \		['xxx', 'vert:', 'trunc:', "trunc:\b"]],
+      \ 'foldclose': [['', 'all'], ['xxx']],
+      \ 'foldmethod': [['manual', 'indent', 'expr', 'marker', 'syntax', 'diff'],
+      \		['', 'xxx', 'expr,diff']],
+      \ 'foldopen': [['', 'all', 'block', 'hor', 'insert', 'jump', 'mark',
+      \		'percent', 'quickfix', 'search', 'tag', 'undo', 'hor,jump'],
+      \		['xxx']],
+      \ 'foldmarker': [['((,))'], ['', 'xxx', '{{{,']],
+      \ 'formatoptions': [['', 't', 'c', 'r', 'o', '/', 'q', 'w', 'a', 'n', '2',
+      \		'v', 'b', 'l', 'm', 'M', 'B', '1', ']', 'j', 'p', 'vt', 'v,t'],
+      \		['xxx']],
+      \ 'guicursor': [['', 'n:block-Cursor'], ['xxx']],
+      \ 'guifont': [['', fontname], []],
+      \ 'guifontwide': [['', fontname], []],
+      \ 'guifontset': [['', fontname], []],
+      \ 'guioptions': [['', '!', 'a', 'P', 'A', 'c', 'd', 'e', 'f', 'i', 'm',
+      \		'M', 'g', 't', 'T', 'r', 'R', 'l', 'L', 'b', 'h', 'v', 'p', 'F',
+      \		'k', '!abvR'],
+      \		['xxx', 'a,b']],
+      \ 'helplang': [['', 'de', 'de,it'], ['xxx']],
+      \ 'highlight': [['', 'e:Error'], ['xxx']],
+      \ 'imactivatekey': [['', 'S-space'], ['xxx']],
+      \ 'isfname': [['', '@', '@,48-52'], ['xxx', '@48']],
+      \ 'isident': [['', '@', '@,48-52'], ['xxx', '@48']],
+      \ 'isexpand': [['', '.,->', '/,/*,\\,'], [',,', '\\,,']],
+      \ 'iskeyword': [['', '@', '@,48-52'], ['xxx', '@48']],
+      \ 'isprint': [['', '@', '@,48-52'], ['xxx', '@48']],
+      \ 'jumpoptions': [['', 'stack'], ['xxx']],
+      \ 'keymap': [['', 'accents'], ['/']],
+      \ 'keymodel': [['', 'startsel', 'stopsel', 'startsel,stopsel'], ['xxx']],
+      \ 'keyprotocol': [['', 'xxx:none', 'yyy:mok2', 'zzz:kitty'],
+      \		['xxx', ':none', 'xxx:', 'x:non', 'y:mok3', 'z:kittty']],
+      \ 'langmap': [['', 'xX', 'aA,bB'], ['xxx']],
+      \ 'lispoptions': [['', 'expr:0', 'expr:1'], ['xxx', 'expr:x', 'expr:']],
+      \ 'listchars': [['', 'eol:x', 'tab:xy', 'tab:xyz', 'space:x',
+      \		'multispace:xxxy', 'lead:x', 'leadmultispace:xxxy', 'trail:x',
+      \		'extends:x', 'precedes:x', 'conceal:x', 'nbsp:x', 'eol:\\x24',
+      \		'eol:\\u21b5', 'eol:\\U000021b5', 'eol:x,space:y'],
+      \		['xxx', 'eol:']],
+      \ 'matchpairs': [['', '(:)', '(:),<:>'], ['xxx']],
+      \ 'messagesopt': [['hit-enter,history:1', 'hit-enter,history:10000',
+      \		'history:100,wait:100', 'history:0,wait:0',
+      \		'hit-enter,history:1,wait:1'],
+      \		['xxx', 'history:500', 'hit-enter,history:-1',
+      \		'hit-enter,history:10001', 'history:0,wait:10001',
+      \		'hit-enter', 'history:10,wait:99999999999999999999',
+      \		'history:99999999999999999999,wait:10', 'wait:10',
+      \		'history:-10', 'history:10,wait:-10']],
+      \ 'mkspellmem': [['10000,100,12'], ['', 'xxx', '10000,100']],
+      \ 'mouse': [['', 'n', 'v', 'i', 'c', 'h', 'a', 'r', 'nvi'],
+      \		['xxx', 'n,v,i']],
+      \ 'mousemodel': [['', 'extend', 'popup', 'popup_setpos'], ['xxx']],
+      \ 'mouseshape': [['', 'n:arrow'], ['xxx']],
+      \ 'nrformats': [['', 'alpha', 'octal', 'hex', 'bin', 'unsigned', 'blank',
+      \		'alpha,hex,bin'],
+      \		['xxx']],
+      \ 'patchmode': [['', 'xxx', '.x'], [&backupext, '*']],
+      \ 'previewpopup': [['', 'height:13', 'width:20', 'highlight:That',
+      \		'align:item', 'align:menu', 'border:on', 'border:off',
+      \		'width:10,height:234,highlight:Mine'],
+      \		['xxx', 'xxx:99', 'height:yes', 'width:no', 'align:xxx',
+      \		'border:maybe', 'border:1', 'border:']],
+      \ 'printmbfont': [['', 'r:some', 'b:some', 'i:some', 'o:some', 'c:yes',
+      \		'c:no', 'a:yes', 'a:no', 'b:Bold,c:yes'],
+      \		['xxx', 'xxx,c:yes', 'xxx:', 'xxx:,c:yes']],
+      \ 'printoptions': [['', 'header:0', 'left:10pc,top:5pc'],
+      \		['xxx', 'header:-1']],
+      \ 'scrollopt': [['', 'ver', 'hor', 'jump', 'ver,hor'], ['xxx']],
+      \ 'renderoptions': [[''], ['xxx']],
+      \ 'rightleftcmd': [['search'], ['xxx']],
+      \ 'rulerformat': [['', 'xxx'], ['%-', '%(', '%15(%%']],
+      \ 'selection': [['old', 'inclusive', 'exclusive'], ['', 'xxx']],
+      \ 'selectmode': [['', 'mouse', 'key', 'cmd', 'key,cmd'], ['xxx']],
+      \ 'sessionoptions': [['', 'blank', 'curdir', 'sesdir',
+      \		'help,options,slash'],
+      \		['xxx', 'curdir,sesdir']],
+      \ 'showcmdloc': [['', 'last', 'statusline', 'tabline'], ['xxx']],
+      \ 'signcolumn': [['', 'auto', 'no', 'yes', 'number'], ['xxx', 'no,yes']],
+      \ 'spellfile': [['', 'file.en.add', 'xxx.en.add,yyy.gb.add,zzz.ja.add',
+      \		'/tmp/dir\ with\ space/en.utf-8.add',
+      \		'/tmp/dir\\,with\\,comma/en.utf-8.add'],
+      \		['xxx', '/tmp/file', '/tmp/dir*with:invalid?char/file.en.add',
+      \		',file.en.add', 'xxx,yyy.en.add', 'xxx.en.add,yyy,zzz.ja.add']],
+      \ 'spelllang': [['', 'xxx', 'sr@latin'], ['not&lang', "that\\\rthere"]],
+      \ 'spelloptions': [['', 'camel'], ['xxx']],
+      \ 'spellsuggest': [['', 'best', 'double', 'fast', '100', 'timeout:100',
+      \		'timeout:-1', 'file:/tmp/file', 'expr:Func()', 'double,33'],
+      \		['xxx', '-1', 'timeout:', 'best,double', 'double,fast']],
+      \ 'splitkeep': [['', 'cursor', 'screen', 'topline'], ['xxx']],
+      \ 'statusline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
+      \ 'swapsync': [['', 'sync', 'fsync'], ['xxx']],
+      \ 'switchbuf': [['', 'useopen', 'usetab', 'split', 'vsplit', 'newtab',
+      \		'uselast', 'split,newtab'],
+      \		['xxx']],
+      \ 'tabclose': [['', 'left', 'uselast', 'left,uselast'], ['xxx']],
+      \ 'tabline': [['', 'xxx'], ['%$', '%{', '%{%', '%{%}', '%(', '%)']],
+      \ 'tabpanel': [['', 'aaa', 'bbb'], []],
+      \ 'tabpanelopt': [['', 'align:left', 'align:right', 'vert', 'columns:0',
+      \		'columns:20', 'columns:999'],
+      \		['xxx', 'align:', 'align:middle', 'colomns:', 'cols:10',
+      \		'cols:-1']],
+      \ 'tagcase': [['followic', 'followscs', 'ignore', 'match', 'smart'],
+      \		['', 'xxx', 'smart,match']],
+      \ 'termencoding': [has('gui_gtk') ? [] : ['', 'utf-8'], ['xxx']],
+      \ 'termwinkey': [['', 'f', '^Y', '^@', '<Esc>', '<t_xx>', "\u3042", '<',
+      \		'^'],
+      \		['<xxx>', '<t_xxx>', '<Esc', '<t_xx']],
+      \ 'termwinsize': [['', '24x80', '0x80', '32x0', '0x0'],
+      \		['xxx', '80', '8ax9', '24x80b']],
+      \ 'termwintype': [['', 'winpty', 'conpty'], ['xxx']],
+      \ 'titlestring': [['', 'xxx', '%('], []],
+      \ 'toolbar': [['', 'icons', 'text', 'horiz', 'tooltips', 'icons,text'],
+      \		['xxx']],
+      \ 'toolbariconsize': [['', 'tiny', 'small', 'medium', 'large', 'huge',
+      \		'giant'],
+      \		['xxx']],
+      \ 'ttymouse': [['', 'xterm'], ['xxx']],
+      \ 'varsofttabstop': [['8', '4,8,16,32'], ['xxx', '-1', '4,-1,20', '1,']],
+      \ 'vartabstop': [['8', '4,8,16,32'], ['xxx', '-1', '4,-1,20', '1,']],
+      \ 'verbosefile': [['', './Xfile'], []],
+      \ 'viewoptions': [['', 'cursor', 'folds', 'options', 'localoptions',
+      \		'slash', 'unix', 'curdir', 'unix,slash'], ['xxx']],
+      \ 'viminfo': [['', '''50', '"30', "'100,<50,s10,h"], ['xxx', 'h']],
+      \ 'virtualedit': [['', 'block', 'insert', 'all', 'onemore', 'none',
+      \		'NONE', 'all,block'],
+      \		['xxx']],
+      \ 'whichwrap': [['', 'b', 's', 'h', 'l', '<', '>', '~', '[', ']', 'b,s',
+      \		'bs'],
+      \		['xxx']],
+      \ 'wildmode': [['', 'full', 'longest', 'list', 'lastused', 'list:full',
+      \		'noselect', 'noselect,full', 'noselect:lastused,full',
+      \		'full,longest', 'full,full,full,full'],
+      \		['xxx', 'a4', 'full,full,full,full,full']],
+      \ 'wildoptions': [['', 'tagfile', 'pum', 'fuzzy'], ['xxx']],
+      \ 'winaltkeys': [['no', 'yes', 'menu'], ['', 'xxx']],
+      \
+      "\ skipped options
+      \ 'luadll': [[], []],
+      \ 'perldll': [[], []],
+      \ 'pythondll': [[], []],
+      \ 'pythonthreedll': [[], []],
+      \ 'pyxversion': [[], []],
+      \ 'rubydll': [[], []],
+      \ 'tcldll': [[], []],
+      \ 'term': [[], []],
+      \ 'ttytype': [[], []],
+      \
+      "\ default behaviours
+      \ 'othernum': [[-1, 0, 100], ['']],
+      \ 'otherstring': [['', 'xxx'], []],
+      \}
+
+" Two lists with values: values that pre- and post-processing in test.
+" Clear out t_WS: we don't want to resize the actual terminal.
+let test_prepost = {
+      \ 'browsedir': [["call mkdir('Xdir with space', 'D')"], []],
+      \ 'columns': [[
+      \		'set t_WS=',
+      \		'let save_columns = &columns'
+      \		], [
+      \		'let &columns = save_columns',
+      \		'set t_WS&'
+      \		]],
+      \ 'lines': [[
+      \		'set t_WS=',
+      \		'let save_lines = &lines'
+      \		], [
+      \		'let &lines = save_lines',
+      \		'set t_WS&'
+      \		]],
+      \ 'verbosefile': [[], ['call delete("Xfile")']],
+      \}
+
+let invalid_options = test_values->keys()
+      \->filter({-> v:val !~# '^other' && !exists($"&{v:val}")})
+for s:skip_option in [
+  \ [!has('tabpanel'), 'tabpanel'],
+  \ [!has('tabpanel'), 'tabpanelopt'],
+  \ [!has('tabpanel'), 'showtabpanel'],
+  \ ]
+  if s:skip_option[0]
+    call remove(invalid_options, s:skip_option[1])
+  endif
+endfor
+if !empty(invalid_options)
+  throw $"Invalid option name in test_values: '{invalid_options->join("', '")}'"
+endif
+
+1
+call search('struct vimoption options')
+while 1
+  if search('{"', 'W') > end
+    break
+  endif
+  let line = getline('.')
+  let fullname = substitute(line, '.*{"\([^"]*\)".*', '\1', '')
+  let shortname = substitute(line, '.*"\([^"]*\)".*', '\1', '')
+
+  let [valid_values, invalid_values] = test_values[
+	\ has_key(test_values, fullname) ? fullname
+	\ : line =~ 'P_NUM' ? 'othernum'
+	\ : 'otherstring']
+
+  if empty(valid_values) && empty(invalid_values)
+    continue
+  endif
+
+  call add(script, $"func Test_opt_set_{fullname}()")
+  call add(script, $"if exists('+{fullname}') && execute('set!') =~# '\\n..{fullname}\\([=\\n]\\|$\\)'")
+  call add(script, $"let l:saved = [&g:{fullname}, &l:{fullname}]")
+  call add(script, 'endif')
+
+  let [pre_processing, post_processing] = get(test_prepost, fullname, [[], []])
+  let script += pre_processing
+
+  " Setting an option can only fail when it's implemented.
+  call add(script, $"if exists('+{fullname}')")
+  if line =~ 'P_BOOL'
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	call add(script, $'{cmd} {opt}')
+	call add(script, $'{cmd} no{opt}')
+	call add(script, $'{cmd} inv{opt}')
+	call add(script, $'{cmd} {opt}!')
+      endfor
+    endfor
+  else  " P_NUM || P_STRING
+    " Normal tests
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	for val in valid_values
+	  if local_noglobals->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip `:setglobal {option}={val}` for local-noglobal option.
+	    " It has no effect.
+	    let pre = '" Skip local-noglobal: '
+	  else
+	    let pre = ''
+	  endif
+	  call add(script, $'{pre}{cmd} {opt}={val}')
+	endfor
+      endfor
+      " Testing to clear the local value and switch back to the global value.
+      if global_locals->has_key(fullname)
+	let switchback_val = global_locals[fullname]
+	call add(script, $'setlocal {opt}={switchback_val}')
+	call add(script, $'call assert_equal(&g:{fullname}, &{fullname})')
+      endif
+    endfor
+
+    " Failure tests
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	for val in invalid_values
+	  if val is# global_locals->get(fullname, {}) && cmd ==# 'setlocal'
+	    " Skip setlocal switchback-value to global-local option. It will
+	    " not result in failure.
+	    let pre = '" Skip global-local: '
+	  elseif local_noglobals->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip setglobal to local-noglobal option. It will not result in
+	    " failure.
+	    let pre = '" Skip local-noglobal: '
+	  elseif skip_setglobal_reasons->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip setglobal to reasoned option. It will not result in failure.
+	    let reason = skip_setglobal_reasons[fullname]
+	    let pre = $'" Skip {reason}: '
+	  else
+	    let pre = ''
+	  endif
+	  let cmdline = $'{cmd} {opt}={val}'
+	  call add(script, $"{pre}silent! call assert_fails({string(cmdline)})")
+	endfor
+      endfor
+    endfor
+  endif
+
+  " Cannot change 'termencoding' in GTK
+  if fullname != 'termencoding' || !has('gui_gtk')
+    call add(script, $'set {fullname}&')
+    call add(script, $'set {shortname}&')
+    call add(script, $"if exists('l:saved')")
+    call add(script, $"let [&g:{fullname}, &l:{fullname}] = l:saved")
+    call add(script, 'endif')
+  endif
+
+  call add(script, "endif")
+
+  let script += post_processing
+  call add(script, 'endfunc')
+endwhile
+
+call writefile(script, 'opt_test.vim')
+
+" Write error messages if error occurs.
+catch
+  " Append errors to test.log
+  let error = $'Error: {v:exception} in {v:throwpoint}'
+  echoc error
+  split test.log
+  call append('$', error)
+  write
+endtry
+
+endif
+
+qa!
+
+" vim:sw=2:ts=8:noet:nosta:
diff --git a/src/testdir/util/gui_init.vim b/src/testdir/util/gui_init.vim
new file mode 100644
index 0000000..4fa6cbc
--- /dev/null
+++ b/src/testdir/util/gui_init.vim
@@ -0,0 +1,6 @@
+" gvimrc for test_gui_init.vim
+
+if has('gui_motif') || has('gui_gtk2') || has('gui_gtk3')
+  set guiheadroom=0
+  set guioptions+=p
+endif
diff --git a/src/testdir/util/gui_preinit.vim b/src/testdir/util/gui_preinit.vim
new file mode 100644
index 0000000..c351b72
--- /dev/null
+++ b/src/testdir/util/gui_preinit.vim
@@ -0,0 +1,7 @@
+" vimrc for test_gui_init.vim
+
+" Note that this flag must be added in the .vimrc file, before switching on
+" syntax or filetype recognition (when the |gvimrc| file is sourced the system
+" menu has already been loaded; the ":syntax on" and ":filetype on" commands
+" load the menu too).
+set guioptions+=M
diff --git a/src/testdir/util/mouse.vim b/src/testdir/util/mouse.vim
new file mode 100644
index 0000000..e2979b7
--- /dev/null
+++ b/src/testdir/util/mouse.vim
@@ -0,0 +1,372 @@
+" Helper functions for generating mouse events
+
+" xterm2 and sgr always work, urxvt is optional.
+let g:Ttymouse_values = ['xterm2', 'sgr']
+if has('mouse_urxvt')
+  call add(g:Ttymouse_values, 'urxvt')
+endif
+
+" dec doesn't support all the functionality
+if has('mouse_dec')
+  let g:Ttymouse_dec = ['dec']
+else
+  let g:Ttymouse_dec = []
+endif
+
+" netterm only supports left click
+if has('mouse_netterm')
+  let g:Ttymouse_netterm = ['netterm']
+else
+  let g:Ttymouse_netterm = []
+endif
+
+" Vim Mouse Codes.
+" Used by the GUI and by MS-Windows Consoles.
+" Keep these in sync with vim.h
+let s:MOUSE_CODE = {
+  \ 'BTN_LEFT'    :  0x00,
+  \ 'BTN_MIDDLE'  :  0x01,
+  \ 'BTN_RIGHT'   :  0x02,
+  \ 'BTN_RELEASE' :  0x03,
+  \ 'BTN_X1'      : 0x300,
+  \ 'BTN_X2'      : 0x400,
+  \ 'SCRL_DOWN'   : 0x100,
+  \ 'SCRL_UP'     : 0x200,
+  \ 'SCRL_LEFT'   : 0x500,
+  \ 'SCRL_RIGHT'  : 0x600,
+  \ 'MOVE'        : 0x700,
+  \ 'MOD_SHIFT'   :  0x04,
+  \ 'MOD_ALT'     :  0x08,
+  \ 'MOD_CTRL'    :  0x10,
+  \ }
+
+
+" Helper function to emit a terminal escape code.
+func TerminalEscapeCode(code, row, col, m)
+  if &ttymouse ==# 'xterm2'
+    " need to use byte encoding here.
+    let str = list2str([a:code + 0x20, a:col + 0x20, a:row + 0x20])
+    if has('iconv')
+      let bytes = str->iconv('utf-8', 'latin1')
+    else
+      " Hopefully the numbers are not too big.
+      let bytes = str
+    endif
+    return "\<Esc>[M" .. bytes
+  elseif &ttymouse ==# 'sgr'
+    return printf("\<Esc>[<%d;%d;%d%s", a:code, a:col, a:row, a:m)
+  elseif &ttymouse ==# 'urxvt'
+    return printf("\<Esc>[%d;%d;%dM", a:code + 0x20, a:col, a:row)
+  endif
+endfunc
+
+func DecEscapeCode(code, down, row, col)
+    return printf("\<Esc>[%d;%d;%d;%d&w", a:code, a:down, a:row, a:col)
+endfunc
+
+func NettermEscapeCode(row, col)
+    return printf("\<Esc>}%d,%d\r", a:row, a:col)
+endfunc
+
+" Send low level mouse event to MS-Windows consoles or GUI
+func MSWinMouseEvent(button, row, col, move, multiclick, modifiers)
+    let args = { }
+    let args.button = a:button
+    " Scroll directions are inverted in the GUI, no idea why.
+    if has('gui_running')
+      if a:button == s:MOUSE_CODE.SCRL_UP
+        let args.button = s:MOUSE_CODE.SCRL_DOWN
+      elseif a:button == s:MOUSE_CODE.SCRL_DOWN
+        let args.button = s:MOUSE_CODE.SCRL_UP
+      elseif a:button == s:MOUSE_CODE.SCRL_LEFT
+        let args.button = s:MOUSE_CODE.SCRL_RIGHT
+      elseif a:button == s:MOUSE_CODE.SCRL_RIGHT
+        let args.button = s:MOUSE_CODE.SCRL_LEFT
+      endif
+    endif
+    let args.row = a:row
+    let args.col = a:col
+    let args.move = a:move
+    let args.multiclick = a:multiclick
+    let args.modifiers = a:modifiers
+    call test_mswin_event("mouse", args)
+    unlet args
+endfunc
+
+func MouseLeftClickCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(2, 4, a:row, a:col)
+  elseif &ttymouse ==# 'netterm'
+    return NettermEscapeCode(a:row, a:col)
+  else
+    return TerminalEscapeCode(0, a:row, a:col, 'M')
+  endif
+endfunc
+
+func MouseLeftClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseLeftClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseMiddleClickCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(4, 2, a:row, a:col)
+  else
+    return TerminalEscapeCode(1, a:row, a:col, 'M')
+  endif
+endfunc
+
+func MouseMiddleClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_MIDDLE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseMiddleClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseRightClickCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(6, 1, a:row, a:col)
+  else
+    return TerminalEscapeCode(2, a:row, a:col, 'M')
+  endif
+endfunc
+
+func MouseRightClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseRightClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseCtrlLeftClickCode(row, col)
+  let ctrl = 0x10
+  return TerminalEscapeCode(0 + ctrl, a:row, a:col, 'M')
+endfunc
+
+func MouseCtrlLeftClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0,
+                                                         \ s:MOUSE_CODE.MOD_CTRL)
+  else
+    call feedkeys(MouseCtrlLeftClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseCtrlRightClickCode(row, col)
+  let ctrl = 0x10
+  return TerminalEscapeCode(2 + ctrl, a:row, a:col, 'M')
+endfunc
+
+func MouseCtrlRightClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_CTRL)
+  else
+    call feedkeys(MouseCtrlRightClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseAltLeftClickCode(row, col)
+  let alt = 0x8
+  return TerminalEscapeCode(0 + alt, a:row, a:col, 'M')
+endfunc
+
+func MouseAltLeftClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_ALT)
+  else
+    call feedkeys(MouseAltLeftClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseAltRightClickCode(row, col)
+  let alt = 0x8
+  return TerminalEscapeCode(2 + alt, a:row, a:col, 'M')
+endfunc
+
+func MouseAltRightClick(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RIGHT, a:row, a:col, 0, 0,
+                                                       \ s:MOUSE_CODE.MOD_ALT)
+  else
+    call feedkeys(MouseAltRightClickCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseLeftReleaseCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(3, 0, a:row, a:col)
+  elseif &ttymouse ==# 'netterm'
+    return ''
+  else
+    return TerminalEscapeCode(3, a:row, a:col, 'm')
+  endif
+endfunc
+
+func MouseLeftRelease(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseLeftReleaseCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseMiddleReleaseCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(5, 0, a:row, a:col)
+  else
+    return TerminalEscapeCode(3, a:row, a:col, 'm')
+  endif
+endfunc
+
+func MouseMiddleRelease(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseMiddleReleaseCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseRightReleaseCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(7, 0, a:row, a:col)
+  else
+    return TerminalEscapeCode(3, a:row, a:col, 'm')
+  endif
+endfunc
+
+func MouseRightRelease(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_RELEASE, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseRightReleaseCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseLeftDragCode(row, col)
+  if &ttymouse ==# 'dec'
+    return DecEscapeCode(1, 4, a:row, a:col)
+  else
+    return TerminalEscapeCode(0x20, a:row, a:col, 'M')
+  endif
+endfunc
+
+func MouseLeftDrag(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.BTN_LEFT, a:row, a:col, 1, 0, 0)
+  else
+    call feedkeys(MouseLeftDragCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseWheelUpCode(row, col)
+  return TerminalEscapeCode(0x40, a:row, a:col, 'M')
+endfunc
+
+func MouseWheelUp(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_UP, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelUpCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseWheelDownCode(row, col)
+  return TerminalEscapeCode(0x41, a:row, a:col, 'M')
+endfunc
+
+func MouseWheelDown(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_DOWN, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelDownCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseWheelLeftCode(row, col)
+  return TerminalEscapeCode(0x42, a:row, a:col, 'M')
+endfunc
+
+func MouseWheelLeft(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_LEFT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelLeftCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseWheelRightCode(row, col)
+  return TerminalEscapeCode(0x43, a:row, a:col, 'M')
+endfunc
+
+func MouseWheelRight(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_RIGHT, a:row, a:col, 0, 0, 0)
+  else
+    call feedkeys(MouseWheelRightCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelUpCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x40, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelUp(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_UP, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelUpCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelDownCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x41, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelDown(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_DOWN, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelDownCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelLeftCode(row, col)
+  " todo feed shift mod.
+  return TerminalEscapeCode(0x42, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelLeft(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_LEFT, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelLeftCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+func MouseShiftWheelRightCode(row, col)
+	" todo feed shift mod.
+  return TerminalEscapeCode(0x43, a:row, a:col, 'M')
+endfunc
+
+func MouseShiftWheelRight(row, col)
+  if has('win32')
+    call MSWinMouseEvent(s:MOUSE_CODE.SCRL_RIGHT, a:row, a:col, 0, 0,
+                                                      \ s:MOUSE_CODE.MOD_SHIFT)
+  else
+    call feedkeys(MouseShiftWheelRightCode(a:row, a:col), 'Lx!')
+  endif
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/util/popupbounce.vim b/src/testdir/util/popupbounce.vim
new file mode 100644
index 0000000..5e63aca
--- /dev/null
+++ b/src/testdir/util/popupbounce.vim
@@ -0,0 +1,80 @@
+" Use this script to measure the redrawing performance when a popup is being
+" displayed.  Usage with gcc:
+"    cd src
+"    # Edit Makefile to uncomment PROFILE_CFLAGS and PROFILE_LIBS
+"    make reconfig
+"    ./vim --clean -S testdir/util/popupbounce.vim main.c
+"    gprof vim gmon.out | vim -
+
+" using line continuation
+set nocp
+
+" don't switch screens when quitting, so we can read the frames/sec
+set t_te=
+
+let winid = popup_create(['line1', 'line2', 'line3', 'line4'], {
+	      \   'line' : 1,
+	      \   'col' : 1,
+	      \   'zindex' : 101,
+	      \ })
+redraw
+
+let start = reltime()
+let framecount = 0
+
+let line = 1.0
+let col = 1
+let downwards = 1
+let col_inc = 1
+let initial_speed = 0.2
+let speed = initial_speed
+let accel = 1.1
+let time = 0.1
+
+let countdown = 0
+
+while 1
+  if downwards
+    let speed += time * accel
+    let line += speed
+  else
+    let speed -= time * accel
+    let line -= speed
+  endif
+
+  if line + 3 >= &lines
+    let downwards = 0
+    let speed = speed * 0.8
+    let line = &lines - 3
+  endif
+  if !downwards && speed < 1.0
+    let downwards = 1
+    let speed = initial_speed
+    if line + 4 > &lines && countdown == 0
+      let countdown = 50
+    endif
+  endif
+
+  let col += col_inc
+  if col + 4 >= &columns
+    let col_inc = -1
+  elseif col <= 1
+    let col_inc = 1
+  endif
+
+  call popup_move(winid, {'line': float2nr(line), 'col': col})
+  redraw
+  let framecount += 1
+  if countdown > 0
+    let countdown -= 1
+    if countdown == 0
+      break
+    endif
+  endif
+
+endwhile
+
+let elapsed = reltimefloat(reltime(start))
+echomsg framecount .. ' frames in ' .. string(elapsed) .. ' seconds, ' .. string(framecount / elapsed) .. ' frames/sec'
+
+qa
diff --git a/src/testdir/util/screendump.vim b/src/testdir/util/screendump.vim
new file mode 100644
index 0000000..be6af86
--- /dev/null
+++ b/src/testdir/util/screendump.vim
@@ -0,0 +1,139 @@
+" Functions shared by tests making screen dumps.
+
+" Only load this script once.
+if exists('*VerifyScreenDump')
+  finish
+endif
+
+" Skip the rest if there is no terminal feature at all.
+if !has('terminal')
+  finish
+endif
+
+" Read a dump file "fname" and if "filter" exists apply it to the text.
+def ReadAndFilter(fname: string, filter: string): list<string>
+  var contents = readfile(fname)
+
+  if filereadable(filter)
+    # do this in the bottom window so that the terminal window is unaffected
+    wincmd j
+    enew
+    setline(1, contents)
+    exe "source " .. filter
+    contents = getline(1, '$')
+    enew!
+    wincmd k
+    redraw
+  endif
+
+  return contents
+enddef
+
+
+" Verify that Vim running in terminal buffer "buf" matches the screen dump.
+" "options" is passed to term_dumpwrite().
+" Additionally, the "wait" entry can specify the maximum time to wait for the
+" screen dump to match in msec (default 1000 msec).
+" The file name used is "dumps/{filename}.dump".
+"
+" To ignore part of the dump, provide a "dumps/{filename}.vim" file with
+" Vim commands to be applied to both the reference and the current dump, so
+" that parts that are irrelevant are not used for the comparison.  The result
+" is NOT written, thus "term_dumpdiff()" shows the difference anyway.
+"
+" Optionally an extra argument can be passed which is prepended to the error
+" message.  Use this when using the same dump file with different options.
+" Returns non-zero when verification fails.
+func VerifyScreenDump(buf, filename, options, ...)
+  if has('gui_running') && exists("g:check_screendump_called") && g:check_screendump_called == v:false
+      echoerr "VerifyScreenDump() called from a test that lacks a CheckScreendump guard."
+      return 1
+  endif
+  let reference = 'dumps/' . a:filename . '.dump'
+  let filter = 'dumps/' . a:filename . '.vim'
+  let testfile = 'failed/' . a:filename . '.dump'
+
+  let max_loops = get(a:options, 'wait', 1000) / 1
+
+  " Starting a terminal to make a screendump is always considered flaky.
+  let g:test_is_flaky = 1
+  let g:giveup_same_error = 0
+
+  " wait for the pending updates to be handled.
+  call TermWait(a:buf, 0)
+
+  " Redraw to execute the code that updates the screen.  Otherwise we get the
+  " text and attributes only from the internal buffer.
+  redraw
+
+  if filereadable(reference)
+    let refdump = ReadAndFilter(reference, filter)
+  else
+    " Must be a new screendump, always fail
+    let refdump = []
+  endif
+
+  let did_mkdir = 0
+  if !isdirectory('failed')
+    let did_mkdir = 1
+    call mkdir('failed')
+  endif
+
+  let i = 0
+  while 1
+    " leave a bit of time for updating the original window while we spin wait.
+    sleep 1m
+    call delete(testfile)
+    call term_dumpwrite(a:buf, testfile, a:options)
+
+    if refdump->empty()
+      let msg = 'See new dump file: call term_dumpload("testdir/' .. testfile .. '")'
+      call assert_report(msg)
+      " no point in retrying
+      let g:run_nr = 10
+      return 1
+    endif
+
+    let testdump = ReadAndFilter(testfile, filter)
+    if refdump == testdump
+      call delete(testfile)
+      if did_mkdir
+	call delete('failed', 'd')
+      endif
+      if i > 0
+	call remove(v:errors, -1)
+      endif
+      break
+    endif
+
+    " Leave the failed dump around for inspection.
+    let msg = 'See dump file difference: call term_dumpdiff("testdir/' .. testfile .. '", "testdir/' .. reference .. '")'
+    if a:0 == 1
+      let msg = a:1 . ': ' . msg
+    endif
+    if len(testdump) != len(refdump)
+      let msg = msg . '; line count is ' . len(testdump) . ' instead of ' . len(refdump)
+    endif
+    for j in range(len(refdump))
+      if j >= len(testdump)
+	break
+      endif
+      if testdump[j] != refdump[j]
+	let msg = msg . '; difference in line ' . (j + 1) . ': "' . testdump[j] . '"'
+      endif
+    endfor
+
+    " Always add the last error so that it is displayed on timeout.
+    " See TestTimeout() in runtest.vim.
+    if i > 0
+      call remove(v:errors, -1)
+    endif
+    call assert_report(msg)
+
+    let i += 1
+    if i >= max_loops
+      return 1
+    endif
+  endwhile
+  return 0
+endfunc
diff --git a/src/testdir/util/script_util.vim b/src/testdir/util/script_util.vim
new file mode 100644
index 0000000..a300b67
--- /dev/null
+++ b/src/testdir/util/script_util.vim
@@ -0,0 +1,69 @@
+" Functions shared by the tests for Vim script
+
+" Commands to track the execution path of a script
+com!		   XpathINIT  let g:Xpath = ''
+com! -nargs=1 -bar Xpath      let g:Xpath ..= <args>
+com!               XloopINIT  let g:Xloop = 1
+com! -nargs=1 -bar Xloop      let g:Xpath ..= <args> .. g:Xloop
+com!               XloopNEXT  let g:Xloop += 1
+
+" MakeScript() - Make a script file from a function.			    {{{2
+"
+" Create a script that consists of the body of the function a:funcname.
+" Replace any ":return" by a ":finish", any argument variable by a global
+" variable, and every ":call" by a ":source" for the next following argument
+" in the variable argument list.  This function is useful if similar tests are
+" to be made for a ":return" from a function call or a ":finish" in a script
+" file.
+func MakeScript(funcname, ...)
+    let script = tempname()
+    execute "redir! >" . script
+    execute "function" a:funcname
+    redir END
+    execute "edit" script
+    " Delete the "function" and the "endfunction" lines.  Do not include the
+    " word "function" in the pattern since it might be translated if LANG is
+    " set.  When MakeScript() is being debugged, this deletes also the debugging
+    " output of its line 3 and 4.
+    exec '1,/.*' . a:funcname . '(.*)/d'
+    /^\d*\s*endfunction\>/,$d
+    %s/^\d*//e
+    %s/return/finish/e
+    %s/\<a:\(\h\w*\)/g:\1/ge
+    normal gg0
+    let cnt = 0
+    while search('\<call\s*\%(\u\|s:\)\w*\s*(.*)', 'W') > 0
+	let cnt = cnt + 1
+	s/\<call\s*\%(\u\|s:\)\w*\s*(.*)/\='source ' . a:{cnt}/
+    endwhile
+    g/^\s*$/d
+    write
+    bwipeout
+    return script
+endfunc
+
+" ExecAsScript - Source a temporary script made from a function.	    {{{2
+"
+" Make a temporary script file from the function a:funcname, ":source" it, and
+" delete it afterwards.  However, if an exception is thrown the file may remain,
+" the caller should call DeleteTheScript() afterwards.
+let s:script_name = ''
+func ExecAsScript(funcname)
+    " Make a script from the function passed as argument.
+    let s:script_name = MakeScript(a:funcname)
+
+    " Source and delete the script.
+    exec "source" s:script_name
+    call delete(s:script_name)
+    let s:script_name = ''
+endfunc
+
+func DeleteTheScript()
+    if s:script_name
+	call delete(s:script_name)
+	let s:script_name = ''
+    endif
+endfunc
+
+com! -nargs=1 -bar ExecAsScript call ExecAsScript(<f-args>)
+
diff --git a/src/testdir/util/setup.vim b/src/testdir/util/setup.vim
new file mode 100644
index 0000000..485675a
--- /dev/null
+++ b/src/testdir/util/setup.vim
@@ -0,0 +1,47 @@
+" Common preparations for running tests.
+
+" Only load this once.
+if 1
+
+  " When using xterm version 377 the response to the modifyOtherKeys status
+  " interferes with some tests.  Remove the request from the t_TI termcap
+  " entry.
+  let &t_TI = substitute(&t_TI, "\<Esc>\\[?4m", '', '')
+
+  if exists('s:did_load')
+    finish
+  endif
+  let s:did_load = 1
+endif
+
+" Make sure 'runtimepath' and 'packpath' does not include $HOME.
+set rtp=$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after
+if has('packages')
+  let &packpath = &rtp
+endif
+
+" Only when the +eval feature is present.
+if 1
+  " Make sure the .Xauthority file can be found after changing $HOME.
+  if $XAUTHORITY == ''
+    let $XAUTHORITY = $HOME . '/.Xauthority'
+  endif
+
+  " Avoid storing shell history.
+  let $HISTFILE = ""
+
+  " Have current $HOME available as $ORIGHOME.  $HOME is used for option
+  " defaults before we get here, and test_mksession checks that.
+  let $ORIGHOME = $HOME
+
+  if !exists('$XDG_CONFIG_HOME')
+    let $XDG_CONFIG_HOME = $HOME .. '/.config'
+  endif
+
+  " Make sure $HOME does not get read or written.
+  " It must exist, gnome tries to create $HOME/.gnome2
+  let $HOME = getcwd() . '/XfakeHOME'
+  if !isdirectory($HOME)
+    call mkdir($HOME)
+  endif
+endif
diff --git a/src/testdir/util/setup_gui.vim b/src/testdir/util/setup_gui.vim
new file mode 100644
index 0000000..2e5e777
--- /dev/null
+++ b/src/testdir/util/setup_gui.vim
@@ -0,0 +1,31 @@
+" Common preparations for running GUI tests.
+
+let g:x11_based_gui = has('gui_motif')
+	\ || has('gui_gtk2') || has('gui_gnome') || has('gui_gtk3')
+
+" Reasons for 'skipped'.
+let g:not_supported   = "Skipped: Feature/Option not supported by this GUI: "
+let g:not_hosted      = "Skipped: Test not hosted by the system/environment"
+
+" For KDE set a font, empty 'guifont' may cause a hang.
+func GUISetUpCommon()
+  if has("gui_kde")
+    set guifont=Courier\ 10\ Pitch/8/-1/5/50/0/0/0/0/0
+  endif
+
+  " Gnome insists on creating $HOME/.gnome2/, set $HOME to avoid changing the
+  " actual home directory.  But avoid triggering fontconfig by setting the
+  " cache directory.  Only needed for Unix.
+  if $XDG_CACHE_HOME == '' && exists('g:tester_HOME')
+    let $XDG_CACHE_HOME = g:tester_HOME . '/.cache'
+  endif
+  call mkdir('Xhome')
+  let $HOME = fnamemodify('Xhome', ':p')
+endfunc
+
+func GUITearDownCommon()
+  call delete('Xhome', 'rf')
+endfunc
+
+" Ignore the "failed to create input context" error.
+call test_ignore_error('E285')
diff --git a/src/testdir/util/shared.vim b/src/testdir/util/shared.vim
new file mode 100644
index 0000000..ddd3f37
--- /dev/null
+++ b/src/testdir/util/shared.vim
@@ -0,0 +1,450 @@
+" Functions shared by several tests.
+
+" Only load this script once.
+if exists('*PythonProg')
+  finish
+endif
+
+source util/view_util.vim
+
+" When 'term' is changed some status requests may be sent.  The responses may
+" interfere with what is being tested.  A short sleep is used to process any of
+" those responses first.
+func WaitForResponses()
+  sleep 50m
+endfunc
+
+" Get the name of the Python executable.
+" Also keeps it in s:python.
+func PythonProg()
+  " This test requires the Python command to run the test server.
+  " This most likely only works on Unix and Windows.
+  if has('unix')
+    " We also need the job feature or the pkill command to make sure the server
+    " can be stopped.
+    if !(has('job') || executable('pkill'))
+      return ''
+    endif
+    if executable('python3')
+      let s:python = 'python3'
+    elseif executable('python')
+      let s:python = 'python'
+    else
+      return ''
+    end
+  elseif has('win32')
+    " Use Python Launcher for Windows (py.exe) if available.
+    " NOTE: if you get a "Python was not found" error, disable the Python
+    " shortcuts in "Windows menu / Settings / Manage App Execution Aliases".
+    if executable('py.exe')
+      let s:python = 'py.exe'
+    elseif executable('python.exe')
+      let s:python = 'python.exe'
+    else
+      return ''
+    endif
+  else
+    return ''
+  endif
+  return s:python
+endfunc
+
+" Run "cmd".  Returns the job if using a job.
+func RunCommand(cmd)
+  " Running an external command can occasionally be slow or fail.
+  let g:test_is_flaky = 1
+
+  let job = 0
+  if has('job')
+    let job = job_start(a:cmd, {"stoponexit": "hup"})
+    call job_setoptions(job, {"stoponexit": "kill"})
+  elseif has('win32')
+    exe 'silent !start cmd /D /c start "test_channel" ' . a:cmd
+  else
+    exe 'silent !' . a:cmd . '&'
+  endif
+  return job
+endfunc
+
+" Read the port number from the Xportnr file.
+func GetPort()
+  let l = []
+  " with 200 it sometimes failed, with 400 is rarily failed
+  for i in range(600)
+    try
+      let l = readfile("Xportnr")
+    catch
+    endtry
+    if len(l) >= 1
+      break
+    endif
+    sleep 10m
+  endfor
+  call delete("Xportnr")
+
+  if len(l) == 0
+    " Can't make the connection, give up.
+    return 0
+  endif
+  return l[0]
+endfunc
+
+" Run a Python server for "cmd" and call "testfunc".
+" Always kills the server before returning.
+func RunServer(cmd, testfunc, args)
+  " The Python program writes the port number in Xportnr.
+  call delete("Xportnr")
+
+  if len(a:args) == 1
+    let arg = ' ' . a:args[0]
+  else
+    let arg = ''
+  endif
+  let pycmd = s:python . " " . a:cmd . arg
+
+  try
+    let g:currentJob = RunCommand(pycmd)
+
+    " Wait for some time for the port number to be there.
+    let port = GetPort()
+    if port == 0
+      call assert_report(strftime("%H:%M:%S") .. " Can't start " .. a:cmd)
+      return
+    endif
+
+    call call(function(a:testfunc), [port])
+  catch /E901.*Address family for hostname not supported/
+    throw 'Skipped: Invalid network setup ("' .. v:exception .. '" in ' .. v:throwpoint .. ')'
+  catch
+    call assert_report('Caught exception: "' . v:exception . '" in ' . v:throwpoint)
+  finally
+    call s:kill_server(a:cmd)
+  endtry
+endfunc
+
+func s:kill_server(cmd)
+  if has('job')
+    if exists('g:currentJob')
+      call job_stop(g:currentJob)
+      unlet g:currentJob
+    endif
+  elseif has('win32')
+    let cmd = substitute(a:cmd, ".py", '', '')
+    call system('taskkill /IM ' . s:python . ' /T /F /FI "WINDOWTITLE eq ' . cmd . '"')
+  else
+    call system("pkill -f " . a:cmd)
+  endif
+endfunc
+
+" Callback function to be invoked by a child terminal job. The parent could
+" then wait for the notification using WaitForChildNotification()
+let g:child_notification = 0
+func Tapi_notify_parent(bufnum, arglist)
+  let g:child_notification = 1
+endfunc
+
+" Generates a command that we can pass to a terminal job that it uses to
+" notify us. Argument 'escape' will specify whether to escape the double
+" quote.
+func TermNotifyParentCmd(escape)
+  call assert_false(has("win32"), 'Windows does not support terminal API right now. Use another method to synchronize timing.')
+  let cmd = '\033]51;["call", "Tapi_notify_parent", []]\007'
+  if a:escape
+    return escape(cmd, '"')
+  endif
+  return cmd
+endfunc
+
+" Wait for a child process to notify us. This allows us to sequence events in
+" conjunction with the child. Currently the only supported notification method
+" is for a terminal job to call Tapi_notify_parent() using terminal API.
+func WaitForChildNotification(...)
+  let timeout = get(a:000, 0, 5000)
+  call WaitFor({-> g:child_notification == 1}, timeout)
+  let g:child_notification = 0
+endfunc
+
+" Wait for up to five seconds for "expr" to become true.  "expr" can be a
+" stringified expression to evaluate, or a funcref without arguments.
+" Using a lambda works best.  Example:
+"	call WaitFor({-> status == "ok"})
+"
+" A second argument can be used to specify a different timeout in msec.
+"
+" When successful the time slept is returned.
+" When running into the timeout an exception is thrown, thus the function does
+" not return.
+func WaitFor(expr, ...)
+  let timeout = get(a:000, 0, 5000)
+  let slept = s:WaitForCommon(a:expr, v:null, timeout)
+  if slept < 0
+    throw 'WaitFor() timed out after ' . timeout . ' msec'
+  endif
+  return slept
+endfunc
+
+" Wait for up to five seconds for "assert" to return zero.  "assert" must be a
+" (lambda) function containing one assert function.  Example:
+"	call WaitForAssert({-> assert_equal("dead", job_status(job)})
+"
+" A second argument can be used to specify a different timeout in msec.
+"
+" Return zero for success, one for failure (like the assert function).
+func g:WaitForAssert(assert, ...)
+  let timeout = get(a:000, 0, 5000)
+  if s:WaitForCommon(v:null, a:assert, timeout) < 0
+    return 1
+  endif
+  return 0
+endfunc
+
+" Common implementation of WaitFor() and WaitForAssert().
+" Either "expr" or "assert" is not v:null
+" Return the waiting time for success, -1 for failure.
+func s:WaitForCommon(expr, assert, timeout)
+  " using reltime() is more accurate, but not always available
+  let slept = 0
+  if exists('*reltimefloat')
+    let start = reltime()
+  endif
+
+  while 1
+    if type(a:expr) == v:t_func
+      let success = a:expr()
+    elseif type(a:assert) == v:t_func
+      let success = a:assert() == 0
+    else
+      let success = eval(a:expr)
+    endif
+    if success
+      return slept
+    endif
+
+    if slept >= a:timeout
+      break
+    endif
+    if type(a:assert) == v:t_func
+      " Remove the error added by the assert function.
+      call remove(v:errors, -1)
+    endif
+
+    sleep 1m
+    if exists('*reltimefloat')
+      let slept = float2nr(reltimefloat(reltime(start)) * 1000)
+    else
+      let slept += 1
+    endif
+  endwhile
+
+  return -1  " timed out
+endfunc
+
+
+" Wait for up to a given milliseconds.
+" With the +timers feature this waits for key-input by getchar(), Resume()
+" feeds key-input and resumes process. Return time waited in milliseconds.
+" Without +timers it uses simply :sleep.
+func Standby(msec)
+  if has('timers') && exists('*reltimefloat')
+    let start = reltime()
+    let g:_standby_timer = timer_start(a:msec, function('s:feedkeys'))
+    call getchar()
+    return float2nr(reltimefloat(reltime(start)) * 1000)
+  else
+    execute 'sleep ' a:msec . 'm'
+    return a:msec
+  endif
+endfunc
+
+func Resume()
+  if exists('g:_standby_timer')
+    call timer_stop(g:_standby_timer)
+    call s:feedkeys(0)
+    unlet g:_standby_timer
+  endif
+endfunc
+
+func s:feedkeys(timer)
+  call feedkeys('x', 'nt')
+endfunc
+
+" Get the name of the Vim executable that we expect has been build in the src
+" directory.
+func s:GetJustBuildVimExe()
+  if has("win32")
+    if !filereadable('..\vim.exe') && filereadable('..\vimd.exe')
+      " looks like the debug executable was intentionally build, so use it
+      return '..\vimd.exe'
+    endif
+    return '..\vim.exe'
+  endif
+  return '../vim'
+endfunc
+
+" Get $VIMPROG to run the Vim executable.
+" The Makefile writes it as the first line in the "vimcmd" file.
+" Falls back to the Vim executable in the src directory.
+func GetVimProg()
+  if filereadable('vimcmd')
+    return readfile('vimcmd')[0]
+  endif
+  echo 'Cannot read the "vimcmd" file, falling back to ../vim.'
+
+  " Probably the script was sourced instead of running "make".
+  " We assume Vim was just build in the src directory then.
+  return s:GetJustBuildVimExe()
+endfunc
+
+let g:valgrind_cnt = 1
+
+" Get the command to run Vim, with -u NONE and --not-a-term arguments.
+" If there is an argument use it instead of "NONE".
+func GetVimCommand(...)
+  if filereadable('vimcmd')
+    let lines = readfile('vimcmd')
+  else
+    echo 'Cannot read the "vimcmd" file, falling back to ../vim.'
+    let lines = [s:GetJustBuildVimExe()]
+  endif
+
+  if a:0 == 0
+    let name = 'NONE'
+  else
+    let name = a:1
+  endif
+  " For Unix Makefile writes the command to use in the second line of the
+  " "vimcmd" file, including environment options.
+  " Other Makefiles just write the executable in the first line, so fall back
+  " to that if there is no second line or it is empty.
+  if len(lines) > 1 && lines[1] != ''
+    let cmd = lines[1]
+  else
+    let cmd = lines[0]
+  endif
+
+  let cmd = substitute(cmd, '-u \f\+', '-u ' . name, '')
+  if cmd !~ '-u '. name
+    let cmd = cmd . ' -u ' . name
+  endif
+  let cmd .= ' --not-a-term'
+  let cmd .= ' --gui-dialog-file guidialogfile'
+  " remove any environment variables
+  let cmd = substitute(cmd, '[A-Z_]\+=\S\+ *', '', 'g')
+
+  " If using valgrind, make sure every run uses a different log file.
+  if cmd =~ 'valgrind.*--log-file='
+    let cmd = substitute(cmd, '--log-file=\(\S*\)', '--log-file=\1.' . g:valgrind_cnt, '')
+    let g:valgrind_cnt += 1
+  endif
+
+  return cmd
+endfunc
+
+" Return one when it looks like the tests are run with valgrind, which means
+" that everything is much slower.
+func RunningWithValgrind()
+  return GetVimCommand() =~ '\<valgrind\>'
+endfunc
+
+func RunningAsan()
+  return exists("$ASAN_OPTIONS")
+endfunc
+
+func ValgrindOrAsan()
+  return RunningWithValgrind() || RunningAsan()
+endfun
+
+" Get the command to run Vim, with --clean instead of "-u NONE".
+func GetVimCommandClean()
+  let cmd = GetVimCommand()
+  let cmd = substitute(cmd, '-u NONE', '--clean', '')
+  let cmd = substitute(cmd, '--not-a-term', '', '')
+
+  " Force using utf-8, Vim may pick up something else from the environment.
+  let cmd ..= ' --cmd "set enc=utf8" '
+
+  " Optionally run Vim under valgrind
+  " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd
+
+  return cmd
+endfunc
+
+" Get the command to run Vim, with --clean, and force to run in terminal so it
+" won't start a new GUI.
+func GetVimCommandCleanTerm()
+  " Add -v to have gvim run in the terminal (if possible)
+  return GetVimCommandClean() .. ' -v '
+endfunc
+
+" Run Vim, using the "vimcmd" file and "-u NORC".
+" "before" is a list of Vim commands to be executed before loading plugins.
+" "after" is a list of Vim commands to be executed after loading plugins.
+" Plugins are not loaded, unless 'loadplugins' is set in "before".
+" Return 1 if Vim could be executed.
+func RunVim(before, after, arguments)
+  return RunVimPiped(a:before, a:after, a:arguments, '')
+endfunc
+
+func RunVimPiped(before, after, arguments, pipecmd)
+  let cmd = GetVimCommand()
+  let args = ''
+  if len(a:before) > 0
+    call writefile(a:before, 'Xbefore.vim')
+    let args .= ' --cmd "so Xbefore.vim"'
+  endif
+  if len(a:after) > 0
+    call writefile(a:after, 'Xafter.vim')
+    let args .= ' -S Xafter.vim'
+  endif
+
+  " Optionally run Vim under valgrind
+  " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd
+
+  exe "silent !" .. a:pipecmd .. ' ' ..  cmd .. args .. ' ' .. a:arguments
+
+  if len(a:before) > 0
+    call delete('Xbefore.vim')
+  endif
+  if len(a:after) > 0
+    call delete('Xafter.vim')
+  endif
+  return 1
+endfunc
+
+func IsRoot()
+  if !has('unix')
+    return v:false
+  elseif $USER == 'root' || system('id -un') =~ '\<root\>'
+    return v:true
+  endif
+  return v:false
+endfunc
+
+" Get all messages but drop the maintainer entry.
+func GetMessages()
+  redir => result
+  redraw | messages
+  redir END
+  let msg_list = split(result, "\n")
+  if msg_list->len() > 0 && msg_list[0] =~ 'Messages maintainer:'
+    return msg_list[1:]
+  endif
+  return msg_list
+endfunc
+
+" Run the list of commands in 'cmds' and look for 'errstr' in exception.
+" Note that assert_fails() cannot be used in some places and this function
+" can be used.
+func AssertException(cmds, errstr)
+  let save_exception = ''
+  try
+    for cmd in a:cmds
+      exe cmd
+    endfor
+  catch
+    let save_exception = v:exception
+  endtry
+  call assert_match(a:errstr, save_exception)
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/util/summarize.vim b/src/testdir/util/summarize.vim
new file mode 100644
index 0000000..d0d4e00
--- /dev/null
+++ b/src/testdir/util/summarize.vim
@@ -0,0 +1,62 @@
+set cpo&vim
+if 1
+  " This is executed only with the eval feature
+  set nocompatible
+  set viminfo=
+  func Count(match, type)
+    if a:type ==# 'executed'
+      let g:executed += (a:match+0)
+    elseif a:type ==# 'failed'
+      let g:failed += a:match+0
+    elseif a:type ==# 'skipped'
+      let g:skipped += 1
+      call extend(g:skipped_output, ["\t" .. a:match])
+    endif
+  endfunc
+
+  let g:executed = 0
+  let g:skipped = 0
+  let g:failed = 0
+  let g:skipped_output = []
+  let g:failed_output = []
+  let output = [""]
+
+  if $TEST_FILTER != ''
+    call extend(g:skipped_output, ["\tAll tests not matching $TEST_FILTER: '" .. $TEST_FILTER .. "'"])
+  endif
+
+  try
+    " This uses the :s command to just fetch and process the output of the
+    " tests, it doesn't actually replace anything.
+    " And it uses "silent" to avoid reporting the number of matches.
+    silent %s/Executed\s\+\zs\d\+\ze\s\+tests\?/\=Count(submatch(0),'executed')/egn
+    silent %s/^SKIPPED \zs.*/\=Count(submatch(0), 'skipped')/egn
+    silent %s/^\(\d\+\)\s\+FAILED:/\=Count(submatch(1), 'failed')/egn
+
+    call extend(output, ["Skipped:"])
+    call extend(output, skipped_output)
+
+    call extend(output, [
+          \ "",
+          \ "-------------------------------",
+          \ printf("Executed: %5d Tests", g:executed),
+          \ printf(" Skipped: %5d Tests", g:skipped),
+          \ printf("  %s: %5d Tests", g:failed == 0 ? 'Failed' : 'FAILED', g:failed),
+          \ "",
+          \ ])
+    if filereadable('test.log')
+      " outputs and indents the failed test result
+      call extend(output, ["", "Failures: "])
+      let failed_output = filter(readfile('test.log'), { v,k -> !empty(k)})
+      call extend(output, map(failed_output, { v,k -> "\t".k}))
+      " Add a final newline
+      call extend(output, [""])
+    endif
+
+  catch  " Catch-all
+  finally
+    call writefile(output, 'test_result.log')  " overwrites an existing file
+  endtry
+endif
+
+q!
diff --git a/src/testdir/util/term_util.vim b/src/testdir/util/term_util.vim
new file mode 100644
index 0000000..61ff9ce
--- /dev/null
+++ b/src/testdir/util/term_util.vim
@@ -0,0 +1,211 @@
+" Functions about terminal shared by several tests
+
+" Only load this script once.
+if exists('*CanRunVimInTerminal')
+  finish
+endif
+
+source util/shared.vim
+
+" For most tests we need to be able to run terminal Vim with 256 colors.  On
+" MS-Windows the console only has 16 colors and the GUI can't run in a
+" terminal.
+func CanRunVimInTerminal()
+  return has('terminal') && !has('win32')
+endfunc
+
+" Skip the rest if there is no terminal feature at all.
+if !has('terminal')
+  finish
+endif
+
+" Stops the shell running in terminal "buf".
+func StopShellInTerminal(buf)
+  call term_sendkeys(a:buf, "exit\r")
+  let job = term_getjob(a:buf)
+  call WaitForAssert({-> assert_equal("dead", job_status(job))})
+  call TermWait(a:buf)
+endfunc
+
+" Wrapper around term_wait() to allow more time for re-runs of flaky tests
+" The second argument is the minimum time to wait in msec, 10 if omitted.
+func TermWait(buf, ...)
+  let wait_time = a:0 ? a:1 : 10
+  if exists('g:run_nr')
+    if g:run_nr == 2
+      let wait_time *= 4
+    elseif g:run_nr > 2
+      let wait_time *= 10
+    endif
+  endif
+  call term_wait(a:buf, wait_time)
+
+  " In case it wasn't set yet.
+  let g:test_is_flaky = 1
+endfunc
+
+" Run Vim with "arguments" in a new terminal window.
+" By default uses a size of 20 lines and 75 columns.
+" Returns the buffer number of the terminal.
+"
+" Options is a dictionary, these items are recognized:
+" "keep_t_u7" - when 1 do not make t_u7 empty (resetting t_u7 avoids clearing
+"               parts of line 2 and 3 on the display)
+" "rows" - height of the terminal window (max. 20)
+" "cols" - width of the terminal window (max. 78)
+" "statusoff" - number of lines the status is offset from default
+" "wait_for_ruler" - if zero then don't wait for ruler to show
+" "no_clean" - if non-zero then remove "--clean" from the command
+" "cmd"  - run any other command, e.g. "xxd" (used in xxd test)
+" "env"  - additional environment variables, e.g. $TERM variable
+func RunVimInTerminal(arguments, options)
+  " If Vim doesn't exit a swap file remains, causing other tests to fail.
+  " Remove it here.
+  call delete(".swp")
+
+  if exists('$COLORFGBG')
+    " Clear $COLORFGBG to avoid 'background' being set to "dark", which will
+    " only be corrected if the response to t_RB is received, which may be too
+    " late.
+    let $COLORFGBG = ''
+  endif
+
+  " Make a horizontal and vertical split, so that we can get exactly the right
+  " size terminal window.  Works only when the current window is full width.
+  call assert_equal(&columns, winwidth(0))
+  split
+  vsplit
+
+  " Always do this with 256 colors and a light background.
+  set t_Co=256 background=light
+  hi Normal ctermfg=NONE ctermbg=NONE
+
+  " Make the window 20 lines high and 75 columns, unless told otherwise or
+  " 'termwinsize' is set.
+  let rows = get(a:options, 'rows', 20)
+  let cols = get(a:options, 'cols', 75)
+  let statusoff = get(a:options, 'statusoff', 1)
+
+  if get(a:options, 'keep_t_u7', 0)
+    let reset_u7 = ''
+  else
+    let reset_u7 = ' --cmd "set t_u7=" '
+  endif
+
+  if empty(get(a:options, 'cmd', ''))
+    let cmd = GetVimCommandCleanTerm() .. reset_u7 .. a:arguments
+  else
+    let cmd = get(a:options, 'cmd')
+  endif
+
+  if get(a:options, 'no_clean', 0)
+    let cmd = substitute(cmd, '--clean', '', '')
+  endif
+
+  let options = #{curwin: 1}
+  if &termwinsize == ''
+    let options.term_rows = rows
+    let options.term_cols = cols
+  endif
+
+  " Accept other options whose name starts with 'term_'.
+  call extend(options, filter(copy(a:options), 'v:key =~# "^term_"'))
+  " Accept the env dict
+  if !empty(get(a:options, 'env', {}))
+    let options.env = get(a:options, 'env')
+  endif
+
+  let buf = term_start(cmd, options)
+
+  if &termwinsize == ''
+    " in the GUI we may end up with a different size, try to set it.
+    if term_getsize(buf) != [rows, cols]
+      call term_setsize(buf, rows, cols)
+    endif
+    call assert_equal([rows, cols], term_getsize(buf))
+  else
+    let rows = term_getsize(buf)[0]
+    let cols = term_getsize(buf)[1]
+  endif
+
+  call TermWait(buf)
+
+  if get(a:options, 'wait_for_ruler', 1) && empty(get(a:options, 'cmd', ''))
+    " Wait for "All" or "Top" of the ruler to be shown in the last line or in
+    " the status line of the last window. This can be quite slow (e.g. when
+    " using valgrind).
+    " If it fails then show the terminal contents for debugging.
+    try
+      call WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - statusoff)) >= cols - 1})
+    catch /timed out after/
+      let lines = map(range(1, rows), {key, val -> term_getline(buf, val)})
+      call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "<NL>"))
+    endtry
+  endif
+
+  " Starting a terminal to run Vim is always considered flaky.
+  let g:test_is_flaky = 1
+
+  return buf
+endfunc
+
+" Stop a Vim running in terminal buffer "buf".
+func StopVimInTerminal(buf, kill = 1)
+  " Using a terminal to run Vim is always considered flaky.
+  let g:test_is_flaky = 1
+
+  call assert_equal("running", term_getstatus(a:buf))
+
+  " Wait for all the pending updates to terminal to complete
+  call TermWait(a:buf, 1)
+
+  " CTRL-O : works both in Normal mode and Insert mode to start a command line.
+  " In Command-line it's inserted, the CTRL-U removes it again.
+  call term_sendkeys(a:buf, "\<C-O>:\<C-U>qa!\<cr>")
+
+  " Wait for all the pending updates to terminal to complete
+  call TermWait(a:buf, 1)
+
+  " Wait for the terminal to end.
+  call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))})
+
+  " If the buffer still exists forcefully wipe it.
+  if a:kill && bufexists(a:buf)
+    exe a:buf .. 'bwipe!'
+  endif
+endfunc
+
+" Open a terminal with a shell, assign the job to g:job and return the buffer
+" number.
+func Run_shell_in_terminal(options)
+  if has('win32')
+    let buf = term_start([&shell, '/D', '/k'], a:options)
+  else
+    let buf = term_start(&shell, a:options)
+  endif
+  let g:test_is_flaky = 1
+
+  let termlist = term_list()
+  call assert_equal(1, len(termlist))
+  call assert_equal(buf, termlist[0])
+
+  let g:job = term_getjob(buf)
+  call assert_equal(v:t_job, type(g:job))
+
+  let string = string({'job': buf->term_getjob()})
+  call assert_match("{'job': 'process \\d\\+ run'}", string)
+
+  " On slower systems it may take a bit of time before the shell is ready to
+  " accept keys.  This mainly matters when using term_sendkeys() next.
+  call TermWait(buf)
+
+  return buf
+endfunc
+
+" Return concatenated lines in terminal.
+func Term_getlines(buf, lines)
+  return join(map(a:lines, 'term_getline(a:buf, v:val)'), '')
+endfunc
+
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/testdir/util/unix.vim b/src/testdir/util/unix.vim
new file mode 100644
index 0000000..8a97129
--- /dev/null
+++ b/src/testdir/util/unix.vim
@@ -0,0 +1,13 @@
+" Settings for test script execution
+" Always use "sh", don't use the value of "$SHELL".
+set shell=sh
+
+" Only when the +eval feature is present.
+if 1
+  " While some tests overwrite $HOME to prevent them from polluting user files,
+  " we need to remember the original value so that we can tell external systems
+  " where to ask about their own user settings.
+  let g:tester_HOME = $HOME
+endif
+
+source util/setup.vim
diff --git a/src/testdir/util/view_util.vim b/src/testdir/util/view_util.vim
new file mode 100644
index 0000000..161c8b2
--- /dev/null
+++ b/src/testdir/util/view_util.vim
@@ -0,0 +1,117 @@
+" Functions about view shared by several tests
+
+" Only load this script once.
+if exists('*Screenline')
+  finish
+endif
+
+" Get line "lnum" as displayed on the screen.
+" Trailing white space is trimmed.
+func Screenline(lnum)
+  let chars = []
+  for c in range(1, winwidth(0))
+    call add(chars, nr2char(screenchar(a:lnum, c)))
+  endfor
+  let line = join(chars, '')
+  return matchstr(line, '^.\{-}\ze\s*$')
+endfunc
+
+" Get text on the screen, including composing characters.
+" ScreenLines(lnum, width) or
+" ScreenLines([start, end], width)
+func ScreenLines(lnum, width) abort
+  redraw!
+  if type(a:lnum) == v:t_list
+    let start = a:lnum[0]
+    let end = a:lnum[1]
+  else
+    let start = a:lnum
+    let end = a:lnum
+  endif
+  let lines = []
+  for l in range(start, end)
+    let lines += [join(map(range(1, a:width), 'screenstring(l, v:val)'), '')]
+  endfor
+  return lines
+endfunc
+
+func ScreenAttrs(lnum, width) abort
+  redraw!
+  if type(a:lnum) == v:t_list
+    let start = a:lnum[0]
+    let end = a:lnum[1]
+  else
+    let start = a:lnum
+    let end = a:lnum
+  endif
+  let attrs = []
+  for l in range(start, end)
+    let attrs += [map(range(1, a:width), 'screenattr(l, v:val)')]
+  endfor
+  return attrs
+endfunc
+
+" Create a new window with the requested size and fix it.
+func NewWindow(height, width) abort
+  exe a:height . 'new'
+  exe a:width . 'vsp'
+  set winfixwidth winfixheight
+  redraw!
+endfunc
+
+func CloseWindow() abort
+  bw!
+  redraw!
+endfunc
+
+
+" When using RunVimInTerminal() we expect modifyOtherKeys level 2 to be enabled
+" automatically.  The key + modifier Escape codes must then use the
+" modifyOtherKeys encoding.  They are recognized anyway, thus it's safer to use
+" than the raw code.
+
+" Return the modifyOtherKeys level 2 encoding for "key" with "modifier"
+" (number value, e.g. CTRL is 5, Shift is 2, Alt is 3).
+func GetEscCodeCSI27(key, modifier)
+  let key = printf("%d", char2nr(a:key))
+  let mod = printf("%d", a:modifier)
+  return "\<Esc>[27;" .. mod .. ';' .. key .. '~'
+endfunc
+
+" Return the modifyOtherKeys level 2 encoding for "key" with "modifier"
+" (character value, e.g. CTRL is "C").
+func GetEscCodeWithModifier(modifier, key)
+  let modifier = get({'C': 5}, a:modifier, '')
+  if modifier == ''
+    echoerr 'Unknown modifier: ' .. a:modifier
+  endif
+  return GetEscCodeCSI27(a:key, modifier)
+endfunc
+
+" Return the kitty keyboard protocol encoding for "key" with "modifier"
+" (number value, e.g. CTRL is 5).
+func GetEscCodeCSIu(key, modifier)
+  let key = printf("%d", char2nr(a:key))
+  let mod = printf("%d", a:modifier)
+  return "\<Esc>[" .. key .. ';' .. mod .. 'u'
+endfunc
+
+" Return the kitty keyboard protocol encoding for a function key:
+" CSI {key}
+" CSS 1;{modifier} {key}
+func GetEscCodeFunckey(key, modifier)
+  if a:modifier == 0
+    return "\<Esc>[" .. a:key
+  endif
+
+  let mod = printf("%d", a:modifier)
+  return "\<Esc>[1;".. mod .. a:key
+endfunc
+
+" Return the kitty keyboard protocol encoding for "key" without a modifier.
+" Used for the Escape key.
+func GetEscCodeCSIuWithoutModifier(key)
+  let key = printf("%d", char2nr(a:key))
+  return "\<Esc>[" .. key .. 'u'
+endfunc
+
diff --git a/src/testdir/util/vim9.vim b/src/testdir/util/vim9.vim
new file mode 100644
index 0000000..b994855
--- /dev/null
+++ b/src/testdir/util/vim9.vim
@@ -0,0 +1,549 @@
+vim9script
+
+# Utility functions for testing Vim9 script
+
+# Use a different file name for each run.
+var sequence = 1
+
+# Check that "lines" inside a ":def" function has no error when called.
+export func CheckDefSuccess(lines)
+  let cwd = getcwd()
+  let fname = 'XdefSuccess' .. s:sequence
+  let s:sequence += 1
+  call writefile(['def Func()'] + a:lines + ['enddef', 'defcompile'], fname)
+  try
+    exe 'so ' .. fname
+    call Func()
+  finally
+    call chdir(cwd)
+    call delete(fname)
+    delfunc! Func
+  endtry
+endfunc
+
+# Check that "lines" inside a ":def" function has no error when compiled.
+export func CheckDefCompileSuccess(lines)
+  let fname = 'XdefSuccess' .. s:sequence
+  let s:sequence += 1
+  call writefile(['def Func()'] + a:lines + ['enddef', 'defcompile'], fname)
+  try
+    exe 'so ' .. fname
+  finally
+    call delete(fname)
+    delfunc! Func
+  endtry
+endfunc
+
+# Check that "lines" inside ":def" results in an "error" message.
+# If "lnum" is given check that the error is reported for this line.
+# Add a line before and after to make it less likely that the line number is
+# accidentally correct.
+export func CheckDefFailure(lines, error, lnum = -3)
+  let cwd = getcwd()
+  let fname = 'XdefFailure' .. s:sequence
+  let s:sequence += 1
+  call writefile(['def Func()', '# comment'] + a:lines + ['#comment', 'enddef', 'defcompile'], fname)
+  try
+    call assert_fails('so ' .. fname, a:error, a:lines, a:lnum + 1)
+  finally
+    call chdir(cwd)
+    call delete(fname)
+    delfunc! Func
+  endtry
+endfunc
+
+# Check that "lines" inside ":def" results in an "error" message when executed.
+# If "lnum" is given check that the error is reported for this line.
+# Add a line before and after to make it less likely that the line number is
+# accidentally correct.
+export func CheckDefExecFailure(lines, error, lnum = -3)
+  let cwd = getcwd()
+  let fname = 'XdefExecFailure' .. s:sequence
+  let s:sequence += 1
+  call writefile(['def Func()', '# comment'] + a:lines + ['#comment', 'enddef'], fname)
+  try
+    exe 'so ' .. fname
+    call assert_fails('call Func()', a:error, a:lines, a:lnum + 1)
+  finally
+    call chdir(cwd)
+    call delete(fname)
+    delfunc! Func
+  endtry
+endfunc
+
+export def CheckScriptFailure(lines: list<string>, error: string, lnum = -3)
+  var cwd = getcwd()
+  var fname = 'XScriptFailure' .. sequence
+  sequence += 1
+  writefile(lines, fname)
+  try
+    assert_fails('so ' .. fname, error, lines, lnum)
+  finally
+    chdir(cwd)
+    delete(fname)
+  endtry
+enddef
+
+export def CheckScriptFailureList(lines: list<string>, errors: list<string>, lnum = -3)
+  var cwd = getcwd()
+  var fname = 'XScriptFailure' .. sequence
+  sequence += 1
+  writefile(lines, fname)
+  try
+    assert_fails('so ' .. fname, errors, lines, lnum)
+  finally
+    chdir(cwd)
+    delete(fname)
+  endtry
+enddef
+
+export def CheckScriptSuccess(lines: list<string>)
+  var cwd = getcwd()
+  var fname = 'XScriptSuccess' .. sequence
+  sequence += 1
+  writefile(lines, fname)
+  try
+    exe 'so ' .. fname
+  finally
+    chdir(cwd)
+    delete(fname)
+  endtry
+enddef
+
+export def CheckDefAndScriptSuccess(lines: list<string>)
+  CheckDefSuccess(lines)
+  CheckScriptSuccess(['vim9script'] + lines)
+enddef
+
+# Check that a command fails when used in a :def function and when used in
+# Vim9 script.
+# When "error" is a string, both with the same error.
+# When "error" is a list, the :def function fails with "error[0]" , the script
+# fails with "error[1]".
+export def CheckDefAndScriptFailure(lines: list<string>, error: any, lnum = -3)
+  var errorDef: string
+  var errorScript: string
+  if type(error) == v:t_string
+    errorDef = error
+    errorScript = error
+  elseif type(error) == v:t_list && len(error) == 2
+    errorDef = error[0]
+    errorScript = error[1]
+  else
+    echoerr 'error argument must be a string or a list with two items'
+    return
+  endif
+  CheckDefFailure(lines, errorDef, lnum)
+  CheckScriptFailure(['vim9script'] + lines, errorScript, lnum + 1)
+enddef
+
+# Check that a command fails when executed in a :def function and when used in
+# Vim9 script.
+# When "error" is a string, both with the same error.
+# When "error" is a list, the :def function fails with "error[0]" , the script
+# fails with "error[1]".
+export def CheckDefExecAndScriptFailure(lines: list<string>, error: any, lnum = -3)
+  var errorDef: string
+  var errorScript: string
+  if type(error) == v:t_string
+    errorDef = error
+    errorScript = error
+  elseif type(error) == v:t_list && len(error) == 2
+    errorDef = error[0]
+    errorScript = error[1]
+  else
+    echoerr 'error argument must be a string or a list with two items'
+    return
+  endif
+  CheckDefExecFailure(lines, errorDef, lnum)
+  CheckScriptFailure(['vim9script'] + lines, errorScript, lnum + 1)
+enddef
+
+
+# Check that "lines" inside a legacy function has no error.
+export func CheckLegacySuccess(lines)
+  let cwd = getcwd()
+  let fname = 'XlegacySuccess' .. s:sequence
+  let s:sequence += 1
+  call writefile(['func Func()'] + a:lines + ['endfunc'], fname)
+  try
+    exe 'so ' .. fname
+    call Func()
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    call delete(fname)
+  endtry
+endfunc
+
+# Check that "lines" inside a legacy function results in the expected error
+export func CheckLegacyFailure(lines, error)
+  let cwd = getcwd()
+  let fname = 'XlegacyFails' .. s:sequence
+  let s:sequence += 1
+  call writefile(['func Func()'] + a:lines + ['endfunc', 'call Func()'], fname)
+  try
+    call assert_fails('so ' .. fname, a:error)
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    call delete(fname)
+  endtry
+endfunc
+
+# Translate "lines" to legacy Vim script
+def LegacyTrans(lines: list<string>): list<string>
+  return lines->mapnew((_, v) =>
+		v->substitute('\<VAR\>', 'let', 'g')
+		->substitute('\<LET\>', 'let', 'g')
+		->substitute('\<LSTART\>', '{', 'g')
+		->substitute('\<LMIDDLE\>', '->', 'g')
+		->substitute('\<LEND\>', '}', 'g')
+		->substitute('\<TRUE\>', '1', 'g')
+		->substitute('\<FALSE\>', '0', 'g')
+		->substitute('#"', ' "', 'g'))
+enddef
+
+# Execute "lines" in a legacy function, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckTransLegacySuccess(lines: list<string>)
+  CheckLegacySuccess(LegacyTrans(lines))
+enddef
+
+export def Vim9Trans(lines: list<string>): list<string>
+  return lines->mapnew((_, v) =>
+	    v->substitute('\<VAR\>', 'var', 'g')
+	    ->substitute('\<LET ', '', 'g')
+	    ->substitute('\<LSTART\>', '(', 'g')
+	    ->substitute('\<LMIDDLE\>', ') =>', 'g')
+	    ->substitute(' *\<LEND\> *', '', 'g')
+	    ->substitute('\<TRUE\>', 'true', 'g')
+	    ->substitute('\<FALSE\>', 'false', 'g'))
+enddef
+
+# Execute "lines" in a :def function, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckTransDefSuccess(lines: list<string>)
+  CheckDefSuccess(Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a Vim9 script, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckTransVim9Success(lines: list<string>)
+  CheckScriptSuccess(['vim9script'] + Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+# Use LSTART arg LMIDDLE expr LEND for lambda
+# Use 'TRUE' for 1 in legacy, true in Vim9
+# Use 'FALSE' for 0 in legacy, false in Vim9
+export def CheckLegacyAndVim9Success(lines: list<string>)
+  CheckTransLegacySuccess(lines)
+  CheckTransDefSuccess(lines)
+  CheckTransVim9Success(lines)
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+export def CheckLegacyAndVim9Failure(lines: list<string>, error: any)
+  var legacyError: string
+  var defError: string
+  var scriptError: string
+
+  if type(error) == type('string')
+    legacyError = error
+    defError = error
+    scriptError = error
+  else
+    legacyError = error[0]
+    defError = error[1]
+    scriptError = error[2]
+  endif
+
+  var legacylines = lines->mapnew((_, v) =>
+				v->substitute('\<VAR\>', 'let', 'g')
+				 ->substitute('\<LET\>', 'let', 'g')
+				 ->substitute('\<LSTART\>', '{', 'g')
+				 ->substitute('\<LMIDDLE\>', '->', 'g')
+				 ->substitute('\<LEND\>', '}', 'g')
+				 ->substitute('\<TRUE\>', '1', 'g')
+				 ->substitute('\<FALSE\>', '0', 'g')
+				 ->substitute('#"', ' "', 'g'))
+  CheckLegacyFailure(legacylines, legacyError)
+
+  var vim9lines = lines->mapnew((_, v) =>
+				v->substitute('\<VAR\>', 'var', 'g')
+				 ->substitute('\<LET ', '', 'g')
+				 ->substitute('\<LSTART\>', '(', 'g')
+				 ->substitute('\<LMIDDLE\>', ') =>', 'g')
+				 ->substitute(' *\<LEND\> *', '', 'g')
+				 ->substitute('\<TRUE\>', 'true', 'g')
+				 ->substitute('\<FALSE\>', 'false', 'g'))
+  CheckDefExecFailure(vim9lines, defError)
+  CheckScriptFailure(['vim9script'] + vim9lines, scriptError)
+enddef
+
+# Check that "lines" inside a legacy function has no error.
+export func CheckSourceLegacySuccess(lines)
+  let cwd = getcwd()
+  new
+  call setline(1, ['func Func()'] + a:lines + ['endfunc', 'call Func()'])
+  let bnr = bufnr()
+  try
+    :source
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Check that "lines" inside a legacy function results in the expected error
+export func CheckSourceLegacyFailure(lines, error)
+  let cwd = getcwd()
+  new
+  call setline(1, ['func Func()'] + a:lines + ['endfunc', 'call Func()'])
+  let bnr = bufnr()
+  try
+    call assert_fails('source', a:error)
+  finally
+    delfunc! Func
+    call chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Execute "lines" in a legacy function, translated as in
+# CheckSourceLegacyAndVim9Success()
+export def CheckSourceTransLegacySuccess(lines: list<string>)
+  CheckSourceLegacySuccess(LegacyTrans(lines))
+enddef
+
+# Execute "lines" in a :def function, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckSourceTransDefSuccess(lines: list<string>)
+  CheckSourceDefSuccess(Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a Vim9 script, translated as in
+# CheckLegacyAndVim9Success()
+export def CheckSourceTransVim9Success(lines: list<string>)
+  CheckSourceScriptSuccess(['vim9script'] + Vim9Trans(lines))
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+# Use LSTART arg LMIDDLE expr LEND for lambda
+# Use 'TRUE' for 1 in legacy, true in Vim9
+# Use 'FALSE' for 0 in legacy, false in Vim9
+export def CheckSourceLegacyAndVim9Success(lines: list<string>)
+  CheckSourceTransLegacySuccess(lines)
+  CheckSourceTransDefSuccess(lines)
+  CheckSourceTransVim9Success(lines)
+enddef
+
+# :source a list of "lines" and check whether it fails with "error"
+export def CheckSourceScriptFailure(lines: list<string>, error: string, lnum = -3)
+  var cwd = getcwd()
+  new
+  setline(1, lines)
+  var bnr = bufnr()
+  try
+    assert_fails('source', error, lines, lnum)
+  finally
+    chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+enddef
+
+# :source a list of "lines" and check whether it fails with the list of
+# "errors"
+export def CheckSourceScriptFailureList(lines: list<string>, errors: list<string>, lnum = -3)
+  var cwd = getcwd()
+  new
+  var bnr = bufnr()
+  setline(1, lines)
+  try
+    assert_fails('source', errors, lines, lnum)
+  finally
+    chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+enddef
+
+# :source a list of "lines" and check whether it succeeds
+export def CheckSourceScriptSuccess(lines: list<string>)
+  var cwd = getcwd()
+  new
+  var bnr = bufnr()
+  setline(1, lines)
+  try
+    :source
+  finally
+    chdir(cwd)
+    exe $':bw! {bnr}'
+  endtry
+enddef
+
+# :source a List of "lines" inside a ":def" function and check that no error
+# occurs when called.
+export func CheckSourceDefSuccess(lines)
+  let cwd = getcwd()
+  new
+  let bnr = bufnr()
+  call setline(1, ['def Func()'] + a:lines + ['enddef', 'defcompile'])
+  try
+    source
+    call Func()
+  finally
+    call chdir(cwd)
+    delfunc! Func
+    exe $'bw! {bnr}'
+  endtry
+endfunc
+
+# Check that "lines" inside a ":def" function has no error when compiled.
+export func CheckSourceDefCompileSuccess(lines)
+  let cwd = getcwd()
+  new
+  let bnr = bufnr()
+  call setline(1, ['def Func()', '# comment'] + a:lines + ['#comment', 'enddef', 'defcompile'])
+  try
+    source
+  finally
+    call chdir(cwd)
+    delfunc! Func
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Check that "lines" inside ":def" results in an "error" message.
+# If "lnum" is given check that the error is reported for this line.
+# Add a line before and after to make it less likely that the line number is
+# accidentally correct.
+export func CheckSourceDefFailure(lines, error, lnum = -3)
+  let cwd = getcwd()
+  new
+  let bnr = bufnr()
+  call setline(1, ['def Func()', '# comment'] + a:lines + ['#comment', 'enddef', 'defcompile'])
+  try
+    call assert_fails('source', a:error, a:lines, a:lnum + 1)
+  finally
+    call chdir(cwd)
+    delfunc! Func
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Check that "lines" inside ":def" results in an "error" message when executed.
+# If "lnum" is given check that the error is reported for this line.
+# Add a line before and after to make it less likely that the line number is
+# accidentally correct.
+export func CheckSourceDefExecFailure(lines, error, lnum = -3)
+  let cwd = getcwd()
+  new
+  let bnr = bufnr()
+  call setline(1, ['def Func()', '# comment'] + a:lines + ['#comment', 'enddef'])
+  try
+    source
+    call assert_fails('call Func()', a:error, a:lines, a:lnum + 1)
+  finally
+    call chdir(cwd)
+    delfunc! Func
+    exe $':bw! {bnr}'
+  endtry
+endfunc
+
+# Check that a command fails when used in a :def function and when used in
+# Vim9 script.
+# When "error" is a string, both with the same error.
+# When "error" is a list, the :def function fails with "error[0]" , the script
+# fails with "error[1]".
+export def CheckSourceDefAndScriptFailure(lines: list<string>, error: any, lnum = -3)
+  var errorDef: string
+  var errorScript: string
+  if type(error) == v:t_string
+    errorDef = error
+    errorScript = error
+  elseif type(error) == v:t_list && len(error) == 2
+    errorDef = error[0]
+    errorScript = error[1]
+  else
+    echoerr 'error argument must be a string or a list with two items'
+    return
+  endif
+  CheckSourceDefFailure(lines, errorDef, lnum)
+  CheckSourceScriptFailure(['vim9script'] + lines, errorScript, lnum + 1)
+enddef
+
+# Check that a command fails when executed in a :def function and when used in
+# Vim9 script.
+# When "error" is a string, both with the same error.
+# When "error" is a list, the :def function fails with "error[0]" , the script
+# fails with "error[1]".
+export def CheckSourceDefExecAndScriptFailure(lines: list<string>, error: any, lnum = -3)
+  var errorDef: string
+  var errorScript: string
+  if type(error) == v:t_string
+    errorDef = error
+    errorScript = error
+  elseif type(error) == v:t_list && len(error) == 2
+    errorDef = error[0]
+    errorScript = error[1]
+  else
+    echoerr 'error argument must be a string or a list with two items'
+    return
+  endif
+  CheckSourceDefExecFailure(lines, errorDef, lnum)
+  CheckSourceScriptFailure(['vim9script'] + lines, errorScript, lnum + 1)
+enddef
+
+export def CheckSourceSuccess(lines: list<string>)
+  CheckSourceScriptSuccess(lines)
+enddef
+
+export def CheckSourceFailure(lines: list<string>, error: string, lnum = -3)
+  CheckSourceScriptFailure(lines, error, lnum)
+enddef
+
+export def CheckSourceFailureList(lines: list<string>, errors: list<string>, lnum = -3)
+  CheckSourceScriptFailureList(lines, errors, lnum)
+enddef
+
+export def CheckSourceDefAndScriptSuccess(lines: list<string>)
+  CheckSourceDefSuccess(lines)
+  CheckSourceScriptSuccess(['vim9script'] + lines)
+enddef
+
+# Execute "lines" in a legacy function, :def function and Vim9 script.
+# Use 'VAR' for a declaration.
+# Use 'LET' for an assignment
+# Use ' #"' for a comment
+export def CheckSourceLegacyAndVim9Failure(lines: list<string>, error: any)
+  var legacyError: string
+  var defError: string
+  var scriptError: string
+
+  if type(error) == type('string')
+    legacyError = error
+    defError = error
+    scriptError = error
+  else
+    legacyError = error[0]
+    defError = error[1]
+    scriptError = error[2]
+  endif
+
+  CheckSourceLegacyFailure(LegacyTrans(lines), legacyError)
+  var vim9lines = Vim9Trans(lines)
+  CheckSourceDefExecFailure(vim9lines, defError)
+  CheckSourceScriptFailure(['vim9script'] + vim9lines, scriptError)
+enddef
+
diff --git a/src/testdir/util/vms.vim b/src/testdir/util/vms.vim
new file mode 100644
index 0000000..0a264e9
--- /dev/null
+++ b/src/testdir/util/vms.vim
@@ -0,0 +1,6 @@
+" Settings for test script execution under OpenVMS
+
+" Do not use any swap files
+set noswapfile
+
+source util/setup.vim
diff --git a/src/testdir/util/window_manager.vim b/src/testdir/util/window_manager.vim
new file mode 100644
index 0000000..7a3c0c0
--- /dev/null
+++ b/src/testdir/util/window_manager.vim
@@ -0,0 +1,107 @@
+CheckFeature job
+CheckUnix
+
+let g:xdisplay_num = 100
+
+" Each key is the display name and its value is the compositor/wm job
+let s:wayland_displays = {}
+let s:x11_displays = {}
+
+command -nargs=0 CheckWaylandCompositor call CheckWaylandCompositor()
+command -nargs=0 CheckXServer call CheckXServer()
+
+func CheckWaylandCompositor()
+  CheckFeature wayland
+
+  if executable("labwc") != 1
+    throw "Skipped: labwc is not available"
+  endif
+endfunc
+
+func CheckXServer()
+  CheckFeature x11
+
+  if executable("Xvfb") != 1
+    throw "Skipped: Xvfb is not available"
+  endif
+  if executable("xdpyinfo") != 1
+    throw "Skipped: xdpyinfo is not available"
+  endif
+endfunc
+
+func s:StartCompositorOutput(channel, msg)
+  let l:display = matchstr(a:msg, 'WAYLAND_DISPLAY=\zs.\+')
+
+  if !empty(l:display)
+    let s:wayland_display_name = l:display
+  endif
+endfunc
+
+func s:StartCompositorExit(job, status)
+    if s:wayland_display_name == ""
+      throw "Skipped: Error: Wayland compositor exited when starting up"
+    endif
+endfunc
+
+func StartWaylandCompositor()
+  let s:wayland_display_name = ""
+
+  let l:wayland_compositor_job = job_start(
+        \ ['labwc', '-c', 'NONE', '-d'], {
+        \ 'err_io': 'pipe',
+        \ 'err_cb': function('s:StartCompositorOutput'),
+        \ 'err_mode': 'nl',
+        \ 'exit_cb': function('s:StartCompositorExit'),
+        \ 'env': { 'WLR_BACKENDS': 'headless' }
+        \ })
+
+  call WaitForAssert({-> assert_equal("run",
+        \ job_status(l:wayland_compositor_job))})
+  call WaitForAssert({-> assert_match('.\+', s:wayland_display_name)})
+
+  let s:wayland_displays[s:wayland_display_name] = l:wayland_compositor_job
+
+  return s:wayland_display_name
+endfunc
+
+func EndWaylandCompositor(display)
+  let l:job = s:wayland_displays[a:display]
+
+  call job_stop(l:job, 'term')
+
+  " Block until compositor is actually gone
+  call WaitForAssert({-> assert_equal("dead", job_status(l:job))})
+
+  unlet s:wayland_displays[a:display]
+endfunc
+
+" Start a separate X11 server instance
+func StartXServer()
+  let l:xdisplay = ':' .. g:xdisplay_num
+
+  let l:x11_server_job = job_start(['Xvfb', l:xdisplay], {})
+
+  call WaitForAssert({-> assert_equal("run", job_status(l:x11_server_job))})
+  " Check if server is ready. Not sure if this is the best way though...
+  call WaitFor({-> system("DISPLAY=" .. l:xdisplay .. " xdpyinfo 2> /dev/null")
+        \ =~? '.\+'})
+
+  g:xdisplay_num += 1
+
+  let s:x11_displays[l:xdisplay] = l:x11_server_job
+
+  return l:xdisplay
+endfunc
+
+func EndXServer(display)
+  let l:job = s:x11_displays[a:display]
+
+  call job_stop(l:job)
+
+  " Block until X server is actually gone
+  call WaitForAssert({-> assert_equal("dead", job_status(l:job))})
+
+  unlet s:x11_displays[a:display]
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab