all repos

utest.nvim @ 2c72626

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