local helpers = require("fzf-lua.test.helpers")
local assert = helpers.assert
---@diagnostic disable-next-line: deprecated
local uv = vim.uv or vim.loop
local libuv = require("fzf-lua.libuv")
local utils = require("fzf-lua.utils")
local eq = assert.are.equal
local fzf_bin = vim.fn.exepath("fzf")
---@param path string
---@param content string
local function write_file(path, content)
local f = io.open(path, "w")
if not f then error("cannot open " .. path .. " for writing") end
f:write(content)
f:close()
end
---@param path string
---@return string
local function read_file(path)
local f = io.open(path, "r")
if not f then return "" end
local content = f:read("*a") or ""
f:close()
return content
end
--- `cmd.exe` builtins (eg. `type`) reject '/'-separated paths; nightly neovim
--- returns forward-slash `vim.fn.tempname()` values on Windows, so convert to
--- native separators before embedding paths into cmd-syntax producer items
---@param p string
---@return string
local function cmd_path(p)
return (p:gsub("/", "\\"))
end
describe("Testing win_spawn (Windows one route)", function()
--- Run one full `libuv.win_spawn` cycle (item producer -> pump -> result
--- file) and return fzf's exit code and the result file contents
---@param item string producer command (cmd syntax)
---@param args string[] raw fzf argv (without the executable)
---@return integer? rc
---@return string out
local function win_run(item, args)
local outfile = utils.tempname()
local rc ---@type integer?
local ok, err = pcall(libuv.win_spawn, {
bin = fzf_bin,
args = args,
item = item,
outfile = outfile,
env = { SHELL = "cmd.exe" },
}, { on_exit = function(code) rc = code end })
if not ok then error(err) end
vim.wait(30000, function() return rc ~= nil end)
local out = read_file(outfile)
vim.fn.delete(outfile)
return rc, out
end
it("pump preserves bytes: argv quoting, streaming and result file", function()
helpers.SKIP_IF_NOT_WIN()
local infile = utils.tempname()
local lines = { "ab c", [[E"f G]], [[H\I]], "汉 é" }
write_file(infile, table.concat(lines, "\n") .. "\n")
local item = string.format('type "%s"', cmd_path(infile))
-- filter arguments with a space (single argv token), quotes, a trailing
-- backslash and multi-byte characters survive the uv quoting + pump
for _, case in ipairs({
{ filter = "ab c", match = "ab c" },
{ filter = [[E"f G]], match = [[E"f G]] },
{ filter = [[H\I]], match = [[H\I]] },
{ filter = "汉 é", match = "汉 é" },
}) do
local rc, out = win_run(item, { "--filter=" .. case.filter })
eq(rc, 0)
eq(out, case.match .. "\n")
end
-- empty filter: match everything, the full stream is byte-identical
local rc, out = win_run(item, { "--filter=", "--no-sort" })
eq(rc, 0)
eq(out, table.concat(lines, "\n") .. "\n")
vim.fn.delete(infile)
end)
it("propagates fzf's exit code when nothing matches", function()
helpers.SKIP_IF_NOT_WIN()
local rc, out = win_run("echo abc", { "--filter=zzz" })
eq(rc, 1)
eq(out, "")
end)
it("streams a large input (backpressure) and matches the last line", function()
helpers.SKIP_IF_NOT_WIN()
local infile = utils.tempname()
local f = io.open(infile, "w")
if not f then error("cannot open " .. infile .. " for writing") end
local buf = {}
for i = 0, 399999 do
buf[#buf + 1] = ("line %06d\n"):format(i)
if #buf >= 4096 then
f:write(table.concat(buf))
buf = {}
end
end
f:write(table.concat(buf))
f:close()
-- ~4.8MB (> the 4MB high watermark): exercises the pump across many
-- chunks with the producer paused by the write queue backpressure
local rc, out = win_run(string.format('type "%s"', cmd_path(infile)),
{ "--filter=line 399999" })
eq(rc, 0)
eq(out, "line 399999\n")
vim.fn.delete(infile)
end)
it("kills the producer's process tree when fzf exits early", function()
helpers.SKIP_IF_NOT_WIN()
-- use a uniquely named copy of PING.EXE so the tasklist check below cannot
-- match an unrelated `ping` running on the machine
local exe_name = "fzflua_spawn_ping.exe"
local tmpdir = utils.tempname() .. "_treekill"
vim.fn.mkdir(tmpdir, "p")
assert.is.True(vim.fn.isdirectory(tmpdir) == 1)
local exe = tmpdir .. "/" .. exe_name
uv.fs_copyfile(vim.fn.exepath("ping"), exe)
assert.is.True(vim.fn.filereadable(exe) == 1)
---@return boolean
local function ping_running()
local out = vim.fn.system({ "tasklist", "/FI", "IMAGENAME eq " .. exe_name, "/NH" })
return out:lower():find(exe_name) ~= nil
end
-- control: the unique binary is detected by `ping_running` while running
local job = vim.fn.jobstart({ exe, "-n", "60", "127.0.0.1" })
assert.is.True(job > 0)
vim.wait(5000, ping_running)
assert.is.True(ping_running())
vim.fn.jobstop(job)
vim.wait(5000, function() return not ping_running() end)
assert.is.False(ping_running())
-- fzf exits immediately with an invalid option while the producer is still
-- alive (output to NUL so only the tree-kill can stop it)
local rc = win_run(string.format('"%s" -n 60 127.0.0.1 >NUL', cmd_path(exe)),
{ "--fzflua-bad-option" })
eq(rc, 2)
assert.is.False(ping_running())
vim.fn.delete(tmpdir, "rf")
end)
it("runs the spec through the win_spawn.lua mediator script", function()
helpers.SKIP_IF_NOT_WIN()
---@diagnostic disable-next-line: deprecated
local _info = debug.getinfo(1, "S")
---@cast _info {source: string}
local root = vim.fn.fnamemodify(_info.source:gsub("^@", ""), ":h:h")
local script = (root .. "/lua/fzf-lua/win_spawn.lua"):gsub("\\", "/")
---@param item string
---@param filter string
---@return integer? rc
---@return string out
local function run_mediator(item, filter)
local outfile = utils.tempname()
local spec_path = utils.tempname()
write_file(spec_path, libuv.serialize({
bin = fzf_bin,
args = { "--filter=" .. filter },
item = item,
outfile = outfile,
env = { SHELL = "cmd.exe" },
}, true))
local rc ---@type integer?
local handle = uv.spawn((vim.v.progpath):gsub("\\", "/"), {
args = { "-u", "NONE", "-l", script, (spec_path:gsub("\\", "/")) },
stdio = { nil, nil, nil },
}, function(code, _)
rc = code
end)
assert.is.True(handle ~= nil)
vim.wait(30000, function() return rc ~= nil end)
local out = read_file(outfile)
vim.fn.delete(outfile)
-- the mediator removes the spec file after reading it
eq(vim.fn.filereadable(spec_path), 0)
return rc, out
end
local infile = utils.tempname()
write_file(infile, "abc\n")
local item = string.format('type "%s"', cmd_path(infile))
-- fzf's exit code becomes the mediator script's exit code
local rc, out = run_mediator(item, "abc")
eq(rc, 0)
eq(out, "abc\n")
rc, out = run_mediator(item, "zzz")
eq(rc, 1)
eq(out, "")
vim.fn.delete(infile)
end)
it("mediator does not leave a default listener (serverlist pollution)", function()
helpers.SKIP_IF_NOT_WIN()
---@diagnostic disable-next-line: deprecated
local _info = debug.getinfo(1, "S")
---@cast _info {source: string}
local root = vim.fn.fnamemodify(_info.source:gsub("^@", ""), ":h:h")
local script = (root .. "/lua/fzf-lua/win_spawn.lua"):gsub("\\", "/")
local outfile = utils.tempname()
local spec_path = utils.tempname()
-- `ping` keeps the mediator alive for a few seconds (fzf exits too fast
-- to sample the pipe namespace reliably)
write_file(spec_path, libuv.serialize({
bin = vim.fn.exepath("ping"),
args = { "-n", "6", "127.0.0.1" },
outfile = outfile,
env = { SHELL = "cmd.exe" },
}, true))
local seen = utils.list_to_map(vim.fn.glob([[\\.\pipe\nvim*]], false, true))
local rc ---@type integer?
local handle = uv.spawn((vim.v.progpath):gsub("\\", "/"), {
args = { "-u", "NONE", "-l", script, (spec_path:gsub("\\", "/")) },
stdio = { nil, nil, nil },
}, function(code, _)
rc = code
end)
assert.is.True(handle ~= nil)
-- sample while the mediator runs: every Windows nvim listens by default
-- on `\\.\pipe\nvim.<pid>.<n>`, so without a `serverstop` guard the
-- mediator registers a listener and the `serverlist` picker lists it
-- (`providers/nvim.lua` discovery globs this namespace)
local leaked = {}
for _ = 1, 3 do
vim.wait(900)
for _, p in ipairs(vim.fn.glob([[\\.\pipe\nvim*]], false, true)) do
if not seen[p] then leaked[#leaked + 1] = p end
end
end
vim.wait(30000, function() return rc ~= nil end)
vim.fn.delete(outfile)
eq(rc, 0)
if #leaked > 0 then
error("mediator left a listener: " .. table.concat(leaked, ", "))
end
end)
end)