Aegisub/subprojects/luabins/test/test.lua
2021-01-10 03:14:12 -05:00

813 lines
20 KiB
Lua

-- ----------------------------------------------------------------------------
-- test.lua
-- Luabins test suite
-- See copyright notice in luabins.h
-- ----------------------------------------------------------------------------
package.cpath = "./?.so;"..package.cpath
local randomseed = 1235134892
--local randomseed = os.time()
print("===== BEGIN LUABINS TEST SUITE (seed " .. randomseed .. ") =====")
math.randomseed(randomseed)
-- ----------------------------------------------------------------------------
-- Utility functions
-- ----------------------------------------------------------------------------
local invariant = function(v)
return function()
return v
end
end
local escape_string = function(str)
return str:gsub(
"[^0-9A-Za-z_%- :]",
function(c)
return ("%%%02X"):format(c:byte())
end
)
end
local ensure_equals = function(msg, actual, expected)
if actual ~= expected then
error(
msg..":\n actual: `"..escape_string(tostring(actual))
.."`\nexpected: `"..escape_string(tostring(expected)).."'"
)
end
end
local ensure_equals_permute
do
-- Based on MIT-licensed
-- http://snippets.luacode.org/sputnik.lua?p=snippets/ \
-- Iterator_over_Permutations_of_a_Table_62
-- Which is based on PiL
local function permgen(a, n, fn)
if n == 0 then
fn(a)
else
for i = 1, n do
-- put i-th element as the last one
a[n], a[i] = a[i], a[n]
-- generate all permutations of the other elements
permgen(a, n - 1, fn)
-- restore i-th element
a[n], a[i] = a[i], a[n]
end
end
end
--- an iterator over all permutations of the elements of a list.
-- Please note that the same list is returned each time,
-- so do not keep references!
-- @param a list-like table
-- @return an iterator which provides the next permutation as a list
local function permute_iter(a, n)
local n = n or #a
local co = coroutine.create(function() permgen(a, n, coroutine.yield) end)
return function() -- iterator
local code, res = coroutine.resume(co)
return res
end
end
ensure_equals_permute = function(
msg,
actual,
expected_prefix,
expected_body,
expected_suffix,
expected_body_size
)
expected_body_size = expected_body_size or #expected_body
local expected
for t in permute_iter(expected_body, expected_body_size) do
expected = expected_prefix .. table.concat(t) .. expected_suffix
if actual == expected then
return actual
end
end
error(
msg..":\nactual: `"..escape_string(tostring(actual))
.."`\nexpected one of permutations: `"
..escape_string(tostring(expected)).."'"
)
end
end
local function deepequals(lhs, rhs)
if type(lhs) ~= "table" or type(rhs) ~= "table" then
return lhs == rhs
end
local checked_keys = {}
for k, v in pairs(lhs) do
checked_keys[k] = true
if not deepequals(v, rhs[k]) then
return false
end
end
for k, v in pairs(rhs) do
if not checked_keys[k] then
return false -- extra key
end
end
return true
end
local nargs = function(...)
return select("#", ...), ...
end
local pack = function(...)
return select("#", ...), { ... }
end
local eat_true = function(t, ...)
if t == nil then
error("failed: " .. (...))
end
return ...
end
-- ----------------------------------------------------------------------------
-- Test helper functions
-- ----------------------------------------------------------------------------
local luabins_local = require 'luabins'
assert(luabins_local == luabins)
assert(type(luabins.save) == "function")
assert(type(luabins.load) == "function")
local check_load_fn_ok = function(eq, saved, ...)
local expected = { nargs(...) }
local loaded = { nargs(eat_true(luabins.load(saved))) }
ensure_equals("num arguments match", loaded[1], expected[1])
for i = 2, expected[1] do
assert(eq(loaded[i], expected[i]))
end
return saved
end
local check_load_ok = function(saved, ...)
return check_load_fn_ok(deepequals, saved, ...)
end
local check_fn_ok = function(eq, ...)
local saved = assert(luabins.save(...))
assert(type(saved) == "string")
print("saved length", #saved, "(display truncated to 70 chars)")
print(escape_string(saved):sub(1, 70))
return check_load_fn_ok(eq, saved, ...)
end
local check_ok = function(...)
print("check_ok")
return check_fn_ok(deepequals, ...)
end
local check_fail_save = function(msg, ...)
print("check_fail_save")
local res, err = luabins.save(...)
ensure_equals("result", res, nil)
ensure_equals("error message", err, msg)
-- print("/check_fail_save")
end
local check_fail_load = function(msg, v)
print("check_fail_load")
local res, err = luabins.load(v)
ensure_equals("result", res, nil)
ensure_equals("error message", err, msg)
-- print("/check_fail_load")
end
print("===== BEGIN LARGE DATA OK =====")
-- Based on actual bug.
-- This dataset triggered Lua C data stack overflow.
-- (Note that bug is not triggered if check_ok is used)
-- Update data with
-- $ lua etc/toluabins.lua test/large_data.lua>test/large_data.luabins
-- WARNING: Keep this test above other tests, so Lua stack is small.
assert(
luabins.load(
assert(io.open("test/large_data.luabins", "r"):read("*a"))
)
)
print("===== LARGE DATA OK =====")
-- ----------------------------------------------------------------------------
-- Basic tests
-- ----------------------------------------------------------------------------
print("===== BEGIN BASIC TESTS =====")
print("---> basic corrupt data tests")
check_fail_load("can't load: corrupt data", "")
check_fail_load("can't load: corrupt data", "bad data")
print("---> basic extra data tests")
do
local s
s = check_ok()
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(nil)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(true)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(false)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(42)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(math.pi)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(1/0)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok(-1/0)
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok("Luabins")
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok({ })
check_fail_load("can't load: extra data at end", s .. "-")
s = check_ok({ a = 1, 2 })
check_fail_load("can't load: extra data at end", s .. "-")
end
print("---> basic type tests")
-- This is the way to detect NaN
check_fn_ok(function(lhs, rhs) return lhs ~= lhs and rhs ~= rhs end, 0/0)
check_ok("")
check_ok("Embedded\0Zero")
check_ok(("longstring"):rep(1024000))
check_fail_save("can't save: unsupported type detected", function() end)
check_fail_save(
"can't save: unsupported type detected",
coroutine.create(function() end)
)
check_fail_save("can't save: unsupported type detected", newproxy())
print("---> basic table tests")
check_ok({ 1 })
check_ok({ a = 1 })
check_ok({ a = 1, 2, [42] = true, [math.pi] = math.huge })
check_ok({ { } })
check_ok({ a = {}, b = { c = 7 } })
check_ok({ 1, 2, 3 })
check_ok({ [1] = 1, [1.5] = 2, [2] = 3 })
check_ok({ 1, nil, 3 })
check_ok({ 1, nil, 3, [{ 1, nil, 3 }] = { 1, nil, 3 } })
print("---> basic tuple tests")
check_ok(nil, nil)
do
local s = check_ok(nil, false, true, 42, "Embedded\0Zero", { { [{3}] = 54 } })
check_fail_load("can't load: extra data at end", s .. "-")
check_ok(check_ok(s)) -- Save data string couple of times more
end
print("---> basic table tuple tests")
check_ok({ a = {}, b = { c = 7 } }, nil, { { } }, 42)
check_ok({ ["1"] = "str", [1] = "num" })
check_ok({ [true] = true })
check_ok({ [true] = true, [false] = false, 1 })
print("---> basic fail save tests")
check_fail_save(
"can't save: unsupported type detected",
{ { function() end } }
)
check_fail_save(
"can't save: unsupported type detected",
nil, false, true, 42, "Embedded\0Zero", function() end,
{ { [{3}] = 54 } }
)
print("---> recursive table test")
local t = {}; t[1] = t
check_fail_save("can't save: nesting is too deep", t)
print("---> metatable test")
check_ok(setmetatable({}, {__index = function(t, k) return k end}))
print("===== BASIC TESTS OK =====")
print("===== BEGIN FORMAT SANITY TESTS =====")
-- Format sanity checks for LJ2 compatibility tests.
-- These tests are intended to help debugging actual problems
-- of test suite, and are not feature complete.
-- What is not checked here, checked in the rest of suite.
do
do
local saved = check_ok(1)
local expected =
"\001".."N"
.. "\000\000\000\000\000\000\240\063" -- Note number is a double
ensure_equals(
"1 as number",
expected,
saved
)
end
do
local saved = check_ok({ [true] = 1 })
local expected =
"\001".."T"
.. "\000\000\000\000".."\001\000\000\000"
.. "1"
.. "N\000\000\000\000\000\000\240\063" -- Note number is a double
ensure_equals(
"1 as value",
expected,
saved
)
end
do
local saved = check_ok({ [1] = true })
local expected =
"\001".."T"
.. "\001\000\000\000".."\000\000\000\000"
.. "N\000\000\000\000\000\000\240\063" -- Note number is a double
.. "1"
ensure_equals(
"1 as key",
expected,
saved
)
end
end
print("===== FORMAT SANITY TESTS OK =====")
print("===== BEGIN AUTOCOLLAPSE TESTS =====")
-- Note: those are ad-hoc tests, tuned for old implementation
-- which generated save data on Lua stack.
-- These tests are kept here for performance comparisons.
local LUABINS_CONCATTHRESHOLD = 1024
local gen_t = function(size)
-- two per numeric entry, three per string entry,
-- two entries per key-value pair
local actual_size = math.ceil(size / (2 + 3))
print("generating table of "..actual_size.." pairs")
local t = {}
for i = 1, actual_size do
t[i] = "a"..i
end
return t
end
-- Test table value autocollapse
check_ok(gen_t(LUABINS_CONCATTHRESHOLD - 100)) -- underflow, no autocollapse
check_ok(gen_t(LUABINS_CONCATTHRESHOLD)) -- autocollapse, no extra elements
check_ok(gen_t(LUABINS_CONCATTHRESHOLD + 100)) -- autocollapse, extra elements
-- Test table key autocollapse
check_ok({ [gen_t(LUABINS_CONCATTHRESHOLD - 4)] = true })
-- Test multiarg autocollapse
check_ok(
1,
gen_t(LUABINS_CONCATTHRESHOLD - 5),
2,
gen_t(LUABINS_CONCATTHRESHOLD - 5),
3
)
print("===== AUTOCOLLAPSE TESTS OK =====")
print("===== BEGIN MIN TABLE SIZE TESTS =====")
do
-- one small key
do
local data = { [true] = true }
local saved = check_ok(data)
ensure_equals(
"format sanity check",
"\001".."T".."\000\000\000\000".."\001\000\000\000".."11",
saved
)
check_fail_load(
"can't load: corrupt data, bad size",
saved:sub(1, #saved - 1)
)
-- As long as array and hash size sum is correct
-- (and both are within limits), load is successful.
-- If values are swapped, we get some performance hit.
check_load_ok(
"\001".."T".."\001\000\000\000".."\000\000\000\000".."11",
data
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T".."\001\000\000\000".."\001\000\000\000".."11"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T".."\000\000\000\000".."\002\000\000\000".."11"
)
check_fail_load(
"can't load: extra data at end",
"\001".."T".."\000\000\000\000".."\000\000\000\000".."11"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T".."\255\255\255\255".."\255\255\255\255".."11"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T".."\000\255\255\255".."\000\255\255\255".."11"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\255".."T".."\000\000\000\000".."\000\000\000\000"
)
end
-- two small keys
do
local data = { [true] = true, [false] = false }
local saved = check_ok({ [true] = true, [false] = false })
ensure_equals_permute(
"format sanity check",
saved,
"\001" .. "T" .. "\000\000\000\000" .. "\002\000\000\000",
{
"0" .. "0";
"1" .. "1";
},
""
)
check_fail_load(
"can't load: corrupt data, bad size",
saved:sub(1, #saved - 1)
)
-- See above about swapped array and hash sizes
check_load_ok(
"\001".."T".."\001\000\000\000".."\001\000\000\000".."1100",
data
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T".."\000\000\000\000".."\003\000\000\000".."0011"
)
end
-- two small and one large key
do
local saved = check_ok({ [true] = true, [false] = false, [1] = true })
ensure_equals_permute(
"format sanity check",
saved,
"\001" .. "T" .. "\001\000\000\000" .. "\002\000\000\000",
{
"0" .. "0";
"1" .. "1";
-- Note number is a double
"N\000\000\000\000\000\000\240\063" .. "1";
},
""
)
check_fail_load(
"can't load: corrupt data",
saved:sub(1, #saved - 1)
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T"
.. "\002\000\000\000".."\002\000\000\000"
.. "0011"
.. "N\000\000\000\000\000\000\240\063"
.. "1"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T"
.. "\001\000\000\000".."\003\000\000\000"
.. "0011"
.. "N\000\000\000\000\000\000\240\063"
.. "1"
)
end
-- two small and two large keys
do
local saved = check_ok(
{ [true] = true, [false] = false, [1] = true, [42] = true }
)
local expected =
"\001".."T"
.. "\001\000\000\000".."\003\000\000\000"
.. "0011"
ensure_equals_permute(
"format sanity check",
saved,
"\001" .. "T" .. "\001\000\000\000" .. "\003\000\000\000",
{
"0" .. "0";
"1" .. "1";
"N\000\000\000\000\000\000\069\064" .. "1";
"N\000\000\000\000\000\000\240\063" .. "1";
},
""
)
check_fail_load(
"can't load: corrupt data",
saved:sub(1, #saved - 1)
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T"
.. "\001\000\000\000".."\005\000\000\000"
.. "0011"
.. "N\000\000\000\000\000\000\069\064"
.. "1"
.. "N\000\000\000\000\000\000\240\063"
.. "1"
)
check_fail_load(
"can't load: corrupt data, bad size",
"\001".."T"
.. "\003\000\000\000".."\003\000\000\000"
.. "0011"
.. "N\000\000\000\000\000\000\069\064"
.. "1"
.. "N\000\000\000\000\000\000\240\063"
.. "1"
)
end
end
print("===== MIN TABLE SIZE TESTS OK =====")
print("===== BEGIN LOAD TRUNCATION TESTS =====")
local function gen_random_dataset(num, nesting)
num = num or math.random(0, 128)
nesting = nesting or 1
local gen_str = function()
local t = {}
local n = math.random(0, 1024)
for i = 1, n do
t[i] = string.char(math.random(0, 255))
end
return table.concat(t)
end
local gen_bool = function() return math.random() >= 0.5 end
local gen_nil = function() return nil end
local generators =
{
gen_nil;
gen_nil;
gen_nil;
gen_bool;
gen_bool;
gen_bool;
function() return math.random() end;
function() return math.random(-10000, 10000) end;
function() return math.random() * math.random(-10000, 10000) end;
gen_str;
gen_str;
gen_str;
function()
if nesting >= 24 then
return nil
end
local t = {}
local n = math.random(0, 24 - nesting)
for i = 1, n do
local k = gen_random_dataset(1, nesting + 1)
if k == nil then
k = "(nil)"
end
t[ k ] = gen_random_dataset(
1,
nesting + 1
)
end
return t
end;
}
local t = {}
for i = 1, num do
local n = math.random(1, #generators)
t[i] = generators[n]()
end
return unpack(t, 0, num)
end
local random_dataset_num, random_dataset_data = pack(gen_random_dataset())
local random_dataset_saved = check_ok(
unpack(random_dataset_data, 0, random_dataset_num)
)
local num_tries = 100
local errors = {}
for i = 1, num_tries do
local to = math.random(1, #random_dataset_saved - 1)
local new_data = random_dataset_saved:sub(1, to)
local res, err = luabins.load(new_data)
ensure_equals("truncated data must not be loaded", res, nil)
errors[err] = (errors[err] or 0) + 1
end
print("truncation errors encountered:")
for err, n in pairs(errors) do
print(err, n)
end
print("===== BASIC LOAD TRUNCATION OK =====")
print("===== BEGIN LOAD MUTATION TESTS =====")
local function mutate_string(str, num, override)
num = num or math.random(1, 8)
if num < 1 then
return str
end
local mutators =
{
-- truncate at end
function(str)
local pos = math.random(1, #str)
return str:sub(1, pos)
end;
-- truncate at beginning
function(str)
local pos = math.random(1, #str)
return str:sub(-pos)
end;
-- cut out the middle
function(str)
local from = math.random(1, #str)
local to = math.random(from, #str)
return str:sub(1, from) .. str:sub(to)
end;
-- swap two halves
function(str)
local pos = math.random(1, #str)
return str:sub(pos + 1, #str) .. str:sub(1, pos)
end;
-- swap two characters
function(str)
local pa, pb = math.random(1, #str), math.random(1, #str)
local a, b = str:sub(pa, pa), str:sub(pb, pb)
return
str:sub(1, pa - 1) ..
a ..
str:sub(pa + 1, pb - 1) ..
b ..
str:sub(pb + 1, #str)
end;
-- replace one character
function(str)
local pos = math.random(1, #str)
return
str:sub(1, pos - 1) ..
string.char(math.random(0, 255)) ..
str:sub(pos + 1, #str)
end;
-- increase one character
function(str)
local pos = math.random(1, #str)
local b = str:byte(pos, pos) + 1
if b > 255 then
b = 0
end
return
str:sub(1, pos - 1) ..
string.char(b) ..
str:sub(pos + 1, #str)
end;
-- decrease one character
function(str)
local pos = math.random(1, #str)
local b = str:byte(pos, pos) - 1
if b < 0 then
b = 255
end
return
str:sub(1, pos - 1) ..
string.char(b) ..
str:sub(pos + 1, #str)
end;
}
local n = override or math.random(1, #mutators)
str = mutators[n](str)
return mutate_string(str, num - 1, override)
end
local num_tries = 100000
local num_successes = 0
local errors = {}
for i = 1, num_tries do
local new_data = mutate_string(random_dataset_saved)
local res, err = luabins.load(new_data)
if res == nil then
errors[err] = (errors[err] or 0) + 1
else
num_successes = num_successes + 1
end
end
if num_successes == 0 then
print("no mutated strings loaded successfully")
else
-- This is ok, since we may corrupt data, not format.
-- If it is an issue for user, he must append checksum to data,
-- as usual.
print("mutated strings loaded successfully: "..num_successes)
end
print("mutation errors encountered:")
for err, n in pairs(errors) do
print(err, n)
end
print("===== BASIC LOAD MUTATION OK =====")
print("OK")