Module eqc_fsm

This module is designed for testing software with a finite number of abstract states--for example software described by UML statecharts.

Copyright © Quviq AB, 2006-2015

Version: 1.34.3

Description

This module is designed for testing software with a finite number of abstract states--for example software described by UML statecharts. It allows users to specify a collection of named states and the transitions between them, along with pre- and post-conditions and state transition functions. Users can assign weights to transitions, to achieve a good statistical distribution of test cases, and eqc_fsm can help users do so by generating visualizations of the state diagram, and by automated weight assignment.

eqc_fsm is closely related to eqc_statem, and generates test cases of precisely the same form. (One way to think of eqc_fsm is that it is to eqc_statem as gen_fsm is to gen_server). We assume that readers are already familiar with eqc_statem; if not, study the eqc_statem documentation before reading further.

Contents

Specifying State Machines

State machines are specified by a module defining call-back functions that are invoked to test the state machine. The call-backs closely resemble those used by eqc_statem (and are actually implemented in terms of them). The main differences are that the command generation call-back command(S) is replaced by a collection of functions corresponding to named states, and that the other call-backs are simpler to define, since QuickCheck can derive some of the information that the eqc_statem call-backs provide from the named state definitions.

The eqc_fsm Header File

Modules using eqc_fsm should begin by including the eqc_fsm header file:
  -include_lib("eqc/include/eqc_fsm.hrl"). 

(If QuickCheck is not installed in your Erlang library directory, then you will need to use -include and give a different path). This imports all the exported functions from eqc_fsm, together with those functions from eqc_statem that are usable with eqc_fsm too. Note that you should not include eqc_statem.hrl in the same module.

State Names and State Data

eqc_fsm tracks the state of each test case, just as eqc_statem does, but eqc_fsm splits the state into two parts: a state name (normally an atom), which corresponds to one of the states in a finite-state machine diagram, and state data, which may be any relevant information, but is normally a record. This is a similar distinction to that made between state names and state data by gen_fsm in the OTP libraries. Complete states are represented by a pair of the state name and the state data.

Specifying Named States

Specifying Transitions from each State

Each named state is defined by a function of the same name, taking the state data as a parameter, which returns a list of target state names to which a transition can be made, paired with generators for symbolic function calls whose execution follows that transition. For example, in a system with two named states, locked and unlocked, the unlocked state might be specified by

  unlocked(S) ->
    [{unlocked,{call,locker,read,[]}},
     {locked,  {call,locker,lock,[]}}].

which specifies that a transition from the unlocked state to the locked one can be made by calling locker:lock(), or we can call locker:read() and remain in the same state. eqc_fsm generates test cases which follow the transitions specified in this way from state to state. To a first approximation, we can think of an implicit eqc_gen:oneof/1 being applied to the result of a state function.

Restrictions on State Data

The state data may only be used in QuickCheck generators in the parameters of the symbolic calls. It is an error for the target states, and the names of the functions to be called, to depend on the value of the state data. As a consequence, it is not possible to use the usual QuickCheck trick of including a transition only if the state data permits it. For example, if the state data is a list, and read must be passed one of its elements, then we cannot write

  unlocked(S) ->
    [{unlocked,{call,locker,read,[elements(S)]}} || S/=[]] ++
    [{locked,  {call,locker,lock,[]}}].

because the transitions in the list returned now depend on the value of S. The solution to this problem follows in the next section.

A state function must return the same list of target states and transition generators, even if the state data is invalid. More precisely, it must do so for the initial state data, and all possible values of the state data in any reachable state. This means it is invalid to pattern match on the state data in the head of a state function definition, unless the pattern matches the initial state data, and all possible later values of the state data. Otherwise, an exception may be raised when QuickCheck analyses the state space.

Avoiding Exceptions in Generators

QuickCheck avoids transitions whose generation raises an exception. This can be used to include "conditional transitions" such as the read transition in the example above. We write instead:

  unlocked(S) ->
    [{unlocked,{call,locker,read,[elements(S)]}},
     {locked,  {call,locker,lock,[]}}].

If the first transition is chosen when S is the empty list, then eqc_gen:elements/1 will raise an exception when QuickCheck tries to generate the call of read. This is detected, and the other transition is chosen instead.

