--- Assertions, the test registry and control of the fake server. -- -- No rock beyond the ones the library already needs: registering a test is -- `test(name, fn)`, a failure is an `error()` out of an assertion, and the -- runner exits non-zero if any of them fires. local http = require("socket.http") local ltn12 = require("ltn12") local socket = require("socket") local json = require("dkjson") local M = {} M.tests = {} --- Register a test. Order of registration is order of execution. function M.test(name, fn) M.tests[#M.tests + 1] = { name = name, fn = fn } end local function fail(message, level) error(message, (level or 2) + 1) end local function show(value) if type(value) == "string" then return string.format("%q", value) end return tostring(value) end function M.ok(value, message) if not value then fail(message or ("expected a truthy value, got " .. show(value))) end end function M.falsy(value, message) if value then fail(message or ("expected a falsy value, got " .. show(value))) end end function M.equal(got, want, message) if got ~= want then fail( (message and (message .. ": ") or "") .. "expected " .. show(want) .. ", got " .. show(got) ) end end function M.near(got, want, message) if type(got) ~= "number" or math.abs(got - want) > 1e-6 then fail((message and (message .. ": ") or "") .. "expected ~" .. tostring(want) .. ", got " .. show(got)) end end function M.is_nil(value, message) if value ~= nil then fail(message or ("expected nil, got " .. show(value))) end end function M.is_false(value, message) if value ~= false then fail(message or ("expected false, got " .. show(value))) end end --- Assert a call failed with a given error kind, and return the error. function M.error_kind(value, err, kind) if value ~= nil then fail("expected a failure, got " .. show(value)) end if type(err) ~= "table" or err.kind == nil then fail("expected a cepis error, got " .. show(err)) end if err.kind ~= kind then fail("expected kind " .. show(kind) .. ", got " .. show(err.kind) .. " (" .. tostring(err.message) .. ")") end return err end --- Assert a call raised through `error()`, for programmer mistakes. function M.raises(fn, ...) local ok = pcall(fn, ...) if ok then fail("expected error() to be raised") end end local function request(url, body) local chunks = {} local headers = nil local source = nil if body then source = ltn12.source.string(body) headers = { ["content-length"] = tostring(#body), ["content-type"] = "application/json", } end local ok, code = http.request({ url = url, method = body and "POST" or "GET", source = source, headers = headers, sink = ltn12.sink.table(chunks), }) if not ok then error("control request to " .. url .. " failed: " .. tostring(code)) end return table.concat(chunks) end local Server = {} Server.__index = Server --- Replace the routes and clear the call log. function Server:program(routes) request(self.url .. "/__program", json.encode(routes)) end --- Every request target the server has seen since the last `program`. function Server:calls() return json.decode(request(self.url .. "/__calls")) or {} end --- How many of them began with `prefix`. function Server:count(prefix) local total = 0 for _, target in ipairs(self:calls()) do if target:sub(1, #prefix) == prefix then total = total + 1 end end return total end function Server:stop() pcall(request, self.url .. "/__stop") end --- Spawn the fake server and wait for it to report its port. function M.start_server() local directory = (arg[0] or "test/run.lua"):match("^(.*)[/\\]") or "." local portfile = os.tmpname() os.remove(portfile) local interpreter = arg[-1] or "lua" os.execute( string.format( "%s %s/server.lua %s >/dev/null 2>&1 &", interpreter, directory, portfile ) ) local port local deadline = socket.gettime() + 10 while socket.gettime() < deadline do local handle = io.open(portfile, "r") if handle then local content = handle:read("*a") handle:close() port = tonumber(content) if port then break end end socket.sleep(0.02) end os.remove(portfile) if not port then error("the fake server did not start") end return setmetatable({ url = "http://127.0.0.1:" .. port, port = port }, Server) end --- Run every registered test, printing one line each. function M.run() local failures = 0 for _, entry in ipairs(M.tests) do local ok, err = pcall(entry.fn) if ok then print("ok - " .. entry.name) else failures = failures + 1 print("FAIL - " .. entry.name) print(" " .. tostring(err)) end end print(string.format("\n%d test(s), %d failure(s)", #M.tests, failures)) return failures end return M