neovim

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

testnvim.lua (30304B)


      1 local uv = vim.uv
      2 local t = require('test.testutil')
      3 local busted = require('busted')
      4 
      5 local Session = require('test.client.session')
      6 local uv_stream = require('test.client.uv_stream')
      7 local SocketStream = uv_stream.SocketStream
      8 local ProcStream = uv_stream.ProcStream
      9 
     10 local check_cores = t.check_cores
     11 local check_logs = t.check_logs
     12 local dedent = t.dedent
     13 local eq = t.eq
     14 local is_os = t.is_os
     15 local ok = t.ok
     16 local sleep = uv.sleep
     17 
     18 --- Functions executing in the current nvim session/process being tested.
     19 local M = {}
     20 
     21 local lib_path = t.paths.test_build_dir .. (t.is_zig_build() and '/lib' or '/lib/nvim')
     22 M.runtime_set = 'set runtimepath^=' .. lib_path
     23 
     24 M.nvim_prog = (os.getenv('NVIM_PRG') or t.paths.test_build_dir .. '/bin/nvim')
     25 -- Default settings for the test session.
     26 M.nvim_set = (
     27  'set shortmess+=IS background=light noswapfile noautoindent startofline'
     28  .. ' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
     29  .. " belloff= wildoptions-=pum joinspaces noshowcmd noruler nomore redrawdebug=invalid shada=!,'100,<50,s10,h"
     30  .. [[ statusline=%<%f\ %{%nvim_eval_statusline('%h%w%m%r',\ {'maxwidth':\ 30}).width\ >\ 0\ ?\ '%h%w%m%r\ '\ :\ ''%}%=%{%\ &showcmdloc\ ==\ 'statusline'\ ?\ '%-10.S\ '\ :\ ''\ %}%{%\ exists('b:keymap_name')\ ?\ '<'..b:keymap_name..'>\ '\ :\ ''\ %}%{%\ &ruler\ ?\ (\ &rulerformat\ ==\ ''\ ?\ '%-14.(%l,%c%V%)\ %P'\ :\ &rulerformat\ )\ :\ ''\ %}]]
     31 )
     32 M.nvim_argv = {
     33  M.nvim_prog,
     34  '-u',
     35  'NONE',
     36  '-i',
     37  'NONE',
     38  -- XXX: find treesitter parsers.
     39  '--cmd',
     40  M.runtime_set,
     41  '--cmd',
     42  M.nvim_set,
     43  -- Remove default user commands and mappings.
     44  '--cmd',
     45  'comclear | mapclear | mapclear!',
     46  -- Make screentest work after changing to the new default color scheme
     47  -- Source 'vim' color scheme without side effects
     48  -- TODO: rewrite tests
     49  '--cmd',
     50  'lua dofile("runtime/colors/vim.lua")',
     51  '--cmd',
     52  'unlet g:colors_name',
     53  '--embed',
     54 }
     55 if os.getenv('OSV_PORT') then
     56  table.insert(M.nvim_argv, '--cmd')
     57  table.insert(
     58    M.nvim_argv,
     59    string.format(
     60      "lua require('osv').launch({ port = %s, blocking = true })",
     61      os.getenv('OSV_PORT')
     62    )
     63  )
     64 end
     65 
     66 -- Directory containing nvim.
     67 M.nvim_dir = M.nvim_prog:gsub('[/\\][^/\\]+$', '')
     68 if M.nvim_dir == M.nvim_prog then
     69  M.nvim_dir = '.'
     70 end
     71 
     72 local prepend_argv --- @type string[]?
     73 
     74 if os.getenv('VALGRIND') then
     75  local log_file = os.getenv('VALGRIND_LOG') or 'valgrind-%p.log'
     76  prepend_argv = {
     77    'valgrind',
     78    '-q',
     79    '--tool=memcheck',
     80    '--leak-check=yes',
     81    '--track-origins=yes',
     82    '--show-possibly-lost=no',
     83    '--suppressions=src/.valgrind.supp',
     84    '--log-file=' .. log_file,
     85  }
     86  if os.getenv('GDB') then
     87    table.insert(prepend_argv, '--vgdb=yes')
     88    table.insert(prepend_argv, '--vgdb-error=0')
     89  end
     90 elseif os.getenv('GDB') then
     91  local gdbserver_port = os.getenv('GDBSERVER_PORT') or '7777'
     92  prepend_argv = { 'gdbserver', 'localhost:' .. gdbserver_port }
     93 end
     94 
     95 if prepend_argv then
     96  local new_nvim_argv = {} --- @type string[]
     97  local len = #prepend_argv
     98  for i = 1, len do
     99    new_nvim_argv[i] = prepend_argv[i]
    100  end
    101  for i = 1, #M.nvim_argv do
    102    new_nvim_argv[i + len] = M.nvim_argv[i]
    103  end
    104  M.nvim_argv = new_nvim_argv
    105  M.prepend_argv = prepend_argv
    106 end
    107 
    108 local session --- @type test.Session?
    109 local loop_running --- @type boolean?
    110 local last_error --- @type string?
    111 local method_error --- @type string?
    112 
    113 if not is_os('win') then
    114  local sigpipe_handler = assert(uv.new_signal())
    115  uv.signal_start(sigpipe_handler, 'sigpipe', function()
    116    print('warning: got SIGPIPE signal. Likely related to a crash in nvim')
    117  end)
    118 end
    119 
    120 function M.get_session()
    121  return session
    122 end
    123 
    124 function M.set_session(s)
    125  session = s
    126 end
    127 
    128 --- @param method string
    129 --- @param ... any
    130 --- @return any
    131 function M.request(method, ...)
    132  assert(session, 'no Nvim session')
    133  assert(not session.eof_err, 'sending request after EOF from Nvim')
    134  local status, rv = session:request(method, ...)
    135  if not status then
    136    if loop_running then
    137      --- @type string
    138      last_error = rv[2]
    139      session:stop()
    140    else
    141      error(rv[2])
    142    end
    143  end
    144  return rv
    145 end
    146 
    147 --- @param method string
    148 --- @param ... any
    149 --- @return any
    150 function M.request_lua(method, ...)
    151  return M.exec_lua([[return vim.api[...](select(2, ...))]], method, ...)
    152 end
    153 
    154 --- @param timeout? integer
    155 --- @return string?
    156 function M.next_msg(timeout)
    157  assert(session)
    158  return session:next_message(timeout or 10000)
    159 end
    160 
    161 function M.expect_twostreams(msgs1, msgs2)
    162  local pos1, pos2 = 1, 1
    163  while pos1 <= #msgs1 or pos2 <= #msgs2 do
    164    local msg = M.next_msg()
    165    if pos1 <= #msgs1 and pcall(eq, msgs1[pos1], msg) then
    166      pos1 = pos1 + 1
    167    elseif pos2 <= #msgs2 then
    168      eq(msgs2[pos2], msg)
    169      pos2 = pos2 + 1
    170    else
    171      -- already failed, but show the right error message
    172      eq(msgs1[pos1], msg)
    173    end
    174  end
    175 end
    176 
    177 -- Expects a sequence of next_msg() results. If multiple sequences are
    178 -- passed they are tried until one succeeds, in order of shortest to longest.
    179 --
    180 -- Can be called with positional args (list of sequences only):
    181 --    expect_msg_seq(seq1, seq2, ...)
    182 -- or keyword args:
    183 --    expect_msg_seq{ignore={...}, seqs={seq1, seq2, ...}}
    184 --
    185 -- ignore:      List of ignored event names.
    186 -- seqs:        List of one or more potential event sequences.
    187 function M.expect_msg_seq(...)
    188  if select('#', ...) < 1 then
    189    error('need at least 1 argument')
    190  end
    191  local arg1 = select(1, ...)
    192  if (arg1['seqs'] and select('#', ...) > 1) or type(arg1) ~= 'table' then
    193    error('invalid args')
    194  end
    195  local ignore = arg1['ignore'] and arg1['ignore'] or {}
    196  --- @type string[]
    197  local seqs = arg1['seqs'] and arg1['seqs'] or { ... }
    198  if type(ignore) ~= 'table' then
    199    error("'ignore' arg must be a list of strings")
    200  end
    201  table.sort(seqs, function(a, b) -- Sort ascending, by (shallow) length.
    202    return #a < #b
    203  end)
    204 
    205  local actual_seq = {}
    206  local nr_ignored = 0
    207  local final_error = ''
    208  local function cat_err(err1, err2)
    209    if err1 == nil then
    210      return err2
    211    end
    212    return string.format('%s\n%s\n%s', err1, string.rep('=', 78), err2)
    213  end
    214  local msg_timeout = M.load_adjust(10000) -- Big timeout for ASAN/valgrind.
    215  for anum = 1, #seqs do
    216    local expected_seq = seqs[anum]
    217    -- Collect enough messages to compare the next expected sequence.
    218    while #actual_seq < #expected_seq do
    219      local msg = M.next_msg(msg_timeout)
    220      local msg_type = msg and msg[2] or nil
    221      if msg == nil then
    222        error(
    223          cat_err(
    224            final_error,
    225            string.format(
    226              'got %d messages (ignored %d), expected %d',
    227              #actual_seq,
    228              nr_ignored,
    229              #expected_seq
    230            )
    231          )
    232        )
    233      elseif vim.tbl_contains(ignore, msg_type) then
    234        nr_ignored = nr_ignored + 1
    235      else
    236        table.insert(actual_seq, msg)
    237      end
    238    end
    239    local status, result = pcall(eq, expected_seq, actual_seq)
    240    if status then
    241      return result
    242    end
    243    local message = result
    244    if type(result) == 'table' then
    245      -- 'eq' returns several things
    246      --- @type string
    247      message = result.message
    248    end
    249    final_error = cat_err(final_error, message)
    250  end
    251  error(final_error)
    252 end
    253 
    254 local function call_and_stop_on_error(lsession, ...)
    255  local status, result = Session.safe_pcall(...) -- luacheck: ignore
    256  if not status then
    257    lsession:stop()
    258    last_error = result
    259    return ''
    260  end
    261  return result
    262 end
    263 
    264 function M.set_method_error(err)
    265  method_error = err
    266 end
    267 
    268 --- Runs the event loop of the given session.
    269 ---
    270 --- @param lsession test.Session
    271 --- @param request_cb function?
    272 --- @param notification_cb function?
    273 --- @param setup_cb function?
    274 --- @param timeout integer
    275 --- @return [integer, string]
    276 function M.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
    277  local on_request --- @type function?
    278  local on_notification --- @type function?
    279  local on_setup --- @type function?
    280 
    281  if request_cb then
    282    function on_request(method, args)
    283      method_error = nil
    284      local result = call_and_stop_on_error(lsession, request_cb, method, args)
    285      if method_error ~= nil then
    286        return method_error, true
    287      end
    288      return result
    289    end
    290  end
    291 
    292  if notification_cb then
    293    function on_notification(method, args)
    294      call_and_stop_on_error(lsession, notification_cb, method, args)
    295    end
    296  end
    297 
    298  if setup_cb then
    299    function on_setup()
    300      call_and_stop_on_error(lsession, setup_cb)
    301    end
    302  end
    303 
    304  loop_running = true
    305  lsession:run(on_request, on_notification, on_setup, timeout)
    306  loop_running = false
    307  if last_error then
    308    local err = last_error
    309    last_error = nil
    310    error(err)
    311  end
    312 
    313  return lsession.eof_err
    314 end
    315 
    316 --- Runs the event loop of the current global session.
    317 function M.run(request_cb, notification_cb, setup_cb, timeout)
    318  assert(session)
    319  return M.run_session(session, request_cb, notification_cb, setup_cb, timeout)
    320 end
    321 
    322 function M.stop()
    323  if loop_running then
    324    assert(session):stop()
    325  end
    326 end
    327 
    328 -- Use for commands which expect nvim to quit.
    329 -- The first argument can also be a timeout.
    330 function M.expect_exit(fn_or_timeout, ...)
    331  local eof_err_msg = 'EOF was received from Nvim. Likely the Nvim process crashed.'
    332  if type(fn_or_timeout) == 'function' then
    333    t.matches(vim.pesc(eof_err_msg), t.pcall_err(fn_or_timeout, ...))
    334  else
    335    t.matches(
    336      vim.pesc(eof_err_msg),
    337      t.pcall_err(function(timeout, fn, ...)
    338        fn(...)
    339        assert(session)
    340        while session:next_message(timeout) do
    341        end
    342        if session.eof_err then
    343          error(session.eof_err[2])
    344        end
    345      end, fn_or_timeout, ...)
    346    )
    347  end
    348 end
    349 
    350 --- Executes a Vimscript function via Lua.
    351 --- Fails on Vimscript error, but does not update v:errmsg.
    352 --- @param name string
    353 --- @param ... any
    354 --- @return any
    355 function M.call_lua(name, ...)
    356  return M.exec_lua([[return vim.call(...)]], name, ...)
    357 end
    358 
    359 --- Sends user input to Nvim.
    360 --- Does not fail on Vimscript error, but v:errmsg will be updated.
    361 --- @param input string
    362 local function nvim_feed(input)
    363  while #input > 0 do
    364    local written = M.request('nvim_input', input)
    365    if written == nil then
    366      M.assert_alive()
    367      error('crash? (nvim_input returned nil)')
    368    end
    369    input = input:sub(written + 1)
    370  end
    371 end
    372 
    373 --- @param ... string
    374 function M.feed(...)
    375  for _, v in ipairs({ ... }) do
    376    nvim_feed(v)
    377  end
    378 end
    379 
    380 ---@param ... string[]?
    381 ---@return string[]
    382 function M.merge_args(...)
    383  local i = 1
    384  local argv = {} --- @type string[]
    385  for anum = 1, select('#', ...) do
    386    --- @type string[]?
    387    local args = select(anum, ...)
    388    if args then
    389      for _, arg in ipairs(args) do
    390        argv[i] = arg
    391        i = i + 1
    392      end
    393    end
    394  end
    395  return argv
    396 end
    397 
    398 --- Removes Nvim startup args from `args` matching items in `args_rm`.
    399 ---
    400 --- - Special case: "-u", "-i", "--cmd" are treated specially: their "values" are also removed.
    401 --- - Special case: "runtimepath" will remove only { '--cmd', 'set runtimepath^=…', }
    402 ---
    403 --- Example:
    404 ---     args={'--headless', '-u', 'NONE'}
    405 ---     args_rm={'--cmd', '-u'}
    406 --- Result:
    407 ---     {'--headless'}
    408 ---
    409 --- All matching cases are removed.
    410 ---
    411 --- Example:
    412 ---     args={'--cmd', 'foo', '-N', '--cmd', 'bar'}
    413 ---     args_rm={'--cmd', '-u'}
    414 --- Result:
    415 ---     {'-N'}
    416 --- @param args string[]
    417 --- @param args_rm string[]
    418 --- @return string[]
    419 local function remove_args(args, args_rm)
    420  local new_args = {} --- @type string[]
    421  local skip_following = { '-u', '-i', '-c', '--cmd', '-s', '--listen' }
    422  if not args_rm or #args_rm == 0 then
    423    return { unpack(args) }
    424  end
    425  for _, v in ipairs(args_rm) do
    426    assert(type(v) == 'string')
    427  end
    428  local last = ''
    429  for _, arg in ipairs(args) do
    430    if vim.tbl_contains(skip_following, last) then
    431      last = ''
    432    elseif vim.tbl_contains(args_rm, arg) then
    433      last = arg
    434    elseif arg == M.runtime_set and vim.tbl_contains(args_rm, 'runtimepath') then
    435      table.remove(new_args) -- Remove the preceding "--cmd".
    436      last = ''
    437    else
    438      table.insert(new_args, arg)
    439    end
    440  end
    441  return new_args
    442 end
    443 
    444 function M.check_close(noblock)
    445  if not session then
    446    return
    447  end
    448 
    449  session:close(nil, noblock)
    450  session = nil
    451 end
    452 
    453 -- Creates a new Session connected by domain socket (named pipe) or TCP.
    454 function M.connect(file_or_address)
    455  local addr, port = string.match(file_or_address, '(.*):(%d+)')
    456  local stream = (addr and port) and SocketStream.connect(addr, port)
    457    or SocketStream.open(file_or_address)
    458  return Session.new(stream)
    459 end
    460 
    461 --- Starts a new, global Nvim session and clears the current one.
    462 ---
    463 --- Note:
    464 --- - Use `new_session()` to start a session without replacing the current one.
    465 --- - Use `spawn_wait()` to start Nvim without connecting a RPC session.
    466 ---
    467 --- Parameters are interpreted as startup args, OR a map with these keys:
    468 --- - args:       List: Args appended to the default `nvim_argv` set.
    469 --- - args_rm:    List: Args removed from the default set. All cases are
    470 ---               removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd"
    471 ---               (and its value) from the default set.
    472 --- - env:        Map: Defines the environment of the new session.
    473 ---
    474 --- Example:
    475 --- ```
    476 --- clear('-e')
    477 --- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
    478 --- ```
    479 ---
    480 --- @param ... string Nvim CLI args
    481 --- @return test.Session
    482 --- @overload fun(opts: test.session.Opts): test.Session
    483 function M.clear(...)
    484  M.set_session(M.new_session(false, ...))
    485  return M.get_session()
    486 end
    487 
    488 local n_processes = 0
    489 
    490 --- Starts a new Nvim process with the given args and returns a msgpack-RPC session.
    491 ---
    492 --- Does not replace the current global session, unlike `clear()`.
    493 ---
    494 --- @param keep boolean (default: false) Don't close the current global session.
    495 --- @param ... string Nvim CLI args
    496 --- @return test.Session
    497 --- @overload fun(keep: boolean, opts: test.session.Opts): test.Session
    498 function M.new_session(keep, ...)
    499  local test_id = _G._nvim_test_id
    500  if not keep and session ~= nil then
    501    -- Don't block for the previous session's exit if it's from a different test.
    502    session:close(nil, session.data and session.data.test_id ~= test_id)
    503    session = nil
    504  end
    505 
    506  local argv, env, io_extra = M._new_argv(...)
    507 
    508  local proc = ProcStream.spawn(argv, env, io_extra, function(closed)
    509    n_processes = n_processes - 1
    510    local delta = 0
    511    if closed then
    512      uv.update_time() -- Update cached value of uv.now() (libuv: uv_now()).
    513      delta = uv.now() - closed
    514    end
    515    if delta > 500 then
    516      print(
    517        ('\nNvim session %s took %d milliseconds to exit\n'):format(test_id, delta)
    518          .. 'This indicates a likely problem with the test even if it passed!'
    519      )
    520      io.stdout:flush()
    521    end
    522  end, true)
    523  n_processes = n_processes + 1
    524 
    525  local new_session = Session.new(proc)
    526  -- Make it possible to check whether two sessions are from the same test.
    527  new_session.data = { test_id = test_id }
    528  return new_session
    529 end
    530 
    531 busted.subscribe({ 'suite', 'end' }, function()
    532  M.check_close(true)
    533  local timed_out = false
    534  local timer = assert(vim.uv.new_timer())
    535  timer:start(10000, 0, function()
    536    timed_out = true
    537  end)
    538  while n_processes > 0 and not timed_out do
    539    uv.run('once')
    540  end
    541  timer:close()
    542  if timed_out then
    543    print(('warning: %d dangling Nvim processes'):format(n_processes))
    544    io.stdout:flush()
    545  end
    546 end)
    547 
    548 --- Starts a (non-RPC, `--headless --listen "Tx"`) Nvim process, waits for exit, and returns result.
    549 ---
    550 --- @param ... string Nvim CLI args, or `test.session.Opts` table.
    551 --- @return test.ProcStream
    552 --- @overload fun(opts: test.session.Opts): test.ProcStream
    553 function M.spawn_wait(...)
    554  local opts = type(...) == 'string' and { args = { ... } } or ...
    555  opts.args_rm = opts.args_rm and opts.args_rm or {}
    556  table.insert(opts.args_rm, '--embed')
    557  local argv, env, io_extra = M._new_argv(opts)
    558  local proc = ProcStream.spawn(argv, env, io_extra)
    559  proc.collect_text = true
    560  proc:read_start()
    561  proc:wait()
    562  proc:close()
    563  return proc
    564 end
    565 
    566 --- @class test.session.Opts
    567 --- Nvim CLI args
    568 --- @field args? string[]
    569 --- Remove these args from the default `nvim_argv` args set. Ignored if `merge=false`.
    570 --- @field args_rm? string[]
    571 --- (default: true) Merge `args` with the default set. Else use only the provided `args`.
    572 --- @field merge? boolean
    573 --- Environment variables
    574 --- @field env? table<string,string>
    575 --- Used for stdin_fd, see `:help ui-option`
    576 --- @field io_extra? uv.uv_pipe_t
    577 
    578 --- @private
    579 ---
    580 --- Builds an argument list for use in `new_session()`, `clear()`, and `spawn_wait()`.
    581 ---
    582 --- @param ... string Nvim CLI args, or `test.session.Opts` table.
    583 --- @return string[]
    584 --- @return string[]?
    585 --- @return uv.uv_pipe_t?
    586 --- @overload fun(opts: test.session.Opts): string[], string[]?, uv.uv_pipe_t?
    587 function M._new_argv(...)
    588  --- @type test.session.Opts|string
    589  local opts = select(1, ...)
    590  local merge = type(opts) ~= 'table' and true or opts.merge ~= false
    591 
    592  local args = merge and { unpack(M.nvim_argv) } or { M.nvim_prog }
    593  if merge then
    594    table.insert(args, '--headless')
    595    if _G._nvim_test_id then
    596      -- Set the server name to the test-id for logging. #8519
    597      table.insert(args, '--listen')
    598      table.insert(args, _G._nvim_test_id)
    599    end
    600  end
    601 
    602  local new_args --- @type string[]
    603  local io_extra --- @type uv.uv_pipe_t?
    604  local env --- @type string[]? List of "key=value" env vars.
    605 
    606  if type(opts) ~= 'table' then
    607    new_args = { ... }
    608  else
    609    args = merge and remove_args(args, opts.args_rm) or args
    610    if opts.env then
    611      local env_opt = {} --- @type table<string,string>
    612      for k, v in pairs(opts.env) do
    613        assert(type(k) == 'string')
    614        assert(type(v) == 'string')
    615        env_opt[k] = v
    616      end
    617      -- Set these from the environment unless the caller defined them.
    618      for _, k in ipairs({
    619        'ASAN_OPTIONS',
    620        'GCOV_ERROR_FILE',
    621        'HOME',
    622        'LD_LIBRARY_PATH',
    623        'MSAN_OPTIONS',
    624        'NVIM_TEST',
    625        'NVIM_LOG_FILE',
    626        'NVIM_RPLUGIN_MANIFEST',
    627        'PATH',
    628        'TMPDIR',
    629        'TSAN_OPTIONS',
    630        'VIMRUNTIME',
    631        'XDG_DATA_DIRS',
    632      }) do
    633        if not env_opt[k] then
    634          env_opt[k] = os.getenv(k)
    635        end
    636      end
    637      env = {}
    638      for k, v in pairs(env_opt) do
    639        env[#env + 1] = k .. '=' .. v
    640      end
    641    end
    642    new_args = opts.args or {}
    643    io_extra = opts.io_extra
    644  end
    645  for _, arg in ipairs(new_args) do
    646    table.insert(args, arg)
    647  end
    648  return args, env, io_extra
    649 end
    650 
    651 --- Dedents string arguments and inserts the resulting text into the current buffer.
    652 --- @param ... string
    653 function M.insert(...)
    654  nvim_feed('i')
    655  for _, v in ipairs({ ... }) do
    656    local escaped = v:gsub('<', '<lt>')
    657    nvim_feed(dedent(escaped))
    658  end
    659  nvim_feed('<ESC>')
    660 end
    661 
    662 --- @deprecated Use `command()` or `feed()` instead.
    663 ---
    664 --- Executes an ex-command by user input. Because nvim_input() is used, Vimscript
    665 --- errors will not manifest as client (lua) errors. Use command() for that.
    666 --- @param ... string
    667 function M.feed_command(...)
    668  for _, v in ipairs({ ... }) do
    669    if v:sub(1, 1) ~= '/' then
    670      -- not a search command, prefix with colon
    671      nvim_feed(':')
    672    end
    673    nvim_feed(v:gsub('<', '<lt>'))
    674    nvim_feed('<CR>')
    675  end
    676 end
    677 
    678 -- @deprecated use nvim_exec2()
    679 function M.source(code)
    680  M.exec(dedent(code))
    681 end
    682 
    683 function M.has_powershell()
    684  return M.eval('executable("pwsh")') == 1
    685 end
    686 
    687 --- Sets Nvim shell to powershell.
    688 ---
    689 --- @param fake boolean? Use a fake if powershell is not found on the system.
    690 --- @returns true if powershell was found on the system, else false.
    691 function M.set_shell_powershell(fake)
    692  local found = M.has_powershell()
    693  if not fake then
    694    assert(found)
    695  end
    696  local shell = found and 'pwsh' or M.testprg('pwsh-test')
    697  local cmd = 'Remove-Item -Force '
    698    .. table.concat(
    699      is_os('win') and { 'alias:cat', 'alias:echo', 'alias:sleep', 'alias:sort', 'alias:tee' }
    700        or { 'alias:echo' },
    701      ','
    702    )
    703    .. ';'
    704  M.exec([[
    705    let &shell = ']] .. shell .. [['
    706    set shellquote= shellxquote=
    707    let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command '
    708    let &shellcmdflag .= '[Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();'
    709    let &shellcmdflag .= '$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';'
    710    let &shellcmdflag .= ']] .. cmd .. [['
    711    let &shellredir = '2>&1 | %%{ "$_" } | Out-File %s; exit $LastExitCode'
    712    let &shellpipe  = '> %s 2>&1'
    713  ]])
    714  return found
    715 end
    716 
    717 ---@param func function
    718 ---@return table<string,function>
    719 function M.create_callindex(func)
    720  return setmetatable({}, {
    721    --- @param tbl table<any,function>
    722    --- @param arg1 string
    723    --- @return function
    724    __index = function(tbl, arg1)
    725      local ret = function(...)
    726        return func(arg1, ...)
    727      end
    728      tbl[arg1] = ret
    729      return ret
    730    end,
    731  })
    732 end
    733 
    734 --- @param method string
    735 --- @param ... any
    736 function M.nvim_async(method, ...)
    737  assert(session, 'no Nvim session')
    738  assert(not session.eof_err, 'sending notification after EOF from Nvim')
    739  session:notify(method, ...)
    740 end
    741 
    742 --- Executes a Vimscript function via RPC.
    743 --- Fails on Vimscript error, but does not update v:errmsg.
    744 --- @param name string
    745 --- @param ... any
    746 --- @return any
    747 function M.call(name, ...)
    748  return M.request('nvim_call_function', name, { ... })
    749 end
    750 
    751 M.async_meths = M.create_callindex(M.nvim_async)
    752 
    753 M.rpc = {
    754  fn = M.create_callindex(M.call),
    755  api = M.create_callindex(M.request),
    756 }
    757 
    758 M.lua = {
    759  fn = M.create_callindex(M.call_lua),
    760  api = M.create_callindex(M.request_lua),
    761 }
    762 
    763 M.describe_lua_and_rpc = function(describe)
    764  return function(what, tests)
    765    local function d(flavour)
    766      describe(string.format('%s (%s)', what, flavour), function(...)
    767        return tests(M[flavour].api, ...)
    768      end)
    769    end
    770 
    771    d('rpc')
    772    d('lua')
    773  end
    774 end
    775 
    776 --- add for typing. The for loop after will overwrite this
    777 M.api = vim.api
    778 M.fn = vim.fn
    779 
    780 for name, fns in pairs(M.rpc) do
    781  --- @diagnostic disable-next-line:no-unknown
    782  M[name] = fns
    783 end
    784 
    785 -- Executes an ex-command. Vimscript errors manifest as client (lua) errors, but
    786 -- v:errmsg will not be updated.
    787 M.command = M.api.nvim_command
    788 
    789 -- Evaluates a Vimscript expression.
    790 -- Fails on Vimscript error, but does not update v:errmsg.
    791 M.eval = M.api.nvim_eval
    792 
    793 function M.poke_eventloop()
    794  -- Execute 'nvim_eval' (a deferred function) to
    795  -- force at least one main_loop iteration
    796  M.api.nvim_eval('1')
    797 end
    798 
    799 function M.buf_lines(bufnr)
    800  return M.exec_lua('return vim.api.nvim_buf_get_lines((...), 0, -1, false)', bufnr)
    801 end
    802 
    803 ---@see buf_lines()
    804 function M.curbuf_contents()
    805  M.poke_eventloop() -- Before inspecting the buffer, do whatever.
    806  return table.concat(M.api.nvim_buf_get_lines(0, 0, -1, true), '\n')
    807 end
    808 
    809 function M.expect(contents)
    810  return eq(dedent(contents), M.curbuf_contents())
    811 end
    812 
    813 function M.expect_any(contents)
    814  contents = dedent(contents)
    815  return ok(nil ~= string.find(M.curbuf_contents(), contents, 1, true))
    816 end
    817 
    818 -- Checks that the Nvim session did not terminate.
    819 function M.assert_alive()
    820  assert(2 == M.eval('1+1'), 'crash? request failed')
    821 end
    822 
    823 -- Asserts that buffer is loaded and visible in the current tabpage.
    824 function M.assert_visible(bufnr, visible)
    825  assert(type(visible) == 'boolean')
    826  eq(visible, M.api.nvim_buf_is_loaded(bufnr))
    827  if visible then
    828    assert(
    829      -1 ~= M.fn.bufwinnr(bufnr),
    830      'expected buffer to be visible in current tabpage: ' .. tostring(bufnr)
    831    )
    832  else
    833    assert(
    834      -1 == M.fn.bufwinnr(bufnr),
    835      'expected buffer NOT visible in current tabpage: ' .. tostring(bufnr)
    836    )
    837  end
    838 end
    839 
    840 local start_dir = uv.cwd()
    841 
    842 function M.rmdir(path)
    843  local ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true })
    844  if not ret and is_os('win') then
    845    -- Maybe "Permission denied"; try again after changing the nvim
    846    -- process to the top-level directory.
    847    M.command([[exe 'cd '.fnameescape(']] .. start_dir .. "')")
    848    ret, _ = pcall(vim.fs.rm, path, { recursive = true, force = true })
    849  end
    850  -- During teardown, the nvim process may not exit quickly enough, then rmdir()
    851  -- will fail (on Windows).
    852  if not ret then -- Try again.
    853    sleep(1000)
    854    vim.fs.rm(path, { recursive = true, force = true })
    855  end
    856 end
    857 
    858 --- @deprecated Use `t.pcall_err()` to check failure, or `n.command()` to check success.
    859 function M.exc_exec(cmd)
    860  M.command(([[
    861    try
    862      execute "%s"
    863    catch
    864      let g:__exception = v:exception
    865    endtry
    866  ]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
    867  local ret = M.eval('get(g:, "__exception", 0)')
    868  M.command('unlet! g:__exception')
    869  return ret
    870 end
    871 
    872 function M.exec(code)
    873  M.api.nvim_exec2(code, {})
    874 end
    875 
    876 --- @param code string
    877 --- @return string
    878 function M.exec_capture(code)
    879  return M.api.nvim_exec2(code, { output = true }).output
    880 end
    881 
    882 --- Execute Lua code in the wrapped Nvim session.
    883 ---
    884 --- When `code` is passed as a function, it is converted into Lua byte code.
    885 ---
    886 --- Direct upvalues are copied over, however upvalues contained
    887 --- within nested functions are not. Upvalues are also copied back when `code`
    888 --- finishes executing. See `:help lua-upvalue`.
    889 ---
    890 --- Only types which can be serialized can be transferred over, e.g:
    891 --- `table`, `number`, `boolean`, `string`.
    892 ---
    893 --- `code` runs with a different environment and thus will have a different global
    894 --- environment. See `:help lua-environments`.
    895 ---
    896 --- Example:
    897 --- ```lua
    898 --- local upvalue1 = 'upvalue1'
    899 --- exec_lua(function(a, b, c)
    900 ---   print(upvalue1, a, b, c)
    901 ---   (function()
    902 ---     print(upvalue2)
    903 ---   end)()
    904 --- end, 'a', 'b', 'c'
    905 --- ```
    906 --- Prints:
    907 --- ```
    908 --- upvalue1 a b c
    909 --- nil
    910 --- ```
    911 ---
    912 --- Not supported:
    913 --- ```lua
    914 --- local a = vim.uv.new_timer()
    915 --- exec_lua(function()
    916 ---   print(a) -- Error: a is of type 'userdata' which cannot be serialized.
    917 --- end)
    918 --- ```
    919 --- @param code string|function
    920 --- @param ... any
    921 --- @return any
    922 function M.exec_lua(code, ...)
    923  if type(code) == 'string' then
    924    return M.api.nvim_exec_lua(code, { ... })
    925  end
    926 
    927  assert(session, 'no Nvim session')
    928  return require('test.functional.testnvim.exec_lua')(session, 2, code, ...)
    929 end
    930 
    931 function M.get_pathsep()
    932  return is_os('win') and '\\' or '/'
    933 end
    934 
    935 --- Gets the filesystem root dir, namely "/" or "C:/".
    936 function M.pathroot()
    937  local pathsep = package.config:sub(1, 1)
    938  return is_os('win') and (M.nvim_dir:sub(1, 2) .. pathsep) or '/'
    939 end
    940 
    941 --- Gets the full `…/build/bin/{name}` path of a test program produced by
    942 --- `test/functional/fixtures/CMakeLists.txt`.
    943 ---
    944 --- @param name (string) Name of the test program.
    945 function M.testprg(name)
    946  local ext = is_os('win') and '.exe' or ''
    947  return ('%s/%s%s'):format(M.nvim_dir, name, ext)
    948 end
    949 
    950 --- Returns a valid, platform-independent Nvim listen address.
    951 --- Useful for communicating with child instances.
    952 ---
    953 --- @return string
    954 function M.new_pipename()
    955  -- HACK: Start a server temporarily, get the name, then stop it.
    956  local pipename = M.eval('serverstart()')
    957  M.fn.serverstop(pipename)
    958  -- Remove the pipe so that trying to connect to it without a server listening
    959  -- will be an error instead of a hang.
    960  os.remove(pipename)
    961  return pipename
    962 end
    963 
    964 --- @param provider string
    965 --- @return string|boolean?
    966 function M.missing_provider(provider)
    967  if provider == 'ruby' or provider == 'perl' then
    968    --- @type string?
    969    local e = M.exec_lua("return {require('vim.provider." .. provider .. "').detect()}")[2]
    970    return e ~= '' and e or false
    971  elseif provider == 'node' then
    972    --- @type string?
    973    local e = M.fn['provider#node#Detect']()[2]
    974    return e ~= '' and e or false
    975  elseif provider == 'python' then
    976    return M.exec_lua([[return {require('vim.provider.python').detect_by_module('neovim')}]])[2]
    977  end
    978  assert(false, 'Unknown provider: ' .. provider)
    979 end
    980 
    981 local load_factor = 1
    982 if t.is_ci() then
    983  -- Compute load factor only once (but outside of any tests).
    984  M.clear()
    985  M.request('nvim_command', 'source test/old/testdir/load.vim')
    986  load_factor = M.request('nvim_eval', 'g:test_load_factor')
    987 end
    988 
    989 --- @param num number
    990 --- @return number
    991 function M.load_adjust(num)
    992  return math.ceil(num * load_factor)
    993 end
    994 
    995 --- @param ctx table<string,any>
    996 --- @return table
    997 function M.parse_context(ctx)
    998  local parsed = {} --- @type table<string,any>
    999  for _, item in ipairs({ 'regs', 'jumps', 'bufs', 'gvars' }) do
   1000    --- @param v any
   1001    parsed[item] = vim.tbl_filter(function(v)
   1002      return type(v) == 'table'
   1003    end, M.call('msgpackparse', ctx[item]))
   1004  end
   1005  parsed['bufs'] = parsed['bufs'][1]
   1006  --- @param v any
   1007  return vim.tbl_map(function(v)
   1008    if #v == 0 then
   1009      return nil
   1010    end
   1011    return v
   1012  end, parsed)
   1013 end
   1014 
   1015 function M.add_builddir_to_rtp()
   1016  -- Add runtime from build dir for doc/tags (used with :help).
   1017  M.command(string.format([[set rtp+=%s/runtime]], t.paths.test_build_dir))
   1018 end
   1019 
   1020 --- Create folder with non existing parents
   1021 ---
   1022 --- TODO(justinmk): lift this and `t.mkdir()` into vim.fs.
   1023 ---
   1024 --- @param path string
   1025 --- @return boolean?
   1026 function M.mkdir_p(path)
   1027  return os.execute(
   1028    (is_os('win') and 'mkdir ' .. string.gsub(path, '/', '\\') or 'mkdir -p ' .. path)
   1029  )
   1030 end
   1031 
   1032 local testid = (function()
   1033  local id = 0
   1034  return function()
   1035    id = id + 1
   1036    return id
   1037  end
   1038 end)()
   1039 
   1040 return function()
   1041  local g = getfenv(2)
   1042 
   1043  --- @type function?
   1044  local before_each = g.before_each
   1045  --- @type function?
   1046  local after_each = g.after_each
   1047 
   1048  if before_each then
   1049    before_each(function()
   1050      local id = ('T%d'):format(testid())
   1051      _G._nvim_test_id = id
   1052    end)
   1053  end
   1054 
   1055  if after_each then
   1056    after_each(function()
   1057      if not vim.endswith(_G._nvim_test_id, 'x') then
   1058        -- Use a different test ID for skipped tests as well as Nvim instances spawned
   1059        -- between this after_each() and the next before_each() (e.g. in setup()).
   1060        _G._nvim_test_id = _G._nvim_test_id .. 'x'
   1061      end
   1062      check_logs()
   1063      check_cores('build/bin/nvim')
   1064      if session then
   1065        local msg = session:next_message(0)
   1066        if msg then
   1067          if msg[1] == 'notification' and msg[2] == 'nvim_error_event' then
   1068            error(msg[3][2])
   1069          end
   1070        end
   1071      end
   1072    end)
   1073  end
   1074 
   1075  return M
   1076 end