Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"errors_test.go",
"fsm_test.go",
"relation_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"errors.go",
"event.go",
"fsm.go",
"realtion.go",
"utils.go",
],
importpath = "go-common/app/job/main/relation/fsm",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
// InvalidEventError is returned by FSM.Event() when the event cannot be called
// in the current state.
type InvalidEventError struct {
Event string
State string
}
func (e InvalidEventError) Error() string {
return "event " + e.Event + " inappropriate in current state " + e.State
}
// UnknownEventError is returned by FSM.Event() when the event is not defined.
type UnknownEventError struct {
Event string
}
func (e UnknownEventError) Error() string {
return "event " + e.Event + " does not exist"
}
// InTransitionError is returned by FSM.Event() when an asynchronous transition
// is already in progress.
type InTransitionError struct {
Event string
}
func (e InTransitionError) Error() string {
return "event " + e.Event + " inappropriate because previous transition did not complete"
}
// NotInTransitionError is returned by FSM.Transition() when an asynchronous
// transition is not in progress.
type NotInTransitionError struct{}
func (e NotInTransitionError) Error() string {
return "transition inappropriate because no state change in progress"
}
// NoTransitionError is returned by FSM.Event() when no transition have happened,
// for example if the source and destination states are the same.
type NoTransitionError struct {
Err error
}
func (e NoTransitionError) Error() string {
if e.Err != nil {
return "no transition with error: " + e.Err.Error()
}
return "no transition"
}
// CanceledError is returned by FSM.Event() when a callback have canceled a
// transition.
type CanceledError struct {
Err error
}
func (e CanceledError) Error() string {
if e.Err != nil {
return "transition canceled with error: " + e.Err.Error()
}
return "transition canceled"
}
// AsyncError is returned by FSM.Event() when a callback have initiated an
// asynchronous state transition.
type AsyncError struct {
Err error
}
func (e AsyncError) Error() string {
if e.Err != nil {
return "async started with error: " + e.Err.Error()
}
return "async started"
}
// InternalError is returned by FSM.Event() and should never occur. It is a
// probably because of a bug.
type InternalError struct{}
func (e InternalError) Error() string {
return "internal error on state transition"
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
import (
"errors"
"testing"
)
func TestInvalidEventError(t *testing.T) {
event := "invalid event"
state := "state"
e := InvalidEventError{Event: event, State: state}
if e.Error() != "event "+e.Event+" inappropriate in current state "+e.State {
t.Error("InvalidEventError string mismatch")
}
}
func TestUnknownEventError(t *testing.T) {
event := "invalid event"
e := UnknownEventError{Event: event}
if e.Error() != "event "+e.Event+" does not exist" {
t.Error("UnknownEventError string mismatch")
}
}
func TestInTransitionError(t *testing.T) {
event := "in transition"
e := InTransitionError{Event: event}
if e.Error() != "event "+e.Event+" inappropriate because previous transition did not complete" {
t.Error("InTransitionError string mismatch")
}
}
func TestNotInTransitionError(t *testing.T) {
e := NotInTransitionError{}
if e.Error() != "transition inappropriate because no state change in progress" {
t.Error("NotInTransitionError string mismatch")
}
}
func TestNoTransitionError(t *testing.T) {
e := NoTransitionError{}
if e.Error() != "no transition" {
t.Error("NoTransitionError string mismatch")
}
e.Err = errors.New("no transition")
if e.Error() != "no transition with error: "+e.Err.Error() {
t.Error("NoTransitionError string mismatch")
}
}
func TestCanceledError(t *testing.T) {
e := CanceledError{}
if e.Error() != "transition canceled" {
t.Error("CanceledError string mismatch")
}
e.Err = errors.New("canceled")
if e.Error() != "transition canceled with error: "+e.Err.Error() {
t.Error("CanceledError string mismatch")
}
}
func TestAsyncError(t *testing.T) {
e := AsyncError{}
if e.Error() != "async started" {
t.Error("AsyncError string mismatch")
}
e.Err = errors.New("async")
if e.Error() != "async started with error: "+e.Err.Error() {
t.Error("AsyncError string mismatch")
}
}
func TestInternalError(t *testing.T) {
e := InternalError{}
if e.Error() != "internal error on state transition" {
t.Error("InternalError string mismatch")
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
// Event is the info that get passed as a reference in the callbacks.
type Event struct {
// FSM is a reference to the current FSM.
FSM *FSM
// Event is the event name.
Event string
// Src is the state before the transition.
Src string
// Dst is the state after the transition.
Dst string
// Err is an optional error that can be returned from a callback.
Err error
// Args is a optinal list of arguments passed to the callback.
Args []interface{}
// canceled is an internal flag set if the transition is canceled.
canceled bool
// async is an internal flag set if the transition should be asynchronous
async bool
}
// Cancel can be called in before_<EVENT> or leave_<STATE> to cancel the
// current transition before it happens. It takes an opitonal error, which will
// overwrite e.Err if set before.
func (e *Event) Cancel(err ...error) {
e.canceled = true
if len(err) > 0 {
e.Err = err[0]
}
}
// Async can be called in leave_<STATE> to do an asynchronous state transition.
//
// The current state transition will be on hold in the old state until a final
// call to Transition is made. This will comlete the transition and possibly
// call the other callbacks.
func (e *Event) Async() {
e.async = true
}

View File

@@ -0,0 +1,432 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package fsm implements a finite state machine.
//
// It is heavily based on two FSM implementations:
//
// Javascript Finite State Machine
// https://github.com/jakesgordon/javascript-state-machine
//
// Fysom for Python
// https://github.com/oxplot/fysom (forked at https://github.com/mriehl/fysom)
//
package fsm
import (
"strings"
"sync"
)
// transitioner is an interface for the FSM's transition function.
type transitioner interface {
transition(*FSM) error
}
// FSM is the state machine that holds the current state.
//
// It has to be created with NewFSM to function properly.
type FSM struct {
// current is the state that the FSM is currently in.
current string
// transitions maps events and source states to destination states.
transitions map[eKey]string
// callbacks maps events and targers to callback functions.
callbacks map[cKey]Callback
// transition is the internal transition functions used either directly
// or when Transition is called in an asynchronous state transition.
transition func()
// transitionerObj calls the FSM's transition() function.
transitionerObj transitioner
// stateMu guards access to the current state.
stateMu sync.RWMutex
// eventMu guards access to Event() and Transition().
eventMu sync.Mutex
}
// EventDesc represents an event when initializing the FSM.
//
// The event can have one or more source states that is valid for performing
// the transition. If the FSM is in one of the source states it will end up in
// the specified destination state, calling all defined callbacks as it goes.
type EventDesc struct {
// Name is the event name used when calling for a transition.
Name string
// Src is a slice of source states that the FSM must be in to perform a
// state transition.
Src []string
// Dst is the destination state that the FSM will be in if the transition
// succeds.
Dst string
}
// Callback is a function type that callbacks should use. Event is the current
// event info as the callback happens.
type Callback func(*Event)
// Events is a shorthand for defining the transition map in NewFSM.
type Events []EventDesc
// Callbacks is a shorthand for defining the callbacks in NewFSM.a
type Callbacks map[string]Callback
// NewFSM constructs a FSM from events and callbacks.
//
// The events and transitions are specified as a slice of Event structs
// specified as Events. Each Event is mapped to one or more internal
// transitions from Event.Src to Event.Dst.
//
// Callbacks are added as a map specified as Callbacks where the key is parsed
// as the callback event as follows, and called in the same order:
//
// 1. before_<EVENT> - called before event named <EVENT>
//
// 2. before_event - called before all events
//
// 3. leave_<OLD_STATE> - called before leaving <OLD_STATE>
//
// 4. leave_state - called before leaving all states
//
// 5. enter_<NEW_STATE> - called after entering <NEW_STATE>
//
// 6. enter_state - called after entering all states
//
// 7. after_<EVENT> - called after event named <EVENT>
//
// 8. after_event - called after all events
//
// There are also two short form versions for the most commonly used callbacks.
// They are simply the name of the event or state:
//
// 1. <NEW_STATE> - called after entering <NEW_STATE>
//
// 2. <EVENT> - called after event named <EVENT>
//
// If both a shorthand version and a full version is specified it is undefined
// which version of the callback will end up in the internal map. This is due
// to the psuedo random nature of Go maps. No checking for multiple keys is
// currently performed.
func NewFSM(initial string, events []EventDesc, callbacks map[string]Callback) *FSM {
f := &FSM{
transitionerObj: &transitionerStruct{},
current: initial,
transitions: make(map[eKey]string),
callbacks: make(map[cKey]Callback),
}
// Build transition map and store sets of all events and states.
allEvents := make(map[string]bool)
allStates := make(map[string]bool)
for _, e := range events {
for _, src := range e.Src {
f.transitions[eKey{e.Name, src}] = e.Dst
allStates[src] = true
allStates[e.Dst] = true
}
allEvents[e.Name] = true
}
// Map all callbacks to events/states.
for name, fn := range callbacks {
var target string
var callbackType int
switch {
case strings.HasPrefix(name, "before_"):
target = strings.TrimPrefix(name, "before_")
if target == "event" {
target = ""
callbackType = callbackBeforeEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackBeforeEvent
}
case strings.HasPrefix(name, "leave_"):
target = strings.TrimPrefix(name, "leave_")
if target == "state" {
target = ""
callbackType = callbackLeaveState
} else if _, ok := allStates[target]; ok {
callbackType = callbackLeaveState
}
case strings.HasPrefix(name, "enter_"):
target = strings.TrimPrefix(name, "enter_")
if target == "state" {
target = ""
callbackType = callbackEnterState
} else if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
}
case strings.HasPrefix(name, "after_"):
target = strings.TrimPrefix(name, "after_")
if target == "event" {
target = ""
callbackType = callbackAfterEvent
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
default:
target = name
if _, ok := allStates[target]; ok {
callbackType = callbackEnterState
} else if _, ok := allEvents[target]; ok {
callbackType = callbackAfterEvent
}
}
if callbackType != callbackNone {
f.callbacks[cKey{target, callbackType}] = fn
}
}
return f
}
// Current returns the current state of the FSM.
func (f *FSM) Current() string {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
return f.current
}
// Is returns true if state is the current state.
func (f *FSM) Is(state string) bool {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
return state == f.current
}
// SetState allows the user to move to the given state from current state.
// The call does not trigger any callbacks, if defined.
func (f *FSM) SetState(state string) {
f.stateMu.Lock()
defer f.stateMu.Unlock()
f.current = state
}
// Can returns true if event can occur in the current state.
func (f *FSM) Can(event string) bool {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
_, ok := f.transitions[eKey{event, f.current}]
return ok && (f.transition == nil)
}
// Cannot returns true if event can not occure in the current state.
// It is a convenience method to help code read nicely.
func (f *FSM) Cannot(event string) bool {
return !f.Can(event)
}
// Event initiates a state transition with the named event.
//
// The call takes a variable number of arguments that will be passed to the
// callback, if defined.
//
// It will return nil if the state change is ok or one of these errors:
//
// - event X inappropriate because previous transition did not complete
//
// - event X inappropriate in current state Y
//
// - event X does not exist
//
// - internal error on state transition
//
// The last error should never occur in this situation and is a sign of an
// internal bug.
func (f *FSM) Event(event string, args ...interface{}) error {
f.eventMu.Lock()
defer f.eventMu.Unlock()
f.stateMu.RLock()
defer f.stateMu.RUnlock()
if f.transition != nil {
return InTransitionError{event}
}
dst, ok := f.transitions[eKey{event, f.current}]
if !ok {
for ekey := range f.transitions {
if ekey.event == event {
return InvalidEventError{event, f.current}
}
}
return UnknownEventError{event}
}
e := &Event{f, event, f.current, dst, nil, args, false, false}
err := f.beforeEventCallbacks(e)
if err != nil {
return err
}
if f.current == dst {
f.afterEventCallbacks(e)
return NoTransitionError{e.Err}
}
// Setup the transition, call it later.
f.transition = func() {
f.stateMu.Lock()
f.current = dst
f.stateMu.Unlock()
f.enterStateCallbacks(e)
f.afterEventCallbacks(e)
}
if err = f.leaveStateCallbacks(e); err != nil {
if _, ok := err.(CanceledError); ok {
f.transition = nil
}
return err
}
// Perform the rest of the transition, if not asynchronous.
f.stateMu.RUnlock()
err = f.doTransition()
f.stateMu.RLock()
if err != nil {
return InternalError{}
}
return e.Err
}
// Transition wraps transitioner.transition.
func (f *FSM) Transition() error {
f.eventMu.Lock()
defer f.eventMu.Unlock()
return f.doTransition()
}
// doTransition wraps transitioner.transition.
func (f *FSM) doTransition() error {
return f.transitionerObj.transition(f)
}
// transitionerStruct is the default implementation of the transitioner
// interface. Other implementations can be swapped in for testing.
type transitionerStruct struct{}
// Transition completes an asynchrounous state change.
//
// The callback for leave_<STATE> must prviously have called Async on its
// event to have initiated an asynchronous state transition.
func (t transitionerStruct) transition(f *FSM) error {
if f.transition == nil {
return NotInTransitionError{}
}
f.transition()
f.transition = nil
return nil
}
// beforeEventCallbacks calls the before_ callbacks, first the named then the
// general version.
func (f *FSM) beforeEventCallbacks(e *Event) error {
if fn, ok := f.callbacks[cKey{e.Event, callbackBeforeEvent}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackBeforeEvent}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
}
}
return nil
}
// leaveStateCallbacks calls the leave_ callbacks, first the named then the
// general version.
func (f *FSM) leaveStateCallbacks(e *Event) error {
if fn, ok := f.callbacks[cKey{f.current, callbackLeaveState}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
} else if e.async {
return AsyncError{e.Err}
}
}
if fn, ok := f.callbacks[cKey{"", callbackLeaveState}]; ok {
fn(e)
if e.canceled {
return CanceledError{e.Err}
} else if e.async {
return AsyncError{e.Err}
}
}
return nil
}
// enterStateCallbacks calls the enter_ callbacks, first the named then the
// general version.
func (f *FSM) enterStateCallbacks(e *Event) {
if fn, ok := f.callbacks[cKey{f.current, callbackEnterState}]; ok {
fn(e)
}
if fn, ok := f.callbacks[cKey{"", callbackEnterState}]; ok {
fn(e)
}
}
// afterEventCallbacks calls the after_ callbacks, first the named then the
// general version.
func (f *FSM) afterEventCallbacks(e *Event) {
if fn, ok := f.callbacks[cKey{e.Event, callbackAfterEvent}]; ok {
fn(e)
}
if fn, ok := f.callbacks[cKey{"", callbackAfterEvent}]; ok {
fn(e)
}
}
const (
callbackNone int = iota
callbackBeforeEvent
callbackLeaveState
callbackEnterState
callbackAfterEvent
)
// cKey is a struct key used for keeping the callbacks mapped to a target.
type cKey struct {
// target is either the name of a state or an event depending on which
// callback type the key refers to. It can also be "" for a non-targeted
// callback like before_event.
target string
// callbackType is the situation when the callback will be run.
callbackType int
}
// eKey is a struct key used for storing the transition map.
type eKey struct {
// event is the name of the event that the keys refers to.
event string
// src is the source from where the event can transition.
src string
}

