Skip to content

Latest commit

 

History

History
126 lines (74 loc) · 9.71 KB

specification.org

File metadata and controls

126 lines (74 loc) · 9.71 KB

Specifications

stateful-check executions happen in three stages: command generation, command execution, and trace verification.

First, commands are generated. In this stage, stateful-check uses your specification to generate a sequence of actions to perform, followed by a configurable number of parallel sequences to run (which defaults to zero). These commands are generated in-line with your specification such that the :requires and :preconditions for each command are valid. See below for more information about these attributes.

Once a sequence of commands has been generated it is executed on a real system. This essentially consists of running each action in turn (i.e. the :command key), and storing its result. Note that no verification of the results is done until the execution has completed. If an exception is thrown then execution stops at that point.

Finally, the results are verified. To do this, stateful-check will invoke each of the :postcondition functions in order to verify that they all return true. In the parallel case this is more difficult, but stateful-check will check to make sure that the observed results match at least one of the expected execution orders (assuming each command is atomic).

This clear separation of stages during execution means that no functions should interact with the System Under Test (SUT) except for the :command function of each command, and the :setup and :cleanup functions of the specification. It may still be necessary to generate commands where the results of previous commands are re-used as arguments to later commands. To facilitate this stateful-check uses “symbolic values” which are replaced with real values during the execution phase.

Commands

A command is a single action which the stateful-test runner may perform on the system. It specifies the command’s expected semantics, which are then checked by random trials.

Required

Commands have one required function:

KeyArguments
:commandsee optional :args function

The :command key specifies what to do when executing this command on the SUT. The arguments to this function are determined by the :args function (specified below). This is the only function in a command specification that can interact with the SUT.

Optional

Commands have a number of optional functions:

KeyArgumentsDefault value
:requires[state](constantly true)

The :requires key specifies a predicate that must be true in order for this command to be generated. If this function returns a falsey value then this command is not valid to generate with the provided state.

KeyArgumentsDefault value
:args[state](constantly nil)

The :args key specifies a function from the abstract state of the system to a generator. Values generated by the generator are then used as the arguments to this function (so the generated value must be seq-able). The value returned by this function is converted into a generator with the following rules:

  • objects matching gen/generator? are left unmodified
  • vectors are turned into a gen/tuple of their contents, after each if converted into a generator
  • maps are turned into a gen/hash-map with the keys held constant, and the values are converted into generators
  • all other values are wrapped in gen/return (that is: generated themselves)

During command execution, any “symbolic values” in the arguments for a command are replaced by the real value that they represent.

KeyArgumentsDefault value
:precondition[state args](constantly true)

The :precondition is very similar to the :requires key, except that it runs after the command’s arguments have been generated, and thus can test a relationship between the state and the generated arguments. If this function returns falsey then this command is not valid to generate with the provided state and args.

KeyArgumentsDefault value
:next-state[state args result](fn [state _ _] state)

The :next-state function denotes the effect of this command on the state. This command is called during command generation, but also during trace verification.

During command generation the result provided to this function is an abstract object used as a “symbolic value” representing the return value of the :command function. These objects do not permit introspection of the return value, but they may be added to the state and used during argument generation for other commands. See the end of this document for some more information about symbolic values.

During trace verification the result provided to this function is the value that was returned during command execution. It is important that the behaviour of the :next-state function is the same whether or not it is given a symbolic value or its real equivalent. If the :next-state function has different behaviours then it may create an inconsistent state object. To guard against this, you should not interact with the result object in any way, except those which are mentioned at the end of this document in the symbolic values section.

KeyArgumentsDefault value
:postcondition[prev-state next-state args result](constantly true)

The :postcondition function is how test assertions are performed. This function is provided with the state before (prev-state) and after (next-state) this command’s :next-state function is called. If this function returns a falsey value then the command did not perform as expected on the SUT and the execution containing it is recorded as a failure.

Everything provided to the :postcondition function is a “real” value. All symbolic values will be replaced before the :postcondition function is called.

Be aware that :postcondition functions run after the execution has completed, and thus any objects which have been mutated or otherwise changed may cause your test to fail. If you are planning to use a postcondition, ensure that your :command function returns an immutable value!

System Specifications

System specifications are a representation of a number of commands which can be performed on an actual system. They specify setup/cleanup operations, initial state, and any extra rules around command generation.

When running stateful-check it always expects a system specification to be provided at the top level.

(is (specification-correct? system-specification))

Required

Specifications have one required key:

KeyShape
:commandsmap of names to commands

The :commands key specifies all of the commands that can be used in this specification. Each command needs a “name”, which will be used in the command output to identify which command is being run.

The values of the map may either be a command map (see above section on their structure), or they may be a var which holds a reference to a command map. If the value is a var then it will be dereferenced whenever the command is generated (this permits the var to be redefined without needing to also redefine the spec).

{:new #'new-command
 :pop #'pop-command
 :push #'push-command}

Optional

Specifications also have a number of optional functions:

KeyArgumentsDefault value
:generate-command[state](gen/elements (:keys system-specification))

The :generate-command function is used to determine which command to add to the command list next. The generator returned by :generate-command is used to generate the name of the next command (which then goes through ordinary command generation).

In general, if your commands are set up appropriately then you will not need to declare a :generate-command function. It can be helpful for changing the distribution of generated commands, or for increasing the efficiency of generation in some cases.

KeyArgumentsDefault value
:setupnonenil
:cleanup[setup] or nonenil

The :setup function is run prior to the real execution phase. It should perform any one-time setup tasks which are necessary to prepare the SUT.

The :cleanup function is run immediately after the real execution phase. It is always run (irrespective of the pass/fail state of the test) and should clean up any necessary resources. If you have declared a :setup function, then :cleanup will be called with its return value as an argument. If you have not declared a :setup function then :cleanup will be called with no arguments.

KeyArgumentsDefault value
:initial-statenone or [setup](constantly nil)

The :initial-state function is used to seed the state value, which is then used extensively throughout command generation and execution.

If a :setup function has been provided then the :initial-state function will be passed a symbolic value representing the result of the setup. During execution the symbolic value will be replaced with the value that :setup returned for that execution.

Symbolic values

Symbolic values are used during the abstract model phase in order to represent the results of real executions of commands. When they are used as the arguments to a command they are replaced by their concrete values.

The only operation permitted on symbolic values is to lookup a key within them. During the real execution phase the corresponding key will be looked up in the concrete value (so (:key symbolic-value) will, during real execution, be replaced with (:key concrete-value)).