neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mtest/functional/fixtures/fake-lsp-server.lua | 57++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtest/functional/plugin/lsp/testutil.lua | 1+
Mtest/functional/plugin/lsp_spec.lua | 26++++++++++++--------------
Mtest/functional/testnvim.lua | 4+++-
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.