If necessary, an exception can be delayed until generation time by enclosing the expression responsible in ?LAZY(...). For example, if the argument to read were hd(S) instead of elements(S), then this would raise an exception if S is the empty list when unlocked is called--and before returning the list of transitions to QuickCheck. To avoid this, we would write ?LAZY(hd(S)) instead, to delay the exception until the moment when QuickCheck has chosen the first transition, and tries to generate the argument of read.

history as a Target State

If the target state name is the atom history, then this represents a transition to the same state. This can be used to abstract a set of transitions that can be used in several different states. For example,

  read_transition(S) ->
    [{history,{call,locker,read,[elements(S)]}}].

could be used to define a read transition that can be included in any other state by adding read_transition(S)++... to the state definition; whatever state it is used in, the read transition returns to the same state.

State Attributes

State functions may take additional parameters before the state data, called attributes. For example,

  unlocked(N,S) ->
    [{{unlocked,N+1},{call,locker,add,[value()]}} || N<4] ++
    [...other transitions...].

might represent a locker containing N values. When attributes are used, then the state names are tuples of the function name and the attribute values (all the parameters except the last). States with different attribute values are considered to be different states: {unlocked,0} and {unlocked,1} are different states in this example. Since QuickCheck enumerates every reachable state, then it is important that there are only finitely many reachable attribute values--this is why we only include an add transition when N is less than 4 in the example above--this ensures that states {unlocked,N} are not reachable for N greater than or equal to 5.

It is perfectly allowable for the list of transitions to depend on the values of attributes, as in this example--it is only the state data whose use is restricted.

Specifying the initial state

Each test case normally starts in the same initial state. The initial state is specified via two callback functions:

Specifying state transitions

eqc_fsm specifications need not specify separately how each command changes the state name--this is already specified by the definitions of each state. But it is necessary to specify how each command changes the state data. This is done via a call-back function

  next_state_data(From,To,S,Res,Call) -> state_data()
where

Just as in eqc_statem, the result of this function may contain symbolic variables and function calls, which are replaced by their values when the test is run.

Specifying preconditions

QuickCheck already knows that each call can only be made in a named state with a corresponding transition, but some calls may have preconditions that depend on the state data. These preconditions are specified by defining the callback

  precondition(From,To,S,Call) -> bool()

where the parameters are as described above for next_state_data. This callback must be defined, and must return true or false for each symbolic call generated. No test cases are ever generated in which a call has a false precondition.

It is allowable for two transitions from a state to call the same function--for example, in a normal case, and in an error case leading to an error state. In such a case, the precondition callback may only return true for one of the calls; otherwise QuickCheck would not be able to tell which transition to follow.

Specifying postconditions

Postconditions are specified by defining the callback

  postcondition(From,To,S,Call,Res) -> bool()

whose parameters are as described above for next_state_data. Postconditions are checked as a test case is run: any return value other than true is considered to be a test failure.

Specifying invariants

Invariants can be specified by defining the callback

  invariant(StateName,S) -> bool()

whose parameters are the current state name and state data. Invariants are checked as a test case is run: any return value other than true is considered to be a test failure. The intention is that invariant should check the actual state of the software under test, and compare it to the modelled state given by StateName and S.

If no call-back with this name is defined, then the invariant is assumed to be true.

Specifying dynamic preconditions

Dynamic preconditions can be specified by defining the callback

  dynamic_precondition(StateName,S,Call) -> bool()

whose parameters are the current state name and state data, and the current call with actual (i.e. not symbolic) argument values. Dynamic preconditions are checked as a test case is run: if the return value is false, then the command is not executed. If no call-back with this name is defined, then the precondition is assumed to be true.

Dynamic preconditions have significant disadvantages, and should only be used if there is no reasonable alternative. In particular, a dynamic precondition should not be used just because it easier to do so, than to enrich the model state with more information so that preconditions can be decided during test case generation. See the discussion in eqc_statem of the disadvantages of dynamic preconditions; in addition, in the context of eqc_fsm, dynamic preconditions cannot be used to disambiguate transitions, while normal preconditions can. So DON'T USE THEM, unless you really must.

Specifying features

Features are specified by defining the callback

  call_features(From,To,S,Call,Res) -> bool()

whose parameters are as described above for postcondition. Features are recorded as a test case is run, and can be recovered from the test history using eqc_statem:command_features/1.

The Property to Test

