diff --git a/README.md b/README.md index e3ed4047..daa924c1 100644 --- a/README.md +++ b/README.md @@ -644,6 +644,17 @@ return { } ``` +## Custom test runners + +In neotest-golang, test runners are treated as first class citizens. The `go` +and `gotestsum` runners are provided out of the box and serves as examples of +how you could implement your own. See +[`options.lua`](lua/neotest-golang/options.lua) for more details. + +This enables supplying your own functions for building the test command and +processing the test output. This feature is not for the faint of heart, as test +output processing is usually a quite complex story. + ## 🙏 PRs are welcome Improvement suggestion PRs to this repo are very much welcome, and I encourage diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 8cd0e1ff..1f355f94 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -1,11 +1,9 @@ --- This is the main entry point for the neotest-golang adapter. It follows the --- Neotest interface: https://github.com/nvim-neotest/neotest/blob/master/lua/neotest/adapters/interface.lua -local logger = require("neotest-golang.logging") local options = require("neotest-golang.options") local query = require("neotest-golang.query") -local runspec = require("neotest-golang.runspec") -local process = require("neotest-golang.process") +local lib = require("neotest-golang.lib") local M = {} @@ -63,68 +61,8 @@ end --- @param args neotest.RunArgs --- @return neotest.RunSpec | neotest.RunSpec[] | nil function M.Adapter.build_spec(args) - --- The tree object, describing the AST-detected tests and their positions. - --- @type neotest.Tree - local tree = args.tree - - --- The position object, describing the current directory, file or test. - --- @type neotest.Position - local pos = args.tree:data() -- NOTE: causes is not accessible by the current user! - - if not tree then - logger.error("Unexpectedly did not receive a neotest.Tree.") - return - end - - -- Below is the main logic of figuring out how to execute tests. In short, - -- a "runspec" is defined for each command to execute. - -- Neotest also distinguishes between different "position types": - -- - "dir": A directory of tests - -- - "file": A single test file - -- - "namespace": A set of tests, collected under the same namespace - -- - "test": A single test - -- - -- If a valid runspec is built and returned from this function, it will be - -- executed by Neotest. But if, for some reason, this function returns nil, - -- Neotest will call this function again, but using the next position type - -- (in this order: dir, file, namespace, test). This gives the ability to - -- have fallbacks. - -- For example, if a runspec cannot be built for a file of tests, we can - -- instead try to build a runspec for each individual test file. The end - -- result would in this case produce multiple commands to execute (for each - -- test) rather than one command for the file. - -- The idea here is not to have such fallbacks take place in the future, but - -- while this adapter is being developed, it can be useful to have such - -- functionality. - - if pos.type == "dir" and pos.path == vim.fn.getcwd() then - -- A runspec is to be created, based on running all tests in the given - -- directory. In this case, the directory is also the current working - -- directory. - return runspec.dir.build(pos) - elseif pos.type == "dir" then - -- A runspec is to be created, based on running all tests in the given - -- directory. In this case, the directory is a sub-directory of the current - -- working directory. - return runspec.dir.build(pos) - elseif pos.type == "file" then - -- A runspec is to be created, based on on running all tests in the given - -- file. - return runspec.file.build(pos, tree, args.strategy) - elseif pos.type == "namespace" then - -- A runspec is to be created, based on running all tests in the given - -- namespace. - return runspec.namespace.build(pos) - elseif pos.type == "test" then - -- A runspec is to be created, based on on running the given test. - return runspec.test.build(pos, args.strategy) - end - - logger.error( - "Unknown Neotest position type, " - .. "cannot build runspec with position type: " - .. pos.type - ) + local runner = lib.cmd.runner_fallback(options.get().runner) + return options.get().runners[runner].build_spec(args) end --- Process the test command output and result. Populate test outcome into the @@ -135,59 +73,8 @@ end --- @param tree neotest.Tree --- @return table | nil function M.Adapter.results(spec, result, tree) - 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 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 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 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) - M.workaround_neotest_issue_391(result) - return results - end - - logger.error( - "Cannot process test results due to unknown Neotest position type:" - .. pos.type - ) -end - ---- Workaround, to avoid JSON in output panel, erase contents of output. ---- @param result neotest.StrategyResult -function M.workaround_neotest_issue_391(result) - -- FIXME: once output is processed, erase file contents, so to avoid JSON in - -- output panel. This is a workaround for now, only because of - -- https://github.com/nvim-neotest/neotest/issues/391 - - -- NOTE: when emptying the file with vim.fn.writefil, this error was hit - -- when debugging: - -- E5560: Vimscript function must not be called in a lua loop callback - -- vim.fn.writefile({ "" }, result.output) - - if result.output ~= nil then -- and vim.fn.filereadable(result.output) == 1 then - local file = io.open(result.output, "w") - if file ~= nil then - file:write("") - file:close() - end - end + local runner = lib.cmd.runner_fallback(options.get().runner) + return options.get().runners[runner].results(spec, result, tree) end --- Adapter options. diff --git a/lua/neotest-golang/lib/cmd.lua b/lua/neotest-golang/lib/cmd.lua index 92c6f153..5b4424fe 100644 --- a/lua/neotest-golang/lib/cmd.lua +++ b/lua/neotest-golang/lib/cmd.lua @@ -1,7 +1,5 @@ --- Helper functions building the command to execute. -local async = require("neotest.async") - local logger = require("neotest-golang.logging") local options = require("neotest-golang.options") local json = require("neotest-golang.lib.json") @@ -46,73 +44,38 @@ function M.golist_command() return cmd end -function M.test_command_in_package(package_or_path) - local go_test_required_args = { package_or_path } - local cmd, json_filepath = M.test_command(go_test_required_args) - return cmd, json_filepath -end - -function M.test_command_in_package_with_regexp(package_or_path, regexp) - local go_test_required_args = { package_or_path, "-run", regexp } - local cmd, json_filepath = M.test_command(go_test_required_args) - return cmd, json_filepath -end +--- @class TestCommandData +--- @field package_name string | nil The Go package name. +--- @field position neotest.Position The position of the test. +--- @field regexp string | nil The regular expression to filter tests. -function M.test_command(go_test_required_args) +--- Generate the test command to execute. +--- @param cmd_data TestCommandData +--- @return table, string | nil +function M.test_command(cmd_data) --- The runner to use for running tests. --- @type string local runner = M.runner_fallback(options.get().runner) - --- The filepath to write test output JSON to, if using `gotestsum`. + --- Optional and custom filepath for writing test output. --- @type string | nil - local json_filepath = nil + local test_output_filepath = nil --- The final test command to execute. --- @type table local cmd = {} - if runner == "go" then - cmd = M.go_test(go_test_required_args) - elseif runner == "gotestsum" then - json_filepath = vim.fs.normalize(async.fn.tempname()) - cmd = M.gotestsum(go_test_required_args, json_filepath) - end - + cmd, test_output_filepath = options.get().runners[runner].cmd(cmd_data) logger.info("Test command: " .. table.concat(cmd, " ")) - return cmd, json_filepath -end - -function M.go_test(go_test_required_args) - local cmd = { "go", "test", "-json" } - local args = options.get().go_test_args - if type(args) == "function" then - args = args() - end - cmd = vim.list_extend(vim.deepcopy(cmd), args) - cmd = vim.list_extend(vim.deepcopy(cmd), go_test_required_args) - return cmd -end - -function M.gotestsum(go_test_required_args, json_filepath) - local cmd = { "gotestsum", "--jsonfile=" .. json_filepath } - local gotestsum_args = options.get().gotestsum_args - if type(gotestsum_args) == "function" then - gotestsum_args = gotestsum_args() - end - local go_test_args = options.get().go_test_args - if type(go_test_args) == "function" then - go_test_args = go_test_args() - end - cmd = vim.list_extend(vim.deepcopy(cmd), gotestsum_args) - cmd = vim.list_extend(vim.deepcopy(cmd), { "--" }) - cmd = vim.list_extend(vim.deepcopy(cmd), go_test_args) - cmd = vim.list_extend(vim.deepcopy(cmd), go_test_required_args) - return cmd + return cmd, test_output_filepath end function M.runner_fallback(executable) if M.system_has(executable) == false then + logger.warn( + "Runner not found: " .. executable .. ". Will fall back to 'go'." + ) options.set({ runner = "go" }) return options.get().runner end diff --git a/lua/neotest-golang/options.lua b/lua/neotest-golang/options.lua index 0d0f7346..c5d6b3c1 100644 --- a/lua/neotest-golang/options.lua +++ b/lua/neotest-golang/options.lua @@ -6,8 +6,8 @@ local logger = require("neotest-golang.logging") local M = {} -local opts = { - runner = "go", -- or "gotestsum" +local defaults = { + runner = "go", -- corresponds to a key in the 'runners' table go_test_args = { "-v", "-race", "-count=1" }, -- NOTE: can also be a function gotestsum_args = { "--format=standard-verbose" }, -- NOTE: can also be a function go_list_args = {}, -- NOTE: can also be a function @@ -20,6 +20,59 @@ local opts = { dev_notifications = false, } +local runner_defaults = { + runners = { + go = { + build_spec = function(args) + local build_runspec = + require("neotest-golang.runners.gotest.build_runspec") + return build_runspec.build_gotest_spec(args) + end, + ---@param cmd_data TestCommandData + cmd = function(cmd_data) + local build_testcmd = + require("neotest-golang.runners.gotest.build_testcmd") + return build_testcmd.test_command_builder(cmd_data, defaults) + end, + output_filepath = function() + return nil + end, + results = function(spec, result, tree) + local process_output = + require("neotest-golang.runners.gotest.process_output") + return process_output.process_gotest_results(spec, result, tree) + end, + }, + gotestsum = { + build_spec = function(args) + -- gotestsum uses the same logic to build the runspec as the 'go' runner + local build_runspec = + require("neotest-golang.runners.gotest.build_runspec") + return build_runspec.build_gotest_spec(args) + end, + ---@param cmd_data TestCommandData + cmd = function(cmd_data) + local build_testcmd = + require("neotest-golang.runners.gotestsum.build_testcmd") + return build_testcmd.test_command_builder(cmd_data, defaults) + end, + output_filepath = function() + local async = require("neotest.async") + local json_filepath = vim.fs.normalize(async.fn.tempname()) + return json_filepath + end, + results = function(spec, result, tree) + -- gotestsum uses the same logic to process the results as the 'go' runner + local process_output = + require("neotest-golang.runners.gotest.process_output") + return process_output.process_gotest_results(spec, result, tree) + end, + }, + }, +} + +local opts = vim.tbl_extend("force", defaults, runner_defaults) + function M.setup(user_opts) if type(user_opts) == "table" and not vim.tbl_isempty(user_opts) then for k, v in pairs(user_opts) do diff --git a/lua/neotest-golang/runners/gotest/build_runspec.lua b/lua/neotest-golang/runners/gotest/build_runspec.lua new file mode 100644 index 00000000..fdaa241a --- /dev/null +++ b/lua/neotest-golang/runners/gotest/build_runspec.lua @@ -0,0 +1,75 @@ +local logger = require("neotest-golang.logging") + +local M = {} + +--- Build the runspec, which describes what command(s) are to be executed. +--- @param args neotest.RunArgs +--- @return neotest.RunSpec | neotest.RunSpec[] | nil +function M.build_gotest_spec(args) + --- The tree object, describing the AST-detected tests and their positions. + --- @type neotest.Tree + local tree = args.tree + + --- The position object, describing the current directory, file or test. + --- @type neotest.Position + local pos = args.tree:data() -- NOTE: causes is not accessible by the current user! + + if not tree then + logger.error("Unexpectedly did not receive a neotest.Tree.") + return + end + + -- Below is the main logic of figuring out how to execute tests. In short, + -- a "runspec" is defined for each command to execute. + -- Neotest also distinguishes between different "position types": + -- - "dir": A directory of tests + -- - "file": A single test file + -- - "namespace": A set of tests, collected under the same namespace + -- - "test": A single test + -- + -- If a valid runspec is built and returned from this function, it will be + -- executed by Neotest. But if, for some reason, this function returns nil, + -- Neotest will call this function again, but using the next position type + -- (in this order: dir, file, namespace, test). This gives the ability to + -- have fallbacks. + -- For example, if a runspec cannot be built for a file of tests, we can + -- instead try to build a runspec for each individual test file. The end + -- result would in this case produce multiple commands to execute (for each + -- test) rather than one command for the file. + -- The idea here is not to have such fallbacks take place in the future, but + -- while this adapter is being developed, it can be useful to have such + -- functionality. + + local runspec = require("neotest-golang.runners.gotest.runspec") + + if pos.type == "dir" and pos.path == vim.fn.getcwd() then + -- A runspec is to be created, based on running all tests in the given + -- directory. In this case, the directory is also the current working + -- directory. + return runspec.dir.build(pos) + elseif pos.type == "dir" then + -- A runspec is to be created, based on running all tests in the given + -- directory. In this case, the directory is a sub-directory of the current + -- working directory. + return runspec.dir.build(pos) + elseif pos.type == "file" then + -- A runspec is to be created, based on on running all tests in the given + -- file. + return runspec.file.build(pos, tree, args.strategy) + elseif pos.type == "namespace" then + -- A runspec is to be created, based on running all tests in the given + -- namespace. + return runspec.namespace.build(pos) + elseif pos.type == "test" then + -- A runspec is to be created, based on on running the given test. + return runspec.test.build(pos, args.strategy) + end + + logger.error( + "Unknown Neotest position type, " + .. "cannot build runspec with position type: " + .. pos.type + ) +end + +return M diff --git a/lua/neotest-golang/runners/gotest/build_testcmd.lua b/lua/neotest-golang/runners/gotest/build_testcmd.lua new file mode 100644 index 00000000..6682a1bb --- /dev/null +++ b/lua/neotest-golang/runners/gotest/build_testcmd.lua @@ -0,0 +1,31 @@ +local M = {} + +function M.test_command_builder(cmd_data, defaults) + local cmd = { "go", "test", "-json" } + local go_test_args = defaults.go_test_args + if type(go_test_args) == "function" then + go_test_args = go_test_args() + end + local required_go_test_args = {} + if + cmd_data.position.type == "test" + or cmd_data.position.type == "namespace" + then + local absolute_folder_path = + vim.fn.fnamemodify(cmd_data.position.path, ":h") + required_go_test_args = { absolute_folder_path, "-run", cmd_data.regexp } + elseif cmd_data.position.type == "file" then + if cmd_data.regexp ~= nil then + required_go_test_args = { cmd_data.package_name, "-run", cmd_data.regexp } + else + required_go_test_args = { cmd_data.package_name } + end + elseif cmd_data.position.type == "dir" then + required_go_test_args = { cmd_data.package_name } + end + cmd = vim.list_extend(vim.deepcopy(cmd), go_test_args) + cmd = vim.list_extend(vim.deepcopy(cmd), required_go_test_args) + return cmd, nil +end + +return M diff --git a/lua/neotest-golang/runners/gotest/init.lua b/lua/neotest-golang/runners/gotest/init.lua new file mode 100644 index 00000000..2d74e79b --- /dev/null +++ b/lua/neotest-golang/runners/gotest/init.lua @@ -0,0 +1,8 @@ +local M = {} + +M.build_runspec = require("neotest-golang.runners.gotest.build_runspec") +M.runspec = require("neotest-golang.runners.gotest.runspec") +M.cmd_data = require("neotest-golang.runners.gotest.cmd_data") +M.processing = require("neotest-golang.runners.gotest.processing") + +return M diff --git a/lua/neotest-golang/process.lua b/lua/neotest-golang/runners/gotest/process_output.lua similarity index 84% rename from lua/neotest-golang/process.lua rename to lua/neotest-golang/runners/gotest/process_output.lua index e09f96ff..89da2d90 100644 --- a/lua/neotest-golang/process.lua +++ b/lua/neotest-golang/runners/gotest/process_output.lua @@ -12,7 +12,7 @@ local lib = require("neotest-golang.lib") --- @field golist_data table The 'go list' JSON data (lua table). --- @field errors? table 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 test_output_filepath? string e.g. a JSON filepath for test runners which uses this. --- @class TestData --- @field status neotest.ResultStatus @@ -29,14 +29,47 @@ local lib = require("neotest-golang.lib") local M = {} +--- Output processing, assuming 'go test' output. +--- @async +--- @param spec neotest.RunSpec +--- @param result neotest.StrategyResult +--- @param tree neotest.Tree +--- @return table | nil +function M.process_gotest_results(spec, result, tree) + 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 = M.test_results(spec, result, tree) + return results + elseif pos.type == "file" then + -- A test command executed a file of tests and the output/status must + -- now be processed. + local results = M.test_results(spec, result, tree) + return results + elseif pos.type == "namespace" then + -- A test command executed a namespace and the output/status must now be + -- processed. + local results = M.test_results(spec, result, tree) + return results + elseif pos.type == "test" then + -- A test command executed a single test and the output/status must now be + -- processed. + local results = M.test_results(spec, result, tree) + return results + end + logger.error( + "Cannot process test results due to unknown Neotest position type:" + .. pos.type + ) +end + --- Process the results from the test command. --- @param spec neotest.RunSpec --- @param result neotest.StrategyResult --- @param tree neotest.Tree --- @return table 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 @@ -63,10 +96,12 @@ function M.test_results(spec, result, tree) --- The raw output from the test command. --- @type table local raw_output = {} - if runner == "go" then + if context.test_output_filepath ~= nil then + -- e.g. gotestsum output is in JSON format. + raw_output = async.fn.readfile(context.test_output_filepath) + else + -- 'go test' output is using neotest's result.output. raw_output = async.fn.readfile(result.output) - elseif runner == "gotestsum" then - raw_output = async.fn.readfile(context.test_output_json_filepath) end logger.debug({ "Raw 'go test' output: ", raw_output }) @@ -144,9 +179,32 @@ function M.test_results(spec, result, tree) logger.debug({ "Final Neotest result data", neotest_result }) + M.workaround_neotest_issue_391(result) + return neotest_result end +--- Workaround, to avoid JSON in output panel, erase contents of output. +--- @param result neotest.StrategyResult +function M.workaround_neotest_issue_391(result) + -- FIXME: once output is processed, erase file contents, so to avoid JSON in + -- output panel. This is a workaround for now, only because of + -- https://github.com/nvim-neotest/neotest/issues/391 + + -- NOTE: when emptying the file with vim.fn.writefil, this error was hit + -- when debugging: + -- E5560: Vimscript function must not be called in a lua loop callback + -- vim.fn.writefile({ "" }, result.output) + + if result.output ~= nil then -- and vim.fn.filereadable(result.output) == 1 then + local file = io.open(result.output, "w") + if file ~= nil then + file:write("") + file:close() + end + end +end + --- Filter on the Output-type parts of the 'go test' output. --- @param gotest_output table --- @return table diff --git a/lua/neotest-golang/runspec/dir.lua b/lua/neotest-golang/runners/gotest/runspec/dir.lua similarity index 89% rename from lua/neotest-golang/runspec/dir.lua rename to lua/neotest-golang/runners/gotest/runspec/dir.lua index 31f4e998..1dec3b29 100644 --- a/lua/neotest-golang/runspec/dir.lua +++ b/lua/neotest-golang/runners/gotest/runspec/dir.lua @@ -47,14 +47,20 @@ function M.build(pos) end end - local test_cmd, json_filepath = lib.cmd.test_command_in_package(package_name) + local cmd_data = { + package_name = package_name, + position = pos, + regexp = nil, + } + + local test_cmd, test_output_filepath = lib.cmd.test_command(cmd_data) --- @type RunspecContext local context = { pos_id = pos.id, golist_data = golist_data, errors = errors, - test_output_json_filepath = json_filepath, + test_output_filepath = test_output_filepath, } --- @type neotest.RunSpec diff --git a/lua/neotest-golang/runspec/file.lua b/lua/neotest-golang/runners/gotest/runspec/file.lua similarity index 88% rename from lua/neotest-golang/runspec/file.lua rename to lua/neotest-golang/runners/gotest/runspec/file.lua index 9f689cf5..b3b5a359 100644 --- a/lua/neotest-golang/runspec/file.lua +++ b/lua/neotest-golang/runners/gotest/runspec/file.lua @@ -39,7 +39,6 @@ function M.build(pos, tree, strategy) local package_name = "./..." local pos_path_filename = vim.fn.fnamemodify(pos.path, ":t") local pos_path_foldername = vim.fn.fnamemodify(pos.path, ":h") - for _, golist_item in ipairs(golist_data) do if golist_item.TestGoFiles ~= nil then if @@ -54,14 +53,24 @@ function M.build(pos, tree, strategy) -- find all top-level tests in pos.path local test_cmd = nil - local json_filepath = nil + local test_output_filepath = nil local regexp = M.get_regexp(pos.path) if regexp ~= nil then - test_cmd, json_filepath = - lib.cmd.test_command_in_package_with_regexp(package_name, regexp) + local cmd_data = { + package_name = package_name, + position = pos, + regexp = regexp, + } + test_cmd, test_output_filepath = lib.cmd.test_command(cmd_data) else -- fallback: run all tests in the package - test_cmd, json_filepath = lib.cmd.test_command_in_package(package_name) + local cmd_data = { + package_name = package_name, + position = pos, + regexp = nil, + } + test_cmd, test_output_filepath = + lib.cmd.test_command_in_package_with_regexp(cmd_data) -- NOTE: could also fall back to running on a per-test basis by using a bare return end @@ -78,7 +87,7 @@ function M.build(pos, tree, strategy) pos_id = pos.id, golist_data = golist_data, errors = errors, - test_output_json_filepath = json_filepath, + test_output_filepath = test_output_filepath, } --- @type neotest.RunSpec diff --git a/lua/neotest-golang/runners/gotest/runspec/init.lua b/lua/neotest-golang/runners/gotest/runspec/init.lua new file mode 100644 index 00000000..24a8ca23 --- /dev/null +++ b/lua/neotest-golang/runners/gotest/runspec/init.lua @@ -0,0 +1,10 @@ +--- Build the neotest.Runspec specification for a test execution. + +local M = {} + +M.dir = require("neotest-golang.runners.gotest.runspec.dir") +M.file = require("neotest-golang.runners.gotest.runspec.file") +M.namespace = require("neotest-golang.runners.gotest.runspec.namespace") +M.test = require("neotest-golang.runners.gotest.runspec.test") + +return M diff --git a/lua/neotest-golang/runspec/namespace.lua b/lua/neotest-golang/runners/gotest/runspec/namespace.lua similarity index 56% rename from lua/neotest-golang/runspec/namespace.lua rename to lua/neotest-golang/runners/gotest/runspec/namespace.lua index dcc5779b..7cb55194 100644 --- a/lua/neotest-golang/runspec/namespace.lua +++ b/lua/neotest-golang/runners/gotest/runspec/namespace.lua @@ -24,19 +24,37 @@ function M.build(pos) end local test_name = lib.convert.to_gotest_test_name(pos.id) - test_name = lib.convert.to_gotest_regex_pattern(test_name) + local test_name_regexp = lib.convert.to_gotest_regex_pattern(test_name) - local test_cmd, json_filepath = lib.cmd.test_command_in_package_with_regexp( - test_folder_absolute_path, - test_name - ) + -- find the go package that corresponds to the pos.path + local package_name = "./..." + local pos_path_filename = vim.fn.fnamemodify(pos.path, ":t") + local pos_path_foldername = vim.fn.fnamemodify(pos.path, ":h") + for _, golist_item in ipairs(golist_data) do + if golist_item.TestGoFiles ~= nil then + if + pos_path_foldername == golist_item.Dir + and vim.tbl_contains(golist_item.TestGoFiles, pos_path_filename) + then + package_name = golist_item.ImportPath + break + end + end + end + + local cmd_data = { + package_name = package_name, + position = pos, + regexp = test_name_regexp, + } + local test_cmd, test_output_filepath = lib.cmd.test_command(cmd_data) --- @type RunspecContext local context = { pos_id = pos.id, golist_data = golist_data, errors = errors, - test_output_json_filepath = json_filepath, + test_output_filepath = test_output_filepath, } --- @type neotest.RunSpec diff --git a/lua/neotest-golang/runspec/test.lua b/lua/neotest-golang/runners/gotest/runspec/test.lua similarity index 66% rename from lua/neotest-golang/runspec/test.lua rename to lua/neotest-golang/runners/gotest/runspec/test.lua index 208a83be..e15860aa 100644 --- a/lua/neotest-golang/runspec/test.lua +++ b/lua/neotest-golang/runners/gotest/runspec/test.lua @@ -26,12 +26,30 @@ function M.build(pos, strategy) end local test_name = lib.convert.to_gotest_test_name(pos.id) - local test_name_regex = lib.convert.to_gotest_regex_pattern(test_name) + local test_name_regexp = lib.convert.to_gotest_regex_pattern(test_name) - local test_cmd, json_filepath = lib.cmd.test_command_in_package_with_regexp( - test_folder_absolute_path, - test_name_regex - ) + -- find the go package that corresponds to the pos.path + local package_name = "./..." + local pos_path_filename = vim.fn.fnamemodify(pos.path, ":t") + local pos_path_foldername = vim.fn.fnamemodify(pos.path, ":h") + for _, golist_item in ipairs(golist_data) do + if golist_item.TestGoFiles ~= nil then + if + pos_path_foldername == golist_item.Dir + and vim.tbl_contains(golist_item.TestGoFiles, pos_path_filename) + then + package_name = golist_item.ImportPath + break + end + end + end + + local cmd_data = { + package_name = package_name, + position = pos, + regexp = test_name_regexp, + } + local test_cmd, test_output_filepath = lib.cmd.test_command(cmd_data) local runspec_strategy = nil if strategy == "dap" then @@ -47,7 +65,7 @@ function M.build(pos, strategy) golist_data = golist_data, errors = errors, process_test_results = true, - test_output_json_filepath = json_filepath, + test_output_filepath = test_output_filepath, } --- @type neotest.RunSpec diff --git a/lua/neotest-golang/runners/gotestsum/build_testcmd.lua b/lua/neotest-golang/runners/gotestsum/build_testcmd.lua new file mode 100644 index 00000000..c4ef3c45 --- /dev/null +++ b/lua/neotest-golang/runners/gotestsum/build_testcmd.lua @@ -0,0 +1,39 @@ +local M = {} + +function M.test_command_builder(cmd_data, defaults) + local async = require("neotest.async") + local json_filepath = vim.fs.normalize(async.fn.tempname()) + local cmd = { "gotestsum", "--jsonfile=" .. json_filepath } + local gotestsum_args = defaults.gotestsum_args + if type(gotestsum_args) == "function" then + gotestsum_args = gotestsum_args() + end + local go_test_args = defaults.go_test_args + if type(go_test_args) == "function" then + go_test_args = go_test_args() + end + local required_go_test_args = {} + if + cmd_data.position.type == "test" + or cmd_data.position.type == "namespace" + then + local absolute_folder_path = + vim.fn.fnamemodify(cmd_data.position.path, ":h") + required_go_test_args = { absolute_folder_path, "-run", cmd_data.regexp } + elseif cmd_data.position.type == "file" then + if cmd_data.regexp ~= nil then + required_go_test_args = { cmd_data.package_name, "-run", cmd_data.regexp } + else + required_go_test_args = { cmd_data.package_name } + end + elseif cmd_data.position.type == "dir" then + required_go_test_args = { cmd_data.package_name } + end + cmd = vim.list_extend(vim.deepcopy(cmd), gotestsum_args) + cmd = vim.list_extend(vim.deepcopy(cmd), { "--" }) + cmd = vim.list_extend(vim.deepcopy(cmd), go_test_args) + cmd = vim.list_extend(vim.deepcopy(cmd), required_go_test_args) + return cmd, json_filepath +end + +return M diff --git a/lua/neotest-golang/runners/gotestsum/init.lua b/lua/neotest-golang/runners/gotestsum/init.lua new file mode 100644 index 00000000..8e40ed8c --- /dev/null +++ b/lua/neotest-golang/runners/gotestsum/init.lua @@ -0,0 +1,5 @@ +local M = {} + +M.cmd_data = require("neotest-golang.runners.gotestsum.cmd_data") + +return M diff --git a/lua/neotest-golang/runspec/init.lua b/lua/neotest-golang/runspec/init.lua deleted file mode 100644 index 09b9edb4..00000000 --- a/lua/neotest-golang/runspec/init.lua +++ /dev/null @@ -1,10 +0,0 @@ ---- Build the neotest.Runspec specification for a test execution. - -local M = {} - -M.dir = require("neotest-golang.runspec.dir") -M.file = require("neotest-golang.runspec.file") -M.namespace = require("neotest-golang.runspec.namespace") -M.test = require("neotest-golang.runspec.test") - -return M diff --git a/tests/unit/options_spec.lua b/tests/unit/options_spec.lua index 199c6a26..e09cc339 100644 --- a/tests/unit/options_spec.lua +++ b/tests/unit/options_spec.lua @@ -3,6 +3,7 @@ local _ = require("plenary") describe("Options are set up", function() it("With defaults", function() + -- arrange local expected_options = { runner = "go", go_test_args = { @@ -20,11 +21,18 @@ describe("Options are set up", function() -- experimental dev_notifications = false, } + + -- act options.setup() - assert.are_same(expected_options, options.get()) + + -- assert + local actual_options = options.get() + expected_options.runners = actual_options.runners -- skip asserting runners + assert.are_same(expected_options, actual_options) end) it("With non-defaults", function() + -- arrange local expected_options = { runner = "go", go_test_args = { @@ -43,11 +51,18 @@ describe("Options are set up", function() -- experimental dev_notifications = false, } + + -- act options.setup(expected_options) - assert.are_same(expected_options, options.get()) + + -- assert + local actual_options = options.get() + expected_options.runners = actual_options.runners -- skip asserting runners + assert.are_same(expected_options, actual_options) end) it("With args as functions", function() + -- arrange local expected_options = { go_test_args = function() return { @@ -74,7 +89,13 @@ describe("Options are set up", function() end, dev_notifications = false, } + + -- act options.setup(expected_options) - assert.are_same(expected_options, options.get()) + + -- assert + local actual_options = options.get() + expected_options.runners = actual_options.runners -- skip asserting runners + assert.are_same(expected_options, actual_options) end) end)