Add basic Lua ALE functions and test coverage

Ensure that basic ALE functions `ale.var`, `ale.escape`, and `ale.env`
are available in Lua. Cover all Lua code so far with busted tests,
fixing bugs where ALE variables can be set with Boolean values instead
of numbers. Document all functionality so far.
This commit is contained in:
w0rp
2025-03-21 23:37:35 +00:00
parent 7f43666fb3
commit f4af0dc84b
16 changed files with 944 additions and 105 deletions

View File

@@ -11,9 +11,30 @@
"pending",
"assert"
],
"workspace.checkThirdParty": false,
"workspace.library": [
"${3rd}/busted/library",
"${env:HOME}/.luarocks/share/lua/5.4",
"${env:HOME}/.luarocks/share/lua/5.3",
"${env:HOME}/.luarocks/share/lua/5.2",
"${env:HOME}/.luarocks/share/lua/5.1",
"../../lua"
],
"runtime.pathStrict": true,
"runtime.path": [
"?.lua",
"?/init.lua",
"../../lua/?.lua",
"../../lua/?/init.lua",
"${env:HOME}/.luarocks/share/lua/5.4/?.lua",
"${env:HOME}/.luarocks/share/lua/5.4/?/init.lua",
"${env:HOME}/.luarocks/share/lua/5.3/?.lua",
"${env:HOME}/.luarocks/share/lua/5.3/?/init.lua",
"${env:HOME}/.luarocks/share/lua/5.2/?.lua",
"${env:HOME}/.luarocks/share/lua/5.2/?/init.lua",
"${env:HOME}/.luarocks/share/lua/5.1/?.lua",
"${env:HOME}/.luarocks/share/lua/5.1/?/init.lua"
],
"runtime.version": "LuaJIT",
"hint.enable": false
}

View File

