Skip to content

Commit 8279b62

Browse files
authored
Merge pull request #104 from qmuntal/addtests
Improve test coverage of state transitions and actions
2 parents 9e21f17 + 33f7c34 commit 8279b62

2 files changed

Lines changed: 265 additions & 0 deletions

File tree

actions_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package stateless
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestStateMachine_Fire_IgnoredTriggerMustBeIgnoredInSubstate(t *testing.T) {
9+
sm := NewStateMachine(stateB)
10+
sm.Configure(stateA).
11+
Permit(triggerX, stateC)
12+
13+
sm.Configure(stateB).
14+
SubstateOf(stateA).
15+
Ignore(triggerX)
16+
17+
sm.Fire(triggerX)
18+
19+
if got := sm.MustState(); got != stateB {
20+
t.Errorf("sm.MustState() = %v, want %v", got, stateB)
21+
}
22+
}
23+
24+
func TestStateMachine_Fire_IgnoreIfTrue_TriggerMustBeIgnored(t *testing.T) {
25+
sm := NewStateMachine(stateB)
26+
sm.Configure(stateA).
27+
Permit(triggerX, stateC)
28+
29+
sm.Configure(stateB).
30+
SubstateOf(stateA).
31+
Ignore(triggerX, func(_ context.Context, _ ...any) bool {
32+
return true
33+
})
34+
35+
sm.Fire(triggerX)
36+
37+
if got := sm.MustState(); got != stateB {
38+
t.Errorf("sm.MustState() = %v, want %v", got, stateB)
39+
}
40+
}
41+
42+
func TestStateMachine_Fire_IgnoreIfFalse_TriggerMustNotBeIgnored(t *testing.T) {
43+
sm := NewStateMachine(stateB)
44+
sm.Configure(stateA).
45+
Permit(triggerX, stateC)
46+
47+
sm.Configure(stateB).
48+
SubstateOf(stateA).
49+
Ignore(triggerX, func(_ context.Context, _ ...any) bool {
50+
return false
51+
})
52+
53+
sm.Fire(triggerX)
54+
55+
if got := sm.MustState(); got != stateC {
56+
t.Errorf("sm.MustState() = %v, want %v", got, stateC)
57+
}
58+
}
59+
60+
func TestStateMachine_Fire_SuperStateShouldNotExitOnSubStateTransition(t *testing.T) {
61+
sm := NewStateMachine(stateA)
62+
record := []string{}
63+
64+
sm.Configure(stateA).
65+
OnEntry(func(_ context.Context, _ ...any) error {
66+
record = append(record, "Entered state A")
67+
return nil
68+
}).
69+
OnExit(func(_ context.Context, _ ...any) error {
70+
record = append(record, "Exited state A")
71+
return nil
72+
}).
73+
Permit(triggerX, stateB)
74+
75+
sm.Configure(stateB). // Our super state
76+
InitialTransition(stateC).
77+
OnEntry(func(_ context.Context, _ ...any) error {
78+
record = append(record, "Entered super state B")
79+
return nil
80+
}).
81+
OnExit(func(_ context.Context, _ ...any) error {
82+
record = append(record, "Exited super state B")
83+
return nil
84+
})
85+
86+
sm.Configure(stateC). // Our first sub state
87+
SubstateOf(stateB).
88+
OnEntry(func(_ context.Context, _ ...any) error {
89+
record = append(record, "Entered sub state C")
90+
return nil
91+
}).
92+
OnExit(func(_ context.Context, _ ...any) error {
93+
record = append(record, "Exited sub state C")
94+
return nil
95+
}).
96+
Permit(triggerY, stateD)
97+
98+
sm.Configure(stateD). // Our second sub state
99+
SubstateOf(stateB).
100+
OnEntry(func(_ context.Context, _ ...any) error {
101+
record = append(record, "Entered sub state D")
102+
return nil
103+
}).
104+
OnExit(func(_ context.Context, _ ...any) error {
105+
record = append(record, "Exited sub state D")
106+
return nil
107+
})
108+
109+
sm.Fire(triggerX)
110+
sm.Fire(triggerY)
111+
112+
expected := []string{
113+
"Exited state A",
114+
"Entered super state B",
115+
"Entered sub state C",
116+
"Exited sub state C",
117+
"Entered sub state D",
118+
}
119+
120+
if len(record) != len(expected) {
121+
t.Errorf("record length = %v, want %v", len(record), len(expected))
122+
return
123+
}
124+
125+
for i, v := range expected {
126+
if record[i] != v {
127+
t.Errorf("record[%d] = %v, want %v", i, record[i], v)
128+
}
129+
}
130+
}

