From 6f4782d81836b5d5f9a5640589a9c324f16c67f3 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 3 Jun 2024 23:18:16 +0200 Subject: [PATCH] feat: support directory (module) execution --- lua/neotest-golang/convert.lua | 23 ++++ lua/neotest-golang/init.lua | 236 ++++++++++++++++++++++++++++++--- 2 files changed, 240 insertions(+), 19 deletions(-) diff --git a/lua/neotest-golang/convert.lua b/lua/neotest-golang/convert.lua index bf39f828..0a6fe899 100644 --- a/lua/neotest-golang/convert.lua +++ b/lua/neotest-golang/convert.lua @@ -18,4 +18,27 @@ function M.to_gotest_test_name(pos_id) return test_name 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. +---@param go_test_name string +---@return string +function M.to_neotest_test_name_pattern(go_test_name) + -- construct the test name + local test_name = go_test_name + -- Add :: before the test name + test_name = "::" .. test_name + -- Replace / with :: + test_name = test_name:gsub("/", "::") + + -- TODO: handle this, but the other way around: + -- Remove double quotes (single quotes are supported) + -- test_name = test_name:gsub('"', "") + + -- Replace _ with space + test_name = test_name:gsub("_", " ") + + return test_name +end + return M diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index d7e54807..396975eb 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -156,31 +156,49 @@ function M.Adapter.build_spec(args) return end - if pos.type == "dir" and pos.path == vim.fn.getcwd() then - -- Test suite - - return -- delegate test execution to per-test execution - - -- NOTE: could potentially run 'go test' on the whole directory, to make - -- tests go a lot faster, but would come with the added complexity of - -- having to traverse the node tree manually and set statuses accordingly. - -- I'm not sure it's worth it... - elseif pos.type == "dir" then + vim.notify("pos.type=" .. pos.type) + + -- if pos.type == "dir" and pos.path == vim.fn.getcwd() then + -- -- Test suite + -- + -- return -- delegate test execution to per-test execution + -- + -- -- NOTE: could potentially run 'go test' on the whole directory, to make + -- -- tests go a lot faster, but would come with the added complexity of + -- -- having to traverse the node tree manually and set statuses accordingly. + -- -- I'm not sure it's worth it... + + if pos.type == "dir" then -- Sub-directory - return -- delegate test execution to per-test execution - -- NOTE: could potentially run 'go test' on the whole file, to make -- tests go a lot faster, but would come with the added complexity of -- having to traverse the node tree manually and set statuses accordingly. -- I'm not sure it's worth it... - -- - -- ---@type string - -- local relative_test_folderpath = vim.fn.fnamemodify(pos.path, ":~:.") - -- ---@type string - -- local relative_test_folderpath_go = "./" - -- .. relative_test_folderpath - -- .. "/..." + + ---@type string + local relative_test_folderpath = vim.fn.fnamemodify(pos.path, ":~:.") + ---@type string + local relative_test_folderpath_go = "./" + .. relative_test_folderpath + .. "/..." + + vim.notify("Full path to folder: " .. pos.path) + + 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 + vim.notify("cwd: " .. cwd) + + -- calculate the relative path to pos.path from cwd + local relative_path = M.remove_base_path(cwd, pos.path) + vim.notify("relative path: " .. relative_path) + + local test_pattern = "./" .. relative_path .. "/..." + + return M.build_dir_test_runspec(pos, cwd, test_pattern) elseif pos.type == "file" then -- Single file @@ -192,6 +210,7 @@ function M.Adapter.build_spec(args) context = { id = pos.id, skip = true, + test_type = "test", -- TODO: to be implemented as "file" later }, } return run_spec @@ -219,6 +238,119 @@ end ---@param tree neotest.Tree ---@return table function M.Adapter.results(spec, result, tree) + if spec.context.test_type == "dir" then + return M.results_dir(spec, result, tree) + elseif spec.context.test_type == "test" then + return M.results_test(spec, result, tree) + end + + vim.notify("ERROR: unknown test type: " .. spec.context.test_type) + return {} +end + +---@param spec neotest.RunSpec +---@param result neotest.StrategyResult +---@param tree neotest.Tree +function M.results_dir(spec, result, tree) + print("PARSE DIR RESULTS") + print(vim.inspect(spec)) + -- print(vim.inspect(result)) + -- print(vim.inspect(tree)) + + ---@type neotest.ResultStatus + local result_status = "skipped" + if result.code == 0 then + result_status = "passed" + else + result_status = "failed" + end + + ---@type table + local raw_output = async.fn.readfile(result.output) + ---@type List + local test_result = {} + ---@type neotest.Error[] + local errors = {} + ---@type List + local jsonlines = M.process_json(raw_output) + + ---@type table + local results = {} + -- results[spec.context.id] = { + -- status = result_status, + -- output = parsed_output_path, + -- errors = errors, + -- } + + -- string.find options + local init = 1 + local is_plain = true + local is_pattern = false + + -- print(vim.inspect(jsonlines)) + + for _, line in ipairs(jsonlines) do + if line.Action == "output" and line.Output ~= nil then + -- record output, prints to output panel + table.insert(test_result, line.Output) -- TODO: refactor + end + + -- if line contains "--- PASS" + if line.Action == "output" and string.match(line.Output, "--- PASS:") then + local go_test_name = + string.match(line.Output, "--- PASS: (.*) %(%d+%.%d+s%)") + if go_test_name ~= nil then + local neotest_test_name = + convert.to_neotest_test_name_pattern(go_test_name) + + for _, node in tree:iter_nodes() do + local node_data = node:data() + + if + string.find(node_data.path, spec.context.id, init, is_plain) + and string.find(node_data.id, neotest_test_name, init, is_pattern) + then + vim.notify(vim.inspect(node_data)) + + -- TODO: refactor how we populate status, output and errors + results[node_data.id] = { + status = "passed", + errors = {}, + } + + -- local expandable = #node:children() > 0 + -- if expandable then + -- vim.notify("EXPANDABLE") + -- vim.notify(vim.inspect(node:children())) + -- end + end + end + end + end + + -- if result.code ~= 0 and line.Output ~= nil then + -- -- record an error + -- end + end + + -- write json_decoded to file + local parsed_output_path = vim.fs.normalize(async.fn.tempname()) + async.fn.writefile(test_result, 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) + + results[spec.context.id] = { + status = result_status, + output = parsed_output_path, + } + + return results +end + +function M.results_test(spec, result, tree) if spec.context.skip then ---@type table local results = {} @@ -300,6 +432,71 @@ function M.Adapter.results(spec, result, tree) return results 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 relative_test_folderpath_go string +---@return neotest.RunSpec +function M.build_dir_test_runspec(pos, cwd, test_pattern) + vim.notify("Dir test runspec") + + local gotest = { + "go", + "test", + "-json", + } + + ---@type table + local go_test_args = { + test_pattern, + } + + local combined_args = + vim.list_extend(vim.deepcopy(M.Adapter._go_test_args), go_test_args) + local gotest_command = vim.list_extend(vim.deepcopy(gotest), combined_args) + + print("Command:") + print(vim.inspect(gotest_command)) + + ---@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 + --- Build runspec for a single test ---@param pos neotest.Position ---@param strategy string @@ -334,6 +531,7 @@ function M.build_single_test_runspec(pos, strategy) context = { id = pos.id, test_filepath = pos.path, + test_type = "test", }, }