Skip to content

Commit

Permalink
chore: cleanup / more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
derkork committed Jun 6, 2024
1 parent a778f0b commit b3ad279
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 6 deletions.
6 changes: 6 additions & 0 deletions godot-state-charts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,17 @@
<Content Include="tests\framework\state_chart_test_base.gd" />
<Content Include="tests\test_all_of_guard.gd" />
<Content Include="tests\test_any_of_guard.gd" />
<Content Include="tests\test_automatic_transition_leaving_compound_state.gd" />
<Content Include="tests\test_automatic_transition_on_property_change.gd" />
<Content Include="tests\test_parallel_state_event_handling.gd" />
<Content Include="tests\test_automatic_transition_on_state_enter.gd" />
<Content Include="tests\test_immediate_condition_state.gd" />
<Content Include="tests\test_property_changes_immediately_visible.gd" />
<Content Include="tests\test_delayed_transition.gd" />
<Content Include="tests\test_expression_guard.gd" />
<Content Include="tests\test_history_state_as_initial_state.gd" />
<Content Include="tests\test_history_state_simple.gd" />
<Content Include="tests\test_explicit_event_consumption.gd" />
<Content Include="tests\test_infinite_loop_detection.gd" />
<Content Include="tests\test_expression_properties.gd" />
<Content Include="tests\test_not_guard.gd" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func _on_walk_state_entered():


# While we are in walk state...
func _on_walk_state_physics_processing(delta):
func _on_walk_state_physics_processing(_delta):
# set a new velocity
velocity = _direction * SPEED
# and move into the given direction
Expand Down
12 changes: 11 additions & 1 deletion manual/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ The signal is only emitted when the transition is taken, not when it is pending.

#### Automatic transitions

