local helpers = require("fzf-lua.test.helpers")
local assert = helpers.assert
local MiniTest = require("mini.test")
local libuv = require("fzf-lua.libuv")
local eq = assert.are.equal
local same = assert.are.same
describe("Testing libuv module", function()
it("is_escaped (posix)", function()
assert.is.False(libuv.is_escaped([[]], false))
assert.is.True(libuv.is_escaped([[""]], false))
assert.is.True(libuv.is_escaped([['']], false))
assert.is.True(libuv.is_escaped([['foo']], false))
end)
it("is_escaped (win)", function()
assert.is.False(libuv.is_escaped([[]], true))
assert.is.True(libuv.is_escaped([[""]], true))
assert.is.True(libuv.is_escaped([[^"^"]], true))
assert.is.False(libuv.is_escaped([['']], true))
end)
it("shellescape (win bslash)", function()
eq(libuv.shellescape([[]], 1), [[""]])
eq(libuv.shellescape([[^]], 1), [["^"]])
eq(libuv.shellescape([[""]], 1), [["\"\""]])
eq(libuv.shellescape([["^"]], 1), [["\"^\""]])
eq(libuv.shellescape([[foo]], 1), [["foo"]])
eq(libuv.shellescape([["foo"]], 1), [["\"foo\""]])
eq(libuv.shellescape([["foo"bar"]], 1), [["\"foo\"bar\""]])
eq(libuv.shellescape([[foo"bar]], 1), [["foo\"bar"]])
eq(libuv.shellescape([[foo""bar]], 1), [["foo\"\"bar"]])
eq(libuv.shellescape([["foo\"bar"]], 1), [["\"foo\\\"bar\""]])
eq(libuv.shellescape([[foo\]], 1), [["foo\\"]])
eq(libuv.shellescape([[foo\\]], 1), [["foo\\\\"]])
eq(libuv.shellescape([[foo\^]], 1), [["foo\^"]])
eq(libuv.shellescape([[foo\\\\]], 1), [["foo\\\\\\\\"]])
eq(libuv.shellescape([[foo\"]], 1), [["foo\\\""]])
eq(libuv.shellescape([["foo\"]], 1), [["\"foo\\\""]])
eq(libuv.shellescape([["foo\""]], 1), [["\"foo\\\"\""]])
eq(libuv.shellescape([[foo\bar]], 1), [["foo\bar"]])
eq(libuv.shellescape([[foo\\bar]], 1), [["foo\\bar"]])
eq(libuv.shellescape([[foo\\"bar]], 1), [["foo\\\\\"bar"]])
eq(libuv.shellescape([[foo\\\"bar]], 1), [["foo\\\\\\\"bar"]])
end)
it("shellescape (win caret)", function()
eq(libuv.shellescape([[]], 2), [[^"^"]])
eq(libuv.shellescape([["]], 2), [[^"\^"^"]])
eq(libuv.shellescape([[^"]], 2), [[^"^^\^"^"]])
eq(libuv.shellescape([[\"]], 2), [[^"\\\^"^"]])
eq(libuv.shellescape([[\^"]], 2), [[^"^^\\\^"^"]])
eq(libuv.shellescape([[^"^"]], 2), [[^"^^\^"^^\^"^"]])
eq(libuv.shellescape([[__^^"^"__]], 2), [[^"__^^^^\^"^^\^"__^"]])
eq(libuv.shellescape([[__!^^"^"__]], 2),
-- 1st: ^"_^!^^^^\^"^^\^"_^"
-- 2nd: ^"_^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"_^"
[[^"__^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]])
eq(libuv.shellescape([[__^^^^\^"^^\^"__]], 2),
[[^"__^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]])
eq(libuv.shellescape([[^]], 2), [[^"^^^"]])
eq(libuv.shellescape([[^^]], 2), [[^"^^^^^"]])
eq(libuv.shellescape([[^^^]], 2), [[^"^^^^^^^"]])
eq(libuv.shellescape([[^!^]], 2), [[^"^^^^^^^!^^^^^"]])
eq(libuv.shellescape([[!^"]], 2),
-- 1st inner: ^!^^\^"
-- 2nd inner: ^^^!^^^^^^\\\^"
[[^"^^^!^^^^^^\\\^"^"]])
eq(libuv.shellescape([[!\"]], 2), [[^"^^^!^^\\\\\\\^"^"]])
eq(libuv.shellescape([[!\^"]], 2), [[^"^^^!^^^^^^\\\\\\\^"^"]])
eq(libuv.shellescape([[()%^"<>&|;]], 2), [[^"^(^)^%^^\^"^<^>^&^|^;^"]])
eq(libuv.shellescape([[()%^"<>&|;!]], 2),
-- 1st inner: ^(^)^%^^\^"^<^>^&^|^!
-- 2nd inner: ^^^(^^^)^^^%^^^^^^\^"^^^<^^^>^^^&^^^|^^^!
[[^"^^^(^^^)^^^%^^^^^^\\\^"^^^<^^^>^^^&^^^|^^^;^^^!^"]])
eq(libuv.shellescape([[foo]], 2), [[^"foo^"]])
eq(libuv.shellescape([[foo\]], 2), [[^"foo\\^"]])
eq(libuv.shellescape([[foo^]], 2), [[^"foo^^^"]])
eq(libuv.shellescape([[foo\\]], 2), [[^"foo\\\\^"]])
eq(libuv.shellescape([[foo\\\]], 2), [[^"foo\\\\\\^"]])
eq(libuv.shellescape([[foo\\\\]], 2), [[^"foo\\\\\\\\^"]])
eq(libuv.shellescape([[f!oo]], 2), [[^"f^^^!oo^"]])
eq(libuv.shellescape([[^"foo^"]], 2), [[^"^^\^"foo^^\^"^"]])
eq(libuv.shellescape([[\^"foo\^"]], 2), [[^"^^\\\^"foo^^\\\^"^"]])
eq(libuv.shellescape([[foo""bar]], 2), [[^"foo\^"\^"bar^"]])
eq(libuv.shellescape([[foo^"^"bar]], 2), [[^"foo^^\^"^^\^"bar^"]])
eq(libuv.shellescape([["foo\"bar"]], 2), [[^"\^"foo\\\^"bar\^"^"]])
eq(libuv.shellescape([[foo\^"]], 2), [[^"foo^^\\\^"^"]])
eq(libuv.shellescape([[foo\"]], 2), [[^"foo\\\^"^"]])
eq(libuv.shellescape([[^"foo\^"^"]], 2), [[^"^^\^"foo^^\\\^"^^\^"^"]])
eq(libuv.shellescape([[foo\"bar]], 2), [[^"foo\\\^"bar^"]])
eq(libuv.shellescape([[foo\\"bar]], 2), [[^"foo\\\\\^"bar^"]])
eq(libuv.shellescape([[foo\\^^"bar]], 2), [[^"foo\\^^^^\^"bar^"]])
eq(libuv.shellescape([[foo\\\^^^"]], 2), [[^"foo\\\^^^^^^\^"^"]])
end)
it("cmd_argv round-trips shellescape (cmd style)", function()
-- `win_style = true` forces the `cmd.exe` caret escaping (the default
-- on Windows), also runs on posix CI
local values = {
"foo", "a b", "x^y", "a^^b", "p%q", "a&b", "it's", [["q"]], [[E\f]],
"汉 é", "bang!x", "eq=a", "semi;x", "lt<x", "pipe|x", "paren(x)",
"amp&pipe|semi;", "trailback\\", " caret^ leading", "^caret",
"trailcaret^", [[a"b]], [[a\"b]], "two spaces", "10%%", "a!b",
"%TEMP%", "a(b)c", "cur|pipe", "d>e", "a<b",
"load:+execute-silent:echo hi",
}
for _, v in ipairs(values) do
same(libuv.cmd_argv(libuv.shellescape(v, true)), { v })
end
-- the pre-escaped user fragment pattern, e.g. test harness
-- `"--bind=" .. shellescape("load:+execute-silent:...")`
same(libuv.cmd_argv("--bind=" .. libuv.shellescape("load:+execute-silent:echo hi", true)),
{ "--bind=load:+execute-silent:echo hi" })
end)
it("cmd_argv parses cmd fragments", function()
same(libuv.cmd_argv(""), {})
same(libuv.cmd_argv(" "), {})
same(libuv.cmd_argv("--no-multi --layout=reverse"), { "--no-multi", "--layout=reverse" })
same(libuv.cmd_argv([[--header="a b" --no-multi]]), { "--header=a b", "--no-multi" })
same(libuv.cmd_argv("--bind=ctrl-a:first --expect=ctrl-e"),
{ "--bind=ctrl-a:first", "--expect=ctrl-e" })
-- caret escaping quotes/spaces (user args are cmd fragments)
same(libuv.cmd_argv([[--header=^"a b^"]]), { "--header=a b" })
same(libuv.cmd_argv([[--bind=^"load:+execute-silent:echo^ hi^"]]),
{ "--bind=load:+execute-silent:echo hi" })
-- carets are only special outside of double quotes (cmd.exe): inside a
-- quoted fragment a caret is literal and only the quote toggles state
same(libuv.cmd_argv([["x^y"]]), { "x^y" })
same(libuv.cmd_argv([["x^^y"]]), { "x^^y" })
same(libuv.cmd_argv([["a^"b"]]), { "a^b" })
same(libuv.cmd_argv([["A ^" B"]]), { "A ^", "B" })
-- a doubled `""` inside quotes is one literal quote, the pair is consumed
-- and the quote state toggles once (Go runtime `os/exec_windows.go`
-- "Prior to 2008" rule; verified against `fzf.exe` driven through
-- `cmd.exe`, e.g. fzf's own `--query` parser)
same(libuv.cmd_argv([[--header="say ""hi"""]]), { [[--header=say "hi"]] })
same(libuv.cmd_argv([["a""b"]]), { [[a"b]] })
-- the state toggles after the pair: the space is not quoted anymore
same(libuv.cmd_argv([["say ""hi"" tail"]]), { [[say "hi]], "tail" })
-- caret inside quotes stays literal (`cmd.exe` pass), pair still collapses
same(libuv.cmd_argv([["a^""b"]]), { [[a^"b]] })
-- dangling caret: dropped outside quotes, literal inside quotes
same(libuv.cmd_argv([["abc^"]]), { "abc^" })
same(libuv.cmd_argv("^^"), { "^" })
-- backslashes: literal unless collapsing before a quote (2n/2n+1 rules)
same(libuv.cmd_argv([[C:\dir\sub]]), { [[C:\dir\sub]] })
same(libuv.cmd_argv([[a\"b]]), { [[a"b]] })
same(libuv.cmd_argv([[trailback\\]]), { [[trailback\\]] })
same(libuv.cmd_argv([[\^^]]), { [[\^]] })
end)
it("cmd_argv expands defined %NAME% variables (cmd /c semantics)", function()
-- `cmd.exe` expands defined `%NAME%` before any other processing
-- (case-insensitive); an undefined name is left as text and the scan
-- resumes one char later, expanded values are not rescanned. All of
-- this was verified against `cmd.exe /d /e:off /f:off /v:off /c ..`
-- (the legacy launcher route) + `fzf.exe`
local env = {
MYTEST = "abc", SPACED = "hello world",
REC = "%MYTEST%", TEMP = [[C:\Temp]],
}
same(libuv.cmd_argv("--query=%MYTEST%", env), { "--query=abc" })
-- env lookups are case-insensitive
same(libuv.cmd_argv("--query=%mytest%", env), { "--query=abc" })
-- undefined and unpaired `%` are preserved
same(libuv.cmd_argv("--query=%NO_SUCH%", env), { "--query=%NO_SUCH%" })
same(libuv.cmd_argv("--query=%MYTEST", env), { "--query=%MYTEST" })
-- undefined pair: the closing `%` pairs with a later one (`cmd.exe`)
same(libuv.cmd_argv("--query=%%MYTEST%%", env), { "--query=%abc%" })
-- expanded values are not rescanned
same(libuv.cmd_argv("--query=%REC%", env), { "--query=%MYTEST%" })
-- expansion happens before tokenization: value spaces split the argv
same(libuv.cmd_argv("--query=%SPACED%", env), { "--query=hello", "world" })
same(libuv.cmd_argv([[--query="%SPACED%"]], env), { "--query=hello world" })
-- caret-protected (shellescape style) names never match: `MYTEST^`
same(libuv.cmd_argv([[--query=^%MYTEST^%]], env), { "--query=%MYTEST%" })
-- the `shellescape` round-trip holds even when the variable is defined
same(libuv.cmd_argv(libuv.shellescape("x %TEMP% y", true), env),
{ "x %TEMP% y" })
same(libuv.cmd_argv(libuv.shellescape("100%%", true), env), { "100%%" })
-- the default env is the nvim process environment
vim.env.FZF_LUA_CMDARGV_TEST = "xyz"
same(libuv.cmd_argv("--query=%FZF_LUA_CMDARGV_TEST%"), { "--query=xyz" })
vim.env.FZF_LUA_CMDARGV_TEST = nil
end)
it("escape_cli escapes value segments per shell, keeps literal segments", function()
local elems = {
{ { text = "--bind=", escape = false }, { text = "ctrl-r:reload(a b)", escape = true } },
{ { text = "--query", escape = false } },
{ { text = "it's", escape = true } },
}
-- cmd: value segments via shellescape, flags literal, element re-fused
local cmd = libuv.escape_cli(elems, "cmd")
eq(cmd[1], "--bind=" .. libuv.shellescape("ctrl-r:reload(a b)"))
eq(cmd[2], "--query")
eq(cmd[3], libuv.shellescape("it's"))
-- posix: value segments via posix shellescape
local posix = libuv.escape_cli(elems, "posix")
eq(posix[1], "--bind=" .. libuv.shellescape("ctrl-r:reload(a b)"))
eq(posix[3], libuv.shellescape("it's"))
end)
it("build_fzf_cli raw segments round-trip to the escaped cli_args", function()
-- the sync invariant: the raw `segments` second return value must, when
-- escaped for the *current* shell, reproduce the legacy escaped `cli_args`
-- byte-for-byte. This guards the defer-escape refactor against drift.
local core = require("fzf-lua.core")
local config = require("fzf-lua.config")
local defaults = require("fzf-lua.defaults").defaults
local opts = config.normalize_opts({ cwd = vim.fn.getcwd() }, defaults.files)
local cli_args, segments = core.build_fzf_cli(opts)
eq(#cli_args, #segments)
-- no marker tables leak into the escaped string form
for _, a in ipairs(cli_args) do eq(type(a), "string") end
-- re-escaping the raw segments for the current shell reproduces cli_args
local shell = helpers.IS_WIN() and "cmd" or "posix"
same(libuv.escape_cli(segments, shell), cli_args)
end)
it("escape {q} (win, fzf v0.50)", function()
eq(libuv.escape_fzf([[]], 0.50, true), [[]])
eq(libuv.escape_fzf([[\]], 0.50, true), [[\]])
eq(libuv.escape_fzf([[\\]], 0.50, true), [[\\]])
eq(libuv.escape_fzf([[foo]], 0.50, true), [[foo]])
eq(libuv.escape_fzf([[\foo]], 0.50, true), [[\\foo]])
eq(libuv.escape_fzf([[\\foo]], 0.50, true), [[\\\\foo]])
eq(libuv.escape_fzf([[\\\foo]], 0.50, true), [[\\\\\\foo]])
eq(libuv.escape_fzf([[\\\\foo]], 0.50, true), [[\\\\\\\\foo]])
eq(libuv.escape_fzf([[foo\]], 0.50, true), [[foo\]])
eq(libuv.escape_fzf([[foo\\]], 0.50, true), [[foo\\]])
end)
it("unescape {q} (win, fzf v0.50)", function()
eq(libuv.unescape_fzf([[]], 0.50, true), [[]])
eq(libuv.unescape_fzf([[\]], 0.50, true), [[\]])
eq(libuv.unescape_fzf([[\\]], 0.50, true), [[\\]])
eq(libuv.unescape_fzf([[foo]], 0.50, true), [[foo]])
eq(libuv.unescape_fzf([[\foo]], 0.50, true), [[\foo]])
eq(libuv.unescape_fzf([[\\foo]], 0.50, true), [[\foo]])
eq(libuv.unescape_fzf([[\\\foo]], 0.50, true), [[\foo]])
eq(libuv.unescape_fzf([[\\\\foo]], 0.50, true), [[\\foo]])
eq(libuv.unescape_fzf([[foo\]], 0.50, true), [[foo\]])
eq(libuv.unescape_fzf([[foo\\]], 0.50, true), [[foo\\]])
end)
it("escape {q} (win, fzf v0.52)", function()
eq(libuv.escape_fzf([[]], 0.52, true), [[]])
eq(libuv.escape_fzf([[\]], 0.52, true), [[\]])
eq(libuv.escape_fzf([[\\]], 0.52, true), [[\\]])
eq(libuv.escape_fzf([[foo]], 0.52, true), [[foo]])
eq(libuv.escape_fzf([[\foo]], 0.52, true), [[\foo]])
eq(libuv.escape_fzf([[\\foo]], 0.52, true), [[\\foo]])
eq(libuv.escape_fzf([[\\\foo]], 0.52, true), [[\\\foo]])
eq(libuv.escape_fzf([[\\\\foo]], 0.52, true), [[\\\\foo]])
eq(libuv.escape_fzf([[foo\]], 0.52, true), [[foo\]])
eq(libuv.escape_fzf([[foo\\]], 0.52, true), [[foo\\]])
end)
it("unescape {q} (win, fzf v0.52)", function()
eq(libuv.unescape_fzf([[]], 0.52, true), [[]])
eq(libuv.unescape_fzf([[\]], 0.52, true), [[\]])
eq(libuv.unescape_fzf([[\\]], 0.52, true), [[\\]])
eq(libuv.unescape_fzf([[foo]], 0.52, true), [[foo]])
eq(libuv.unescape_fzf([[\foo]], 0.52, true), [[\foo]])
eq(libuv.unescape_fzf([[\\foo]], 0.52, true), [[\\foo]])
eq(libuv.unescape_fzf([[\\\foo]], 0.52, true), [[\\\foo]])
eq(libuv.unescape_fzf([[\\\\foo]], 0.52, true), [[\\\\foo]])
eq(libuv.unescape_fzf([[foo\]], 0.52, true), [[foo\]])
eq(libuv.unescape_fzf([[foo\\]], 0.52, true), [[foo\\]])
end)
it("field index", function()
helpers.SKIP_IF_WIN() -- skip on windows (cannot expand correctly, also segment fault)
local curwin = vim.api.nvim_get_current_win()
vim.cmd("botright new")
local splitwin = vim.api.nvim_get_current_win()
local fzf = require("fzf-lua.fzf")
local selected, exit_code
coroutine.wrap(function()
selected, exit_code = fzf.raw_fzf(
helpers.IS_WIN() and "echo foo&& echo bar" or "echo foo\nbar",
{
"--sync",
"--query f",
-- no quote
"--bind start:+transform:" .. libuv.shellescape([[echo print:{q}]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:{}]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:{+}]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:{n}]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:{+n}]]),
-- double quote
"--bind start:+transform:" .. libuv.shellescape([[echo print:"{q}"]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:"{}"]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:"{+}"]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:"{n}"]]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:"{+n}"]]),
-- single quote
"--bind start:+transform:" .. libuv.shellescape([[echo print:'{q}']]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:'{}']]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:'{+}']]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:'{n}']]),
"--bind start:+transform:" .. libuv.shellescape([[echo print:'{+n}']]),
-- force expand on windows?
-- "--bind result:+transform:" .. libuv.shellescape([[echo print:{q} {} {+} {n} {+n}]]),
-- "--bind result:+select-all",
"--bind start:+accept",
}, { is_fzf_tmux = false })
end)()
-- usually new job channel is the last one
local chans = vim.api.nvim_list_chans()
local job = (chans[#chans] or {}).id
eq({ 0 }, vim.fn.jobwait({ job }))
eq(0, exit_code)
eq(selected, {
"f", "foo", "foo", "0", "0",
"'f'", "'foo'", "'foo'", "0", "0",
"f", "foo", "foo", "0", "0",
-- "f foo foo 0 0",
"foo",
})
vim.api.nvim_set_current_win(curwin)
vim.api.nvim_win_close(splitwin, true)
FzfLua.utils.send_ctrl_c()
end)
it("serialize/deserialize with shared function reference", function()
local func = function() return "test" end
local obj = { fn1 = func, fn2 = func }
local serialized = libuv.serialize(obj, false)
local deserialized = libuv.deserialize(serialized, false)
eq(type(deserialized.fn1), "function")
eq(type(deserialized.fn2), "function")
-- also verify both return the same value as original
eq(deserialized.fn1, deserialized.fn2)
eq(deserialized.fn1(), "test")
eq(deserialized.fn2(), "test")
end)
it("win_console_stdio (posix)", function()
helpers.SKIP_IF_WIN()
eq(nil, libuv.win_console_stdio())
end)
--- Spawn a headless nvim that reports the handle type of its stderr and
--- return that string ("tty" when the child sees a real console).
---@param stdio integer[]
local function probe_stderr_handle(stdio)
local file = vim.fn.tempname()
local probe = vim.fn.tempname() .. ".lua"
vim.fn.writefile({
("vim.fn.writefile({ tostring(vim.uv.guess_handle(2)) }, %s)")
:format(vim.inspect(file)),
}, probe)
local done = false
libuv.uv_spawn(vim.v.progpath, {
args = { "--headless", "--clean", "-l", probe },
stdio = stdio,
}, function() done = true end)
eq(true, vim.wait(10000, function() return done end))
local got = vim.fn.readfile(file)[1]
vim.fn.delete(file)
vim.fn.delete(probe)
return got
end
it("win_console_stdio reopens the console (win)", function()
helpers.SKIP_IF_NOT_WIN()
local fds = libuv.win_console_stdio()
if fds == nil then
-- no console attached (e.g. CI): nil is the documented result and a
-- spawned probe child can't see a console either, nothing to assert
return MiniTest.skip("no console attached")
end
eq({ "table", "number", "number", "number" },
{ type(fds), type(fds[1]), type(fds[2]), type(fds[3]) })
eq(fds[2], fds[3])
-- a child spawned with these fds must see a console on stderr
eq("tty", probe_stderr_handle(fds))
end)
it("win_stderr_fd (posix)", function()
helpers.SKIP_IF_WIN()
eq(2, libuv.win_stderr_fd())
end)
it("win_stderr_fd gives a spawned child a console stderr (win)", function()
helpers.SKIP_IF_NOT_WIN()
local fd = libuv.win_stderr_fd()
eq("number", type(fd))
-- with a console available the child must see one; otherwise the only
-- possible correct result is the fallback to the inherited fd 2
if libuv.win_console_stdio() then
eq("tty", probe_stderr_handle({ 0, 1, fd }))
else
eq(2, fd)
end
end)
--- Read the handle flags of stdio `fd`; nil when it has no valid handle.
--- Reuses the declarations made by `libuv.win_stdio_no_inherit`:
--- `require("ffi")` returns the same cached FFI table (sharing the CType
--- state), while `package.preload.ffi()` would re-initialize it and drop
--- the declarations (see `win_ffi_init` in libuv.lua).
---@param fd integer
---@return integer? flags
local function stdio_handle_flags(fd)
local ffi = require("ffi")
-- STD_INPUT_HANDLE/STD_OUTPUT_HANDLE/STD_ERROR_HANDLE
local STD_HANDLES = { 0xFFFFFFF6, 0xFFFFFFF5, 0xFFFFFFF4 }
local k32 = ffi.load("kernel32")
local flags = ffi.new("unsigned long[1]")
if k32.GetHandleInformation(k32.GetStdHandle(STD_HANDLES[fd + 1]), flags) == 0 then
return
end
---@diagnostic disable-next-line: undefined-field
return flags[0]
end
it("win_stdio_no_inherit (posix)", function()
helpers.SKIP_IF_WIN()
eq(nil, libuv.win_stdio_no_inherit({ 3, 4, 4 }))
end)
it("win_stdio_no_inherit clears and restores the flag (win)", function()
helpers.SKIP_IF_NOT_WIN()
if not package.preload.ffi then return MiniTest.skip("no ffi") end
-- no-op call to trigger the module's one-time FFI declarations
eq(nil, libuv.win_stdio_no_inherit({ 0, 1, 2 }))
local before = stdio_handle_flags(1)
if not before then return MiniTest.skip("no valid stdio handle") end
-- fds 0-2 are not part of the child's stdio: their inherit flag must be
-- cleared for the spawn and restored afterwards (libuv <= 1.52 leaks
-- every inheritable handle to the child regardless of `stdio`)
local restore = libuv.win_stdio_no_inherit({ 3, 4, 4 })
eq("function", type(restore))
---@cast restore fun()
local hidden = stdio_handle_flags(1)
---@cast hidden integer
eq(0, hidden % 2)
restore()
eq(before, stdio_handle_flags(1))
-- all stdio fds in use: nothing to hide
eq(nil, libuv.win_stdio_no_inherit({ 0, 1, 2 }))
end)
end)