summaryrefslogtreecommitdiff
path: root/spec/util/quick.lua
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-02-03 21:29:42 +0100
committerMike Vink <mike@pionative.com>2025-02-03 21:29:42 +0100
commit5155816b7b925dec5d5feb1568b1d7ceb00938b9 (patch)
treedeca28ea15e79f6f804c3d90d2ba757881638af5 /spec/util/quick.lua
fetch tarballHEADmaster
Diffstat (limited to 'spec/util/quick.lua')
-rw-r--r--spec/util/quick.lua472
1 files changed, 472 insertions, 0 deletions
diff --git a/spec/util/quick.lua b/spec/util/quick.lua
new file mode 100644
index 0000000..a525aa9
--- /dev/null
+++ b/spec/util/quick.lua
@@ -0,0 +1,472 @@
+local quick = {}
+
+local dir_sep = package.config:sub(1, 1)
+
+local cfg, dir, fs, versions
+local initialized = false
+
+local function initialize()
+ if initialized then
+ return
+ end
+ initialized = true
+
+ cfg = require("luarocks.core.cfg")
+ dir = require("luarocks.dir")
+ fs = require("luarocks.fs")
+ versions = require("spec.util.versions")
+ cfg.init()
+ fs.init()
+end
+
+local function native_slash(pathname)
+ return (pathname:gsub("[/\\]", dir_sep))
+end
+
+local function parse_cmd(line)
+ local cmd, arg = line:match("^%s*([A-Z_]+):%s*(.*)%s*$")
+ return cmd, arg
+end
+
+local function is_blank(line)
+ return not not line:match("^%s*$")
+end
+
+local function is_hr(line)
+ return not not line:match("^%-%-%-%-%-")
+end
+
+local function parse(filename)
+ local fd = assert(io.open(filename, "rb"))
+ local input = assert(fd:read("*a"))
+ fd:close()
+
+ initialize()
+
+ local tests = {}
+
+ local cur_line = 0
+ local cur_suite = ""
+ local cur_test
+ local cur_op
+ local cur_block
+ local cur_block_name
+ local stack = { "start" }
+
+ local function start_test(arg)
+ cur_test = {
+ name = cur_suite .. arg,
+ ops = {},
+ }
+ cur_op = nil
+ table.insert(tests, cur_test)
+ table.insert(stack, "test")
+ end
+
+ local function fail(msg)
+ io.stderr:write("Error reading " .. filename .. ":" .. cur_line .. ": " .. msg .. "\n")
+ os.exit(1)
+ end
+
+ local function bool_arg(cmd, cur, field, arg)
+ if arg ~= "true" and arg ~= "false" then
+ fail(cmd .. " argument must be 'true' or 'false'")
+ end
+ cur[field] = (arg == "true")
+ end
+
+ local function block_start_arg(cmd, cur, field)
+ if not cur or cur.op ~= "RUN" then
+ fail(cmd .. " must be given in the context of a RUN")
+ end
+ if cur[field] then
+ fail(cmd .. " was already declared")
+ end
+
+ cur[field] = {
+ data = {}
+ }
+ cur_block = cur[field]
+ cur_block_name = cmd
+ table.insert(stack, "block start")
+ end
+
+ local test_env = require("spec.util.test_env")
+ local function expand_vars(line)
+ if not line then
+ return nil
+ end
+ return (line:gsub("%%%b{}", function(var)
+ var = var:sub(3, -2)
+ local fn, fnarg = var:match("^%s*([a-z_]+)%s*%(%s*([^)]+)%s*%)%s*$")
+
+ local value
+ if var == "tmpdir" then
+ value = "%{tmpdir}"
+ elseif var == "url(%{tmpdir})" then
+ value = "%{url(%{tmpdir})}"
+ elseif fn == "url" then
+ value = expand_vars(fnarg)
+ value = value:gsub("\\", "/")
+ elseif fn == "path" then
+ value = expand_vars(fnarg)
+ value = value:gsub("[/\\]", dir_sep)
+ elseif fn == "version" then
+ value = versions[fnarg:lower()] or ""
+ elseif fn == "version_" then
+ value = (versions[fnarg:lower()] or ""):gsub("[%.%-]", "_")
+ else
+ value = test_env.testing_paths[var]
+ or test_env.env_variables[var]
+ or test_env[var]
+ or ""
+ end
+
+ return value
+ end))
+ end
+
+ if input:sub(#input, #input) ~= "\n" then
+ input = input .. "\n"
+ end
+
+ for line in input:gmatch("([^\r\n]*)\r?\n?") do
+ cur_line = cur_line + 1
+
+ local state = stack[#stack]
+ if state == "start" then
+ local cmd, arg = parse_cmd(line)
+ if cmd == "TEST" then
+ start_test(arg)
+ elseif cmd == "SUITE" then
+ cur_suite = arg .. ": "
+ elseif cmd then
+ fail("expected TEST, got " .. cmd)
+ elseif is_blank(line) then
+ -- skip blank lines and arbitrary text,
+ -- which is interpreted as a comment
+ end
+ elseif state == "test" then
+ local cmd, arg = parse_cmd(line)
+ arg = expand_vars(arg)
+ if cmd == "FILE" then
+ cur_op = {
+ op = "FILE",
+ name = arg,
+ data = {},
+ }
+ table.insert(cur_test.ops, cur_op)
+ cur_block = cur_op
+ cur_block_name = "FILE"
+ table.insert(stack, "block start")
+ elseif cmd == "FILE_CONTENTS" then
+ cur_op = {
+ op = "FILE_CONTENTS",
+ name = arg,
+ data = {},
+ }
+ table.insert(cur_test.ops, cur_op)
+ cur_block = cur_op
+ cur_block_name = "FILE_CONTENTS"
+ table.insert(stack, "block start")
+ elseif cmd == "RUN" then
+ local program, args = arg:match("([^ ]+)%s*(.*)$")
+ if not program then
+ fail("expected a program argument in RUN")
+ end
+
+ cur_op = {
+ op = "RUN",
+ exit = 0,
+ exit_line = cur_line,
+ line = cur_line,
+ program = program,
+ args = args,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "EXISTS" then
+ cur_op = {
+ op = "EXISTS",
+ name = dir.normalize(arg),
+ line = cur_line,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "NOT_EXISTS" then
+ cur_op = {
+ op = "NOT_EXISTS",
+ name = dir.normalize(arg),
+ line = cur_line,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "MKDIR" then
+ cur_op = {
+ op = "MKDIR",
+ name = dir.normalize(arg),
+ line = cur_line,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "RMDIR" then
+ cur_op = {
+ op = "RMDIR",
+ name = dir.normalize(arg),
+ line = cur_line,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "RM" then
+ cur_op = {
+ op = "RM",
+ name = dir.normalize(arg),
+ line = cur_line,
+ }
+ table.insert(cur_test.ops, cur_op)
+ elseif cmd == "EXIT" then
+ if not cur_op or cur_op.op ~= "RUN" then
+ fail("EXIT must be given in the context of a RUN")
+ end
+
+ local code = tonumber(arg)
+ if not code and not (code >= 0 and code <= 128) then
+ fail("EXIT code must be a number in the range 0-128, got " .. arg)
+ end
+
+ cur_op.exit = code
+ cur_op.exit_line = cur_line
+ elseif cmd == "VERBOSE" then
+ if not cur_op or cur_op.op ~= "RUN" then
+ fail("VERBOSE must be given in the context of a RUN")
+ end
+
+ bool_arg("VERBOSE", cur_op, "verbose", arg)
+ elseif cmd == "STDERR" then
+ block_start_arg("STDERR", cur_op, "stderr")
+ elseif cmd == "NOT_STDERR" then
+ block_start_arg("NOT_STDERR", cur_op, "not_stderr")
+ elseif cmd == "STDOUT" then
+ block_start_arg("STDOUT", cur_op, "stdout")
+ elseif cmd == "NOT_STDOUT" then
+ block_start_arg("NOT_STDOUT", cur_op, "not_stdout")
+ elseif cmd == "PENDING" then
+ bool_arg("PENDING", cur_test, "pending", arg)
+ elseif cmd == "TEST" then
+ table.remove(stack)
+ start_test(arg)
+ elseif cmd then
+ fail("expected a command, got " .. cmd)
+ else
+ -- skip blank lines and arbitrary text,
+ -- which is interpreted as a comment
+ end
+ elseif state == "block start" then
+ local cmd, arg = parse_cmd(line)
+ if is_blank(line) then
+ -- skip
+ elseif is_hr(line) then
+ stack[#stack] = "block data"
+ cur_block.start = cur_line
+ elseif cmd == "PLAIN" then
+ bool_arg("PLAIN", cur_block, "plain", arg)
+ else
+ fail("expected '-----' to start " .. cur_block_name .. " block")
+ end
+ elseif state == "block data" then
+ if is_hr(line) then
+ cur_block = nil
+ table.remove(stack)
+ else
+ if not cur_block.plain then
+ line = expand_vars(line)
+ end
+ table.insert(cur_block.data, line)
+ end
+ end
+ end
+
+ return tests
+end
+
+local function check_output(write, block, block_name, data_var)
+ if block then
+ local is_positive = not block_name:match("NOT")
+ local err_msg = is_positive and "did not match" or "did match unwanted output"
+
+ write([=[ do ]=])
+ write([=[ local block_at = 1 ]=])
+ write([=[ local s, e, line, ok ]=])
+ for i, line in ipairs(block.data) do
+ write(([=[ line = %q ]=]):format(line))
+ write(([=[ s, e = string.find(%s, line, block_at, true) ]=]):format(data_var))
+ write(is_positive and ([=[ ok = s; if e then block_at = e + 1 end ]=]):format(i)
+ or ([=[ ok = not s ]=]))
+ write(([=[ assert(ok, error_message(%d, "%s %s: " .. line, %s)) ]=]):format(block.start + i, block_name, err_msg, data_var))
+ end
+ write([=[ end ]=])
+ end
+end
+
+function quick.compile(filename, env)
+ local tests = parse(filename)
+
+-- local dev_null = (package.config:sub(1, 1) == "/")
+-- and "/dev/null"
+-- or "NUL"
+
+ local cmd_helpers = {
+ ["luarocks"] = "luarocks_cmd",
+ ["luarocks-admin"] = "luarocks_admin_cmd",
+ }
+
+ for tn, t in ipairs(tests) do
+ local code = {}
+ local function write(...)
+ table.insert(code, table.concat({...}))
+ end
+
+ write(([=[ ]=]))
+ write(([=[ -- **************************************** ]=]))
+ write(([=[ -- %s ]=]):format(t.name))
+ write(([=[ -- **************************************** ]=]))
+ write(([=[ ]=]))
+
+ write([=[ local test_env = require("spec.util.test_env") ]=])
+ write([=[ local lfs = require("lfs") ]=])
+ write([=[ local fs = require("lfs") ]=])
+ write([=[ local dir_sep = package.config:sub(1, 1) ]=])
+ write([=[ local coverage = " -e \"require('luacov.runner')([[" .. test_env.testing_paths.testrun_dir .. dir_sep .. "luacov.config]])\" " ]=])
+ write([=[ local luarocks_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. coverage .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks", false, test_env.env_variables):sub(1, -5) ]=])
+ write([=[ local luarocks_admin_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. coverage .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks-admin", false, test_env.env_variables):sub(1, -5) ]=])
+
+ write([=[ local function make_dir(dirname) ]=])
+ write([=[ local bits = {} ]=])
+ write([=[ if dirname:sub(1, 1) == dir_sep then bits[1] = "" end ]=])
+ write([=[ local ok, err ]=])
+ write([=[ for p in dirname:gmatch("[^" .. dir_sep .. "]+") do ]=])
+ write([=[ table.insert(bits, p) ]=])
+ write([=[ ok, err = lfs.mkdir(table.concat(bits, dir_sep)) ]=])
+ write([=[ end ]=])
+ write([=[ local exists = (lfs.attributes(dirname) or {}).mode == "directory" ]=])
+ write([=[ return exists, (not exists) and err ]=])
+ write([=[ end ]=])
+
+ write(([=[ local function error_message(line, msg, input) ]=]))
+ write(([=[ local out = {"\n\n", %q, ":", line, ": ", msg} ]=]):format(filename))
+ write(([=[ if input then ]=]))
+ write(([=[ if input:match("\n") then ]=]))
+ write(([=[ table.insert(out, "\n") ]=]))
+ write(([=[ table.insert(out, ("-"):rep(40)) ]=]))
+ write(([=[ table.insert(out, "\n") ]=]))
+ write(([=[ table.insert(out, input) ]=]))
+ write(([=[ table.insert(out, ("-"):rep(40)) ]=]))
+ write(([=[ table.insert(out, "\n") ]=]))
+ write(([=[ else ]=]))
+ write(([=[ table.insert(out, ": ") ]=]))
+ write(([=[ table.insert(out, input) ]=]))
+ write(([=[ end ]=]))
+ write(([=[ end ]=]))
+ write(([=[ return table.concat(out) ]=]))
+ write(([=[ end ]=]))
+
+ write([=[ return function() ]=])
+ write([=[ test_env.run_in_tmp(function(tmpdir) ]=])
+ write([=[ local function handle_tmpdir(s) ]=])
+ write([=[ return (s:gsub("%%{url%(%%{tmpdir}%)}", (tmpdir:gsub("\\", "/"))) ]=])
+ write([=[ :gsub("%%{tmpdir}", (tmpdir:gsub("[\\/]", dir_sep)))) ]=])
+ write([=[ end ]=])
+ write([=[ local ok, err ]=])
+ for _, op in ipairs(t.ops) do
+ if op.name then
+ op.name = native_slash(op.name)
+ write(([=[ local name = handle_tmpdir(%q) ]=]):format(op.name))
+ end
+ if op.op == "FILE" then
+ if op.name:match("[\\/]") then
+ write(([=[ make_dir(handle_tmpdir(%q)) ]=]):format(dir.dir_name(op.name)))
+ end
+ write([=[ test_env.write_file(name, handle_tmpdir([=====[ ]=])
+ for _, line in ipairs(op.data) do
+ write(line)
+ end
+ write([=[ ]=====]), finally) ]=])
+ elseif op.op == "EXISTS" then
+ write(([=[ ok, err = lfs.attributes(name) ]=]))
+ write(([=[ assert.truthy(ok, error_message(%d, "EXISTS failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line))
+ elseif op.op == "NOT_EXISTS" then
+ write(([=[ assert.falsy(lfs.attributes(name), error_message(%d, "NOT_EXISTS failed: " .. name .. " exists" )) ]=]):format(op.line))
+ elseif op.op == "MKDIR" then
+ write(([=[ ok, err = make_dir(name) ]=]))
+ write(([=[ assert.truthy((lfs.attributes(name) or {}).mode == "directory", error_message(%d, "MKDIR failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line))
+ elseif op.op == "RMDIR" then
+ write(([=[ ok, err = test_env.remove_dir(name) ]=]))
+ write(([=[ assert.falsy((lfs.attributes(name) or {}).mode == "directory", error_message(%d, "MKDIR failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line))
+ elseif op.op == "RM" then
+ write(([=[ ok, err = os.remove(name) ]=]))
+ write(([=[ assert.falsy((lfs.attributes(name) or {}).mode == "file", error_message(%d, "RM failed: " .. name .. " - " .. (err or "") )) ]=]):format(op.line))
+ elseif op.op == "FILE_CONTENTS" then
+ write(([=[ do ]=]))
+ write(([=[ local fd_file = assert(io.open(name, "rb")) ]=]))
+ write(([=[ local file_data = fd_file:read("*a") ]=]))
+ write(([=[ fd_file:close() ]=]))
+ write([=[ local block_at = 1 ]=])
+ write([=[ local s, e, line ]=])
+ for i, line in ipairs(op.data) do
+ write(([=[ line = %q ]=]):format(line))
+ write(([=[ s, e = string.find(file_data, line, 1, true) ]=]))
+ write(([=[ assert(s, error_message(%d, "FILE_CONTENTS " .. name .. " did not match: " .. line, file_data)) ]=]):format(op.start + i))
+ write(([=[ block_at = e + 1 ]=]):format(i))
+ end
+ write([=[ end ]=])
+ elseif op.op == "RUN" then
+ local cmd_helper = cmd_helpers[op.program] or ("%q"):format(op.program)
+ local redirs = " 1>stdout.txt 2>stderr.txt "
+ write(([=[ local ok, _, code = os.execute(%s .. " " .. %q .. %q) ]=]):format(cmd_helper, op.args, redirs))
+ write([=[ if type(ok) == "number" then code = (ok >= 256 and ok / 256 or ok) end ]=])
+
+ write([=[ local fd_stderr = assert(io.open("stderr.txt", "rb")) ]=])
+ write([=[ local stderr_data = fd_stderr:read("*a") ]=])
+ write([=[ fd_stderr:close() ]=])
+
+ write([=[ if stderr_data:match("please report") then ]=])
+ write(([=[ assert(false, error_message(%d, "RUN crashed: ", stderr_data)) ]=]):format(op.line))
+ write([=[ end ]=])
+
+ if op.stdout or op.not_stdout or op.verbose then
+ write([=[ local fd_stdout = assert(io.open("stdout.txt", "rb")) ]=])
+ write([=[ local stdout_data = fd_stdout:read("*a") ]=])
+ write([=[ fd_stdout:close() ]=])
+ end
+
+ if op.verbose then
+ write([=[ print() ]=])
+ write([=[ print("STDOUT: --" .. ("-"):rep(70)) ]=])
+ write([=[ print(stdout_data) ]=])
+ write([=[ print("STDERR: --" .. ("-"):rep(70)) ]=])
+ write([=[ print(stderr_data) ]=])
+ write([=[ print(("-"):rep(80)) ]=])
+ write([=[ print() ]=])
+ end
+
+ check_output(write, op.stdout, "STDOUT", "stdout_data")
+ check_output(write, op.stderr, "STDERR", "stderr_data")
+
+ check_output(write, op.not_stdout, "NOT_STDOUT", "stdout_data")
+ check_output(write, op.not_stderr, "NOT_STDERR", "stderr_data")
+
+ if op.exit then
+ write(([=[ assert.same(%d, code, error_message(%d, "EXIT did not match: " .. %d, stderr_data)) ]=]):format(op.exit, op.exit_line, op.exit))
+ end
+ end
+ end
+ write([=[ end, finally) ]=])
+ write([=[ end ]=])
+
+ local program = table.concat(code, "\n")
+ local chunk = assert(load(program, "@" .. filename .. ":[TEST " .. tn .. "]", "t", env or _ENV))
+ if env and setfenv then
+ setfenv(chunk, env)
+ end
+ t.fn = chunk()
+ end
+
+ return tests
+end
+
+return quick