Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show compilation failure as test output #176

Merged
merged 15 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions lua/neotest-golang/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,25 +135,27 @@ end
--- @param tree neotest.Tree
--- @return table<string, neotest.Result> | 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)
Expand All @@ -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

Expand Down
37 changes: 27 additions & 10 deletions lua/neotest-golang/lib/cmd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions lua/neotest-golang/lib/json.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
150 changes: 85 additions & 65 deletions lua/neotest-golang/process.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> 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<string, string> The 'go list' JSON data (lua table).
--- @field errors? table<string> 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
Expand All @@ -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<string, neotest.Result>
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<string, neotest.Result>
local results = {}
results[context.pos_id] = {
---@type neotest.ResultStatus
--- Final Neotest results, the way Neotest wants it returned.
--- @type table<string, neotest.Result>
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
Expand All @@ -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<string, neotest.Result>
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<string, TestData>
Expand All @@ -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<string>
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
Expand Down Expand Up @@ -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<string, TestData>
--- @param gotest_output table
--- @return table<string, neotest.Result>
function M.to_neotest_result(spec, result, res, gotest_output)
function M.to_neotest_result(res)
--- Neotest results.
--- @type table<string, neotest.Result>
local neotest_result = {}
Expand Down
13 changes: 10 additions & 3 deletions lua/neotest-golang/runspec/dir.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "./..."
Expand All @@ -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,
}

Expand Down
Loading
Loading