diff --git a/lua/neotest-busted/config.lua b/lua/neotest-busted/config.lua new file mode 100644 index 0000000..89b8c94 --- /dev/null +++ b/lua/neotest-busted/config.lua @@ -0,0 +1,112 @@ +local config = {} + +---@type neotest-busted.Config +local default_config = { + busted_command = nil, + busted_args = nil, + busted_paths = nil, + busted_cpaths = nil, + minimal_init = nil, +} + +local _user_config = default_config + +---@param value any +---@return boolean +local function is_non_empty_string(value) + return value == nil or (type(value) == "string" and #value > 0) +end + +---@param value any +---@return boolean +---@return string? +local function is_optional_string_list(value) + if value == nil then + return true + end + + if not vim.tbl_islist(value) then + return false, "must be a list-like table" + end + + for idx, item in ipairs(value) do + if type(item) ~= "string" then + return false, "item at index " .. tostring(idx) + end + end + + return true +end + +--- Validate a config +---@param _config neotest-busted.Config +---@return boolean +---@return any? +function config.validate(_config) + -- stylua: ignore start + local ok, error = pcall(vim.validate, { + busted_command = { + _config.busted_command, + is_non_empty_string, + "optional non-empty string" + }, + busted_args = { + _config.busted_args, + is_optional_string_list, + "an optional string list", + }, + busted_paths = { + _config.busted_paths, + is_optional_string_list, + "an optional string list", + }, + busted_cpaths = { + _config.busted_cpaths, + is_optional_string_list, + "an optional string list", + }, + minimal_init = { + _config.minimal_init, + is_non_empty_string, + "optional non-empty string" + }, + }) + -- stylua: ignore end + + if not ok then + return ok, error + end + + if type(_config.busted_command) == "string" then + if vim.fn.executable(_config.busted_command) == 0 then + return false, "busted command in configuration is not executable" + end + end + + return ok +end + +---@param user_config table? +---@return boolean +---@return any? +function config.configure(user_config) + _user_config = vim.tbl_deep_extend("keep", user_config or {}, default_config) + + local ok, error = config.validate(_user_config) + + if not ok then + vim.api.nvim_echo({ + { "[neotest-busted]: ", "ErrorMsg" }, + { "Invalid config: " }, + { error }, + }, true, {}) + end + + return ok, error +end + +return setmetatable(config, { + __index = function(_, key) + return _user_config[key] + end, +}) diff --git a/lua/neotest-busted/health.lua b/lua/neotest-busted/health.lua new file mode 100644 index 0000000..3bfb363 --- /dev/null +++ b/lua/neotest-busted/health.lua @@ -0,0 +1,58 @@ +local health = {} + +local adapter = require("neotest-busted") +local config = require("neotest-busted.config") + +local min_neovim_version = "0.9.0" + +---@param module_name string +local function check_module_installed(module_name) + local installed, _ = pcall(require, module_name) + + if installed then + vim.health.report_ok(("`%s` is installed"):format(module_name)) + else + vim.health.report_error(("`%s` is not installed"):format(module_name)) + end +end + +function health.check() + vim.health.report_start("neotest-busted") + + if vim.fn.has("nvim-" .. min_neovim_version) == 1 then + vim.health.report_ok(("has neovim %s+"):format(min_neovim_version)) + else + vim.health.report_error("neotest-busted requires at least neovim " .. min_neovim_version) + end + + -- NOTE: We cannot check the neotest version because it isn't avertised as + -- part of its public api + check_module_installed("neotest") + check_module_installed("nio") + + local ok, error = config.validate(config) + + if ok then + vim.health.report_ok("found no errors in config") + else + vim.health.report_error("config has errors: " .. error) + end + + local busted = adapter.find_busted_command(true) + + if busted then + vim.health.report_ok( + ("found `busted` (type: %s) at\n%s"):format( + busted.type, + vim.loop.fs_realpath(busted.command) + ) + ) + else + vim.health.report_warn( + "could not find busted executable globally or in user home folder", + "if not already installed locally, please install busted using luarocks (https://luarocks.org/)" + ) + end +end + +return health diff --git a/lua/neotest-busted/init.lua b/lua/neotest-busted/init.lua index fd7d71e..fc6fea2 100644 --- a/lua/neotest-busted/init.lua +++ b/lua/neotest-busted/init.lua @@ -1,3 +1,4 @@ +local config = require("neotest-busted.config") local util = require("neotest-busted.util") local async = require("neotest.async") @@ -6,54 +7,53 @@ local logger = require("neotest.logging") local types = require("neotest.types") local function get_strategy_config(strategy) - local config = { + local strategy_configs = { dap = nil, -- TODO: Add dap config } - if config[strategy] then - return config[strategy]() + if strategy_configs[strategy] then + return strategy_configs[strategy]() end end ----@type neotest-busted.Config -local config = { - busted_command = nil, - busted_args = nil, - busted_paths = nil, - busted_cpaths = nil, - minimal_init = nil, -} +---@type neotest.Adapter +local BustedNeotestAdapter = { name = "neotest-busted" } --- Find busted command and additional paths +---@param ignore_local? boolean ---@return neotest-busted.BustedCommand? -local function find_busted_command() +function BustedNeotestAdapter.find_busted_command(ignore_local) if config.busted_command and #config.busted_command > 0 then logger.debug("Using busted command from config") return { + type = "config", command = config.busted_command, lua_paths = {}, lua_cpaths = {}, } end - -- Try to find a directory-local busted executable - local local_globs = - util.glob(util.create_path("lua_modules", "lib", "luarocks", "**", "bin", "busted")) - - if #local_globs > 0 then - logger.debug("Using project-local busted executable") - - return { - command = local_globs[1], - lua_paths = { - util.create_path("lua_modules", "share", "lua", "5.1", "?.lua"), - util.create_path("lua_modules", "share", "lua", "5.1", "?", "init.lua"), - }, - lua_cpaths = { - util.create_path("lua_modules", "lib", "lua", "5.1", "?.so"), - util.create_path("lua_modules", "lib", "lua", "5.1", "?", "?.so"), - }, - } + if not ignore_local then + -- Try to find a directory-local busted executable + local local_globs = + util.glob(util.create_path("lua_modules", "lib", "luarocks", "**", "bin", "busted")) + + if #local_globs > 0 then + logger.debug("Using project-local busted executable") + + return { + type = "project", + command = local_globs[1], + lua_paths = { + util.create_path("lua_modules", "share", "lua", "5.1", "?.lua"), + util.create_path("lua_modules", "share", "lua", "5.1", "?", "init.lua"), + }, + lua_cpaths = { + util.create_path("lua_modules", "lib", "lua", "5.1", "?.so"), + util.create_path("lua_modules", "lib", "lua", "5.1", "?", "?.so"), + }, + } + end end -- Try to find a local (user home directory) busted executable @@ -64,6 +64,7 @@ local function find_busted_command() logger.debug("Using local (~/.luarocks) busted executable") return { + type = "local", command = user_globs[1], lua_paths = { util.create_path("~", ".luarocks", "share", "lua", "5.1", "?.lua"), @@ -81,6 +82,7 @@ local function find_busted_command() logger.debug("Using global busted executable") return { + type = "global", command = "busted", lua_paths = {}, lua_cpaths = {}, @@ -144,7 +146,7 @@ end ---@param filters string[] ---@return neotest-busted.BustedCommand? local function create_busted_command(results_path, paths, filters) - local busted = find_busted_command() + local busted = BustedNeotestAdapter.find_busted_command() if not busted then return @@ -218,9 +220,6 @@ local function create_busted_command(results_path, paths, filters) } end ----@type neotest.Adapter -local BustedNeotestAdapter = { name = "neotest-busted" } - BustedNeotestAdapter.root = lib.files.match_root_pattern(".busted", ".luarocks", "lua_modules", "*.rockspec") @@ -493,20 +492,7 @@ end setmetatable(BustedNeotestAdapter, { ---@param user_config neotest-busted.Config? __call = function(_, user_config) - local _user_config = user_config or {} - - if type(_user_config.busted_command) == "string" and #_user_config.busted_command > 0 then - if vim.fn.executable(_user_config.busted_command) == 0 then - vim.notify( - ("Busted command in configuration is not executable: '%s'"):format( - _user_config.busted_command - ), - vim.log.levels.ERROR - ) - end - end - - config = vim.tbl_extend("force", config, _user_config) + config.configure(user_config) return BustedNeotestAdapter end, diff --git a/lua/neotest-busted/types.lua b/lua/neotest-busted/types.lua index fd08a13..e718bb6 100644 --- a/lua/neotest-busted/types.lua +++ b/lua/neotest-busted/types.lua @@ -6,7 +6,8 @@ ---@field minimal_init string? ---@class neotest-busted.BustedCommand ----@field command string[] +---@field type "config" | "project" | "user" | "global" +---@field command string ---@field lua_paths string[] ---@field lua_cpaths string[] diff --git a/tests/adapter_build_spec_spec.lua b/tests/adapter_build_spec_spec.lua index b2879a0..dd8a4fe 100644 --- a/tests/adapter_build_spec_spec.lua +++ b/tests/adapter_build_spec_spec.lua @@ -6,6 +6,14 @@ local stub = require("luassert.stub") local async = _async.tests describe("adapter.build_spec", function() + before_each(function() + stub(vim.api, "nvim_echo") + end) + + after_each(function() + vim.api.nvim_echo:revert() + end) + local function assert_spec_command(spec_command, items) assert.are.same(#spec_command, #items) diff --git a/tests/config_spec.lua b/tests/config_spec.lua new file mode 100644 index 0000000..cdb8736 --- /dev/null +++ b/tests/config_spec.lua @@ -0,0 +1,92 @@ +local config = require("neotest-busted.config") +local stub = require("luassert.stub") + +describe("config", function() + it("handles invalid configs", function() + local non_empty_string = "optional non-empty string" + local optional_string_list = "an optional string list" + + local invalid_config_tests = { + { + config = { busted_command = 1 }, + error_message = non_empty_string, + }, + { + config = { busted_command = "" }, + error_message = non_empty_string, + }, + { + config = { busted_args = { 1, 2, 3 } }, + error_message = optional_string_list, + }, + { + config = { busted_args = 1 }, + error_message = optional_string_list, + }, + { + config = { busted_paths = { 1, 2, 3 } }, + error_message = optional_string_list, + }, + { + config = { busted_paths = 1 }, + error_message = optional_string_list, + }, + { + config = { busted_cpaths = { 1, 2, 3 } }, + error_message = optional_string_list, + }, + { + config = { busted_cpaths = 1 }, + error_message = optional_string_list, + }, + { + config = { minimal_init = false }, + error_message = non_empty_string, + }, + { + config = { minimal_init = "" }, + error_message = non_empty_string, + }, + } + + stub(vim.api, "nvim_echo") + + for _, invalid_config_test in ipairs(invalid_config_tests) do + local ok, error = config.configure(invalid_config_test.config) + + if ok then + vim.print(invalid_config_test) + end + + assert.is_false(ok) + + assert.stub(vim.api.nvim_echo).was.called_with({ + { "[neotest-busted]: ", "ErrorMsg" }, + { "Invalid config: " }, + { error }, + }, true, {}) + end + + vim.api.nvim_echo:revert() + end) + + it("throws no errors for a valid config", function() + local ok = config.configure({ + busted_command = nil, + busted_args = { "--shuffle-tests" }, + busted_paths = { "some/path" }, + busted_cpaths = {}, + minimal_init = "some_init_file.lua", + }) + + assert.is_true(ok) + end) + + it("throws no errors for empty user config", function() + assert.is_true(config.configure({})) + end) + + it("throws no errors for no user config", function() + assert.is_true(config.configure()) + end) +end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 435a737..8dadf0f 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,8 +1,7 @@ vim.opt.rtp:append(".") vim.opt.rtp:append("~/.vim-plug/plenary.nvim") vim.opt.rtp:append("~/.vim-plug/neotest") +vim.opt.rtp:append("~/.vim-plug/nvim-nio") vim.opt.rtp:append("~/.vim-plug/nvim-treesitter") -vim.cmd.runtime({ "lua/neotest-busted/init.lua", bang = false }) + vim.cmd.runtime({ "plugin/plenary.vim", bang = false }) -vim.cmd.runtime({ "plugin/neotest.lua", bang = false }) -vim.cmd.runtime({ "plugin/nvim-treesitter.lua", bang = false }) diff --git a/tests/util_spec.lua b/tests/util_spec.lua index e13b3a2..b5f8447 100644 --- a/tests/util_spec.lua +++ b/tests/util_spec.lua @@ -24,6 +24,8 @@ describe("util", function() assert.are.same(util.glob(path), { "lua/neotest-busted/async.lua", + "lua/neotest-busted/config.lua", + "lua/neotest-busted/health.lua", "lua/neotest-busted/init.lua", "lua/neotest-busted/output_handler.lua", "lua/neotest-busted/types.lua", @@ -34,7 +36,7 @@ describe("util", function() describe("create_package_path_argument", function() it("creates package path argument", function() - local args = { "some/path", "some\\other/path" } + local args = { "some/path", "some/other/path" } assert.are.same(util.create_package_path_argument("package.path", args), { "-c",