diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
| commit | 7fd555fcaf4304cb0dff3e8f5661565c3618299e (patch) | |
| tree | 61199793c3046d73b97e9822895ede2a33034d41 /test/harness.lua | |
| download | lua-7fd555fcaf4304cb0dff3e8f5661565c3618299e.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test/harness.lua')
| -rw-r--r-- | test/harness.lua | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/test/harness.lua b/test/harness.lua new file mode 100644 index 0000000..b6b3a65 --- /dev/null +++ b/test/harness.lua @@ -0,0 +1,205 @@ +--- 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 |