From e7c6500a613a58b5c57ba3be596f20dbaa1ba83b Mon Sep 17 00:00:00 2001 From: Yaro Date: Mon, 25 Nov 2024 12:05:22 +0000 Subject: [PATCH 1/2] fix(output_panel): recreate window and term channel if panel closed manually --- lua/neotest/consumers/output_panel/init.lua | 13 +- lua/neotest/lib/window.lua | 12 +- tests/unit/consumers/output_panel_spec.lua | 182 ++++++++++++++++++++ 3 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/unit/consumers/output_panel_spec.lua diff --git a/lua/neotest/consumers/output_panel/init.lua b/lua/neotest/consumers/output_panel/init.lua index a65949c2..32b05e66 100644 --- a/lua/neotest/consumers/output_panel/init.lua +++ b/lua/neotest/consumers/output_panel/init.lua @@ -25,7 +25,12 @@ local init = function(client) if partial then return end - if not chan then + + local channel_is_valid = function(chan_id) + return chan_id and pcall(vim.api.nvim_chan_send, chan_id, "\n") + end + + if not channel_is_valid(chan) then chan = lib.ui.open_term(panel.win:buffer()) -- neovim sometimes adds random blank lines when creating a terminal buffer nio.api.nvim_buf_set_option(panel.win:buffer(), "modifiable", true) @@ -50,7 +55,11 @@ local init = function(client) for file, _ in pairs(files_to_read) do local output = lib.files.read(file) local dos_newlines = string.find(output, "\r\n") ~= nil - nio.api.nvim_chan_send(chan, dos_newlines and output or output:gsub("\n", "\r\n")) + if not pcall(nio.api.nvim_chan_send, chan, dos_newlines and output or output:gsub("\n", "\r\n")) then + lib.notify(("Error sending output to term channel: %s"):format(chan), vim.log.levels.ERROR) + chan = nil + break + end end end end diff --git a/lua/neotest/lib/window.lua b/lua/neotest/lib/window.lua index 73eb0db6..58667a98 100644 --- a/lua/neotest/lib/window.lua +++ b/lua/neotest/lib/window.lua @@ -61,10 +61,20 @@ function PersistentWindow:open() end function PersistentWindow:buffer() - if self._bufnr and vim.fn.bufexists(self._bufnr) == 1 then + if + self._bufnr + and nio.api.nvim_buf_is_valid(self._bufnr) + and nio.api.nvim_buf_is_loaded(self._bufnr) + then return self._bufnr end + for _, bufnr in ipairs(nio.api.nvim_list_bufs()) do + if nio.api.nvim_buf_get_name(bufnr):find(self.name, 1, true) then + nio.api.nvim_buf_delete(bufnr, { force = true }) + end + end + self._bufnr = nio.api.nvim_create_buf(false, true) nio.api.nvim_buf_set_name(self._bufnr, self.name) for k, v in pairs(self._bufopts) do diff --git a/tests/unit/consumers/output_panel_spec.lua b/tests/unit/consumers/output_panel_spec.lua new file mode 100644 index 00000000..da3ae219 --- /dev/null +++ b/tests/unit/consumers/output_panel_spec.lua @@ -0,0 +1,182 @@ +local neotest = require("neotest") + +local nio = require("nio") +local a = nio.tests + +local stub = require("luassert.stub") +local Tree = require("neotest.types").Tree +local lib = require("neotest.lib") + +local NeotestClient = require("neotest.client") +local AdapterGroup = require("neotest.adapters") + +describe("neotest consumer - output_panel", function() + ---@type neotest.Client + local client + + ---@type neotest.Adapter + local mock_adapter + local mock_strategy + local exit_future_1, exit_future_2 + + local dir = vim.loop.cwd() + local files + local dirs = { dir } + + ---@return neotest.Tree + local get_pos = function(...) + ---@diagnostic disable-next-line + return client:get_position(...) + end + + before_each(function() + dirs = { dir } + files = { dir .. "/test_file_1", dir .. "/test_file_2" } + + stub(lib.files, "find", files) + stub(lib.files, "read", "Test results - passed and failed\r\n") + stub(lib.files, "is_dir", function(path) + return vim.tbl_contains(dirs, path) + end) + stub(lib.files, "exists", function(path) + return path ~= "" + end) + + exit_future_1, exit_future_2 = nio.control.future(), nio.control.future() + + mock_adapter = { + name = "adapter", + + is_test_file = function(file_path) + return file_path ~= "" and not vim.endswith(file_path, lib.files.sep) + end, + + root = function() + return dir + end, + + discover_positions = function(file_path) + return Tree.from_list({ + { id = file_path, type = "file", path = file_path, name = file_path }, + { + { + id = file_path .. "::namespace", + type = "namespace", + path = file_path, + name = "namespace", + range = { 5, 0, 50, 0 }, + }, + { + id = file_path .. "::test_a", + type = "test", + path = file_path, + name = "test_a", + range = { 10, 0, 20, 50 }, + }, + }, + }, function(pos) + return pos.id + end) + end, + + build_spec = function() + return { strategy = { output = "not_a_file" } } + end, + + results = function(_, _, tree) + return {} + end, + } + + mock_strategy = function(spec) + return { + is_complete = function() + return true + end, + + output = function() + return type(spec.strategy) == "table" and spec.strategy.output or "not_a_file" + end, + + stop = function() + exit_future_1.set() + exit_future_2.set() + end, + + result = function() + if not exit_future_1.is_set() then + exit_future_1.wait() + else + exit_future_2.wait() + end + return type(spec.strategy) == "table" and spec.strategy.exit_code or 0 + end, + } + end + + client = NeotestClient(AdapterGroup()) + ---@diagnostic disable-next-line + neotest.setup({ adapters = { mock_adapter }, output_panel = { enabled = true } }) + + require("neotest.consumers.output_panel")(client) + ---@diagnostic disable-next-line + client.listeners.results = { output_panel = client.listeners.results } + end) + + after_each(function() + lib.files.find:revert() + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_delete(buf, { force = true }) + end + end) + + describe("user forcefully closes the panel", function() + local panel_bufnr = function() + return vim.tbl_filter(function(bufnr) + return nio.api.nvim_buf_get_name(bufnr):find("Neotest Output Panel") + end, nio.api.nvim_list_bufs())[1] + end + + before_each(function() + neotest.output_panel.open() + end) + + a.it("recreates terminal session if term channel is invalid", function() + local tree = get_pos(dir .. "/test_file_1") + + nio.run(function() + client:run_tree(tree, { strategy = mock_strategy }) + end) + exit_future_1.set() + + nio.api.nvim_buf_delete(panel_bufnr(), { force = true }) + neotest.output_panel.open() + + nio.run(function() + assert.has_no_error(function() + client:run_tree(tree, { strategy = mock_strategy }) + end) + end) + exit_future_2.set() + end) + + it("recreates panel buffer if it was closed", function() + vim.api.nvim_buf_delete(panel_bufnr(), { force = true }) + + assert.has_no_error(function() + neotest.output_panel.open() + end) + end) + + it("deletes panel buffer if it already exists with the same name", function() + vim.api.nvim_buf_delete(panel_bufnr(), { force = true }) + + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(buf, "Neotest Output Panel") + + assert.has_no_error(function() + neotest.output_panel.open() + end) + end) + end) +end) From 55083477b7769eb70ad81a86cfbf167988aa3e78 Mon Sep 17 00:00:00 2001 From: Yaro Date: Thu, 28 Nov 2024 00:38:01 +0000 Subject: [PATCH 2/2] fix(watcher): race condition between positions update and watch run on BufWritePost --- lua/neotest/consumers/watch/init.lua | 13 ++++++++++++- lua/neotest/consumers/watch/watcher.lua | 8 ++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lua/neotest/consumers/watch/init.lua b/lua/neotest/consumers/watch/init.lua index 6c9283c9..1f56fa66 100644 --- a/lua/neotest/consumers/watch/init.lua +++ b/lua/neotest/consumers/watch/init.lua @@ -29,6 +29,16 @@ local neotest = {} ---@class neotest.consumers.watch neotest.watch = {} +local init = function(client) + client.listeners.discover_positions = function(_, tree) + for _, watcher in pairs(watchers) do + if watcher.tree:data().path == tree:data().path then + watcher.discover_positions_event.set() + end + end + end +end + local function get_valid_client(bufnr) local clients = nio.lsp.get_clients({ bufnr = bufnr }) for _, client in ipairs(clients) do @@ -200,7 +210,8 @@ function neotest.watch.is_watching(position_id) end neotest.watch = setmetatable(neotest.watch, { - __call = function() + __call = function(_, client) + init(client) return neotest.watch end, }) diff --git a/lua/neotest/consumers/watch/watcher.lua b/lua/neotest/consumers/watch/watcher.lua index c77ba358..c7058cd7 100644 --- a/lua/neotest/consumers/watch/watcher.lua +++ b/lua/neotest/consumers/watch/watcher.lua @@ -6,6 +6,8 @@ local config = require("neotest.config") ---@class neotest.consumers.watch.Watcher ---@field lsp_client nio.lsp.Client ---@field autocmd_id? string +---@field tree neotest.Tree +---@field discover_positions_event nio.control.Future local Watcher = {} function Watcher:new(lsp_client) @@ -159,6 +161,9 @@ function Watcher:watch(tree, args) logger.debug("Built dependencies in", elapsed, "ms for", tree:data().id, ":", dependencies) local dependants = self:_build_dependants(dependencies) + self.tree = tree + self.discover_positions_event = nio.control.future() + self.autocmd_id = nio.api.nvim_create_autocmd("BufWritePost", { callback = function(autocmd_args) if type(args.run_predicate) == "function" and not args.run_predicate(autocmd_args.buf) then @@ -172,6 +177,9 @@ function Watcher:watch(tree, args) return end + self.discover_positions_event.wait() + self.discover_positions_event = nio.control.future() + if tree:data().type ~= "dir" then run.run(vim.tbl_extend("keep", { tree:data().id }, args)) else