mirror of
https://github.com/neovim/neovim.git
synced 2024-12-29 14:41:06 -07:00
573a71469d
Problem: `'scrollbind'` does not work properly if the window being scrolled automatically contains any filler/virtual lines (except for diff filler lines). This is because when the scrollbind check is done, the logic only considers changes to topline which are represented as line numbers. Solution: Write the logic for determine the scroll amount to take into account filler/virtual lines. Fixes #29751
1023 lines
27 KiB
Lua
1023 lines
27 KiB
Lua
local uv = vim.uv
|
|
local t = require('test.testutil')
|
|
|
|
local Session = require('test.client.session')
|
|
local uv_stream = require('test.client.uv_stream')
|
|
local SocketStream = uv_stream.SocketStream
|
|
local ChildProcessStream = uv_stream.ChildProcessStream
|
|
|
|
local check_cores = t.check_cores
|
|
local check_logs = t.check_logs
|
|
local dedent = t.dedent
|
|
local eq = t.eq
|
|
local is_os = t.is_os
|
|
local ok = t.ok
|
|
local sleep = uv.sleep
|
|
|
|
--- This module uses functions from the context of the test session, i.e. in the context of the
|
|
--- nvim being tests.
|
|
local M = {}
|
|
|
|
local runtime_set = 'set runtimepath^=./build/lib/nvim/'
|
|
M.nvim_prog = (os.getenv('NVIM_PRG') or t.paths.test_build_dir .. '/bin/nvim')
|
|
-- Default settings for the test session.
|
|
M.nvim_set = (
|
|
'set shortmess+=IS background=light noswapfile noautoindent startofline'
|
|
.. ' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
|
|
.. ' belloff= wildoptions-=pum joinspaces noshowcmd noruler nomore redrawdebug=invalid'
|
|
)
|
|
M.nvim_argv = {
|
|
M.nvim_prog,
|
|
'-u',
|
|
'NONE',
|
|
'-i',
|
|
'NONE',
|
|
-- XXX: find treesitter parsers.
|
|
'--cmd',
|
|
runtime_set,
|
|
'--cmd',
|
|
M.nvim_set,
|
|
-- Remove default user commands and mappings.
|
|
'--cmd',
|
|
'comclear | mapclear | mapclear!',
|
|
-- Make screentest work after changing to the new default color scheme
|
|
-- Source 'vim' color scheme without side effects
|
|
-- TODO: rewrite tests
|
|
'--cmd',
|
|
'lua dofile("runtime/colors/vim.lua")',
|
|
'--cmd',
|
|
'unlet g:colors_name',
|
|
'--embed',
|
|
}
|
|
|
|
-- Directory containing nvim.
|
|
M.nvim_dir = M.nvim_prog:gsub('[/\\][^/\\]+$', '')
|
|
if M.nvim_dir == M.nvim_prog then
|
|
M.nvim_dir = '.'
|
|
end
|
|
|
|
local prepend_argv --- @type string[]?
|
|
|
|
if os.getenv('VALGRIND') then
|
|
local log_file = os.getenv('VALGRIND_LOG') or 'valgrind-%p.log'
|
|
prepend_argv = {
|
|
'valgrind',
|
|
'-q',
|
|
'--tool=memcheck',
|
|
'--leak-check=yes',
|
|
'--track-origins=yes',
|
|
'--show-possibly-lost=no',
|
|
'--suppressions=src/.valgrind.supp',
|
|
'--log-file=' .. log_file,
|
|
}
|
|
if os.getenv('GDB') then
|
|
table.insert(prepend_argv, '--vgdb=yes')
|
|
table.insert(prepend_argv, '--vgdb-error=0')
|
|
end
|
|
elseif os.getenv('GDB') then
|
|
local gdbserver_port = os.getenv('GDBSERVER_PORT') or '7777'
|
|
prepend_argv = { 'gdbserver', 'localhost:' .. gdbserver_port }
|
|
end
|
|
|
|
if prepend_argv then
|
|
local new_nvim_argv = {} --- @type string[]
|
|
local len = #prepend_argv
|
|
for i = 1, len do
|
|
new_nvim_argv[i] = prepend_argv[i]
|
|
end
|
|
for i = 1, #M.nvim_argv do
|
|
new_nvim_argv[i + len] = M.nvim_argv[i]
|
|
end
|
|
M.nvim_argv = new_nvim_argv
|
|
M.prepend_argv = prepend_argv
|
|
end
|
|
|
|
local session --- @type test.Session?
|
|
local loop_running --- @type boolean?
|
|
local last_error --- @type string?
|
|
local method_error --- @type string?
|
|
|
|
if not is_os('win') then
|
|
local sigpipe_handler = assert(uv.new_signal())
|
|
uv.signal_start(sigpipe_handler, 'sigpipe', function()
|
|
print('warning: got SIGPIPE signal. Likely related to a crash in nvim')
|
|
end)
|
|
end
|
|
|
|
function M.get_session()
|
|
return session
|
|
end
|
|
|
|
function M.set_session(s)
|
|
session = s
|
|
end
|
|
|
|
--- @param method string
|
|
--- @param ... any
|
|
--- @return any
|
|
function M.request(method, ...)
|
|
assert(session, 'no Nvim session')
|
|
local status, rv = session:request(method, ...)
|
|
if not status then
|
|
if loop_running then
|
|
--- @type string
|
|
last_error = rv[2]
|
|
session:stop()
|
|
else
|
|
error(rv[2])
|
|
end
|
|
end
|
|
return rv
|
|
end
|
|
|
|
--- @param method string
|
|
--- @param ... any
|
|
--- @return any
|
|
function M.request_lua(method, ...)
|
|
return M.exec_lua([[return vim.api[...](select(2, ...))]], method, ...)
|
|
end
|
|
|
|
--- @param timeout? integer
|
|
--- @return string?
|
|
function M.next_msg(timeout)
|
|
assert(session)
|
|
return session:next_message(timeout or 10000)
|
|
end
|
|
|
|
function M.expect_twostreams(msgs1, msgs2)
|
|
local pos1, pos2 = 1, 1
|
|
while pos1 <= #msgs1 or pos2 <= #msgs2 do
|
|
local msg = M.next_msg()
|
|
if pos1 <= #msgs1 and pcall(eq, msgs1[pos1], msg) then
|
|
pos1 = pos1 + 1
|
|
elseif pos2 <= #msgs2 then
|
|
eq(msgs2[pos2], msg)
|
|
pos2 = pos2 + 1
|
|
else
|
|
-- already failed, but show the right error message
|
|
eq(msgs1[pos1], msg)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Expects a sequence of next_msg() results. If multiple sequences are
|
|
-- passed they are tried until one succeeds, in order of shortest to longest.
|
|
--
|
|
-- Can be called with positional args (list of sequences only):
|
|
-- expect_msg_seq(seq1, seq2, ...)
|
|
-- or keyword args:
|
|
-- expect_msg_seq{ignore={...}, seqs={seq1, seq2, ...}}
|
|
--
|
|
-- ignore: List of ignored event names.
|
|
-- seqs: List of one or more potential event sequences.
|
|
function M.expect_msg_seq(...)
|
|
if select('#', ...) < 1 then
|
|
error('need at least 1 argument')
|
|
end
|
|
local arg1 = select(1, ...)
|
|
if (arg1['seqs'] and select('#', ...) > 1) or type(arg1) ~= 'table' then
|
|
error('invalid args')
|
|
end
|
|
local ignore = arg1['ignore'] and arg1['ignore'] or {}
|
|
--- @type string[]
|
|
local seqs = arg1['seqs'] and arg1['seqs'] or { ... }
|
|
if type(ignore) ~= 'table' then
|
|
error("'ignore' arg must be a list of strings")
|
|
end
|
|
table.sort(seqs, function(a, b) -- Sort ascending, by (shallow) length.
|
|
return #a < #b
|
|
end)
|
|
|
|
local actual_seq = {}
|
|
local nr_ignored = 0
|
|
local final_error = ''
|
|
local function cat_err(err1, err2)
|
|
if err1 == nil then
|
|
return err2
|
|
end
|
|
return string.format('%s\n%s\n%s', err1, string.rep('=', 78), err2)
|
|
end
|
|
local msg_timeout = M.load_adjust(10000) -- Big timeout for ASAN/valgrind.
|
|
for anum = 1, #seqs do
|
|
local expected_seq = seqs[anum]
|
|
-- Collect enough messages to compare the next expected sequence.
|
|
while #actual_seq < #expected_seq do
|
|
local msg = M.next_msg(msg_timeout)
|
|
local msg_type = msg and msg[2] or nil
|
|
if msg == nil then
|
|
error(
|
|
cat_err(
|
|
final_error,
|
|
string.format(
|
|
'got %d messages (ignored %d), expected %d',
|
|
#actual_seq,
|
|
nr_ignored,
|
|
#expected_seq
|
|
)
|
|
)
|
|
)
|
|
elseif vim.tbl_contains(ignore, msg_type) then
|
|
nr_ignored = nr_ignored + 1
|
|
else
|
|
table.insert(actual_seq, msg)
|
|
end
|
|
end
|
|
local status, result = pcall(eq, expected_seq, actual_seq)
|
|
if status then
|
|
return result
|
|
end
|
|
local message = result
|
|
if type(result) == 'table' then
|
|
-- 'eq' returns several things
|
|
--- @type string
|
|
message = result.message
|
|
end
|
|
final_error = cat_err(final_error, message)
|
|
end
|
|
error(final_error)
|
|
end
|
|
|
|
local function call_and_stop_on_error(lsession, ...)
|
|
local status, result = Session.safe_pcall(...) -- luacheck: ignore
|
|
if not status then
|
|
lsession:stop()
|
|
last_error = result
|
|
return ''
|
|
end
|
|
return result
|
|
end
|
|
|
|
function M.set_method_error(err)
|
|
method_error = err
|
|
end
|
|
|
|
--- @param lsession test.Session
|
|
--- @param request_cb function?
|
|
--- @param notification_cb function?
|
|
--- @param setup_cb function?
|
|
--- @param timeout integer
|
|
--- @return [integer, string]
|
|
function M.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
|
|
local on_request --- @type function?
|
|
local on_notification --- @type function?
|
|
local on_setup --- @type function?
|
|
|
|
if request_cb then
|
|
function on_request(method, args)
|
|
method_error = nil
|
|
local result = call_and_stop_on_error(lsession, request_cb, method, args)
|
|
if method_error ~= nil then
|
|
return method_error, true
|
|
end
|
|
return result
|
|
end
|
|
end
|
|
|
|
if notification_cb then
|
|
function on_notification(method, args)
|
|
call_and_stop_on_error(lsession, notification_cb, method, args)
|
|
end
|
|
end
|
|
|
|
if setup_cb then
|
|
function on_setup()
|
|
call_and_stop_on_error(lsession, setup_cb)
|
|
end
|
|
end
|
|
|
|
loop_running = true
|
|
lsession:run(on_request, on_notification, on_setup, timeout)
|
|
loop_running = false
|
|
if last_error then
|
|
local err = last_error
|
|
last_error = nil
|
|
error(err)
|
|
end
|
|
|
|
return lsession.eof_err
|
|
end
|
|
|
|
function M.run(request_cb, notification_cb, setup_cb, timeout)
|
|
assert(session)
|
|
return M.run_session(session, request_cb, notification_cb, setup_cb, timeout)
|
|
end
|
|
|
|
function M.stop()
|
|
assert(session):stop()
|
|
end
|
|
|
|
function M.nvim_prog_abs()
|
|
-- system(['build/bin/nvim']) does not work for whatever reason. It must
|
|
-- be executable searched in $PATH or something starting with / or ./.
|
|
if M.nvim_prog:match('[/\\]') then
|
|
return M.request('nvim_call_function', 'fnamemodify', { M.nvim_prog, ':p' })
|
|
else
|
|
return M.nvim_prog
|
|
end
|
|
end
|
|
|
|
-- Use for commands which expect nvim to quit.
|
|
-- The first argument can also be a timeout.
|
|
function M.expect_exit(fn_or_timeout, ...)
|
|
local eof_err_msg = 'EOF was received from Nvim. Likely the Nvim process crashed.'
|
|
if type(fn_or_timeout) == 'function' then
|
|
eq(eof_err_msg, t.pcall_err(fn_or_timeout, ...))
|
|
else
|
|
eq(
|
|
eof_err_msg,
|
|
t.pcall_err(function(timeout, fn, ...)
|
|
fn(...)
|
|
assert(session)
|
|
while session:next_message(timeout) do
|
|
end
|
|
if session.eof_err then
|
|
error(session.eof_err[2])
|
|
end
|
|
end, fn_or_timeout, ...)
|
|
)
|
|
end
|
|
end
|
|
|
|
--- Executes a Vimscript function via Lua.
|
|
--- Fails on Vimscript error, but does not update v:errmsg.
|
|
--- @param name string
|
|
--- @param ... any
|
|
--- @return any
|
|
function M.call_lua(name, ...)
|
|
return M.exec_lua([[return vim.call(...)]], name, ...)
|
|
end
|
|
|
|
--- Sends user input to Nvim.
|
|
--- Does not fail on Vimscript error, but v:errmsg will be updated.
|
|
--- @param input string
|
|
local function nvim_feed(input)
|
|
while #input > 0 do
|
|
local written = M.request('nvim_input', input)
|
|
if written == nil then
|
|
M.assert_alive()
|
|
error('crash? (nvim_input returned nil)')
|
|
end
|
|
input = input:sub(written + 1)
|
|
end
|
|
end
|
|
|
|
--- @param ... string
|
|
function M.feed(...)
|
|
for _, v in ipairs({ ... }) do
|
|
nvim_feed(dedent(v))
|
|
end
|
|
end
|
|
|
|
---@param ... string[]?
|
|
---@return string[]
|
|
function M.merge_args(...)
|
|
local i = 1
|
|
local argv = {} --- @type string[]
|
|
for anum = 1, select('#', ...) do
|
|
--- @type string[]?
|
|
local args = select(anum, ...)
|
|
if args then
|
|
for _, arg in ipairs(args) do
|
|
argv[i] = arg
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
return argv
|
|
end
|
|
|
|
--- Removes Nvim startup args from `args` matching items in `args_rm`.
|
|
---
|
|
--- - Special case: "-u", "-i", "--cmd" are treated specially: their "values" are also removed.
|
|
--- - Special case: "runtimepath" will remove only { '--cmd', 'set runtimepath^=…', }
|
|
---
|
|
--- Example:
|
|
--- args={'--headless', '-u', 'NONE'}
|
|
--- args_rm={'--cmd', '-u'}
|
|
--- Result:
|
|
--- {'--headless'}
|
|
---
|
|
--- All matching cases are removed.
|
|
---
|
|
--- Example:
|
|
--- args={'--cmd', 'foo', '-N', '--cmd', 'bar'}
|
|
--- args_rm={'--cmd', '-u'}
|
|
--- Result:
|
|
--- {'-N'}
|
|
--- @param args string[]
|
|
--- @param args_rm string[]
|
|
--- @return string[]
|
|
local function remove_args(args, args_rm)
|
|
local new_args = {} --- @type string[]
|
|
local skip_following = { '-u', '-i', '-c', '--cmd', '-s', '--listen' }
|
|
if not args_rm or #args_rm == 0 then
|
|
return { unpack(args) }
|
|
end
|
|
for _, v in ipairs(args_rm) do
|
|
assert(type(v) == 'string')
|
|
end
|
|
local last = ''
|
|
for _, arg in ipairs(args) do
|
|
if vim.tbl_contains(skip_following, last) then
|
|
last = ''
|
|
elseif vim.tbl_contains(args_rm, arg) then
|
|
last = arg
|
|
elseif arg == runtime_set and vim.tbl_contains(args_rm, 'runtimepath') then
|
|
table.remove(new_args) -- Remove the preceding "--cmd".
|
|
last = ''
|
|
else
|
|
table.insert(new_args, arg)
|
|
end
|
|
end
|
|
return new_args
|
|
end
|
|
|
|
function M.check_close()
|
|
if not session then
|
|
return
|
|
end
|
|
local start_time = uv.now()
|
|
session:close()
|
|
uv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
|
|
local end_time = uv.now()
|
|
local delta = end_time - start_time
|
|
if delta > 500 then
|
|
print(
|
|
'nvim took '
|
|
.. delta
|
|
.. ' milliseconds to exit after last test\n'
|
|
.. 'This indicates a likely problem with the test even if it passed!\n'
|
|
)
|
|
io.stdout:flush()
|
|
end
|
|
session = nil
|
|
end
|
|
|
|
--- @param argv string[]
|
|
--- @param merge boolean?
|
|
--- @param env string[]?
|
|
--- @param keep boolean
|
|
--- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option
|
|
--- @return test.Session
|
|
function M.spawn(argv, merge, env, keep, io_extra)
|
|
if not keep then
|
|
M.check_close()
|
|
end
|
|
|
|
local child_stream =
|
|
ChildProcessStream.spawn(merge and M.merge_args(prepend_argv, argv) or argv, env, io_extra)
|
|
return Session.new(child_stream)
|
|
end
|
|
|
|
-- Creates a new Session connected by domain socket (named pipe) or TCP.
|
|
function M.connect(file_or_address)
|
|
local addr, port = string.match(file_or_address, '(.*):(%d+)')
|
|
local stream = (addr and port) and SocketStream.connect(addr, port)
|
|
or SocketStream.open(file_or_address)
|
|
return Session.new(stream)
|
|
end
|
|
|
|
-- Starts (and returns) a new global Nvim session.
|
|
--
|
|
-- Parameters are interpreted as startup args, OR a map with these keys:
|
|
-- args: List: Args appended to the default `nvim_argv` set.
|
|
-- args_rm: List: Args removed from the default set. All cases are
|
|
-- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd"
|
|
-- (and its value) from the default set.
|
|
-- env: Map: Defines the environment of the new session.
|
|
--
|
|
-- Example:
|
|
-- clear('-e')
|
|
-- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
|
|
function M.clear(...)
|
|
M.set_session(M.spawn_argv(false, ...))
|
|
return M.get_session()
|
|
end
|
|
|
|
--- same params as clear, but does returns the session instead
|
|
--- of replacing the default session
|
|
--- @return test.Session
|
|
function M.spawn_argv(keep, ...)
|
|
local argv, env, io_extra = M.new_argv(...)
|
|
return M.spawn(argv, nil, env, keep, io_extra)
|
|
end
|
|
|
|
--- @class test.new_argv.Opts
|
|
--- @field args? string[]
|
|
--- @field args_rm? string[]
|
|
--- @field env? table<string,string>
|
|
--- @field io_extra? uv.uv_pipe_t
|
|
|
|
--- Builds an argument list for use in clear().
|
|
---
|
|
--- @see clear() for parameters.
|
|
--- @param ... string
|
|
--- @return string[]
|
|
--- @return string[]?
|
|
--- @return uv.uv_pipe_t?
|
|
function M.new_argv(...)
|
|
local args = { unpack(M.nvim_argv) }
|
|
table.insert(args, '--headless')
|
|
if _G._nvim_test_id then
|
|
-- Set the server name to the test-id for logging. #8519
|
|
table.insert(args, '--listen')
|
|
table.insert(args, _G._nvim_test_id)
|
|
end
|
|
local new_args --- @type string[]
|
|
local io_extra --- @type uv.uv_pipe_t?
|
|
local env --- @type string[]?
|
|
--- @type test.new_argv.Opts|string
|
|
local opts = select(1, ...)
|
|
if type(opts) ~= 'table' then
|
|
new_args = { ... }
|
|
else
|
|
args = remove_args(args, opts.args_rm)
|
|
if opts.env then
|
|
local env_opt = {} --- @type table<string,string>
|
|
for k, v in pairs(opts.env) do
|
|
assert(type(k) == 'string')
|
|
assert(type(v) == 'string')
|
|
env_opt[k] = v
|
|
end
|
|
for _, k in ipairs({
|
|
'HOME',
|
|
'ASAN_OPTIONS',
|
|
'TSAN_OPTIONS',
|
|
'MSAN_OPTIONS',
|
|
'LD_LIBRARY_PATH',
|
|
'PATH',
|
|
'NVIM_LOG_FILE',
|
|
'NVIM_RPLUGIN_MANIFEST',
|
|
'GCOV_ERROR_FILE',
|
|
'XDG_DATA_DIRS',
|
|
'TMPDIR',
|
|
'VIMRUNTIME',
|
|
}) do
|
|
-- Set these from the environment unless the caller defined them.
|
|
if not env_opt[k] then
|
|
env_opt[k] = os.getenv(k)
|
|
end
|
|
end
|
|
env = {}
|
|
for k, v in pairs(env_opt) do
|
|
env[#env + 1] = k .. '=' .. v
|
|
end
|
|
end
|
|
new_args = opts.args or {}
|
|
io_extra = opts.io_extra
|
|
end
|
|
for _, arg in ipairs(new_args) do
|
|
table.insert(args, arg)
|
|
end
|
|
return args, env, io_extra
|
|
end
|
|
|
|
--- @param ... string
|
|
function M.insert(...)
|
|
nvim_feed('i')
|
|
for _, v in ipairs({ ... }) do
|
|
local escaped = v:gsub('<', '<lt>')
|
|
M.feed(escaped)
|
|
end
|
|
nvim_feed('<ESC>')
|
|
end
|
|
|
|
--- Executes an ex-command by user input. Because nvim_input() is used, Vimscript
|
|
--- errors will not manifest as client (lua) errors. Use command() for that.
|
|
--- @param ... string
|
|
function M.feed_command(...)
|
|
for _, v in ipairs({ ... }) do
|
|
if v:sub(1, 1) ~= '/' then
|
|
-- not a search command, prefix with colon
|
|
nvim_feed(':')
|
|
end
|
|
nvim_feed(v:gsub('<', '<lt>'))
|
|
nvim_feed('<CR>')
|
|
end
|
|
end
|
|
|
|
-- @deprecated use nvim_exec2()
|
|
function M.source(code)
|
|
M.exec(dedent(code))
|
|
end
|
|
|
|
function M.has_powershell()
|
|
return M.eval('executable("' .. (is_os('win') and 'powershell' or 'pwsh') .. '")') == 1
|
|
end
|
|
|
|
--- Sets Nvim shell to powershell.
|
|
---
|
|
--- @param fake (boolean) If true, a fake will be used if powershell is not
|
|
--- found on the system.
|
|
--- @returns true if powershell was found on the system, else false.
|
|
function M.set_shell_powershell(fake)
|
|
local found = M.has_powershell()
|
|
if not fake then
|
|
assert(found)
|
|
end
|
|
local shell = found and (is_os('win') and 'powershell' or 'pwsh') or M.testprg('pwsh-test')
|
|
local cmd = 'Remove-Item -Force '
|
|
.. table.concat(
|
|
is_os('win') and { 'alias:cat', 'alias:echo', 'alias:sleep', 'alias:sort', 'alias:tee' }
|
|
or { 'alias:echo' },
|
|
','
|
|
)
|
|
.. ';'
|
|
M.exec([[
|
|
let &shell = ']] .. shell .. [['
|
|
set shellquote= shellxquote=
|
|
let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command '
|
|
let &shellcmdflag .= '[Console]::InputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new();'
|
|
let &shellcmdflag .= '$PSDefaultParameterValues[''Out-File:Encoding'']=''utf8'';'
|
|
let &shellcmdflag .= ']] .. cmd .. [['
|
|
let &shellredir = '2>&1 | %%{ "$_" } | Out-File %s; exit $LastExitCode'
|
|
let &shellpipe = '2>&1 | %%{ "$_" } | tee %s; exit $LastExitCode'
|
|
]])
|
|
return found
|
|
end
|
|
|
|
---@param func function
|
|
---@return table<string,function>
|
|
function M.create_callindex(func)
|
|
return setmetatable({}, {
|
|
--- @param tbl table<any,function>
|
|
--- @param arg1 string
|
|
--- @return function
|
|
__index = function(tbl, arg1)
|
|
local ret = function(...)
|
|
return func(arg1, ...)
|
|
end
|
|
tbl[arg1] = ret
|
|
return ret
|
|
end,
|
|
})
|
|
end
|
|
|
|
--- @param method string
|
|
--- @param ... any
|
|
function M.nvim_async(method, ...)
|
|
assert(session):notify(method, ...)
|
|
end
|
|
|
|
--- Executes a Vimscript function via RPC.
|
|
--- Fails on Vimscript error, but does not update v:errmsg.
|
|
--- @param name string
|
|
--- @param ... any
|
|
--- @return any
|
|
function M.call(name, ...)
|
|
return M.request('nvim_call_function', name, { ... })
|
|
end
|
|
|
|
M.async_meths = M.create_callindex(M.nvim_async)
|
|
|
|
M.rpc = {
|
|
fn = M.create_callindex(M.call),
|
|
api = M.create_callindex(M.request),
|
|
}
|
|
|
|
M.lua = {
|
|
fn = M.create_callindex(M.call_lua),
|
|
api = M.create_callindex(M.request_lua),
|
|
}
|
|
|
|
M.describe_lua_and_rpc = function(describe)
|
|
return function(what, tests)
|
|
local function d(flavour)
|
|
describe(string.format('%s (%s)', what, flavour), function(...)
|
|
return tests(M[flavour].api, ...)
|
|
end)
|
|
end
|
|
|
|
d('rpc')
|
|
d('lua')
|
|
end
|
|
end
|
|
|
|
--- add for typing. The for loop after will overwrite this
|
|
M.api = vim.api
|
|
M.fn = vim.fn
|
|
|
|
for name, fns in pairs(M.rpc) do
|
|
--- @diagnostic disable-next-line:no-unknown
|
|
M[name] = fns
|
|
end
|
|
|
|
-- Executes an ex-command. Vimscript errors manifest as client (lua) errors, but
|
|
-- v:errmsg will not be updated.
|
|
M.command = M.api.nvim_command
|
|
|
|
-- Evaluates a Vimscript expression.
|
|
-- Fails on Vimscript error, but does not update v:errmsg.
|
|
M.eval = M.api.nvim_eval
|
|
|
|
function M.poke_eventloop()
|
|
-- Execute 'nvim_eval' (a deferred function) to
|
|
-- force at least one main_loop iteration
|
|
M.api.nvim_eval('1')
|
|
end
|
|
|
|
function M.buf_lines(bufnr)
|
|
return M.exec_lua('return vim.api.nvim_buf_get_lines((...), 0, -1, false)', bufnr)
|
|
end
|
|
|
|
---@see buf_lines()
|
|
function M.curbuf_contents()
|
|
M.poke_eventloop() -- Before inspecting the buffer, do whatever.
|
|
return table.concat(M.api.nvim_buf_get_lines(0, 0, -1, true), '\n')
|
|
end
|
|
|
|
function M.expect(contents)
|
|
return eq(dedent(contents), M.curbuf_contents())
|
|
end
|
|
|
|
function M.expect_any(contents)
|
|
contents = dedent(contents)
|
|
return ok(nil ~= string.find(M.curbuf_contents(), contents, 1, true))
|
|
end
|
|
|
|
-- Checks that the Nvim session did not terminate.
|
|
function M.assert_alive()
|
|
assert(2 == M.eval('1+1'), 'crash? request failed')
|
|
end
|
|
|
|
-- Asserts that buffer is loaded and visible in the current tabpage.
|
|
function M.assert_visible(bufnr, visible)
|
|
assert(type(visible) == 'boolean')
|
|
eq(visible, M.api.nvim_buf_is_loaded(bufnr))
|
|
if visible then
|
|
assert(
|
|
-1 ~= M.fn.bufwinnr(bufnr),
|
|
'expected buffer to be visible in current tabpage: ' .. tostring(bufnr)
|
|
)
|
|
else
|
|
assert(
|
|
-1 == M.fn.bufwinnr(bufnr),
|
|
'expected buffer NOT visible in current tabpage: ' .. tostring(bufnr)
|
|
)
|
|
end
|
|
end
|
|
|
|
--- @param path string
|
|
local function do_rmdir(path)
|
|
local stat = uv.fs_stat(path)
|
|
if stat == nil then
|
|
return
|
|
end
|
|
if stat.type ~= 'directory' then
|
|
error(string.format('rmdir: not a directory: %s', path))
|
|
end
|
|
for file in vim.fs.dir(path) do
|
|
if file ~= '.' and file ~= '..' then
|
|
local abspath = path .. '/' .. file
|
|
if t.isdir(abspath) then
|
|
do_rmdir(abspath) -- recurse
|
|
else
|
|
local ret, err = os.remove(abspath)
|
|
if not ret then
|
|
if not session then
|
|
error('os.remove: ' .. err)
|
|
else
|
|
-- Try Nvim delete(): it handles `readonly` attribute on Windows,
|
|
-- and avoids Lua cross-version/platform incompatibilities.
|
|
if -1 == M.call('delete', abspath) then
|
|
local hint = (is_os('win') and ' (hint: try :%bwipeout! before rmdir())' or '')
|
|
error('delete() failed' .. hint .. ': ' .. abspath)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local ret, err = uv.fs_rmdir(path)
|
|
if not ret then
|
|
error('luv.fs_rmdir(' .. path .. '): ' .. err)
|
|
end
|
|
end
|
|
|
|
local start_dir = uv.cwd()
|
|
|
|
function M.rmdir(path)
|
|
local ret, _ = pcall(do_rmdir, path)
|
|
if not ret and is_os('win') then
|
|
-- Maybe "Permission denied"; try again after changing the nvim
|
|
-- process to the top-level directory.
|
|
M.command([[exe 'cd '.fnameescape(']] .. start_dir .. "')")
|
|
ret, _ = pcall(do_rmdir, path)
|
|
end
|
|
-- During teardown, the nvim process may not exit quickly enough, then rmdir()
|
|
-- will fail (on Windows).
|
|
if not ret then -- Try again.
|
|
sleep(1000)
|
|
do_rmdir(path)
|
|
end
|
|
end
|
|
|
|
function M.exc_exec(cmd)
|
|
M.command(([[
|
|
try
|
|
execute "%s"
|
|
catch
|
|
let g:__exception = v:exception
|
|
endtry
|
|
]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
|
|
local ret = M.eval('get(g:, "__exception", 0)')
|
|
M.command('unlet! g:__exception')
|
|
return ret
|
|
end
|
|
|
|
function M.exec(code)
|
|
M.api.nvim_exec2(code, {})
|
|
end
|
|
|
|
--- @param code string
|
|
--- @return string
|
|
function M.exec_capture(code)
|
|
return M.api.nvim_exec2(code, { output = true }).output
|
|
end
|
|
|
|
--- @param code string|function
|
|
--- @return any
|
|
function M.exec_lua(code, ...)
|
|
if type(code) == 'function' then
|
|
return M.api.nvim_exec_lua(
|
|
[[
|
|
local code = ...
|
|
return loadstring(code)(select(2, ...))
|
|
]],
|
|
{ string.dump(code), ... }
|
|
)
|
|
end
|
|
return M.api.nvim_exec_lua(code, { ... })
|
|
end
|
|
|
|
function M.get_pathsep()
|
|
return is_os('win') and '\\' or '/'
|
|
end
|
|
|
|
--- Gets the filesystem root dir, namely "/" or "C:/".
|
|
function M.pathroot()
|
|
local pathsep = package.config:sub(1, 1)
|
|
return is_os('win') and (M.nvim_dir:sub(1, 2) .. pathsep) or '/'
|
|
end
|
|
|
|
--- Gets the full `…/build/bin/{name}` path of a test program produced by
|
|
--- `test/functional/fixtures/CMakeLists.txt`.
|
|
---
|
|
--- @param name (string) Name of the test program.
|
|
function M.testprg(name)
|
|
local ext = is_os('win') and '.exe' or ''
|
|
return ('%s/%s%s'):format(M.nvim_dir, name, ext)
|
|
end
|
|
|
|
function M.is_asan()
|
|
local version = M.eval('execute("verbose version")')
|
|
return version:match('-fsanitize=[a-z,]*address')
|
|
end
|
|
|
|
-- Returns a valid, platform-independent Nvim listen address.
|
|
-- Useful for communicating with child instances.
|
|
function M.new_pipename()
|
|
-- HACK: Start a server temporarily, get the name, then stop it.
|
|
local pipename = M.eval('serverstart()')
|
|
M.fn.serverstop(pipename)
|
|
-- Remove the pipe so that trying to connect to it without a server listening
|
|
-- will be an error instead of a hang.
|
|
os.remove(pipename)
|
|
return pipename
|
|
end
|
|
|
|
--- @param provider string
|
|
--- @return string|boolean?
|
|
function M.missing_provider(provider)
|
|
if provider == 'ruby' or provider == 'perl' then
|
|
--- @type string?
|
|
local e = M.exec_lua("return {require('vim.provider." .. provider .. "').detect()}")[2]
|
|
return e ~= '' and e or false
|
|
elseif provider == 'node' then
|
|
--- @type string?
|
|
local e = M.fn['provider#node#Detect']()[2]
|
|
return e ~= '' and e or false
|
|
elseif provider == 'python' then
|
|
return M.exec_lua([[return {require('vim.provider.python').detect_by_module('neovim')}]])[2]
|
|
end
|
|
assert(false, 'Unknown provider: ' .. provider)
|
|
end
|
|
|
|
--- @param obj string|table
|
|
--- @return any
|
|
function M.alter_slashes(obj)
|
|
if not is_os('win') then
|
|
return obj
|
|
end
|
|
if type(obj) == 'string' then
|
|
local ret = obj:gsub('/', '\\')
|
|
return ret
|
|
elseif type(obj) == 'table' then
|
|
--- @cast obj table<any,any>
|
|
local ret = {} --- @type table<any,any>
|
|
for k, v in pairs(obj) do
|
|
ret[k] = M.alter_slashes(v)
|
|
end
|
|
return ret
|
|
end
|
|
assert(false, 'expected string or table of strings, got ' .. type(obj))
|
|
end
|
|
|
|
local load_factor = 1
|
|
if t.is_ci() then
|
|
-- Compute load factor only once (but outside of any tests).
|
|
M.clear()
|
|
M.request('nvim_command', 'source test/old/testdir/load.vim')
|
|
load_factor = M.request('nvim_eval', 'g:test_load_factor')
|
|
end
|
|
|
|
--- @param num number
|
|
--- @return number
|
|
function M.load_adjust(num)
|
|
return math.ceil(num * load_factor)
|
|
end
|
|
|
|
--- @param ctx table<string,any>
|
|
--- @return table
|
|
function M.parse_context(ctx)
|
|
local parsed = {} --- @type table<string,any>
|
|
for _, item in ipairs({ 'regs', 'jumps', 'bufs', 'gvars' }) do
|
|
--- @param v any
|
|
parsed[item] = vim.tbl_filter(function(v)
|
|
return type(v) == 'table'
|
|
end, M.call('msgpackparse', ctx[item]))
|
|
end
|
|
parsed['bufs'] = parsed['bufs'][1]
|
|
--- @param v any
|
|
return vim.tbl_map(function(v)
|
|
if #v == 0 then
|
|
return nil
|
|
end
|
|
return v
|
|
end, parsed)
|
|
end
|
|
|
|
function M.add_builddir_to_rtp()
|
|
-- Add runtime from build dir for doc/tags (used with :help).
|
|
M.command(string.format([[set rtp+=%s/runtime]], t.paths.test_build_dir))
|
|
end
|
|
|
|
--- Kill (reap) a process by PID.
|
|
--- @param pid string
|
|
--- @return boolean?
|
|
function M.os_kill(pid)
|
|
return os.execute(
|
|
(
|
|
is_os('win') and 'taskkill /f /t /pid ' .. pid .. ' > nul'
|
|
or 'kill -9 ' .. pid .. ' > /dev/null'
|
|
)
|
|
)
|
|
end
|
|
|
|
--- Create folder with non existing parents
|
|
--- @param path string
|
|
--- @return boolean?
|
|
function M.mkdir_p(path)
|
|
return os.execute((is_os('win') and 'mkdir ' .. path or 'mkdir -p ' .. path))
|
|
end
|
|
|
|
local testid = (function()
|
|
local id = 0
|
|
return function()
|
|
id = id + 1
|
|
return id
|
|
end
|
|
end)()
|
|
|
|
return function()
|
|
local g = getfenv(2)
|
|
|
|
--- @type function?
|
|
local before_each = g.before_each
|
|
--- @type function?
|
|
local after_each = g.after_each
|
|
|
|
if before_each then
|
|
before_each(function()
|
|
local id = ('T%d'):format(testid())
|
|
_G._nvim_test_id = id
|
|
end)
|
|
end
|
|
|
|
if after_each then
|
|
after_each(function()
|
|
check_logs()
|
|
check_cores('build/bin/nvim')
|
|
if session then
|
|
local msg = session:next_message(0)
|
|
if msg then
|
|
if msg[1] == 'notification' and msg[2] == 'nvim_error_event' then
|
|
error(msg[3][2])
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
return M
|
|
end
|