Skip to content

Commit

Permalink
feat: associate test-output/position with 'test list' output
Browse files Browse the repository at this point in the history
This vastly improves the support for position type 'dir', as Neotest
will now execute a Go package (and ./... when one cannot be detected).
  • Loading branch information
fredrikaverpil committed Jun 20, 2024
1 parent 41b1ca7 commit 8d053b4
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 108 deletions.
2 changes: 1 addition & 1 deletion lua/neotest-golang/convert.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ end
--- @param str string
function M.to_lua_pattern(str)
local special_characters = {
".",
"%",
".",
"+",
"*",
"-",
Expand Down
24 changes: 24 additions & 0 deletions lua/neotest-golang/json.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,28 @@ function M.process_json(raw_output)
return jsonlines
end

function M.parse_jsonlines(jsonlines)
-- Split the input into separate JSON objects
local json_objects = {}
local current_object = ""
for line in jsonlines:gmatch("[^\r\n]+") do
if line:match("^%s*{") and current_object ~= "" then
table.insert(json_objects, current_object)
current_object = ""
end
current_object = current_object .. line
end
table.insert(json_objects, current_object)

-- Parse each JSON object
local objects = {}
for _, json_object in ipairs(json_objects) do
local obj = vim.fn.json_decode(json_object)
table.insert(objects, obj)
end

-- Return the table of objects
return objects
end

return M
83 changes: 50 additions & 33 deletions lua/neotest-golang/results_dir.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function M.results(spec, result, tree)
--- @type table
local gotest_output = json.process_json(raw_output)

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

--- @type table<string, neotest.Result>
local neotest_result = {}

Expand Down Expand Up @@ -77,7 +80,7 @@ function M.results(spec, result, tree)

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

-- DEBUG: enable the following to see the internal test result data.
-- vim.notify(vim.inspect(res), vim.log.levels.DEBUG)
Expand All @@ -100,10 +103,12 @@ end
--- Aggregate neotest data and 'go test' output data.
--- @param tree neotest.Tree
--- @param gotest_output table
--- @param golist_output table
--- @return table<string, TestData>
function M.aggregate_data(spec, tree, gotest_output)
function M.aggregate_data(tree, gotest_output, golist_output)
local res = M.gather_neotest_data_and_set_defaults(tree)
res = M.decorate_with_go_package_and_test_name(spec, res, gotest_output)
res =
M.decorate_with_go_package_and_test_name(res, gotest_output, golist_output)
res = M.decorate_with_go_test_results(res, gotest_output)
return res
end
Expand Down Expand Up @@ -152,42 +157,54 @@ function M.gather_neotest_data_and_set_defaults(tree)
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 spec neotest.RunSpec
--- This is an important step, in which we figure out exactly which test output
--- belongs to which test in the Neotest position tree.
---
--- The strategy here is to loop over the Neotest position data, and figure out
--- which position belongs to a specific Go package (using the output from
--- 'go list -json').
--- @param res table<string, TestData>
--- @param gotest_output table
--- @param golist_output table
--- @return table<string, TestData>
function M.decorate_with_go_package_and_test_name(spec, res, gotest_output)
function M.decorate_with_go_package_and_test_name(
res,
gotest_output,
golist_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)
local tweaked_pos_id = pos_id:gsub(" ", "_")
tweaked_pos_id = tweaked_pos_id:gsub('"', "")
tweaked_pos_id = tweaked_pos_id:gsub("::", "/")
if common_path ~= "" then
-- tests not in the "main" package
local combined_pattern = convert.to_lua_pattern(common_path)
.. "/(.-)/"
.. convert.to_lua_pattern(line.Test)
.. "$"
match = tweaked_pos_id:find(combined_pattern, 1, false)
elseif common_path == "" then
-- tests in the "main" package
local pattern = convert.to_lua_pattern(spec.cwd)
.. "/(.-)/"
.. convert.to_lua_pattern(line.Test)
.. "$"
match = tweaked_pos_id:find(pattern, 1, false)
local match = nil
local folderpath = vim.fn.fnamemodify(test_data.neotest_data.path, ":h")
local tweaked_pos_id = pos_id:gsub(" ", "_")
tweaked_pos_id = tweaked_pos_id:gsub('"', "")
tweaked_pos_id = tweaked_pos_id:gsub("::", "/")

for _, golistline in ipairs(golist_output) do
if folderpath == golistline.Dir then
for _, gotestline in ipairs(gotest_output) do
if gotestline.Action == "run" and gotestline.Test ~= nil then
if gotestline.Package == golistline.ImportPath then
local pattern = convert.to_lua_pattern(folderpath)
.. "/(.-)/"
.. convert.to_lua_pattern(gotestline.Test)
.. "$"
match = tweaked_pos_id:find(pattern, 1, false)
if match ~= nil then
test_data.gotest_data.pkg = gotestline.Package
test_data.gotest_data.name = gotestline.Test
break
end
end
if match ~= nil then
break
end
end
if match ~= nil then
break
end
end

if match ~= nil then
test_data.gotest_data.pkg = line.Package
test_data.gotest_data.name = line.Test
break -- avoid iterating more JSON lines for this test
break
end
end
end
Expand Down
51 changes: 42 additions & 9 deletions lua/neotest-golang/runspec_dir.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local options = require("neotest-golang.options")
local json = require("neotest-golang.json")

local M = {}

Expand All @@ -13,6 +14,10 @@ local M = {}
function M.build(pos)
local go_mod_filepath = M.find_file_upwards("go.mod", pos.path)

-- if go_mod_filepath == nil then
-- go_mod_filepath = M.find_file_upwards("go.work", pos.path)
-- end

-- if no go.mod file was found up the directory tree, until reaching $CWD,
-- then we cannot determine the Go project root.
if go_mod_filepath == nil then
Expand All @@ -33,14 +38,28 @@ function M.build(pos)
local go_mod_folderpath = vim.fn.fnamemodify(go_mod_filepath, ":h")
local cwd = go_mod_folderpath

-- calculate the relative path to pos.path from cwd
local relative_path = M.remove_base_path(cwd, pos.path)
local test_pattern = "./..."
if relative_path ~= "" then
test_pattern = "./" .. relative_path .. "/..."
-- call 'go list -json ./...' to get test file data
local go_list_command = {
"go",
"list",
"-json",
"./...",
}
local go_list_command_result = vim.fn.system(
"cd " .. go_mod_folderpath .. " && " .. table.concat(go_list_command, " ")
)
local golist_output = json.parse_jsonlines(go_list_command_result)

-- find the go module that corresponds to the go_mod_folderpath
local module_name = "./..." -- if no go module, run all tests at the $CWD
for _, golist_item in ipairs(golist_output) do
if pos.path == golist_item.Dir then
module_name = golist_item.ImportPath
break
end
end

return M.build_dir_test_runspec(pos, cwd, test_pattern)
return M.build_dir_test_runspec(pos, cwd, golist_output, module_name)
end

--- Find a file upwards in the directory tree and return its path, if found.
Expand All @@ -62,6 +81,18 @@ function M.find_file_upwards(filename, start_path)
end
start_path = vim.fn.fnamemodify(start_path, ":h") -- go up one directory
end

if found_filepath == nil then
-- check if filename exists in the current directory
local files = scan.scan_dir(
start_path,
{ search_pattern = filename, hidden = true, depth = 1 }
)
if #files > 0 then
found_filepath = files[1]
end
end

return found_filepath
end

Expand All @@ -76,9 +107,10 @@ end
--- Build runspec for a directory of tests
--- @param pos neotest.Position
--- @param cwd string
--- @param test_pattern string
--- @param golist_output table
--- @param module_name string
--- @return neotest.RunSpec | neotest.RunSpec[] | nil
function M.build_dir_test_runspec(pos, cwd, test_pattern)
function M.build_dir_test_runspec(pos, cwd, golist_output, module_name)
local gotest = {
"go",
"test",
Expand All @@ -87,7 +119,7 @@ function M.build_dir_test_runspec(pos, cwd, test_pattern)

--- @type table
local required_go_test_args = {
test_pattern,
module_name,
}

local combined_args = vim.list_extend(
Expand All @@ -103,6 +135,7 @@ function M.build_dir_test_runspec(pos, cwd, test_pattern)
context = {
id = pos.id,
test_filepath = pos.path,
golist_output = golist_output,
pos_type = "dir",
},
}
Expand Down
19 changes: 0 additions & 19 deletions lua/neotest-golang/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,4 @@ function M.table_is_empty(t)
return next(t) == nil
end

--- Find the common path of two folderpaths.
--- @param path1 string
--- @param path2 string
--- @return string
function M.find_common_path(path1, path2)
local common = {}
local path1_parts = vim.split(path1, "/")
local path2_parts = vim.split(path2, "/")
for i = #path1_parts, 1, -1 do
if path1_parts[i] == path2_parts[#path2_parts] then
table.insert(common, 1, path1_parts[i])
table.remove(path2_parts)
else
break
end
end
return table.concat(common, "/")
end

return M
91 changes: 91 additions & 0 deletions tests/unit/json_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
local json = require("neotest-golang.json")
local _ = require("plenary")

describe("Go list", function()
-- it("Returns tables", function()
-- local input = {}
-- local expected = {}
-- assert.are_same(expected, json.parse_jsonlines(input))
-- end)

it("Returns one entry", function()
local input = [[{
"Dir": "foo"
}]]
local expected = { { Dir = "foo" } }
assert.are_same(
vim.inspect(expected),
vim.inspect(json.parse_jsonlines(input))
)
end)

it("Returns two entries", function()
local input = [[{
"Dir": "foo"
}
{
"Dir": "bar"
}
]]
local expected = { { Dir = "foo" }, { Dir = "bar" } }
assert.are_same(
vim.inspect(expected),
vim.inspect(json.parse_jsonlines(input))
)
end)

it("Returns three entries", function()
local input = [[{
"Dir": "foo"
}
{
"Dir": "bar"
}
{
"Dir": "baz"
}
]]
local expected = { { Dir = "foo" }, { Dir = "bar" }, { Dir = "baz" } }
assert.are_same(
vim.inspect(expected),
vim.inspect(json.parse_jsonlines(input))
)
end)
it("Returns three entries with multiple fields", function()
local input = [[{
"Dir": /Users/fredrik/code/public/neotest-golang/tests/go",
"Module": {
"Path": "github.com/fredrikaverpil/neotest-golang",
"Main": true,
"Dir": "/Users/fredrik/code/public/neotest-golang/tests/go",
"GoMod": "/Users/fredrik/code/public/neotest-golang/tests/go/go.mod",
"GoVersion": "1.22.2"
}
}
{
"Dir": "bar"
}
{
"Dir": "baz"
}
]]
local expected = {
{
Dir = "/Users/fredrik/code/public/neotest-golang/tests/go",
Module = {
Path = "github.com/fredrikaverpil/neotest-golang",
Main = true,
Dir = "/Users/fredrik/code/public/neotest-golang/tests/go",
GoMod = "/Users/fredrik/code/public/neotest-golang/tests/go/go.mod",
GoVersion = "1.22.2",
},
},
{ Dir = "bar" },
{ Dir = "baz" },
}
assert.are_same(
vim.inspect(expected),
vim.inspect(json.parse_jsonlines(input))
)
end)
end)
Loading

0 comments on commit 8d053b4

Please sign in to comment.