Sismic user manual¶
About¶
Statecharts are a well-known visual modeling language for specifying the executable behavior of reactive event-based systems. The essential complexity of statechart models solicits the need for advanced model testing and validation techniques. Sismic is a statechart library for Python (version 3.5 or higher) providing a set of tools to define, validate, simulate, execute and test statecharts. Sismic is a recursive acronym that stands for Sismic Interactive Statechart Model Interpreter and Checker.
Sismic is mainly developed by Dr. Alexandre Decan as part of his research activities at the Software Engineering Lab of the University of Mons. Sismic is released as open source under the GNU Lesser General Public Licence version 3.0 (LGPLv3).
The scientific article A method for testing and validating executable statechart models, published in 2018 in the Springer Software & Systems Modeling journal, describes the method and techniques for modeling, testing and validating statecharts, as supported by Sismic.
Features¶
Sismic provides the following features:
- An easy way to define and to import statecharts, based on the human-friendly YAML markup language
- A statechart interpreter offering a discrete, step-by-step, and fully observable simulation engine
- Fully controllable simulation clock, with support for real and simulated time
- Built-in support for expressing actions and guards using regular Python code, can be easily extended to other programming languages
- A design-by-contract approach for statecharts: contracts can be specified to express invariants, pre- and postconditions on states and transitions
- Runtime checking of behavioral properties expressed as statecharts
- Built-in support for behavior-driven development
- Support for communication between statecharts
- Synchronous and asynchronous executions
- Statechart visualization using PlantUML
The semantics of the statechart interpreter is based on the specification of the SCXML semantics (with a few exceptions), but can be easily tuned to other semantics. Sismic statecharts provides full support for the majority of the UML 2 statechart concepts:
- simple states, composite states, orthogonal (parallel) states, initial and final states, shallow and deep history states
- state transitions, guarded transitions, automatic (eventless) transitions, internal transitions and transition priorities
- statechart variables and their initialisation
- state entry and exit actions, transition actions
- internal and external events, parametrized events, delayed events
Installation¶
Using pip¶
Sismic requires Python >=3.5, and can be installed using pip
as usual: pip install sismic
.
This will install the latest stable version.
Starting from release 1.0.0, Sismic adheres to a semantic versioning scheme.
You can isolate Sismic installation by using virtual environments:
- Get the tool to create virtual environments:
pip install virtualenv
- Create the environment:
virtualenv -p python3.5 env
- Jump into:
source env/bin/activate
- Install Sismic:
pip install sismic
Consider using pew or pipenv to manage your virtual environments.
The development version can also be installed directly from its git repository:
pip install git+git://github.com/AlexandreDecan/sismic.git
From GitHub¶
You can also install Sismic from its repository by cloning it.
- Get the tool to create virtual environments:
pip install virtualenv
- Create the environment:
virtualenv -p python3.5 env
- Jump into:
source env/bin/activate
- Clone the repository:
git clone https://github.com/AlexandreDecan/sismic
- Install Sismic:
pip install .
orpip install -e .
(editable mode) - Install test dependencies:
pip install -r requirements.txt
Sismic is now available from the root directory. Its code is in the sismic directory.
The documentation can be built from the docs directory using make html
.
Tests are available both for the code and the documentation:
make doctest
in the docs directory (documentation tests)python -m pytest tests/
from the root directory (code tests)
Statecharts definition¶
About statecharts¶
Statecharts are a well-known visual language for modeling the executable behavior of complex reactive event-based systems. They were invented in the 1980s by David Harel, and have gained a more widespread adoption since they became part of the UML modeling standard.
Statecharts offer more sophisticated modeling concepts than the more classical state diagrams of finite state machines. For example, they support hierarchical composition of states, orthogonal regions to express parallel execution, guarded transitions, and actions on transitions or states. Different flavours of executable semantics for statecharts have been proposed in the literature and in existing tools.
Defining statecharts in YAML¶
Because Sismic is supposed to be independent of a particular visual modeling tool, and easy to integrate in other programs without requiring the implementation of a visual notation, statecharts are expressed using YAML, a human-friendly textual notation (the alternative of using something like SCXML was discarded because its notation is too verbose and not really “human-readable”).
This section explains how the elements that compose a valid statechart in Sismic can be defined using YAML. If you are not familiar with YAML, have a look at YAML official documentation.
See also
While statecharts can be defined in YAML, they can be defined in pure
Python too. Moreover, Statechart
instances exhibit several methods to query and
manipulate statecharts (e.g.: rename_state()
,
rotate_transition()
, copy_from_statechart()
, etc.).
Consider looking at Statechart
API for more information.
See also
Experimental import/export support for AMOLA specifications of statecharts is available as an extension of Sismic. AMOLA is notably used in ASEME IDE, which can be used to graphically create, edit and visualize statecharts. More information on Extensions for Sismic.
Statechart¶
The root of the YAML file must declare a statechart:
statechart:
name: Name of the statechart
description: Description of the statechart
root state:
[...]
The name and the root state keys are mandatory, the description is optional. The root state key contains a state definition (see below). If specific code needs to be executed during initialization of the statechart, this can be specified using preamble. In this example, the code is written in Python.
statechart:
name: statechart containing initialization code
preamble: x = 1
Code can be written on multiple lines as follows:
preamble: |
x = 1
y = 2
States¶
A statechart must declare a root state. Each state consist of at least a mandatory name. Depending on the state type, other optional fields can be declared.
statechart:
name: statechart with defined state
root state:
name: root
Entry and exit actions¶
For each declared state, the optional on entry and on exit fields can be used to specify the code that has to be executed when entering and leaving the state:
- name: s1
on entry: x += 1
on exit: |
x -= 1
y = 2
Shallow and deep history states¶
History states can be declared as follows:
- type: shallow history to declare a shallow history state;
- type: deep history to declare a deep history state.
- name: history state
type: shallow history
A history state can optionally declare a default initial memory using memory. Importantly, the memory value must refer to a parent’s substate.
- name: history state
type: deep history
memory: s1
Composite states¶
A state that is neither a final state nor a history state can contain nested states. Such a state is commonly called a composite state.
- name: composite state
states:
- name: nested state 1
- name: nested state 2
states:
- name: nested state 2a
A composite state can define its initial state using initial.
- name: composite state
initial: nested state 1
states:
- name: nested state 1
- name: nested state 2
initial: nested state a2
states:
- name: nested state 2a
Note
Unlike UML, but similarly to SCXML, Sismic does not explicitly represent the concept of region. A region is essentially a logical set of nested states, and thus can be viewed as a specialization of a composite state.
Orthogonal states¶
Orthogonal states (sometimes referred as parallel states) allow to specify multiple nested statecharts running in parallel. They must declare their nested states using parallel states instead of states.
statechart:
name: statechart containing multiple orthogonal states
root state:
name: processes
parallel states:
- name: process 1
- name: process 2
Transitions¶
Transitions between states, compound states and parallel states can be declared with the transitions field. Transitions typically specify a target state using the target field:
- name: state with transitions
transitions:
- target: other state
Other optional fields can be specified for a transition: a guard (a Boolean expression that will be evaluated to determine if the transition can be followed), an event (name of the event that will trigger the transition), an action (code that will be executed if the transition is processed). Here is a full example of a transition specification:
- name: state with an outgoing transition
transitions:
- target: some other state
event: click
guard: x > 1
action: print('Hello World!')
One type of transition, called an internal transition, does not require to declare a target. Instead, it must either define an event or define a guard to determine when it should become active (otherwise, infinite loops would occur during simulation or execution).
Notice that such a transition does not trigger the on entry and on exit of its state, and can thus be used to model an internal action.
Priorities can be set for transitions using the priority property. By default, all transitions have a priority of 0. A priority can be any integer, or low (equivalent to -1) or high (equivalent to 1).
Note
Transition priorities are considered after the default semantics of Sismic, i.e., after the eventless transition first and inner-first/source state semantics. Because of this, transition priorities cannot be used to, e.g., give priority to a transition with event versus a transition without event.
Statechart examples¶
Elevator¶
The Elevator statechart is one of the running examples in this documentation. Its visual description generated from Sismic using PlantUML looks as follows:

The corresponding YAML description is given below.
statechart:
name: Elevator
preamble: |
current = 0
destination = 0
doors_open = True
root state:
name: active
parallel states:
- name: movingElevator
initial: doorsOpen
states:
- name: doorsOpen
transitions:
- target: doorsClosed
guard: destination != current
action: doors_open = False
- target: doorsClosed
guard: after(10) and current > 0
action: |
destination = 0
doors_open = False
- name: doorsClosed
transitions:
- target: movingUp
guard: destination > current
- target: movingDown
guard: destination < current and destination >= 0
- name: moving
transitions:
- target: doorsOpen
guard: destination == current
action: doors_open = True
states:
- name: movingUp
on entry: current = current + 1
transitions:
- target: movingUp
guard: destination > current
- name: movingDown
on entry: current = current - 1
transitions:
- target: movingDown
guard: destination < current
- name: floorListener
initial: floorSelecting
states:
- name: floorSelecting
transitions:
- target: floorSelecting
event: floorSelected
action: destination = event.floor
Other examples¶
Some other examples can be found in the Git repository of the project, in docs/examples.
Importing and validating statecharts¶
The Statechart
class provides several methods to construct, to query and to manipulate a statechart.
A YAML definition of a statechart can be easily imported to a Statechart
instance.
The module sismic.io
provides a convenient loader import_from_yaml()
which takes a textual YAML definition of a statechart and returns a Statechart
instance.
-
sismic.io.
import_from_yaml
(text=None, filepath=None, *, ignore_schema=False, ignore_validation=False) Import a statechart from a YAML representation (first argument) or a YAML file (filepath argument).
Unless specified, the structure contained in the YAML is validated against a predefined schema (see sismic.io.SCHEMA), and the resulting statechart is validated using its validate() method.
Parameters: Return type: Statechart
Returns: a Statechart instance
For example:
from sismic.io import import_from_yaml
from sismic.model import Statechart
with open('examples/elevator/elevator.yaml') as f:
statechart = import_from_yaml(f)
assert isinstance(statechart, Statechart)
The function also supports importing from a given filepath:
statechart = import_from_yaml(filepath='examples/elevator/elevator.yaml')
assert isinstance(statechart, Statechart)
The parser performs several checks using Statechart
’s validate
method.
It also does an automatic validation against some kind of schema to prevent erroneous keys.
See schema library for more information about the semantics.
class SCHEMA:
contract = {schema.Or('before', 'after', 'always'): schema.Use(str)}
transition = {
schema.Optional('target'): schema.Use(str),
schema.Optional('event'): schema.Use(str),
schema.Optional('guard'): schema.Use(str),
schema.Optional('action'): schema.Use(str),
schema.Optional('contract'): [contract],
schema.Optional('priority'): schema.Or(schema.Use(int), 'high', 'low'),
}
state = dict() # type: ignore
state.update({
'name': schema.Use(str),
schema.Optional('type'): schema.Or('final', 'shallow history', 'deep history'),
schema.Optional('on entry'): schema.Use(str),
schema.Optional('on exit'): schema.Use(str),
schema.Optional('transitions'): [transition],
schema.Optional('contract'): [contract],
schema.Optional('initial'): schema.Use(str),
schema.Optional('parallel states'): [state],
schema.Optional('states'): [state],
})
statechart = {
'statechart': {
'name': schema.Use(str),
schema.Optional('description'): schema.Use(str),
schema.Optional('preamble'): schema.Use(str),
'root state': state,
}
}
Visualising statecharts¶
Sismic is not bundle with any graphical tool that can be used to edit or even view a statechart.
Module sismic.io
contains routines that can be used to (import and) export statecharts to other formats,
some of them being used by third-party tools that support visualising (or editing) statecharts.
Notably, module sismic.io
contains a function export_to_plantuml()
that exports a given statechart to
PlantUML, a tool based on graphviz that can automatically render statecharts (to some extent).
An online version of PlantUML can be found here.
For example, the elevator statechart can be exported to the following PlantUML file, which in turns can be used to generate the previously given representation of the elevator.
@startuml
title Elevator
state "active" as active {
state "floorListener" as floorListener {
[*] -right-> floorSelecting
state "floorSelecting" as floorSelecting {
floorSelecting --> floorSelecting : floorSelected / destination = event.floor
}
}
--
state "movingElevator" as movingElevator {
[*] -right-> doorsOpen
state "moving" as moving {
moving --> doorsOpen : [destination == current] / doors_open = True
state "movingDown" as movingDown {
movingDown : **entry** / current = current - 1
movingDown --> movingDown : [destination < current]
}
state "movingUp" as movingUp {
movingUp : **entry** / current = current + 1
movingUp --> movingUp : [destination > current]
}
}
state "doorsClosed" as doorsClosed {
doorsClosed --> movingUp : [destination > current]
doorsClosed --> movingDown : [destination < current and destination >= 0]
}
state "doorsOpen" as doorsOpen {
doorsOpen -right-> doorsClosed : [destination != current] / doors_open = False
doorsOpen -right-> doorsClosed : [after(10) and current > 0] / destination = 0; doors_open = False
}
}
}
@enduml
See also
PlantUML’s rendering can be modified to some extent by adjusting the notation used for transitions.
By default, -->
transitions correspond to downward transitions of good length.
A transition can be shortened by using ->
instead of -->
, and the direction of a transition can be
changed by using -up->
, -right->
, -down->
or -left->
. Both changes can be applied at the same time
using -u->
, -r->
, -d->
or -l->
.
See PlantUML documentation for more information.
If you already exported a statechart to PlantUML and made some changes to the direction or length of the transitions, it is likely that you want to keep these changes when exporting again the (possibly modified) statechart to PlantUML.
The export_to_plantuml()
function accepts two optional (mutually exclusive) parameters based_on
and based_on_filepath
that can be used to provide an earlier version of a PlantUML text representation
(or a path to such a version if based_on_filepath
is used).
This will then be used to incorporate as much as possible the changes made on transitions.
-
sismic.io.
export_to_plantuml
(statechart, filepath=None, *, based_on=None, based_on_filepath=None, statechart_name=True, statechart_description=False, statechart_preamble=False, state_contracts=False, state_action=True, transition_contracts=False, transition_action=True) Export given statechart to plantUML (see http://plantuml/plantuml). If a filepath is provided, also save the output to this file.
Due to the way statecharts are representing, and due to the presence of features that are specific to Sismic, the resulting statechart representation does not include all the informations. For example, final states and history states won’t have name, actions and contracts.
If a previously exported representation for the statechart is provided, either as text (based_on parameter) or as a filepath (based_on_filepath parameter), it will attempt to reuse the modifications made to the transitions (their direction and length).
Parameters: - statechart (
Statechart
) – statechart to export - filepath (
Optional
[str
]) – save output to given filepath, if provided - based_on (
Optional
[str
]) – existing representation of the statechart in PlantUML - based_on_filepath (
Optional
[str
]) – filepath to an existing representation of the statechart in PlantUML - statechart_name (
bool
) – include the name of the statechart - statechart_description (
bool
) – include the description of the statechart - statechart_preamble (
bool
) – include the preamble of the statechart - state_contracts (
bool
) – include state contracts - state_action (
bool
) – include state actions (on entry, on exit and internal transitions) - transition_contracts (
bool
) – include transition contracts - transition_action (
bool
) – include actions on transition
Return type: Returns: textual representation using plantuml
- statechart (
Statecharts visualization¶
Sismic is not bundle with any graphical tool that can be used to edit or even view a statechart.
Module sismic.io
contains routines that can be used to (import and) export statecharts to other formats,
some of them being used by third-party tools that support visualising (or editing) statecharts.
Notably, module sismic.io
contains a function export_to_plantuml()
that exports a given statechart to
PlantUML, a tool based on graphviz that can automatically render statecharts (to some extent).
An online version of PlantUML can be found here.
Function export_to_plantuml()
can be directly called from the command-line
without having to run a Python interpreter, through the sismic-plantuml
(or python -m sismic.io.plantuml
)
command-line interface.
usage: sismic-plantuml [-h] [--based-on based] [--show-description]
[--show-preamble] [--show-state-contracts]
[--show-transition-contracts] [--hide-state-action]
[--hide-name] [--hide-transition-action]
statechart
Command-line utility to export Sismic statecharts to plantUML.
See sismic.io.export_to_plantuml for more informations.
positional arguments:
statechart A YAML file describing a statechart
optional arguments:
-h, --help show this help message and exit
--based-on based A previously exported PlantUML representation
for this statechart
--show-description Show statechart description
--show-preamble Show statechart preamble
--show-state-contracts Show state contracts
--show-transition-contracts Show transition contracts
--hide-state-action Hide state action
--hide-name Hide statechart name
--hide-transition-action Hide transition action
For example, the elevator statechart presented in the previous section can be exported to the following PlantUML excerpt.
@startuml
title Elevator
state "active" as active {
state "floorListener" as floorListener {
[*] -right-> floorSelecting
state "floorSelecting" as floorSelecting {
floorSelecting --> floorSelecting : floorSelected / destination = event.floor
}
}
--
state "movingElevator" as movingElevator {
[*] -right-> doorsOpen
state "moving" as moving {
moving --> doorsOpen : [destination == current] / doors_open = True
state "movingDown" as movingDown {
movingDown : **entry** / current = current - 1
movingDown --> movingDown : [destination < current]
}
state "movingUp" as movingUp {
movingUp : **entry** / current = current + 1
movingUp --> movingUp : [destination > current]
}
}
state "doorsClosed" as doorsClosed {
doorsClosed --> movingUp : [destination > current]
doorsClosed --> movingDown : [destination < current and destination >= 0]
}
state "doorsOpen" as doorsOpen {
doorsOpen -right-> doorsClosed : [destination != current] / doors_open = False
doorsOpen -right-> doorsClosed : [after(10) and current > 0] / destination = 0; doors_open = False
}
}
}
@enduml
This PlantUML description can automatically be converted to the following statechart representation using the PlantUML tool (an online version can be found here).

See also
PlantUML’s rendering can be modified to some extent by adjusting the notation used for transitions.
By default, -->
transitions correspond to downward transitions of good length.
A transition can be shortened by using ->
instead of -->
, and the direction of a transition can be
changed by using -up->
, -right->
, -down->
or -left->
. Both changes can be applied at the same time
using -u->
, -r->
, -d->
or -l->
.
See PlantUML documentation for more information.
If you already exported a statechart to PlantUML and made some changes to the direction or length of the transitions, it is likely that you want to keep these changes when exporting again the (possibly modified) statechart to PlantUML.
The export_to_plantuml()
function accepts two optional (mutually exclusive) parameters based_on
and based_on_filepath
that can be used to provide an earlier version of a PlantUML text representation
(or a path to such a version if based_on_filepath
is used).
This will then be used to incorporate as much as possible the changes made on transitions.
-
sismic.io.
export_to_plantuml
(statechart, filepath=None, *, based_on=None, based_on_filepath=None, statechart_name=True, statechart_description=False, statechart_preamble=False, state_contracts=False, state_action=True, transition_contracts=False, transition_action=True) Export given statechart to plantUML (see http://plantuml/plantuml). If a filepath is provided, also save the output to this file.
Due to the way statecharts are representing, and due to the presence of features that are specific to Sismic, the resulting statechart representation does not include all the informations. For example, final states and history states won’t have name, actions and contracts.
If a previously exported representation for the statechart is provided, either as text (based_on parameter) or as a filepath (based_on_filepath parameter), it will attempt to reuse the modifications made to the transitions (their direction and length).
Parameters: - statechart (
Statechart
) – statechart to export - filepath (
Optional
[str
]) – save output to given filepath, if provided - based_on (
Optional
[str
]) – existing representation of the statechart in PlantUML - based_on_filepath (
Optional
[str
]) – filepath to an existing representation of the statechart in PlantUML - statechart_name (
bool
) – include the name of the statechart - statechart_description (
bool
) – include the description of the statechart - statechart_preamble (
bool
) – include the preamble of the statechart - state_contracts (
bool
) – include state contracts - state_action (
bool
) – include state actions (on entry, on exit and internal transitions) - transition_contracts (
bool
) – include transition contracts - transition_action (
bool
) – include actions on transition
Return type: Returns: textual representation using plantuml
- statechart (
Statecharts execution¶
Statechart semantics¶
The module interpreter
contains an Interpreter
class that
interprets a statechart mainly following the SCXML 1.0 semantics.
In particular, eventless transitions are processed before transitions containing events, internal events are consumed
before external events, and the simulation follows a inner-first/source-state and run-to-completion semantics.
The main difference between SCXML and Sismic’s default interpreter resides in how multiple transitions can be triggered simultaneously. This may occur for transitions in orthogonal/parallel states, or when transitions declaring the same event have guards that are not mutually exclusive.
Simulating the simultaneous triggering of multiple transitions is problematic, since it implies to make a non-deterministic choice on the order in which the transitions must be processed, and on the order in which the source states must the exited and the target states must be entered. The UML 2.5 specification explicitly leaves this issue unresolved, thereby delegating the decision to tool developers:
“Due to the presence of orthogonal Regions, it is possible that multiple Transitions (in different Regions) can be triggered by the same Event occurrence. The order in which these Transitions are executed is left undefined.” — UML 2.5 Specification
The SCXML specification addresses the issue by using the document order (i.e., the order in which the transitions appear in the SCXML file) as the order in which (non-parallel) transitions should be processed.
“If multiple matching transitions are present, take the first in document order.” — SCXML Specification
From our point of view, this solution is not satisfactory. The execution should not depend on the (often arbitrary) order in which items happen to be declared in some document, in particular when there may be many different ways to construct or to import a statechart.
Another statechart tool does not even define any order on the transitions in such situations:
“Rhapsody detects such cases of nondeterminism during code generation and does not allow them. The motivation for this is that the generated code is intended to serve as a final implementation and for most embedded software systems such nondeterminism is not acceptable.” — The Rhapsody Semantics of Statecharts
We decide to follow Rhapsody and to raise an error (in fact, a NonDeterminismError
) if such cases of
nondeterminism occur during the execution. Notice that this only concerns multiple transitions in the same
composite state, not in parallel states.
Note
Sismic allows to define priorities on transitions. This can be used to address some cases of nondeterminism. During execution, if a transition can be triggered, then transitions originating from the same state and whose priority is strictly lower than the selected one won’t be considered. Note that, as usual, transitions with no event are considered before transitions with event, regardless of the associated priorities.
When multiple transitions are triggered from within distinct parallel states, the situation is even more intricate. According to the Rhapsody implementation:
“The order of firing transitions of orthogonal components is not defined, and depends on an arbitrary traversal in the implementation. Also, the actions on the transitions of the orthogonal components are interleaved in an arbitrary way.” — The Rhapsody Semantics of Statecharts
SCXML circumvents this problem by relying again on the document order.
“enabledTransitions will contain multiple transitions only if a parallel state is active. In that case, we may have one transition selected for each of its children. […] If multiple states are active (i.e., we are in a parallel region), then there may be multiple transitions, one per active atomic state (though some states may not select a transition.) In this case, the transitions are taken in the document order of the atomic states that selected them.” — SCXML Specification
Again, Sismic does not agree with SCXML on this, and instead defines that multiple orthogonal/parallel transitions should be processed in a decreasing source state depth order. This is perfectly coherent with our aforementioned inner-first/source-state semantics, as “deeper” transitions are processed before “less nested” ones. In case of ties, the lexicographic order of the source state names will prevail.
Note that in an ideal world, orthogonal/parallel regions should be independent, implying that in principle such situations should not arise (“the designer does not rely on any particular order for event instances to be dispatched to the relevant orthogonal regions”, UML specification). In practice, however, it is often desirable to allow such situations.
See also
Other semantics can be quite easily implemented. For example, the extension sismic-semantics already provides support for outer-first/source-state semantics and priority to transitions with event. More information on Extensions for Sismic.
Using Interpreter¶
An Interpreter
instance is constructed upon a Statechart
instance and an optional callable that returns an Evaluator
.
This callable must accept an interpreter and an initial execution context as input (see Include code in statecharts).
If not specified, a PythonEvaluator
will be used.
This default evaluator can parse and interpret Python code in statecharts.
Consider the following example:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
# Load statechart from yaml file
elevator = import_from_yaml(filepath='examples/elevator/elevator.yaml')
# Create an interpreter for this statechart
interpreter = Interpreter(elevator)
When an interpreter is built, the statechart is not yet in an initial configuration.
To put the statechart in its initial configuration (and to further execute the statechart),
call execute_once()
.
print('Before:', interpreter.configuration)
step = interpreter.execute_once()
print('After:', interpreter.configuration)
Before: []
After: ['active', 'floorListener', 'movingElevator', 'doorsOpen', 'floorSelecting']
The method execute_once()
returns information about what happened
during the execution, including the transitions that were processed, the event that was consumed and the
sequences of entered and exited states (see Macro and micro steps and sismic.model.MacroStep
).
for attribute in ['event', 'transitions', 'entered_states', 'exited_states', 'sent_events']:
print('{}: {}'.format(attribute, getattr(step, attribute)))
event: None
transitions: []
entered_states: ['active', ...]
exited_states: []
sent_events: []
One can send events to the statechart using its sismic.interpreter.Interpreter.queue()
method.
This method accepts either an Event
instance, or the name of an event.
Multiple events (or names) can be provided at once.
interpreter.queue('click')
interpreter.execute_once() # Process the "click" event
interpreter.queue('clack') # An event name can be provided as well
interpreter.execute_once() # Process the "clack" event
interpreter.queue('click', 'clack')
interpreter.execute_once() # Process "click"
interpreter.execute_once() # Process "clack"
For convenience, queue()
returns the interpreter and thus can be chained:
interpreter.queue('click', 'clack', 'clock').execute_once()
Notice that execute_once()
consumes at most one event at a time.
In the above example, the clack event is not yet processed.
This can be checked by looking at the external event queue of the interpreter.
for time, event in interpreter._external_queue:
print(event.name)
clack
clock
Note
An interpreter has two event queues, one for external events (the ones that are added using
queue()
), and one for internal events (the ones that
are sent from within the statechart). External events are stored in _external_queue
while
internal events are stored in _internal_queue
. Internal events are always processed before
external ones. To access the next event that will be processed by the interpreter, use the
_select_event()
method.
To process all events at once, one can repeatedly call execute_once()
until
it returns a None
value, meaning that nothing happened during the last call. For instance:
while interpreter.execute_once():
pass
For convenience, an interpreter has an execute()
method that repeatedly
call execute_once()
and that returns a list of its output (a list of
sismic.model.MacroStep
).
from sismic.model import MacroStep
interpreter.queue('click', 'clack')
for step in interpreter.execute():
assert isinstance(step, MacroStep)
Notice that a call to execute()
first computes the list and then returns
it, meaning that all the steps are already processed when the call returns.
As a call to execute()
could lead to an infinite execution
(see for example simple/infinite.yaml),
an additional parameter max_steps
can be specified to limit the number of steps that are computed
and executed by the method. By default, this parameter is set to -1
, meaning there is no limit on the number
of underlying calls to execute_once()
.
interpreter.queue('click', 'clack', 'clock')
assert len(interpreter.execute(max_steps=2)) <= 2
# 'clock' is not yet processed
assert len(interpreter.execute()) == 1
The statechart used for these examples did not react to click, clack and clock because none of these events are expected to be received by the statechart (or, in other words, the statechart was not written to react to these events).
For convenience, a Statechart
has an events_for()
method
that returns the list of all possible events that are expected by this statechart.
print(elevator.events_for(interpreter.configuration))
['floorSelected']
The elevator statechart, the one used for this example, only reacts to floorSelected events.
Moreover, it assumes that floorSelected events have an additional parameter named floor
.
These events are parametrized events, and their parameters be accessed by action code and guards
in the statechart during execution.
For example, the floorSelecting state of the elevator example has a transition
floorSelected / destination = event.floor
that stores the value of the floor parameter
into the destination variable.
To add parameters to an event, simply pass these parameters as named arguments to the
queue()
method of the interpreter.
print('Current floor is', interpreter.context['current'])
interpreter.queue('floorSelected', floor=1)
interpreter.execute()
print('Current floor is', interpreter.context['current'])
Current floor is 0
Current floor is 1
Notice how we can access the current values of internal variables by use of interpreter.context
.
This attribute is a mapping between internal variable names and their current value.
Macro and micro steps¶
An interpreter execute_once()
(resp. execute()
) method returns
an instance of (resp. a list of) sismic.model.MacroStep
.
A macro step corresponds to the process of consuming an event, regardless of the number and the type (eventless or not)
of triggered transitions. A macro step also includes every consecutive stabilization step
(i.e., the steps that are needed to enter nested states, or to switch into the configuration of a history state).
A MacroStep
exposes the consumed event
if any, a (possibly
empty) list transition
of Transition
instances,
and two aggregated ordered sequences of state names, entered_states
and
exited_states
.
In addition, a MacroStep
exposes a list sent_events
of
events that were fired by the statechart during the considered step.
The order of states in those lists determines the order in which their on entry and on exit actions were processed.
As transitions are atomically processed, this means that they could exit a state in
entered_states
that is entered before some state in
exited_states
is exited.
The exact order in which states are exited and entered is indirectly available through the
steps
attribute that is a list of all the MicroStep
that were executed. Each of them contains the states that were exited and entered during its execution, and the a list
of events that were sent during the step.
A micro step is the smallest, atomic step that a statechart can execute.
A MacroStep
instance thus can be viewed (and is!) an aggregate of
MicroStep
instances.
This way, a complete run of a statechart can be summarized as an ordered list of
MacroStep
instances,
and details can be obtained using the MicroStep
list of a
MacroStep
.
Observing the execution¶
The interpreter is fully observable during its execution. It provides many methods and attributes that can be used to see what happens. In particular:
- The
execute_once()
(resp.execute()
) method returns an instance of (resp. a list of)sismic.model.MacroStep
.- The
log_trace()
function can be used to log all the steps that were processed during the execution of an interpreter. This methods takes an interpreter and returns a (dynamic) list of macro steps.- The list of active states can be retrieved using
configuration
.- The context of the execution is available using
context
(see Include code in statecharts).- It is possible to bind a callable that will be called each time an event is sent by the statechart using the
bind()
method of an interpreter (see Running multiple statecharts).- Meta-events are raised by the interpreter for specific events (e.g. a state is entered, a state is exited, etc.). Listeners can subscribe to these meta-events with
attach
.
Asynchronous execution¶
The calls to execute()
or execute_once()
are blocking calls, i.e. they are performed synchronously. To allow asynchronous execution of a statechart, one
has, e.g., to run the interpreter in a separate thread or to continuously loop over these calls.
Module runner
contains an AsyncRunner
that provides basic
support for continuous asynchronous execution of statecharts:
-
class
sismic.runner.
AsyncRunner
(interpreter, interval=0.1, execute_all=False) An asynchronous runner that repeatedly execute given interpreter.
The runner tries to call its execute method every interval seconds, assuming that a call to that method takes less time than interval. If not, subsequent call is queued and will occur as soon as possible with no delay. The runner stops as soon as the underlying interpreter reaches a final configuration.
The execution must be started with the start method, and can be (definitively) stopped with the stop method. An execution can be temporarily suspended using the pause and unpause methods. A call to wait blocks until the statechart reaches a final configuration.
The current state of a runner can be obtained using its running and paused properties.
While this runner can be used “as is”, it is designed to be subclassed and as such, proposes several hooks to control the execution and additional behaviours:
- before_run: called (only once !) when the runner is started. By default, do nothing.
- after_run: called (only once !) when the interpreter reaches a final configuration. configuration of the underlying interpreter is reached. By default, do nothing.
- execute: called at each step of the run. By default, call the execute_once method of the underlying interpreter and returns a list of macro steps.
- before_execute: called right before the call to execute(). By default, do nothing.
- after_execute: called right after the call to execute() with the returned value of execute(). By default, do nothing.
By default, this runner calls the interpreter’s execute_once method only once per cycle (meaning at least one macro step is processed during each cycle). If execute_all is set to True, then execute_once is repeatedly called until no macro step can be processed in the current cycle.
Parameters: - interpreter (
Interpreter
) – interpreter instance to run. - interval (
float
) – interval between two calls to execute - execute_all – Repeatedly call interpreter’s execute_once method at each step.
Anatomy of the interpreter¶
Note
This section explains which are the methods that are called during the execution of a statechart, and is mainly useful if you plan to extend or alter the semantics of the execution.
An Interpreter
makes use of several private methods for its initialization and computations.
These methods computes the transition(s) that should be processed, the resulting steps, etc.
These methods can be overridden or combined to define variants of statechart semantics.
-
Interpreter.
_compute_steps
()¶ Compute and returns the next steps based on current configuration and event queues.
Return type: List
[MicroStep
]Returns: a possibly empty list of steps
-
Interpreter.
_select_event
(*, consume=False)¶ Return the next event to process. Internal events have priority over external ones.
Parameters: consume ( bool
) – Indicates whether event should be consumed, default to False.Return type: Optional
[Event
]Returns: An instance of Event or None if no event is available
-
Interpreter.
_select_transitions
(event, states, *, eventless_first=True, inner_first=True)¶ Select and return the transitions that are triggered, based on given event (or None if no event can be consumed) and given list of states.
By default, this function prioritizes eventless transitions and follows inner-first/source state semantics.
Parameters: - event (
Optional
[Event
]) – event to consider, possibly None. - states (
Iterable
[str
]) – state names to consider. - eventless_first – True to prioritize eventless transitions.
- inner_first – True to follow inner-first/source state semantics.
Return type: List
[Transition
]Returns: list of triggered transitions.
- event (
-
Interpreter.
_sort_transitions
(transitions)¶ Given a list of triggered transitions, return a list of transitions in an order that represents the order in which they have to be processed.
Parameters: transitions ( List
[Transition
]) – a list of Transition instancesReturn type: List
[Transition
]Returns: an ordered list of Transition instances Raises: ExecutionError – In case of non-determinism (NonDeterminismError) or conflicting transitions (ConflictingTransitionsError).
-
Interpreter.
_create_steps
(event, transitions)¶ Return a (possibly empty) list of micro steps. Each micro step corresponds to the process of a transition matching given event.
Parameters: - event (
Optional
[Event
]) – the event to consider, if any - transitions (
Iterable
[Transition
]) – the transitions that should be processed
Return type: List
[MicroStep
]Returns: a list of micro steps.
- event (
-
Interpreter.
_create_stabilization_step
(names)¶ Return a stabilization step, ie. a step that lead to a more stable situation for the current statechart. Stabilization means:
- Enter the initial state of a compound state with no active child
- Enter the memory of a history state
- Enter the children of an orthogonal state with no active child
- Empty active configuration if root’s child is a final state
Parameters: names ( Iterable
[str
]) – List of states to consider (usually, the active configuration)Return type: Optional
[MicroStep
]Returns: A MicroStep instance or None if this statechart can not be more stabilized
-
Interpreter.
_apply_step
(step)¶ Apply given MicroStep on this statechart
Parameters: step ( MicroStep
) – MicroStep instanceReturn type: MicroStep
Returns: a new MicroStep, completed with sent events
These methods are all used (even indirectly) by execute_once
.
See also
Consider looking at the source of execute_once
to understand
how these methods are related and organized.
Include code in statecharts¶
Python code evaluator¶
A statechart can specify code that needs to be executed under some circumstances. For example, the preamble of a statechart, the guard or action of a transition or the on entry and on exit of a state may all contain code.
In Sismic, these pieces of code can be evaluated and executed by Evaluator
instances.
By default, when an interpreter is created, a PythonEvaluator
is created and allows
the interpreter to evaluate and execute Python code contained in a statechart.
Alternatively, a DummyEvaluator
that always evaluates conditions to True
and
silently ignores actions can be used, but is clearly of less interest.
In the following, we will implicitly assume that the code evaluator is an instance of PythonEvaluator
.
Context of the Python code evaluator¶
When a code evaluator is created or provided to an interpreter, all the variables that are defined or used by the
statechart are stored in an execution context. This context is exposed through the context
attribute of the interpreter and can be seen as a mapping between variable names and their values.
When a piece of code contained in a statechart has to be evaluated or executed, the context of the evaluator is used to
populate the local and global variables that are available for this piece of code.
As an example, consider the following partial statechart definition.
statechart:
# ...
preamble: |
x = 1
y = 0
root state:
name: s1
on entry: x += 1
When an interpreter is created for this statechart, its preamble is executed and the context of the code evaluator is
populated with {'x': 1, 'y': 0}
. When the statechart is further executed (initialized), and its root state
s1 is entered, the code x += 1
contained in the on entry
field of s1 is then executed in this context.
After execution, the context is {'x': 2, 'y': 0}
.
The default code evaluator uses a global context, meaning that all variables that are defined in the statechart are exposed by the evaluator when a piece of code has to be evaluated or executed. The main limitation of this approach is that you cannot have distinct variables with a same name in different states or, in other words, there is only one scope for all your variables.
The preamble of a statechart can be used to provide default values for some variables. However, the preamble is part of
the statechart and as such, cannot be used to parametrize the statechart. To circumvent this, an initial context
can be specified when a PythonEvaluator
is created. For convenience, this initial context
can also be passed to the constructor of an Interpreter
.
Considered the following toy example:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
yaml = """statechart:
name: example
preamble:
x = DEFAULT_X
root state:
name: s
"""
statechart = import_from_yaml(yaml)
Notice that variable DEFAULT_X
is used in the preamble but not defined. The statechart expects this
variable to be provided in the initial context, as illustrated next:
interpreter = Interpreter(statechart, initial_context={'DEFAULT_X': 1})
We can check that the value of x
is 1
by accessing the context
attribute of the interpreter:
assert interpreter.context['x'] == 1
Omitting to provide the DEFAULT_X
variable in the initial context leads to an error, as an unknown
variable is accessed by the preamble:
try:
Interpreter(statechart)
except Exception as e:
print(e)
"name 'DEFAULT_X' is not defined" occurred while executing "x = DEFAULT_X"
It could be tempting to define a default value for x
in the preamble and overriding this
value by providing an initial context where x
is defined. However, the initial context of an
interpreter is set before executing the preamble of a statechart. As a consequence, if a variable
is defined both in the initial context and the preamble, its value will be overridden by the preamble.
Consider the following example where x
is both defined in the initial context and the preamble:
yaml = """statechart:
name: example
preamble:
x = 1
root state:
name: s
"""
statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2})
assert interpreter.context['x'] == 1
The value of x
is eventually set to 1
.
While the initial context provided to the interpreter defined the value of x
to 2
, the code
contained in the preamble overrode its value. If you want to make use of the initial context to
somehow parametrize the execution of the statechart while still providing default values for
these parameters, you should either check the existence of the variables before setting their values
or rely on the setdefault
function that is exposed by the Python code evaluator when a piece of
code is executed (not only in the preamble).
This function can be used to define (and return) a variable, very similarly to the
setdefault
method of a dictionary. Using this function, we can easily rewrite the preamble
of our statechart to deal with the optional default values of x
(and y
and z
in this
example):
yaml = """statechart:
name: example
preamble: |
x = setdefault('x', 1)
setdefault('y', 1) # Value is affected to y implicitly
setdefault('z', 1) # Value is affected to z implicitly
root state:
name: s
on entry: print(x, y, z)
"""
statechart = import_from_yaml(yaml)
interpreter = Interpreter(statechart, initial_context={'x': 2, 'z': 3})
interpreter.execute()
2 1 3
Warning
Under the hood, a Python evaluator makes use of eval()
and exec()
with global and local contexts.
This can lead to some weird issues with variable scope (as in list comprehensions or lambda’s).
See this question on Stackoverflow for more information.
Predefined variables and functions¶
When a piece of code is evaluated or executed, the default Python code evaluator enriches its local context with several predefined variables and functions. These predefined objects depend on the situation triggering a code evaluation or a code execution (entry or exit actions, guard evaluation, transition action, …).
These entries are covered in the docstring of a PythonEvaluator
:
-
class
sismic.code.
PythonEvaluator
(interpreter=None, *, initial_context=None) A code evaluator that understands Python.
This evaluator exposes some additional functions/variables:
- On both code execution and code evaluation:
- A time: float value that represents the current time exposed by interpreter clock.
- An active(name: str) -> bool Boolean function that takes a state name and return True if and only
if this state is currently active, ie. it is in the active configuration of the
Interpreter
instance that makes use of this evaluator.
- On code execution:
- A send(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises an internal event with it. Raised events are propagated to bound statecharts as external events and to the current statechart as internal event. If delay is provided, a delayed event is created.
- A notify(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises a meta-event with it. Meta-events are only sent to bound property statecharts.
- If the code is related to a transition, the event: Event that fires the transition is exposed.
- A setdefault(name:str, value: Any) -> Any function that defines and returns variable name in the global scope if it is not yet defined.
- On guard or contract evaluation:
- If the code is related to a transition, an event: Optional[Event] variable is exposed. This variable contains the currently considered event, or None.
- On guard or contract (except preconditions) evaluation:
- An after(sec: float) -> bool Boolean function that returns True if and only if the source state was entered more than sec seconds ago. The time is evaluated according to Interpreter’s clock.
- A idle(sec: float) -> bool Boolean function that returns True if and only if the source state did not fire a transition for more than sec ago. The time is evaluated according to Interpreter’s clock.
- On contract (except preconditions) evaluation:
- A variable __old__ that has an attribute x for every x in the context when either the state was entered (if the condition involves a state) or the transition was processed (if the condition involves a transition). The value of __old__.x is a shallow copy of x at that time.
- On contract evaluation:
- A sent(name: str) -> bool function that takes an event name and return True if an event with the same name was sent during the current step.
- A received(name: str) -> bool function that takes an event name and return True if an event with the same name is currently processed in this step.
If an exception occurred while executing or evaluating a piece of code, it is propagated by the evaluator.
Parameters: - interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
- initial_context (
Optional
[Mapping
[str
,Any
]]) – a dictionary that will be used as __locals__
Anatomy of a code evaluator¶
Note
This section explains which are the methods that are called during the execution or evaluation of a piece of code, and is mainly useful if you plan to write your own statechart code interpreter.
An Evaluator
subclass must at lest implement the following methods and attributes:
-
Evaluator.
_evaluate_code
(code, *, additional_context=None) Generic method to evaluate a piece of code. This method is a fallback if one of the other evaluate_* methods is not overridden.
Parameters: Return type: Returns: truth value of code
-
Evaluator.
_execute_code
(code, *, additional_context=None) Generic method to execute a piece of code. This method is a fallback if one of the other execute_* methods is not overridden.
Parameters: Return type: List
[Event
]Returns: a list of sent events
-
Evaluator.
context
The context of this evaluator. A context is a dict-like mapping between variables and values that is expected to be exposed when the code is evaluated.
Return type: Mapping
[str
,Any
]
Note
None of those two methods are actually called by the interpreter during the execution of a statechart.
These methods are fallback methods that are used by other methods that are implicitly called depending on what is
currently being processed in the statechart. The documentation of Evaluator
covers this:
-
class
sismic.code.
Evaluator
(interpreter=None, *, initial_context=None) Abstract base class for any evaluator.
An instance of this class defines what can be done with piece of codes contained in a statechart (condition, action, etc.).
Notice that the execute_* methods are called at each step, even if there is no code to execute. This allows the evaluator to keep track of the states that are entered or exited, and of the transitions that are processed.
Parameters: - interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
- initial_context (
Optional
[Mapping
[str
,Any
]]) – an optional dictionary to populate the context
-
execute_statechart
(statechart) Execute the initial code of a statechart. This method is called at the very beginning of the execution.
Parameters: statechart ( Statechart
) – statechart to consider
-
evaluate_guard
(transition, event=None) Evaluate the guard for given transition.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: Optional
[bool
]Returns: truth value of code
- transition (
-
execute_action
(transition, event=None) Execute the action for given transition. This method is called for every transition that is processed, even those with no action.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: List
[Event
]Returns: a list of sent events
- transition (
-
execute_on_entry
(state) Execute the on entry action for given state. This method is called for every state that is entered, even those with no on_entry.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_on_exit
(state) Execute the on exit action for given state. This method is called for every state that is exited, even those with no on_exit.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
evaluate_preconditions
(obj, event=None) Evaluate the preconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_invariants
(obj, event=None) Evaluate the invariants for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_postconditions
(obj, event=None) Evaluate the postconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
Design by Contract for statecharts¶
About Design by Contract¶
Design by Contract (DbC) was introduced by Bertrand Meyer and popularised through his object-oriented Eiffel programming language. Several other programming languages also provide support for DbC. The main idea is that the specification of a software component (e.g., a method, function or class) is extended with a so-called contract that needs to be respected when using this component. Typically, the contract is expressed in terms of preconditions, postconditions and invariants.
Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software. It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as “contracts”, in accordance with a conceptual metaphor with the conditions and obligations of business contracts. — Wikipedia
DbC for statechart models¶
While DbC has gained some amount of acceptance at the programming level, there is hardly any support for it at the modeling level.
Sismic aims to change this, by integrating support for Design by Contract for statecharts.
The basic idea is that contracts can be defined on statechart componnents (states or transitions),
by specifying preconditions, postconditions, and invariants on them.
At runtime, Sismic will verify the conditions specified by the contracts.
If a condition is not satisfied, a ContractError
will be raised.
More specifically, one of the following 4 error types wil be raised: PreconditionError
,
PostconditionError
, or InvariantError
.
Contracts can be specified for any state contained in the statechart, and for any transition contained in the statechart. A state contract can contain preconditions, postconditions, and/or invariants. The semantics for evaluating a contract is as follows:
- For states:
- state preconditions are checked before the state is entered (i.e., before executing on entry), in the order of occurrence of the preconditions.
- state postconditions are checked after the state is exited (i.e., after executing on exit), in the order of occurrence of the postconditions.
- state invariants are checked at the end of each macro step, in the order of occurrence of the invariants. The state must be in the active configuration.
- For transitions:
- the preconditions are checked before starting the process of the transition (and before executing the optional transition action).
- the postconditions are checked after finishing the process of the transition (and after executing the optional transition action).
- the invariants are checked twice: one before starting and a second time after finishing the process of the transition.
Defining contracts in YAML¶
Contracts can easily be added to the YAML definition of a statechart (see Defining statecharts in YAML) through the use of the contract property. Preconditions, postconditions, and invariants are defined as nested items of the contract property. The name of these optional contractual conditions is respectively before (for preconditions), after (for postconditions), and always (for invariants):
contract:
- before: ...
- after: ...
- always: ...
Obviously, more than one condition of each type can be specified:
contract:
- before: ...
- before: ...
- before: ...
- after: ...
A condition is an expression that will be evaluated by an Evaluator
instance (see Include code in statecharts).
contract:
- before: x > 0
- before: y > 0
- after: x + y == 0
- always: x + y >= 0
Here is an example of a contracts defined at state level:
statechart:
name: example
root state:
name: root
contract:
- always: x >= 0
- always: not active('other state') or x > 0
If the default PythonEvaluator
is used,
it is possible to refer to the old value of some variable used in the statechart, by prepending __old__
.
This is particularly useful when specifying postconditions and invariants:
contract:
always: d > __old__.d
after: (x - __old__.x) < d
See the documentation of PythonEvaluator
for more information.
Executing statecharts containing contracts¶
The execution of a statechart that contains contracts does not essentially differ
from the execution of a statechart that does not.
The only difference is that conditions of each contract are checked
at runtime (as explained above) and may raise a subclass of ContractError
.
from sismic.interpreter import Interpreter, Event
from sismic.io import import_from_yaml
statechart = import_from_yaml(filepath='examples/elevator/elevator_contract.yaml')
# Make the run fails
statechart.state_for('movingUp').preconditions[0] = 'current > destination'
interpreter = Interpreter(statechart)
interpreter.queue('floorSelected', floor=4)
interpreter.execute()
Here we manually changed one of the preconditions such that it failed at runtime. The exception displays some relevant information to help debug:
Traceback (most recent call last):
...
sismic.exceptions.PreconditionError: PreconditionError
Object: BasicState('movingUp')
Assertion: current > destination
Configuration: ['active', 'floorListener', 'movingElevator', 'floorSelecting', 'moving']
Step: MicroStep(transition=Transition('doorsClosed', 'movingUp', event=None), entered_states=['moving', 'movingUp'], exited_states=['doorsClosed'])
Context:
- current = 0
- destination = 4
- doors_open = False
If you do not want the execution to be interrupted by such exceptions, you can set the ignore_contract
parameter to True
when constructing an Interpreter
.
This way, no contract checking will be done during the execution.
Monitoring properties¶
About runtime verification¶
Another key feature of Sismic’s interpreter is its support for monitoring properties at runtime, not only contracts. To avoid a statechart designer needing to learn a different language for expressing such properties, these properties are expressed using the statechart notation. These properties are then called property statecharts
Using statecharts to express properties¶
Property statecharts can be used to express functional properties of the intended behaviour in terms of the events that are consumed or sent, or in terms of the states that are entered or exited by a statechart. When a statechart is executed by Sismic, specific meta-events are created based on the events that are sent or consumed, the states that are entered of exited, etc. When the statechart being monitored is executed, the meta-events are propagated to all associated property statecharts. The property statecharts will look for property violations based on those meta-events, following a fail fast approach: they will report a failure as soon as the monitored behavior leads to a final state of the property statechart.
Due to the meta-events being considered and the “fail-fast” approach adopted by Sismic for their verification, property statecharts are mainly intended to check for the presence of undesirable behavior (safety properties), i.e., properties that can be checked on a (finite) prefix of a (possibly infinite) execution trace. While it is technically possible to use property statecharts to express liveliness properties (something desirable eventually happens), this would require additional code for their verification since liveliness properties are not supported “as is” by Sismic.
During the execution of a statechart, several meta-events are created depending on what happens in the statechart being executed. Those meta-events are automatically send to any previously bound property statechart.
To bind a property statechart to an interpreter, it suffices to provide the property statechart as a
parameter of the bind_property_statechart()
method of an interpreter.
This method accepts a Statechart
instance that is used to create an interpreter for
the property statechart. This method also accepts an optional interpreter_klass
parameter as a callable accepting
a Statechart
and a named parameter clock
(a Clock
instance)
and is used to build an interpreter for the property statechart.
If a property statechart reaches a final state during its execution, then the property is considered as not
satisfied, and a PropertyStatechartError
is raised.
This exception provides access to the interpreter that executed the property, the active configuration of statechart
being executed, the latest executed MacroStep
and the current context of the interpreter.
Meta-events generated by the interpreter¶
The complete list of MetaEvent
that are created by the interpreter is described in the
documentation of the attach()
method:
-
Interpreter.
attach
(listener) Attach given listener to the current interpreter.
The listener is called each time a meta-event is emitted by current interpreter. Emitted meta-events are:
- step started: when a (possibly empty) macro step starts. The current time of the step is available through the
time
attribute. - step ended: when a (possibly empty) macro step ends.
- event consumed: when an event is consumed. The consumed event is exposed through the
event
attribute. - event sent: when an event is sent. The sent event is exposed through the
event
attribute. - state exited: when a state is exited. The exited state is exposed through the
state
attribute. - state entered: when a state is entered. The entered state is exposed through the
state
attribute. - transition processed: when a transition is processed. The source state, target state and the event are
exposed respectively through the
source
,target
andevent
attribute. - Every meta-event that is sent from within the statechart.
This is a low-level interface for
self.bind
andself.bind_property_statechart
.Consult
sismic.interpreter.listener
for common listeners/wrappers.Parameters: listener ( Callable
[[MetaEvent
],Any
]) – A callable that accepts meta-event instances.Return type: None
- step started: when a (possibly empty) macro step starts. The current time of the step is available through the
Note
Property statecharts are not the only way to listen to these meta-events. Any listener
that is attached with attach()
will receive these
meta-events.
Property statecharts can listen to what happens in an interpreter when they are bound to
this interpreter, using bind_property_statechart()
method:
-
Interpreter.
bind_property_statechart
(statechart, *, interpreter_klass=None) Bind a property statechart to the current interpreter.
A property statechart receives meta-events from the current interpreter depending on what happens. See
attach
method for a full list of meta-events.The internal clock of all property statecharts is synced with the one of the current interpreter. As soon as a property statechart reaches a final state, a
PropertyStatechartError
will be raised, meaning that the property expressed by the corresponding property statechart is not satisfied. Property statecharts are automatically executed when they are bound to an interpreter.Since Sismic 1.4.0: passing an interpreter as first argument is deprecated.
This method is a higher-level interface for
self.attach
. Ifx = interpreter.bind_property_statechart(...)
, useinterpreter.detach(x)
to unbind a previously bound property statechart.Parameters: - statechart (
Statechart
) – A statechart instance. - interpreter_klass (
Optional
[Callable
]) – An optional callable that accepts a statechart as first parameter and a named parameter clock. Default to Interpreter.
Return type: Callable
[[MetaEvent
],Any
]Returns: the resulting attached listener.
- statechart (
Internally, this method wraps given property statechart to an appropriate listener, and
calls attach()
so you don’t have to.
Bound property statecharts can be unbound from the interpreter by calling the
detach()
method. This method accepts a
previously attached listener, so you’ll need to keep track of the listener returned
by the initial call to bind_property_statechart()
.
Examples of property statecharts¶
7th floor is never reached¶
This property statechart ensures that the 7th floor is never reached. It stores the current floor based on the number of times the elevator goes up and goes down.
statechart:
name: Test that the elevator never reachs 7th floor
preamble: floor = 0
root state:
name: root
initial: standing
states:
- name: standing
transitions:
- event: state entered
guard: event.state == 'moving'
target: moving
- guard: floor == 7
target: fail
- name: moving
transitions:
- event: state entered
guard: event.state == 'movingUp'
action: floor += 1
- event: state entered
guard: event.state == 'movingDown'
action: floor -= 1
- event: state exited
guard: event.state == 'moving'
target: standing
- name: fail
type: final
Elevator moves after 10 seconds¶
This property statechart checks that the elevator automatically moves after some idle time if it is not on the ground floor. The test sets a timeout of 12 seconds, but it should work for any number strictly greater than 10 seconds.
statechart:
name: Test that the elevator goes to ground floor after 10 seconds (timeout set to 12 seconds)
preamble: floor = 0
root state:
name: root
initial: active
states:
- name: active
parallel states:
- name: guess floor
transitions:
- event: state entered
guard: event.state == 'movingUp'
action: floor += 1
- event: state entered
guard: event.state == 'movingDown'
action: floor -= 1
- name: check timeout
initial: standing
states:
- name: standing
transitions:
- event: state entered
guard: event.state == 'moving'
target: moving
- guard: after(12) and floor != 0
target: timeout
- name: moving
transitions:
- event: state exited
guard: event.state == 'moving'
target: standing
- name: timeout
type: final
Heating does not start if door is opened¶
This property statechart checks that the heating of a microwave could not start if the door is currently opened.
statechart:
name: Heating does not start if door is opened
root state:
name: root
initial: door is closed
states:
- name: door is closed
transitions:
- target: door is opened
event: event consumed
guard: event.event.name == 'door_opened'
- name: door is opened
transitions:
- target: door is closed
event: event consumed
guard: event.event.name == 'door_closed'
- target: failure
event: event sent
guard: event.event.name == 'heating_on'
- name: failure
type: final
Heating must stop when door is opened¶
This property statechart ensures that the heating should quickly stop when the door is open while cooking occurs.
statechart:
name: Test that the elevator never reachs 7th floor
preamble: floor = 0
root state:
name: root
initial: standing
states:
- name: standing
transitions:
- event: state entered
guard: event.state == 'moving'
target: moving
- guard: floor == 7
target: fail
- name: moving
transitions:
- event: state entered
guard: event.state == 'movingUp'
action: floor += 1
- event: state entered
guard: event.state == 'movingDown'
action: floor -= 1
- event: state exited
guard: event.state == 'moving'
target: standing
- name: fail
type: final
Behavior-Driven Development¶
About Behavior-Driven Development¶
This introduction is inspired by the documentation of Behave, a Python library for Behavior-Driven Development (BDD). BDD is an agile software development technique that encourages collaboration between developers, QA and non-technical or business participants in a software project. It was originally named in 2003 by Dan North as a response to test-driven development (TDD), including acceptance test or customer test driven development practices as found in extreme programming.
BDD focuses on obtaining a clear understanding of desired software behavior through discussion with stakeholders. It extends TDD by writing test cases in a natural language that non-programmers can read. Behavior-driven developers use their native language in combination with the language of domain-driven design to describe the purpose and benefit of their code. This allows developers to focus on why the code should be created, rather than the technical details, and minimizes translation between the technical language in which the code is written and the domain language spoken by the business, users, stakeholders, project management, etc.
The Gherkin language¶
The Gherkin language is a business readable, domain specific language created to support behavior descriptions in BDD. It lets you describe software’s behaviour without the need to know its implementation details. Gherkin allows the user to describe a software feature or part of a feature by means of representative scenarios of expected outcomes. Like YAML or Python, Gherkin aims to be a human-readable line-oriented language.
Here is an example of a feature and scenario description with Gherkin, describing part of the intended behaviour of the Unix ls command:
Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
Scenario: List 2 files in a directory
Given I am in a directory "test"
And I have a file named "foo"
And I have a file named "bar"
When I run "ls"
Then I should get:
"""
bar
foo
"""
As can be seen above, Gherkin files should be written using natural language - ideally by the non-technical business participants in the software project. Such feature files serve two purposes: documentation and automated tests. Using one of the available Gherkin parsers, it is possible to execute the described scenarios and check the expected outcomes.
See also
A quite complete overview of the Gherkin language is available here.
Sismic support for BDD¶
Since statecharts are executable pieces of software, it is desirable for statechart users to be able to describe
the intended behavior in terms of feature and scenario descriptions.
While it is possible to manually integrate the BDD process with any library or software, Sismic is bundled with a
command-line utility sismic-bdd
(or python -m sismic.bdd
) that automates the integration of BDD.
Sismic support for BDD relies on Behave, a Python library for BDD with full support of the Gherkin language.
As an illustrative example, let us define the desired behavior of our elevator statechart. We first create a feature file that contains several scenarios of interest. By convention, this file has the extension .feature, but this is not mandatory. The example illustrates that Sismic provides a set of predefined steps (e.g., given, when, then) to describe common statechart behavior without having to write a single line of Python code.
Feature: Elevator
Scenario: Elevator starts on ground floor
When I do nothing
Then variable current equals 0
And variable destination equals 0
Scenario: Elevator can move to 7th floor
When I send event floorSelected with floor=7
Then variable current equals 7
Scenario: Elevator can move to 4th floor
When I send event floorSelected
| parameter | value |
| floor | 4 |
| dummy | None |
Then variable current equals 4
Scenario: Elevator reaches ground floor after 10 seconds
When I reproduce "Elevator can move to 7th floor"
Then variable current equals 7
When I wait 10 seconds
Then variable current equals 0
# Example using another step:
And expression "current == 0" holds
Scenario Outline: Elevator can reach floor from 0 to 5
When I send event floorSelected with floor=<floor>
Then variable current equals <floor>
Examples:
| floor |
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
Let us save this file as elevator.feature in the same directory as the statechart description, elevator.yaml.
We can then instruct sismic-bdd
to run on this statechart the scenarios described in the feature file:
sismic-bdd elevator.yaml --features elevator.feature
Under the hood, sismic-bdd
will create a temporary directory where all the files required to execute
Behave are put. It also makes available a list of predefined given, when, and then steps and sets up many
hooks that are required to integrate Sismic and Behave.
Note
Module sismic.bdd
exposes a execute_bdd()
function that is internally
used by sismic-bdd
CLI, and that can be used if programmatic access to these features is required.
When sismic-bdd
is executed, it will somehow translate the feature file into executable code, compute the outcomes
of the scenarios, check whether they match what is expected, and display as summary of all executed scenarios and
encountered errors:
[...]
1 feature passed, 0 failed, 0 skipped
10 scenarios passed, 0 failed, 0 skipped
22 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.027s
The sismic-bdd
command-line interface accepts several other parameters:
usage: sismic-bdd [-h] --features features [features ...]
[--steps steps [steps ...]]
[--properties properties [properties ...]] [--show-steps]
[--debug-on-error]
statechart
Command-line utility to execute Gherkin feature files using Behave. Extra parameters will be passed to Behave.
positional arguments:
statechart A YAML file describing a statechart
optional arguments:
-h, --help show this help message and exit
--features features [features ...]
A list of files containing features
--steps steps [steps ...]
A list of files containing steps implementation
--properties properties [properties ...]
A list of filepaths pointing to YAML property
statecharts. They will be checked at runtime following
a fail fast approach.
--show-steps Display a list of available steps (equivalent to
Behave's --steps parameter
--debug-on-error Drop in a debugger in case of step failure (ipdb if
available)
Additionally, any extra parameter provided to sismic-bdd
will be passed to Behave.
See command-line parameters of Behave
for more information.
Predefined steps¶
In order to be able to execute scenarios, a Python developer needs to write code defining the mapping from the actions and assertions expressed as natural language sentences in the scenarios (using specific keywords such as given, when or then) to Python code that manipulates the statechart. To facilitate the implementation of this mapping, Sismic provides a set of predefined statechart-specific steps.
By convention, steps starting with given or when correspond to actions that must be applied on the statechart,
while steps starting with then correspond to assertions about the execution or the current state of the statechart.
More precisely, (1) all given or when steps implicitly call the execute()
method of the underlying interpreter, (2) all when steps capture the output of these calls, and (3) we developed all
predefined then steps to assert things based on the captured output (implying that only the steps that start
with when will be monitored in practice).
“Given” and “when” steps¶
Given/when I send event {name}
This step queues an event with provided name.
Given/when I send event {name} with {parameter}={value}
This step queues an event with provided name and parameter. More than one parameter can be specified when using Gherkin tables, as follows:
Scenario: Elevator can move to 4th floor When I send event floorSelected | parameter | value | | floor | 4 | | dummy | None |
Given/when I wait {seconds:g} seconds
Given/when I wait {seconds:g} second
These steps increase the internal clock of the interpreter.
Given/when I do nothing
This step does nothing. It’s main usage is when assertions using then steps are written as first steps of a scenario. As they require a when step to be present, use “when I do nothing”.
Given/when I reproduce “{scenario}”
This step reproduces all the given and when steps that are contained in provided scenario. When this step is prefixed with given (resp. when), the steps of the provided scenario will be reproduced using given (resp. when).
Scenario: Elevator can move to 7th floor When I send event floorSelected with floor=7 Then variable current equals 7 Scenario: Elevator reaches ground floor after 10 seconds When I reproduce "Elevator can move to 7th floor" Then variable current equals 7 When I wait 10 seconds Then variable current equals 0
Given/when I repeat “{step}” {repeat:d} times
This step repeats given step several times. The text of the step must be provided without its keyword, and will be executed using the current keyword (given or when).
“Then” steps¶
Then state {name} is entered
Then state {name} is not entered
Then state {name} is exited
Then state {name} is not exited
These steps assert that a state with provided name was respectively entered, not entered, exited, not exited.
Then state {name} is active
Then state {name} is not active
These steps assert that a state with provided name is (not) in the active configuration of the statechart.
Then event {name} is fired
Then event {name} is fired with {parameter}={value}
These steps assert that an event with provided name was sent. Additional parameters can be provided using Gherkin tables.
Then event {name} is not fired
This step asserts that no event with provided name was sent.
Then no event is fired
This step asserts that no event was fired.
Then variable {variable} equals {value}
This step asserts that the context of the statechart has a variable with a given name and a given value.
Then variable {variable} does not equal {value}
This step asserts that the context of a statechart has a variable with a given name, but a value different than the one that is provided.
Then expression “{expression}” holds
Then expression “{expression}” does not hold
These steps assert that given expression holds (does not hold). The expression will be evaluated by the underlying code evaluator (aPythonEvaluator
by default) using the current context.
Then statechart is in a final configuration
Then statechart is not in a final configuration
These steps assert that the statechart is (not) in a final configuration.
Implementing new steps¶
While the steps that are already predefined should be sufficient to manipulate the statechart, it is more intuitive
to use domain-specific steps to write scenarios.
For example, if the statechart being tested encodes the behavior of a microwave oven, the domain-specific step
“Given I open the door” corresponds to the action of sending an event door_opened
to the statechart, and is
more intuitive to use when writing scenarios.
Consider the following scenarios expressed using a domain-specific language:
Feature: Cooking
Scenario: Start cooking food
Given I open the door
And I place an item in the oven
And I close the door
And I press increase timer button 5 times
And I press increase power button
When I press start button
Then heating turns on
Scenario: Stop cooking food
Given I reproduce "Start cooking food"
When 2 seconds elapsed
Then variable timer equals 3
When I press stop button
Then variable timer equals 0
And heating turns off
Scenario: Cooking stops after preset time
Given I reproduce "Start cooking food"
When 5 seconds elapsed
Then variable timer equals 0
And heating turns off
The mapping from domain-specific step “Given I open the door” to the action of sending a door opened event to the statechart could be defined using plain Python code, by defining a new step following Python Step Implementations of Behave.
from behave import given, when
@given('I open the door')
@when('I open the door')
def opening_door(context):
context.interpreter.queue('door_opened')
For convenience, the context
parameter automatically provided by Behave at runtime exposes three Sismic-specific
attributes, namely interpreter
, trace
and monitored_trace
.
The first one corresponds to the interpreter being executed, the second one is a list of all executed macro steps,
and the third one is list of executed macro steps restricted to the ones that were performed during the
execution of the previous block of when steps.
However, this domain-specific step can also be implemented more easily as an alias of predefined step “Given I send event door_opened”. As we believe that most of the domain-specific steps are just aliases or combinations of predefined steps, Sismic provides two convenient helpers to map new steps to predefined ones:
-
sismic.bdd.
map_action
(step_text, existing_step_or_steps) Map new “given”/”when” steps to one or many existing one(s). Parameters are propagated to the original step(s) as well, as expected.
Examples:
- map_action(‘I open door’, ‘I send event open_door’)
- map_action(‘Event {name} has to be sent’, ‘I send event {name}’)
- map_action(‘I do two things’, [‘First thing to do’, ‘Second thing to do’])
Parameters: Return type: None
-
sismic.bdd.
map_assertion
(step_text, existing_step_or_steps) Map a new “then” step to one or many existing one(s). Parameters are propagated to the original step(s) as well, as expected.
map_assertion(‘door is open’, ‘state door open is active’) map_assertion(‘{x} seconds elapsed’, ‘I wait for {x} seconds’) map_assertion(‘assert two things’, [‘first thing to assert’, ‘second thing to assert’])
Parameters: Return type: None
Using these helpers, one can easily implement the domain-specific steps of our example:
from sismic.bdd import map_action, map_assertion
map_action('I open the door', 'I send event door_opened')
map_action('I close the door', 'I send event door_closed')
map_action('I place an item in the oven', 'I send event item_placed')
map_action('I press increase timer button {time} times', 'I repeat "I send event timer_inc" {time} times')
map_action('I press increase power button', 'I send event power_inc')
map_action('I press start button', 'I send event cooking_start')
map_action('I press stop button', 'I send event cooking_stop')
map_action('{tick} seconds elapsed', 'I repeat "I send event timer_tick" {tick} times')
map_assertion('Heating turns on', 'Event heating_on is fired')
map_assertion('Heating does not turn on', 'Event heating_on is not fired')
map_assertion('heating turns off', 'Event heating_off is fired')
map_assertion('lamp turns on', 'Event lamp_switch_on is fired')
map_assertion('lamp turns off', 'Event lamp_switch_off is fired')
Assuming that the features are defined in cooking.feature
, these steps in steps.py
, and the microwave in
microwave.yaml
, then sismic-bdd
can be used as follows:
$ sismic-bdd microwave.yaml --steps steps.py --features cooking.feature
Feature: Cooking # cooking.feature:1
[...]
1 feature passed, 0 failed, 0 skipped
3 scenarios passed, 0 failed, 0 skipped
17 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.040s
Statechart unit testing¶
About unit testing¶
Like any executable software artefacts, statecharts can and should be tested during their development. Like most other software libraries, the execution of Sismic can be checked with unit tests, including the execution of statecharts with Sismic.
There are many unit testing frameworks in Python. Internally, Sismic relies on pytest to check its own implementation as well as the execution of the statecharts provided as examples in the documentation.
Sismic API (especially the interpreter API) is open enough to make advanced unit testing possible.
To ease the writing of unit tests, Sismic is bundled with sismic.testing
module providing a set of common test primitives that can be used independently of a specific test framework.
Of course, these primitives do not cover all use cases, but cover the most frequent assertions that can be expected when the execution of a statechart has to be checked. Also, because some primitives are very easy to implement using the existing API, they are not included with this module.
For example:
- State ‘name’ is active:
'name' in interpreter.configuration
;- Variable ‘x’ has value y:
interpreter.context['x'] = y
;- Statechart is in a final configuration:
interpreter.final
;- …
Primitives for unit testing¶
-
sismic.testing.
state_is_entered
(steps, name) Holds if state was entered during given steps.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - name (
str
) – name of a state
Return type: Returns: given state was entered
- steps (
-
sismic.testing.
state_is_exited
(steps, name) Holds if state was exited during given steps.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - name (
str
) – name of a state
Return type: Returns: given state was exited
- steps (
-
sismic.testing.
event_is_fired
(steps, name, parameters=None) Holds if an event was fired during given steps.
If name is None, this function looks for any event. If parameters are provided, their values are compared with the respective attribute of the event. Not all parameters have to be provided, as only the ones that are provided are actually compared.
Parameters: Return type: Returns: event was fired
-
sismic.testing.
event_is_consumed
(steps, name, parameters=None) Holds if an event was consumed during given steps.
If name is None, this function looks for any event. If parameters are provided, their values are compared with the respective attribute of the event. Not all parameters have to be provided, as only the ones that are provided are actually compared.
Parameters: Return type: Returns: event was consumed
-
sismic.testing.
transition_is_processed
(steps, transition=None) Holds if a transition was processed during given steps.
If no transition is provided, this function looks for any transition.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - transition (
Optional
[Transition
]) – a transition
Return type: Returns: transition was processed
- steps (
Dealing with time¶
It is quite usual in statecharts to find notations such as “after 30 seconds”, often expressed as specific events on a transition. Sismic does not support the use of these special events, and proposes instead to deal with time by making use of some specifics provided by its interpreter and the default Python code evaluator.
Every interpreter has an internal clock that is exposed through its clock
attribute and that can be used to manipulate the time of the simulation.
The built-in Python code evaluator allows one to make use of after(...)
, idle(...)
in guards or contracts.
These two Boolean predicates can be used to automatically compare the current time (as exposed by interpreter clock)
with a predefined value that depends on the state in which the predicate is used. For instance, after(x)
will
evaluate to True
if the current time of the interpreter is at least x
seconds greater than the time when the
state using this predicate (or source state in the case of a transition) was entered.
Similarly, idle(x)
evaluates to True
if no transition was triggered during the last x
seconds.
These two predicates rely on the time
attribute of an interpreter.
The value of that attribute is computed at the beginning of each executed step based on a clock.
Note
The interpreter’s time is set by the clock each time execute_once()
is called.
Consequently, a call to execute()
(that repeatedly calls execute_once()
) could lead to macro steps with different time values, depending on the duration required to process the underlying calls to execute_once()
.
Interpreter clock¶
Sismic provides three implementations of Clock
in its sismic.clock
module.
The first one is a SimulatedClock
that can be manually or automatically incremented. In the latter case,
the speed of the clock can be easily changed. The second implementation is a classical UtcClock
that corresponds
to a wall-clock in UTC with no flourish. The third implemention is a SynchronizedClock
that synchronizes its time value
based on the one of an interpreter. Its main use case is to support the co-execution of property statecharts.
By default, the interpreter uses a SimulatedClock
. If you want the
interpreter to rely on another kind of clock, pass an instance of Clock
as the clock
parameter of an interpreter constructor.
Simulated clock¶
The default clock is a SimulatedClock
instance.
Its current time value can be read from the time
attribute.
The clock starts at 0 and can either be manually changed by setting its time value, or
automatically (after having called its start()
method).
from sismic.clock import SimulatedClock
clock = SimulatedClock()
print('initial time:', clock.time)
clock.time += 10
print('new time:', clock.time)
initial time: 0
new time: 10
Note
Notice that time is expected to be monotonic: it is not allowed to set a new value that is strictly lower than the previous one.
To support pseudo real time, a SimulatedClock
instance exposes two methods, namely
start()
and stop()
.
When the start()
method is called, the clock measures the elapsed time
using Python’s time.time()
function.
from time import sleep
clock = SimulatedClock()
clock.start()
sleep(0.1)
print('after 0.1: {:.1f}'.format(clock.time))
after 0.1: 0.1
You can still change the current time value even if the clock is started:
clock.time = 10
print('after having been set to 10: {:.1f}'.format(clock.time))
sleep(0.1)
print('after 0.1: {:.1f}'.format(clock.time))
after having been set to 10: 10.0
after 0.1: 10.1
Finally, a simulated clock can be accelerated or slowed down by changing the value
of its speed
attribute. By default, the value of this
attribute is set to 1
. A higher value (e.g., 2
) means that the clock will be faster
than real time (e.g., 2 times faster), while a lower value slows down the clock.
clock = SimulatedClock()
clock.speed = 100
clock.start()
sleep(0.1)
clock.stop()
print('new time: {:.0f}'.format(clock.time))
new time: 10
Example: manual time¶
The following example illustrates a statechart modelling the behavior of a simple elevator. If the elevator is sent to the 4th floor then, according to the YAML definition of this statechart, the elevator should automatically go back to the ground floor after 10 seconds.
- target: doorsClosed
guard: after(10) and current > 0
action: destination = 0
Rather than waiting for 10 seconds, one can simulate this. First, one should load the statechart and initialize the interpreter:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter, Event
statechart = import_from_yaml(filepath='examples/elevator/elevator.yaml')
interpreter = Interpreter(statechart)
The time of the internal clock of our interpreter is set to 0
by default.
We now ask our elevator to go to the 4th floor.
interpreter.queue(Event('floorSelected', floor=4))
interpreter.execute()
The elevator should now be on the 4th floor. We inform the interpreter that 2 seconds have elapsed:
interpreter.clock.time += 2
print(interpreter.execute())
The output should be an empty list []
.
Of course, nothing happened since the condition after(10)
is not
satisfied yet.
We now inform the interpreter that 8 additional seconds have elapsed.
interpreter.clock.time += 8
interpreter.execute()
The elevator must has moved down to the ground floor. Let’s check the current floor:
print(interpreter.context.get('current'))
0
Example: automatic time¶
If the execution of a statechart needs to rely on a real clock, the simplest way to achieve this
is by using the start()
method of an interpreter clock.
Let us first initialize an interpreter using one of our statechart example, the elevator:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter, Event
statechart = import_from_yaml(filepath='examples/elevator/elevator.yaml')
interpreter = Interpreter(statechart)
Initially, the internal clock is set to 0. As we want to simulate the statechart based on real-time, we need to start the clock. For this example, as we don’t want to have to wait 10 seconds for the elevator to move to the ground floor, we speed up the internal clock by a factor of 100:
interpreter.clock.speed = 100
interpreter.clock.start()
We can now execute the statechart by sending a floorSelected
event, and wait for the output.
For our example, we first ask the statechart to send to elevator to the 4th floor.
interpreter.queue(Event('floorSelected', floor=4))
interpreter.execute()
print('Current floor:', interpreter.context.get('current'))
print('Current time:', int(interpreter.clock.time))
At this point, the elevator is on the 4th floor and is waiting for another input event. The internal clock value is still close to 0.
Current floor: 4
Current time: 0
Let’s wait 0.1 second (remember that we speed up the internal clock, so 0.1 second means 10 seconds for our elevator):
from time import sleep
sleep(0.1)
interpreter.execute()
We can now check that our elevator is on the ground floor:
print(interpreter.context.get('current'))
0
Wall-clock¶
The second clock provided by Sismic is a UtcClock
whose time
is synchronized with system time (it relies on the time.time()
function of Python).
from sismic.clock import UtcClock
from time import time
clock = UtcClock()
assert (time() - clock.time) <= 1
Synchronized clock¶
The third clock is a SynchronizedClock
that expects an
Interpreter
instance, and synchronizes its time
value based on the value of the time
attribute of the interpreter.
The main use cases are when statechart executions have to be synchronized to the point where a shared clock instance is not sufficient because executions should occur at exactly the same time, up to the milliseconds. Internally, this clock is used when property statecharts are bound to an interpreter, as they need to be executed at the exact same time.
Implementing other clocks¶
You can quite easily write your own clock implementation, for example if you need to
synchronize different distributed interpreters.
Simply subclass the Clock
base class.
-
class
sismic.clock.
Clock
Abstract implementation of a clock, as used by an interpreter.
The purpose of a clock instance is to provide a way for the interpreter to get the current time during the execution of a statechart.
-
time
Current time
Return type: float
-
Delayed events¶
Sismic also has support for delayed events, i.e. events that will be triggered in the future.
When a delayed event is queued in an interpreter at time T
with delay D
,
it is not processed by a call to execute()
or to execute_once()
unless the current clock
time value exceeds T + D
.
Delayed events can be created simply by providing a delay
parameter when an
Event
instance is created, or when calling
an interpreter’s queue()
method.
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
statechart = import_from_yaml(filepath='examples/elevator/elevator.yaml')
interpreter = Interpreter(statechart)
interpreter.queue('floorSelected', floor=4, delay=5)
Delayed events are not processed by the interpreter, as long as the current clock as not reach given delay.
print('Current time:', interpreter.clock.time) # 0
interpreter.execute()
print('Current floor:', interpreter.context['current']) # Still on ground floor
Current time: 0
Current floor: 0
They are processed as soon as the clock time value exceeds the expected delay:
interpreter.clock.time = 5
interpreter.execute()
print('Current floor:', interpreter.context['current']) # Still on ground floor
Current floor: 4
Notice that the time when a delayed event will be processed is based on the time value of
the clock when the queue()
method is called, not
the time
attribute that corresponds to the time of
the last executed step.
interpreter.clock.time = 6
print('Interpreter time:', interpreter.time)
print('Clock time:', interpreter.clock.time)
interpreter.queue('floorSelected', floor=2, delay=1)
Interpreter time: 5
Clock time: 6
interpreter.clock.time = 7
interpreter.execute() # Event is processed, because 6 + 1 <= 7
print('Current floor:', interpreter.context['current'])
Current floor: 2
Running multiple statecharts¶
It is not unusual to have to deal with multiple distinct components in which the behavior of a component is driven
by things that happen in the other components.
One can model such a situation using a single statechart with parallel states, or by plugging several statecharts
into one main statechart (see sismic.model.Statechart.copy_from_statechart()
).
The communication and synchronization between the components can be done either by using active(state_name)
in
guards, or by sending internal events that address other components.
However, we believe that this approach is not very convenient:
- all the components must be defined in a single statechart;
- state name collision could occur;
- components must share a single execution context;
- component composition is not easy to achieve
- …
Sismic allows to define multiple components in multiple statecharts, and brings a way for those statecharts to communicate and synchronize via events.
Communicating statecharts¶
Every instance of Interpreter
exposes a bind()
method which allows to bind statecharts.
-
Interpreter.
bind
(interpreter_or_callable) Bind an interpreter (or a callable) to the current interpreter.
Internal events sent by this interpreter will be propagated as external events. If interpreter_or_callable is an Interpreter instance, its queue method is called. This is, if i1 and i2 are interpreters, i1.bind(i2) is equivalent to i1.bind(i2.queue).
This method is a higher-level interface for
self.attach
. Ifx = interpreter.bind(...)
, useinterpreter.detach(x)
to unbind a previously bound interpreter.Parameters: interpreter_or_callable ( Union
[Interpreter
,Callable
[[Event
],Any
]]) – interpreter or callable to bind.Return type: Callable
[[MetaEvent
],Any
]Returns: the resulting attached listener.
When an interpreter interpreter_1
is bound to an interpreter interpreter_2
using
interpreter_1.bind(interpreter_2)
, the internal events that are sent by interpreter_1
are automatically
propagated as external events to interpreter_2
.
The binding is not restricted to only two statecharts.
For example, assume we have three instances of Interpreter
:
assert isinstance(interpreter_1, Interpreter)
assert isinstance(interpreter_2, Interpreter)
assert isinstance(interpreter_3, Interpreter)
We define a bidirectional communication between the two first interpreters:
interpreter_1.bind(interpreter_2)
interpreter_2.bind(interpreter_1)
We also bind the third interpreters with the two first ones.
interpreter_3.bind(interpreter_1)
interpreter_3.bind(interpreter_2)
When an internal event is sent by an interpreter, the bound interpreters also receive this event as an external
event.
In the last example, when an internal event is sent by interpreter_3
, then a corresponding external event
is sent both to interpreter_1
and interpreter_2
.
Note
Practically, unless you subclassed Interpreter
, the only difference between
internal and external events are the priority order in which they are processed by the interpreter.
from sismic.interpreter import InternalEvent
# Manually create and raise an internal event
interpreter_3._raise_event(InternalEvent('test'))
print('Events for interpreter_1:', interpreter_1._select_event(consume=False))
print('Events for interpreter_2:', interpreter_2._select_event(consume=False))
print('Events for interpreter_3:', interpreter_3._select_event(consume=False))
Events for interpreter_1: Event('test')
Events for interpreter_2: Event('test')
Events for interpreter_3: InternalEvent('test')
Note
The bind()
method is a high-level interface for
attach()
. Internally, the former wraps given
interpreter or callable with an appropriate listener before calling
attach()
. You can unbound a previously
bound interpreter with detach()
method.
This method accepts a previously attached listener, so you’ll need to keep track of the listener returned
by the initial call to bind()
.
Example of communicating statecharts¶
Consider our running example, the elevator statechart. This statechart expects to receive floorSelected events (with a floor parameter representing the selected floor). The statechart operates autonomously, provided that we send such events.
Let us define a new statechart that models a panel of buttons for our elevator. For example, we consider that our panel has 4 buttons numbered 0 to 3.
statechart:
name: Elevator buttons
description: |
Buttons that remotely control the elevator.
root state:
name: active
parallel states:
- name: button_0
transitions:
- event: button_0_pushed
action: send('floorSelected', floor= 0)
- name: button_1
transitions:
- event: button_1_pushed
action: send('floorSelected', floor= 1)
- name: button_2
transitions:
- event: button_2_pushed
action: send('floorSelected', floor= 2)
- name: button_3
transitions:
- event: button_3_pushed
action: send('floorSelected', floor= 3)
As you can see in the YAML version of this statechart, the panel expects an event for each button: button_0_pushed, button_1_pushed, button_2_pushed and button_3_pushed. Each of those event causes the execution of a transition which, in turn, creates and sends a floorSelected event. The floor parameter of this event corresponds to the button number.
We bind our panel with our elevator, such that the panel can control the elevator:
from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
elevator = Interpreter(import_from_yaml(filepath='examples/elevator/elevator.yaml'))
buttons = Interpreter(import_from_yaml(filepath='examples/elevator/elevator_buttons.yaml'))
# Elevator will receive events from buttons
buttons.bind(elevator)
Events that are sent to buttons
are not propagated, but events that are sent by buttons
are automatically propagated to elevator
:
print('Awaiting event in buttons:', buttons._select_event()) # None
buttons.queue('button_2_pushed')
print('Awaiting event in buttons:', buttons._select_event()) # External event
print('Awaiting event in elevator:', elevator._select_event()) # None
buttons.execute(max_steps=2) # (1) initialize buttons, and (2) consume button_2_pushed
print('Awaiting event in buttons:', buttons._select_event()) # Internal event
print('Awaiting event in elevator:', elevator._select_event()) # External event
Awaiting event in buttons: None
Awaiting event in buttons: Event('button_2_pushed')
Awaiting event in elevator: None
Awaiting event in buttons: InternalEvent('floorSelected', floor=2)
Awaiting event in elevator: Event('floorSelected', floor=2)
The execution of bound statecharts does not differ from the execution of unbound statecharts:
elevator.execute()
print('Current floor:', elevator.context.get('current'))
Current floor: 2
Synchronizing the clock¶
Each interpreter in Sismic has its own clock to deal with time (see Dealing with time).
When creating an interpreter, it is possible to specify which clock should be used to compute the
time
attribute of the interpreter.
When multiple statecharts have to be run concurrently, it is often convenient to have their
time synchronized. This can be achieved (to some extent) by providing a shared instance
of a clock to their interpreter.
from sismic.io import import_from_yaml
elevator_sc = import_from_yaml(filepath='examples/elevator/elevator.yaml')
buttons_sc = import_from_yaml(filepath='examples/elevator/elevator_buttons.yaml')
from sismic.clock import SimulatedClock
from sismic.interpreter import Interpreter
# Create the clock and share its instance with all interpreters
clock = SimulatedClock()
elevator = Interpreter(elevator_sc, clock=clock)
buttons = Interpreter(buttons_sc, clock=clock)
Note
As SimulatedClock
is the default clock used in Sismic, we could have written the three
last lines of this example as follow:
elevator = Interpreter(elevator_sc)
buttons = Interpreter(buttons_sc, clock=elevator.clock)
We can now execute the statecharts and check their time value.
clock.start()
elevator_step = elevator.execute_once()
buttons_step = buttons.execute_once()
clock.stop()
As a single instance of a clock is used by both interpreter, the values exposed by their clocks are obviously the same:
assert elevator.clock.time == buttons.clock.time
However, even if the clock is the same for all interpreters, this does not always mean that the calls
to execute_once()
are all performed at the same time.
Depending on the time required to process the first execute_once
, the second one will be called
with a delay of (at least) a few milliseconds.
We can check this by looking at the time
attribute of the returned
steps, or by looking at the time
attribute of the interpreter
that corresponds to the time of the last executed step:
assert elevator_step.time != buttons_step.time
assert elevator.time != buttons.time
To avoid these slight variations between different calls to execute_once()
,
Sismic offers a SynchronizedClock
whose value is based on another interpreter’s time.
from sismic.clock import SynchronizedClock
elevator = Interpreter(elevator_sc)
buttons = Interpreter(buttons_sc, clock=SynchronizedClock(elevator))
With the help of this SynchronizedClock
, it is possible to perfectly “align” the time of several
interpreters. Obviously, in this context, we first need to execute the interpreter that “drives” the time:
elevator.clock.start()
elevator_step = elevator.execute_once()
buttons_step = buttons.execute_once()
elevator.clock.stop()
Now we can check that the time of the last executed steps are the same:
assert elevator_step.time == buttons_step.time
assert elevator.time == buttons.time
Note
While the two interpreters were virtually executed at the same time value,
their clocks still have different values as a SynchronizedClock
is based
on the time
attribute of given interpreter and not on its internal clock.
assert elevator.clock.time != buttons.clock.time
Warning
Because the time of an interpreter is set by the clock each time execute_once()
is called, you should avoid using execute()
(that repeatedly calls execute_once()
) if you want a perfect synchronization between two or more interpreters.
In our example, a call to execute()
instead of execute_once()
for the first interpreter implies that the time value of the second interpreter will equal the time value of the first interpreter after having executed all its macro steps.
In other words, the execution of the second interpreter will be synchronized with the execution of the last macro step of the first interpreter in that case.
Integrate statecharts into your code¶
Sismic provides several ways to integrate executable statecharts into your Python source code. The simplest way is to directly embed the entire code in the statechart’s description. This was illustrated with the Elevator example in Include code in statecharts. Its code is part of the YAML file of the statechart, and interpreted by Sismic during the statechart’s execution.
In order to make a statechart communicate with the source code contained in the environment in which it is executed, there are basically two approaches:
- The statechart sends events to, or receives external events from the environment.
- The environment stores shared objects in the statechart’s initial context, and the statechart calls operations on these objects and/or accesses the variables contained in it.
Of course, one could also use a hybrid approach, combining ideas from the three approches above.
Running example¶
In this document, we will present the main differences between the two approaches, on the basis of a simple example of a Graphical User Interface (GUI) whose behaviour is defined by a statechart. All the source code and YAML files for this example, discussed in more detail below, is available in the docs/examples directory of Sismic’s repository.
The example represents a simple stopwatch, i.e., a timer than can be started, stopped and reset. It also provides a split time feature and a display of the elapsed time. A button-controlled GUI of such a stopwatch looks as follows (inactive buttons are greyed out):

Essentially, the stopwatch simply displays a value, representing the elapsed time (expressed in seconds), which is initially 0. By clicking on the start button the stopwatch starts running. When clicking on stop, the stopwatch stops running. By using split, the time being displayed is temporarily frozen, although the stopwatch continues to run. Clicking on unsplit while continue to display the actual elapsed time. reset will restart from 0, and quit will quit the stopwatch application.
The idea is that the buttons will trigger state changes and actions carried out by an underlying statechart. Taking abstraction of the concrete implementation, the statechart would essentially look as follows, with one main active state containing two parallel substates timer and display.

Controlling a statechart from within the environment¶
Let us illustrate how to control a statechart through source code that executes in the environment containing the statechart. The statechart’s behaviour is triggered by external events sent to it by the source code each time one of the buttons in the GUI is pressed. Conversely, the statechart itself can send events back to the source code to update its display.
This statechart looks as follows:

Here is the YAML file containing the textual description of this statechart:
statechart:
name: Stopwatch
description: |
A simple stopwatch which support "start", "stop", "split", and "reset".
These features are triggered respectively using "start", "stop", "split", and "reset".
The stopwatch sends an "refresh" event each time the display is updated.
The value to display is attached to the event under the key "time".
The statechart is composed of two parallel regions:
- A "timer" region which increments "elapsed_time" if timer is running
- A "display" region that refreshes the display according to the actual time/lap time feature
preamble: elapsed_time = 0
root state:
name: active
parallel states:
- name: timer
initial: stopped
transitions:
- event: reset
action: elapsed_time = 0
states:
- name: running
transitions:
- event: stop
target: stopped
- guard: after(1)
target: running
action: elapsed_time += 1
- name: stopped
transitions:
- event: start
target: running
- name: display
initial: actual time
states:
- name: actual time
transitions:
- guard: after(0.2)
target: actual time
action: |
send('refresh', time=elapsed_time)
- event: split
target: lap time
- name: lap time
transitions:
- event: split
target: actual time
We observe that the statechart contains an elapsed_time
variable, that is updated every second while the stopwatch is in the running state.
The statechart will modify its behaviour by receiving start, stop, reset and split events from its external environment.
In parallel to this, every 100 milliseconds, the display state of the statechart sends a refresh event (parameterised by the time
variable containing the elapsed_time
value) back to its external environment.
In the lap time state (reached through a split event) , this regular refreshing is stopped until a new split event is received.
The source code (shown below) that defines the GUI of the stopwatch, and that controls the statechart by sending it events, is implemented using the Tkinter
library.
Each button of the GUI is bound to a Python method in which the corresponding event is created and sent to the statechart.
The statechart is bound to the source code by defining a new Interpreter
that contains the parsed YAML specification, and using the bind()
method. The event_handler
passed to it allows the Python source code to receive events back from the statechart.
In particular, the w_timer
field of the GUI will be updated with a new value of the time whenever the statechart sends a refresh event.
The run
method, which is put in Tk’s mainloop, updates the internal clock of the interpreter and executes the interpreter.
import time
import tkinter as tk
from sismic.interpreter import Interpreter
from sismic.io import import_from_yaml
# The two following lines are NOT needed in a typical environment.
# These lines make sismic available in our testing environment
import sys
sys.path.append('../../..')
# Create a tiny GUI
class StopwatchApplication(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# Initialize widgets
self.create_widgets()
# Create a Stopwatch interpreter
with open('stopwatch.yaml') as f:
statechart = import_from_yaml(f)
self.interpreter = Interpreter(statechart)
self.interpreter.time = time.time()
# Bind interpreter events to the GUI
self.interpreter.bind(self.event_handler)
# Run the interpreter
self.run()
def run(self):
# This function does essentially the same job than ``sismic.interpreter.run_in_background``
# but uses Tkinter's mainloop instead of a Thread, which is more adequate.
# Update internal clock and execute interpreter
self.interpreter.time = time.time()
self.interpreter.execute()
# Queue a call every 100ms on tk's mainloop
self.after(100, self.run)
# Update the widget that contains the list of active states.
self.w_states['text'] = 'active states: ' + ', '.join(self.interpreter.configuration)
def create_widgets(self):
self.pack()
# Add buttons
self.w_btn_start = tk.Button(self, text='start', command=self._start)
self.w_btn_stop = tk.Button(self, text='stop', command=self._stop)
self.w_btn_split = tk.Button(self, text='split', command=self._split)
self.w_btn_unsplit = tk.Button(self, text='unsplit', command=self._unsplit)
self.w_btn_reset = tk.Button(self, text='reset', command=self._reset)
self.w_btn_quit = tk.Button(self, text='quit', command=self._quit)
# Initial button states
self.w_btn_stop['state'] = tk.DISABLED
self.w_btn_unsplit['state'] = tk.DISABLED
# Pack
self.w_btn_start.pack(side=tk.LEFT,)
self.w_btn_stop.pack(side=tk.LEFT,)
self.w_btn_split.pack(side=tk.LEFT,)
self.w_btn_unsplit.pack(side=tk.LEFT,)
self.w_btn_reset.pack(side=tk.LEFT,)
self.w_btn_quit.pack(side=tk.LEFT,)
# Active states label
self.w_states = tk.Label(root)
self.w_states.pack(side=tk.BOTTOM, fill=tk.X)
# Timer label
self.w_timer = tk.Label(root, font=("Helvetica", 16), pady=5)
self.w_timer.pack(side=tk.BOTTOM, fill=tk.X)
def event_handler(self, event):
# Update text widget when timer value is updated
if event.name == 'refresh':
self.w_timer['text'] = event.time
def _start(self):
self.interpreter.queue('start')
self.w_btn_start['state'] = tk.DISABLED
self.w_btn_stop['state'] = tk.NORMAL
def _stop(self):
self.interpreter.queue('stop')
self.w_btn_start['state'] = tk.NORMAL
self.w_btn_stop['state'] = tk.DISABLED
def _reset(self):
self.interpreter.queue('reset')
def _split(self):
self.interpreter.queue('split')
self.w_btn_split['state'] = tk.DISABLED
self.w_btn_unsplit['state'] = tk.NORMAL
def _unsplit(self):
self.interpreter.queue('split')
self.w_btn_split['state'] = tk.NORMAL
self.w_btn_unsplit['state'] = tk.DISABLED
def _quit(self):
self.master.destroy()
if __name__ == '__main__':
# Create GUI
root = tk.Tk()
root.wm_title('StopWatch')
app = StopwatchApplication(master=root)
app.mainloop()
Controlling the environment from within the statechart¶
In this second example, we basically reverse the idea: now the Python code that resides in the environment contains the
logic (e.g., the elapsed_time
variable), and this code is exposed to, and controlled by, a statechart that represents
the main loop of the program and calls the necessary methods in the source code.
These method calls are associated to actions on the statechart’s transitions.
With this solution, the statechart is no longer a black box, since it needs to be aware of the source code, in particular
the methods it needs to call in this code.
An example of the Python code that is controlled by the statechart is given below:
class Stopwatch:
def __init__(self):
self.elapsed_time = 0
self.split_time = 0
self.is_split = False
self.running = False
def start(self):
# Start internal timer
self.running = True
def stop(self):
# Stop internal timer
self.running = False
def reset(self):
# Reset internal timer
self.elapsed_time = 0
def split(self):
# Split time
if not self.is_split:
self.is_split = True
self.split_time = self.elapsed_time
def unsplit(self):
# Unsplit time
if self.is_split:
self.is_split = False
def display(self):
# Return the value to display
if self.is_split:
return int(self.split_time)
else:
return int(self.elapsed_time)
def update(self, delta):
# Update internal timer of ``delta`` seconds
if self.running:
self.elapsed_time += delta
The statechart expects such a Stopwatch
instance to be created and provided in its initial context.
Recall that an Interpreter
accepts an optional initial_context
parameter.
In this example, initial_context={'stopwatch': Stopwatch()}
.
The statechart is simpler than in the previous example: one parallel region handles the running status of the stopwatch, and a second one handles its split features.

statechart:
name: Stopwatch
description: |
A simple stopwatch which support "start", "stop", "split", and "reset".
These features are triggered respectively using "start", "stop", "split", and "reset".
The stopwatch expects a "stopwatch" object in its initial context.
This object should support the following methods: "start", "stop", "split", "reset", and "unsplit".
root state:
name: active
parallel states:
- name: timer
initial: stopped
transitions:
- event: reset
action: stopwatch.reset()
states:
- name: running
transitions:
- event: stop
target: stopped
action: stopwatch.stop()
- name: stopped
transitions:
- event: start
target: running
action: stopwatch.start()
- name: display
initial: actual time
states:
- name: actual time
transitions:
- event: split
target: lap time
action: stopwatch.split()
- name: lap time
transitions:
- event: split
target: actual time
action: stopwatch.unsplit()
The Python code of the GUI no longer needs to listen to the events sent by the interpreter. It should, of course, continue to send events (corresponding to button presses) to the statechart using send
.
The binding between the statechart and the GUI is now achieved differently, by simply passing the stopwatch
object to the Interpreter
as its initial_context
.
import time
import tkinter as tk
from sismic.interpreter import Interpreter
from sismic.io import import_from_yaml
from stopwatch import Stopwatch
# The two following lines are NOT needed in a typical environment.
# These lines make sismic available in our testing environment
import sys
sys.path.append('../../..')
# Create a tiny GUI
class StopwatchApplication(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
# Initialize widgets
self.create_widgets()
# Create a Stopwatch interpreter
with open('stopwatch_external.yaml') as f:
statechart = import_from_yaml(f)
# Create a stopwatch object and pass it to the interpreter
self.stopwatch = Stopwatch()
self.interpreter = Interpreter(statechart, initial_context={'stopwatch': self.stopwatch})
self.interpreter.clock.start()
# Run the interpreter
self.run()
# Update the stopwatch every 100ms
self.after(100, self.update_stopwatch)
def update_stopwatch(self):
self.stopwatch.update(delta=0.1)
self.after(100, self.update_stopwatch)
# Update timer label
self.w_timer['text'] = self.stopwatch.display()
def run(self):
# Queue a call every 100ms on tk's mainloop
self.interpreter.execute()
self.after(100, self.run)
self.w_states['text'] = 'active states: ' + ', '.join(self.interpreter.configuration)
def create_widgets(self):
self.pack()
# Add buttons
self.w_btn_start = tk.Button(self, text='start', command=self._start)
self.w_btn_stop = tk.Button(self, text='stop', command=self._stop)
self.w_btn_split = tk.Button(self, text='split', command=self._split)
self.w_btn_unsplit = tk.Button(self, text='unsplit', command=self._unsplit)
self.w_btn_reset = tk.Button(self, text='reset', command=self._reset)
self.w_btn_quit = tk.Button(self, text='quit', command=self._quit)
# Initial button states
self.w_btn_stop['state'] = tk.DISABLED
self.w_btn_unsplit['state'] = tk.DISABLED
# Pack
self.w_btn_start.pack(side=tk.LEFT,)
self.w_btn_stop.pack(side=tk.LEFT,)
self.w_btn_split.pack(side=tk.LEFT,)
self.w_btn_unsplit.pack(side=tk.LEFT,)
self.w_btn_reset.pack(side=tk.LEFT,)
self.w_btn_quit.pack(side=tk.LEFT,)
# Active states label
self.w_states = tk.Label(root)
self.w_states.pack(side=tk.BOTTOM, fill=tk.X)
# Timer label
self.w_timer = tk.Label(root, font=("Helvetica", 16), pady=5)
self.w_timer.pack(side=tk.BOTTOM, fill=tk.X)
def _start(self):
self.interpreter.queue('start')
self.w_btn_start['state'] = tk.DISABLED
self.w_btn_stop['state'] = tk.NORMAL
def _stop(self):
self.interpreter.queue('stop')
self.w_btn_start['state'] = tk.NORMAL
self.w_btn_stop['state'] = tk.DISABLED
def _reset(self):
self.interpreter.queue('reset')
def _split(self):
self.interpreter.queue('split')
self.w_btn_split['state'] = tk.DISABLED
self.w_btn_unsplit['state'] = tk.NORMAL
def _unsplit(self):
self.interpreter.queue('split')
self.w_btn_split['state'] = tk.NORMAL
self.w_btn_unsplit['state'] = tk.DISABLED
def _quit(self):
self.master.destroy()
if __name__ == '__main__':
# Create GUI
root = tk.Tk()
root.wm_title('StopWatch (external)')
app = StopwatchApplication(master=root)
app.mainloop()
Credits¶
Development Lead¶
- Alexandre Decan
Contributors¶
- Tom Mens
- Mathieu Goeminne
- Ali Parsai
- Nikos Spanoudakis
- Timothy Rule
- Jan Wouter Versluis
Changelog¶
1.5.0 (2020-03-09)¶
- (Added) A
sismic-plantuml
command-line utility to accesssismic.io.export_to_plantuml
.
1.4.2 (2019-07-19)¶
- (Fixed) Transitions are no longer duplicated when calling
copy_from_statechart
(#91).
1.4.1 (2019-01-15)¶
The internals required to expose time and event-related predicates in a PythonEvaluator
are moved
to the Interpreter
instead of being handled by context providers. This eases the implementation by other
code evaluators of uniform semantics for these predicates. This change does not affect Sismic public API.
- (Deprecated) Internal module
sismic.code.context
.
1.4.0 (2018-10-21)¶
This new release contains many internal changes. While the public API is stable and/or backwards compatible, expect some breaking changes if you relied on Sismic internal API.
A new binding/monitoring system has been deployed on Interpreter
, allowing listeners to be notified about
meta-events. Listeners are simply callables that accept meta-events instances.
- (Added) An
Interpreter.attach
method that accepts any callable. Meta-events raised by the interpreter are propagated to attached listeners.- (Added) An
Interpreter.detach
method to detach a previously attached listener.- (Added) Module
sismic.interpreter.listener
with two convenient listeners for the newly introducedInterpreter.attach
method. TheInternalEventListener
identifies sent events and propagates them as external events. ThePropertyStatechartListener
propagates meta-events, executes and checks property statecharts.- (Changed)
Interpreter.bind
is built on top ofattach
andInternalEventListener
.- (Changed)
Interpreter.bind_property_statechart
is built on top ofattach
andPropertyStatechartListener
.- (Changed) Meta-Event step started has a
time
attribute.- (Changed) Property statecharts are checked for each meta-events, not only at the end of the step.
- (Changed) Meta-events step started and step ended are sent even if no step can be processed.
- (Deprecated) Passing an interpreter to
bind_property_statechart
is deprecated, useinterpreter_klass
instead.
Time and event related predicates were extracted from PythonEvaluator
to ease their reuse.
They can be found in TimeContextProvider
and EventContextProvider
of sismic.code.context
and
rely on the new monitoring system:
- (Added)
TimeContextProvider
andEventContextProvider
insismic.code.context
that exposes most of the predicates that are used inPythonEvaluator
.- (Added) A
setdefault
function that can be used in the preamble and actions of a statechart to assign a default value to a variable.- (Changed) Most predicates exposed by
PythonEvaluator
are implemented by context providers.- (Deprecated)
on_step_starts
method of anEvaluator
.
We also refactored how events are passed and processed by the interpreter. The main visible consequences are:
- (Added) Event parameters can be directly passed to
Interpreter.queue
.- (Fixed) Internal events are processed before external ones (regression introduced in 1.3.0).
- (Fixed) Optional transition for
testing.transition_is_processed
, as promised by its documentation but not implemented.- (Removed) Internal module
sismic.interpreter.queue
.- (Deprecated)
DelayedEvent
, useEvent
with adelay
parameter instead.- (Deprecated) BDD step delayed event sent, use event sent instead.
And some other small changes:
- (Added) Documentation about concurrently running multiple statecharts.
- (Changed) State invariants are checked even if no step can be processed.
- (Fixed) Hook-errors reported by
sismic-bdd
CLI are a little bit more verbose (#81).
1.3.0 (2018-07-06)¶
Priority can be defined on transitions, allowing to simulate default transitions and to break non-deterministic situations when many transitions are triggered for a single source state:
- (Added) Priority can be set for transitions (using low, high or any integer in yaml). Transitions are selected according to their priorities (still following eventless and inner-first/source state semantics).
- (Added) Interpreter’s
_select_transitions
gets two new parameters,eventless_first
andinner_first
. Both default toTrue
and can be used in subclasses to change the default semantics of the interpreter.
The current time of an interpreter is now clock-based driven, thanks to the Clock
base class and its implementations.
- (Added) A
sismic.clock
module with aClock
base class and three direct implementations, namelySimulatedClock
,UtcClock
andSynchronizedClock
. ASimulatedClock
allows to manually or automatically change the time, while aUtcClock
as the expected behaviour of a wall-clock and aSynchronizedClock
is a clock that synchronizes with another interpreter.Clock
instances are used by the interpreter to get the current time during execution. See documentation for more information.- (Added) An
Interpreter.clock
attribute that stores an instance of the newly addedClock
class.- (Changed)
interpreter.time
represents the time of the last executed step, not the current time. Useinterpreter.clock.time
instead.- (Deprecated) Setting
Interpreter.time
is deprecated, set time withInterpreter.clock.time
instead.
Queued events can be delayed when they are added to the interpreter event queue.
- (Added) Delayed events are supported through
DelayedEvent
andDelayedInternalEvent
. If a delayed event with delay d is queued or sent by an interpreter at time t, it will not be processed unless execute or execute_once is called after the current clock exceeds t + d.- (Added) Property statecharts receive a delayed event sent meta-event when a delayed event is sent by a statechart.
- (Added) Delayed events can be sent from within a statechart by specifying a
delay
parameter to thesent
function.- (Added) An
EventQueue
class (insismic.interpreter.queue
) that controls how (delayed) events are handled by an interpreter.
A new interpreter runner that benefit from the clock-based handling of time and delayed events:
- (Added) An
AsyncRunner
in the newly addedrunner
module to asynchronously run an interpreter at regular interval.- (Changed)
helpers.run_in_background
no longer synchronizes the interpreter clock. Use thestart()
method ofinterpreter.clock
or anUtcClock
instance instead.- (Deprecated)
helpers.run_in_background
is deprecated, userunner.AsyncRunner
instead.
And other small changes:
- (Added) A
sismic.testing
module containing some testing primitives to ease the writing of unit tests.- (Changed)
Interpreter.queue
does not longer acceptInternalEvent
.- (Fixed) State on entry time (used for
idle
andafter
) is set after the on entry action is executed, making the two predicates more accurate when long-running actions are executed when a state is entered. Similarly,idle
is reset after the action of a transition is performed, not before.- (Changed) Drop official support for Python 3.4.
1.2.2 (2018-06-21)¶
- (Fixed) Event shouldn’t be exposed when guards of eventless transitions are evaluated (regression introduced in version 1.2.1).
- (Changed) Improve performances when selecting transitions that could/will be triggered.
1.2.1 (2018-06-19)¶
- (Fixed) Transitions are evaluated according to their event (eventless ones first) and inner-first/source state semantics, allowing to bypass many useless guard evaluations.
1.2.0 (2018-06-11)¶
- (Added) A
notify
function that can be used in the action code fragments of a statechart to send user-defined meta-events to the bound property statecharts (#67).
1.1.1 (2018-04-26)¶
- (Fixed) Whitespaces in event parameters used in BDD steps are stripped before they are evaluated.
1.1.0 (2018-04-23)¶
- (Added)
Interpreter._select_event
accepts an additional parameterconsume
that can be used to select an event without consuming it. - (Added) Documentation for extensions, and two (not included in Sismic!) extensions providing import/export with AMOLA, and new semantics for the interpreter.
- (Fixed) Final states remain in the active configuration unless they are all children of the root state. In this case, statechart execution is stopped. Previously, if all leaf states of the active configuration were final states, the execution stopped even if these final states were nested in an orthogonal or compound state. The corrected behavior strictly adheres to SCXML 1.0 semantics. This could be a backward incompatible change if you explicitly relied on the previously wrong behaviour.
1.0.1 (2018-04-18)¶
- (Fixed) BDD steps that involve a state raise a
StatechartError
if state does not exist. This prevents state X is active (and its variants) to fail, e.g., because X is misspelled.
1.0.0 (2018-04-11)¶
After more than two years of development, Sismic is stable enough to be released in version 1.0.0. Consequently, Sismic will adhere to semantic versioning (see semver.org), meaning that breaking changes will only occur in major releases, backward compatible changes in minor releases, and bug fixes in patches.
0.26.9 (2018-04-03)¶
- (Fixed)
based_on
forexport_to_plantuml
correctly takes into account states whose name contains whitespaces. - (Fixed)
export_to_plantuml
properly exports transition with no event, no guard and no action. - (Changed)
export_to_yaml
does not add quotes by default.
0.26.8 (2018-03-23)¶
- (Added)
import_from_yaml
accepts afilepath
argument. - (Added)
based_on
andbased_on_filepath
parameters forexport_to_plantuml
so a previously generated PlantUML file can be used as a basis for a new one (including its modifications related to the direction and length of transitions).
0.26.7 (2018-03-21)¶
- (Removed) Nested context (ie. nested variable scopes) for the Python code evaluator.
- (Fixed) BDD step expression {expression} holds.
0.26.6 (2018-03-17)¶
- (Changed) Export to PlantUML uses short arrows by default.
- (Changed) Many improvements related to the transitions when using
export_to_plantuml
.
0.26.4 (2018-03-16)¶
- (Added)
sismic.bdd.execute_bdd
can be used to execute BDD tests programmatically. - (Added)
sismic.bdd.__main__
is the CLI interface forsismic-behave
and can now be executed usingpython -m sismic.bdd
too if sismic is available but not installed. - (Added) Many tests for BDD steps.
- (Changed)
Statechart.copy_from_statechart
has only its first argument that can be provided by position. The remaining ones (esp.source
andreplace
) should be provided by name. - (Fixed) Sismic requires behave >= 1.6.0.
- (Fixed) Older versions of typing do not contain
Deque
. - (Removed)
sismic.bdd.cli.execute_behave
, subsumed bysismic.bdd.execute_bdd
.
0.26.3 (2018-03-15)¶
- (Added)
sismic.bdd
exposessismic.bdd.cli.execute_behave
function to programmatically usesismic-bdd
. - (Changed)
execute_behave
function has only two required parameters, and the remaining ones (that have default values) can only be set by name, not by position. - (Changed)
action_alias
andassertion_alias
of modulesismic.bdd.steps
are renamed tomap_action
andmap_assertion
and are directly available fromsismic.bdd
.
0.26.2 (2018-03-15)¶
- (Fixed) Step Given/when I repeat “{step}” {repeat} times requires step to be provided with no Gherkin keyword. The current keyword (either given or when) is automatically used.
- (Fixed) Escape expression in then expression “{expression}” holds and its negative counterpart.
0.26.0 (2018-03-15)¶
Sismic support for BDD was completely rewritten. The CLI is now sismic-bdd
, pointing to the cli
submodule of
the newly created sismic.bdd
module. All steps that are related to Sismic internals were removed, and only
steps that manipulate the statechart are kept. Check the documentation and sismic.bdd.steps
for more information.
Execution semantics have slightly changed but shouldn’t have any impact when running BDD tests.
Predefined steps can be easily extended thanks to the action_alias
and assertion_alias
helpers.
See documentation for more details.
- (Changed)
sismic-behave
CLI is nowsismic-bdd
. - (Removed)
--coverage
option fromsismic-behave
CLI. - (Changed) Rename
sismic.testing
tosismic.bdd
, andsismic.testing.behave
tosismic.bdd.cli
. - (Changed) A new list of predefined steps, available in
sismic.bdd.steps
, see documentation. - (Changed) A “when” step is now required before any “then” step. The “then” steps assert on what happens during the “when” steps, and not on the whole execution or the last step as before.
- (Added)
sismic.bdd.steps
providesaction_alias
andassertion_alias
to make defining new steps easy. - (Changed) BDD tests are directly executed by
pytest
(instead of being triggered by Travis-CI).
Other changes:
- (Changed)
Interpreter.bind_property
becomesInterpreter.bind_property_statechart
. - (Changed)
helpers.coverage_from_trace
returns a dict with “entered states”, “exited states” and “processed transitions”. - (Removed) Unused
io.text
.
0.25.3 (2018-03-13)¶
- (Fixed)
export_to_dict
(and by extension,export_to_yaml
) didn’t export transition contracts. - (Changed) All the tests are now written using
pytest
instead ofunittest
.
0.25.2 (2018-03-11)¶
- (Added) Make
Event
,InternalEvent
andMetaEvent
available frominterpreter
as well. - (Changed) Move
helpers
fromsismic.interpreter.helpers
tosismic.helpers
. - (Removed) Remove module
stories
, not really required anymore.
0.25.1 (2018-03-09)¶
- (Added) Full equality comparison (
__eq__
) for states and transitions (including all relevant attributes). - (Added)
Interpreter.queue
also accepts an event name in addition to anEvent
instance. - (Added)
Interpreter.queue
accepts more than one event (or name) at once. - (Changed)
Evaluator.execute_onentry
andexecute_onexit
becomeexecute_on_entry
andexecute_on_exit
. - (Changed) Many type annotations were added or fixed.
- (Changed)
Interpreter.bind
can no longer be chained.
0.25.0 (2018-03-09)¶
Property statecharts do not require anymore the use of an ExecutionWatcher
and are now directly supported
by the interpreter. The documentation contains a new page, Monitoring properties, that explains how to monitor
properties at runtime and provides some examples of property statecharts.
- (Added) Property statechart can be bound to an interpreter with
interpreter.bound_property
method, that accepts either aStatechart
or anInterpreter
instance. - (Added) A
PropertyStatechartError
that is raised when a property statechart reaches a final state. - (Added) A
MetaEvent
class to represent meta-events sent by the interpreter for property statechart checking. - (Added)
Interpreter._notify_property(event_name, **kwargs)
andInterpreter._check_properties(macro_step)
that are used internally to respectively send meta-events to bound properties, and to check these properties. - (Changed)
Interpreter.raise_event
is nowInterpreter._raise_event
as it’s not supposed to be part of the public API. - (Removed)
sismic.testing
module was removed (including theExecutionWatcher
andTestStoryFromTrace
). - (Removed) BDD steps related to the execution watcher, in
sismic.testing.steps
. - (Fixed)
Interpreter.time
cannot be set to a lower value than the current one (ie. time is monotonic). - (Fixed) A statechart preamble cannot be used to send events.
0.24.3 (2018-03-08)¶
- (Fixed)
ExecutionWatcher.stop()
was not called at the end of the execution whensismic-behave
was called with--properties
. - (Removed) Unused dependency on
pyparsing
.
0.24.2 (2018-02-27)¶
- (Added)
sismic.io
contains anexport_to_plantuml
function to export a statechart to PlantUML. - (Added)
sismic-behave
accepts a--properties
argument, pointing to a list of YAML files containing property statecharts that will be checked during execution (in a fail fast mode). - (Changed)
sismic.io.export_to_yaml
accepts an additionalfilepath
argument. - (Fixed) Whitespaces in strings are trimmed when using
import_from_dict
(and hence, usingimport_from_yaml
).
0.23.1 (2018-02-20)¶
- (Fixed) An exited state is removed from the current configuration before its postconditions are checked.
- (Removed) Sequential conditions that were introduced in 0.22.0.
0.22.11 (2017-01-12)¶
- (Fixed) Path error when using
sismic-behave
on Windows.
0.22.10 (2016-11-25)¶
- (Added) A
--debug-on-error
parameter forsismic-behave
.
0.22.9 (2016-11-25)¶
- (Fixed) Behave step “Event x should be fired” now checks that the event was fired during the last execution.
0.22.8 (2016-10-19)¶
- (Fixed) YAML values like “1”, “1.0”, “yes”, “True” are converted to strings, not to int, float and bool respectively.
- (Changed)
ruamel.yaml
replacespyyaml
as supported YAML parser. - (Changed) Use
schema
instead ofpykwalify
(which unfortunately freezes its dependencies versions) to validate (the structure of) YAML files. - (Changed)
import_from_yaml
raisesStatechartError
instead ofSchemaError
if it cannot validate given YAML against the predefined schema.
0.22.7 (2016-08-19)¶
- (Added) A new helper
coverage_from_trace
that returns coverage information (in absolute numbers) from a trace. - (Added) Parameter
fails_fast
(default isFalse
, behavior preserved) forExecutionWatcher.watch_with
methods. This parameter allows the watcher to raise anAssertionError
as soon as the added watcher reaches a final configuration. - (Changed)
StateMixin
,Transition
andEvent
’s__eq__
method returns aNotImplemented
object if the other object involved in the comparison is not an instance of the same class, meaning thatEvent('a') == 1
now raises aNotImplementedError
instead of beingFalse
.
0.22.6 (2016-08-03)¶
- (Changed)
Event
,MacroStep
,MicroStep
,StateMixin
,Transition
,Statechart
andInterpreter
’s__repr__
returns a valid Python expression. - (Changed) The context returned by a
PythonEvaluator
(and thus by the defaultInterpreter
) exhibits nested variables (the ones that are not defined in the preamble of a statechart). Those variables are prefixed by the name of the state in which they are declared, to avoid name clashing. - (Changed) Context variables are sorted in exceptions’
.__str__
methods.
0.22.4 (2016-07-08)¶
- (Added)
sismic-behave
CLI now accepts a--steps
parameter, which is a list of file paths containing the steps implementation. - (Added)
sismic-behave
CLI now accepts a--show-steps
parameter, which list the steps (equivalent to Behave’s overriden--steps
parameter). - (Added)
sismic-behave
now returns an appropriate exit code. - (Changed) Reorganisation of
docs/examples
. - (Fixed) Coverage data for
sismic-behave
takes the initialization step into account (regression introduced in 0.21.0).
0.22.3 (2016-07-06)¶
- (Added)
sent
andreceived
are also available in preconditions and postconditions.
0.22.2 (2016-07-01)¶
- (Added)
model.Event
is now correctly pickled, meaning that Sismic can be used in a multiprocessing environment.
0.22.1 (2016-06-29)¶
- (Added) A event {event_name} should not be fired steps for BDD.
- (Added) Both
MicroStep
andMacroStep
have a listsent_events
of events that were sent during the step. - (Added) Property statecharts receive a
event sent
event when an event is sent by the statechart under test. - (Changed) Events fired from within the statechart are now collected and sent at the end of the current micro step, instead of being immediately sent.
- (Changed) Invariants and sequential contracts are now evaluated ordered by their state’s depth
0.22.0 (2016-06-13)¶
- (Added) Support for sequential conditions in contracts (see documentation for more information).
- (Added) Python code evaluator: after and idle are now available in postconditions and invariants.
- (Added) Python code evaluator: received and sent are available in invariants.
- (Added) An
Evaluator
has now aon_step_starts
method which is called at the beginning of each step, with the current event (if any) being processed. - (Added)
Interpreter.raise_event
to send events from within the statechart. - (Added) A
copy_from_statechart
method for aStatechart
instance that allows to copy (part of) a statechart into a state. - (Added) Microwave controller example (see docs/examples/microwave.[yaml|py]).
- (Changed) Events sent by a code evaluator are now returned by the
execute_*
methods instead of being automatically added to the interpreter’s queue. - (Changed) Moved
run_in_background
andlog_trace
fromsismic.interpreter
to the newly addedsismic.interpreter.helpers
. - (Changed) Internal API changes: rename
self.__x
toself._x
to avoid (mostly) useless name mangling.
0.21.0 (2016-04-22)¶
Changes for interpreter.Interpreter
class:
- (Removed)
_select_eventless_transition
which is a special case of_select_transition
. - (Added)
_select_event
, extracted fromexecute_once
. - (Added)
_filter_transitions
, extracted from_select_transition
. - (Changed)
_execute_step
is now_apply_step
. - (Changed)
_compute_stabilization_step
is now_create_stabilization_step
and accepts a list of state names - (Changed)
_compute_transitions_step
is now_create_steps
. - (Changed) Except for the
statechart
parameter, all the parameters forInterpreter
’s constructor can now be only provided by name. - (Fixed) Contracts on a transition are checked (if not explicitly disabled) even if the transition has no action.
- (Fixed)
Evaluator.execute_action
is called even if the transition has no action. - (Fixed) States are added/removed from the active configuration as soon as they are entered/exited.
Previously, the configuration was only updated at the end of the step (and could possibly lead to inaccurate results
when using
active(name)
in aPythonEvaluator
).
The default PythonEvaluator
class has been completely rewritten:
- (Changed) Code contained in states and/or transitions is now executed with a local context instead of a global one. The local context of a state is built upon the local context of its parent, and so one until the local context of the statechart is reached. This should facilitate the use of dummy variables in nested states and transitions.
- (Changed) The code is now compiled (once) before is evaluation/execution. This should increase performance.
- (Changed) The frozen context of a state (ie.
__old__
) is now computed only if contracts are checked, and only if at least one invariant or one postcondition exists. - (Changed) The
initial_context
parameter ofEvaluator
’s constructor can now only be provided by name. - (Changed) The
additional_context
parameter ofEvaluator._evaluate_code
andEvaluator._execute_code
can now only be provided by name.
Miscellaneous:
- (Fixed) Step I load the statechart now executes (once) the statechart in order to put it into a stable initial configuration (regression introduced in 0.20.0).
0.20.5 (2016-04-14)¶
- (Added) Type hinting (see PEP484 and mypy-lang project)
0.20.4 (2016-03-25)¶
- (Changed) Statechart testers are now called property statechart.
- (Changed) Property statechart can describe desirable and undesirable properties.
0.20.3 (2016-03-22)¶
- (Changed) Step Event x should be fired now checks sent events from the beginning of the test, not only for the last executed step.
- (Fixed) Internal events that are sequentially sent are now sequentially consumed (and not anymore in reverse order).
0.20.2 (2016-02-24)¶
- (Fixed)
interpreter.log_trace
does not anymore log empty macro step.
0.20.1 (2016-02-19)¶
- (Added) A step ended event at the end of each step in a tester story.
- (Changed) The name of the events and attributes that are exposed in a tester story has changed. Consult the documentation for more information.
0.20.0 (2016-02-17)¶
- (Added) Module
interpreter
provides alog_trace
function that takes an interpreter instance and returns a (dynamic) list of executed macro steps. - (Added) Module
testing
exposes anExecutionWatcher
class that can be used to check statechart properties with tester statecharts at runtime. - (Changed)
Interpreter.__init__
does not anymore stabilize the statechart. Stabilization is done during the first call ofexecute_once
. - (Changed)
Story.tell
returns a list ofMacroStep
(the trace) instead of anInterpreter
instance. - (Changed) The name of some attributes of an event in a tester story changes (e.g. event becomes consumed_event, state becomes entered_state or exited_state or source_state or target_state).
- (Removed)
Interpreter.trace
, as it can be easily obtained fromexecute_once
or usinglog_trace
. - (Removed)
Interpreter.__init__
does not accept aninitial_time
parameter. - (Fixed) Parallel state without children does not any more result into an infinite loop.
0.19.0 (2016-02-10)¶
- (Added) BDD can now output coverage data using
--coverage
command-line argument. - (Changed) The YAML definition of a statechart must use root state: instead of initial state:.
- (Changed) When a contract is evaluated by a
PythonEvaluator
,__old__.x
raises anAttributeError
instead of aKeyError
ifx
does not exist. - (Changed) Behave is now called from Python instead of using a subprocess and thus allows debugging.
0.18.1 (2016-02-03)¶
- (Added) Support for behavior-driven-development using Behave.
0.17.3 (2016-01-29)¶
- (Added) An
io.text.export_to_tree
that returns a textual representation of the states. - (Changed)
Statechart.rename_to
does not anymore raiseKeyError
butexceptions.StatechartError
. - (Changed) Wheel build should work on Windows
0.17.1 (2016-01-25)¶
Many backward incompatible changes in this update, especially if you used to work with model
.
The YAML format of a statechart also changed, look carefully at the changelog and the documentation.
- (Added) YAML: an history state can declare on entry and on exit.
- (Added) Statechart: new methods to manipulate transitions:
transitions_from
,transitions_to
,transitions_with
,remove_transition
androtate_transition
. - (Added) Statechart: new methods to manipulate states:
remove_state
,rename_state
,move_state
,state_for
,parent_for
,children_for
. - (Added) Steps:
__eq__
forMacroStep
andMicroStep
. - (Added) Stories:
tell_by_step
method for aStory
. - (Added) Testing:
teststory_from_trace
generates a step event at the beginning of each step. - (Added) Module: a new exceptions hierarchy (see
exceptions
module). The new exceptions are used in place of the old ones (Warning
,AssertionError
andValueError
). - (Changed) YAML: uppermost states: should be replaced by initial state: and can contain at most one state.
- (Changed) YAML: uppermost on entry: should be replaced by preamble:.
- (Changed) YAML: initial memory of an history state should be specified using memory instead of initial.
- (Changed) YAML: contracts for a statechart must be declared on its root state.
- (Changed) Statechart: rename
StateChart
toStatechart
. - (Changed) Statechart: rename
events
toevents_for
. - (Changed) Statechart:
states
attribute is nowStatechart.state_for
method. - (Changed) Statechart:
register_state
is nowadd_state
. - (Changed) Statechart:
register_transition
is nowadd_transition
. - (Changed) Statechart: now defines a root state.
- (Changed) Statechart: checks done in
validate
. - (Changed) Transition:
.event
is a string instead of anEvent
instance. - (Changed) Transition: attributes
from_state
andto_state
are renamed intosource
andtarget
. - (Changed) Event:
__eq__
takesdata
attribute into account. - (Changed) Event:
event.foo
raises anAttributeError
instead of aKeyError
iffoo
is not defined. - (Changed) State:
StateMixin.name
is now read-only (useStatechart.rename_state
). - (Changed) State: split
HistoryState
into a mixinHistoryStateMixin
and two concrete subclasses, namelyShallowHistoryState
andDeepHistoryState
. - (Changed) IO: Complete rewrite of
io.import_from_yaml
to load states before transitions. Parameter names have changed. - (Changed) Module: adapt module hierarchy (no visible API change).
- (Changed) Module: expose module content through
__all__
. - (Removed) Transition:
transitions
attribute onTransitionStateMixin
, useStatechart.transitions_for
instead. - (Removed) State:
CompositeStateMixin.children
, useStatechart.children_for
instead.
0.16.0 (2016-01-15)¶
- (Added) An
InternalEvent
subclass formodel.Event
. - (Added)
Interpreter
now exposes itsstatechart
. - (Added)
Statechart.validate
checks that a targeted compound state declares an initial state. - (Changed)
Interpreter.queue
does not accept anymore aninternal
parameter. Use an instance ofInternalEvent
instead (#20). - (Fixed)
Story.story_from_trace
now ignores internal events (#19). - (Fixed) Condition C3 in
Statechart.validate
.
0.15.0 (2016-01-12)¶
- (Changed) Rename
Interpreter.send
toInterpreter.queue
(#18). - (Changed) Rename
evaluator
module tocode
.
0.14.3 (2016-01-12)¶
- (Added) Changelog.
- (Fixed) Missing files in MANIFEST.in
API Reference¶
Module bdd¶
-
sismic.bdd.
execute_bdd
(statechart, feature_filepaths, *, step_filepaths=None, property_statecharts=None, interpreter_klass=<class 'sismic.interpreter.default.Interpreter'>, debug_on_error=False, behave_parameters=None)¶ Execute BDD tests for a statechart.
Parameters: - statechart (
Statechart
) – statechart to test - feature_filepaths (
List
[str
]) – list of filepaths to feature files. - step_filepaths (
Optional
[List
[str
]]) – list of filepaths to step definitions. - property_statecharts (
Optional
[List
[Statechart
]]) – list of property statecharts - interpreter_klass (
Callable
[[Statechart
],Interpreter
]) – a callable that accepts a statechart and an optional clock and returns an Interpreter - debug_on_error (
bool
) – set to True to drop to (i)pdb in case of error. - behave_parameters (
Optional
[List
[str
]]) – additional CLI parameters used by Behave (see http://behave.readthedocs.io/en/latest/behave.html#command-line-arguments)
Return type: Returns: exit code of behave CLI.
- statechart (
-
sismic.bdd.
map_action
(step_text, existing_step_or_steps)¶ Map new “given”/”when” steps to one or many existing one(s). Parameters are propagated to the original step(s) as well, as expected.
Examples:
- map_action(‘I open door’, ‘I send event open_door’)
- map_action(‘Event {name} has to be sent’, ‘I send event {name}’)
- map_action(‘I do two things’, [‘First thing to do’, ‘Second thing to do’])
Parameters: Return type: None
-
sismic.bdd.
map_assertion
(step_text, existing_step_or_steps)¶ Map a new “then” step to one or many existing one(s). Parameters are propagated to the original step(s) as well, as expected.
map_assertion(‘door is open’, ‘state door open is active’) map_assertion(‘{x} seconds elapsed’, ‘I wait for {x} seconds’) map_assertion(‘assert two things’, [‘first thing to assert’, ‘second thing to assert’])
Parameters: Return type: None
Module clock¶
-
class
sismic.clock.
Clock
¶ Bases:
object
Abstract implementation of a clock, as used by an interpreter.
The purpose of a clock instance is to provide a way for the interpreter to get the current time during the execution of a statechart.
-
class
sismic.clock.
SimulatedClock
¶ Bases:
sismic.clock.clock.Clock
A simulated clock, starting from 0, that can be manually or automatically incremented.
Manual incrementation can be done by setting a new value to the time attribute. Automatic incrementation occurs when start() is called, until stop() is called. In that case, clock speed can be adjusted with the speed attribute. A value strictly greater than 1 increases clock speed while a value strictly lower than 1 slows down the clock.
-
start
()¶ Clock will be automatically updated both based on real time and its speed attribute.
Return type: None
-
stop
()¶ Clock won’t be automatically updated.
Return type: None
-
-
class
sismic.clock.
UtcClock
¶ Bases:
sismic.clock.clock.Clock
A clock that simulates a wall clock in UTC.
The returned time value is based on Python time.time() function.
-
class
sismic.clock.
SynchronizedClock
(interpreter)¶ Bases:
sismic.clock.clock.Clock
A clock that is synchronized with a given interpreter.
The synchronization is based on the interpreter’s internal time value, not on its clock. As a consequence, the time value of a SynchronizedClock only changes when the underlying interpreter is executed.
Parameters: interpreter – an interpreter instance
Module code¶
-
class
sismic.code.
Evaluator
(interpreter=None, *, initial_context=None)¶ Bases:
object
Abstract base class for any evaluator.
An instance of this class defines what can be done with piece of codes contained in a statechart (condition, action, etc.).
Notice that the execute_* methods are called at each step, even if there is no code to execute. This allows the evaluator to keep track of the states that are entered or exited, and of the transitions that are processed.
Parameters: - interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
- initial_context (
Optional
[Mapping
[str
,Any
]]) – an optional dictionary to populate the context
-
context
¶ The context of this evaluator. A context is a dict-like mapping between variables and values that is expected to be exposed when the code is evaluated.
Return type: Mapping
[str
,Any
]
-
execute_statechart
(statechart)¶ Execute the initial code of a statechart. This method is called at the very beginning of the execution.
Parameters: statechart ( Statechart
) – statechart to consider
-
evaluate_guard
(transition, event=None)¶ Evaluate the guard for given transition.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: Optional
[bool
]Returns: truth value of code
- transition (
-
execute_action
(transition, event=None)¶ Execute the action for given transition. This method is called for every transition that is processed, even those with no action.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: List
[Event
]Returns: a list of sent events
- transition (
-
execute_on_entry
(state)¶ Execute the on entry action for given state. This method is called for every state that is entered, even those with no on_entry.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_on_exit
(state)¶ Execute the on exit action for given state. This method is called for every state that is exited, even those with no on_exit.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
evaluate_preconditions
(obj, event=None)¶ Evaluate the preconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_invariants
(obj, event=None)¶ Evaluate the invariants for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_postconditions
(obj, event=None)¶ Evaluate the postconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
class
sismic.code.
DummyEvaluator
(interpreter=None, *, initial_context=None)¶ Bases:
sismic.code.evaluator.Evaluator
A dummy evaluator that does nothing and evaluates every condition to True.
-
context
¶ The context of this evaluator. A context is a dict-like mapping between variables and values that is expected to be exposed when the code is evaluated.
-
evaluate_guard
(transition, event=None)¶ Evaluate the guard for given transition.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: Optional
[bool
]Returns: truth value of code
- transition (
-
evaluate_invariants
(obj, event=None)¶ Evaluate the invariants for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_postconditions
(obj, event=None)¶ Evaluate the postconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
evaluate_preconditions
(obj, event=None)¶ Evaluate the preconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterable
[str
]Returns: list of unsatisfied conditions
-
execute_action
(transition, event=None)¶ Execute the action for given transition. This method is called for every transition that is processed, even those with no action.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: List
[Event
]Returns: a list of sent events
- transition (
-
execute_on_entry
(state)¶ Execute the on entry action for given state. This method is called for every state that is entered, even those with no on_entry.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_on_exit
(state)¶ Execute the on exit action for given state. This method is called for every state that is exited, even those with no on_exit.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_statechart
(statechart)¶ Execute the initial code of a statechart. This method is called at the very beginning of the execution.
Parameters: statechart ( Statechart
) – statechart to consider
-
-
class
sismic.code.
PythonEvaluator
(interpreter=None, *, initial_context=None)¶ Bases:
sismic.code.evaluator.Evaluator
A code evaluator that understands Python.
This evaluator exposes some additional functions/variables:
- On both code execution and code evaluation:
- A time: float value that represents the current time exposed by interpreter clock.
- An active(name: str) -> bool Boolean function that takes a state name and return True if and only
if this state is currently active, ie. it is in the active configuration of the
Interpreter
instance that makes use of this evaluator.
- On code execution:
- A send(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises an internal event with it. Raised events are propagated to bound statecharts as external events and to the current statechart as internal event. If delay is provided, a delayed event is created.
- A notify(name: str, **kwargs) -> None function that takes an event name and additional keyword parameters and raises a meta-event with it. Meta-events are only sent to bound property statecharts.
- If the code is related to a transition, the event: Event that fires the transition is exposed.
- A setdefault(name:str, value: Any) -> Any function that defines and returns variable name in the global scope if it is not yet defined.
- On guard or contract evaluation:
- If the code is related to a transition, an event: Optional[Event] variable is exposed. This variable contains the currently considered event, or None.
- On guard or contract (except preconditions) evaluation:
- An after(sec: float) -> bool Boolean function that returns True if and only if the source state was entered more than sec seconds ago. The time is evaluated according to Interpreter’s clock.
- A idle(sec: float) -> bool Boolean function that returns True if and only if the source state did not fire a transition for more than sec ago. The time is evaluated according to Interpreter’s clock.
- On contract (except preconditions) evaluation:
- A variable __old__ that has an attribute x for every x in the context when either the state was entered (if the condition involves a state) or the transition was processed (if the condition involves a transition). The value of __old__.x is a shallow copy of x at that time.
- On contract evaluation:
- A sent(name: str) -> bool function that takes an event name and return True if an event with the same name was sent during the current step.
- A received(name: str) -> bool function that takes an event name and return True if an event with the same name is currently processed in this step.
If an exception occurred while executing or evaluating a piece of code, it is propagated by the evaluator.
Parameters: - interpreter – the interpreter that will use this evaluator, is expected to be an Interpreter instance
- initial_context (
Optional
[Mapping
[str
,Any
]]) – a dictionary that will be used as __locals__
-
context
¶ The context of this evaluator. A context is a dict-like mapping between variables and values that is expected to be exposed when the code is evaluated.
Return type: Mapping
-
evaluate_guard
(transition, event=None)¶ Evaluate the guard for given transition.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: Returns: truth value of code
- transition (
-
evaluate_preconditions
(obj, event=None)¶ Evaluate the preconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterator
[str
]Returns: list of unsatisfied conditions
-
evaluate_invariants
(obj, event=None)¶ Evaluate the invariants for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterator
[str
]Returns: list of unsatisfied conditions
-
evaluate_postconditions
(obj, event=None)¶ Evaluate the postconditions for given object (either a StateMixin or a Transition) and return a list of conditions that are not satisfied.
Parameters: - obj – the considered state or transition
- event (
Optional
[Event
]) – an optional Event instance, if any
Return type: Iterator
[str
]Returns: list of unsatisfied conditions
-
execute_action
(transition, event=None)¶ Execute the action for given transition. This method is called for every transition that is processed, even those with no action.
Parameters: - transition (
Transition
) – the considered transition - event (
Optional
[Event
]) – instance of Event if any
Return type: List
[Event
]Returns: a list of sent events
- transition (
-
execute_on_entry
(state)¶ Execute the on entry action for given state. This method is called for every state that is entered, even those with no on_entry.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_on_exit
(state)¶ Execute the on exit action for given state. This method is called for every state that is exited, even those with no on_exit.
Parameters: state ( StateMixin
) – the considered stateReturn type: List
[Event
]Returns: a list of sent events
-
execute_statechart
(statechart)¶ Execute the initial code of a statechart. This method is called at the very beginning of the execution.
Parameters: statechart ( Statechart
) – statechart to consider
Module exceptions¶
-
exception
sismic.exceptions.
SismicError
¶ Bases:
Exception
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
StatechartError
¶ Bases:
sismic.exceptions.SismicError
Base error for anything that is related to a statechart.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
CodeEvaluationError
¶ Bases:
sismic.exceptions.SismicError
Base error for anything related to the evaluation of the code contained in a statechart.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
ExecutionError
¶ Bases:
sismic.exceptions.SismicError
Base error for anything related to the execution of a statechart.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
ConflictingTransitionsError
¶ Bases:
sismic.exceptions.ExecutionError
When multiple conflicting (parallel) transitions can be processed at the same time.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
NonDeterminismError
¶ Bases:
sismic.exceptions.ExecutionError
In case of non-determinism.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
PropertyStatechartError
(property_statechart)¶ Bases:
sismic.exceptions.SismicError
Raised when a property statechart reaches a final state.
Parameters: property_statechart – the property statechart that reaches a final state -
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
ContractError
(configuration=None, step=None, obj=None, assertion=None, context=None)¶ Bases:
sismic.exceptions.SismicError
Base exception for situations in which a contract is not satisfied. All the parameters are optional, and are exposed to ease debug.
Parameters: - configuration – list of active states
- step – a MicroStep or MacroStep instance.
- obj – the object that is concerned by the assertion
- assertion – the assertion that failed
- context – the context in which the condition failed
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
exception
sismic.exceptions.
PreconditionError
(configuration=None, step=None, obj=None, assertion=None, context=None)¶ Bases:
sismic.exceptions.ContractError
A precondition is not satisfied.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
PostconditionError
(configuration=None, step=None, obj=None, assertion=None, context=None)¶ Bases:
sismic.exceptions.ContractError
A postcondition is not satisfied.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
-
exception
sismic.exceptions.
InvariantError
(configuration=None, step=None, obj=None, assertion=None, context=None)¶ Bases:
sismic.exceptions.ContractError
An invariant is not satisfied.
-
with_traceback
()¶ Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.
-
Module helpers¶
-
sismic.helpers.
log_trace
(interpreter)¶ Return a list that will be populated by each value returned by the execute_once method of given interpreter.
Parameters: interpreter ( Interpreter
) – an Interpreter instanceReturn type: List
[MacroStep
]Returns: a list of MacroStep
-
sismic.helpers.
coverage_from_trace
(trace)¶ Given a list of macro steps considered as the trace of a statechart execution, return Counter objects that counts the states that were entered, the states that were exited and the transitions that were processed.
Parameters: trace ( List
[MacroStep
]) – A list of macro stepsReturn type: Mapping
[str
,Counter
]Returns: A dict whose keys are “entered states”, “exited states” and “processed transitions” and whose values are Counter object.
-
sismic.helpers.
run_in_background
(interpreter, delay=0.05, callback=None)¶ Run given interpreter in background. The interpreter is ran until it reaches a final configuration. You can manually stop the thread using the added stop of the returned Thread object. This is for convenience only and should be avoided, because a call to stop puts the interpreter in an empty (and thus final) configuration, without properly leaving the active states.
Parameters: - interpreter (
Interpreter
) – an interpreter - delay (
float
) – delay between each call to execute() - callback (
Optional
[Callable
[[List
[MacroStep
]],Any
]]) – a function that accepts the result of execute.
Return type: Returns: started thread (instance of threading.Thread)
Deprecated: since 1.3.0, use runner.AsyncRunner instead.
- interpreter (
Module interpreter¶
-
class
sismic.interpreter.
Interpreter
(statechart, *, evaluator_klass=<class 'sismic.code.python.PythonEvaluator'>, initial_context=None, clock=None, ignore_contract=False)¶ Bases:
object
A discrete interpreter that executes a statechart according to a semantic close to SCXML (eventless transitions first, inner-first/source state semantics).
Parameters: - statechart (
Statechart
) – statechart to interpret - evaluator_klass (
Callable
[…,Evaluator
]) – An optional callable (e.g. a class) that takes an interpreter and an optional initial context as input and returns an Evaluator instance that will be used to initialize the interpreter. By default, the PythonEvaluator class will be used. - initial_context (
Optional
[Mapping
[str
,Any
]]) – an optional initial context that will be provided to the evaluator. By default, an empty context is provided - clock (
Optional
[Clock
]) – A BaseClock instance that will be used to set this interpreter internal time. By default, a SimulatedClock is used. - ignore_contract (
bool
) – set to True to ignore contract checking during the execution.
-
configuration
¶ List of active states names, ordered by depth. Ties are broken according to the lexicographic order on the state name.
Return type: List
[str
]
-
statechart
¶ Embedded statechart
Return type: Statechart
-
attach
(listener)¶ Attach given listener to the current interpreter.
The listener is called each time a meta-event is emitted by current interpreter. Emitted meta-events are:
- step started: when a (possibly empty) macro step starts. The current time of the step is available through the
time
attribute. - step ended: when a (possibly empty) macro step ends.
- event consumed: when an event is consumed. The consumed event is exposed through the
event
attribute. - event sent: when an event is sent. The sent event is exposed through the
event
attribute. - state exited: when a state is exited. The exited state is exposed through the
state
attribute. - state entered: when a state is entered. The entered state is exposed through the
state
attribute. - transition processed: when a transition is processed. The source state, target state and the event are
exposed respectively through the
source
,target
andevent
attribute. - Every meta-event that is sent from within the statechart.
This is a low-level interface for
self.bind
andself.bind_property_statechart
.Consult
sismic.interpreter.listener
for common listeners/wrappers.Parameters: listener ( Callable
[[MetaEvent
],Any
]) – A callable that accepts meta-event instances.Return type: None
- step started: when a (possibly empty) macro step starts. The current time of the step is available through the
-
detach
(listener)¶ Remove given listener from the ones that are currently attached to this interpreter.
Parameters: listener ( Callable
[[MetaEvent
],Any
]) – A previously attached listener.Return type: None
-
bind
(interpreter_or_callable)¶ Bind an interpreter (or a callable) to the current interpreter.
Internal events sent by this interpreter will be propagated as external events. If interpreter_or_callable is an Interpreter instance, its queue method is called. This is, if i1 and i2 are interpreters, i1.bind(i2) is equivalent to i1.bind(i2.queue).
This method is a higher-level interface for
self.attach
. Ifx = interpreter.bind(...)
, useinterpreter.detach(x)
to unbind a previously bound interpreter.Parameters: interpreter_or_callable ( Union
[Interpreter
,Callable
[[Event
],Any
]]) – interpreter or callable to bind.Return type: Callable
[[MetaEvent
],Any
]Returns: the resulting attached listener.
-
bind_property_statechart
(statechart, *, interpreter_klass=None)¶ Bind a property statechart to the current interpreter.
A property statechart receives meta-events from the current interpreter depending on what happens. See
attach
method for a full list of meta-events.The internal clock of all property statecharts is synced with the one of the current interpreter. As soon as a property statechart reaches a final state, a
PropertyStatechartError
will be raised, meaning that the property expressed by the corresponding property statechart is not satisfied. Property statecharts are automatically executed when they are bound to an interpreter.Since Sismic 1.4.0: passing an interpreter as first argument is deprecated.
This method is a higher-level interface for
self.attach
. Ifx = interpreter.bind_property_statechart(...)
, useinterpreter.detach(x)
to unbind a previously bound property statechart.Parameters: - statechart (
Statechart
) – A statechart instance. - interpreter_klass (
Optional
[Callable
]) – An optional callable that accepts a statechart as first parameter and a named parameter clock. Default to Interpreter.
Return type: Callable
[[MetaEvent
],Any
]Returns: the resulting attached listener.
- statechart (
-
queue
(event_or_name, *event_or_names, **parameters)¶ Create and queue given events to the external event queue.
If an event has a delay parameter, it will be processed by the first call to execute_once as soon as self.clock.time exceeds current self.time + event.delay.
If named parameters are provided, they will be added to all events that are provided by name.
Parameters: Return type: Interpreter
Returns: self so it can be chained.
-
execute
(max_steps=-1)¶ Repeatedly calls execute_once and return a list containing the returned values of execute_once.
Notice that this does NOT return an iterator but computes the whole list first before returning it.
Parameters: max_steps ( int
) – An upper bound on the number steps that are computed and returned. Default is -1, no limit. Set to a positive integer to avoid infinite loops in the statechart execution.Return type: List
[MacroStep
]Returns: A list of MacroStep instances
-
execute_once
()¶ Select transitions that can be fired based on available queued events, process them and stabilize the interpreter. When multiple transitions are selected, they are atomically processed: states are exited, transition is processed, states are entered, statechart is stabilized and only after that, the next transition is processed.
Return type: Optional
[MacroStep
]Returns: a macro step or None if nothing happened
- statechart (
-
class
sismic.interpreter.
Event
(name, **additional_parameters)¶ Bases:
object
An event with a name and (optionally) some data passed as named parameters.
The list of parameters can be obtained using dir(event). Notice that name and data are reserved names. If a delay parameter is provided, then this event will be considered as a delayed event (and won’t be executed until given delay has elapsed).
When two events are compared, they are considered equal if their names and their data are equal.
Parameters: - name (
str
) – name of the event. - data – additional data passed as named parameters.
- name (
-
class
sismic.interpreter.
InternalEvent
(name, **additional_parameters)¶ Bases:
sismic.model.events.Event
Subclass of Event that represents an internal event.
-
class
sismic.interpreter.
MetaEvent
(name, **additional_parameters)¶ Bases:
sismic.model.events.Event
Subclass of Event that represents a MetaEvent, as used in property statecharts.
Module io¶
-
sismic.io.
import_from_yaml
(text=None, filepath=None, *, ignore_schema=False, ignore_validation=False)¶ Import a statechart from a YAML representation (first argument) or a YAML file (filepath argument).
Unless specified, the structure contained in the YAML is validated against a predefined schema (see sismic.io.SCHEMA), and the resulting statechart is validated using its validate() method.
Parameters: Return type: Statechart
Returns: a Statechart instance
-
sismic.io.
export_to_yaml
(statechart, filepath=None)¶ Export given Statechart instance to YAML. Its YAML representation is returned by this function. Automatically save the output to filepath, if provided.
Parameters: - statechart (
Statechart
) – statechart to export - filepath (
Optional
[str
]) – save output to given filepath, if provided
Return type: Returns: A textual YAML representation
- statechart (
-
sismic.io.
export_to_plantuml
(statechart, filepath=None, *, based_on=None, based_on_filepath=None, statechart_name=True, statechart_description=False, statechart_preamble=False, state_contracts=False, state_action=True, transition_contracts=False, transition_action=True)¶ Export given statechart to plantUML (see http://plantuml/plantuml). If a filepath is provided, also save the output to this file.
Due to the way statecharts are representing, and due to the presence of features that are specific to Sismic, the resulting statechart representation does not include all the informations. For example, final states and history states won’t have name, actions and contracts.
If a previously exported representation for the statechart is provided, either as text (based_on parameter) or as a filepath (based_on_filepath parameter), it will attempt to reuse the modifications made to the transitions (their direction and length).
Parameters: - statechart (
Statechart
) – statechart to export - filepath (
Optional
[str
]) – save output to given filepath, if provided - based_on (
Optional
[str
]) – existing representation of the statechart in PlantUML - based_on_filepath (
Optional
[str
]) – filepath to an existing representation of the statechart in PlantUML - statechart_name (
bool
) – include the name of the statechart - statechart_description (
bool
) – include the description of the statechart - statechart_preamble (
bool
) – include the preamble of the statechart - state_contracts (
bool
) – include state contracts - state_action (
bool
) – include state actions (on entry, on exit and internal transitions) - transition_contracts (
bool
) – include transition contracts - transition_action (
bool
) – include actions on transition
Return type: Returns: textual representation using plantuml
- statechart (
Module model¶
-
class
sismic.model.
ActionStateMixin
(on_entry=None, on_exit=None)¶ Bases:
object
State that can define actions on entry and on exit.
Parameters:
-
class
sismic.model.
BasicState
(name, on_entry=None, on_exit=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
,sismic.model.elements.TransitionStateMixin
A basic state, with a name, transitions, actions, etc. but no child state.
Parameters:
-
class
sismic.model.
CompoundState
(name, initial=None, on_entry=None, on_exit=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
,sismic.model.elements.TransitionStateMixin
,sismic.model.elements.CompositeStateMixin
Compound states must have children states.
Parameters:
-
class
sismic.model.
ContractMixin
¶ Bases:
object
Mixin with a contract: preconditions, postconditions and invariants.
-
class
sismic.model.
DeepHistoryState
(name, on_entry=None, on_exit=None, memory=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
,sismic.model.elements.HistoryStateMixin
A deep history state resumes the execution of its parent, and of every nested active states in its parent.
Parameters:
-
class
sismic.model.
Event
(name, **additional_parameters)¶ Bases:
object
An event with a name and (optionally) some data passed as named parameters.
The list of parameters can be obtained using dir(event). Notice that name and data are reserved names. If a delay parameter is provided, then this event will be considered as a delayed event (and won’t be executed until given delay has elapsed).
When two events are compared, they are considered equal if their names and their data are equal.
Parameters: - name (
str
) – name of the event. - data – additional data passed as named parameters.
- name (
-
class
sismic.model.
FinalState
(name, on_entry=None, on_exit=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
Final state has NO transition and is used to detect state machine termination.
Parameters:
-
class
sismic.model.
HistoryStateMixin
(memory=None)¶ Bases:
object
History state has a memory that can be resumed.
Parameters: memory ( Optional
[str
]) – name of the initial state
-
class
sismic.model.
InternalEvent
(name, **additional_parameters)¶ Bases:
sismic.model.events.Event
Subclass of Event that represents an internal event.
-
class
sismic.model.
MacroStep
(time, steps)¶ Bases:
object
A macro step is a list of micro steps.
Parameters: - time (
float
) – the time at which this step was executed - steps (
List
[MicroStep
]) – a list of MicroStep instances
-
steps
¶ List of micro steps
Return type: List
[MicroStep
]
-
event
¶ Event (or None) that was consumed.
Return type: Optional
[Event
]
-
transitions
¶ A (possibly empty) list of transitions that were triggered.
Return type: List
[Transition
]
-
sent_events
¶ List of events that were sent during this step.
Return type: List
[Event
]
- time (
-
class
sismic.model.
MetaEvent
(name, **additional_parameters)¶ Bases:
sismic.model.events.Event
Subclass of Event that represents a MetaEvent, as used in property statecharts.
-
class
sismic.model.
MicroStep
(event=None, transition=None, entered_states=None, exited_states=None, sent_events=None)¶ Bases:
object
Create a micro step.
A step consider event, takes a transition and results in a list of entered_states and a list of exited_states. Order in the two lists is REALLY important!
Parameters: - event (
Optional
[Event
]) – Event or None in case of eventless transition - transition (
Optional
[Transition
]) – a Transition or None if no processed transition - entered_states (
Optional
[List
[str
]]) – possibly empty list of entered states - exited_states (
Optional
[List
[str
]]) – possibly empty list of exited states - sent_events (
Optional
[List
[Event
]]) – a possibly empty list of events that are sent during the step
- event (
-
class
sismic.model.
OrthogonalState
(name, on_entry=None, on_exit=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
,sismic.model.elements.TransitionStateMixin
,sismic.model.elements.CompositeStateMixin
Orthogonal states run their children simultaneously.
Parameters:
-
class
sismic.model.
ShallowHistoryState
(name, on_entry=None, on_exit=None, memory=None)¶ Bases:
sismic.model.elements.ContractMixin
,sismic.model.elements.StateMixin
,sismic.model.elements.ActionStateMixin
,sismic.model.elements.HistoryStateMixin
A shallow history state resumes the execution of its parent. It activates the latest visited state of its parent.
Parameters:
-
class
sismic.model.
StateMixin
(name)¶ Bases:
object
State element with a name.
Parameters: name ( str
) – name of the state
-
class
sismic.model.
Statechart
(name, description=None, preamble=None)¶ Bases:
object
Python structure for a statechart
Parameters: -
preamble
¶ Preamble code
-
states
¶ List of state names in lexicographic order.
-
state_for
(name)¶ Return the state instance that has given name.
Parameters: name ( str
) – a state nameReturn type: StateMixin
Returns: a StateMixin that has the same name or None Raises: StatechartError – if state does not exist
-
parent_for
(name)¶ Return the name of the parent of given state name.
Parameters: name ( str
) – a state nameReturn type: Optional
[str
]Returns: its parent name, or None. Raises: StatechartError – if state does not exist
-
children_for
(name)¶ Return the names of the children of the given state.
Parameters: name ( str
) – a state nameReturn type: List
[str
]Returns: a (possibly empty) list of children Raises: StatechartError – if state does not exist
-
ancestors_for
(name)¶ Return an ordered list of ancestors for the given state. Ancestors are ordered by decreasing depth.
Parameters: name ( str
) – name of the stateReturn type: List
[str
]Returns: state’s ancestors Raises: StatechartError – if state does not exist
-
descendants_for
(name)¶ Return an ordered list of descendants for the given state. Descendants are ordered by increasing depth.
Parameters: name ( str
) – name of the stateReturn type: List
[str
]Returns: state’s descendants Raises: StatechartError – if state does not exist
-
depth_for
(name)¶ Return the depth of given state (1-indexed).
Parameters: name ( str
) – name of the stateReturn type: int
Returns: state depth Raises: StatechartError – if state does not exist
-
least_common_ancestor
(name_first, name_second)¶ Return the deepest common ancestor for s1 and s2, or None if there is no common ancestor except root (top-level) state.
Parameters: Return type: Optional
[str
]Returns: name of deepest common ancestor or None
Raises: StatechartError – if state does not exist
-
leaf_for
(names)¶ Return the leaves of names.
Considering the list of states names in names, return a list containing each element of names such that this element has no descendant in names.
Parameters: names ( Iterable
[str
]) – a list of state namesReturn type: List
[str
]Returns: the names of the leaves in names Raises: StatechartError – if a state does not exist
-
transitions
¶ List of available transitions
-
add_transition
(transition)¶ Register given transition and register it on the source state
Parameters: transition ( Transition
) – transition to addRaises: StatechartError – Return type: None
-
remove_transition
(transition)¶ Remove given transitions.
Parameters: transition ( Transition
) – a Transition instanceRaises: StatechartError – if transition is not registered Return type: None
-
rotate_transition
(transition, new_source='', new_target='')¶ Rotate given transition.
You MUST specify either new_source (a valid state name) or new_target (a valid state name or None) or both.
Parameters: Raises: StatechartError – if given transition or a given state does not exist.
Return type: None
-
transitions_from
(source)¶ Return the list of transitions whose source is given name.
Parameters: source ( str
) – name of source stateReturn type: List
[Transition
]Returns: a list of Transition instances Raises: StatechartError – if state does not exist
-
transitions_to
(target)¶ Return the list of transitions whose target is given name. Internal transitions are returned too.
Parameters: target ( str
) – name of target stateReturn type: List
[Transition
]Returns: a list of Transition instances Raises: StatechartError – if state does not exist
-
transitions_with
(event)¶ Return the list of transitions that can be triggered by given event name.
Parameters: event ( str
) – name of the eventReturn type: List
[Transition
]Returns: a list of Transition instances
-
events_for
(name_or_names=None)¶ Return a list containing the name of every event that guards a transition in this statechart.
If name_or_names is specified, it must be the name of a state (or a list of such names). Only transitions that have a source state from this list will be considered. By default, the list contains all the states.
Parameters: name_or_names ( Union
[str
,List
[str
],None
]) – None, a state name or a list of state names.Return type: List
[str
]Returns: A list of event names
-
add_state
(state, parent)¶ Add given state (a StateMixin instance) on given parent (its name as an str). If given state should be use as a root state, set parent to None.
Parameters: - state (
StateMixin
) – state to add - parent (
Optional
[str
]) – name of its parent, or None
Raises: Return type: None
- state (
-
remove_state
(name)¶ Remove given state.
The transitions that involve this state will also be removed. If the state is the target of an initial or memory property, their value will be set to None. If the state has children, they will be removed too.
Parameters: name ( str
) – name of a stateRaises: StatechartError – Return type: None
-
rename_state
(old_name, new_name)¶ Change state name, and adapt transitions, initial state, memory, etc.
Parameters: Return type: None
-
move_state
(name, new_parent)¶ Move given state (and its children) such that its new parent is new_parent.
Notice that a state cannot be moved inside itself or inside one of its descendants. If the state to move is the target of an initial or memory property of its parent, this property will be set to None. The same occurs if given state is an history state.
Parameters: Return type: None
-
copy_from_statechart
(statechart, *, source, replace, renaming_func=<function Statechart.<lambda>>)¶ Copy (a part of) given statechart into current one.
Copy source state, all its descendants and all involved transitions from statechart into current statechart. The source state will override replace state (but will be renamed to replace), and all its descendants in statechart will be copied into current statechart. All the transitions that are involved in the process must be fully contained in source state (ie. for all transition T: S->T, if S (resp. T) is a descendant-or-self of source, then T (resp. S) must be a descendant-or-self of source).
If necessary, callable renaming_func can be provided. This function should accept a (state) name and return a (new state) name. Use renaming_func to avoid conflicting names in target statechart.
Parameters: Return type: None
-
validate
()¶ Checks that every CompoundState’s initial state refer to one of its children Checks that every HistoryStateMixin’s memory refer to one of its parent’s children
Return type: bool
Returns: True Raises: StatechartError –
-
-
class
sismic.model.
Transition
(source, target=None, event=None, guard=None, action=None, priority=None)¶ Bases:
sismic.model.elements.ContractMixin
Represent a transition from a source state to a target state.
A transition can be eventless (no event) or internal (no target). A condition (code as string) can be specified as a guard.
Parameters: - source (
str
) – name of the source state - target (
Optional
[str
]) – name of the target state (if transition is not internal) - event (
Optional
[str
]) – event name (if any) - guard (
Optional
[str
]) – condition as code (if any) - action (
Optional
[str
]) – action as code (if any) - priority – priority (default to 0)
-
internal
¶ Boolean indicating whether this transition is an internal transition.
-
eventless
¶ Boolean indicating whether this transition is an eventless transition.
- source (
Module runner¶
-
class
sismic.runner.
AsyncRunner
(interpreter, interval=0.1, execute_all=False)¶ Bases:
object
An asynchronous runner that repeatedly execute given interpreter.
The runner tries to call its execute method every interval seconds, assuming that a call to that method takes less time than interval. If not, subsequent call is queued and will occur as soon as possible with no delay. The runner stops as soon as the underlying interpreter reaches a final configuration.
The execution must be started with the start method, and can be (definitively) stopped with the stop method. An execution can be temporarily suspended using the pause and unpause methods. A call to wait blocks until the statechart reaches a final configuration.
The current state of a runner can be obtained using its running and paused properties.
While this runner can be used “as is”, it is designed to be subclassed and as such, proposes several hooks to control the execution and additional behaviours:
- before_run: called (only once !) when the runner is started. By default, do nothing.
- after_run: called (only once !) when the interpreter reaches a final configuration. configuration of the underlying interpreter is reached. By default, do nothing.
- execute: called at each step of the run. By default, call the execute_once method of the underlying interpreter and returns a list of macro steps.
- before_execute: called right before the call to execute(). By default, do nothing.
- after_execute: called right after the call to execute() with the returned value of execute(). By default, do nothing.
By default, this runner calls the interpreter’s execute_once method only once per cycle (meaning at least one macro step is processed during each cycle). If execute_all is set to True, then execute_once is repeatedly called until no macro step can be processed in the current cycle.
Parameters: - interpreter (
Interpreter
) – interpreter instance to run. - interval (
float
) – interval between two calls to execute - execute_all – Repeatedly call interpreter’s execute_once method at each step.
-
running
¶ Holds if execution is currently running (even if it’s paused).
-
paused
¶ Holds if execution is running but paused.
-
start
()¶ Start the execution.
-
stop
()¶ Stop the execution.
-
pause
()¶ Pause the execution.
-
unpause
()¶ Unpause the execution.
-
wait
()¶ Wait for the execution to finish.
-
execute
()¶ Called each time the interpreter has to be executed.
Return type: List
[MacroStep
]
-
before_execute
()¶ Called before each call to execute().
-
after_execute
(steps)¶ Called after each call to self.execute(). Receives the return value of self.execute().
Parameters: steps ( List
[MacroStep
]) – List of macrosteps returned by self.execute()
-
before_run
()¶ Called before running the execution.
-
after_run
()¶ Called after a final configuration is reached.
Module testing¶
-
sismic.testing.
state_is_entered
(steps, name)¶ Holds if state was entered during given steps.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - name (
str
) – name of a state
Return type: Returns: given state was entered
- steps (
-
sismic.testing.
state_is_exited
(steps, name)¶ Holds if state was exited during given steps.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - name (
str
) – name of a state
Return type: Returns: given state was exited
- steps (
-
sismic.testing.
event_is_fired
(steps, name, parameters=None)¶ Holds if an event was fired during given steps.
If name is None, this function looks for any event. If parameters are provided, their values are compared with the respective attribute of the event. Not all parameters have to be provided, as only the ones that are provided are actually compared.
Parameters: Return type: Returns: event was fired
-
sismic.testing.
event_is_consumed
(steps, name, parameters=None)¶ Holds if an event was consumed during given steps.
If name is None, this function looks for any event. If parameters are provided, their values are compared with the respective attribute of the event. Not all parameters have to be provided, as only the ones that are provided are actually compared.
Parameters: Return type: Returns: event was consumed
-
sismic.testing.
transition_is_processed
(steps, transition=None)¶ Holds if a transition was processed during given steps.
If no transition is provided, this function looks for any transition.
Parameters: - steps (
Union
[MacroStep
,List
[MacroStep
]]) – a macrostep or list of macrosteps - transition (
Optional
[Transition
]) – a transition
Return type: Returns: transition was processed
- steps (
Credits¶
The Sismic library for Python is mainly developed by Alexandre Decan at the University of Mons with the help of many contributors.
Sismic is released publicly under the GNU Lesser General Public Licence version 3.0 (LGPLv3).
The source code is available on GitHub: https://github.com/AlexandreDecan/sismic
Use GitHub’s integrated services to contribute suggestions and feature requests for this library or to report bugs.
You can cite the research article describing the method and techniques supported by Sismic using:
@article{sismic2018-sosym,
author = {Mens, Tom and Decan, Alexandre and Spanoudakis, Nikolaos},
journal = {Software and Systems Modeling},
publisher = {Springer},
year = 2018,
title = {A method for testing and validating executable statechart models},
doi = {10.1007/s10270-018-0676-3},
url = {https://link.springer.com/article/10.1007/s10270-018-0676-3},
}
You can cite the Sismic library itself using:
@software{sismic,
author = {Decan, Alexandre},
title = {Sismic Interactive Statechart Model Interpreter and Checker},
url = {https://github.com/AlexandreDecan/sismic},
}