eqc_fsm specifications are tested using very similar properties to eqc_statem specifications: the only difference is that the commands/1 and run_commands/2 functions are imported from eqc_fsm instead (which is done by the eqc_fsm.hrl include file). Thus a suitable propery might look something like this:

  prop_locker() ->
      ?FORALL(Cmds,commands(?MODULE),
  	    begin
  		locker:start(),
  		{H,S,Res} = run_commands(?MODULE,Cmds),
  		locker:stop(),
  		Res == ok
  	    end).

Weighting Transitions

The definitions of state functions described above offer no opportunity to specify how often each transition should be chosen. This is done instead by a separate callback:

  weight(From,To,Call) -> integer()

This callback is optional: if it is omitted (or not exported), then the transitions from each state are chosen with equal probability. If weight is defined, then the weights it assigns to each transition are used in the same way as the weights passed to the eqc_gen:frequency/1 function: the probability of choosing a transition is proportional to the weight assigned to it.

It is important to measure the resulting distribution of transitions in the generated test data. This can be done during testing by amending the last line of the property above to

  aggregate(zip(state_names(H),command_names(Cmds)),
            Res == ok)

using the functions eqc:aggregate/2, state_names/1, eqc_statem:command_names/1, and eqc_statem:zip/2. The output generated shows the proportion of each state name/function name combination in the total set of transitions run.

Visualizing State Machines

eqc_fsm can generate diagrams visualizing the state space and the transitions between states, along with the frequency with which each transition is tested.

Visualization Tools

QuickCheck uses external tools to generate and display visualizations. Users need to install these tools separately, and make them available to QuickCheck.

Graph Drawing Using GraphViz

QuickCheck generates visualizations of the state space in the dot graph description language. These can be converted to a wide variety of image formats using the GraphViz tools, which can be downloaded from www.graphviz.org for a wide variety of platforms, under the open-source Common Public Licence.

Once installed on your system, ensure that dot (or dot.exe) is accessible via your path. If you can invoke dot in your shell by typing dot, then QuickCheck should be able to make use of it. Alternatively, you can specify the location of this file in the environment variable EQC_DOT. QuickCheck will use dot to generate JPEG visualizations (or another supported image type of your choice).

Configuring a JPEG viewer

If you provide QuickCheck with a JPEG viewer, then it will use it to display visualizations automatically. Under Windows XP and Vista, nothing needs to be done--your default JPEG viewer will be used automatically. Under other operating systems (or if you want to use a different viewer), you should set the environment variable EQC_VIEWER to the location of the viewer of your choice. QuickCheck displays images by running a shell command of the form <EQC_VIEWER> <filename>.jpg. You can specify another image type supported by GraphViz when generating images; in this case, the viewer should of course be suitable for the type of image you choose.

Creating visualizations

Visualizations are created and displayed by visualize/1. If only the dot file is required, then it can be generated by dot/1. The visualizations include the estimated frequency of each transition when tests are run.

Analysing State Machines

QuickCheck can predict the frequency with which each transition in the state machine will be followed during testing, and can also use these predictions to suggest a suitable choice of weights.

Analyzing Transition Frequencies

Calling analyze(M) builds the state space for the state machine specified in M, and predicts the frequency with which each transition is tested, taking into account the weights specified in the weight callback. Calling visualize/1 performs the analysis, and displays a visualization of the result, such as this example:

The percentages shown are the number of times the labelled transition will be followed during testing, as a percentage of the total number of transitions followed. The result of the analysis should agree with measured transition frequencies when sufficiently any tests are run (several thousands)--but the analysis is much quicker.

The results can be used to help assign appropriate weights to transitions. Users should aim to avoid orphan transitions, which are tested very rarely, because bugs which depend on such orphans will take a very long time to find.

Compensating for failed preconditions

The analysis assumes that once a transition has been chosen, then a suitable call can always be generated. If attempts to generate a call can fail--either because the generation raises an exception, or because the precondition is false--then the analysis will overestimate the frequency of that and subsequent transitions. It is important to compare the analysis results with actual measured transition frequencies, to see to what extent this is occuring.

To perform an accurate analysis, QuickCheck needs to know how often the attempt to generate a suitable call fails--which cannot be determined in advance. Users can provide an estimate of this by defining the (optional) call-back

  precondition_probability(From,To,CallGen)

