From fa78d7cf0445083243cdd8feee4923f99d984e30 Mon Sep 17 00:00:00 2001 From: William Boman Date: Wed, 13 Jul 2022 16:21:16 +0200 Subject: tests: add some InstallHandle tests (#67) --- lua/mason-core/installer/context.lua | 4 +- lua/mason-core/installer/handle.lua | 68 +++++++++--------- lua/mason/ui/instance.lua | 8 +-- tests/mason-core/installer/handle_spec.lua | 100 ++++++++++++++++++++++++++ tests/mason-core/installer/installer_spec.lua | 100 ++++++++++++++++++++++++++ tests/mason-core/installer_spec.lua | 100 -------------------------- 6 files changed, 240 insertions(+), 140 deletions(-) create mode 100644 tests/mason-core/installer/handle_spec.lua create mode 100644 tests/mason-core/installer/installer_spec.lua delete mode 100644 tests/mason-core/installer_spec.lua diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua index 9dfbb7f1..3e701d8e 100644 --- a/lua/mason-core/installer/context.lua +++ b/lua/mason-core/installer/context.lua @@ -26,14 +26,14 @@ function ContextualSpawn.__index(self, cmd) local captured_handle args.on_spawn = function(handle, stdio, pid, ...) captured_handle = handle - self.handle:push_spawninfo(handle, pid, cmd, spawn._flatten_cmd_args(args)) + self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args)) if on_spawn then on_spawn(handle, stdio, pid, ...) end end local function pop_spawn_stack() if captured_handle then - self.handle:pop_spawninfo(captured_handle) + self.handle:deregister_spawn_handle(captured_handle) end end -- We get_or_throw() here for convenience reasons. diff --git a/lua/mason-core/installer/handle.lua b/lua/mason-core/installer/handle.lua index 459e3704..f2e7aa1d 100644 --- a/lua/mason-core/installer/handle.lua +++ b/lua/mason-core/installer/handle.lua @@ -15,20 +15,20 @@ local uv = vim.loop --- | '"ACTIVE"' --- | '"CLOSED"' ----@class InstallHandleSpawnInfo ----@field handle luv_handle +---@class InstallHandleSpawnHandle +---@field uv_handle luv_handle ---@field pid integer ---@field cmd string ---@field args string[] -local InstallHandleSpawnInfo = {} -InstallHandleSpawnInfo.__index = InstallHandleSpawnInfo +local InstallHandleSpawnHandle = {} +InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle ----@param fields InstallHandleSpawnInfo -function InstallHandleSpawnInfo.new(fields) - return setmetatable(fields, InstallHandleSpawnInfo) +---@param fields InstallHandleSpawnHandle +function InstallHandleSpawnHandle.new(fields) + return setmetatable(fields, InstallHandleSpawnHandle) end -function InstallHandleSpawnInfo:__tostring() +function InstallHandleSpawnHandle:__tostring() return ("%s %s"):format(self.cmd, table.concat(self.args, " ")) end @@ -37,7 +37,7 @@ end ---@field state InstallHandleState ---@field stdio { buffers: { stdout: string[], stderr: string[] }, sink: StdioSink } ---@field is_terminated boolean ----@field private spawninfo_stack InstallHandleSpawnInfo[] +---@field private spawn_handles InstallHandleSpawnHandle[] local InstallHandle = setmetatable({}, { __index = EventEmitter }) local InstallHandleMt = { __index = InstallHandle } @@ -64,7 +64,7 @@ function InstallHandle.new(pkg) local self = EventEmitter.init(setmetatable({}, InstallHandleMt)) self.state = "IDLE" self.package = pkg - self.spawninfo_stack = {} + self.spawn_handles = {} self.stdio = new_sink(self) self.is_terminated = false return self @@ -74,34 +74,34 @@ end ---@param pid integer ---@param cmd string ---@param args string[] -function InstallHandle:push_spawninfo(luv_handle, pid, cmd, args) - local spawninfo = InstallHandleSpawnInfo.new { - handle = luv_handle, +function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args) + local spawn_handles = InstallHandleSpawnHandle.new { + uv_handle = luv_handle, pid = pid, cmd = cmd, args = args, } - log.fmt_trace("Pushing spawninfo stack for %s: %s (pid: %s)", self, spawninfo, pid) - self.spawninfo_stack[#self.spawninfo_stack + 1] = spawninfo - self:emit "spawninfo:change" + log.fmt_trace("Pushing spawn_handles stack for %s: %s (pid: %s)", self, spawn_handles, pid) + self.spawn_handles[#self.spawn_handles + 1] = spawn_handles + self:emit "spawn_handles:change" end ---@param luv_handle luv_handle -function InstallHandle:pop_spawninfo(luv_handle) - for i = #self.spawninfo_stack, 1, -1 do - if self.spawninfo_stack[i].handle == luv_handle then - log.fmt_trace("Popping spawninfo stack for %s: %s", self, self.spawninfo_stack[i]) - table.remove(self.spawninfo_stack, i) - self:emit "spawninfo:change" +function InstallHandle:deregister_spawn_handle(luv_handle) + for i = #self.spawn_handles, 1, -1 do + if self.spawn_handles[i].uv_handle == luv_handle then + log.fmt_trace("Popping spawn_handles stack for %s: %s", self, self.spawn_handles[i]) + table.remove(self.spawn_handles, i) + self:emit "spawn_handles:change" return true end end return false end ----@return Optional @Optional -function InstallHandle:peek_spawninfo_stack() - return Optional.of_nilable(self.spawninfo_stack[#self.spawninfo_stack]) +---@return Optional @Optional +function InstallHandle:peek_spawn_handle() + return Optional.of_nilable(self.spawn_handles[#self.spawn_handles]) end function InstallHandle:is_idle() @@ -132,8 +132,8 @@ end function InstallHandle:kill(signal) assert(not self:is_closed(), "Cannot kill closed handle.") log.fmt_trace("Sending signal %s to luv handles in %s", signal, self) - for _, spawninfo in pairs(self.spawninfo_stack) do - process.kill(spawninfo.handle, signal) + for _, spawn_handles in pairs(self.spawn_handles) do + process.kill(spawn_handles.uv_handle, signal) end self:emit("kill", signal) end @@ -157,8 +157,8 @@ function InstallHandle:terminate() log.fmt_trace("Terminating %s", self) -- https://github.com/libuv/libuv/issues/1133 if platform.is.win then - for _, spawninfo in ipairs(self.spawninfo_stack) do - win_taskkill(spawninfo.pid) + for _, spawn_handles in ipairs(self.spawn_handles) do + win_taskkill(spawn_handles.pid) end else self:kill(15) -- SIGTERM @@ -167,8 +167,8 @@ function InstallHandle:terminate() self:emit "terminate" local check = uv.new_check() check:start(function() - for _, spawninfo in ipairs(self.spawninfo_stack) do - local luv_handle = spawninfo.handle + for _, spawn_handles in ipairs(self.spawn_handles) do + local luv_handle = spawn_handles.uv_handle local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) if ok and not is_closing then return @@ -194,14 +194,14 @@ end function InstallHandle:close() log.fmt_trace("Closing %s", self) assert(not self:is_closed(), "Handle is already closed.") - for _, spawninfo in ipairs(self.spawninfo_stack) do - local luv_handle = spawninfo.handle + for _, spawn_handles in ipairs(self.spawn_handles) do + local luv_handle = spawn_handles.uv_handle local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) if ok then assert(is_closing, "There are open libuv handles.") end end - self.spawninfo_stack = {} + self.spawn_handles = {} self:set_state "CLOSED" self:emit "closed" self:clear_event_handlers() diff --git a/lua/mason/ui/instance.lua b/lua/mason/ui/instance.lua index beb73705..7a9b98fa 100644 --- a/lua/mason/ui/instance.lua +++ b/lua/mason/ui/instance.lua @@ -187,10 +187,10 @@ local function setup_handle(handle) end end - local function handle_spawninfo_change() + local function handle_spawnhandle_change() mutate_state(function(state) state.packages.states[handle.package.name].latest_spawn = - handle:peek_spawninfo_stack():map(tostring):or_else(nil) + handle:peek_spawn_handle():map(tostring):or_else(nil) end) end @@ -229,14 +229,14 @@ local function setup_handle(handle) handle:on("terminate", handle_terminate) handle:on("state:change", handle_state_change) - handle:on("spawninfo:change", handle_spawninfo_change) + handle:on("spawn_handles:change", handle_spawnhandle_change) handle:on("stdout", handle_output) handle:on("stderr", handle_output) -- hydrate initial state handle_state_change(handle.state) handle_terminate() - handle_spawninfo_change() + handle_spawnhandle_change() mutate_state(function(state) state.packages.states[handle.package.name].tailed_output = {} end) diff --git a/tests/mason-core/installer/handle_spec.lua b/tests/mason-core/installer/handle_spec.lua new file mode 100644 index 00000000..02fcc358 --- /dev/null +++ b/tests/mason-core/installer/handle_spec.lua @@ -0,0 +1,100 @@ +local a = require "mason-core.async" +local mock = require "luassert.mock" +local stub = require "luassert.stub" +local spy = require "luassert.spy" +local InstallHandle = require "mason-core.installer.handle" + +describe("installer handle", function() + it("should register spawn handle", function() + local handle = InstallHandle.new(mock.new {}) + local spawn_handle_change_handler = spy.new() + handle:once("spawn_handles:change", spawn_handle_change_handler) + local luv_handle = mock.new {} + handle:register_spawn_handle(luv_handle, 1337, "tar", { "-xvf", "file" }) + assert.same({ + uv_handle = luv_handle, + pid = 1337, + cmd = "tar", + args = { "-xvf", "file" }, + }, handle:peek_spawn_handle():get()) + assert.spy(spawn_handle_change_handler).was_called(1) + end) + + it("should deregister spawn handle", function() + local handle = InstallHandle.new(mock.new {}) + local spawn_handle_change_handler = spy.new() + handle:once("spawn_handles:change", spawn_handle_change_handler) + local luv_handle1 = mock.new {} + local luv_handle2 = mock.new {} + handle:register_spawn_handle(luv_handle1, 42, "curl", { "someurl" }) + handle:register_spawn_handle(luv_handle2, 1337, "tar", { "-xvf", "file" }) + assert.is_true(handle:deregister_spawn_handle(luv_handle1)) + assert.equals(1, #handle.spawn_handles) + assert.same({ + uv_handle = luv_handle2, + pid = 1337, + cmd = "tar", + args = { "-xvf", "file" }, + }, handle:peek_spawn_handle():get()) + assert.spy(spawn_handle_change_handler).was_called(3) + end) + + it("should change state", function() + local handle = InstallHandle.new(mock.new {}) + local state_change_handler = spy.new() + handle:once("state:change", state_change_handler) + handle:set_state "QUEUED" + assert.equals("QUEUED", handle.state) + assert.spy(state_change_handler).was_called(1) + assert.spy(state_change_handler).was_called_with("QUEUED", "IDLE") + end) + + it("should send signals to registered handles", function() + local process = require "mason-core.process" + stub(process, "kill") + local uv_handle = {} + local handle = InstallHandle.new(mock.new {}) + local kill_handler = spy.new() + handle:once("kill", kill_handler) + handle.state = "ACTIVE" + handle.spawn_handles = { { uv_handle = uv_handle } } + + handle:kill(9) + assert.spy(process.kill).was_called(1) + assert.spy(process.kill).was_called_with(uv_handle, 9) + assert.spy(kill_handler).was_called(1) + assert.spy(kill_handler).was_called_with(9) + end) + + it( + "should terminate handle", + async_test(function() + local process = require "mason-core.process" + stub(process, "kill") + local uv_handle1 = {} + local uv_handle2 = {} + local handle = InstallHandle.new(mock.new {}) + local kill_handler = spy.new() + local terminate_handler = spy.new() + local closed_handler = spy.new() + handle:once("kill", kill_handler) + handle:once("terminate", terminate_handler) + handle:once("closed", closed_handler) + handle.state = "ACTIVE" + handle.spawn_handles = { { uv_handle = uv_handle2 }, { uv_handle = uv_handle2 } } + + handle:terminate() + assert.spy(process.kill).was_called(2) + assert.spy(process.kill).was_called_with(uv_handle1, 15) + assert.spy(process.kill).was_called_with(uv_handle2, 15) + assert.spy(kill_handler).was_called(1) + assert.spy(kill_handler).was_called_with(15) + assert.spy(terminate_handler).was_called(1) + assert.is_true(handle.is_terminated) + assert.wait_for(function() + assert.is_true(handle:is_closed()) + assert.spy(closed_handler).was_called(1) + end) + end) + ) +end) diff --git a/tests/mason-core/installer/installer_spec.lua b/tests/mason-core/installer/installer_spec.lua new file mode 100644 index 00000000..8dc9b516 --- /dev/null +++ b/tests/mason-core/installer/installer_spec.lua @@ -0,0 +1,100 @@ +local spy = require "luassert.spy" +local match = require "luassert.match" +local fs = require "mason-core.fs" +local a = require "mason-core.async" +local path = require "mason-core.path" +local installer = require "mason-core.installer" +local InstallContext = require "mason-core.installer.context" + +local function timestamp() + local seconds, microseconds = vim.loop.gettimeofday() + return (seconds * 1000) + math.floor(microseconds / 1000) +end + +describe("installer", function() + before_each(function() + package.loaded["dummy_package"] = nil + end) + + it( + "should call installer", + async_test(function() + spy.on(fs.async, "mkdirp") + spy.on(fs.async, "rename") + + local handle = InstallHandleGenerator "dummy" + spy.on(handle.package.spec, "install") + local result = installer.execute(handle, {}) + + assert.is_nil(result:err_or_nil()) + assert.spy(handle.package.spec.install).was_called(1) + assert.spy(handle.package.spec.install).was_called_with(match.instanceof(InstallContext)) + assert.spy(fs.async.mkdirp).was_called_with(path.package_build_prefix "dummy") + assert.spy(fs.async.rename).was_called_with(path.package_build_prefix "dummy", path.package_prefix "dummy") + end) + ) + + it( + "should return failure if installer errors", + async_test(function() + spy.on(fs.async, "rmrf") + spy.on(fs.async, "rename") + local installer_fn = spy.new(function() + error("something went wrong. don't try again.", 0) + end) + local handler = InstallHandleGenerator "dummy" + handler.package.spec.install = installer_fn + local result = installer.execute(handler, {}) + assert.spy(installer_fn).was_called(1) + assert.is_true(result:is_failure()) + assert.is_true(match.has_match "^.*: something went wrong. don't try again.$"(result:err_or_nil())) + assert.spy(fs.async.rmrf).was_called_with(path.package_build_prefix "dummy") + assert.spy(fs.async.rename).was_not_called() + end) + ) + + it( + "should write receipt", + async_test(function() + spy.on(fs.async, "write_file") + local handle = InstallHandleGenerator "dummy" + installer.execute(handle, {}) + assert.spy(fs.async.write_file).was_called(1) + assert + .spy(fs.async.write_file) + .was_called_with(("%s/mason-receipt.json"):format(handle.package:get_install_path()), match.is_string()) + end) + ) + + it( + "should run async functions concurrently", + async_test(function() + spy.on(fs.async, "write_file") + local capture = spy.new() + local start = timestamp() + local handle = InstallHandleGenerator "dummy" + handle.package.spec.install = function(ctx) + capture(installer.run_concurrently { + function() + a.sleep(100) + return installer.context() + end, + function() + a.sleep(100) + return "two" + end, + function() + a.sleep(100) + return "three" + end, + }) + ctx.receipt:with_primary_source { type = "dummy" } + end + installer.execute(handle, {}) + local stop = timestamp() + local grace_ms = 25 + assert.is_true((stop - start) >= (100 - grace_ms)) + assert.spy(capture).was_called_with(match.instanceof(InstallContext), "two", "three") + end) + ) +end) diff --git a/tests/mason-core/installer_spec.lua b/tests/mason-core/installer_spec.lua deleted file mode 100644 index 8dc9b516..00000000 --- a/tests/mason-core/installer_spec.lua +++ /dev/null @@ -1,100 +0,0 @@ -local spy = require "luassert.spy" -local match = require "luassert.match" -local fs = require "mason-core.fs" -local a = require "mason-core.async" -local path = require "mason-core.path" -local installer = require "mason-core.installer" -local InstallContext = require "mason-core.installer.context" - -local function timestamp() - local seconds, microseconds = vim.loop.gettimeofday() - return (seconds * 1000) + math.floor(microseconds / 1000) -end - -describe("installer", function() - before_each(function() - package.loaded["dummy_package"] = nil - end) - - it( - "should call installer", - async_test(function() - spy.on(fs.async, "mkdirp") - spy.on(fs.async, "rename") - - local handle = InstallHandleGenerator "dummy" - spy.on(handle.package.spec, "install") - local result = installer.execute(handle, {}) - - assert.is_nil(result:err_or_nil()) - assert.spy(handle.package.spec.install).was_called(1) - assert.spy(handle.package.spec.install).was_called_with(match.instanceof(InstallContext)) - assert.spy(fs.async.mkdirp).was_called_with(path.package_build_prefix "dummy") - assert.spy(fs.async.rename).was_called_with(path.package_build_prefix "dummy", path.package_prefix "dummy") - end) - ) - - it( - "should return failure if installer errors", - async_test(function() - spy.on(fs.async, "rmrf") - spy.on(fs.async, "rename") - local installer_fn = spy.new(function() - error("something went wrong. don't try again.", 0) - end) - local handler = InstallHandleGenerator "dummy" - handler.package.spec.install = installer_fn - local result = installer.execute(handler, {}) - assert.spy(installer_fn).was_called(1) - assert.is_true(result:is_failure()) - assert.is_true(match.has_match "^.*: something went wrong. don't try again.$"(result:err_or_nil())) - assert.spy(fs.async.rmrf).was_called_with(path.package_build_prefix "dummy") - assert.spy(fs.async.rename).was_not_called() - end) - ) - - it( - "should write receipt", - async_test(function() - spy.on(fs.async, "write_file") - local handle = InstallHandleGenerator "dummy" - installer.execute(handle, {}) - assert.spy(fs.async.write_file).was_called(1) - assert - .spy(fs.async.write_file) - .was_called_with(("%s/mason-receipt.json"):format(handle.package:get_install_path()), match.is_string()) - end) - ) - - it( - "should run async functions concurrently", - async_test(function() - spy.on(fs.async, "write_file") - local capture = spy.new() - local start = timestamp() - local handle = InstallHandleGenerator "dummy" - handle.package.spec.install = function(ctx) - capture(installer.run_concurrently { - function() - a.sleep(100) - return installer.context() - end, - function() - a.sleep(100) - return "two" - end, - function() - a.sleep(100) - return "three" - end, - }) - ctx.receipt:with_primary_source { type = "dummy" } - end - installer.execute(handle, {}) - local stop = timestamp() - local grace_ms = 25 - assert.is_true((stop - start) >= (100 - grace_ms)) - assert.spy(capture).was_called_with(match.instanceof(InstallContext), "two", "three") - end) - ) -end) -- cgit v1.2.3-70-g09d2