diff options
Diffstat (limited to 'tests/mason-core')
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) |