@@ -0,0 +1,232 @@
local eq = assert.are.same
local diagnostics
describe("ale.diagnostics.send", function()
local buffer_map
local signs_config
local diagnostic_set_calls
setup(function()
_G.vim = {
api = {
nvim_buf_get_var = function(buffer, key)
local buffer_table = buffer_map[buffer] or {}
local value = buffer_table[key]
if value == nil then
error(key .. " is missing")
end
return value
end,
nvim_create_namespace = function()
return 42
end,
},
diagnostic = {
severity = {ERROR = 1, WARN = 2, INFO = 3},
config = function()
return {signs = signs_config}
end,
set = function(namespace, bufnr, _diagnostics, opts)
table.insert(diagnostic_set_calls, {
namespace = namespace,
bufnr = bufnr,
diagnostics = _diagnostics,
opts = opts,
})
end,
},
tbl_extend = function(behavior, ...)
assert(behavior == "force", "We should only use `force`")
local merged = {}
for _, arg in ipairs({...}) do
for key, value in pairs(arg) do
merged[key] = value
end
end
return merged
end,
g = {},
}
diagnostics = require("ale.diagnostics")
end)
teardown(function()
_G.vim = nil
end)
before_each(function()
buffer_map = {}
diagnostic_set_calls = {}
signs_config = false
_G.vim.g = {}
end)
it("should set an empty list of diagnostics correctly", function()
diagnostics.send(7, {})
eq(
{
{
namespace = 42,
bufnr = 7,
diagnostics = {},
opts = {virtual_text = false}
},
},
diagnostic_set_calls
)
end)
it("should handle basic case with all fields", function()
diagnostics.send(1, {
{
bufnr = 1,
lnum = 2,
end_lnum = 3,
col = 4,
end_col = 5,
type = "W",
code = "123",
text = "Warning message",
linter_name = "eslint",
},
})
eq({
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = vim.diagnostic.severity.WARN,
code = "123",
message = "Warning message",
source = "eslint",
},
}, diagnostic_set_calls[1].diagnostics)
end)
it("should default end_lnum to lnum when missing", function()
diagnostics.send(1, {
{
bufnr = 1,
lnum = 5,
col = 2,
end_col = 8,
type = "E",
text = "Error message",
linter_name = "mylinter",
},
})
eq({
{
lnum = 4,
end_lnum = 4,
col = 1,
end_col = 8,
severity = vim.diagnostic.severity.ERROR,
code = nil,
message = "Error message",
source = "mylinter",
},
}, diagnostic_set_calls[1].diagnostics)
end)
it("should default col to 0 when missing", function()
diagnostics.send(1, {
{
bufnr = 1,
lnum = 10,
end_lnum = 12,
end_col = 6,
type = "I",
text = "Info message",
},
})
eq({
{
lnum = 9,
end_lnum = 11,
col = 0,
end_col = 6,
severity = vim.diagnostic.severity.INFO,
code = nil,
message = "Info message",
source = nil,
},
}, diagnostic_set_calls[1].diagnostics)
end)
it("should ignore non-matching buffers", function()
diagnostics.send(1, {
{
bufnr = 2,
lnum = 1,
end_lnum = 2,
col = 1,
end_col = 4,
type = "W",
text = "Message",
},
})
eq({}, diagnostic_set_calls[1].diagnostics)
end)
for _, set_signs_value in ipairs {1, true} do
describe("signs with setting set_signs = " .. tostring(set_signs_value), function()
before_each(function()
_G.vim.g.ale_set_signs = set_signs_value
_G.vim.g.ale_sign_priority = 10
end)
it("and global config as `false` should enable signs with the given priority", function()
diagnostics.send(7, {})
eq({priority = 10}, diagnostic_set_calls[1].opts.signs)
end)
it("and global config as a table should enable signs with the given priority", function()
signs_config = {foo = "bar", priority = 5}
diagnostics.send(7, {})
eq(
{foo = "bar", priority = 10},
diagnostic_set_calls[1].opts.signs
)
end)
it("and global config as a function should enable signs with the given priority", function()
signs_config = function()
return {foo = "bar", priority = 5}
end
diagnostics.send(7, {})
local local_signs = diagnostic_set_calls[1].opts.signs
eq("function", type(local_signs))
eq({foo = "bar", priority = 10}, local_signs())
end)
end)
end
it("should toggle virtual_text correctly", function()
for _, value in ipairs({"all", "2", 2, "current", "1", 1, true}) do
diagnostic_set_calls = {}
_G.vim.g.ale_virtualtext_cursor = value
diagnostics.send(7, {})
eq({virtual_text = true}, diagnostic_set_calls[1].opts)
end
for _, value in ipairs({"disabled", "0", 0, false, nil}) do
diagnostic_set_calls = {}
_G.vim.g.ale_virtualtext_cursor = value
diagnostics.send(7, {})
eq({virtual_text = false}, diagnostic_set_calls[1].opts)
end
end)
end)

56
test/lua/ale_env_spec.lua Normal file
View File

@@ -0,0 +1,56 @@
local eq = assert.are.same
local ale = require("ale")
describe("ale.env", function()
local is_win32 = false
setup(function()
_G.vim = {
o = setmetatable({}, {
__index = function(_, key)
if key == "shell" then
if is_win32 then
return "cmd.exe"
end
return "bash"
end
return nil
end
}),
fn = {
has = function(feature)
return feature == "win32" and is_win32
end,
-- Mock a very poor version of shellescape() for Unix
-- This shouldn't be called for Windows
shellescape = function(str)
return "'" .. str .. "'"
end,
fnamemodify = function(shell, _)
return shell
end
}
}
end)
teardown(function()
_G.vim = nil
end)
before_each(function()
is_win32 = false
end)
it("should escape values correctly on Unix", function()
eq("name='xxx' ", ale.env('name', 'xxx'))
eq("name='foo bar' ", ale.env('name', 'foo bar'))
end)
it("should escape values correctly on Windows", function()
is_win32 = true
eq('set name=xxx && ', ale.env('name', 'xxx'))
eq('set "name=foo bar" && ', ale.env('name', 'foo bar'))
end)
end)

224
test/lua/ale_lsp_spec.lua Normal file
View File

