Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Feb 15, 2016
1 parent 842f307 commit 466e1e5
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 84 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Changelog
- (Changed) ``Interpreter.__init__`` does not anymore stabilize the statechart. Stabilization is done during the
first call of ``execute_once``.
- (Changed) ``Story.tell`` returns a list of ``MacroStep`` (the *trace*) instead of an ``Interpreter`` instance.
- (Changed) The name of some attributes of an event in a tester story changes (e.g. *event* becomes *consumed event*,
- (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 from ``execute_once`` or using ``log``.
- (Removed) ``Interpreter.__init__`` does not accept an ``initial_time`` parameter.
Expand Down
154 changes: 86 additions & 68 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ A *tester statechart* defines a property that should (or not) be satisfied by ot
A *tester statechart* is like any other statechart, in the sense that neither their syntax nor their semantics
differs from any other statechart. The difference comes from the events it receives and the role it plays.
A run of a *tester statechart* must end in a final state, meaning the test did not fail.

.. note::

This is more a convention than a requirement, but you should follow it.

The run of such a *tester statechart* is driven by a specific sequence of events and pauses, which represents
what happens during the execution of a *statechart under test*.

For example, such a sequence contains *consumed* events, *entered* events, *exited* events, ...

In particular, the following events are generated:

- A *started* event is sent at the beginning.
Expand All @@ -51,76 +55,21 @@ In particular, the following events are generated:
the name of the state is available through the *entered_state* attribute.
- A *stopped* event is sent at the end.

Class :py:class:`~sismic.testing.ExecutionWatcher` can be used to associate a statechart tester with a *statechart under test*:

.. autoclass:: sismic.testing.ExecutionWatcher
:noindex:
:members: start, stop, watch_with


To summarize, if you want to test (**at runtime**) the execution of a *statechart under test* ``tested``, you need to:

1. create an :py:class:`~sismic.testing.ExecutionWatcher` with ``tested``.
2. construct at least one *tester statechart* ``tester`` that expresses the property you want to test.
3. associate each ``tester`` to the watcher with :py:meth:`~sismic.testing.ExecutionWatcher.watch_with`.
4. start watching with :py:meth:`~sismic.testing.ExecutionWatcher.start`.
5. execute ``tested`` (using a story or directly by sending events).
6. stop watching with :py:meth:`~sismic.testing.ExecutionWatcher.stop`.

If ``tester`` ends in a final configuration, ie. ``tester.final`` holds, then the test is **considered** successful.


The following *tester statechart* examples are relative to :ref:`this statechart <elevator_example>`.
They show the specification of some testers in YAML, and how to execute them.

Destination should be reached
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This *tester statechart* ensures that every chosen destination is finally reached.

.. literalinclude:: /examples/tester_elevator_destination_reached.yaml
:language: yaml

It can be tested as follows:
The sequence does follow the interpretation order:

.. testcode:: success
1. an event is possibly consumed
2. For each transition

from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
from sismic.testing import ExecutionWatcher
from sismic.model import Event

# Load statecharts
with open('examples/elevator.yaml') as f:
elevator_statechart = import_from_yaml(f)
with open('examples/tester_elevator_destination_reached.yaml') as f:
tester_statechart = import_from_yaml(f)

# Create the interpreter and the watcher
interpreter = Interpreter(elevator_statechart)
watcher = ExecutionWatcher(interpreter)

# Add the tester and start watching
tester = watcher.watch_with(tester_statechart)
watcher.start()

# Send the elevator to 4th
interpreter.queue(Event('floorSelected', floor=4)).execute(max_steps=2)
assert tester.context['destinations'] == [4]

interpreter.execute()
assert tester.context['destinations'] == []

# Stop watching. The tester must be in a final state
watcher.stop()

assert tester.final
a. states are exited
b. transition is processed
c. states are entered
d. statechart is stabilized (some states are exited and/or entered)


Testing an execution trace
--------------------------
Using statechart to check properties on a trace
-----------------------------------------------

The trace of an interpreter is the list of executed macro steps. The trace can be built upon the values returned by
The trace of an interpreter is the list of its executed macro steps. The trace can be built upon the values returned by
each call to :py:meth:`~sismic.interpreter.Interpreter.execute` (or :py:meth:`~sismic.interpreter.Interpreter.execute_once`),
or can be automatically built using :py:meth:`~sismic.interpreter.log` function.

Expand All @@ -135,14 +84,16 @@ Notice that using this function, the statechart tester can not access the contex
To summarize, if you want to test the **trace** of a *statechart under test* ``tested``, you need to:

1. construct a *tester statechart* ``tester`` that expresses the property you want to test.
2. execute ``tested`` (using a story or directly by sending events) and log its trace using :py:func:`~sismic.interpreter.log` for example.
2. execute ``tested`` (using a story or directly by sending events) and log its trace.
3. generate a new story from this trace with :py:func:`~sismic.testing.teststory_from_trace`.
4. tell this story to the *tester statechart* ``tester``.

If ``tester`` ends in a final configuration, ie. ``tester.final`` holds, then the test is **considered** successful.

The following *tester statechart* examples are relative to :ref:`this statechart <elevator_example>`.
They show the specification of some testers in YAML, and how to execute them.

Examples follow. Note that these testers are currently used as unit tests for Sismic.
Note that these testers are currently used as unit tests for Sismic.

7th floor is never reached
^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -178,3 +129,70 @@ We check this tester using several stories, as follows:

.. literalinclude:: ../tests/test_testing.py
:pyobject: ElevatorStoryTests.test_elevator_moves_after_10s



Using statecharts to check properties at runtime
------------------------------------------------

Sismic provides a convenience class to allow *tester statecharts* to check properties at runtime.
Class :py:class:`~sismic.testing.ExecutionWatcher` can be used to associate a statechart tester with a *statechart under test*:

.. autoclass:: sismic.testing.ExecutionWatcher
:noindex:
:members: start, stop, watch_with


To summarize, if you want to test (**at runtime**) the execution of a *statechart under test* ``tested``, you need to:

1. create an :py:class:`~sismic.testing.ExecutionWatcher` with ``tested``.
2. construct at least one *tester statechart* ``tester`` that expresses the property you want to test.
3. associate each ``tester`` to the watcher with :py:meth:`~sismic.testing.ExecutionWatcher.watch_with`.
4. start watching with :py:meth:`~sismic.testing.ExecutionWatcher.start`.
5. execute ``tested`` (using a story or directly by sending events).
6. stop watching with :py:meth:`~sismic.testing.ExecutionWatcher.stop`.

If ``tester`` ends in a final configuration, ie. ``tester.final`` holds, then the test is **considered** successful.

Destination should be reached
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This *tester statechart* ensures that every chosen destination is finally reached.

.. literalinclude:: /examples/tester_elevator_destination_reached.yaml
:language: yaml

It can be tested as follows:

.. testcode:: success

from sismic.io import import_from_yaml
from sismic.interpreter import Interpreter
from sismic.testing import ExecutionWatcher
from sismic.model import Event

# Load statecharts
with open('examples/elevator.yaml') as f:
elevator_statechart = import_from_yaml(f)
with open('examples/tester_elevator_destination_reached.yaml') as f:
tester_statechart = import_from_yaml(f)

# Create the interpreter and the watcher
interpreter = Interpreter(elevator_statechart)
watcher = ExecutionWatcher(interpreter)

# Add the tester and start watching
tester = watcher.watch_with(tester_statechart)
watcher.start()

# Send the elevator to 4th
interpreter.queue(Event('floorSelected', floor=4)).execute(max_steps=2)
assert tester.context['destinations'] == [4]

interpreter.execute()
assert tester.context['destinations'] == []

# Stop watching. The tester must be in a final state
watcher.stop()

assert tester.final
17 changes: 4 additions & 13 deletions sismic/testing/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ class ExecutionWatcher:
(and a set of optional parameters that can be used to tune the interpreter that will be built upon this tester statechart)
and returns the resulting *Interpreter* instance for this tester.
If started (using *start*), whenever something happens during the execution of the tested interpreter, events are automatically sent to every
associated tester statecharts. Their internal clock are synchronized and, the context of the tested statechart is
If started (using *start*), whenever something happens during the execution of the tested interpreter, events are
automatically sent to every associated tester statecharts.
Their internal clock are synchronized and, the context of the tested statechart is
also exposed to the statechart testers, ie. if *x* is a variable in the context of a tested statechart, then
*context.x* is dynamically exposed to every associated statechart tester.
Expand Down Expand Up @@ -70,7 +71,7 @@ def watch_with(self, tester_statechart: Statechart, interpreter_klass=None, **kw
:param tester_statechart: a tester statechart (instance of *Statechart*)
:param interpreter_klass: a callable that accepts a *Statechart* instance, an *initial_context* and any
additional (optional) parameters provided to this method.
additional (optional) parameters provided to this method.
:return: the interpreter instance that wraps given tester statechart.
"""
interpreter_klass = interpreter_klass if interpreter_klass else Interpreter
Expand Down Expand Up @@ -136,16 +137,6 @@ def teststory_from_trace(trace: list) -> Story:
Notice that this function adds a *pause* if there is any delay between pairs of consecutive steps.
The story does follow the interpretation order:
1. an event is possibly consumed
2. For each transition
a. states are exited
b. transition is processed
c. states are entered
d. statechart is stabilized (some states are exited and/or entered)
:param trace: a list of *MacroStep* instances
:return: A story
"""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def setUp(self):

def test_7th_floor_never_reached(self):
story = Story([Event('floorSelected', floor=8)])
trace = story.tell(self.tested)
trace = story.tell(self.tested) # self.tested is an interpreter for our elevator

test_story = teststory_from_trace(trace)

Expand All @@ -59,7 +59,7 @@ def test_7th_floor_never_reached(self):

def test_7th_floor_never_reached_fails(self):
story = Story([Event('floorSelected', floor=4), Pause(2), Event('floorSelected', floor=7)])
trace = story.tell(self.tested)
trace = story.tell(self.tested) # self.tested is an interpreter for our elevator

test_story = teststory_from_trace(trace)

Expand Down

0 comments on commit 466e1e5

Please sign in to comment.