View File

@@ -0,0 +1,791 @@
// Copyright (c) 2013 - Max Persson <max@looplab.se>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
import (
"fmt"
"sync"
"testing"
"time"
)
type fakeTransitionerObj struct {
}
func (t fakeTransitionerObj) transition(f *FSM) error {
return &InternalError{}
}
func TestSameState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "start"},
},
Callbacks{},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestSetState(t *testing.T) {
fsm := NewFSM(
"walking",
Events{
{Name: "walk", Src: []string{"start"}, Dst: "walking"},
},
Callbacks{},
)
fsm.SetState("start")
if fsm.Current() != "start" {
t.Error("expected state to be 'walking'")
}
err := fsm.Event("walk")
if err != nil {
t.Error("transition is expected no error")
}
}
func TestBadTransition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "running"},
},
Callbacks{},
)
fsm.transitionerObj = new(fakeTransitionerObj)
err := fsm.Event("run")
if err == nil {
t.Error("bad transition should give an error")
}
}
func TestInappropriateEvent(t *testing.T) {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
err := fsm.Event("close")
if e, ok := err.(InvalidEventError); !ok && e.Event != "close" && e.State != "closed" {
t.Error("expected 'InvalidEventError' with correct state and event")
}
}
func TestInvalidEvent(t *testing.T) {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
err := fsm.Event("lock")
if e, ok := err.(UnknownEventError); !ok && e.Event != "close" {
t.Error("expected 'UnknownEventError' with correct event")
}
}
func TestMultipleSources(t *testing.T) {
fsm := NewFSM(
"one",
Events{
{Name: "first", Src: []string{"one"}, Dst: "two"},
{Name: "second", Src: []string{"two"}, Dst: "three"},
{Name: "reset", Src: []string{"one", "two", "three"}, Dst: "one"},
},
Callbacks{},
)
fsm.Event("first")
if fsm.Current() != "two" {
t.Error("expected state to be 'two'")
}
fsm.Event("reset")
if fsm.Current() != "one" {
t.Error("expected state to be 'one'")
}
fsm.Event("first")
fsm.Event("second")
if fsm.Current() != "three" {
t.Error("expected state to be 'three'")
}
fsm.Event("reset")
if fsm.Current() != "one" {
t.Error("expected state to be 'one'")
}
}
func TestMultipleEvents(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "first", Src: []string{"start"}, Dst: "one"},
{Name: "second", Src: []string{"start"}, Dst: "two"},
{Name: "reset", Src: []string{"one"}, Dst: "reset_one"},
{Name: "reset", Src: []string{"two"}, Dst: "reset_two"},
{Name: "reset", Src: []string{"reset_one", "reset_two"}, Dst: "start"},
},
Callbacks{},
)
fsm.Event("first")
fsm.Event("reset")
if fsm.Current() != "reset_one" {
t.Error("expected state to be 'reset_one'")
}
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Event("second")
fsm.Event("reset")
if fsm.Current() != "reset_two" {
t.Error("expected state to be 'reset_two'")
}
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestGenericCallbacks(t *testing.T) {
beforeEvent := false
leaveState := false
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
beforeEvent = true
},
"leave_state": func(e *Event) {
leaveState = true
},
"enter_state": func(e *Event) {
enterState = true
},
"after_event": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(beforeEvent && leaveState && enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestSpecificCallbacks(t *testing.T) {
beforeEvent := false
leaveState := false
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
beforeEvent = true
},
"leave_start": func(e *Event) {
leaveState = true
},
"enter_end": func(e *Event) {
enterState = true
},
"after_run": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(beforeEvent && leaveState && enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestSpecificCallbacksShortform(t *testing.T) {
enterState := false
afterEvent := false
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"end": func(e *Event) {
enterState = true
},
"run": func(e *Event) {
afterEvent = true
},
},
)
fsm.Event("run")
if !(enterState && afterEvent) {
t.Error("expected all callbacks to be called")
}
}
func TestBeforeEventWithoutTransition(t *testing.T) {
beforeEvent := true
fsm := NewFSM(
"start",
Events{
{Name: "dontrun", Src: []string{"start"}, Dst: "start"},
},
Callbacks{
"before_event": func(e *Event) {
beforeEvent = true
},
},
)
err := fsm.Event("dontrun")
if e, ok := err.(NoTransitionError); !ok && e.Err != nil {
t.Error("expected 'NoTransitionError' without custom error")
}
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
if !beforeEvent {
t.Error("expected callback to be called")
}
}
func TestCancelBeforeGenericEvent(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelBeforeSpecificEvent(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelLeaveGenericState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_state": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelLeaveSpecificState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Cancel()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestCancelWithError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_event": func(e *Event) {
e.Cancel(fmt.Errorf("error"))
},
},
)
err := fsm.Event("run")
if _, ok := err.(CanceledError); !ok {
t.Error("expected only 'CanceledError'")
}
if e, ok := err.(CanceledError); ok && e.Err.Error() != "error" {
t.Error("expected 'CanceledError' with correct custom error")
}
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestAsyncTransitionGenericState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_state": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Transition()
if fsm.Current() != "end" {
t.Error("expected state to be 'end'")
}
}
func TestAsyncTransitionSpecificState(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
fsm.Transition()
if fsm.Current() != "end" {
t.Error("expected state to be 'end'")
}
}
func TestAsyncTransitionInProgress(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
{Name: "reset", Src: []string{"end"}, Dst: "start"},
},
Callbacks{
"leave_start": func(e *Event) {
e.Async()
},
},
)
fsm.Event("run")
err := fsm.Event("reset")
if e, ok := err.(InTransitionError); !ok && e.Event != "reset" {
t.Error("expected 'InTransitionError' with correct state")
}
fsm.Transition()
fsm.Event("reset")
if fsm.Current() != "start" {
t.Error("expected state to be 'start'")
}
}
func TestAsyncTransitionNotInProgress(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
{Name: "reset", Src: []string{"end"}, Dst: "start"},
},
Callbacks{},
)
err := fsm.Transition()
if _, ok := err.(NotInTransitionError); !ok {
t.Error("expected 'NotInTransitionError'")
}
}
func TestCallbackNoError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
},
},
)
e := fsm.Event("run")
if e != nil {
t.Error("expected no error")
}
}
func TestCallbackError(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
e.Err = fmt.Errorf("error")
},
},
)
e := fsm.Event("run")
if e.Error() != "error" {
t.Error("expected error to be 'error'")
}
}
func TestCallbackArgs(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
if len(e.Args) != 1 {
t.Error("too few arguments")
}
arg, ok := e.Args[0].(string)
if !ok {
t.Error("not a string argument")
}
if arg != "test" {
t.Error("incorrect argument")
}
},
},
)
fsm.Event("run", "test")
}
func TestNoDeadLock(t *testing.T) {
var fsm *FSM
fsm = NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
fsm.Current() // Should not result in a panic / deadlock
},
},
)
fsm.Event("run")
}
func TestThreadSafetyRaceCondition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"run": func(e *Event) {
},
},
)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = fsm.Current()
}()
fsm.Event("run")
wg.Wait()
}
func TestDoubleTransition(t *testing.T) {
var fsm *FSM
var wg sync.WaitGroup
wg.Add(2)
fsm = NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "end"},
},
Callbacks{
"before_run": func(e *Event) {
wg.Done()
// Imagine a concurrent event coming in of the same type while
// the data access mutex is unlocked because the current transition
// is running its event callbacks, getting around the "active"
// transition checks
if len(e.Args) == 0 {
// Must be concurrent so the test may pass when we add a mutex that synchronizes
// calls to Event(...). It will then fail as an inappropriate transition as we
// have changed state.
go func() {
if err := fsm.Event("run", "second run"); err != nil {
fmt.Println(err)
wg.Done() // It should fail, and then we unfreeze the test.
}
}()
time.Sleep(20 * time.Millisecond)
} else {
panic("Was able to reissue an event mid-transition")
}
},
},
)
if err := fsm.Event("run"); err != nil {
fmt.Println(err)
}
wg.Wait()
}
func TestNoTransition(t *testing.T) {
fsm := NewFSM(
"start",
Events{
{Name: "run", Src: []string{"start"}, Dst: "start"},
},
Callbacks{},
)
err := fsm.Event("run")
if _, ok := err.(NoTransitionError); !ok {
t.Error("expected 'NoTransitionError'")
}
}
func ExampleNewFSM() {
fsm := NewFSM(
"green",
Events{
{Name: "warn", Src: []string{"green"}, Dst: "yellow"},
{Name: "panic", Src: []string{"yellow"}, Dst: "red"},
{Name: "panic", Src: []string{"green"}, Dst: "red"},
{Name: "calm", Src: []string{"red"}, Dst: "yellow"},
{Name: "clear", Src: []string{"yellow"}, Dst: "green"},
},
Callbacks{
"before_warn": func(e *Event) {
fmt.Println("before_warn")
},
"before_event": func(e *Event) {
fmt.Println("before_event")
},
"leave_green": func(e *Event) {
fmt.Println("leave_green")
},
"leave_state": func(e *Event) {
fmt.Println("leave_state")
},
"enter_yellow": func(e *Event) {
fmt.Println("enter_yellow")
},
"enter_state": func(e *Event) {
fmt.Println("enter_state")
},
"after_warn": func(e *Event) {
fmt.Println("after_warn")
},
"after_event": func(e *Event) {
fmt.Println("after_event")
},
},
)
fmt.Println(fsm.Current())
err := fsm.Event("warn")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// green
// before_warn
// before_event
// leave_green
// leave_state
// enter_yellow
// enter_state
// after_warn
// after_event
// yellow
}
func ExampleFSM_Current() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Current())
// Output: closed
}
func ExampleFSM_Is() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Is("closed"))
fmt.Println(fsm.Is("open"))
// Output:
// true
// false
}
func ExampleFSM_Can() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Can("open"))
fmt.Println(fsm.Can("close"))
// Output:
// true
// false
}
func ExampleFSM_Cannot() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Cannot("open"))
fmt.Println(fsm.Cannot("close"))
// Output:
// false
// true
}
func ExampleFSM_Event() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{},
)
fmt.Println(fsm.Current())
err := fsm.Event("open")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
err = fsm.Event("close")
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// closed
// open
// closed
}
func ExampleFSM_Transition() {
fsm := NewFSM(
"closed",
Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
Callbacks{
"leave_closed": func(e *Event) {
e.Async()
},
},
)
err := fsm.Event("open")
if e, ok := err.(AsyncError); !ok && e.Err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
err = fsm.Transition()
if err != nil {
fmt.Println(err)
}
fmt.Println(fsm.Current())
// Output:
// closed
// open
}

