5 files changed,
166 insertions(+),
145 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-03-30 20:18:10 +0300
Change ID:
oxkuwwnrkvsvpmtokzmszpqzlltkrwmw
Parent:
2c72626
jump to
| M | lua/utest.lua |
| M | lua/utest/golang.lua |
| A | lua/utest/health.lua |
| M | readme |
| A | todo |
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