test: improve Nvim EOF handling (for Windows CI) #40841

Problem:
Mysterious Windows CI failure points to resource exhaustion, but we have
no visibility/instrumentation:

    RUN      T610 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <S-Left>: 93.70 ms OK
    RUN      T611 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <F12><F2><Tab>: 49.48 ms FAIL
    …\testnvim.lua:144: EOF was received from Nvim. Likely the Nvim process crashed.
    stack traceback:
            D:/a/neovim/neovim/test/functional/testnvim.lua:144: in function 'nvim_set_keymap'
            …/api/keymap_spec.lua:769: in function <…/api/keymap_spec.lua:768>
    RUN      T612 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <Space><Tab>: 18.49 ms FAIL
    …\testnvim.lua:144: EOF was received from Nvim. Likely the Nvim process crashed.
    stack traceback:
            D:/a/neovim/neovim/test/functional/testnvim.lua:144: in function 'nvim_set_keymap'
            …/api/keymap_spec.lua:769: in function <.../build/Xtest_xdg_api/test/functional/api/keymap_spec.lua:768>

Solution:
Append the child exit-code and signal to the "EOF … crashed" message.

Example:

    Nvim EOF (crash?) exit code: 42 (0x0000002A)

On the next failing Windows run, the first crash line will classify the failure:
- 0xC0000005 → access violation (nvim bug)
- 0xC0000374 → heap corruption (nvim bug)
- spawn/resource error or "clean" exit-code → system resource exhaustion
This commit is contained in:
Justin M. Keyes
2026-07-19 12:45:40 -04:00
committed by GitHub
parent d1b3a9924d
commit 40ab941f79
2 changed files with 40 additions and 13 deletions

View File

@@ -14,7 +14,9 @@ local RpcStream = require('test.client.rpc_stream')
--- @field private _timer uv.uv_timer_t
--- @field private _is_running boolean true during `Session:run()` scope.
--- @field data table Arbitrary user data.
local Session = {}
local Session = {
eof_err_msg = 'Nvim EOF (crash?)',
}
Session.__index = Session
if package.loaded['jit'] then
-- luajit pcall is already coroutine safe
@@ -227,15 +229,10 @@ function Session:_run(request_cb, notification_cb, timeout)
self._prepare:stop()
end)
end
local got_eof = false
self._rpc_stream:read_start(request_cb, notification_cb, function()
uv.stop()
--- @diagnostic disable-next-line: invisible
local stderr = self._rpc_stream._stream.stderr --[[@as string?]]
-- See if `ProcStream.stderr` has anything useful.
stderr = '' ~= ((stderr or ''):match('^%s*(.*%S)') or '') and ' stderr:\n' .. stderr or ''
self.eof_err = { 1, 'EOF was received from Nvim. Likely the Nvim process crashed.' .. stderr }
got_eof = true
end)
local ret, err, _ = uv.run()
if ret == nil then
@@ -244,6 +241,38 @@ function Session:_run(request_cb, notification_cb, timeout)
self._prepare:stop()
self._timer:stop()
self._rpc_stream:read_stop()
if got_eof then
self.eof_err = { 1, self:_eof_msg() }
end
end
--- Message shown on unexpected stdout EOF.
--- @private
--- @return string
function Session:_eof_msg()
local msg = self.eof_err_msg
--- @diagnostic disable-next-line: access-invisible
local proc = self._rpc_stream._stream --[[@as test.ProcStream]]
-- ProcStream (spawned child) has stderr + exit-code; SocketStream (a socket peer) does not.
if proc.stderr ~= nil then
-- stdout EOF and process-exit are separate libuv events. Wait for up to 1s.
vim.wait(1000, function()
return proc.status ~= nil or proc.signal ~= nil
end)
if proc.status ~= nil then
msg = ('%s exit code: %d (0x%08X)'):format(msg, proc.status, proc.status % 2 ^ 32)
end
if proc.signal ~= nil and proc.signal ~= 0 then
msg = ('%s signal: %d'):format(msg, proc.signal)
end
end
local stderr = proc.stderr --[[@as string?]]
if '' ~= ((stderr or ''):match('^%s*(.*%S)') or '') then
msg = msg .. ' stderr:\n' .. stderr
end
return msg
end
--- Nvim msgpack-RPC session.

View File

@@ -9,7 +9,6 @@ local SocketStream = uv_stream.SocketStream
local ProcStream = uv_stream.ProcStream
local check_cores = t.check_cores
local pcall_err = t.pcall_err
local check_logs = t.check_logs
local dedent = t.dedent
local eq = t.eq
@@ -133,7 +132,7 @@ end
--- @return any
function M.request(method, ...)
assert(session, 'no Nvim session')
assert(not session.eof_err, 'sending request after EOF from Nvim')
assert(not session.eof_err, 'RPC request after Nvim EOF')
local status, rv = session:request(method, ...)
if not status then
if loop_running then
@@ -331,12 +330,11 @@ 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
t.matches(vim.pesc(eof_err_msg), t.pcall_err(fn_or_timeout, ...))
t.matches(vim.pesc(Session.eof_err_msg), t.pcall_err(fn_or_timeout, ...))
else
t.matches(
vim.pesc(eof_err_msg),
vim.pesc(Session.eof_err_msg),
t.pcall_err(function(timeout, fn, ...)
fn(...)
assert(session)