@@ -0,0 +1,224 @@
local eq = assert.are.same
local lsp = require("ale.lsp")
describe("ale.lsp.start", function()
local start_calls
local rpc_connect_calls
local vim_fn_calls
local defer_calls
setup(function()
_G.vim = {
defer_fn = function(func, delay)
table.insert(defer_calls, {func, delay})
end,
fn = setmetatable({}, {
__index = function(_, key)
return function(...)
table.insert(vim_fn_calls, {key, ...})
if key == "ale#lsp#GetLanguage" then
return "python"
end
if key ~= "ale#lsp_linter#HandleLSPResponse"
and key ~= "ale#lsp#UpdateCapabilities"
and key ~= "ale#lsp#CallInitCallbacks"
then
assert(false, "Invalid ALE function: " .. key)
end
return nil
end
end,
}),
lsp = {
rpc = {
connect = function(host, port)
return function(dispatch)
table.insert(rpc_connect_calls, {
host = host,
port = port,
dispatch = dispatch,
})
end
end,
},
start = function(...)
table.insert(start_calls, {...})
return 42
end,
},
}
end)
teardown(function()
_G.vim = nil
end)
before_each(function()
start_calls = {}
rpc_connect_calls = {}
vim_fn_calls = {}
defer_calls = {}
end)
it("should start lsp programs with the correct arguments", function()
lsp.start({
name = "server:/code",
cmd = "server",
root_dir = "/code",
-- This Boolean value somehow ends up in Dictionaries from
-- Vim for init_options, and we need to remove it.
init_options = {[true] = 123},
})
-- Remove arguments with functions we can't apply equality checks
-- for easily.
for _, args in pairs(start_calls) do
args[1].handlers = nil
args[1].on_init = nil
args[1].get_language_id = nil
end
eq({
{
{
cmd = "server",
name = "server:/code",
root_dir = "/code",
init_options = {},
},
{attach = false, silent = true}
}
}, start_calls)
eq({}, vim_fn_calls)
end)
it("should start lsp socket connections with the correct arguments", function()
lsp.start({
name = "localhost:1234:/code",
host = "localhost",
port = 1234,
root_dir = "/code",
init_options = {foo = "bar"},
})
local cmd
-- Remove arguments with functions we can't apply equality checks
-- for easily.
for _, args in pairs(start_calls) do
cmd = args[1].cmd
args[1].cmd = nil
args[1].handlers = nil
args[1].on_init = nil
args[1].get_language_id = nil
end
eq({
{
{
name = "localhost:1234:/code",
root_dir = "/code",
init_options = {foo = "bar"},
},
{attach = false, silent = true}
}
}, start_calls)
cmd("dispatch_value")
eq({
{dispatch = "dispatch_value", host = "localhost", port = 1234},
}, rpc_connect_calls)
eq({}, vim_fn_calls)
end)
it("should return the client_id value from vim.lsp.start", function()
eq(42, lsp.start({}))
end)
it("should implement get_language_id correctly", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
eq("python", start_calls[1][1].get_language_id(347, "ftype"))
eq({{"ale#lsp#GetLanguage", "server:/code", 347}}, vim_fn_calls)
end)
it("should initialize clients with ALE correctly", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
start_calls[1][1].on_init({server_capabilities = {cap = 1}})
eq({
{"ale#lsp#UpdateCapabilities", "server:/code", {cap = 1}},
}, vim_fn_calls)
eq(1, #defer_calls)
eq(2, #defer_calls[1])
eq("function", type(defer_calls[1][1]))
eq(0, defer_calls[1][2])
defer_calls[1][1]()
eq({
{"ale#lsp#UpdateCapabilities", "server:/code", {cap = 1}},
{"ale#lsp#CallInitCallbacks", "server:/code"},
}, vim_fn_calls)
end)
it("should configure handlers correctly", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
local handlers = start_calls[1][1].handlers
local handler_names = {}
-- get keys from handlers
for key, _ in pairs(handlers) do
-- add key to handler_names mapping
handler_names[key] = true
end
eq({["textDocument/publishDiagnostics"] = true}, handler_names)
handlers["textDocument/publishDiagnostics"](nil, {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
},
})
eq({
{
"ale#lsp_linter#HandleLSPResponse",
"server:/code",
{
jsonrpc = "2.0",
method = "textDocument/publishDiagnostics",
params = {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
},
}
}
},
}, vim_fn_calls)
end)
end)

