Open brunobmello25 opened 7 months ago
to add to this, I believe that it would be hard to implement since there are a lot of variances you can have for a test table setup, but maybe we could provide an input suggestion for the test name?
for example, when we run debug_test()
with the cursor inside a string, it could prompt us to choose if we want to just run the test function (current behavior) or the test with the name that is inside that string. Would that be possible?
You are right that implementing this logic wouldn't be trivial. There is no pattern to follow as test tables in go is not really standardized by the language. As a workaround, what I do is adding a skip
attribute in every test case and call test.SkipNow()
if it is set to true. Not ideal but gets the job done.
is it viable to implement passing test names to debug_test? maybe with that we could open the possibility to run a test with a specific name inside a function, and the user figures out how to pass this name to the function
I managed to do it the following way: I added a new configuration:
{
type = "go",
name = "Debug test case",
request = "launch",
mode = "test",
program = "./${relativeFileDirname}",
args = function()
return { "-test.run", require("extensions.gotest").tbl_testcase_name() }
end,
}
This will run just the test that's name is determined by the function called in args
. For the sake of completeness here is how I implemented it (it relies on the fact that all of my table tests have a name as a string
as the first case-property:
local M = {}
local func_query_parsed = nil
local func_query = function()
if func_query_parsed == nil then
func_query_parsed = vim.treesitter.query.parse(
"go",
[[
(function_declaration
name: (identifier) @test.name
parameters: (parameter_list
. (parameter_declaration
type: (pointer_type) @type) .)
(#match? @type "*testing.(T|M)")
(#match? @test.name "^Test.+$")) @test.function
]]
)
end
return func_query_parsed
end
local tbl_case_query_parsed = nil
local tbl_case_query = function()
if tbl_case_query_parsed == nil then
tbl_case_query_parsed = vim.treesitter.query.parse(
"go",
[[
(literal_value (
literal_element (
literal_value .(
keyed_element
(literal_element (identifier))
(literal_element (interpreted_string_literal) @testcase.name)
)
) @testcase.struct
))
]]
)
end
return tbl_case_query_parsed
end
local name_from_query = function(bufnr, query, capture_name_node, capture_block_node)
local bufn = bufnr or vim.api.nvim_get_current_buf()
local ok, parser = pcall(vim.treesitter.get_parser, bufn)
if not ok or not parser then
return
end
local tree = parser:parse()
tree = tree[1]
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
row = row - 1
for _, match, _ in query:iter_matches(tree:root(), bufn, 0, row, { all = true }) do
local name = nil
local cursor_in_result = false
for id, nodes in pairs(match) do
local capture = query.captures[id]
for _, node in ipairs(nodes) do
if capture == capture_name_node then
name = vim.treesitter.get_node_text(node, bufn)
end
if capture == capture_block_node then
cursor_in_result = vim.treesitter.is_in_node_range(node, row, col)
end
end
end
if cursor_in_result then
return name
end
end
return nil
end
local format_test_name = function(name, opts)
opts = opts or {}
name = name:gsub('"', "")
-- NOTE: \Q...\E encloses literal text in go's regex engine (https://github.com/google/re2/wiki/Syntax)
local esacped_name = string.format([[^\Q%s\E$]], name)
if opts.quote then
return "'" .. esacped_name .. "'"
end
return esacped_name
end
M.test_func_name = function(bufnr, opts)
local test_name = name_from_query(bufnr, func_query(), "test.name", "test.function")
if test_name == "" or test_name == nil then
return nil
end
return format_test_name(test_name, opts)
end
M.tbl_testcase_name = function(bufnr, opts)
local etn = M.test_func_name(bufnr, opts)
if etn == "" or etn == nil then
return
end
local testcase_name = name_from_query(bufnr, tbl_case_query(), "testcase.name", "testcase.struct")
if testcase_name == "" or testcase_name == nil then
return nil
end
return etn .. "/" .. format_test_name(testcase_name, opts)
end
return M
Acknowledgements: all of that code is cobbled together from various plugins determining test- and test-case names (partially from this plugin, partially from ray-x/go.nvim
, partially from crispgm/nvim-go
).
For some more inspiration the neotest-go adapter for the neotest plugin also managed to implement, very successfully, running individual test in a table driven test, both for slices and maps of tests, see this issue for an example: https://github.com/nvim-neotest/neotest-go/issues/43.
I've been using neotest
with the golang adapter for quite some time, and it's been working flawlessly for me.
I managed to get it working for normal test running, but this fails when trying to debug a test, because it seems that neotest-go doesn't support nvim-dap
My comment above was just to show that another plugin was able to isolate the subtests inside a table test, so in theory, we should be able to use the same treesitter query to do the same with nvim-dap-go
.
As I'm not sure when if ever this will be implemented in nvim-dap-go
, I'd like to document here the 2 methods I use to debug individual subtests inside a table driven test setup.
(PS: Leoluz's workaround works, but it's too many code changes, especially if you have lots of test cases)
Let's take Bruno's example code and add a little bit to it:
func SomeFunction(a, b int) int {
return a + b
}
func TestSomeFunction(t *testing.T) {
tests := []struct {
name string
a int
b int
expectedResult int
}{
{
name: "first test",
a: 0,
b: 0,
expectedResult: 0,
},
{
name: "second test",
a: 10,
b: 20,
expectedResult: 30,
},
{
name: "third test",
a: 1,
b: 5,
expectedResult: 6,
},
{
name: "fourth test",
a: 100,
b: 10,
expectedResult: 110,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := SomeFunction(test.a, test.b)
if result != test.expectedResult {
t.Errorf("expected %d, got %d", test.expectedResult, result)
}
})
}
}
With neotest
I'm able to run individual tests inside a table (regardless of whether it's a slice or a map containing the tests).
When it comes to debugging the tests individually, I too, have a "workaround". In particular, I use 2 methods.
The first method is to copy the test name, and set a conditional breakpoint when the test name matches the string literal.
So, if I want to specifically debug the third test, I'll place my conditional breakpoint on line result := SomeFunction(test.a, test.b)
and the condition will look like test.name == "third test"
. The debugger will run and stop on that line exactly when the name of the test matches what I want.
This isn't much work, since I just move to the line with the test name do a vi"
to select the test name inside quotes, then "+y
to copy it to the clipboard, then move to the line I want to place my conditional breakpoint in, hit a keymap that will show a popup where I can just type the condition above and paste the name of the test. It's very fast.
NOTE: DO NOT set the conditional breakpoint on the line with the t.Run()
method call. Delve seems to get confused and tries to evaluate the variable test
inside the code in the standard library for t.Run
which obviously doesn't exist (I'm not talking about the closure here, I'm talking about the actual code that is part of t.Run. But I'm not entirely sure what's the problem there, I just know there is defo a problem!).
This works like a charm, however, there is a second method I occasionally use, which is to literally put a temporary if statement in my code and then set a normal breakpoint inside of it. I don't tend to use this to select a specific test name, but rather when I have more complex conditional logic. This is only temporary code that I make sure to delete once I'm done with the debugging session.
With the code above, that would look like:
(...)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.a == test.b && test.a > 50 {
t.Log("Set breakpoint on this line!")
}
result := SomeFunction(test.a, test.b)
if result != test.expectedResult {
t.Errorf("expected %d, got %d", test.expectedResult, result)
}
})
}
(...)
As I said, this second method is particularly powerful when you have complex conditional logic that may even require you calling other methods in your code. This is nice because delve has limitations on what you can use as an expression. However, as powerful as this can be, it's not really necessary when all you want is to select a specific subtest in a table test. In that case, just use method 1.
Hope that helps 🙌
PS: Leo thanks a bunch for creating this plugin and maintaining it. I rely on it on a daily basis!! 🦸
@gustavooferreira tks a lot for your feedback. Now I am curious to see how neotest
achieved this. :)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Say I have this test structure:
When I run debug test it will run all four tests in this table. Is it possible to make
require('dap-go').debug_test()
debug only a specific test in this array of tests?