View File

@@ -0,0 +1,144 @@
package fsm
// RelationState is relation state type
type RelationState string
// RelationEvent is relation event type
type RelationEvent string
// consts for relation
var (
// states
StateNoRelation = RelationState("no_relation")
StateWhisper = RelationState("whisper")
StateFollowing = RelationState("following")
StateBlacked = RelationState("blacked")
StateFriend = RelationState("friend") // StateFriend is the most special state
// events
EventAddFollowing = RelationEvent("add_following")
EventDelFollowing = RelationEvent("del_following")
EventAddWhisper = RelationEvent("add_whisper")
EventDelWhisper = RelationEvent("del_whisper")
EventAddBlack = RelationEvent("add_black")
EventDelBlack = RelationEvent("del_black")
EventDelFollower = RelationEvent("del_follower")
EventBeFriend = RelationEvent("be_friend") // EventBeFriend is the most special event
)
// RelationEventHandler is used to handle all state change for relation
type RelationEventHandler interface {
AddFollowing(*Event)
DelFollowing(*Event)
AddWhisper(*Event)
DelWhisper(*Event)
AddBlack(*Event)
DelBlack(*Event)
DelFollower(*Event)
}
// DefaultHandler is the default RelationEventHandler
var DefaultHandler = &defaultHandlerImpl{}
type defaultHandlerImpl struct{}
func (*defaultHandlerImpl) AddFollowing(*Event) {}
func (*defaultHandlerImpl) DelFollowing(*Event) {}
func (*defaultHandlerImpl) AddWhisper(*Event) {}
func (*defaultHandlerImpl) DelWhisper(*Event) {}
func (*defaultHandlerImpl) AddBlack(*Event) {}
func (*defaultHandlerImpl) DelBlack(*Event) {}
func (*defaultHandlerImpl) DelFollower(*Event) {}
// RelationStateMachine is used to describe all state change for relation
type RelationStateMachine struct {
*FSM
}
// NewRelationStateMachine will create a RelationStateMachine
func NewRelationStateMachine(initial RelationState, handler RelationEventHandler) *RelationStateMachine {
rs := &RelationStateMachine{
FSM: NewFSM(
string(StateNoRelation),
Events{
{
Name: string(EventAddFollowing),
Src: []string{
string(StateNoRelation),
string(StateWhisper),
string(StateBlacked),
},
Dst: string(StateFollowing),
},
{
Name: string(EventDelFollowing),
Src: []string{
string(StateFollowing),
string(StateFriend),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventAddWhisper),
Src: []string{
string(StateNoRelation),
string(StateFollowing),
string(StateBlacked),
string(StateFriend),
},
Dst: string(StateWhisper),
},
{
Name: string(EventDelWhisper),
Src: []string{
string(StateWhisper),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventAddBlack),
Src: []string{
string(StateNoRelation),
string(StateFollowing),
string(StateFriend),
string(StateWhisper),
},
Dst: string(StateBlacked),
},
{
Name: string(EventDelBlack),
Src: []string{
string(StateBlacked),
},
Dst: string(StateNoRelation),
},
{
Name: string(EventDelBlack),
Src: []string{
string(StateBlacked),
},
Dst: string(StateNoRelation),
},
},
Callbacks{
string(EventAddFollowing): handler.AddFollowing,
string(EventDelFollowing): handler.DelFollowing,
string(EventAddWhisper): handler.AddWhisper,
string(EventDelWhisper): handler.DelWhisper,
string(EventAddBlack): handler.AddBlack,
string(EventDelBlack): handler.DelBlack,
string(EventDelFollower): handler.DelFollower,
},
),
}
return rs
}
// Event is used to execute any events
func (r *RelationStateMachine) Event(event RelationEvent, args ...interface{}) error {
return r.FSM.Event(string(event), args...)
}
// SetState is used to set state
func (r *RelationStateMachine) SetState(state RelationState) {
r.FSM.SetState(string(state))
}

