-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for position type 'dir'
This adds substantial performance improvements on being able to run all tests in a Go project using one 'go test' command, instead of executing on a per-test basis.
- Loading branch information
1 parent
ee6cd4c
commit 993bb3d
Showing
6 changed files
with
497 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
local async = require("neotest.async") | ||
|
||
local convert = require("neotest-golang.convert") | ||
local json = require("neotest-golang.json") | ||
local utils = require("neotest-golang.utils") | ||
|
||
--- @class TestData | ||
--- @field status neotest.ResultStatus | ||
--- @field short? string Shortened output string | ||
--- @field errors? neotest.Error[] | ||
--- @field neotest_data neotest.Position | ||
--- @field gotest_data GoTestData | ||
--- @field duplicate_test_detected boolean | ||
|
||
--- @class GoTestData | ||
--- @field name string Go test name. | ||
--- @field pkg string Go package. | ||
--- @field output? string[] Go test output. | ||
|
||
local M = {} | ||
|
||
--- Process the results from the test command executing all tests in a | ||
--- directory. | ||
--- @param spec neotest.RunSpec | ||
--- @param result neotest.StrategyResult | ||
--- @param tree neotest.Tree | ||
--- @return table<string, neotest.Result> | ||
function M.results(spec, result, tree) | ||
--- The raw output from the 'go test -json' command. | ||
--- @type table | ||
local raw_output = async.fn.readfile(result.output) | ||
|
||
--- The 'go test' JSON output, converted into a lua table. | ||
--- @type table | ||
local gotest_output = json.process_json(raw_output) | ||
|
||
--- Internal data structure to store test result data. | ||
--- @type table<string, TestData> | ||
local res = M.aggregate_data(tree, gotest_output) | ||
|
||
M.show_warnings(res) | ||
|
||
-- DEBUG: enable the following to see the internal test result data. | ||
-- vim.notify(vim.inspect(res), vim.log.levels.DEBUG) | ||
|
||
local neotest_results = M.to_neotest_results(spec, result, res, gotest_output) | ||
|
||
-- DEBUG: enable the following to see the final Neotest result. | ||
-- vim.notify(vim.inspect(neotest_results), vim.log.levels.DEBUG) | ||
|
||
return neotest_results | ||
end | ||
|
||
--- Aggregate neotest data and 'go test' output data. | ||
--- @param tree neotest.Tree | ||
--- @param gotest_output table | ||
--- @return table<string, TestData> | ||
function M.aggregate_data(tree, gotest_output) | ||
local res = M.gather_neotest_data_and_set_defaults(tree) | ||
res = M.decorate_with_go_package_and_test_name(res, gotest_output) | ||
res = M.decorate_with_go_test_results(res, gotest_output) | ||
return res | ||
end | ||
|
||
--- Generate the internal test result data which will be used by neotest-golang | ||
--- before handing over the final results onto Neotest. | ||
--- @param tree neotest.Tree | ||
--- @return table<string, TestData> | ||
function M.gather_neotest_data_and_set_defaults(tree) | ||
--- Internal data structure to store test result data. | ||
--- @type table<string, TestData> | ||
local res = {} | ||
|
||
--- Table storing the name of the test (position.id) and the number of times | ||
--- it was found in the tree. | ||
--- @type table<string, number> | ||
local dupes = {} | ||
|
||
for _, node in tree:iter_nodes() do | ||
--- @type neotest.Position | ||
local pos = node:data() | ||
|
||
if pos.type == "test" then | ||
res[pos.id] = { | ||
status = "skipped", | ||
errors = {}, | ||
neotest_data = pos, | ||
gotest_data = { | ||
name = "", | ||
pkg = "", | ||
output = {}, | ||
}, | ||
duplicate_test_detected = false, | ||
} | ||
|
||
-- detect duplicate test names | ||
if dupes[pos.id] == nil then | ||
dupes[pos.id] = 1 | ||
else | ||
dupes[pos.id] = dupes[pos.id] + 1 | ||
res[pos.id].duplicate_test_detected = true | ||
end | ||
end | ||
end | ||
return res | ||
end | ||
|
||
--- Decorate the internal test result data with go package and test name. | ||
--- This is an important step to associate the test results with the tree nodes | ||
--- as the 'go test' JSON output contains keys 'Package' and 'Test'. | ||
--- @param res table<string, TestData> | ||
--- @param gotest_output table | ||
--- @return table<string, TestData> | ||
function M.decorate_with_go_package_and_test_name(res, gotest_output) | ||
for pos_id, test_data in pairs(res) do | ||
for _, line in ipairs(gotest_output) do | ||
if line.Action == "run" and line.Test ~= nil then | ||
local folderpath = vim.fn.fnamemodify(test_data.neotest_data.path, ":h") | ||
local match = nil | ||
local common_path = utils.find_common_path(line.Package, folderpath) | ||
|
||
if common_path ~= "" then | ||
local tweaked_pos_od = pos_id:gsub(" ", "_") | ||
tweaked_pos_od = tweaked_pos_od:gsub('"', "") | ||
tweaked_pos_od = tweaked_pos_od:gsub("::", "/") | ||
|
||
local combined_pattern = convert.to_lua_pattern(common_path) | ||
.. "/(.-)/" | ||
.. convert.to_lua_pattern(line.Test) | ||
.. "$" | ||
|
||
match = tweaked_pos_od:match(combined_pattern) | ||
end | ||
if match ~= nil then | ||
test_data.gotest_data.pkg = line.Package | ||
test_data.gotest_data.name = line.Test | ||
break -- avoid iterating over the rest of the 'go test' output lines | ||
end | ||
end | ||
end | ||
end | ||
|
||
return res | ||
end | ||
|
||
--- Decorate the internal test result data with data from the 'go test' output. | ||
--- @param res table<string, TestData> | ||
--- @param gotest_output table | ||
--- @return table<string, TestData> | ||
function M.decorate_with_go_test_results(res, gotest_output) | ||
for pos_id, test_data in pairs(res) do | ||
for _, line in ipairs(gotest_output) do | ||
if | ||
test_data.gotest_data.pkg == line.Package | ||
and test_data.gotest_data.name == line.Test | ||
then | ||
-- record test status | ||
if line.Action == "pass" then | ||
test_data.status = "passed" | ||
elseif line.Action == "fail" then | ||
test_data.status = "failed" | ||
elseif line.Action == "output" then | ||
test_data.gotest_data.output = | ||
vim.list_extend(test_data.gotest_data.output, { line.Output }) | ||
|
||
-- determine test filename | ||
local test_filename = "_test.go" -- approximate test filename | ||
if test_data.neotest_data ~= nil then | ||
-- node data is available, get the exact test filename | ||
local test_filepath = test_data.neotest_data.path | ||
test_filename = vim.fn.fnamemodify(test_filepath, ":t") | ||
end | ||
|
||
-- search for error message and line number | ||
local matched_line_number = | ||
string.match(line.Output, test_filename .. ":(%d+):") | ||
if matched_line_number ~= nil then | ||
local line_number = tonumber(matched_line_number) | ||
local message = | ||
string.match(line.Output, test_filename .. ":%d+: (.*)") | ||
if line_number ~= nil and message ~= nil then | ||
table.insert(test_data.errors, { | ||
line = line_number - 1, -- neovim lines are 0-indexed | ||
message = message, | ||
}) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
return res | ||
end | ||
|
||
--- Show warnings. | ||
--- @param d table<string, TestData> | ||
--- @return nil | ||
function M.show_warnings(d) | ||
-- warn if Go package/test is missing from tree node. | ||
-- TODO: make configurable to skip this or use different log level? | ||
for pos_id, test_data in pairs(d) do | ||
if test_data.gotest_data.pkg == "" or test_data.gotest_data.name == "" then | ||
vim.notify( | ||
"Unable to associate go package/test with neotest tree node: " .. pos_id, | ||
vim.log.levels.WARN | ||
) | ||
end | ||
end | ||
|
||
-- warn about duplicate tests | ||
-- TODO: make debug level configurable | ||
for pos_id, test_data in pairs(d) do | ||
if test_data.duplicate_test_detected == true then | ||
vim.notify( | ||
"Duplicate test name detected: " | ||
.. test_data.gotest_data.pkg | ||
.. "/" | ||
.. test_data.gotest_data.name, | ||
vim.log.levels.WARN | ||
) | ||
end | ||
end | ||
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_results(spec, result, res, gotest_output) | ||
--- Neotest results. | ||
--- @type table<string, neotest.Result> | ||
local neotest_results = {} | ||
|
||
-- populate all test results onto the Neotest format. | ||
for pos_id, test_data in pairs(res) do | ||
local test_output_path = vim.fs.normalize(async.fn.tempname()) | ||
async.fn.writefile(test_data.gotest_data.output, test_output_path) | ||
neotest_results[pos_id] = { | ||
status = test_data.status, | ||
errors = test_data.errors, | ||
output = test_output_path, -- NOTE: could be slow when running many tests? | ||
} | ||
end | ||
|
||
neotest_results = | ||
M.decorate_with_command_data(spec, result, gotest_output, neotest_results) | ||
|
||
return neotest_results | ||
end | ||
|
||
--- Decorate the final Neotest results with the data from the test command that | ||
--- was executed. | ||
--- @param spec neotest.RunSpec | ||
--- @param result neotest.StrategyResult | ||
--- @param gotest_output table | ||
--- @param neotest_results table<string, neotest.Result> | ||
--- @return table<string, neotest.Result> | ||
function M.decorate_with_command_data( | ||
spec, | ||
result, | ||
gotest_output, | ||
neotest_results | ||
) | ||
--- 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). | ||
--- @type table | ||
local full_output = {} | ||
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(full_output, line.Output) | ||
end | ||
end | ||
async.fn.writefile(full_output, test_command_output_path) | ||
|
||
-- register properties on the directory node that was run | ||
neotest_results[spec.context.id] = { | ||
status = test_command_status, | ||
output = test_command_output_path, | ||
} | ||
|
||
return neotest_results | ||
end | ||
|
||
return M |
Oops, something went wrong.