where From and To are the source and target state names, and CallGen is the generator for a symbolic function call specified in the definition of the From state. The result should be a float between 0 and 1, which is the estimated probability of succeeding to generate a call satisfying the precondition for this transition. For example, if we expect generation of a read call in an unlocked state to fail half the time, then we could define

 precondition_probability(unlocked,_,{call,locker,read,_}) -> 0.5;
 precondition_probability(_,_,_) -> 1.0.

Automatic weight assignment

QuickCheck can assign weights to transitions automatically, taking into account the user's estimates of precondition probabilities specified above. QuickCheck compensates for a low precondition probability by trying to choose that transition more often. Weights are assigned so as to avoid orphan transitions, as far as possible--although in most cases, no assignment of weights can ensure an absolutely even distribution of testing effort.

The weight assignment is computed by automate_weights/1, which outputs a candidate definition of the weight call-back, and (if GraphViz and a JPEG viewer are available) visualizes the resulting transition frequencies. The definition of weight can be pasted back into the eqc_fsm specification, to cause QuickCheck to use the computed weights.

The assigned weights are not necessarily "optimal" in any sense, but are often better than a hand-assignment. It is still important to measure actual transition distribution, and tune the assigned weights if necessary.

Prioritizing Transitions

Often, some transitions should be tested more often than others: for example, one transition may call a function with no arguments, while another may have many complex arguments, with a wide variety of choices to explore. Of course, the latter needs to be tested more often than the former. When weights are assigned manually, then the user can take this into account by weighting the latter transition more highly, but when they are assigned automatically then a different mechanism is required.

Users can prioritize transitions by defining the optional call-back

  priority(From,To,CallGen) -> integer() | float() 
with the same parameters as precondition_probability. The automated weight assignment will then choose weights that increase the execution frequency of highly prioritized transitions. For example, using the same state machine as in the diagram above, we could increase the priority of lock transitions by defining
  priority(unlocked,_,{call,_,lock,_}) -> 10;
  priority(_,_,_) -> 1.

The resulting transition frequencies are as shown here:

Note that both the lock and unlock transitions are assigned a greater weight--the latter, because choosing unlock is necessary to permit another lock, so unlocking more often permits more tests of locking. Automated weight assignment takes into account these kinds of interactions between transitions, which is hard to do well by hand.

Data Types

callback_module()

callback_module() = atom()

The name of a module containing the eqc_fsm callbacks, as specified above.

command()

command() = eqc_statem:command()

As in eqc_statem.

flows()

flows() = [{float(), transition_pattern()}]

A list of transition patterns, paired with the proportion of executed transitions that match that pattern. The proportions sum to 1.

gen()

gen(A) = eqc_gen:gen(A)

As in eqc_gen.

pattern()

pattern() = any()

A term possibly containing the atom '_', which matches any similar term where occurrences of '_' may match any value.

state_data()

state_data() = any()

state_name()

state_name() = atom() | tuple()

transition_pattern()

transition_pattern() = {state_name(), state_name(), pattern()}

A pattern matching a transition from the first state to the second, making a symbolic call matching the pattern.

Function Index

analyze/1Analyzes the state machine specified in M, to predict the frequency with which each transition is tested, taking into account the weights specified in the weight callback and the probability of preconditions failing specified in the precondition_probability call-back.
automate_weights/1Computes an appropriate set of transition weights for the transitions in a callback module, using the priority callback to guide the distribution of transitions.
automate_weights/2Like automate_weights/1, but takes the image type as a parameter.
commands/1Generates a list of commands, just like eqc_statem:commands/1.
commands/2Generates a list of commands, starting from the given initial state with the given state data, just like eqc_statem:commands/2.
dot/1Visualizes the state graph of the callback module, creating a file M.dot which can be viewed with GraphViz.
parallel_commands/1Generate a parallel test case from the callbacks in the client module Mod.
parallel_commands/2Behaves like parallel_commands/1, but generates a test case starting in the state S.
run_commands/2Runs a list of commands generated using commands/1, just as does eqc_statem:run_commands/2.
run_commands/3Behaves like run_commands/2, but also takes an environment containing values for additional variables that may be referred to in test cases.
run_parallel_commands/2Runs a parallel test case, and returns the history of the prefix, each of the parallel tasks, and the overall result.
run_parallel_commands/3Like run_commands/2, but also takes an environment binding variables, like run_commands/3.
state_after/2Returns the symbolic state after a list of commands is run.
state_names/1Extracts the state names from a history.
states/1
visualize/1Visualizes the state graph of the callback module, and the transition frequencies computed by analyze/1.
visualize/2Like visualize/1, but takes the image type as a parameter.

