all repos

utest.nvim @ 1f8965579bd7be6519643e17f5364b3190c63f42

test runner that you shouldn't be using

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

1
local golang = {}
2
golang.ft = "go"
3
4
-- source: https://github.com/fredrikaverpil/neotest-golang/blob/main/lua/neotest-golang/queries/go
5
golang.query = [[
6
; func TestXxx(t *testing.T) / func ExampleXxx()
7
((function_declaration
8
  name: (identifier) @test.name)
9
  (#match? @test.name "^(Test|Example)")
10
  (#not-match? @test.name "^TestMain$")) @test.definition
11
12
; t.Run("subtest", func(t *testing.T) {...})
13
(call_expression
14
  function: (selector_expression
15
    operand: (identifier) @_operand
16
    (#match? @_operand "^(t|s|suite)$")
17
    field: (field_identifier) @_method)
18
  (#match? @_method "^Run$")
19
  arguments: (argument_list . (interpreted_string_literal) @test.name)) @test.definition
20
21
; Table tests: named variable, keyed fields
22
;   tt := []struct{ name string }{ {name: "test1"} }
23
;   for _, tc := range tt { t.Run(tc.name, ...) }
24
(block
25
  (statement_list
26
    (short_var_declaration
27
      left: (expression_list
28
        (identifier) @test.cases)
29
      right: (expression_list
30
        (composite_literal
31
          (literal_value
32
            (literal_element
33
              (literal_value
34
                (keyed_element
35
                  (literal_element
36
                    (identifier) @test.field.name)
37
                  (literal_element
38
                    (interpreted_string_literal) @test.name)))) @test.definition))))
39
    (for_statement
40
      (range_clause
41
        left: (expression_list
42
          (identifier) @test.case)
43
        right: (identifier) @test.cases1
44
        (#eq? @test.cases @test.cases1))
45
      body: (block
46
        (statement_list
47
          (expression_statement
48
            (call_expression
49
              function: (selector_expression
50
                operand: (identifier) @test.operand
51
                (#match? @test.operand "^[t]$")
52
                field: (field_identifier) @test.method
53
                (#match? @test.method "^Run$"))
54
              arguments: (argument_list
55
                (selector_expression
56
                  operand: (identifier) @test.case1
57
                  (#eq? @test.case @test.case1)
58
                  field: (field_identifier) @test.field.name1
59
                  (#eq? @test.field.name @test.field.name1))))))))))
60
61
; Table tests: named variable, unkeyed (positional) fields
62
;   tt := []struct{ name string }{ {"test1"} }
63
;   for _, tc := range tt { t.Run(tc.name, ...) }
64
(block
65
  (statement_list
66
    (short_var_declaration
67
      left: (expression_list
68
        (identifier) @test.cases)
69
      right: (expression_list
70
        (composite_literal
71
          body: (literal_value
72
            (literal_element
73
              (literal_value
74
                .
75
                (literal_element
76
                  (interpreted_string_literal) @test.name)
77
                (literal_element)) @test.definition)))))
78
    (for_statement
79
      (range_clause
80
        left: (expression_list
81
          (identifier) @test.key.name
82
          (identifier) @test.case)
83
        right: (identifier) @test.cases1
84
        (#eq? @test.cases @test.cases1))
85
      body: (block
86
        (statement_list
87
          (expression_statement
88
            (call_expression
89
              function: (selector_expression
90
                operand: (identifier) @test.operand
91
                (#match? @test.operand "^[t]$")
92
                field: (field_identifier) @test.method
93
                (#match? @test.method "^Run$"))
94
              arguments: (argument_list
95
                (selector_expression
96
                  operand: (identifier) @test.case1
97
                  (#eq? @test.case @test.case1))))))))))
98
99
; Inline table tests: keyed fields
100
;   for _, tc := range []struct{ name string }{ {name: "test1"} } {
101
;     t.Run(tc.name, ...)
102
;   }
103
(for_statement
104
  (range_clause
105
    left: (expression_list
106
      (identifier)
107
      (identifier) @test.case)
108
    right: (composite_literal
109
      type: (slice_type
110
        element: (struct_type
111
          (field_declaration_list
112
            (field_declaration
113
              name: (field_identifier)
114
              type: (type_identifier)))))
115
      body: (literal_value
116
        (literal_element
117
          (literal_value
118
            (keyed_element
119
              (literal_element
120
                (identifier)) @test.field.name
121
              (literal_element
122
                (interpreted_string_literal) @test.name))) @test.definition))))
123
  body: (block
124
    (statement_list
125
      (expression_statement
126
        (call_expression
127
          function: (selector_expression
128
            operand: (identifier)
129
            field: (field_identifier))
130
          arguments: (argument_list
131
            (selector_expression
132
              operand: (identifier)
133
              field: (field_identifier) @test.field.name1)
134
            (#eq? @test.field.name @test.field.name1)))))))
135
136
; Inline table tests: unkeyed (positional) fields
137
;   for _, tc := range []struct{ name string }{ {"test1"} } {
138
;     t.Run(tc.name, ...)
139
;   }
140
(for_statement
141
  (range_clause
142
    left: (expression_list
143
      (identifier)
144
      (identifier) @test.case)
145
    right: (composite_literal
146
      type: (slice_type
147
        element: (struct_type
148
          (field_declaration_list
149
            (field_declaration
150
              name: (field_identifier) @test.field.name
151
              type: (type_identifier) @field.type
152
              (#eq? @field.type "string")))))
153
      body: (literal_value
154
        (literal_element
155
          (literal_value
156
            .
157
            (literal_element
158
              (interpreted_string_literal) @test.name)
159
            (literal_element)) @test.definition))))
160
  body: (block
161
    (statement_list
162
      (expression_statement
163
        (call_expression
164
          function: (selector_expression
165
            operand: (identifier) @test.operand
166
            (#match? @test.operand "^[t]$")
167
            field: (field_identifier) @test.method
168
            (#match? @test.method "^Run$"))
169
          arguments: (argument_list
170
            (selector_expression
171
              operand: (identifier) @test.case1
172
              (#eq? @test.case @test.case1)
173
              field: (field_identifier) @test.field.name1
174
              (#eq? @test.field.name @test.field.name1))))))))
175
176
; Inline pointer slice table tests: keyed fields
177
;   for _, tc := range []*Type{ {Name: "test1"} } {
178
;     t.Run(tc.Name, ...)
179
;   }
180
(for_statement
181
  (range_clause
182
    left: (expression_list
183
      (identifier)
184
      (identifier) @test.case)
185
    right: (composite_literal
186
      type: (slice_type
187
        element: (pointer_type))
188
      body: (literal_value
189
        (literal_element
190
          (literal_value
191
            (keyed_element
192
              (literal_element
193
                (identifier) @test.field.name)
194
              (literal_element
195
                (interpreted_string_literal) @test.name))) @test.definition))))
196
  body: (block
197
    (statement_list
198
      (expression_statement
199
        (call_expression
200
          function: (selector_expression
201
            operand: (identifier) @test.operand
202
            (#match? @test.operand "^[t]$")
203
            field: (field_identifier) @test.method
204
            (#match? @test.method "^Run$"))
205
          arguments: (argument_list
206
            (selector_expression
207
              operand: (identifier) @test.case1
208
              (#eq? @test.case @test.case1)
209
              field: (field_identifier) @test.field.name1
210
              (#eq? @test.field.name @test.field.name1))))))))
211
212
; Map-based table tests
213
;   testCases := map[string]struct{ want int }{
214
;     "test1": {want: 1},
215
;   }
216
;   for name, tc := range testCases { t.Run(name, ...) }
217
(block
218
  (statement_list
219
    (short_var_declaration
220
      left: (expression_list
221
        (identifier) @test.cases)
222
      right: (expression_list
223
        (composite_literal
224
          (literal_value
225
            (keyed_element
226
              (literal_element
227
                (interpreted_string_literal) @test.name)
228
              (literal_element
229
                (literal_value) @test.definition))))))
230
    (for_statement
231
      (range_clause
232
        left: (expression_list
233
          (identifier) @test.key.name
234
          (identifier) @test.case)
235
        right: (identifier) @test.cases1
236
        (#eq? @test.cases @test.cases1))
237
      body: (block
238
        (statement_list
239
          (expression_statement
240
            (call_expression
241
              function: (selector_expression
242
                operand: (identifier) @test.operand
243
                (#match? @test.operand "^[t]$")
244
                field: (field_identifier) @test.method
245
                (#match? @test.method "^Run$"))
246
              arguments: (argument_list
247
                ((identifier) @test.key.name1
248
                  (#eq? @test.key.name @test.key.name1))))))))))
249
]]
250
251
---@param name string
252
---@return boolean
253
function golang.is_subtest(name)
254
  return not name:match "^Test" and not name:match "^Example"
255
end
256
257
---@param file string
258
---@return string
259
function golang.get_cwd(file)
260
  return vim.fn.fnamemodify(file, ":h")
261
end
262
263
---@param file string
264
---@return string[]
265
function golang.test_file_command(file)
266
  local pkg_dir = golang.get_cwd(file)
267
  return { "go", "test", "-vet=off", "-json", "-v", "-count=1", pkg_dir }
268
end
269
270
---@param test utest.Test Test info with name, parent, is_subtest fields
271
---@param file string File path
272
---@return string[] Command arguments
273
function golang.test_command(test, file)
274
  local pkg_dir = golang.get_cwd(file)
275
  local run_pattern = ""
276
  if test.is_subtest and test.parent then
277
    run_pattern = "^"
278
      .. vim.fn.escape(test.parent, "[](){}.*+?^$\\")
279
      .. "/"
280
      .. vim.fn.escape(test.name:gsub(" ", "_"), "[](){}.*+?^$\\")
281
      .. "$"
282
  else
283
    run_pattern = "^" .. vim.fn.escape(test.name, "[](){}.*+?^$\\") .. "$"
284
  end
285
286
  return { "go", "test", "-vet=off", "-json", "-v", "-count=1", "-run", run_pattern, pkg_dir }
287
end
288
289
function golang.parse_output(output, file)
290
  local results = {}
291
  local file_basename = vim.fn.fnamemodify(file, ":t")
292
  local test_outputs = {} ---@type table<string, string[]>
293
  local test_status = {} ---@type table<string, utest.TestStatus>
294
  for _, line in ipairs(output) do
295
    if line == "" then goto continue end
296
297
    local ok, event = pcall(vim.json.decode, line)
298
    if not ok or not event then goto continue end
299
300
    local test_name = event.Test
301
    if not test_name then goto continue end
302
303
    if event.Action == "run" then
304
      test_outputs[test_name] = test_outputs[test_name] or {}
305
    elseif event.Action == "output" then
306
      test_outputs[test_name] = test_outputs[test_name] or {}
307
      if event.Output then
308
        local output_line = (event.Output:gsub("\n$", ""))
309
        table.insert(test_outputs[test_name], output_line)
310
      end
311
    elseif event.Action == "pass" then
312
      test_status[test_name] = "success"
313
    elseif event.Action == "skip" then
314
      test_status[test_name] = "skipped"
315
    elseif event.Action == "fail" then
316
      test_status[test_name] = "fail"
317
    end
318
319
    ::continue::
320
  end
321
322
  -- build results
323
  for name, status in pairs(test_status) do
324
    local output_lines = test_outputs[name] or {}
325
    local error_line = nil
326
    if status == "fail" then
327
      for _, out_line in ipairs(output_lines) do
328
        local line_num = out_line:match(file_basename .. ":(%d+):")
329
        if line_num then
330
          error_line = tonumber(line_num)
331
          break
332
        end
333
      end
334
    end
335
336
    table.insert(results, {
337
      name = name,
338
      status = status,
339
      output = output_lines,
340
      error_line = error_line,
341
    })
342
  end
343
344
  return results
345
end
346
347
---@param output string[] output lines, json format
348
---@param test_name string|nil specific test name to get output for
349
---@return string[]
350
function golang.extract_test_output(output, test_name)
351
  local result = {}
352
  for _, line in ipairs(output) do
353
    local ok, event = pcall(vim.json.decode, line)
354
    if ok and event and event.Action == "output" and event.Output then
355
      local trimmed = vim.trim(event.Output)
356
      if trimmed ~= "" then
357
        if test_name then
358
          if (event.Test or "") == test_name then table.insert(result, trimmed) end
359
        else
360
          table.insert(result, trimmed)
361
        end
362
      end
363
    end
364
  end
365
  return result
366
end
367
368
---@param output string[]
369
---@return string|nil
370
function golang.extract_error_message(output)
371
  for _, line in ipairs(output) do
372
    if line:match "%.go:%d+:" then return vim.trim(line) end
373
  end
374
  return nil
375
end
376
377
return golang