From fe87656f29e933b63f5d4dd03b3c0be3ed4ecf5f Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 2 Jan 2025 21:17:27 +0100 Subject: fix(grid): grid_line_start NULL access with 'redrawdebug' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: This test causes a null pointer dereference: local proc = n.spawn_wait('-l', 'test/functional/fixtures/startup-fail.lua') RUN T1565 startup -l Lua Lua-error sets Nvim exitcode: 241.00 ms OK ==================== File …/build/log/asan.13763 ==================== = …/src/nvim/grid.c:389:12: runtime error: null pointer passed as argument 1, which is declared to never be null = /usr/include/string.h:61:62: note: nonnull attribute specified here = 0 0x55cc2d869762 in grid_line_start …/src/nvim/grid.c:389:5 = 1 0x55cc2d8717ca in grid_clear …/src/nvim/grid.c:618:5 = 2 0x55cc2dbe0f6f in msg_clr_eos_force …/src/nvim/message.c:3085:3 = 3 0x55cc2dbbbdec in msg_clr_eos …/src/nvim/message.c:3061:5 = 4 0x55cc2dbbae2c in msg_multiline …/src/nvim/message.c:281:9 = 5 0x55cc2dbba2b4 in msg_keep …/src/nvim/message.c:364:5 = 6 0x55cc2dbc4992 in emsg_multiline …/src/nvim/message.c:773:10 = 7 0x55cc2dbc5d43 in semsg_multiline …/src/nvim/message.c:824:9 = 8 0x55cc2d9c5945 in nlua_error …/src/nvim/lua/executor.c:158:5 = 9 0x55cc2d9c89fd in nlua_exec_file …/src/nvim/lua/executor.c:1862:5 = 10 0x55cc2d9f4d69 in main …/src/nvim/main.c:637:19 = 11 0x7f319b62a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 = 12 0x7f319b62a28a in __libc_start_main csu/../csu/libc-start.c:360:3 = 13 0x55cc2ced0f64 in _start (…/build/bin/nvim+0xc48f64) (BuildId: 309c83f8d74297c89719dae9c271dd8ec23e64c3) Cause: The tests use `redrawdebug=invalid` by default, but `default_grid_alloc` skips calling `grid_alloc` when not `full_screen`. Solution: Check for `full_screen`. --- src/nvim/grid.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nvim/grid.c b/src/nvim/grid.c index e863cb3476..df93ad1655 100644 --- a/src/nvim/grid.c +++ b/src/nvim/grid.c @@ -383,7 +383,8 @@ void grid_line_start(ScreenGrid *grid, int row) assert((size_t)grid_line_maxcol <= linebuf_size); - if (rdb_flags & kOptRdbFlagInvalid) { + if (full_screen && (rdb_flags & kOptRdbFlagInvalid)) { + assert(linebuf_char); // Current batch must not depend on previous contents of linebuf_char. // Set invalid values which will cause assertion failures later if they are used. memset(linebuf_char, 0xFF, sizeof(schar_T) * linebuf_size); -- cgit From a1ba655dee0f89230ea09712e4df981cc3b15bea Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 12 Sep 2024 03:04:33 +0200 Subject: test: spawn_wait() starts a non-RPC Nvim process Problem: Can't use `n.clear()` to test non-RPC `nvim` invocations. So tests end up creating ad-hoc wrappers around `system()` or `jobstart()`. Solution: - Introduce `n.spawn_wait()` - TODO (followup PR): Rename `n.spawn()` and `n.spawn_wait()`. It's misleading that `n.spawn()` returns a RPC session... --- MAINTAIN.md | 2 + runtime/lua/coxpcall.lua | 4 ++ src/nvim/main.c | 1 + test/client/msgpack_rpc_stream.lua | 105 ------------------------------- test/client/rpc_stream.lua | 112 ++++++++++++++++++++++++++++++++++ test/client/session.lua | 52 ++++++++++++---- test/client/uv_stream.lua | 99 ++++++++++++++++++++++++------ test/functional/core/startup_spec.lua | 14 ++--- test/functional/testnvim.lua | 71 ++++++++++++++------- 9 files changed, 297 insertions(+), 163 deletions(-) delete mode 100644 test/client/msgpack_rpc_stream.lua create mode 100644 test/client/rpc_stream.lua diff --git a/MAINTAIN.md b/MAINTAIN.md index cd3dacb964..1442faeff8 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -143,6 +143,8 @@ These dependencies are "vendored" (inlined), we must update the sources manually * `src/mpack/`: [libmpack](https://github.com/libmpack/libmpack) * send improvements upstream! +* `src/mpack/lmpack.c`: [libmpack-lua](https://github.com/libmpack/libmpack-lua) + * send improvements upstream! * `src/xdiff/`: [xdiff](https://github.com/git/git/tree/master/xdiff) * `src/cjson/`: [lua-cjson](https://github.com/openresty/lua-cjson) * `src/klib/`: [Klib](https://github.com/attractivechaos/klib) diff --git a/runtime/lua/coxpcall.lua b/runtime/lua/coxpcall.lua index 6b179f1ef0..75e7a43567 100644 --- a/runtime/lua/coxpcall.lua +++ b/runtime/lua/coxpcall.lua @@ -1,6 +1,10 @@ ------------------------------------------------------------------------------- +-- (Not needed for LuaJIT or Lua 5.2+) +-- -- Coroutine safe xpcall and pcall versions -- +-- https://keplerproject.github.io/coxpcall/ +-- -- Encapsulates the protected calls with a coroutine based loop, so errors can -- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines -- yielding inside the call to pcall or xpcall. diff --git a/src/nvim/main.c b/src/nvim/main.c index 348f246d27..2b55a48c12 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -634,6 +634,7 @@ int main(int argc, char **argv) if (params.luaf != NULL) { // Like "--cmd", "+", "-c" and "-S", don't truncate messages. msg_scroll = true; + DLOG("executing Lua -l script"); bool lua_ok = nlua_exec_file(params.luaf); TIME_MSG("executing Lua -l script"); if (msg_didout) { diff --git a/test/client/msgpack_rpc_stream.lua b/test/client/msgpack_rpc_stream.lua deleted file mode 100644 index 7131940a58..0000000000 --- a/test/client/msgpack_rpc_stream.lua +++ /dev/null @@ -1,105 +0,0 @@ -local mpack = vim.mpack - -local Response = {} -Response.__index = Response - -function Response.new(msgpack_rpc_stream, request_id) - return setmetatable({ - _msgpack_rpc_stream = msgpack_rpc_stream, - _request_id = request_id, - }, Response) -end - -function Response:send(value, is_error) - local data = self._msgpack_rpc_stream._session:reply(self._request_id) - if is_error then - data = data .. self._msgpack_rpc_stream._pack(value) - data = data .. self._msgpack_rpc_stream._pack(mpack.NIL) - else - data = data .. self._msgpack_rpc_stream._pack(mpack.NIL) - data = data .. self._msgpack_rpc_stream._pack(value) - end - self._msgpack_rpc_stream._stream:write(data) -end - ---- @class test.MsgpackRpcStream ---- @field private _stream test.Stream ---- @field private __pack table -local MsgpackRpcStream = {} -MsgpackRpcStream.__index = MsgpackRpcStream - -function MsgpackRpcStream.new(stream) - return setmetatable({ - _stream = stream, - _pack = mpack.Packer(), - _session = mpack.Session({ - unpack = mpack.Unpacker({ - ext = { - -- Buffer - [0] = function(_c, s) - return mpack.decode(s) - end, - -- Window - [1] = function(_c, s) - return mpack.decode(s) - end, - -- Tabpage - [2] = function(_c, s) - return mpack.decode(s) - end, - }, - }), - }), - }, MsgpackRpcStream) -end - -function MsgpackRpcStream:write(method, args, response_cb) - local data - if response_cb then - assert(type(response_cb) == 'function') - data = self._session:request(response_cb) - else - data = self._session:notify() - end - - data = data .. self._pack(method) .. self._pack(args) - self._stream:write(data) -end - -function MsgpackRpcStream:read_start(request_cb, notification_cb, eof_cb) - self._stream:read_start(function(data) - if not data then - return eof_cb() - end - local type, id_or_cb, method_or_error, args_or_result - local pos = 1 - local len = #data - while pos <= len do - type, id_or_cb, method_or_error, args_or_result, pos = self._session:receive(data, pos) - if type == 'request' or type == 'notification' then - if type == 'request' then - request_cb(method_or_error, args_or_result, Response.new(self, id_or_cb)) - else - notification_cb(method_or_error, args_or_result) - end - elseif type == 'response' then - if method_or_error == mpack.NIL then - method_or_error = nil - else - args_or_result = nil - end - id_or_cb(method_or_error, args_or_result) - end - end - end) -end - -function MsgpackRpcStream:read_stop() - self._stream:read_stop() -end - -function MsgpackRpcStream:close(signal) - self._stream:close(signal) -end - -return MsgpackRpcStream diff --git a/test/client/rpc_stream.lua b/test/client/rpc_stream.lua new file mode 100644 index 0000000000..9f2672bcf9 --- /dev/null +++ b/test/client/rpc_stream.lua @@ -0,0 +1,112 @@ +--- +--- Reading/writing of msgpack over any of the stream types from `uv_stream.lua`. +--- Does not implement the RPC protocol, see `session.lua` for that. +--- + +local mpack = vim.mpack + +local Response = {} +Response.__index = Response + +function Response.new(rpc_stream, request_id) + return setmetatable({ + _rpc_stream = rpc_stream, + _request_id = request_id, + }, Response) +end + +function Response:send(value, is_error) + local data = self._rpc_stream._session:reply(self._request_id) + if is_error then + data = data .. self._rpc_stream._pack(value) + data = data .. self._rpc_stream._pack(mpack.NIL) + else + data = data .. self._rpc_stream._pack(mpack.NIL) + data = data .. self._rpc_stream._pack(value) + end + self._rpc_stream._stream:write(data) +end + +--- Nvim msgpack RPC stream. +--- +--- @class test.RpcStream +--- @field private _stream test.Stream +--- @field private __pack table +local RpcStream = {} +RpcStream.__index = RpcStream + +function RpcStream.new(stream) + return setmetatable({ + _stream = stream, + _pack = mpack.Packer(), + _session = mpack.Session({ + unpack = mpack.Unpacker({ + ext = { + -- Buffer + [0] = function(_c, s) + return mpack.decode(s) + end, + -- Window + [1] = function(_c, s) + return mpack.decode(s) + end, + -- Tabpage + [2] = function(_c, s) + return mpack.decode(s) + end, + }, + }), + }), + }, RpcStream) +end + +function RpcStream:write(method, args, response_cb) + local data + if response_cb then + assert(type(response_cb) == 'function') + data = self._session:request(response_cb) + else + data = self._session:notify() + end + + data = data .. self._pack(method) .. self._pack(args) + self._stream:write(data) +end + +function RpcStream:read_start(on_request, on_notification, on_eof) + self._stream:read_start(function(data) + if not data then + return on_eof() + end + local type, id_or_cb, method_or_error, args_or_result + local pos = 1 + local len = #data + while pos <= len do + type, id_or_cb, method_or_error, args_or_result, pos = self._session:receive(data, pos) + if type == 'request' or type == 'notification' then + if type == 'request' then + on_request(method_or_error, args_or_result, Response.new(self, id_or_cb)) + else + on_notification(method_or_error, args_or_result) + end + elseif type == 'response' then + if method_or_error == mpack.NIL then + method_or_error = nil + else + args_or_result = nil + end + id_or_cb(method_or_error, args_or_result) + end + end + end) +end + +function RpcStream:read_stop() + self._stream:read_stop() +end + +function RpcStream:close(signal) + self._stream:close(signal) +end + +return RpcStream diff --git a/test/client/session.lua b/test/client/session.lua index f1f46c5efe..5b7f1a7caa 100644 --- a/test/client/session.lua +++ b/test/client/session.lua @@ -1,13 +1,21 @@ +--- +--- Nvim msgpack-RPC protocol session. Manages requests/notifications/responses. +--- + local uv = vim.uv -local MsgpackRpcStream = require('test.client.msgpack_rpc_stream') +local RpcStream = require('test.client.rpc_stream') +--- Nvim msgpack-RPC protocol session. Manages requests/notifications/responses. +--- --- @class test.Session ---- @field private _pending_messages string[] ---- @field private _msgpack_rpc_stream test.MsgpackRpcStream +--- @field private _pending_messages string[] Requests/notifications received from the remote end. +--- @field private _rpc_stream test.RpcStream --- @field private _prepare uv.uv_prepare_t --- @field private _timer uv.uv_timer_t ---- @field private _is_running boolean --- @field exec_lua_setup boolean +--- @field private _is_running boolean true during `Session:run()` scope. +--- @field private _stdout_buffer string[] Stores stdout chunks +--- @field public stdout string Full stdout after the process exits local Session = {} Session.__index = Session if package.loaded['jit'] then @@ -51,9 +59,10 @@ local function coroutine_exec(func, ...) end)) end +--- Creates a new msgpack-RPC session. function Session.new(stream) return setmetatable({ - _msgpack_rpc_stream = MsgpackRpcStream.new(stream), + _rpc_stream = RpcStream.new(stream), _pending_messages = {}, _prepare = uv.new_prepare(), _timer = uv.new_timer(), @@ -91,10 +100,13 @@ function Session:next_message(timeout) return table.remove(self._pending_messages, 1) end +--- Sends a notification to the RPC endpoint. function Session:notify(method, ...) - self._msgpack_rpc_stream:write(method, { ... }) + self._rpc_stream:write(method, { ... }) end +--- Sends a request to the RPC endpoint. +--- --- @param method string --- @param ... any --- @return boolean, table @@ -114,8 +126,16 @@ function Session:request(method, ...) return true, result end ---- Runs the event loop. +--- Processes incoming RPC requests/notifications until exhausted. +--- +--- TODO(justinmk): luaclient2 avoids this via uvutil.cb_wait() + uvutil.add_idle_call()? +--- +--- @param request_cb function Handles requests from the sever to the local end. +--- @param notification_cb function Handles notifications from the sever to the local end. +--- @param setup_cb function +--- @param timeout number function Session:run(request_cb, notification_cb, setup_cb, timeout) + --- Handles an incoming request. local function on_request(method, args, response) coroutine_exec(request_cb, method, args, function(status, result, flag) if status then @@ -126,6 +146,7 @@ function Session:run(request_cb, notification_cb, setup_cb, timeout) end) end + --- Handles an incoming notification. local function on_notification(method, args) coroutine_exec(notification_cb, method, args) end @@ -160,39 +181,45 @@ function Session:close(signal) if not self._prepare:is_closing() then self._prepare:close() end - self._msgpack_rpc_stream:close(signal) + self._rpc_stream:close(signal) self.closed = true end +--- Sends a request to the RPC endpoint, without blocking (schedules a coroutine). function Session:_yielding_request(method, args) return coroutine.yield(function(co) - self._msgpack_rpc_stream:write(method, args, function(err, result) + self._rpc_stream:write(method, args, function(err, result) resume(co, err, result) end) end) end +--- Sends a request to the RPC endpoint, and blocks (polls event loop) until a response is received. function Session:_blocking_request(method, args) local err, result + -- Invoked when a request is received from the remote end. local function on_request(method_, args_, response) table.insert(self._pending_messages, { 'request', method_, args_, response }) end + -- Invoked when a notification is received from the remote end. local function on_notification(method_, args_) table.insert(self._pending_messages, { 'notification', method_, args_ }) end - self._msgpack_rpc_stream:write(method, args, function(e, r) + self._rpc_stream:write(method, args, function(e, r) err = e result = r uv.stop() end) + -- Poll for incoming requests/notifications received from the remote end. self:_run(on_request, on_notification) return (err or self.eof_err), result end +--- Polls for incoming requests/notifications received from the remote end. function Session:_run(request_cb, notification_cb, timeout) if type(timeout) == 'number' then self._prepare:start(function() @@ -202,14 +229,15 @@ function Session:_run(request_cb, notification_cb, timeout) self._prepare:stop() end) end - self._msgpack_rpc_stream:read_start(request_cb, notification_cb, function() + self._rpc_stream:read_start(request_cb, notification_cb, function() uv.stop() self.eof_err = { 1, 'EOF was received from Nvim. Likely the Nvim process crashed.' } end) uv.run() self._prepare:stop() self._timer:stop() - self._msgpack_rpc_stream:read_stop() + self._rpc_stream:read_stop() end +--- Nvim msgpack-RPC session. return Session diff --git a/test/client/uv_stream.lua b/test/client/uv_stream.lua index adf002ba1e..ac84cbf9bc 100644 --- a/test/client/uv_stream.lua +++ b/test/client/uv_stream.lua @@ -1,3 +1,8 @@ +--- +--- Basic stream types. +--- See `rpc_stream.lua` for the msgpack layer. +--- + local uv = vim.uv --- @class test.Stream @@ -6,6 +11,8 @@ local uv = vim.uv --- @field read_stop fun(self) --- @field close fun(self, signal?: string) +--- Stream over given pipes. +--- --- @class vim.StdioStream : test.Stream --- @field private _in uv.uv_pipe_t --- @field private _out uv.uv_pipe_t @@ -45,6 +52,8 @@ function StdioStream:close() self._out:close() end +--- Stream over a named pipe or TCP socket. +--- --- @class test.SocketStream : test.Stream --- @field package _stream_error? string --- @field package _socket uv.uv_pipe_t @@ -109,26 +118,46 @@ function SocketStream:close() uv.close(self._socket) end ---- @class test.ChildProcessStream : test.Stream +--- Stream over child process stdio. +--- +--- @class test.ProcStream : test.Stream --- @field private _proc uv.uv_process_t --- @field private _pid integer --- @field private _child_stdin uv.uv_pipe_t --- @field private _child_stdout uv.uv_pipe_t +--- @field private _child_stderr uv.uv_pipe_t +--- @field stdout string +--- @field stderr string +--- @field stdout_eof boolean +--- @field stderr_eof boolean +--- @field private collect_output boolean +--- Exit code --- @field status integer --- @field signal integer -local ChildProcessStream = {} -ChildProcessStream.__index = ChildProcessStream +local ProcStream = {} +ProcStream.__index = ProcStream +--- Starts child process specified by `argv`. +--- --- @param argv string[] --- @param env string[]? --- @param io_extra uv.uv_pipe_t? ---- @return test.ChildProcessStream -function ChildProcessStream.spawn(argv, env, io_extra) +--- @return test.ProcStream +function ProcStream.spawn(argv, env, io_extra) local self = setmetatable({ - _child_stdin = uv.new_pipe(false), - _child_stdout = uv.new_pipe(false), + collect_output = false, + output = '', + stdout = '', + stderr = '', + stdout_error = nil, -- TODO: not used, remove + stderr_error = nil, -- TODO: not used, remove + stdout_eof = false, + stderr_eof = false, + _child_stdin = assert(uv.new_pipe(false)), + _child_stdout = assert(uv.new_pipe(false)), + _child_stderr = assert(uv.new_pipe(false)), _exiting = false, - }, ChildProcessStream) + }, ProcStream) local prog = argv[1] local args = {} --- @type string[] for i = 2, #argv do @@ -136,13 +165,14 @@ function ChildProcessStream.spawn(argv, env, io_extra) end --- @diagnostic disable-next-line:missing-fields self._proc, self._pid = uv.spawn(prog, { - stdio = { self._child_stdin, self._child_stdout, 1, io_extra }, + stdio = { self._child_stdin, self._child_stdout, self._child_stderr, io_extra }, args = args, --- @diagnostic disable-next-line:assign-type-mismatch env = env, }, function(status, signal) - self.status = status self.signal = signal + -- "Abort" exit may not set status; force to nonzero in that case. + self.status = (0 ~= (status or 0) or 0 == (signal or 0)) and status or (128 + (signal or 0)) end) if not self._proc then @@ -153,24 +183,56 @@ function ChildProcessStream.spawn(argv, env, io_extra) return self end -function ChildProcessStream:write(data) +function ProcStream:write(data) self._child_stdin:write(data) end -function ChildProcessStream:read_start(cb) - self._child_stdout:read_start(function(err, chunk) - if err then - error(err) +function ProcStream:on_read(stream, cb, err, chunk) + if err then + -- stderr_error/stdout_error + self[stream .. '_error'] = err ---@type string + -- error(err) + elseif chunk then + -- 'stderr' or 'stdout' + if self.collect_output then + self[stream] = self[stream] .. chunk ---@type string + --- Collects both stdout + stderr. + self.output = self[stream] .. chunk ---@type string end + else + -- stderr_eof/stdout_eof + self[stream .. '_eof'] = true ---@type boolean + end + + -- Handler provided by the caller. + if cb then cb(chunk) + end +end + +--- Collects output until the process exits. +function ProcStream:wait() + self.collect_output = true + while not (self.stdout_eof and self.stderr_eof and (self.status or self.signal)) do + uv.run('once') + end +end + +function ProcStream:read_start(on_stdout, on_stderr) + self._child_stdout:read_start(function(err, chunk) + self:on_read('stdout', on_stdout, err, chunk) + end) + self._child_stderr:read_start(function(err, chunk) + self:on_read('stderr', on_stderr, err, chunk) end) end -function ChildProcessStream:read_stop() +function ProcStream:read_stop() self._child_stdout:read_stop() + self._child_stderr:read_stop() end -function ChildProcessStream:close(signal) +function ProcStream:close(signal) if self._closed then return end @@ -178,6 +240,7 @@ function ChildProcessStream:close(signal) self:read_stop() self._child_stdin:close() self._child_stdout:close() + self._child_stderr:close() if type(signal) == 'string' then self._proc:kill('sig' .. signal) end @@ -189,6 +252,6 @@ end return { StdioStream = StdioStream, - ChildProcessStream = ChildProcessStream, + ProcStream = ProcStream, SocketStream = SocketStream, } diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 76b0755441..c9ea280646 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -154,8 +154,9 @@ describe('startup', function() it('failure modes', function() -- nvim -l - matches('nvim%.?e?x?e?: Argument missing after: "%-l"', fn.system({ nvim_prog, '-l' })) - eq(1, eval('v:shell_error')) + local proc = n.spawn_wait('-l') + matches('nvim%.?e?x?e?: Argument missing after: "%-l"', proc.stderr) + eq(1, proc.status) end) it('os.exit() sets Nvim exitcode', function() @@ -182,12 +183,11 @@ describe('startup', function() end) it('Lua-error sets Nvim exitcode', function() + local proc = n.spawn_wait('-l', 'test/functional/fixtures/startup-fail.lua') + matches('E5113: .* my pearls!!', proc.output) + eq(1, proc.status) + eq(0, eval('v:shell_error')) - matches( - 'E5113: .* my pearls!!', - fn.system({ nvim_prog, '-l', 'test/functional/fixtures/startup-fail.lua' }) - ) - eq(1, eval('v:shell_error')) matches( 'E5113: .* %[string "error%("whoa"%)"%]:1: whoa', fn.system({ nvim_prog, '-l', '-' }, 'error("whoa")') diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua index 675ad9e3d7..d65cbf685e 100644 --- a/test/functional/testnvim.lua +++ b/test/functional/testnvim.lua @@ -4,7 +4,7 @@ local t = require('test.testutil') local Session = require('test.client.session') local uv_stream = require('test.client.uv_stream') local SocketStream = uv_stream.SocketStream -local ChildProcessStream = uv_stream.ChildProcessStream +local ProcStream = uv_stream.ProcStream local check_cores = t.check_cores local check_logs = t.check_logs @@ -465,10 +465,12 @@ function M.check_close() session = nil end +--- Starts `argv` process as a Nvim msgpack-RPC session. +--- --- @param argv string[] --- @param merge boolean? --- @param env string[]? ---- @param keep boolean? +--- @param keep boolean? Don't close the current global session. --- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option --- @return test.Session function M.spawn(argv, merge, env, keep, io_extra) @@ -476,9 +478,8 @@ function M.spawn(argv, merge, env, keep, io_extra) M.check_close() end - local child_stream = - ChildProcessStream.spawn(merge and M.merge_args(prepend_argv, argv) or argv, env, io_extra) - return Session.new(child_stream) + local proc = ProcStream.spawn(merge and M.merge_args(prepend_argv, argv) or argv, env, io_extra) + return Session.new(proc) end -- Creates a new Session connected by domain socket (named pipe) or TCP. @@ -489,31 +490,59 @@ function M.connect(file_or_address) return Session.new(stream) end --- Starts (and returns) a new global Nvim session. --- --- Parameters are interpreted as startup args, OR a map with these keys: --- args: List: Args appended to the default `nvim_argv` set. --- args_rm: List: Args removed from the default set. All cases are --- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd" --- (and its value) from the default set. --- env: Map: Defines the environment of the new session. --- --- Example: --- clear('-e') --- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} +--- Starts (and returns) a new global Nvim session. +--- +--- Use `spawn_argv()` to get a new session without replacing the current global session. +--- +--- Parameters are interpreted as startup args, OR a map with these keys: +--- - args: List: Args appended to the default `nvim_argv` set. +--- - args_rm: List: Args removed from the default set. All cases are +--- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd" +--- (and its value) from the default set. +--- - env: Map: Defines the environment of the new session. +--- +--- Example: +--- ``` +--- clear('-e') +--- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} +--- ``` +--- +--- @param ... string Nvim CLI args +--- @return test.Session +--- @overload fun(opts: test.new_argv.Opts): test.Session function M.clear(...) M.set_session(M.spawn_argv(false, ...)) return M.get_session() end ---- same params as clear, but does returns the session instead ---- of replacing the default session +--- Same as clear(), but doesn't replace the current global session. +--- +--- @param keep boolean Don't close the current global session. +--- @param ... string Nvim CLI args --- @return test.Session +--- @overload fun(opts: test.new_argv.Opts): test.Session function M.spawn_argv(keep, ...) local argv, env, io_extra = M.new_argv(...) return M.spawn(argv, nil, env, keep, io_extra) end +--- Starts a (`--headless`, non-RPC) Nvim process, waits for exit, and returns output + info. +--- +--- @param ... string Nvim CLI args +--- @return test.ProcStream +--- @overload fun(opts: test.new_argv.Opts): test.ProcStream +function M.spawn_wait(...) + local opts = type(...) == 'string' and { args = { ... } } or ... + opts.args_rm = opts.args_rm and opts.args_rm or {} + table.insert(opts.args_rm, '--embed') + local argv, env, io_extra = M.new_argv(opts) + local proc = ProcStream.spawn(argv, env, io_extra) + proc:read_start() + proc:wait() + proc:close() + return proc +end + --- @class test.new_argv.Opts --- @field args? string[] --- @field args_rm? string[] @@ -522,11 +551,11 @@ end --- Builds an argument list for use in clear(). --- ---- @see clear() for parameters. ---- @param ... string +--- @param ... string See clear(). --- @return string[] --- @return string[]? --- @return uv.uv_pipe_t? +--- @overload fun(opts: test.new_argv.Opts): string[], string[]?, uv.uv_pipe_t? function M.new_argv(...) local args = { unpack(M.nvim_argv) } table.insert(args, '--headless') -- cgit From 700a25e6218e016b5adb0ddee740be4618d717a2 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 3 Jan 2025 18:39:42 +0100 Subject: test: include stderr in EOF failure message --- test/client/session.lua | 12 ++++++++---- test/client/uv_stream.lua | 18 ++++++++++-------- test/functional/testnvim.lua | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/test/client/session.lua b/test/client/session.lua index 5b7f1a7caa..a5839e012a 100644 --- a/test/client/session.lua +++ b/test/client/session.lua @@ -12,10 +12,8 @@ local RpcStream = require('test.client.rpc_stream') --- @field private _rpc_stream test.RpcStream --- @field private _prepare uv.uv_prepare_t --- @field private _timer uv.uv_timer_t ---- @field exec_lua_setup boolean --- @field private _is_running boolean true during `Session:run()` scope. ---- @field private _stdout_buffer string[] Stores stdout chunks ---- @field public stdout string Full stdout after the process exits +--- @field exec_lua_setup boolean local Session = {} Session.__index = Session if package.loaded['jit'] then @@ -231,7 +229,13 @@ function Session:_run(request_cb, notification_cb, timeout) end self._rpc_stream:read_start(request_cb, notification_cb, function() uv.stop() - self.eof_err = { 1, 'EOF was received from Nvim. Likely the Nvim process crashed.' } + + --- @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 } end) uv.run() self._prepare:stop() diff --git a/test/client/uv_stream.lua b/test/client/uv_stream.lua index ac84cbf9bc..a9ef2db115 100644 --- a/test/client/uv_stream.lua +++ b/test/client/uv_stream.lua @@ -127,9 +127,11 @@ end --- @field private _child_stdout uv.uv_pipe_t --- @field private _child_stderr uv.uv_pipe_t --- @field stdout string +--- stderr is always collected in this field, regardless of `collect_output`. --- @field stderr string --- @field stdout_eof boolean --- @field stderr_eof boolean +--- Collects stdout in the `stdout` field, and stdout+stderr in `output` field. --- @field private collect_output boolean --- Exit code --- @field status integer @@ -149,8 +151,6 @@ function ProcStream.spawn(argv, env, io_extra) output = '', stdout = '', stderr = '', - stdout_error = nil, -- TODO: not used, remove - stderr_error = nil, -- TODO: not used, remove stdout_eof = false, stderr_eof = false, _child_stdin = assert(uv.new_pipe(false)), @@ -189,14 +189,16 @@ end function ProcStream:on_read(stream, cb, err, chunk) if err then - -- stderr_error/stdout_error - self[stream .. '_error'] = err ---@type string - -- error(err) + error(err) -- stream read failed? elseif chunk then - -- 'stderr' or 'stdout' + -- Always collect stderr, in case it gives useful info on failure. + if stream == 'stderr' then + self.stderr = self.stderr .. chunk ---@type string + end + -- Collect stdout if `collect_output` is enabled. if self.collect_output then - self[stream] = self[stream] .. chunk ---@type string - --- Collects both stdout + stderr. + self.stdout = self.stdout .. chunk ---@type string + --- Collect both stdout + stderr in the `output` field. self.output = self[stream] .. chunk ---@type string end else diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua index d65cbf685e..c8f1c35555 100644 --- a/test/functional/testnvim.lua +++ b/test/functional/testnvim.lua @@ -333,9 +333,9 @@ end 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 - eq(eof_err_msg, t.pcall_err(fn_or_timeout, ...)) + t.matches(eof_err_msg, t.pcall_err(fn_or_timeout, ...)) else - eq( + t.matches( eof_err_msg, t.pcall_err(function(timeout, fn, ...) fn(...) -- cgit