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.
472 lines
19 KiB
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
|
|
|