mirror of
https://github.com/neovim/neovim.git
synced 2026-02-09 05:18:45 +00:00
Bumping to windows-2025 seems to fix at least one case of spaces having wrong attributes in TUI tests, which allow unskipping dozens of tests.
509 lines
15 KiB
Lua
509 lines
15 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
|
|
local clear, eq, eval, next_msg, ok, source = n.clear, t.eq, n.eval, n.next_msg, t.ok, n.source
|
|
local command, fn, api = n.command, n.fn, n.api
|
|
local feed = n.feed
|
|
local exec_lua = n.exec_lua
|
|
local matches = t.matches
|
|
local sleep = vim.uv.sleep
|
|
local get_session, set_session = n.get_session, n.set_session
|
|
local nvim_prog = n.nvim_prog
|
|
local is_os = t.is_os
|
|
local retry = t.retry
|
|
local expect_twostreams = n.expect_twostreams
|
|
local assert_alive = n.assert_alive
|
|
local pcall_err = t.pcall_err
|
|
local skip = t.skip
|
|
|
|
describe('channels', function()
|
|
local init = [[
|
|
function! Normalize(data) abort
|
|
" Windows: remove ^M
|
|
return type([]) == type(a:data)
|
|
\ ? mapnew(a:data, 'substitute(v:val, "\r", "", "g")')
|
|
\ : a:data
|
|
endfunction
|
|
function! OnEvent(id, data, event) dict
|
|
call rpcnotify(1, a:event, a:id, a:data)
|
|
endfunction
|
|
]]
|
|
before_each(function()
|
|
clear()
|
|
source(init)
|
|
end)
|
|
|
|
pending('can connect to socket', function()
|
|
local server = n.new_session(true)
|
|
set_session(server)
|
|
local address = fn.serverlist()[1]
|
|
local client = n.new_session(true)
|
|
set_session(client)
|
|
source(init)
|
|
|
|
api.nvim_set_var('address', address)
|
|
command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
|
|
local id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
command("call chansend(g:id, msgpackdump([[2,'nvim_set_var',['code',23]]]))")
|
|
set_session(server)
|
|
retry(nil, 1000, function()
|
|
eq(23, api.nvim_get_var('code'))
|
|
end)
|
|
set_session(client)
|
|
|
|
command("call chansend(g:id, msgpackdump([[0,0,'nvim_eval',['2+3']]]))")
|
|
|
|
local res = eval('msgpackdump([[1,0,v:null,5]])')
|
|
eq({ '\148\001\n\192\005' }, res)
|
|
eq({ 'notification', 'data', { id, res } }, next_msg())
|
|
command("call chansend(g:id, msgpackdump([[2,'nvim_command',['quit']]]))")
|
|
eq({ 'notification', 'data', { id, { '' } } }, next_msg())
|
|
end)
|
|
|
|
it('dont crash due to garbage in rpc #23781', function()
|
|
local client = get_session()
|
|
local server = n.new_session(true)
|
|
set_session(server)
|
|
local address = fn.serverlist()[1]
|
|
set_session(client)
|
|
|
|
api.nvim_set_var('address', address)
|
|
command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
|
|
local id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
command("call chansend(g:id, 'F')")
|
|
eq({ 'notification', 'data', { id, { '' } } }, next_msg())
|
|
set_session(server)
|
|
assert_alive()
|
|
|
|
set_session(client)
|
|
command('call chanclose(g:id)')
|
|
command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})")
|
|
id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
command("call chansend(g:id, msgpackdump([[2, 'redraw', 'F']], 'B')[:-4])")
|
|
set_session(server)
|
|
assert_alive()
|
|
set_session(client)
|
|
command("call chansend(g:id, 'F')")
|
|
eq({ 'notification', 'data', { id, { '' } } }, next_msg())
|
|
|
|
set_session(server)
|
|
assert_alive()
|
|
set_session(client)
|
|
command('call chanclose(g:id)')
|
|
server:close()
|
|
end)
|
|
|
|
it('can use stdio channel', function()
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_stdout': function('OnEvent'),
|
|
\ 'on_stderr': function('OnEvent'),
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ }
|
|
]])
|
|
api.nvim_set_var('nvim_prog', nvim_prog)
|
|
api.nvim_set_var(
|
|
'code',
|
|
[[
|
|
function! OnEvent(id, data, event) dict
|
|
let text = string([a:id, a:data, a:event])
|
|
call chansend(g:x, text)
|
|
|
|
if a:data == ['']
|
|
call chansend(v:stderr, "*dies*")
|
|
quit
|
|
endif
|
|
endfunction
|
|
let g:x = stdioopen({'on_stdin':'OnEvent'})
|
|
call chansend(x, "hello")
|
|
]]
|
|
)
|
|
command(
|
|
"let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
|
|
)
|
|
local id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
eq({ 'notification', 'stdout', { id, { 'hello' } } }, next_msg())
|
|
|
|
command("call chansend(id, 'howdy')")
|
|
eq({ 'notification', 'stdout', { id, { "[1, ['howdy'], 'stdin']" } } }, next_msg())
|
|
|
|
command('call chansend(id, 0z686f6c61)')
|
|
eq({ 'notification', 'stdout', { id, { "[1, ['hola'], 'stdin']" } } }, next_msg())
|
|
|
|
command("call chanclose(id, 'stdin')")
|
|
expect_twostreams({
|
|
{ 'notification', 'stdout', { id, { "[1, [''], 'stdin']" } } },
|
|
{ 'notification', 'stdout', { id, { '' } } },
|
|
}, {
|
|
{ 'notification', 'stderr', { id, { '*dies*' } } },
|
|
{ 'notification', 'stderr', { id, { '' } } },
|
|
})
|
|
eq({ 'notification', 'exit', { 3, 0 } }, next_msg())
|
|
end)
|
|
|
|
it('can use stdio channel and on_print callback', function()
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_stdout': function('OnEvent'),
|
|
\ 'on_stderr': function('OnEvent'),
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ }
|
|
]])
|
|
api.nvim_set_var('nvim_prog', nvim_prog)
|
|
api.nvim_set_var(
|
|
'code',
|
|
[[
|
|
function! OnStdin(id, data, event) dict
|
|
echo string([a:id, a:data, a:event])
|
|
if a:data == ['']
|
|
quit
|
|
endif
|
|
endfunction
|
|
function! OnPrint(text) dict
|
|
call chansend(g:x, ['OnPrint:' .. a:text])
|
|
endfunction
|
|
let g:x = stdioopen({'on_stdin': funcref('OnStdin'), 'on_print':'OnPrint'})
|
|
call chansend(x, "hello")
|
|
]]
|
|
)
|
|
command(
|
|
"let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
|
|
)
|
|
local id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
eq({ 'notification', 'stdout', { id, { 'hello' } } }, next_msg())
|
|
|
|
command("call chansend(id, 'howdy')")
|
|
eq({ 'notification', 'stdout', { id, { "OnPrint:[1, ['howdy'], 'stdin']" } } }, next_msg())
|
|
end)
|
|
|
|
-- Helper to accumulate PTY stdout data until expected string is received.
|
|
-- PTY reads are non-atomic and may deliver data in chunks.
|
|
local function expect_stdout(id, expected)
|
|
local accumulated = ''
|
|
while #accumulated < #expected do
|
|
local msg = next_msg(1000)
|
|
if not msg then
|
|
break
|
|
end
|
|
eq('notification', msg[1])
|
|
eq('stdout', msg[2])
|
|
eq(id, msg[3][1])
|
|
accumulated = accumulated .. table.concat(msg[3][2], '\n')
|
|
end
|
|
eq(expected, accumulated)
|
|
end
|
|
|
|
it('can use stdio channel with pty', function()
|
|
skip(is_os('win'))
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_stdout': function('OnEvent'),
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ 'pty': v:true,
|
|
\ }
|
|
]])
|
|
api.nvim_set_var('nvim_prog', nvim_prog)
|
|
api.nvim_set_var(
|
|
'code',
|
|
[[
|
|
function! OnEvent(id, data, event) dict
|
|
let text = string([a:id, a:data, a:event])
|
|
call chansend(g:x, text)
|
|
endfunction
|
|
let g:x = stdioopen({'on_stdin':'OnEvent'})
|
|
]]
|
|
)
|
|
command(
|
|
"let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
|
|
)
|
|
local id = eval('g:id')
|
|
ok(id > 0)
|
|
|
|
command("call chansend(id, 'TEXT\n')")
|
|
expect_stdout(id, "TEXT\r\n[1, ['TEXT', ''], 'stdin']")
|
|
|
|
command('call chansend(id, 0z426c6f6273210a)')
|
|
expect_stdout(id, "Blobs!\r\n[1, ['Blobs!', ''], 'stdin']")
|
|
|
|
command("call chansend(id, 'neovan')")
|
|
expect_stdout(id, 'neovan')
|
|
command("call chansend(id, '\127\127im\n')")
|
|
expect_stdout(id, "\b \b\b \bim\r\n[1, ['neovim', ''], 'stdin']")
|
|
|
|
command("call chansend(id, 'incomplet\004')")
|
|
|
|
local bsdlike = is_os('bsd') or is_os('mac')
|
|
local extra = bsdlike and '^D\008\008' or ''
|
|
expect_stdout(id, 'incomplet' .. extra .. "[1, ['incomplet'], 'stdin']")
|
|
|
|
command("call chansend(id, '\004')")
|
|
expect_stdout(id, extra .. "[1, [''], 'stdin']")
|
|
|
|
-- channel is still open
|
|
command("call chansend(id, 'hi again!\n')")
|
|
expect_stdout(id, 'hi again!\r\n')
|
|
end)
|
|
|
|
it('stdio channel can use rpc and stderr simultaneously', function()
|
|
skip(is_os('win'))
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_stderr': function('OnEvent'),
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ 'rpc': v:true,
|
|
\ }
|
|
]])
|
|
api.nvim_set_var('nvim_prog', nvim_prog)
|
|
api.nvim_set_var(
|
|
'code',
|
|
[[
|
|
let id = stdioopen({'rpc':v:true})
|
|
call rpcnotify(id,"nvim_call_function", "rpcnotify", [1, "message", "hi there!", id])
|
|
call chansend(v:stderr, "trouble!")
|
|
]]
|
|
)
|
|
command(
|
|
"let id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)"
|
|
)
|
|
eq({ 'notification', 'message', { 'hi there!', 1 } }, next_msg())
|
|
eq({ 'notification', 'stderr', { 3, { 'trouble!' } } }, next_msg())
|
|
|
|
eq(30, eval("rpcrequest(id, 'nvim_eval', '[chansend(v:stderr, \"math??\"), 5*6][1]')"))
|
|
eq({ 'notification', 'stderr', { 3, { 'math??' } } }, next_msg())
|
|
|
|
local _, err =
|
|
pcall(command, "call rpcrequest(id, 'nvim_command', 'call chanclose(v:stderr, \"stdin\")')")
|
|
matches('E906: invalid stream for channel', err)
|
|
|
|
eq(1, eval("rpcrequest(id, 'nvim_eval', 'chanclose(v:stderr, \"stderr\")')"))
|
|
eq({ 'notification', 'stderr', { 3, { '' } } }, next_msg())
|
|
|
|
command("call rpcnotify(id, 'nvim_command', 'quit')")
|
|
eq({ 'notification', 'exit', { 3, 0 } }, next_msg())
|
|
end)
|
|
|
|
it('stdio channel works with stdout redirected to file #30509', function()
|
|
t.write_file(
|
|
'Xstdio_write.vim',
|
|
[[
|
|
let chan = stdioopen({})
|
|
call chansend(chan, 'foo')
|
|
call chansend(chan, 'bar')
|
|
qall!
|
|
]]
|
|
)
|
|
local fd = assert(vim.uv.fs_open('Xstdio_redir', 'w', 420))
|
|
local exit_code, exit_signal
|
|
local handle = vim.uv.spawn(nvim_prog, {
|
|
args = { '-u', 'NONE', '-i', 'NONE', '--headless', '-S', 'Xstdio_write.vim' },
|
|
-- Simulate shell redirection: "nvim ... > Xstdio_redir". #30509
|
|
stdio = { nil, fd, nil },
|
|
}, function(code, signal)
|
|
vim.uv.stop()
|
|
exit_code, exit_signal = code, signal
|
|
end)
|
|
finally(function()
|
|
handle:close()
|
|
vim.uv.fs_close(fd)
|
|
os.remove('Xstdio_write.vim')
|
|
os.remove('Xstdio_redir')
|
|
end)
|
|
vim.uv.run('default')
|
|
eq({ 0, 0 }, { exit_code, exit_signal })
|
|
eq('foobar', t.read_file('Xstdio_redir'))
|
|
end)
|
|
|
|
it('can use buffered output mode', function()
|
|
skip(fn.executable('grep') == 0, 'missing "grep" command')
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_stdout': function('OnEvent'),
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ 'stdout_buffered': v:true,
|
|
\ }
|
|
]])
|
|
command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
|
|
local id = eval('g:id')
|
|
|
|
command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]])
|
|
sleep(10)
|
|
command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]])
|
|
command("call chanclose(id, 'stdin')")
|
|
|
|
eq({
|
|
'notification',
|
|
'stdout',
|
|
{ id, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
|
|
}, next_msg())
|
|
eq({ 'notification', 'exit', { id, 0 } }, next_msg())
|
|
|
|
command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
|
|
id = eval('g:id')
|
|
|
|
command([[call chansend(id, "is no number\nnot at all")]])
|
|
command("call chanclose(id, 'stdin')")
|
|
|
|
-- works correctly with no output
|
|
eq({ 'notification', 'stdout', { id, { '' } } }, next_msg())
|
|
eq({ 'notification', 'exit', { id, 1 } }, next_msg())
|
|
end)
|
|
|
|
it('can use buffered output mode with no stream callback', function()
|
|
skip(fn.executable('grep') == 0, 'missing "grep" command')
|
|
source([[
|
|
function! OnEvent(id, data, event) dict
|
|
call rpcnotify(1, a:event, a:id, a:data, self.stdout)
|
|
endfunction
|
|
let g:job_opts = {
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ 'stdout_buffered': v:true,
|
|
\ }
|
|
]])
|
|
command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
|
|
local id = eval('g:id')
|
|
|
|
command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]])
|
|
sleep(10)
|
|
command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]])
|
|
command("call chanclose(id, 'stdin')")
|
|
|
|
eq({
|
|
'notification',
|
|
'exit',
|
|
{ id, 0, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
|
|
}, next_msg())
|
|
|
|
-- if dict is reused the new value is not stored,
|
|
-- but nvim also does not crash
|
|
command("let id = jobstart(['cat'], g:job_opts)")
|
|
id = eval('g:id')
|
|
|
|
command([[call chansend(id, "cat text\n")]])
|
|
sleep(10)
|
|
command("call chanclose(id, 'stdin')")
|
|
|
|
-- old value was not overwritten
|
|
eq({
|
|
'notification',
|
|
'exit',
|
|
{ id, 0, { '10 PRINT "NVIM"', '20 GOTO 10', '' } },
|
|
}, next_msg())
|
|
|
|
-- and an error was thrown.
|
|
eq(
|
|
"E5210: dict key 'stdout' already set for buffered stream in channel " .. id,
|
|
eval('v:errmsg')
|
|
)
|
|
|
|
-- reset dictionary
|
|
source([[
|
|
let g:job_opts = {
|
|
\ 'on_exit': function('OnEvent'),
|
|
\ 'stdout_buffered': v:true,
|
|
\ }
|
|
]])
|
|
command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)")
|
|
id = eval('g:id')
|
|
|
|
command([[call chansend(id, "is no number\nnot at all")]])
|
|
command("call chanclose(id, 'stdin')")
|
|
|
|
-- works correctly with no output
|
|
eq({ 'notification', 'exit', { id, 1, { '' } } }, next_msg())
|
|
end)
|
|
|
|
it('ChanOpen works with vim.wait() from another autocommand #32706', function()
|
|
exec_lua([[
|
|
vim.api.nvim_create_autocmd('ChanOpen', {
|
|
callback = function(ev)
|
|
_G.chan = vim.v.event.info.id
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('InsertEnter', {
|
|
buffer = 0,
|
|
callback = function()
|
|
local chan = vim.fn.jobstart({ 'cat' })
|
|
_G.result = vim.wait(3000, function()
|
|
return _G.chan == chan
|
|
end)
|
|
end,
|
|
})
|
|
]])
|
|
feed('i')
|
|
retry(nil, 4000, function()
|
|
eq(true, exec_lua('return _G.result'))
|
|
end)
|
|
end)
|
|
|
|
it('ChanInfo works with vim.wait() from another autocommand #32706', function()
|
|
exec_lua([[
|
|
vim.api.nvim_create_autocmd('ChanInfo', {
|
|
callback = function(ev)
|
|
_G.foo = vim.v.event.info.client.attributes.foo
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('InsertEnter', {
|
|
buffer = 0,
|
|
callback = function()
|
|
local chan = vim.fn.jobstart({ 'cat' })
|
|
_G.result = vim.wait(3000, function()
|
|
return _G.foo == 'bar'
|
|
end)
|
|
end,
|
|
})
|
|
]])
|
|
feed('i')
|
|
api.nvim_set_client_info('test', {}, 'remote', {}, { foo = 'bar' })
|
|
retry(nil, 4000, function()
|
|
eq(true, exec_lua('return _G.result'))
|
|
end)
|
|
end)
|
|
|
|
describe('sockconnect() reports error when connection fails', function()
|
|
it('in "pipe" mode', function()
|
|
eq(
|
|
'Vim:connection failed: connection refused',
|
|
pcall_err(fn.sockconnect, 'pipe', n.new_pipename())
|
|
)
|
|
end)
|
|
|
|
it('in "tcp" mode', function()
|
|
eq(
|
|
'Vim:connection failed: connection refused',
|
|
pcall_err(fn.sockconnect, 'pipe', '127.0.0.1:0')
|
|
)
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
describe('loopback', function()
|
|
before_each(function()
|
|
clear()
|
|
command("let chan = sockconnect('pipe', v:servername, {'rpc': v:true})")
|
|
end)
|
|
|
|
it('does not crash when sending raw data', function()
|
|
eq(
|
|
"Vim(call):Can't send raw data to rpc channel",
|
|
pcall_err(command, "call chansend(chan, 'test')")
|
|
)
|
|
assert_alive()
|
|
end)
|
|
|
|
it('are released when closed', function()
|
|
local chans = eval('len(nvim_list_chans())')
|
|
command('call chanclose(chan)')
|
|
eq(chans - 1, eval('len(nvim_list_chans())'))
|
|
end)
|
|
end)
|