all repos

utest.nvim @ 13e8b10

test runner that you shouldn't be using
5 files changed, 166 insertions(+), 145 deletions(-)
more types; fix outputs; health
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-03-30 20:18:10 +0300
Change ID: oxkuwwnrkvsvpmtokzmszpqzlltkrwmw
Parent: 2c72626
M lua/utest.lua
···
                1
                1
                 ---@class utest.Adapter

              
                2
                2
                 ---@field ft string

              
                3
                3
                 ---@field query string

              
                4
                
                ----@field get_package_dir fun(file:string):string

              
                5
                
                ----@field build_file_command fun(file:string):string[]

              
                6
                
                ----@field build_command fun(test:table, file:string) -- FIXME: `table`

              
                
                4
                +---@field is_subtest fun(name:string):boolean

              
                
                5
                +---@field get_cwd fun(file:string):string

              
                
                6
                +---@field test_file_command fun(file:string):string[]

              
                
                7
                +---@field test_command fun(test:utest.Test, file:string)

              
                7
                8
                 ---@field parse_output fun(output:string[], file:string):utest.AdapterTestResult

              
                8
                9
                 ---@field extract_test_output fun(output:string[], test_name:string|nil):string[]

              
                9
                10
                 ---@field extract_error_message fun(output:string[]):string|nil

              
                10
                11
                 

              
                11
                
                ----@alias utest.TestStatus "pass"|"fail"|"running"

              
                
                12
                +---@alias utest.TestStatus "success"|"fail"|"running"|"skipped"

              
                12
                13
                 

              
                13
                14
                 ---@class utest.Test

              
                14
                15
                 ---@field name string

              ···
                22
                23
                 

              
                23
                24
                 ---@class utest.AdapterTestResult

              
                24
                25
                 ---@field name string

              
                25
                
                ----@field status "pass"|"fail"

              
                
                26
                +---@field status utest.TestStatus

              
                26
                27
                 ---@field output string[]

              
                27
                28
                 ---@field error_line number|nil

              
                28
                29
                 

              
                29
                
                -local S = { jobs = {}, results = {} }

              
                
                30
                +---@class utest.Job

              
                
                31
                +---@field job_id number

              
                
                32
                +---@field test_id string

              
                
                33
                +---@field file string

              
                
                34
                +---@field name string

              
                
                35
                +---@field line number

              
                
                36
                +---@field start_time number

              
                
                37
                +

              
                
                38
                +---@class utest.JobResult

              
                
                39
                +---@field status utest.TestStatus

              
                
                40
                +---@field output string

              
                
                41
                +---@field raw_output? string

              
                
                42
                +---@field timestamp number

              
                
                43
                +---@field file string

              
                
                44
                +---@field error_message? string

              
                
                45
                +---@field name? string

              
                
                46
                +---@field line? number

              
                
                47
                +

              
                
                48
                +local test_failed_msg = "[Test failed]"

              
                30
                49
                 local H = {

              
                31
                50
                   ---@type table<string, utest.Adapter>

              
                32
                51
                   adapters = {},

              
                33
                52
                   ---@type table<string, vim.treesitter.Query>

              
                34
                53
                   queries = {},

              
                35
                54
                 

              
                
                55
                +  ---@type table<string, utest.Job>

              
                
                56
                +  jobs = {},

              
                
                57
                +

              
                
                58
                +  ---@type table<string, utest.JobResult>

              
                
                59
                +  results = {},

              
                
                60
                +

              
                36
                61
                   sns = nil,

              
                37
                62
                   dns = nil,

              
                38
                63
                   extmarks = {},

              ···
                41
                66
                 

              
                42
                67
                 local utest = {}

              
                43
                68
                 utest.config = {

              
                44
                
                -  icons = { success = "", failed = "", running = "" }, -- TODO: add skipped

              
                45
                69
                   timeout = 30,

              
                
                70
                +  icons = {

              
                
                71
                +    failed = "",

              
                
                72
                +    running = "",

              
                
                73
                +    skipped = "",

              
                
                74
                +    success = "",

              
                
                75
                +  },

              
                46
                76
                 }

              
                47
                77
                 

              
                48
                78
                 function utest.setup(opts)

              
                49
                79
                   utest.config = vim.tbl_deep_extend("keep", utest.config, opts)

              
                50
                
                -

              
                51
                80
                   H.sns = vim.api.nvim_create_namespace "utest_signs"

              
                52
                81
                   H.dns = vim.api.nvim_create_namespace "utest_diagnostics"

              
                53
                82
                   H.adapters.go = require "utest.golang"

              ···
                61
                90
                     return

              
                62
                91
                   end

              
                63
                92
                 

              
                64
                
                -  local cursor = vim.api.nvim_win_get_cursor(0)

              
                65
                
                -  local cursor_line = cursor[1] - 1

              
                66
                
                -

              
                
                93
                +  local cursor_line = vim.api.nvim_win_get_cursor(0)[1] - 1

              
                67
                94
                   local test = H.find_nearest_test(bufnr, cursor_line, adapter)

              
                68
                95
                   if not test then

              
                69
                96
                     vim.notify("[utest] no near test found", vim.log.levels.INFO)

              ···
                94
                121
                 

              
                95
                122
                 function utest.cancel()

              
                96
                123
                   local cancelled = 0

              
                97
                
                -  for id, info in pairs(S.jobs) do

              
                
                124
                +  for id, info in pairs(H.jobs) do

              
                98
                125
                     pcall(vim.fn.jobstop, id)

              
                99
                126
                     if info.test_id then

              
                100
                
                -      S.results[info.test_id] = {

              
                
                127
                +      H.results[info.test_id] = {

              
                101
                128
                         status = "fail",

              
                102
                129
                         output = "",

              
                103
                130
                         error_message = "Test cancelled",

              ···
                109
                136
                     end

              
                110
                137
                     cancelled = cancelled + 1

              
                111
                138
                   end

              
                112
                
                -  S.jobs = {}

              
                
                139
                +  H.jobs = {}

              
                113
                140
                 

              
                114
                141
                   if cancelled > 0 then

              
                115
                142
                     vim.notify("[utest] cancelled running test(s)", vim.log.levels.INFO)

              ···
                120
                147
                 

              
                121
                148
                 function utest.clear()

              
                122
                149
                   local bufnr = vim.api.nvim_get_current_buf()

              
                123
                
                -  S.clear_file(vim.api.nvim_buf_get_name(bufnr))

              
                
                150
                +  H.clear_file(vim.api.nvim_buf_get_name(bufnr))

              
                124
                151
                   H.signs_clear_buffer(bufnr)

              
                125
                152
                   H.diagnostics_clear_buffer(bufnr)

              
                126
                153
                   H.qf_clear()

              ···
                128
                155
                 

              
                129
                156
                 function utest.qf()

              
                130
                157
                   local qfitems = {}

              
                131
                
                -  for test_id, result in pairs(S.get_failed()) do

              
                
                158
                +  for test_id, result in pairs(H.get_failed()) do

              
                132
                159
                     local file, line, name = test_id:match "^(.+):(%d+):(.+)$"

              
                133
                160
                     if file and line then

              
                134
                
                -      local error_text = result.test_output or result.error_message or "[Test failed]"

              
                
                161
                +      local error_text = result.output or result.error_message or test_failed_msg

              
                135
                162
                       local lines = vim.split(error_text, "\n", { plain = true })

              
                136
                163
                       for i, lcontent in ipairs(lines) do

              
                137
                164
                         lcontent = vim.trim(lcontent)

              ···
                157
                184
                   vim.fn.setqflist({}, "r", { title = "utest: failed tests", items = qfitems })

              
                158
                185
                 end

              
                159
                186
                 

              
                160
                
                --- STATE ======================================================================

              
                
                187
                +-- HELPERS ====================================================================

              
                161
                188
                 

              
                162
                
                -function S.make_test_id(file, line, name)

              
                
                189
                +function H.make_test_id(file, line, name)

              
                163
                190
                   return string.format("%s:%d:%s", file, line, name)

              
                164
                191
                 end

              
                165
                192
                 

              
                166
                
                -function S.clear_file(file)

              
                167
                
                -  for id, _ in pairs(S.results) do

              
                168
                
                -    if id:match("^" .. vim.pesc(file) .. ":") then S.results[id] = nil end

              
                
                193
                +function H.clear_file(file)

              
                
                194
                +  for id, _ in pairs(H.results) do

              
                
                195
                +    if id:match("^" .. vim.pesc(file) .. ":") then H.results[id] = nil end

              
                169
                196
                   end

              
                170
                197
                 end

              
                171
                198
                 

              
                172
                
                -function S.get_failed()

              
                
                199
                +function H.get_failed()

              
                173
                200
                   local f = {}

              
                174
                
                -  for id, r in pairs(S.results) do

              
                
                201
                +  for id, r in pairs(H.results) do

              
                175
                202
                     if r.status == "fail" then f[id] = r end

              
                176
                203
                   end

              
                177
                204
                   return f

              
                178
                205
                 end

              
                179
                206
                 

              
                180
                
                --- HELPERS ====================================================================

              
                181
                
                -

              
                182
                207
                 function H.qf_clear()

              
                183
                208
                   local qf = vim.fn.getqflist { title = 1 }

              
                184
                209
                   if qf.title == "utest: failed tests" then vim.fn.setqflist({}, "r") end

              ···
                186
                211
                 

              
                187
                212
                 -- SIGNS

              
                188
                213
                 

              
                
                214
                +---@type table<utest.TestStatus, string>

              
                189
                215
                 local sign_highlights = {

              
                190
                
                -  pass = "DiagnosticOk",

              
                
                216
                +  success = "DiagnosticOk",

              
                
                217
                +  skipped = "DiagnosticInfo",

              
                191
                218
                   fail = "DiagnosticError",

              
                192
                219
                   running = "DiagnosticInfo",

              
                193
                220
                 }

              ···
                197
                224
                 ---@param status utest.TestStatus

              
                198
                225
                 ---@param test_id string

              
                199
                226
                 function H.sign_place(bufnr, line, status, test_id)

              
                200
                
                -  local icon = utest.config.icons.success

              
                201
                
                -  if status == "fail" then

              
                202
                
                -    icon = utest.config.icons.failed

              
                203
                
                -  elseif status == "running" then

              
                204
                
                -    icon = utest.config.icons.running

              
                205
                
                -  end

              
                206
                
                -

              
                207
                227
                   if not H.extmarks[bufnr] then H.extmarks[bufnr] = {} end

              
                208
                228
                 

              
                209
                229
                   local existing_id = H.extmarks[bufnr][test_id]

              ···
                212
                232
                     H.extmarks[bufnr][test_id] = nil

              
                213
                233
                   end

              
                214
                234
                 

              
                215
                
                -  local hl = sign_highlights[status] -- FIXME: might fail if status is invalid

              
                
                235
                +  -- FIXME: might fail if status is invalid

              
                
                236
                +  local icon = utest.config.icons[status]

              
                
                237
                +  local hl = sign_highlights[status]

              
                
                238
                +

              
                216
                239
                   local ok, res = pcall(vim.api.nvim_buf_set_extmark, bufnr, H.sns, line, 0, {

              
                217
                240
                     priority = 1000,

              
                218
                241
                     sign_text = icon,

              ···
                227
                250
                 function H.sign_get_current_line(bufnr, test_id)

              
                228
                251
                   if not H.extmarks[bufnr] or not H.extmarks[bufnr][test_id] then return nil end

              
                229
                252
                   local ok, mark =

              
                230
                
                -      pcall(vim.api.nvim_buf_get_extmark_by_id, bufnr, H.sns, H.extmarks[bufnr][test_id], {})

              
                
                253
                +    pcall(vim.api.nvim_buf_get_extmark_by_id, bufnr, H.sns, H.extmarks[bufnr][test_id], {})

              
                231
                254
                   if ok and mark and #mark >= 1 then return mark[1] end

              
                232
                255
                   return nil

              
                233
                256
                 end

              ···
                259
                282
                 ---@param bufnr number

              
                260
                283
                 ---@param line number 0-indexed line number

              
                261
                284
                 ---@param message string|nil

              
                262
                
                ----@param output string[]|nil Full output lines

              
                263
                
                -function H.diagnostics_set(bufnr, line, message, output)

              
                
                285
                +function H.diagnostics_set(bufnr, line, message)

              
                264
                286
                   if not H.diagnostics[bufnr] then H.diagnostics[bufnr] = {} end

              
                265
                
                -  local msg = message or "[Test failed]"

              
                266
                
                -  if output and #output > 0 then

              
                267
                
                -    local readable_lines = {}

              
                268
                
                -    for _, out_line in ipairs(output) do

              
                269
                
                -      --  TODO: this should be in utest.golang, only support plain text

              
                270
                
                -      local trimmed = vim.trim(out_line)

              
                271
                
                -      if trimmed ~= "" then table.insert(readable_lines, trimmed) end

              
                272
                
                -    end

              
                273
                
                -

              
                274
                
                -    if #readable_lines > 0 then

              
                275
                
                -      local output_text = table.concat(readable_lines, "\n")

              
                276
                
                -      if output_text ~= "" then msg = msg .. "\n" .. output_text end

              
                277
                
                -    end

              
                278
                
                -  end

              
                279
                
                -

              
                280
                287
                   H.diagnostics[bufnr][line] = {

              
                281
                288
                     lnum = line,

              
                282
                289
                     col = 0,

              
                283
                290
                     severity = vim.diagnostic.severity.ERROR,

              
                284
                291
                     source = "utest",

              
                285
                
                -    message = msg,

              
                
                292
                +    message = message or test_failed_msg,

              
                286
                293
                   }

              
                287
                294
                   H.diagnostics_refresh(bufnr)

              
                288
                295
                 end

              ···
                301
                308
                 

              
                302
                309
                 -- RUNNER

              
                303
                310
                 

              
                304
                
                --- TODO: refactor

              
                305
                
                ----@param test unknown TODO: fix type

              
                
                311
                +---@param test utest.Test

              
                306
                312
                 ---@param adapter utest.Adapter

              
                307
                313
                 ---@param bufnr number

              
                308
                314
                 function H.execute_test(test, adapter, bufnr)

              
                309
                
                -  local cmd = adapter.build_command(test, test.file)

              
                310
                
                -  local test_id = S.make_test_id(test.file, test.line, test.name)

              
                
                315
                +  local cmd = adapter.test_command(test, test.file)

              
                
                316
                +  local test_id = H.make_test_id(test.file, test.line, test.name)

              
                311
                317
                   local output_lines = {}

              
                312
                318
                   local timed_out = false

              
                313
                319
                 

              
                314
                
                -  S.results[test_id] = {

              
                
                320
                +  H.results[test_id] = {

              
                315
                321
                     status = "running",

              
                316
                322
                     output = "",

              
                317
                323
                     timestamp = os.time(),

              
                
                324
                +    file = test.file,

              
                318
                325
                   }

              
                319
                326
                   H.sign_place(bufnr, test.line, "running", test_id)

              
                320
                327
                 

              ···
                325
                332
                       timeout_timer:close()

              
                326
                333
                       timeout_timer = nil

              
                327
                334
                     end

              
                328
                
                -    if job_id and S.jobs[job_id] then S.jobs[job_id] = nil end

              
                
                335
                +    if job_id and H.jobs[job_id] then H.jobs[job_id] = nil end

              
                329
                336
                   end

              
                330
                337
                 

              
                331
                338
                   local function on_output(_, data, _)

              ···
                360
                367
                     if not test_result then

              
                361
                368
                       test_result = {

              
                362
                369
                         name = test.name,

              
                363
                
                -        status = exit_code == 0 and "pass" or "fail",

              
                
                370
                +        status = exit_code == 0 and "success" or "fail",

              
                364
                371
                         output = output_lines,

              
                365
                372
                         error_line = nil,

              
                366
                373
                       }

              ···
                368
                375
                 

              
                369
                376
                     -- ensure status validity

              
                370
                377
                     local final_status = test_result.status

              
                371
                
                -    if final_status ~= "pass" and final_status ~= "fail" then

              
                372
                
                -      final_status = exit_code == 0 and "pass" or "fail"

              
                
                378
                +    if final_status ~= "success" and final_status ~= "fail" and final_status ~= "skipped" then

              
                
                379
                +      final_status = exit_code == 0 and "success" or "fail"

              
                373
                380
                     end

              
                374
                381
                     test_result.status = final_status

              
                375
                382
                 

              
                376
                
                -    -- get human redabble output

              
                377
                
                -    local test_output = {}

              
                378
                
                -    if adapter.extract_test_output then

              
                379
                
                -      test_output = adapter.extract_test_output(output_lines, search_name)

              
                380
                
                -    end

              
                
                383
                +    local test_output = adapter.extract_test_output(output_lines, search_name)

              
                
                384
                +    local error_message = test_result.status == "fail"

              
                
                385
                +        and adapter.extract_error_message(output_lines)

              
                
                386
                +      or nil

              
                381
                387
                 

              
                382
                
                -    S.results[test_id] = {

              
                
                388
                +    H.results[test_id] = {

              
                383
                389
                       status = test_result.status,

              
                384
                
                -      output = full_output,

              
                385
                
                -      test_output = table.concat(test_output, "\n"),

              
                386
                
                -      error_message = test_result.status == "fail" and adapter.extract_error_message(output_lines)

              
                387
                
                -          or nil,

              
                
                390
                +      output = table.concat(test_output, "\n"),

              
                
                391
                +      raw_output = full_output,

              
                
                392
                +      error_message = error_message,

              
                388
                393
                       timestamp = os.time(),

              
                389
                394
                       file = test.file,

              
                390
                395
                       line = test.line,

              ···
                403
                408
                           H.diagnostics_set(

              
                404
                409
                             bufnr,

              
                405
                410
                             test.line,

              
                406
                
                -            S.results[test_id].test_output or S.results[test_id].error_message or "[Test failed]",

              
                407
                
                -            test_output

              
                
                411
                +            H.results[test_id].output or H.results[test_id].error_message or test_failed_msg

              
                408
                412
                           )

              
                409
                413
                         end

              
                410
                414
                       else

              ···
                414
                418
                   end

              
                415
                419
                 

              
                416
                420
                   job_id = vim.fn.jobstart(cmd, {

              
                417
                
                -    cwd = adapter.get_package_dir(test.file),

              
                
                421
                +    cwd = adapter.get_cwd(test.file),

              
                418
                422
                     on_stdout = on_output,

              
                419
                423
                     on_stderr = on_output,

              
                420
                424
                     on_exit = on_exit,

              ···
                428
                432
                     return

              
                429
                433
                   end

              
                430
                434
                 

              
                431
                
                -  S.jobs[job_id] = {

              
                
                435
                +  H.jobs[job_id] = {

              
                432
                436
                     job_id = job_id,

              
                433
                437
                     test_id = test_id,

              
                434
                438
                     file = test.file,

              ···
                443
                447
                 

              
                444
                448
                   -- stylua: ignore

              
                445
                449
                   timeout_timer:start(timeout, 0, vim.schedule_wrap(function() ---@diagnostic disable-line: need-check-nil

              
                446
                
                -    if S.jobs[job_id] then

              
                
                450
                +    if H.jobs[job_id] then

              
                447
                451
                       timed_out = true

              
                448
                452
                       vim.fn.jobstop(job_id)

              
                449
                
                -      S.results[test_id] = {

              
                
                453
                +      H.results[test_id] = {

              
                450
                454
                         status = "fail",

              
                451
                455
                         output = table.concat(output_lines, "\n"),

              
                452
                456
                         error_message = "Test timed out after " .. utest.config.timeout .. "s",

              ···
                456
                460
                         name = test.name,

              
                457
                461
                       }

              
                458
                462
                       H.sign_place(bufnr, test.line, "fail", test_id)

              
                459
                
                -      H.diagnostics_set(bufnr, test.line, "Test timed out", output_lines)

              
                
                463
                +      H.diagnostics_set(bufnr, test.line, "Test timed out")

              
                460
                464
                       cleanup()

              
                461
                465
                     end

              
                462
                466
                   end))

              ···
                477
                481
                   return q

              
                478
                482
                 end

              
                479
                483
                 

              
                480
                
                --- TODO: refactor

              
                481
                484
                 ---@param bufnr number

              
                482
                485
                 ---@param adapter utest.Adapter

              
                483
                486
                 ---@return utest.Test[]

              
                484
                487
                 function H.find_tests(bufnr, adapter)

              
                485
                488
                   local query = H.get_query(adapter.ft, adapter.query)

              
                486
                
                -  if not query then return {} end -- TODO: show error

              
                
                489
                +  if not query then

              
                
                490
                +    vim.notify("[utest] failed to run " .. adapter.ft .. " adapter query", vim.log.levels.ERROR)

              
                
                491
                +    return {}

              
                
                492
                +  end

              
                487
                493
                 

              
                488
                494
                   local pok, parser = pcall(vim.treesitter.get_parser, bufnr, adapter.ft)

              
                489
                
                -  if not pok or not parser then return {} end -- TODO: show error

              
                
                495
                +  if not pok or not parser then

              
                
                496
                +    vim.notify("[utest] failed to get treesitter parser", vim.log.levels.ERROR)

              
                
                497
                +    return {}

              
                
                498
                +  end

              
                490
                499
                 

              
                491
                500
                   local tree = parser:parse()[1]

              
                492
                501
                   if not tree then return {} end

              ···
                514
                523
                     end

              
                515
                524
                 

              
                516
                525
                     if test_name and test_def then

              
                517
                
                -      -- TODO: it's knows about go too much

              
                518
                526
                       local start_row, start_col, end_row, end_col = test_def:range()

              
                519
                
                -      local is_subtest = not test_name:match "^Test" and not test_name:match "^Example"

              
                520
                527
                       table.insert(tests, {

              
                521
                528
                         name = test_name,

              
                522
                529
                         file = file,

              ···
                524
                531
                         col = start_col,

              
                525
                532
                         end_line = end_row,

              
                526
                533
                         end_col = end_col,

              
                527
                
                -        is_subtest = is_subtest,

              
                
                534
                +        is_subtest = adapter.is_subtest(test_name),

              
                528
                535
                         parent = nil,

              
                529
                536
                       })

              
                530
                537
                     end

              ···
                558
                565
                 

              
                559
                566
                       if innermost_parent then

              
                560
                567
                         -- Build full parent path for nested subtests

              
                561
                
                -        if innermost_parent.parent then

              
                
                568
                +        if innermost_parent.parent then -- TODO: too go dependent

              
                562
                569
                           test.parent = innermost_parent.parent .. "/" .. innermost_parent.name

              
                563
                570
                         else

              
                564
                571
                           test.parent = innermost_parent.name

              
M lua/utest/golang.lua
···
                1
                1
                 local golang = {}

              
                2
                2
                 golang.ft = "go"

              
                3
                3
                 golang.query = [[

              
                4
                
                -; func TestXxx(t *testing.T) and func ExampleXxx()

              
                
                4
                +; func TestXxx(t *testing.T)

              
                5
                5
                 ((function_declaration

              
                6
                6
                   name: (identifier) @test.name)

              
                7
                
                -  (#match? @test.name "^(Test|Example)")

              
                
                7
                +  (#match? @test.name "^Test")

              
                8
                8
                   (#not-match? @test.name "^TestMain$")) @test.definition

              
                9
                9
                 

              
                10
                10
                 ; t.Run("subtest name", func(t *testing.T) {...})

              ···
                14
                14
                     (#match? @_operand "^(t|s|suite)$")

              
                15
                15
                     field: (field_identifier) @_method)

              
                16
                16
                   (#match? @_method "^Run$")

              
                17
                
                -  arguments: (argument_list

              
                18
                
                -    .

              
                19
                
                -    (interpreted_string_literal) @test.name)) @test.definition

              
                
                17
                +  arguments: (argument_list . (interpreted_string_literal) @test.name)) @test.definition

              
                20
                18
                 

              
                21
                19
                 ; ============================================================================

              
                22
                20
                 ; Table-driven tests with named slice variable and keyed fields

              ···
                32
                30
                 (block

              
                33
                31
                   (statement_list

              
                34
                32
                     (short_var_declaration

              
                35
                
                -      left: (expression_list

              
                36
                
                -        (identifier) @test.cases)

              
                
                33
                +      left: (expression_list (identifier) @test.cases)

              
                37
                34
                       right: (expression_list

              
                38
                35
                         (composite_literal

              
                39
                36
                           (literal_value

              
                40
                37
                             (literal_element

              
                41
                38
                               (literal_value

              
                42
                39
                                 (keyed_element

              
                43
                
                -                  (literal_element

              
                44
                
                -                    (identifier) @test.field.name)

              
                45
                
                -                  (literal_element

              
                46
                
                -                    (interpreted_string_literal) @test.name)))) @test.definition))))

              
                
                40
                +                  (literal_element (identifier) @test.field.name)

              
                
                41
                +                  (literal_element (interpreted_string_literal) @test.name)))) @test.definition))))

              
                47
                42
                     (for_statement

              
                48
                43
                       (range_clause

              
                49
                
                -        left: (expression_list

              
                50
                
                -          (identifier) @test.case)

              
                
                44
                +        left: (expression_list (identifier) @test.case)

              
                51
                45
                         right: (identifier) @test.cases1

              
                52
                46
                         (#eq? @test.cases @test.cases1))

              
                53
                47
                       body: (block

              ···
                80
                74
                 (block

              
                81
                75
                   (statement_list

              
                82
                76
                     (short_var_declaration

              
                83
                
                -      left: (expression_list

              
                84
                
                -        (identifier) @test.cases)

              
                
                77
                +      left: (expression_list (identifier) @test.cases)

              
                85
                78
                       right: (expression_list

              
                86
                79
                         (composite_literal

              
                87
                80
                           (literal_value

              
                88
                81
                             (keyed_element

              
                89
                
                -              (literal_element

              
                90
                
                -                (interpreted_string_literal) @test.name)

              
                91
                
                -              (literal_element

              
                92
                
                -                (literal_value) @test.definition))))))

              
                
                82
                +              (literal_element (interpreted_string_literal) @test.name)

              
                
                83
                +              (literal_element (literal_value) @test.definition))))))

              
                93
                84
                     (for_statement

              
                94
                85
                       (range_clause

              
                95
                86
                         left: (expression_list

              ···
                111
                102
                                   (#eq? @test.key.name @test.key.name1))))))))))

              
                112
                103
                 ]]

              
                113
                104
                 

              
                
                105
                +---@param name string

              
                
                106
                +---@return boolean

              
                
                107
                +function golang.is_subtest(name)

              
                
                108
                +  return not name:match "^Test"

              
                
                109
                +end

              
                
                110
                +

              
                114
                111
                 ---@param file string

              
                115
                112
                 ---@return string

              
                116
                
                -function golang.get_package_dir(file)

              
                
                113
                +function golang.get_cwd(file)

              
                117
                114
                   return vim.fn.fnamemodify(file, ":h")

              
                118
                115
                 end

              
                119
                116
                 

              
                120
                117
                 ---@param file string

              
                121
                118
                 ---@return string[]

              
                122
                
                -function golang.build_file_command(file)

              
                123
                
                -  local pkg_dir = golang.get_package_dir(file)

              
                
                119
                +function golang.test_file_command(file)

              
                
                120
                +  local pkg_dir = golang.get_cwd(file)

              
                124
                121
                   return { "go", "test", "-vet=off", "-json", "-v", "-count=1", pkg_dir }

              
                125
                122
                 end

              
                126
                123
                 

              
                127
                124
                 ---@param test table Test info with name, parent, is_subtest fields

              
                128
                125
                 ---@param file string File path

              
                129
                126
                 ---@return string[] Command arguments

              
                130
                
                -function golang.build_command(test, file)

              
                131
                
                -  local pkg_dir = golang.get_package_dir(file)

              
                132
                
                -  local run_pattern

              
                
                127
                +function golang.test_command(test, file)

              
                
                128
                +  local pkg_dir = golang.get_cwd(file)

              
                
                129
                +  local run_pattern = ""

              
                133
                130
                   if test.is_subtest and test.parent then

              
                134
                131
                     run_pattern = "^"

              
                135
                132
                       .. vim.fn.escape(test.parent, "[](){}.*+?^$\\")

              ···
                140
                137
                     run_pattern = "^" .. vim.fn.escape(test.name, "[](){}.*+?^$\\") .. "$"

              
                141
                138
                   end

              
                142
                139
                 

              
                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
                
                -  }

              
                
                140
                +  return { "go", "test", "-vet=off", "-json", "-v", "-count=1", "-run", run_pattern, pkg_dir }

              
                154
                141
                 end

              
                155
                142
                 

              
                156
                143
                 function golang.parse_output(output, file)

              
                157
                144
                   local results = {}

              
                158
                
                -  local test_outputs = {} ---@type table<string, string[]>

              
                159
                
                -  local test_status = {} ---@type table<string, "pass"|"fail">

              
                160
                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>

              
                161
                148
                   for _, line in ipairs(output) do

              
                162
                
                -    -- Skip empty lines

              
                163
                149
                     if line == "" then goto continue end

              
                164
                150
                 

              
                165
                
                -    -- Parse JSON

              
                166
                151
                     local ok, event = pcall(vim.json.decode, line)

              
                167
                152
                     if not ok or not event then goto continue end

              
                168
                153
                 

              
                169
                154
                     local test_name = event.Test

              
                170
                155
                     if not test_name then goto continue end

              
                171
                156
                 

              
                172
                
                -    -- Handle different actions

              
                173
                157
                     if event.Action == "run" then

              
                174
                158
                       test_outputs[test_name] = test_outputs[test_name] or {}

              
                175
                159
                     elseif event.Action == "output" then

              ···
                178
                162
                         local output_line = (event.Output:gsub("\n$", ""))

              
                179
                163
                         table.insert(test_outputs[test_name], output_line)

              
                180
                164
                       end

              
                181
                
                -    elseif event.Action == "pass" or event.Action == "skip" then

              
                182
                
                -      test_status[test_name] = "pass"

              
                
                165
                +    elseif event.Action == "pass" then

              
                
                166
                +      test_status[test_name] = "success"

              
                
                167
                +    elseif event.Action == "skip" then

              
                
                168
                +      test_status[test_name] = "skipped"

              
                183
                169
                     elseif event.Action == "fail" then

              
                184
                170
                       test_status[test_name] = "fail"

              
                185
                171
                     end

              ···
                187
                173
                     ::continue::

              
                188
                174
                   end

              
                189
                175
                 

              
                190
                
                -  -- Build results from collected data

              
                
                176
                +  -- build results

              
                191
                177
                   for name, status in pairs(test_status) do

              
                192
                178
                     local output_lines = test_outputs[name] or {}

              
                193
                
                -

              
                194
                
                -    -- Extract error line if failed

              
                195
                179
                     local error_line = nil

              
                196
                180
                     if status == "fail" then

              
                197
                181
                       for _, out_line in ipairs(output_lines) do

              ···
                214
                198
                   return results

              
                215
                199
                 end

              
                216
                200
                 

              
                217
                
                ---- TODO: fix me daddy

              
                218
                201
                 ---@param output string[] output lines, json format

              
                219
                
                ----@param test_name string|nil Specific test name to get output for

              
                
                202
                +---@param test_name string|nil specific test name to get output for

              
                220
                203
                 ---@return string[]

              
                221
                204
                 function golang.extract_test_output(output, test_name)

              
                222
                205
                   local result = {}

              
                223
                206
                   for _, line in ipairs(output) do

              
                224
                207
                     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)

              
                
                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

              
                233
                216
                       end

              
                234
                217
                     end

              
                235
                218
                   end

              
                236
                219
                   return result

              
                237
                220
                 end

              
                238
                221
                 

              
                239
                
                ----@param output string[] Output lines

              
                
                222
                +---@param output string[]

              
                240
                223
                 ---@return string|nil

              
                241
                224
                 function golang.extract_error_message(output)

              
                242
                225
                   for _, line in ipairs(output) do

              
A lua/utest/health.lua
···
                
                1
                +---@param parser string

              
                
                2
                +local function check_treesitter(parser)

              
                
                3
                +  local ok, p = pcall(vim.treesitter.get_parser, 0, parser)

              
                
                4
                +  if ok and p ~= nil then

              
                
                5
                +    vim.health.ok("`" .. parser .. "` parser is installed")

              
                
                6
                +  else

              
                
                7
                +    vim.health.error("`" .. parser .. "` parser not found")

              
                
                8
                +  end

              
                
                9
                +end

              
                
                10
                +

              
                
                11
                +---@param bin string

              
                
                12
                +local function check_binary(bin)

              
                
                13
                +  if vim.fn.executable(bin) == 1 then

              
                
                14
                +    vim.health.ok(bin .. " is found oh PATH: `" .. vim.fn.exepath(bin) .. "`")

              
                
                15
                +  else

              
                
                16
                +    vim.health.error(bin .. " not found on PATH")

              
                
                17
                +  end

              
                
                18
                +end

              
                
                19
                +

              
                
                20
                +local health = {}

              
                
                21
                +function health.check()

              
                
                22
                +  vim.health.start "Go adapter"

              
                
                23
                +  check_treesitter "go"

              
                
                24
                +  check_binary "go"

              
                
                25
                +end

              
                
                26
                +

              
                
                27
                +return health

              
M readme
···
                26
                26
                   require("utest").setup {

              
                27
                27
                     timeout = 30, -- (seconds) timeout for tests

              
                28
                28
                     icons = {

              
                29
                
                -      success = "",

              
                30
                29
                       failed = "",

              
                31
                30
                       running = "",

              
                
                31
                +      skipped = "",

              
                
                32
                +      success = "",

              
                32
                33
                     }

              
                33
                34
                   }

              
                34
                35
                 

              
A todo
···
                
                1
                +- batch tests on .run_file()

              
                
                2
                +- tests

              
                
                3
                +- ocaml adapter