neovim/test/functional/helpers.lua
bfredl b04286a187 feat(extmark): support proper multiline ranges
The removes the previous restriction that nvim_buf_set_extmark()
could not be used to highlight arbitrary multi-line regions

The problem can be summarized as follows: let's assume an extmark with a
hl_group is placed covering the region (5,0) to (50,0) Now, consider
what happens if nvim needs to redraw a window covering the lines 20-30.
It needs to be able to ask the marktree what extmarks cover this region,
even if they don't begin or end here.

Therefore the marktree needs to be augmented with the information covers
a point, not just what marks begin or end there. To do this, we augment
each node with a field "intersect" which is a set the ids of the
marks which overlap this node, but only if it is not part of the set of
any parent. This ensures the number of nodes that need to be explicitly
marked grows only logarithmically with the total number of explicitly
nodes (and thus the number of of overlapping marks).

Thus we can quickly iterate all marks which overlaps any query position
by looking up what leaf node contains that position. Then we only need
to consider all "start" marks within that leaf node, and the "intersect"
set of that node and all its parents.

Now, and the major source of complexity is that the tree restructuring
operations (to ensure that each node has T-1 <= size <= 2*T-1) also need
to update these sets. If a full inner node is split in two, one of the
new parents might start to completely overlap some ranges and its ids
will need to be moved from its children's sets to its own set.
Similarly, if two undersized nodes gets joined into one, it might no
longer completely overlap some ranges, and now the children which do
needs to have the have the ids in its set instead. And then there are
the pivots! Yes the pivot operations when a child gets moved from one
parent to another.
2023-09-12 10:38:23 +02:00

970 lines
27 KiB
Lua

