2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-13 13:10:30 +00:00
loop/fsm/example_fsm_test.go
sputn1ck 20db07dccf
fsm: add fsm module
This commit adds a module for a finite state machine. The goal of the
module is to provide a simple, easy to use, and easy to understand
finite state machine. The module is designed to be used in future
loop subsystems. Additionally a state visualizer is provided to
help with understanding the state machine.
2023-09-07 17:41:15 +02:00

246 lines
5.0 KiB
Go

package fsm
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
var (
errService = errors.New("service error")
errStore = errors.New("store error")
)
type mockStore struct {
storeErr error
}
func (m *mockStore) StoreStuff() error {
return m.storeErr
}
type mockService struct {
respondChan chan bool
respondErr error
}
func (m *mockService) WaitForStuffHappening() (<-chan bool, error) {
return m.respondChan, m.respondErr
}
func newInitStuffRequest() *InitStuffRequest {
return &InitStuffRequest{
Stuff: "stuff",
respondChan: make(chan<- string, 1),
}
}
func TestExampleFSM(t *testing.T) {
testCases := []struct {
name string
expectedState StateType
eventCtx EventContext
expectedLastActionError error
sendEvent EventType
sendEventErr error
serviceErr error
storeErr error
}{
{
name: "success",
expectedState: StuffSuccess,
eventCtx: newInitStuffRequest(),
sendEvent: OnRequestStuff,
},
{
name: "service error",
expectedState: StuffFailed,
eventCtx: newInitStuffRequest(),
sendEvent: OnRequestStuff,
serviceErr: errService,
expectedLastActionError: errService,
},
{
name: "store error",
expectedLastActionError: errStore,
storeErr: errStore,
sendEvent: OnRequestStuff,
expectedState: StuffFailed,
eventCtx: newInitStuffRequest(),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
respondChan := make(chan string, 1)
if req, ok := tc.eventCtx.(*InitStuffRequest); ok {
req.respondChan = respondChan
}
serviceResponseChan := make(chan bool, 1)
serviceResponseChan <- true
service := &mockService{
respondChan: serviceResponseChan,
respondErr: tc.serviceErr,
}
store := &mockStore{
storeErr: tc.storeErr,
}
exampleContext := NewExampleFSMContext(service, store)
cachedObserver := NewCachedObserver(100)
exampleContext.RegisterObserver(cachedObserver)
err := exampleContext.SendEvent(
tc.sendEvent, tc.eventCtx,
)
require.Equal(t, tc.sendEventErr, err)
require.Equal(
t,
tc.expectedLastActionError,
exampleContext.LastActionError,
)
err = cachedObserver.WaitForState(
context.Background(),
time.Second,
tc.expectedState,
)
require.NoError(t, err)
})
}
}
// getTestContext returns a test context for the example FSM and a cached
// observer that can be used to verify the state transitions.
func getTestContext() (*ExampleFSM, *CachedObserver) {
service := &mockService{
respondChan: make(chan bool, 1),
}
service.respondChan <- true
store := &mockStore{}
exampleContext := NewExampleFSMContext(service, store)
cachedObserver := NewCachedObserver(100)
exampleContext.RegisterObserver(cachedObserver)
return exampleContext, cachedObserver
}
// TestExampleFSMFlow tests different flows that the example FSM can go through.
func TestExampleFSMFlow(t *testing.T) {
testCases := []struct {
name string
expectedStateFlow []StateType
expectedEventFlow []EventType
storeError error
serviceError error
}{
{
name: "success",
expectedStateFlow: []StateType{
InitFSM,
StuffSentOut,
StuffSuccess,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnStuffSentOut,
OnStuffSuccess,
},
},
{
name: "failure on store",
expectedStateFlow: []StateType{
InitFSM,
StuffFailed,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnError,
},
storeError: errStore,
},
{
name: "failure on service",
expectedStateFlow: []StateType{
InitFSM,
StuffSentOut,
StuffFailed,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnStuffSentOut,
OnError,
},
serviceError: errService,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
exampleContext, cachedObserver := getTestContext()
if tc.storeError != nil {
exampleContext.store.(*mockStore).
storeErr = tc.storeError
}
if tc.serviceError != nil {
exampleContext.service.(*mockService).
respondErr = tc.serviceError
}
go func() {
err := exampleContext.SendEvent(
OnRequestStuff,
newInitStuffRequest(),
)
require.NoError(t, err)
}()
// Wait for the final state.
err := cachedObserver.WaitForState(
context.Background(),
time.Second,
tc.expectedStateFlow[len(
tc.expectedStateFlow,
)-1],
)
require.NoError(t, err)
allNotifications := cachedObserver.
GetCachedNotifications()
for index, notification := range allNotifications {
require.Equal(
t,
tc.expectedStateFlow[index],
notification.NextState,
)
require.Equal(
t,
tc.expectedEventFlow[index],
notification.Event,
)
}
})
}
}