diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index bc024375..fae98ed5 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -135,25 +135,27 @@ end --- @param tree neotest.Tree --- @return table | nil function M.Adapter.results(spec, result, tree) - if spec.context.pos_type == "dir" then + local pos = tree:data() + + if pos.type == "dir" then -- A test command executed a directory of tests and the output/status must -- now be processed. local results = process.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) return results - elseif spec.context.pos_type == "file" then + elseif pos.type == "file" then -- A test command executed a file of tests and the output/status must -- now be processed. local results = process.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) return results - elseif spec.context.pos_type == "namespace" then + elseif pos.type == "namespace" then -- A test command executed a namespace and the output/status must now be -- processed. local results = process.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) return results - elseif spec.context.pos_type == "test" then + elseif pos.type == "test" then -- A test command executed a single test and the output/status must now be -- processed. local results = process.test_results(spec, result, tree) @@ -163,7 +165,7 @@ function M.Adapter.results(spec, result, tree) logger.error( "Cannot process test results due to unknown Neotest position type:" - .. spec.context.pos_type + .. pos.type ) end diff --git a/lua/neotest-golang/lib/cmd.lua b/lua/neotest-golang/lib/cmd.lua index d227c810..a1e31c39 100644 --- a/lua/neotest-golang/lib/cmd.lua +++ b/lua/neotest-golang/lib/cmd.lua @@ -8,21 +8,38 @@ local json = require("neotest-golang.lib.json") local M = {} +--- Call 'go list -json {go_list_args...} ./...' to get test file data +--- @param cwd string function M.golist_data(cwd) - -- call 'go list -json {go_list_args...} ./...' to get test file data + local cmd = M.golist_command() + local go_list_command_concat = table.concat(cmd, " ") + logger.debug("Running Go list: " .. go_list_command_concat .. " in " .. cwd) + local result = vim.system(cmd, { cwd = cwd, text = true }):wait() + + local err = nil + if result.code == 1 then + err = "go list:" + if result.stdout ~= nil and result.stdout ~= "" then + err = err .. " " .. result.stdout + end + if result.stdout ~= nil and result.stderr ~= "" then + err = err .. " " .. result.stderr + end + logger.debug({ "Go list error: ", err }) + end + + local output = result.stdout or "" + + local golist_output = json.decode_from_string(output) + logger.debug({ "JSON-decoded 'go list' output: ", golist_output }) + return golist_output, err +end - -- combine base command, user args and packages(./...) +function M.golist_command() local cmd = { "go", "list", "-json" } vim.list_extend(cmd, options.get().go_list_args or {}) vim.list_extend(cmd, { "./..." }) - - local go_list_command_concat = table.concat(cmd, " ") - logger.debug("Running Go list: " .. go_list_command_concat .. " in " .. cwd) - local output = vim.system(cmd, { cwd = cwd, text = true }):wait().stdout or "" - if output == "" then - logger.error({ "Execution of 'go list' failed, output:", output }) - end - return json.decode_from_string(output) + return cmd end function M.test_command_in_package(package_or_path) diff --git a/lua/neotest-golang/lib/json.lua b/lua/neotest-golang/lib/json.lua index 2c0b909d..840a067d 100644 --- a/lua/neotest-golang/lib/json.lua +++ b/lua/neotest-golang/lib/json.lua @@ -6,8 +6,9 @@ local M = {} --- Decode JSON from a table of strings into a table of objects. --- @param tbl table +--- @param construct_invalid boolean --- @return table -function M.decode_from_table(tbl) +function M.decode_from_table(tbl, construct_invalid) local jsonlines = {} for _, line in ipairs(tbl) do if string.match(line, "^%s*{") then -- must start with the `{` character @@ -19,7 +20,11 @@ function M.decode_from_table(tbl) logger.warn("Failed to decode JSON line: " .. line) end else - -- vim.notify("Not valid JSON: " .. line, vim.log.levels.DEBUG) + logger.debug({ "Not valid JSON:", line }) + if construct_invalid then + -- this is for example errors from stderr, when there is a compilation error + table.insert(jsonlines, { Action = "output", Output = line }) + end end end return jsonlines @@ -40,7 +45,7 @@ function M.decode_from_string(str) current_object = current_object .. line end table.insert(tbl, current_object) - return M.decode_from_table(tbl) + return M.decode_from_table(tbl, false) end return M diff --git a/lua/neotest-golang/process.lua b/lua/neotest-golang/process.lua index 2d836be9..e09f96ff 100644 --- a/lua/neotest-golang/process.lua +++ b/lua/neotest-golang/process.lua @@ -7,16 +7,12 @@ local logger = require("neotest-golang.logging") local options = require("neotest-golang.options") local lib = require("neotest-golang.lib") --- TODO: remove pos_type when properly supporting all position types. --- and instead get this from the pos.type field. - --- @class RunspecContext --- @field pos_id string Neotest tree position id. ---- @field pos_type neotest.PositionType Neotest tree position type. ---- @field golist_data table Filepath to 'go list' JSON data (lua table). -- TODO: rename to golist_data ---- @field parse_test_results boolean If true, parsing of test output will occur. +--- @field golist_data table The 'go list' JSON data (lua table). +--- @field errors? table Non-gotest errors to show in the final output. +--- @field is_dap_active boolean? If true, parsing of test output will occur. --- @field test_output_json_filepath? string Gotestsum JSON filepath. ---- @field dummy_test? boolean Temporary workaround before supporting position type 'test'. --- @class TestData --- @field status neotest.ResultStatus @@ -33,39 +29,32 @@ local lib = require("neotest-golang.lib") local M = {} ---- Process the results from the test command executing all tests in a ---- directory. +--- Process the results from the test command. --- @param spec neotest.RunSpec --- @param result neotest.StrategyResult --- @param tree neotest.Tree --- @return table function M.test_results(spec, result, tree) + -- TODO: refactor this function into function calls; return_early, process_test_results, override_test_results. + --- @type RunspecContext local context = spec.context - if context.parse_test_results == false then - ---@type table - local results = {} - results[context.pos_id] = { - ---@type neotest.ResultStatus + --- Final Neotest results, the way Neotest wants it returned. + --- @type table + local neotest_result = {} + + -- ////// RETURN EARLY FOR DAP DEBUGGING ////// + + if context.is_dap_active then + -- return early if test result processing is not desired. + neotest_result[context.pos_id] = { status = "skipped", } - return results -- return early, fail fast + return neotest_result end - --- The Neotest position tree node for this execution. - --- @type neotest.Position - local pos = tree:data() - - -- Sanity check - -- TODO: refactor so that we use pos.type and pos.id instead of passing them separately on the context - if options.get().dev_notifications == true then - if pos.id ~= context.pos_id then - logger.error( - "Neotest position id mismatch: " .. pos.id .. " vs " .. context.pos_id - ) - end - end + -- ////// PROCESS TEST RESULTS FOR ALL POSITIONS (NODES) EXECUTED ////// --- The runner to use for running tests. --- @type string @@ -81,41 +70,12 @@ function M.test_results(spec, result, tree) end logger.debug({ "Raw 'go test' output: ", raw_output }) - local gotest_output = lib.json.decode_from_table(raw_output) - logger.debug({ "Parsed 'go test' output: ", gotest_output }) - --- The 'go list -json' output, converted into a lua table. local golist_output = context.golist_data - logger.debug({ "Parsed 'go list' output: ", golist_output }) - - --- @type table - local neotest_result = {} - - --- Test command (e.g. 'go test') status. - --- @type neotest.ResultStatus - local test_command_status = "skipped" - if result.code == 0 then - test_command_status = "passed" - else - test_command_status = "failed" - end - --- Full 'go test' output (parsed from JSON). + --- Go test output. --- @type table - local o = {} - local test_command_output_path = vim.fs.normalize(async.fn.tempname()) - for _, line in ipairs(gotest_output) do - if line.Action == "output" then - table.insert(o, line.Output) - end - end - async.fn.writefile(o, test_command_output_path) - - -- register properties on the directory node that was run - neotest_result[pos.id] = { - status = test_command_status, - output = test_command_output_path, - } + local gotest_output = lib.json.decode_from_table(raw_output, true) --- Internal data structure to store test result data. --- @type table @@ -126,17 +86,80 @@ function M.test_results(spec, result, tree) -- show various warnings M.show_warnings(res) - -- Convert internal test result data into final Neotest result. - local test_results = M.to_neotest_result(spec, result, res, gotest_output) + -- convert internal test result data into Neotest result. + local test_results = M.to_neotest_result(res) for k, v in pairs(test_results) do neotest_result[k] = v end + -- ////// OVERRIDE TEST RESULTS FOR THE POSITION (NODE) EXECUTED ////// + + --- The Neotest position tree node for this execution. + --- @type neotest.Position + local pos = tree:data() + + --- Test command (e.g. 'go test') status. + --- @type neotest.ResultStatus + local result_status = nil + if neotest_result[pos.id] and neotest_result[pos.id].status == "skipped" then + -- keep the status if it was already decided to be skipped. + result_status = "skipped" + elseif context.errors ~= nil and #context.errors > 0 then + -- mark as failed if a non-gotest error occurred. + result_status = "failed" + elseif result.code > 0 then + -- mark as failed if the go test command failed. + result_status = "failed" + elseif result.code == 0 then + -- mark as passed if the 'go test' command passed. + result_status = "passed" + else + logger.error( + "Unexpected state when determining test status. Exit code was: " + .. result.code + ) + end + + -- override the position which was executed with the full + -- command execution output. + local cmd_output = M.filter_gotest_output(gotest_output) + cmd_output = vim.list_extend(context.errors or {}, cmd_output) + if #cmd_output == 0 and result.code ~= 0 and runner == "gotestsum" then + -- special case; gotestsum does not capture compilation errors from stderr. + cmd_output = { "Failed to run 'go test'. Compilation error?" } + end + local cmd_output_path = vim.fs.normalize(async.fn.tempname()) + async.fn.writefile(cmd_output, cmd_output_path) + if neotest_result[pos.id] == nil then + -- set status and output as none of them have yet to be set. + neotest_result[pos.id] = { + status = result_status, + output = cmd_output_path, + } + else + -- only override status and output, keep errors. + neotest_result[pos.id].status = result_status + neotest_result[pos.id].output = cmd_output_path + end + logger.debug({ "Final Neotest result data", neotest_result }) return neotest_result end +--- Filter on the Output-type parts of the 'go test' output. +--- @param gotest_output table +--- @return table +function M.filter_gotest_output(gotest_output) + local o = {} + for _, line in ipairs(gotest_output) do + if line.Action == "output" then + table.insert(o, line.Output) + end + end + return o +end + --- Aggregate neotest data and 'go test' output data. --- @param tree neotest.Tree --- @param gotest_output table @@ -352,12 +375,9 @@ function M.show_warnings(d) end --- Populate final Neotest results based on internal test result data. ---- @param spec neotest.RunSpec ---- @param result neotest.StrategyResult --- @param res table ---- @param gotest_output table --- @return table -function M.to_neotest_result(spec, result, res, gotest_output) +function M.to_neotest_result(res) --- Neotest results. --- @type table local neotest_result = {} diff --git a/lua/neotest-golang/runspec/dir.lua b/lua/neotest-golang/runspec/dir.lua index 237d0ac6..31f4e998 100644 --- a/lua/neotest-golang/runspec/dir.lua +++ b/lua/neotest-golang/runspec/dir.lua @@ -24,7 +24,15 @@ function M.build(pos) end local go_mod_folderpath = vim.fn.fnamemodify(go_mod_filepath, ":h") - local golist_data = lib.cmd.golist_data(go_mod_folderpath) + local golist_data, golist_error = lib.cmd.golist_data(go_mod_folderpath) + + local errors = nil + if golist_error ~= nil then + if errors == nil then + errors = {} + end + table.insert(errors, golist_error) + end -- find the go package that corresponds to the go_mod_folderpath local package_name = "./..." @@ -44,9 +52,8 @@ function M.build(pos) --- @type RunspecContext local context = { pos_id = pos.id, - pos_type = "dir", golist_data = golist_data, - parse_test_results = true, + errors = errors, test_output_json_filepath = json_filepath, } diff --git a/lua/neotest-golang/runspec/file.lua b/lua/neotest-golang/runspec/file.lua index 531304ec..be9a7957 100644 --- a/lua/neotest-golang/runspec/file.lua +++ b/lua/neotest-golang/runspec/file.lua @@ -23,7 +23,15 @@ function M.build(pos, tree) end local go_mod_folderpath = vim.fn.fnamemodify(go_mod_filepath, ":h") - local golist_data = lib.cmd.golist_data(go_mod_folderpath) + local golist_data, golist_error = lib.cmd.golist_data(go_mod_folderpath) + + local errors = nil + if golist_error ~= nil then + if errors == nil then + errors = {} + end + table.insert(errors, golist_error) + end -- find the go package that corresponds to the pos.path local package_name = "./..." @@ -58,9 +66,8 @@ function M.build(pos, tree) --- @type RunspecContext local context = { pos_id = pos.id, - pos_type = "file", golist_data = golist_data, - parse_test_results = true, + errors = errors, test_output_json_filepath = json_filepath, } @@ -79,9 +86,7 @@ function M.return_skipped(pos) --- @type RunspecContext local context = { pos_id = pos.id, - pos_type = "file", golist_data = {}, -- no golist output - parse_test_results = false, } --- Runspec designed for files that contain no tests. diff --git a/lua/neotest-golang/runspec/namespace.lua b/lua/neotest-golang/runspec/namespace.lua index 4165ea46..dcc5779b 100644 --- a/lua/neotest-golang/runspec/namespace.lua +++ b/lua/neotest-golang/runspec/namespace.lua @@ -1,5 +1,6 @@ --- Helpers to build the command and context around running all tests in a namespace. +local logger = require("neotest-golang.logging") local lib = require("neotest-golang.lib") local M = {} @@ -8,12 +9,20 @@ local M = {} --- @param pos neotest.Position --- @return neotest.RunSpec | neotest.RunSpec[] | nil function M.build(pos) - --- @type string local test_folder_absolute_path = string.match(pos.path, "(.+)" .. lib.find.os_path_sep) - local golist_data = lib.cmd.golist_data(test_folder_absolute_path) - --- @type string + local golist_data, golist_error = + lib.cmd.golist_data(test_folder_absolute_path) + + local errors = nil + if golist_error ~= nil then + if errors == nil then + errors = {} + end + table.insert(errors, golist_error) + end + local test_name = lib.convert.to_gotest_test_name(pos.id) test_name = lib.convert.to_gotest_regex_pattern(test_name) @@ -25,9 +34,8 @@ function M.build(pos) --- @type RunspecContext local context = { pos_id = pos.id, - pos_type = "namespace", golist_data = golist_data, - parse_test_results = true, + errors = errors, test_output_json_filepath = json_filepath, } diff --git a/lua/neotest-golang/runspec/test.lua b/lua/neotest-golang/runspec/test.lua index b9a3a058..1831178d 100644 --- a/lua/neotest-golang/runspec/test.lua +++ b/lua/neotest-golang/runspec/test.lua @@ -11,12 +11,20 @@ local M = {} --- @param strategy string --- @return neotest.RunSpec | neotest.RunSpec[] | nil function M.build(pos, strategy) - --- @type string local test_folder_absolute_path = string.match(pos.path, "(.+)" .. lib.find.os_path_sep) - local golist_data = lib.cmd.golist_data(test_folder_absolute_path) - --- @type string + local golist_data, golist_error = + lib.cmd.golist_data(test_folder_absolute_path) + + local errors = nil + if golist_error ~= nil then + if errors == nil then + errors = {} + end + table.insert(errors, golist_error) + end + local test_name = lib.convert.to_gotest_test_name(pos.id) local test_name_regex = lib.convert.to_gotest_regex_pattern(test_name) @@ -36,9 +44,9 @@ function M.build(pos, strategy) --- @type RunspecContext local context = { pos_id = pos.id, - pos_type = "test", golist_data = golist_data, - parse_test_results = true, + errors = errors, + process_test_results = true, test_output_json_filepath = json_filepath, } @@ -51,7 +59,7 @@ function M.build(pos, strategy) if runspec_strategy ~= nil then run_spec.strategy = runspec_strategy - run_spec.context.parse_test_results = false + run_spec.context.is_dap_active = true end logger.debug({ "RunSpec:", run_spec })