diff options
| author | William Boman <william@redwill.se> | 2022-03-26 13:41:50 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-03-26 13:41:50 +0100 |
| commit | 212d17a039da449043b67529c29851db37acc236 (patch) | |
| tree | 38411b14487895cef0d7648e198b79fd28793fe6 | |
| parent | run autogen_metadata.lua (diff) | |
| download | mason-212d17a039da449043b67529c29851db37acc236.tar mason-212d17a039da449043b67529c29851db37acc236.tar.gz mason-212d17a039da449043b67529c29851db37acc236.tar.bz2 mason-212d17a039da449043b67529c29851db37acc236.tar.lz mason-212d17a039da449043b67529c29851db37acc236.tar.xz mason-212d17a039da449043b67529c29851db37acc236.tar.zst mason-212d17a039da449043b67529c29851db37acc236.zip | |
add async managers (#536)
52 files changed, 3271 insertions, 687 deletions
diff --git a/lua/nvim-lsp-installer/core/async/init.lua b/lua/nvim-lsp-installer/core/async/init.lua index 6ecb5a61..0c278373 100644 --- a/lua/nvim-lsp-installer/core/async/init.lua +++ b/lua/nvim-lsp-installer/core/async/init.lua @@ -91,8 +91,8 @@ local function new_execution_context(suspend_fn, callback, ...) end end -exports.run = function(suspend_fn, callback) - return new_execution_context(suspend_fn, callback) +exports.run = function(suspend_fn, callback, ...) + return new_execution_context(suspend_fn, callback, ...) end exports.scope = function(suspend_fn) diff --git a/lua/nvim-lsp-installer/core/async/uv.lua b/lua/nvim-lsp-installer/core/async/uv.lua new file mode 100644 index 00000000..a2249be9 --- /dev/null +++ b/lua/nvim-lsp-installer/core/async/uv.lua @@ -0,0 +1,55 @@ +local a = require "nvim-lsp-installer.core.async" + +---@type Record<UvMethod, async fun(...)> +local M = setmetatable({}, { + __index = function(_, method) + ---@async + return function(...) + local err, result = a.promisify(vim.loop[method])(...) + if err then + error(err, 2) + end + return result + end + end, +}) + +return M + +---@alias UvMethod +---| '"fs_close"' +---| '"fs_open"' +---| '"fs_read"' +---| '"fs_unlink"' +---| '"fs_write"' +---| '"fs_mkdir"' +---| '"fs_mkdtemp"' +---| '"fs_mkstemp"' +---| '"fs_rmdir"' +---| '"fs_scandir"' +---| '"fs_stat"' +---| '"fs_fstat"' +---| '"fs_lstat"' +---| '"fs_rename"' +---| '"fs_fsync"' +---| '"fs_fdatasync"' +---| '"fs_ftruncate"' +---| '"fs_sendfile"' +---| '"fs_access"' +---| '"fs_chmod"' +---| '"fs_fchmod"' +---| '"fs_utime"' +---| '"fs_futime"' +---| '"fs_lutime"' +---| '"fs_link"' +---| '"fs_symlink"' +---| '"fs_readlink"' +---| '"fs_realpath"' +---| '"fs_chown"' +---| '"fs_fchown"' +---| '"fs_lchown"' +---| '"fs_copyfile"' +---| '"fs_opendir"' +---| '"fs_readdir"' +---| '"fs_closedir"' +---| '"fs_statfs"' diff --git a/lua/nvim-lsp-installer/core/context.lua b/lua/nvim-lsp-installer/core/context.lua new file mode 100644 index 00000000..128ad417 --- /dev/null +++ b/lua/nvim-lsp-installer/core/context.lua @@ -0,0 +1,167 @@ +local spawn = require "nvim-lsp-installer.core.spawn" +local log = require "nvim-lsp-installer.log" +local Optional = require "nvim-lsp-installer.core.optional" +local fs = require "nvim-lsp-installer.core.fs" +local settings = require "nvim-lsp-installer.settings" +local Result = require "nvim-lsp-installer.core.result" +local path = require "nvim-lsp-installer.path" +local platform = require "nvim-lsp-installer.platform" + +---@class ContextualSpawn +---@field cwd CwdManager +---@field stdio_sink StdioSink +local ContextualSpawn = {} + +---@param cwd CwdManager +---@param stdio_sink StdioSink +function ContextualSpawn.new(cwd, stdio_sink) + return setmetatable({ cwd = cwd, stdio_sink = stdio_sink }, ContextualSpawn) +end +function ContextualSpawn.__index(self, cmd) + return function(args) + args.cwd = args.cwd or self.cwd:get() + args.stdio_sink = args.stdio_sink or self.stdio_sink + -- We get_or_throw() here for convenience reasons. + -- Almost every time spawn is called via context we want the command to succeed. + return spawn[cmd](args):get_or_throw() + end +end + +---@class ContextualFs +---@field private cwd CwdManager +local ContextualFs = {} +ContextualFs.__index = ContextualFs + +---@param cwd CwdManager +function ContextualFs.new(cwd) + return setmetatable({ cwd = cwd }, ContextualFs) +end + +---@async +---@param rel_path string @The relative path from the current working directory to the file to append. +---@param contents string +function ContextualFs:append_file(rel_path, contents) + return fs.append_file(path.concat { self.cwd:get(), rel_path }, contents) +end + +---@async +---@param rel_path string @The relative path from the current working directory. +function ContextualFs:file_exists(rel_path) + return fs.file_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string @The relative path from the current working directory. +function ContextualFs:dir_exists(rel_path) + return fs.dir_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@class CwdManager +local CwdManager = {} +CwdManager.__index = CwdManager +function CwdManager.new(cwd) + return setmetatable({ cwd = cwd }, CwdManager) +end +function CwdManager:get() + return self.cwd +end +function CwdManager:set(new_cwd) + self.cwd = new_cwd +end + +---@class InstallContext +---@field public receipt InstallReceiptBuilder +---@field public requested_version Optional +---@field public fs ContextualFs +---@field public spawn JobSpawn +---@field private cwd_manager CwdManager +---@field private destination_dir string +local InstallContext = {} +InstallContext.__index = InstallContext + +function InstallContext.new(opts) + local cwd_manager = CwdManager.new(opts.cwd) + return setmetatable({ + cwd_manager = cwd_manager, + spawn = ContextualSpawn.new(cwd_manager, opts.stdio_sink), + fs = ContextualFs.new(cwd_manager), + receipt = opts.receipt, + requested_version = opts.requested_version, + destination_dir = opts.destination_dir, + }, InstallContext) +end + +---@deprecated +---@param ctx ServerInstallContext +---@param destination_dir string +function InstallContext.from_server_context(ctx, destination_dir) + return InstallContext.new { + cwd = ctx.install_dir, + receipt = ctx.receipt, + stdio_sink = ctx.stdio_sink, + requested_version = Optional.of_nilable(ctx.requested_server_version), + destination_dir = destination_dir, + } +end + +function InstallContext:cwd() + return self.cwd_manager:get() +end + +---@param new_cwd string @The new cwd (absolute path). +function InstallContext:set_cwd(new_cwd) + self + :ensure_path_ownership(new_cwd) + :map(function(p) + self.cwd_manager:set(p) + return p + end) + :get_or_throw() +end + +---@param abs_path string +function InstallContext:ensure_path_ownership(abs_path) + if path.is_subdirectory(self:cwd_manager(), abs_path) or self.destination_dir == abs_path then + return Result.success(abs_path) + else + return Result.failure( + ("Path %q is outside of current path ownership (%q)."):format(abs_path, settings.current.install_root_dir) + ) + end +end + +---@async +function InstallContext:promote_cwd() + local cwd = self:cwd() + if self.destination_dir == cwd then + log.fmt_debug("cwd %s is already promoted (at %s)", cwd, self.destination_dir) + return Result.success "Current working dir is already in destination." + end + log.fmt_debug("Promoting cwd %s to %s", cwd, self.destination_dir) + return Result.run_catching(function() + -- 1. Remove destination dir, if it exists + if fs.dir_exists(self.destination_dir) then + fs.rmrf(self.destination_dir) + end + return self.destination_dir + end) + :map_catching(function(destination_dir) + -- 2. Prepare for renaming cwd to destination + if platform.is_unix then + -- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist. + fs.mkdir(destination_dir) + end + return destination_dir + end) + :map_catching(function(destination_dir) + -- 3. Move the cwd to the final installation directory + fs.rename(cwd, destination_dir) + return destination_dir + end) + :map_catching(function(destination_dir) + -- 4. Update cwd + self:set_cwd(destination_dir) + end) +end + +return InstallContext diff --git a/lua/nvim-lsp-installer/core/fs.lua b/lua/nvim-lsp-installer/core/fs.lua new file mode 100644 index 00000000..f3db813a --- /dev/null +++ b/lua/nvim-lsp-installer/core/fs.lua @@ -0,0 +1,68 @@ +local uv = require "nvim-lsp-installer.core.async.uv" +local log = require "nvim-lsp-installer.log" +local a = require "nvim-lsp-installer.core.async" + +local M = {} + +---@async +---@param path string +---@param contents string +function M.append_file(path, contents) + local fd = uv.fs_open(path, "a", 438) + uv.fs_write(fd, contents, -1) + uv.fs_close(fd) +end + +---@async +---@param path string +function M.file_exists(path) + local ok, fd = pcall(uv.fs_open, path, "r", 438) + if not ok then + return false + end + local fstat = uv.fs_fstat(fd) + uv.fs_close(fd) + return fstat.type == "file" +end + +---@async +---@param path string +function M.dir_exists(path) + local ok, fd = pcall(uv.fs_open, path, "r", 438) + if not ok then + return false + end + local fstat = uv.fs_fstat(fd) + uv.fs_close(fd) + return fstat.type == "directory" +end + +---@async +---@param path string +function M.rmrf(path) + log.debug("fs: rmrf", path) + if vim.in_fast_event() then + a.scheduler() + end + if vim.fn.delete(path, "rf") ~= 0 then + log.debug "fs: rmrf failed" + error(("rmrf: Could not remove directory %q."):format(path)) + end +end + +---@async +---@param path string +function M.mkdir(path) + log.debug("fs: mkdir", path) + uv.fs_mkdir(path, 493) -- 493(10) == 755(8) +end + +---@async +---@param path string +---@param new_path string +function M.rename(path, new_path) + log.debug("fs: rename", path, new_path) + uv.fs_rename(path, new_path) +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/cargo/init.lua b/lua/nvim-lsp-installer/core/managers/cargo/init.lua new file mode 100644 index 00000000..29c07868 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/cargo/init.lua @@ -0,0 +1,100 @@ +local process = require "nvim-lsp-installer.process" +local path = require "nvim-lsp-installer.path" +local spawn = require "nvim-lsp-installer.core.spawn" +local a = require "nvim-lsp-installer.core.async" +local Optional = require "nvim-lsp-installer.core.optional" +local crates = require "nvim-lsp-installer.core.clients.crates" +local Result = require "nvim-lsp-installer.core.result" + +local fetch_crate = a.promisify(crates.fetch_crate, true) + +local M = {} + +---@param crate string The crate to install. +---@param opts {git:boolean, features:string|nil} +function M.crate(crate, opts) + ---@async + ---@param ctx InstallContext + return function(ctx) + opts = opts or {} + ctx.requested_version:if_present(function() + assert(not opts.git, "Providing a version when installing a git crate is not allowed.") + end) + + ctx.receipt:with_primary_source(ctx.receipt.cargo(crate)) + + ctx.spawn.cargo { + "install", + "--root", + ".", + "--locked", + ctx.requested_version + :map(function(version) + return { "--version", version } + end) + :or_else(vim.NIL), + opts.features and { "--features", opts.features } or vim.NIL, + opts.git and { "--git", crate } or crate, + } + end +end + +---@param output string @The `cargo install --list` output. +---@return Record<string, string> @Key is the crate name, value is its version. +function M.parse_installed_crates(output) + local installed_crates = {} + for _, line in ipairs(vim.split(output, "\n")) do + local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]" + if name and version then + installed_crates[name] = version + end + end + return installed_crates +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + local installed_version = M.get_installed_primary_package_version(receipt, install_dir):get_or_throw() + + local response = fetch_crate(receipt.primary_source.package) + if installed_version ~= response.crate.max_stable_version then + return Result.success { + name = receipt.primary_source.package, + current_version = installed_version, + latest_version = response.crate.max_stable_version, + } + else + return Result.failure "Primary package is not outdated." + end +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + return spawn.cargo({ + "install", + "--list", + "--root", + ".", + cwd = install_dir, + }):map_catching(function(result) + local installed_crates = M.parse_installed_crates(result.stdout) + if vim.in_fast_event() then + a.scheduler() -- needed because vim.fn.* call + end + local package = vim.fn.fnamemodify(receipt.primary_source.package, ":t") + return Optional.of_nilable(installed_crates[package]):or_else_throw "Failed to find cargo package version." + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + PATH = process.extend_path { path.concat { install_dir, "bin" } }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/composer/init.lua b/lua/nvim-lsp-installer/core/managers/composer/init.lua new file mode 100644 index 00000000..9a1e2dd4 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/composer/init.lua @@ -0,0 +1,106 @@ +local Data = require "nvim-lsp-installer.data" +local process = require "nvim-lsp-installer.process" +local path = require "nvim-lsp-installer.path" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" +local Optional = require "nvim-lsp-installer.core.optional" + +local list_copy, list_find_first = Data.list_copy, Data.list_find_first + +local M = {} + +---@param packages string[] The composer packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. +function M.require(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local pkgs = list_copy(packages) + + ctx.receipt:with_primary_source(ctx.receipt.composer(pkgs[1])) + for i = 2, #pkgs do + ctx.receipt:with_secondary_source(ctx.receipt.composer(pkgs[i])) + end + + if not ctx.fs:file_exists "composer.json" then + ctx.spawn.composer { "init", "--no-interaction", "--stability=stable" } + end + + ctx.requested_version:if_present(function(version) + pkgs[1] = ("%s:%s"):format(pkgs[1], version) + end) + + ctx.spawn.composer { "require", pkgs } + end +end + +function M.install() + ---@async + ---@param ctx InstallContext + return function(ctx) + ctx.spawn.composer { + "install", + "--no-interaction", + "--no-dev", + "--optimize-autoloader", + "--classmap-authoritative", + } + end +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + if receipt.primary_source.type ~= "composer" then + return Result.failure "Receipt does not have a primary source of type composer" + end + return spawn.composer({ + "outdated", + "--no-interaction", + "--format=json", + cwd = install_dir, + }):map_catching(function(result) + local outdated_packages = vim.json.decode(result.stdout) + local outdated_package = list_find_first(outdated_packages.installed, function(package) + return package.name == receipt.primary_source.package + end) + return Optional.of_nilable(outdated_package) + :map(function(package) + if package.version ~= package.latest then + return { + name = package.name, + current_version = package.version, + latest_version = package.latest, + } + end + end) + :or_else_throw "Primary package is not outdated." + end) +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + if receipt.primary_source.type ~= "composer" then + return Result.failure "Receipt does not have a primary source of type composer" + end + return spawn.composer({ + "info", + "--format=json", + receipt.primary_source.package, + cwd = install_dir, + }):map_catching(function(result) + local info = vim.json.decode(result.stdout) + return info.versions[1] + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + PATH = process.extend_path { path.concat { install_dir, "vendor", "bin" } }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/dotnet/init.lua b/lua/nvim-lsp-installer/core/managers/dotnet/init.lua new file mode 100644 index 00000000..c862146f --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/dotnet/init.lua @@ -0,0 +1,31 @@ +local process = require "nvim-lsp-installer.process" +local M = {} + +---@param package string +function M.package(package) + ---@async + ---@param ctx InstallContext + return function(ctx) + ctx.receipt:with_primary_source(ctx.receipt.dotnet(package)) + ctx.spawn.dotnet { + "tool", + "update", + "--tool-path", + ".", + ctx.requested_version + :map(function(version) + return { "--version", version } + end) + :or_else(vim.NIL), + package, + } + end +end + +function M.env(root_dir) + return { + PATH = process.extend_path { root_dir }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/gem/init.lua b/lua/nvim-lsp-installer/core/managers/gem/init.lua new file mode 100644 index 00000000..7b030797 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/gem/init.lua @@ -0,0 +1,132 @@ +local Data = require "nvim-lsp-installer.data" +local process = require "nvim-lsp-installer.process" +local path = require "nvim-lsp-installer.path" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" +local Optional = require "nvim-lsp-installer.core.optional" + +local list_copy, list_find_first = Data.list_copy, Data.list_find_first + +local M = {} + +---@param packages string[] @The Gem packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. +function M.packages(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local pkgs = list_copy(packages or {}) + + ctx.receipt:with_primary_source(ctx.receipt.gem(pkgs[1])) + for i = 2, #pkgs do + ctx.receipt:with_secondary_source(ctx.receipt.gem(pkgs[i])) + end + + ctx.requested_version:if_present(function(version) + pkgs[1] = ("%s:%s"):format(pkgs[1], version) + end) + + ctx.spawn.gem { + "install", + "--no-user-install", + "--install-dir=.", + "--bindir=bin", + "--no-document", + pkgs, + } + end +end + +---@alias GemOutdatedPackage {name:string, current_version: string, latest_version: string} + +---Parses a string input like "package (0.1.0 < 0.2.0)" into its components +---@param outdated_gem string +---@return GemOutdatedPackage +function M.parse_outdated_gem(outdated_gem) + local package_name, version_expression = outdated_gem:match "^(.+) %((.+)%)" + if not package_name or not version_expression then + -- unparseable + return nil + end + local current_version, latest_version = unpack(vim.split(version_expression, "<")) + + ---@type GemOutdatedPackage + local outdated_package = { + name = vim.trim(package_name), + current_version = vim.trim(current_version), + latest_version = vim.trim(latest_version), + } + return outdated_package +end + +---Parses the stdout of the `gem list` command into a Record<package_name, version> +---@param output string +function M.parse_gem_list_output(output) + ---@type Record<string, string> + local gem_versions = {} + for _, line in ipairs(vim.split(output, "\n")) do + local gem_package, version = line:match "^(%S+) %((%S+)%)$" + if gem_package and version then + gem_versions[gem_package] = version + end + end + return gem_versions +end + +local function not_empty(s) + return s ~= nil and s ~= "" +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + if receipt.primary_source.type ~= "gem" then + return Result.failure "Receipt does not have a primary source of type gem" + end + return spawn.gem({ "outdated", cwd = install_dir, env = process.graft_env(M.env(install_dir)) }):map_catching( + function(result) + ---@type string[] + local lines = vim.split(result.stdout, "\n") + local outdated_gems = vim.tbl_map(M.parse_outdated_gem, vim.tbl_filter(not_empty, lines)) + + local outdated_gem = list_find_first(outdated_gems, function(gem) + return gem.name == receipt.primary_source.package and gem.current_version ~= gem.latest_version + end) + + return Optional.of_nilable(outdated_gem) + :map(function(gem) + return { + name = receipt.primary_source.package, + current_version = assert(gem.current_version), + latest_version = assert(gem.latest_version), + } + end) + :or_else_throw "Primary package is not outdated." + end + ) +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + return spawn.gem({ + "list", + cwd = install_dir, + env = process.graft_env(M.env(install_dir)), + }):map_catching(function(result) + local gems = M.parse_gem_list_output(result.stdout) + return Optional.of_nilable(gems[receipt.primary_source.package]):or_else_throw "Failed to find gem package version." + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + GEM_HOME = install_dir, + GEM_PATH = install_dir, + PATH = process.extend_path { path.concat { install_dir, "bin" } }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/git/init.lua b/lua/nvim-lsp-installer/core/managers/git/init.lua new file mode 100644 index 00000000..bdd79917 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/git/init.lua @@ -0,0 +1,54 @@ +local spawn = require "nvim-lsp-installer.core.spawn" +local Result = require "nvim-lsp-installer.core.result" +local M = {} + +---@param opts {[1]:string} @The first item in the table is the repository to clone. +function M.clone(opts) + ---@async + ---@param ctx InstallContext + return function(ctx) + local repo = assert(opts[1], "No git URL provided.") + ctx.spawn.git { "clone", "--depth", "1", repo, "." } + ctx.requested_version:if_present(function(version) + ctx.spawn.git { "fetch", "--depth", "1", "origin", version } + ctx.spawn.git { "checkout", "FETCH_HEAD" } + end) + ctx.receipt:with_primary_source(ctx.receipt.git_remote(repo)) + end +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_git_clone(receipt, install_dir) + if receipt.primary_source.type ~= "git" then + return Result.failure "Receipt does not have a primary source of type git" + end + return spawn.git({ "fetch", "origin", "HEAD", cwd = install_dir }):map_catching(function() + local result = spawn.git({ "rev-parse", "FETCH_HEAD", "HEAD", cwd = install_dir }):get_or_throw() + local remote_head, local_head = unpack(vim.split(result.stdout, "\n")) + if remote_head == local_head then + error("Git clone is up to date.", 2) + end + return { + name = receipt.primary_source.remote, + current_version = assert(local_head), + latest_version = assert(remote_head), + } + end) +end + +---@async +---@param install_dir string +function M.get_installed_revision(install_dir) + return spawn.git({ + "rev-parse", + "--short", + "HEAD", + cwd = install_dir, + }):map_catching(function(result) + return assert(vim.trim(result.stdout)) + end) +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/go/init.lua b/lua/nvim-lsp-installer/core/managers/go/init.lua new file mode 100644 index 00000000..167cfff4 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/go/init.lua @@ -0,0 +1,109 @@ +local process = require "nvim-lsp-installer.process" +local spawn = require "nvim-lsp-installer.core.spawn" +local a = require "nvim-lsp-installer.core.async" +local Optional = require "nvim-lsp-installer.core.optional" + +local M = {} + +---@param packages string[] The Go packages to install. The first item in this list will be the recipient of the server version, should the user request a specific one. +function M.packages(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local env = process.graft_env { + GOBIN = ctx.cwd(), + } + -- Install the head package + do + local head_package = packages[1] + ctx.receipt:with_primary_source(ctx.receipt.go(head_package)) + local version = ctx.requested_version:or_else "latest" + ctx.spawn.go { + "install", + "-v", + ("%s@%s"):format(head_package, version), + env = env, + } + end + + -- Install secondary packages + for i = 2, #packages do + local package = packages[i] + ctx.receipt:with_secondary_source(ctx.receipt.go(package)) + ctx.spawn.go { "install", "-v", ("%s@latest"):format(package), env = env } + end + end +end + +---@param output string @The output from `go version -m` command. +function M.parse_mod_version_output(output) + ---@type {path: string[], mod: string[], dep: string[], build: string[]} + local result = {} + local lines = vim.split(output, "\n") + for _, line in ipairs { unpack(lines, 2) } do + local type, id, value = unpack(vim.split(line, "%s+", { trimempty = true })) + if type and id then + result[type] = result[type] or {} + result[type][id] = value or "" + end + end + return result +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + if vim.in_fast_event() then + a.scheduler() + end + -- trims e.g. golang.org/x/tools/gopls to gopls + local executable = vim.fn.fnamemodify(receipt.primary_source.package, ":t") + return spawn.go({ + "version", + "-m", + executable, + cwd = install_dir, + }):map_catching(function(result) + local parsed_output = M.parse_mod_version_output(result.stdout) + return Optional.of_nilable(parsed_output.mod[receipt.primary_source.package]):or_else_throw "Failed to parse mod version" + end) +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + return spawn.go({ + "list", + "-json", + "-m", + "-versions", + receipt.primary_source.package, + cwd = install_dir, + }):map_catching(function(result) + ---@type {Path: string, Versions: string[]} + local output = vim.json.decode(result.stdout) + return Optional.of_nilable(output.Versions[#output.Versions]) + :map(function(latest_version) + local installed_version = M.get_installed_primary_package_version(receipt, install_dir):get_or_throw() + if installed_version ~= latest_version then + return { + name = receipt.primary_source.package, + current_version = assert(installed_version), + latest_version = assert(latest_version), + } + end + end) + :or_else_throw "Primary package is not outdated." + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + PATH = process.extend_path { install_dir }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/npm/init.lua b/lua/nvim-lsp-installer/core/managers/npm/init.lua new file mode 100644 index 00000000..c3be01da --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/npm/init.lua @@ -0,0 +1,129 @@ +local Data = require "nvim-lsp-installer.data" +local spawn = require "nvim-lsp-installer.core.spawn" +local Optional = require "nvim-lsp-installer.core.optional" +local Result = require "nvim-lsp-installer.core.result" +local process = require "nvim-lsp-installer.process" +local path = require "nvim-lsp-installer.path" + +local list_copy = Data.list_copy + +local M = {} + +---@async +---@param ctx InstallContext +local function ensure_npm_root(ctx) + if not (ctx.fs:dir_exists "node_modules" or ctx.fs:file_exists "package.json") then + -- Create a package.json to set a boundary for where npm installs packages. + ctx.spawn.npm { "init", "--yes", "--scope=lsp-installer" } + end +end + +---@param packages string[] @The npm packages to install. The first item in this list will be the recipient of the requested version, if set. +function M.packages(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local pkgs = list_copy(packages) + + -- Use global-style. The reasons for this are: + -- a) To avoid polluting the executables (aka bin-links) that npm creates. + -- b) The installation is, after all, more similar to a "global" installation. We don't really gain + -- any of the benefits of not using global style (e.g., deduping the dependency tree). + -- + -- We write to .npmrc manually instead of going through npm because managing a local .npmrc file + -- is a bit unreliable across npm versions (especially <7), so we take extra measures to avoid + -- inadvertently polluting global npm config. + ctx.fs:append_file(".npmrc", "global-style=true") + + ctx.receipt:with_primary_source(ctx.receipt.npm(pkgs[1])) + for i = 2, #pkgs do + ctx.receipt:with_secondary_source(ctx.receipt.npm(pkgs[i])) + end + + ctx.requested_version:if_present(function(version) + pkgs[1] = ("%s@%s"):format(pkgs[1], version) + end) + + M.install(pkgs)(ctx) + end +end + +---@param packages string[] @The npm packages to install. +function M.install(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + ensure_npm_root(ctx) + ctx.spawn.npm { "install", packages } + end +end + +---@param exec_args string[] @The arguments to pass to npm exec. +function M.exec(exec_args) + ---@async + ---@param ctx InstallContext + return function(ctx) + ctx.spawn.npm { "exec", "--yes", exec_args } + end +end + +---@param script string @The npm script to run. +function M.run(script) + ---@async + ---@param ctx InstallContext + return function(ctx) + ctx.spawn.npm { "run", script } + end +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + if receipt.primary_source.type ~= "npm" then + return Result.failure "Receipt does not have a primary source of type npm" + end + return spawn.npm({ "ls", "--json", cwd = install_dir }):map_catching(function(result) + local npm_packages = vim.json.decode(result.stdout) + return npm_packages.dependencies[receipt.primary_source.package].version + end) +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + if receipt.primary_source.type ~= "npm" then + return Result.failure "Receipt does not have a primary source of type npm" + end + local primary_package = receipt.primary_source.package + local npm_outdated = spawn.npm { "outdated", "--json", primary_package, cwd = install_dir } + if npm_outdated:is_success() then + return Result.failure "Primary package is not outdated." + end + return npm_outdated:recover_catching(function(result) + assert(result.exit_code == 1, "Expected npm outdated to return exit code 1.") + local data = vim.json.decode(result.stdout) + + return Optional.of_nilable(data[primary_package]) + :map(function(outdated_package) + if outdated_package.current ~= outdated_package.latest then + return { + name = primary_package, + current_version = assert(outdated_package.current), + latest_version = assert(outdated_package.latest), + } + end + end) + :or_else_throw() + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + PATH = process.extend_path { path.concat { install_dir, "node_modules", ".bin" } }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/opam/init.lua b/lua/nvim-lsp-installer/core/managers/opam/init.lua new file mode 100644 index 00000000..8fd60de7 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/opam/init.lua @@ -0,0 +1,41 @@ +local Data = require "nvim-lsp-installer.data" +local path = require "nvim-lsp-installer.path" +local process = require "nvim-lsp-installer.process" + +local M = {} + +local list_copy = Data.list_copy + +---@param packages string[] @The opam packages to install. The first item in this list will be the recipient of the requested version, if set. +function M.packages(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local pkgs = list_copy(packages) + + ctx.receipt:with_primary_source(ctx.receipt.opam(pkgs[1])) + for i = 2, #pkgs do + ctx.receipt:with_secondary_source(ctx.receipt.opam(pkgs[i])) + end + + ctx.requested_version:if_present(function(version) + pkgs[1] = ("%s.%s"):format(pkgs[1], version) + end) + + ctx.spawn.opam { + "install", + "--destdir=.", + "--yes", + "--verbose", + pkgs, + } + end +end + +function M.env(root_dir) + return { + PATH = process.extend_path { path.concat { root_dir, "bin" } }, + } +end + +return M diff --git a/lua/nvim-lsp-installer/core/managers/pip3/init.lua b/lua/nvim-lsp-installer/core/managers/pip3/init.lua new file mode 100644 index 00000000..61130a96 --- /dev/null +++ b/lua/nvim-lsp-installer/core/managers/pip3/init.lua @@ -0,0 +1,144 @@ +local Data = require "nvim-lsp-installer.data" +local settings = require "nvim-lsp-installer.settings" +local process = require "nvim-lsp-installer.process" +local path = require "nvim-lsp-installer.path" +local platform = require "nvim-lsp-installer.platform" +local Optional = require "nvim-lsp-installer.core.optional" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" + +local list_find_first, list_copy, list_not_nil = Data.list_find_first, Data.list_copy, Data.list_not_nil +local VENV_DIR = "venv" + +local M = {} + +---@param packages string[] @The pip packages to install. The first item in this list will be the recipient of the requested version, if set. +function M.packages(packages) + ---@async + ---@param ctx InstallContext + return function(ctx) + local pkgs = list_copy(packages) + + ctx.receipt:with_primary_source(ctx.receipt.pip3(pkgs[1])) + for i = 2, #pkgs do + ctx.receipt:with_secondary_source(ctx.receipt.pip3(pkgs[i])) + end + + ctx.requested_version:if_present(function(version) + pkgs[1] = ("%s==%s"):format(pkgs[1], version) + end) + + local executables = platform.is_win and list_not_nil(vim.g.python3_host_prog, "python", "python3") + or list_not_nil(vim.g.python3_host_prog, "python3", "python") + + -- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path. + ctx:promote_cwd():get_or_throw() + + -- Find first executable that manages to create venv + local executable = list_find_first(executables, function(executable) + return pcall(ctx.spawn[executable], { "-m", "venv", VENV_DIR }) + end) + + Optional.of_nilable(executable) + :if_present(function(python3) + ctx.spawn[python3] { + env = process.graft_env(M.env(ctx:cwd())), -- use venv env + "-m", + "pip", + "install", + "-U", + settings.current.pip.install_args, + pkgs, + } + end) + :or_else_throw "Unable to create python3 venv environment." + end +end + +---@param package string +---@return string +function M.normalize_package(package) + -- https://stackoverflow.com/a/60307740 + local s = package:gsub("%[.*%]", "") + return s +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + if receipt.primary_source.type ~= "pip3" then + return Result.failure "Receipt does not have a primary source of type pip3" + end + local normalized_package = M.normalize_package(receipt.primary_source.package) + return spawn.python({ + "-m", + "pip", + "list", + "--outdated", + "--format=json", + cwd = install_dir, + env = process.graft_env(M.env(install_dir)), -- use venv + }):map_catching(function(result) + ---@alias PipOutdatedPackage {name: string, version: string, latest_version: string} + ---@type PipOutdatedPackage[] + local packages = vim.json.decode(result.stdout) + + local outdated_primary_package = list_find_first(packages, function(outdated_package) + return outdated_package.name == normalized_package + and outdated_package.version ~= outdated_package.latest_version + end) + + return Optional.of_nilable(outdated_primary_package) + :map(function(package) + return { + name = normalized_package, + current_version = assert(package.version), + latest_version = assert(package.latest_version), + } + end) + :or_else_throw "Primary package is not outdated." + end) +end + +---@async +---@param receipt InstallReceipt +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + if receipt.primary_source.type ~= "pip3" then + return Result.failure "Receipt does not have a primary source of type pip3" + end + return spawn.python({ + "-m", + "pip", + "list", + "--format=json", + cwd = install_dir, + env = process.graft_env(M.env(install_dir)), -- use venv env + }):map_catching(function(result) + local pip_packages = vim.json.decode(result.stdout) + local normalized_pip_package = M.normalize_package(receipt.primary_source.package) + local pip_package = list_find_first(pip_packages, function(package) + return package.name == normalized_pip_package + end) + return Optional.of_nilable(pip_package) + :map(function(package) + return package.version + end) + :or_else_throw "Unable to find pip package." + end) +end + +---@param install_dir string +function M.env(install_dir) + return { + PATH = process.extend_path { M.venv_path(install_dir) }, + } +end + +---@param install_dir string +function M.venv_path(install_dir) + return path.concat { install_dir, VENV_DIR, platform.is_win and "Scripts" or "bin" } +end + +return M diff --git a/lua/nvim-lsp-installer/core/optional.lua b/lua/nvim-lsp-installer/core/optional.lua new file mode 100644 index 00000000..d37a7e69 --- /dev/null +++ b/lua/nvim-lsp-installer/core/optional.lua @@ -0,0 +1,90 @@ +---@class Optional +---@field private _value unknown +local Optional = {} +Optional.__index = Optional + +---@param value any +function Optional.new(value) + return setmetatable({ _value = value }, Optional) +end + +local EMPTY = Optional.new(nil) + +---@param value any +function Optional.of_nilable(value) + if value == nil then + return EMPTY + else + return Optional.new(value) + end +end + +function Optional.empty() + return EMPTY +end + +---@param value any +function Optional.of(value) + return Optional.new(value) +end + +---@param mapper_fn fun(value: any): any +function Optional:map(mapper_fn) + if self:is_present() then + return Optional.of_nilable(mapper_fn(self._value)) + else + return EMPTY + end +end + +function Optional:get() + if not self:is_present() then + error("No value present.", 2) + end + return self._value +end + +---@param value any +function Optional:or_else(value) + if self:is_present() then + return self._value + else + return value + end +end + +---@param supplier fun(): Optional +function Optional:when_empty(supplier) + if self:is_present() then + return self._value + else + return supplier() + end +end + +---@param exception any @(optional) The exception to throw if the result is a failure. +function Optional:or_else_throw(exception) + if self:is_present() then + return self._value + else + if exception then + error(exception, 2) + else + error("No value present.", 2) + end + end +end + +---@param fn fun(value: any) +function Optional:if_present(fn) + if self:is_present() then + fn(self._value) + end + return self +end + +function Optional:is_present() + return self._value ~= nil +end + +return Optional diff --git a/lua/nvim-lsp-installer/core/result.lua b/lua/nvim-lsp-installer/core/result.lua index a94ffe52..f5c2d747 100644 --- a/lua/nvim-lsp-installer/core/result.lua +++ b/lua/nvim-lsp-installer/core/result.lua @@ -32,11 +32,24 @@ function Result:get_or_nil() end end -function Result:get_or_throw() +function Result:get_or_else(value) if self:is_success() then return self.value else - error(self.value.error, 2) + return value + end +end + +---@param exception any @(optional) The exception to throw if the result is a failure. +function Result:get_or_throw(exception) + if self:is_success() then + return self.value + else + if exception ~= nil then + error(exception, 2) + else + error(self.value.error, 2) + end end end @@ -77,4 +90,38 @@ function Result:map_catching(mapper_fn) end end +---@param recover_fn fun(value: any): any +function Result:recover(recover_fn) + if self:is_failure() then + return Result.success(recover_fn(self:err_or_nil())) + else + return self + end +end + +---@param recover_fn fun(value: any): any +function Result:recover_catching(recover_fn) + if self:is_failure() then + local ok, value = pcall(recover_fn, self:err_or_nil()) + if ok then + return Result.success(value) + else + return Result.failure(value) + end + else + return self + end +end + +---@param fn fun(): any +---@return Result +function Result.run_catching(fn) + local ok, result = pcall(fn) + if ok then + return Result.success(result) + else + return Result.failure(result) + end +end + return Result diff --git a/lua/nvim-lsp-installer/core/async/spawn.lua b/lua/nvim-lsp-installer/core/spawn.lua index 5fc7eee7..992b1557 100644 --- a/lua/nvim-lsp-installer/core/async/spawn.lua +++ b/lua/nvim-lsp-installer/core/spawn.lua @@ -5,11 +5,22 @@ local platform = require "nvim-lsp-installer.platform" local async_spawn = a.promisify(process.spawn) ----@type Record<string, fun(opts: JobSpawnOpts): Result> +---@alias JobSpawn Record<string, async fun(opts: JobSpawnOpts): Result> +---@type JobSpawn local spawn = { - aliases = { + _aliases = { npm = platform.is_win and "npm.cmd" or "npm", + gem = platform.is_win and "gem.cmd" or "gem", + composer = platform.is_win and "composer.bat" or "composer", }, + -- Utility function for optionally including arguments. + ---@generic T + ---@param condition boolean + ---@param value T + ---@return T + _when = function(condition, value) + return condition and value or vim.NIL + end, } local function Failure(err, cmd) @@ -22,10 +33,15 @@ end setmetatable(spawn, { __index = function(self, k) + ---@param args string|nil|string[][] return function(args) local cmd_args = {} - for i, arg in ipairs(args) do - cmd_args[i] = arg + for _, arg in ipairs(args) do + if type(arg) == "table" then + vim.list_extend(cmd_args, arg) + elseif arg ~= vim.NIL then + cmd_args[#cmd_args + 1] = arg + end end ---@type JobSpawnOpts local spawn_args = { @@ -41,7 +57,7 @@ setmetatable(spawn, { spawn_args.stdio_sink = stdio.sink end - local cmd = self.aliases[k] or k + local cmd = self._aliases[k] or k local _, exit_code = async_spawn(cmd, spawn_args) if exit_code == 0 then diff --git a/lua/nvim-lsp-installer/installers/npm.lua b/lua/nvim-lsp-installer/installers/npm.lua index f891589e..12ca407f 100644 --- a/lua/nvim-lsp-installer/installers/npm.lua +++ b/lua/nvim-lsp-installer/installers/npm.lua @@ -1,3 +1,4 @@ +---@deprecated Will be replaced by core.managers.npm local path = require "nvim-lsp-installer.path" local fs = require "nvim-lsp-installer.fs" local Data = require "nvim-lsp-installer.data" diff --git a/lua/nvim-lsp-installer/installers/pip3.lua b/lua/nvim-lsp-installer/installers/pip3.lua index 9476318d..67032a40 100644 --- a/lua/nvim-lsp-installer/installers/pip3.lua +++ b/lua/nvim-lsp-installer/installers/pip3.lua @@ -1,3 +1,4 @@ +---@deprecated Will be replaced by core.managers.pip3 local path = require "nvim-lsp-installer.path" local Data = require "nvim-lsp-installer.data" local installers = require "nvim-lsp-installer.installers" diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua deleted file mode 100644 index 19fe7448..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/cargo.lua +++ /dev/null @@ -1,59 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" -local crates = require "nvim-lsp-installer.core.clients.crates" - ----@param output string The `cargo install --list` output. -local function parse_installed_crates(output) - local installed_crates = {} - for _, line in ipairs(vim.split(output, "\n")) do - local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]" - if name and version then - installed_crates[name] = version - end - end - return installed_crates -end - ----@param server Server ----@param source InstallReceiptSource ----@param on_result fun(result: VersionCheckResult) -local function cargo_check(server, source, on_result) - local stdio = process.in_memory_sink() - process.spawn("cargo", { - args = { "install", "--list", "--root", "." }, - cwd = server.root_dir, - stdio_sink = stdio.sink, - }, function(success) - if not success then - return on_result(VersionCheckResult.fail(server)) - end - local installed_crates = parse_installed_crates(table.concat(stdio.buffers.stdout, "")) - if not installed_crates[source.package] then - return on_result(VersionCheckResult.fail(server)) - end - crates.fetch_crate(source.package, function(err, response) - if err then - return on_result(VersionCheckResult.fail(server)) - end - if response.crate.max_stable_version ~= installed_crates[source.package] then - return on_result(VersionCheckResult.success(server, { - { - name = source.package, - current_version = installed_crates[source.package], - latest_version = response.crate.max_stable_version, - }, - })) - else - return on_result(VersionCheckResult.empty(server)) - end - end) - end) -end - -return setmetatable({ - parse_installed_crates = parse_installed_crates, -}, { - __call = function(_, ...) - return cargo_check(...) - end, -}) diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua deleted file mode 100644 index b8e75424..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/composer.lua +++ /dev/null @@ -1,42 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local composer = require "nvim-lsp-installer.installers.composer" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" - ----@param server Server ----@param source InstallReceiptSource ----@param on_check_complete fun(result: VersionCheckResult) -local function composer_checker(server, source, on_check_complete) - local stdio = process.in_memory_sink() - process.spawn(composer.composer_cmd, { - args = { "outdated", "--no-interaction", "--format=json" }, - cwd = server.root_dir, - stdio_sink = stdio.sink, - }, function(success) - if not success then - return on_check_complete(VersionCheckResult.fail(server)) - end - ---@type {installed: {name: string, version: string, latest: string}[]} - local decode_ok, outdated_json = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, "")) - - if not decode_ok then - return on_check_complete(VersionCheckResult.fail(server)) - end - - ---@type OutdatedPackage[] - local outdated_packages = {} - - for _, outdated_package in ipairs(outdated_json.installed) do - if outdated_package.name == source.package and outdated_package.version ~= outdated_package.latest then - table.insert(outdated_packages, { - name = outdated_package.name, - current_version = outdated_package.version, - latest_version = outdated_package.latest, - }) - end - end - - on_check_complete(VersionCheckResult.success(server, outdated_packages)) - end) -end - -return composer_checker diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua deleted file mode 100644 index cf880fd0..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/gem.lua +++ /dev/null @@ -1,95 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local gem = require "nvim-lsp-installer.installers.gem" -local log = require "nvim-lsp-installer.log" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" - -local function not_empty(s) - return s ~= nil and s ~= "" -end - ----Parses a string input like "package (0.1.0 < 0.2.0)" into its components ----@param outdated_gem string ----@return GemOutdatedPackage -local function parse_outdated_gem(outdated_gem) - local package_name, version_expression = outdated_gem:match "^(.+) %((.+)%)" - if not package_name or not version_expression then - -- unparseable - return nil - end - local current_version, latest_version = unpack(vim.split(version_expression, "<")) - - ---@alias GemOutdatedPackage {name:string, current_version: string, latest_version: string} - local outdated_package = { - name = vim.trim(package_name), - current_version = vim.trim(current_version), - latest_version = vim.trim(latest_version), - } - return outdated_package -end - ----@param output string -local function parse_gem_list_output(output) - ---@type Record<string, string> - local gem_versions = {} - for _, line in ipairs(vim.split(output, "\n")) do - local gem_package, version = line:match "^(%S+) %((%S+)%)$" - if gem_package and version then - gem_versions[gem_package] = version - end - end - return gem_versions -end - ----@param server Server ----@param source InstallReceiptSource ----@param on_check_complete fun(result: VersionCheckResult) -local function gem_checker(server, source, on_check_complete) - local stdio = process.in_memory_sink() - process.spawn( - "gem", - { - args = { "outdated" }, - cwd = server.root_dir, - stdio_sink = stdio.sink, - env = process.graft_env(gem.env(server.root_dir)), - }, - vim.schedule_wrap(function(success) - if not success then - return on_check_complete(VersionCheckResult.fail(server)) - end - ---@type string[] - local lines = vim.split(table.concat(stdio.buffers.stdout, ""), "\n") - log.trace("Gem outdated lines output", lines) - local outdated_gems = vim.tbl_map(parse_outdated_gem, vim.tbl_filter(not_empty, lines)) - log.trace("Gem outdated packages", outdated_gems) - - ---@type OutdatedPackage[] - local outdated_packages = {} - - for _, outdated_gem in ipairs(outdated_gems) do - if - outdated_gem.name == source.package - and outdated_gem.current_version ~= outdated_gem.latest_version - then - table.insert(outdated_packages, { - name = outdated_gem.name, - current_version = outdated_gem.current_version, - latest_version = outdated_gem.latest_version, - }) - end - end - - on_check_complete(VersionCheckResult.success(server, outdated_packages)) - end) - ) -end - --- to allow tests to access internals -return setmetatable({ - parse_outdated_gem = parse_outdated_gem, - parse_gem_list_output = parse_gem_list_output, -}, { - __call = function(_, ...) - return gem_checker(...) - end, -}) diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua deleted file mode 100644 index 74007abc..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/git.lua +++ /dev/null @@ -1,42 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" - ----@param server Server ----@param source InstallReceiptSource ----@param on_check_complete fun(result: VersionCheckResult) -return function(server, source, on_check_complete) - process.spawn("git", { - -- We assume git installation track the remote HEAD branch - args = { "fetch", "origin", "HEAD" }, - cwd = server.root_dir, - stdio_sink = process.empty_sink(), - }, function(fetch_success) - local stdio = process.in_memory_sink() - if not fetch_success then - return on_check_complete(VersionCheckResult.fail(server)) - end - process.spawn("git", { - args = { "rev-parse", "FETCH_HEAD", "HEAD" }, - cwd = server.root_dir, - stdio_sink = stdio.sink, - }, function(success) - if success then - local stdout = table.concat(stdio.buffers.stdout, "") - local remote_head, local_head = unpack(vim.split(stdout, "\n")) - if remote_head ~= local_head then - on_check_complete(VersionCheckResult.success(server, { - { - name = source.remote, - latest_version = remote_head, - current_version = local_head, - }, - })) - else - on_check_complete(VersionCheckResult.empty(server)) - end - else - on_check_complete(VersionCheckResult.fail(server)) - end - end) - end) -end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua index 37e30798..527a37ff 100644 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_release_file.lua @@ -1,29 +1,22 @@ +local a = require "nvim-lsp-installer.core.async" +local Result = require "nvim-lsp-installer.core.result" local github = require "nvim-lsp-installer.core.clients.github" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" ----@param server Server ----@param source InstallReceiptSource ----@param on_result fun(result: VersionCheckResult) -return function(server, source, on_result) - github.fetch_latest_release( - source.repo, - { tag_name_pattern = source.tag_name_pattern }, - function(err, latest_release) - if err then - return on_result(VersionCheckResult.fail(server)) - end +local fetch_latest_release = a.promisify(github.fetch_latest_release, true) - if source.release ~= latest_release.tag_name then - return on_result(VersionCheckResult.success(server, { - { - name = source.repo, - current_version = source.release, - latest_version = latest_release.tag_name, - }, - })) - else - return on_result(VersionCheckResult.empty(server)) - end +---@async +---@param receipt InstallReceipt +return function(receipt) + local source = receipt.primary_source + return Result.run_catching(function() + local latest_release = fetch_latest_release(source.repo, { tag_name_pattern = source.tag_name_pattern }) + if source.release ~= latest_release.tag_name then + return { + name = source.repo, + current_version = source.release, + latest_version = latest_release.tag_name, + } end - ) + error "Primary package is not outdated." + end) end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua index 6f45b4ba..f9df2438 100644 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/github_tag.lua @@ -1,25 +1,23 @@ +local a = require "nvim-lsp-installer.core.async" +local Result = require "nvim-lsp-installer.core.result" local github = require "nvim-lsp-installer.core.clients.github" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" ----@param server Server ----@param source InstallReceiptSource ----@param on_result fun(result: VersionCheckResult) -return function(server, source, on_result) - github.fetch_latest_tag(source.repo, function(err, latest_tag) - if err then - return on_result(VersionCheckResult.fail(server)) - end +local fetch_latest_tag = a.promisify(github.fetch_latest_tag, true) + +---@async +---@param receipt InstallReceipt +return function(receipt) + local source = receipt.primary_source + return Result.run_catching(function() + local latest_tag = fetch_latest_tag(source.repo) if source.tag ~= latest_tag.name then - return on_result(VersionCheckResult.success(server, { - { - name = source.repo, - current_version = source.tag, - latest_version = latest_tag.name, - }, - })) - else - return on_result(VersionCheckResult.empty(server)) + return { + name = source.repo, + current_version = source.tag, + latest_version = latest_tag.name, + } end + error "Primary package is not outdated." end) end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua index b892e847..e50ca9fd 100644 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/init.lua @@ -1,16 +1,18 @@ +local a = require "nvim-lsp-installer.core.async" local JobExecutionPool = require "nvim-lsp-installer.jobs.pool" local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" local log = require "nvim-lsp-installer.log" -local npm_check = require "nvim-lsp-installer.jobs.outdated-servers.npm" -local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo" -local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3" -local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem" -local git_check = require "nvim-lsp-installer.jobs.outdated-servers.git" +local npm = require "nvim-lsp-installer.core.managers.npm" +local pip3 = require "nvim-lsp-installer.core.managers.pip3" +local git = require "nvim-lsp-installer.core.managers.git" +local gem = require "nvim-lsp-installer.core.managers.gem" +local go = require "nvim-lsp-installer.core.managers.go" +local cargo = require "nvim-lsp-installer.core.managers.cargo" +local composer = require "nvim-lsp-installer.core.managers.composer" local github_release_file_check = require "nvim-lsp-installer.jobs.outdated-servers.github_release_file" local github_tag_check = require "nvim-lsp-installer.jobs.outdated-servers.github_tag" -local jdtls = require "nvim-lsp-installer.jobs.outdated-servers.jdtls" -local composer_check = require "nvim-lsp-installer.jobs.outdated-servers.composer" +local jdtls_check = require "nvim-lsp-installer.jobs.outdated-servers.jdtls" local M = {} @@ -18,27 +20,18 @@ local jobpool = JobExecutionPool:new { size = 4, } -local function noop(server, _, on_result) - on_result(VersionCheckResult.empty(server)) -end - ----@type Record<InstallReceiptSourceType, function> +---@type Record<InstallReceiptSourceType, async fun(receipt: InstallReceipt, install_dir: string): Result> local checkers = { - ["npm"] = npm_check, - ["pip3"] = pip3_check, - ["cargo"] = cargo_check, - ["gem"] = gem_check, - ["composer"] = composer_check, - ["go"] = noop, -- TODO - ["dotnet"] = noop, -- TODO - ["r_package"] = noop, -- TODO - ["unmanaged"] = noop, - ["system"] = noop, - ["jdtls"] = jdtls, - ["git"] = git_check, + ["npm"] = npm.check_outdated_primary_package, + ["pip3"] = pip3.check_outdated_primary_package, + ["git"] = git.check_outdated_git_clone, + ["cargo"] = cargo.check_outdated_primary_package, + ["composer"] = composer.check_outdated_primary_package, + ["gem"] = gem.check_outdated_primary_package, + ["go"] = go.check_outdated_primary_package, + ["jdtls"] = jdtls_check, ["github_release_file"] = github_release_file_check, ["github_tag"] = github_tag_check, - ["opam"] = noop, } local pending_servers = {} @@ -73,7 +66,13 @@ function M.identify_outdated_servers(servers, on_result) local checker = checkers[receipt.primary_source.type] if checker then - checker(server, receipt.primary_source, complete) + a.run(checker, function(success, result) + if success and result:is_success() then + complete(VersionCheckResult.success(server, { result:get_or_nil() })) + else + complete(VersionCheckResult.fail(server)) + end + end, receipt, server.root_dir) else complete(VersionCheckResult.empty(server)) log.fmt_error("Unable to find checker for source=%s", receipt.primary_source.type) diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua index bb40714a..3b10f42a 100644 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua +++ b/lua/nvim-lsp-installer/jobs/outdated-servers/jdtls.lua @@ -1,24 +1,21 @@ +local a = require "nvim-lsp-installer.core.async" +local Result = require "nvim-lsp-installer.core.result" local eclipse = require "nvim-lsp-installer.core.clients.eclipse" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" ----@param server Server ----@param source InstallReceiptSource ----@param on_check_result fun(result: VersionCheckResult) -return function(server, source, on_check_result) - eclipse.fetch_latest_jdtls_version(function(err, latest_version) - if err then - return on_check_result(VersionCheckResult.fail(server)) - end - if source.version ~= latest_version then - return on_check_result(VersionCheckResult.success(server, { - { - name = "jdtls", - current_version = source.version, - latest_version = latest_version, - }, - })) - else - return on_check_result(VersionCheckResult.empty(server)) +local fetch_latest_jdtls_version = a.promisify(eclipse.fetch_latest_jdtls_version, true) + +---@async +---@param receipt InstallReceipt +return function(receipt) + return Result.run_catching(function() + local latest_version = fetch_latest_jdtls_version() + if receipt.primary_source.version ~= latest_version then + return { + name = "jdtls", + current_version = receipt.primary_source.version, + latest_version = latest_version, + } end + error "Primary package is not outdated." end) end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua deleted file mode 100644 index bad76e81..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/npm.lua +++ /dev/null @@ -1,45 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local npm = require "nvim-lsp-installer.installers.npm" -local log = require "nvim-lsp-installer.log" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" - ----@param server Server ----@param source InstallReceiptSource ----@param on_check_complete fun(result: VersionCheckResult) -return function(server, source, on_check_complete) - local stdio = process.in_memory_sink() - process.spawn( - npm.npm_command, - { - args = vim.list_extend({ "outdated", "--json" }, { source.package }), - cwd = server.root_dir, - stdio_sink = stdio.sink, - }, - -- Note that `npm outdated` exits with code 1 if it finds outdated packages - vim.schedule_wrap(function() - ---@alias NpmOutdatedPackage {current: string, wanted: string, latest: string, dependent: string, location: string} - ---@type table<string, NpmOutdatedPackage> - local ok, data = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, "")) - - if not ok then - log.fmt_error("Failed to parse npm outdated --json output. %s", data) - return on_check_complete(VersionCheckResult.fail(server)) - end - - ---@type OutdatedPackage[] - local outdated_packages = {} - - for package, outdated_package in pairs(data) do - if outdated_package.current ~= outdated_package.latest then - table.insert(outdated_packages, { - name = package, - current_version = outdated_package.current, - latest_version = outdated_package.latest, - }) - end - end - - on_check_complete(VersionCheckResult.success(server, outdated_packages)) - end) - ) -end diff --git a/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua b/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua deleted file mode 100644 index 9534617a..00000000 --- a/lua/nvim-lsp-installer/jobs/outdated-servers/pip3.lua +++ /dev/null @@ -1,72 +0,0 @@ -local process = require "nvim-lsp-installer.process" -local pip3 = require "nvim-lsp-installer.installers.pip3" -local VersionCheckResult = require "nvim-lsp-installer.jobs.outdated-servers.version-check-result" -local log = require "nvim-lsp-installer.log" - ----@param package string ----@return string -local function normalize_package(package) - -- https://stackoverflow.com/a/60307740 - local s = package:gsub("%[.*%]", "") - return s -end - ----@param server Server ----@param source InstallReceiptSource ----@param on_check_complete fun(result: VersionCheckResult) -local function pip3_check(server, source, on_check_complete) - local normalized_package = normalize_package(source.package) - log.fmt_trace("Normalized package from %s to %s.", source.package, normalized_package) - local stdio = process.in_memory_sink() - process.spawn( - "python", - { - args = { "-m", "pip", "list", "--outdated", "--format=json" }, - cwd = server.root_dir, - stdio_sink = stdio.sink, - env = process.graft_env(pip3.env(server.root_dir)), - }, - vim.schedule_wrap(function(success) - if not success then - return on_check_complete(VersionCheckResult.fail(server)) - end - ---@alias PipOutdatedPackage {name: string, version: string, latest_version: string} - ---@type PipOutdatedPackage[] - local ok, packages = pcall(vim.json.decode, table.concat(stdio.buffers.stdout, "")) - - if not ok then - log.fmt_error("Failed to parse pip3 output. %s", packages) - return on_check_complete(VersionCheckResult.fail(server)) - end - - log.trace("Outdated packages", packages) - - ---@type OutdatedPackage[] - local outdated_packages = {} - - for _, outdated_package in ipairs(packages) do - if - outdated_package.name == normalized_package - and outdated_package.version ~= outdated_package.latest_version - then - table.insert(outdated_packages, { - name = outdated_package.name, - current_version = outdated_package.version, - latest_version = outdated_package.latest_version, - }) - end - end - - on_check_complete(VersionCheckResult.success(server, outdated_packages)) - end) - ) -end - --- to allow tests to access internals -return setmetatable({ - normalize_package = normalize_package, -}, { - __call = function(_, ...) - return pip3_check(...) - end, -}) diff --git a/lua/nvim-lsp-installer/jobs/version-check/init.lua b/lua/nvim-lsp-installer/jobs/version-check/init.lua index da33ea9d..ef36fefa 100644 --- a/lua/nvim-lsp-installer/jobs/version-check/init.lua +++ b/lua/nvim-lsp-installer/jobs/version-check/init.lua @@ -1,12 +1,11 @@ -local a = require "nvim-lsp-installer.core.async" local Result = require "nvim-lsp-installer.core.result" -local process = require "nvim-lsp-installer.process" -local pip3 = require "nvim-lsp-installer.installers.pip3" -local gem = require "nvim-lsp-installer.installers.gem" -local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo" -local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem" -local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3" -local spawn = require "nvim-lsp-installer.core.async.spawn" +local npm = require "nvim-lsp-installer.core.managers.npm" +local cargo = require "nvim-lsp-installer.core.managers.cargo" +local pip3 = require "nvim-lsp-installer.core.managers.pip3" +local gem = require "nvim-lsp-installer.core.managers.gem" +local go = require "nvim-lsp-installer.core.managers.go" +local git = require "nvim-lsp-installer.core.managers.git" +local composer = require "nvim-lsp-installer.core.managers.composer" local M = {} @@ -26,89 +25,35 @@ local function noop() return Result.failure "Unable to detect version." end ----@type Record<InstallReceiptSourceType, fun(server: Server, receipt: InstallReceipt): Result> +---@type Record<InstallReceiptSourceType, async fun(server: Server, receipt: InstallReceipt): Result> local version_checker = { ["npm"] = function(server, receipt) - return spawn.npm({ - "ls", - "--json", - cwd = server.root_dir, - }):map_catching(function(result) - local npm_packages = vim.json.decode(result.stdout) - return npm_packages.dependencies[receipt.primary_source.package].version - end) + return npm.get_installed_primary_package_version(receipt, server.root_dir) end, ["pip3"] = function(server, receipt) - return spawn.python3({ - "-m", - "pip", - "list", - "--format", - "json", - cwd = server.root_dir, - env = process.graft_env(pip3.env(server.root_dir)), - }):map_catching(function(result) - local pip_packages = vim.json.decode(result.stdout) - local normalized_pip_package = pip3_check.normalize_package(receipt.primary_source.package) - for _, pip_package in ipairs(pip_packages) do - if pip_package.name == normalized_pip_package then - return pip_package.version - end - end - error "Unable to find pip package." - end) + return pip3.get_installed_primary_package_version(receipt, server.root_dir) end, ["gem"] = function(server, receipt) - return spawn.gem({ - "list", - cwd = server.root_dir, - env = process.graft_env(gem.env(server.root_dir)), - }):map_catching(function(result) - local gems = gem_check.parse_gem_list_output(result.stdout) - if gems[receipt.primary_source.package] then - return gems[receipt.primary_source.package] - else - error "Failed to find gem package version." - end - end) + return gem.get_installed_primary_package_version(receipt, server.root_dir) end, ["cargo"] = function(server, receipt) - return spawn.cargo({ - "install", - "--list", - "--root", - server.root_dir, - cwd = server.root_dir, - }):map_catching(function(result) - local crates = cargo_check.parse_installed_crates(result.stdout) - a.scheduler() -- needed because vim.fn.* call - local package = vim.fn.fnamemodify(receipt.primary_source.package, ":t") - if crates[package] then - return crates[package] - else - error "Failed to find cargo package version." - end - end) + return cargo.get_installed_primary_package_version(receipt, server.root_dir) + end, + ["composer"] = function(server, receipt) + return composer.get_installed_primary_package_version(receipt, server.root_dir) end, ["git"] = function(server) - return spawn.git({ - "rev-parse", - "--short", - "HEAD", - cwd = server.root_dir, - }):map_catching(function(result) - return vim.trim(result.stdout) - end) + return git.get_installed_revision(server.root_dir) + end, + ["go"] = function(server, receipt) + return go.get_installed_primary_package_version(receipt, server.root_dir) end, - ["opam"] = noop, - ["dotnet"] = noop, - ["r_package"] = noop, ["github_release_file"] = version_in_receipt "release", ["github_tag"] = version_in_receipt "tag", ["jdtls"] = version_in_receipt "version", } ---- Async function. +---@async ---@param server Server ---@return Result function M.check_server_version(server) diff --git a/lua/nvim-lsp-installer/path.lua b/lua/nvim-lsp-installer/path.lua index 5d3fcee4..2f0fd84b 100644 --- a/lua/nvim-lsp-installer/path.lua +++ b/lua/nvim-lsp-installer/path.lua @@ -27,6 +27,8 @@ function M.concat(path_components) return table.concat(path_components, sep) end +---@path root_path string +---@path path string function M.is_subdirectory(root_path, path) return root_path == path or path:sub(1, #root_path + 1) == root_path .. sep end diff --git a/scripts/autogen_metadata.lua b/scripts/autogen_metadata.lua index 7ff2dc24..46b939af 100644 --- a/scripts/autogen_metadata.lua +++ b/scripts/autogen_metadata.lua @@ -4,6 +4,8 @@ local Path = require "nvim-lsp-installer.path" local fetch = require "nvim-lsp-installer.core.fetch" local Data = require "nvim-lsp-installer.data" +local async_fetch = a.promisify(fetch, true) + local coalesce = Data.coalesce package.loaded["nvim-lsp-installer.servers"] = nil @@ -74,6 +76,7 @@ local function get_supported_filetypes(server) return filetypes end +---@async local function create_filetype_map() local filetype_map = {} @@ -91,6 +94,7 @@ local function create_filetype_map() write_file(Path.concat { generated_dir, "filetype_map.lua" }, "return " .. vim.inspect(filetype_map), "w") end +---@async local function create_autocomplete_map() ---@type table<string, Server> local language_map = {} @@ -133,6 +137,7 @@ local function create_autocomplete_map() ) end +---@async local function create_server_metadata() local metadata = {} @@ -149,19 +154,18 @@ local function create_server_metadata() write_file(Path.concat { generated_dir, "metadata.lua" }, "return " .. vim.inspect(metadata), "w") end +---@async local function create_setting_schema_files() local available_servers = servers.get_available_servers() - local gist_err, gist_data = - a.promisify(fetch) "https://gist.githubusercontent.com/williamboman/a01c3ce1884d4b57cc93422e7eae7702/raw/lsp-packages.json" - assert(not gist_err, "Failed to fetch gist.") + local gist_data = + async_fetch "https://gist.githubusercontent.com/williamboman/a01c3ce1884d4b57cc93422e7eae7702/raw/lsp-packages.json" local package_json_mappings = vim.json.decode(gist_data) for _, server in pairs(available_servers) do local package_json_url = package_json_mappings[server.name] if package_json_url then print(("Fetching %q..."):format(package_json_url)) - local err, response = a.promisify(fetch)(package_json_url) - assert(not err, "Failed to fetch package.json for " .. server.name) + local response = async_fetch(package_json_url) local schema = vim.json.decode(response) if schema.contributes and schema.contributes.configuration then schema = schema.contributes.configuration diff --git a/tests/core/async/async_spec.lua b/tests/core/async/async_spec.lua index 662e493a..0e881c8b 100644 --- a/tests/core/async/async_spec.lua +++ b/tests/core/async/async_spec.lua @@ -13,14 +13,29 @@ describe("async", function() it("should run in blocking mode", function() local start = timestamp() a.run_blocking(function() - a.sleep(1000) + a.sleep(100) end) local stop = timestamp() local grace_ms = 25 - assert.is_true((stop - start) >= (1000 - grace_ms)) + assert.is_true((stop - start) >= (100 - grace_ms)) end) it( + "should pass arguments to .run", + async_test(function() + local callback = spy.new() + local start = timestamp() + a.run(a.sleep, callback, 100) + assert.wait_for(function() + assert.spy(callback).was_called(1) + local stop = timestamp() + local grace_ms = 25 + assert.is_true((stop - start) >= (100 - grace_ms)) + end, 150) + end) + ) + + it( "should wrap callback-style async functions", async_test(function() local stdio = process.in_memory_sink() diff --git a/tests/core/managers/cargo_spec.lua b/tests/core/managers/cargo_spec.lua new file mode 100644 index 00000000..303bf5d0 --- /dev/null +++ b/tests/core/managers/cargo_spec.lua @@ -0,0 +1,210 @@ +local spy = require "luassert.spy" +local match = require "luassert.match" +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local cargo = require "nvim-lsp-installer.core.managers.cargo" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("cargo manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + cargo = mockx.returns {}, + }, + } + end) + + it( + "should call cargo install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + cargo.crate "my-crate"(ctx) + assert.spy(ctx.spawn.cargo).was_called(1) + assert.spy(ctx.spawn.cargo).was_called_with { + "install", + "--root", + ".", + "--locked", + { "--version", "42.13.37" }, + vim.NIL, -- --features + "my-crate", + } + end) + ) + + it( + "should call cargo install with git source", + async_test(function() + cargo.crate("https://my-crate.git", { git = true })(ctx) + assert.spy(ctx.spawn.cargo).was_called(1) + assert.spy(ctx.spawn.cargo).was_called_with { + "install", + "--root", + ".", + "--locked", + vim.NIL, + vim.NIL, -- --features + { "--git", "https://my-crate.git" }, + } + end) + ) + + it( + "should respect options", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + cargo.crate("my-crate", { + features = "lsp", + })(ctx) + assert.spy(ctx.spawn.cargo).was_called(1) + assert.spy(ctx.spawn.cargo).was_called_with { + "install", + "--root", + ".", + "--locked", + { "--version", "42.13.37" }, + { "--features", "lsp" }, + "my-crate", + } + end) + ) + + it( + "should not allow combining version with git crate", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + local err = assert.has_error(function() + cargo.crate("my-crate", { + git = true, + })(ctx) + end) + assert.equals("Providing a version when installing a git crate is not allowed.", err) + assert.spy(ctx.spawn.cargo).was_called(0) + end) + ) + + it( + "should provide receipt information", + async_test(function() + cargo.crate "main-package"(ctx) + assert.equals( + vim.inspect { + type = "cargo", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + end) + ) +end) + +describe("cargo version check", function() + it("parses cargo installed packages output", function() + assert.equal( + vim.inspect { + ["bat"] = "0.18.3", + ["exa"] = "0.10.1", + ["git-select-branch"] = "0.1.1", + ["hello_world"] = "0.0.1", + ["rust-analyzer"] = "0.0.0", + ["stylua"] = "0.11.2", + ["zoxide"] = "0.5.0", + }, + vim.inspect(cargo.parse_installed_crates [[bat v0.18.3: + bat +exa v0.10.1: + exa +git-select-branch v0.1.1: + git-select-branch +hello_world v0.0.1 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.LGbguATJHj): + hello_world +rust-analyzer v0.0.0 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.YlsHeA9JVL/crates/rust-analyzer): + rust-analyzer +stylua v0.11.2: + stylua +zoxide v0.5.0: + zoxide +]]) + ) + end) + + it( + "should return current version", + async_test(function() + spawn.cargo = spy.new(function() + return Result.success { + stdout = [[flux-lsp v0.8.8 (https://github.com/influxdata/flux-lsp#4e452f07): + flux-lsp +]], + } + end) + + local result = cargo.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "cargo", + package = "https://github.com/influxdata/flux-lsp", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.cargo).was_called(1) + assert.spy(spawn.cargo).was_called_with(match.tbl_containing { + "install", + "--list", + "--root", + ".", + cwd = "/tmp/install/dir", + }) + assert.is_true(result:is_success()) + assert.equals("0.8.8", result:get_or_nil()) + + spawn.cargo = nil + end) + ) + + -- XXX: This test will actually send http request to crates.io's API. It's not mocked. + it( + "should return outdated primary package", + async_test(function() + spawn.cargo = spy.new(function() + return Result.success { + stdout = [[lelwel v0.4.0: + lelwel-ls +]], + } + end) + + local result = cargo.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "cargo", + package = "lelwel", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.cargo).was_called(1) + assert.spy(spawn.cargo).was_called_with(match.tbl_containing { + "install", + "--list", + "--root", + ".", + cwd = "/tmp/install/dir", + }) + assert.is_true(result:is_success()) + assert.is_true(match.tbl_containing { + current_version = "0.4.0", + latest_version = match.matches "%d.%d.%d", + name = "lelwel", + }(result:get_or_nil())) + + spawn.cargo = nil + end) + ) +end) diff --git a/tests/core/managers/composer_spec.lua b/tests/core/managers/composer_spec.lua new file mode 100644 index 00000000..caa0721e --- /dev/null +++ b/tests/core/managers/composer_spec.lua @@ -0,0 +1,188 @@ +local spy = require "luassert.spy" +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local composer = require "nvim-lsp-installer.core.managers.composer" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("composer manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + composer = mockx.returns {}, + }, + } + end) + + it( + "should call composer require", + async_test(function() + ctx.fs.file_exists = mockx.returns(false) + ctx.requested_version = Optional.of "42.13.37" + composer.require { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.composer).was_called(2) + assert.spy(ctx.spawn.composer).was_called_with { + "init", + "--no-interaction", + "--stability=stable", + } + assert.spy(ctx.spawn.composer).was_called_with { + "require", + { + "main-package:42.13.37", + "supporting-package", + "supporting-package2", + }, + } + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + composer.require { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "composer", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "composer", + package = "supporting-package", + }, + { + type = "composer", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) + +describe("composer version check", function() + it( + "should return current version", + async_test(function() + spawn.composer = spy.new(function() + return Result.success { + stdout = [[ +{ + "name": "vimeo/psalm", + "versions": [ + "4.0.0" + ] +} +]], + } + end) + + local result = composer.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "composer", + package = "vimeo/psalm", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.composer).was_called(1) + assert.spy(spawn.composer).was_called_with { + "info", + "--format=json", + "vimeo/psalm", + cwd = "/tmp/install/dir", + } + assert.is_true(result:is_success()) + assert.equals("4.0.0", result:get_or_nil()) + + spawn.composer = nil + end) + ) + + it( + "should return outdated primary package", + async_test(function() + spawn.composer = spy.new(function() + return Result.success { + stdout = [[ +{ + "installed": [ + { + "name": "vimeo/psalm", + "version": "4.0.0", + "latest": "4.22.0", + "latest-status": "semver-safe-update", + "description": "A static analysis tool for finding errors in PHP applications" + } + ] +} +]], + } + end) + + local result = composer.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "composer", + package = "vimeo/psalm", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.composer).was_called(1) + assert.spy(spawn.composer).was_called_with { + "outdated", + "--no-interaction", + "--format=json", + cwd = "/tmp/install/dir", + } + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "vimeo/psalm", + current_version = "4.0.0", + latest_version = "4.22.0", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.composer = nil + end) + ) + + it( + "should return failure if primary package is not outdated", + async_test(function() + spawn.composer = spy.new(function() + return Result.success { + stdout = [[{"installed": []}]], + } + end) + + local result = composer.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "composer", + package = "vimeo/psalm", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_failure()) + assert.equals("Primary package is not outdated.", result:err_or_nil()) + spawn.composer = nil + end) + ) +end) diff --git a/tests/core/managers/dotnet_spec.lua b/tests/core/managers/dotnet_spec.lua new file mode 100644 index 00000000..4a6887da --- /dev/null +++ b/tests/core/managers/dotnet_spec.lua @@ -0,0 +1,47 @@ +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local dotnet = require "nvim-lsp-installer.core.managers.dotnet" + +describe("dotnet manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + dotnet = mockx.returns {}, + }, + } + end) + + it( + "should call dotnet tool update", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + dotnet.package "main-package"(ctx) + assert.spy(ctx.spawn.dotnet).was_called(1) + assert.spy(ctx.spawn.dotnet).was_called_with { + "tool", + "update", + "--tool-path", + ".", + { "--version", "42.13.37" }, + "main-package", + } + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + dotnet.package "main-package"(ctx) + assert.equals( + vim.inspect { + type = "dotnet", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + end) + ) +end) diff --git a/tests/core/managers/gem_spec.lua b/tests/core/managers/gem_spec.lua new file mode 100644 index 00000000..e258c65b --- /dev/null +++ b/tests/core/managers/gem_spec.lua @@ -0,0 +1,226 @@ +local spy = require "luassert.spy" +local match = require "luassert.match" +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local gem = require "nvim-lsp-installer.core.managers.gem" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("gem manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + gem = mockx.returns {}, + }, + } + end) + + it( + "should call gem install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + gem.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.gem).was_called(1) + assert.spy(ctx.spawn.gem).was_called_with(match.tbl_containing { + "install", + "--no-user-install", + "--install-dir=.", + "--bindir=bin", + "--no-document", + match.tbl_containing { + "main-package:42.13.37", + "supporting-package", + "supporting-package2", + }, + }) + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + gem.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "gem", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "gem", + package = "supporting-package", + }, + { + type = "gem", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) + +describe("gem version check", function() + it( + "should return current version", + async_test(function() + spawn.gem = spy.new(function() + return Result.success { + stdout = [[shellwords (default: 0.1.0) +singleton (default: 0.1.1) +solargraph (0.44.0) +stringio (default: 3.0.1) +strscan (default: 3.0.1) +]], + } + end) + + local result = gem.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "gem", + package = "solargraph", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.gem).was_called(1) + assert.spy(spawn.gem).was_called_with(match.tbl_containing { + "list", + cwd = "/tmp/install/dir", + env = match.all_of( + match.list_containing "GEM_HOME=/tmp/install/dir", + match.list_containing "GEM_PATH=/tmp/install/dir" + ), + }) + assert.is_true(result:is_success()) + assert.equals("0.44.0", result:get_or_nil()) + + spawn.gem = nil + end) + ) + + it( + "should return outdated primary package", + async_test(function() + spawn.gem = spy.new(function() + return Result.success { + stdout = [[bigdecimal (3.1.1 < 3.1.2) +cgi (0.3.1 < 0.3.2) +logger (1.5.0 < 1.5.1) +ostruct (0.5.2 < 0.5.3) +reline (0.3.0 < 0.3.1) +securerandom (0.1.1 < 0.2.0) +solargraph (0.44.0 < 0.44.3) +]], + } + end) + + local result = gem.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "gem", + package = "solargraph", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.gem).was_called(1) + assert.spy(spawn.gem).was_called_with(match.tbl_containing { + "outdated", + cwd = "/tmp/install/dir", + env = match.all_of( + match.list_containing "GEM_HOME=/tmp/install/dir", + match.list_containing "GEM_PATH=/tmp/install/dir" + ), + }) + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "solargraph", + current_version = "0.44.0", + latest_version = "0.44.3", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.gem = nil + end) + ) + + it( + "should return failure if primary package is not outdated", + async_test(function() + spawn.gem = spy.new(function() + return Result.success { + stdout = "", + } + end) + + local result = gem.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "gem", + package = "solargraph", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_failure()) + assert.equals("Primary package is not outdated.", result:err_or_nil()) + spawn.gem = nil + end) + ) + + it("parses outdated gem output", function() + local normalize = gem.parse_outdated_gem + assert.equal( + vim.inspect { + name = "solargraph", + current_version = "0.42.2", + latest_version = "0.44.2", + }, + vim.inspect(normalize [[solargraph (0.42.2 < 0.44.2)]]) + ) + assert.equal( + vim.inspect { + name = "sorbet-runtime", + current_version = "0.5.9307", + latest_version = "0.5.9468", + }, + vim.inspect(normalize [[sorbet-runtime (0.5.9307 < 0.5.9468)]]) + ) + end) + + it("returns nil when unable to parse outdated gem", function() + assert.is_nil(gem.parse_outdated_gem "a whole bunch of gibberish!") + assert.is_nil(gem.parse_outdated_gem "") + end) + + it("should parse gem list output", function() + assert.equals( + vim.inspect { + ["solargraph"] = "0.44.3", + ["unicode-display_width"] = "2.1.0", + }, + vim.inspect(gem.parse_gem_list_output [[ + +*** LOCAL GEMS *** + +nokogiri (1.13.3 arm64-darwin) +solargraph (0.44.3) +unicode-display_width (2.1.0) +]]) + ) + end) +end) diff --git a/tests/core/managers/git_spec.lua b/tests/core/managers/git_spec.lua new file mode 100644 index 00000000..2f6c6ace --- /dev/null +++ b/tests/core/managers/git_spec.lua @@ -0,0 +1,179 @@ +local spy = require "luassert.spy" +local mock = require "luassert.mock" +local spawn = require "nvim-lsp-installer.core.spawn" +local Result = require "nvim-lsp-installer.core.result" + +local git = require "nvim-lsp-installer.core.managers.git" +local Optional = require "nvim-lsp-installer.core.optional" + +describe("git manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + git = mockx.returns {}, + }, + } + end) + + it( + "should fail if no git repo provided", + async_test(function() + local err = assert.has_errors(function() + git.clone {}(ctx) + end) + assert.equals("No git URL provided.", err) + assert.spy(ctx.spawn.git).was_not_called() + end) + ) + + it( + "should clone provided repo", + async_test(function() + git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx) + assert.spy(ctx.spawn.git).was_called(1) + assert.spy(ctx.spawn.git).was_called_with { + "clone", + "--depth", + "1", + "https://github.com/williamboman/nvim-lsp-installer.git", + ".", + } + end) + ) + + it( + "should fetch and checkout revision if requested", + async_test(function() + ctx.requested_version = Optional.of "1337" + git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx) + assert.spy(ctx.spawn.git).was_called(3) + assert.spy(ctx.spawn.git).was_called_with { + "clone", + "--depth", + "1", + "https://github.com/williamboman/nvim-lsp-installer.git", + ".", + } + assert.spy(ctx.spawn.git).was_called_with { + "fetch", + "--depth", + "1", + "origin", + "1337", + } + assert.spy(ctx.spawn.git).was_called_with { "checkout", "FETCH_HEAD" } + end) + ) + + it( + "should provide receipt information", + async_test(function() + git.clone { "https://github.com/williamboman/nvim-lsp-installer.git" }(ctx) + assert.equals( + vim.inspect { + type = "git", + remote = "https://github.com/williamboman/nvim-lsp-installer.git", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.is_true(#ctx.receipt.secondary_sources == 0) + end) + ) +end) + +describe("git version check", function() + it( + "should return current version", + async_test(function() + spawn.git = spy.new(function() + return Result.success { + stdout = [[19c668c]], + } + end) + + local result = git.get_installed_revision "/tmp/install/dir" + + assert.spy(spawn.git).was_called(1) + assert.spy(spawn.git).was_called_with { "rev-parse", "--short", "HEAD", cwd = "/tmp/install/dir" } + assert.is_true(result:is_success()) + assert.equals("19c668c", result:get_or_nil()) + + spawn.git = nil + end) + ) + + it( + "should check for outdated git clone", + async_test(function() + spawn.git = spy.new(function() + return Result.success { + stdout = [[728307a74cd5f2dec7ca2ca164785c25673d6328 +19c668cd10695b243b09452f0dfd53570c1a2e7d]], + } + end) + + local result = git.check_outdated_git_clone( + mock.new { + primary_source = mock.new { + type = "git", + remote = "https://github.com/williamboman/nvim-lsp-installer.git", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.git).was_called(2) + assert.spy(spawn.git).was_called_with { + "fetch", + "origin", + "HEAD", + cwd = "/tmp/install/dir", + } + assert.spy(spawn.git).was_called_with { + "rev-parse", + "FETCH_HEAD", + "HEAD", + cwd = "/tmp/install/dir", + } + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "https://github.com/williamboman/nvim-lsp-installer.git", + current_version = "19c668cd10695b243b09452f0dfd53570c1a2e7d", + latest_version = "728307a74cd5f2dec7ca2ca164785c25673d6328", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.git = nil + end) + ) + + it( + "should return failure if clone is not outdated", + async_test(function() + spawn.git = spy.new(function() + return Result.success { + stdout = [[19c668cd10695b243b09452f0dfd53570c1a2e7d +19c668cd10695b243b09452f0dfd53570c1a2e7d]], + } + end) + + local result = git.check_outdated_git_clone( + mock.new { + primary_source = mock.new { + type = "git", + remote = "https://github.com/williamboman/nvim-lsp-installer.git", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_failure()) + assert.equals("Git clone is up to date.", result:err_or_nil()) + spawn.git = nil + end) + ) +end) diff --git a/tests/core/managers/go_spec.lua b/tests/core/managers/go_spec.lua new file mode 100644 index 00000000..4152d671 --- /dev/null +++ b/tests/core/managers/go_spec.lua @@ -0,0 +1,187 @@ +local match = require "luassert.match" +local mock = require "luassert.mock" +local stub = require "luassert.stub" +local spy = require "luassert.spy" +local Optional = require "nvim-lsp-installer.core.optional" +local Result = require "nvim-lsp-installer.core.result" +local go = require "nvim-lsp-installer.core.managers.go" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("go manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + go = mockx.returns {}, + }, + } + end) + + it( + "should call go install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + go.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.go).was_called(3) + assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing { + "install", + "-v", + "main-package@42.13.37", + env = match.list_containing "GOBIN=/tmp/install-dir", + }) + assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing { + "install", + "-v", + "supporting-package@latest", + env = match.list_containing "GOBIN=/tmp/install-dir", + }) + assert.spy(ctx.spawn.go).was_called_with(match.tbl_containing { + "install", + "-v", + "supporting-package2@latest", + env = match.list_containing "GOBIN=/tmp/install-dir", + }) + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + go.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "go", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "go", + package = "supporting-package", + }, + { + type = "go", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) + +describe("go version check", function() + local go_version_output = [[ +gopls: go1.18 + path golang.org/x/tools/gopls + mod golang.org/x/tools/gopls v0.8.1 h1:q5nDpRopYrnF4DN/1o8ZQ7Oar4Yd4I5OtGMx5RyV2/8= + dep github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= + dep mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= + build -compiler=gc + build GOOS=darwin +]] + + it("should parse go version output", function() + local parsed = go.parse_mod_version_output(go_version_output) + assert.equals( + vim.inspect { + path = { ["golang.org/x/tools/gopls"] = "" }, + mod = { ["golang.org/x/tools/gopls"] = "v0.8.1" }, + dep = { ["github.com/google/go-cmp"] = "v0.5.7", ["mvdan.cc/xurls/v2"] = "v2.4.0" }, + build = { ["-compiler=gc"] = "", ["GOOS=darwin"] = "" }, + }, + vim.inspect(parsed) + ) + end) + + it( + "should return current version", + async_test(function() + spawn.go = spy.new(function() + return Result.success { stdout = go_version_output } + end) + + local result = go.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "go", + package = "golang.org/x/tools/gopls", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.go).was_called(1) + assert.spy(spawn.go).was_called_with { + "version", + "-m", + "gopls", + cwd = "/tmp/install/dir", + } + assert.is_true(result:is_success()) + assert.equals("v0.8.1", result:get_or_nil()) + + spawn.go = nil + end) + ) + + it( + "should return outdated primary package", + async_test(function() + stub(spawn, "go") + spawn.go.on_call_with({ + "list", + "-json", + "-m", + "-versions", + "golang.org/x/tools/gopls", + cwd = "/tmp/install/dir", + }).returns(Result.success { + stdout = [[ + { + "Path": "/tmp/install/dir", + "Versions": [ + "v1.0.0", + "v1.0.1", + "v2.0.0" + ] + } + ]], + }) + spawn.go.on_call_with({ + "version", + "-m", + "gopls", + cwd = "/tmp/install/dir", + }).returns(Result.success { + stdout = go_version_output, + }) + + local result = go.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "go", + package = "golang.org/x/tools/gopls", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "golang.org/x/tools/gopls", + current_version = "v0.8.1", + latest_version = "v2.0.0", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.go = nil + end) + ) +end) diff --git a/tests/core/managers/npm_spec.lua b/tests/core/managers/npm_spec.lua new file mode 100644 index 00000000..c5e2b2b9 --- /dev/null +++ b/tests/core/managers/npm_spec.lua @@ -0,0 +1,205 @@ +local spy = require "luassert.spy" +local match = require "luassert.match" +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local npm = require "nvim-lsp-installer.core.managers.npm" +local Result = require "nvim-lsp-installer.core.result" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("npm manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + npm = mockx.returns {}, + }, + } + end) + + it( + "should call npm install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.npm).was_called(1) + assert.spy(ctx.spawn.npm).was_called_with(match.tbl_containing { + "install", + match.tbl_containing { + "main-package@42.13.37", + "supporting-package", + "supporting-package2", + }, + }) + end) + ) + + it( + "should call npm init if node_modules and package.json doesnt exist", + async_test(function() + ctx.fs.file_exists = mockx.returns(false) + ctx.fs.dir_exists = mockx.returns(false) + npm.install { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.npm).was_called_with { + "init", + "--yes", + "--scope=lsp-installer", + } + end) + ) + + it( + "should append .npmrc file", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.fs.append_file).was_called(1) + assert.spy(ctx.fs.append_file).was_called_with(ctx.fs, ".npmrc", "global-style=true") + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + npm.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "npm", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "npm", + package = "supporting-package", + }, + { + type = "npm", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) + +describe("npm version check", function() + it( + "should return current version", + async_test(function() + spawn.npm = spy.new(function() + return Result.success { + stdout = [[ + { + "name": "bash", + "dependencies": { + "bash-language-server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bash-language-server/-/bash-language-server-2.0.0.tgz" + } + } + } + ]], + } + end) + + local result = npm.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "npm", + package = "bash-language-server", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.npm).was_called(1) + assert.spy(spawn.npm).was_called_with { "ls", "--json", cwd = "/tmp/install/dir" } + assert.is_true(result:is_success()) + assert.equals("2.0.0", result:get_or_nil()) + + spawn.npm = nil + end) + ) + + it( + "should return outdated primary package", + async_test(function() + spawn.npm = spy.new(function() + -- npm outdated returns with exit code 1 if outdated packages are found! + return Result.failure { + exit_code = 1, + stdout = [[ + { + "bash-language-server": { + "current": "1.17.0", + "wanted": "1.17.0", + "latest": "2.0.0", + "dependent": "bash", + "location": "/tmp/install/dir" + } + } + ]], + } + end) + + local result = npm.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "npm", + package = "bash-language-server", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.npm).was_called(1) + assert.spy(spawn.npm).was_called_with { + "outdated", + "--json", + "bash-language-server", + cwd = "/tmp/install/dir", + } + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "bash-language-server", + current_version = "1.17.0", + latest_version = "2.0.0", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.npm = nil + end) + ) + + it( + "should return failure if primary package is not outdated", + async_test(function() + spawn.npm = spy.new(function() + return Result.success { + stdout = "{}", + } + end) + + local result = npm.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "npm", + package = "bash-language-server", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_failure()) + assert.equals("Primary package is not outdated.", result:err_or_nil()) + spawn.npm = nil + end) + ) +end) diff --git a/tests/core/managers/opam_spec.lua b/tests/core/managers/opam_spec.lua new file mode 100644 index 00000000..0ec003ec --- /dev/null +++ b/tests/core/managers/opam_spec.lua @@ -0,0 +1,64 @@ +local match = require "luassert.match" +local mock = require "luassert.mock" +local Optional = require "nvim-lsp-installer.core.optional" +local opam = require "nvim-lsp-installer.core.managers.opam" + +describe("opam manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + opam = mockx.returns {}, + }, + } + end) + + it( + "should call opam install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + opam.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.spawn.opam).was_called(1) + assert.spy(ctx.spawn.opam).was_called_with(match.tbl_containing { + "install", + "--destdir=.", + "--yes", + "--verbose", + match.tbl_containing { + "main-package.42.13.37", + "supporting-package", + "supporting-package2", + }, + }) + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + opam.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "opam", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "opam", + package = "supporting-package", + }, + { + type = "opam", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) diff --git a/tests/core/managers/pip3_spec.lua b/tests/core/managers/pip3_spec.lua new file mode 100644 index 00000000..704a32e8 --- /dev/null +++ b/tests/core/managers/pip3_spec.lua @@ -0,0 +1,253 @@ +local mock = require "luassert.mock" +local spy = require "luassert.spy" +local match = require "luassert.match" + +local pip3 = require "nvim-lsp-installer.core.managers.pip3" +local Optional = require "nvim-lsp-installer.core.optional" +local Result = require "nvim-lsp-installer.core.result" +local settings = require "nvim-lsp-installer.settings" +local spawn = require "nvim-lsp-installer.core.spawn" + +describe("pip3 manager", function() + ---@type InstallContext + local ctx + before_each(function() + ctx = InstallContextGenerator { + spawn = mock.new { + python3 = mockx.returns {}, + }, + } + end) + + it("normalizes pip3 packages", function() + local normalize = pip3.normalize_package + assert.equal("python-lsp-server", normalize "python-lsp-server[all]") + assert.equal("python-lsp-server", normalize "python-lsp-server[]") + assert.equal("python-lsp-server", normalize "python-lsp-server[[]]") + end) + + it( + "should create venv and call pip3 install", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + pip3.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.spy(ctx.promote_cwd).was_called(1) + assert.spy(ctx.spawn.python3).was_called(2) + assert.spy(ctx.spawn.python3).was_called_with { + "-m", + "venv", + "venv", + } + assert.spy(ctx.spawn.python3).was_called_with(match.tbl_containing { + "-m", + "pip", + "install", + "-U", + match.table(), + match.tbl_containing { + "main-package==42.13.37", + "supporting-package", + "supporting-package2", + }, + env = match.is_table(), + }) + end) + ) + + it( + "should exhaust python3 executable candidates if all fail", + async_test(function() + vim.g.python3_host_prog = "/my/python3" + ctx.spawn = mock.new { + python3 = mockx.throws(), + python = mockx.throws(), + [vim.g.python3_host_prog] = mockx.throws(), + } + local err = assert.has_error(function() + pip3.packages { "package" }(ctx) + end) + vim.g.python3_host_prog = nil + + assert.equals("Unable to create python3 venv environment.", err) + assert.spy(ctx.spawn["/my/python3"]).was_called(1) + assert.spy(ctx.spawn.python3).was_called(1) + assert.spy(ctx.spawn.python).was_called(1) + end) + ) + + it( + "should not exhaust python3 executable if one succeeds", + async_test(function() + vim.g.python3_host_prog = "/my/python3" + ctx.spawn = mock.new { + python3 = mockx.throws(), + python = mockx.throws(), + [vim.g.python3_host_prog] = mockx.returns {}, + } + pip3.packages { "package" }(ctx) + vim.g.python3_host_prog = nil + assert.spy(ctx.spawn.python3).was_called(0) + assert.spy(ctx.spawn.python).was_called(0) + assert.spy(ctx.spawn["/my/python3"]).was_called() + end) + ) + + it( + "should use install_args from settings", + async_test(function() + settings.set { + pip = { + install_args = { "--proxy", "http://localhost:8080" }, + }, + } + pip3.packages { "package" }(ctx) + settings.set(settings._DEFAULT_SETTINGS) + assert.spy(ctx.spawn.python3).was_called_with(match.tbl_containing { + "-m", + "pip", + "install", + "-U", + match.tbl_containing { "--proxy", "http://localhost:8080" }, + match.tbl_containing { "package" }, + env = match.is_table(), + }) + end) + ) + + it( + "should provide receipt information", + async_test(function() + ctx.requested_version = Optional.of "42.13.37" + pip3.packages { "main-package", "supporting-package", "supporting-package2" }(ctx) + assert.equals( + vim.inspect { + type = "pip3", + package = "main-package", + }, + vim.inspect(ctx.receipt.primary_source) + ) + assert.equals( + vim.inspect { + { + type = "pip3", + package = "supporting-package", + }, + { + type = "pip3", + package = "supporting-package2", + }, + }, + vim.inspect(ctx.receipt.secondary_sources) + ) + end) + ) +end) + +describe("pip3 version check", function() + it( + "should return current version", + async_test(function() + spawn.python = spy.new(function() + return Result.success { + stdout = [[ + [{"name": "astroid", "version": "2.9.3"}, {"name": "mccabe", "version": "0.6.1"}, {"name": "python-lsp-server", "version": "1.3.0", "latest_version": "1.4.0", "latest_filetype": "wheel"}, {"name": "wrapt", "version": "1.13.3", "latest_version": "1.14.0", "latest_filetype": "wheel"}] + ]], + } + end) + + local result = pip3.get_installed_primary_package_version( + mock.new { + primary_source = mock.new { + type = "pip3", + package = "python-lsp-server", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.python).was_called(1) + assert.spy(spawn.python).was_called_with(match.tbl_containing { + "-m", + "pip", + "list", + "--format=json", + cwd = "/tmp/install/dir", + env = match.table(), + }) + assert.is_true(result:is_success()) + assert.equals("1.3.0", result:get_or_nil()) + + spawn.python = nil + end) + ) + + it( + "should return outdated primary package", + async_test(function() + spawn.python = spy.new(function() + return Result.success { + stdout = [[ +[{"name": "astroid", "version": "2.9.3", "latest_version": "2.11.0", "latest_filetype": "wheel"}, {"name": "mccabe", "version": "0.6.1", "latest_version": "0.7.0", "latest_filetype": "wheel"}, {"name": "python-lsp-server", "version": "1.3.0", "latest_version": "1.4.0", "latest_filetype": "wheel"}, {"name": "wrapt", "version": "1.13.3", "latest_version": "1.14.0", "latest_filetype": "wheel"}] + ]], + } + end) + + local result = pip3.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "pip3", + package = "python-lsp-server", + }, + }, + "/tmp/install/dir" + ) + + assert.spy(spawn.python).was_called(1) + assert.spy(spawn.python).was_called_with(match.tbl_containing { + "-m", + "pip", + "list", + "--outdated", + "--format=json", + cwd = "/tmp/install/dir", + env = match.table(), + }) + assert.is_true(result:is_success()) + assert.equals( + vim.inspect { + name = "python-lsp-server", + current_version = "1.3.0", + latest_version = "1.4.0", + }, + vim.inspect(result:get_or_nil()) + ) + + spawn.python = nil + end) + ) + + it( + "should return failure if primary package is not outdated", + async_test(function() + spawn.python = spy.new(function() + return Result.success { + stdout = "[]", + } + end) + + local result = pip3.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "pip3", + package = "python-lsp-server", + }, + }, + "/tmp/install/dir" + ) + + assert.is_true(result:is_failure()) + assert.equals("Primary package is not outdated.", result:err_or_nil()) + spawn.python = nil + end) + ) +end) diff --git a/tests/core/optional_spec.lua b/tests/core/optional_spec.lua new file mode 100644 index 00000000..02f8800e --- /dev/null +++ b/tests/core/optional_spec.lua @@ -0,0 +1,63 @@ +local Optional = require "nvim-lsp-installer.core.optional" +local spy = require "luassert.spy" + +describe("Optional.of_nilable", function() + it("should create empty optionals", function() + local empty = Optional.empty() + assert.is_false(empty:is_present()) + end) + + it("should create non-empty optionals", function() + local empty = Optional.of_nilable "value" + assert.is_true(empty:is_present()) + end) + + it("should use memoized empty value", function() + assert.is_true(Optional.empty() == Optional.empty()) + end) +end) + +describe("Optional.get()", function() + it("should map non-empty values", function() + local str = Optional.of_nilable("world!") + :map(function(val) + return "Hello " .. val + end) + :get() + assert.equals("Hello world!", str) + end) + + it("should raise error when getting empty value", function() + local err = assert.has_error(function() + Optional.empty():get() + end) + assert.equals("No value present.", err) + end) +end) + +describe("Optional.or_else()", function() + it("should use .or_else() value if empty", function() + local value = Optional.empty():or_else "Hello!" + assert.equals("Hello!", value) + end) + + it("should not use .or_else() value if not empty", function() + local value = Optional.of_nilable("Good bye!"):or_else "Hello!" + assert.equals("Good bye!", value) + end) +end) + +describe("Optional.if_present()", function() + it("should not call .if_present() if value is empty", function() + local present = spy.new() + Optional.empty():if_present(present) + assert.spy(present).was_not_called() + end) + + it("should call .if_present() if value is not empty", function() + local present = spy.new() + Optional.of_nilable("value"):if_present(present) + assert.spy(present).was_called(1) + assert.spy(present).was_called_with "value" + end) +end) diff --git a/tests/core/result_spec.lua b/tests/core/result_spec.lua index 725041de..cd10acb5 100644 --- a/tests/core/result_spec.lua +++ b/tests/core/result_spec.lua @@ -85,4 +85,36 @@ describe("result", function() assert.is_true(mapped:is_failure()) assert.is_true(match.has_match "This is an error$"(mapped:err_or_nil())) end) + + it("should recover errors", function() + local result = Result.failure("call an ambulance"):recover(function(err) + return err .. ". but not for me!" + end) + assert.is_true(result:is_success()) + assert.equals("call an ambulance. but not for me!", result:get_or_nil()) + end) + + it("should catch errors in recover", function() + local result = Result.failure("call an ambulance"):recover_catching(function(err) + error("Oh no... " .. err, 2) + end) + assert.is_true(result:is_failure()) + assert.equals("Oh no... call an ambulance", result:err_or_nil()) + end) + + it("should return results in run_catching", function() + local result = Result.run_catching(function() + return "Hello world!" + end) + assert.is_true(result:is_success()) + assert.equals("Hello world!", result:get_or_nil()) + end) + + it("should return failures in run_catching", function() + local result = Result.run_catching(function() + error("Oh noes", 2) + end) + assert.is_true(result:is_failure()) + assert.equals("Oh noes", result:err_or_nil()) + end) end) diff --git a/tests/core/async/spawn_spec.lua b/tests/core/spawn_spec.lua index 721e4d08..3bfafbc3 100644 --- a/tests/core/async/spawn_spec.lua +++ b/tests/core/spawn_spec.lua @@ -1,4 +1,4 @@ -local spawn = require "nvim-lsp-installer.core.async.spawn" +local spawn = require "nvim-lsp-installer.core.spawn" local process = require "nvim-lsp-installer.process" describe("async spawn", function() @@ -44,4 +44,36 @@ describe("async spawn", function() assert.equals("", result:get_or_nil().stderr) end) ) + + it( + "should ignore vim.NIL args", + async_test(function() + local result = spawn.bash { + vim.NIL, + spawn._when(true, "-c"), + spawn._when(false, "shouldnotbeincluded"), + vim.NIL, + 'echo "Hello $VAR"', + env = { "VAR=world" }, + } + + assert.is_true(result:is_success()) + assert.equals("Hello world\n", result:get_or_nil().stdout) + assert.equals("", result:get_or_nil().stderr) + end) + ) + + it( + "should flatten table args", + async_test(function() + local result = spawn.bash { + { "-c", 'echo "Hello $VAR"' }, + env = { "VAR=world" }, + } + + assert.is_true(result:is_success()) + assert.equals("Hello world\n", result:get_or_nil().stdout) + assert.equals("", result:get_or_nil().stderr) + end) + ) end) diff --git a/tests/helpers/lua/luassertx.lua b/tests/helpers/lua/luassertx.lua new file mode 100644 index 00000000..06d08ede --- /dev/null +++ b/tests/helpers/lua/luassertx.lua @@ -0,0 +1,62 @@ +local assert = require "luassert" +local match = require "luassert.match" +local a = require "nvim-lsp-installer.core.async" + +local function wait_for(_, arguments) + ---@type fun() @Function to execute until it does not error. + local assertions_fn = arguments[1] + ---@type number @Timeout in milliseconds. Defaults to 5000. + local timeout = arguments[2] + timeout = timeout or 15000 + + local start = vim.loop.hrtime() + local is_ok, err + repeat + is_ok, err = pcall(assertions_fn) + if not is_ok then + a.sleep(math.min(timeout, 100)) + end + until is_ok or ((vim.loop.hrtime() - start) / 1e6) > timeout + + if not is_ok then + error(err) + end + + return is_ok +end + +local function tbl_containing(_, arguments, _) + return function(value) + local expected = arguments[1] + for key, val in pairs(expected) do + if match.is_matcher(val) then + if not val(value[key]) then + return false + end + elseif value[key] ~= val then + return false + end + end + return true + end +end + +local function list_containing(_, arguments, _) + return function(value) + local expected = arguments[1] + for _, val in pairs(value) do + if match.is_matcher(expected) then + if expected(val) then + return true + end + elseif expected == val then + return true + end + end + return false + end +end + +assert:register("matcher", "tbl_containing", tbl_containing) +assert:register("matcher", "list_containing", list_containing) +assert:register("assertion", "wait_for", wait_for) diff --git a/tests/helpers/lua/test_helpers.lua b/tests/helpers/lua/test_helpers.lua new file mode 100644 index 00000000..23353fcb --- /dev/null +++ b/tests/helpers/lua/test_helpers.lua @@ -0,0 +1,75 @@ +---@diagnostic disable: lowercase-global +local mock = require "luassert.mock" +local util = require "luassert.util" + +local a = require "nvim-lsp-installer.core.async" +local process = require "nvim-lsp-installer.process" +local server = require "nvim-lsp-installer.server" +local Optional = require "nvim-lsp-installer.core.optional" +local Result = require "nvim-lsp-installer.core.result" +local receipt = require "nvim-lsp-installer.core.receipt" + +function async_test(suspend_fn) + return function() + local ok, err = pcall(a.run_blocking, suspend_fn) + if not ok then + error(err, util.errorlevel()) + end + end +end + +mockx = { + just_runs = function() end, + returns = function(val) + return function() + return val + end + end, + throws = function(exception) + return function() + error(exception, 2) + end + end, +} + +function ServerGenerator(opts) + return server.Server:new(vim.tbl_deep_extend("force", { + name = "dummy", + languages = { "dummylang" }, + root_dir = server.get_server_root_path "dummy", + homepage = "https://dummylang.org", + installer = function(_, callback, ctx) + ctx.stdio_sink.stdout "Installing dummy!\n" + callback(true) + end, + }, opts)) +end + +function FailingServerGenerator(opts) + return ServerGenerator(vim.tbl_deep_extend("force", { + installer = function(_, callback, ctx) + ctx.stdio_sink.stdout "Installing failing dummy!\n" + callback(false) + end, + }, opts)) +end + +function InstallContextGenerator(opts) + ---@type InstallContext + local default_opts = { + fs = mock.new { + append_file = mockx.just_runs, + dir_exists = mockx.returns(true), + file_exists = mockx.returns(true), + }, + spawn = mock.new {}, + cwd = function() + return "/tmp/install-dir" + end, + promote_cwd = mockx.returns(Result.success()), + receipt = receipt.InstallReceiptBuilder.new(), + requested_version = Optional.empty(), + } + local merged_opts = vim.tbl_deep_extend("force", default_opts, opts) + return mock.new(merged_opts) +end diff --git a/tests/jobs/outdated-servers/cargo_spec.lua b/tests/jobs/outdated-servers/cargo_spec.lua deleted file mode 100644 index f99e6b7c..00000000 --- a/tests/jobs/outdated-servers/cargo_spec.lua +++ /dev/null @@ -1,32 +0,0 @@ -local cargo_check = require "nvim-lsp-installer.jobs.outdated-servers.cargo" - -describe("cargo outdated package checker", function() - it("parses cargo installed packages output", function() - assert.equal( - vim.inspect { - ["bat"] = "0.18.3", - ["exa"] = "0.10.1", - ["git-select-branch"] = "0.1.1", - ["hello_world"] = "0.0.1", - ["rust-analyzer"] = "0.0.0", - ["stylua"] = "0.11.2", - ["zoxide"] = "0.5.0", - }, - vim.inspect(cargo_check.parse_installed_crates [[bat v0.18.3: - bat -exa v0.10.1: - exa -git-select-branch v0.1.1: - git-select-branch -hello_world v0.0.1 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.LGbguATJHj): - hello_world -rust-analyzer v0.0.0 (/private/var/folders/ky/s6yyhm_d24d0jsrql4t8k4p40000gn/T/tmp.YlsHeA9JVL/crates/rust-analyzer): - rust-analyzer -stylua v0.11.2: - stylua -zoxide v0.5.0: - zoxide -]]) - ) - end) -end) diff --git a/tests/jobs/outdated-servers/gem_spec.lua b/tests/jobs/outdated-servers/gem_spec.lua deleted file mode 100644 index eb2ce086..00000000 --- a/tests/jobs/outdated-servers/gem_spec.lua +++ /dev/null @@ -1,45 +0,0 @@ -local gem_check = require "nvim-lsp-installer.jobs.outdated-servers.gem" - -describe("gem outdated package checker", function() - it("parses outdated gem output", function() - local normalize = gem_check.parse_outdated_gem - assert.equal( - vim.inspect { - name = "solargraph", - current_version = "0.42.2", - latest_version = "0.44.2", - }, - vim.inspect(normalize [[solargraph (0.42.2 < 0.44.2)]]) - ) - assert.equal( - vim.inspect { - name = "sorbet-runtime", - current_version = "0.5.9307", - latest_version = "0.5.9468", - }, - vim.inspect(normalize [[sorbet-runtime (0.5.9307 < 0.5.9468)]]) - ) - end) - - it("returns nil when unable to parse outdated gem", function() - assert.is_nil(gem_check.parse_outdated_gem "a whole bunch of gibberish!") - assert.is_nil(gem_check.parse_outdated_gem "") - end) - - it("should parse gem list output", function() - assert.equals( - vim.inspect { - ["solargraph"] = "0.44.3", - ["unicode-display_width"] = "2.1.0", - }, - vim.inspect(gem_check.parse_gem_list_output [[ - -*** LOCAL GEMS *** - -nokogiri (1.13.3 arm64-darwin) -solargraph (0.44.3) -unicode-display_width (2.1.0) -]]) - ) - end) -end) diff --git a/tests/jobs/outdated-servers/pip3_spec.lua b/tests/jobs/outdated-servers/pip3_spec.lua deleted file mode 100644 index f22bb7e1..00000000 --- a/tests/jobs/outdated-servers/pip3_spec.lua +++ /dev/null @@ -1,10 +0,0 @@ -local pip3_check = require "nvim-lsp-installer.jobs.outdated-servers.pip3" - -describe("pip3 outdated package checker", function() - it("normalizes pip3 packages", function() - local normalize = pip3_check.normalize_package - assert.equal("python-lsp-server", normalize "python-lsp-server[all]") - assert.equal("python-lsp-server", normalize "python-lsp-server[]") - assert.equal("python-lsp-server", normalize "python-lsp-server[[]]") - end) -end) diff --git a/tests/luassertx/lua/luassertx.lua b/tests/luassertx/lua/luassertx.lua deleted file mode 100644 index 0722a6d7..00000000 --- a/tests/luassertx/lua/luassertx.lua +++ /dev/null @@ -1,38 +0,0 @@ -local a = require "nvim-lsp-installer.core.async" -local assert = require "luassert" - -local util = require "luassert.util" - -function async_test(suspend_fn) - return function() - local ok, err = pcall(a.run_blocking, suspend_fn) - if not ok then - error(err, util.errorlevel()) - end - end -end - -local function wait_for(_, arguments) - ---@type fun() @Function to execute until it does not error. - local assertions_fn = arguments[1] - ---@type number @Timeout in milliseconds. Defaults to 5000. - local timeout = arguments[2] - timeout = timeout or 15000 - - local start = vim.loop.hrtime() - local is_ok, err - repeat - is_ok, err = pcall(assertions_fn) - if not is_ok then - a.sleep(math.min(timeout, 100)) - end - until is_ok or ((vim.loop.hrtime() - start) / 1e6) > timeout - - if not is_ok then - error(err) - end - - return is_ok -end - -assert:register("assertion", "wait_for", wait_for) diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim index 35bc2bcb..9af729b0 100644 --- a/tests/minimal_init.vim +++ b/tests/minimal_init.vim @@ -4,41 +4,16 @@ set directory="" set noswapfile let $lsp_installer = getcwd() -let $luassertx_rtp = getcwd() .. "/tests/luassertx" +let $test_helpers = getcwd() .. "/tests/helpers" let $dependencies = getcwd() .. "/dependencies" -set rtp+=$lsp_installer,$luassertx_rtp +set rtp+=$lsp_installer,$test_helpers set packpath=$dependencies packloadall -lua <<EOF -local server = require("nvim-lsp-installer.server") -function ServerGenerator(opts) - return server.Server:new(vim.tbl_deep_extend("force", { - name = "dummy", - languages = { "dummylang" }, - root_dir = server.get_server_root_path("dummy"), - homepage = "https://dummylang.org", - installer = function(_, callback, ctx) - ctx.stdio_sink.stdout "Installing dummy!\n" - callback(true) - end - }, opts)) -end - -function FailingServerGenerator(opts) - return ServerGenerator(vim.tbl_deep_extend("force", { - installer = function(_, callback, ctx) - ctx.stdio_sink.stdout "Installing failing dummy!\n" - callback(false) - end - }, opts)) -end -EOF - -" Luassert extensions lua require("luassertx") +lua require("test_helpers") lua <<EOF require("nvim-lsp-installer").settings { diff --git a/tests/server_spec.lua b/tests/server_spec.lua index 81ae83b0..41a40750 100644 --- a/tests/server_spec.lua +++ b/tests/server_spec.lua @@ -55,6 +55,28 @@ describe("server", function() end) ) + -- it( + -- "should be able to run async installer functions", + -- async_test(function() + -- local srv = ServerGenerator { + -- name = "async_installer_fixture", + -- root_dir = server.get_server_root_path "async_installer_fixture", + -- async = true, + -- installer = function() + -- a.sleep(130) + -- end, + -- } + -- local start = timestamp() + -- srv:install() + -- a.sleep(100) + -- assert.wait_for(function() + -- assert.is_true(srv:is_installed()) + -- end) + -- local stop = timestamp() + -- assert.is_true(stop - start >= 100) + -- end) + -- ) + it( "should remove directories upon installation failure", async_test(function() |
