-- Config local CONF = { VERBOSE = false, -- Spew verbosely while testing? WIDTH = 32, -- Total width of numbers in bits. PRECISION = 17, -- Fractional precision in bits. MAX_DEPTH = 5, -- Limit on nesting depth of test formats. COMPOUND_LENGTH = 2, -- # formats in a sequence. SEED = false, -- Seed for generating tests. NROF_TESTS = 2^10, -- number of random test cases to run } -- Features to use when making format strings to test. local USE = { LEFT_REPETITION = true, --n*fmt GROUPING = true, --{ fmt } NESTING = true, --( fmt ) NAMING = true, --id:fmt ENDIAN = true, -->< EMPTY = true, -- COMPOUND = true --fmt fmt ... fmt } -- Non-config local PROTECT_STRUCT = true -- If set, wrap calls to struct with pcall. local TEST_KIND = nil --"read"|"write" -- Should every format string explicitly set the endianness at the start? -- Setting this to true mask(s|ed?) a bug where the endianness controls leak -- between calls to struct.pack(). local PREFIX_ENDIAN = true local PREFIX_ENDIAN_SEP = " " --"" gives format string parse errors local TOKEN_SEP = "" -- Separator between tokens. local MAX_BITS = 52 -- Maximum bit width we care about. local MACHINE_ENDIAN = "<" -- Native endianness. local COMPOUND_SEP = " " -- Parse arguments and sort then out into USE vs. CONF options. for _,given_arg in ipairs(arg) do local k,v = given_arg:match("^([^=]+)=(.+)") k = k or given_arg k = k:upper():gsub("-", "_") v = v or true if nil ~= CONF[k] then CONF[k] = assert(loadstring("return "..tostring(v)))() elseif nil ~= USE[k] then USE[k] = v ~= "FALSE" and true or false elseif k == "NONE" then for feature,_ in pairs(USE) do USE[feature] = false end elseif given_arg == "read" or given_arg == "write" then TEST_KIND = given_arg else io.stderr:write(("unknown argument %q\n"):format(given_arg)) os.exit(1) end end -- Imports local io, math, os, string, table = io, math, os, string, table local assert, ipairs, pairs, loadstring, pcall = assert, ipairs, pairs, loadstring, pcall local require, setmetatable, tonumber, tostring, type = require, setmetatable, tonumber, tostring, type local struct = require "vstruct"; struct.cache = nil local test = require "vstruct.test.common" local record = test.record local __EOG__ = __EOG__ local ceil,floor,fmod = math.ceil, math.floor, math.fmod local char_ = string.char local function char(x) return char_(x % 256) end -- Structural equality (no cycles please) local function eq(a,b) if a == b then return true end local t = type(a) if t ~= type(b) then return false end if t ~= "table" then return false end for k,av in pairs(a) do local bv = b[k] if not eq(av,bv) then return false end end for k,bv in pairs(b) do local av = a[k] if not eq(av,bv) then return false end end return true end -- str = dump(value) local dump = {} setmetatable(dump, { __call = function (self, value) local t = type(value) local f = dump[t] assert(f, "don't know how to dump ".. "values of type "..t) return f(value) end }) dump["nil"] = function() return "nil" end function dump.string(s) return ("%q"):format(s) end function dump.boolean(b) return tostring(b) end function dump.number(x) local m, e = math.frexp(x) --0.5 <= m <= 1, e an int for i=1,53 do --scale m to an integer if floor(m) == m then break end m = m*2 e = e - 1 end if e == 0 then return ("%.0f"):format(m) end local op = e < 0 and "/" or "*" e = math.abs(e) local exp = e > 1 and "2^"..e or "2" return ("%.0f%s%s"):format(m, op, exp) end function dump.table(t) local dumped = {} local pieces = {} local function push(x) table.insert(pieces, x) end push("{") for i, v in ipairs(t) do if #pieces > 1 then push(", ") end push(dump(v)) dumped[i] = true end for k,v in pairs(t) do if not dumped[k] then if #pieces > 1 then push(", ") end if type(k) == "string" and k:match("^%a[a-zA-Z0-9_]*$") then push(k) else push("[") push(dump(k)) push("]") end push("=") push(dump(v)) end end push("}") return table.concat(pieces) end -- List module local list list = { reverse = function (t) local out = {} local n = #t for i=1,n do local j = n-i+1 out[j] = t[i] end return out end, iter = function (t, f) for i,v in ipairs(t) do f(v) end return t end, map = function (t, f) local out = {} for i,v in ipairs(t) do out[i] = f(v) end return out end, foldl = function (t, f, accu) for i,v in ipairs(t) do accu = f(v, accu) end return accu end, merge = function (dst, src) local merged = {} for k,v in ipairs(src) do --append the array part table.insert(dst, v) merged[k] = true end for k,v in pairs(src) do --overwrite the hash part if not merged[k] then dst[k] = v end end return dst end, zeros = function (n) local t={} for i=1,n do t[i] = 0 end return t end, range = function (first, last, step) local t = {} if nil == last then first,last,step = 1,first,step end step = step or 1 for i=first,last,step do t[#t+1] = i end return t end, cat = function (...) local accu = {} local function loop(t,...) if nil == t then return accu end list.merge(accu, t) return loop(...) end return loop(...) end, rep = function (t, count) count = count or 0 local accu = {} for i=1,count do list.merge(accu, t) end return accu end } -- MRG32k3a random number generator. -- Returns numbers which are random for the full range of the IEEE -- double mantissa. local function make_rand(seed) local s10, s11, s12 = 12345,12345,12345 local s20, s21, s22 = 12345,12345,12345 local function rand() -- Return the next random number in the range [0,4294967087). local p1 = (1403580*s11 - 810728*s10) % 4294967087 local p2 = (527612*s22 - 1370589*s20) % 4294944443 s10,s11,s12 = s11,s12,p1 s20,s21,s22 = s21,s22,p2 return (p1 - p2) % 4294967087 end if seed then seed = tonumber(seed) or 12345 local m = 4294967087 local a = seed % 4294967087 a = a==a and a or 12345 s10,s11,s12,s20,s21,s22 = a,a+1,a+2,a+3,a+4,a+5 -- not all zero rand(); rand(); rand(); -- pump out the seed. end local frexp = math.frexp local function rand52() local hi,lo = rand(), rand() local m,e = frexp(hi * 4294967087 + lo) -- 0.5 <= m < 1 local t = m*2^53 - 2^52 -- map mantissa to 0 <= t < 2^52 range return t end return rand52 end local function byte_rand(rand) return function () return rand() % 256 end end local function bool_rand(rand) return function () return rand() % 2 == 0 end end local function char_rand(rand) return function () return char(rand()) end end local function clamped_rand(rand, lo, hi) local wid = hi - lo + 1 return function () return lo + (rand() % wid) end end local function string_rand(rand, minlen, maxlen) minlen = minlen or 0 maxlen = maxlen or 512 local gen_len = clamped_rand(rand, minlen, maxlen) local gen_char = char_rand(clamped_rand(rand, 66, 90)) return function (len) len = len or gen_len() return table.concat(list.map(list.range(len), gen_char)) end end -- Return an array of width representing value in two's -- complement in little-endian format. local function int_to_bytes_le(value, nbytes) local t = {} for i=1,nbytes do local r = value % 256 t[i] = r value = (value - r) / 256 end return t end local function keys(tab) local ks = {} for k,_ in pairs(tab) do table.insert(ks, k) end return ks end local function bless(obj, class) --make obj an instance of class return setmetatable(obj, {__index = class}) end -- Test case interface. local Test = { new = function (class, self) return bless(self or {}, class) end, get_naked = nil, --pack/unpack format string w/o endian control get_data = nil, --data byte string get_values = nil, --value list get_length = nil, --# top level entities in the format get_endian = nil, --before/after endian; nil=don't care get_format = function (self, endian, fmt) local fmt = fmt or self:get_naked() local before, after = self:get_endian() endian = endian or before before = before or endian after = after or before if before ~= endian then fmt = before..fmt end return fmt, after end } local function as_hex(str) if not str then return nil end return str:gsub('.', function(c) return string.format("%02X ", c:byte()) end) end local function perform_write_test(test, endian) endian = endian or MACHINE_ENDIAN local format = test:get_format(endian) local want_data = test:get_data() local values = test:get_values() if PREFIX_ENDIAN then format = "=" .. PREFIX_ENDIAN_SEP .. format end local pcall = pcall if not PROTECT_STRUCT then pcall = function (f,...) return true, f(...) end end local errmsg = nil local success, got_data = pcall(struct.pack, format, values) if not success then errmsg = got_data got_data = nil end success = success and eq(got_data, want_data) record("random write test", success, dump { got_data = got_data and as_hex(got_data) or nil, want_data = as_hex(want_data), format = format, values = values, error = errmsg, ok = success }) end local function perform_read_test(test, endian) endian = endian or MACHINE_ENDIAN local format = test:get_format(endian) local from_data = test:get_data() local want_values = test:get_values() if PREFIX_ENDIAN then format = "=" .. PREFIX_ENDIAN_SEP .. format end local pcall = pcall if not PROTECT_STRUCT then pcall = function (f,...) return true, f(...) end end local errmsg = nil local success, got_values = pcall(struct.unpack, format, from_data) if not success then errmsg = got_values got_values = nil end success = success and eq(got_values, want_values) record("random read test", success, dump { got_values = got_values, want_values = want_values, from_data = as_hex(from_data), format = format, error = errmsg, ok = success }) end local ScalarTest = Test:new{ new = function (class, self) self = Test.new(class, self) self.is_scalar = true return self end, get_naked = function (self, endian) return self.format end, get_values = function (self) return { self.value } end, get_data = function (self) return self.data end, get_length = function (self) return self.length or 1 end, get_endian = function (self) return self.endian, self.endian end, } local EmptyTest = ScalarTest:new{ format = "", value = nil, data = "", length = 0, endian = nil } -- A compound test models a sequence of unnested or ungrouped formats. local CompoundTest = Test:new{ new = function (class, self) self = Test.new(class, self) self.tests = {} return self end, add_test = function (self, t) table.insert(self.tests, t) return self end, get_data = function (self) return table.concat(list.map(self.tests, function (t) return t:get_data() end)) end, get_format = function (self, endian) return table.concat(list.map(self.tests, function (t) local fmt fmt,endian = t:get_format(endian) return fmt end), COMPOUND_SEP), endian end, get_endian = function (self) local n = #(self.tests) if n == 0 then return nil,nil end local before, after for i=1,n do local b,a = self.tests[i]:get_endian() before = before or b after = a or after end return before, after end, get_values = function (self) local accu = {} list.iter(self.tests, function (t) list.merge(accu, t:get_values()) end) return accu end, get_length = function (self) return #(self.tests) end, } -- A ProxyTest just resends its messages to a test named in self.body. local ProxyTest = Test:new{ new = function (class, self, body) local self = Test.new(class, self) self.body = self.body or body return self end, get_naked = function (self) return self.body:get_naked() end, get_data = function (self) return self.body:get_data() end, get_values = function (self) return self.body:get_values() end, get_length = function (self) return self.body:get_length() end, get_endian = function (self) return self.body:get_endian() end } -- Gives the self.body test a self.name. local NamedTest = ProxyTest:new{ new = function (class, self) self = ProxyTest.new(class, self) assert(self.name, "NamedTest unnamed") return self end, get_naked = function (self) return self.name ..":" .. self.body:get_naked() end, get_values = function (self) local values = self.body:get_values() local key, value for k,v in ipairs(values) do assert(nil == value, "NamedTest bound to multiple values") value = v key = k end for k,v in pairs(values) do assert(nil == value or key == k, "NamedTest bound to multiple values") value = v end assert(nil ~= value, "NamedTest unbound to a value") return { [self.name] = value } end } -- Models a test group nested in curlies. local GroupedTest = ProxyTest:new{ get_format = function (self, endian) local fmt,after = self.body:get_format(endian) return "{"..fmt.."}", after end, get_values = function (self) return { self.body:get_values() } end, get_length = function (self) return 1 end } -- Models a test nested within parens. local NestedTest = ProxyTest:new{ get_format = function (self, endian) local fmt,after = self.body:get_format(endian) return "("..fmt..")", after end, get_length = function (self) return 1 end } local LeftRepetitionTest = ProxyTest:new{ new = function (class, self) self = ProxyTest.new(class, self) self.count = self.count or 1 return self end, get_format = function (self, endian) local before = self.body:get_endian() local fmt, after = self.body:get_format(endian) before = before or endian after = after or before after = self.count > 0 and after or endian assert(after == before or self.count == 0, ("Non-idempotent left repetition for format '%s'"):format(fmt)) fmt = "("..fmt..")" return self.count..TOKEN_SEP.."*"..TOKEN_SEP..fmt, after end, get_data = function (self) return self.body:get_data():rep(self.count) end, get_values = function (self) return list.rep(self.body:get_values(), self.count) end, get_length = function (self) return 1 end, get_endian = function (self) if self.count > 0 then return self.body:get_endian() end end, } -- Test generation state. local State = { new = function (class, seed) local self = { rand = make_rand(seed), --data source endian = nil, --nil|">"|"<" width = 56, precision = 16, depth = 0, --recursion depth uniq = 0 --unique name seq } return bless(self, class) end, is_big_endian = function (self) return self.endian == ">" --XXX: should test vs. native also? end, raw_of_list = --byte list, endian -> byte string function (self, t, reverse) if nil == reverse then reverse = self:is_big_endian() end local chars = list.map(t, char) if reverse then chars = list.reverse(chars) end return table.concat(chars) end, raw_of_int = --int, width, endian -> byte string function (self, val, W, reverse) return self:raw_of_list(int_to_bytes_le(val, W), reverse) end } local function zero_pad_to_length(s, len) local pad_len = len - #s if pad_len <= 0 then return s end return s .. ("\0"):rep(pad_len) end local function cruft_pad_to_length(s, len) local cruft = "cruft" while #s < len do local ix = 1 + #s % #cruft s = s .. cruft:sub(ix,ix) end return s end -- Scalar test generators -- TODO: fW -- manually tested: @A +A -A local ScalarGen ScalarGen = { bitmask = function (state, w) local W = ceil(w/8) w = w < MAX_BITS and w or MAX_BITS local value = state.rand() local value = fmod(value,2^w) local write_bits = {} local read_bits = {} local n = 0 local v = value while n < 8*W do local r = v % 2 table.insert(read_bits, r == 1) if v > 0 then table.insert(write_bits, r == 1) end v = (v - r) / 2 n = n+1 end local write_test = ScalarTest:new{ endian = state.endian, format = ("m%d"):format(W), value = write_bits, data = state:raw_of_int(value, W) } local read_test = ScalarTest:new{ endian = state.endian, format = ("m%d"):format(W), value = read_bits, data = state:raw_of_int(value, W) } return write_test, read_test end, skip_pad = function (state, w) local W = ceil(w/8) return ScalarTest:new{ format = "x"..W, value = nil, --? data = zero_pad_to_length("", W) }, ScalarTest:new{ format = "x"..W, value = nil, --? data = cruft_pad_to_length("", W) } end, cstring_implicit = function (state, w) local W = ceil(w/8) local len = W-1 local s = string_rand(state.rand)(len) local write_test = ScalarTest:new{ endian = state.endian, format = "z", value = s, data = s.."\0" } local read_test = ScalarTest:new{ endian = state.endian, format = ("z%d"):format(W), value = s, data = write_test.data } return write_test, read_test end, cstring_short = function (state, w) local W = ceil(w/8) -->0 local len = state.rand() % (W) -- must be < W local s = string_rand(state.rand)(len) local data = s.."\0" local write_data = zero_pad_to_length(data, W) local read_data = cruft_pad_to_length(data, W) local write_test = ScalarTest:new{ endian = state.endian, format = ("z%d"):format(W), value = s, data = write_data:sub(1,W) } local read_test = ScalarTest:new{ endian = state.endian, format = ("z%d"):format(W), value = s, data = read_data:sub(1,W) } return write_test, read_test end, cstring_long = function (state, w) local W = ceil(w/8) -->0 local s = string_rand(state.rand)(W-1) local cruft = string_rand(state.rand,1,W)() local write_test = ScalarTest:new{ endian = state.endian, format = ("z%d"):format(W), value = s .. cruft, data = s.."\0" } local read_test = ScalarTest:new{ endian = state.endian, format = ("z%d"):format(W), value = s, data = write_test.data } return write_test, read_test end, boolean = function (state, w) local W = ceil(w/8) local value = fmod((state.rand() % 2) * state.rand(), 2^8) local flag = value ~= 0 and w > 0 if not flag then value = 0 end local write_test = ScalarTest:new{ endian = state.endian, format = ("b%d"):format(W), value = flag, data = state:raw_of_int(flag and 1 or 0, W) } local read_test = ScalarTest:new{ endian = state.endian, format = ("b%d"):format(W), value = flag, data = state:raw_of_int(value, W) } return write_test, read_test end, unsigned_int = function (state, w) local W = ceil(w/8) w = w < MAX_BITS and w or MAX_BITS local value = state.rand() local value = fmod(value,2^w) return ScalarTest:new{ endian = state.endian, format = ("u%d"):format(W), value = value, data = state:raw_of_int(value, W) } end, signed_int = function (state, w) local W = ceil(w/8) w = w < MAX_BITS and w or MAX_BITS local bias = 2^(w-1) local value = (fmod(state.rand(), 2^w) - bias) if w == 0 then value = 0 end return ScalarTest:new{ endian = state.endian, format = ("i%d"):format(W), value = value, data = state:raw_of_int(value, W) } end, --[[ fixed_rational_bit = function (state, w, f) local W = ceil(w/8) local test = ScalarGen.signed_int(state, w) test.format = ("P%d.%d"):format(8*W - f, f) test.value = test.value / 2^f return test end, fixed_rational_byte = function (state, w, f) local W = ceil(w/8) local F = ceil(f/8) local test = ScalarGen.signed_int(state, w) test.format = ("p%d.%d"):format(W - F, F) test.value = test.value / 2^(8*F) return test end, --]] string_implicit = function (state, w) local W = ceil(w/8) local len = W local s = string_rand(state.rand)(len) local write_test = ScalarTest:new{ endian = state.endian, format = "s", value = s, data = s } local read_test = ScalarTest:new{ endian = state.endian, format = ("s%d"):format(W), value = s, data = write_test.data } return write_test, read_test end, string_short = function (state, w) local W = ceil(w/8) local len = state.rand() % (W+1) local s = string_rand(state.rand)(len) local pad = ("\0"):rep(W - #s) local write_test = ScalarTest:new{ endian = state.endian, format = ("s%d"):format(W), value = s, data = s .. pad } local read_test = ScalarTest:new{ endian = state.endian, format = ("s%d"):format(W), value = s .. pad, data = write_test.data } return write_test, read_test end, string_long = function (state, w) local W = ceil(w/8) local s = string_rand(state.rand)(W) local cruft = string_rand(state.rand,0,W)() local write_test = ScalarTest:new{ endian = state.endian, format = ("s%d"):format(W), value = s .. cruft, data = s } local read_test = ScalarTest:new{ endian = state.endian, format = ("s%d"):format(W), value = s, data = write_test.data } return write_test, read_test end, counted_string = function (state, w) local W = ceil(w/8) w = w > 4 and 4 or w local count = fmod(state.rand(), 2^w) local count_data = state:raw_of_int(count, W) local s = string_rand(state.rand)(count) return ScalarTest:new{ endian = state.endian, format = ("c%d"):format(W), value = s, data = count_data..s } end, } local EmptyGen = {} if USE.EMPTY then function EmptyGen.empty(state) return EmptyTest:new() end function EmptyGen.space(state) return EmptyTest:new{ format = " " } end function EmptyGen.tab(state) return EmptyTest:new{ format = "\t" } end function EmptyGen.nl(state) return EmptyTest:new{ format = "\n" } end --function EmptyGen.cr(state) return EmptyTest:new{ format = "\r" } end end if USE.ENDIAN then function EmptyGen.big_endian(state) state.endian = ">" local self = EmptyTest:new() self.endian = state.endian return self end function EmptyGen.little_endian(state) state.endian = "<" local self = EmptyTest:new() self.endian = state.endian return self end end -- Leaf tests don't recurse. local LeafGen = {} for _,f in pairs(EmptyGen) do table.insert(LeafGen, f) end for _,f in pairs(ScalarGen) do table.insert(LeafGen, f) end local gen_random_leaf local function gen_random_leaf_for_write(state, w, f) local i = 1 + state.rand() % #LeafGen local gen = LeafGen[i] return gen(state, w or state.width, f or state.precision) end local function gen_random_leaf_for_read(state, w, f) local write,read = gen_random_leaf_for_write(state, w, f) read = read or write return read end -- List of all test generators (populated below) local TestGen = {} local function gen_random_test(state, w, f) if state.depth >= CONF.MAX_DEPTH then return gen_random_leaf(state, w, f) end state.depth = state.depth + 1 local i = 1 + state.rand() % #TestGen local gen = TestGen[i] local t = gen(state, w, f) state.depth = state.depth - 1 return t end -- Populate the test generators. table.insert(TestGen, function (...) return gen_random_leaf(...) end) if USE.COMPOUND then table.insert(TestGen, function (state,w,f) local n = CONF.COMPOUND_LENGTH local t = CompoundTest:new() for i=1,n do t:add_test(gen_random_test(state, w, f)) end return t end) end if USE.NESTING then table.insert(TestGen, function (state,w,f) local t = gen_random_test(state, w, f) return NestedTest:new{body = t} end) end if USE.GROUPING then table.insert(TestGen, function (state,w,f) local t = gen_random_test(state, w, f) return GroupedTest:new{body = t} end) end if USE.NAMING then table.insert(TestGen, function (state,w,f) local t = gen_random_test(state, w, f) local vs = t:get_values() if #vs == 1 and t.is_scalar then state.uniq = state.uniq + 1 t = NamedTest:new{body = t, name="u"..state.uniq} end return t end) end if USE.LEFT_REPETITION then table.insert(TestGen, function (state,w,f) local t = gen_random_test(state, w, f) local before, after = t:get_endian() if before == after then t = LeftRepetitionTest:new{ body = t, count = state.rand() % 5 } end return t end) end local function main_write() test.group "randomly generated write tests" local state = State:new(CONF.SEED or os.time()) state.width = CONF.WIDTH state.precision = CONF.PRECISION gen_random_leaf = gen_random_leaf_for_write for k,gen in pairs(EmptyGen) do for _,e in ipairs{"<",">"} do state.endian = e local t = gen(state, state.width, state.precision) perform_write_test(t, "=") end end for k,gen in pairs(ScalarGen) do for _,e in ipairs{"<",">"} do state.endian = e local t = gen(state, state.width, state.precision) perform_write_test(t, "=") end end for i=1,CONF.NROF_TESTS do for _,e in ipairs{"<",">"} do state.endian = e local t = gen_random_test(state, state.width, state.precision) perform_write_test(t) end end end local function main_read() test.group "randomly generated read tests" local state = State:new(CONF.SEED or os.time()) state.width = CONF.WIDTH state.precision = CONF.PRECISION gen_random_leaf = gen_random_leaf_for_read for k,gen in pairs(EmptyGen) do for _,e in ipairs{"<",">"} do state.endian = e local w,r = gen(state, state.width, state.precision) local t = r or w perform_read_test(t, "=") end end for k,gen in pairs(ScalarGen) do for _,e in ipairs{"<",">"} do state.endian = e local w,r = gen(state, state.width, state.precision) local t = r or w perform_read_test(t, "=") end end for i=1,CONF.NROF_TESTS do for _,e in ipairs{"<",">"} do state.endian = e local t = gen_random_test(state, state.width, state.precision) perform_read_test(t) end end end if TEST_KIND == "read" then main_read() elseif TEST_KIND == "write" then main_write() else local function opt_of_keyword(k) local o = k:lower():gsub("_", "-") return o end io.write"usage: read|write\n\n" io.write"Features:\n" io.write("\tnone") for i,k in ipairs(keys(USE)) do io.write", " if i % 5 == 0 then io.write"\n\t" end io.write(opt_of_keyword(k)) end io.write"\n" io.write"Options:\n" for k, v in pairs(CONF) do io.write("\t",opt_of_keyword(k),"=",tostring(v),"\n") end end