From e01f4ad464be336c8f9e8e632c1961151f2d8142 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Sun, 9 Jun 2024 22:16:28 +0200 Subject: [PATCH] feat: execute all tests in dir --- lua/neotest-golang/convert.lua | 13 ++- lua/neotest-golang/init.lua | 21 +++-- lua/neotest-golang/results_dir.lua | 134 ++++++++++++++++++++++++++++ lua/neotest-golang/results_test.lua | 2 +- lua/neotest-golang/runspec_dir.lua | 88 ++++++++++++++++++ tests/go/testname_test.go | 8 +- 6 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 lua/neotest-golang/results_dir.lua create mode 100644 lua/neotest-golang/runspec_dir.lua diff --git a/lua/neotest-golang/convert.lua b/lua/neotest-golang/convert.lua index c1f68c3..da37a62 100644 --- a/lua/neotest-golang/convert.lua +++ b/lua/neotest-golang/convert.lua @@ -22,6 +22,8 @@ end -- Converts the `go test` command test name into Neotest node test name format. -- Note that a pattern can returned, not the exact test name, so to support -- escaped quotes etc. +-- NOTE: double quotes must be removed from the string matching against. + ---@param go_test_name string ---@return string function M.to_neotest_test_name_pattern(go_test_name) @@ -32,14 +34,21 @@ function M.to_neotest_test_name_pattern(go_test_name) -- Replace / with :: test_name = test_name:gsub("/", "::") - -- NOTE: double quotes are removed from the string we match against. - -- Replace _ with space test_name = test_name:gsub("_", " ") -- Mark the end of the test name pattern test_name = test_name .. "$" + -- Percentage sign must be escaped + test_name = test_name:gsub("%%", "%%%%") + + -- Literal brackets and parantheses must be escaped + test_name = test_name:gsub("%[", "%%[") + test_name = test_name:gsub("%]", "%%]") + test_name = test_name:gsub("%(", "%%(") + test_name = test_name:gsub("%)", "%%)") + return test_name end diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 7fdb673..d0aba9c 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -1,6 +1,10 @@ +local _ = require("neotest") + local options = require("neotest-golang.options") local discover_positions = require("neotest-golang.discover_positions") +local runspec_dir = require("neotest-golang.runspec_dir") local runspec_test = require("neotest-golang.runspec_test") +local results_dir = require("neotest-golang.results_dir") local results_test = require("neotest-golang.results_test") local utils = require("neotest-golang.utils") @@ -74,12 +78,10 @@ function M.Adapter.build_spec(args) if pos.type == "dir" and pos.path == vim.fn.getcwd() then -- Test suite - - return -- delegate test execution to per-test execution + return runspec_dir.build(pos) elseif pos.type == "dir" then -- Sub-directory - - return -- delegate test execution to per-test execution + return runspec_dir.build(pos) elseif pos.type == "file" then -- Single file @@ -102,6 +104,8 @@ function M.Adapter.build_spec(args) -- to compile. This approach is too brittle, and therefore this mode is not -- supported. Instead, the tests of a file are run as if pos.typ == "test". + vim.notify("Would've executed a file: " .. pos.path) + return -- delegate test execution to per-test execution end elseif pos.type == "test" then @@ -121,7 +125,14 @@ end ---@param tree neotest.Tree ---@return table function M.Adapter.results(spec, result, tree) - return results_test.results_test(spec, result, tree) + if spec.context.test_type == "dir" then + return results_dir.results(spec, result, tree) + elseif spec.context.test_type == "test" then + return results_test.results(spec, result, tree) + end + + vim.notify("Error: [results] unknown test type: " .. spec.context.test_type) + return {} end setmetatable(M.Adapter, { diff --git a/lua/neotest-golang/results_dir.lua b/lua/neotest-golang/results_dir.lua new file mode 100644 index 0000000..7a43d6f --- /dev/null +++ b/lua/neotest-golang/results_dir.lua @@ -0,0 +1,134 @@ +local async = require("neotest.async") + +local convert = require("neotest-golang.convert") +local json = require("neotest-golang.json") + +local M = {} + +---@param spec neotest.RunSpec +---@param result neotest.StrategyResult +---@param tree neotest.Tree +function M.results(spec, result, tree) + ---@type table + local raw_output = async.fn.readfile(result.output) + + ---@type List + local jsonlines = json.process_json(raw_output) + + ---@type List + local full_test_output = {} + + --- neotest results + ---@type table + local neotest_results = {} + + --- internal results struct + ---@type table + local internal_results = {} + + -- record test names + for _, line in ipairs(jsonlines) do + if line.Action == "run" and line.Test ~= nil then + internal_results[line.Test] = { + status = "skipped", + output = {}, + errors = {}, + node_data = nil, + } + end + end + + -- record test status + for _, line in ipairs(jsonlines) do + if line.Action == "pass" and line.Test ~= nil then + internal_results[line.Test].status = "passed" + elseif line.Action == "fail" and line.Test ~= nil then + internal_results[line.Test].status = "failed" + end + end + + -- record error output + for _, line in ipairs(jsonlines) do + if line.Action == "output" and line.Output ~= nil and line.Test ~= nil then + -- append line.Output to output field + internal_results[line.Test].output = + vim.list_extend(internal_results[line.Test].output, { line.Output }) + -- search for error message and line number + local matched_line_number = string.match(line.Output, "_test%.go:(%d+):") + if matched_line_number ~= nil then + local line_number = tonumber(matched_line_number) + local message = string.match(line.Output, "_test%.go:%d+: (.*)") + if line_number ~= nil and message ~= nil then + table.insert(internal_results[line.Test].errors, { + line = line_number - 1, -- neovim lines are 0-indexed + message = message, + }) + end + end + end + end + + -- associate internal results with neotest node data + for test_name, test_properties in pairs(internal_results) do + local test_name_pattern = convert.to_neotest_test_name_pattern(test_name) + for _, node in tree:iter_nodes() do + local node_data = node:data() + + -- WARNING: workarounds + local tweaked_node_data_id = node_data.id:gsub('"', "") -- workaround, since we cannot know where double quotes might appear + local tweaked_node_data_id = tweaked_node_data_id:gsub("_", " ") -- NOTE: look into making this more clear... + + if + string.find(node_data.path, spec.context.id, 1, true) + and string.find(tweaked_node_data_id, test_name_pattern, 1, false) + then + internal_results[test_name].node_data = node_data + end + end + end + + -- populate neotest results + for test_name, test_properties in pairs(internal_results) do + if test_properties.node_data ~= nil then + local test_output_path = vim.fs.normalize(async.fn.tempname()) + async.fn.writefile(test_properties.output, test_output_path) + neotest_results[test_properties.node_data.id] = { + status = test_properties.status, + output = test_output_path, -- NOTE: could be slow when running many tests? + errors = test_properties.errors, + } + end + end + + ---@type neotest.ResultStatus + local test_command_status = "skipped" + if result.code == 0 then + test_command_status = "passed" + else + test_command_status = "failed" + end + + -- write full test command output + local parsed_output_path = vim.fs.normalize(async.fn.tempname()) + for _, line in ipairs(jsonlines) do + if line.Action == "output" then + table.insert(full_test_output, line.Output) + end + end + async.fn.writefile(full_test_output, parsed_output_path) + + -- register properties on the directory node that was run + neotest_results[spec.context.id] = { + status = test_command_status, + output = parsed_output_path, + } + + -- FIXME: once output is parsed, erase file contents, so to avoid JSON in + -- output panel. This is a workaround for now, only because of + -- https://github.com/nvim-neotest/neotest/issues/391 + vim.fn.writefile({ "" }, result.output) + + return neotest_results +end + +return M diff --git a/lua/neotest-golang/results_test.lua b/lua/neotest-golang/results_test.lua index bc2b685..ffafafa 100644 --- a/lua/neotest-golang/results_test.lua +++ b/lua/neotest-golang/results_test.lua @@ -7,7 +7,7 @@ local M = {} ---@param result neotest.StrategyResult ---@param tree neotest.Tree ---@return table -function M.results_test(spec, result, tree) +function M.results(spec, result, tree) if spec.context.skip then ---@type table local results = {} diff --git a/lua/neotest-golang/runspec_dir.lua b/lua/neotest-golang/runspec_dir.lua new file mode 100644 index 0000000..401d7b3 --- /dev/null +++ b/lua/neotest-golang/runspec_dir.lua @@ -0,0 +1,88 @@ +local _ = require("neotest") -- fix LSP errors + +local options = require("neotest-golang.options") + +local M = {} + +--- Build runspec for a directory. +---@param pos neotest.Position +---@return neotest.RunSpec +function M.build(pos) + -- Strategy: + -- 1. Find the go.mod file from pos.path. + -- 2. Run `go test` from the directory containing the go.mod file. + -- 3. Use the relative path from the go.mod file to pos.path as the test pattern. + + local go_mod_filepath = M.find_file_upwards("go.mod", pos.path) + local go_mod_folderpath = vim.fn.fnamemodify(go_mod_filepath, ":h") + local cwd = go_mod_folderpath + + -- calculate the relative path to pos.path from cwd + local relative_path = M.remove_base_path(cwd, pos.path) + local test_pattern = "./" .. relative_path .. "/..." + + return M.build_dir_test_runspec(pos, cwd, test_pattern) +end + +function M.find_file_upwards(filename, start_path) + local scan = require("plenary.scandir") + local cwd = vim.fn.getcwd() -- get the current working directory + local found_filepath = nil + while start_path ~= cwd do + local files = scan.scan_dir( + start_path, + { search_pattern = filename, hidden = true, depth = 1 } + ) + if #files > 0 then + found_filepath = files[1] + break + end + start_path = vim.fn.fnamemodify(start_path, ":h") -- go up one directory + end + return found_filepath +end + +function M.remove_base_path(base_path, target_path) + if string.find(target_path, base_path, 1, true) == 1 then + return string.sub(target_path, string.len(base_path) + 2) + end + + return target_path +end + +--- Build runspec for a directory of tests +---@param pos neotest.Position +---@param cwd string +---@param test_pattern string +---@return neotest.RunSpec +function M.build_dir_test_runspec(pos, cwd, test_pattern) + local gotest = { + "go", + "test", + "-json", + } + + ---@type table + local go_test_args = { + test_pattern, + } + + local combined_args = + vim.list_extend(vim.deepcopy(options._go_test_args), go_test_args) + local gotest_command = vim.list_extend(vim.deepcopy(gotest), combined_args) + + ---@type neotest.RunSpec + local run_spec = { + command = gotest_command, + cwd = cwd, + context = { + id = pos.id, + test_filepath = pos.path, + test_type = "dir", + }, + } + + return run_spec +end + +return M diff --git a/tests/go/testname_test.go b/tests/go/testname_test.go index 0d880df..88e8369 100644 --- a/tests/go/testname_test.go +++ b/tests/go/testname_test.go @@ -10,7 +10,7 @@ func TestNames(t *testing.T) { } }) - t.Run("Comma , and ' are ok to use", func(t *testing.T) { + t.Run("Comma , and apostrophy ' are ok to use", func(t *testing.T) { if Add(1, 2) != 3 { t.Fail() } @@ -21,4 +21,10 @@ func TestNames(t *testing.T) { t.Fail() } }) + + t.Run("Percentage sign like 50% is ok", func(t *testing.T) { + if Add(1, 2) != 3 { + t.Fail() + } + }) }