commit dec3c6fa346b298e7feb72398f9fdd5d59c11444
parent 08f4811061c9fde22b730e5d73f7fb49f61f7184
Author: zeertzjq <zeertzjq@outlook.com>
Date: Fri, 20 Feb 2026 02:38:14 +0800
test(lsp): fix fake LSP server timeout not working (#37970)
Problem: Fake LSP server does not timeout or respond to SIGTERM as it
does not run the event loop.
Solution: Instead of io.read(), use stdioopen()'s on_stdin callback to
accumulate input and use vim.wait() to wait for input.
Also, in the test suite, don't stop a session when it's not running, as
calling uv.stop() outside uv.run() will instead cause the next uv.run()
to stop immediately, which cancels the next RPC request.
Diffstat:
4 files changed, 62 insertions(+), 26 deletions(-)
diff --git a/test/functional/fixtures/fake-lsp-server.lua b/test/functional/fixtures/fake-lsp-server.lua
@@ -1,10 +1,22 @@
local protocol = require 'vim.lsp.protocol'
+local pid = vim.uv.os_getpid()
+local stdin = ''
+vim.fn.stdioopen({
+ on_stdin = function(_, data, _)
+ stdin = stdin .. table.concat(data, '\n')
+ end,
+})
+
-- Logs to $NVIM_LOG_FILE.
--
-- TODO(justinmk): remove after https://github.com/neovim/neovim/pull/7062
local function log(loglevel, area, msg)
- vim.fn.writefile({ string.format('%s %s: %s', loglevel, area, msg) }, vim.env.NVIM_LOG_FILE, 'a')
+ vim.fn.writefile(
+ { string.format('%d %s %s: %s', pid, loglevel, area, msg) },
+ vim.env.NVIM_LOG_FILE,
+ 'a'
+ )
end
local function message_parts(sep, ...)
@@ -46,10 +58,30 @@ local function format_message_with_content_length(encoded_message)
}
end
+local function read_line()
+ vim.wait(math.huge, function()
+ return stdin:find('\n') ~= nil
+ end, 1)
+ local eol = assert(stdin:find('\n'))
+ local line = stdin:sub(1, eol - 1)
+ stdin = stdin:sub(eol + 1)
+ return line
+end
+
+--- @param len integer
+local function read_len(len)
+ vim.wait(math.huge, function()
+ return stdin:len() >= len
+ end, 1)
+ local content = stdin:sub(1, len)
+ stdin = stdin:sub(len + 1)
+ return content
+end
+
local function read_message()
- local line = io.read('*l')
+ local line = read_line()
local length = line:lower():match('content%-length:%s*(%d+)')
- return vim.json.decode(io.read(2 + length):sub(2))
+ return vim.json.decode(read_len(2 + length):sub(2))
end
local function send(payload)
@@ -1045,21 +1077,24 @@ end
-- Tests will be indexed by test_name
local test_name = arg[1]
-local timeout = arg[2]
+local timeout = tonumber(arg[2])
assert(type(test_name) == 'string', 'test_name must be specified as first arg.')
-local kill_timer = assert(vim.uv.new_timer())
-kill_timer:start(timeout or 1e3, 0, function()
- kill_timer:stop()
- kill_timer:close()
+local kill_timer = vim.defer_fn(function()
log('ERROR', 'LSP', 'TIMEOUT')
io.stderr:write('TIMEOUT')
os.exit(100)
-end)
+end, timeout or 1e3)
+
+-- Close the timer on exit (deadly signal or :cquit) to avoid delay with ASAN/TSAN.
+vim.api.nvim_create_autocmd('VimLeave', {
+ callback = function()
+ kill_timer:stop()
+ kill_timer:close()
+ end,
+})
local status, err = pcall(assert(tests[test_name], 'Test not found'))
-kill_timer:stop()
-kill_timer:close()
if not status then
log('ERROR', 'LSP', tostring(err))
io.stderr:write(err)
diff --git a/test/functional/plugin/lsp/testutil.lua b/test/functional/plugin/lsp/testutil.lua
@@ -110,6 +110,7 @@ M.create_server_definition = function()
function srv.terminate()
closing = true
+ dispatchers.on_exit(0, 15)
end
return srv
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
@@ -88,7 +88,11 @@ describe('LSP', function()
after_each(function()
stop()
- exec_lua('vim.iter(lsp.get_clients()):each(function(client) client:stop(true) end)')
+ exec_lua(function()
+ vim.iter(vim.lsp.get_clients({ _uninitialized = true })):each(function(client)
+ client:stop(true)
+ end)
+ end)
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
end)
@@ -206,18 +210,18 @@ describe('LSP', function()
it('does not reuse an already-stopping client #33616', function()
-- we immediately try to start a second client with the same name/root
-- before the first one has finished shutting down; we must get a new id.
- local clients = exec_lua([[
- local client1 = vim.lsp.start({
+ local clients = exec_lua(function()
+ local client1 = assert(vim.lsp.start({
name = 'dup-test',
cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' },
- }, { attach = false })
+ }, { attach = false }))
vim.lsp.get_client_by_id(client1):stop()
- local client2 = vim.lsp.start({
+ local client2 = assert(vim.lsp.start({
name = 'dup-test',
cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' },
- }, { attach = false })
+ }, { attach = false }))
return { client1, client2 }
- ]])
+ end)
local c1, c2 = clients[1], clients[2]
eq(false, c1 == c2, 'Expected a fresh client while the old one is stopping')
end)
@@ -320,14 +324,8 @@ describe('LSP', function()
)
it('should succeed with manual shutdown', function()
- if is_ci() then
- pending('hangs the build on CI #14028, re-enable with freeze timeout #14204')
- return
- elseif t.skip_fragile(pending) then
- return
- end
local expected_handlers = {
- { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1, version = 0 } },
+ { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1, request_id = 2, version = 0 } },
{ NIL, {}, { method = 'test', client_id = 1 } },
}
test_rpc_server {
diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua
@@ -320,7 +320,9 @@ function M.run(request_cb, notification_cb, setup_cb, timeout)
end
function M.stop()
- assert(session):stop()
+ if loop_running then
+ assert(session):stop()
+ end
end
-- Use for commands which expect nvim to quit.