diff options
author | dundargoc <gocdundar@gmail.com> | 2024-04-20 17:44:13 +0200 |
---|---|---|
committer | dundargoc <33953936+dundargoc@users.noreply.github.com> | 2024-04-23 18:17:04 +0200 |
commit | 052498ed42780a76daea589d063cd8947a894673 (patch) | |
tree | b6c85416a4d7ced5eabb0a7a3866f5e0fee886cc /test/functional/testnvim.lua | |
parent | c5af5c0b9ab84c86f84e32210512923e7eb641ba (diff) | |
download | rneovim-052498ed42780a76daea589d063cd8947a894673.tar.gz rneovim-052498ed42780a76daea589d063cd8947a894673.tar.bz2 rneovim-052498ed42780a76daea589d063cd8947a894673.zip |
test: improve test conventions
Specifically, functions that are run in the context of the test runner
are put in module `test/testutil.lua` while the functions that are run
in the context of the test session are put in
`test/functional/testnvim.lua`.
Closes https://github.com/neovim/neovim/issues/27004.
Diffstat (limited to 'test/functional/testnvim.lua')
-rw-r--r-- | test/functional/testnvim.lua | 1013 |
1 files changed, 1013 insertions, 0 deletions
diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua new file mode 100644 index 0000000000..0533e8ad0a --- /dev/null +++ b/test/functional/testnvim.lua @@ -0,0 +1,1013 @@ +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 termguicolors 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) + 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 {[1]: integer, [2]: 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 +--- @return any +function M.exec_lua(code, ...) + 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 |