refactor: async processes

This commit is contained in:
Folke Lemaitre 2024-06-28 16:08:26 +02:00
parent 4319846b8c
commit a36ebd2a75
No known key found for this signature in database
GPG Key ID: 41F8B1FBACAE2040
12 changed files with 394 additions and 379 deletions

View File

@ -1,78 +1,122 @@
---@class AsyncOpts
---@field on_done? fun()
---@field on_error? fun(err:string)
---@field on_yield? fun(res:any)
local M = {} local M = {}
---@type Async[] ---@type Async[]
M._queue = {} M._queue = {}
M._executor = assert(vim.loop.new_check()) M._executor = assert(vim.loop.new_check())
M._running = false M._running = false
---@type Async
M.current = nil ---@type table<thread, Async>
M._threads = setmetatable({}, { __mode = "k" })
---@alias AsyncEvent "done" | "error" | "yield" | "ok"
---@class Async ---@class Async
---@field co thread ---@field _co thread
---@field opts AsyncOpts ---@field _fn fun()
---@field sleeping? boolean ---@field _suspended? boolean
---@field _on table<AsyncEvent, fun(res:any, async:Async)[]>
local Async = {} local Async = {}
---@param fn async fun() ---@param fn async fun()
---@param opts? AsyncOpts
---@return Async ---@return Async
function Async.new(fn, opts) function Async.new(fn)
local self = setmetatable({}, { __index = Async }) local self = setmetatable({}, { __index = Async })
self.co = coroutine.create(fn) return self:init(fn)
self.opts = opts or {} end
---@param fn async fun()
---@return Async
function Async:init(fn)
self._fn = fn
self._on = {}
self._co = coroutine.create(function()
local ok, err = pcall(self._fn)
if not ok then
self:_emit("error", err)
end
self:_emit("done")
end)
M._threads[self._co] = self
return M.add(self)
end
function Async:restart()
assert(not self:running(), "Cannot restart a running async")
self:init(self._fn)
end
---@param event AsyncEvent
---@param cb async fun(res:any, async:Async)
function Async:on(event, cb)
self._on[event] = self._on[event] or {}
table.insert(self._on[event], cb)
return self return self
end end
function Async:running() ---@private
return coroutine.status(self.co) ~= "dead" ---@param event AsyncEvent
---@param res any
function Async:_emit(event, res)
for _, cb in ipairs(self._on[event] or {}) do
cb(res, self)
end
end end
function Async:running()
return coroutine.status(self._co) ~= "dead"
end
---@async
function Async:sleep(ms) function Async:sleep(ms)
self.sleeping = true self._suspended = true
vim.defer_fn(function() vim.defer_fn(function()
self.sleeping = false self._suspended = false
end, ms) end, ms)
coroutine.yield() coroutine.yield()
end end
---@async
function Async:suspend() function Async:suspend()
self.sleeping = true self._suspended = true
if coroutine.running() == self._co then
coroutine.yield()
end
end end
function Async:resume() function Async:resume()
self.sleeping = false self._suspended = false
end
function Async:wait()
local async = M.running()
if coroutine.running() == self._co then
error("Cannot wait on self")
end
while self:running() do
if async then
coroutine.yield()
else
vim.wait(10)
end
end
return self
end end
function Async:step() function Async:step()
if self.sleeping then if self._suspended then
return true return true
end end
local status = coroutine.status(self.co) local status = coroutine.status(self._co)
if status == "suspended" then if status == "suspended" then
M.current = self local ok, res = coroutine.resume(self._co)
local ok, res = coroutine.resume(self.co)
M.current = nil
if not ok then if not ok then
if self.opts.on_error then error(res)
self.opts.on_error(tostring(res))
end
elseif res then elseif res then
if self.opts.on_yield then self:_emit("yield", res)
self.opts.on_yield(res)
end end
end end
end return self:running()
if self:running() then
return true
end
if self.opts.on_done then
self.opts.on_done()
end
end end
function M.step() function M.step()
@ -107,32 +151,24 @@ function M.add(async)
return async return async
end end
---@param fn async fun() function M.running()
---@param opts? AsyncOpts local co = coroutine.running()
function M.run(fn, opts) if co then
return M.add(Async.new(fn, opts)) local async = M._threads[co]
end assert(async, "In coroutine without async context")
return async
---@generic T: async fun()
---@param fn T
---@param opts? AsyncOpts
---@return T
function M.wrap(fn, opts)
return function(...)
local args = { ... }
---@async
local wrapped = function()
return fn(unpack(args))
end
return M.run(wrapped, opts)
end end
end end
---@async ---@async
---@param ms number ---@param ms number
function M.sleep(ms) function M.sleep(ms)
assert(M.current, "Not in an async context") local async = M.running()
M.current:sleep(ms) assert(async, "Not in an async context")
async:sleep(ms)
end end
M.Async = Async
M.new = Async.new
return M return M

