Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: testify test suite support #58

Merged
merged 28 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ffd87a3
feat(poc): support running testify suite test function
fredrikaverpil Jul 3, 2024
b104f7a
fix(treesitter): use nvim-treesitter instead of vim.treesitter
fredrikaverpil Jul 7, 2024
fe5e7f1
feat: rebuild neotest tree, remove hacks
fredrikaverpil Jul 7, 2024
fea4463
fix: set namespace type, merge namespaces
fredrikaverpil Jul 7, 2024
182ef50
fix(E565): Not allowed to change text or change window
fredrikaverpil Jul 8, 2024
9129da8
fix: receiver method detection as opt-in
fredrikaverpil Jul 8, 2024
79f651f
fix: provide output for namespace execution
fredrikaverpil Jul 8, 2024
54be433
feat: support alternative receiver/suite syntax
fredrikaverpil Jul 8, 2024
9d555b9
feat: cleanup queries
fredrikaverpil Jul 8, 2024
2d44a0a
fix: remove
fredrikaverpil Jul 8, 2024
69452b8
fix: typo
fredrikaverpil Jul 8, 2024
dc69d04
fix: documentation
fredrikaverpil Jul 8, 2024
75de935
fix: do not confuse regular test functions with suites
fredrikaverpil Jul 9, 2024
05aaca4
refactor: break into modules
fredrikaverpil Jul 9, 2024
c280c7d
docs: add types/docs
fredrikaverpil Jul 10, 2024
cb309fc
fix: warning
fredrikaverpil Jul 10, 2024
0ebab3a
refactor: break out inner functions
fredrikaverpil Jul 10, 2024
3ae360c
docs: add types, docs
fredrikaverpil Jul 10, 2024
8b9ddfe
fix: docs and improvements to treesitter querying
fredrikaverpil Jul 10, 2024
5cc1e3c
refactor: move query module into testify feature
fredrikaverpil Jul 10, 2024
fcb12d4
refactor: do not return early
fredrikaverpil Jul 10, 2024
641f40a
fix: add match for ^Test in query
fredrikaverpil Jul 10, 2024
e76cc55
fix: allow multiple suite structs per file
fredrikaverpil Jul 10, 2024
c947f7e
fix: do not detect test methods unless testify is enabled
fredrikaverpil Jul 11, 2024
92a0f4d
test: testify suites
fredrikaverpil Jul 11, 2024
d7500ca
test: testify lookup
fredrikaverpil Jul 11, 2024
f44809b
fix(option): testify -> testify_enabled
fredrikaverpil Jul 11, 2024
c108e31
docs: update readme
fredrikaverpil Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +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()`. |
| `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

Expand Down Expand Up @@ -354,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
Expand Down
36 changes: 22 additions & 14 deletions lua/neotest-golang/ast.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

local lib = require("neotest.lib")

local options = require("neotest-golang.options")
local testify = require("neotest-golang.features.testify")

local M = {}

--- Detect test names in Go *._test.go files.
--- @param file_path string
function M.detect_tests(file_path)
local test_function = [[
M.test_function = [[
; query for test function
((function_declaration
name: (identifier) @test.name) (#match? @test.name "^(Test|Example)"))
Expand All @@ -21,13 +21,7 @@ function M.detect_tests(file_path)
@test.definition
]]

local test_method = [[
; query for test method
(method_declaration
name: (field_identifier) @test.name (#match? @test.name "^(Test|Example)")) @test.definition
]]

local table_tests = [[
M.table_tests = [[
;; query for list table tests
(block
(short_var_declaration
Expand Down Expand Up @@ -127,13 +121,27 @@ function M.detect_tests(file_path)
(#eq? @test.key.name @test.key.name1))))))))
]]

local query = test_function .. test_method .. table_tests
--- 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.table_tests

if options.get().testify_enabled == true then
-- detect receiver types (as namespaces) and test methods.
query = query
.. testify.query.namespace_query
.. testify.query.test_method_query
end

---@type neotest.Tree
local positions = lib.treesitter.parse_positions(file_path, query, opts)
local tree = lib.treesitter.parse_positions(file_path, query, opts)

if options.get().testify_enabled == true then
tree = testify.tree_modification.modify_neotest_tree(tree)
end

return positions
return tree
end

return M
Empty file.
8 changes: 8 additions & 0 deletions lua/neotest-golang/features/testify/init.lua
Original file line number Diff line number Diff line change
@@ -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
117 changes: 117 additions & 0 deletions lua/neotest-golang/features/testify/lookup.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
--- 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 = {}

--- 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))))))
]]

--- The lookup table.
--- @type table<string, table>
local lookup_table = {}

--- Get the current lookup table, generating it if empty.
--- @return table<string, 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<string, 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
98 changes: 98 additions & 0 deletions lua/neotest-golang/features/testify/query.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
--- Helper functions around running Treesitter queries.

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
--- @return table<string, 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<string, 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
Loading
Loading