neovim/test/functional/helpers.lua

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1052 lines
29 KiB
Lua
Raw Normal View History

local uv = vim.uv
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 is_os = global_helpers.is_os
local ok = global_helpers.ok
2024-01-12 04:41:09 -07:00
local sleep = uv.sleep
local fail = global_helpers.fail
local module = {}
local runtime_set = 'set runtimepath^=./build/lib/nvim/'
2024-01-15 12:41:22 -07:00
module.nvim_prog = (os.getenv('NVIM_PRG') or global_helpers.paths.test_build_dir .. '/bin/nvim')
-- Default settings for the test session.
module.nvim_set = (
'set shortmess+=IS background=light termguicolors 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,
-- Remove default mappings.
'--cmd',
'mapclear | mapclear!',
feat(highlight): update default color scheme Problem: Default color scheme is suboptimal. Solution: Start using new color scheme. Introduce new `vim` color scheme for opt-in backward compatibility. ------ Main design ideas - Be "Neovim branded". - Be minimal for 256 colors with a bit more shades for true colors. - Be accessible through high enough contrast ratios. - Be suitable for dark and light backgrounds via exchange of dark and light palettes. ------ Palettes - Have dark and light variants. Implemented through exporeted `NvimDark*` and `NvimLight*` hex colors. - Palettes have 4 shades of grey for UI elements and 6 colors (red, yellow, green, cyan, blue, magenta). - Actual values are computed procedurally in Oklch color space based on a handful of hyperparameters. - Each color has a 256 colors variant with perceptually closest color. ------ Highlight groups Use: - Grey shades for general UI according to their design. - Bold text for keywords (`Statement` highlight group). This is an important choice to increase accessibility for people with color deficiencies, as it doesn't rely on actual color. - Green for strings, `DiffAdd` (as background), `DiagnosticOk`, and some minor text UI elements. - Cyan as main syntax color, i.e. for function usage (`Function` highlight group), `DiffText`, `DiagnosticInfo`, and some minor text UI elements. - Red to generally mean high user attention, i.e. errors; in particular for `ErrorMsg`, `DiffDelete`, `DiagnosticError`. - Yellow very sparingly only with true colors to mean mild user attention, i.e. warnings. That is, `DiagnosticWarn` and `WarningMsg`. - Blue very sparingly only with true colors as `DiagnosticHint` and some additional important syntax group (like `Identifier`). - Magenta very carefully (if at all). ------ Notes - To make tests work without relatively larege updates, each one is prepended with an equivalent of the call `:colorscheme vim`. Plus some tests which spawn new Neovim instances also now use 'vim' color scheme. In some cases tests are updated to fit new default color scheme.
2023-11-29 13:16:09 -07:00
-- 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")',
feat(highlight): update default color scheme Problem: Default color scheme is suboptimal. Solution: Start using new color scheme. Introduce new `vim` color scheme for opt-in backward compatibility. ------ Main design ideas - Be "Neovim branded". - Be minimal for 256 colors with a bit more shades for true colors. - Be accessible through high enough contrast ratios. - Be suitable for dark and light backgrounds via exchange of dark and light palettes. ------ Palettes - Have dark and light variants. Implemented through exporeted `NvimDark*` and `NvimLight*` hex colors. - Palettes have 4 shades of grey for UI elements and 6 colors (red, yellow, green, cyan, blue, magenta). - Actual values are computed procedurally in Oklch color space based on a handful of hyperparameters. - Each color has a 256 colors variant with perceptually closest color. ------ Highlight groups Use: - Grey shades for general UI according to their design. - Bold text for keywords (`Statement` highlight group). This is an important choice to increase accessibility for people with color deficiencies, as it doesn't rely on actual color. - Green for strings, `DiffAdd` (as background), `DiagnosticOk`, and some minor text UI elements. - Cyan as main syntax color, i.e. for function usage (`Function` highlight group), `DiffText`, `DiagnosticInfo`, and some minor text UI elements. - Red to generally mean high user attention, i.e. errors; in particular for `ErrorMsg`, `DiffDelete`, `DiagnosticError`. - Yellow very sparingly only with true colors to mean mild user attention, i.e. warnings. That is, `DiagnosticWarn` and `WarningMsg`. - Blue very sparingly only with true colors as `DiagnosticHint` and some additional important syntax group (like `Identifier`). - Magenta very carefully (if at all). ------ Notes - To make tests work without relatively larege updates, each one is prepended with an equivalent of the call `:colorscheme vim`. Plus some tests which spawn new Neovim instances also now use 'vim' color scheme. In some cases tests are updated to fit new default color scheme.
2023-11-29 13:16:09 -07:00
'--cmd',
'unlet g:colors_name',
'--embed',
}
2017-04-09 16:38:20 -07:00
-- Directory containing nvim.
module.nvim_dir = module.nvim_prog:gsub('[/\\][^/\\]+$', '')
if module.nvim_dir == module.nvim_prog then
module.nvim_dir = '.'
end
2024-01-15 12:49:08 -07:00
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
2023-12-13 06:04:24 -07:00
local gdbserver_port = os.getenv('GDBSERVER_PORT') or '7777'
prepend_argv = { 'gdbserver', 'localhost:' .. gdbserver_port }
end
if prepend_argv then
2024-01-15 12:49:08 -07:00
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, #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
2024-01-15 12:49:08 -07:00
local session --- @type test.Session?
local loop_running --- @type boolean?
local last_error --- @type string?
local method_error --- @type string?
refactor(map): enhanced implementation, Clean Code™, etc etc This involves two redesigns of the map.c implementations: 1. Change of macro style and code organization The old khash.h and map.c implementation used huge #define blocks with a lot of backslash line continuations. This instead uses the "implementation file" .c.h pattern. Such a file is meant to be included multiple times, with different macros set prior to inclusion as parameters. we already use this pattern e.g. for eval/typval_encode.c.h to implement different typval encoders reusing a similar structure. We can structure this code into two parts. one that only depends on key type and is enough to implement sets, and one which depends on both key and value to implement maps (as a wrapper around sets, with an added value[] array) 2. Separate the main hash buckets from the key / value arrays Change the hack buckets to only contain an index into separate key / value arrays This is a common pattern in modern, state of the art hashmap implementations. Even though this leads to one more allocated array, it is this often is a net reduction of memory consumption. Consider key+value consuming at least 12 bytes per pair. On average, we will have twice as many buckets per item. Thus old implementation: 2*12 = 24 bytes per item New implementation 1*12 + 2*4 = 20 bytes per item And the difference gets bigger with larger items. One might think we have pulled a fast one here, as wouldn't the average size of the new key/value arrays be 1.5 slots per items due to amortized grows? But remember, these arrays are fully dense, and thus the accessed memory, measured in _cache lines_, the unit which actually matters, will be the fully used memory but just rounded up to the nearest cache line boundary. This has some other interesting properties, such as an insert-only set/map will be fully ordered by insert only. Preserving this ordering in face of deletions is more tricky tho. As we currently don't use ordered maps, the "delete" operation maintains compactness of the item arrays in the simplest way by breaking the ordering. It would be possible to implement an order-preserving delete although at some cost, like allowing the items array to become non-dense until the next rehash. Finally, in face of these two major changes, all code used in khash.h has been integrated into map.c and friends. Given the heavy edits it makes no sense to "layer" the code into a vendored and a wrapper part. Rather, the layered cake follows the specialization depth: code shared for all maps, code specialized to a key type (and its equivalence relation), and finally code specialized to value+key type.
2023-05-17 07:08:06 -07:00
if not is_os('win') then
2024-01-15 12:49:08 -07:00
local sigpipe_handler = assert(uv.new_signal())
uv.signal_start(sigpipe_handler, 'sigpipe', function()
refactor(map): enhanced implementation, Clean Code™, etc etc This involves two redesigns of the map.c implementations: 1. Change of macro style and code organization The old khash.h and map.c implementation used huge #define blocks with a lot of backslash line continuations. This instead uses the "implementation file" .c.h pattern. Such a file is meant to be included multiple times, with different macros set prior to inclusion as parameters. we already use this pattern e.g. for eval/typval_encode.c.h to implement different typval encoders reusing a similar structure. We can structure this code into two parts. one that only depends on key type and is enough to implement sets, and one which depends on both key and value to implement maps (as a wrapper around sets, with an added value[] array) 2. Separate the main hash buckets from the key / value arrays Change the hack buckets to only contain an index into separate key / value arrays This is a common pattern in modern, state of the art hashmap implementations. Even though this leads to one more allocated array, it is this often is a net reduction of memory consumption. Consider key+value consuming at least 12 bytes per pair. On average, we will have twice as many buckets per item. Thus old implementation: 2*12 = 24 bytes per item New implementation 1*12 + 2*4 = 20 bytes per item And the difference gets bigger with larger items. One might think we have pulled a fast one here, as wouldn't the average size of the new key/value arrays be 1.5 slots per items due to amortized grows? But remember, these arrays are fully dense, and thus the accessed memory, measured in _cache lines_, the unit which actually matters, will be the fully used memory but just rounded up to the nearest cache line boundary. This has some other interesting properties, such as an insert-only set/map will be fully ordered by insert only. Preserving this ordering in face of deletions is more tricky tho. As we currently don't use ordered maps, the "delete" operation maintains compactness of the item arrays in the simplest way by breaking the ordering. It would be possible to implement an order-preserving delete although at some cost, like allowing the items array to become non-dense until the next rehash. Finally, in face of these two major changes, all code used in khash.h has been integrated into map.c and friends. Given the heavy edits it makes no sense to "layer" the code into a vendored and a wrapper part. Rather, the layered cake follows the specialization depth: code shared for all maps, code specialized to a key type (and its equivalence relation), and finally code specialized to value+key type.
2023-05-17 07:08:06 -07:00
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
2024-01-15 12:49:08 -07:00
--- @param method string
--- @param ... any
--- @return any
function module.request(method, ...)
2024-01-15 12:49:08 -07:00
assert(session)
local status, rv = session:request(method, ...)
if not status then
if loop_running then
2024-01-15 12:49:08 -07:00
--- @type string
last_error = rv[2]
session:stop()
else
error(rv[2])
end
end
return rv
end
2024-01-15 12:49:08 -07:00
--- @param method string
--- @param ... any
--- @return any
function module.request_lua(method, ...)
return module.exec_lua([[return vim.api[...](select(2, ...))]], method, ...)
end
2024-01-15 12:49:08 -07:00
--- @param timeout? integer
--- @return string?
function module.next_msg(timeout)
2024-01-15 12:49:08 -07:00
assert(session)
return session:next_message(timeout or 10000)
end
function module.expect_twostreams(msgs1, msgs2)
2017-06-10 06:25:23 -07:00
local pos1, pos2 = 1, 1
while pos1 <= #msgs1 or pos2 <= #msgs2 do
local msg = module.next_msg()
2017-06-10 06:25:23 -07:00
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 {}
2024-01-15 12:49:08 -07:00
--- @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 = 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
)
2024-01-02 18:09:18 -07:00
)
)
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
2024-01-15 12:49:08 -07:00
--- @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 module.set_method_error(err)
method_error = err
end
2024-01-15 12:49:08 -07:00
--- @param lsession test.Session
--- @param request_cb function?
--- @param notification_cb function?
--- @param setup_cb function?
2024-01-15 12:49:08 -07:00
--- @param timeout integer
--- @return {[1]: integer, [2]: string}
function module.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
2024-01-15 12:49:08 -07:00
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
2022-05-02 12:10:01 -07:00
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
2022-05-02 12:10:01 -07:00
return lsession.eof_err
end
function module.run(request_cb, notification_cb, setup_cb, timeout)
2024-01-15 12:49:08 -07:00
assert(session)
return module.run_session(session, request_cb, notification_cb, setup_cb, timeout)
end
function module.stop()
2024-01-15 12:49:08 -07:00
assert(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
-- 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(...)
2024-01-15 12:49:08 -07:00
assert(session)
while session:next_message(timeout) do
end
if session.eof_err then
error(session.eof_err[2])
end
end, fn_or_timeout, ...)
2024-01-02 18:09:18 -07:00
)
end
end
2024-01-15 12:49:08 -07:00
--- Executes a Vimscript function via Lua.
--- Fails on Vimscript error, but does not update v:errmsg.
--- @param name string
--- @param ... any
--- @return any
function module.call_lua(name, ...)
return module.exec_lua([[return vim.call(...)]], name, ...)
end
2024-01-15 12:49:08 -07:00
--- 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 = module.request('nvim_input', input)
2019-08-09 16:26:43 -07:00
if written == nil then
module.assert_alive()
error('crash? (nvim_input returned nil)')
2019-08-09 16:26:43 -07:00
end
input = input:sub(written + 1)
end
end
2024-01-15 12:49:08 -07:00
--- @param ... string
function module.feed(...)
for _, v in ipairs({ ... }) do
nvim_feed(dedent(v))
end
end
2024-01-15 12:49:08 -07:00
--- @param ... string
function module.rawfeed(...)
for _, v in ipairs({ ... }) do
nvim_feed(dedent(v))
end
end
2024-01-15 12:49:08 -07:00
---@param ... string[]?
---@return string[]
function module.merge_args(...)
local i = 1
2024-01-15 12:49:08 -07:00
local argv = {} --- @type string[]
for anum = 1, select('#', ...) do
2024-01-15 12:49:08 -07:00
--- @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
2024-01-15 12:49:08 -07:00
--- 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[]
2019-04-16 16:08:48 -07:00
local function remove_args(args, args_rm)
2024-01-15 12:49:08 -07:00
local new_args = {} --- @type string[]
2019-04-16 16:08:48 -07:00
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
2019-04-16 16:08:48 -07:00
last = ''
elseif vim.tbl_contains(args_rm, arg) then
2019-04-16 16:08:48 -07:00
last = arg
elseif arg == runtime_set and vim.tbl_contains(args_rm, 'runtimepath') then
table.remove(new_args) -- Remove the preceding "--cmd".
last = ''
2019-04-16 16:08:48 -07:00
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 = 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
2024-01-15 12:49:08 -07:00
--- @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
2022-04-22 11:56:31 -07:00
function module.spawn(argv, merge, env, keep, io_extra)
if not keep then
module.check_close()
end
2016-06-26 08:16:54 -07:00
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 (and returns) a new global Nvim session.
2019-04-16 16:08:48 -07:00
--
2018-05-29 22:21:45 -07:00
-- Parameters are interpreted as startup args, OR a map with these keys:
2019-04-16 16:08:48 -07:00
-- 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.
2018-05-29 22:21:45 -07:00
--
-- Example:
-- clear('-e')
2019-04-16 16:08:48 -07:00
-- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
function module.clear(...)
2022-05-02 12:10:01 -07:00
module.set_session(module.spawn_argv(false, ...))
return module.get_session()
2022-05-02 12:10:01 -07:00
end
2024-01-15 12:49:08 -07:00
--- same params as clear, but does returns the session instead
--- of replacing the default session
--- @return test.Session
2022-05-02 12:10:01 -07:00
function module.spawn_argv(keep, ...)
2022-04-22 11:56:31 -07:00
local argv, env, io_extra = module.new_argv(...)
2022-05-02 12:10:01 -07:00
return module.spawn(argv, nil, env, keep, io_extra)
end
2024-01-15 12:49:08 -07:00
--- @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 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
2024-01-15 12:49:08 -07:00
local new_args --- @type string[]
local io_extra --- @type uv.uv_pipe_t?
local env --- @type string[]?
--- @type test.new_argv.Opts|string
2016-06-26 08:16:54 -07:00
local opts = select(1, ...)
if type(opts) ~= 'table' then
new_args = { ... }
else
2019-04-16 16:08:48 -07:00
args = remove_args(args, opts.args_rm)
2016-06-26 08:16:54 -07:00
if opts.env then
2024-01-15 12:49:08 -07:00
local env_opt = {} --- @type table<string,string>
2016-06-26 08:16:54 -07:00
for k, v in pairs(opts.env) do
assert(type(k) == 'string')
assert(type(v) == 'string')
env_opt[k] = v
2016-06-26 08:16:54 -07:00
end
for _, k in ipairs({
'HOME',
'ASAN_OPTIONS',
'TSAN_OPTIONS',
'MSAN_OPTIONS',
2019-04-16 16:08:48 -07:00
'LD_LIBRARY_PATH',
'PATH',
2016-06-26 08:16:54 -07:00
'NVIM_LOG_FILE',
'NVIM_RPLUGIN_MANIFEST',
'GCOV_ERROR_FILE',
'XDG_DATA_DIRS',
'TMPDIR',
'VIMRUNTIME',
2016-06-26 08:16:54 -07:00
}) do
-- Set these from the environment unless the caller defined them.
if not env_opt[k] then
env_opt[k] = os.getenv(k)
end
2016-06-26 08:16:54 -07:00
end
env = {}
for k, v in pairs(env_opt) do
2016-06-26 08:16:54 -07:00
env[#env + 1] = k .. '=' .. v
end
end
new_args = opts.args or {}
2022-04-22 11:56:31 -07:00
io_extra = opts.io_extra
2016-06-26 08:16:54 -07:00
end
for _, arg in ipairs(new_args) do
table.insert(args, arg)
end
2022-04-22 11:56:31 -07:00
return args, env, io_extra
end
2024-01-15 12:49:08 -07:00
--- @param ... string
function module.insert(...)
nvim_feed('i')
for _, v in ipairs({ ... }) do
local escaped = v:gsub('<', '<lt>')
module.rawfeed(escaped)
end
nvim_feed('<ESC>')
end
2024-01-15 12:49:08 -07:00
--- 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 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
2019-12-31 12:21:57 -07:00
function module.has_powershell()
return module.eval('executable("' .. (is_os('win') and 'powershell' or 'pwsh') .. '")') == 1
2019-12-31 12:21:57 -07:00
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' },
','
2024-01-02 18:09:18 -07:00
)
.. ';'
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'
2017-01-11 21:08:19 -07:00
]])
return found
2017-01-11 21:08:19 -07:00
end
---@param func function
---@return table<string,function>
function module.create_callindex(func)
return setmetatable({}, {
2024-01-15 12:49:08 -07:00
--- @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
2024-01-15 12:49:08 -07:00
--- @param method string
--- @param ... any
function module.nvim_async(method, ...)
2024-01-15 12:49:08 -07:00
assert(session):notify(method, ...)
end
2024-01-15 12:49:08 -07:00
--- Executes a Vimscript function via RPC.
--- Fails on Vimscript error, but does not update v:errmsg.
--- @param name string
--- @param ... any
--- @return any
function module.call(name, ...)
return module.request('nvim_call_function', name, { ... })
end
module.async_meths = module.create_callindex(module.nvim_async)
module.rpc = {
fn = module.create_callindex(module.call),
api = module.create_callindex(module.request),
}
module.lua = {
fn = module.create_callindex(module.call_lua),
api = module.create_callindex(module.request_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
2024-01-12 05:44:54 -07:00
--- add for typing. The for loop after will overwrite this
module.api = vim.api
module.fn = vim.fn
2024-01-12 05:44:54 -07:00
for name, fns in pairs(module.rpc) do
2024-01-15 12:49:08 -07:00
--- @diagnostic disable-next-line:no-unknown
module[name] = fns
end
-- Executes an ex-command. Vimscript errors manifest as client (lua) errors, but
-- v:errmsg will not be updated.
module.command = module.api.nvim_command
-- Evaluates a Vimscript expression.
-- Fails on Vimscript error, but does not update v:errmsg.
module.eval = module.api.nvim_eval
function module.poke_eventloop()
-- Execute 'nvim_eval' (a deferred function) to
-- force at least one main_loop iteration
module.api.nvim_eval('1')
end
2020-01-08 10:32:49 -07:00
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.api.nvim_buf_get_lines(0, 0, -1, true), '\n')
end
function module.expect(contents)
return eq(dedent(contents), module.curbuf_contents())
end
function module.expect_any(contents)
2017-02-28 01:34:02 -07:00
contents = dedent(contents)
return ok(nil ~= string.find(module.curbuf_contents(), contents, 1, true))
2017-02-28 01:34:02 -07:00
end
2024-01-15 12:49:08 -07:00
--- @param expected any[]
--- @param received any[]
--- @param kind string
--- @return any
function module.expect_events(expected, received, kind)
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
2024-01-12 04:51:31 -07:00
msg = msg .. ' ' .. vim.inspect(e) .. ';\n'
end
msg = msg .. '\nexpected events:\n'
for _, e in ipairs(expected) do
2024-01-12 04:51:31 -07:00
msg = msg .. ' ' .. vim.inspect(e) .. ';\n'
end
fail(msg)
end
return received
end
2019-08-09 16:26:43 -07:00
-- Checks that the Nvim session did not terminate.
function module.assert_alive()
assert(2 == module.eval('1+1'), 'crash? request failed')
2019-08-09 16:26:43 -07:00
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.api.nvim_buf_is_loaded(bufnr))
if visible then
assert(
-1 ~= module.fn.bufwinnr(bufnr),
'expected buffer to be visible in current tabpage: ' .. tostring(bufnr)
2024-01-02 18:09:18 -07:00
)
else
assert(
-1 == module.fn.bufwinnr(bufnr),
'expected buffer NOT visible in current tabpage: ' .. tostring(bufnr)
2024-01-02 18:09:18 -07:00
)
end
end
2024-01-15 12:49:08 -07:00
--- @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
2016-06-04 23:51:50 -07:00
local abspath = path .. '/' .. file
if global_helpers.isdir(abspath) then
do_rmdir(abspath) -- recurse
2016-06-04 23:51:50 -07:00
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
2016-06-04 23:51:50 -07:00
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 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
2024-01-15 12:49:08 -07:00
--- @param cond boolean
--- @param reason string
--- @return boolean
function module.skip(cond, reason)
if cond then
2024-01-15 12:49:08 -07:00
--- @type fun(reason: string)
local pending = getfenv(2).pending
pending(reason or 'FIXME')
return true
end
2024-01-15 12:49:08 -07:00
return false
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.api.nvim_exec2(code, {})
end
2024-01-15 12:49:08 -07:00
--- @param code string
--- @return string
function module.exec_capture(code)
return module.api.nvim_exec2(code, { output = true }).output
end
--- @param code string
--- @return any
function module.exec_lua(code, ...)
return module.api.nvim_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
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.
2020-11-22 02:10:37 -07:00
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.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
2024-01-15 12:49:08 -07:00
--- @param provider string
--- @return string|boolean?
function module.missing_provider(provider)
2024-01-22 14:07:14 -07:00
if provider == 'ruby' or provider == 'perl' then
2024-01-15 12:49:08 -07:00
--- @type string?
2024-01-22 14:07:14 -07:00
local e = module.exec_lua("return {require('vim.provider." .. provider .. "').detect()}")[2]
return e ~= '' and e or false
2024-01-22 14:07:14 -07:00
elseif provider == 'node' then
--- @type string?
local e = module.fn['provider#node#Detect']()[2]
return e ~= '' and e or false
elseif provider == 'python' then
return module.exec_lua([[return {require('vim.provider.python').detect_by_module('neovim')}]])[2]
end
2024-01-15 12:49:08 -07:00
assert(false, 'Unknown provider: ' .. provider)
end
2024-01-15 12:49:08 -07:00
--- @param obj string|table
--- @return any
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
2024-01-15 12:49:08 -07:00
--- @cast obj table<any,any>
local ret = {} --- @type table<any,any>
for k, v in pairs(obj) do
ret[k] = module.alter_slashes(v)
end
return ret
end
2024-01-15 12:49:08 -07:00
assert(false, 'expected string or table of strings, got ' .. type(obj))
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
2024-01-15 12:49:08 -07:00
--- @param num number
--- @return number
function module.load_adjust(num)
return math.ceil(num * load_factor)
end
2024-01-15 12:49:08 -07:00
--- @param ctx table<string,any>
--- @return table
function module.parse_context(ctx)
2024-01-15 12:49:08 -07:00
local parsed = {} --- @type table<string,any>
for _, item in ipairs({ 'regs', 'jumps', 'bufs', 'gvars' }) do
2024-01-15 12:49:08 -07:00
--- @param v any
parsed[item] = vim.tbl_filter(function(v)
2019-07-17 13:00:50 -07:00
return type(v) == 'table'
end, module.call('msgpackparse', ctx[item]))
2019-07-17 13:00:50 -07:00
end
parsed['bufs'] = parsed['bufs'][1]
2024-01-15 12:49:08 -07:00
--- @param v any
return vim.tbl_map(function(v)
2019-07-17 13:00:50 -07:00
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).
2024-01-15 12:41:22 -07:00
module.command(string.format([[set rtp+=%s/runtime]], module.paths.test_build_dir))
end
2024-01-15 12:49:08 -07:00
--- Kill (reap) a process by PID.
--- @param pid string
--- @return boolean?
function module.os_kill(pid)
return os.execute(
2024-01-02 18:09:18 -07:00
(
is_os('win') and 'taskkill /f /t /pid ' .. pid .. ' > nul'
or 'kill -9 ' .. pid .. ' > /dev/null'
)
2024-01-02 18:09:18 -07:00
)
end
2024-01-15 12:49:08 -07:00
--- Create folder with non existing parents
--- @param path string
--- @return boolean?
function module.mkdir_p(path)
return os.execute((is_os('win') and 'mkdir ' .. path or 'mkdir -p ' .. path))
end
--- @class test.functional.helpers: test.helpers
module = vim.tbl_extend('error', module, global_helpers)
--- @return test.functional.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