From 8d053b45e970bb173337e4c93c2acbfb070fcbd8 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 19 Jun 2024 23:50:05 +0200 Subject: [PATCH] feat: associate test-output/position with 'test list' output This vastly improves the support for position type 'dir', as Neotest will now execute a Go package (and ./... when one cannot be detected). --- lua/neotest-golang/convert.lua | 2 +- lua/neotest-golang/json.lua | 24 ++++++++ lua/neotest-golang/results_dir.lua | 83 ++++++++++++++++----------- lua/neotest-golang/runspec_dir.lua | 51 ++++++++++++++--- lua/neotest-golang/utils.lua | 19 ------- tests/unit/json_spec.lua | 91 ++++++++++++++++++++++++++++++ tests/unit/results_dir_spec.lua | 46 --------------- 7 files changed, 208 insertions(+), 108 deletions(-) create mode 100644 tests/unit/json_spec.lua delete mode 100644 tests/unit/results_dir_spec.lua diff --git a/lua/neotest-golang/convert.lua b/lua/neotest-golang/convert.lua index 89d8fba..567509c 100644 --- a/lua/neotest-golang/convert.lua +++ b/lua/neotest-golang/convert.lua @@ -37,8 +37,8 @@ end --- @param str string function M.to_lua_pattern(str) local special_characters = { - ".", "%", + ".", "+", "*", "-", diff --git a/lua/neotest-golang/json.lua b/lua/neotest-golang/json.lua index 7cbdfaf..fe97c31 100644 --- a/lua/neotest-golang/json.lua +++ b/lua/neotest-golang/json.lua @@ -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 diff --git a/lua/neotest-golang/results_dir.lua b/lua/neotest-golang/results_dir.lua index bffb209..37d24b6 100644 --- a/lua/neotest-golang/results_dir.lua +++ b/lua/neotest-golang/results_dir.lua @@ -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 local neotest_result = {} @@ -77,7 +80,7 @@ function M.results(spec, result, tree) --- Internal data structure to store test result data. --- @type table - 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) @@ -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 -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 @@ -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 --- @param gotest_output table +--- @param golist_output table --- @return table -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 diff --git a/lua/neotest-golang/runspec_dir.lua b/lua/neotest-golang/runspec_dir.lua index 7053cea..082fd2e 100644 --- a/lua/neotest-golang/runspec_dir.lua +++ b/lua/neotest-golang/runspec_dir.lua @@ -1,4 +1,5 @@ local options = require("neotest-golang.options") +local json = require("neotest-golang.json") local M = {} @@ -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 @@ -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. @@ -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 @@ -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", @@ -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( @@ -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", }, } diff --git a/lua/neotest-golang/utils.lua b/lua/neotest-golang/utils.lua index 63322a7..9ad7a85 100644 --- a/lua/neotest-golang/utils.lua +++ b/lua/neotest-golang/utils.lua @@ -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 diff --git a/tests/unit/json_spec.lua b/tests/unit/json_spec.lua new file mode 100644 index 0000000..d1c1511 --- /dev/null +++ b/tests/unit/json_spec.lua @@ -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) diff --git a/tests/unit/results_dir_spec.lua b/tests/unit/results_dir_spec.lua deleted file mode 100644 index b40b9e1..0000000 --- a/tests/unit/results_dir_spec.lua +++ /dev/null @@ -1,46 +0,0 @@ -local utils = require("neotest-golang.utils") - -describe("Common parts of Go package and folderpath", function() - it("Go package is repo", function() - local go_package = "github.com/fredrikaverpil/my-service" - local test_file_folderpath = "/Users/fredrik/code/work/private/my-service" - - local common_path = utils.find_common_path(go_package, test_file_folderpath) - assert.are_equal(common_path, "my-service") - end) - - it("Go package is repo sub-folder", function() - local go_package = "github.com/fredrikaverpil/my-service/backend" - local test_file_folderpath = - "/Users/fredrik/code/work/private/my-service/backend" - - local common_path = utils.find_common_path(go_package, test_file_folderpath) - assert.are_equal(common_path, "my-service/backend") - end) - - it("Go package is deep repo sub folder", function() - local go_package = - "github.com/fredrikaverpil/my-service/backend/internal/outbound/spanner" - local test_file_folderpath = - "/Users/fredrik/code/work/private/my-service/backend/internal/outbound/spanner" - - local common_path = utils.find_common_path(go_package, test_file_folderpath) - assert.are_equal( - common_path, - "my-service/backend/internal/outbound/spanner" - ) - end) - - it( - "Go package does not share a common path with the tests folderpath", - function() - local go_package = "github.com/fredrikaverpil/neotest-golang" - local test_file_folderpath = - "/Users/fredrik/code/work/public/neotest-golang/go/test" - - local common_path = - utils.find_common_path(go_package, test_file_folderpath) - assert.are_equal(common_path, "") - end - ) -end)