It is possible to have transitions with an empty _Event_ field. These transitions will be evaluated whenever you enter a state, send an event or set an expression property (see [expression guards](#expression-guards)). This is useful for modeling [condition states](https://statecharts.dev/glossary/condition-state.html) or react to changes in expression properties. Usually you will put a guard on such an automatic transition to make sure it is only taken when a certain condition is met.
It is possible to have transitions with an empty _Event_ field. These transitions will be evaluated whenever you change a state, send an event or set an expression property (see [expression guards](#expression-guards)). This is useful for modeling [condition states](https://statecharts.dev/glossary/condition-state.html) or react to changes in expression properties. Usually you will put a guard on such an automatic transition to make sure it is only taken when a certain condition is met.

![Alt text](immediate_transition.png)

Expand Down Expand Up @@ -552,6 +552,16 @@ Godot has a very nice built-in comment field named "Editor Description". Use thi

Usually you don't need to worry too much about the order in which state changes are processed but there are some instances where it is important to know the order in which events are processed. The following will give you an overview on the inner workings and the order in which events are processed.

#### Generic event handling rules

The state chart reacts to these events:

- an explicit event was sent to the state chart node using the `send_event` function.
- an expression property was changed using the `set_expression_property` function.
-
Whenever an event occurs, the state chart will try to find transitions that react to this event. Only transitions in states that are currently active will be considered. Transitions will be checked in a depth-first manner. So the innermost transition that handles any given event (be it explicit or automatic) will run. When a transition runs, the event is considered as handled and will no longer be processed by any other transition, except if that other transition happens to live in a parallel state (each parallel state can handle events even if that event was already handled by another parallel state). If the transition has a guard and it evaluates to `false` then the next transition that reacts to the event will be checked. If no transition reacts to the event, the event will bubble up to the parent state. This process will continue until the event is handled or the root state is reached. If the event is not handled by any state, it will be ignored.

#### Example
For this example we will use the following state chart:

![Example state chart for the order of events](order_of_events_chart.png)
Expand Down
8 changes: 6 additions & 2 deletions tests/framework/state_chart_test_base.gd
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func parallel_state(name: String, parent: StateChartState = null) -> ParallelSta

@warning_ignore("shadowed_variable_base_class")
func atomic_state( name: String, parent: StateChartState) -> AtomicState:
assert(not(parent is AtomicState))
assert(not(parent is HistoryState))

var state: AtomicState = AtomicState.new()
state.name = name
parent.add_child(state)
Expand All @@ -82,7 +85,7 @@ func atomic_state( name: String, parent: StateChartState) -> AtomicState:


@warning_ignore("shadowed_variable_base_class")
func transition(from: StateChartState, to: StateChartState, event: String, delay: String = "0", guard: Guard = null) -> Transition:
func transition(from: StateChartState, to: StateChartState, event: String = "", delay: String = "0", guard: Guard = null) -> Transition:
@warning_ignore("shadowed_variable")
var transition: Transition = Transition.new()
from.add_child(transition)
Expand All @@ -94,11 +97,12 @@ func transition(from: StateChartState, to: StateChartState, event: String, delay


@warning_ignore("shadowed_variable_base_class")
func history_state(name: String, parent: CompoundState, deep:bool = false) -> HistoryState:
func history_state(name: String, parent: CompoundState, default_state:StateChartState, deep:bool = false) -> HistoryState:
var state: HistoryState = HistoryState.new()
state.name = name
state.deep = deep
parent.add_child(state)
state.default_state = state.get_path_to(default_state)
# we don't set the initial state here, as it is not needed for history states
return state

Expand Down
25 changes: 25 additions & 0 deletions tests/test_automatic_transition_leaving_compound_state.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
extends StateChartTestBase

# Test that when automatic transition works when a compound state is left
func test_automatic_transition_leaving_compound_state():
var root := compound_state("root")
var a := compound_state("a", root)

var a1 := atomic_state("a1", a)
var a2 := atomic_state("a2", a)

var b := atomic_state("b", root)

transition(a, b, "")

await finish_setup()

# because we have the automatic transition to b, we should be in b
# right after the setup phase
assert_active(b)

assert_inactive(a)
assert_inactive(a1)
assert_inactive(a2)


2 changes: 1 addition & 1 deletion tests/test_automatic_transition_on_state_enter.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ func test_automatic_transition_on_state_enter():
# because we have the automatic transition to b, we should be in b
# right after the setup phase
assert_active(b)

assert_inactive(a)

36 changes: 36 additions & 0 deletions tests/test_explicit_event_consumption.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
extends StateChartTestBase

func test_event_consumption():
var root := compound_state("root")
var a := compound_state("a", root)
var b := compound_state("b", root)
# add a transition from a to b reacting on the "switch" event
var t1 := transition(a, b, "switch")

var a1 := atomic_state("a1", a)
var a2 := atomic_state("a2", a)
# add a transition from a1 to a2 reacting on the "switch" event
var t2 := transition(a1, a2, "switch")

await finish_setup()
assert_active(a)
assert_active(a1)

watch_signals(t1)
watch_signals(t2)

# when i send the "switch" event...
send_event("switch")

# then
# the transition from a1 to a2 should be taken, consuming the event
assert_active(a)
assert_active(a2)
assert_inactive(a1)
assert_inactive(b)

# the transition t2 should have emitted a "taken" signal
assert_signal_emitted(t2, "taken")

# the transition t1 should not have emitted a "taken" signal
assert_signal_not_emitted(t1, "taken")
19 changes: 19 additions & 0 deletions tests/test_history_state_as_initial_state.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
extends StateChartTestBase

# Tests that a history state used as an initial state will work.
func test_history_state_as_initial_state():
var root := compound_state("root")

var a := compound_state("a", root)
atomic_state("a1", a)
var a2 := atomic_state("a2", a)
var h := history_state("h", a, a2)

# overwrite the initial state of a to be h
a.initial_state = a.get_path_to(h)

await finish_setup()

# now a2 should be active, because h was initial state and the default
# state of h is a2
assert_active(a2)
2 changes: 1 addition & 1 deletion tests/test_history_state_simple.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ func test_history_state_simple():
var a := compound_state("a", root)
var a1 := atomic_state("a1", a)
var a2 := atomic_state("a2", a)
var h := history_state("h", a)
var h := history_state("h", a, a1)

transition(a1, a2, "to_a2")

Expand Down
47 changes: 47 additions & 0 deletions tests/test_immediate_condition_state.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
extends StateChartTestBase

# This tests a condition state where the target states are immediate children of the source state
# but the source state holds all the transitions.
func test_immediate_condition_state():
var root := compound_state("root")
var a := atomic_state("a", root)
var b := atomic_state("b", root)
var c := atomic_state("c", root)


transition(root, a, "", "0", expression_guard("state == 'a'"))
transition(root, b, "", "0", expression_guard("state == 'b'"))
transition(root, c, "", "0", expression_guard("state == 'c'"))

set_initial_expression_properties({"state": "a"})

await finish_setup()

# root state is active
assert_active(a)
assert_inactive(b)
assert_inactive(c)

# WHEN: i change the property to enter state b
set_expression_property("state", "b")

# THEN: state b is active
assert_active(b)
assert_inactive(a)
assert_inactive(c)

# WHEN: i change the property to enter state c
set_expression_property("state", "c")

# THEN: state c is active
assert_active(c)
assert_inactive(a)
assert_inactive(b)

# WHEN: i change the property to enter state a
set_expression_property("state", "a")

# THEN: state a is active
assert_active(a)
assert_inactive(b)
assert_inactive(c)
30 changes: 30 additions & 0 deletions tests/test_parallel_state_event_handling.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
extends StateChartTestBase

func test_parallel_state_event_handling():
var root := parallel_state("root")

var a := compound_state("a", root)
var a1 := atomic_state("a1", a)
var a2 := atomic_state("a2", a)

var b := compound_state("b", root)
var b1 := atomic_state("b1", b)
var b2 := atomic_state("b2", b)

transition(a1, a2, "to_a2")
transition( b1, b2, "", "0", state_is_active_guard(a2))

await finish_setup()

assert_active(a1)
assert_active(b1)

# when i send an event to switch to a2, b1 should automatically switch to b2
send_event("to_a2")

assert_active(a2)
# this only works because b2 is a child of b which is a parallel state, so b's transition
# will be triggered even if a1 consumes the event
assert_active(b2)


36 changes: 36 additions & 0 deletions tests/test_property_changes_immediately_visible.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
extends StateChartTestBase

# This tests that property changes are immediately visible to all guards.
# see https://github.com/derkork/godot-statecharts/issues/82#issuecomment-1963417766
func test_property_changes_immediately_visible():
var root := compound_state("root")
var a := atomic_state("a", root)
var b := atomic_state("b", root)
var c := atomic_state("c", root)

var d := atomic_state("d", root)
var e := atomic_state("e", root)

transition(a, b, "to_b")
transition(b, c)
transition(c, d, "", "0", expression_guard("x > 0"))
transition(c, e, "", "0", expression_guard("x = 0"))


a.state_entered.connect(func(): set_expression_property("x", 0))
b.state_entered.connect(func(): set_expression_property("x", 1))
await finish_setup()

# root state is active
assert_active(a)

# when I transition to b
send_event("to_b")

# then b is entered, which sets x to 1
# then c is entered and because x is 1, d is entered
assert_active(d)
assert_inactive(e)
assert_inactive(a)
assert_inactive(b)
assert_inactive(c)

0 comments on commit b3ad279

Please sign in to comment.