View File

@@ -0,0 +1,16 @@
package fsm
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRelationStateMachine(t *testing.T) {
rs := NewRelationStateMachine(StateNoRelation, DefaultHandler)
assert.NotNil(t, rs)
assert.Equal(t, RelationState(rs.Current()), StateNoRelation)
assert.NoError(t, rs.Event(EventAddFollowing))
assert.Equal(t, StateFollowing, RelationState(rs.Current()))
}

View File

@@ -0,0 +1,45 @@
package fsm
import (
"bytes"
"fmt"
)
// Visualize outputs a visualization of a FSM in Graphviz format.
func Visualize(fsm *FSM) string {
var buf bytes.Buffer
states := make(map[string]int)
buf.WriteString(fmt.Sprintf(`digraph fsm {`))
buf.WriteString("\n")
// make sure the initial state is at top
for k, v := range fsm.transitions {
if k.src == fsm.current {
states[k.src]++
states[v]++
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}
for k, v := range fsm.transitions {
if k.src != fsm.current {
states[k.src]++
states[v]++
buf.WriteString(fmt.Sprintf(` "%s" -> "%s" [ label = "%s" ];`, k.src, v, k.event))
buf.WriteString("\n")
}
}
buf.WriteString("\n")
for k := range states {
buf.WriteString(fmt.Sprintf(` "%s";`, k))
buf.WriteString("\n")
}
buf.WriteString(fmt.Sprintln("}"))
return buf.String()
}