From 7960fccb99031661f38a630e57757aec8fd2233a Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 4 Jul 2024 09:15:34 +0200 Subject: [PATCH] feat: run all tests in file using one 'go test' command --- lua/neotest-golang/cmd.lua | 47 ++++++++++++-- lua/neotest-golang/init.lua | 20 +++++- lua/neotest-golang/json.lua | 2 +- lua/neotest-golang/parse.lua | 2 +- lua/neotest-golang/runspec_file.lua | 99 +++++++++++++++++++++++------ 5 files changed, 139 insertions(+), 31 deletions(-) diff --git a/lua/neotest-golang/cmd.lua b/lua/neotest-golang/cmd.lua index cb196dd9..42cc38a8 100644 --- a/lua/neotest-golang/cmd.lua +++ b/lua/neotest-golang/cmd.lua @@ -2,6 +2,7 @@ local async = require("neotest.async") +local convert = require("neotest-golang.convert") local options = require("neotest-golang.options") local json = require("neotest-golang.json") @@ -20,6 +21,46 @@ function M.golist_data(cwd) return json.process_golist_output(output) end +function M.gotest_list_data(go_mod_folderpath, module_name) + local cmd = { "go", "test", "-v", "-json", "-list", "^Test", module_name } + local output = vim.fn.system( + "cd " .. go_mod_folderpath .. " && " .. table.concat(cmd, " ") + ) + + -- FIXME: weird... would've expected to call process_gotest_output + local json_output = json.process_golist_output(output) + + --- @type string[] + local test_names = {} + for _, v in ipairs(json_output) do + if v.Action == "output" then + --- @type string + local test_name = string.gsub(v.Output, "\n", "") + if string.match(test_name, "^Test") then + test_names = vim.list_extend( + test_names, + { convert.to_gotest_regex_pattern(test_name) } + ) + end + end + end + + return test_names +end + +function M.test_command_for_dir(module_name) + local go_test_required_args = { module_name } + local cmd, json_filepath = M.test_command(go_test_required_args) + return cmd, json_filepath +end + +function M.test_command_for_file(module_name, test_names_regexp) + local go_test_required_args = { "-run", test_names_regexp, module_name } + local cmd, json_filepath = M.test_command(go_test_required_args) + + return cmd, json_filepath +end + function M.test_command_for_individual_test( test_folder_absolute_path, test_name @@ -29,12 +70,6 @@ function M.test_command_for_individual_test( return cmd, json_filepath end -function M.test_command_for_dir(module_name) - local go_test_required_args = { module_name } - local cmd, json_filepath = M.test_command(go_test_required_args) - return cmd, json_filepath -end - function M.test_command(go_test_required_args) --- The runner to use for running tests. --- @type string diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 8ee40e62..1b5a6ad0 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -150,6 +150,12 @@ function M.Adapter.results(spec, result, tree) local results = parse.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) return results + elseif spec.context.pos_type == "file" then + -- A test command executed a file of tests and the output/status must + -- now be processed. + local results = parse.test_results(spec, result, tree) + M.workaround_neotest_issue_391(result) + return results elseif spec.context.pos_type == "test" then -- A test command executed a single test and the output/status must now be -- processed. @@ -171,8 +177,18 @@ function M.workaround_neotest_issue_391(result) -- 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 - if result.output ~= nil then - vim.fn.writefile({ "" }, result.output) + + -- NOTE: when emptying the file with vim.fn.writefil, this error was hit + -- when debugging: + -- E5560: Vimscript function must not be called in a lua loop callback + -- vim.fn.writefile({ "" }, result.output) + + if result.output ~= nil then -- and vim.fn.filereadable(result.output) == 1 then + local file = io.open(result.output, "w") + if file ~= nil then + file:write("") + file:close() + end end end diff --git a/lua/neotest-golang/json.lua b/lua/neotest-golang/json.lua index 81c3511a..528eaaac 100644 --- a/lua/neotest-golang/json.lua +++ b/lua/neotest-golang/json.lua @@ -5,7 +5,7 @@ local M = {} --- Process output from 'go test -json' and return an iterable table. --- @param raw_output table --- @return table -function M.process_gotest_output(raw_output) +function M.process_gotest_json_output(raw_output) local jsonlines = {} for _, line in ipairs(raw_output) do if string.match(line, "^%s*{") then -- must start with the `{` character diff --git a/lua/neotest-golang/parse.lua b/lua/neotest-golang/parse.lua index ca47de21..b523053e 100644 --- a/lua/neotest-golang/parse.lua +++ b/lua/neotest-golang/parse.lua @@ -79,7 +79,7 @@ function M.test_results(spec, result, tree) raw_output = async.fn.readfile(context.test_output_json_filepath) end - local gotest_output = json.process_gotest_output(raw_output) + local gotest_output = json.process_gotest_json_output(raw_output) --- The 'go list -json' output, converted into a lua table. local golist_output = context.golist_data diff --git a/lua/neotest-golang/runspec_file.lua b/lua/neotest-golang/runspec_file.lua index 7d418f3a..3b4f02c8 100644 --- a/lua/neotest-golang/runspec_file.lua +++ b/lua/neotest-golang/runspec_file.lua @@ -1,5 +1,8 @@ --- Helpers to build the command and context around running all tests of a file. +local cmd = require("neotest-golang.cmd") +local runspec_dir = require("neotest-golang.runspec_dir") + local M = {} --- Build runspec for a directory. @@ -8,28 +11,82 @@ local M = {} --- @return neotest.RunSpec | neotest.RunSpec[] | nil function M.build(pos, tree) if vim.tbl_isempty(tree:children()) then - --- @type RunspecContext - local context = { - pos_id = pos.id, - pos_type = "test", -- TODO: to be implemented as "file" later - golist_data = {}, -- no golist output - parse_test_results = true, - dummy_test = true, - } - - --- Runspec designed for files that contain no tests. - --- @type neotest.RunSpec - local run_spec = { - command = { "echo", "No tests found in file" }, - context = context, - } - return run_spec - else - -- TODO: Implement a runspec for a file of tests. - -- A bare return will delegate test execution to per-test execution, which - -- will have to do for now. - return + M.fail_fast(pos) + end + + local go_mod_filepath = runspec_dir.find_file_upwards("go.mod", pos.path) + if go_mod_filepath == nil then + -- if no go.mod file was found up the directory tree, until reaching $CWD, + -- then we cannot determine the Go project root. + return M.fail_fast(pos) + end + + local go_mod_folderpath = vim.fn.fnamemodify(go_mod_filepath, ":h") + local golist_data = cmd.golist_data(go_mod_folderpath) + + -- find the go module that corresponds to the go_mod_folderpath + local module_name = "./..." -- if no go module, run all tests at the $CWD + + local test_names_regexp = + M.find_tests_in_file(pos, golist_data, go_mod_folderpath, module_name) + local test_cmd, json_filepath = + cmd.test_command_for_file(module_name, test_names_regexp) + + --- @type RunspecContext + local context = { + pos_id = pos.id, + pos_type = "file", + golist_data = golist_data, + parse_test_results = true, + test_output_json_filepath = json_filepath, + } + + --- @type neotest.RunSpec + local run_spec = { + command = test_cmd, + cwd = go_mod_folderpath, + context = context, + } + + return run_spec +end + +function M.fail_fast(pos) + --- @type RunspecContext + local context = { + pos_id = pos.id, + pos_type = "file", + golist_data = {}, -- no golist output + parse_test_results = true, + dummy_test = true, + } + + --- Runspec designed for files that contain no tests. + --- @type neotest.RunSpec + local run_spec = { + command = { "echo", "No tests found in file" }, + context = context, + } + return run_spec +end + +function M.find_tests_in_file(pos, golist_data, go_mod_folderpath, module_name) + local pos_path_filename = vim.fn.fnamemodify(pos.path, ":t") + + for _, golist_item in ipairs(golist_data) do + if golist_item.TestGoFiles ~= nil then + if vim.tbl_contains(golist_item.TestGoFiles, pos_path_filename) then + module_name = golist_item.ImportPath + break + end + end end + + -- FIXME: this grabs all test files from the package. We only want the one in the file. + local test_names = cmd.gotest_list_data(go_mod_folderpath, module_name) + local test_names_regexp = "^(" .. table.concat(test_names, "|") .. ")$" + + return test_names_regexp end return M