fix(tests): use uv.spawn instead of io.popen for unittest helpers

The old implementation of repeated_read_cmd would attempt to run the
command multiple times to handle racyness of async output. Code
like this should not be written. Instead, use the libuv event
loop to read until the process has exited and the pipe has been closed.

This causes some previous discarded errors to be propagated. Fix these
as well.
This commit is contained in:
bfredl
2025-05-20 12:10:37 +02:00
parent 272e041780
commit c81af9428c
4 changed files with 40 additions and 31 deletions

View File

@ -5,17 +5,6 @@ local Paths = require('test.cmakeconfig.paths')
luaassert:set_parameter('TableFormatLevel', 100)
local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote)
--- @param str string
--- @return string
local function shell_quote(str)
if string.find(str, quote_me) or str == '' then
return '"' .. str:gsub('[$%%"\\]', '\\%0') .. '"'
end
return str
end
--- Functions executing in the context of the test runner (not the current nvim test session).
--- @class test.testutil
local M = {
@ -66,19 +55,15 @@ function M.argss_to_cmd(...)
for i = 1, select('#', ...) do
local arg = select(i, ...)
if type(arg) == 'string' then
cmd[#cmd + 1] = shell_quote(arg)
cmd[#cmd + 1] = arg
else
--- @cast arg string[]
for _, subarg in ipairs(arg) do
cmd[#cmd + 1] = shell_quote(subarg)
cmd[#cmd + 1] = subarg
end
end
end
return table.concat(cmd, ' ')
end
function M.popen_r(...)
return io.popen(M.argss_to_cmd(...), 'r')
return cmd
end
--- Calls fn() until it succeeds, up to `max` times or until `max_ms`
@ -356,7 +341,7 @@ function M.check_logs()
local status, f
local out = io.stdout
if os.getenv('SYMBOLIZER') then
status, f = pcall(M.popen_r, os.getenv('SYMBOLIZER'), '-l', file)
status, f = pcall(M.repeated_read_cmd, os.getenv('SYMBOLIZER'), '-l', file)
end
out:write(start_msg .. '\n')
if status then
@ -539,16 +524,37 @@ end
--- @return string?
function M.repeated_read_cmd(...)
for _ = 1, 10 do
local stream = M.popen_r(...)
local ret = stream:read('*a')
stream:close()
if ret then
return ret
local cmd = M.argss_to_cmd(...)
local data = {}
local got_code = nil
local stdout = assert(vim.uv.new_pipe(false))
local handle = assert(
vim.uv.spawn(
cmd[1],
{ args = vim.list_slice(cmd, 2), stdio = { nil, stdout, 2 }, hide = true },
function(code, _signal)
got_code = code
end
)
)
stdout:read_start(function(err, chunk)
if err or chunk == nil then
stdout:read_stop()
stdout:close()
else
table.insert(data, chunk)
end
print('ERROR: Failed to execute ' .. M.argss_to_cmd(...) .. ': nil return after 10 attempts')
return nil
end)
while not stdout:is_closing() or got_code == nil do
vim.uv.run('once')
end
if got_code ~= 0 then
error('command ' .. vim.inspect(cmd) .. 'unexpectedly exited with status ' .. got_code)
end
handle:close()
return table.concat(data)
end
--- @generic T

View File

@ -18,7 +18,7 @@ local type_key = api_t.type_key
local obj2lua = api_t.obj2lua
local func_type = api_t.func_type
local api = cimport('./src/nvim/api/private/t.h', './src/nvim/api/private/converter.h')
local api = cimport('./src/nvim/api/private/helpers.h', './src/nvim/api/private/converter.h')
describe('vim_to_object', function()
local vim_to_object = function(l)

View File

@ -13,8 +13,11 @@ local int_type = t_eval.int_type
local flt_type = t_eval.flt_type
local type_key = t_eval.type_key
local api =
cimport('./src/nvim/api/private/defs.h', './src/nvim/api/private/t.h', './src/nvim/memory.h')
local api = cimport(
'./src/nvim/api/private/defs.h',
'./src/nvim/api/private/helpers.h',
'./src/nvim/memory.h'
)
local obj2lua

View File

@ -94,7 +94,7 @@ local vterm = t.cimport(
'./src/nvim/vterm/screen.h',
'./src/nvim/vterm/state.h',
'./src/nvim/vterm/vterm.h',
'./src/nvim/vterm/vterm_internal.h',
'./src/nvim/vterm/vterm_internal_defs.h',
'./test/unit/fixtures/vterm_test.h'
)