From 1a30b61741811659f755bbed2296bf8fcdf3a8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20L=C3=B6tscher?= Date: Sun, 3 Sep 2023 08:59:08 +0200 Subject: [PATCH 1/5] Add lunatest module --- test/unit_tests/lua/lunatest.lua | 1142 ++++++++++++++++++++++++++++++ 1 file changed, 1142 insertions(+) create mode 100644 test/unit_tests/lua/lunatest.lua diff --git a/test/unit_tests/lua/lunatest.lua b/test/unit_tests/lua/lunatest.lua new file mode 100644 index 000000000000..a48b48d49162 --- /dev/null +++ b/test/unit_tests/lua/lunatest.lua @@ -0,0 +1,1142 @@ +----------------------------------------------------------------------- +-- +-- Copyright (c) 2009-12 Scott Vokes +-- +-- Permission is hereby granted, free of charge, to any person +-- obtaining a copy of this software and associated documentation +-- files (the "Software"), to deal in the Software without +-- restriction, including without limitation the rights to use, +-- copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following +-- conditions: +-- +-- The above copyright notice and this permission notice shall be +-- included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +-- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +-- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +-- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +-- OTHER DEALINGS IN THE SOFTWARE. +-- +------------------------------------------------------------------------ +-- +-- This is a library for randomized testing with Lua. +-- For usage and examples, see README and the test suite. +-- +------------------------------------------------------------------------ + +------------ +-- Module -- +------------ + +-- standard libraries used +local debug, io, math, os, string, table = + debug, io, math, os, string, table + +-- required core global functions +local assert, error, ipairs, pairs, pcall, print, setmetatable, tonumber = + assert, error, ipairs, pairs, pcall, print, setmetatable, tonumber +local fmt, tostring, type = string.format, tostring, type +local unpack = table.unpack or unpack +local getmetatable, rawget, setmetatable, xpcall = + getmetatable, rawget, setmetatable, xpcall +local exit, next, require = os.exit, next, require + +-- Get containing env, using 5.1's getfenv or emulating it in 5.2 +local getenv = getfenv or function(level) + local info = debug.getinfo(level or 2) + local n, v = debug.getupvalue(info.func, 1) + assert(n == "_ENV", n) + return v +end + +---Use lhf's random, if available. It provides an RNG with better +-- statistical properties, and it gives consistent values across OSs. +-- http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#lrandom +local random = pcall(require, "random") and package.loaded.random or nil + +-- Use the debug API to get line numbers, if available. +local debug = pcall(require, "debug") and package.loaded.debug or debug + +-- Use luasocket's gettime(), luaposix' gettimeofday(), or os.time for +-- timestamps +local now = pcall(require, "socket") and package.loaded.socket.gettime + or pcall(require, "posix") and package.loaded.posix.gettimeofday and + function () + local t = package.loaded.posix.gettimeofday() + local s, us = t.sec, t.usec + return s + us / 1000000 + end + or os.time + +-- Check command line arguments: +-- -v / --verbose, default to verbose_hooks. +-- -s or --suite, only run the named suite(s). +-- -t or --test, only run tests matching the pattern. +local lt_arg = arg + +-- ##################### +-- # Utility functions # +-- ##################### + +local function printf(...) print(string.format(...)) end + +local function result_table(name) + return { name=name, pass={}, fail={}, skip={}, err={} } +end + +local function combine_results(to, from) + local s_name = from.name + for _,set in ipairs{"pass", "fail", "skip", "err" } do + local fs, ts = from[set], to[set] + for name,val in pairs(fs) do + ts[s_name .. "." .. name] = val + end + end +end + +local function is_func(v) return type(v) == "function" end + +local function count(t) + local ct = 0 + for _ in pairs(t) do ct = ct + 1 end + return ct +end + + +-- ########### +-- # Results # +-- ########### + +local function msec(t) + if t and type(t) == "number" then + return fmt(" (%.2fms)", t * 1000) + else + return "" + end +end + + +local RPass = {} +local passMT = {__index=RPass} +function RPass:tostring_char() return "." end +function RPass:add(s, name) s.pass[name] = self end +function RPass:type() return "pass" end +function RPass:tostring(name) + return fmt("PASS: %s%s%s", + name or "(unknown)", msec(self.elapsed), + self.msg and (": " .. tostring(self.msg)) or "") +end + + +local RFail = {} +local failMT = {__index=RFail} +function RFail:tostring_char() return "F" end +function RFail:add(s, name) s.fail[name] = self end +function RFail:type() return "fail" end +function RFail:tostring(name) + return fmt("FAIL: %s%s: %s%s%s", + name or "(unknown)", + msec(self.elapsed), + self.reason or "", + self.msg and (" - " .. tostring(self.msg)) or "", + self.line and (" (%d)"):format(self.line) or "") +end + + +local RSkip = {} +local skipMT = {__index=RSkip} +function RSkip:tostring_char() return "s" end +function RSkip:add(s, name) s.skip[name] = self end +function RSkip:type() return "skip" end +function RSkip:tostring(name) + return fmt("SKIP: %s()%s", name or "unknown", + self.msg and (" - " .. tostring(self.msg)) or "") +end + + +local RError = {} +local errorMT = {__index=RError} +function RError:tostring_char() return "E" end +function RError:add(s, name) s.err[name] = self end +function RError:type() return "error" end +function RError:tostring(name) + return self.msg or + fmt("ERROR (in %s%s, couldn't get traceback)", + msec(self.elapsed), name or "(unknown)") +end + + +local function Pass(t) return setmetatable(t or {}, passMT) end +local function Fail(t) return setmetatable(t, failMT) end +local function Skip(t) return setmetatable(t, skipMT) end +local function Error(t) return setmetatable(t, errorMT) end + + +-- ############## +-- # Assertions # +-- ############## + +---Renamed standard assert. +local checked = 0 +local TS = tostring + +local function wraptest(flag, msg, t) + checked = checked + 1 + t.msg = msg + if debug then + local info = debug.getinfo(3, "l") + t.line = info.currentline + end + if not flag then error(Fail(t)) end +end + +-- @module lunatest +local lunatest = {} +lunatest.VERSION = 0.95 + +---Fail a test. +-- @param no_exit Unless set to true, the presence of any failures +-- causes the test suite to terminate with an exit status of 1. +function lunatest.fail(msg, no_exit) + local line + if debug then + local info = debug.getinfo(2, "l") + line = info.currentline + end + error(Fail { msg=msg, reason="(Failed)", no_exit=no_exit, line=line }) +end + + +---Skip a test, with a note, e.g. "TODO". +function lunatest.skip(msg) error(Skip { msg=msg }) end + + +---got == true. +-- (Named "assert_true" to not conflict with standard assert.) +-- @param msg Message to display with the result. +function lunatest.assert_true(got, msg) + wraptest(got, msg, { reason=fmt("Expected success, got %s.", TS(got)) }) +end + +---got == false. +function lunatest.assert_false(got, msg) + wraptest(not got, msg, + { reason=fmt("Expected false, got %s", TS(got)) }) +end + +--got == nil +function lunatest.assert_nil(got, msg) + wraptest(got == nil, msg, + { reason=fmt("Expected nil, got %s", TS(got)) }) +end + +--got ~= nil +function lunatest.assert_not_nil(got, msg) + wraptest(got ~= nil, msg, + { reason=fmt("Expected non-nil value, got %s", TS(got)) }) +end + +local function tol_or_msg(t, m) + if not t and not m then return 0, nil + elseif type(t) == "string" then return 0, t + elseif type(t) == "number" then return t, m + else error("Neither a numeric tolerance nor string") + end +end + + +---exp == got. +function lunatest.assert_equal(exp, got, tol, msg) + tol, msg = tol_or_msg(tol, msg) + if type(exp) == "number" and type(got) == "number" then + wraptest(math.abs(exp - got) <= tol, msg, + { reason=fmt("Expected %s +/- %s, got %s", + TS(exp), TS(tol), TS(got)) }) + else + wraptest(exp == got, msg, + { reason=fmt("Expected %q, got %q", TS(exp), TS(got)) }) + end +end + +---exp ~= got. +function lunatest.assert_not_equal(exp, got, msg) + wraptest(exp ~= got, msg, + { reason="Expected something other than " .. TS(exp) }) +end + +---val > lim. +function lunatest.assert_gt(lim, val, msg) + wraptest(val > lim, msg, + { reason=fmt("Expected a value > %s, got %s", + TS(lim), TS(val)) }) +end + +---val >= lim. +function lunatest.assert_gte(lim, val, msg) + wraptest(val >= lim, msg, + { reason=fmt("Expected a value >= %s, got %s", + TS(lim), TS(val)) }) +end + +---val < lim. +function lunatest.assert_lt(lim, val, msg) + wraptest(val < lim, msg, + { reason=fmt("Expected a value < %s, got %s", + TS(lim), TS(val)) }) +end + +---val <= lim. +function lunatest.assert_lte(lim, val, msg) + wraptest(val <= lim, msg, + { reason=fmt("Expected a value <= %s, got %s", + TS(lim), TS(val)) }) +end + +---#val == len. +function lunatest.assert_len(len, val, msg) + wraptest(#val == len, msg, + { reason=fmt("Expected #val == %d, was %d", + len, #val) }) +end + +---#val ~= len. +function lunatest.assert_not_len(len, val, msg) + wraptest(#val ~= len, msg, + { reason=fmt("Expected length other than %d", len) }) +end + +---Test that the string s matches the pattern exp. +function lunatest.assert_match(pat, s, msg) + s = tostring(s) + wraptest(type(s) == "string" and s:match(pat), msg, + { reason=fmt("Expected string to match pattern %s, was %s", + pat, + (s:len() > 30 and (s:sub(1,30) .. "...")or s)) }) +end + +---Test that the string s doesn't match the pattern exp. +function lunatest.assert_not_match(pat, s, msg) + wraptest(type(s) ~= "string" or not s:match(pat), msg, + { reason=fmt("Should not match pattern %s", pat) }) +end + +---Test that val is a boolean. +function lunatest.assert_boolean(val, msg) + wraptest(type(val) == "boolean", msg, + { reason=fmt("Expected type boolean but got %s", + type(val)) }) +end + +---Test that val is not a boolean. +function lunatest.assert_not_boolean(val, msg) + wraptest(type(val) ~= "boolean", msg, + { reason=fmt("Expected type other than boolean but got %s", + type(val)) }) +end + +---Test that val is a number. +function lunatest.assert_number(val, msg) + wraptest(type(val) == "number", msg, + { reason=fmt("Expected type number but got %s", + type(val)) }) +end + +---Test that val is not a number. +function lunatest.assert_not_number(val, msg) + wraptest(type(val) ~= "number", msg, + { reason=fmt("Expected type other than number but got %s", + type(val)) }) +end + +---Test that val is a string. +function lunatest.assert_string(val, msg) + wraptest(type(val) == "string", msg, + { reason=fmt("Expected type string but got %s", + type(val)) }) +end + +---Test that val is not a string. +function lunatest.assert_not_string(val, msg) + wraptest(type(val) ~= "string", msg, + { reason=fmt("Expected type other than string but got %s", + type(val)) }) +end + +---Test that val is a table. +function lunatest.assert_table(val, msg) + wraptest(type(val) == "table", msg, + { reason=fmt("Expected type table but got %s", + type(val)) }) +end + +---Test that val is not a table. +function lunatest.assert_not_table(val, msg) + wraptest(type(val) ~= "table", msg, + { reason=fmt("Expected type other than table but got %s", + type(val)) }) +end + +---Test that val is a function. +function lunatest.assert_function(val, msg) + wraptest(type(val) == "function", msg, + { reason=fmt("Expected type function but got %s", + type(val)) }) +end + +---Test that val is not a function. +function lunatest.assert_not_function(val, msg) + wraptest(type(val) ~= "function", msg, + { reason=fmt("Expected type other than function but got %s", + type(val)) }) +end + +---Test that val is a thread (coroutine). +function lunatest.assert_thread(val, msg) + wraptest(type(val) == "thread", msg, + { reason=fmt("Expected type thread but got %s", + type(val)) }) +end + +---Test that val is not a thread (coroutine). +function lunatest.assert_not_thread(val, msg) + wraptest(type(val) ~= "thread", msg, + { reason=fmt("Expected type other than thread but got %s", + type(val)) }) +end + +---Test that val is a userdata (light or heavy). +function lunatest.assert_userdata(val, msg) + wraptest(type(val) == "userdata", msg, + { reason=fmt("Expected type userdata but got %s", + type(val)) }) +end + +---Test that val is not a userdata (light or heavy). +function lunatest.assert_not_userdata(val, msg) + wraptest(type(val) ~= "userdata", msg, + { reason=fmt("Expected type other than userdata but got %s", + type(val)) }) +end + +---Test that a value has the expected metatable. +function lunatest.assert_metatable(exp, val, msg) + local mt = getmetatable(val) + wraptest(mt == exp, msg, + { reason=fmt("Expected metatable %s but got %s", + TS(exp), TS(mt)) }) +end + +---Test that a value does not have a given metatable. +function lunatest.assert_not_metatable(exp, val, msg) + local mt = getmetatable(val) + wraptest(mt ~= exp, msg, + { reason=fmt("Expected metatable other than %s", + TS(exp)) }) +end + +---Test that the function raises an error when called. +function lunatest.assert_error(f, msg) + local ok, err = pcall(f) + local got = ok or err + wraptest(not ok, msg, + { exp="an error", got=got, + reason=fmt("Expected an error, got %s", TS(got)) }) +end + +-- ######### +-- # Hooks # +-- ######### + +local dot_ct = 0 +local cols = 70 + +local iow = io.write + +-- Print a char ([.fEs], etc.), wrapping at 70 columns. +local function dot(c) + c = c or "." + io.write(c) + dot_ct = dot_ct + 1 + if dot_ct > cols then + io.write("\n ") + dot_ct = 0 + end + io.stdout:flush() +end + +local function print_totals(r) + local ps, fs = count(r.pass), count(r.fail) + local ss, es = count(r.skip), count(r.err) + if checked == 0 then return end + local el, unit = r.t_post - r.t_pre, "s" + if el < 1 then unit = "ms"; el = el * 1000 end + local elapsed = fmt(" in %.2f %s", el, unit) + local buf = {"\n---- Testing finished%s, ", + "with %d assertion(s) ----\n", + " %d passed, %d failed, ", + "%d error(s), %d skipped."} + printf(table.concat(buf), elapsed, checked, ps, fs, es, ss) +end + + +---Default behavior. +default_hooks = { + begin = false, + begin_suite = function(s_env, tests) + iow(fmt("\n-- Starting suite %q, %d test(s)\n ", + s_env.name, count(tests))) + end, + end_suite = false, + pre_test = false, + post_test = function(name, res) + dot(res:tostring_char()) + end, + done = function(r) + print_totals(r) + for _,ts in ipairs{ r.fail, r.err, r.skip } do + for name,res in pairs(ts) do + printf("%s", res:tostring(name)) + end + end + end, +} + + +---Default verbose behavior. +verbose_hooks = { + begin = function(res, suites) + local s_ct = count(suites) + if s_ct > 0 then + printf("Starting tests, %d suite(s)", s_ct) + end + end, + begin_suite = function(s_env, tests) + dot_ct = 0 + printf("-- Starting suite %q, %d test(s)", + s_env.name, count(tests)) + end, + end_suite = + function(s_env) + local ps, fs = count(s_env.pass), count(s_env.fail) + local ss, es = count(s_env.skip), count(s_env.err) + dot_ct = 0 + printf(" Finished suite %q, +%d -%d E%d s%d", + s_env.name, ps, fs, es, ss) + end, + pre_test = false, + post_test = function(name, res) + printf("%s", res:tostring(name)) + dot_ct = 0 + end, + done = function(r) print_totals(r) end +} + +setmetatable(verbose_hooks, {__index = default_hooks }) + + +-- ################ +-- # Registration # +-- ################ + +local suites = {} +local failed_suites = {} + +---Check if a function name should be considered a test key. +-- Defaults to functions starting or ending with "test" +local function is_test_key(k) + return type(k) == "string" and (k:match("^test.*") or k:match("test$")) +end + +-- export is_test_key to enable user to customize this matching function +lunatest.is_test_key = is_test_key + +local function get_tests(mod) + local ts = {} + if type(mod) == "table" then + for k,v in pairs(mod) do + if is_test_key(k) and type(v) == "function" then + ts[k] = v + end + end + ts.setup = rawget(mod, "setup") + ts.teardown = rawget(mod, "teardown") + ts.ssetup = rawget(mod, "suite_setup") + ts.steardown = rawget(mod, "suite_teardown") + return ts + end + return {} +end + +---Add a file as a test suite. +-- @param modname The module to load as a suite. The file is +-- interpreted in the same manner as require "modname". +-- Which functions are tests is determined by is_test_key(name). +function lunatest.suite(modname) + local ok, err = pcall( + function() + local mod, r_err = require(modname) + table.insert(suites, {name = modname, tests = get_tests(mod)}) + end) + if not ok then + print(fmt(" * Error loading test suite %q:\n%s", + modname, tostring(err))) + failed_suites[#failed_suites+1] = modname + end +end + + +-- ########### +-- # Running # +-- ########### + +local ok_types = { pass=true, fail=true, skip=true } + +local function err_handler(name) + return function (e) + if type(e) == "table" and e.type and ok_types[e.type()] then return e end + local msg = fmt("\nERROR in %s():\n\t%s", name, tostring(e)) + msg = debug.traceback(msg, 3) + return Error { msg=msg } + end +end + + +local function run_test(name, test, suite, hooks, setup, teardown) + local result + if is_func(hooks.pre_test) then hooks.pre_test(name) end + local t_pre, t_post, elapsed --timestamps. requires luasocket. + local ok, err + + if is_func(setup) then + ok, err = xpcall(function() setup(name) end, err_handler(name)) + else + ok = true + end + + if ok then + t_pre = now() + ok, err = xpcall(test, err_handler(name)) + t_post = now() + elapsed = t_post - t_pre + + if is_func(teardown) then + if ok then + ok, err = xpcall(function() teardown(name, elapsed) end, + err_handler(name)) + else + xpcall(function() teardown(name, elapsed) end, + function(info) + print "\n===============================================" + local msg = fmt("ERROR in teardown handler: %s", info) + print(msg) + os.exit(1) + end) + end + end + end + + if ok then err = Pass() end + result = err + result.elapsed = elapsed + + -- TODO: log tests w/ no assertions? + result:add(suite, name) + + if is_func(hooks.post_test) then hooks.post_test(name, result) end +end + + +local function cmd_line_switches(arg) + local arg = arg or {} + local opts = {} + for i=1,#arg do + local v = arg[i] + if v == "-v" or v == "--verbose" then opts.verbose=true + elseif v == "-s" or v == "--suite" then + opts.suite_pat = arg[i+1] + elseif v == "-t" or v == "--test" then + opts.test_pat = arg[i+1] + end + end + return opts +end + + +local function failure_or_error_count(r) + local t = 0 + for k,f in pairs(r.err) do + t = t + 1 + end + for k,f in pairs(r.fail) do + if not f.no_exit then t = t + 1 end + end + return t +end + +local function run_suite(hooks, opts, results, sname, tests) + local ssetup, steardown = tests.ssetup, tests.steardown + tests.ssetup, tests.steardown = nil, nil + + if not opts.suite_pat or sname:match(opts.suite_pat) then + local run_suite = true + local res = result_table(sname) + + if ssetup then + local ok, err = pcall(ssetup) + if not ok or (ok and err == false) then + run_suite = false + local msg = fmt("Error in %s's suite_setup: %s", sname, tostring(err)) + failed_suites[#failed_suites+1] = sname + results.err[sname] = Error{msg=msg} + end + end + + if run_suite and count(tests) > 0 then + local setup, teardown = tests.setup, tests.teardown + tests.setup, tests.teardown = nil, nil + + if hooks.begin_suite then hooks.begin_suite(res, tests) end + res.tests = tests + for name, test in pairs(tests) do + if not opts.test_pat or name:match(opts.test_pat) then + run_test(name, test, res, hooks, setup, teardown) + end + end + if steardown then pcall(steardown) end + if hooks.end_suite then hooks.end_suite(res) end + combine_results(results, res) + end + end +end + +---Run all known test suites, with given configuration hooks. +-- @param hooks Override the default hooks. +-- @param opts Override command line arguments. +-- @usage If no hooks are provided and arg[1] == "-v", the verbose_hooks will +-- be used. opts is expected to be a table of command line arguments. +function lunatest.run(hooks, opts) + -- also check the namespace it's run in + local opts = opts and cmd_line_switches(opts) or cmd_line_switches(lt_arg) + + -- Make stdout line-buffered for better interactivity when the output is + -- not going to the terminal, e.g. is piped to another program. + io.stdout:setvbuf("line") + + if hooks == true or opts.verbose then + hooks = verbose_hooks + else + hooks = hooks or {} + end + + setmetatable(hooks, {__index = default_hooks}) + + local results = result_table("main") + results.t_pre = now() + + -- If it's all in one test file, check its environment, too. + local env = getenv(3) + if env then + local main_suite = {name = "main", tests = get_tests(env)} + table.insert(suites, main_suite) + end + + if hooks.begin then hooks.begin(results, suites) end + + for _,suite in ipairs(suites) do + run_suite(hooks, opts, results, suite.name, suite.tests) + end + results.t_post = now() + if hooks.done then hooks.done(results) end + + local failures = failure_or_error_count(results) + if failures > 0 then os.exit(failures) end + if #failed_suites > 0 then os.exit(#failed_suites) end +end + + +-- ######################## +-- # Randomization basics # +-- ######################## + +local set_seed +local random_int +local random_bool +local random_float + +if random then + local _r = random.new() + + set_seed = function(s) _r:seed(s) end + random_int = function(low, high) + if not high then high = low; low = 0 end + return _r:value(low, high) + end + random_bool = function() return random_int(0, 1) == 1 end + random_float = function(low, high) + return random_int(low, high - 1) + _r:value() + end +else + set_seed = math.randomseed + random_bool = function() return math.random(0, 1) == 1 end + random_float = function(l, h) + return random_int(l, h - 1) + math.random() + end + random_int = function(l, h) + if not h then h = l; l = 0 end + return math.random(l, h) + end +end + +-- Lua_number's bits of precision. IEEE 754 doubles have 52. +local function determine_accuracy() + for i=1,128 do + if 2^i == (2^i + 1) then return i - 1 end + end + return 128 --long long ints? +end +local bits_of_accuracy = determine_accuracy() + + +-- ################## +-- # Random strings # +-- ################## + + +-- For valid char classes, see Lua Reference Manual 5.1, p. 77 +-- or http://www.lua.org/manual/5.1/manual.html#5.4.1 . +local function charclass(pat) + local m = {} + + local match, char = string.match, string.char + for i=0,255 do + local c = char(i) + if match(c, pat) then m[#m+1] = c end + end + + return table.concat(m) +end + + +-- Return a (() -> random char) iterator from a pattern. +local function parse_pattern(pattern) + local cs = {} --charset + local idx = 1 + local len = string.len(pattern) + assert(len > 0, "Cannot generate pattern from empty string.") + + local function at_either_end() return #cs == 0 or #cs == len end + local function slice(i) return string.sub(pattern, i, i) end + + while idx <= len do + local c = slice(idx) + + if c == "-" then + if at_either_end() then + cs[#cs+1] = c --literal - at start or end + else --range + local low = string.byte(slice(idx-1)) + 1 + local high = string.byte(slice(idx+1)) + assert(low < high, "Invalid character range: " .. pattern) + for asc=low,high do + cs[#cs+1] = string.char(asc) + end + idx = idx + 1 + end + + elseif c == "%" then + local nextc = slice(idx + 1) + cs[#cs+1] = charclass("%" .. nextc) + idx = idx + 1 + + else + cs[#cs+1] = c + end + idx = idx + 1 + end + + cs = table.concat(cs) + local len = string.len(cs) + assert(len > 0, "Empty charset") + + return function() + local idx = random_int(1, len) + return string.sub(cs, idx, idx) + end +end + + +-- Read a random string spec, return a config table. +local function parse_randstring(s) + local low, high, rest = string.match(s, "([0-9]+),?([0-9]*) (.*)") + if low then --any match + if high == "" then high = low end + return { low = tonumber(low), + high = tonumber(high), + gen = parse_pattern(rest) } + else + local err = "Invalid random string spec: " .. s + error(err, 2) + end +end + + +-- Generate a random string. +-- @usage e.g. "20 listoftwentycharstogenerate" or "10,20 %l". +local function random_string(spec) + local info = parse_randstring(spec) + local ct, diff + diff = info.high - info.low + if diff == 0 then ct = info.low else + ct = random_int(diff) + info.low + end + + local acc = {} + for i=1,ct do + acc[i] = info.gen(self) + end + local res = table.concat(acc) + assert(res:len() == ct, "Bad string gen") + return res +end + + +-- ######################### +-- # General random values # +-- ######################### + +-- Generate a random number, according to arg. +local function gen_number(arg) + arg = arg or math.huge + local signed = (arg < 0) + local float + if signed then float = (math.ceil(arg) ~= arg) else + float = (math.floor(arg) ~= arg) + end + + local f = float and random_float or random_int + if signed then + return f(arg, -arg) + else + return f(0, arg) + end +end + + +-- Create an arbitrary instance of a value. +local function generate_arbitrary(arg) + local t = type(arg) + if t == "number" then + return gen_number(arg) + elseif t == "function" then + return arg(gen_number()) -- assume f(number) -> val + elseif t == "string" then + return random_string(arg) + elseif t == "table" or t == "userdata" then + assert(arg.__random, t .. " has no __random method") + -- assume arg.__random(number) -> val + return arg.__random(gen_number()) + elseif t == "boolean" then + return random_bool() + else + error("Cannot randomly generate values of type " .. t .. ".") + end +end + + +local random_test_defaults = { + count = 100, + max_failures = 10, + max_errors = 5, + max_skips = 50, + random_bound = 2^bits_of_accuracy, + seed_limit = math.min(1e13, 2^bits_of_accuracy), + always = {}, + seed = nil, + show_progress = true +} + + +local function random_args(args) + local as = {} + for i=1,#args do + as[i] = generate_arbitrary(args[i]) + end + return as +end + + +local function new_seed(limit) + limit = limit or 1e13 + return random_int(0, limit) +end + + +local function get_seeds_and_args(t) + local ss = {} + for _,r in ipairs(t) do + if r.seed then + ss[#ss+1] = fmt("%s %s\n Seed: %s", + r.reason or "", r.msg and ("\n " .. r.msg) or "", r.seed) + end + if r.args then + for i,arg in ipairs(r.args) do + ss[#ss+1] = " * " .. arg + end + end + ss[#ss+1] = "" + end + return ss +end + + +local function run_randtest(seed, f, args, r, limit) + local try_ct = 0 + while r.tried[seed] and try_ct < 50 do + seed = new_seed(limit) + try_ct = try_ct + 1 + end + if try_ct >= 50 then + error(Fail { reason = "Exhausted all seeds" }) + end + set_seed(seed) + r.tried[seed] = true + + local result + local r_args = random_args(args) + local ok, err = pcall(function() f(unpack(r_args)) end) + if ok then + result = Pass() + result.seed = seed + r.ps[#r.ps+1] = result + else + -- So errors in the suite itself get through... + if type(err) == "string" then error(err) end + result = err + result.seed = seed + local rt = result:type() + if rt == "pass" then r.ps[#r.ps+1] = result + elseif rt == "fail" then r.fs[#r.fs+1] = result + elseif rt == "error" then r.es[#r.es+1] = result + elseif rt == "skip" then r.ss[#r.ss+1] = result + else error("unmatched") + end + end + + seed = new_seed(limit) + r.ts = r.ts + 1 + local str_args = {} + -- Convert args to strs (for display) and add to result. + for i,v in ipairs(r_args) do + str_args[i] = tostring(v) + end + result.args = str_args + return seed +end + + +local function report_trial(r, opt) + if #r.es > 0 then + local seeds = get_seeds_and_args(r.es) + error(Fail { reason = fmt("%d tests, %d error(s).\n %s", + r.ts, #r.es, + table.concat(seeds, "\n ")), + seeds = seeds}) + elseif #r.fs > 0 then + local seeds = get_seeds_and_args(r.fs) + error(Fail { reason = fmt("%d tests, %d failure(s).\n %s", + r.ts, #r.fs, + table.concat(seeds, "\n ")), + seeds = seeds}) + elseif #r.ss >= opt.max_skips then + error(Fail { reason = fmt("Too many cases skipped.")}) + else + error(Pass { reason = fmt(": %d cases passed.", #r.ps) }) + end +end + + +---Set random seed. +lunatest.set_seed = set_seed + +---Get a random value low <= x <= high. +lunatest.random_int = random_int + +---Get a random bool. +lunatest.random_bool = random_bool + +---Get a random float low <= x < high. +lunatest.random_float = random_float + +---Get a random string +lunatest.random_string = random_string + +---Get a random argument +lunatest.random_args = random_args + +---Run a test case with randomly instantiated arguments, +-- running the test function f opt.count (default: 100) times. +-- @param opt A table with options, or just a test name string.
+-- opt.count: how many random trials to perform
+-- opt.seed: Start the batch of trials with a specific seed
+-- opt.always: Always test these seeds (for regressions)
+-- opt.show_progress: Whether to print a . after every opt.tick trials.
+-- opt.seed_limit: Max seed to allow.
+-- opt.max_failures, max_errors, max_skips: Give up after X of each.
+-- @param f A test function, run as f(unpack(randomized_args(...))) +-- @param ... the arg specification. For each argument, creates a +-- random instance of that type.
+-- boolean: return true or false
+-- number n: returns 0 <= x < n, or -n <= x < n if negative. +-- If n has a decimal component, so will the result.
+-- string: Specifiedd as "(len[,maxlen]) (pattern)".
+-- "10 %l" means 10 random lowercase letters.
+-- "10,30 [aeiou]" means between 10-30 vowels.
+-- function: Just call (as f()) and return result.
+-- table or userdata: Call v.__random() and return result.
+-- @usage +function lunatest.assert_random(opt, f, ...) + local args = { ... } + if type(opt) == "string" then + opt = { name=opt } + elseif type(opt) == "function" then + table.insert(args, 1, f) + f = opt + opt = {} + end + + setmetatable(opt, { __index=random_test_defaults }) + + local seed = opt.seed or os.time() + local r = { ps={}, fs={}, es={}, ss={}, ts=0, tried={} } + + -- Run these seeds every time, for easy regression testing. + for _,s in ipairs(opt.always) do + run_randtest(s, f, args, r, opt.seed_limit) + end + + set_seed(seed) + local tick = opt.tick or opt.count / 10 + + for i=1,opt.count do + seed = run_randtest(seed, f, args, r, opt.seed_limit) + if #r.ss >= opt.max_skips or + #r.fs >= opt.max_failures or + #r.es >= opt.max_errors then break + end + if opt.show_progress and i % tick == 0 then + dot(".") + end + end + local overall_status = (passed == count and "PASS" or "FAIL") + + report_trial(r, opt) +end + +-- export module +return lunatest From 6ce7069235fa087565c982e6107ee82b26e2639a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20L=C3=B6tscher?= Date: Sat, 2 Sep 2023 21:13:23 +0200 Subject: [PATCH 2/5] First working Lua unit tests --- src/core/gui/GladeGui.cpp | 2 +- test/unit_tests/lua/LuaTest.cpp | 79 +++++++++++++++++++++++++++++++ test/unit_tests/lua/plugin.ini | 15 ++++++ test/unit_tests/lua/test.lua | 66 ++++++++++++++++++++++++++ test/unit_tests/lua/testDoc.xopp | Bin 0 -> 41567 bytes 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 test/unit_tests/lua/LuaTest.cpp create mode 100644 test/unit_tests/lua/plugin.ini create mode 100644 test/unit_tests/lua/test.lua create mode 100644 test/unit_tests/lua/testDoc.xopp diff --git a/src/core/gui/GladeGui.cpp b/src/core/gui/GladeGui.cpp index 0d602a2a3fb8..6822b0f5e2de 100644 --- a/src/core/gui/GladeGui.cpp +++ b/src/core/gui/GladeGui.cpp @@ -37,7 +37,7 @@ GladeGui::GladeGui(GladeSearchpath* gladeSearchPath, const std::string& glade, c GladeGui::~GladeGui() { if (!gtk_widget_get_parent(window)) { - gtk_widget_destroy(window); + // gtk_widget_destroy(window); } } diff --git a/test/unit_tests/lua/LuaTest.cpp b/test/unit_tests/lua/LuaTest.cpp new file mode 100644 index 000000000000..816d5faf7bb5 --- /dev/null +++ b/test/unit_tests/lua/LuaTest.cpp @@ -0,0 +1,79 @@ +/* + * Xournal++ + * + * This file is part of the Xournal UnitTests + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#include + +#include +#include // for GApplication, G_APPLICATION +#include // for G_CALLBACK, g_signal_con... +#include +#include + +#include "control/Control.h" +#include "control/jobs/XournalScheduler.h" // for XournalScheduler +#include "gui/GladeSearchpath.h" // for GladeSearchpath + +extern "C" { +#include // for luaL_Reg, luaL_newstate, luaL_requiref +#include // for lua_getglobal, lua_getfield, lua_setf... +#include // for luaL_openlibs +} + +#include "plugin/luapi_application.h" // for luaopen_app + + +struct Data { + Data() = default; + Data(Data&&) = delete; + Data(Data const&) = delete; + auto operator=(Data&&) -> Data = delete; + auto operator=(Data const&) -> Data = delete; + + ~Data() {} + + std::unique_ptr gladePath; + std::unique_ptr control; + std::unique_ptr win; +}; + +void on_activate(GtkApplication* app, Data* data) { + auto uiPath = fs::path(PROJECT_SOURCE_DIR) / "ui"; + data->gladePath = std::make_unique(); + data->gladePath->addSearchDirectory(uiPath); + + data->control = std::make_unique(G_APPLICATION(app), data->gladePath.get(), true); + data->win = std::make_unique(data->gladePath.get(), data->control.get(), app); + data->control->initWindow(data->win.get()); + data->control->getScheduler()->start(); + Util::execInUiThread([=]() { data->control->getWindow()->getXournal()->layoutPages(); }); + gtk_application_add_window(app, GTK_WINDOW(data->win->getWindow())); + + data->win->show(nullptr); + data->control->addDefaultPage(""); + + auto pluginPath = fs::path(PROJECT_SOURCE_DIR) / "test" / "unit_tests" / "lua"; + auto plugin = std::make_unique(data->control.get(), pluginPath.filename().string(), pluginPath); + ASSERT_TRUE(plugin->isValid()); + plugin->loadScript(); + + g_application_quit(G_APPLICATION(app)); +} + +void on_shutdown(GApplication*, Data* data) { data->control->getScheduler()->stop(); } + +TEST(LuaTest, testPage) { + Data data; + GtkApplication* app = gtk_application_new("com.github.xournalpp.xournalpp", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(&on_activate), &data); + g_signal_connect(app, "shutdown", G_CALLBACK(&on_shutdown), &data); + g_application_run(G_APPLICATION(app), 0, NULL); + g_object_unref(app); +} diff --git a/test/unit_tests/lua/plugin.ini b/test/unit_tests/lua/plugin.ini new file mode 100644 index 000000000000..85a001c7e01f --- /dev/null +++ b/test/unit_tests/lua/plugin.ini @@ -0,0 +1,15 @@ +[about] +## Author / Copyright notice +author=Xournal++ Team + +description=This is used for unit tests + +## If the plugin is packed with Xournal++, use +## then it gets the same version number +version= + +[default] +enabled=true + +[plugin] +mainfile=test.lua diff --git a/test/unit_tests/lua/test.lua b/test/unit_tests/lua/test.lua new file mode 100644 index 000000000000..b8a20729a9dd --- /dev/null +++ b/test/unit_tests/lua/test.lua @@ -0,0 +1,66 @@ +local lunatest = require "lunatest" + +local assert_true, assert_false = lunatest.assert_true, lunatest.assert_false +local assert_diffvars = lunatest.assert_diffvars +local assert_boolean, assert_not_boolean = lunatest.assert_boolean, lunatest.assert_not_boolean +local assert_len, assert_not_len = lunatest.assert_len, lunatest.assert_not_len +local assert_match, assert_not_match = lunatest.assert_match, lunatest.assert_not_match +local assert_error = lunatest.assert_error +local assert_lt, assert_lte = lunatest.assert_lt, lunatest.assert_lte +local assert_gt, assert_gte = lunatest.assert_gt, lunatest.assert_gte +local assert_nil, assert_not_nil = lunatest.assert_nil, lunatest.assert_not_nil +local assert_equal, assert_not_equal = lunatest.assert_equal, lunatest.assert_not_equal +local assert_string, assert_not_string = lunatest.assert_string, lunatest.assert_not_string +local assert_metatable, assert_not_metatable = lunatest.assert_metatable, lunatest.assert_not_metatable +local assert_userdata, assert_not_userdata = lunatest.assert_userdata, lunatest.assert_not_userdata +local assert_thread, assert_not_thread = lunatest.assert_thread, lunatest.assert_not_thread +local assert_function, assert_not_function = lunatest.assert_function, lunatest.assert_not_function +local assert_table, assert_not_table = lunatest.assert_table, lunatest.assert_not_table +local assert_number, assert_not_number = lunatest.assert_number, lunatest.assert_not_number +local skip, fail = lunatest.skip, lunatest.fail + + +local sep = package.config:sub(1,1) -- "/" on Linux and MacOS, "\\" (escaped backslash) on Windows +local sourceDir = debug.getinfo(1).source:sub(2):match("(.*[/\\])"):gsub("/", sep) -- directory containing the Lua script +local testDoc = sourceDir .. "testDoc.xopp" -- path of the test document + +function test_docStructure() + local success = app.openFile(testDoc) + assert_true(success) + + app.setCurrentPage(3) + app.setCurrentLayer(2) + local doc = app.getDocumentStructure() + + assert_equal(doc["xoppFilename"], testDoc) + assert_equal(doc["pdfBackgroundFilename"], "") + assert_equal(doc["currentPage"], 3) + + local pages = doc["pages"] + assert_equal(#pages, 3) + + assert_equal(pages[3]["currentLayer"], 2) + + assert_true(pages[1]["isAnnotated"]) + assert_false(pages[2]["isAnnotated"]) + assert_true(pages[3]["isAnnotated"]) + + assert_equal(#pages[1]["layers"], 1) + assert_equal(#pages[3]["layers"], 3) + assert_true(pages[3]["layers"][1]["isAnnotated"]) + assert_true(pages[3]["layers"][2]["isAnnotated"]) + assert_false(pages[3]["layers"][3]["isAnnotated"]) + + assert_equal(pages[1]["pageTypeFormat"], "graph") + assert_equal(pages[2]["pageTypeFormat"], "plain") + assert_equal(pages[3]["pageTypeFormat"], "ruled") +end + +function test_sidebarPage() + app.setSidebarPageNo(1) + assert_equal(app.getSidebarPageNo(), 1) + app.setSidebarPageNo(2) + assert_equal(app.getSidebarPageNo(), 2) +end + +lunatest.run() diff --git a/test/unit_tests/lua/testDoc.xopp b/test/unit_tests/lua/testDoc.xopp new file mode 100644 index 0000000000000000000000000000000000000000..3fb56205847ebe3ffc39272737b75f1de3226c27 GIT binary patch literal 41567 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 literal 0 HcmV?d00001 From 77e792dd65164bd9f52ce422dec38f3905ed8f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20L=C3=B6tscher?= Date: Sun, 3 Sep 2023 11:40:03 +0200 Subject: [PATCH 3/5] Fix pipeline (connect to X server) to make LuaTest work on Linux --- azure-pipelines/continuous-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azure-pipelines/continuous-integration.yml b/azure-pipelines/continuous-integration.yml index 923513b24d85..b6835343c9f5 100644 --- a/azure-pipelines/continuous-integration.yml +++ b/azure-pipelines/continuous-integration.yml @@ -29,6 +29,9 @@ stages: displayName: 'Generate test locales' - bash: | cmake --build . --target test-units + Xvfb :99 & + export DISPLAY=:99 + sleep 3 # give xvfb some time to start CI=true ctest --verbose workingDirectory: ./build displayName: 'Run tests' From 33c0025e3555a253c1d97bed7da56b876cb68c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20L=C3=B6tscher?= Date: Sun, 3 Sep 2023 16:52:56 +0200 Subject: [PATCH 4/5] add layer test --- test/unit_tests/lua/test.lua | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/unit_tests/lua/test.lua b/test/unit_tests/lua/test.lua index b8a20729a9dd..43145490cdcf 100644 --- a/test/unit_tests/lua/test.lua +++ b/test/unit_tests/lua/test.lua @@ -27,7 +27,7 @@ local testDoc = sourceDir .. "testDoc.xopp" function test_docStructure() local success = app.openFile(testDoc) assert_true(success) - + app.setCurrentPage(3) app.setCurrentLayer(2) local doc = app.getDocumentStructure() @@ -63,4 +63,28 @@ function test_sidebarPage() assert_equal(app.getSidebarPageNo(), 2) end +function test_layers() + + function getNumberOfLayers() + local doc = app.getDocumentStructure() + local curPage = doc["currentPage"] + return #doc["pages"][curPage]["layers"] + end + function getCurrentLayer() + local doc = app.getDocumentStructure() + local curPage = doc["currentPage"] + return doc["pages"][curPage]["currentLayer"] + end + + local numLayer, curLayer = getNumberOfLayers(), getCurrentLayer() + print(numLayer, curLayer) + app.layerAction("ACTION_NEW_LAYER") + assert_equal(getNumberOfLayers(), numLayer + 1) + assert_equal(getCurrentLayer(), curLayer + 1) + app.layerAction("ACTION_DELETE_LAYER") + assert_equal(getNumberOfLayers(), numLayer) + local expectedLayer = curLayer < numLayer and curLayer + 1 or curLayer + assert_equal(getCurrentLayer(), expectedLayer) +end + lunatest.run() From 19618638b4f50d9ea92a9896059199da1ca7945a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20L=C3=B6tscher?= Date: Sun, 3 Sep 2023 17:20:37 +0200 Subject: [PATCH 5/5] Write expected value as first argument of assert --- test/unit_tests/lua/test.lua | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/unit_tests/lua/test.lua b/test/unit_tests/lua/test.lua index 43145490cdcf..135d3b10cedf 100644 --- a/test/unit_tests/lua/test.lua +++ b/test/unit_tests/lua/test.lua @@ -32,35 +32,35 @@ function test_docStructure() app.setCurrentLayer(2) local doc = app.getDocumentStructure() - assert_equal(doc["xoppFilename"], testDoc) - assert_equal(doc["pdfBackgroundFilename"], "") - assert_equal(doc["currentPage"], 3) + assert_equal(testDoc, doc["xoppFilename"]) + assert_equal("", doc["pdfBackgroundFilename"]) + assert_equal(3, doc["currentPage"]) local pages = doc["pages"] - assert_equal(#pages, 3) + assert_equal(3, #pages) - assert_equal(pages[3]["currentLayer"], 2) + assert_equal(2, pages[3]["currentLayer"]) assert_true(pages[1]["isAnnotated"]) assert_false(pages[2]["isAnnotated"]) assert_true(pages[3]["isAnnotated"]) - assert_equal(#pages[1]["layers"], 1) - assert_equal(#pages[3]["layers"], 3) + assert_equal(1, #pages[1]["layers"]) + assert_equal(3, #pages[3]["layers"]) assert_true(pages[3]["layers"][1]["isAnnotated"]) assert_true(pages[3]["layers"][2]["isAnnotated"]) assert_false(pages[3]["layers"][3]["isAnnotated"]) - assert_equal(pages[1]["pageTypeFormat"], "graph") - assert_equal(pages[2]["pageTypeFormat"], "plain") - assert_equal(pages[3]["pageTypeFormat"], "ruled") + assert_equal("graph", pages[1]["pageTypeFormat"]) + assert_equal("plain", pages[2]["pageTypeFormat"]) + assert_equal("ruled", pages[3]["pageTypeFormat"]) end function test_sidebarPage() app.setSidebarPageNo(1) - assert_equal(app.getSidebarPageNo(), 1) + assert_equal(1, app.getSidebarPageNo()) app.setSidebarPageNo(2) - assert_equal(app.getSidebarPageNo(), 2) + assert_equal(2, app.getSidebarPageNo()) end function test_layers() @@ -79,12 +79,12 @@ function test_layers() local numLayer, curLayer = getNumberOfLayers(), getCurrentLayer() print(numLayer, curLayer) app.layerAction("ACTION_NEW_LAYER") - assert_equal(getNumberOfLayers(), numLayer + 1) - assert_equal(getCurrentLayer(), curLayer + 1) + assert_equal(numLayer + 1, getNumberOfLayers()) + assert_equal(curLayer + 1, getCurrentLayer()) app.layerAction("ACTION_DELETE_LAYER") - assert_equal(getNumberOfLayers(), numLayer) + assert_equal(numLayer, getNumberOfLayers()) local expectedLayer = curLayer < numLayer and curLayer + 1 or curLayer - assert_equal(getCurrentLayer(), expectedLayer) + assert_equal(expectedLayer, getCurrentLayer()) end lunatest.run()