summaryrefslogtreecommitdiffstats
path: root/test/harness.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/harness.lua')
-rw-r--r--test/harness.lua205
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