diff --git a/Makefile b/Makefile index 96401d8..0c69f47 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ all: $(EXE) $(EXE): $(SRC) echo "#!/usr/bin/env lua" > $@ - LUA_PATH="./src/?.lua;./src/?/init.lua" FENNEL_PATH="./src/?.fnl;./src/?/init.fnl" \ + LUA_PATH="./src/?.lua" \ + FENNEL_PATH="./src/?.fnl" \ $(FENNEL) --require-as-include --compile src/fennel-ls.fnl >> $@ chmod 755 $@ @@ -22,8 +23,8 @@ clean: rm -f $(EXE) test: - # requires busted to be installed - FENNEL_PATH="./src/?.fnl;./src/?/init.fnl" $(FENNEL) --correlate test/init.fnl --verbose + LUA_PATH="./src/?.lua;./?.lua" FENNEL_PATH="./src/?.fnl;./?.fnl" \ + $(FENNEL) test/init.fnl install: $(EXE) mkdir -p $(DESTDIR)$(BIN_DIR) && cp $< $(DESTDIR)$(BIN_DIR)/ diff --git a/test/completion-test.fnl b/test/completion-test.fnl index fcd8945..69528a5 100644 --- a/test/completion-test.fnl +++ b/test/completion-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: view} (require :fennel)) (local {: ROOT-URI @@ -20,7 +20,7 @@ suggestion.label suggestion.label)] (if expected (each [_ exp (ipairs expected)] - (is.truthy (. seen exp) (.. exp " was not suggested, but should be")))) + (is (. seen exp) (.. exp " was not suggested, but should be")))) (if ?unexpected (each [_ exp (ipairs ?unexpected)] (is.nil (. seen exp) (.. exp " was suggested, but shouldn't be")))))) diff --git a/test/diagnostic-test.fnl b/test/diagnostic-test.fnl index a9f80f7..507eb82 100644 --- a/test/diagnostic-test.fnl +++ b/test/diagnostic-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: view} (require :fennel)) (local {: ROOT-URI diff --git a/test/goto-definition-test.fnl b/test/goto-definition-test.fnl index 0888019..2a166cc 100644 --- a/test/goto-definition-test.fnl +++ b/test/goto-definition-test.fnl @@ -1,6 +1,6 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: ROOT-URI : setup-server} (require :test.utils)) diff --git a/test/hover-test.fnl b/test/hover-test.fnl index cbf005d..37589cf 100644 --- a/test/hover-test.fnl +++ b/test/hover-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: view} (require :fennel)) (local {: ROOT-URI diff --git a/test/init-macros.fnl b/test/init-macros.fnl index cddf0b4..7266941 100644 --- a/test/init-macros.fnl +++ b/test/init-macros.fnl @@ -2,24 +2,24 @@ "This document does not include tests. Instead it includes macros that are used for tests." (fn it [desc ...] - "busted's `it` function" + "lust's `it` function" (let [body [...]] (table.insert body `nil) - `((. (require :busted) :it) + `((. (require :test.lust) :it) ,desc (fn [] ,desc ,(unpack body))))) (fn describe [desc ...] - "busted's `describe` function" + "lust's `describe` function" (let [body [...]] (table.insert body `nil) - `((. (require :busted) :describe) + `((. (require :test.lust) :describe) ,desc (fn [] ,desc ,(unpack body))))) (fn before-each [...] - "busted's `describe` function" + "lust's `before_each` function" (let [body [...]] (table.insert body `nil) - `((. (require :busted) :before_each) + `((. (require :test.lust) :before_each) (fn [] ,(unpack body))))) diff --git a/test/init.fnl b/test/init.fnl index fb9861a..637dd75 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -1,5 +1,3 @@ -((require :busted.runner)) - (require :test.completion-test) (require :test.diagnostic-test) (require :test.goto-definition-test) @@ -8,3 +6,7 @@ (require :test.misc-test) (require :test.string-processing-test) (require :test.settings-test) + +(let [{: passes : errors} (require :test.lust)] + (print (.. passes " passes. " errors " errors.")) + (os.exit errors)) diff --git a/test/is.fnl b/test/is.fnl new file mode 100644 index 0000000..2650492 --- /dev/null +++ b/test/is.fnl @@ -0,0 +1,10 @@ +;; this package is here to translate into lust's weird dsl +(local {: view} (require :fennel)) +(local {: expect} (require :test.lust)) +;; lust uses weird terminology, but equal is by __eq, same is by recursively having the same contents +(setmetatable {:equal #((. (expect $1) :to :be) $2) + :same #((. (expect $1) :to :equal) $2) + :nil #((. (expect $1) :to_not :exist)) + :not {:nil #((. (expect $1) :to :exist))} + :truthy #((. (expect $1) :to :be :truthy))} + {:__call #((. (expect $2) :to :be :truthy))}) diff --git a/test/json-rpc-test.fnl b/test/json-rpc-test.fnl index b3d6c1b..1ff315e 100644 --- a/test/json-rpc-test.fnl +++ b/test/json-rpc-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local stringio (require :test.pl.stringio)) (local json-rpc (require :fennel-ls.json-rpc)) diff --git a/test/lust.lua b/test/lust.lua new file mode 100644 index 0000000..e753d1d --- /dev/null +++ b/test/lust.lua @@ -0,0 +1,242 @@ +-- lust v0.1.0 - Lua test framework +-- https://github.com/bjornbytes/lust +-- MIT LICENSE + +local lust = {} +lust.level = 0 +lust.passes = 0 +lust.errors = 0 +lust.befores = {} +lust.afters = {} + +local red = string.char(27) .. '[31m' +local green = string.char(27) .. '[32m' +local normal = string.char(27) .. '[0m' +local function indent(level) return string.rep('\t', level or lust.level) end + +local function has(t, x) + for k, v in pairs(t) do + if v == x then return true end + end + return false +end + +function lust.nocolor() + red, green, normal = '', '', '' + return lust +end + +function lust.describe(name, fn) + print(indent() .. name) + lust.level = lust.level + 1 + fn() + lust.befores[lust.level] = {} + lust.afters[lust.level] = {} + lust.level = lust.level - 1 +end + +function lust.it(name, fn) + for level = 1, lust.level do + if lust.befores[level] then + for i = 1, #lust.befores[level] do + lust.befores[level][i](name) + end + end + end + + local success, err = xpcall(fn, require('fennel').traceback) + if success then lust.passes = lust.passes + 1 + else lust.errors = lust.errors + 1 end + local color = success and green or red + local label = success and 'PASS' or 'FAIL' + print(indent() .. color .. label .. normal .. ' ' .. name) + if err then + print(indent(lust.level + 1) .. red .. tostring(err) .. normal) + end + + for level = 1, lust.level do + if lust.afters[level] then + for i = 1, #lust.afters[level] do + lust.afters[level][i](name) + end + end + end + + if has(arg, "--quit-at-first-test") then os.exit(1) end +end + +function lust.before(fn) + lust.befores[lust.level] = lust.befores[lust.level] or {} + table.insert(lust.befores[lust.level], fn) +end + +function lust.after(fn) + lust.afters[lust.level] = lust.afters[lust.level] or {} + table.insert(lust.afters[lust.level], fn) +end + +-- Assertions +local function isa(v, x) + if type(x) == 'string' then + return type(v) == x, + 'expected ' .. tostring(v) .. ' to be a ' .. x, + 'expected ' .. tostring(v) .. ' to not be a ' .. x + elseif type(x) == 'table' then + if type(v) ~= 'table' then + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + local seen = {} + local meta = v + while meta and not seen[meta] do + if meta == x then return true end + seen[meta] = true + meta = getmetatable(meta) and getmetatable(meta).__index + end + + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + error('invalid type ' .. tostring(x)) +end + +local function strict_eq(t1, t2) + if type(t1) ~= type(t2) then return false end + if type(t1) ~= 'table' then return t1 == t2 end + for k, _ in pairs(t1) do + if not strict_eq(t1[k], t2[k]) then return false end + end + for k, _ in pairs(t2) do + if not strict_eq(t2[k], t1[k]) then return false end + end + return true +end + +local paths = { + [''] = { 'to', 'to_not' }, + to = { 'have', 'equal', 'be', 'exist', 'fail', 'match' }, + to_not = { 'have', 'equal', 'be', 'exist', 'fail', 'match', chain = function(a) a.negate = not a.negate end }, + a = { test = isa }, + an = { test = isa }, + be = { 'a', 'an', 'truthy', + test = function(v, x) + return v == x, + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be equal', + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be equal' + end + }, + exist = { + test = function(v) + return v ~= nil, + 'expected ' .. tostring(v) .. ' to exist', + 'expected ' .. tostring(v) .. ' to not exist' + end + }, + truthy = { + test = function(v) + return v, + 'expected ' .. tostring(v) .. ' to be truthy', + 'expected ' .. tostring(v) .. ' to not be truthy' + end + }, + equal = { + test = function(v, x) + return strict_eq(v, x), + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be exactly equal', + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be exactly equal' + end + }, + have = { + test = function(v, x) + if type(v) ~= 'table' then + error('expected ' .. tostring(v) .. ' to be a table') + end + + return has(v, x), + 'expected ' .. tostring(v) .. ' to contain ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not contain ' .. tostring(x) + end + }, + fail = { + test = function(v) + return not pcall(v), + 'expected ' .. tostring(v) .. ' to fail', + 'expected ' .. tostring(v) .. ' to not fail' + end + }, + match = { + test = function(v, p) + if type(v) ~= 'string' then v = tostring(v) end + local result = string.find(v, p) + return result ~= nil, + 'expected ' .. v .. ' to match pattern [[' .. p .. ']]', + 'expected ' .. v .. ' to not match pattern [[' .. p .. ']]' + end + }, +} + +function lust.expect(v) + local assertion = {} + assertion.val = v + assertion.action = '' + assertion.negate = false + + setmetatable(assertion, { + __index = function(t, k) + if has(paths[rawget(t, 'action')], k) then + rawset(t, 'action', k) + local chain = paths[rawget(t, 'action')].chain + if chain then chain(t) end + return t + end + return rawget(t, k) + end, + __call = function(t, ...) + if paths[t.action].test then + local res, err, nerr = paths[t.action].test(t.val, ...) + if assertion.negate then + res = not res + err = nerr or err + end + if not res then + error(err or 'unknown failure', 2) + end + end + end + }) + + return assertion +end + +function lust.spy(target, name, run) + local spy = {} + local subject + + local function capture(...) + table.insert(spy, {...}) + return subject(...) + end + + if type(target) == 'table' then + subject = target[name] + target[name] = capture + else + run = name + subject = target or function() end + end + + setmetatable(spy, {__call = function(_, ...) return capture(...) end}) + + if run then run() end + + return spy +end + +lust.test = lust.it +lust.paths = paths + +return lust diff --git a/test/misc-test.fnl b/test/misc-test.fnl index b633ffe..5c27364 100644 --- a/test/misc-test.fnl +++ b/test/misc-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: view &as fennel} (require :fennel)) (local {: setup-server diff --git a/test/settings-test.fnl b/test/settings-test.fnl index 7f9903c..f78c932 100644 --- a/test/settings-test.fnl +++ b/test/settings-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it : before-each} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local {: view} (require :fennel)) (local {: ROOT-URI diff --git a/test/string-processing-test.fnl b/test/string-processing-test.fnl index 5b65be8..550c194 100644 --- a/test/string-processing-test.fnl +++ b/test/string-processing-test.fnl @@ -1,5 +1,5 @@ (import-macros {: is-matching : describe : it} :test) -(local is (require :luassert)) +(local is (require :test.is)) (local fennel (require :fennel)) (local utils (require :fennel-ls.utils))