transition_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package stateless
2+
3+
import (
4+
"context"
5+
"testing"
6+
)
7+
8+
func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_UsesSubstateTransition(t *testing.T) {
9+
sm := NewStateMachine(stateA)
10+
sm.Configure(stateA).
11+
Permit(triggerX, stateB)
12+
13+
sm.Configure(stateB).
14+
SubstateOf(stateA).
15+
Permit(triggerX, stateC)
16+
17+
sm.Fire(triggerX)
18+
if got := sm.MustState(); got != stateB {
19+
t.Errorf("sm.MustState() = %v, want %v", got, stateB)
20+
}
21+
22+
sm.Fire(triggerX)
23+
if got := sm.MustState(); got != stateC {
24+
t.Errorf("sm.MustState() = %v, want %v", got, stateC)
25+
}
26+
}
27+
28+
func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_SubstateGuardBlocked_UsesSuperstateTransition(t *testing.T) {
29+
guardConditionValue := false
30+
sm := NewStateMachine(stateB)
31+
32+
sm.Configure(stateA).
33+
Permit(triggerX, stateD)
34+
35+
sm.Configure(stateB).
36+
SubstateOf(stateA).
37+
Permit(triggerX, stateC, func(_ context.Context, _ ...any) bool {
38+
return guardConditionValue
39+
})
40+
41+
sm.Fire(triggerX)
42+
if got := sm.MustState(); got != stateD {
43+
t.Errorf("sm.MustState() = %v, want %v", got, stateD)
44+
}
45+
}
46+
47+
func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_SubstateGuardOpen_UsesSubstateTransition(t *testing.T) {
48+
guardConditionValue := true
49+
sm := NewStateMachine(stateB)
50+
51+
sm.Configure(stateA).
52+
Permit(triggerX, stateD)
53+
54+
sm.Configure(stateB).
55+
SubstateOf(stateA).
56+
Permit(triggerX, stateC, func(_ context.Context, _ ...any) bool {
57+
return guardConditionValue
58+
})
59+
60+
sm.Fire(triggerX)
61+
if got := sm.MustState(); got != stateC {
62+
t.Errorf("sm.MustState() = %v, want %v", got, stateC)
63+
}
64+
}
65+
66+
func TestStateMachine_InternalTransitionIf_ExecutesOnlyFirstMatchingAction(t *testing.T) {
67+
sm := NewStateMachine(1)
68+
executed := []int{}
69+
70+
sm.Configure(1).
71+
InternalTransition(1, func(_ context.Context, _ ...any) error {
72+
executed = append(executed, 1)
73+
return nil
74+
}, func(_ context.Context, _ ...any) bool {
75+
return true
76+
}).
77+
InternalTransition(1, func(_ context.Context, _ ...any) error {
78+
executed = append(executed, 2)
79+
return nil
80+
}, func(_ context.Context, _ ...any) bool {
81+
return false
82+
})
83+
84+
sm.Fire(1)
85+
86+
if len(executed) != 1 || executed[0] != 1 {
87+
t.Errorf("expected only first action to execute, got executions: %v", executed)
88+
}
89+
}
90+
91+
func TestStateMachine_Fire_MultiLayerSubstates_ClosestAncestorTransitionUsed(t *testing.T) {
92+
tests := []struct {
93+
name string
94+
parentGuardConditionValue bool
95+
childGuardConditionValue bool
96+
grandchildGuardConditionValue bool
97+
expectedState string
98+
}{
99+
{"GrandchildOpen", false, false, true, "GrandchildStateTarget"},
100+
{"ChildOpen_GrandchildClosed", false, true, false, "ChildStateTarget"},
101+
{"ChildOpen_GrandchildOpen", false, true, true, "GrandchildStateTarget"},
102+
{"ParentOpen_ChildClosed_GrandchildClosed", true, false, false, "ParentStateTarget"},
103+
{"ParentOpen_ChildClosed_GrandchildOpen", true, false, true, "GrandchildStateTarget"},
104+
{"ParentOpen_ChildOpen_GrandchildClosed", true, true, false, "ChildStateTarget"},
105+
{"AllOpen", true, true, true, "GrandchildStateTarget"},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
sm := NewStateMachine("GrandchildState")
111+
112+
sm.Configure("ParentState").
113+
Permit(triggerX, "ParentStateTarget", func(_ context.Context, _ ...any) bool {
114+
return tt.parentGuardConditionValue
115+
})
116+
117+
sm.Configure("ChildState").
118+
SubstateOf("ParentState").
119+
Permit(triggerX, "ChildStateTarget", func(_ context.Context, _ ...any) bool {
120+
return tt.childGuardConditionValue
121+
})
122+
123+
sm.Configure("GrandchildState").
124+
SubstateOf("ChildState").
125+
Permit(triggerX, "GrandchildStateTarget", func(_ context.Context, _ ...any) bool {
126+
return tt.grandchildGuardConditionValue
127+
})
128+
129+
sm.Fire(triggerX)
130+
if got := sm.MustState(); got != tt.expectedState {
131+
t.Errorf("sm.MustState() = %v, want %v", got, tt.expectedState)
132+
}
133+
})
134+
}
135+
}

0 commit comments

Comments
 (0)