💡
neotestを拡張してApex言語の関数テストを実行できるようにする
はじめに
neotestでは、neovimから関数単位でテストを実行するのに使えます。
私はF4でカーソル位置関数のテスト実行するようにしています。
設定例
最小ではないですが、こんな感じで設定しています。
- @isTestの下の行の関数名を取得している
- 関数ブロックの検出は実装できておらず、関数定義のところにカーソルを合わせないといけない。
local Tree = require("neotest.types").Tree
local function make_test_id(path, method)
return path .. "::" .. method
end
local function discover_positions(path)
local lines = vim.fn.readfile(path)
if #lines == 0 then
return
end
local class_name
local tests = {}
for i, line in ipairs(lines) do
if not class_name then
class_name = line:match("class%s+([%w_]+)")
end
-- @isTestの直後のstatic void
if line:match("^%s*@isTest") and lines[i + 1] then
local method = lines[i + 1]:match("^%s*static%s+void%s+([%w_]+)%s*%(")
if method then
table.insert(tests, {
id = make_test_id(path, method),
type = "test",
name = method,
path = path,
range = { i, 0, i + 1, 0 },
class = class_name,
method = method,
})
end
end
-- 1行で@isTest static void <method>()
local method_inline = line:match("^%s*@isTest%s+static%s+void%s+([%w_]+)%s*%(")
if method_inline then
table.insert(tests, {
id = make_test_id(path or "", method_inline),
type = "test",
name = method_inline,
path = path,
range = { i - 1, 0, i - 1, 0 },
class = class_name,
method = method_inline,
})
end
end
local nodes = {
{
id = path,
type = "file",
path = path,
name = class_name or path,
range = { 0, 0, #lines, 0 },
},
}
for _, test in ipairs(tests) do
table.insert(nodes, test)
end
return Tree.from_list(nodes, function(pos)
return pos.id
end)
end
local fixed_dir = "/tmp/sf-test-result"
local apex_adapter = {
name = "apex",
root = function(dir)
-- 例: .sfdxやsfdx-project.jsonがあるディレクトリをルートとみなす
if vim.fn.glob(dir .. "/sfdx-project.json") ~= "" then
return dir
end
return nil
end,
is_test_file = function(file_path)
return file_path:match("%.cls$")
end,
-- discover_positions = function(path)
-- return discover_positions(path)
-- end,
-- テストメソッドの位置を特定する関数
discover_positions = discover_positions,
build_spec = function(args)
local data = args.tree:data()
local class = data.class or data.name
local method = data.method
local test_arg = class .. "." .. method
local command = {
"sf",
"apex",
"run",
"test",
"--tests",
test_arg,
"--result-format",
"json",
"--synchronous",
"-d",
fixed_dir,
}
return {
command = table.concat(command, " "),
cwd = vim.fn.getcwd(),
}
end,
results = function(spec, result, tree)
local result_path = fixed_dir .. "/test-result.json"
local output
if vim.fn.filereadable(result_path) == 1 then
output = table.concat(vim.fn.readfile(result_path), "\n")
end
local ok, parsed = pcall(vim.fn.json_decode, output)
if not ok then
return {}
end
local results = {}
for _, test in ipairs(parsed.tests) do
for _, node in tree:iter_nodes() do
local data = node:data()
-- tree:iter_nodes()でidが一致するノードを探す
local test_id = make_test_id(data.path, test.MethodName)
if data.id == test_id then
results[data.id] = {
status = (string.lower(test.Outcome) == "pass") and "passed" or "failed",
short = test.Message and test.StackTrace and (test.Message .. "\n" .. test.StackTrace)
or test.Message
or test.StackTrace
or "",
}
end
end
end
return results
end,
}
require("neotest").setup({
adapters = {
apex_adapter,
},
})
vim.api.nvim_set_keymap("n", "<F4>", ':lua require("neotest").run.run()<CR>', { noremap = true, silent = true })
Discussion