local luv = require('luv')
local global_helpers = require('test.helpers')
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 = global_helpers.check_cores
local check_logs = global_helpers.check_logs
local dedent = global_helpers.dedent
local eq = global_helpers.eq
local filter = global_helpers.tbl_filter
local is_os = global_helpers.is_os
local map = global_helpers.tbl_map
local ok = global_helpers.ok
local sleep = global_helpers.sleep
local tbl_contains = global_helpers.tbl_contains
local fail = global_helpers.fail
local module = {
}
local start_dir = luv.cwd()
local runtime_set = 'set runtimepath^=./build/lib/nvim/'
module.nvim_prog = (
os.getenv('NVIM_PRG')
or global_helpers.test_build_dir .. '/bin/nvim'
)
-- Default settings for the test session.
module.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')
module.nvim_argv = {
module.nvim_prog, '-u', 'NONE', '-i', 'NONE',
-- XXX: find treesitter parsers.
'--cmd', runtime_set,
'--cmd', module.nvim_set,
'--cmd', 'mapclear',
'--cmd', 'mapclear!',
'--embed'}
-- Directory containing nvim.
module.nvim_dir = module.nvim_prog:gsub("[/\\][^/\\]+$", "")
if module.nvim_dir == module.nvim_prog then
module.nvim_dir = "."
end
local prepend_argv
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 = '7777'
if os.getenv('GDBSERVER_PORT') then
gdbserver_port = os.getenv('GDBSERVER_PORT')
end
prepend_argv = {'gdbserver', 'localhost:'..gdbserver_port}
end
if prepend_argv then
local new_nvim_argv = {}
local len = #prepend_argv
for i = 1, len do
new_nvim_argv[i] = prepend_argv[i]
end
for i = 1, #module.nvim_argv do
new_nvim_argv[i + len] = module.nvim_argv[i]
end
module.nvim_argv = new_nvim_argv
module.prepend_argv = prepend_argv
end
local session, loop_running, last_error, method_error
if not is_os('win') then
local sigpipe_handler = luv.new_signal()
luv.signal_start(sigpipe_handler, "sigpipe", function()
print("warning: got SIGPIPE signal. Likely related to a crash in nvim")
end)
end
function module.get_session()
return session
end
function module.set_session(s)
session = s
end
function module.request(method, ...)
local status, rv = session:request(method, ...)
if not status then
if loop_running then
last_error = rv[2]
session:stop()
else
error(rv[2])
end
end
return rv
end
function module.request_lua(method, ...)
return module.exec_lua([[return vim.api[...](select(2, ...))]], method, ...)
end
function module.next_msg(timeout)
return session:next_message(timeout and timeout or 10000)
end
function module.expect_twostreams(msgs1, msgs2)
local pos1, pos2 = 1, 1
while pos1 <= #msgs1 or pos2 <= #msgs2 do
local msg = module.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 module.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 {}
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 = module.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 = module.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 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
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 module.set_method_error(err)
method_error = err
end
function module.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
local on_request, on_notification, on_setup
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 module.run(request_cb, notification_cb, setup_cb, timeout)
return module.run_session(session, request_cb, notification_cb, setup_cb, timeout)
end
function module.stop()
session:stop()
end
function module.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 module.nvim_prog:match('[/\\]') then
return module.request('nvim_call_function', 'fnamemodify', {module.nvim_prog, ':p'})
else
return module.nvim_prog
end
end
-- Executes an ex-command. Vimscript errors manifest as client (lua) errors, but
-- v:errmsg will not be updated.
function module.command(cmd)
module.request('nvim_command', cmd)
end
-- Use for commands which expect nvim to quit.
-- The first argument can also be a timeout.
function module.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, module.pcall_err(fn_or_timeout, ...))
else
eq(eof_err_msg, module.pcall_err(function(timeout, fn, ...)
fn(...)
while session:next_message(timeout) do
end
if session.eof_err then
error(session.eof_err[2])
end
end, fn_or_timeout, ...))
end
end
-- Evaluates a Vimscript expression.
-- Fails on Vimscript error, but does not update v:errmsg.
function module.eval(expr)
return module.request('nvim_eval', expr)
end
-- Executes a Vimscript function via RPC.
-- Fails on Vimscript error, but does not update v:errmsg.
function module.call(name, ...)
return module.request('nvim_call_function', name, {...})
end
-- Executes a Vimscript function via Lua.
-- Fails on Vimscript error, but does not update v:errmsg.
function module.call_lua(name, ...)
return module.exec_lua([[return vim.call(...)]], name, ...)
end
-- Sends user input to Nvim.
-- Does not fail on Vimscript error, but v:errmsg will be updated.
local function nvim_feed(input)
while #input > 0 do
local written = module.request('nvim_input', input)
if written == nil then
module.assert_alive()
error('crash? (nvim_input returned nil)')
end
input = input:sub(written + 1)
end
end
function module.feed(...)
for _, v in ipairs({...}) do
nvim_feed(dedent(v))
end
end
function module.rawfeed(...)
for _, v in ipairs({...}) do
nvim_feed(dedent(v))
end
end
function module.merge_args(...)
local i = 1
local argv = {}
for anum = 1,select('#', ...) do
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'}
local function remove_args(args, args_rm)
local new_args = {}
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 tbl_contains(skip_following, last) then
last = ''
elseif tbl_contains(args_rm, arg) then
last = arg
elseif arg == runtime_set and 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 module.check_close()
if not session then
return
end
local start_time = luv.now()
session:close()
luv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
local end_time = luv.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 io_extra used for stdin_fd, see :help ui-option
function module.spawn(argv, merge, env, keep, io_extra)
if not keep then
module.check_close()
end
local child_stream = ChildProcessStream.spawn(
merge and module.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 module.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 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 module.clear(...)
module.set_session(module.spawn_argv(false, ...))
end
-- same params as clear, but does returns the session instead
-- of replacing the default session
function module.spawn_argv(keep, ...)
local argv, env, io_extra = module.new_argv(...)
return module.spawn(argv, nil, env, keep, io_extra)
end
-- Builds an argument list for use in clear().
--
---@see clear() for parameters.
function module.new_argv(...)
local args = {unpack(module.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
local io_extra
local env = nil
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 = {}
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
function module.insert(...)
nvim_feed('i')
for _, v in ipairs({...}) do
local escaped = v:gsub('<', '<lt>')
module.rawfeed(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.
function module.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 module.source(code)
module.exec(dedent(code))
end
function module.has_powershell()
return module.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 module.set_shell_powershell(fake)
local found = module.has_powershell()
if not fake then
assert(found)
end
local shell = found and (is_os('win') and 'powershell' or 'pwsh') or module.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'}, ',')..';'
module.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
function module.create_callindex(func)
local table = {}
setmetatable(table, {
__index = function(tbl, arg1)
local ret = function(...) return func(arg1, ...) end
tbl[arg1] = ret
return ret
end,
})
return table
end
local function ui(method, ...)
return module.request('nvim_ui_'..method, ...)
end
function module.nvim_async(method, ...)
session:notify('nvim_'..method, ...)
end
module.async_meths = module.create_callindex(module.nvim_async)
module.uimeths = module.create_callindex(ui)
local function create_api(request, call)
local m = {}
function m.nvim(method, ...)
return request('nvim_'..method, ...)
end
function m.buffer(method, ...)
return request('nvim_buf_'..method, ...)
end
function m.window(method, ...)
return request('nvim_win_'..method, ...)
end
function m.tabpage(method, ...)
return request('nvim_tabpage_'..method, ...)
end
function m.curbuf(method, ...)
if not method then
return m.nvim('get_current_buf')
end
return m.buffer(method, 0, ...)
end
function m.curwin(method, ...)
if not method then
return m.nvim('get_current_win')
end
return m.window(method, 0, ...)
end
function m.curtab(method, ...)
if not method then
return m.nvim('get_current_tabpage')
end
return m.tabpage(method, 0, ...)
end
m.funcs = module.create_callindex(call)
m.meths = module.create_callindex(m.nvim)
m.bufmeths = module.create_callindex(m.buffer)
m.winmeths = module.create_callindex(m.window)
m.tabmeths = module.create_callindex(m.tabpage)
m.curbufmeths = module.create_callindex(m.curbuf)
m.curwinmeths = module.create_callindex(m.curwin)
m.curtabmeths = module.create_callindex(m.curtab)
return m
end
module.rpc = {
api = create_api(module.request, module.call),
}
module.lua = {
api = create_api(module.request_lua, module.call_lua),
}
module.describe_lua_and_rpc = function(describe)
return function(what, tests)
local function d(flavour)
describe(string.format('%s (%s)', what, flavour), function(...)
return tests(module[flavour].api, ...)
end)
end
d('rpc')
d('lua')
end
end
for name, fn in pairs(module.rpc.api) do
module[name] = fn
end
function module.poke_eventloop()
-- Execute 'nvim_eval' (a deferred function) to
-- force at least one main_loop iteration
session:request('nvim_eval', '1')
end
function module.buf_lines(bufnr)
return module.exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr)
end
---@see buf_lines()
function module.curbuf_contents()
module.poke_eventloop() -- Before inspecting the buffer, do whatever.
return table.concat(module.curbuf('get_lines', 0, -1, true), '\n')
end
function module.expect(contents)
return eq(dedent(contents), module.curbuf_contents())
end
function module.expect_any(contents)
contents = dedent(contents)
return ok(nil ~= string.find(module.curbuf_contents(), contents, 1, true))
end
function module.expect_events(expected, received, kind)
local inspect = require'vim.inspect'
if not pcall(eq, expected, received) then
local msg = 'unexpected '..kind..' received.\n\n'
msg = msg .. 'received events:\n'
for _, e in ipairs(received) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
msg = msg .. '\nexpected events:\n'
for _, e in ipairs(expected) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
fail(msg)
end
return received
end
-- Checks that the Nvim session did not terminate.
function module.assert_alive()
assert(2 == module.eval('1+1'), 'crash? request failed')
end
-- Asserts that buffer is loaded and visible in the current tabpage.
function module.assert_visible(bufnr, visible)
assert(type(visible) == 'boolean')
eq(visible, module.bufmeths.is_loaded(bufnr))
if visible then
assert(-1 ~= module.funcs.bufwinnr(bufnr),
'expected buffer to be visible in current tabpage: '..tostring(bufnr))
else
assert(-1 == module.funcs.bufwinnr(bufnr),
'expected buffer NOT visible in current tabpage: '..tostring(bufnr))
end
end
local function do_rmdir(path)
local stat = luv.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 global_helpers.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 == module.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 = luv.fs_rmdir(path)
if not ret then
error('luv.fs_rmdir('..path..'): '..err)
end
end
function module.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.
module.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 module.exc_exec(cmd)
module.command(([[
try
execute "%s"
catch
let g:__exception = v:exception
endtry
]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
local ret = module.eval('get(g:, "__exception", 0)')
module.command('unlet! g:__exception')
return ret
end
function module.skip(cond, reason)
if cond then
local pending = getfenv(2).pending
pending(reason or 'FIXME')
return true
else
return false
end
end
-- Calls pending() and returns `true` if the system is too slow to
-- run fragile or expensive tests. Else returns `false`.
function module.skip_fragile(pending_fn, cond)
if pending_fn == nil or type(pending_fn) ~= type(function()end) then
error("invalid pending_fn")
end
if cond then
pending_fn("skipped (test is fragile on this system)", function() end)
return true
elseif os.getenv("TEST_SKIP_FRAGILE") then
pending_fn("skipped (TEST_SKIP_FRAGILE)", function() end)
return true
end
return false
end
function module.exec(code)
module.meths.exec2(code, {})
end
function module.exec_capture(code)
return module.meths.exec2(code, { output = true }).output
end
function module.exec_lua(code, ...)
return module.meths.exec_lua(code, {...})
end
function module.get_pathsep()
return is_os('win') and '\\' or '/'
end
--- Gets the filesystem root dir, namely "/" or "C:/".
function module.pathroot()
local pathsep = package.config:sub(1,1)
return is_os('win') and (module.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 module.testprg(name)
local ext = module.is_os('win') and '.exe' or ''
return ('%s/%s%s'):format(module.nvim_dir, name, ext)
end
function module.is_asan()
local version = module.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 module.new_pipename()
-- HACK: Start a server temporarily, get the name, then stop it.
local pipename = module.eval('serverstart()')
module.funcs.serverstop(pipename)
return pipename
end
function module.missing_provider(provider)
if provider == 'ruby' or provider == 'node' or provider == 'perl' then
local e = module.funcs['provider#'..provider..'#Detect']()[2]
return e ~= '' and e or false
elseif provider == 'python' or provider == 'python3' then
local py_major_version = (provider == 'python3' and 3 or 2)
local e = module.funcs['provider#pythonx#Detect'](py_major_version)[2]
return e ~= '' and e or false
else
assert(false, 'Unknown provider: '..provider)
end
end
function module.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
local ret = {}
for k, v in pairs(obj) do
ret[k] = module.alter_slashes(v)
end
return ret
else
assert(false, 'expected string or table of strings, got '..type(obj))
end
end
local load_factor = 1
if global_helpers.is_ci() then
-- Compute load factor only once (but outside of any tests).
module.clear()
module.request('nvim_command', 'source test/old/testdir/load.vim')
load_factor = module.request('nvim_eval', 'g:test_load_factor')
end
function module.load_adjust(num)
return math.ceil(num * load_factor)
end
function module.parse_context(ctx)
local parsed = {}
for _, item in ipairs({'regs', 'jumps', 'bufs', 'gvars'}) do
parsed[item] = filter(function(v)
return type(v) == 'table'
end, module.call('msgpackparse', ctx[item]))
end
parsed['bufs'] = parsed['bufs'][1]
return map(function(v)
if #v == 0 then
return nil
end
return v
end, parsed)
end
function module.add_builddir_to_rtp()
-- Add runtime from build dir for doc/tags (used with :help).
module.command(string.format([[set rtp+=%s/runtime]], module.test_build_dir))
end
-- Kill process with given pid
function module.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
function module.mkdir_p(path)
return os.execute((is_os('win')
and 'mkdir '..path
or 'mkdir -p '..path))
end
module = global_helpers.tbl_extend('error', module, global_helpers)
return function(after_each)
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 module
end