View File

@ -236,7 +236,7 @@ function M.clear(plugins)
if plugin._.tasks then if plugin._.tasks then
---@param task LazyTask ---@param task LazyTask
plugin._.tasks = vim.tbl_filter(function(task) plugin._.tasks = vim.tbl_filter(function(task)
return task:is_running() or task:has_errors() return task:running() or task:has_errors()
end, plugin._.tasks) end, plugin._.tasks)
end end
end end

View File

@ -1,67 +1,133 @@
local Async = require("lazy.async")
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local M = {}
---@type table<uv_process_t, true>
M.running = {}
M.signals = {
"HUP",
"INT",
"QUIT",
"ILL",
"TRAP",
"ABRT",
"BUS",
"FPE",
"KILL",
"USR1",
"SEGV",
"USR2",
"PIPE",
"ALRM",
"TERM",
"CHLD",
"CONT",
"STOP",
"TSTP",
"TTIN",
"TTOU",
"URG",
"XCPU",
"XFSZ",
"VTALRM",
"PROF",
"WINCH",
"IO",
"PWR",
"EMT",
"SYS",
"INFO",
}
---@diagnostic disable-next-line: no-unknown ---@diagnostic disable-next-line: no-unknown
local uv = vim.uv local uv = vim.uv
---@class ProcessOpts ---@class ProcessOpts
---@field args string[] ---@field args string[]
---@field cwd? string ---@field cwd? string
---@field on_line? fun(string) ---@field on_line? fun(line:string)
---@field on_exit? fun(ok:boolean, output:string) ---@field on_exit? fun(ok:boolean, output:string)
---@field on_data? fun(string) ---@field on_data? fun(data:string, is_stderr?:boolean)
---@field timeout? number ---@field timeout? number
---@field env? table<string,string> ---@field env? table<string,string>
---@param opts? ProcessOpts local M = {}
---@param cmd string
function M.spawn(cmd, opts)
opts = opts or {}
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
---@type table<uv_process_t, LazyProcess>
M.running = setmetatable({}, { __mode = "k" })
---@class LazyProcess: Async
---@field handle? uv_process_t
---@field pid? number
---@field cmd string
---@field opts ProcessOpts
---@field timeout? uv_timer_t
---@field timedout? boolean
---@field data string
---@field check? uv_check_t
---@field code? number
---@field signal? number
local Process = setmetatable({}, { __index = Async.Async })
---@param cmd string|string[]
---@param opts? ProcessOpts
function Process.new(cmd, opts)
local self = setmetatable({}, { __index = Process })
---@async
Process.init(self, function()
self:_run()
end)
opts = opts or {}
opts.args = opts.args or {}
if type(cmd) == "table" then
self.cmd = table.remove(cmd, 1)
vim.list_extend(opts.args, cmd)
else
self.cmd = cmd
end
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
-- make sure the cwd is valid
if not opts.cwd and type(uv.cwd()) ~= "string" then
opts.cwd = uv.os_homedir()
end
opts.on_line = opts.on_line and vim.schedule_wrap(opts.on_line) or nil
opts.on_data = opts.on_data and vim.schedule_wrap(opts.on_data) or nil
self.data = ""
self.opts = opts
self.code = 1
self.signal = 0
return self
end
---@async
function Process:_run()
self:guard()
local stdout = assert(uv.new_pipe())
local stderr = assert(uv.new_pipe())
self.handle = uv.spawn(self.cmd, {
stdio = { nil, stdout, stderr },
args = self.opts.args,
cwd = self.opts.cwd,
env = self:env(),
}, function(code, signal)
self.code = code
self.signal = signal
if self.timeout then
self.timeout:stop()
end
self.handle:close()
stdout:close()
stderr:close()
self:resume()
end)
if self.handle then
M.running[self.handle] = self
stdout:read_start(function(err, data)
self:on_data(err, data)
end)
stderr:read_start(function(err, data)
self:on_data(err, data, true)
end)
self:suspend()
while not (self.handle:is_closing() and stdout:is_closing() and stderr:is_closing()) do
coroutine.yield()
end
else
self.data = "Failed to spawn process " .. self.cmd .. " " .. vim.inspect(self.opts)
end
self:on_exit()
end
function Process:on_exit()
self.data = self.data:gsub("[^\r\n]+\r", "")
if self.timedout then
self.data = self.data .. "\n" .. "Process was killed because it reached the timeout"
elseif self.signal ~= 0 then
self.data = self.data .. "\n" .. "Process was killed with SIG" .. M.signals[self.signal]:upper()
end
if self.opts.on_exit then
self.opts.on_exit(self.code == 0 and self.signal == 0, self.data)
end
end
function Process:guard()
if self.opts.timeout then
self.timeout = assert(uv.new_timer())
self.timeout:start(self.opts.timeout, 0, function()
self.timedout = true
self:kill()
end)
end
end
function Process:env()
---@type table<string, string> ---@type table<string, string>
local env = vim.tbl_extend("force", { local env = vim.tbl_extend("force", {
GIT_SSH_COMMAND = "ssh -oBatchMode=yes", GIT_SSH_COMMAND = "ssh -oBatchMode=yes",
}, uv.os_environ(), opts.env or {}) }, uv.os_environ(), self.opts.env or {})
env.GIT_DIR = nil env.GIT_DIR = nil
env.GIT_WORK_TREE = nil env.GIT_WORK_TREE = nil
env.GIT_TERMINAL_PROMPT = "0" env.GIT_TERMINAL_PROMPT = "0"
@ -72,147 +138,105 @@ function M.spawn(cmd, opts)
for k, v in pairs(env) do for k, v in pairs(env) do
env_flat[#env_flat + 1] = k .. "=" .. v env_flat[#env_flat + 1] = k .. "=" .. v
end end
return env_flat
local stdout = assert(uv.new_pipe())
local stderr = assert(uv.new_pipe())
local output = ""
---@type uv_process_t?
local handle = nil
---@type uv_timer_t
local timeout
local killed = false
if opts.timeout then
timeout = assert(uv.new_timer())
timeout:start(opts.timeout, 0, function()
if M.kill(handle) then
killed = true
end
end)
end end
-- make sure the cwd is valid ---@param signals uv.aliases.signals|uv.aliases.signals[]|nil
if not opts.cwd and type(uv.cwd()) ~= "string" then function Process:kill(signals)
opts.cwd = uv.os_homedir() if not self.handle or self.handle:is_closing() then
end
handle = uv.spawn(cmd, {
stdio = { nil, stdout, stderr },
args = opts.args,
cwd = opts.cwd,
env = env_flat,
}, function(exit_code, signal)
---@cast handle uv_process_t
M.running[handle] = nil
if timeout then
timeout:stop()
timeout:close()
end
handle:close()
stdout:close()
stderr:close()
local check = assert(uv.new_check())
check:start(function()
if not stdout:is_closing() or not stderr:is_closing() then
return return
end end
check:stop() signals = signals or { "sigterm", "sigkill" }
if opts.on_exit then signals = type(signals) == "table" and signals or { signals }
output = output:gsub("[^\r\n]+\r", "") ---@cast signals uv.aliases.signals[]
if killed then local timer = assert(uv.new_timer())
output = output .. "\n" .. "Process was killed because it reached the timeout" timer:start(0, 1000, function()
elseif signal ~= 0 then if self.handle and not self.handle:is_closing() and #signals > 0 then
output = output .. "\n" .. "Process was killed with SIG" .. M.signals[signal] self.handle:kill(table.remove(signals, 1))
end else
timer:stop()
vim.schedule(function()
opts.on_exit(exit_code == 0 and signal == 0, output)
end)
end end
end) end)
end)
if not handle then
if opts.on_exit then
opts.on_exit(false, "Failed to spawn process " .. cmd .. " " .. vim.inspect(opts))
end end
return
end
M.running[handle] = true
---@param err? string
---@param data? string ---@param data? string
local function on_output(err, data) ---@param is_stderr? boolean
function Process:on_data(err, data, is_stderr)
assert(not err, err) assert(not err, err)
if not data then
if data then return
if opts.on_data then
vim.schedule(function()
opts.on_data(data)
end)
end end
output = output .. data:gsub("\r\n", "\n")
local lines = vim.split(vim.trim(output:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
if opts.on_line then if self.opts.on_data then
vim.schedule(function() self.opts.on_data(data, is_stderr)
opts.on_line(lines[#lines])
end)
end end
self.data = self.data .. data:gsub("\r\n", "\n")
local lines = vim.split(vim.trim(self.data:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
if self.opts.on_line then
self.opts.on_line(lines[#lines])
end end
end end
uv.read_start(stdout, on_output) M.signals = {
uv.read_start(stderr, on_output) "hup",
"int",
"quit",
"ill",
"trap",
"abrt",
"bus",
"fpe",
"kill",
"usr1",
"segv",
"usr2",
"pipe",
"alrm",
"term",
"chld",
"cont",
"stop",
"tstp",
"ttin",
"ttou",
"urg",
"xcpu",
"xfsz",
"vtalrm",
"prof",
"winch",
"io",
"pwr",
"emt",
"sys",
"info",
}
return handle ---@param cmd string|string[]
end ---@param opts? ProcessOpts
function M.spawn(cmd, opts)
function M.kill(handle) return Process.new(cmd, opts)
if handle and not handle:is_closing() then
M.running[handle] = nil
uv.process_kill(handle, "sigint")
return true
end
end end
function M.abort() function M.abort()
for handle in pairs(M.running) do for _, proc in pairs(M.running) do
M.kill(handle) proc:kill()
end end
end end
---@param cmd string[] ---@async
---@param opts? {cwd:string, env:table} ---@param cmd string|string[]
---@param opts? ProcessOpts
function M.exec(cmd, opts) function M.exec(cmd, opts)
opts = opts or {} opts = opts or {}
---@type string[] local proc = M.spawn(cmd, opts)
local lines proc:wait()
local job = vim.fn.jobstart(cmd, { if proc.code ~= 0 then
cwd = opts.cwd, error("Process failed with code " .. proc.code)
pty = false,
env = opts.env,
stdout_buffered = true,
on_stdout = function(_, _lines)
lines = _lines
end,
})
if job <= 0 then
error("Failed to start job: " .. vim.inspect(cmd))
end end
return vim.split(proc.data, "\n")
local Async = require("lazy.async")
local async = Async.current
if async then
while vim.fn.jobwait({ job }, 0)[1] == -1 do
async:sleep(10)
end
else
vim.fn.jobwait({ job })
end
return lines
end end
return M return M

View File

@ -16,7 +16,6 @@ local Task = require("lazy.manage.task")
---@class Runner ---@class Runner
---@field _plugins table<string,LazyPlugin> ---@field _plugins table<string,LazyPlugin>
---@field _pipeline PipelineStep[] ---@field _pipeline PipelineStep[]
---@field _on_done fun()[]
---@field _opts RunnerOpts ---@field _opts RunnerOpts
---@field _running? Async ---@field _running? Async
local Runner = {} local Runner = {}
@ -38,7 +37,6 @@ function Runner.new(opts)
for _, plugin in ipairs(pp) do for _, plugin in ipairs(pp) do
self._plugins[plugin.name] = plugin self._plugins[plugin.name] = plugin
end end
self._on_done = {}
---@param step string|(TaskOptions|{[1]:string}) ---@param step string|(TaskOptions|{[1]:string})
self._pipeline = vim.tbl_map(function(step) self._pipeline = vim.tbl_map(function(step)
@ -61,15 +59,9 @@ end
function Runner:start() function Runner:start()
---@async ---@async
self._running = Async.run(function() self._running = Async.new(function()
self:_start() self:_start()
end, { end)
on_done = function()
for _, cb in ipairs(self._on_done) do
cb()
end
end,
})
end end
---@async ---@async
@ -97,7 +89,7 @@ function Runner:_start()
for _, name in ipairs(names) do for _, name in ipairs(names) do
state[name] = state[name] or { step = 0 } state[name] = state[name] or { step = 0 }
local s = state[name] local s = state[name]
local is_running = s.task and s.task:is_running() local is_running = s.task and s.task:running()
local step = self._pipeline[s.step] local step = self._pipeline[s.step]
if is_running then if is_running then
@ -185,14 +177,10 @@ function Runner:wait(cb)
end end
return self return self
end end
if cb then if cb then
table.insert(self._on_done, cb) self._running:on("done", cb)
else else
-- sync wait self._running:wait()
while self:is_running() do
vim.wait(10)
end
end end
return self return self
end end

View File

@ -15,16 +15,15 @@ local colors = Config.options.headless.colors
---@field msg string ---@field msg string
---@field level? number ---@field level? number
---@class LazyTask ---@class LazyTask: Async
---@field plugin LazyPlugin ---@field plugin LazyPlugin
---@field name string ---@field name string
---@field private _log LazyMsg[] ---@field private _log LazyMsg[]
---@field private _started? number ---@field private _started number
---@field private _ended? number ---@field private _ended? number
---@field private _opts TaskOptions ---@field private _opts TaskOptions
---@field private _running Async
---@field private _level number ---@field private _level number
local Task = {} local Task = setmetatable({}, { __index = Async.Async })
---@class TaskOptions: {[string]:any} ---@class TaskOptions: {[string]:any}
---@field on_done? fun(task:LazyTask) ---@field on_done? fun(task:LazyTask)
@ -35,17 +34,21 @@ local Task = {}
---@param task LazyTaskFn ---@param task LazyTaskFn
function Task.new(plugin, name, task, opts) function Task.new(plugin, name, task, opts)
local self = setmetatable({}, { __index = Task }) local self = setmetatable({}, { __index = Task })
---@async
Task.init(self, function()
self:_run(task)
end)
self:set_level()
self._opts = opts or {} self._opts = opts or {}
self._log = {} self._log = {}
self:set_level()
self.plugin = plugin self.plugin = plugin
self.name = name self.name = name
self._started = vim.uv.hrtime()
---@param other LazyTask ---@param other LazyTask
plugin._.tasks = vim.tbl_filter(function(other) plugin._.tasks = vim.tbl_filter(function(other)
return other.name ~= name or other:is_running() return other.name ~= name or other:running()
end, plugin._.tasks or {}) end, plugin._.tasks or {})
table.insert(plugin._.tasks, self) table.insert(plugin._.tasks, self)
self:_start(task)
return self return self
end end
@ -75,10 +78,6 @@ function Task:status()
return msg ~= "" and msg or nil return msg ~= "" and msg or nil
end end
function Task:is_running()
return self._ended == nil
end
function Task:has_errors() function Task:has_errors()
return self._level >= vim.log.levels.ERROR return self._level >= vim.log.levels.ERROR
end end
@ -92,31 +91,24 @@ function Task:set_level(level)
self._level = level or vim.log.levels.TRACE self._level = level or vim.log.levels.TRACE
end end
---@private ---@async
---@param task LazyTaskFn ---@param task LazyTaskFn
function Task:_start(task) function Task:_run(task)
assert(not self._started, "task already started")
assert(not self._ended, "task already done")
if Config.headless() and Config.options.headless.task then if Config.headless() and Config.options.headless.task then
self:log("Running task " .. self.name, vim.log.levels.INFO) self:log("Running task " .. self.name, vim.log.levels.INFO)
end end
self._started = vim.uv.hrtime() self
---@async :on("done", function()
self._running = Async.run(function()
task(self, self._opts)
end, {
on_done = function()
self:_done() self:_done()
end, end)
on_error = function(err) :on("error", function(err)
self:error(err) self:error(err)
end, end)
on_yield = function(res) :on("yield", function(msg)
self:log(res) self:log(msg)
end, end)
}) task(self, self._opts)
end end
---@param msg string|string[] ---@param msg string|string[]
@ -163,13 +155,6 @@ end
---@private ---@private
function Task:_done() function Task:_done()
assert(self._started, "task not started")
assert(not self._ended, "task already done")
if self._running and self._running:running() then
return
end
if Config.headless() and Config.options.headless.task then if Config.headless() and Config.options.headless.task then
local ms = math.floor(self:time() + 0.5) local ms = math.floor(self:time() + 0.5)
self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO) self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO)
@ -186,13 +171,7 @@ function Task:_done()
end end
function Task:time() function Task:time()
if not self._started then return ((self._ended or vim.uv.hrtime()) - self._started) / 1e6
return 0
end
if not self._ended then
return (vim.uv.hrtime() - self._started) / 1e6
end
return (self._ended - self._started) / 1e6
end end
---@async ---@async
@ -201,7 +180,6 @@ end
function Task:spawn(cmd, opts) function Task:spawn(cmd, opts)
opts = opts or {} opts = opts or {}
local on_line = opts.on_line local on_line = opts.on_line
local on_exit = opts.on_exit
local headless = Config.headless() and Config.options.headless.process local headless = Config.headless() and Config.options.headless.process
@ -214,35 +192,28 @@ function Task:spawn(cmd, opts)
end end
end end
self._running:suspend()
local running = true
local ret = { ok = true, output = "" }
---@param output string
function opts.on_exit(ok, output)
if not headless then
self:log(vim.trim(output), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
end
ret = { ok = ok, output = output }
running = false
self._running:resume()
end
if headless then if headless then
opts.on_data = function(data) opts.on_data = function(data)
-- prefix with plugin name -- prefix with plugin name
local prefix = self:prefix() io.write(Terminal.prefix(data, self:prefix()))
io.write(Terminal.prefix(data, prefix))
end end
end end
Process.spawn(cmd, opts)
coroutine.yield() local proc = Process.spawn(cmd, opts)
assert(not running, "process still running?") proc:wait()
if on_exit then
pcall(on_exit, ret.ok, ret.output) local ok = proc.code == 0 and proc.signal == 0
if not headless then
local msg = vim.trim(proc.data)
if #msg > 0 then
self:log(vim.trim(proc.data), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
end end
coroutine.yield() end
return ret.ok
if opts.on_exit then
pcall(opts.on_exit, ok, proc.data)
end
return ok
end end
function Task:prefix() function Task:prefix()
@ -253,10 +224,4 @@ function Task:prefix()
or plugin .. " " .. task .. " | " or plugin .. " " .. task .. " | "
end end
function Task:wait()
while self:is_running() do
vim.wait(10)
end
end
return Task return Task

View File

@ -76,6 +76,13 @@ function M.throttle(ms, fn)
local timer = vim.uv.new_timer() local timer = vim.uv.new_timer()
local pending = false local pending = false
---@type Async
local async
local function running()
return async and async:running()
end
return function() return function()
if timer:is_active() then if timer:is_active() then
pending = true pending = true
@ -85,7 +92,10 @@ function M.throttle(ms, fn)
0, 0,
ms, ms,
vim.schedule_wrap(function() vim.schedule_wrap(function()
fn() if running() then
return
end
async = require("lazy.async").new(fn)
if pending then if pending then
pending = false pending = false
else else

View File

@ -51,7 +51,7 @@ function M:update()
if plugin._.tasks then if plugin._.tasks then
for _, task in ipairs(plugin._.tasks) do for _, task in ipairs(plugin._.tasks) do
self.progress.total = self.progress.total + 1 self.progress.total = self.progress.total + 1
if not task:is_running() then if not task:running() then
self.progress.done = self.progress.done + 1 self.progress.done = self.progress.done + 1
end end
end end
@ -356,7 +356,7 @@ end
function M:diagnostics(plugin) function M:diagnostics(plugin)
local skip = false local skip = false
for _, task in ipairs(plugin._.tasks or {}) do for _, task in ipairs(plugin._.tasks or {}) do
if task:is_running() then if task:running() then
self:diagnostic({ self:diagnostic({
severity = vim.diagnostic.severity.WARN, severity = vim.diagnostic.severity.WARN,
message = task.name .. (task:status() and (": " .. task:status()) or ""), message = task.name .. (task:status() and (": " .. task:status()) or ""),

View File

@ -28,7 +28,7 @@ return {
return true return true
end end
return has_task(plugin, function(task) return has_task(plugin, function(task)
return task:is_running() return task:running()
end) end)
end, end,
title = "Working", title = "Working",

View File

@ -0,0 +1,20 @@
---@module 'luassert'
local Async = require("lazy.async")
local Process = require("lazy.manage.process")
describe("process", function()
it("runs sync", function()
local lines = Process.exec({ "echo", "-n", "hello" })
assert.are.same({ "hello" }, lines)
end)
it("runs sync from async context", function()
local lines ---@type string[]
local async = Async.new(function()
lines = Process.exec({ "echo", "-n", "hello" })
end)
async:wait()
assert.are.same({ "hello" }, lines)
end)
end)

View File

@ -21,9 +21,9 @@ describe("task", function()
it("simple function", function() it("simple function", function()
local task = Task.new(plugin, "test", function() end, opts) local task = Task.new(plugin, "test", function() end, opts)
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert(not task:is_running()) assert(not task:running())
assert(task_result.done) assert(task_result.done)
end) end)
@ -31,9 +31,9 @@ describe("task", function()
local task = Task.new(plugin, "test", function() local task = Task.new(plugin, "test", function()
error("test") error("test")
end, opts) end, opts)
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert(not task:is_running()) assert(not task:running())
assert(task_result.done) assert(task_result.done)
assert(task_result.error) assert(task_result.error)
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test")) assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test"))
@ -46,12 +46,12 @@ describe("task", function()
coroutine.yield() coroutine.yield()
running = false running = false
end, opts) end, opts)
assert(task:is_running()) assert(task:running())
assert(running) assert(running)
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert(not running) assert(not running)
assert(not task:is_running()) assert(not task:running())
assert(task_result.done) assert(task_result.done)
assert(not task:has_errors()) assert(not task:has_errors())
end) end)
@ -60,19 +60,19 @@ describe("task", function()
local task = Task.new(plugin, "spawn_errors", function(task) local task = Task.new(plugin, "spawn_errors", function(task)
task:spawn("foobar") task:spawn("foobar")
end, opts) end, opts)
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert(not task:is_running()) assert(not task:running())
assert(task_result.done) assert(task_result.done)
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task.output) assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task:output())
end) end)
it("spawn", function() it("spawn", function()
local task = Task.new(plugin, "test", function(task) local task = Task.new(plugin, "test", function(task)
task:spawn("echo", { args = { "foo" } }) task:spawn("echo", { args = { "foo" } })
end, opts) end, opts)
assert(task:is_running()) assert(task:running())
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert.same(task:output(), "foo") assert.same(task:output(), "foo")
assert(task_result.done) assert(task_result.done)
@ -84,8 +84,8 @@ describe("task", function()
task:spawn("echo", { args = { "foo" } }) task:spawn("echo", { args = { "foo" } })
task:spawn("echo", { args = { "bar" } }) task:spawn("echo", { args = { "bar" } })
end, opts) end, opts)
assert(task:is_running()) assert(task:running())
assert(task:is_running()) assert(task:running())
task:wait() task:wait()
assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output()) assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output())
assert(task_result.done) assert(task_result.done)

View File

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
nvim -l tests/busted.lua tests -o utfTerminal nvim -l tests/busted.lua tests -o utfTerminal "$@"

View File

@ -8,42 +8,14 @@ any = true
[jit] [jit]
any = true any = true
[[describe.args]] [assert]
type = "string"
[[describe.args]]
type = "function"
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[before_each.args]]
type = "function"
[[after_each.args]]
type = "function"
[assert.is_not]
any = true any = true
[[assert.equals.args]] [describe]
type = "any" any = true
[[assert.equals.args]]
type = "any"
[[assert.equals.args]]
type = "any"
required = false
[[assert.same.args]] [it]
type = "any" any = true
[[assert.same.args]]
type = "any"
[[assert.truthy.args]] [before_each.args]
type = "any" any = true
[[assert.spy.args]]
type = "any"
[[assert.stub.args]]
type = "any"