Skip to content

Commit

Permalink
feat(validation): verify if a state machine has a single component.
Browse files Browse the repository at this point in the history
test(validation): create tests to enforce this verification.
  • Loading branch information
rafaelrds committed Aug 16, 2019
1 parent 33ed846 commit 58df091
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 0 deletions.
24 changes: 24 additions & 0 deletions statemachine/statemachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,27 @@ def __repr__(self):
self.current_state.identifier,
)

def _visitable_states(self, start_state, visited_states):
if start_state in visited_states:
return visited_states

visited_states.append(start_state)

for transition in start_state.transitions:
for neighbour_state in transition.destinations:
if neighbour_state not in visited_states:
self._visitable_states(neighbour_state, visited_states)
return visited_states

def _is_connected(self, starting_state):
initial_state = filter(
lambda state: state._initial, self.states
)[0]
visited_states = []
visitable_states = self._visitable_states(initial_state, visited_states)

return len(visitable_states) == len(self.states)

def check(self):
if not self.states:
raise InvalidDefinition(_('There are no states.'))
Expand All @@ -289,6 +310,9 @@ def check(self):
raise InvalidDefinition(_('There should be one and only one initial state. '
'Your currently have these: {!r}'.format(initials)))
self.initial_state = initials[0]
if (not self._is_connected(self.initial_state)):
raise InvalidDefinition(_('There are unreachable states. '
'The statemachine graph should have a single component.'))

if self.current_state_value is None:
if self.start_value:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_statemachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,36 @@ class NoTransitionsMachine(StateMachine):

with pytest.raises(exceptions.InvalidDefinition):
NoTransitionsMachine()


def visitable_nodes(start_state, visited):
if start_state in visited:
return visited

visited.append(start_state)

for transition in start_state.transitions:
for neighbour_state in transition.destinations:
if neighbour_state not in visited:
visitable_nodes(neighbour_state, visited)
return visited


def test_perfectly_fine_machine_should_be_connected(traffic_light_machine):
model = MyModel()
machine = traffic_light_machine(model)
initial_state = filter(lambda state: state._initial, machine.states)[0]
assert machine._is_connected(initial_state)


def test_should_not_create_disconnected_machine(broken_traffic_light_machine):
class BrokenTrafficLightMachine(StateMachine):
"A broken traffic light machine"
green = State('Green', initial=True)
yellow = State('Yellow')
blue = State('Blue') # This state is unreachable

cycle = green.to(yellow) | yellow.to(green)

with pytest.raises(exceptions.InvalidDefinition):
BrokenTrafficLightMachine()

0 comments on commit 58df091

Please sign in to comment.