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 build error in test output #219

Merged
merged 2 commits into from
Nov 30, 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
2 changes: 1 addition & 1 deletion lua/neotest-golang/lib/cmd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function M.golist_data(cwd)
if result.stdout ~= nil and result.stderr ~= "" then
err = err .. " " .. result.stderr
end
logger.debug({ "Go list error: ", err })
logger.warn({ "Go list error: ", err })
end

local output = result.stdout or ""
Expand Down
213 changes: 189 additions & 24 deletions lua/neotest-golang/process.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,49 @@ function M.test_results(spec, result, tree)
local runner = options.get().runner

--- The raw output from the test command.
--- @type table
local raw_output = {}
--- @type table<string>
local raw_output = async.fn.readfile(result.output)
--- @type table<string>
local runner_raw_output = {}
if runner == "go" then
raw_output = async.fn.readfile(result.output)
runner_raw_output = raw_output
elseif runner == "gotestsum" then
raw_output = async.fn.readfile(context.test_output_json_filepath)
runner_raw_output = async.fn.readfile(context.test_output_json_filepath)
end
logger.debug({ "Raw 'go test' output: ", raw_output })
logger.debug({ "Runner '" .. runner .. "', raw output: ", runner_raw_output })

--- The 'go list -json' output, converted into a lua table.
local golist_output = context.golist_data

--- Go test output.
--- @type table
local gotest_output = lib.json.decode_from_table(raw_output, true)
local gotest_output = lib.json.decode_from_table(runner_raw_output, true)

-- detect go list error
local golist_failed = false
if context.errors ~= nil and #context.errors > 0 then
golist_failed = true
end

-- detect build error
local build_failed = M.detect_build_errors(result, raw_output)
local build_failure_lookup = {}
if build_failed then
build_failure_lookup =
M.build_failure_lookup(raw_output, gotest_output, golist_output, runner)
end

--- Internal data structure to store test result data.
--- @type table<string, TestData>
local res = M.aggregate_data(tree, gotest_output, golist_output)
local res =
M.aggregate_data(tree, gotest_output, golist_output, build_failure_lookup)

logger.debug({ "Final internal test result data", res })

-- show various warnings
M.show_warnings(res)
-- show various warnings, unless `go list` failed or build failed.
if not golist_failed and not build_failed then
M.show_warnings(res)
end

-- convert internal test result data into Neotest result.
local test_results = M.to_neotest_result(res)
Expand All @@ -101,12 +120,17 @@ function M.test_results(spec, result, tree)
--- 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"
if build_failed then
-- mark as failed if the build failed.
result_status = "failed"
elseif context.errors ~= nil and #context.errors > 0 then
-- mark as failed if a non-gotest error occurred.
result_status = "failed"
elseif
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 result.code > 0 then
-- mark as failed if the go test command failed.
result_status = "failed"
Expand All @@ -122,14 +146,15 @@ function M.test_results(spec, result, tree)

-- override the position which was executed with the full
-- command execution output.
local cmd_output = M.filter_gotest_output(gotest_output)
local cmd_output =
M.filter_gotest_output(raw_output, gotest_output, build_failed, runner)
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

-- write output to final file.
local cmd_output_path = vim.fs.normalize(async.fn.tempname())
async.fn.writefile(cmd_output, cmd_output_path)

