From ffd87a33a9596e1cce64f0b1c3d600bbd20fdabf Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 3 Jul 2024 17:08:19 +0200 Subject: [PATCH 01/28] feat(poc): support running testify suite test function This uses vim.treesitter instead of nvim-treesitter. There are hacks injected all over the place and the solution is brittle. --- lua/neotest-golang/ast.lua | 66 ++++++++++++- lua/neotest-golang/init.lua | 7 ++ lua/neotest-golang/parse.lua | 21 +++- lua/neotest-golang/runspec_namespace.lua | 15 +++ lua/neotest-golang/runspec_test.lua | 15 ++- lua/neotest-golang/testify.lua | 121 +++++++++++++++++++++++ tests/go/go.mod | 8 ++ tests/go/go.sum | 10 ++ tests/go/testify_test.go | 36 +++++++ 9 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 lua/neotest-golang/runspec_namespace.lua create mode 100644 lua/neotest-golang/testify.lua create mode 100644 tests/go/testify_test.go diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 595bdeb..f2eebe8 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -2,6 +2,11 @@ local lib = require("neotest.lib") +local testify = require("neotest-golang.testify") + +local ts = require("nvim-treesitter.ts_utils") +local parsers = require("nvim-treesitter.parsers") + local M = {} --- Detect test names in Go *._test.go files. @@ -27,6 +32,16 @@ function M.detect_tests(file_path) name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition ]] + local receiver_method = [[ + ; query for receiver method, to be used as test suite namespace + (method_declaration + receiver: (parameter_list + (parameter_declaration + ; name: (identifier) + type: (pointer_type + (type_identifier) @namespace.name )))) @namespace.definition + ]] + local table_tests = [[ ;; query for list table tests (block @@ -127,13 +142,58 @@ function M.detect_tests(file_path) (#eq? @test.key.name @test.key.name1)))))))) ]] - local query = test_function .. test_method .. table_tests + local query = test_function .. test_method .. table_tests .. receiver_method local opts = { nested_tests = true } ---@type neotest.Tree - local positions = lib.treesitter.parse_positions(file_path, query, opts) + local tree = lib.treesitter.parse_positions(file_path, query, opts) + + -- HACK: code below for testify suite support. + -- TODO: hide functionality behind opt-in option. + local tree_with_merged_namespaces = + testify.merge_duplicate_namespaces(tree:root()) + local testify_query = [[ + ; query + (function_declaration ; [38, 0] - [40, 1] + name: (identifier) @testify.function_name ; [38, 5] - [38, 14] + ;parameters: (parameter_list ; [38, 14] - [38, 28] + ; (parameter_declaration ; [38, 15] - [38, 27] + ; name: (identifier) ; [38, 15] - [38, 16] + ; type: (pointer_type ; [38, 17] - [38, 27] + ; (qualified_type ; [38, 18] - [38, 27] + ; package: (package_identifier) ; [38, 18] - [38, 25] + ; name: (type_identifier))))) ; [38, 26] - [38, 27] + body: (block ; [38, 29] - [40, 1] + (expression_statement ; [39, 1] - [39, 34] + (call_expression ; [39, 1] - [39, 34] + function: (selector_expression ; [39, 1] - [39, 10] + operand: (identifier) @testify.module ; [39, 1] - [39, 6] + field: (field_identifier) @testify.run ) @testify.call ; [39, 7] - [39, 10] + arguments: (argument_list ; [39, 10] - [39, 34] + (identifier) @testify.t ; [39, 11] - [39, 12] + (call_expression ; [39, 14] - [39, 33] + function: (identifier) ; [39, 14] - [39, 17] + arguments: (argument_list ; [39, 17] - [39, 33] + (type_identifier) @testify.receiver ))))))) @testify.definition + ]] + + local testify_nodes = testify.run_query_on_file(file_path, testify_query) + + for test_function, data in pairs(testify_nodes) do + local function_name = nil + local receiver = nil + for _, node in ipairs(data) do + if node.name == "testify.function_name" then + function_name = node.text + end + if node.name == "testify.receiver" then + receiver = node.text + end + end + testify.add(file_path, function_name, receiver) -- FIXME: accumulates forever + end - return positions + return tree_with_merged_namespaces end return M diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index be641ff..6cb3f97 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -5,6 +5,7 @@ local options = require("neotest-golang.options") local ast = require("neotest-golang.ast") local runspec_dir = require("neotest-golang.runspec_dir") local runspec_file = require("neotest-golang.runspec_file") +local runspec_namespace = require("neotest-golang.runspec_namespace") local runspec_test = require("neotest-golang.runspec_test") local parse = require("neotest-golang.parse") @@ -115,6 +116,12 @@ function M.Adapter.build_spec(args) -- A runspec is to be created, based on on running all tests in the given -- file. return runspec_file.build(pos, tree) + 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) + return -- delegate to type 'test' 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) diff --git a/lua/neotest-golang/parse.lua b/lua/neotest-golang/parse.lua index 424e5df..730840a 100644 --- a/lua/neotest-golang/parse.lua +++ b/lua/neotest-golang/parse.lua @@ -6,6 +6,7 @@ local async = require("neotest.async") local options = require("neotest-golang.options") local convert = require("neotest-golang.convert") local json = require("neotest-golang.json") +local testify = require("neotest-golang.testify") -- TODO: remove pos_type when properly supporting all position types. -- and instead get this from the pos.type field. @@ -192,6 +193,22 @@ function M.gather_neotest_data_and_set_defaults(tree) return res end +local function hack(test_name) + -- HACK: replace receiver with suite for testify. + -- TODO: place this under opt-in option. + -- TODO: could make more efficient by matching on filename first? + for filename, data in pairs(testify.get()) do + for _, entry in ipairs(data) do + -- TODO: better, more reliable matching needed + if string.match(test_name, "^" .. entry.suite .. "/") then + test_name = string.gsub(test_name, entry.suite, entry.receiver) + return test_name + end + end + end + return test_name +end + --- Decorate the internal test result data with go package and test name. --- This is an important step, in which we figure out exactly which test output --- belongs to which test in the Neotest position tree. @@ -225,7 +242,7 @@ function M.decorate_with_go_package_and_test_name( if gotestline.Package == golistline.ImportPath then local pattern = convert.to_lua_pattern(folderpath) .. "/(.-)/" - .. convert.to_lua_pattern(gotestline.Test) + .. convert.to_lua_pattern(hack(gotestline.Test)) .. "$" match = tweaked_pos_id:find(pattern, 1, false) if match ~= nil then @@ -233,6 +250,8 @@ function M.decorate_with_go_package_and_test_name( test_data.gotest_data.name = gotestline.Test break end + + -- HACK: testify suites end if match ~= nil then break diff --git a/lua/neotest-golang/runspec_namespace.lua b/lua/neotest-golang/runspec_namespace.lua new file mode 100644 index 0000000..f2570c9 --- /dev/null +++ b/lua/neotest-golang/runspec_namespace.lua @@ -0,0 +1,15 @@ +local M = {} + +--- Build runspec for a namespace. +--- @param pos neotest.Position +--- @return neotest.RunSpec | neotest.RunSpec[] | nil +function M.build(pos) + -- vim.notify(vim.inspect(pos), vim.levels.log.DEBUG) -- FIXME: remove when done implementing/debugging + + -- TODO: Implement a runspec for a namespace of tests. + -- A bare return will delegate test execution to per-test execution, which + -- will have to do for now. + return +end + +return M diff --git a/lua/neotest-golang/runspec_test.lua b/lua/neotest-golang/runspec_test.lua index e1bb50c..7be3f95 100644 --- a/lua/neotest-golang/runspec_test.lua +++ b/lua/neotest-golang/runspec_test.lua @@ -4,6 +4,7 @@ local convert = require("neotest-golang.convert") local options = require("neotest-golang.options") local cmd = require("neotest-golang.cmd") local dap = require("neotest-golang.dap") +local testify = require("neotest-golang.testify") local M = {} @@ -16,8 +17,20 @@ function M.build(pos, strategy) local test_folder_absolute_path = string.match(pos.path, "(.+)/") local golist_data = cmd.golist_data(test_folder_absolute_path) + local pos_id = pos.id + + -- HACK: replace receiver with suite for testify. + -- TODO: place this under opt-in option. + for filename, data in pairs(testify.get()) do + for _, entry in ipairs(data) do + if string.match(pos_id, "::" .. entry.receiver .. "::") then + pos_id = string.gsub(pos_id, entry.receiver, entry.suite) + end + end + end + --- @type string - local test_name = convert.to_gotest_test_name(pos.id) + local test_name = convert.to_gotest_test_name(pos_id) test_name = convert.to_gotest_regex_pattern(test_name) local test_cmd, json_filepath = cmd.test_command_in_package_with_regexp( diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua new file mode 100644 index 0000000..f296101 --- /dev/null +++ b/lua/neotest-golang/testify.lua @@ -0,0 +1,121 @@ +local M = {} + +--- A lookup map between receiver method name and suite name. +--- Example: + +local lookup_map = {} + +function M.get() + return lookup_map +end + +function M.add(file_name, suite_name, receiver_name) + if not lookup_map[file_name] then + lookup_map[file_name] = {} + end + table.insert( + lookup_map[file_name], + { suite = suite_name, receiver = receiver_name } + ) +end + +function M.clear() + lookup_map = {} +end + +function M.merge_duplicate_namespaces(node) + if not node._children or #node._children == 0 then + return node + end + + local namespaces = {} + local new_children = {} + + for _, child in ipairs(node._children) do + if child._data.type == "namespace" then + local existing = namespaces[child._data.name] + if existing then + -- Merge children of duplicate namespace + for _, grandchild in ipairs(child._children) do + table.insert(existing._children, grandchild) + grandchild._parent = existing + end + else + namespaces[child._data.name] = child + table.insert(new_children, child) + end + else + table.insert(new_children, child) + end + end + + -- Recursively process children + for _, child in ipairs(new_children) do + M.merge_duplicate_namespaces(child) + end + + node._children = new_children + return node +end + +function M.find_parent_function(node) + while node do + if node:type() == "function_declaration" then + return node + end + node = node:parent() + end + return nil +end + +function M.get_function_name(func_node, content) + for child in func_node:iter_children() do + if child:type() == "identifier" then + return vim.treesitter.get_node_text(child, content) + end + end + return "anonymous" +end + +function M.run_query_on_file(filepath, query_string) + local file = io.open(filepath, "r") + if not file then + error("Could not open file: " .. filepath) + end + local content = file:read("*all") + file:close() + + local lang = "go" + local parser = vim.treesitter.get_string_parser(content, lang) + local tree = parser:parse()[1] + local root = tree:root() + + local query = vim.treesitter.query.parse(lang, query_string) + local matches = {} + + for id, node, metadata in query:iter_captures(root, content, 0, -1) do + local name = query.captures[id] + local text = vim.treesitter.get_node_text(node, content) + + local func_node = M.find_parent_function(node) + if func_node then + local func_name = M.get_function_name(func_node, content) + if not matches[func_name] then + matches[func_name] = {} + end + table.insert( + matches[func_name], + { name = name, node = node, text = text } + ) + else + if not matches["global"] then + matches["global"] = {} + end + table.insert(matches["global"], { name = name, node = node, text = text }) + end + end + + return matches +end + +return M diff --git a/tests/go/go.mod b/tests/go/go.mod index 7316961..e517f0f 100644 --- a/tests/go/go.mod +++ b/tests/go/go.mod @@ -1,3 +1,11 @@ module github.com/fredrikaverpil/neotest-golang go 1.22.2 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tests/go/go.sum b/tests/go/go.sum index e69de29..60ce688 100644 --- a/tests/go/go.sum +++ b/tests/go/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tests/go/testify_test.go b/tests/go/testify_test.go new file mode 100644 index 0000000..0adf90a --- /dev/null +++ b/tests/go/testify_test.go @@ -0,0 +1,36 @@ +package main + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + suite.Equal(5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} From b104f7af4c19969cc58e8e54517dbba0f16759bb Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Sun, 7 Jul 2024 11:03:14 +0200 Subject: [PATCH 02/28] fix(treesitter): use nvim-treesitter instead of vim.treesitter --- lua/neotest-golang/ast.lua | 5 +- lua/neotest-golang/testify.lua | 108 ++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index f2eebe8..9950757 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -4,9 +4,6 @@ local lib = require("neotest.lib") local testify = require("neotest-golang.testify") -local ts = require("nvim-treesitter.ts_utils") -local parsers = require("nvim-treesitter.parsers") - local M = {} --- Detect test names in Go *._test.go files. @@ -179,7 +176,7 @@ function M.detect_tests(file_path) local testify_nodes = testify.run_query_on_file(file_path, testify_query) - for test_function, data in pairs(testify_nodes) do + for test_fun, data in pairs(testify_nodes) do local function_name = nil local receiver = nil for _, node in ipairs(data) do diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index f296101..7a7a5c6 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -1,5 +1,33 @@ +local ts = require("nvim-treesitter.ts_utils") +local parsers = require("nvim-treesitter.parsers") + local M = {} +local testify_query = [[ + ; query + (function_declaration ; [38, 0] - [40, 1] + name: (identifier) @testify.function_name ; [38, 5] - [38, 14] + ;parameters: (parameter_list ; [38, 14] - [38, 28] + ; (parameter_declaration ; [38, 15] - [38, 27] + ; name: (identifier) ; [38, 15] - [38, 16] + ; type: (pointer_type ; [38, 17] - [38, 27] + ; (qualified_type ; [38, 18] - [38, 27] + ; package: (package_identifier) ; [38, 18] - [38, 25] + ; name: (type_identifier))))) ; [38, 26] - [38, 27] + body: (block ; [38, 29] - [40, 1] + (expression_statement ; [39, 1] - [39, 34] + (call_expression ; [39, 1] - [39, 34] + function: (selector_expression ; [39, 1] - [39, 10] + operand: (identifier) @testify.module ; [39, 1] - [39, 6] + field: (field_identifier) @testify.run ) @testify.call ; [39, 7] - [39, 10] + arguments: (argument_list ; [39, 10] - [39, 34] + (identifier) @testify.t ; [39, 11] - [39, 12] + (call_expression ; [39, 14] - [39, 33] + function: (identifier) ; [39, 14] - [39, 17] + arguments: (argument_list ; [39, 17] - [39, 33] + (type_identifier) @testify.receiver ))))))) @testify.definition + ]] + --- A lookup map between receiver method name and suite name. --- Example: @@ -58,63 +86,61 @@ function M.merge_duplicate_namespaces(node) return node end -function M.find_parent_function(node) - while node do - if node:type() == "function_declaration" then - return node - end - node = node:parent() +function M.get_node_text(node, bufnr) + local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter + if type(text) == "table" then + return table.concat(text, "\n") end - return nil -end - -function M.get_function_name(func_node, content) - for child in func_node:iter_children() do - if child:type() == "identifier" then - return vim.treesitter.get_node_text(child, content) - end - end - return "anonymous" + return text end function M.run_query_on_file(filepath, query_string) - local file = io.open(filepath, "r") - if not file then - error("Could not open file: " .. filepath) + -- Create a new buffer and set its content + local bufnr = vim.api.nvim_create_buf(false, true) + local content = vim.fn.readfile(filepath) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) + + -- Set the buffer's filetype to Go + vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) + + -- Ensure the Go parser is available + if not parsers.has_parser("go") then + error("Go parser is not available. Please ensure it's installed.") end - local content = file:read("*all") - file:close() - local lang = "go" - local parser = vim.treesitter.get_string_parser(content, lang) + -- Parse the buffer + local parser = parsers.get_parser(bufnr, "go") local tree = parser:parse()[1] local root = tree:root() - local query = vim.treesitter.query.parse(lang, query_string) + -- Create a query + local query = vim.treesitter.query.parse("go", query_string) + local matches = {} - for id, node, metadata in query:iter_captures(root, content, 0, -1) do - local name = query.captures[id] - local text = vim.treesitter.get_node_text(node, content) + for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do + local function_name = nil + local current_function = {} - local func_node = M.find_parent_function(node) - if func_node then - local func_name = M.get_function_name(func_node, content) - if not matches[func_name] then - matches[func_name] = {} - end - table.insert( - matches[func_name], - { name = name, node = node, text = text } - ) - else - if not matches["global"] then - matches["global"] = {} + for id, node in pairs(match) do + local name = query.captures[id] + local text = M.get_node_text(node, bufnr) + + if name == "testify.function_name" then + function_name = text end - table.insert(matches["global"], { name = name, node = node, text = text }) + + table.insert(current_function, { name = name, node = node, text = text }) + end + + if function_name then + matches[function_name] = current_function end end + -- Clean up: delete the temporary buffer + vim.api.nvim_buf_delete(bufnr, { force = true }) + return matches end From fe5e7f105b2256d196060acccb1bf02754a4e7b4 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Sun, 7 Jul 2024 15:56:24 +0200 Subject: [PATCH 03/28] feat: rebuild neotest tree, remove hacks This relies on AST-parsing of the *_test.go files ahead of running the adapter. --- README.md | 1 + lua/neotest-golang/ast.lua | 67 ++--- lua/neotest-golang/init.lua | 8 +- lua/neotest-golang/options.lua | 1 + lua/neotest-golang/parse.lua | 21 +- lua/neotest-golang/runspec_test.lua | 15 +- lua/neotest-golang/testify.lua | 380 ++++++++++++++++++++++------ tests/go/testify1_test.go | 41 +++ tests/go/testify2_test.go | 32 +++ tests/unit/options_spec.lua | 14 +- 10 files changed, 410 insertions(+), 170 deletions(-) create mode 100644 tests/go/testify1_test.go create mode 100644 tests/go/testify2_test.go diff --git a/README.md b/README.md index e434b2a..22378ed 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ return { | `go_test_args` | `{ "-v", "-race", "-count=1" }` | Arguments to pass into `go test`. | | `dap_go_enabled` | `false` | Leverage [leoluz/nvim-dap-go](https://github.com/leoluz/nvim-dap-go) for debugging tests. | | `dap_go_opts` | `{}` | Options to pass into `require("dap-go").setup()`. | +| `testify` | `false` | Enable support for [stretchr/testify](https://github.com/stretchr/testify) suites. | | `warn_test_name_dupes` | `true` | Warn about duplicate test names within the same Go package. | | `warn_test_not_executed` | `true` | Warn if test was not executed. | diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 9950757..accb0f2 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -2,14 +2,12 @@ local lib = require("neotest.lib") +local options = require("neotest-golang.options") local testify = require("neotest-golang.testify") local M = {} ---- Detect test names in Go *._test.go files. ---- @param file_path string -function M.detect_tests(file_path) - local test_function = [[ +local test_function = [[ ; query for test function ((function_declaration name: (identifier) @test.name) (#match? @test.name "^(Test|Example)")) @@ -23,13 +21,13 @@ function M.detect_tests(file_path) @test.definition ]] - local test_method = [[ +local test_method = [[ ; query for test method (method_declaration name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition ]] - local receiver_method = [[ +local receiver_method = [[ ; query for receiver method, to be used as test suite namespace (method_declaration receiver: (parameter_list @@ -39,7 +37,7 @@ function M.detect_tests(file_path) (type_identifier) @namespace.name )))) @namespace.definition ]] - local table_tests = [[ +local table_tests = [[ ;; query for list table tests (block (short_var_declaration @@ -139,58 +137,23 @@ function M.detect_tests(file_path) (#eq? @test.key.name @test.key.name1)))))))) ]] - local query = test_function .. test_method .. table_tests .. receiver_method +local query = test_function .. test_method .. table_tests .. receiver_method + +--- Detect test names in Go *._test.go files. +--- @param file_path string +function M.detect_tests(file_path) local opts = { nested_tests = true } ---@type neotest.Tree local tree = lib.treesitter.parse_positions(file_path, query, opts) - -- HACK: code below for testify suite support. - -- TODO: hide functionality behind opt-in option. - local tree_with_merged_namespaces = - testify.merge_duplicate_namespaces(tree:root()) - local testify_query = [[ - ; query - (function_declaration ; [38, 0] - [40, 1] - name: (identifier) @testify.function_name ; [38, 5] - [38, 14] - ;parameters: (parameter_list ; [38, 14] - [38, 28] - ; (parameter_declaration ; [38, 15] - [38, 27] - ; name: (identifier) ; [38, 15] - [38, 16] - ; type: (pointer_type ; [38, 17] - [38, 27] - ; (qualified_type ; [38, 18] - [38, 27] - ; package: (package_identifier) ; [38, 18] - [38, 25] - ; name: (type_identifier))))) ; [38, 26] - [38, 27] - body: (block ; [38, 29] - [40, 1] - (expression_statement ; [39, 1] - [39, 34] - (call_expression ; [39, 1] - [39, 34] - function: (selector_expression ; [39, 1] - [39, 10] - operand: (identifier) @testify.module ; [39, 1] - [39, 6] - field: (field_identifier) @testify.run ) @testify.call ; [39, 7] - [39, 10] - arguments: (argument_list ; [39, 10] - [39, 34] - (identifier) @testify.t ; [39, 11] - [39, 12] - (call_expression ; [39, 14] - [39, 33] - function: (identifier) ; [39, 14] - [39, 17] - arguments: (argument_list ; [39, 17] - [39, 33] - (type_identifier) @testify.receiver ))))))) @testify.definition - ]] - - local testify_nodes = testify.run_query_on_file(file_path, testify_query) - - for test_fun, data in pairs(testify_nodes) do - local function_name = nil - local receiver = nil - for _, node in ipairs(data) do - if node.name == "testify.function_name" then - function_name = node.text - end - if node.name == "testify.receiver" then - receiver = node.text - end - end - testify.add(file_path, function_name, receiver) -- FIXME: accumulates forever + if options.get().testify == true then + local tree_modified_for_testify = + testify.modify_neotest_tree(file_path, tree) + return tree_modified_for_testify end - return tree_with_merged_namespaces + return tree end return M diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 6cb3f97..2b17934 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -5,9 +5,9 @@ local options = require("neotest-golang.options") local ast = require("neotest-golang.ast") local runspec_dir = require("neotest-golang.runspec_dir") local runspec_file = require("neotest-golang.runspec_file") -local runspec_namespace = require("neotest-golang.runspec_namespace") local runspec_test = require("neotest-golang.runspec_test") local parse = require("neotest-golang.parse") +local testify = require("neotest-golang.testify") local M = {} @@ -195,6 +195,12 @@ end setmetatable(M.Adapter, { __call = function(_, opts) M.Adapter.options = options.setup(opts) + + -- FIXME: not the best place to put this. Does Neotest provide a callback? + if options.get().testify == true then + testify.generate_lookup_map() + end + return M.Adapter end, }) diff --git a/lua/neotest-golang/options.lua b/lua/neotest-golang/options.lua index 1a4f6df..b983052 100644 --- a/lua/neotest-golang/options.lua +++ b/lua/neotest-golang/options.lua @@ -10,6 +10,7 @@ local opts = { dap_go_opts = {}, warn_test_name_dupes = true, warn_test_not_executed = true, + testify = false, -- experimental, for now undocumented, options runner = "go", -- or "gotestsum" diff --git a/lua/neotest-golang/parse.lua b/lua/neotest-golang/parse.lua index 730840a..424e5df 100644 --- a/lua/neotest-golang/parse.lua +++ b/lua/neotest-golang/parse.lua @@ -6,7 +6,6 @@ local async = require("neotest.async") local options = require("neotest-golang.options") local convert = require("neotest-golang.convert") local json = require("neotest-golang.json") -local testify = require("neotest-golang.testify") -- TODO: remove pos_type when properly supporting all position types. -- and instead get this from the pos.type field. @@ -193,22 +192,6 @@ function M.gather_neotest_data_and_set_defaults(tree) return res end -local function hack(test_name) - -- HACK: replace receiver with suite for testify. - -- TODO: place this under opt-in option. - -- TODO: could make more efficient by matching on filename first? - for filename, data in pairs(testify.get()) do - for _, entry in ipairs(data) do - -- TODO: better, more reliable matching needed - if string.match(test_name, "^" .. entry.suite .. "/") then - test_name = string.gsub(test_name, entry.suite, entry.receiver) - return test_name - end - end - end - return test_name -end - --- Decorate the internal test result data with go package and test name. --- This is an important step, in which we figure out exactly which test output --- belongs to which test in the Neotest position tree. @@ -242,7 +225,7 @@ function M.decorate_with_go_package_and_test_name( if gotestline.Package == golistline.ImportPath then local pattern = convert.to_lua_pattern(folderpath) .. "/(.-)/" - .. convert.to_lua_pattern(hack(gotestline.Test)) + .. convert.to_lua_pattern(gotestline.Test) .. "$" match = tweaked_pos_id:find(pattern, 1, false) if match ~= nil then @@ -250,8 +233,6 @@ function M.decorate_with_go_package_and_test_name( test_data.gotest_data.name = gotestline.Test break end - - -- HACK: testify suites end if match ~= nil then break diff --git a/lua/neotest-golang/runspec_test.lua b/lua/neotest-golang/runspec_test.lua index 7be3f95..e1bb50c 100644 --- a/lua/neotest-golang/runspec_test.lua +++ b/lua/neotest-golang/runspec_test.lua @@ -4,7 +4,6 @@ local convert = require("neotest-golang.convert") local options = require("neotest-golang.options") local cmd = require("neotest-golang.cmd") local dap = require("neotest-golang.dap") -local testify = require("neotest-golang.testify") local M = {} @@ -17,20 +16,8 @@ function M.build(pos, strategy) local test_folder_absolute_path = string.match(pos.path, "(.+)/") local golist_data = cmd.golist_data(test_folder_absolute_path) - local pos_id = pos.id - - -- HACK: replace receiver with suite for testify. - -- TODO: place this under opt-in option. - for filename, data in pairs(testify.get()) do - for _, entry in ipairs(data) do - if string.match(pos_id, "::" .. entry.receiver .. "::") then - pos_id = string.gsub(pos_id, entry.receiver, entry.suite) - end - end - end - --- @type string - local test_name = convert.to_gotest_test_name(pos_id) + local test_name = convert.to_gotest_test_name(pos.id) test_name = convert.to_gotest_regex_pattern(test_name) local test_cmd, json_filepath = cmd.test_command_in_package_with_regexp( diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index 7a7a5c6..6bbc3a4 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -1,147 +1,369 @@ -local ts = require("nvim-treesitter.ts_utils") local parsers = require("nvim-treesitter.parsers") local M = {} +local lookup_map = {} -local testify_query = [[ +M.testify_query = [[ ; query - (function_declaration ; [38, 0] - [40, 1] - name: (identifier) @testify.function_name ; [38, 5] - [38, 14] - ;parameters: (parameter_list ; [38, 14] - [38, 28] - ; (parameter_declaration ; [38, 15] - [38, 27] - ; name: (identifier) ; [38, 15] - [38, 16] - ; type: (pointer_type ; [38, 17] - [38, 27] - ; (qualified_type ; [38, 18] - [38, 27] - ; package: (package_identifier) ; [38, 18] - [38, 25] - ; name: (type_identifier))))) ; [38, 26] - [38, 27] - body: (block ; [38, 29] - [40, 1] - (expression_statement ; [39, 1] - [39, 34] - (call_expression ; [39, 1] - [39, 34] - function: (selector_expression ; [39, 1] - [39, 10] - operand: (identifier) @testify.module ; [39, 1] - [39, 6] - field: (field_identifier) @testify.run ) @testify.call ; [39, 7] - [39, 10] - arguments: (argument_list ; [39, 10] - [39, 34] - (identifier) @testify.t ; [39, 11] - [39, 12] - (call_expression ; [39, 14] - [39, 33] - function: (identifier) ; [39, 14] - [39, 17] - arguments: (argument_list ; [39, 17] - [39, 33] - (type_identifier) @testify.receiver ))))))) @testify.definition + (package_clause + (package_identifier) @package) + (type_declaration + (type_spec + name: (type_identifier) @struct_name + type: (struct_type))) + (method_declaration + receiver: (parameter_list + (parameter_declaration + type: (pointer_type + (type_identifier) @receiver_type))) + name: (field_identifier) @method_name) + (function_declaration + name: (identifier) @function_name) + (call_expression + function: (selector_expression + operand: (identifier) @module + field: (field_identifier) @run (#eq? @run "Run")) + arguments: (argument_list + (identifier) + (call_expression + arguments: (argument_list + (type_identifier) @suite_receiver)))) ]] ---- A lookup map between receiver method name and suite name. ---- Example: +---@param file_path string +---@param tree neotest.Tree +function M.modify_neotest_tree(file_path, tree) + local lookup = M.get_lookup_map() + local file_lookup = lookup[file_path] -local lookup_map = {} + if file_lookup then + M.replace_receiver_with_suite(tree, file_lookup) + end + + -- Now merge the namespaces after applying the name changes + local tree_with_merged_namespaces = M.merge_duplicate_namespaces(tree:root()) + return tree_with_merged_namespaces +end + +function M.get_lookup_map() + -- FIXME: this is what the map looks like now. + local wrong = { + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { + { + receiver = "receiverStruct", + suite = "TestSuite", + }, + }, + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { + { + receiver = "receiverStruct2", + suite = "TestSuite2", + }, + }, + } + -- but we want this: + local right = { + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { + { + receiver = "receiverStruct", + suite = "TestSuite", + }, + { + receiver = "receiverStruct2", + suite = "TestSuite2", + }, + }, + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { + { + receiver = "receiverStruct", + suite = "TestSuite", + }, + { + receiver = "receiverStruct2", + suite = "TestSuite2", + }, + }, + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify_test.go"] = { + { + receiver = "ExampleTestSuite", + suite = "TestExampleTestSuite", + }, + }, + } + local current = { + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { + { + package = "main", + receiver = "receiverStruct", + suite = "TestSuite", + }, + }, + ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { + { + package = "main", + receiver = "receiverStruct2", + suite = "TestSuite2", + }, + }, + } + + -- local lookup_map = { + -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { + -- package = "main", + -- receivers = { "receiverStruct", "receiverStruct2" }, + -- suites = { + -- receiverStruct = "TestSuite", + -- receiverStruct2 = "TestSuite2", + -- }, + -- }, + -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { + -- package = "main", + -- receivers = { "receiverStruct", "receiverStruct2" }, + -- suites = { + -- receiverStruct = "TestSuite", + -- receiverStruct2 = "TestSuite2", + -- }, + -- }, + -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify_test.go"] = { + -- package = "main", + -- receivers = { "ExampleTestSuite" }, + -- suites = { + -- ExampleTestSuite = "TestExampleTestSuite", + -- }, + -- }, + -- } -function M.get() + if vim.tbl_isempty(lookup_map) then + lookup_map = M.generate_lookup_map() + end return lookup_map end -function M.add(file_name, suite_name, receiver_name) +function M.add_to_lookup_map(file_name, package_name, suite_name, receiver_name) if not lookup_map[file_name] then lookup_map[file_name] = {} end - table.insert( - lookup_map[file_name], - { suite = suite_name, receiver = receiver_name } - ) + local new_entry = { + package = package_name, + suite = suite_name, + receiver = receiver_name, + } + -- Check if entry already exists + for _, entry in ipairs(lookup_map[file_name]) do + if + entry.package == new_entry.package + and entry.suite == new_entry.suite + and entry.receiver == new_entry.receiver + then + return + end + end + table.insert(lookup_map[file_name], new_entry) end -function M.clear() +function M.clear_lookup_map() lookup_map = {} end -function M.merge_duplicate_namespaces(node) - if not node._children or #node._children == 0 then - return node - end +function M.generate_lookup_map() + local cwd = vim.fn.getcwd() + local go_files = M.get_go_files(cwd) + local lookup = {} + local all_receivers = {} + local all_suites = {} - local namespaces = {} - local new_children = {} + -- First pass: collect all receivers and suites + for _, filepath in ipairs(go_files) do + local matches = M.run_query_on_file(filepath, M.testify_query) + local package_name = matches.package + and matches.package[1] + and matches.package[1].text + or "unknown" - for _, child in ipairs(node._children) do - if child._data.type == "namespace" then - local existing = namespaces[child._data.name] - if existing then - -- Merge children of duplicate namespace - for _, grandchild in ipairs(child._children) do - table.insert(existing._children, grandchild) - grandchild._parent = existing + lookup[filepath] = { + package = package_name, + receivers = {}, + suites = {}, + } + + -- Collect all receivers + for _, struct in ipairs(matches.struct_name or {}) do + lookup[filepath].receivers[struct.text] = true + all_receivers[struct.text] = true + end + + -- Collect all test suite functions and their receivers + for _, func in ipairs(matches.function_name or {}) do + if func.text:match("^Test") then + for _, node in ipairs(matches.suite_receiver or {}) do + lookup[filepath].suites[node.text] = func.text + all_suites[node.text] = func.text end - else - namespaces[child._data.name] = child - table.insert(new_children, child) end - else - table.insert(new_children, child) end end - -- Recursively process children - for _, child in ipairs(new_children) do - M.merge_duplicate_namespaces(child) + -- Second pass: ensure all files have all receivers and suites + for filepath, file_data in pairs(lookup) do + for receiver, _ in pairs(all_receivers) do + if not file_data.receivers[receiver] then + file_data.receivers[receiver] = true + end + end + for receiver, suite in pairs(all_suites) do + if not file_data.suites[receiver] then + file_data.suites[receiver] = suite + end + end end - node._children = new_children - return node + return lookup end -function M.get_node_text(node, bufnr) - local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter - if type(text) == "table" then - return table.concat(text, "\n") +-- Function to get all .go files in a directory recursively +function M.get_go_files(directory) + local files = {} + local function scan_dir(dir) + local p = io.popen('find "' .. dir .. '" -type f -name "*_test.go"') + for file in p:lines() do + table.insert(files, file) + end end - return text + scan_dir(directory) + return files end function M.run_query_on_file(filepath, query_string) - -- Create a new buffer and set its content local bufnr = vim.api.nvim_create_buf(false, true) local content = vim.fn.readfile(filepath) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) - -- Set the buffer's filetype to Go vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) - -- Ensure the Go parser is available if not parsers.has_parser("go") then error("Go parser is not available. Please ensure it's installed.") end - -- Parse the buffer local parser = parsers.get_parser(bufnr, "go") local tree = parser:parse()[1] local root = tree:root() - -- Create a query local query = vim.treesitter.query.parse("go", query_string) local matches = {} - for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do - local function_name = nil - local current_function = {} + local function add_match(name, node) + if not matches[name] then + matches[name] = {} + end + table.insert( + matches[name], + { name = name, node = node, text = M.get_node_text(node, bufnr) } + ) + end + for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do for id, node in pairs(match) do local name = query.captures[id] - local text = M.get_node_text(node, bufnr) + add_match(name, node) + end + end + + vim.api.nvim_buf_delete(bufnr, { force = true }) + + return matches +end + +function M.get_node_text(node, bufnr) + local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter + if type(text) == "table" then + return table.concat(text, "\n") + end + return text +end + +function M.replace_receiver_with_suite(node, file_lookup) + local function replace_in_string(str, old, new) + return str + :gsub("::" .. old .. "::", "::" .. new .. "::") + :gsub("::" .. old .. "$", "::" .. new) + end + + local function update_node(n, replacements) + for old, new in pairs(replacements) do + if n._data.name == old then + n._data.name = new + end + n._data.id = replace_in_string(n._data.id, old, new) + end + end - if name == "testify.function_name" then - function_name = text + local function update_nodes_table(nodes, replacements) + local new_nodes = {} + for key, value in pairs(nodes) do + local new_key = key + for old, new in pairs(replacements) do + new_key = replace_in_string(new_key, old, new) end + new_nodes[new_key] = value + end + return new_nodes + end - table.insert(current_function, { name = name, node = node, text = text }) + local function recursive_update(n, replacements) + update_node(n, replacements) + n._nodes = update_nodes_table(n._nodes, replacements) + for _, child in ipairs(n:children()) do + recursive_update(child, replacements) end + end + + recursive_update(node, file_lookup.suites) - if function_name then - matches[function_name] = current_function + -- After updating all nodes, ensure parent-child relationships are correct + local function fix_relationships(n) + for _, child in ipairs(n:children()) do + child._parent = n + fix_relationships(child) end end - -- Clean up: delete the temporary buffer - vim.api.nvim_buf_delete(bufnr, { force = true }) + fix_relationships(node) +end - return matches +function M.merge_duplicate_namespaces(node) + if not node._children or #node._children == 0 then + return node + end + + local namespaces = {} + local new_children = {} + + for _, child in ipairs(node._children) do + if child._data.type == "namespace" then + local existing = namespaces[child._data.name] + if existing then + -- Merge children of duplicate namespace + for _, grandchild in ipairs(child._children) do + table.insert(existing._children, grandchild) + grandchild._parent = existing + end + else + namespaces[child._data.name] = child + table.insert(new_children, child) + end + else + table.insert(new_children, child) + end + end + + -- Recursively process children + for _, child in ipairs(new_children) do + M.merge_duplicate_namespaces(child) + end + + node._children = new_children + return node end return M diff --git a/tests/go/testify1_test.go b/tests/go/testify1_test.go new file mode 100644 index 0000000..415a518 --- /dev/null +++ b/tests/go/testify1_test.go @@ -0,0 +1,41 @@ +package main + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type receiverStruct struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *receiverStruct) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *receiverStruct) TestExample1() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + suite.Equal(5, suite.VariableThatShouldStartAtFive) +} + +func (suite *receiverStruct) TestExample2() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + suite.Equal(5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestSuite(t *testing.T) { + suite.Run(t, new(receiverStruct)) +} diff --git a/tests/go/testify2_test.go b/tests/go/testify2_test.go new file mode 100644 index 0000000..d387a13 --- /dev/null +++ b/tests/go/testify2_test.go @@ -0,0 +1,32 @@ +package main + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type receiverStruct2 struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +func (suite *receiverStruct2) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +func (suite *receiverStruct2) TestExample3() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + suite.Equal(5, suite.VariableThatShouldStartAtFive) +} + +func (suite *receiverStruct) TestExample4() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + suite.Equal(5, suite.VariableThatShouldStartAtFive) +} + +func TestSuite2(t *testing.T) { + suite.Run(t, new(receiverStruct2)) +} diff --git a/tests/unit/options_spec.lua b/tests/unit/options_spec.lua index acf3d50..f009a7b 100644 --- a/tests/unit/options_spec.lua +++ b/tests/unit/options_spec.lua @@ -5,15 +5,18 @@ describe("Options are set up", function() local expected_options = { dap_go_enabled = false, dap_go_opts = {}, - runner = "go", go_test_args = { "-v", "-race", "-count=1", }, - gotestsum_args = { "--format=standard-verbose" }, warn_test_name_dupes = true, warn_test_not_executed = true, + testify = false, + + -- experimental + runner = "go", + gotestsum_args = { "--format=standard-verbose" }, dev_notifications = false, } options.setup() @@ -24,16 +27,19 @@ describe("Options are set up", function() local expected_options = { dap_go_enabled = false, dap_go_opts = {}, - runner = "go", go_test_args = { "-v", "-race", "-count=1", "-parallel=1", -- non-default }, - gotestsum_args = { "--format=standard-verbose" }, warn_test_name_dupes = true, warn_test_not_executed = true, + testify = false, + + -- experimental + runner = "go", + gotestsum_args = { "--format=standard-verbose" }, dev_notifications = false, } options.setup(expected_options) From fea446316a38f2ea1a93c9fcb4f42e14f0217745 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Sun, 7 Jul 2024 17:30:26 +0200 Subject: [PATCH 04/28] fix: set namespace type, merge namespaces --- lua/neotest-golang/init.lua | 4 +- lua/neotest-golang/inspect.lua | 38 +++++ lua/neotest-golang/runspec_namespace.lua | 15 -- lua/neotest-golang/testify.lua | 169 +++++++++-------------- 4 files changed, 101 insertions(+), 125 deletions(-) create mode 100644 lua/neotest-golang/inspect.lua delete mode 100644 lua/neotest-golang/runspec_namespace.lua diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 2b17934..57c7bf4 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -119,9 +119,7 @@ function M.Adapter.build_spec(args) 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) - return -- delegate to type 'test' + return -- delegate to type 'test' as it requires the same logic 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) diff --git a/lua/neotest-golang/inspect.lua b/lua/neotest-golang/inspect.lua new file mode 100644 index 0000000..e01e0b9 --- /dev/null +++ b/lua/neotest-golang/inspect.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.deep(t, indent, done) + done = done or {} + indent = indent or "" + local output = {} + + for k, v in pairs(t) do + if type(v) == "table" and not done[v] then + done[v] = true + table.insert( + output, + indent .. tostring(k) .. ":\n" .. M.deep(v, indent .. " ", done) + ) + else + table.insert(output, indent .. tostring(k) .. ": " .. tostring(v)) + end + end + + return table.concat(output, "\n") +end + +function M.dump(o) + if type(o) == "table" then + local s = "{ " + for k, v in pairs(o) do + if type(k) ~= "number" then + k = '"' .. k .. '"' + end + s = s .. "[" .. k .. "] = " .. M.dump(v) .. "," + end + return s .. "} " + else + return tostring(o) + end +end + +return M diff --git a/lua/neotest-golang/runspec_namespace.lua b/lua/neotest-golang/runspec_namespace.lua deleted file mode 100644 index f2570c9..0000000 --- a/lua/neotest-golang/runspec_namespace.lua +++ /dev/null @@ -1,15 +0,0 @@ -local M = {} - ---- Build runspec for a namespace. ---- @param pos neotest.Position ---- @return neotest.RunSpec | neotest.RunSpec[] | nil -function M.build(pos) - -- vim.notify(vim.inspect(pos), vim.levels.log.DEBUG) -- FIXME: remove when done implementing/debugging - - -- TODO: Implement a runspec for a namespace of tests. - -- A bare return will delegate test execution to per-test execution, which - -- will have to do for now. - return -end - -return M diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index 6bbc3a4..e3c4439 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -1,3 +1,6 @@ +--- Opt-in functionality to support testify suites. +--- See https://github.com/stretchr/testify for more info. + local parsers = require("nvim-treesitter.parsers") local M = {} @@ -34,105 +37,17 @@ M.testify_query = [[ ---@param tree neotest.Tree function M.modify_neotest_tree(file_path, tree) local lookup = M.get_lookup_map() - local file_lookup = lookup[file_path] - if file_lookup then - M.replace_receiver_with_suite(tree, file_lookup) + if not lookup then + return tree end - -- Now merge the namespaces after applying the name changes + local modified_tree = M.replace_receiver_with_suite(tree:root(), lookup) local tree_with_merged_namespaces = M.merge_duplicate_namespaces(tree:root()) return tree_with_merged_namespaces end function M.get_lookup_map() - -- FIXME: this is what the map looks like now. - local wrong = { - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { - { - receiver = "receiverStruct", - suite = "TestSuite", - }, - }, - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { - { - receiver = "receiverStruct2", - suite = "TestSuite2", - }, - }, - } - -- but we want this: - local right = { - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { - { - receiver = "receiverStruct", - suite = "TestSuite", - }, - { - receiver = "receiverStruct2", - suite = "TestSuite2", - }, - }, - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { - { - receiver = "receiverStruct", - suite = "TestSuite", - }, - { - receiver = "receiverStruct2", - suite = "TestSuite2", - }, - }, - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify_test.go"] = { - { - receiver = "ExampleTestSuite", - suite = "TestExampleTestSuite", - }, - }, - } - local current = { - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { - { - package = "main", - receiver = "receiverStruct", - suite = "TestSuite", - }, - }, - ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { - { - package = "main", - receiver = "receiverStruct2", - suite = "TestSuite2", - }, - }, - } - - -- local lookup_map = { - -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify1_test.go"] = { - -- package = "main", - -- receivers = { "receiverStruct", "receiverStruct2" }, - -- suites = { - -- receiverStruct = "TestSuite", - -- receiverStruct2 = "TestSuite2", - -- }, - -- }, - -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify2_test.go"] = { - -- package = "main", - -- receivers = { "receiverStruct", "receiverStruct2" }, - -- suites = { - -- receiverStruct = "TestSuite", - -- receiverStruct2 = "TestSuite2", - -- }, - -- }, - -- ["/Users/fredrik/code/public/neotest-golang/tests/go/testify_test.go"] = { - -- package = "main", - -- receivers = { "ExampleTestSuite" }, - -- suites = { - -- ExampleTestSuite = "TestExampleTestSuite", - -- }, - -- }, - -- } - if vim.tbl_isempty(lookup_map) then lookup_map = M.generate_lookup_map() end @@ -166,11 +81,29 @@ function M.clear_lookup_map() end function M.generate_lookup_map() + -- local example = { + -- ["/path/to/file1_test.go"] = { + -- package = "main", + -- receivers = { receiverStruct = true, receiverStruct2 = true }, + -- suites = { + -- receiverStruct = "TestSuite", + -- receiverStruct2 = "TestSuite2", + -- }, + -- }, + -- ["/path/to/file2_test.go"] = { + -- package = "main", + -- receivers = { receiverStruct3 = true }, + -- suites = { + -- receiverStruct3 = "TestSuite3", + -- }, + -- }, + -- -- ... other files ... + -- } + local cwd = vim.fn.getcwd() local go_files = M.get_go_files(cwd) local lookup = {} - local all_receivers = {} - local all_suites = {} + local global_suites = {} -- First pass: collect all receivers and suites for _, filepath in ipairs(go_files) do @@ -189,7 +122,6 @@ function M.generate_lookup_map() -- Collect all receivers for _, struct in ipairs(matches.struct_name or {}) do lookup[filepath].receivers[struct.text] = true - all_receivers[struct.text] = true end -- Collect all test suite functions and their receivers @@ -197,7 +129,7 @@ function M.generate_lookup_map() if func.text:match("^Test") then for _, node in ipairs(matches.suite_receiver or {}) do lookup[filepath].suites[node.text] = func.text - all_suites[node.text] = func.text + global_suites[node.text] = func.text end end end @@ -205,16 +137,11 @@ function M.generate_lookup_map() -- Second pass: ensure all files have all receivers and suites for filepath, file_data in pairs(lookup) do - for receiver, _ in pairs(all_receivers) do - if not file_data.receivers[receiver] then + for receiver, suite in pairs(global_suites) do + if not file_data.receivers[receiver] and file_data.suites[receiver] then file_data.receivers[receiver] = true end end - for receiver, suite in pairs(all_suites) do - if not file_data.suites[receiver] then - file_data.suites[receiver] = suite - end - end end return lookup @@ -283,16 +210,23 @@ function M.get_node_text(node, bufnr) end function M.replace_receiver_with_suite(node, file_lookup) + if not file_lookup then + return node + end + local function replace_in_string(str, old, new) return str :gsub("::" .. old .. "::", "::" .. new .. "::") :gsub("::" .. old .. "$", "::" .. new) end - local function update_node(n, replacements) + local function update_node(n, replacements, suite_names) for old, new in pairs(replacements) do if n._data.name == old then n._data.name = new + n._data.type = "namespace" + elseif suite_names[n._data.name] then + n._data.type = "namespace" end n._data.id = replace_in_string(n._data.id, old, new) end @@ -310,15 +244,34 @@ function M.replace_receiver_with_suite(node, file_lookup) return new_nodes end - local function recursive_update(n, replacements) - update_node(n, replacements) + local function recursive_update(n, replacements, suite_names) + update_node(n, replacements, suite_names) n._nodes = update_nodes_table(n._nodes, replacements) for _, child in ipairs(n:children()) do - recursive_update(child, replacements) + recursive_update(child, replacements, suite_names) + end + end + + -- Create a global replacements table and suite names set + local global_replacements = {} + local suite_names = {} + for file_path, file_data in pairs(file_lookup) do + if file_data.suites then + for receiver, suite in pairs(file_data.suites) do + global_replacements[receiver] = suite + suite_names[suite] = true + end + else + -- no suites found for file end end - recursive_update(node, file_lookup.suites) + if vim.tbl_isempty(global_replacements) then + -- no replacements found + return node + end + + recursive_update(node, global_replacements, suite_names) -- After updating all nodes, ensure parent-child relationships are correct local function fix_relationships(n) @@ -329,6 +282,8 @@ function M.replace_receiver_with_suite(node, file_lookup) end fix_relationships(node) + + return node end function M.merge_duplicate_namespaces(node) From 182ef503c9a6922193108a1f585a1eaefff0b8b8 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 8 Jul 2024 07:55:05 +0200 Subject: [PATCH 05/28] fix(E565): Not allowed to change text or change window --- lua/neotest-golang/init.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 57c7bf4..c8297f4 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -16,6 +16,11 @@ local M = {} --- @field name string M.Adapter = { name = "neotest-golang", + init = function() + if options.get().testify == true then + testify.generate_lookup_map() + end + end, } --- Find the project root directory given a current directory to work from. @@ -193,12 +198,6 @@ end setmetatable(M.Adapter, { __call = function(_, opts) M.Adapter.options = options.setup(opts) - - -- FIXME: not the best place to put this. Does Neotest provide a callback? - if options.get().testify == true then - testify.generate_lookup_map() - end - return M.Adapter end, }) From 9129da8c3c4668195084e719b439fa37c6213136 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 8 Jul 2024 22:13:19 +0200 Subject: [PATCH 06/28] fix: receiver method detection as opt-in --- lua/neotest-golang/ast.lua | 24 +++++++++--------------- lua/neotest-golang/testify.lua | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index accb0f2..19ddc8d 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -7,7 +7,7 @@ local testify = require("neotest-golang.testify") local M = {} -local test_function = [[ +M.test_function = [[ ; query for test function ((function_declaration name: (identifier) @test.name) (#match? @test.name "^(Test|Example)")) @@ -21,23 +21,13 @@ local test_function = [[ @test.definition ]] -local test_method = [[ +M.test_method = [[ ; query for test method (method_declaration name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition ]] -local receiver_method = [[ - ; query for receiver method, to be used as test suite namespace - (method_declaration - receiver: (parameter_list - (parameter_declaration - ; name: (identifier) - type: (pointer_type - (type_identifier) @namespace.name )))) @namespace.definition - ]] - -local table_tests = [[ +M.table_tests = [[ ;; query for list table tests (block (short_var_declaration @@ -137,12 +127,16 @@ local table_tests = [[ (#eq? @test.key.name @test.key.name1)))))))) ]] -local query = test_function .. test_method .. table_tests .. receiver_method - --- Detect test names in Go *._test.go files. --- @param file_path string function M.detect_tests(file_path) local opts = { nested_tests = true } + local query = M.test_function .. M.test_method .. M.table_tests + + if options.get().testify == true then + -- only detect receiver methods if testify is enabled, to avoid confusion + query = query .. testify.receiver_method_query + end ---@type neotest.Tree local tree = lib.treesitter.parse_positions(file_path, query, opts) diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index e3c4439..93c6a64 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -6,8 +6,8 @@ local parsers = require("nvim-treesitter.parsers") local M = {} local lookup_map = {} -M.testify_query = [[ - ; query +M.lookup_query = [[ + ; query for detecting package, struct and test suite, for use in lookup. (package_clause (package_identifier) @package) (type_declaration @@ -33,6 +33,16 @@ M.testify_query = [[ (type_identifier) @suite_receiver)))) ]] +M.receiver_method_query = [[ + ; query for receiver method, to be used as test suite namespace. + (method_declaration + receiver: (parameter_list + (parameter_declaration + ; name: (identifier) + type: (pointer_type + (type_identifier) @namespace.name )))) @namespace.definition + ]] + ---@param file_path string ---@param tree neotest.Tree function M.modify_neotest_tree(file_path, tree) @@ -107,7 +117,7 @@ function M.generate_lookup_map() -- First pass: collect all receivers and suites for _, filepath in ipairs(go_files) do - local matches = M.run_query_on_file(filepath, M.testify_query) + local matches = M.run_query_on_file(filepath, M.lookup_query) local package_name = matches.package and matches.package[1] and matches.package[1].text From 79f651f0c5b5e9e3bdcdf55a849ae328f8bee959 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 8 Jul 2024 23:10:20 +0200 Subject: [PATCH 07/28] fix: provide output for namespace execution --- lua/neotest-golang/init.lua | 9 ++++- lua/neotest-golang/runspec_namespace.lua | 46 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 lua/neotest-golang/runspec_namespace.lua diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index c8297f4..24cab3b 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -5,6 +5,7 @@ local options = require("neotest-golang.options") local ast = require("neotest-golang.ast") local runspec_dir = require("neotest-golang.runspec_dir") local runspec_file = require("neotest-golang.runspec_file") +local runspec_namespace = require("neotest-golang.runspec_namespace") local runspec_test = require("neotest-golang.runspec_test") local parse = require("neotest-golang.parse") local testify = require("neotest-golang.testify") @@ -124,7 +125,7 @@ function M.Adapter.build_spec(args) elseif pos.type == "namespace" then -- A runspec is to be created, based on running all tests in the given -- namespace. - return -- delegate to type 'test' as it requires the same logic + 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) @@ -158,6 +159,12 @@ function M.Adapter.results(spec, result, tree) local results = parse.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) return results + elseif spec.context.pos_type == "namespace" then + -- A test command executed a single test and the output/status must now be + -- processed. + local results = parse.test_results(spec, result, tree) + M.workaround_neotest_issue_391(result) + return results elseif spec.context.pos_type == "test" then -- A test command executed a single test and the output/status must now be -- processed. diff --git a/lua/neotest-golang/runspec_namespace.lua b/lua/neotest-golang/runspec_namespace.lua new file mode 100644 index 0000000..a0d6e52 --- /dev/null +++ b/lua/neotest-golang/runspec_namespace.lua @@ -0,0 +1,46 @@ +--- Helpers to build the command and context around running a single test. + +local convert = require("neotest-golang.convert") +local cmd = require("neotest-golang.cmd") + +local M = {} + +--- Build runspec for a single test +--- @param pos neotest.Position +--- @return neotest.RunSpec | neotest.RunSpec[] | nil +function M.build(pos) + --- @type string + local test_folder_absolute_path = string.match(pos.path, "(.+)/") + local golist_data = cmd.golist_data(test_folder_absolute_path) + + --- @type string + local test_name = convert.to_gotest_test_name(pos.id) + test_name = convert.to_gotest_regex_pattern(test_name) + + local test_cmd, json_filepath = cmd.test_command_in_package_with_regexp( + test_folder_absolute_path, + test_name + ) + + vim.notify(vim.inspect(test_cmd)) + + --- @type RunspecContext + local context = { + pos_id = pos.id, + pos_type = "namespace", + golist_data = golist_data, + parse_test_results = true, + test_output_json_filepath = json_filepath, + } + + --- @type neotest.RunSpec + local run_spec = { + command = test_cmd, + cwd = test_folder_absolute_path, + context = context, + } + + return run_spec +end + +return M From 54be4336a80f96969016ca1b9808c7e0a2edf4fe Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 8 Jul 2024 23:11:01 +0200 Subject: [PATCH 08/28] feat: support alternative receiver/suite syntax --- lua/neotest-golang/testify.lua | 42 +++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index 93c6a64..b426a56 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -31,6 +31,23 @@ M.lookup_query = [[ (call_expression arguments: (argument_list (type_identifier) @suite_receiver)))) + +; capture variable declarations +(short_var_declaration + left: (expression_list + (identifier) @var_name) + right: (expression_list + (unary_expression + operand: (composite_literal + type: (type_identifier) @struct_type)))) + +(assignment_statement + left: (expression_list + (identifier) @var_name) + right: (expression_list + (unary_expression + operand: (composite_literal + type: (type_identifier) @struct_type)))) ]] M.receiver_method_query = [[ @@ -53,7 +70,8 @@ function M.modify_neotest_tree(file_path, tree) end local modified_tree = M.replace_receiver_with_suite(tree:root(), lookup) - local tree_with_merged_namespaces = M.merge_duplicate_namespaces(tree:root()) + local tree_with_merged_namespaces = + M.merge_duplicate_namespaces(modified_tree) return tree_with_merged_namespaces end @@ -143,6 +161,26 @@ function M.generate_lookup_map() end end end + + -- Handle var_name and struct_type matches + if matches.var_name and matches.struct_type then + for i, var in ipairs(matches.var_name) do + local struct_type = matches.struct_type[i] + if struct_type then + -- Add the struct_type as a receiver + lookup[filepath].receivers[struct_type.text] = true + + -- Find the corresponding function_name (test suite) + for _, func in ipairs(matches.function_name or {}) do + if func.text:match("^Test") then + lookup[filepath].suites[struct_type.text] = func.text + global_suites[struct_type.text] = func.text + break + end + end + end + end + end end -- Second pass: ensure all files have all receivers and suites @@ -208,6 +246,8 @@ function M.run_query_on_file(filepath, query_string) vim.api.nvim_buf_delete(bufnr, { force = true }) + vim.notify(vim.inspect(matches)) + return matches end From 9d555b903023d1bba5916262a45f7754b9f01ac1 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 8 Jul 2024 23:23:07 +0200 Subject: [PATCH 09/28] feat: cleanup queries --- lua/neotest-golang/ast.lua | 2 +- lua/neotest-golang/testify.lua | 109 +++++++++++++++------------------ 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 19ddc8d..2e75243 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -135,7 +135,7 @@ function M.detect_tests(file_path) if options.get().testify == true then -- only detect receiver methods if testify is enabled, to avoid confusion - query = query .. testify.receiver_method_query + query = query .. testify.namespace_query end ---@type neotest.Tree diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index b426a56..34c3b54 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -7,51 +7,62 @@ local M = {} local lookup_map = {} M.lookup_query = [[ - ; query for detecting package, struct and test suite, for use in lookup. + ; query for the lookup between receiver and test suite. + + ; package main // @package (package_clause (package_identifier) @package) - (type_declaration - (type_spec - name: (type_identifier) @struct_name - type: (struct_type))) - (method_declaration - receiver: (parameter_list - (parameter_declaration - type: (pointer_type - (type_identifier) @receiver_type))) - name: (field_identifier) @method_name) + + ; func TestSuite(t *testing.T) { // @test_function + ; suite.Run(t, new(testSuiteStruct)) // @suite_receiver + ; } (function_declaration - name: (identifier) @function_name) + name: (identifier) @test_function (#match? @test_function "^Test") ) (call_expression function: (selector_expression - operand: (identifier) @module - field: (field_identifier) @run (#eq? @run "Run")) + operand: (identifier) + field: (field_identifier)) arguments: (argument_list (identifier) (call_expression arguments: (argument_list - (type_identifier) @suite_receiver)))) - -; capture variable declarations -(short_var_declaration - left: (expression_list - (identifier) @var_name) - right: (expression_list - (unary_expression - operand: (composite_literal - type: (type_identifier) @struct_type)))) - -(assignment_statement - left: (expression_list - (identifier) @var_name) - right: (expression_list - (unary_expression - operand: (composite_literal - type: (type_identifier) @struct_type)))) + (type_identifier) @suite_struct)))) + + ; func TestSuite(t *testing.T) { // + ; s := &testSuiteStruct{} // @suite_struct + ; suite.Run(t, s) + ; } + (function_declaration + name: (identifier) @test_function (#match? @test_function "^Test") + parameters: (parameter_list + (parameter_declaration + name: (identifier) + type: (pointer_type + (qualified_type + package: (package_identifier) + name: (type_identifier))))) + body: (block + (short_var_declaration + left: (expression_list + (identifier)) + right: (expression_list + (unary_expression + operand: (composite_literal + type: (type_identifier) @suite_struct + body: (literal_value))))) + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) + field: (field_identifier)) + arguments: (argument_list + (identifier) + (identifier)))))) + ]] -M.receiver_method_query = [[ - ; query for receiver method, to be used as test suite namespace. +M.namespace_query = [[ + ; query for receiver method, to be used as test suite namespace initially (will be replaced later). (method_declaration receiver: (parameter_list (parameter_declaration @@ -147,40 +158,20 @@ function M.generate_lookup_map() suites = {}, } - -- Collect all receivers - for _, struct in ipairs(matches.struct_name or {}) do + -- Collect all receivers (same name as suite structs) + for _, struct in ipairs(matches.suite_struct or {}) do lookup[filepath].receivers[struct.text] = true end -- Collect all test suite functions and their receivers - for _, func in ipairs(matches.function_name or {}) do + for _, func in ipairs(matches.test_function or {}) do if func.text:match("^Test") then - for _, node in ipairs(matches.suite_receiver or {}) do + for _, node in ipairs(matches.suite_struct or {}) do lookup[filepath].suites[node.text] = func.text global_suites[node.text] = func.text end end end - - -- Handle var_name and struct_type matches - if matches.var_name and matches.struct_type then - for i, var in ipairs(matches.var_name) do - local struct_type = matches.struct_type[i] - if struct_type then - -- Add the struct_type as a receiver - lookup[filepath].receivers[struct_type.text] = true - - -- Find the corresponding function_name (test suite) - for _, func in ipairs(matches.function_name or {}) do - if func.text:match("^Test") then - lookup[filepath].suites[struct_type.text] = func.text - global_suites[struct_type.text] = func.text - break - end - end - end - end - end end -- Second pass: ensure all files have all receivers and suites @@ -246,8 +237,6 @@ function M.run_query_on_file(filepath, query_string) vim.api.nvim_buf_delete(bufnr, { force = true }) - vim.notify(vim.inspect(matches)) - return matches end From 2d44a0a000bb58beb3df1d8abaf308c299fda329 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Tue, 9 Jul 2024 00:21:37 +0200 Subject: [PATCH 10/28] fix: remove --- lua/neotest-golang/inspect.lua | 38 ------------------------ lua/neotest-golang/runspec_namespace.lua | 2 -- 2 files changed, 40 deletions(-) delete mode 100644 lua/neotest-golang/inspect.lua diff --git a/lua/neotest-golang/inspect.lua b/lua/neotest-golang/inspect.lua deleted file mode 100644 index e01e0b9..0000000 --- a/lua/neotest-golang/inspect.lua +++ /dev/null @@ -1,38 +0,0 @@ -local M = {} - -function M.deep(t, indent, done) - done = done or {} - indent = indent or "" - local output = {} - - for k, v in pairs(t) do - if type(v) == "table" and not done[v] then - done[v] = true - table.insert( - output, - indent .. tostring(k) .. ":\n" .. M.deep(v, indent .. " ", done) - ) - else - table.insert(output, indent .. tostring(k) .. ": " .. tostring(v)) - end - end - - return table.concat(output, "\n") -end - -function M.dump(o) - if type(o) == "table" then - local s = "{ " - for k, v in pairs(o) do - if type(k) ~= "number" then - k = '"' .. k .. '"' - end - s = s .. "[" .. k .. "] = " .. M.dump(v) .. "," - end - return s .. "} " - else - return tostring(o) - end -end - -return M diff --git a/lua/neotest-golang/runspec_namespace.lua b/lua/neotest-golang/runspec_namespace.lua index a0d6e52..19d21e8 100644 --- a/lua/neotest-golang/runspec_namespace.lua +++ b/lua/neotest-golang/runspec_namespace.lua @@ -22,8 +22,6 @@ function M.build(pos) test_name ) - vim.notify(vim.inspect(test_cmd)) - --- @type RunspecContext local context = { pos_id = pos.id, From 69452b82609f4e3a40f83cfae3cfa8c852ecda7b Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Tue, 9 Jul 2024 00:24:12 +0200 Subject: [PATCH 11/28] fix: typo --- lua/neotest-golang/runspec_namespace.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/neotest-golang/runspec_namespace.lua b/lua/neotest-golang/runspec_namespace.lua index 19d21e8..3aa5de5 100644 --- a/lua/neotest-golang/runspec_namespace.lua +++ b/lua/neotest-golang/runspec_namespace.lua @@ -1,4 +1,4 @@ ---- Helpers to build the command and context around running a single test. +--- Helpers to build the command and context around running all tests in a namespace. local convert = require("neotest-golang.convert") local cmd = require("neotest-golang.cmd") From dc69d04781f90e1abb83f232d5ac19fbac7ffb38 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Tue, 9 Jul 2024 00:34:02 +0200 Subject: [PATCH 12/28] fix: documentation --- lua/neotest-golang/init.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 24cab3b..2bbca5d 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -91,15 +91,14 @@ function M.Adapter.build_spec(args) -- 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 - -- - "namespace": ? (unclear to me at this point what this is) - -- Depending on the current position type, different ways to build the - -- runspec are used. -- -- 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, test). This gives the ability to have fallbacks. + -- (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 @@ -160,7 +159,7 @@ function M.Adapter.results(spec, result, tree) M.workaround_neotest_issue_391(result) return results elseif spec.context.pos_type == "namespace" then - -- A test command executed a single test and the output/status must now be + -- A test command executed a namespace and the output/status must now be -- processed. local results = parse.test_results(spec, result, tree) M.workaround_neotest_issue_391(result) From 75de9357587cb71653c8562de1d0b77b854e69c1 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Tue, 9 Jul 2024 06:13:03 +0200 Subject: [PATCH 13/28] fix: do not confuse regular test functions with suites --- lua/neotest-golang/testify.lua | 44 +++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua index 34c3b54..a11ce5a 100644 --- a/lua/neotest-golang/testify.lua +++ b/lua/neotest-golang/testify.lua @@ -14,23 +14,25 @@ M.lookup_query = [[ (package_identifier) @package) ; func TestSuite(t *testing.T) { // @test_function - ; suite.Run(t, new(testSuiteStruct)) // @suite_receiver + ; suite.Run(t, new(testSuitestruct)) // @suite_lib, @run_method, @suite_receiver ; } (function_declaration - name: (identifier) @test_function (#match? @test_function "^Test") ) - (call_expression - function: (selector_expression - operand: (identifier) - field: (field_identifier)) - arguments: (argument_list - (identifier) - (call_expression - arguments: (argument_list - (type_identifier) @suite_struct)))) - - ; func TestSuite(t *testing.T) { // + name: (identifier) @test_function (#match? @test_function "^Test") + body: (block + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) + arguments: (argument_list + (identifier) + (call_expression + arguments: (argument_list + (type_identifier) @suite_struct))))))) + + ; func TestSuite(t *testing.T) { // @test_function ; s := &testSuiteStruct{} // @suite_struct - ; suite.Run(t, s) + ; suite.Run(t, s) // @suite_lib, @run_method ; } (function_declaration name: (identifier) @test_function (#match? @test_function "^Test") @@ -53,13 +55,12 @@ M.lookup_query = [[ (expression_statement (call_expression function: (selector_expression - operand: (identifier) - field: (field_identifier)) + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) arguments: (argument_list (identifier) - (identifier)))))) - - ]] + (identifier)))))) +]] M.namespace_query = [[ ; query for receiver method, to be used as test suite namespace initially (will be replaced later). @@ -147,6 +148,9 @@ function M.generate_lookup_map() -- First pass: collect all receivers and suites for _, filepath in ipairs(go_files) do local matches = M.run_query_on_file(filepath, M.lookup_query) + + -- vim.notify(vim.inspect(matches)) + local package_name = matches.package and matches.package[1] and matches.package[1].text @@ -183,6 +187,8 @@ function M.generate_lookup_map() end end + -- vim.notify(vim.inspect(lookup)) + return lookup end From 05aaca40f8f7564aaacc9fe38389e505e5866f30 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Tue, 9 Jul 2024 19:49:08 +0200 Subject: [PATCH 14/28] refactor: break into modules --- lua/neotest-golang/ast.lua | 8 +- lua/neotest-golang/features/init.lua | 0 lua/neotest-golang/features/testify/init.lua | 8 + .../features/testify/lookup.lua | 169 ++++++++ lua/neotest-golang/features/testify/query.lua | 13 + .../features/testify/tree_modification.lua | 134 +++++++ lua/neotest-golang/{path.lua => find.lua} | 18 +- lua/neotest-golang/init.lua | 4 +- lua/neotest-golang/query.lua | 54 +++ lua/neotest-golang/runspec_dir.lua | 4 +- lua/neotest-golang/runspec_file.lua | 4 +- lua/neotest-golang/testify.lua | 369 ------------------ 12 files changed, 404 insertions(+), 381 deletions(-) create mode 100644 lua/neotest-golang/features/init.lua create mode 100644 lua/neotest-golang/features/testify/init.lua create mode 100644 lua/neotest-golang/features/testify/lookup.lua create mode 100644 lua/neotest-golang/features/testify/query.lua create mode 100644 lua/neotest-golang/features/testify/tree_modification.lua rename lua/neotest-golang/{path.lua => find.lua} (65%) create mode 100644 lua/neotest-golang/query.lua delete mode 100644 lua/neotest-golang/testify.lua diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 2e75243..cd6c949 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -3,7 +3,7 @@ local lib = require("neotest.lib") local options = require("neotest-golang.options") -local testify = require("neotest-golang.testify") +local testify = require("neotest-golang.features.testify") local M = {} @@ -134,8 +134,8 @@ function M.detect_tests(file_path) local query = M.test_function .. M.test_method .. M.table_tests if options.get().testify == true then - -- only detect receiver methods if testify is enabled, to avoid confusion - query = query .. testify.namespace_query + -- detect receiver method structs as namespaces. + query = query .. testify.query.namespace_query end ---@type neotest.Tree @@ -143,7 +143,7 @@ function M.detect_tests(file_path) if options.get().testify == true then local tree_modified_for_testify = - testify.modify_neotest_tree(file_path, tree) + testify.tree_modification.modify_neotest_tree(file_path, tree) return tree_modified_for_testify end diff --git a/lua/neotest-golang/features/init.lua b/lua/neotest-golang/features/init.lua new file mode 100644 index 0000000..e69de29 diff --git a/lua/neotest-golang/features/testify/init.lua b/lua/neotest-golang/features/testify/init.lua new file mode 100644 index 0000000..424ed6d --- /dev/null +++ b/lua/neotest-golang/features/testify/init.lua @@ -0,0 +1,8 @@ +local M = {} + +M.lookup = require("neotest-golang.features.testify.lookup") +M.query = require("neotest-golang.features.testify.query") +M.tree_modification = + require("neotest-golang.features.testify.tree_modification") + +return M diff --git a/lua/neotest-golang/features/testify/lookup.lua b/lua/neotest-golang/features/testify/lookup.lua new file mode 100644 index 0000000..57fd4dd --- /dev/null +++ b/lua/neotest-golang/features/testify/lookup.lua @@ -0,0 +1,169 @@ +local parsers = require("nvim-treesitter.parsers") + +local find = require("neotest-golang.find") +local query = require("neotest-golang.query") + +local M = {} + +local lookup_map = {} + +M.query = [[ + ; query for the lookup between receiver and test suite. + + ; package main // @package + (package_clause + (package_identifier) @package) + + ; func TestSuite(t *testing.T) { // @test_function + ; suite.Run(t, new(testSuitestruct)) // @suite_lib, @run_method, @suite_receiver + ; } + (function_declaration + name: (identifier) @test_function (#match? @test_function "^Test") + body: (block + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) + arguments: (argument_list + (identifier) + (call_expression + arguments: (argument_list + (type_identifier) @suite_struct))))))) + + ; func TestSuite(t *testing.T) { // @test_function + ; s := &testSuiteStruct{} // @suite_struct + ; suite.Run(t, s) // @suite_lib, @run_method + ; } + (function_declaration + name: (identifier) @test_function (#match? @test_function "^Test") + parameters: (parameter_list + (parameter_declaration + name: (identifier) + type: (pointer_type + (qualified_type + package: (package_identifier) + name: (type_identifier))))) + body: (block + (short_var_declaration + left: (expression_list + (identifier)) + right: (expression_list + (unary_expression + operand: (composite_literal + type: (type_identifier) @suite_struct + body: (literal_value))))) + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) + arguments: (argument_list + (identifier) + (identifier)))))) +]] + +function M.generate() + -- local example = { + -- ["/path/to/file1_test.go"] = { + -- package = "main", + -- receivers = { receiverStruct = true, receiverStruct2 = true }, + -- suites = { + -- receiverStruct = "TestSuite", + -- receiverStruct2 = "TestSuite2", + -- }, + -- }, + -- ["/path/to/file2_test.go"] = { + -- package = "main", + -- receivers = { receiverStruct3 = true }, + -- suites = { + -- receiverStruct3 = "TestSuite3", + -- }, + -- }, + -- -- ... other files ... + -- } + + local cwd = vim.fn.getcwd() + local filepaths = find.go_test_filepaths(cwd) + local lookup = {} + local global_suites = {} + + -- First pass: collect all receivers and suites + for _, filepath in ipairs(filepaths) do + local matches = query.run_query_on_file(filepath, M.query) + + -- vim.notify(vim.inspect(matches)) + + local package_name = matches.package + and matches.package[1] + and matches.package[1].text + or "unknown" + + lookup[filepath] = { + package = package_name, + receivers = {}, + suites = {}, + } + + -- Collect all receivers (same name as suite structs) + for _, struct in ipairs(matches.suite_struct or {}) do + lookup[filepath].receivers[struct.text] = true + end + + -- Collect all test suite functions and their receivers + for _, func in ipairs(matches.test_function or {}) do + if func.text:match("^Test") then + for _, node in ipairs(matches.suite_struct or {}) do + lookup[filepath].suites[node.text] = func.text + global_suites[node.text] = func.text + end + end + end + end + + -- Second pass: ensure all files have all receivers and suites + for filepath, file_data in pairs(lookup) do + for receiver, suite in pairs(global_suites) do + if not file_data.receivers[receiver] and file_data.suites[receiver] then + file_data.receivers[receiver] = true + end + end + end + + return lookup +end + +function M.get() + if vim.tbl_isempty(lookup_map) then + lookup_map = M.generate() + end + return lookup_map +end + +function M.add(file_name, package_name, suite_name, receiver_name) + if not lookup_map[file_name] then + lookup_map[file_name] = {} + end + local new_entry = { + package = package_name, + suite = suite_name, + receiver = receiver_name, + } + -- Check if entry already exists + for _, entry in ipairs(lookup_map[file_name]) do + if + entry.package == new_entry.package + and entry.suite == new_entry.suite + and entry.receiver == new_entry.receiver + then + return + end + end + table.insert(lookup_map[file_name], new_entry) +end + +function M.clear() + lookup_map = {} +end + +return M diff --git a/lua/neotest-golang/features/testify/query.lua b/lua/neotest-golang/features/testify/query.lua new file mode 100644 index 0000000..b593cc6 --- /dev/null +++ b/lua/neotest-golang/features/testify/query.lua @@ -0,0 +1,13 @@ +local M = {} + +M.namespace_query = [[ + ; query for receiver method, to be used as test suite namespace initially (will be replaced later). + (method_declaration + receiver: (parameter_list + (parameter_declaration + ; name: (identifier) + type: (pointer_type + (type_identifier) @namespace.name )))) @namespace.definition + ]] + +return M diff --git a/lua/neotest-golang/features/testify/tree_modification.lua b/lua/neotest-golang/features/testify/tree_modification.lua new file mode 100644 index 0000000..fcf04e5 --- /dev/null +++ b/lua/neotest-golang/features/testify/tree_modification.lua @@ -0,0 +1,134 @@ +--- Opt-in functionality to support testify suites. + +local lookup = require("neotest-golang.features.testify.lookup") + +local M = {} + +---@param file_path string +---@param tree neotest.Tree +function M.modify_neotest_tree(file_path, tree) + local lookup_map = lookup.get() + + if not lookup_map then + return tree + end + + local modified_tree = M.replace_receiver_with_suite(tree:root(), lookup_map) + local tree_with_merged_namespaces = + M.merge_duplicate_namespaces(modified_tree) + return tree_with_merged_namespaces +end + +function M.replace_receiver_with_suite(node, file_lookup) + if not file_lookup then + return node + end + + local function replace_in_string(str, old, new) + return str + :gsub("::" .. old .. "::", "::" .. new .. "::") + :gsub("::" .. old .. "$", "::" .. new) + end + + local function update_node(n, replacements, suite_names) + for old, new in pairs(replacements) do + if n._data.name == old then + n._data.name = new + n._data.type = "namespace" + elseif suite_names[n._data.name] then + n._data.type = "namespace" + end + n._data.id = replace_in_string(n._data.id, old, new) + end + end + + local function update_nodes_table(nodes, replacements) + local new_nodes = {} + for key, value in pairs(nodes) do + local new_key = key + for old, new in pairs(replacements) do + new_key = replace_in_string(new_key, old, new) + end + new_nodes[new_key] = value + end + return new_nodes + end + + local function recursive_update(n, replacements, suite_names) + update_node(n, replacements, suite_names) + n._nodes = update_nodes_table(n._nodes, replacements) + for _, child in ipairs(n:children()) do + recursive_update(child, replacements, suite_names) + end + end + + -- Create a global replacements table and suite names set + local global_replacements = {} + local suite_names = {} + for file_path, file_data in pairs(file_lookup) do + if file_data.suites then + for receiver, suite in pairs(file_data.suites) do + global_replacements[receiver] = suite + suite_names[suite] = true + end + else + -- no suites found for file + end + end + + if vim.tbl_isempty(global_replacements) then + -- no replacements found + return node + end + + recursive_update(node, global_replacements, suite_names) + + -- After updating all nodes, ensure parent-child relationships are correct + local function fix_relationships(n) + for _, child in ipairs(n:children()) do + child._parent = n + fix_relationships(child) + end + end + + fix_relationships(node) + + return node +end + +function M.merge_duplicate_namespaces(node) + if not node._children or #node._children == 0 then + return node + end + + local namespaces = {} + local new_children = {} + + for _, child in ipairs(node._children) do + if child._data.type == "namespace" then + local existing = namespaces[child._data.name] + if existing then + -- Merge children of duplicate namespace + for _, grandchild in ipairs(child._children) do + table.insert(existing._children, grandchild) + grandchild._parent = existing + end + else + namespaces[child._data.name] = child + table.insert(new_children, child) + end + else + table.insert(new_children, child) + end + end + + -- Recursively process children + for _, child in ipairs(new_children) do + M.merge_duplicate_namespaces(child) + end + + node._children = new_children + return node +end + +return M diff --git a/lua/neotest-golang/path.lua b/lua/neotest-golang/find.lua similarity index 65% rename from lua/neotest-golang/path.lua rename to lua/neotest-golang/find.lua index 03e0f18..7465320 100644 --- a/lua/neotest-golang/path.lua +++ b/lua/neotest-golang/find.lua @@ -6,9 +6,8 @@ local M = {} --- @param filename string --- @param start_path string --- @return string | nil -function M.find_file_upwards(filename, start_path) +function M.file_upwards(filename, start_path) local scan = require("plenary.scandir") - local cwd = vim.fn.getcwd() local found_filepath = nil while start_path ~= vim.fn.expand("$HOME") do local files = scan.scan_dir( @@ -36,4 +35,19 @@ function M.find_file_upwards(filename, start_path) return found_filepath end +-- Get all *_test.go files in a directory recursively. +-- FIXME: do not use `find`, as this puts unnecessary dependency here? +-- Might want to use plenary instead. +function M.go_test_filepaths(folderpath) + local files = {} + local function scan_dir(dir) + local p = io.popen('find "' .. dir .. '" -type f -name "*_test.go"') + for file in p:lines() do + table.insert(files, file) + end + end + scan_dir(folderpath) + return files +end + return M diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index 2bbca5d..a0aaa76 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -8,7 +8,7 @@ local runspec_file = require("neotest-golang.runspec_file") local runspec_namespace = require("neotest-golang.runspec_namespace") local runspec_test = require("neotest-golang.runspec_test") local parse = require("neotest-golang.parse") -local testify = require("neotest-golang.testify") +local testify = require("neotest-golang.features.testify") local M = {} @@ -19,7 +19,7 @@ M.Adapter = { name = "neotest-golang", init = function() if options.get().testify == true then - testify.generate_lookup_map() + testify.lookup.generate() end end, } diff --git a/lua/neotest-golang/query.lua b/lua/neotest-golang/query.lua new file mode 100644 index 0000000..e62ccbb --- /dev/null +++ b/lua/neotest-golang/query.lua @@ -0,0 +1,54 @@ +local parsers = require("nvim-treesitter.parsers") + +local M = {} + +function M.run_query_on_file(filepath, query_string) + local bufnr = vim.api.nvim_create_buf(false, true) + local content = vim.fn.readfile(filepath) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) + + vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) + + if not parsers.has_parser("go") then + error("Go parser is not available. Please ensure it's installed.") + end + + local parser = parsers.get_parser(bufnr, "go") + local tree = parser:parse()[1] + local root = tree:root() + + local query = vim.treesitter.query.parse("go", query_string) + + local matches = {} + + local function add_match(name, node) + if not matches[name] then + matches[name] = {} + end + table.insert( + matches[name], + { name = name, node = node, text = M.get_node_text(node, bufnr) } + ) + end + + for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do + for id, node in pairs(match) do + local name = query.captures[id] + add_match(name, node) + end + end + + vim.api.nvim_buf_delete(bufnr, { force = true }) + + return matches +end + +function M.get_node_text(node, bufnr) + local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter + if type(text) == "table" then + return table.concat(text, "\n") + end + return text +end + +return M diff --git a/lua/neotest-golang/runspec_dir.lua b/lua/neotest-golang/runspec_dir.lua index d62945b..dd0c56c 100644 --- a/lua/neotest-golang/runspec_dir.lua +++ b/lua/neotest-golang/runspec_dir.lua @@ -2,7 +2,7 @@ --- a Go package. local cmd = require("neotest-golang.cmd") -local path = require("neotest-golang.path") +local find = require("neotest-golang.find") local M = {} @@ -15,7 +15,7 @@ local M = {} --- @param pos neotest.Position --- @return neotest.RunSpec | nil function M.build(pos) - local go_mod_filepath = path.find_file_upwards("go.mod", pos.path) + local go_mod_filepath = find.file_upwards("go.mod", pos.path) if go_mod_filepath == nil then -- if no go.mod file was found up the directory tree, until reaching $CWD, -- then we cannot determine the Go project root. diff --git a/lua/neotest-golang/runspec_file.lua b/lua/neotest-golang/runspec_file.lua index 617582f..2baf95d 100644 --- a/lua/neotest-golang/runspec_file.lua +++ b/lua/neotest-golang/runspec_file.lua @@ -2,7 +2,7 @@ local cmd = require("neotest-golang.cmd") local convert = require("neotest-golang.convert") -local path = require("neotest-golang.path") +local find = require("neotest-golang.find") local M = {} @@ -15,7 +15,7 @@ function M.build(pos, tree) return M.fail_fast(pos) end - local go_mod_filepath = path.find_file_upwards("go.mod", pos.path) + local go_mod_filepath = find.file_upwards("go.mod", pos.path) if go_mod_filepath == nil then -- if no go.mod file was found up the directory tree, until reaching $CWD, -- then we cannot determine the Go project root. diff --git a/lua/neotest-golang/testify.lua b/lua/neotest-golang/testify.lua deleted file mode 100644 index a11ce5a..0000000 --- a/lua/neotest-golang/testify.lua +++ /dev/null @@ -1,369 +0,0 @@ ---- Opt-in functionality to support testify suites. ---- See https://github.com/stretchr/testify for more info. - -local parsers = require("nvim-treesitter.parsers") - -local M = {} -local lookup_map = {} - -M.lookup_query = [[ - ; query for the lookup between receiver and test suite. - - ; package main // @package - (package_clause - (package_identifier) @package) - - ; func TestSuite(t *testing.T) { // @test_function - ; suite.Run(t, new(testSuitestruct)) // @suite_lib, @run_method, @suite_receiver - ; } - (function_declaration - name: (identifier) @test_function (#match? @test_function "^Test") - body: (block - (expression_statement - (call_expression - function: (selector_expression - operand: (identifier) @suite_lib (#eq? @suite_lib "suite") - field: (field_identifier) @run_method (#eq? @run_method "Run")) - arguments: (argument_list - (identifier) - (call_expression - arguments: (argument_list - (type_identifier) @suite_struct))))))) - - ; func TestSuite(t *testing.T) { // @test_function - ; s := &testSuiteStruct{} // @suite_struct - ; suite.Run(t, s) // @suite_lib, @run_method - ; } - (function_declaration - name: (identifier) @test_function (#match? @test_function "^Test") - parameters: (parameter_list - (parameter_declaration - name: (identifier) - type: (pointer_type - (qualified_type - package: (package_identifier) - name: (type_identifier))))) - body: (block - (short_var_declaration - left: (expression_list - (identifier)) - right: (expression_list - (unary_expression - operand: (composite_literal - type: (type_identifier) @suite_struct - body: (literal_value))))) - (expression_statement - (call_expression - function: (selector_expression - operand: (identifier) @suite_lib (#eq? @suite_lib "suite") - field: (field_identifier) @run_method (#eq? @run_method "Run")) - arguments: (argument_list - (identifier) - (identifier)))))) -]] - -M.namespace_query = [[ - ; query for receiver method, to be used as test suite namespace initially (will be replaced later). - (method_declaration - receiver: (parameter_list - (parameter_declaration - ; name: (identifier) - type: (pointer_type - (type_identifier) @namespace.name )))) @namespace.definition - ]] - ----@param file_path string ----@param tree neotest.Tree -function M.modify_neotest_tree(file_path, tree) - local lookup = M.get_lookup_map() - - if not lookup then - return tree - end - - local modified_tree = M.replace_receiver_with_suite(tree:root(), lookup) - local tree_with_merged_namespaces = - M.merge_duplicate_namespaces(modified_tree) - return tree_with_merged_namespaces -end - -function M.get_lookup_map() - if vim.tbl_isempty(lookup_map) then - lookup_map = M.generate_lookup_map() - end - return lookup_map -end - -function M.add_to_lookup_map(file_name, package_name, suite_name, receiver_name) - if not lookup_map[file_name] then - lookup_map[file_name] = {} - end - local new_entry = { - package = package_name, - suite = suite_name, - receiver = receiver_name, - } - -- Check if entry already exists - for _, entry in ipairs(lookup_map[file_name]) do - if - entry.package == new_entry.package - and entry.suite == new_entry.suite - and entry.receiver == new_entry.receiver - then - return - end - end - table.insert(lookup_map[file_name], new_entry) -end - -function M.clear_lookup_map() - lookup_map = {} -end - -function M.generate_lookup_map() - -- local example = { - -- ["/path/to/file1_test.go"] = { - -- package = "main", - -- receivers = { receiverStruct = true, receiverStruct2 = true }, - -- suites = { - -- receiverStruct = "TestSuite", - -- receiverStruct2 = "TestSuite2", - -- }, - -- }, - -- ["/path/to/file2_test.go"] = { - -- package = "main", - -- receivers = { receiverStruct3 = true }, - -- suites = { - -- receiverStruct3 = "TestSuite3", - -- }, - -- }, - -- -- ... other files ... - -- } - - local cwd = vim.fn.getcwd() - local go_files = M.get_go_files(cwd) - local lookup = {} - local global_suites = {} - - -- First pass: collect all receivers and suites - for _, filepath in ipairs(go_files) do - local matches = M.run_query_on_file(filepath, M.lookup_query) - - -- vim.notify(vim.inspect(matches)) - - local package_name = matches.package - and matches.package[1] - and matches.package[1].text - or "unknown" - - lookup[filepath] = { - package = package_name, - receivers = {}, - suites = {}, - } - - -- Collect all receivers (same name as suite structs) - for _, struct in ipairs(matches.suite_struct or {}) do - lookup[filepath].receivers[struct.text] = true - end - - -- Collect all test suite functions and their receivers - for _, func in ipairs(matches.test_function or {}) do - if func.text:match("^Test") then - for _, node in ipairs(matches.suite_struct or {}) do - lookup[filepath].suites[node.text] = func.text - global_suites[node.text] = func.text - end - end - end - end - - -- Second pass: ensure all files have all receivers and suites - for filepath, file_data in pairs(lookup) do - for receiver, suite in pairs(global_suites) do - if not file_data.receivers[receiver] and file_data.suites[receiver] then - file_data.receivers[receiver] = true - end - end - end - - -- vim.notify(vim.inspect(lookup)) - - return lookup -end - --- Function to get all .go files in a directory recursively -function M.get_go_files(directory) - local files = {} - local function scan_dir(dir) - local p = io.popen('find "' .. dir .. '" -type f -name "*_test.go"') - for file in p:lines() do - table.insert(files, file) - end - end - scan_dir(directory) - return files -end - -function M.run_query_on_file(filepath, query_string) - local bufnr = vim.api.nvim_create_buf(false, true) - local content = vim.fn.readfile(filepath) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) - - vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) - - if not parsers.has_parser("go") then - error("Go parser is not available. Please ensure it's installed.") - end - - local parser = parsers.get_parser(bufnr, "go") - local tree = parser:parse()[1] - local root = tree:root() - - local query = vim.treesitter.query.parse("go", query_string) - - local matches = {} - - local function add_match(name, node) - if not matches[name] then - matches[name] = {} - end - table.insert( - matches[name], - { name = name, node = node, text = M.get_node_text(node, bufnr) } - ) - end - - for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do - for id, node in pairs(match) do - local name = query.captures[id] - add_match(name, node) - end - end - - vim.api.nvim_buf_delete(bufnr, { force = true }) - - return matches -end - -function M.get_node_text(node, bufnr) - local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter - if type(text) == "table" then - return table.concat(text, "\n") - end - return text -end - -function M.replace_receiver_with_suite(node, file_lookup) - if not file_lookup then - return node - end - - local function replace_in_string(str, old, new) - return str - :gsub("::" .. old .. "::", "::" .. new .. "::") - :gsub("::" .. old .. "$", "::" .. new) - end - - local function update_node(n, replacements, suite_names) - for old, new in pairs(replacements) do - if n._data.name == old then - n._data.name = new - n._data.type = "namespace" - elseif suite_names[n._data.name] then - n._data.type = "namespace" - end - n._data.id = replace_in_string(n._data.id, old, new) - end - end - - local function update_nodes_table(nodes, replacements) - local new_nodes = {} - for key, value in pairs(nodes) do - local new_key = key - for old, new in pairs(replacements) do - new_key = replace_in_string(new_key, old, new) - end - new_nodes[new_key] = value - end - return new_nodes - end - - local function recursive_update(n, replacements, suite_names) - update_node(n, replacements, suite_names) - n._nodes = update_nodes_table(n._nodes, replacements) - for _, child in ipairs(n:children()) do - recursive_update(child, replacements, suite_names) - end - end - - -- Create a global replacements table and suite names set - local global_replacements = {} - local suite_names = {} - for file_path, file_data in pairs(file_lookup) do - if file_data.suites then - for receiver, suite in pairs(file_data.suites) do - global_replacements[receiver] = suite - suite_names[suite] = true - end - else - -- no suites found for file - end - end - - if vim.tbl_isempty(global_replacements) then - -- no replacements found - return node - end - - recursive_update(node, global_replacements, suite_names) - - -- After updating all nodes, ensure parent-child relationships are correct - local function fix_relationships(n) - for _, child in ipairs(n:children()) do - child._parent = n - fix_relationships(child) - end - end - - fix_relationships(node) - - return node -end - -function M.merge_duplicate_namespaces(node) - if not node._children or #node._children == 0 then - return node - end - - local namespaces = {} - local new_children = {} - - for _, child in ipairs(node._children) do - if child._data.type == "namespace" then - local existing = namespaces[child._data.name] - if existing then - -- Merge children of duplicate namespace - for _, grandchild in ipairs(child._children) do - table.insert(existing._children, grandchild) - grandchild._parent = existing - end - else - namespaces[child._data.name] = child - table.insert(new_children, child) - end - else - table.insert(new_children, child) - end - end - - -- Recursively process children - for _, child in ipairs(new_children) do - M.merge_duplicate_namespaces(child) - end - - node._children = new_children - return node -end - -return M From c280c7de4c0a1c17554ed1cde8e52e0b1cf55c63 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 14:53:17 +0200 Subject: [PATCH 15/28] docs: add types/docs --- lua/neotest-golang/ast.lua | 2 +- .../features/testify/tree_modification.lua | 83 +++++++++++++------ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index cd6c949..a6d1e7f 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -143,7 +143,7 @@ function M.detect_tests(file_path) if options.get().testify == true then local tree_modified_for_testify = - testify.tree_modification.modify_neotest_tree(file_path, tree) + testify.tree_modification.modify_neotest_tree(tree) return tree_modified_for_testify end diff --git a/lua/neotest-golang/features/testify/tree_modification.lua b/lua/neotest-golang/features/testify/tree_modification.lua index fcf04e5..5d7984d 100644 --- a/lua/neotest-golang/features/testify/tree_modification.lua +++ b/lua/neotest-golang/features/testify/tree_modification.lua @@ -4,9 +4,14 @@ local lookup = require("neotest-golang.features.testify.lookup") local M = {} ----@param file_path string ----@param tree neotest.Tree -function M.modify_neotest_tree(file_path, tree) +--- Modify the neotest tree, so that testify suites are properly described. +--- +--- When testify tests are discovered, they are discovered with the receiver as +--- Neotest namespace. This is incorrect, and to fix this, we need to do a +--- search-replace of the receiver with the suite name. +--- @param tree neotest.Tree The original neotest tree +--- @return neotest.Tree The modified tree. +function M.modify_neotest_tree(tree) local lookup_map = lookup.get() if not lookup_map then @@ -19,41 +24,65 @@ function M.modify_neotest_tree(file_path, tree) return tree_with_merged_namespaces end -function M.replace_receiver_with_suite(node, file_lookup) +--- Replace receiver methods with their corresponding test suites in the tree. +--- @param tree neotest.Tree The tree to modify +--- @param file_lookup table The lookup table containing receiver-to-suite mappings +--- @return neotest.Tree The modified tree with receivers replaced by suites +function M.replace_receiver_with_suite(tree, file_lookup) if not file_lookup then - return node + return tree end - local function replace_in_string(str, old, new) + --- Perform the neotest.Position id replacement. + --- + --- Namespaces and tests are delimited by "::" and we need to replace the receiver + --- with the suite name here. + --- @param str string The neotest.Position id, e.g. "/project/main_test.go::myReceiver::TestFunction" + --- @param receiver string The receiver name, e.g. "myReceiver" + --- @param suite string The suite name, e.g. "TestSuite" + --- @return string The modified string, where receiver is replaced by suite, e.g. "/project/main_test.go::TestSuite::TestFunction" + local function replace_receiver_in_pos_id(str, receiver, suite) return str - :gsub("::" .. old .. "::", "::" .. new .. "::") - :gsub("::" .. old .. "$", "::" .. new) + :gsub("::" .. receiver .. "::", "::" .. suite .. "::") + :gsub("::" .. receiver .. "$", "::" .. suite) end + --- Update a single neotest.Tree node with the given replacements. + --- @param n neotest.Tree The node to update + --- @param replacements table A table of old-to-new replacements + --- @param suite_names table A set of known suite names local function update_node(n, replacements, suite_names) - for old, new in pairs(replacements) do - if n._data.name == old then - n._data.name = new + for receiver, suite in pairs(replacements) do + if n._data.name == receiver then + n._data.name = suite n._data.type = "namespace" elseif suite_names[n._data.name] then n._data.type = "namespace" end - n._data.id = replace_in_string(n._data.id, old, new) + n._data.id = replace_receiver_in_pos_id(n._data.id, receiver, suite) end end + --- Update the nodes table with the given replacements. + --- @param nodes table The table of nodes to update + --- @param replacements table A table of old-to-new replacements + --- @return table The updated nodes table local function update_nodes_table(nodes, replacements) local new_nodes = {} for key, value in pairs(nodes) do local new_key = key for old, new in pairs(replacements) do - new_key = replace_in_string(new_key, old, new) + new_key = replace_receiver_in_pos_id(new_key, old, new) end new_nodes[new_key] = value end return new_nodes end + --- Recursively update a tree/node and its children with the given replacements. + --- @param n neotest.Tree The tree to update recursively + --- @param replacements table A table of old-to-new replacements + --- @param suite_names table A set of known suite names local function recursive_update(n, replacements, suite_names) update_node(n, replacements, suite_names) n._nodes = update_nodes_table(n._nodes, replacements) @@ -78,12 +107,13 @@ function M.replace_receiver_with_suite(node, file_lookup) if vim.tbl_isempty(global_replacements) then -- no replacements found - return node + return tree end - recursive_update(node, global_replacements, suite_names) + recursive_update(tree, global_replacements, suite_names) - -- After updating all nodes, ensure parent-child relationships are correct + --- Ensure parent-child relationships are correct after updating all nodes. + --- @param n neotest.Tree The node to fix relationships for local function fix_relationships(n) for _, child in ipairs(n:children()) do child._parent = n @@ -91,20 +121,25 @@ function M.replace_receiver_with_suite(node, file_lookup) end end - fix_relationships(node) + fix_relationships(tree) - return node + return tree end -function M.merge_duplicate_namespaces(node) - if not node._children or #node._children == 0 then - return node +--- Merge duplicate namespaces in the tree. +--- +--- Without this merging, the namespace will reappear once for each testify test. +--- @param tree neotest.Tree The tree to merge duplicate namespaces in +--- @return neotest.Tree The tree with merged duplicate namespaces +function M.merge_duplicate_namespaces(tree) + if not tree._children or #tree._children == 0 then + return tree end local namespaces = {} local new_children = {} - for _, child in ipairs(node._children) do + for _, child in ipairs(tree._children) do if child._data.type == "namespace" then local existing = namespaces[child._data.name] if existing then @@ -127,8 +162,8 @@ function M.merge_duplicate_namespaces(node) M.merge_duplicate_namespaces(child) end - node._children = new_children - return node + tree._children = new_children + return tree end return M From cb309fca7d36ea27c4438385e613589011cbedcb Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 14:54:14 +0200 Subject: [PATCH 16/28] fix: warning --- lua/neotest-golang/features/testify/tree_modification.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/neotest-golang/features/testify/tree_modification.lua b/lua/neotest-golang/features/testify/tree_modification.lua index 5d7984d..dc96457 100644 --- a/lua/neotest-golang/features/testify/tree_modification.lua +++ b/lua/neotest-golang/features/testify/tree_modification.lua @@ -42,9 +42,9 @@ function M.replace_receiver_with_suite(tree, file_lookup) --- @param suite string The suite name, e.g. "TestSuite" --- @return string The modified string, where receiver is replaced by suite, e.g. "/project/main_test.go::TestSuite::TestFunction" local function replace_receiver_in_pos_id(str, receiver, suite) - return str - :gsub("::" .. receiver .. "::", "::" .. suite .. "::") - :gsub("::" .. receiver .. "$", "::" .. suite) + local modified = str:gsub("::" .. receiver .. "::", "::" .. suite .. "::") + modified = modified:gsub("::" .. receiver .. "$", "::" .. suite) + return modified end --- Update a single neotest.Tree node with the given replacements. From 0ebab3a8b50cf6c78449bebc040b0e5c5207afff Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 15:09:23 +0200 Subject: [PATCH 17/28] refactor: break out inner functions --- .../features/testify/tree_modification.lua | 155 +++++++++--------- 1 file changed, 79 insertions(+), 76 deletions(-) diff --git a/lua/neotest-golang/features/testify/tree_modification.lua b/lua/neotest-golang/features/testify/tree_modification.lua index dc96457..acba259 100644 --- a/lua/neotest-golang/features/testify/tree_modification.lua +++ b/lua/neotest-golang/features/testify/tree_modification.lua @@ -6,15 +6,19 @@ local M = {} --- Modify the neotest tree, so that testify suites are properly described. --- ---- When testify tests are discovered, they are discovered with the receiver as ---- Neotest namespace. This is incorrect, and to fix this, we need to do a ---- search-replace of the receiver with the suite name. +--- When testify tests are discovered, they are discovered with the Go receiver +--- as the Neotest namespace. This is incorrect, and to fix this, we need to do +--- a search-replace of the receiver with the suite name. --- @param tree neotest.Tree The original neotest tree --- @return neotest.Tree The modified tree. function M.modify_neotest_tree(tree) local lookup_map = lookup.get() if not lookup_map then + vim.notify( + "No lookup found. Could not modify Neotest tree for testify suite support", + vim.log.levels.WARN + ) return tree end @@ -33,75 +37,15 @@ function M.replace_receiver_with_suite(tree, file_lookup) return tree end - --- Perform the neotest.Position id replacement. - --- - --- Namespaces and tests are delimited by "::" and we need to replace the receiver - --- with the suite name here. - --- @param str string The neotest.Position id, e.g. "/project/main_test.go::myReceiver::TestFunction" - --- @param receiver string The receiver name, e.g. "myReceiver" - --- @param suite string The suite name, e.g. "TestSuite" - --- @return string The modified string, where receiver is replaced by suite, e.g. "/project/main_test.go::TestSuite::TestFunction" - local function replace_receiver_in_pos_id(str, receiver, suite) - local modified = str:gsub("::" .. receiver .. "::", "::" .. suite .. "::") - modified = modified:gsub("::" .. receiver .. "$", "::" .. suite) - return modified - end - - --- Update a single neotest.Tree node with the given replacements. - --- @param n neotest.Tree The node to update - --- @param replacements table A table of old-to-new replacements - --- @param suite_names table A set of known suite names - local function update_node(n, replacements, suite_names) - for receiver, suite in pairs(replacements) do - if n._data.name == receiver then - n._data.name = suite - n._data.type = "namespace" - elseif suite_names[n._data.name] then - n._data.type = "namespace" - end - n._data.id = replace_receiver_in_pos_id(n._data.id, receiver, suite) - end - end - - --- Update the nodes table with the given replacements. - --- @param nodes table The table of nodes to update - --- @param replacements table A table of old-to-new replacements - --- @return table The updated nodes table - local function update_nodes_table(nodes, replacements) - local new_nodes = {} - for key, value in pairs(nodes) do - local new_key = key - for old, new in pairs(replacements) do - new_key = replace_receiver_in_pos_id(new_key, old, new) - end - new_nodes[new_key] = value - end - return new_nodes - end - - --- Recursively update a tree/node and its children with the given replacements. - --- @param n neotest.Tree The tree to update recursively - --- @param replacements table A table of old-to-new replacements - --- @param suite_names table A set of known suite names - local function recursive_update(n, replacements, suite_names) - update_node(n, replacements, suite_names) - n._nodes = update_nodes_table(n._nodes, replacements) - for _, child in ipairs(n:children()) do - recursive_update(child, replacements, suite_names) - end - end - -- Create a global replacements table and suite names set local global_replacements = {} local suite_names = {} - for file_path, file_data in pairs(file_lookup) do + for _, file_data in pairs(file_lookup) do if file_data.suites then for receiver, suite in pairs(file_data.suites) do global_replacements[receiver] = suite suite_names[suite] = true end - else - -- no suites found for file end end @@ -110,18 +54,8 @@ function M.replace_receiver_with_suite(tree, file_lookup) return tree end - recursive_update(tree, global_replacements, suite_names) - - --- Ensure parent-child relationships are correct after updating all nodes. - --- @param n neotest.Tree The node to fix relationships for - local function fix_relationships(n) - for _, child in ipairs(n:children()) do - child._parent = n - fix_relationships(child) - end - end - - fix_relationships(tree) + M.recursive_update(tree, global_replacements, suite_names) + M.fix_relationships(tree) return tree end @@ -166,4 +100,73 @@ function M.merge_duplicate_namespaces(tree) return tree end +-- Utility functions + +--- Perform the neotest.Position id replacement. +--- +--- Namespaces and tests are delimited by "::" and we need to replace the receiver +--- with the suite name here. +--- @param str string The neotest.Position id +--- @param receiver string The receiver name +--- @param suite string The suite name +--- @return string The modified neotest.Position id string +function M.replace_receiver_in_pos_id(str, receiver, suite) + local modified = str:gsub("::" .. receiver .. "::", "::" .. suite .. "::") + modified = modified:gsub("::" .. receiver .. "$", "::" .. suite) + return modified +end + +--- Update a single neotest.Tree node with the given replacements. +--- @param n neotest.Tree The node to update +--- @param replacements table A table of old-to-new replacements +--- @param suite_names table A set of known suite names +function M.update_node(n, replacements, suite_names) + for receiver, suite in pairs(replacements) do + if n._data.name == receiver then + n._data.name = suite + n._data.type = "namespace" + elseif suite_names[n._data.name] then + n._data.type = "namespace" + end + n._data.id = M.replace_receiver_in_pos_id(n._data.id, receiver, suite) + end +end + +--- Update the nodes table with the given replacements. +--- @param nodes table The table of nodes to update +--- @param replacements table A table of old-to-new replacements +--- @return table The updated nodes table +function M.update_nodes_table(nodes, replacements) + local new_nodes = {} + for key, value in pairs(nodes) do + local new_key = key + for old, new in pairs(replacements) do + new_key = M.replace_receiver_in_pos_id(new_key, old, new) + end + new_nodes[new_key] = value + end + return new_nodes +end + +--- Recursively update a tree/node and its children with the given replacements. +--- @param n neotest.Tree The tree to update recursively +--- @param replacements table A table of old-to-new replacements +--- @param suite_names table A set of known suite names +function M.recursive_update(n, replacements, suite_names) + M.update_node(n, replacements, suite_names) + n._nodes = M.update_nodes_table(n._nodes, replacements) + for _, child in ipairs(n:children()) do + M.recursive_update(child, replacements, suite_names) + end +end + +--- Ensure parent-child relationships are correct after updating all nodes. +--- @param n neotest.Tree The node to fix relationships for +function M.fix_relationships(n) + for _, child in ipairs(n:children()) do + child._parent = n + M.fix_relationships(child) + end +end + return M From 3ae360cb4ecbc0d16f1c403d8e93b4974925445c Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 15:25:49 +0200 Subject: [PATCH 18/28] docs: add types, docs --- .../features/testify/lookup.lua | 161 +++++++++--------- lua/neotest-golang/features/testify/query.lua | 6 + 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/lua/neotest-golang/features/testify/lookup.lua b/lua/neotest-golang/features/testify/lookup.lua index 57fd4dd..7b0f8bd 100644 --- a/lua/neotest-golang/features/testify/lookup.lua +++ b/lua/neotest-golang/features/testify/lookup.lua @@ -1,88 +1,26 @@ -local parsers = require("nvim-treesitter.parsers") +--- Lookup table for testify suite receivers/suite names. local find = require("neotest-golang.find") local query = require("neotest-golang.query") local M = {} +--- The lookup map which is required for running testify suites and their tests. +--- @type table local lookup_map = {} -M.query = [[ - ; query for the lookup between receiver and test suite. - - ; package main // @package - (package_clause - (package_identifier) @package) - - ; func TestSuite(t *testing.T) { // @test_function - ; suite.Run(t, new(testSuitestruct)) // @suite_lib, @run_method, @suite_receiver - ; } - (function_declaration - name: (identifier) @test_function (#match? @test_function "^Test") - body: (block - (expression_statement - (call_expression - function: (selector_expression - operand: (identifier) @suite_lib (#eq? @suite_lib "suite") - field: (field_identifier) @run_method (#eq? @run_method "Run")) - arguments: (argument_list - (identifier) - (call_expression - arguments: (argument_list - (type_identifier) @suite_struct))))))) - - ; func TestSuite(t *testing.T) { // @test_function - ; s := &testSuiteStruct{} // @suite_struct - ; suite.Run(t, s) // @suite_lib, @run_method - ; } - (function_declaration - name: (identifier) @test_function (#match? @test_function "^Test") - parameters: (parameter_list - (parameter_declaration - name: (identifier) - type: (pointer_type - (qualified_type - package: (package_identifier) - name: (type_identifier))))) - body: (block - (short_var_declaration - left: (expression_list - (identifier)) - right: (expression_list - (unary_expression - operand: (composite_literal - type: (type_identifier) @suite_struct - body: (literal_value))))) - (expression_statement - (call_expression - function: (selector_expression - operand: (identifier) @suite_lib (#eq? @suite_lib "suite") - field: (field_identifier) @run_method (#eq? @run_method "Run")) - arguments: (argument_list - (identifier) - (identifier)))))) -]] +--- Get the current lookup map, generating it if empty. +--- @return table The lookup map containing testify suite information +function M.get() + if vim.tbl_isempty(lookup_map) then + lookup_map = M.generate() + end + return lookup_map +end +--- Generate the lookup map for testify suites. +--- @return table The generated lookup map function M.generate() - -- local example = { - -- ["/path/to/file1_test.go"] = { - -- package = "main", - -- receivers = { receiverStruct = true, receiverStruct2 = true }, - -- suites = { - -- receiverStruct = "TestSuite", - -- receiverStruct2 = "TestSuite2", - -- }, - -- }, - -- ["/path/to/file2_test.go"] = { - -- package = "main", - -- receivers = { receiverStruct3 = true }, - -- suites = { - -- receiverStruct3 = "TestSuite3", - -- }, - -- }, - -- -- ... other files ... - -- } - local cwd = vim.fn.getcwd() local filepaths = find.go_test_filepaths(cwd) local lookup = {} @@ -92,8 +30,6 @@ function M.generate() for _, filepath in ipairs(filepaths) do local matches = query.run_query_on_file(filepath, M.query) - -- vim.notify(vim.inspect(matches)) - local package_name = matches.package and matches.package[1] and matches.package[1].text @@ -133,13 +69,11 @@ function M.generate() return lookup end -function M.get() - if vim.tbl_isempty(lookup_map) then - lookup_map = M.generate() - end - return lookup_map -end - +--- Add a new entry to the lookup map. +--- @param file_name string The name of the file +--- @param package_name string The name of the package +--- @param suite_name string The name of the test suite +--- @param receiver_name string The name of the receiver function M.add(file_name, package_name, suite_name, receiver_name) if not lookup_map[file_name] then lookup_map[file_name] = {} @@ -162,8 +96,67 @@ function M.add(file_name, package_name, suite_name, receiver_name) table.insert(lookup_map[file_name], new_entry) end +--- Clear the lookup map. function M.clear() lookup_map = {} end +--- TreeSitter query for identifying testify suites and their components. +--- @type string +M.query = [[ + ; query for the lookup between receiver and test suite. + + ; package main // @package + (package_clause + (package_identifier) @package) + + ; func TestSuite(t *testing.T) { // @test_function + ; suite.Run(t, new(testSuitestruct)) // @suite_lib, @run_method, @suite_receiver + ; } + (function_declaration + name: (identifier) @test_function (#match? @test_function "^Test") + body: (block + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) + arguments: (argument_list + (identifier) + (call_expression + arguments: (argument_list + (type_identifier) @suite_struct))))))) + + ; func TestSuite(t *testing.T) { // @test_function + ; s := &testSuiteStruct{} // @suite_struct + ; suite.Run(t, s) // @suite_lib, @run_method + ; } + (function_declaration + name: (identifier) @test_function (#match? @test_function "^Test") + parameters: (parameter_list + (parameter_declaration + name: (identifier) + type: (pointer_type + (qualified_type + package: (package_identifier) + name: (type_identifier))))) + body: (block + (short_var_declaration + left: (expression_list + (identifier)) + right: (expression_list + (unary_expression + operand: (composite_literal + type: (type_identifier) @suite_struct + body: (literal_value))))) + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @suite_lib (#eq? @suite_lib "suite") + field: (field_identifier) @run_method (#eq? @run_method "Run")) + arguments: (argument_list + (identifier) + (identifier)))))) +]] + return M diff --git a/lua/neotest-golang/features/testify/query.lua b/lua/neotest-golang/features/testify/query.lua index b593cc6..154f3d7 100644 --- a/lua/neotest-golang/features/testify/query.lua +++ b/lua/neotest-golang/features/testify/query.lua @@ -1,7 +1,13 @@ +--- Query for detecting namespaces in Go test files, using testify suites. + local M = {} M.namespace_query = [[ ; query for receiver method, to be used as test suite namespace initially (will be replaced later). + + ; func (suite *testSuite) TestSomething() { // @namespace.name + ; // test code + ; } (method_declaration receiver: (parameter_list (parameter_declaration From 8b9ddfe1bb05986589f43767bac56a5d0ed230b3 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 15:36:20 +0200 Subject: [PATCH 19/28] fix: docs and improvements to treesitter querying --- lua/neotest-golang/query.lua | 54 ++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/lua/neotest-golang/query.lua b/lua/neotest-golang/query.lua index e62ccbb..51a23de 100644 --- a/lua/neotest-golang/query.lua +++ b/lua/neotest-golang/query.lua @@ -2,6 +2,10 @@ local parsers = require("nvim-treesitter.parsers") local M = {} +--- Run a TreeSitter query on a file and return the matches. +--- @param filepath string The path to the file to query +--- @param query_string string The TreeSitter query string +--- @return table A table of matches, where each key is a capture name and the value is a table of nodes function M.run_query_on_file(filepath, query_string) local bufnr = vim.api.nvim_create_buf(false, true) local content = vim.fn.readfile(filepath) @@ -17,24 +21,19 @@ function M.run_query_on_file(filepath, query_string) local tree = parser:parse()[1] local root = tree:root() + ---@type vim.treesitter.Query local query = vim.treesitter.query.parse("go", query_string) local matches = {} - local function add_match(name, node) - if not matches[name] then - matches[name] = {} - end - table.insert( - matches[name], - { name = name, node = node, text = M.get_node_text(node, bufnr) } - ) - end - - for pattern, match, metadata in query:iter_matches(root, bufnr, 0, -1) do - for id, node in pairs(match) do + for pattern, match, metadata in + query:iter_matches(root, bufnr, 0, -1, { all = true }) + do + for id, nodes in pairs(match) do local name = query.captures[id] - add_match(name, node) + for _, node in ipairs(nodes) do + M.add_match(matches, name, node, bufnr, metadata[id]) + end end end @@ -43,8 +42,33 @@ function M.run_query_on_file(filepath, query_string) return matches end -function M.get_node_text(node, bufnr) - local text = vim.treesitter.get_node_text(node, bufnr) -- NOTE: uses vim.treesitter +--- Add a match to the matches table +--- @param matches table The table of matches to add to +--- @param name string The name of the capture +--- @param node TSNode The TreeSitter node +--- @param bufnr integer The buffer number +--- @param metadata? table Optional metadata for the node +function M.add_match(matches, name, node, bufnr, metadata) + if not matches[name] then + matches[name] = {} + end + table.insert( + matches[name], + { + name = name, + node = node, + text = M.get_node_text(node, bufnr, { metadata = metadata }), + } + ) +end + +--- Get the text of a TreeSitter node. +--- @param node TSNode The TreeSitter node +--- @param bufnr integer|string The buffer number or content +--- @param opts? table Optional parameters (e.g., metadata for a specific capture) +--- @return string The text of the node +function M.get_node_text(node, bufnr, opts) + local text = vim.treesitter.get_node_text(node, bufnr, opts) -- NOTE: uses vim.treesitter if type(text) == "table" then return table.concat(text, "\n") end From 5cc1e3ca3f5c1f75133e6ac3fc1987b031419cc8 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 15:48:56 +0200 Subject: [PATCH 20/28] refactor: move query module into testify feature --- lua/neotest-golang/ast.lua | 2 +- lua/neotest-golang/features/testify/init.lua | 1 + .../features/testify/lookup.lua | 2 +- .../features/testify/namespace.lua | 19 +++++ lua/neotest-golang/features/testify/query.lua | 84 +++++++++++++++---- lua/neotest-golang/query.lua | 78 ----------------- 6 files changed, 92 insertions(+), 94 deletions(-) create mode 100644 lua/neotest-golang/features/testify/namespace.lua delete mode 100644 lua/neotest-golang/query.lua diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index a6d1e7f..a4ea869 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -135,7 +135,7 @@ function M.detect_tests(file_path) if options.get().testify == true then -- detect receiver method structs as namespaces. - query = query .. testify.query.namespace_query + query = query .. testify.namespace.query end ---@type neotest.Tree diff --git a/lua/neotest-golang/features/testify/init.lua b/lua/neotest-golang/features/testify/init.lua index 424ed6d..d477944 100644 --- a/lua/neotest-golang/features/testify/init.lua +++ b/lua/neotest-golang/features/testify/init.lua @@ -1,5 +1,6 @@ local M = {} +M.namespace = require("neotest-golang.features.testify.namespace") M.lookup = require("neotest-golang.features.testify.lookup") M.query = require("neotest-golang.features.testify.query") M.tree_modification = diff --git a/lua/neotest-golang/features/testify/lookup.lua b/lua/neotest-golang/features/testify/lookup.lua index 7b0f8bd..4410094 100644 --- a/lua/neotest-golang/features/testify/lookup.lua +++ b/lua/neotest-golang/features/testify/lookup.lua @@ -1,7 +1,7 @@ --- Lookup table for testify suite receivers/suite names. local find = require("neotest-golang.find") -local query = require("neotest-golang.query") +local query = require("neotest-golang.features.testify.query") local M = {} diff --git a/lua/neotest-golang/features/testify/namespace.lua b/lua/neotest-golang/features/testify/namespace.lua new file mode 100644 index 0000000..0b3b56b --- /dev/null +++ b/lua/neotest-golang/features/testify/namespace.lua @@ -0,0 +1,19 @@ +--- Query for detecting namespaces in Go test files, using testify suites. + +local M = {} + +M.query = [[ + ; query for receiver method, to be used as test suite namespace initially (will be replaced later). + + ; func (suite *testSuite) TestSomething() { // @namespace.name + ; // test code + ; } + (method_declaration + receiver: (parameter_list + (parameter_declaration + ; name: (identifier) + type: (pointer_type + (type_identifier) @namespace.name )))) @namespace.definition + ]] + +return M diff --git a/lua/neotest-golang/features/testify/query.lua b/lua/neotest-golang/features/testify/query.lua index 154f3d7..49cb11b 100644 --- a/lua/neotest-golang/features/testify/query.lua +++ b/lua/neotest-golang/features/testify/query.lua @@ -1,19 +1,75 @@ ---- Query for detecting namespaces in Go test files, using testify suites. +local parsers = require("nvim-treesitter.parsers") local M = {} -M.namespace_query = [[ - ; query for receiver method, to be used as test suite namespace initially (will be replaced later). - - ; func (suite *testSuite) TestSomething() { // @namespace.name - ; // test code - ; } - (method_declaration - receiver: (parameter_list - (parameter_declaration - ; name: (identifier) - type: (pointer_type - (type_identifier) @namespace.name )))) @namespace.definition - ]] +--- Run a TreeSitter query on a file and return the matches. +--- @param filepath string The path to the file to query +--- @param query_string string The TreeSitter query string +--- @return table A table of matches, where each key is a capture name and the value is a table of nodes +function M.run_query_on_file(filepath, query_string) + local bufnr = vim.api.nvim_create_buf(false, true) + local content = vim.fn.readfile(filepath) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) + + vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) + + if not parsers.has_parser("go") then + error("Go parser is not available. Please ensure it's installed.") + end + + local parser = parsers.get_parser(bufnr, "go") + local tree = parser:parse()[1] + local root = tree:root() + + ---@type vim.treesitter.Query + local query = vim.treesitter.query.parse("go", query_string) + + local matches = {} + + for pattern, match, metadata in + query:iter_matches(root, bufnr, 0, -1, { all = true }) + do + for id, nodes in pairs(match) do + local name = query.captures[id] + for _, node in ipairs(nodes) do + M.add_match(matches, name, node, bufnr, metadata[id]) + end + end + end + + vim.api.nvim_buf_delete(bufnr, { force = true }) + + return matches +end + +--- Add a match to the matches table +--- @param matches table The table of matches to add to +--- @param name string The name of the capture +--- @param node TSNode The TreeSitter node +--- @param bufnr integer The buffer number +--- @param metadata? table Optional metadata for the node +function M.add_match(matches, name, node, bufnr, metadata) + if not matches[name] then + matches[name] = {} + end + table.insert(matches[name], { + name = name, + node = node, + text = M.get_node_text(node, bufnr, { metadata = metadata }), + }) +end + +--- Get the text of a TreeSitter node. +--- @param node TSNode The TreeSitter node +--- @param bufnr integer|string The buffer number or content +--- @param opts? table Optional parameters (e.g., metadata for a specific capture) +--- @return string The text of the node +function M.get_node_text(node, bufnr, opts) + local text = vim.treesitter.get_node_text(node, bufnr, opts) -- NOTE: uses vim.treesitter + if type(text) == "table" then + return table.concat(text, "\n") + end + return text +end return M diff --git a/lua/neotest-golang/query.lua b/lua/neotest-golang/query.lua deleted file mode 100644 index 51a23de..0000000 --- a/lua/neotest-golang/query.lua +++ /dev/null @@ -1,78 +0,0 @@ -local parsers = require("nvim-treesitter.parsers") - -local M = {} - ---- Run a TreeSitter query on a file and return the matches. ---- @param filepath string The path to the file to query ---- @param query_string string The TreeSitter query string ---- @return table A table of matches, where each key is a capture name and the value is a table of nodes -function M.run_query_on_file(filepath, query_string) - local bufnr = vim.api.nvim_create_buf(false, true) - local content = vim.fn.readfile(filepath) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) - - vim.api.nvim_set_option_value("filetype", "go", { buf = bufnr }) - - if not parsers.has_parser("go") then - error("Go parser is not available. Please ensure it's installed.") - end - - local parser = parsers.get_parser(bufnr, "go") - local tree = parser:parse()[1] - local root = tree:root() - - ---@type vim.treesitter.Query - local query = vim.treesitter.query.parse("go", query_string) - - local matches = {} - - for pattern, match, metadata in - query:iter_matches(root, bufnr, 0, -1, { all = true }) - do - for id, nodes in pairs(match) do - local name = query.captures[id] - for _, node in ipairs(nodes) do - M.add_match(matches, name, node, bufnr, metadata[id]) - end - end - end - - vim.api.nvim_buf_delete(bufnr, { force = true }) - - return matches -end - ---- Add a match to the matches table ---- @param matches table The table of matches to add to ---- @param name string The name of the capture ---- @param node TSNode The TreeSitter node ---- @param bufnr integer The buffer number ---- @param metadata? table Optional metadata for the node -function M.add_match(matches, name, node, bufnr, metadata) - if not matches[name] then - matches[name] = {} - end - table.insert( - matches[name], - { - name = name, - node = node, - text = M.get_node_text(node, bufnr, { metadata = metadata }), - } - ) -end - ---- Get the text of a TreeSitter node. ---- @param node TSNode The TreeSitter node ---- @param bufnr integer|string The buffer number or content ---- @param opts? table Optional parameters (e.g., metadata for a specific capture) ---- @return string The text of the node -function M.get_node_text(node, bufnr, opts) - local text = vim.treesitter.get_node_text(node, bufnr, opts) -- NOTE: uses vim.treesitter - if type(text) == "table" then - return table.concat(text, "\n") - end - return text -end - -return M From fcb12d4be13c3c562e89c71a7f1665685bbaeb9b Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Wed, 10 Jul 2024 15:52:42 +0200 Subject: [PATCH 21/28] refactor: do not return early --- lua/neotest-golang/ast.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index a4ea869..7d8ed00 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -142,9 +142,7 @@ function M.detect_tests(file_path) local tree = lib.treesitter.parse_positions(file_path, query, opts) if options.get().testify == true then - local tree_modified_for_testify = - testify.tree_modification.modify_neotest_tree(tree) - return tree_modified_for_testify + tree = testify.tree_modification.modify_neotest_tree(tree) end return tree From 641f40a0fcd1a53b8a6019ba9e5f0b7b61927bad Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 00:04:49 +0200 Subject: [PATCH 22/28] fix: add match for ^Test in query --- lua/neotest-golang/features/testify/lookup.lua | 8 +++----- lua/neotest-golang/features/testify/namespace.lua | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lua/neotest-golang/features/testify/lookup.lua b/lua/neotest-golang/features/testify/lookup.lua index 4410094..02b05fb 100644 --- a/lua/neotest-golang/features/testify/lookup.lua +++ b/lua/neotest-golang/features/testify/lookup.lua @@ -48,11 +48,9 @@ function M.generate() -- Collect all test suite functions and their receivers for _, func in ipairs(matches.test_function or {}) do - if func.text:match("^Test") then - for _, node in ipairs(matches.suite_struct or {}) do - lookup[filepath].suites[node.text] = func.text - global_suites[node.text] = func.text - end + for _, node in ipairs(matches.suite_struct or {}) do + lookup[filepath].suites[node.text] = func.text + global_suites[node.text] = func.text end end end diff --git a/lua/neotest-golang/features/testify/namespace.lua b/lua/neotest-golang/features/testify/namespace.lua index 0b3b56b..0ea5d00 100644 --- a/lua/neotest-golang/features/testify/namespace.lua +++ b/lua/neotest-golang/features/testify/namespace.lua @@ -1,9 +1,9 @@ ---- Query for detecting namespaces in Go test files, using testify suites. +--- Query for detecting receiver type and treat as Neotest namespace. local M = {} M.query = [[ - ; query for receiver method, to be used as test suite namespace initially (will be replaced later). + ; query for detecting receiver type and treat as Neotest namespace. ; func (suite *testSuite) TestSomething() { // @namespace.name ; // test code @@ -14,6 +14,7 @@ M.query = [[ ; name: (identifier) type: (pointer_type (type_identifier) @namespace.name )))) @namespace.definition + name: (field_identifier) @test_function (#match? @test_function "^Test") ]] return M From e76cc5567ae9c48be1b9884e79683203b77164f5 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 00:42:15 +0200 Subject: [PATCH 23/28] fix: allow multiple suite structs per file --- .../features/testify/lookup.lua | 147 +++++++----------- lua/neotest-golang/features/testify/query.lua | 2 + .../features/testify/tree_modification.lua | 68 ++++---- 3 files changed, 91 insertions(+), 126 deletions(-) diff --git a/lua/neotest-golang/features/testify/lookup.lua b/lua/neotest-golang/features/testify/lookup.lua index 02b05fb..f06c8ba 100644 --- a/lua/neotest-golang/features/testify/lookup.lua +++ b/lua/neotest-golang/features/testify/lookup.lua @@ -1,104 +1,10 @@ ---- Lookup table for testify suite receivers/suite names. +--- Lookup table for renaming Neotest namespaces (receiver type to testify suite function). local find = require("neotest-golang.find") local query = require("neotest-golang.features.testify.query") local M = {} ---- The lookup map which is required for running testify suites and their tests. ---- @type table -local lookup_map = {} - ---- Get the current lookup map, generating it if empty. ---- @return table The lookup map containing testify suite information -function M.get() - if vim.tbl_isempty(lookup_map) then - lookup_map = M.generate() - end - return lookup_map -end - ---- Generate the lookup map for testify suites. ---- @return table The generated lookup map -function M.generate() - local cwd = vim.fn.getcwd() - local filepaths = find.go_test_filepaths(cwd) - local lookup = {} - local global_suites = {} - - -- First pass: collect all receivers and suites - for _, filepath in ipairs(filepaths) do - local matches = query.run_query_on_file(filepath, M.query) - - local package_name = matches.package - and matches.package[1] - and matches.package[1].text - or "unknown" - - lookup[filepath] = { - package = package_name, - receivers = {}, - suites = {}, - } - - -- Collect all receivers (same name as suite structs) - for _, struct in ipairs(matches.suite_struct or {}) do - lookup[filepath].receivers[struct.text] = true - end - - -- Collect all test suite functions and their receivers - for _, func in ipairs(matches.test_function or {}) do - for _, node in ipairs(matches.suite_struct or {}) do - lookup[filepath].suites[node.text] = func.text - global_suites[node.text] = func.text - end - end - end - - -- Second pass: ensure all files have all receivers and suites - for filepath, file_data in pairs(lookup) do - for receiver, suite in pairs(global_suites) do - if not file_data.receivers[receiver] and file_data.suites[receiver] then - file_data.receivers[receiver] = true - end - end - end - - return lookup -end - ---- Add a new entry to the lookup map. ---- @param file_name string The name of the file ---- @param package_name string The name of the package ---- @param suite_name string The name of the test suite ---- @param receiver_name string The name of the receiver -function M.add(file_name, package_name, suite_name, receiver_name) - if not lookup_map[file_name] then - lookup_map[file_name] = {} - end - local new_entry = { - package = package_name, - suite = suite_name, - receiver = receiver_name, - } - -- Check if entry already exists - for _, entry in ipairs(lookup_map[file_name]) do - if - entry.package == new_entry.package - and entry.suite == new_entry.suite - and entry.receiver == new_entry.receiver - then - return - end - end - table.insert(lookup_map[file_name], new_entry) -end - ---- Clear the lookup map. -function M.clear() - lookup_map = {} -end - --- TreeSitter query for identifying testify suites and their components. --- @type string M.query = [[ @@ -157,4 +63,55 @@ M.query = [[ (identifier)))))) ]] +--- The lookup table. +--- @type table +local lookup_table = {} + +--- Get the current lookup table, generating it if empty. +--- @return table The lookup table containing testify suite information +function M.get() + if vim.tbl_isempty(lookup_table) then + lookup_table = M.generate() + end + return lookup_table +end + +--- Generate the lookup table for testify suites. +--- @return table The generated lookup table +function M.generate() + local cwd = vim.fn.getcwd() + local filepaths = find.go_test_filepaths(cwd) + local lookup = {} + -- local global_suites = {} + + -- First pass: collect all data for the lookup table. + for _, filepath in ipairs(filepaths) do + local matches = query.run_query_on_file(filepath, M.query) + + local package_name = matches.package + and matches.package[1] + and matches.package[1].text + or "unknown" + + lookup[filepath] = { + package = package_name, + replacements = {}, + } + + for i, struct in ipairs(matches.suite_struct or {}) do + local func = matches.test_function[i] + if func then + lookup[filepath].replacements[struct.text] = func.text + end + end + end + + return lookup +end + +--- Clear the lookup table. +function M.clear() + lookup_table = {} +end + return M diff --git a/lua/neotest-golang/features/testify/query.lua b/lua/neotest-golang/features/testify/query.lua index 49cb11b..cf39559 100644 --- a/lua/neotest-golang/features/testify/query.lua +++ b/lua/neotest-golang/features/testify/query.lua @@ -1,3 +1,5 @@ +--- Helper functions around running Treesitter queries. + local parsers = require("nvim-treesitter.parsers") local M = {} diff --git a/lua/neotest-golang/features/testify/tree_modification.lua b/lua/neotest-golang/features/testify/tree_modification.lua index acba259..6864e50 100644 --- a/lua/neotest-golang/features/testify/tree_modification.lua +++ b/lua/neotest-golang/features/testify/tree_modification.lua @@ -1,14 +1,16 @@ ---- Opt-in functionality to support testify suites. +--- Functions to modify the Neotest tree, for testify suite support. local lookup = require("neotest-golang.features.testify.lookup") local M = {} ---- Modify the neotest tree, so that testify suites are properly described. +--- Modify the neotest tree, so that testify suites can be executed +--- as Neotest namespaces. --- --- When testify tests are discovered, they are discovered with the Go receiver ---- as the Neotest namespace. This is incorrect, and to fix this, we need to do ---- a search-replace of the receiver with the suite name. +--- type as the Neotest namespace. However, to produce a valid test path, +--- this receiver type must be replaced with the testify suite name in the +--- Neotest tree. --- @param tree neotest.Tree The original neotest tree --- @return neotest.Tree The modified tree. function M.modify_neotest_tree(tree) @@ -30,31 +32,33 @@ end --- Replace receiver methods with their corresponding test suites in the tree. --- @param tree neotest.Tree The tree to modify ---- @param file_lookup table The lookup table containing receiver-to-suite mappings +--- @param lookup_table table The lookup table containing receiver-to-suite mappings --- @return neotest.Tree The modified tree with receivers replaced by suites -function M.replace_receiver_with_suite(tree, file_lookup) - if not file_lookup then +function M.replace_receiver_with_suite(tree, lookup_table) + if not lookup_table then return tree end - -- Create a global replacements table and suite names set - local global_replacements = {} - local suite_names = {} - for _, file_data in pairs(file_lookup) do - if file_data.suites then - for receiver, suite in pairs(file_data.suites) do - global_replacements[receiver] = suite - suite_names[suite] = true + -- TODO: To make this more robust, it would be a good idea to only perform replacements + -- within the relevant Go package. Right now, this implementation is naive and will + -- not check for package boundaries. The file lookup contains all data required for this. + local replacements = {} + local suite_functions = {} + for _, file_data in pairs(lookup_table) do + if file_data.replacements then + for receiver_type, suite_function in pairs(file_data.replacements) do + replacements[receiver_type] = suite_function + suite_functions[suite_function] = true end end end - if vim.tbl_isempty(global_replacements) then + if vim.tbl_isempty(replacements) then -- no replacements found return tree end - M.recursive_update(tree, global_replacements, suite_names) + M.recursive_update(tree, replacements, suite_functions) M.fix_relationships(tree) return tree @@ -100,32 +104,34 @@ function M.merge_duplicate_namespaces(tree) return tree end --- Utility functions - --- Perform the neotest.Position id replacement. --- --- Namespaces and tests are delimited by "::" and we need to replace the receiver --- with the suite name here. --- @param str string The neotest.Position id ---- @param receiver string The receiver name ---- @param suite string The suite name +--- @param receiver_type string The receiver type +--- @param suite_function string The suite function name --- @return string The modified neotest.Position id string -function M.replace_receiver_in_pos_id(str, receiver, suite) - local modified = str:gsub("::" .. receiver .. "::", "::" .. suite .. "::") - modified = modified:gsub("::" .. receiver .. "$", "::" .. suite) +function M.replace_receiver_in_pos_id(str, receiver_type, suite_function) + local modified = + str:gsub("::" .. receiver_type .. "::", "::" .. suite_function .. "::") + modified = modified:gsub("::" .. receiver_type .. "$", "::" .. suite_function) return modified end --- Update a single neotest.Tree node with the given replacements. --- @param n neotest.Tree The node to update --- @param replacements table A table of old-to-new replacements ---- @param suite_names table A set of known suite names -function M.update_node(n, replacements, suite_names) +--- @param suite_functions table A set of known suite functions +function M.update_node(n, replacements, suite_functions) + -- TODO: To make this more robust, it would be a good idea to only perform replacements + -- within the relevant Go package. Right now, this implementation is naive and will + -- not check for package boundaries. for receiver, suite in pairs(replacements) do if n._data.name == receiver then n._data.name = suite n._data.type = "namespace" - elseif suite_names[n._data.name] then + elseif suite_functions[n._data.name] then n._data.type = "namespace" end n._data.id = M.replace_receiver_in_pos_id(n._data.id, receiver, suite) @@ -151,12 +157,12 @@ end --- Recursively update a tree/node and its children with the given replacements. --- @param n neotest.Tree The tree to update recursively --- @param replacements table A table of old-to-new replacements ---- @param suite_names table A set of known suite names -function M.recursive_update(n, replacements, suite_names) - M.update_node(n, replacements, suite_names) +--- @param suite_functions table A set of known suite functions +function M.recursive_update(n, replacements, suite_functions) + M.update_node(n, replacements, suite_functions) n._nodes = M.update_nodes_table(n._nodes, replacements) for _, child in ipairs(n:children()) do - M.recursive_update(child, replacements, suite_names) + M.recursive_update(child, replacements, suite_functions) end end From c947f7e3013737cba52f4976e89104e9311c579c Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 18:55:29 +0200 Subject: [PATCH 24/28] fix: do not detect test methods unless testify is enabled --- lua/neotest-golang/ast.lua | 14 +++++-------- lua/neotest-golang/features/testify/init.lua | 1 - .../features/testify/namespace.lua | 20 ------------------ lua/neotest-golang/features/testify/query.lua | 21 +++++++++++++++++++ 4 files changed, 26 insertions(+), 30 deletions(-) delete mode 100644 lua/neotest-golang/features/testify/namespace.lua diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 7d8ed00..3fb6595 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -21,12 +21,6 @@ M.test_function = [[ @test.definition ]] -M.test_method = [[ - ; query for test method - (method_declaration - name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition - ]] - M.table_tests = [[ ;; query for list table tests (block @@ -131,11 +125,13 @@ M.table_tests = [[ --- @param file_path string function M.detect_tests(file_path) local opts = { nested_tests = true } - local query = M.test_function .. M.test_method .. M.table_tests + local query = M.test_function .. M.table_tests if options.get().testify == true then - -- detect receiver method structs as namespaces. - query = query .. testify.namespace.query + -- detect receiver types (as namespaces) and test methods. + query = query + .. testify.query.namespace_query + .. testify.query.test_method_query end ---@type neotest.Tree diff --git a/lua/neotest-golang/features/testify/init.lua b/lua/neotest-golang/features/testify/init.lua index d477944..424ed6d 100644 --- a/lua/neotest-golang/features/testify/init.lua +++ b/lua/neotest-golang/features/testify/init.lua @@ -1,6 +1,5 @@ local M = {} -M.namespace = require("neotest-golang.features.testify.namespace") M.lookup = require("neotest-golang.features.testify.lookup") M.query = require("neotest-golang.features.testify.query") M.tree_modification = diff --git a/lua/neotest-golang/features/testify/namespace.lua b/lua/neotest-golang/features/testify/namespace.lua deleted file mode 100644 index 0ea5d00..0000000 --- a/lua/neotest-golang/features/testify/namespace.lua +++ /dev/null @@ -1,20 +0,0 @@ ---- Query for detecting receiver type and treat as Neotest namespace. - -local M = {} - -M.query = [[ - ; query for detecting receiver type and treat as Neotest namespace. - - ; func (suite *testSuite) TestSomething() { // @namespace.name - ; // test code - ; } - (method_declaration - receiver: (parameter_list - (parameter_declaration - ; name: (identifier) - type: (pointer_type - (type_identifier) @namespace.name )))) @namespace.definition - name: (field_identifier) @test_function (#match? @test_function "^Test") - ]] - -return M diff --git a/lua/neotest-golang/features/testify/query.lua b/lua/neotest-golang/features/testify/query.lua index cf39559..0359961 100644 --- a/lua/neotest-golang/features/testify/query.lua +++ b/lua/neotest-golang/features/testify/query.lua @@ -4,6 +4,27 @@ local parsers = require("nvim-treesitter.parsers") local M = {} +M.namespace_query = [[ + ; query for detecting receiver type and treat as Neotest namespace. + + ; func (suite *testSuite) TestSomething() { // @namespace.name + ; // test code + ; } + (method_declaration + receiver: (parameter_list + (parameter_declaration + ; name: (identifier) + type: (pointer_type + (type_identifier) @namespace.name )))) @namespace.definition + name: (field_identifier) @test_function (#match? @test_function "^(Test|Example)") + ]] + +M.test_method_query = [[ + ; query for test method + (method_declaration + name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition + ]] + --- Run a TreeSitter query on a file and return the matches. --- @param filepath string The path to the file to query --- @param query_string string The TreeSitter query string From 92a0f4d26e9cf6a47aa6f3dce0b685bb77a93b4c Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 19:06:28 +0200 Subject: [PATCH 25/28] test: testify suites --- tests/go/testify/othersuite_test.go | 23 ++++ tests/go/testify/positions_spec.lua | 172 ++++++++++++++++++++++++++++ tests/go/testify/positions_test.go | 72 ++++++++++++ tests/go/testify1_test.go | 41 ------- tests/go/testify2_test.go | 32 ------ tests/go/testify_test.go | 36 ------ 6 files changed, 267 insertions(+), 109 deletions(-) create mode 100644 tests/go/testify/othersuite_test.go create mode 100644 tests/go/testify/positions_spec.lua create mode 100644 tests/go/testify/positions_test.go delete mode 100644 tests/go/testify1_test.go delete mode 100644 tests/go/testify2_test.go delete mode 100644 tests/go/testify_test.go diff --git a/tests/go/testify/othersuite_test.go b/tests/go/testify/othersuite_test.go new file mode 100644 index 0000000..8c91ae8 --- /dev/null +++ b/tests/go/testify/othersuite_test.go @@ -0,0 +1,23 @@ +package testify + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type OtherTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// A second suite setup method. +func (suite *OtherTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +func TestOtherTestSuite(t *testing.T) { + s := &OtherTestSuite{} + suite.Run(t, s) +} diff --git a/tests/go/testify/positions_spec.lua b/tests/go/testify/positions_spec.lua new file mode 100644 index 0000000..2a2853c --- /dev/null +++ b/tests/go/testify/positions_spec.lua @@ -0,0 +1,172 @@ +local nio = require("nio") +local _ = require("plenary") + +local adapter = require("neotest-golang") +local options = require("neotest-golang.options") +local testify = require("neotest-golang.features.testify") + +local function compareIgnoringKeys(t1, t2, ignoreKeys) + local function copyTable(t, ignoreKeys) + local copy = {} + for k, v in pairs(t) do + if not ignoreKeys[k] then + if type(v) == "table" then + copy[k] = copyTable(v, ignoreKeys) + else + copy[k] = v + end + end + end + return copy + end + return copyTable(t1, ignoreKeys), copyTable(t2, ignoreKeys) +end + +describe("With testify_enabled=false", function() + it("Discover test functions", function() + -- Arrange + local test_filepath = vim.loop.cwd() + .. "/tests/go/testify/positions_test.go" + local expected = { + { + id = test_filepath, + name = "positions_test.go", + path = test_filepath, + type = "file", + }, + { + { + id = test_filepath .. "::TestExampleTestSuite", + name = "TestExampleTestSuite", + path = test_filepath, + type = "test", + }, + }, + { + { + id = test_filepath .. "::TestTrivial", + name = "TestTrivial", + path = test_filepath, + type = "test", + }, + }, + } + + -- Act + ---@type neotest.Tree + local tree = + nio.tests.with_async_context(adapter.discover_positions, test_filepath) + + -- Assert + local result = tree:to_list() + local ignoreKeys = { range = true } + local expectedCopy, resultCopy = + compareIgnoringKeys(expected, result, ignoreKeys) + assert.are.same(vim.inspect(expectedCopy), vim.inspect(resultCopy)) + assert.are.same(expectedCopy, resultCopy) + end) +end) + +describe("With testify_enabled=true", function() + it("Discover namespaces, test methods and test function", function() + -- Arrange + local test_filepath = vim.loop.cwd() + .. "/tests/go/testify/positions_test.go" + options.set({ testify = true }) -- enable testify + testify.lookup.generate() -- generate lookup + + local expected = { + { + id = test_filepath, + name = "positions_test.go", + path = test_filepath, + type = "file", + }, + { + { + id = test_filepath .. "::TestExampleTestSuite", + name = "TestExampleTestSuite", + path = test_filepath, + type = "namespace", + }, + { + { + id = test_filepath .. "::TestExampleTestSuite::TestExample", + name = "TestExample", + path = test_filepath, + type = "test", + }, + }, + { + { + id = test_filepath .. "::TestExampleTestSuite::TestExample2", + name = "TestExample2", + path = test_filepath, + type = "test", + }, + }, + }, + { + { + id = test_filepath .. "::ExampleTestSuite2", + name = "ExampleTestSuite2", + path = test_filepath, + type = "namespace", + }, + { + { + id = test_filepath .. "::ExampleTestSuite2::TestExample", + name = "TestExample", + path = test_filepath, + type = "test", + }, + }, + { + { + id = test_filepath .. "::ExampleTestSuite2::TestExample2", + name = "TestExample2", + path = test_filepath, + type = "test", + }, + }, + }, + { + { + id = test_filepath .. "::TestTrivial", + name = "TestTrivial", + path = test_filepath, + type = "test", + }, + }, + { + { + id = test_filepath .. "::TestOtherTestSuite", + name = "TestOtherTestSuite", + path = test_filepath, + type = "namespace", + }, + { + { + id = test_filepath .. "::TestOtherTestSuite::TestOther", + name = "TestOther", + path = test_filepath, + type = "test", + }, + }, + }, + } + + -- Act + ---@type neotest.Tree + local tree = + nio.tests.with_async_context(adapter.discover_positions, test_filepath) + + -- Assert + local result = tree:to_list() + local ignoreKeys = { range = true } + local expectedCopy, resultCopy = + compareIgnoringKeys(expected, result, ignoreKeys) + assert.are.same(vim.inspect(expectedCopy), vim.inspect(resultCopy)) + assert.are.same(expectedCopy, resultCopy) + end) +end) diff --git a/tests/go/testify/positions_test.go b/tests/go/testify/positions_test.go new file mode 100644 index 0000000..d31dd9f --- /dev/null +++ b/tests/go/testify/positions_test.go @@ -0,0 +1,72 @@ +package testify + +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +func (suite *ExampleTestSuite) TestExample2() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} + +// -------------------------------------------------------------------- + +type ExampleTestSuite2 struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +func (suite *ExampleTestSuite2) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +func (suite *ExampleTestSuite2) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +func (suite *ExampleTestSuite2) TestExample2() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +// -------------------------------------------------------------------- + +// Just a regular test. +func TestTrivial(t *testing.T) { + assert.Equal(t, 1, 1) +} + +// -------------------------------------------------------------------- + +// A test method which uses a receiver type defined by struct in another file. +func (suite *OtherTestSuite) TestOther() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} diff --git a/tests/go/testify1_test.go b/tests/go/testify1_test.go deleted file mode 100644 index 415a518..0000000 --- a/tests/go/testify1_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -// Basic imports -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -// Define the suite, and absorb the built-in basic suite -// functionality from testify - including a T() method which -// returns the current testing context -type receiverStruct struct { - suite.Suite - VariableThatShouldStartAtFive int -} - -// Make sure that VariableThatShouldStartAtFive is set to five -// before each test -func (suite *receiverStruct) SetupTest() { - suite.VariableThatShouldStartAtFive = 5 -} - -// All methods that begin with "Test" are run as tests within a -// suite. -func (suite *receiverStruct) TestExample1() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) - suite.Equal(5, suite.VariableThatShouldStartAtFive) -} - -func (suite *receiverStruct) TestExample2() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) - suite.Equal(5, suite.VariableThatShouldStartAtFive) -} - -// In order for 'go test' to run this suite, we need to create -// a normal test function and pass our suite to suite.Run -func TestSuite(t *testing.T) { - suite.Run(t, new(receiverStruct)) -} diff --git a/tests/go/testify2_test.go b/tests/go/testify2_test.go deleted file mode 100644 index d387a13..0000000 --- a/tests/go/testify2_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -// Basic imports -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type receiverStruct2 struct { - suite.Suite - VariableThatShouldStartAtFive int -} - -func (suite *receiverStruct2) SetupTest() { - suite.VariableThatShouldStartAtFive = 5 -} - -func (suite *receiverStruct2) TestExample3() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) - suite.Equal(5, suite.VariableThatShouldStartAtFive) -} - -func (suite *receiverStruct) TestExample4() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) - suite.Equal(5, suite.VariableThatShouldStartAtFive) -} - -func TestSuite2(t *testing.T) { - suite.Run(t, new(receiverStruct2)) -} diff --git a/tests/go/testify_test.go b/tests/go/testify_test.go deleted file mode 100644 index 0adf90a..0000000 --- a/tests/go/testify_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -// Basic imports -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -// Define the suite, and absorb the built-in basic suite -// functionality from testify - including a T() method which -// returns the current testing context -type ExampleTestSuite struct { - suite.Suite - VariableThatShouldStartAtFive int -} - -// Make sure that VariableThatShouldStartAtFive is set to five -// before each test -func (suite *ExampleTestSuite) SetupTest() { - suite.VariableThatShouldStartAtFive = 5 -} - -// All methods that begin with "Test" are run as tests within a -// suite. -func (suite *ExampleTestSuite) TestExample() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) - suite.Equal(5, suite.VariableThatShouldStartAtFive) -} - -// In order for 'go test' to run this suite, we need to create -// a normal test function and pass our suite to suite.Run -func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(ExampleTestSuite)) -} From d7500ca9d167ca71d83d49f1102dc39c796860a8 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 19:18:36 +0200 Subject: [PATCH 26/28] test: testify lookup --- tests/go/testify/lookup_spec.lua | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/go/testify/lookup_spec.lua diff --git a/tests/go/testify/lookup_spec.lua b/tests/go/testify/lookup_spec.lua new file mode 100644 index 0000000..82cee93 --- /dev/null +++ b/tests/go/testify/lookup_spec.lua @@ -0,0 +1,42 @@ +local _ = require("plenary") + +local options = require("neotest-golang.options") +local testify = require("neotest-golang.features.testify") + +describe("Lookup", function() + it("Generates tree replacement instructions", function() + -- Arrange + options.set({ testify = true }) -- enable testify + local folderpath = vim.loop.cwd() .. "/tests/go" + local expected_lookup = { + [folderpath .. "/positions_test.go"] = { + package = "main", + replacements = {}, + }, + [folderpath .. "/testify/othersuite_test.go"] = { + package = "testify", + replacements = { + OtherTestSuite = "TestOtherTestSuite", + }, + }, + [folderpath .. "/testify/positions_test.go"] = { + package = "testify", + replacements = { + ExampleTestSuite = "TestExampleTestSuite", + }, + }, + [folderpath .. "/testname_test.go"] = { + package = "main", + replacements = {}, + }, + } + + -- Act + testify.lookup.generate() -- generate lookup + + -- Assert + local lookup = testify.lookup.get() + assert.are.same(vim.inspect(expected_lookup), vim.inspect(lookup)) + assert.are.same(expected_lookup, lookup) + end) +end) From f44809b580320a1202d3ce36b9d14f73678c7e71 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 19:24:03 +0200 Subject: [PATCH 27/28] fix(option): testify -> testify_enabled --- README.md | 2 +- lua/neotest-golang/ast.lua | 4 ++-- lua/neotest-golang/init.lua | 2 +- lua/neotest-golang/options.lua | 2 +- tests/go/testify/lookup_spec.lua | 2 +- tests/go/testify/positions_spec.lua | 2 +- tests/unit/options_spec.lua | 12 ++++++------ 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 22378ed..93baaf7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ return { | `go_test_args` | `{ "-v", "-race", "-count=1" }` | Arguments to pass into `go test`. | | `dap_go_enabled` | `false` | Leverage [leoluz/nvim-dap-go](https://github.com/leoluz/nvim-dap-go) for debugging tests. | | `dap_go_opts` | `{}` | Options to pass into `require("dap-go").setup()`. | -| `testify` | `false` | Enable support for [stretchr/testify](https://github.com/stretchr/testify) suites. | +| `testify_enabled` | `false` | Enable support for [stretchr/testify](https://github.com/stretchr/testify) suites. | | `warn_test_name_dupes` | `true` | Warn about duplicate test names within the same Go package. | | `warn_test_not_executed` | `true` | Warn if test was not executed. | diff --git a/lua/neotest-golang/ast.lua b/lua/neotest-golang/ast.lua index 3fb6595..43b8382 100644 --- a/lua/neotest-golang/ast.lua +++ b/lua/neotest-golang/ast.lua @@ -127,7 +127,7 @@ function M.detect_tests(file_path) local opts = { nested_tests = true } local query = M.test_function .. M.table_tests - if options.get().testify == true then + if options.get().testify_enabled == true then -- detect receiver types (as namespaces) and test methods. query = query .. testify.query.namespace_query @@ -137,7 +137,7 @@ function M.detect_tests(file_path) ---@type neotest.Tree local tree = lib.treesitter.parse_positions(file_path, query, opts) - if options.get().testify == true then + if options.get().testify_enabled == true then tree = testify.tree_modification.modify_neotest_tree(tree) end diff --git a/lua/neotest-golang/init.lua b/lua/neotest-golang/init.lua index a0aaa76..912616f 100644 --- a/lua/neotest-golang/init.lua +++ b/lua/neotest-golang/init.lua @@ -18,7 +18,7 @@ local M = {} M.Adapter = { name = "neotest-golang", init = function() - if options.get().testify == true then + if options.get().testify_enabled == true then testify.lookup.generate() end end, diff --git a/lua/neotest-golang/options.lua b/lua/neotest-golang/options.lua index b983052..d8ce358 100644 --- a/lua/neotest-golang/options.lua +++ b/lua/neotest-golang/options.lua @@ -8,9 +8,9 @@ local opts = { go_test_args = { "-v", "-race", "-count=1" }, dap_go_enabled = false, dap_go_opts = {}, + testify_enabled = false, warn_test_name_dupes = true, warn_test_not_executed = true, - testify = false, -- experimental, for now undocumented, options runner = "go", -- or "gotestsum" diff --git a/tests/go/testify/lookup_spec.lua b/tests/go/testify/lookup_spec.lua index 82cee93..28839a0 100644 --- a/tests/go/testify/lookup_spec.lua +++ b/tests/go/testify/lookup_spec.lua @@ -6,7 +6,7 @@ local testify = require("neotest-golang.features.testify") describe("Lookup", function() it("Generates tree replacement instructions", function() -- Arrange - options.set({ testify = true }) -- enable testify + options.set({ testify_enabled = true }) -- enable testify local folderpath = vim.loop.cwd() .. "/tests/go" local expected_lookup = { [folderpath .. "/positions_test.go"] = { diff --git a/tests/go/testify/positions_spec.lua b/tests/go/testify/positions_spec.lua index 2a2853c..2b051d6 100644 --- a/tests/go/testify/positions_spec.lua +++ b/tests/go/testify/positions_spec.lua @@ -72,7 +72,7 @@ describe("With testify_enabled=true", function() -- Arrange local test_filepath = vim.loop.cwd() .. "/tests/go/testify/positions_test.go" - options.set({ testify = true }) -- enable testify + options.set({ testify_enabled = true }) -- enable testify testify.lookup.generate() -- generate lookup local expected = { diff --git a/tests/unit/options_spec.lua b/tests/unit/options_spec.lua index f009a7b..8a6f424 100644 --- a/tests/unit/options_spec.lua +++ b/tests/unit/options_spec.lua @@ -3,16 +3,16 @@ local options = require("neotest-golang.options") describe("Options are set up", function() it("With defaults", function() local expected_options = { - dap_go_enabled = false, - dap_go_opts = {}, go_test_args = { "-v", "-race", "-count=1", }, + dap_go_enabled = false, + dap_go_opts = {}, + testify_enabled = false, warn_test_name_dupes = true, warn_test_not_executed = true, - testify = false, -- experimental runner = "go", @@ -25,17 +25,17 @@ describe("Options are set up", function() it("With non-defaults", function() local expected_options = { - dap_go_enabled = false, - dap_go_opts = {}, go_test_args = { "-v", "-race", "-count=1", "-parallel=1", -- non-default }, + dap_go_enabled = false, + dap_go_opts = {}, + testify_enabled = false, warn_test_name_dupes = true, warn_test_not_executed = true, - testify = false, -- experimental runner = "go", From c108e31b777913bfe9a6b72d1c43ed7b848969ac Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 11 Jul 2024 19:44:32 +0200 Subject: [PATCH 28/28] docs: update readme --- README.md | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 93baaf7..7bb51d0 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,14 @@ return { ## ⚙️ Configuration -| Argument | Default value | Description | -| ------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------- | -| `go_test_args` | `{ "-v", "-race", "-count=1" }` | Arguments to pass into `go test`. | -| `dap_go_enabled` | `false` | Leverage [leoluz/nvim-dap-go](https://github.com/leoluz/nvim-dap-go) for debugging tests. | -| `dap_go_opts` | `{}` | Options to pass into `require("dap-go").setup()`. | -| `testify_enabled` | `false` | Enable support for [stretchr/testify](https://github.com/stretchr/testify) suites. | -| `warn_test_name_dupes` | `true` | Warn about duplicate test names within the same Go package. | -| `warn_test_not_executed` | `true` | Warn if test was not executed. | +| Argument | Default value | Description | +| ------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `go_test_args` | `{ "-v", "-race", "-count=1" }` | Arguments to pass into `go test`. | +| `dap_go_enabled` | `false` | Leverage [leoluz/nvim-dap-go](https://github.com/leoluz/nvim-dap-go) for debugging tests. | +| `dap_go_opts` | `{}` | Options to pass into `require("dap-go").setup()`. | +| `testify_enabled` | `false` | Enable support for [testify](https://github.com/stretchr/testify) suites. See [here](https://github.com/fredrikaverpil/#testify-suites) for more info. | +| `warn_test_name_dupes` | `true` | Warn about duplicate test names within the same Go package. | +| `warn_test_not_executed` | `true` | Warn if test was not executed. | ### Example configuration: custom `go test` arguments @@ -355,6 +355,27 @@ and number of tests to run in parallel using the `-p` and `-parallel` flags, respectively. Execute `go help test`, `go help testflag`, `go help build` for more information on this. +### Testify suites + +> [!WARNING] +> This feature comes with some caveats and nuances, which is why it +> is not enabled by default. I respectfully advise you to only enable this if +> you really need it. + +There are some real shenaningans going on behind the scenes to make this work. +First, a lookup of "receiver type-to-suite test function" must be created of all +Go test files in your project. Then, the generated Neotest node tree needs to be +modified by mutating private attributes and merging of nodes to avoid +duplicates. I'm personally a bit afraid of the maintenance burden of this +feature... + +> [!NOTE] +> Right now, there is no way to update the lookup other than restarting +> Neotest/Neovim. So in case you are implementing a new suite, please restart to +> see the new suites/tests appear in e.g. the summary window. Also, nested tests +> or table tests are not supported, but can be added at any time. Feel free to +> open a PR! + ## 🙏 PRs are welcome Improvement suggestion PRs to this repo are very much welcome, and I encourage