-
-
Notifications
You must be signed in to change notification settings - Fork 69
Improve test coverage of state transitions and actions #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| package stateless | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestStateMachine_Fire_IgnoredTriggerMustBeIgnoredInSubstate(t *testing.T) { | ||
| sm := NewStateMachine(stateB) | ||
| sm.Configure(stateA). | ||
| Permit(triggerX, stateC) | ||
|
|
||
| sm.Configure(stateB). | ||
| SubstateOf(stateA). | ||
| Ignore(triggerX) | ||
|
|
||
| sm.Fire(triggerX) | ||
|
|
||
| if got := sm.MustState(); got != stateB { | ||
| t.Errorf("sm.MustState() = %v, want %v", got, stateB) | ||
| } | ||
| } | ||
|
|
||
| func TestStateMachine_Fire_IgnoreIfTrue_TriggerMustBeIgnored(t *testing.T) { | ||
| sm := NewStateMachine(stateB) | ||
| sm.Configure(stateA). | ||
| Permit(triggerX, stateC) | ||
|
|
||
| sm.Configure(stateB). | ||
| SubstateOf(stateA). | ||
| Ignore(triggerX, func(_ context.Context, _ ...any) bool { | ||
| return true | ||
| }) | ||
|
|
||
| sm.Fire(triggerX) | ||
|
|
||
| if got := sm.MustState(); got != stateB { | ||
| t.Errorf("sm.MustState() = %v, want %v", got, stateB) | ||
| } | ||
| } | ||
|
|
||
| func TestStateMachine_Fire_IgnoreIfFalse_TriggerMustNotBeIgnored(t *testing.T) { | ||
| sm := NewStateMachine(stateB) | ||
| sm.Configure(stateA). | ||
| Permit(triggerX, stateC) | ||
|
|
||
| sm.Configure(stateB). | ||
| SubstateOf(stateA). | ||
| Ignore(triggerX, func(_ context.Context, _ ...any) bool { | ||
| return false | ||
| }) | ||
|
|
||
| sm.Fire(triggerX) | ||
|
|
||
| if got := sm.MustState(); got != stateC { | ||
| t.Errorf("sm.MustState() = %v, want %v", got, stateC) | ||
| } | ||
| } | ||
|
|
||
| func TestStateMachine_Fire_SuperStateShouldNotExitOnSubStateTransition(t *testing.T) { | ||
| sm := NewStateMachine(stateA) | ||
| record := []string{} | ||
|
|
||
| sm.Configure(stateA). | ||
| OnEntry(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Entered state A") | ||
| return nil | ||
| }). | ||
| OnExit(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Exited state A") | ||
| return nil | ||
| }). | ||
| Permit(triggerX, stateB) | ||
|
|
||
| sm.Configure(stateB). // Our super state | ||
| InitialTransition(stateC). | ||
| OnEntry(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Entered super state B") | ||
| return nil | ||
| }). | ||
| OnExit(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Exited super state B") | ||
| return nil | ||
| }) | ||
|
|
||
| sm.Configure(stateC). // Our first sub state | ||
| SubstateOf(stateB). | ||
| OnEntry(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Entered sub state C") | ||
| return nil | ||
| }). | ||
| OnExit(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Exited sub state C") | ||
| return nil | ||
| }). | ||
| Permit(triggerY, stateD) | ||
|
|
||
| sm.Configure(stateD). // Our second sub state | ||
| SubstateOf(stateB). | ||
| OnEntry(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Entered sub state D") | ||
| return nil | ||
| }). | ||
| OnExit(func(_ context.Context, _ ...any) error { | ||
| record = append(record, "Exited sub state D") | ||
| return nil | ||
| }) | ||
|
|
||
| sm.Fire(triggerX) | ||
| sm.Fire(triggerY) | ||
|
Comment on lines
+109
to
+110
|
||
|
|
||
| expected := []string{ | ||
| "Exited state A", | ||
| "Entered super state B", | ||
| "Entered sub state C", | ||
| "Exited sub state C", | ||
| "Entered sub state D", | ||
| } | ||
|
|
||
| if len(record) != len(expected) { | ||
| t.Errorf("record length = %v, want %v", len(record), len(expected)) | ||
| return | ||
| } | ||
|
|
||
| for i, v := range expected { | ||
| if record[i] != v { | ||
| t.Errorf("record[%d] = %v, want %v", i, record[i], v) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,135 @@ | ||||||
| package stateless | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "testing" | ||||||
| ) | ||||||
|
|
||||||
| func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_UsesSubstateTransition(t *testing.T) { | ||||||
| sm := NewStateMachine(stateA) | ||||||
| sm.Configure(stateA). | ||||||
| Permit(triggerX, stateB) | ||||||
|
|
||||||
| sm.Configure(stateB). | ||||||
| SubstateOf(stateA). | ||||||
| Permit(triggerX, stateC) | ||||||
|
|
||||||
| sm.Fire(triggerX) | ||||||
| if got := sm.MustState(); got != stateB { | ||||||
| t.Errorf("sm.MustState() = %v, want %v", got, stateB) | ||||||
| } | ||||||
|
|
||||||
|
Comment on lines
+17
to
+21
|
||||||
| sm.Fire(triggerX) | ||||||
| if got := sm.MustState(); got != stateC { | ||||||
| t.Errorf("sm.MustState() = %v, want %v", got, stateC) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_SubstateGuardBlocked_UsesSuperstateTransition(t *testing.T) { | ||||||
| guardConditionValue := false | ||||||
| sm := NewStateMachine(stateB) | ||||||
|
|
||||||
| sm.Configure(stateA). | ||||||
| Permit(triggerX, stateD) | ||||||
|
|
||||||
| sm.Configure(stateB). | ||||||
| SubstateOf(stateA). | ||||||
| Permit(triggerX, stateC, func(_ context.Context, _ ...any) bool { | ||||||
| return guardConditionValue | ||||||
| }) | ||||||
|
|
||||||
| sm.Fire(triggerX) | ||||||
| if got := sm.MustState(); got != stateD { | ||||||
| t.Errorf("sm.MustState() = %v, want %v", got, stateD) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestStateMachine_Fire_TriggerHandledOnSuperStateAndSubState_SubstateGuardOpen_UsesSubstateTransition(t *testing.T) { | ||||||
| guardConditionValue := true | ||||||
| sm := NewStateMachine(stateB) | ||||||
|
|
||||||
| sm.Configure(stateA). | ||||||
| Permit(triggerX, stateD) | ||||||
|
|
||||||
| sm.Configure(stateB). | ||||||
| SubstateOf(stateA). | ||||||
| Permit(triggerX, stateC, func(_ context.Context, _ ...any) bool { | ||||||
| return guardConditionValue | ||||||
| }) | ||||||
|
|
||||||
| sm.Fire(triggerX) | ||||||
| if got := sm.MustState(); got != stateC { | ||||||
| t.Errorf("sm.MustState() = %v, want %v", got, stateC) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestStateMachine_InternalTransitionIf_ExecutesOnlyFirstMatchingAction(t *testing.T) { | ||||||
|
||||||
| func TestStateMachine_InternalTransitionIf_ExecutesOnlyFirstMatchingAction(t *testing.T) { | |
| func TestStateMachine_InternalTransition_ExecutesOnlyFirstMatchingAction(t *testing.T) { |
Copilot
AI
Feb 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current setup doesn’t actually verify any “first match wins” behavior: only the first internal transition’s guard can ever match (the second returns false). Also, the implementation panics when multiple trigger behaviours match the same trigger in a state, so configuring both guards to match would make this test crash rather than select the first. Consider either renaming this test to reflect what it checks (single guarded internal transition executes) or changing it to assert that multiple matching behaviours cause a panic / require mutually-exclusive guards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This configuration block is not
gofmt-formatted (indentation is inconsistent), which will create noisy diffs and diverge from standard Go formatting used elsewhere in the repo. Please rungofmton this file.