diff options
| -rw-r--r-- | lua/mason-core/managers/cargo/init.lua | 167 | ||||
| -rw-r--r-- | lua/mason-core/managers/github/client.lua | 26 | ||||
| -rw-r--r-- | lua/mason-core/optional.lua | 10 | ||||
| -rw-r--r-- | lua/mason-core/result.lua | 9 | ||||
| -rw-r--r-- | lua/mason-registry/flux-lsp/init.lua | 7 | ||||
| -rw-r--r-- | lua/mason-registry/move-analyzer/init.lua | 4 | ||||
| -rw-r--r-- | lua/mason-registry/wgsl-analyzer/init.lua | 9 | ||||
| -rw-r--r-- | tests/mason-core/managers/cargo_spec.lua | 172 | ||||
| -rw-r--r-- | tests/mason-core/managers/github_client_spec.lua | 21 | ||||
| -rw-r--r-- | tests/mason-core/optional_spec.lua | 17 | ||||
| -rw-r--r-- | tests/mason-core/result_spec.lua | 13 |
11 files changed, 369 insertions, 86 deletions
diff --git a/lua/mason-core/managers/cargo/init.lua b/lua/mason-core/managers/cargo/init.lua index b8cd4fa7..8890daf6 100644 --- a/lua/mason-core/managers/cargo/init.lua +++ b/lua/mason-core/managers/cargo/init.lua @@ -5,6 +5,8 @@ local a = require "mason-core.async" local Optional = require "mason-core.optional" local installer = require "mason-core.installer" local client = require "mason-core.managers.cargo.client" +local github = require "mason-core.managers.github" +local github_client = require "mason-core.managers.github.client" local _ = require "mason-core.functional" local get_bin_path = _.compose(path.concat, function(executable) @@ -23,32 +25,50 @@ local M = {} ---@async ---@param crate string The crate to install. ----@param opts {git: boolean | string, features: string?, bin: string[]? }? +---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }? function M.crate(crate, opts) return function() - M.install(crate, opts).with_receipt() + if opts and opts.git and opts.git.tag then + local ctx = installer.context() + local repo = assert(opts.git.url:match "^https://github%.com/(.+)$", "git url needs to be github.com") + local source = github.tag { repo = repo } + source.with_receipt() + ctx.requested_version = Optional.of(source.tag) + M.install(crate, opts) + else + M.install(crate, opts).with_receipt() + end end end ---@async ---@param crate string The crate to install. ----@param opts {git: boolean | string, features: string?, bin: string[]? }? +---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }? function M.install(crate, opts) local ctx = installer.context() 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) - ---@type string | string[] - local final_crate = crate + local version if opts.git then - final_crate = { "--git" } - if type(opts.git) == "string" then - table.insert(final_crate, opts.git) + if opts.git.tag then + assert(ctx.requested_version:is_present(), "version is required when installing tagged git crate.") end - table.insert(final_crate, crate) + version = ctx.requested_version + :map(function(version) + if opts.git.tag then + return { "--tag", version } + else + return { "--rev", version } + end + end) + :or_else(vim.NIL) + else + version = ctx.requested_version + :map(function(version) + return { "--version", version } + end) + :or_else(vim.NIL) end ctx.spawn.cargo { @@ -56,13 +76,10 @@ function M.install(crate, opts) "--root", ".", "--locked", - ctx.requested_version - :map(function(version) - return { "--version", version } - end) - :or_else(vim.NIL), + version, + opts.git and { "--git", opts.git.url } or vim.NIL, opts.features and { "--features", opts.features } or vim.NIL, - final_crate, + crate, } if opts.bin then @@ -76,42 +93,39 @@ function M.install(crate, opts) } end +---@alias InstalledCrate { name: string, version: string, github_ref: { owner: string, repo: string, ref: string }? } + +---@param line string +---@return InstalledCrate? crate +local function parse_installed_crate(line) + local name, version, context = line:match "^(.+)%s+v([^%s:]+) ?(.*):$" + if context then + local owner, repo, ref = context:match "^%(https://github%.com/(.+)/([^?]+).*#(.+)%)$" + if ref then + return { name = name, version = ref, github_ref = { owner = owner, repo = repo, ref = ref } } + end + end + if name and version then + return { name = name, version = version } + end +end + ---@param output string The `cargo install --list` output. ----@return table<string, string> # Key is the crate name, value is its version. +---@return table<string, InstalledCrate> # 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 + local installed_crate = parse_installed_crate(line) + if installed_crate then + installed_crates[installed_crate.name] = installed_crate end end return installed_crates end ---@async ----@param receipt InstallReceipt<InstallReceiptPackageSource> ---@param install_dir string -function M.check_outdated_primary_package(receipt, install_dir) - return M.get_installed_primary_package_version(receipt, install_dir):map_catching(function(installed_version) - ---@type CrateResponse - local crate_response = client.fetch_crate(receipt.primary_source.package):get_or_throw() - if installed_version ~= crate_response.crate.max_stable_version then - return { - name = receipt.primary_source.package, - current_version = installed_version, - latest_version = crate_response.crate.max_stable_version, - } - else - error "Primary package is not outdated." - end - end) -end - ----@async ----@param receipt InstallReceipt<InstallReceiptPackageSource> ----@param install_dir string -function M.get_installed_primary_package_version(receipt, install_dir) +local function get_installed_crates(install_dir) return spawn .cargo({ "install", @@ -121,13 +135,68 @@ function M.get_installed_primary_package_version(receipt, install_dir) 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 pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t") - return Optional.of_nilable(installed_crates[pkg]):or_else_throw "Failed to find cargo package version." + return M.parse_installed_crates(result.stdout) end) end +---@async +---@param receipt InstallReceipt<InstallReceiptPackageSource> +---@param install_dir string +function M.check_outdated_primary_package(receipt, install_dir) + if vim.in_fast_event() then + a.scheduler() + end + local crate_name = vim.fn.fnamemodify(receipt.primary_source.package, ":t") + return get_installed_crates(install_dir) + :ok() + :map(_.prop(crate_name)) + :map( + ---@param installed_crate InstalledCrate + function(installed_crate) + if installed_crate.github_ref then + ---@type GitHubCommit + local latest_commit = github_client + .fetch_commits( + ("%s/%s"):format(installed_crate.github_ref.owner, installed_crate.github_ref.repo), + { page = 1, per_page = 1 } + ) + :get_or_throw("Failed to fetch latest commits.")[1] + if not vim.startswith(latest_commit.sha, installed_crate.github_ref.ref) then + return { + name = receipt.primary_source.package, + current_version = installed_crate.github_ref.ref, + latest_version = latest_commit.sha, + } + end + else + ---@type CrateResponse + local crate_response = client.fetch_crate(crate_name):get_or_throw() + if installed_crate.version ~= crate_response.crate.max_stable_version then + return { + name = receipt.primary_source.package, + current_version = installed_crate.version, + latest_version = crate_response.crate.max_stable_version, + } + end + end + end + ) + :ok_or(_.always "Primary package is not outdated.") +end + +---@async +---@param receipt InstallReceipt<InstallReceiptPackageSource> +---@param install_dir string +function M.get_installed_primary_package_version(receipt, install_dir) + return get_installed_crates(install_dir):map(function(pkgs) + if vim.in_fast_event() then + a.scheduler() + end + local pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t") + return Optional.of_nilable(pkgs[pkg]) + :map(_.prop "version") + :or_else_throw "Failed to find cargo package version." + end) +end + return M diff --git a/lua/mason-core/managers/github/client.lua b/lua/mason-core/managers/github/client.lua index 8f4545e8..788b402b 100644 --- a/lua/mason-core/managers/github/client.lua +++ b/lua/mason-core/managers/github/client.lua @@ -8,10 +8,16 @@ local M = {} ---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer} ---@alias GitHubRelease {tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[]} ---@alias GitHubTag {name: string} +---@alias GitHubCommit {sha: string} ---@param path string +---@param opts { params: table<string, any>? }? ---@return Result # JSON decoded response. -local function api_call(path) +local function api_call(path, opts) + if opts and opts.params then + local params = _.join("&", _.map(_.join "=", _.sort_by(_.head, _.to_pairs(opts.params)))) + path = ("%s?%s"):format(path, params) + end return spawn .gh({ "api", path, env = { CLICOLOR_FORCE = 0 } }) :map(_.prop "stdout") @@ -21,6 +27,8 @@ local function api_call(path) :map_catching(vim.json.decode) end +M.api_call = api_call + ---@async ---@param repo string The GitHub repo ("username/repo"). ---@return Result # Result<GitHubRelease[]> @@ -105,6 +113,22 @@ function M.fetch_latest_tag(repo) :map(_.prop "tag") end +---@async +---@param repo string The GitHub repo ("username/repo"). +---@param opts { page: integer?, per_page: integer? }? +---@return Result # Result<GitHubCommit[]> +function M.fetch_commits(repo, opts) + local path = ("repos/%s/commits"):format(repo) + return api_call(path, { + params = { + page = opts and opts.page or 1, + per_page = opts and opts.per_page or 30, + }, + }):map_err(function() + return ("Failed to fetch commits for GitHub repository %s."):format(repo) + end) +end + ---@alias GitHubRateLimit {limit: integer, remaining: integer, reset: integer, used: integer} ---@alias GitHubRateLimitResponse {resources: { core: GitHubRateLimit }} diff --git a/lua/mason-core/optional.lua b/lua/mason-core/optional.lua index 65317b7f..6eb0719e 100644 --- a/lua/mason-core/optional.lua +++ b/lua/mason-core/optional.lua @@ -105,4 +105,14 @@ function Optional:is_present() return self._value ~= nil end +---@param err fun(): any +function Optional:ok_or(err) + local Result = require "mason-core.result" + if self:is_present() then + return Result.success(self:get()) + else + return Result.failure(err()) + end +end + return Optional diff --git a/lua/mason-core/result.lua b/lua/mason-core/result.lua index f2efd36a..95c69afa 100644 --- a/lua/mason-core/result.lua +++ b/lua/mason-core/result.lua @@ -138,6 +138,15 @@ function Result:on_success(fn) return self end +function Result:ok() + local Optional = require "mason-core.optional" + if self:is_success() then + return Optional.of(self.value) + else + return Optional.empty() + end +end + ---@param fn fun(): any ---@return Result function Result.run_catching(fn) diff --git a/lua/mason-registry/flux-lsp/init.lua b/lua/mason-registry/flux-lsp/init.lua index 2568f951..afef8c72 100644 --- a/lua/mason-registry/flux-lsp/init.lua +++ b/lua/mason-registry/flux-lsp/init.lua @@ -7,8 +7,11 @@ return Pkg.new { homepage = "https://github.com/influxdata/flux-lsp", languages = { Pkg.Lang.Flux }, categories = { Pkg.Cat.LSP }, - install = cargo.crate("https://github.com/influxdata/flux-lsp", { - git = true, + install = cargo.crate("flux-lsp", { + git = { + url = "https://github.com/influxdata/flux-lsp", + tag = true, + }, bin = { "flux-lsp" }, }), } diff --git a/lua/mason-registry/move-analyzer/init.lua b/lua/mason-registry/move-analyzer/init.lua index 7ef589d7..e7e15093 100644 --- a/lua/mason-registry/move-analyzer/init.lua +++ b/lua/mason-registry/move-analyzer/init.lua @@ -8,7 +8,9 @@ return Pkg.new { languages = { Pkg.Lang.Move }, categories = { Pkg.Cat.LSP }, install = cargo.crate("move-analyzer", { - git = "https://github.com/move-language/move", + git = { + url = "https://github.com/move-language/move", + }, bin = { "move-analyzer" }, }), } diff --git a/lua/mason-registry/wgsl-analyzer/init.lua b/lua/mason-registry/wgsl-analyzer/init.lua index 965d4a01..c5ff2bb8 100644 --- a/lua/mason-registry/wgsl-analyzer/init.lua +++ b/lua/mason-registry/wgsl-analyzer/init.lua @@ -1,16 +1,17 @@ local Pkg = require "mason-core.package" local cargo = require "mason-core.managers.cargo" -local github_url = "https://github.com/wgsl-analyzer/wgsl-analyzer" - return Pkg.new { name = "wgsl-analyzer", desc = [[A language server implementation for the WGSL shading language]], - homepage = github_url, + homepage = "https://github.com/wgsl-analyzer/wgsl-analyzer", languages = { Pkg.Lang.WGSL }, categories = { Pkg.Cat.LSP }, install = cargo.crate("wgsl_analyzer", { - git = github_url, + git = { + url = "https://github.com/wgsl-analyzer/wgsl-analyzer", + tag = true, + }, bin = { "wgsl_analyzer" }, }), } diff --git a/tests/mason-core/managers/cargo_spec.lua b/tests/mason-core/managers/cargo_spec.lua index ddcb7300..27de7253 100644 --- a/tests/mason-core/managers/cargo_spec.lua +++ b/tests/mason-core/managers/cargo_spec.lua @@ -4,7 +4,9 @@ local match = require "luassert.match" local mock = require "luassert.mock" local installer = require "mason-core.installer" local cargo = require "mason-core.managers.cargo" -local client = require "mason-core.managers.cargo.client" +local github = require "mason-core.managers.github" +local cargo_client = require "mason-core.managers.cargo.client" +local github_client = require "mason-core.managers.github.client" local Result = require "mason-core.result" local spawn = require "mason-core.spawn" local path = require "mason-core.path" @@ -23,6 +25,7 @@ describe("cargo manager", function() ".", "--locked", { "--version", "42.13.37" }, + vim.NIL, -- --git vim.NIL, -- --features "my-crate", } @@ -34,16 +37,17 @@ describe("cargo manager", function() async_test(function() local handle = InstallHandleGenerator "dummy" local ctx = InstallContextGenerator(handle) - installer.run_installer(ctx, cargo.crate("https://my-crate.git", { git = true })) + installer.run_installer(ctx, cargo.crate("my-crate", { git = { url = "https://my-crate.git" } })) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", "--locked", - vim.NIL, - vim.NIL, -- --features + vim.NIL, -- version { "--git", "https://my-crate.git" }, + vim.NIL, -- --features + "my-crate", } end) ) @@ -53,16 +57,17 @@ describe("cargo manager", function() async_test(function() local handle = InstallHandleGenerator "dummy" local ctx = InstallContextGenerator(handle) - installer.run_installer(ctx, cargo.crate("crate-name", { git = "https://my-crate.git" })) + installer.run_installer(ctx, cargo.crate("crate-name", { git = { url = "https://my-crate.git" } })) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", "--locked", - vim.NIL, + vim.NIL, -- version + { "--git", "https://my-crate.git" }, vim.NIL, -- --features - { "--git", "https://my-crate.git", "crate-name" }, + "crate-name", } end) ) @@ -80,6 +85,7 @@ describe("cargo manager", function() ".", "--locked", { "--version", "42.13.37" }, + vim.NIL, -- --git { "--features", "lsp" }, "my-crate", } @@ -87,20 +93,32 @@ describe("cargo manager", function() ) it( - "should not allow combining version with git crate", + "should target tagged git crates", async_test(function() local handle = InstallHandleGenerator "dummy" + stub(github, "tag") + github.tag.returns { tag = "v2.1.1", with_receipt = mockx.just_runs } local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" }) - local err = assert.has_error(function() - installer.run_installer( - ctx, - cargo.crate("my-crate", { - git = true, - }) - ) - end) - assert.equals("Providing a version when installing a git crate is not allowed.", err) - assert.spy(ctx.spawn.cargo).was_called(0) + installer.run_installer( + ctx, + cargo.crate("my-crate", { + git = { + url = "https://github.com/crate/my-crate", + tag = true, + }, + features = "lsp", + }) + ) + assert.spy(ctx.spawn.cargo).was_called_with { + "install", + "--root", + ".", + "--locked", + { "--tag", "v2.1.1" }, + { "--git", "https://github.com/crate/my-crate" }, -- --git + { "--features", "lsp" }, + "my-crate", + } end) ) @@ -122,13 +140,22 @@ describe("cargo version check", function() it("parses cargo installed packages output", function() assert.same( { - ["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", + ["bat"] = { name = "bat", version = "0.18.3" }, + ["exa"] = { name = "exa", version = "0.10.1" }, + ["git-select-branch"] = { name = "git-select-branch", version = "0.1.1" }, + ["hello_world"] = { name = "hello_world", version = "0.0.1" }, + ["rust-analyzer"] = { + name = "rust-analyzer", + version = "187bee0b", + github_ref = { owner = "rust-lang", repo = "rust-analyzer", ref = "187bee0b" }, + }, + ["move-analyzer"] = { + name = "move-analyzer", + version = "3cef7fa8", + github_ref = { owner = "move-language", repo = "move", ref = "3cef7fa8" }, + }, + ["stylua"] = { name = "stylua", version = "0.11.2" }, + ["zoxide"] = { name = "zoxide", version = "0.5.0" }, }, cargo.parse_installed_crates [[bat v0.18.3: bat @@ -138,7 +165,9 @@ 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): +move-analyzer v1.0.0 (https://github.com/move-language/move#3cef7fa8): + move-analyzer +rust-analyzer v0.0.0 (https://github.com/rust-lang/rust-analyzer?tag=2022-09-19#187bee0b): rust-analyzer stylua v0.11.2: stylua @@ -178,7 +207,7 @@ zoxide v0.5.0: cwd = path.package_prefix "dummy", }) assert.is_true(result:is_success()) - assert.equals("0.8.8", result:get_or_nil()) + assert.equals("4e452f07", result:get_or_nil()) spawn.cargo = nil end) @@ -194,8 +223,8 @@ zoxide v0.5.0: ]], } end) - stub(client, "fetch_crate") - client.fetch_crate.returns(Result.success { + stub(cargo_client, "fetch_crate") + cargo_client.fetch_crate.returns(Result.success { crate = { id = "lelwel", max_stable_version = "0.4.2", @@ -232,4 +261,89 @@ zoxide v0.5.0: spawn.cargo = nil end) ) + + it( + "should recognize up-to-date crates", + async_test(function() + spawn.cargo = spy.new(function() + return Result.success { + stdout = [[lelwel v0.4.0: + lelwel-ls +]], + } + end) + stub(cargo_client, "fetch_crate") + cargo_client.fetch_crate.returns(Result.success { + crate = { + id = "lelwel", + max_stable_version = "0.4.0", + max_version = "0.4.0", + newest_version = "0.4.0", + }, + }) + + local result = cargo.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "cargo", + package = "lelwel", + }, + }, + path.package_prefix "dummy" + ) + + assert.is_true(result:is_failure()) + assert.equals("Primary package is not outdated.", result:err_or_nil()) + spawn.cargo = nil + end) + ) + + it( + "should return outdated primary package from git source", + async_test(function() + spawn.cargo = spy.new(function() + return Result.success { + stdout = [[move-analyzer v1.0.0 (https://github.com/move-language/move#3cef7fa8): + move-analyzer +]], + } + end) + + stub(github_client, "fetch_commits") + github_client.fetch_commits + .on_call_with("move-language/move", { page = 1, per_page = 1 }) + .returns(Result.success { + { + sha = "b243f1fb", + }, + }) + + local result = cargo.check_outdated_primary_package( + mock.new { + primary_source = mock.new { + type = "cargo", + package = "move-analyzer", + }, + }, + path.package_prefix "dummy" + ) + + assert.spy(spawn.cargo).was_called(1) + assert.spy(spawn.cargo).was_called_with(match.tbl_containing { + "install", + "--list", + "--root", + ".", + cwd = path.package_prefix "dummy", + }) + assert.is_true(result:is_success()) + assert.is_true(match.tbl_containing { + current_version = "3cef7fa8", + latest_version = "b243f1fb", + name = "move-analyzer", + }(result:get_or_nil())) + + spawn.cargo = nil + end) + ) end) diff --git a/tests/mason-core/managers/github_client_spec.lua b/tests/mason-core/managers/github_client_spec.lua index 11b06880..8dec1ca8 100644 --- a/tests/mason-core/managers/github_client_spec.lua +++ b/tests/mason-core/managers/github_client_spec.lua @@ -1,4 +1,8 @@ +local spy = require "luassert.spy" +local stub = require "luassert.stub" local client = require "mason-core.managers.github.client" +local spawn = require "mason-core.spawn" +local Result = require "mason-core.result" describe("github client", function() ---@type GitHubRelease @@ -38,4 +42,21 @@ describe("github client", function() assert.is_false(predicate(stub_release { prerelease = true })) assert.is_false(predicate(stub_release { draft = true })) end) + + it("should provide query parameters in api calls", function() + stub(spawn, "gh") + spawn.gh.returns(Result.success { stdout = "response data" }) + client.api_call("repos/some/repo", { + params = { + page = 23, + page_limit = 82, + }, + }) + assert.spy(spawn.gh).was_called(1) + assert.spy(spawn.gh).was_called_with { + "api", + "repos/some/repo?page=23&page_limit=82", + env = { CLICOLOR_FORCE = 0 }, + } + end) end) diff --git a/tests/mason-core/optional_spec.lua b/tests/mason-core/optional_spec.lua index 4e6b1325..b1a4f41a 100644 --- a/tests/mason-core/optional_spec.lua +++ b/tests/mason-core/optional_spec.lua @@ -1,4 +1,5 @@ local Optional = require "mason-core.optional" +local Result = require "mason-core.result" local spy = require "luassert.spy" describe("Optional.of_nilable", function() @@ -75,3 +76,19 @@ describe("Optional.if_not_present()", function() assert.spy(present).was_called(1) end) end) + +describe("Optional.ok_or()", function() + it("should return success variant if non-empty", function() + local result = Optional.of_nilable("Hello world!"):ok_or() + assert.is_true(getmetatable(result) == Result) + assert.equals("Hello world!", result:get_or_nil()) + end) + + it("should return failure variant if empty", function() + local result = Optional.empty():ok_or(function() + return "I'm empty." + end) + assert.is_true(getmetatable(result) == Result) + assert.equals("I'm empty.", result:err_or_nil()) + end) +end) diff --git a/tests/mason-core/result_spec.lua b/tests/mason-core/result_spec.lua index 777e268b..737d8c56 100644 --- a/tests/mason-core/result_spec.lua +++ b/tests/mason-core/result_spec.lua @@ -1,6 +1,7 @@ local Result = require "mason-core.result" local match = require "luassert.match" local spy = require "luassert.spy" +local Optional = require "mason-core.optional" describe("result", function() it("should create success", function() @@ -140,4 +141,16 @@ describe("result", function() assert.spy(on_success).was_called(1) assert.spy(on_success).was_called_with "Oh noes" end) + + it("should convert success variants to non-empty optionals", function() + local opt = Result.success("Hello world!"):ok() + assert.is_true(getmetatable(opt) == Optional) + assert.equals("Hello world!", opt:get()) + end) + + it("should convert failure variants to empty optionals", function() + local opt = Result.failure("Hello world!"):ok() + assert.is_true(getmetatable(opt) == Optional) + assert.is_false(opt:is_present()) + end) end) |
