all repos

utest.nvim @ 8b770ef

test runner that you shouldn't be using

utest.nvim/lua/utest/golang.lua (view raw)

1
local golang = {}
2
golang.ft = "go"
3
golang.query = [[
4
; func TestXxx(t *testing.T)
5
((function_declaration
6
  name: (identifier) @test.name)
7
  (#match? @test.name "^Test")
8
  (#not-match? @test.name "^TestMain$")) @test.definition
9
10
; t.Run("subtest name", func(t *testing.T) {...})
11
(call_expression
12
  function: (selector_expression
13
    operand: (identifier) @_operand
14
    (#match? @_operand "^(t|s|suite)$")
15
    field: (field_identifier) @_method)
16
  (#match? @_method "^Run$")
17
  arguments: (argument_list . (interpreted_string_literal) @test.name)) @test.definition
18
19
; ============================================================================
20
; Table-driven tests with named slice variable and keyed fields
21
; Detects table tests with struct fields using keys (e.g., {name: "test1"}).
22
; Pattern:
23
;   tt := []struct{ name string }{
24
;     {name: "test1"},  // @test.name = "test1"
25
;     {name: "test2"},  // @test.name = "test2"
26
;   }
27
;   for _, tc := range tt {
28
;     t.Run(tc.name, func(t *testing.T) { ... })
29
;   }
30
(block
31
  (statement_list
32
    (short_var_declaration
33
      left: (expression_list (identifier) @test.cases)
34
      right: (expression_list
35
        (composite_literal
36
          (literal_value
37
            (literal_element
38
              (literal_value
39
                (keyed_element
40
                  (literal_element (identifier) @test.field.name)
41
                  (literal_element (interpreted_string_literal) @test.name)))) @test.definition))))
42
    (for_statement
43
      (range_clause
44
        left: (expression_list (identifier) @test.case)
45
        right: (identifier) @test.cases1
46
        (#eq? @test.cases @test.cases1))
47
      body: (block
48
        (statement_list
49
          (expression_statement
50
            (call_expression
51
              function: (selector_expression
52
                operand: (identifier) @test.operand
53
                (#match? @test.operand "^[t]$")
54
                field: (field_identifier) @test.method
55
                (#match? @test.method "^Run$"))
56
              arguments: (argument_list
57
                (selector_expression
58
                  operand: (identifier) @test.case1
59
                  (#eq? @test.case @test.case1)
60
                  field: (field_identifier) @test.field.name1
61
                  (#eq? @test.field.name @test.field.name1))))))))))
62
63
; ============================================================================
64
; Map-based table-driven tests
65
; Detects table tests where test cases are defined in a map with string keys.
66
; Pattern:
67
;   testCases := map[string]struct{ want int }{
68
;     "test1": {want: 1},  // @test.name = "test1"
69
;     "test2": {want: 2},  // @test.name = "test2"
70
;   }
71
;   for name, tc := range testCases {
72
;     t.Run(name, func(t *testing.T) { ... })
73
;   }
74
(block
75
  (statement_list
76
    (short_var_declaration
77
      left: (expression_list (identifier) @test.cases)
78
      right: (expression_list
79
        (composite_literal
80
          (literal_value
81
            (keyed_element
82
              (literal_element (interpreted_string_literal) @test.name)
83
              (literal_element (literal_value) @test.definition))))))
84
    (for_statement
85
      (range_clause
86
        left: (expression_list
87
          (identifier) @test.key.name
88
          (identifier) @test.case)
89
        right: (identifier) @test.cases1
90
        (#eq? @test.cases @test.cases1))
91
      body: (block
92
        (statement_list
93
          (expression_statement
94
            (call_expression
95
              function: (selector_expression
96
                operand: (identifier) @test.operand
97
                (#match? @test.operand "^[t]$")
98
                field: (field_identifier) @test.method
99
                (#match? @test.method "^Run$"))
100
              arguments: (argument_list
101
                ((identifier) @test.key.name1
102
                  (#eq? @test.key.name @test.key.name1))))))))))
103
]]
104
105
---@param name string
106
---@return boolean
107
function golang.is_subtest(name)
108
  return not name:match "^Test"
109
end
110
111
---@param file string
112
---@return string
113
function golang.get_cwd(file)
114
  return vim.fn.fnamemodify(file, ":h")
115
end
116
117
---@param file string
118
---@return string[]
119
function golang.test_file_command(file)
120
  local pkg_dir = golang.get_cwd(file)
121
  return { "go", "test", "-vet=off", "-json", "-v", "-count=1", pkg_dir }
122
end
123
124
---@param test table Test info with name, parent, is_subtest fields
125
---@param file string File path
126
---@return string[] Command arguments
127
function golang.test_command(test, file)
128
  local pkg_dir = golang.get_cwd(file)
129
  local run_pattern = ""
130
  if test.is_subtest and test.parent then
131
    run_pattern = "^"
132
      .. vim.fn.escape(test.parent, "[](){}.*+?^$\\")
133
      .. "/"
134
      .. vim.fn.escape(test.name:gsub(" ", "_"), "[](){}.*+?^$\\")
135
      .. "$"
136
  else
137
    run_pattern = "^" .. vim.fn.escape(test.name, "[](){}.*+?^$\\") .. "$"
138
  end
139
140
  return { "go", "test", "-vet=off", "-json", "-v", "-count=1", "-run", run_pattern, pkg_dir }
141
end
142
143
function golang.parse_output(output, file)
144
  local results = {}
145
  local file_basename = vim.fn.fnamemodify(file, ":t")
146
  local test_outputs = {} ---@type table<string, string[]>
147
  local test_status = {} ---@type table<string, utest.TestStatus>
148
  for _, line in ipairs(output) do
149
    if line == "" then goto continue end
150
151
    local ok, event = pcall(vim.json.decode, line)
152
    if not ok or not event then goto continue end
153
154
    local test_name = event.Test
155
    if not test_name then goto continue end
156
157
    if event.Action == "run" then
158
      test_outputs[test_name] = test_outputs[test_name] or {}
159
    elseif event.Action == "output" then
160
      test_outputs[test_name] = test_outputs[test_name] or {}
161
      if event.Output then
162
        local output_line = (event.Output:gsub("\n$", ""))
163
        table.insert(test_outputs[test_name], output_line)
164
      end
165
    elseif event.Action == "pass" then
166
      test_status[test_name] = "success"
167
    elseif event.Action == "skip" then
168
      test_status[test_name] = "skipped"
169
    elseif event.Action == "fail" then
170
      test_status[test_name] = "fail"
171
    end
172
173
    ::continue::
174
  end
175
176
  -- build results
177
  for name, status in pairs(test_status) do
178
    local output_lines = test_outputs[name] or {}
179
    local error_line = nil
180
    if status == "fail" then
181
      for _, out_line in ipairs(output_lines) do
182
        local line_num = out_line:match(file_basename .. ":(%d+):")
183
        if line_num then
184
          error_line = tonumber(line_num)
185
          break
186
        end
187
      end
188
    end
189
190
    table.insert(results, {
191
      name = name,
192
      status = status,
193
      output = output_lines,
194
      error_line = error_line,
195
    })
196
  end
197
198
  return results
199
end
200
201
---@param output string[] output lines, json format
202
---@param test_name string|nil specific test name to get output for
203
---@return string[]
204
function golang.extract_test_output(output, test_name)
205
  local result = {}
206
  for _, line in ipairs(output) do
207
    local ok, event = pcall(vim.json.decode, line)
208
    if ok and event and event.Action == "output" and event.Output then
209
      local trimmed = vim.trim(event.Output)
210
      if trimmed ~= "" then
211
        if test_name then
212
          if (event.Test or "") == test_name then table.insert(result, trimmed) end
213
        else
214
          table.insert(result, trimmed)
215
        end
216
      end
217
    end
218
  end
219
  return result
220
end
221
222
---@param output string[]
223
---@return string|nil
224
function golang.extract_error_message(output)
225
  for _, line in ipairs(output) do
226
    if line:match "%.go:%d+:" then return vim.trim(line) end
227
  end
228
  return nil
229
end
230
231
return golang