51
test/lua/ale_var_spec.lua Normal file
View File

@@ -0,0 +1,51 @@
local eq = assert.are.same
local ale = require("ale")
describe("ale.var", function()
local buffer_map
setup(function()
_G.vim = {
api = {
nvim_buf_get_var = function(buffer, key)
local buffer_table = buffer_map[buffer] or {}
local value = buffer_table[key]
if value == nil then
error(key .. " is missing")
end
return value
end,
},
g = {
},
}
end)
teardown(function()
_G.vim = nil
end)
before_each(function()
buffer_map = {}
_G.vim.g = {}
end)
it("should return nil for undefined variables", function()
eq(nil, ale.var(1, "foo"))
end)
it("should return buffer-local values, if set", function()
_G.vim.g.ale_foo = "global-value"
buffer_map[1] = {ale_foo = "buffer-value"}
eq("buffer-value", ale.var(1, "foo"))
end)
it("should return global values, if set", function()
_G.vim.g.ale_foo = "global-value"
eq("global-value", ale.var(1, "foo"))
end)
end)

View File

@@ -0,0 +1,61 @@
local eq = assert.are.same
local ale = require("ale")
describe("ale.escape for cmd.exe", function()
setup(function()
_G.vim = {
o = {
shell = "cmd.exe"
},
fn = {
fnamemodify = function(shell, _)
return shell
end
}
}
end)
teardown(function()
_G.vim = nil
end)
it("should allow not escape paths without special characters", function()
eq("C:", ale.escape("C:"))
eq("C:\\", ale.escape("C:\\"))
eq("python", ale.escape("python"))
eq("C:\\foo\\bar", ale.escape("C:\\foo\\bar"))
eq("/bar/baz", ale.escape("/bar/baz"))
eq("nul", ale.escape("nul"))
eq("'foo'", ale.escape("'foo'"))
end)
it("should escape Windows paths with spaces appropriately", function()
eq('"C:\\foo bar\\baz"', ale.escape('C:\\foo bar\\baz'))
eq('"^foo bar^"', ale.escape('^foo bar^'))
eq('"&foo bar&"', ale.escape('&foo bar&'))
eq('"|foo bar|"', ale.escape('|foo bar|'))
eq('"<foo bar<"', ale.escape('<foo bar<'))
eq('">foo bar>"', ale.escape('>foo bar>'))
eq('"^foo bar^"', ale.escape('^foo bar^'))
eq('"\'foo\' \'bar\'"', ale.escape('\'foo\' \'bar\''))
end)
it("should use caret escapes on special characters", function()
eq('^^foo^^', ale.escape('^foo^'))
eq('^&foo^&', ale.escape('&foo&'))
eq('^|foo^|', ale.escape('|foo|'))
eq('^<foo^<', ale.escape('<foo<'))
eq('^>foo^>', ale.escape('>foo>'))
eq('^^foo^^', ale.escape('^foo^'))
eq('\'foo\'^^\'bar\'', ale.escape('\'foo\'^\'bar\''))
end)
it("should escape percent characters", function()
eq('%%foo%%', ale.escape('%foo%'))
eq('C:\foo%%\bar\baz%%', ale.escape('C:\foo%\bar\baz%'))
eq('"C:\foo bar%%\baz%%"', ale.escape('C:\foo bar%\baz%'))
eq('^^%%foo%%', ale.escape('^%foo%'))
eq('"^%%foo%% %%bar%%"', ale.escape('^%foo% %bar%'))
eq('"^%%foo%% %%bar%% """""', ale.escape('^%foo% %bar% ""'))
end)
end)