You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
791 lines
16 KiB
791 lines
16 KiB
// Copyright (c) 2013 - Max Persson <[email protected]> |
|
// |
|
// 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 |
|
}
|
|
|