diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb7f1667..b2a14d09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,7 @@ jobs: - '--vim-90-only' - '--neovim-07-only' - '--neovim-08-only' + - '--lua-only' - '--linters-only' steps: - uses: actions/checkout@v4 diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 00000000..b7a80308 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "diagnostics.globals": [ + "vim" + ], + "workspace.ignoreDir": [ + "test" + ], + "workspace.library": [ + "/usr/share/nvim/runtime/lua" + ], + "runtime.pathStrict": true, + "runtime.path": [ + "lua/?.lua", + "lua/?/init.lua" + ], + "runtime.version": "LuaJIT", + "hint.enable": false +} diff --git a/Dockerfile b/Dockerfile index ed81e93d..b47fa9dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,10 @@ ARG TESTBED_VIM_VERSION=24 FROM testbed/vim:${TESTBED_VIM_VERSION} -RUN install_vim -tag v8.0.0027 -build \ - -tag v9.0.0297 -build \ - -tag neovim:v0.7.0 -build \ - -tag neovim:v0.8.0 -build - ENV PACKAGES="\ + lua5.1 \ + lua5.1-dev \ + lua5.1-busted \ bash \ git \ python2 \ @@ -19,6 +17,11 @@ ENV PACKAGES="\ RUN apk --update add $PACKAGES && \ rm -rf /var/cache/apk/* /tmp/* /var/tmp/* +RUN install_vim -tag v8.0.0027 -build \ + -tag v9.0.0297 -build \ + -tag neovim:v0.7.0 -build \ + -tag neovim:v0.8.0 -build + RUN pip install vim-vint==0.3.21 RUN git clone https://github.com/junegunn/vader.vim vader && \ diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index c96091e1..b19b2761 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -259,7 +259,7 @@ function! ale#engine#SendResultsToNeovimDiagnostics(buffer, loclist) abort " Keep the Lua surface area really small in the VimL part of ALE, " and just require the diagnostics.lua module on demand. - let l:SendDiagnostics = luaeval('require("ale.diagnostics").sendAleResultsToDiagnostics') + let l:SendDiagnostics = luaeval('require("ale.diagnostics").send') call l:SendDiagnostics(a:buffer, a:loclist) endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 772ed989..d3bf8224 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -4453,6 +4453,7 @@ Vim autocmd names `ale#Foo` are available in the Vim context, and functions documented with dot names `ale.foo` are available in Lua scripts. +ale.env(variable_name, value) *ale.env()* ale#Env(variable_name, value) *ale#Env()* Given a variable name and a string value, produce a string for including in @@ -4464,6 +4465,18 @@ ale#Env(variable_name, value) *ale#Env()* 'set VAR="some value" && command' # On Windows +ale.escape(str) *ale.escape()* +ale#Escape(str) *ale#Escape()* + + Given a string, escape that string so it is ready for shell execution. + + If the shell is detected to be `cmd.exe`, ALE will apply its own escaping + that tries to avoid escaping strings unless absolutely necessary to avoid + issues with Windows programs that do not properly handle quoted arguments. + + In all other cases, ALE will call |shellescape|. + + ale#GetFilenameMappings(buffer, name) *ale#GetFilenameMappings()* Given a `buffer` and the `name` of either a linter for fixer, return a @@ -4509,7 +4522,7 @@ ale#Queue(delay, [linting_flag, buffer_number]) *ale#Queue()* is broken, or when developing ALE itself. -ale.setup(config) *ale.setup* +ale.setup(config) *ale.setup()* Configure ALE global settings, which are documented in |ale-options|. For example: > @@ -4524,7 +4537,7 @@ ale.setup(config) *ale.setup* ALE is being configured in less ambiguous if you like. -ale.setup.buffer(config) *ale.setup.buffer* +ale.setup.buffer(config) *ale.setup.buffer()* Configure ALE buffer-local settings, which are documented in |ale-options|. For example: > @@ -4534,6 +4547,18 @@ ale.setup.buffer(config) *ale.setup.buffer* }) < +ale.var(buffer, variable_name) *ale.var()* +ale#Var(buffer, variable_name) *ale#Var()* + + Given a buffer number and an ALE variable name return the value of that + if defined in the buffer, and if not defined in the buffer return the + global value. The `ale_` prefix will be added to the Vim variable name. + + The `ale#Var` Vim function will return errors if the variable is not defined + in either the buffer or globally. The `ale.var` Lua function will return + `nil` if the variable is not defined in either the buffer or globally. + + ale#command#CreateDirectory(buffer) *ale#command#CreateDirectory()* Create a new temporary directory with a unique name, and manage that diff --git a/lua/ale/diagnostics.lua b/lua/ale/diagnostics.lua index 93f94859..77e2491f 100644 --- a/lua/ale/diagnostics.lua +++ b/lua/ale/diagnostics.lua @@ -1,96 +1,95 @@ +local ale = require("ale") + local module = {} -local ale_type_to_diagnostic_severity = { - E = vim.diagnostic.severity.ERROR, - W = vim.diagnostic.severity.WARN, - I = vim.diagnostic.severity.INFO +local diagnostic_severity_map = { + E = vim.diagnostic.severity.ERROR, + W = vim.diagnostic.severity.WARN, + I = vim.diagnostic.severity.INFO } --- Equivalent to ale#Var, only we can't error on missing global keys. -module.aleVar = function(buffer, key) - key = "ale_" .. key - local exists, value = pcall(vim.api.nvim_buf_get_var, buffer, key) - - if exists then - return value - end - - return vim.g[key] -end - -module.sendAleResultsToDiagnostics = function(buffer, loclist) - local diagnostics = {} - - -- Convert all the ALE loclist items to the shape that Neovim's diagnostic - -- API is expecting. - for _, location in ipairs(loclist) do - if location.bufnr == buffer then - table.insert( - diagnostics, - -- All line numbers from ALE are 1-indexed, but all line numbers - -- in the diagnostics API are 0-indexed, so we have to subtract 1 - -- to make this work. - { - lnum = location.lnum - 1, - -- Ending line number, or if we don't have one, just make it the same - -- as the starting line number - end_lnum = (location.end_lnum or location.lnum) - 1, - -- Which column does the error start on? - col = math.max((location.col or 1) - 1, 0), - -- end_col does *not* appear to need 1 subtracted, so we don't. - end_col = location.end_col, - -- Which severity: error, warning, or info? - severity = ale_type_to_diagnostic_severity[location.type] or "E", - -- An error code - code = location.code, - -- The error message - message = location.text, - -- e.g. "rubocop" - source = location.linter_name, - } - ) - end - end - - local virtualtext_enabled_set = { - ['all'] = true, - ['2'] = true, +-- A map of all possible values that we can consider virtualtext enabled for +-- from ALE's setting. +local virtualtext_enabled_set = { + ["all"] = true, + ["2"] = true, [2] = true, - ['current'] = true, - ['1'] = true, + ["current"] = true, + ["1"] = true, [1] = true, - } + [true] = true, +} - local set_signs = module.aleVar(buffer, 'set_signs') - local sign_priority = module.aleVar(buffer, 'sign_priority') - local signs +---Send diagnostics to the Neovim diagnostics API +---@param buffer number The buffer number to retreive the variable for. +---@param loclist table The loclist array to report as diagnostics. +---@return nil +module.send = function(buffer, loclist) + local diagnostics = {} - if set_signs == 1 and sign_priority then - -- If signs are enabled, set the priority for them. - local local_cfg = { priority = sign_priority } - local global_cfg = vim.diagnostic.config().signs - - if type(global_cfg) == 'boolean' then - signs = local_cfg - elseif type(global_cfg) == 'table' then - signs = vim.tbl_extend('force', global_cfg, local_cfg) - else - signs = function(...) - local calculated = global_cfg(...) - return vim.tbl_extend('force', calculated, local_cfg) - end + -- Convert all the ALE loclist items to the shape that Neovim's diagnostic + -- API is expecting. + for _, location in ipairs(loclist) do + if location.bufnr == buffer then + table.insert( + diagnostics, + -- All line numbers from ALE are 1-indexed, but all line + -- numbers in the diagnostics API are 0-indexed, so we have to + -- subtract 1 to make this work. + { + lnum = location.lnum - 1, + -- Ending line number, or if we don't have one, just make + -- it the same as the starting line number + end_lnum = (location.end_lnum or location.lnum) - 1, + -- Which column does the error start on? + col = math.max((location.col or 1) - 1, 0), + -- end_col does not appear to need 1 subtracted. + end_col = location.end_col, + -- Which severity: error, warning, or info? + severity = diagnostic_severity_map[location.type] or "E", + -- An error code + code = location.code, + -- The error message + message = location.text, + -- e.g. "rubocop" + source = location.linter_name, + } + ) + end end - end - vim.diagnostic.set( - vim.api.nvim_create_namespace('ale'), - buffer, - diagnostics, - { - virtual_text = virtualtext_enabled_set[vim.g.ale_virtualtext_cursor] ~= nil, - signs = signs, - } - ) + local set_signs = ale.var(buffer, "set_signs") + local sign_priority = ale.var(buffer, "sign_priority") + local signs + + if (set_signs == 1 or set_signs == true) and sign_priority then + -- If signs are enabled, set the priority for them. + local local_cfg = { priority = sign_priority } + local global_cfg = vim.diagnostic.config().signs + + if type(global_cfg) == "boolean" then + signs = local_cfg + elseif type(global_cfg) == "table" then + signs = vim.tbl_extend("force", global_cfg, local_cfg) + else + -- If a global function is defined, then define a function + -- that calls that function when Neovim calls our function. + signs = function(...) + return vim.tbl_extend("force", global_cfg(...), local_cfg) + end + end + end + + vim.diagnostic.set( + vim.api.nvim_create_namespace("ale"), + buffer, + diagnostics, + { + virtual_text = + virtualtext_enabled_set[vim.g.ale_virtualtext_cursor] ~= nil, + signs = signs, + } + ) end return module diff --git a/lua/ale/init.lua b/lua/ale/init.lua index a08e66db..521ef9ea 100644 --- a/lua/ale/init.lua +++ b/lua/ale/init.lua @@ -1,19 +1,19 @@ local ale = {} local global_settings = setmetatable({}, { - __index = function (_, key) + __index = function(_, key) return vim.g['ale_' .. key] end, - __newindex = function (_, key, value) + __newindex = function(_, key, value) vim.g['ale_' .. key] = value end }) local buffer_settings = setmetatable({}, { - __index = function (_, key) + __index = function(_, key) return vim.b['ale_' .. key] end, - __newindex = function (_, key, value) + __newindex = function(_, key, value) vim.b['ale_' .. key] = value end }) @@ -30,17 +30,79 @@ ale.set_buffer = function(c) end end +---(when called) Set global ALE settings, just like ale.setup.global. +---@class ALESetup +---@field global fun(c: table): nil -- Set global ALE settings. +---@field buffer fun(c: table): nil -- Set buffer-local ALE settings. +---@overload fun(c: table): nil +---@type ALESetup ale.setup = setmetatable({ - global = function(c) - ale.set_global(c) - end, - buffer = function(c) - ale.set_buffer(c) - end, + ---Set global ALE settings. + ---@param c table The table of ALE settings to set. + ---@return nil + global = function(c) + ale.set_global(c) + end, + ---Set buffer-local ALE settings. + ---@param c table The table of ALE settings to set. + ---@return nil + buffer = function(c) + ale.set_buffer(c) + end, }, { - __call = function(self, c) - self.global(c) - end, + __call = function(self, c) + self.global(c) + end, }) +---Get an ALE variable for a buffer (first) or globally (second) +---@param buffer number The buffer number to retreive the variable for. +---@param variable_name string The variable to retrieve. +---@return any value The value for the ALE variable +ale.var = function(buffer, variable_name) + variable_name = "ale_" .. variable_name + local exists, value = pcall(vim.api.nvim_buf_get_var, buffer, variable_name) + + if exists then + return value + end + + return vim.g[variable_name] +end + +---Escape a string for use in a shell command +---@param str string The string to escape. +---@return string escaped The escaped string. +ale.escape = function(str) + local shell = vim.fn.fnamemodify(vim.o.shell, ":t") + + if shell:lower() == "cmd.exe" then + local step1 + + if str:find(" ") then + step1 = '"' .. str:gsub('"', '""') .. '"' + else + step1 = str:gsub("([&|<>^])", "^%1") + end + + local percent_subbed = step1:gsub("%%", "%%%%") + + return percent_subbed + end + + return vim.fn.shellescape(str) +end + +---Create a prefix for a shell command for adding environment variables. +---@param variable_name string The environment variable name. +---@param value string The value to set for the environment variable. +---@return string prefix The shell code for prefixing a command. +ale.env = function(variable_name, value) + if vim.fn.has("win32") then + return "set " .. ale.escape(variable_name .. "=" .. value) .. " && " + end + + return variable_name .. "=" .. ale.escape(value) .. " " +end + return ale diff --git a/lua/ale/lsp.lua b/lua/ale/lsp.lua index 01c59c86..b5529af4 100644 --- a/lua/ale/lsp.lua +++ b/lua/ale/lsp.lua @@ -2,7 +2,9 @@ local module = {} module.start = function(config) -- Neovim's luaeval sometimes adds a Boolean key to table we need to remove. - if config.init_options[true] ~= nil then + if type(config.init_options) == "table" + and config.init_options[true] ~= nil + then config.init_options[true] = nil end diff --git a/run-tests b/run-tests index fb3e750d..a4ddb051 100755 --- a/run-tests +++ b/run-tests @@ -29,6 +29,7 @@ run_neovim_07_tests=1 run_neovim_08_tests=1 run_vim_80_tests=1 run_vim_90_tests=1 +run_lua_tests=1 run_linters=1 while [ $# -ne 0 ]; do @@ -46,12 +47,14 @@ while [ $# -ne 0 ]; do run_vim_90_tests=0 run_neovim_07_tests=0 run_neovim_08_tests=0 + run_lua_tests=0 run_linters=0 shift ;; --neovim-only) run_vim_80_tests=0 run_vim_90_tests=0 + run_lua_tests=0 run_linters=0 shift ;; @@ -59,6 +62,7 @@ while [ $# -ne 0 ]; do run_neovim_08_tests=0 run_vim_80_tests=0 run_vim_90_tests=0 + run_lua_tests=0 run_linters=0 shift ;; @@ -66,12 +70,14 @@ while [ $# -ne 0 ]; do run_neovim_07_tests=0 run_vim_80_tests=0 run_vim_90_tests=0 + run_lua_tests=0 run_linters=0 shift ;; --vim-only) run_neovim_07_tests=0 run_neovim_08_tests=0 + run_lua_tests=0 run_linters=0 shift ;; @@ -79,6 +85,7 @@ while [ $# -ne 0 ]; do run_neovim_07_tests=0 run_neovim_08_tests=0 run_vim_90_tests=0 + run_lua_tests=0 run_linters=0 shift ;; @@ -86,6 +93,7 @@ while [ $# -ne 0 ]; do run_neovim_07_tests=0 run_neovim_08_tests=0 run_vim_80_tests=0 + run_lua_tests=0 run_linters=0 shift ;; @@ -94,6 +102,15 @@ while [ $# -ne 0 ]; do run_vim_90_tests=0 run_neovim_07_tests=0 run_neovim_08_tests=0 + run_lua_tests=0 + shift + ;; + --lua-only) + run_vim_80_tests=0 + run_vim_90_tests=0 + run_neovim_07_tests=0 + run_neovim_08_tests=0 + run_linters=0 shift ;; --fast) @@ -119,6 +136,7 @@ while [ $# -ne 0 ]; do echo ' --vim-only Run tests only for Vim' echo ' --vim-80-only Run tests only for Vim 8.2' echo ' --vim-90-only Run tests only for Vim 9.0' + echo ' --lua-only Run only Lua tests' echo ' --linters-only Run only Vint and custom checks' echo ' --fast Run only the fastest Vim and custom checks' echo ' --help Show this help text' @@ -147,6 +165,7 @@ if [ $# -ne 0 ]; then # Don't run other tools when targeting tests. run_linters=0 + run_lua_tests=0 fi # Delete .swp files in the test directory, which cause Vim 8 to hang. @@ -250,6 +269,13 @@ for vim in $("$DOCKER" run --rm "$DOCKER_RUN_IMAGE" ls /vim-build/bin | grep '^n fi done +if ((run_lua_tests)); then + echo "Starting Lua tests..." + file_number=$((file_number+1)) + test/script/run-lua-tests $quiet_flag > "$output_dir/$file_number" 2>&1 & + pid_list="$pid_list $!" +fi + if ((run_linters)); then echo "Starting Vint..." file_number=$((file_number+1)) diff --git a/test/lua/.luarc.json b/test/lua/.luarc.json index 98766310..e6c6668f 100644 --- a/test/lua/.luarc.json +++ b/test/lua/.luarc.json @@ -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 } diff --git a/test/lua/ale_diagnostics_spec.lua b/test/lua/ale_diagnostics_spec.lua new file mode 100644 index 00000000..ae2ad894 --- /dev/null +++ b/test/lua/ale_diagnostics_spec.lua @@ -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) diff --git a/test/lua/ale_env_spec.lua b/test/lua/ale_env_spec.lua new file mode 100644 index 00000000..1cceb5be --- /dev/null +++ b/test/lua/ale_env_spec.lua @@ -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) diff --git a/test/lua/ale_lsp_spec.lua b/test/lua/ale_lsp_spec.lua new file mode 100644 index 00000000..8f9b2974 --- /dev/null +++ b/test/lua/ale_lsp_spec.lua @@ -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) diff --git a/test/lua/ale_var_spec.lua b/test/lua/ale_var_spec.lua new file mode 100644 index 00000000..5c912724 --- /dev/null +++ b/test/lua/ale_var_spec.lua @@ -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) diff --git a/test/lua/windows_escaping_spec.lua b/test/lua/windows_escaping_spec.lua new file mode 100644 index 00000000..86f5f52e --- /dev/null +++ b/test/lua/windows_escaping_spec.lua @@ -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\'')) + 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\'^^\'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) diff --git a/test/script/run-lua-tests b/test/script/run-lua-tests new file mode 100755 index 00000000..ac538425 --- /dev/null +++ b/test/script/run-lua-tests @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e +set -u + +docker_flags=(--rm -v "$PWD:/testplugin" -v "$PWD/test:/home" -w /testplugin/test/lua "$DOCKER_RUN_IMAGE") + +quiet=0 + +while [ $# -ne 0 ]; do + case $1 in + -q) + quiet=1 + shift + ;; + --) + shift + break + ;; + -?*) + echo "Invalid argument: $1" 1>&2 + exit 1 + ;; + *) + break + ;; + esac +done + +function filter-busted-output() { + local hit_failure_line=0 + + while read -r; do + if ((quiet)); then + # If we're using the quiet flag, the filter out lines until we hit + # the first line with "Failure" and then print the rest. + if ((hit_failure_line)); then + echo "$REPLY" + elif [[ "$REPLY" = *'Failure'* ]]; then + hit_failure_line=1 + echo "$REPLY" + fi + else + echo "$REPLY" + fi + done +} + +exit_code=0 + +set -o pipefail +"$DOCKER" run -a stdout "${docker_flags[@]}" /usr/bin/busted-5.1 \ + -m '../../lua/?.lua;../../lua/?/init.lua' . \ + --output utfTerminal | filter-busted-output || exit_code=$? +set +o pipefail + +exit "$exit_code"