-- construct final result for the position.
if neotest_result[pos.id] == nil then
-- set status and output as none of them have yet to be set.
neotest_result[pos.id] = {
Expand All @@ -148,31 +173,130 @@ function M.test_results(spec, result, tree)
end

--- Filter on the Output-type parts of the 'go test' output.
--- @param gotest_output table
--- @param raw_output table<string>
--- @param gotest_output table<string>
--- @param build_failed boolean
--- @param runner string
--- @return table<string>
function M.filter_gotest_output(gotest_output)
function M.filter_gotest_output(raw_output, gotest_output, build_failed, runner)
local o = {}
for _, line in ipairs(gotest_output) do
if line.Action == "output" then
if options.get().colorize_test_output == true then

if not build_failed or (build_failed and runner == "go") then
for _, line in ipairs(gotest_output) do
if line.Action == "output" then
line.Output = M.colorizer(line.Output)
table.insert(o, line.Output)
end
end
else
if build_failed and runner == "gotestsum" then
for _, line in ipairs(raw_output) do
line = M.colorizer(line)
table.insert(o, line)
end
table.insert(o, line.Output)
end
end

return o
end

--- Detect build errors.
--- @param result neotest.StrategyResult
--- @param raw_output table<string>
--- @return boolean
function M.detect_build_errors(result, raw_output)
if result.code ~= 0 and #raw_output > 0 then
for _, line in pairs(raw_output) do
if string.find(line, "build failed", 1, true) then
return true
elseif string.find(line, "setup failed", 1, true) then
return true
elseif string.find(line, "#", 1, true) then
return true
end
end
end
return false
end

--- Build lookup table with package as key and directories as values.
--- @param raw_output table<string>
--- @param gotest_output table<string>
--- @param golist_output table
--- @param runner string
--- @return table<string, string[]>
function M.build_failure_lookup(
raw_output,
gotest_output,
golist_output,
runner
)
-- vim.notify(vim.inspect(build_failure_output))

local output = {}
if runner == "go" then
output = gotest_output
elseif runner == "gotestsum" then
output = raw_output
end

local failed_packages = {}
for _, value in pairs(output) do
-- if the output starts with '#'
if
runner == "go"
and value.Action == "output"
and string.find(value.Output, "#", 1, true)
then
local failed_package = vim.split(value.Output, " ")[2]
if not vim.tbl_contains(failed_packages, failed_package) then
table.insert(failed_packages, failed_package)
end
elseif runner == "gotestsum" and string.find(value, "#", 1, true) then
local failed_package = vim.split(value, " ")[2]
if not vim.tbl_contains(failed_packages, failed_package) then
table.insert(failed_packages, failed_package)
end
end
end

--- @type table<string, string[]>
local failed_package_lookup = {}
for _, data in ipairs(golist_output) do
for _, pkg in ipairs(failed_packages) do
if data.ImportPath == pkg then
-- vim.notify("Match: " .. data.ImportPath .. " == " .. pkg)
if failed_package_lookup[data.ImportPath] == nil then
failed_package_lookup[data.ImportPath] = {}
end

table.insert(failed_package_lookup[data.ImportPath], data.Dir)
end
end
end

-- vim.notify(vim.inspect(failed_package_lookup))

return failed_package_lookup
end

--- Aggregate neotest data and 'go test' output data.
--- @param tree neotest.Tree
--- @param gotest_output table
--- @param golist_output table
--- @param build_failure_lookup table<string, string[]>
--- @return table<string, TestData>
function M.aggregate_data(tree, gotest_output, golist_output)
function M.aggregate_data(
tree,
gotest_output,
golist_output,
build_failure_lookup
)
local res = M.gather_neotest_data_and_set_defaults(tree)
res =
M.decorate_with_go_package_and_test_name(res, gotest_output, golist_output)
res = M.decorate_with_go_test_results(res, gotest_output)
res = M.decorate_with_build_failures(res, build_failure_lookup)
return res
end

Expand Down Expand Up @@ -339,6 +463,47 @@ function M.decorate_with_go_test_results(res, gotest_output)
return res
end

function M.decorate_with_build_failures(res, build_failure_lookup)
-- vim.notify(vim.inspect(build_failure_lookup))

for pos_id, test_data in pairs(res) do
-- pos_id = '/Users/fredrik/code/public/neotest-golang/tests/go/positions_test.go::TestSubTestTableTestInlineStructLoop::"SubTest"'
--
-- test_data = {
-- duplicate_test_detected = false,
-- errors = {},
-- gotest_data = {
-- name = "",
-- output = {},
-- pkg = ""
-- },
-- neotest_data = {
-- id = '/Users/fredrik/code/public/neotest-golang/tests/go/positions_test.go::TestSubTestTableTestInlineStructLoop::"SubTest"',
-- name = '"SubTest"',
-- path = "/Users/fredrik/code/public/neotest-golang/tests/go/positions_test.go",
-- range = { 144, 1, 160, 3 },
-- type = "test"
-- },
-- status = "skipped"
-- }

local pos_dir = vim.fn.fnamemodify(test_data.neotest_data.path, ":h")
for pkg, parent_paths in pairs(build_failure_lookup) do
if vim.tbl_contains(parent_paths, pos_dir) then
test_data.status = "failed"
table.insert(test_data.errors, {
line = test_data.neotest_data.range[0],
message = "Build failed for package: " .. pkg,
})
end
end
end

-- vim.notify(vim.inspect(res))

return res
end

--- Colorize the test output based on the test result.
---
--- It will colorize the test output line based on the test result (PASS - green, FAIL - red, SKIP - yellow).
Expand Down
9 changes: 9 additions & 0 deletions tests/go/subpackage/subpackage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package subpackage

import "testing"

func TestSubpackage(t *testing.T) {
if (1 + 2) != 3 {
t.Fail()
}
}
4 changes: 4 additions & 0 deletions tests/go/testify/lookup_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ describe("Lookup", function()
package = "main",
replacements = {},
},
[folderpath .. "/subpackage/subpackage_test.go"] = {
package = "subpackage",
replacements = {},
},
[folderpath .. "/testify/othersuite_test.go"] = {
package = "testify",
replacements = {
Expand Down
Loading