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.
zeus/tools/lqc/fsm/algorithm.lua

472 lines
19 KiB
Lua

--- Module describing the algorithm used by the FSM properties.
-- @module lqc.fsm.algorithm
-- @alias lib
local Vector = require 'lqc.helpers.vector'
local Var = require 'lqc.fsm.var'
local Command = require 'lqc.fsm.command'
local Action = require 'lqc.fsm.action'
local random = require 'lqc.random'
local deep_copy = require 'lqc.helpers.deep_copy'
local report = require 'lqc.report'
local unpack = unpack or table.unpack -- for compatibility reasons
local lib = {}
--- Creates a small helper object that keeps track of a counter
-- @return counter object
function lib.make_counter()
local Counter = {
val = 1,
increase = function(self)
self.val = self.val + 1
end,
value = function(self)
return self.val
end,
}
return setmetatable(Counter, {
__index = Counter,
})
end
--- Checks if x lies between min and max
-- @param x A number
-- @param min Minimum value
-- @param max Maximum value
-- @return true if min <= x and x <= max; otherwise false.
local function is_between(x, min, max)
return min <= x and x <= max
end
--- Determines how many items should be shrunk
-- @param max_amount Maximum amount of items that are allowed to be shrunk down
-- @return random number between 1 and max_amount (inclusive)
local function shrink_how_many(max_amount)
if max_amount <= 1 then
return 1
end
return random.between(1, max_amount)
end
--- Determines if an an action should be marked for deletion
-- @return true if it should be deleted; otherwise false
local function should_select_action()
return random.between(1, 4) == 1 -- 25% chance
end
--- Finds a specific state in the list of states based on name of the state.
-- @param states List of states to search in
-- @param state_name Name of the state to find
-- @return the state with that name;
-- raises an error if no state is present with the specified name
function lib.find_state(states, state_name)
for i = 1, #states do
local state = states[i]
if state.name == state_name then
return state
end
end
error('State "' .. state_name .. '" not found in list of FSM states!')
end
--- Finds the next command based on the FSM model and the current state.
-- @param fsm table describing the FSM property
-- @param current_state Variable containing the current state of the FSM
-- @param var_counter Number indicating how many variables have already been used
-- @return 3 values: chosen_command, cmd_generator, updated_current_state
function lib.find_next_action(fsm, current_state, var_counter)
local numtests, commands, states = fsm.numtests, fsm.commands, fsm.states
local cmd_gen = commands(current_state)
for _ = 1, 100 do -- TODO make configurable?
local cmd = cmd_gen:pick(numtests)
local selected_state = lib.find_state(states, cmd.state_name)
if selected_state.precondition(current_state, cmd.args) then -- valid command
local variable = Var.new(var_counter:value())
var_counter:increase()
current_state = selected_state.next_state(current_state, variable, cmd.args)
return Action.new(variable, cmd, cmd_gen), current_state
end
end
-- Could not find a next action -> stop generating further actions
return Action.new(Var.new(var_counter:value()), Command.stop, nil), current_state
end
--- Generates a list of steps for a FSM specification
-- @param fsm_table table containing description of a FSM property
-- @return list of generated actions
function lib.generate_actions(fsm_table)
local generated_actions = Vector.new()
local counter = lib.make_counter()
local state = fsm_table.initial_state()
repeat
local action, next_state = lib.find_next_action(fsm_table, state, counter)
state = next_state
generated_actions:push_back(action)
until action.command.state_name == 'stop'
return generated_actions
end
--- Slices of the last actions past index
-- @param action_vector Vector of actions
-- @param index Last position in the vector that should not be removed
-- @return the action vector (modified in place)
function lib.slice_last_actions(action_vector, index)
local action_vector_copy = deep_copy(action_vector)
local last_pos = action_vector_copy:size() - 1 -- -1 since we want to keep stop action!
for i = last_pos, index + 1, -1 do
action_vector_copy:remove_index(i)
end
return action_vector_copy
end
--- Selects at most 'how_many' amount of actions from the vector of actions
-- to be marked for deletion.
-- @param action_vector Vector containing list of actions
-- @return vector of actions which should be deleted.
function lib.select_actions(action_vector)
local selected_actions, idx_vector = Vector.new(), Vector.new()
if action_vector:size() <= 2 then
return selected_actions, idx_vector
end
local amount = 0
local size = action_vector:size() - 2 -- don't remove stop action, keep atleast 1 other action
local how_many = shrink_how_many(size)
for i = 1, size do
if amount >= how_many then
break
end
if should_select_action() then -- TODO make this a variable function and use a while loop?
idx_vector:push_back(i)
amount = amount + 1
end
end
for i = 1, amount do
selected_actions:push_back(action_vector:get(idx_vector:get(i)))
end
return selected_actions, idx_vector
end
--- Removes all elements of 'which_actions' from 'action_vector'
-- @param action_vector Vector of actions from which actions will be removed
-- @param which_actions actions to be removed
-- @return an updated vector
function lib.delete_actions(action_vector, which_actions)
local action_vector_copy = deep_copy(action_vector)
for i = 1, which_actions:size() do
action_vector_copy:remove(which_actions:get(i))
end
return action_vector_copy
end
--- Does the actual execution of the FSM by executing the list of actions
-- If one of the postconditions fail after an action is applied, then the
-- actions will be shrunk down to a simpler scenario.
-- At the end, the state is cleaned up by the cleanup-callback.
-- Returns 4 values:
-- 1) true if the FSM property succeeded for these actions; false otherwise.
-- 2) index of last successful step (1-based)
-- 3) state of the model right before the failing action
-- 4) result of the last failing action
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions list of actions to be executed for this FSM
-- @return 4 values:
-- 1. bool: true = FSM succeeded; false = FSM failed
-- 2. Amount of actions executed
-- 3. Last state of the FSM
-- 4. Last result (return value of last command)
function lib.execute_fsm(fsm_table, generated_actions)
local state = fsm_table.initial_state()
local last_state, last_result = state, nil
for i = 1, generated_actions:size() do
local command = generated_actions:get(i).command
local selected_state = lib.find_state(fsm_table.states, command.state_name)
local result = command.func(unpack(command.args)) -- side effects happen here
local updated_state = selected_state.next_state(state, result, command.args)
last_state, last_result = state, result
-- and verify the model matches the actual system
-- NOTE: the state passed in is the state that the system had BEFORE
-- executing this specific action!
if not selected_state.postcondition(state, result, command.args) then
fsm_table.cleanup(state)
return false, i, last_state, last_result
end
state = updated_state -- update model
end
fsm_table.cleanup(state)
return true, generated_actions:size() - 1, last_state, last_result
end
--- Is the list of actions valid to execute on this FSM?
-- Replays sequence symbolically to verify if it is indeed valid.
-- @param fsm_table table containing description of a FSM property
-- @param action_vector list of actions to be checked
-- @return true if list of actions valid; otherwise false.
function lib.is_action_sequence_valid(fsm_table, action_vector)
local states = fsm_table.states
local state = fsm_table.initial_state()
for i = 1, action_vector:size() do
local action = action_vector:get(i)
local selected_state = lib.find_state(states, action.command.state_name)
if not selected_state.precondition(state, action.command.args) then
return false
end
state = selected_state.next_state(state, action.variable, action.command.args)
end
return true
end
--- Tries to shrink the list of actions to a simpler form by removing steps of
-- the sequence and checking if it is still valid.
-- The function is recursive and will loop until tries_left is 0.
-- @param fsm_table table containing description of a FSM property
-- @param action_list list of actions to be shrunk down
-- @return 2 values:
-- 1) action_list if shrinking was not possible after X amount of tries;
-- otherwise it will return a shrunk list of actions.
-- 2) list of deleted actions (empty if shrinking failed after X tries)
local function do_shrink_actions(fsm_table, action_list)
local which_actions = lib.select_actions(action_list)
local shrunk_actions = lib.delete_actions(action_list, which_actions)
if not lib.is_action_sequence_valid(fsm_table, shrunk_actions) then
return action_list, Vector.new()
end
return shrunk_actions, which_actions
end
--- Does the shrinking of the FSM actions
-- choose 1 - N steps and delete them from list of actions
-- repeat X amount of times (recursively)
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions List of actions to be shrunk down
-- @param removed_actions Already removed actions
-- @param tries Remaining tries to recusively try shrinking the actions
-- @return the shrunk down list of actions
local function shrink_actions(fsm_table, generated_actions, removed_actions, tries)
if tries == 1 then
return generated_actions, removed_actions
end
local shrunk_actions, deleted_actions = do_shrink_actions(fsm_table, generated_actions)
local total_removed_actions = removed_actions:append(deleted_actions)
-- TODO add execute fsm here and shrink deleted actions?
return shrink_actions(fsm_table, shrunk_actions, total_removed_actions, tries - 1)
end
--- Tries to shrink the list of failing actions by selecting a subset and
-- checking if the combination is now valid and if the FSM still fails or not
-- This is a recursive function.
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions List of actions to be shrunk down
-- @param deleted_actions Already deleted actions
-- @param tries Remaining tries to recusively try shrinking the actions
-- @return 3 things:
-- 1. the shrunk list of actions (or the original list if no better solution was found)
-- 2. the end state of the fsm after these actions
-- 3. result of the last action
local function shrink_deleted_actions(fsm_table, generated_actions, deleted_actions, tries)
if tries == 1 then
return generated_actions
end
local which_actions = lib.select_actions(deleted_actions)
local shrunk_actions = lib.delete_actions(generated_actions, which_actions)
-- Retry if invalid sequence
local is_valid = lib.is_action_sequence_valid(fsm_table, shrunk_actions)
if not is_valid then
return shrink_deleted_actions(fsm_table, generated_actions, deleted_actions, tries - 1)
end
-- Check FSM again
-- if FSM succeeds now, (one or more of) the failing actions have been deleted
-- -> simply retry TODO there is an optimisation possible here..
-- else FSM still failed -> chosen actions did not matter, ignore them and
-- further try shrinking
local is_successful = lib.execute_fsm(fsm_table, shrunk_actions)
if is_successful then
deleted_actions = lib.delete_actions(deleted_actions, which_actions)
end
return shrink_deleted_actions(fsm_table, shrunk_actions, deleted_actions, tries - 1)
end
--- Tries to shrink down the list of FSM actions. This function is called
-- recursively until all tries are used.
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions List of actions to be shrunk down
-- @param step last step in the series of actions that succeeded while testing
-- the FSM property
-- @param tries Remaining tries to recusively try shrinking the actions
-- @return list of shrunk down actions
function lib.shrink_fsm_actions(fsm_table, generated_actions, step, tries)
if tries == 1 then
return generated_actions
end
local sliced_actions = lib.slice_last_actions(generated_actions, step) -- cut off actions after failure..
local shrunk_actions, deleted_actions =
shrink_actions(fsm_table, sliced_actions, Vector.new(), fsm_table.numshrinks)
if deleted_actions:size() == 0 then
-- shrinking did not help, try again
return lib.shrink_fsm_actions(fsm_table, sliced_actions, step, tries - 1)
end
-- shrinking did help, retry FSM:
local is_successful1, new_step1 = lib.execute_fsm(fsm_table, shrunk_actions)
if not is_successful1 then
-- FSM still fails, deleted actions can be ignored, try further shrinking
return lib.shrink_fsm_actions(fsm_table, shrunk_actions, new_step1, tries - 1)
end
-- now FSM works -> faulty action is in the just deleted actions
local shrunk_down_actions = shrink_deleted_actions(fsm_table, sliced_actions, deleted_actions, fsm_table.numshrinks)
-- retry fsm:
-- if a solution could not be found by shrinking down the deleted actions
-- -> sliced actions is smallest solution found
-- else if FSM still fails after shrinking deleted_actions
-- -> try further shrinking
local is_successful2, new_step2 = lib.execute_fsm(fsm_table, shrunk_down_actions)
local minimal_actions = is_successful2 and sliced_actions or shrunk_down_actions
return lib.shrink_fsm_actions(fsm_table, minimal_actions, new_step2, tries - 1)
end
--- Select a group of actions to be marked for shrinking down.
-- @param action_list List of actions to be shrunk down.
-- @return vector of indices for the actions which should be removed
local function select_actions_for_arg_shrinking(action_list)
local _, idx_vector = lib.select_actions(action_list)
if idx_vector:size() == 0 and is_between(action_list:size(), 2, 10) then
-- try shrinking 1 action anyway
local idx = random.between(1, action_list:size() - 1) -- don't shrink stop action!
idx_vector:push_back(idx)
end
return idx_vector
end
--- Does the actual shrinking of the command arguments of a sequence of actions.
-- @param fsm_table table containing description of a FSM property
-- @param action_list List of actions to be shrunk down
-- @return an updated sequence of the action list (original is modified!) with
-- shrunk arguments.
local function shrink_args(fsm_table, action_list)
local idx_vector = select_actions_for_arg_shrinking(action_list)
for i = idx_vector:size(), 1, -1 do -- shrunk from end to beginning (most likely to succeed)
local idx = idx_vector:get(i)
local action = action_list:get(idx)
for _ = 1, fsm_table.numshrinks do
local command_copy = action.command -- shallow copy (reference only)
action.command = action.cmd_gen:shrink(action.command)
-- revert if shrink is not valid
local is_valid = lib.is_action_sequence_valid(fsm_table, action_list)
if not is_valid then
action.command = command_copy;
break
end
end
end
return action_list
end
--- Shrinks down the arguments provided to the sequence of actions
-- This function is called recursively until all tries are used.
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions List of actions to be shrunk down
-- @param tries Remaining tries to recusively try shrinking the actions
-- @return
local function shrink_fsm_args(fsm_table, generated_actions, tries)
if tries == 1 then
return generated_actions
end
local shrunk_actions = shrink_args(fsm_table, deep_copy(generated_actions)) -- , fsm_table.numshrinks)
-- retry FSM
local is_successful = lib.execute_fsm(fsm_table, shrunk_actions)
if not is_successful then
-- FSM still fails, shrinking of args was successful, try further shrinking
return shrink_fsm_args(fsm_table, shrunk_actions, tries - 1)
end
-- FSM works now, shrinking of args unsuccessful -> retry
return shrink_fsm_args(fsm_table, generated_actions, tries - 1)
end
--- Replays the FSM property.
-- @param fsm_table table containing description of a FSM property
-- @param action_vector List of actions to replay
-- @return last state and result while executing the FSM.
local function replay_fsm(fsm_table, action_vector)
local _, _, last_state, last_result = lib.execute_fsm(fsm_table, action_vector)
return last_state, last_result
end
--- Shrinks the list of generated actions for a given FSM.
-- This is a recursive function which keeps trying for X amount of times.
-- @param fsm_table table containing description of a FSM property
-- @param generated_actions List of actions to be shrunk down
-- @param step Last successful step that was executed before FSM failed
-- @return shrunk list of actions or original action list if shrinking did not help.
local function shrink_fsm(fsm_table, generated_actions, step)
local fsm_shrink_amount = fsm_table.numshrinks
local shrunk_actions = lib.shrink_fsm_actions(fsm_table, generated_actions, step, fsm_shrink_amount)
local shrunk_actions_and_args = shrink_fsm_args(fsm_table, shrunk_actions, fsm_shrink_amount)
local lst_state, lst_result = replay_fsm(fsm_table, shrunk_actions_and_args)
return shrunk_actions_and_args, lst_state, lst_result
end
--- The main checking function for FSM specifications.
-- Checks a number of times (according to FSM spec) if property is true.
-- If the specification failed, then the result will be shrunk down to a
-- simpler case.
-- @param description string description of the FSM property
-- @param fsm_table table containing description of a FSM property
-- @return nil on success; otherwise a table containing info related to FSM error.
function lib.check(description, fsm_table)
for _ = 1, fsm_table.numtests do
local generated_actions = lib.generate_actions(fsm_table)
local is_successful, last_step = lib.execute_fsm(fsm_table, generated_actions)
if not is_successful then
report.report_failed()
local shrunk_actions, last_state, last_result = shrink_fsm(fsm_table, generated_actions, last_step)
fsm_table.when_fail(shrunk_actions:to_table(), last_state, last_result)
return {
description = description,
generated_actions = generated_actions,
shrunk_actions = shrunk_actions,
}
end
report.report_success()
end
return nil
end
return lib