aboutsummaryrefslogtreecommitdiffstats
path: root/tests/mason-core
diff options
context:
space:
mode:
Diffstat (limited to 'tests/mason-core')
-rw-r--r--tests/mason-core/async_spec.lua198
-rw-r--r--tests/mason-core/clients/eclipse_spec.lua10
-rw-r--r--tests/mason-core/fetch_spec.lua106
-rw-r--r--tests/mason-core/fs_spec.lua24
-rw-r--r--tests/mason-core/functional/data_spec.lua28
-rw-r--r--tests/mason-core/functional/function_spec.lua142
-rw-r--r--tests/mason-core/functional/list_spec.lua193
-rw-r--r--tests/mason-core/functional/logic_spec.lua57
-rw-r--r--tests/mason-core/functional/number_spec.lua50
-rw-r--r--tests/mason-core/functional/relation_spec.lua36
-rw-r--r--tests/mason-core/functional/string_spec.lua43
-rw-r--r--tests/mason-core/functional/table_spec.lua51
-rw-r--r--tests/mason-core/functional/type_spec.lua26
-rw-r--r--tests/mason-core/installer_spec.lua100
-rw-r--r--tests/mason-core/managers/cargo_spec.lua226
-rw-r--r--tests/mason-core/managers/composer_spec.lua179
-rw-r--r--tests/mason-core/managers/dotnet_spec.lua35
-rw-r--r--tests/mason-core/managers/gem_spec.lua206
-rw-r--r--tests/mason-core/managers/git_spec.lua181
-rw-r--r--tests/mason-core/managers/github_client_spec.lua41
-rw-r--r--tests/mason-core/managers/go_spec.lua174
-rw-r--r--tests/mason-core/managers/luarocks_spec.lua109
-rw-r--r--tests/mason-core/managers/npm_spec.lua194
-rw-r--r--tests/mason-core/managers/opam_spec.lua51
-rw-r--r--tests/mason-core/managers/pip3_spec.lua242
-rw-r--r--tests/mason-core/optional_spec.lua63
-rw-r--r--tests/mason-core/path_spec.lua23
-rw-r--r--tests/mason-core/platform_spec.lua159
-rw-r--r--tests/mason-core/process_spec.lua28
-rw-r--r--tests/mason-core/result_spec.lua143
-rw-r--r--tests/mason-core/spawn_spec.lua216
-rw-r--r--tests/mason-core/ui_spec.lua301
32 files changed, 3635 insertions, 0 deletions
diff --git a/tests/mason-core/async_spec.lua b/tests/mason-core/async_spec.lua
new file mode 100644
index 00000000..72cae450
--- /dev/null
+++ b/tests/mason-core/async_spec.lua
@@ -0,0 +1,198 @@
+local assert = require "luassert"
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local a = require "mason-core.async"
+local process = require "mason-core.process"
+
+local function timestamp()
+ local seconds, microseconds = vim.loop.gettimeofday()
+ return (seconds * 1000) + math.floor(microseconds / 1000)
+end
+
+describe("async", function()
+ it("should run in blocking mode", function()
+ local start = timestamp()
+ a.run_blocking(function()
+ a.sleep(100)
+ end)
+ local stop = timestamp()
+ local grace_ms = 50
+ assert.is_true((stop - start) >= (100 - grace_ms))
+ end)
+
+ it("should return values in blocking mode", function()
+ local function slow_maths(arg1, arg2)
+ a.sleep(10)
+ return arg1 + arg2 - 42
+ end
+ local value = a.run_blocking(slow_maths, 13, 37)
+ assert.equals(8, value)
+ 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()
+ local success, exit_code = a.promisify(process.spawn)("env", {
+ args = {},
+ env = { "FOO=BAR", "BAR=BAZ" },
+ stdio_sink = stdio.sink,
+ })
+ assert.is_true(success)
+ assert.equals(0, exit_code)
+ assert.equals("FOO=BAR\nBAR=BAZ\n", table.concat(stdio.buffers.stdout, ""))
+ end)
+ )
+
+ it(
+ "should reject callback-style functions",
+ async_test(function()
+ local err = assert.has_error(function()
+ a.promisify(function(arg1, cb)
+ cb(arg1, nil)
+ end, true) "påskmust"
+ end)
+ assert.equals(err, "påskmust")
+ end)
+ )
+
+ it(
+ "should return all values",
+ async_test(function()
+ local val1, val2, val3 = a.wait(function(resolve)
+ resolve(1, 2, 3)
+ end)
+ assert.equals(1, val1)
+ assert.equals(2, val2)
+ assert.equals(3, val3)
+ end)
+ )
+
+ it(
+ "should cancel coroutine",
+ async_test(function()
+ local james_bond = spy.new()
+ local poutine = a.scope(function()
+ a.sleep(100)
+ james_bond()
+ end)()
+ poutine()
+ a.sleep(200)
+ assert.spy(james_bond).was_not.called()
+ end)
+ )
+
+ it(
+ "should raise error if async function raises error",
+ async_test(function()
+ local err = assert.has.errors(a.promisify(function()
+ error "something went wrong"
+ end))
+ assert.is_true(match.has_match "something went wrong$"(err))
+ end)
+ )
+
+ it(
+ "should raise error if async function rejects",
+ async_test(function()
+ local err = assert.has.errors(function()
+ a.wait(function(_, reject)
+ reject "This is an error"
+ end)
+ end)
+ assert.equals("This is an error", err)
+ end)
+ )
+
+ it(
+ "should pass nil arguments to promisified functions",
+ async_test(function()
+ local fn = spy.new(function(_, _, _, _, _, _, _, cb)
+ cb()
+ end)
+ a.promisify(fn)(nil, 2, nil, 4, nil, nil, 7)
+ assert.spy(fn).was_called_with(nil, 2, nil, 4, nil, nil, 7, match.is_function())
+ end)
+ )
+
+ it("should accept yielding non-promise values to parent coroutine context", function()
+ local thread = coroutine.create(function(val)
+ a.run_blocking(function()
+ coroutine.yield(val)
+ end)
+ end)
+ local ok, value = coroutine.resume(thread, 1337)
+ assert.is_true(ok)
+ assert.equals(1337, value)
+ end)
+
+ it(
+ "should run all suspending functions concurrently",
+ async_test(function()
+ local start = timestamp()
+ local function sleep(ms, ret_val)
+ return function()
+ a.sleep(ms)
+ return ret_val
+ end
+ end
+ local one, two, three, four, five = a.wait_all {
+ sleep(100, 1),
+ sleep(100, "two"),
+ sleep(100, "three"),
+ sleep(100, 4),
+ sleep(100, 5),
+ }
+ local grace = 50
+ local delta = timestamp() - start
+ assert.is_true(delta <= (100 + grace))
+ assert.is_true(delta >= (100 - grace))
+ assert.equals(1, one)
+ assert.equals("two", two)
+ assert.equals("three", three)
+ assert.equals(4, four)
+ assert.equals(5, five)
+ end)
+ )
+
+ it(
+ "should run all suspending functions concurrently",
+ async_test(function()
+ local start = timestamp()
+ local called = spy.new()
+ local function sleep(ms, ret_val)
+ return function()
+ a.sleep(ms)
+ called()
+ return ret_val
+ end
+ end
+ local first = a.wait_first {
+ sleep(150, 1),
+ sleep(50, "first"),
+ sleep(150, "three"),
+ sleep(150, 4),
+ sleep(150, 5),
+ }
+ local grace = 20
+ local delta = timestamp() - start
+ assert.is_true(delta <= (50 + grace))
+ assert.equals("first", first)
+ end)
+ )
+end)
diff --git a/tests/mason-core/clients/eclipse_spec.lua b/tests/mason-core/clients/eclipse_spec.lua
new file mode 100644
index 00000000..0ef0864a
--- /dev/null
+++ b/tests/mason-core/clients/eclipse_spec.lua
@@ -0,0 +1,10 @@
+local eclipse = require "mason-core.clients.eclipse"
+
+describe("eclipse client", function()
+ it("parses jdtls version strings", function()
+ assert.equal(
+ "1.8.0-202112170540",
+ eclipse._parse_jdtls_version_string "jdt-language-server-1.8.0-202112170540.tar.gz"
+ )
+ end)
+end)
diff --git a/tests/mason-core/fetch_spec.lua b/tests/mason-core/fetch_spec.lua
new file mode 100644
index 00000000..5385f546
--- /dev/null
+++ b/tests/mason-core/fetch_spec.lua
@@ -0,0 +1,106 @@
+local stub = require "luassert.stub"
+local match = require "luassert.match"
+local fetch = require "mason-core.fetch"
+local spawn = require "mason-core.spawn"
+local Result = require "mason-core.result"
+
+describe("fetch", function()
+ it(
+ "should exhaust all candidates",
+ async_test(function()
+ stub(spawn, "wget")
+ stub(spawn, "curl")
+ spawn.wget.returns(Result.failure "wget failure")
+ spawn.curl.returns(Result.failure "curl failure")
+
+ local result = fetch("https://api.github.com", {
+ headers = { ["X-Custom-Header"] = "here" },
+ })
+ assert.is_true(result:is_failure())
+ assert.spy(spawn.wget).was_called(1)
+ assert.spy(spawn.curl).was_called(1)
+ assert.spy(spawn.wget).was_called_with {
+ {
+ "--header='User-Agent: mason.nvim (+https://github.com/williamboman/mason.nvim)'",
+ "--header='X-Custom-Header: here'",
+ },
+ "-nv",
+ "-O",
+ "-",
+ "--method=GET",
+ vim.NIL, -- body-data
+ "https://api.github.com",
+ }
+
+ assert.spy(spawn.curl).was_called_with(match.tbl_containing {
+ match.same {
+ {
+ "-H",
+ "User-Agent: mason.nvim (+https://github.com/williamboman/mason.nvim)",
+ },
+ {
+ "-H",
+ "X-Custom-Header: here",
+ },
+ },
+ "-fsSL",
+ match.same { "-X", "GET" },
+ vim.NIL, -- data
+ vim.NIL, -- out file
+ "https://api.github.com",
+ on_spawn = match.is_function(),
+ })
+ end)
+ )
+
+ it(
+ "should return stdout",
+ async_test(function()
+ stub(spawn, "wget")
+ spawn.wget.returns(Result.success {
+ stdout = [[{"data": "here"}]],
+ })
+ local result = fetch "https://api.github.com/data"
+ assert.is_true(result:is_success())
+ assert.equals([[{"data": "here"}]], result:get_or_throw())
+ end)
+ )
+
+ it(
+ "should respect out_file opt",
+ async_test(function()
+ stub(spawn, "wget")
+ stub(spawn, "curl")
+ spawn.wget.returns(Result.failure "wget failure")
+ spawn.curl.returns(Result.failure "curl failure")
+ fetch("https://api.github.com/data", { out_file = "/test.json" })
+
+ assert.spy(spawn.wget).was_called_with {
+ {
+ "--header='User-Agent: mason.nvim (+https://github.com/williamboman/mason.nvim)'",
+ },
+ "-nv",
+ "-O",
+ "/test.json",
+ "--method=GET",
+ vim.NIL, -- body-data
+ "https://api.github.com/data",
+ }
+
+ assert.spy(spawn.curl).was_called_with(match.tbl_containing {
+ match.same {
+ {
+ "-H",
+ "User-Agent: mason.nvim (+https://github.com/williamboman/mason.nvim)",
+ },
+ },
+ "-fsSL",
+ match.same { "-X", "GET" },
+ vim.NIL, -- data
+ match.same { "-o", "/test.json" },
+ "https://api.github.com/data",
+ on_spawn = match.is_function(),
+ })
+ end)
+ )
+end)
diff --git a/tests/mason-core/fs_spec.lua b/tests/mason-core/fs_spec.lua
new file mode 100644
index 00000000..c3d5fea7
--- /dev/null
+++ b/tests/mason-core/fs_spec.lua
@@ -0,0 +1,24 @@
+local fs = require "mason-core.fs"
+local mason = require "mason"
+
+describe("fs", function()
+ before_each(function()
+ mason.setup {
+ install_root_dir = "/foo",
+ }
+ end)
+
+ it(
+ "refuses to rmrf paths outside of boundary",
+ async_test(function()
+ local e = assert.has.errors(function()
+ fs.async.rmrf "/thisisa/path"
+ end)
+
+ assert.equal(
+ [[Refusing to rmrf "/thisisa/path" which is outside of the allowed boundary "/foo". Please report this error at https://github.com/williamboman/mason.nvim/issues/new]],
+ e
+ )
+ end)
+ )
+end)
diff --git a/tests/mason-core/functional/data_spec.lua b/tests/mason-core/functional/data_spec.lua
new file mode 100644
index 00000000..e2f8f7ee
--- /dev/null
+++ b/tests/mason-core/functional/data_spec.lua
@@ -0,0 +1,28 @@
+local _ = require "mason-core.functional"
+
+describe("functional: data", function()
+ it("creates enums", function()
+ local colors = _.enum {
+ "BLUE",
+ "YELLOW",
+ }
+ assert.same({
+ ["BLUE"] = "BLUE",
+ ["YELLOW"] = "YELLOW",
+ }, colors)
+ end)
+
+ it("creates sets", function()
+ local colors = _.set_of {
+ "BLUE",
+ "YELLOW",
+ "BLUE",
+ "RED",
+ }
+ assert.same({
+ ["BLUE"] = true,
+ ["YELLOW"] = true,
+ ["RED"] = true,
+ }, colors)
+ end)
+end)
diff --git a/tests/mason-core/functional/function_spec.lua b/tests/mason-core/functional/function_spec.lua
new file mode 100644
index 00000000..8c4b41ef
--- /dev/null
+++ b/tests/mason-core/functional/function_spec.lua
@@ -0,0 +1,142 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local _ = require "mason-core.functional"
+
+describe("functional: function", function()
+ it("curries functions", function()
+ local function sum(...)
+ local res = 0
+ for i = 1, select("#", ...) do
+ res = res + select(i, ...)
+ end
+ return res
+ end
+ local arity0 = _.curryN(sum, 0)
+ local arity1 = _.curryN(sum, 1)
+ local arity2 = _.curryN(sum, 2)
+ local arity3 = _.curryN(sum, 3)
+
+ assert.equals(0, arity0(42))
+ assert.equals(42, arity1(42))
+ assert.equals(3, arity2(1)(2))
+ assert.equals(3, arity2(1, 2))
+ assert.equals(6, arity3(1)(2)(3))
+ assert.equals(6, arity3(1, 2, 3))
+
+ -- should discard superfluous args
+ assert.equals(0, arity1(0, 10, 20, 30))
+ end)
+
+ it("coalesces first non-nil value", function()
+ assert.equal("Hello World!", _.coalesce(nil, nil, "Hello World!", ""))
+ end)
+
+ it("should compose functions", function()
+ local function add(x)
+ return function(y)
+ return y + x
+ end
+ end
+ local function subtract(x)
+ return function(y)
+ return y - x
+ end
+ end
+ local function multiply(x)
+ return function(y)
+ return y * x
+ end
+ end
+
+ local big_maths = _.compose(add(1), subtract(3), multiply(5))
+
+ assert.equals(23, big_maths(5))
+ end)
+
+ it("should not allow composing no functions", function()
+ local e = assert.error(function()
+ _.compose()
+ end)
+ assert.equals("compose requires at least one function", e)
+ end)
+
+ it("should partially apply functions", function()
+ local funcy = spy.new()
+ local partially_funcy = _.partial(funcy, "a", "b", "c")
+ partially_funcy("d", "e", "f")
+ assert.spy(funcy).was_called_with("a", "b", "c", "d", "e", "f")
+ end)
+
+ it("should partially apply functions with nil arguments", function()
+ local funcy = spy.new()
+ local partially_funcy = _.partial(funcy, "a", nil, "c")
+ partially_funcy("d", nil, "f")
+ assert.spy(funcy).was_called_with("a", nil, "c", "d", nil, "f")
+ end)
+
+ it("memoizes functions with default cache mechanism", function()
+ local expensive_function = spy.new(function(s)
+ return s
+ end)
+ local memoized_fn = _.memoize(expensive_function)
+ assert.equal("key", memoized_fn "key")
+ assert.equal("key", memoized_fn "key")
+ assert.equal("new_key", memoized_fn "new_key")
+ assert.spy(expensive_function).was_called(2)
+ end)
+
+ it("memoizes function with custom cache mechanism", function()
+ local expensive_function = spy.new(function(arg1, arg2)
+ return arg1 .. arg2
+ end)
+ local memoized_fn = _.memoize(expensive_function, function(arg1, arg2)
+ return arg1 .. arg2
+ end)
+ assert.equal("key1key2", memoized_fn("key1", "key2"))
+ assert.equal("key1key2", memoized_fn("key1", "key2"))
+ assert.equal("key1key3", memoized_fn("key1", "key3"))
+ assert.spy(expensive_function).was_called(2)
+ end)
+
+ it("should evaluate functions lazily", function()
+ local impl = spy.new(function()
+ return {}, {}
+ end)
+ local lazy_fn = _.lazy(impl)
+ assert.spy(impl).was_called(0)
+ local a, b = lazy_fn()
+ assert.spy(impl).was_called(1)
+ assert.is_true(match.is_table()(a))
+ assert.is_true(match.is_table()(b))
+ local new_a, new_b = lazy_fn()
+ assert.spy(impl).was_called(1)
+ assert.is_true(match.is_ref(a)(new_a))
+ assert.is_true(match.is_ref(b)(new_b))
+ end)
+
+ it("should support nil return values in lazy functions", function()
+ local lazy_fn = _.lazy(function()
+ return nil, 2
+ end)
+ local a, b = lazy_fn()
+ assert.is_nil(a)
+ assert.equal(2, b)
+ end)
+
+ it("should provide identity value", function()
+ local obj = {}
+ assert.equals(2, _.identity(2))
+ assert.equals(obj, _.identity(obj))
+ end)
+
+ it("should always return bound value", function()
+ local obj = {}
+ assert.equals(2, _.always(2)())
+ assert.equals(obj, _.always(obj)())
+ end)
+
+ it("true is true and false is false", function()
+ assert.is_true(_.T())
+ assert.is_false(_.F())
+ end)
+end)
diff --git a/tests/mason-core/functional/list_spec.lua b/tests/mason-core/functional/list_spec.lua
new file mode 100644
index 00000000..999b3625
--- /dev/null
+++ b/tests/mason-core/functional/list_spec.lua
@@ -0,0 +1,193 @@
+local spy = require "luassert.spy"
+local _ = require "mason-core.functional"
+local Optional = require "mason-core.optional"
+
+describe("functional: list", function()
+ it("should produce list without nils", function()
+ assert.same({ 1, 2, 3, 4 }, _.list_not_nil(nil, 1, 2, nil, 3, nil, 4, nil))
+ end)
+
+ it("makes a shallow copy of a list", function()
+ local list = { "BLUE", { nested = "TABLE" }, "RED" }
+ local list_copy = _.list_copy(list)
+ assert.same({ "BLUE", { nested = "TABLE" }, "RED" }, list_copy)
+ assert.is_not.is_true(list == list_copy)
+ assert.is_true(list[2] == list_copy[2])
+ end)
+
+ it("reverses lists", function()
+ local colors = { "BLUE", "YELLOW", "RED" }
+ assert.same({
+ "RED",
+ "YELLOW",
+ "BLUE",
+ }, _.reverse(colors))
+ -- should not modify in-place
+ assert.same({ "BLUE", "YELLOW", "RED" }, colors)
+ end)
+
+ it("maps over list", function()
+ local colors = { "BLUE", "YELLOW", "RED" }
+ assert.same(
+ {
+ "LIGHT_BLUE",
+ "LIGHT_YELLOW",
+ "LIGHT_RED",
+ },
+ _.map(function(color)
+ return "LIGHT_" .. color
+ end, colors)
+ )
+ -- should not modify in-place
+ assert.same({ "BLUE", "YELLOW", "RED" }, colors)
+ end)
+
+ it("filter_map over list", function()
+ local colors = { "BROWN", "BLUE", "YELLOW", "GREEN", "CYAN" }
+ assert.same(
+ {
+ "BROWN EYES",
+ "BLUE EYES",
+ "GREEN EYES",
+ },
+ _.filter_map(function(color)
+ if _.any_pass({ _.equals "BROWN", _.equals "BLUE", _.equals "GREEN" }, color) then
+ return Optional.of(("%s EYES"):format(color))
+ else
+ return Optional.empty()
+ end
+ end, colors)
+ )
+ end)
+
+ it("finds first item that fulfills predicate", function()
+ local predicate = spy.new(function(item)
+ return item == "Waldo"
+ end)
+
+ assert.equal(
+ "Waldo",
+ _.find_first(predicate, {
+ "Where",
+ "On Earth",
+ "Is",
+ "Waldo",
+ "?",
+ })
+ )
+ assert.spy(predicate).was.called(4)
+ end)
+
+ it("determines whether any item in the list fulfills predicate", function()
+ local predicate = spy.new(function(item)
+ return item == "On Earth"
+ end)
+
+ assert.is_true(_.any(predicate, {
+ "Where",
+ "On Earth",
+ "Is",
+ "Waldo",
+ "?",
+ }))
+
+ assert.spy(predicate).was.called(2)
+ end)
+
+ it("should iterate list in .each", function()
+ local list = { "BLUE", "YELLOW", "RED" }
+ local iterate_fn = spy.new()
+ _.each(iterate_fn, list)
+ assert.spy(iterate_fn).was_called(3)
+ assert.spy(iterate_fn).was_called_with("BLUE", 1)
+ assert.spy(iterate_fn).was_called_with("YELLOW", 2)
+ assert.spy(iterate_fn).was_called_with("RED", 3)
+ end)
+
+ it("should concat list tables", function()
+ local list = { "monstera", "tulipa", "carnation" }
+ assert.same({ "monstera", "tulipa", "carnation", "rose", "daisy" }, _.concat(list, { "rose", "daisy" }))
+ assert.same({ "monstera", "tulipa", "carnation" }, list) -- does not mutate list
+ end)
+
+ it("should concat strings", function()
+ assert.equals("FooBar", _.concat("Foo", "Bar"))
+ end)
+
+ it("should zip list into table", function()
+ local fnkey = function() end
+ assert.same({
+ a = "a",
+ [fnkey] = 1,
+ }, _.zip_table({ "a", fnkey }, { "a", 1 }))
+ end)
+
+ it("should get nth item", function()
+ assert.equals("first", _.nth(1, { "first", "middle", "last" }))
+ assert.equals("last", _.nth(-1, { "first", "middle", "last" }))
+ assert.equals("middle", _.nth(-2, { "first", "middle", "last" }))
+ assert.equals("a", _.nth(1, "abc"))
+ assert.equals("c", _.nth(-1, "abc"))
+ assert.equals("b", _.nth(-2, "abc"))
+ assert.is_nil(_.nth(0, { "value" }))
+ assert.equals("", _.nth(0, "abc"))
+ end)
+
+ it("should get length", function()
+ assert.equals(0, _.length {})
+ assert.equals(0, _.length { nil })
+ assert.equals(0, _.length { obj = "doesnt count" })
+ assert.equals(0, _.length "")
+ assert.equals(1, _.length { "" })
+ assert.equals(4, _.length "fire")
+ end)
+
+ it("should sort by comparator", function()
+ local list = {
+ {
+ name = "William",
+ },
+ {
+ name = "Boman",
+ },
+ }
+ assert.same({
+ {
+ name = "Boman",
+ },
+ {
+ name = "William",
+ },
+ }, _.sort_by(_.prop "name", list))
+
+ -- Should not mutate original list
+ assert.same({
+ {
+ name = "William",
+ },
+ {
+ name = "Boman",
+ },
+ }, list)
+ end)
+
+ it("should append to list", function()
+ local list = { "Earth", "Wind" }
+ assert.same({ "Earth", "Wind", { "Fire" } }, _.append({ "Fire" }, list))
+
+ -- Does not mutate original list
+ assert.same({ "Earth", "Wind" }, list)
+ end)
+
+ it("should prepend to list", function()
+ local list = { "Fire" }
+ assert.same({ { "Earth", "Wind" }, "Fire" }, _.prepend({ "Earth", "Wind" }, list))
+
+ -- Does not mutate original list
+ assert.same({ "Fire" }, list)
+ end)
+
+ it("joins lists", function()
+ assert.equals("Hello, John", _.join(", ", { "Hello", "John" }))
+ end)
+end)
diff --git a/tests/mason-core/functional/logic_spec.lua b/tests/mason-core/functional/logic_spec.lua
new file mode 100644
index 00000000..7c795443
--- /dev/null
+++ b/tests/mason-core/functional/logic_spec.lua
@@ -0,0 +1,57 @@
+local spy = require "luassert.spy"
+local _ = require "mason-core.functional"
+
+describe("functional: logic", function()
+ it("should check that all_pass checks that all predicates pass", function()
+ local is_waldo = _.equals "waldo"
+ assert.is_true(_.all_pass { _.T, _.T, is_waldo, _.T } "waldo")
+ assert.is_false(_.all_pass { _.T, _.T, is_waldo, _.F } "waldo")
+ assert.is_false(_.all_pass { _.T, _.T, is_waldo, _.T } "waldina")
+ end)
+
+ it("should check that any_pass checks that any predicates pass", function()
+ local is_waldo = _.equals "waldo"
+ local is_waldina = _.equals "waldina"
+ local is_luigi = _.equals "luigi"
+
+ assert.is_true(_.any_pass { is_waldo, is_waldina } "waldo")
+ assert.is_false(_.any_pass { is_waldina, is_luigi } "waldo")
+ assert.is_true(_.any_pass { is_waldina, is_luigi } "waldina")
+ end)
+
+ it("should branch if_else", function()
+ local a = spy.new()
+ local b = spy.new()
+ _.if_else(_.T, a, b) "a"
+ _.if_else(_.F, a, b) "b"
+ assert.spy(a).was_called(1)
+ assert.spy(a).was_called_with "a"
+ assert.spy(b).was_called(1)
+ assert.spy(b).was_called_with "b"
+ end)
+
+ it("should flip booleans", function()
+ assert.is_true(_.is_not(false))
+ assert.is_false(_.is_not(true))
+ end)
+
+ it("should resolve correct cond", function()
+ local planetary_object = _.cond {
+ {
+ _.equals "Moon!",
+ _.format "to the %s",
+ },
+ {
+ _.equals "World!",
+ _.format "Hello %s",
+ },
+ }
+ assert.equals("Hello World!", planetary_object "World!")
+ assert.equals("to the Moon!", planetary_object "Moon!")
+ end)
+
+ it("should give complements", function()
+ assert.is_true(_.complement(_.is_nil, "not nil"))
+ assert.is_false(_.complement(_.is_nil, nil))
+ end)
+end)
diff --git a/tests/mason-core/functional/number_spec.lua b/tests/mason-core/functional/number_spec.lua
new file mode 100644
index 00000000..db523c2d
--- /dev/null
+++ b/tests/mason-core/functional/number_spec.lua
@@ -0,0 +1,50 @@
+local _ = require "mason-core.functional"
+
+describe("functional: number", function()
+ it("should negate numbers", function()
+ assert.equals(-42, _.negate(42))
+ assert.equals(42, _.negate(-42))
+ end)
+
+ it("should check numbers greater than value", function()
+ local greater_than_life = _.gt(42)
+ assert.is_false(greater_than_life(0))
+ assert.is_false(greater_than_life(42))
+ assert.is_true(greater_than_life(43))
+ end)
+
+ it("should check numbers greater or equal than value", function()
+ local greater_or_equal_to_life = _.gte(42)
+ assert.is_false(greater_or_equal_to_life(0))
+ assert.is_true(greater_or_equal_to_life(42))
+ assert.is_true(greater_or_equal_to_life(43))
+ end)
+
+ it("should check numbers lower than value", function()
+ local lesser_than_life = _.lt(42)
+ assert.is_true(lesser_than_life(0))
+ assert.is_false(lesser_than_life(42))
+ assert.is_false(lesser_than_life(43))
+ end)
+
+ it("should check numbers lower or equal than value", function()
+ local lesser_or_equal_to_life = _.lte(42)
+ assert.is_true(lesser_or_equal_to_life(0))
+ assert.is_true(lesser_or_equal_to_life(42))
+ assert.is_false(lesser_or_equal_to_life(43))
+ end)
+
+ it("should increment numbers", function()
+ local add_5 = _.inc(5)
+ assert.equals(0, add_5(-5))
+ assert.equals(5, add_5(0))
+ assert.equals(7, add_5(2))
+ end)
+
+ it("should decrement numbers", function()
+ local subtract_5 = _.dec(5)
+ assert.equals(5, subtract_5(10))
+ assert.equals(-5, subtract_5(0))
+ assert.equals(-3, subtract_5(2))
+ end)
+end)
diff --git a/tests/mason-core/functional/relation_spec.lua b/tests/mason-core/functional/relation_spec.lua
new file mode 100644
index 00000000..a3eee722
--- /dev/null
+++ b/tests/mason-core/functional/relation_spec.lua
@@ -0,0 +1,36 @@
+local _ = require "mason-core.functional"
+
+describe("functional: relation", function()
+ it("should check equality", function()
+ local tbl = {}
+ local is_tbl = _.equals(tbl)
+ local is_a = _.equals "a"
+ local is_42 = _.equals(42)
+
+ assert.is_true(is_tbl(tbl))
+ assert.is_true(is_a "a")
+ assert.is_true(is_42(42))
+ assert.is_false(is_a "b")
+ assert.is_false(is_42(32))
+ end)
+
+ it("should check property equality", function()
+ local fn_key = function() end
+ local tbl = { a = "a", b = "b", number = 42, [fn_key] = "fun" }
+ assert.is_true(_.prop_eq("a", "a", tbl))
+ assert.is_true(_.prop_eq(fn_key, "fun", tbl))
+ assert.is_true(_.prop_eq(fn_key) "fun"(tbl))
+ end)
+
+ it("should check whether property satisfies predicate", function()
+ local obj = {
+ low = 0,
+ med = 10,
+ high = 15,
+ }
+
+ assert.is_false(_.prop_satisfies(_.gt(10), "low", obj))
+ assert.is_false(_.prop_satisfies(_.gt(10), "med")(obj))
+ assert.is_true(_.prop_satisfies(_.gt(10)) "high"(obj))
+ end)
+end)
diff --git a/tests/mason-core/functional/string_spec.lua b/tests/mason-core/functional/string_spec.lua
new file mode 100644
index 00000000..25409f64
--- /dev/null
+++ b/tests/mason-core/functional/string_spec.lua
@@ -0,0 +1,43 @@
+local _ = require "mason-core.functional"
+
+describe("functional: string", function()
+ it("matches string patterns", function()
+ assert.is_true(_.matches("foo", "foo"))
+ assert.is_true(_.matches("bar", "foobarbaz"))
+ assert.is_true(_.matches("ba+r", "foobaaaaaaarbaz"))
+
+ assert.is_false(_.matches("ba+r", "foobharbaz"))
+ assert.is_false(_.matches("bar", "foobaz"))
+ end)
+
+ it("should format strings", function()
+ assert.equals("Hello World!", _.format("%s", "Hello World!"))
+ assert.equals("special manouvers", _.format("%s manouvers", "special"))
+ end)
+
+ it("should split strings", function()
+ assert.same({ "This", "is", "a", "sentence" }, _.split("%s", "This is a sentence"))
+ assert.same({ "This", "is", "a", "sentence" }, _.split("|", "This|is|a|sentence"))
+ end)
+
+ it("should gsub strings", function()
+ assert.same("predator", _.gsub("^apex%s*", "", "apex predator"))
+ end)
+
+ it("should dedent strings", function()
+ assert.equals(
+ [[Lorem
+Ipsum
+ Dolor
+ Sit
+ Amet]],
+ _.dedent [[
+ Lorem
+ Ipsum
+ Dolor
+ Sit
+ Amet
+]]
+ )
+ end)
+end)
diff --git a/tests/mason-core/functional/table_spec.lua b/tests/mason-core/functional/table_spec.lua
new file mode 100644
index 00000000..012c981c
--- /dev/null
+++ b/tests/mason-core/functional/table_spec.lua
@@ -0,0 +1,51 @@
+local _ = require "mason-core.functional"
+
+describe("functional: table", function()
+ it("retrieves property of table", function()
+ assert.equals("hello", _.prop("a", { a = "hello" }))
+ end)
+
+ it("picks properties of table", function()
+ local function fn() end
+ assert.same(
+ {
+ ["key1"] = 1,
+ [fn] = 2,
+ },
+ _.pick({ "key1", fn }, {
+ ["key1"] = 1,
+ [fn] = 2,
+ [3] = 3,
+ })
+ )
+ end)
+
+ it("converts table to pairs", function()
+ assert.same(
+ _.sort_by(_.nth(1), {
+ {
+ "skies",
+ "cloudy",
+ },
+ {
+ "temperature",
+ "20°",
+ },
+ }),
+ _.sort_by(_.nth(1), _.to_pairs { skies = "cloudy", temperature = "20°" })
+ )
+ end)
+
+ it("should invert tables", function()
+ assert.same(
+ {
+ val1 = "key1",
+ val2 = "key2",
+ },
+ _.invert {
+ key1 = "val1",
+ key2 = "val2",
+ }
+ )
+ end)
+end)
diff --git a/tests/mason-core/functional/type_spec.lua b/tests/mason-core/functional/type_spec.lua
new file mode 100644
index 00000000..e75a5647
--- /dev/null
+++ b/tests/mason-core/functional/type_spec.lua
@@ -0,0 +1,26 @@
+local _ = require "mason-core.functional"
+
+describe("functional: type", function()
+ it("should check nil value", function()
+ assert.is_true(_.is_nil(nil))
+ assert.is_false(_.is_nil(1))
+ assert.is_false(_.is_nil {})
+ assert.is_false(_.is_nil(function() end))
+ end)
+
+ it("should check types", function()
+ local is_fun = _.is "function"
+ local is_string = _.is "string"
+ local is_number = _.is "number"
+ local is_boolean = _.is "boolean"
+
+ assert.is_true(is_fun(function() end))
+ assert.is_false(is_fun(1))
+ assert.is_true(is_string "")
+ assert.is_false(is_string(1))
+ assert.is_true(is_number(1))
+ assert.is_false(is_number "")
+ assert.is_true(is_boolean(true))
+ assert.is_false(is_boolean(1))
+ end)
+end)
diff --git a/tests/mason-core/installer_spec.lua b/tests/mason-core/installer_spec.lua
new file mode 100644
index 00000000..8dc9b516
--- /dev/null
+++ b/tests/mason-core/installer_spec.lua
@@ -0,0 +1,100 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local fs = require "mason-core.fs"
+local a = require "mason-core.async"
+local path = require "mason-core.path"
+local installer = require "mason-core.installer"
+local InstallContext = require "mason-core.installer.context"
+
+local function timestamp()
+ local seconds, microseconds = vim.loop.gettimeofday()
+ return (seconds * 1000) + math.floor(microseconds / 1000)
+end
+
+describe("installer", function()
+ before_each(function()
+ package.loaded["dummy_package"] = nil
+ end)
+
+ it(
+ "should call installer",
+ async_test(function()
+ spy.on(fs.async, "mkdirp")
+ spy.on(fs.async, "rename")
+
+ local handle = InstallHandleGenerator "dummy"
+ spy.on(handle.package.spec, "install")
+ local result = installer.execute(handle, {})
+
+ assert.is_nil(result:err_or_nil())
+ assert.spy(handle.package.spec.install).was_called(1)
+ assert.spy(handle.package.spec.install).was_called_with(match.instanceof(InstallContext))
+ assert.spy(fs.async.mkdirp).was_called_with(path.package_build_prefix "dummy")
+ assert.spy(fs.async.rename).was_called_with(path.package_build_prefix "dummy", path.package_prefix "dummy")
+ end)
+ )
+
+ it(
+ "should return failure if installer errors",
+ async_test(function()
+ spy.on(fs.async, "rmrf")
+ spy.on(fs.async, "rename")
+ local installer_fn = spy.new(function()
+ error("something went wrong. don't try again.", 0)
+ end)
+ local handler = InstallHandleGenerator "dummy"
+ handler.package.spec.install = installer_fn
+ local result = installer.execute(handler, {})
+ assert.spy(installer_fn).was_called(1)
+ assert.is_true(result:is_failure())
+ assert.is_true(match.has_match "^.*: something went wrong. don't try again.$"(result:err_or_nil()))
+ assert.spy(fs.async.rmrf).was_called_with(path.package_build_prefix "dummy")
+ assert.spy(fs.async.rename).was_not_called()
+ end)
+ )
+
+ it(
+ "should write receipt",
+ async_test(function()
+ spy.on(fs.async, "write_file")
+ local handle = InstallHandleGenerator "dummy"
+ installer.execute(handle, {})
+ assert.spy(fs.async.write_file).was_called(1)
+ assert
+ .spy(fs.async.write_file)
+ .was_called_with(("%s/mason-receipt.json"):format(handle.package:get_install_path()), match.is_string())
+ end)
+ )
+
+ it(
+ "should run async functions concurrently",
+ async_test(function()
+ spy.on(fs.async, "write_file")
+ local capture = spy.new()
+ local start = timestamp()
+ local handle = InstallHandleGenerator "dummy"
+ handle.package.spec.install = function(ctx)
+ capture(installer.run_concurrently {
+ function()
+ a.sleep(100)
+ return installer.context()
+ end,
+ function()
+ a.sleep(100)
+ return "two"
+ end,
+ function()
+ a.sleep(100)
+ return "three"
+ end,
+ })
+ ctx.receipt:with_primary_source { type = "dummy" }
+ end
+ installer.execute(handle, {})
+ local stop = timestamp()
+ local grace_ms = 25
+ assert.is_true((stop - start) >= (100 - grace_ms))
+ assert.spy(capture).was_called_with(match.instanceof(InstallContext), "two", "three")
+ end)
+ )
+end)
diff --git a/tests/mason-core/managers/cargo_spec.lua b/tests/mason-core/managers/cargo_spec.lua
new file mode 100644
index 00000000..a938452b
--- /dev/null
+++ b/tests/mason-core/managers/cargo_spec.lua
@@ -0,0 +1,226 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "mason-core.optional"
+local installer = require "mason-core.installer"
+local cargo = require "mason-core.managers.cargo"
+local Result = require "mason-core.result"
+local spawn = require "mason-core.spawn"
+local path = require "mason-core.path"
+
+describe("cargo manager", function()
+ it(
+ "should call cargo install",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, cargo.crate "my-crate")
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, cargo.crate("https://my-crate.git", { git = true }))
+ 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 call cargo install with git source and a specific crate",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, cargo.crate("crate-name", { git = "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
+ { "--git", "https://my-crate.git", "crate-name" },
+ }
+ end)
+ )
+
+ it(
+ "should respect options",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, cargo.crate("my-crate", { features = "lsp" }))
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ 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)
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, cargo.crate "main-package")
+ assert.same({
+ type = "cargo",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ end)
+ )
+end)
+
+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",
+ },
+ 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",
+ },
+ },
+ 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.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",
+ },
+ },
+ 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 = "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/mason-core/managers/composer_spec.lua b/tests/mason-core/managers/composer_spec.lua
new file mode 100644
index 00000000..eccaf1c5
--- /dev/null
+++ b/tests/mason-core/managers/composer_spec.lua
@@ -0,0 +1,179 @@
+local spy = require "luassert.spy"
+local mock = require "luassert.mock"
+local installer = require "mason-core.installer"
+local Optional = require "mason-core.optional"
+local composer = require "mason-core.managers.composer"
+local Result = require "mason-core.result"
+local spawn = require "mason-core.spawn"
+local path = require "mason-core.path"
+
+describe("composer manager", function()
+ it(
+ "should call composer require",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ ctx.fs.file_exists = spy.new(mockx.returns(false))
+ installer.run_installer(
+ ctx,
+ composer.packages { "main-package", "supporting-package", "supporting-package2" }
+ )
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(
+ ctx,
+ composer.packages { "main-package", "supporting-package", "supporting-package2" }
+ )
+ assert.same({
+ type = "composer",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "composer",
+ package = "supporting-package",
+ },
+ {
+ type = "composer",
+ package = "supporting-package2",
+ },
+ }, 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.composer).was_called(1)
+ assert.spy(spawn.composer).was_called_with {
+ "info",
+ "--format=json",
+ "vimeo/psalm",
+ cwd = path.package_prefix "dummy",
+ }
+ 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.composer).was_called(1)
+ assert.spy(spawn.composer).was_called_with {
+ "outdated",
+ "--no-interaction",
+ "--format=json",
+ cwd = path.package_prefix "dummy",
+ }
+ assert.is_true(result:is_success())
+ assert.same({
+ name = "vimeo/psalm",
+ current_version = "4.0.0",
+ latest_version = "4.22.0",
+ }, 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ 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/mason-core/managers/dotnet_spec.lua b/tests/mason-core/managers/dotnet_spec.lua
new file mode 100644
index 00000000..8c04b875
--- /dev/null
+++ b/tests/mason-core/managers/dotnet_spec.lua
@@ -0,0 +1,35 @@
+local installer = require "mason-core.installer"
+local dotnet = require "mason-core.managers.dotnet"
+
+describe("dotnet manager", function()
+ it(
+ "should call dotnet tool update",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, dotnet.package "main-package")
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, dotnet.package "main-package")
+ assert.same({
+ type = "dotnet",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ end)
+ )
+end)
diff --git a/tests/mason-core/managers/gem_spec.lua b/tests/mason-core/managers/gem_spec.lua
new file mode 100644
index 00000000..82ad35e5
--- /dev/null
+++ b/tests/mason-core/managers/gem_spec.lua
@@ -0,0 +1,206 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local installer = require "mason-core.installer"
+local Optional = require "mason-core.optional"
+local gem = require "mason-core.managers.gem"
+local Result = require "mason-core.result"
+local spawn = require "mason-core.spawn"
+
+describe("gem manager", function()
+ it(
+ "should call gem install",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, gem.packages { "main-package", "supporting-package", "supporting-package2" })
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, gem.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.same({
+ type = "gem",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "gem",
+ package = "supporting-package",
+ },
+ {
+ type = "gem",
+ package = "supporting-package2",
+ },
+ }, 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.tbl_containing {
+ GEM_HOME = "/tmp/install/dir",
+ GEM_PATH = "/tmp/install/dir",
+ PATH = match.matches "^/tmp/install/dir/bin:.*$",
+ },
+ })
+ 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.tbl_containing {
+ GEM_HOME = "/tmp/install/dir",
+ GEM_PATH = "/tmp/install/dir",
+ PATH = match.matches "^/tmp/install/dir/bin:.*$",
+ },
+ })
+ assert.is_true(result:is_success())
+ assert.same({
+ name = "solargraph",
+ current_version = "0.44.0",
+ latest_version = "0.44.3",
+ }, 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.same({
+ name = "solargraph",
+ current_version = "0.42.2",
+ latest_version = "0.44.2",
+ }, normalize [[solargraph (0.42.2 < 0.44.2)]])
+ assert.same({
+ name = "sorbet-runtime",
+ current_version = "0.5.9307",
+ latest_version = "0.5.9468",
+ }, 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.same(
+ {
+ ["solargraph"] = "0.44.3",
+ ["unicode-display_width"] = "2.1.0",
+ },
+ 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/mason-core/managers/git_spec.lua b/tests/mason-core/managers/git_spec.lua
new file mode 100644
index 00000000..b7da068e
--- /dev/null
+++ b/tests/mason-core/managers/git_spec.lua
@@ -0,0 +1,181 @@
+local spy = require "luassert.spy"
+local mock = require "luassert.mock"
+local spawn = require "mason-core.spawn"
+local Result = require "mason-core.result"
+local installer = require "mason-core.installer"
+
+local git = require "mason-core.managers.git"
+local Optional = require "mason-core.optional"
+
+describe("git manager", function()
+ it(
+ "should fail if no git repo provided",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ local err = assert.has_errors(function()
+ installer.run_installer(ctx, function()
+ git.clone {}
+ end)
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, function()
+ git.clone { "https://github.com/williamboman/mason.nvim.git" }
+ end)
+ assert.spy(ctx.spawn.git).was_called(1)
+ assert.spy(ctx.spawn.git).was_called_with {
+ "clone",
+ "--depth",
+ "1",
+ vim.NIL,
+ "https://github.com/williamboman/mason.nvim.git",
+ ".",
+ }
+ end)
+ )
+
+ it(
+ "should fetch and checkout revision if requested",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "1337" })
+ installer.run_installer(ctx, function()
+ git.clone { "https://github.com/williamboman/mason.nvim.git" }
+ end)
+ assert.spy(ctx.spawn.git).was_called(3)
+ assert.spy(ctx.spawn.git).was_called_with {
+ "clone",
+ "--depth",
+ "1",
+ vim.NIL,
+ "https://github.com/williamboman/mason.nvim.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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, function()
+ git.clone({ "https://github.com/williamboman/mason.nvim.git" }).with_receipt()
+ end)
+ assert.same({
+ type = "git",
+ remote = "https://github.com/williamboman/mason.nvim.git",
+ }, 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({ type = "git" }, "/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/mason.nvim.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.same({
+ name = "https://github.com/williamboman/mason.nvim.git",
+ current_version = "19c668cd10695b243b09452f0dfd53570c1a2e7d",
+ latest_version = "728307a74cd5f2dec7ca2ca164785c25673d6328",
+ }, 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/mason.nvim.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/mason-core/managers/github_client_spec.lua b/tests/mason-core/managers/github_client_spec.lua
new file mode 100644
index 00000000..11b06880
--- /dev/null
+++ b/tests/mason-core/managers/github_client_spec.lua
@@ -0,0 +1,41 @@
+local client = require "mason-core.managers.github.client"
+
+describe("github client", function()
+ ---@type GitHubRelease
+ local release = {
+ tag_name = "v0.1.0",
+ prerelease = false,
+ draft = false,
+ assets = {},
+ }
+
+ local function stub_release(mock)
+ return setmetatable(mock, { __index = release })
+ end
+
+ it("should identify stable prerelease", function()
+ local predicate = client.release_predicate {
+ include_prerelease = false,
+ }
+
+ assert.is_false(predicate(stub_release { prerelease = true }))
+ assert.is_true(predicate(stub_release { prerelease = false }))
+ end)
+
+ it("should identify stable release with tag name pattern", function()
+ local predicate = client.release_predicate {
+ tag_name_pattern = "^lsp%-server.*$",
+ }
+
+ assert.is_false(predicate(stub_release { tag_name = "v0.1.0" }))
+ assert.is_true(predicate(stub_release { tag_name = "lsp-server-v0.1.0" }))
+ end)
+
+ it("should identify stable release", function()
+ local predicate = client.release_predicate {}
+
+ assert.is_true(predicate(stub_release { tag_name = "v0.1.0" }))
+ assert.is_false(predicate(stub_release { prerelease = true }))
+ assert.is_false(predicate(stub_release { draft = true }))
+ end)
+end)
diff --git a/tests/mason-core/managers/go_spec.lua b/tests/mason-core/managers/go_spec.lua
new file mode 100644
index 00000000..ebeb66ad
--- /dev/null
+++ b/tests/mason-core/managers/go_spec.lua
@@ -0,0 +1,174 @@
+local mock = require "luassert.mock"
+local stub = require "luassert.stub"
+local spy = require "luassert.spy"
+local Result = require "mason-core.result"
+local go = require "mason-core.managers.go"
+local spawn = require "mason-core.spawn"
+local installer = require "mason-core.installer"
+local path = require "mason-core.path"
+
+describe("go manager", function()
+ it(
+ "should call go install",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, go.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.spy(ctx.spawn.go).was_called(3)
+ assert.spy(ctx.spawn.go).was_called_with {
+ "install",
+ "-v",
+ "main-package@42.13.37",
+ env = { GOBIN = path.package_build_prefix "dummy" },
+ }
+ assert.spy(ctx.spawn.go).was_called_with {
+ "install",
+ "-v",
+ "supporting-package@latest",
+ env = { GOBIN = path.package_build_prefix "dummy" },
+ }
+ assert.spy(ctx.spawn.go).was_called_with {
+ "install",
+ "-v",
+ "supporting-package2@latest",
+ env = { GOBIN = path.package_build_prefix "dummy" },
+ }
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, go.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.same({
+ type = "go",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "go",
+ package = "supporting-package",
+ },
+ {
+ type = "go",
+ package = "supporting-package2",
+ },
+ }, ctx.receipt.secondary_sources)
+ end)
+ )
+end)
+
+describe("go version check", function()
+ local go_version_output = [[
+gopls: go1.18
+ path golang.org/x/tools/cmd
+ mod golang.org/x/tools/cmd 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.same({
+ path = { ["golang.org/x/tools/cmd"] = "" },
+ mod = { ["golang.org/x/tools/cmd"] = "v0.8.1" },
+ dep = { ["github.com/google/go-cmp"] = "v0.5.7", ["mvdan.cc/xurls/v2"] = "v2.4.0" },
+ build = { ["-compiler=gc"] = "", ["GOOS=darwin"] = "" },
+ }, 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/cmd/gopls/...",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.go).was_called(1)
+ assert.spy(spawn.go).was_called_with {
+ "version",
+ "-m",
+ "gopls",
+ cwd = path.package_prefix "dummy",
+ }
+ print(result:err_or_nil())
+ 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",
+ "golang.org/x/tools/cmd@latest",
+ cwd = path.package_prefix "dummy",
+ })
+ .returns(Result.success {
+ stdout = ([[
+ {
+ "Path": %q,
+ "Version": "v2.0.0"
+ }
+ ]]):format(path.package_prefix "dummy"),
+ })
+ spawn.go
+ .on_call_with({
+ "version",
+ "-m",
+ "gopls",
+ cwd = path.package_prefix "dummy",
+ })
+ .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/cmd/gopls/...",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.is_true(result:is_success())
+ assert.same({
+ name = "golang.org/x/tools/cmd",
+ current_version = "v0.8.1",
+ latest_version = "v2.0.0",
+ }, result:get_or_nil())
+
+ spawn.go = nil
+ end)
+ )
+
+ it("should parse package mod names", function()
+ assert.equals("github.com/cweill/gotests", go.parse_package_mod "github.com/cweill/gotests/...")
+ assert.equals("golang.org/x/tools/gopls", go.parse_package_mod "golang.org/x/tools/gopls/...")
+ assert.equals("golang.org/x/crypto", go.parse_package_mod "golang.org/x/crypto/...")
+ assert.equals("github.com/go-delve/delve", go.parse_package_mod "github.com/go-delve/delve/cmd/dlv")
+ end)
+end)
diff --git a/tests/mason-core/managers/luarocks_spec.lua b/tests/mason-core/managers/luarocks_spec.lua
new file mode 100644
index 00000000..2139e4a6
--- /dev/null
+++ b/tests/mason-core/managers/luarocks_spec.lua
@@ -0,0 +1,109 @@
+local installer = require "mason-core.installer"
+local luarocks = require "mason-core.managers.luarocks"
+local path = require "mason-core.path"
+
+describe("luarocks manager", function()
+ it(
+ "should install provided package",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, luarocks.package "lua-cjson")
+ assert.spy(ctx.spawn.luarocks).was_called(1)
+ print(vim.inspect(ctx.spawn.luarocks))
+ assert.spy(ctx.spawn.luarocks).was_called_with {
+ "install",
+ "--tree",
+ path.package_prefix "dummy",
+ vim.NIL, -- --dev flag
+ "lua-cjson",
+ vim.NIL, -- version
+ }
+ end)
+ )
+
+ it(
+ "should install provided version",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "1.2.3" })
+ installer.run_installer(ctx, luarocks.package "lua-cjson")
+ assert.spy(ctx.spawn.luarocks).was_called(1)
+ assert.spy(ctx.spawn.luarocks).was_called_with {
+ "install",
+ "--tree",
+ path.package_prefix "dummy",
+ vim.NIL, -- --dev flag
+ "lua-cjson",
+ "1.2.3",
+ }
+ end)
+ )
+
+ it(
+ "should provide --dev flag",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, luarocks.package("lua-cjson", { dev = true }))
+ assert.spy(ctx.spawn.luarocks).was_called(1)
+ assert.spy(ctx.spawn.luarocks).was_called_with {
+ "install",
+ "--tree",
+ path.package_prefix "dummy",
+ "--dev",
+ "lua-cjson",
+ vim.NIL, -- version
+ }
+ end)
+ )
+
+ it("should parse outdated luarocks", function()
+ assert.same(
+ {
+ {
+ name = "lua-cjson",
+ installed = "2.1.0-1",
+ available = "2.1.0.6-1",
+ repo = "https://luarocks.org",
+ },
+ {
+ name = "lua-resty-influx-mufanh",
+ installed = "0.2.1-0",
+ available = "0.2.1-1",
+ repo = "https://luarocks.org",
+ },
+ },
+ luarocks.parse_outdated_rocks [[lua-cjson 2.1.0-1 2.1.0.6-1 https://luarocks.org
+lua-resty-influx-mufanh 0.2.1-0 0.2.1-1 https://luarocks.org]]
+ )
+ end)
+
+ it("should parse listed luarocks", function()
+ assert.same(
+ {
+ {
+ package = "lua-cjson",
+ version = "2.1.0-1",
+ arch = "installed",
+ nrepo = "/my/luarock/loc",
+ },
+ {
+ package = "lua-resty-http",
+ version = "0.17.0.beta.1-0",
+ arch = "installed",
+ nrepo = "/my/luarock/loc",
+ },
+ {
+ package = "lua-resty-influx-mufanh",
+ version = "0.2.1-0",
+ arch = "installed",
+ nrepo = "/my/luarock/loc",
+ },
+ },
+ luarocks.parse_installed_rocks [[lua-cjson 2.1.0-1 installed /my/luarock/loc
+lua-resty-http 0.17.0.beta.1-0 installed /my/luarock/loc
+lua-resty-influx-mufanh 0.2.1-0 installed /my/luarock/loc]]
+ )
+ end)
+end)
diff --git a/tests/mason-core/managers/npm_spec.lua b/tests/mason-core/managers/npm_spec.lua
new file mode 100644
index 00000000..78845ff1
--- /dev/null
+++ b/tests/mason-core/managers/npm_spec.lua
@@ -0,0 +1,194 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local installer = require "mason-core.installer"
+local npm = require "mason-core.managers.npm"
+local Result = require "mason-core.result"
+local spawn = require "mason-core.spawn"
+local path = require "mason-core.path"
+
+describe("npm manager", function()
+ it(
+ "should call npm install",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, npm.packages { "main-package", "supporting-package", "supporting-package2" })
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ ctx.fs.file_exists = mockx.returns(false)
+ ctx.fs.dir_exists = mockx.returns(false)
+ installer.run_installer(ctx, function()
+ npm.install { "main-package", "supporting-package", "supporting-package2" }
+ end)
+ assert.spy(ctx.spawn.npm).was_called_with {
+ "init",
+ "--yes",
+ "--scope=mason",
+ }
+ end)
+ )
+
+ it(
+ "should append .npmrc file",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ ctx.fs.append_file = spy.new(mockx.just_runs())
+ installer.run_installer(ctx, npm.packages { "main-package", "supporting-package", "supporting-package2" })
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, npm.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.same({
+ type = "npm",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "npm",
+ package = "supporting-package",
+ },
+ {
+ type = "npm",
+ package = "supporting-package2",
+ },
+ }, 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.npm).was_called(1)
+ assert.spy(spawn.npm).was_called_with { "ls", "--json", cwd = path.package_prefix "dummy" }
+ 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.npm).was_called(1)
+ assert.spy(spawn.npm).was_called_with {
+ "outdated",
+ "--json",
+ "bash-language-server",
+ cwd = path.package_prefix "dummy",
+ }
+ assert.is_true(result:is_success())
+ assert.same({
+ name = "bash-language-server",
+ current_version = "1.17.0",
+ latest_version = "2.0.0",
+ }, 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ 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/mason-core/managers/opam_spec.lua b/tests/mason-core/managers/opam_spec.lua
new file mode 100644
index 00000000..143e4dac
--- /dev/null
+++ b/tests/mason-core/managers/opam_spec.lua
@@ -0,0 +1,51 @@
+local match = require "luassert.match"
+local mock = require "luassert.mock"
+local Optional = require "mason-core.optional"
+local installer = require "mason-core.installer"
+local opam = require "mason-core.managers.opam"
+
+describe("opam manager", function()
+ it(
+ "should call opam install",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, opam.packages { "main-package", "supporting-package", "supporting-package2" })
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, opam.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.same({
+ type = "opam",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "opam",
+ package = "supporting-package",
+ },
+ {
+ type = "opam",
+ package = "supporting-package2",
+ },
+ }, ctx.receipt.secondary_sources)
+ end)
+ )
+end)
diff --git a/tests/mason-core/managers/pip3_spec.lua b/tests/mason-core/managers/pip3_spec.lua
new file mode 100644
index 00000000..60bb373a
--- /dev/null
+++ b/tests/mason-core/managers/pip3_spec.lua
@@ -0,0 +1,242 @@
+local mock = require "luassert.mock"
+local spy = require "luassert.spy"
+local path = require "mason-core.path"
+
+local pip3 = require "mason-core.managers.pip3"
+local Optional = require "mason-core.optional"
+local installer = require "mason-core.installer"
+local Result = require "mason-core.result"
+local settings = require "mason.settings"
+local spawn = require "mason-core.spawn"
+
+describe("pip3 manager", function()
+ 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()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, pip3.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.equals(path.package_prefix "dummy", ctx.cwd:get()) -- should've promoted cwd
+ assert.spy(ctx.spawn.python3).was_called(1)
+ assert.spy(ctx.spawn.python3).was_called_with {
+ "-m",
+ "venv",
+ "venv",
+ }
+ assert.spy(ctx.spawn.python).was_called(1)
+ assert.spy(ctx.spawn.python).was_called_with {
+ "-m",
+ "pip",
+ "--disable-pip-version-check",
+ "install",
+ "-U",
+ {},
+ {
+ "main-package==42.13.37",
+ "supporting-package",
+ "supporting-package2",
+ },
+ with_paths = { path.concat { path.package_prefix "dummy", "venv", "bin" } },
+ }
+ end)
+ )
+
+ it(
+ "should exhaust python3 executable candidates if all fail",
+ async_test(function()
+ vim.g.python3_host_prog = "/my/python3"
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ ctx.spawn.python3 = spy.new(mockx.throws())
+ ctx.spawn.python = spy.new(mockx.throws())
+ ctx.spawn[vim.g.python3_host_prog] = spy.new(mockx.throws())
+ local err = assert.has_error(function()
+ installer.run_installer(ctx, pip3.packages { "package" })
+ 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"
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ ctx.spawn.python3 = spy.new(mockx.throws())
+ ctx.spawn.python = spy.new(mockx.returns {})
+ ctx.spawn[vim.g.python3_host_prog] = spy.new(mockx.returns {})
+
+ installer.run_installer(ctx, pip3.packages { "package" })
+ vim.g.python3_host_prog = nil
+ assert.spy(ctx.spawn.python3).was_called(0)
+ assert.spy(ctx.spawn.python).was_called(1)
+ assert.spy(ctx.spawn["/my/python3"]).was_called(1)
+ end)
+ )
+
+ it(
+ "should use install_args from settings",
+ async_test(function()
+ settings.set {
+ pip = {
+ install_args = { "--proxy", "http://localhost:8080" },
+ },
+ }
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ installer.run_installer(ctx, pip3.packages { "package" })
+ assert.spy(ctx.spawn.python).was_called_with {
+ "-m",
+ "pip",
+ "--disable-pip-version-check",
+ "install",
+ "-U",
+ { "--proxy", "http://localhost:8080" },
+ { "package" },
+ with_paths = { path.concat { path.package_prefix "dummy", "venv", "bin" } },
+ }
+ end)
+ )
+
+ it(
+ "should provide receipt information",
+ async_test(function()
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle, { requested_version = "42.13.37" })
+ installer.run_installer(ctx, pip3.packages { "main-package", "supporting-package", "supporting-package2" })
+ assert.same({
+ type = "pip3",
+ package = "main-package",
+ }, ctx.receipt.primary_source)
+ assert.same({
+ {
+ type = "pip3",
+ package = "supporting-package",
+ },
+ {
+ type = "pip3",
+ package = "supporting-package2",
+ },
+ }, 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.python).was_called(1)
+ assert.spy(spawn.python).was_called_with {
+ "-m",
+ "pip",
+ "list",
+ "--format=json",
+ cwd = path.package_prefix "dummy",
+ with_paths = { path.concat { path.package_prefix "dummy", "venv", "bin" } },
+ }
+ 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",
+ },
+ },
+ path.package_prefix "dummy"
+ )
+
+ assert.spy(spawn.python).was_called(1)
+ assert.spy(spawn.python).was_called_with {
+ "-m",
+ "pip",
+ "list",
+ "--outdated",
+ "--format=json",
+ cwd = path.package_prefix "dummy",
+ with_paths = { path.concat { path.package_prefix "dummy", "venv", "bin" } },
+ }
+ assert.is_true(result:is_success())
+ assert.same({
+ name = "python-lsp-server",
+ current_version = "1.3.0",
+ latest_version = "1.4.0",
+ }, 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/mason-core/optional_spec.lua b/tests/mason-core/optional_spec.lua
new file mode 100644
index 00000000..e0068b10
--- /dev/null
+++ b/tests/mason-core/optional_spec.lua
@@ -0,0 +1,63 @@
+local Optional = require "mason-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/mason-core/path_spec.lua b/tests/mason-core/path_spec.lua
new file mode 100644
index 00000000..7c7d417f
--- /dev/null
+++ b/tests/mason-core/path_spec.lua
@@ -0,0 +1,23 @@
+local path = require "mason-core.path"
+
+describe("path", function()
+ it("concatenates paths", function()
+ assert.equal("foo/bar/baz/~", path.concat { "foo", "bar", "baz", "~" })
+ end)
+
+ it("concatenates paths on Windows", function()
+ local old_os = jit.os
+ jit.os = "windows"
+ package.loaded["mason-core.path"] = nil
+ local path = require "mason-core.path"
+ assert.equal([[foo\bar\baz\~]], path.concat { "foo", "bar", "baz", "~" })
+ jit.os = old_os
+ end)
+
+ it("identifies subdirectories", function()
+ assert.is_true(path.is_subdirectory("/foo/bar", "/foo/bar/baz"))
+ assert.is_true(path.is_subdirectory("/foo/bar", "/foo/bar"))
+ assert.is_false(path.is_subdirectory("/foo/bar", "/foo/bas/baz"))
+ assert.is_false(path.is_subdirectory("/foo/bar", "/foo/bars/baz"))
+ end)
+end)
diff --git a/tests/mason-core/platform_spec.lua b/tests/mason-core/platform_spec.lua
new file mode 100644
index 00000000..2c9d2035
--- /dev/null
+++ b/tests/mason-core/platform_spec.lua
@@ -0,0 +1,159 @@
+local stub = require "luassert.stub"
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+
+describe("platform", function()
+ local function platform()
+ package.loaded["mason-core.platform"] = nil
+ return require "mason-core.platform"
+ end
+
+ local function stub_mac(arch)
+ arch = arch or "x86_64"
+ stub(vim.fn, "has")
+ stub(vim.loop, "os_uname")
+ vim.loop.os_uname.returns { machine = arch }
+ vim.fn.has.on_call_with("mac").returns(1)
+ vim.fn.has.on_call_with("unix").returns(1)
+ vim.fn.has.on_call_with(match._).returns(0)
+ end
+
+ local function stub_linux()
+ stub(vim.fn, "has")
+ vim.fn.has.on_call_with("mac").returns(0)
+ vim.fn.has.on_call_with("unix").returns(1)
+ vim.fn.has.on_call_with(match._).returns(0)
+ end
+
+ local function stub_windows()
+ stub(vim.fn, "has")
+ vim.fn.has.on_call_with("win32").returns(1)
+ vim.fn.has.on_call_with(match._).returns(0)
+ end
+
+ it("should be able to detect platform and arch", function()
+ stub_mac "arm64"
+ assert.is_true(platform().is.mac_arm64)
+ assert.is_false(platform().is.mac_x64)
+ assert.is_false(platform().is.nothing)
+ end)
+
+ it("should be able to detect macos", function()
+ stub_mac()
+ assert.is_true(platform().is_mac)
+ assert.is_true(platform().is.mac)
+ assert.is_true(platform().is_unix)
+ assert.is_true(platform().is.unix)
+ assert.is_false(platform().is_linux)
+ assert.is_false(platform().is.linux)
+ assert.is_false(platform().is_win)
+ assert.is_false(platform().is.win)
+ end)
+
+ it("should be able to detect linux", function()
+ stub_linux()
+ assert.is_false(platform().is_mac)
+ assert.is_false(platform().is.mac)
+ assert.is_true(platform().is_unix)
+ assert.is_true(platform().is.unix)
+ assert.is_true(platform().is_linux)
+ assert.is_true(platform().is.linux)
+ assert.is_false(platform().is_win)
+ assert.is_false(platform().is.win)
+ end)
+
+ it("should be able to detect windows", function()
+ stub_windows()
+ assert.is_false(platform().is_mac)
+ assert.is_false(platform().is.mac)
+ assert.is_false(platform().is_unix)
+ assert.is_false(platform().is.unix)
+ assert.is_false(platform().is_linux)
+ assert.is_false(platform().is.linux)
+ assert.is_true(platform().is_win)
+ assert.is_true(platform().is.win)
+ end)
+
+ it("should run correct case on linux", function()
+ local unix = spy.new()
+ local win = spy.new()
+ local mac = spy.new()
+ local linux = spy.new()
+
+ stub_linux()
+ platform().when {
+ unix = unix,
+ win = win,
+ linux = linux,
+ mac = mac,
+ }
+ assert.spy(unix).was_not_called()
+ assert.spy(mac).was_not_called()
+ assert.spy(win).was_not_called()
+ assert.spy(linux).was_called(1)
+ end)
+
+ it("should run correct case on mac", function()
+ local unix = spy.new()
+ local win = spy.new()
+ local mac = spy.new()
+ local linux = spy.new()
+
+ stub_mac()
+ platform().when {
+ unix = unix,
+ win = win,
+ linux = linux,
+ mac = mac,
+ }
+ assert.spy(unix).was_not_called()
+ assert.spy(mac).was_called(1)
+ assert.spy(win).was_not_called()
+ assert.spy(linux).was_not_called()
+ end)
+
+ it("should run correct case on windows", function()
+ local unix = spy.new()
+ local win = spy.new()
+ local mac = spy.new()
+ local linux = spy.new()
+
+ stub_windows()
+ platform().when {
+ unix = unix,
+ win = win,
+ linux = linux,
+ mac = mac,
+ }
+ assert.spy(unix).was_not_called()
+ assert.spy(mac).was_not_called()
+ assert.spy(win).was_called(1)
+ assert.spy(linux).was_not_called()
+ end)
+
+ it("should run correct case on mac (unix)", function()
+ local unix = spy.new()
+ local win = spy.new()
+
+ stub_mac()
+ platform().when {
+ unix = unix,
+ win = win,
+ }
+ assert.spy(unix).was_called(1)
+ assert.spy(win).was_not_called()
+ end)
+
+ it("should run correct case on linux (unix)", function()
+ local unix = spy.new()
+ local win = spy.new()
+
+ stub_linux()
+ platform().when {
+ unix = unix,
+ win = win,
+ }
+ assert.spy(unix).was_called(1)
+ assert.spy(win).was_not_called()
+ end)
+end)
diff --git a/tests/mason-core/process_spec.lua b/tests/mason-core/process_spec.lua
new file mode 100644
index 00000000..f3c00385
--- /dev/null
+++ b/tests/mason-core/process_spec.lua
@@ -0,0 +1,28 @@
+local spy = require "luassert.spy"
+local match = require "luassert.match"
+local process = require "mason-core.process"
+
+describe("process.spawn", function()
+ -- Unix only
+ it(
+ "should spawn command and feed output to sink",
+ async_test(function()
+ local stdio = process.in_memory_sink()
+ local callback = spy.new()
+ process.spawn("env", {
+ args = {},
+ env = {
+ "HELLO=world",
+ "MY_ENV=var",
+ },
+ stdio_sink = stdio.sink,
+ }, callback)
+
+ assert.wait_for(function()
+ assert.spy(callback).was_called(1)
+ assert.spy(callback).was_called_with(true, 0, match.is_number())
+ assert.equal(table.concat(stdio.buffers.stdout, ""), "HELLO=world\nMY_ENV=var\n")
+ end)
+ end)
+ )
+end)
diff --git a/tests/mason-core/result_spec.lua b/tests/mason-core/result_spec.lua
new file mode 100644
index 00000000..777e268b
--- /dev/null
+++ b/tests/mason-core/result_spec.lua
@@ -0,0 +1,143 @@
+local Result = require "mason-core.result"
+local match = require "luassert.match"
+local spy = require "luassert.spy"
+
+describe("result", function()
+ it("should create success", function()
+ local result = Result.success "Hello!"
+ assert.is_true(result:is_success())
+ assert.is_false(result:is_failure())
+ assert.equals("Hello!", result:get_or_nil())
+ end)
+
+ it("should create failure", function()
+ local result = Result.failure "Hello!"
+ assert.is_true(result:is_failure())
+ assert.is_false(result:is_success())
+ assert.equals("Hello!", result:err_or_nil())
+ end)
+
+ it("should return value on get_or_throw()", function()
+ local result = Result.success "Hello!"
+ local val = result:get_or_throw()
+ assert.equals("Hello!", val)
+ end)
+
+ it("should throw failure on get_or_throw()", function()
+ local result = Result.failure "Hello!"
+ local err = assert.has_error(function()
+ result:get_or_throw()
+ end)
+ assert.equals("Hello!", err)
+ end)
+
+ it("should map() success", function()
+ local result = Result.success "Hello"
+ local mapped = result:map(function(x)
+ return x .. " World!"
+ end)
+ assert.equals("Hello World!", mapped:get_or_nil())
+ assert.is_nil(mapped:err_or_nil())
+ end)
+
+ it("should not map() failure", function()
+ local result = Result.failure "Hello"
+ local mapped = result:map(function(x)
+ return x .. " World!"
+ end)
+ assert.equals("Hello", mapped:err_or_nil())
+ assert.is_nil(mapped:get_or_nil())
+ end)
+
+ it("should raise exceptions in map()", function()
+ local result = Result.success "failure"
+ local err = assert.has_error(function()
+ result:map(function()
+ error "error"
+ end)
+ end)
+ assert.equals("error", err)
+ end)
+
+ it("should map_catching() success", function()
+ local result = Result.success "Hello"
+ local mapped = result:map_catching(function(x)
+ return x .. " World!"
+ end)
+ assert.equals("Hello World!", mapped:get_or_nil())
+ assert.is_nil(mapped:err_or_nil())
+ end)
+
+ it("should not map_catching() failure", function()
+ local result = Result.failure "Hello"
+ local mapped = result:map_catching(function(x)
+ return x .. " World!"
+ end)
+ assert.equals("Hello", mapped:err_or_nil())
+ assert.is_nil(mapped:get_or_nil())
+ end)
+
+ it("should catch errors in map_catching()", function()
+ local result = Result.success "value"
+ local mapped = result:map_catching(function()
+ error "This is an error"
+ end)
+ assert.is_false(mapped:is_success())
+ 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)
+
+ it("should run on_failure if failure", function()
+ local on_success = spy.new()
+ local on_failure = spy.new()
+ local result = Result.failure("Oh noes"):on_failure(on_failure):on_success(on_success)
+ assert.is_true(result:is_failure())
+ assert.equals("Oh noes", result:err_or_nil())
+ assert.spy(on_failure).was_called(1)
+ assert.spy(on_success).was_called(0)
+ assert.spy(on_failure).was_called_with "Oh noes"
+ end)
+
+ it("should run on_success if success", function()
+ local on_success = spy.new()
+ local on_failure = spy.new()
+ local result = Result.success("Oh noes"):on_failure(on_failure):on_success(on_success)
+ assert.is_true(result:is_success())
+ assert.equals("Oh noes", result:get_or_nil())
+ assert.spy(on_failure).was_called(0)
+ assert.spy(on_success).was_called(1)
+ assert.spy(on_success).was_called_with "Oh noes"
+ end)
+end)
diff --git a/tests/mason-core/spawn_spec.lua b/tests/mason-core/spawn_spec.lua
new file mode 100644
index 00000000..f9f90411
--- /dev/null
+++ b/tests/mason-core/spawn_spec.lua
@@ -0,0 +1,216 @@
+local spy = require "luassert.spy"
+local stub = require "luassert.stub"
+local match = require "luassert.match"
+local spawn = require "mason-core.spawn"
+local process = require "mason-core.process"
+
+describe("async spawn", function()
+ it(
+ "should spawn commands and return stdout & stderr",
+ async_test(function()
+ local result = spawn.env {
+ env_raw = { "FOO=bar" },
+ }
+ assert.is_true(result:is_success())
+ assert.equals("FOO=bar\n", result:get_or_nil().stdout)
+ assert.equals("", result:get_or_nil().stderr)
+ end)
+ )
+
+ it(
+ "should use provided stdio_sink",
+ async_test(function()
+ local stdio = process.in_memory_sink()
+ local result = spawn.env {
+ env_raw = { "FOO=bar" },
+ stdio_sink = stdio.sink,
+ }
+ assert.is_true(result:is_success())
+ assert.equals(nil, result:get_or_nil().stdout)
+ assert.equals(nil, result:get_or_nil().stderr)
+ assert.equals("FOO=bar\n", table.concat(stdio.buffers.stdout, ""))
+ assert.equals("", table.concat(stdio.buffers.stderr, ""))
+ end)
+ )
+
+ it(
+ "should pass command arguments",
+ 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)
+ )
+
+ it(
+ "should ignore vim.NIL args",
+ async_test(function()
+ spy.on(process, "spawn")
+ local result = spawn.bash {
+ vim.NIL,
+ vim.NIL,
+ "-c",
+ { vim.NIL, 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)
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "bash",
+ match.tbl_containing {
+ stdio_sink = match.tbl_containing {
+ stdout = match.is_function(),
+ stderr = match.is_function(),
+ },
+ env = match.list_containing "VAR=world",
+ args = match.tbl_containing {
+ "-c",
+ 'echo "Hello $VAR"',
+ },
+ },
+ match.is_function()
+ )
+ 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)
+ )
+
+ it(
+ "should call on_spawn",
+ async_test(function()
+ local on_spawn = spy.new(function(_, stdio)
+ local stdin = stdio[1]
+ stdin:write "im so piped rn"
+ stdin:close()
+ end)
+
+ local result = spawn.cat {
+ { "-" },
+ on_spawn = on_spawn,
+ }
+
+ assert.spy(on_spawn).was_called(1)
+ assert.spy(on_spawn).was_called_with(match.is_not.is_nil(), match.is_table(), match.is_number())
+ assert.is_true(result:is_success())
+ assert.equals("im so piped rn", result:get_or_nil().stdout)
+ end)
+ )
+
+ it(
+ "should not call on_spawn if spawning fails",
+ async_test(function()
+ local on_spawn = spy.new()
+
+ local result = spawn.this_cmd_doesnt_exist {
+ on_spawn = on_spawn,
+ }
+
+ assert.spy(on_spawn).was_called(0)
+ assert.is_true(result:is_failure())
+ end)
+ )
+
+ it(
+ "should handle failure to spawn process",
+ async_test(function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(false)
+ end)
+
+ local result = spawn.my_cmd {}
+ assert.is_true(result:is_failure())
+ assert.is_nil(result:err_or_nil().exit_code)
+ end)
+ )
+
+ it(
+ "should format failure message",
+ async_test(function()
+ stub(process, "spawn", function(cmd, opts, callback)
+ opts.stdio_sink.stderr(("This is an error message for %s!"):format(cmd))
+ callback(false, 127)
+ end)
+
+ local result = spawn.bash {}
+ assert.is_true(result:is_failure())
+ assert.equals(
+ "spawn: bash failed with exit code 127 and signal -. This is an error message for bash!",
+ tostring(result:err_or_nil())
+ )
+ end)
+ )
+
+ it(
+ "should check whether command is executable",
+ async_test(function()
+ local result = spawn.my_cmd {}
+ assert.is_true(result:is_failure())
+ assert.equals(
+ "spawn: my_cmd failed with exit code - and signal -. my_cmd is not executable",
+ tostring(result:err_or_nil())
+ )
+ end)
+ )
+
+ it(
+ "should skip checking whether command is executable",
+ async_test(function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(false, 127)
+ end)
+
+ local result = spawn.my_cmd { "arg1", check_executable = false }
+ assert.is_true(result:is_failure())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "my_cmd",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
+ end)
+ )
+
+ it(
+ "should skip checking whether command is executable if with_paths is provided",
+ async_test(function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(false, 127)
+ end)
+
+ local result = spawn.my_cmd { "arg1", with_paths = {} }
+ assert.is_true(result:is_failure())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "my_cmd",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
+ end)
+ )
+end)
diff --git a/tests/mason-core/ui_spec.lua b/tests/mason-core/ui_spec.lua
new file mode 100644
index 00000000..83609890
--- /dev/null
+++ b/tests/mason-core/ui_spec.lua
@@ -0,0 +1,301 @@
+local match = require "luassert.match"
+local spy = require "luassert.spy"
+local display = require "mason-core.ui.display"
+local Ui = require "mason-core.ui"
+local a = require "mason-core.async"
+
+describe("ui", function()
+ it("produces a correct tree", function()
+ local function renderer(state)
+ return Ui.CascadingStyleNode({ "INDENT" }, {
+ Ui.When(not state.is_active, function()
+ return Ui.Text {
+ "I'm not active",
+ "Another line",
+ }
+ end),
+ Ui.When(state.is_active, function()
+ return Ui.Text {
+ "I'm active",
+ "Yet another line",
+ }
+ end),
+ })
+ end
+
+ assert.same({
+ children = {
+ {
+ type = "HL_TEXT",
+ lines = {
+ { { "I'm not active", "" } },
+ { { "Another line", "" } },
+ },
+ },
+ {
+ type = "NODE",
+ children = {},
+ },
+ },
+ styles = { "INDENT" },
+ type = "CASCADING_STYLE",
+ }, renderer { is_active = false })
+
+ assert.same({
+ children = {
+ {
+ type = "NODE",
+ children = {},
+ },
+ {
+ type = "HL_TEXT",
+ lines = {
+ { { "I'm active", "" } },
+ { { "Yet another line", "" } },
+ },
+ },
+ },
+ styles = { "INDENT" },
+ type = "CASCADING_STYLE",
+ }, renderer { is_active = true })
+ end)
+
+ it("renders a tree correctly", function()
+ local render_output = display._render_node(
+ {
+ win_width = 120,
+ },
+ Ui.CascadingStyleNode({ "INDENT" }, {
+ Ui.Keybind("i", "INSTALL_PACKAGE", { "sumneko_lua" }, true),
+ Ui.HlTextNode {
+ {
+ { "Hello World!", "MyHighlightGroup" },
+ },
+ {
+ { "Another Line", "Comment" },
+ },
+ },
+ Ui.HlTextNode {
+ {
+ { "Install something idk", "Stuff" },
+ },
+ },
+ Ui.StickyCursor { id = "sticky" },
+ Ui.Keybind("<CR>", "INSTALL_PACKAGE", { "tsserver" }, false),
+ Ui.DiagnosticsNode {
+ message = "yeah this one's outdated",
+ severity = vim.diagnostic.severity.WARN,
+ source = "trust me bro",
+ },
+ Ui.Text { "I'm a text node" },
+ })
+ )
+
+ assert.same({
+ highlights = {
+ {
+ col_start = 2,
+ col_end = 14,
+ line = 0,
+ hl_group = "MyHighlightGroup",
+ },
+ {
+ col_start = 2,
+ col_end = 14,
+ line = 1,
+ hl_group = "Comment",
+ },
+ {
+ col_start = 2,
+ col_end = 23,
+ line = 2,
+ hl_group = "Stuff",
+ },
+ },
+ lines = { " Hello World!", " Another Line", " Install something idk", " I'm a text node" },
+ virt_texts = {},
+ sticky_cursors = { line_map = { [3] = "sticky" }, id_map = { ["sticky"] = 3 } },
+ keybinds = {
+ {
+ effect = "INSTALL_PACKAGE",
+ key = "i",
+ line = -1,
+ payload = { "sumneko_lua" },
+ },
+ {
+ effect = "INSTALL_PACKAGE",
+ key = "<CR>",
+ line = 3,
+ payload = { "tsserver" },
+ },
+ },
+ diagnostics = {
+ {
+ line = 3,
+ message = "yeah this one's outdated",
+ source = "trust me bro",
+ severity = vim.diagnostic.severity.WARN,
+ },
+ },
+ }, render_output)
+ end)
+end)
+
+describe("integration test", function()
+ it(
+ "calls vim APIs as expected during rendering",
+ async_test(function()
+ local window = display.new_view_only_win("test", "my-filetype")
+
+ window.view(function(state)
+ return Ui.Node {
+ Ui.Keybind("U", "EFFECT", nil, true),
+ Ui.Text {
+ "Line number 1!",
+ state.text,
+ },
+ Ui.Keybind("R", "R_EFFECT", { state.text }),
+ Ui.HlTextNode {
+ {
+ { "My highlighted text", "MyHighlightGroup" },
+ },
+ },
+ }
+ end)
+
+ local mutate_state = window.state { text = "Initial state" }
+
+ local clear_namespace = spy.on(vim.api, "nvim_buf_clear_namespace")
+ local buf_set_option = spy.on(vim.api, "nvim_buf_set_option")
+ local win_set_option = spy.on(vim.api, "nvim_win_set_option")
+ local set_lines = spy.on(vim.api, "nvim_buf_set_lines")
+ local set_extmark = spy.on(vim.api, "nvim_buf_set_extmark")
+ local set_hl = spy.on(vim.api, "nvim_set_hl")
+ local add_highlight = spy.on(vim.api, "nvim_buf_add_highlight")
+ local set_keymap = spy.on(vim.keymap, "set")
+
+ window.init {
+ effects = {
+ ["EFFECT"] = function() end,
+ ["R_EFFECT"] = function() end,
+ },
+ highlight_groups = {
+ MyHighlight = { bold = true },
+ },
+ }
+ window.open { border = "none" }
+
+ -- Initial window and buffer creation + initial render
+ a.scheduler()
+
+ assert.spy(set_hl).was_called(1)
+ assert.spy(set_hl).was_called_with(match.is_number(), "MyHighlight", match.same { bold = true })
+
+ assert.spy(win_set_option).was_called(8)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "number", false)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "relativenumber", false)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "wrap", false)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "spell", false)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "foldenable", false)
+ assert.spy(win_set_option).was_called_with(match.is_number(), "signcolumn", "no")
+ assert.spy(win_set_option).was_called_with(match.is_number(), "colorcolumn", "")
+ assert.spy(win_set_option).was_called_with(match.is_number(), "cursorline", true)
+
+ assert.spy(buf_set_option).was_called(10)
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "modifiable", false)
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "swapfile", false)
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "textwidth", 0)
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "buftype", "nofile")
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "bufhidden", "wipe")
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "buflisted", false)
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "filetype", "my-filetype")
+ assert.spy(buf_set_option).was_called_with(match.is_number(), "undolevels", -1)
+
+ assert.spy(set_lines).was_called(1)
+ assert
+ .spy(set_lines)
+ .was_called_with(match.is_number(), 0, -1, false, { "Line number 1!", "Initial state", "My highlighted text" })
+
+ assert.spy(set_extmark).was_called(0)
+
+ assert.spy(add_highlight).was_called(1)
+ assert
+ .spy(add_highlight)
+ .was_called_with(match.is_number(), match.is_number(), "MyHighlightGroup", 2, 0, 19)
+
+ assert.spy(set_keymap).was_called(2)
+ assert.spy(set_keymap).was_called_with(
+ "n",
+ "U",
+ match.is_function(),
+ match.tbl_containing { nowait = true, silent = true, buffer = match.is_number() }
+ )
+ assert.spy(set_keymap).was_called_with(
+ "n",
+ "R",
+ match.is_function(),
+ match.tbl_containing { nowait = true, silent = true, buffer = match.is_number() }
+ )
+
+ assert.spy(clear_namespace).was_called(1)
+ assert.spy(clear_namespace).was_called_with(match.is_number(), match.is_number(), 0, -1)
+
+ mutate_state(function(state)
+ state.text = "New state"
+ end)
+
+ assert.spy(set_lines).was_called(1)
+ a.scheduler()
+ assert.spy(set_lines).was_called(2)
+
+ assert
+ .spy(set_lines)
+ .was_called_with(match.is_number(), 0, -1, false, { "Line number 1!", "New state", "My highlighted text" })
+ end)
+ )
+
+ it(
+ "anchors to sticky cursor",
+ async_test(function()
+ local window = display.new_view_only_win("test", "my-filetype")
+ window.view(function(state)
+ local extra_lines = state.show_extra_lines
+ and Ui.Text {
+ "More",
+ "Lines",
+ "Here",
+ }
+ or Ui.Node {}
+ return Ui.Node {
+ extra_lines,
+ Ui.Text {
+ "Line 1",
+ "Line 2",
+ "Line 3",
+ "Line 4",
+ "Special line",
+ },
+ Ui.StickyCursor { id = "special" },
+ Ui.Text {
+ "Line 6",
+ "Line 7",
+ "Line 8",
+ "Line 9",
+ "Line 10",
+ },
+ }
+ end)
+ local mutate_state = window.state { show_extra_lines = false }
+ window.init {}
+ window.open { border = "none" }
+ a.scheduler()
+ window.set_cursor { 5, 3 } -- move cursor to sticky line
+ mutate_state(function(state)
+ state.show_extra_lines = true
+ end)
+ a.scheduler()
+ local cursor = window.get_cursor()
+ assert.same({ 8, 3 }, cursor)
+ end)
+ )
+end)