--- Module for generating tables of varying sizes and types -- @module lqc.generators.table -- @alias new_table local Gen = require 'lqc.generator' local random = require 'lqc.random' local bool = require 'lqc.generators.bool' local int = require 'lqc.generators.int' local float = require 'lqc.generators.float' local string = require 'lqc.generators.string' local lqc = require 'lqc.quickcheck' local lqc_gen = require 'lqc.lqc_gen' local oneof = lqc_gen.oneof local frequency = lqc_gen.frequency local deep_equals = require 'lqc.helpers.deep_equals' local deep_copy = require 'lqc.helpers.deep_copy' --- Checks if an object is equal to another (shallow equals) -- @param a first value -- @param b second value -- @return true if a equals b; otherwise false local function normal_equals(a, b) return a == b end --- Determines if the shrink mechanism should try to reduce the table in size. -- @param size size of the table to shrink down -- @return true if size should be reduced; otherwise false local function should_shrink_smaller(size) if size == 0 then return false end return random.between(1, 5) == 1 end --- Determines how many items in the table should be shrunk down -- @param size size of the table -- @return amount of values in the table that should be shrunk down local function shrink_how_many(size) -- 20% chance between 1 and size, 80% 1 element. if random.between(1, 5) == 1 then return random.between(1, size) end return 1 end --- Creates a generator for a table of arbitrary or specific size -- @param table_size size of the table to be generated -- @return generator that can generate tables local function new_table(table_size) -- Keep a list of generators used in this new table -- This variable is needed to share state (which generators) between -- the pick and shrink function local generators = {} --- Shrinks the table by 1 randomly chosen element -- @param tbl previously generated table value -- @param size size of the table -- @return shrunk down table local function shrink_smaller(tbl, size) local idx = random.between(1, size) table.remove(tbl, idx) table.remove(generators, idx) -- also update the generators for this table!! return tbl end --- Shrink a value in the table. -- @param tbl table to shrink down -- @param size size of the table -- @param iterations_count remaining amount of times the shrinking should be retried -- @return shrunk down table local function do_shrink_values(tbl, size, iterations_count) local idx = random.between(1, size) local old_value = tbl[idx] local new_value = generators[idx]:shrink(old_value) -- Check if we should retry shrinking: if iterations_count ~= 0 then local check_equality = (type(new_value) == 'table') and deep_equals or normal_equals if check_equality(new_value, old_value) then -- Shrink introduced no simpler result, retry at other index. return do_shrink_values(tbl, size, iterations_count - 1) end end tbl[idx] = new_value return tbl end --- Shrinks an amount of values in the table -- @param tbl table to shrink down -- @param size size of the table -- @param how_many amount of values to shrink down -- @return shrunk down table local function shrink_values(tbl, size, how_many) if how_many ~= 0 then local new_tbl = do_shrink_values(tbl, size, lqc.numshrinks) return shrink_values(new_tbl, size, how_many - 1) end return tbl end --- Generates a random table with a certain size -- @param size size of the table to generate -- @return tableof a specific size with random values local function do_generic_pick(size) local result = {} for idx = 1, size do -- TODO: Figure out a better way to decrease table size rapidly, maybe use -- math.log (e.g. ln(size + 1) ? local subtable_size = math.floor(size * 0.01) local generator = frequency {{10, new_table(subtable_size)}, {90, oneof {bool(), int(size), float(), string(size)}}} generators[idx] = generator result[idx] = generator:pick(size) end return result end -- Now actually generate a table: if table_size then -- Specific size --- Helper function for generating a table of a specific size -- @param size size of the table to generate -- @return function that can generate a table of a specific size local function specific_size_pick(size) local function do_pick() return do_generic_pick(size) end return do_pick end --- Shrinks a table without removing any elements. -- @param prev previously generated table value -- @return shrunk down table local function specific_size_shrink(prev) local size = #prev if size == 0 then return prev end -- handle empty tables return shrink_values(prev, size, shrink_how_many(size)) end return Gen.new(specific_size_pick(table_size), specific_size_shrink) end -- Arbitrary size --- Generate a (nested / empty) table of an arbitrary size. -- @param numtests Amount of times the property calls this generator; used to -- guide the optimatization process. -- @return table of an arbitrary size local function arbitrary_size_pick(numtests) local size = random.between(0, numtests) return do_generic_pick(size) end --- Shrinks a table by removing elements or shrinking values in the table. -- @param prev previously generated value -- @return shrunk down table value local function arbitrary_size_shrink(prev) local size = #prev if size == 0 then return prev end -- handle empty tables if should_shrink_smaller(size) then return shrink_smaller(prev, size) end local tbl_copy = deep_copy(prev) local new_tbl = shrink_values(tbl_copy, size, shrink_how_many(size)) if deep_equals(prev, new_tbl) then -- shrinking didn't help, remove an element return shrink_smaller(prev, size) end -- table shrunk successfully! return new_tbl end return Gen.new(arbitrary_size_pick, arbitrary_size_shrink) end return new_table