mirror of
https://github.com/lightninglabs/loop
synced 2024-11-13 13:10:30 +00:00
20db07dccf
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.
246 lines
5.0 KiB
Go
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,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|