You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
7.7 KiB
Lua
229 lines
7.7 KiB
Lua
--- Module for creating properties. Provides a small domain specific language
|
|
-- for ease of use.
|
|
-- @module lqc.property
|
|
-- @alias property
|
|
local lqc = require 'lqc.quickcheck'
|
|
local report = require 'lqc.report'
|
|
local results = require 'lqc.property_result'
|
|
local unpack = unpack or table.unpack -- for compatibility reasons
|
|
|
|
|
|
--- Helper function, checks if x is an integer.
|
|
-- @param x a value to be checked
|
|
-- @return true if x is an integer; false otherwise.
|
|
local function is_integer(x)
|
|
return type(x) == 'number' and x % 1 == 0
|
|
end
|
|
|
|
--- Adds a small wrapper around the check function indicating success or failure
|
|
-- @param prop_table Property to be wrapped.
|
|
local function add_check_wrapper(prop_table)
|
|
local check_func = prop_table.check
|
|
prop_table.check = function(...)
|
|
if check_func(...) then
|
|
return results.SUCCESS
|
|
else
|
|
return results.FAILURE
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Adds an 'implies' wrapper to the check function
|
|
-- @param prop_table Property to be wrapped.
|
|
local function add_implies(prop_table)
|
|
local check_func = prop_table.check
|
|
prop_table.check = function(...)
|
|
if prop_table.implies(...) == false then
|
|
return results.SKIPPED
|
|
end
|
|
|
|
return check_func(...)
|
|
end
|
|
end
|
|
|
|
--- Adds a 'when_fail' wrapper to the check function
|
|
-- @param prop_table Property to be wrapped.
|
|
local function add_when_fail(prop_table)
|
|
local check_func = prop_table.check
|
|
prop_table.check = function(...)
|
|
local result = check_func(...)
|
|
|
|
if result == results.FAILURE then
|
|
prop_table.when_fail(...)
|
|
end
|
|
|
|
return result
|
|
end
|
|
end
|
|
|
|
--- Shrinks a property that failed with a certain set of inputs.
|
|
-- This function is called recursively if a shrink fails.
|
|
-- This function returns a simplified list or inputs.
|
|
-- @param property the property that failed
|
|
-- @param generated_values array of values to be shrunk down
|
|
-- @param tries Amount of shrinking tries already done
|
|
-- @return array of shrunk down values
|
|
local function do_shrink(property, generated_values, tries)
|
|
if not tries then
|
|
tries = 1
|
|
end
|
|
local shrunk_values = property.shrink(unpack(generated_values))
|
|
local result = property(unpack(shrunk_values))
|
|
|
|
if tries == property.numshrinks then
|
|
-- Maximum amount of shrink attempts exceeded.
|
|
return generated_values
|
|
end
|
|
|
|
if result == results.FAILURE then
|
|
-- further try to shrink down
|
|
return do_shrink(property, shrunk_values, tries + 1)
|
|
elseif result == results.SKIPPED then
|
|
-- shrunk to invalid situation, retry
|
|
return do_shrink(property, generated_values, tries + 1)
|
|
end
|
|
|
|
-- return generated values since they were last values for which property failed!
|
|
return generated_values
|
|
end
|
|
|
|
--- Function that checks if the property is valid for a set amount of inputs.
|
|
-- 1. check result of property X amount of times:
|
|
-- - SUCCESS = OK, print '.'
|
|
-- - SKIPPED = OK, print 'x'
|
|
-- - FAILURE = NOT OK, see 2.
|
|
-- 2. if FAILURE:
|
|
-- 2.1 print property info, values for which it fails
|
|
-- 2.2 do shrink to find minimal error case
|
|
-- 2.3 when shrink stays the same or max amount exceeded -> print minimal example
|
|
-- @param property Property to be checked X amount of times.
|
|
-- @return nil on success; otherwise returns a table containing error info.
|
|
local function do_check(property)
|
|
for _ = 1, property.numtests do
|
|
local generated_values = property.pick()
|
|
local result = property(unpack(generated_values))
|
|
|
|
if result == results.SUCCESS then
|
|
report.report_success()
|
|
elseif result == results.SKIPPED then
|
|
report.report_skipped()
|
|
else
|
|
report.report_failed()
|
|
if #generated_values == 0 then
|
|
-- Empty list of generators -> no further shrinking possible!
|
|
return {
|
|
property = property,
|
|
generated_values = generated_values,
|
|
shrunk_values = generated_values,
|
|
}
|
|
end
|
|
|
|
local shrunk_values = do_shrink(property, generated_values)
|
|
return {
|
|
property = property,
|
|
generated_values = generated_values,
|
|
shrunk_values = shrunk_values,
|
|
}
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
--- Creates a new property.
|
|
-- NOTE: property is limited to 1 implies, for_all, when_fail
|
|
-- more complex scenarios should be handled with state machine.
|
|
-- @param descr String containing description of the property
|
|
-- @param property_func Function to be checked X amount of times
|
|
-- @param generators List of generators used to generate values supplied to 'property_func'
|
|
-- @param numtests Number of times the property should be checked
|
|
-- @param numshrinks Number of times a failing property should be shrunk down
|
|
-- @return property object
|
|
local function new(descr, property_func, generators, numtests, numshrinks)
|
|
local prop = {
|
|
description = descr,
|
|
numtests = numtests,
|
|
numshrinks = numshrinks,
|
|
}
|
|
|
|
-- Generates a new set of inputs for this property.
|
|
-- Returns the newly generated set of inputs as a table.
|
|
function prop.pick()
|
|
local generated_values = {}
|
|
for i = 1, #generators do
|
|
generated_values[i] = generators[i]:pick(numtests)
|
|
end
|
|
return generated_values
|
|
end
|
|
|
|
-- Shrink 1 value randomly out of the given list of values.
|
|
function prop.shrink(...)
|
|
local values = {...}
|
|
local which = math.random(#values)
|
|
local shrunk_value = generators[which]:shrink(values[which])
|
|
values[which] = shrunk_value
|
|
return values
|
|
end
|
|
|
|
-- Function that checks if the property is valid for a set amount of inputs.
|
|
function prop:check()
|
|
return do_check(self)
|
|
end
|
|
|
|
return setmetatable(prop, {
|
|
__call = function(_, ...)
|
|
return property_func(...)
|
|
end,
|
|
})
|
|
end
|
|
|
|
--- Inserts the property into the list of existing properties.
|
|
-- @param descr String containing text description of the property
|
|
-- @param prop_info_table Table containing information of the property
|
|
-- @return nil; raises an error if prop_info_table is not in a valid format
|
|
local function property(descr, prop_info_table)
|
|
local function prop_func(prop_table)
|
|
local generators = prop_table.generators
|
|
if not generators or type(generators) ~= 'table' then
|
|
error('Need to supply generators in property!')
|
|
end
|
|
|
|
local check_type = type(prop_table.check)
|
|
if check_type ~= 'function' and check_type ~= 'table' then
|
|
error('Need to provide a check function to property!')
|
|
end
|
|
|
|
add_check_wrapper(prop_table)
|
|
|
|
local implies_type = type(prop_table.implies)
|
|
if implies_type == 'function' or implies_type == 'table' then
|
|
add_implies(prop_table)
|
|
end
|
|
|
|
local when_fail_type = type(prop_table.when_fail)
|
|
if when_fail_type == 'function' or when_fail_type == 'table' then
|
|
add_when_fail(prop_table)
|
|
end
|
|
|
|
local it_amount = prop_table.numtests
|
|
local shrink_amount = prop_table.numshrinks
|
|
local numtests = is_integer(it_amount) and it_amount or lqc.numtests
|
|
local numshrinks = is_integer(shrink_amount) and shrink_amount or lqc.numshrinks
|
|
local new_prop = new(descr, prop_table.check, prop_table.generators, numtests, numshrinks)
|
|
table.insert(lqc.properties, new_prop)
|
|
end
|
|
|
|
-- property called without DSL-like syntax
|
|
if prop_info_table then
|
|
prop_func(prop_info_table)
|
|
return function()
|
|
end
|
|
end
|
|
|
|
-- property called with DSL syntax!
|
|
return prop_func
|
|
end
|
|
|
|
return property
|
|
|