mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 19:38:20 +00:00

Hope this will make people using feed_command less likely: this hides bugs. Already found at least two: 1. msgpackparse() will show internal error: hash_add() in case of duplicate keys, though it will still work correctly. Currently silenced. 2. ttimeoutlen was spelled incorrectly, resulting in option not being set when expected. Test was still functioning somehow though. Currently fixed.
633 lines
15 KiB
Lua
633 lines
15 KiB
Lua
require('coxpcall')
|
|
local luv = require('luv')
|
|
local lfs = require('lfs')
|
|
local global_helpers = require('test.helpers')
|
|
|
|
-- nvim client: Found in .deps/usr/share/lua/<version>/nvim/ if "bundled".
|
|
local Session = require('nvim.session')
|
|
local TcpStream = require('nvim.tcp_stream')
|
|
local SocketStream = require('nvim.socket_stream')
|
|
local ChildProcessStream = require('nvim.child_process_stream')
|
|
|
|
local check_cores = global_helpers.check_cores
|
|
local check_logs = global_helpers.check_logs
|
|
local neq = global_helpers.neq
|
|
local eq = global_helpers.eq
|
|
local ok = global_helpers.ok
|
|
local map = global_helpers.map
|
|
local filter = global_helpers.filter
|
|
local dedent = global_helpers.dedent
|
|
|
|
local start_dir = lfs.currentdir()
|
|
-- XXX: NVIM_PROG takes precedence, QuickBuild sets it.
|
|
local nvim_prog = os.getenv('NVIM_PROG') or os.getenv('NVIM_PRG') or 'build/bin/nvim'
|
|
-- Default settings for the test session.
|
|
local nvim_set = 'set shortmess+=I background=light noswapfile noautoindent'
|
|
..' laststatus=1 undodir=. directory=. viewdir=. backupdir=.'
|
|
..' belloff= noshowcmd noruler nomore'
|
|
local nvim_argv = {nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N',
|
|
'--cmd', nvim_set, '--embed'}
|
|
|
|
local mpack = require('mpack')
|
|
|
|
local tmpname = global_helpers.tmpname
|
|
local uname = global_helpers.uname
|
|
|
|
-- Formulate a path to the directory containing nvim. We use this to
|
|
-- help run test executables. It helps to keep the tests working, even
|
|
-- when the build is not in the default location.
|
|
local nvim_dir = nvim_prog:gsub("[/\\][^/\\]+$", "")
|
|
if nvim_dir == nvim_prog then
|
|
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, #nvim_argv do
|
|
new_nvim_argv[i + len] = nvim_argv[i]
|
|
end
|
|
nvim_argv = new_nvim_argv
|
|
end
|
|
|
|
local session, loop_running, last_error
|
|
|
|
local function set_session(s)
|
|
if session then
|
|
session:close()
|
|
end
|
|
session = s
|
|
end
|
|
|
|
local function 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
|
|
|
|
local function next_message()
|
|
return session:next_message()
|
|
end
|
|
|
|
local function call_and_stop_on_error(...)
|
|
local status, result = copcall(...) -- luacheck: ignore
|
|
if not status then
|
|
session:stop()
|
|
last_error = result
|
|
return ''
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function run(request_cb, notification_cb, setup_cb, timeout)
|
|
local on_request, on_notification, on_setup
|
|
|
|
if request_cb then
|
|
function on_request(method, args)
|
|
return call_and_stop_on_error(request_cb, method, args)
|
|
end
|
|
end
|
|
|
|
if notification_cb then
|
|
function on_notification(method, args)
|
|
call_and_stop_on_error(notification_cb, method, args)
|
|
end
|
|
end
|
|
|
|
if setup_cb then
|
|
function on_setup()
|
|
call_and_stop_on_error(setup_cb)
|
|
end
|
|
end
|
|
|
|
loop_running = true
|
|
session: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
|
|
end
|
|
|
|
local function stop()
|
|
session:stop()
|
|
end
|
|
|
|
-- Executes an ex-command. VimL errors manifest as client (lua) errors, but
|
|
-- v:errmsg will not be updated.
|
|
local function nvim_command(cmd)
|
|
request('nvim_command', cmd)
|
|
end
|
|
|
|
-- Evaluates a VimL expression.
|
|
-- Fails on VimL error, but does not update v:errmsg.
|
|
local function nvim_eval(expr)
|
|
return request('nvim_eval', expr)
|
|
end
|
|
|
|
local os_name = (function()
|
|
local name = nil
|
|
return (function()
|
|
if not name then
|
|
if nvim_eval('has("win32")') == 1 then
|
|
name = 'windows'
|
|
elseif nvim_eval('has("macunix")') == 1 then
|
|
name = 'osx'
|
|
else
|
|
name = 'unix'
|
|
end
|
|
end
|
|
return name
|
|
end)
|
|
end)()
|
|
|
|
local function iswin()
|
|
return os_name() == 'windows'
|
|
end
|
|
|
|
-- Executes a VimL function.
|
|
-- Fails on VimL error, but does not update v:errmsg.
|
|
local function nvim_call(name, ...)
|
|
return request('nvim_call_function', name, {...})
|
|
end
|
|
|
|
-- Sends user input to Nvim.
|
|
-- Does not fail on VimL error, but v:errmsg will be updated.
|
|
local function nvim_feed(input)
|
|
while #input > 0 do
|
|
local written = request('nvim_input', input)
|
|
input = input:sub(written + 1)
|
|
end
|
|
end
|
|
|
|
local function feed(...)
|
|
for _, v in ipairs({...}) do
|
|
nvim_feed(dedent(v))
|
|
end
|
|
end
|
|
|
|
local function rawfeed(...)
|
|
for _, v in ipairs({...}) do
|
|
nvim_feed(dedent(v))
|
|
end
|
|
end
|
|
|
|
local function 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
|
|
|
|
local function spawn(argv, merge, env)
|
|
local child_stream = ChildProcessStream.spawn(
|
|
merge and merge_args(prepend_argv, argv) or argv,
|
|
env)
|
|
return Session.new(child_stream)
|
|
end
|
|
|
|
-- Creates a new Session connected by domain socket (named pipe) or TCP.
|
|
local function connect(file_or_address)
|
|
local addr, port = string.match(file_or_address, "(.*):(%d+)")
|
|
local stream = (addr and port) and TcpStream.open(addr, port) or
|
|
SocketStream.open(file_or_address)
|
|
return Session.new(stream)
|
|
end
|
|
|
|
-- Calls fn() until it succeeds, up to `max` times or until `max_ms`
|
|
-- milliseconds have passed.
|
|
local function retry(max, max_ms, fn)
|
|
local tries = 1
|
|
local timeout = (max_ms and max_ms > 0) and max_ms or 10000
|
|
local start_time = luv.now()
|
|
while true do
|
|
local status, result = pcall(fn)
|
|
if status then
|
|
return result
|
|
end
|
|
if (max and tries >= max) or (luv.now() - start_time > timeout) then
|
|
break
|
|
end
|
|
tries = tries + 1
|
|
end
|
|
-- Do not use pcall() for the final attempt, let the failure bubble up.
|
|
return fn()
|
|
end
|
|
|
|
local function clear(...)
|
|
local args = {unpack(nvim_argv)}
|
|
local new_args
|
|
local env = nil
|
|
local opts = select(1, ...)
|
|
if type(opts) == 'table' then
|
|
if opts.env then
|
|
local env_tbl = {}
|
|
for k, v in pairs(opts.env) do
|
|
assert(type(k) == 'string')
|
|
assert(type(v) == 'string')
|
|
env_tbl[k] = v
|
|
end
|
|
for _, k in ipairs({
|
|
'HOME',
|
|
'ASAN_OPTIONS',
|
|
'LD_LIBRARY_PATH', 'PATH',
|
|
'NVIM_LOG_FILE',
|
|
'NVIM_RPLUGIN_MANIFEST',
|
|
}) do
|
|
if not env_tbl[k] then
|
|
env_tbl[k] = os.getenv(k)
|
|
end
|
|
end
|
|
env = {}
|
|
for k, v in pairs(env_tbl) do
|
|
env[#env + 1] = k .. '=' .. v
|
|
end
|
|
end
|
|
new_args = opts.args or {}
|
|
else
|
|
new_args = {...}
|
|
end
|
|
for _, arg in ipairs(new_args) do
|
|
table.insert(args, arg)
|
|
end
|
|
set_session(spawn(args, nil, env))
|
|
end
|
|
|
|
local function insert(...)
|
|
nvim_feed('i')
|
|
for _, v in ipairs({...}) do
|
|
local escaped = v:gsub('<', '<lt>')
|
|
rawfeed(escaped)
|
|
end
|
|
nvim_feed('<ESC>')
|
|
end
|
|
|
|
-- Executes an ex-command by user input. Because nvim_input() is used, VimL
|
|
-- errors will not manifest as client (lua) errors. Use command() for that.
|
|
local function 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
|
|
|
|
-- Dedent the given text and write it to the file name.
|
|
local function write_file(name, text, dont_dedent)
|
|
local file = io.open(name, 'w')
|
|
if not dont_dedent then
|
|
text = dedent(text)
|
|
end
|
|
file:write(text)
|
|
file:flush()
|
|
file:close()
|
|
end
|
|
|
|
local function read_file(name)
|
|
local file = io.open(name, 'r')
|
|
if not file then
|
|
return nil
|
|
end
|
|
local ret = file:read('*a')
|
|
file:close()
|
|
return ret
|
|
end
|
|
|
|
local function source(code)
|
|
local fname = tmpname()
|
|
write_file(fname, code)
|
|
nvim_command('source '..fname)
|
|
os.remove(fname)
|
|
return fname
|
|
end
|
|
|
|
local function set_shell_powershell()
|
|
source([[
|
|
set shell=powershell shellquote=\" shellpipe=\| shellredir=>
|
|
set shellcmdflag=\ -ExecutionPolicy\ RemoteSigned\ -Command
|
|
let &shellxquote=' '
|
|
]])
|
|
end
|
|
|
|
local function nvim(method, ...)
|
|
return request('nvim_'..method, ...)
|
|
end
|
|
|
|
local function ui(method, ...)
|
|
return request('nvim_ui_'..method, ...)
|
|
end
|
|
|
|
local function nvim_async(method, ...)
|
|
session:notify('nvim_'..method, ...)
|
|
end
|
|
|
|
local function buffer(method, ...)
|
|
return request('nvim_buf_'..method, ...)
|
|
end
|
|
|
|
local function window(method, ...)
|
|
return request('nvim_win_'..method, ...)
|
|
end
|
|
|
|
local function tabpage(method, ...)
|
|
return request('nvim_tabpage_'..method, ...)
|
|
end
|
|
|
|
local function curbuf(method, ...)
|
|
if not method then
|
|
return nvim('get_current_buf')
|
|
end
|
|
return buffer(method, 0, ...)
|
|
end
|
|
|
|
local function wait()
|
|
-- Execute 'vim_eval' (a deferred function) to block
|
|
-- until all pending input is processed.
|
|
session:request('vim_eval', '1')
|
|
end
|
|
|
|
-- sleeps the test runner (_not_ the nvim instance)
|
|
local function sleep(ms)
|
|
run(nil, nil, nil, ms)
|
|
end
|
|
|
|
local function curbuf_contents()
|
|
wait() -- Before inspecting the buffer, process all input.
|
|
return table.concat(curbuf('get_lines', 0, -1, true), '\n')
|
|
end
|
|
|
|
local function curwin(method, ...)
|
|
if not method then
|
|
return nvim('get_current_win')
|
|
end
|
|
return window(method, 0, ...)
|
|
end
|
|
|
|
local function curtab(method, ...)
|
|
if not method then
|
|
return nvim('get_current_tabpage')
|
|
end
|
|
return tabpage(method, 0, ...)
|
|
end
|
|
|
|
local function expect(contents)
|
|
return eq(dedent(contents), curbuf_contents())
|
|
end
|
|
|
|
local function expect_any(contents)
|
|
contents = dedent(contents)
|
|
return ok(nil ~= string.find(curbuf_contents(), contents, 1, true))
|
|
end
|
|
|
|
local function do_rmdir(path)
|
|
if lfs.attributes(path, 'mode') ~= 'directory' then
|
|
return nil
|
|
end
|
|
for file in lfs.dir(path) do
|
|
if file ~= '.' and file ~= '..' then
|
|
local abspath = path..'/'..file
|
|
if lfs.attributes(abspath, 'mode') == 'directory' then
|
|
local ret = do_rmdir(abspath) -- recurse
|
|
if not ret then
|
|
return nil
|
|
end
|
|
else
|
|
local ret, err = os.remove(abspath)
|
|
if not ret then
|
|
error('os.remove: '..err)
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local ret, err = lfs.rmdir(path)
|
|
if not ret then
|
|
error('lfs.rmdir('..path..'): '..err)
|
|
end
|
|
return ret
|
|
end
|
|
|
|
local function rmdir(path)
|
|
local ret, _ = pcall(do_rmdir, path)
|
|
if not ret and os_name() == "windows" then
|
|
-- Maybe "Permission denied"; try again after changing the nvim
|
|
-- process to the top-level directory.
|
|
nvim_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
|
|
|
|
local exc_exec = function(cmd)
|
|
nvim_command(([[
|
|
try
|
|
execute "%s"
|
|
catch
|
|
let g:__exception = v:exception
|
|
endtry
|
|
]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
|
|
local ret = nvim_eval('get(g:, "__exception", 0)')
|
|
nvim_command('unlet! g:__exception')
|
|
return ret
|
|
end
|
|
|
|
local function redir_exec(cmd)
|
|
nvim_command(([[
|
|
redir => g:__output
|
|
silent! execute "%s"
|
|
redir END
|
|
]]):format(cmd:gsub('\n', '\\n'):gsub('[\\"]', '\\%0')))
|
|
local ret = nvim_eval('get(g:, "__output", 0)')
|
|
nvim_command('unlet! g:__output')
|
|
return ret
|
|
end
|
|
|
|
local function 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
|
|
|
|
-- Helper to skip tests. Returns true in Windows systems.
|
|
-- pending_fn is pending() from busted
|
|
local function pending_win32(pending_fn)
|
|
if uname() == 'Windows' then
|
|
if pending_fn ~= nil then
|
|
pending_fn('FIXME: Windows', function() end)
|
|
end
|
|
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`.
|
|
local function 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
|
|
|
|
local function meth_pcall(...)
|
|
local ret = {pcall(...)}
|
|
if type(ret[2]) == 'string' then
|
|
ret[2] = ret[2]:gsub('^[^:]+:%d+: ', '')
|
|
end
|
|
return ret
|
|
end
|
|
|
|
local funcs = create_callindex(nvim_call)
|
|
local meths = create_callindex(nvim)
|
|
local uimeths = create_callindex(ui)
|
|
local bufmeths = create_callindex(buffer)
|
|
local winmeths = create_callindex(window)
|
|
local tabmeths = create_callindex(tabpage)
|
|
local curbufmeths = create_callindex(curbuf)
|
|
local curwinmeths = create_callindex(curwin)
|
|
local curtabmeths = create_callindex(curtab)
|
|
|
|
local function get_pathsep()
|
|
return funcs.fnamemodify('.', ':p'):sub(-1)
|
|
end
|
|
|
|
local M = {
|
|
prepend_argv = prepend_argv,
|
|
clear = clear,
|
|
connect = connect,
|
|
retry = retry,
|
|
spawn = spawn,
|
|
dedent = dedent,
|
|
source = source,
|
|
rawfeed = rawfeed,
|
|
insert = insert,
|
|
iswin = iswin,
|
|
feed = feed,
|
|
feed_command = feed_command,
|
|
eval = nvim_eval,
|
|
call = nvim_call,
|
|
command = nvim_command,
|
|
request = request,
|
|
next_message = next_message,
|
|
run = run,
|
|
stop = stop,
|
|
eq = eq,
|
|
neq = neq,
|
|
expect = expect,
|
|
expect_any = expect_any,
|
|
ok = ok,
|
|
map = map,
|
|
filter = filter,
|
|
nvim = nvim,
|
|
nvim_async = nvim_async,
|
|
nvim_prog = nvim_prog,
|
|
nvim_set = nvim_set,
|
|
nvim_dir = nvim_dir,
|
|
buffer = buffer,
|
|
window = window,
|
|
tabpage = tabpage,
|
|
curbuf = curbuf,
|
|
curwin = curwin,
|
|
curtab = curtab,
|
|
curbuf_contents = curbuf_contents,
|
|
wait = wait,
|
|
sleep = sleep,
|
|
set_session = set_session,
|
|
write_file = write_file,
|
|
read_file = read_file,
|
|
os_name = os_name,
|
|
rmdir = rmdir,
|
|
mkdir = lfs.mkdir,
|
|
exc_exec = exc_exec,
|
|
redir_exec = redir_exec,
|
|
merge_args = merge_args,
|
|
funcs = funcs,
|
|
meths = meths,
|
|
bufmeths = bufmeths,
|
|
winmeths = winmeths,
|
|
tabmeths = tabmeths,
|
|
uimeths = uimeths,
|
|
curbufmeths = curbufmeths,
|
|
curwinmeths = curwinmeths,
|
|
curtabmeths = curtabmeths,
|
|
pending_win32 = pending_win32,
|
|
skip_fragile = skip_fragile,
|
|
set_shell_powershell = set_shell_powershell,
|
|
tmpname = tmpname,
|
|
meth_pcall = meth_pcall,
|
|
NIL = mpack.NIL,
|
|
get_pathsep = get_pathsep,
|
|
}
|
|
|
|
return function(after_each)
|
|
if after_each then
|
|
after_each(function()
|
|
check_logs()
|
|
check_cores('build/bin/nvim')
|
|
end)
|
|
end
|
|
return M
|
|
end
|