Function Details

analyze/1

analyze(M::callback_module()) -> flows()

Analyzes the state machine specified in M, to predict the frequency with which each transition is tested, taking into account the weights specified in the weight callback and the probability of preconditions failing specified in the precondition_probability call-back. The result of the analysis should agree with measured transition frequencies, when several thousand tests are run.

automate_weights/1

automate_weights(M::callback_module()) -> any()

Computes an appropriate set of transition weights for the transitions in a callback module, using the priority callback to guide the distribution of transitions. Outputs a definition of the weight callback that users can, if they wish, make use of by pasting the definition into their callback module. Generates a visualization of the resulting distribution of transitions in Mod.dot, which can be visualized using GraphViz. Displays the visualization immediately, if GraphViz and a JPEG viewer are available. The weight assignment is based on the analysis implemented by analyze/1 and visualize/1.

automate_weights/2

automate_weights(M::callback_module(), ImageType::atom()) -> any()

Like automate_weights/1, but takes the image type as a parameter. This permits generation of other image types than JPEG.

commands/1

commands(M::callback_module()) -> gen([command()])

Generates a list of commands, just like eqc_statem:commands/1. The form of commands generated is exactly the same.

commands/2

commands(M::callback_module(), InitS::{state_name(), any()}) -> gen([command()])

Generates a list of commands, starting from the given initial state with the given state data, just like eqc_statem:commands/2.

dot/1

dot(M::callback_module()) -> flows()

Visualizes the state graph of the callback module, creating a file M.dot which can be viewed with GraphViz. See visualize/1 for details. The result is the same as the result of analyze/1.

parallel_commands/1

parallel_commands(Mod::atom()) -> gen(parallel_test_case())

Generate a parallel test case from the callbacks in the client module Mod. These test cases are used to test for race conditions that make the commands in the tests behave non-atomically. Blocking operations can be specified by defining the blocking/4 call-back:

  blocking(From,To,S,Call) -> bool()
  
See the documentation of eqc_statem for details.

parallel_commands/2

parallel_commands(Mod::atom(), S::symbolic_state()) -> gen(parallel_test_case())

Behaves like parallel_commands/1, but generates a test case starting in the state S.

run_commands/2

run_commands(M::callback_module(), Cmds::[command()]) -> {history(), {state_name(), dynamic_state()}, reason()}

Runs a list of commands generated using commands/1, just as does eqc_statem:run_commands/2. The result has the same form, except that the states are represented as pairs of state names and state data.

run_commands/3

run_commands(M::callback_module(), Cmds::[command()], Env::[{atom(), term()}]) -> {history(), {state_name(), dynamic_state()}, reason()}

Behaves like run_commands/2, but also takes an environment containing values for additional variables that may be referred to in test cases. Cf eqc_statem:run_commands/3.

run_parallel_commands/2

run_parallel_commands(Mod::atom(), ParCmds::parallel_test_case()) -> {command_history(), [command_history()], reason()}

Runs a parallel test case, and returns the history of the prefix, each of the parallel tasks, and the overall result.

run_parallel_commands/3

run_parallel_commands(Mod::atom(), ParCmds::parallel_test_case(), Env::[{atom(), term()}]) -> {command_history(), [command_history()], reason()}

Like run_commands/2, but also takes an environment binding variables, like run_commands/3.

state_after/2

state_after(M::callback_module(), Cmds::[command()]) -> symbolic_state()

Returns the symbolic state after a list of commands is run. The commands are not executed.

state_names/1

state_names(H::history()) -> [state_name()]

Extracts the state names from a history. This is useful in conjunction with eqc:aggregate/2.

To collect statistics on transitions together with their source states, use

aggregate(zip(state_names(H),command_names(Cmds)),...)
in your property.

states/1

states(M) -> any()

visualize/1

visualize(M::callback_module()) -> ok

Visualizes the state graph of the callback module, and the transition frequencies computed by analyze/1. The graph is saved in a file M.dot, which can be viewed using GraphViz. If visualization tools are correctly installed, then a M.jpg file will also be generated, and opened in a JPEG viewer.

visualize/2

visualize(M::callback_module(), ImageType::atom()) -> ok

Like visualize/1, but takes the image type as a parameter. This permits generation of other image types than JPEG.


Generated by EDoc, May 11 2015, 12:46:12.