diff --git a/lua/test-samurai/config.lua b/lua/test-samurai/config.lua index e13eddd..d3538cb 100644 --- a/lua/test-samurai/config.lua +++ b/lua/test-samurai/config.lua @@ -6,6 +6,7 @@ local defaults = { "test-samurai.runners.js-jest", "test-samurai.runners.js-mocha", "test-samurai.runners.js-vitest", + "test-samurai.runners.lua-plenary", }, } diff --git a/lua/test-samurai/runners/lua-plenary.lua b/lua/test-samurai/runners/lua-plenary.lua new file mode 100644 index 0000000..d3eeecb --- /dev/null +++ b/lua/test-samurai/runners/lua-plenary.lua @@ -0,0 +1,240 @@ +local util = require("test-samurai.util") + +local runner = { + name = "lua-plenary", + framework = "plenary", +} + +local function is_lua_test_path(path) + if not path or path == "" then + return false + end + if not path:match("%.lua$") then + return false + end + if path:match("_spec%.lua$") then + return true + end + if path:match("/tests/") then + return true + end + return false +end + +function runner.is_test_file(bufnr) + local path = util.get_buf_path(bufnr) + return is_lua_test_path(path) +end + +local function find_repo_root(file) + local root = util.find_root(file, { ".git", "tests", "lua", "plugin" }) + if not root or root == "" then + root = vim.loop.cwd() + end + return root +end + +local function minimal_init_for(root) + return vim.fs.joinpath(root, "tests", "minimal_init.lua") +end + +local function escape_for_ex_double_quotes(s) + s = s or "" + s = s:gsub("\\", "\\\\") + s = s:gsub('"', '\\"') + return s +end + +local function count_keyword(line, kw) + local c = 0 + local pat = "%f[%w]" .. kw .. "%f[%W]" + for _ in line:gmatch(pat) do + c = c + 1 + end + return c +end + +local function find_end_lua_block(lines, start_idx) + local depth = 0 + local started = false + for i = start_idx, #lines do + local line = lines[i] + local fnc = count_keyword(line, "function") + local endc = count_keyword(line, "end") + if fnc > 0 then + depth = depth + fnc + started = true + end + if started and endc > 0 then + depth = depth - endc + if depth <= 0 then + return i - 1 + end + end + end + return #lines - 1 +end + +local function collect_lua_structs(lines) + local describes = {} + local tests = {} + + for i, line in ipairs(lines) do + local dname = line:match("^%s*describe%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if dname then + local start0 = i - 1 + local end0 = find_end_lua_block(lines, i) + table.insert(describes, { kind = "describe", name = dname, start = start0, ["end"] = end0 }) + else + local tname = line:match("^%s*it%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if tname then + local start0 = i - 1 + local end0 = find_end_lua_block(lines, i) + table.insert(tests, { kind = "it", name = tname, start = start0, ["end"] = end0 }) + end + end + end + + return describes, tests +end + +local function build_full_name(lines, idx, leaf_name) + local parts = { leaf_name } + for i = idx - 1, 1, -1 do + local line = lines[i] + local dname = line:match("^%s*describe%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if dname then + table.insert(parts, 1, dname) + end + end + return table.concat(parts, " ") +end + +function runner.find_nearest(bufnr, row, _col) + if not runner.is_test_file(bufnr) then + return nil, "not a lua test file" + end + + local lines = util.get_buf_lines(bufnr) + local describes, tests = collect_lua_structs(lines) + + for _, t in ipairs(tests) do + if row >= t.start and row <= t["end"] then + local full = build_full_name(lines, t.start + 1, t.name) + local file = util.get_buf_path(bufnr) + local root = find_repo_root(file) + return { file = file, cwd = root, test_name = t.name, full_name = full, kind = "it" } + end + end + + local best_describe = nil + for _, d in ipairs(describes) do + if row >= d.start and row <= d["end"] then + if not best_describe or d.start >= best_describe.start then + best_describe = d + end + end + end + if best_describe then + local full = build_full_name(lines, best_describe.start + 1, best_describe.name) + local file = util.get_buf_path(bufnr) + local root = find_repo_root(file) + return { file = file, cwd = root, test_name = best_describe.name, full_name = full, kind = "describe" } + end + + local start = row + 1 + if start > #lines then + start = #lines + elseif start < 1 then + start = 1 + end + for i = start, 1, -1 do + local line = lines[i] + local tname = line:match("^%s*it%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if tname then + local full = build_full_name(lines, i, tname) + local file = util.get_buf_path(bufnr) + local root = find_repo_root(file) + return { file = file, cwd = root, test_name = tname, full_name = full, kind = "it" } + end + local dname = line:match("^%s*describe%s*%(%s*['\"`]([^'\"`]+)['\"`]") + if dname then + local full = build_full_name(lines, i, dname) + local file = util.get_buf_path(bufnr) + local root = find_repo_root(file) + return { file = file, cwd = root, test_name = dname, full_name = full, kind = "describe" } + end + end + + return nil, "no test call found" +end + +local function ex_plenary_busted_file(file, filter_name) + local ex = "PlenaryBustedFile " .. vim.fn.fnameescape(file) + if filter_name and filter_name ~= "" then + local f = escape_for_ex_double_quotes(filter_name) + ex = ex .. ' { busted_args = { "--filter", "' .. f .. '" } }' + end + return ex +end + +function runner.build_command(spec) + local root = spec and spec.cwd or vim.loop.cwd() + local minit = minimal_init_for(root) + + local cmd = { + "nvim", + "--headless", + "-u", + minit, + "-c", + ex_plenary_busted_file(spec.file, spec.test_name), + "-c", + "qa", + } + + return { cmd = cmd, cwd = root } +end + +function runner.build_file_command(bufnr) + local file = util.get_buf_path(bufnr) + if not file or file == "" then + return nil + end + local root = find_repo_root(file) + local minit = minimal_init_for(root) + + local cmd = { + "nvim", + "--headless", + "-u", + minit, + "-c", + ex_plenary_busted_file(file, nil), + "-c", + "qa", + } + + return { cmd = cmd, cwd = root } +end + +function runner.build_all_command(bufnr) + local file = util.get_buf_path(bufnr) + local root = find_repo_root(file) + local minit = minimal_init_for(root) + + local cmd = { + "nvim", + "--headless", + "-u", + minit, + "-c", + "PlenaryBustedDirectory tests", + "-c", + "qa", + } + + return { cmd = cmd, cwd = root } +end + +return runner diff --git a/session.vim b/session.vim new file mode 100644 index 0000000..5d2a778 --- /dev/null +++ b/session.vim @@ -0,0 +1,97 @@ +let SessionLoad = 1 +let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 +let v:this_session=expand(":p") +silent only +silent tabonly +cd ~/Projekte/Neovim-Plugins/test-samurai +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +let s:shortmess_save = &shortmess +if &shortmess =~ 'A' + set shortmess=aoOA +else + set shortmess=aoO +endif +badd +17 tests/test_samurai_lua_spec.lua +badd +1 ~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua +argglobal +%argdel +edit tests/test_samurai_lua_spec.lua +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +vsplit +1wincmd h +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe 'vert 1resize ' . ((&columns * 119 + 119) / 239) +exe 'vert 2resize ' . ((&columns * 119 + 119) / 239) +argglobal +balt ~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua +setlocal foldmethod=expr +setlocal foldexpr=v:lua.vim.treesitter.foldexpr() +setlocal foldmarker={{{,}}} +setlocal foldignore=# +setlocal foldlevel=99 +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldenable +let s:l = 17 - ((16 * winheight(0) + 27) / 54) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 17 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua", ":p")) | buffer ~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua | else | edit ~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua | endif +if &buftype ==# 'terminal' + silent file ~/Projekte/Neovim-Plugins/test-samurai/lua/test-samurai/runners/lua-plenary.lua +endif +balt tests/test_samurai_lua_spec.lua +setlocal foldmethod=expr +setlocal foldexpr=v:lua.vim.treesitter.foldexpr() +setlocal foldmarker={{{,}}} +setlocal foldignore=# +setlocal foldlevel=99 +setlocal foldminlines=1 +setlocal foldnestmax=20 +setlocal foldenable +let s:l = 1 - ((0 * winheight(0) + 27) / 54) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 1 +normal! 0 +wincmd w +exe 'vert 1resize ' . ((&columns * 119 + 119) / 239) +exe 'vert 2resize ' . ((&columns * 119 + 119) / 239) +tabnext 1 +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 +let &shortmess = s:shortmess_save +let &winminheight = s:save_winminheight +let &winminwidth = s:save_winminwidth +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &g:so = s:so_save | let &g:siso = s:siso_save +set hlsearch +nohlsearch +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : diff --git a/tests/test_samurai_lua_spec.lua b/tests/test_samurai_lua_spec.lua new file mode 100644 index 0000000..25286d2 --- /dev/null +++ b/tests/test_samurai_lua_spec.lua @@ -0,0 +1,130 @@ +local test_samurai = require("test-samurai") +local lua_runner = require("test-samurai.runners.lua-plenary") +local util = require("test-samurai.util") + +local function mkbuf(path, ft, lines) + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = ft + if lines then + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + end + return bufnr +end + +describe("test-samurai lua runner (plenary)", function() + it("detects lua spec files by suffix", function() + local bufnr = mkbuf("/tmp/test_samurai_lua_spec_unique_1_spec.lua", "lua") + assert.is_true(lua_runner.is_test_file(bufnr)) + end) + + it("finds nearest it() when cursor is inside it block and builds filtered command", function() + local bufnr = mkbuf("/tmp/project/tests/test_samurai_lua_nearest_unique_2_spec.lua", "lua", { + "describe('outer', function()", + " it('inner 1', function()", + " local x = 1", + " end)", + "", + " it('inner 2', function()", + " local y = 2", + " end)", + "end)", + }) + + local orig_find_root = util.find_root + util.find_root = function() + return "/tmp/project" + end + + local spec, err = lua_runner.find_nearest(bufnr, 6, 0) + + util.find_root = orig_find_root + + assert.is_nil(err) + assert.is_not_nil(spec) + assert.equals("inner 2", spec.test_name) + assert.equals("outer inner 2", spec.full_name) + assert.equals("/tmp/project", spec.cwd) + + local cmd_spec = lua_runner.build_command(spec) + assert.equals("/tmp/project", cmd_spec.cwd) + assert.are.same({ + "nvim", + "--headless", + "-u", + "/tmp/project/tests/minimal_init.lua", + "-c", + 'PlenaryBustedFile ' .. spec.file .. ' { busted_args = { "--filter", "inner 2" } }', + "-c", + "qa", + }, cmd_spec.cmd) + end) + + it("returns describe block when cursor is between it() calls", function() + local bufnr = mkbuf("/tmp/project/tests/test_samurai_lua_between_unique_3_spec.lua", "lua", { + "describe('outer', function()", + " it('inner 1', function()", + " local x = 1", + " end)", + "", + " it('inner 2', function()", + " local y = 2", + " end)", + "end)", + }) + + local orig_find_root = util.find_root + util.find_root = function() + return "/tmp/project" + end + + local spec, err = lua_runner.find_nearest(bufnr, 4, 0) + + util.find_root = orig_find_root + + assert.is_nil(err) + assert.is_not_nil(spec) + assert.equals("outer", spec.test_name) + assert.equals("outer", spec.full_name) + assert.equals("describe", spec.kind) + end) + + it("builds all command via PlenaryBustedDirectory", function() + local bufnr = mkbuf("/tmp/project/tests/test_samurai_core_spec_unique_4_spec.lua", "lua") + + local orig_find_root = util.find_root + util.find_root = function() + return "/tmp/project" + end + + local cmd_spec = lua_runner.build_all_command(bufnr) + + util.find_root = orig_find_root + + assert.are.same({ + "nvim", + "--headless", + "-u", + "/tmp/project/tests/minimal_init.lua", + "-c", + "PlenaryBustedDirectory tests", + "-c", + "qa", + }, cmd_spec.cmd) + assert.equals("/tmp/project", cmd_spec.cwd) + end) + + it("core selects lua runner for *_spec.lua buffers", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.lua-plenary", + }, + }) + + local bufnr = mkbuf("/tmp/project/tests/test_samurai_core_spec_unique_5_spec.lua", "lua") + + local runner = require("test-samurai.core").get_runner_for_buf(bufnr) + assert.is_not_nil(runner) + assert.equals("lua-plenary", runner.name) + end) +end)