diff --git a/pkg/machine/machine_test.go b/pkg/machine/machine_test.go index 10ef963..23be82d 100644 --- a/pkg/machine/machine_test.go +++ b/pkg/machine/machine_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "golang.org/x/exp/maps" ) func ExampleNew() { @@ -101,50 +102,71 @@ func NewCustomStates(t *testing.T, states Struct) *Machine { func TestSingleStateActive(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() assertStates(t, m, S{"A"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestMultipleStatesActive(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() m.Add(S{"B"}, nil) assertStates(t, m, S{"A", "B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestExposeAllStateNames(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() assert.ElementsMatch(t, S{"A", "B", "C", "D", "Exception"}, m.StateNames) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestStateSet(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() m.Set(S{"B"}, nil) assertStates(t, m, S{"B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestStateAdd(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() m.Add(S{"B"}, nil) assertStates(t, m, S{"A", "B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestStateRemove(t *testing.T) { m := NewNoRels(t, S{"B", "C"}) - defer m.Dispose() m.Remove(S{"C"}, nil) assertStates(t, m, S{"B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestPanicWhenStateIsUnknown(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() assert.Panics(t, func() { m.Set(S{"E"}, nil) }) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestGetStateRelations(t *testing.T) { @@ -164,6 +186,10 @@ func TestGetStateRelations(t *testing.T) { assert.Nil(t, err) assert.Equal(t, []Relation{RelationAdd, RelationRequire}, of) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestGetRelationsBetweenStates(t *testing.T) { @@ -183,6 +209,10 @@ func TestGetRelationsBetweenStates(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []Relation{RelationAdd}, between) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestSingleToSingleStateTransition(t *testing.T) { @@ -208,11 +238,14 @@ func TestSingleToSingleStateTransition(t *testing.T) { assert.Equal(t, events, history.Order) // assert event counts assertEventCounts(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestSingleToMultiStateTransition(t *testing.T) { m := NewNoRels(t, S{"A"}) - defer m.Dispose() events := []string{ "AExit", "AB", "AC", "AAny", "AnyB", "BEnter", "AnyC", "CEnter", "BState", "CState", @@ -230,11 +263,14 @@ func TestSingleToMultiStateTransition(t *testing.T) { assert.Equal(t, events, history.Order) // assert event counts assertEventCounts(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestMultiToMultiStateTransition(t *testing.T) { m := NewNoRels(t, S{"A", "B"}) - defer m.Dispose() events := []string{ "AExit", "AD", "AC", "AAny", "BExit", "BD", "BC", "BAny", "AnyD", "DEnter", "AnyC", "CEnter", @@ -252,11 +288,14 @@ func TestMultiToMultiStateTransition(t *testing.T) { assert.Equal(t, events, history.Order) // assert event counts assertEventCounts(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestMultiToSingleStateTransition(t *testing.T) { m := NewNoRels(t, S{"A", "B"}) - defer m.Dispose() events := []string{ "AExit", "AC", "AAny", "BExit", "BC", "BAny", "AnyC", "CEnter", @@ -274,87 +313,127 @@ func TestMultiToSingleStateTransition(t *testing.T) { assert.Equal(t, events, history.Order) // assert event counts assertEventCounts(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestTransitionToActiveState(t *testing.T) { m := NewNoRels(t, S{"A", "B"}) defer m.Dispose() + // history events := []string{"AExit", "AnyA", "AnyA"} history := trackTransitions(m, events) - // transition + + // test m.Set(S{"A"}, nil) + // assert the final state assert.ElementsMatch(t, S{"A"}, m.activeStates) // assert event counts (none should happen) for _, count := range history.Counter { assert.Equal(t, 0, count) } + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestAfterRelationWhenEntering(t *testing.T) { m := NewNoRels(t, S{"A", "B"}) defer m.Dispose() + // relations m.states["C"] = State{After: S{"D"}} m.states["A"] = State{After: S{"B"}} + // history events := []string{"AD", "AC", "AnyD", "DEnter", "AnyC", "CEnter"} history := trackTransitions(m, events) - // transition + + // test m.Set(S{"C", "D"}, nil) + // assert the final state assertStates(t, m, S{"C", "D"}) // assert event counts for _, count := range history.Counter { assert.Equal(t, 1, count) } + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestAfterRelationWhenExiting(t *testing.T) { m := NewNoRels(t, S{"A", "B"}) defer m.Dispose() + // relations m.states["C"] = State{After: S{"D"}} m.states["A"] = State{After: S{"B"}} + // history events := []string{"BExit", "BD", "BC", "BAny", "AExit", "AD", "AC", "AAny"} history := trackTransitions(m, events) - // transition + + // test m.Set(S{"C", "D"}, nil) + // assert the final state assertStates(t, m, S{"C", "D"}) // assert event counts for _, count := range history.Counter { assert.Equal(t, 1, count) } + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRemoveRelation(t *testing.T) { m := NewNoRels(t, S{"D"}) - defer m.Dispose() + // relations m.states["C"] = State{Remove: S{"D"}} + // C deactivates D m.Add(S{"C"}, nil) assertStates(t, m, S{"C"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRemoveRelationSimultaneous(t *testing.T) { // init m := NewNoRels(t, S{"D"}) defer m.Dispose() + // logger log := "" captureLog(t, m, &log) + // relations m.states["C"] = State{Remove: S{"D"}} + // test r := m.Set(S{"C", "D"}, nil) + // assert assert.Equal(t, Canceled, r) assert.Contains(t, log, "[rel:remove] D by C") assertStates(t, m, S{"D"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRemoveRelationCrossBlocking(t *testing.T) { @@ -399,10 +478,10 @@ func TestRemoveRelationCrossBlocking(t *testing.T) { }, }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { m := NewNoRels(t, S{"D"}) - defer m.Dispose() // C and D are cross blocking each other via Remove m.states["C"] = State{Remove: S{"D"}} m.states["D"] = State{Remove: S{"C"}} @@ -427,34 +506,53 @@ func TestAddRelation(t *testing.T) { m.Set(S{"A", "C"}, nil) assertStates(t, m, S{"A", "C"}, "state should be skipped if "+ "blocked at the same time") + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRequireRelation(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // relations m.states["C"] = State{Require: S{"D"}} - // run the test + + // test m.Set(S{"C", "D"}, nil) + // assert assertStates(t, m, S{"C", "D"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRequireRelationWhenRequiredIsntActive(t *testing.T) { m := NewNoRels(t, S{"A"}) defer m.Dispose() + // relations m.states["C"] = State{Require: S{"D"}} + // logger log := "" captureLog(t, m, &log) + // test m.Set(S{"C", "A"}, nil) + // assert assertStates(t, m, S{"A"}, "target state shouldnt be activated") assert.Contains(t, log, "[reject] C(-D)", "log should explain the reason of rejection") + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestQueue @@ -469,17 +567,25 @@ func TestQueue(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // history events := []string{"CEnter", "AExit"} history := trackTransitions(m, events) + // handlers err := m.BindHandlers(&TestQueueHandlers{}) assert.NoError(t, err) + // triggers Add(C) from BEnter m.Set(S{"B"}, nil) + // assert assertEventCounts(t, history, 1) assertStates(t, m, S{"C", "B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestNegotiationCancel @@ -514,24 +620,33 @@ func TestNegotiationCancel(t *testing.T) { regexp.MustCompile(`\[cancel] \(D A\) by DEnter`), }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // bind handlers err := m.BindHandlers(&TestNegotiationCancelHandlers{}) assert.NoError(t, err) + // bind logger log := "" captureLog(t, m, &log) - // run the test + + // test result := test.fn(t, m) + // assert assert.Equal(t, Canceled, result, "transition should be canceled") assertStates(t, m, S{"A"}, "state shouldnt be changed") assert.Regexp(t, test.log, log, "log should explain the reason of cancellation") + + // dispose + m.Dispose() + <-m.WhenDisposed() }) } } @@ -539,7 +654,6 @@ func TestNegotiationCancel(t *testing.T) { func TestAutoStates(t *testing.T) { // init m := NewNoRels(t, nil) - defer m.Dispose() // relations m.states["B"] = State{ Auto: true, @@ -548,7 +662,7 @@ func TestAutoStates(t *testing.T) { // bind logger log := "" captureLog(t, m, &log) - // run the test + // test result := m.Add(S{"A"}, nil) // assert assert.Equal(t, Executed, result, "transition should be executed") @@ -567,6 +681,7 @@ func TestNegotiationRemove(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // bind handlers err := m.BindHandlers(&TestNegotiationRemoveHandlers{}) assert.NoError(t, err) @@ -574,14 +689,20 @@ func TestNegotiationRemove(t *testing.T) { // bind logger log := "" captureLog(t, m, &log) - // run the test + + // test // AExit will cancel the transition result := m.Remove(S{"A"}, nil) + // assert assert.Equal(t, Canceled, result, "transition should be canceled") assertStates(t, m, S{"A"}, "state shouldnt be changed") assert.Regexp(t, `\[cancel] \(\) by AExit`, log, "log should explain the reason of cancellation") + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestHandlerStateInfo @@ -593,27 +714,138 @@ func (h *TestHandlerStateInfoHandlers) DEnter(e *Event) { "provide the previous states of the transition") assert.ElementsMatch(t, S{"D"}, e.Transition().TargetStates, "provide the target states of the transition") + assert.True(t, e.Mutation().StateWasCalled("D"), + "provide the called states of the transition") + tStr := "D -> requested\nD -> set\nA -> remove" + assert.Equal(t, tStr, e.Transition().String(), + "provide a string version of the transition") } func TestHandlerStateInfo(t *testing.T) { // init m := NewNoRels(t, S{"A"}) - defer m.Dispose() + err := m.VerifyStates(S{"A", "B", "C", "D", "Exception"}) + if err != nil { + assert.NoError(t, err) + } + // bind history events := []string{"DEnter"} history := trackTransitions(m, events) + // bind handlers - err := m.BindHandlers(&TestHandlerStateInfoHandlers{}) + err = m.BindHandlers(&TestHandlerStateInfoHandlers{}) assert.NoError(t, err) - // bind logger - log := "" - captureLog(t, m, &log) - // run the test + // test // DEnter will assert m.Set(S{"D"}, A{"t": t}) + // assert assertEventCounts(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestGetters(t *testing.T) { + // init + m := NewNoRels(t, S{"A"}) + mapper := NewArgsMapper([]string{"arg", "arg2"}, 5) + m.SetLogArgs(mapper) + m.SetLogLevel(LogEverything) + + // assert + assert.Equal(t, LogEverything, m.GetLogLevel()) + assert.Equal(t, 0, len(m.Queue())) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestSwitch(t *testing.T) { + // init + m := NewNoRels(t, S{"A"}) + defer m.Dispose() + + caseA := false + switch m.Switch("A", "B") { + case "A": + caseA = true + case "B": + } + assert.Equal(t, true, caseA) + + caseDef := false + switch m.Switch("C", "B") { + case "B": + default: + caseDef = true + } + assert.Equal(t, true, caseDef) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestDispose(t *testing.T) { + // init + m := NewNoRels(t, S{"A"}) + ran := false + m.RegisterDisposalHandler(func() { + ran = true + }) + + // test + m.Dispose() + time.Sleep(10 * time.Millisecond) + assert.Equal(t, true, ran) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestDisposeForce(t *testing.T) { + // init + m := NewNoRels(t, S{"A"}) + ran := false + m.RegisterDisposalHandler(func() { + ran = true + }) + + // test + m.DisposeForce() + assert.Equal(t, true, ran) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestLogArgs(t *testing.T) { + // init + m := NewNoRels(t, S{"A"}) + mapper := NewArgsMapper([]string{"arg", "arg2"}, 5) + m.SetLogArgs(mapper) + + // bind logger + log := "" + captureLog(t, m, &log) + + // run test + args := A{"arg": "foofoofoo"} + m.Add1("D", args) + + // assert + assert.Contains(t, log, "(arg=fo...)", "Arg should be in the log") + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestHandlerArgs @@ -641,20 +873,27 @@ func TestHandlerArgs(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // bind history events := []string{"AExit", "BEnter", "CState"} history := trackTransitions(m, events) + // bind handlers err := m.BindHandlers(&TestHandlerArgsHandlers{}) assert.NoError(t, err) - // run the test + // test // handlers will assert m.Add(S{"B"}, A{"t": t, "foo": "bar"}) m.Remove(S{"A"}, A{"t": t, "foo": "bar"}) m.Set(S{"C"}, A{"t": t, "foo": "bar"}) + // assert assertEventCountsMin(t, history, 1) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestSelfHandlersCancellable @@ -670,33 +909,40 @@ func TestSelfHandlersCancellable(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // bind history events := []string{"AA", "AnyB"} history := trackTransitions(m, events) + // bind handlers err := m.BindHandlers(&TestSelfHandlersCancellableHandlers{}) assert.NoError(t, err) - // run the test + + // test // handlers will assert AACounter := 0 m.Set(S{"A", "B"}, A{"AACounter": &AACounter}) + // assert assert.Equal(t, 1, AACounter, "AA call count") assert.Equal(t, 0, history.Counter["AA"], "AA call count") assert.Equal(t, 0, history.Counter["AnyB"], "AnyB call count") + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestSelfHandlersOrder(t *testing.T) { // init m := NewNoRels(t, S{"A"}) - defer m.Dispose() m.SetLogLevel(LogEverything) // bind history events := []string{"AA", "AnyB", "BEnter"} history := trackTransitions(m, events) - // run the test + // test m.Set(S{"A", "B"}, nil) // wait for history to drain the channel @@ -704,16 +950,22 @@ func TestSelfHandlersOrder(t *testing.T) { // assert assert.Equal(t, events, history.Order) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestSelfHandlersForCalledOnly(t *testing.T) { // init m := NewNoRels(t, S{"A"}) defer m.Dispose() + // bind history events := []string{"AA", "BB"} history := trackTransitions(m, events) - // run the test + + // test m.Add(S{"B"}, nil) m.Add(S{"A"}, nil) @@ -723,6 +975,10 @@ func TestSelfHandlersForCalledOnly(t *testing.T) { // assert assert.Equal(t, 1, history.Counter["AA"], "AA call count") assert.Equal(t, 0, history.Counter["BB"], "BB call count") + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRegressionRemoveCrossBlockedByImplied(t *testing.T) { @@ -733,10 +989,16 @@ func TestRegressionRemoveCrossBlockedByImplied(t *testing.T) { "Z": {Add: S{"B"}}, }) defer m.Dispose() - // run the test + + // test m.Set(S{"Z"}, nil) + // assert assertStates(t, m, S{"Z", "B"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestRegressionImpliedBlockByBeingRemoved(t *testing.T) { @@ -746,12 +1008,17 @@ func TestRegressionImpliedBlockByBeingRemoved(t *testing.T) { "Dry": {Remove: S{"Wet"}}, "Water": {Add: S{"Wet"}, Remove: S{"Dry"}}, }) - defer m.Dispose() - // run the test + + // test m.Set(S{"Dry"}, nil) m.Set(S{"Water"}, nil) + // assert assertStates(t, m, S{"Water", "Wet"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestWhen @@ -766,7 +1033,6 @@ func (h *TestWhenHandlers) AState(e *Event) { }() } -// TODO func TestWhen(t *testing.T) { // init m := NewNoRels(t, nil) @@ -776,12 +1042,50 @@ func TestWhen(t *testing.T) { err := m.BindHandlers(&TestWhenHandlers{}) assert.NoError(t, err) - // run the test + // test m.Set(S{"A"}, nil) <-m.When(S{"B", "C"}, nil) // assert assertStates(t, m, S{"A", "B", "C"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestWhen2(t *testing.T) { + // init + m := NewNoRels(t, nil) + + // test + + ready1 := make(chan struct{}) + pass1 := make(chan struct{}) + go func() { + close(ready1) + <-m.When(S{"A", "B"}, nil) + close(pass1) + }() + + ready2 := make(chan struct{}) + pass2 := make(chan struct{}) + go func() { + close(ready2) + <-m.When(S{"A", "B"}, nil) + close(pass2) + }() + + <-ready1 + <-ready2 + + m.Add(S{"A", "B"}, nil) + + <-pass1 + <-pass2 + + m.Dispose() + <-m.WhenDisposed() } func TestWhenActive(t *testing.T) { @@ -789,11 +1093,15 @@ func TestWhenActive(t *testing.T) { m := NewNoRels(t, S{"A"}) defer m.Dispose() - // run the test + // test <-m.When(S{"A"}, nil) // assert assertStates(t, m, S{"A"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestWhenNot @@ -802,41 +1110,86 @@ type TestWhenNotHandlers struct{} func (h *TestWhenNotHandlers) AState(e *Event) { go func() { time.Sleep(10 * time.Millisecond) - e.Machine.Remove(S{"B"}, nil) + e.Machine.Remove1("B", nil) time.Sleep(10 * time.Millisecond) - e.Machine.Remove(S{"C"}, nil) + e.Machine.Remove1("C", nil) }() } func TestWhenNot(t *testing.T) { // init m := NewNoRels(t, S{"B", "C"}) - defer m.Dispose() + // bind handlers err := m.BindHandlers(&TestWhenNotHandlers{}) assert.NoError(t, err) - // run the test + // test m.Add(S{"A"}, nil) <-m.WhenNot(S{"B", "C"}, nil) + // assert assertStates(t, m, S{"A"}) assertNoException(t, m) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestWhenNot2(t *testing.T) { + // init + m := NewNoRels(t, S{"A", "B"}) + + // test + + ready1 := make(chan struct{}) + pass1 := make(chan struct{}) + go func() { + close(ready1) + <-m.WhenNot(S{"A", "B"}, nil) + close(pass1) + }() + + ready2 := make(chan struct{}) + pass2 := make(chan struct{}) + go func() { + close(ready2) + <-m.WhenNot(S{"A", "B"}, nil) + close(pass2) + }() + + <-ready1 + <-ready2 + + m.Remove(S{"A", "B"}, nil) + + <-pass1 + <-pass2 + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestWhenNotActive(t *testing.T) { // init m := NewNoRels(t, S{"A"}) - defer m.Dispose() - // run the test + + // test <-m.WhenNot(S{"B"}, nil) + // assert assertStates(t, m, S{"A"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestPartialNegotiationPanic type TestPartialNegotiationPanicHandlers struct { - ExceptionHandler + *ExceptionHandler } func (h *TestPartialNegotiationPanicHandlers) BEnter(_ *Event) { @@ -846,25 +1199,30 @@ func (h *TestPartialNegotiationPanicHandlers) BEnter(_ *Event) { func TestPartialNegotiationPanic(t *testing.T) { // init m := NewNoRels(t, S{"A"}) - defer m.Dispose() + // logger log := "" captureLog(t, m, &log) + // bind handlers err := m.BindHandlers(&TestPartialNegotiationPanicHandlers{}) assert.NoError(t, err) - // run the test + // test assert.Equal(t, Canceled, m.Add(S{"B"}, nil)) // assert assertStates(t, m, S{"A", "Exception"}) assert.Regexp(t, `\[cancel] \(B A\) by recover`, log, "log contains the target states and handler") + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestPartialFinalPanic type TestPartialFinalPanicHandlers struct { - ExceptionHandler + *ExceptionHandler } func (h *TestPartialFinalPanicHandlers) BState(_ *Event) { @@ -875,15 +1233,18 @@ func TestPartialFinalPanic(t *testing.T) { // init m := NewNoRels(t, nil) defer m.Dispose() + // logger log := "" captureLog(t, m, &log) + // bind handlers err := m.BindHandlers(&TestPartialFinalPanicHandlers{}) assert.NoError(t, err) - // run the test + // test m.Add(S{"A", "B", "C"}, nil) + // assert assertStates(t, m, S{"A", "Exception"}) assert.Contains(t, log, "[error:add] A B C (BState", @@ -892,11 +1253,15 @@ func TestPartialFinalPanic(t *testing.T) { "log contains the stack trace") assert.Regexp(t, `\[cancel] \(A B C\) by recover`, log, "log contains the target states and handler") + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestStateCtx type TestStateCtxHandlers struct { - ExceptionHandler + *ExceptionHandler callbackCh chan bool } @@ -917,11 +1282,13 @@ func TestStateCtx(t *testing.T) { // init m := NewNoRels(t, nil) defer m.Dispose() + // bind handlers handlers := &TestStateCtxHandlers{} err := m.BindHandlers(handlers) assert.NoError(t, err) - // run the test + + // test // BState will assert stepCh := make(chan bool) m.Add(S{"A"}, A{ @@ -931,13 +1298,18 @@ func TestStateCtx(t *testing.T) { m.Remove(S{"A"}, nil) stepCh <- true <-handlers.callbackCh + // assert assertStates(t, m, S{}) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestQueueCheckable type TestQueueCheckableHandlers struct { - ExceptionHandler + *ExceptionHandler assertsCount int } @@ -961,7 +1333,7 @@ func (h *TestQueueCheckableHandlers) AState(e *Event) { func TestQueueCheckable(t *testing.T) { // init m := NewNoRels(t, nil) - defer m.Dispose() + // bind handlers handlers := &TestQueueCheckableHandlers{} err := m.BindHandlers(handlers) @@ -969,15 +1341,20 @@ func TestQueueCheckable(t *testing.T) { // test m.Add(S{"A"}, A{"t": t}) + // assert assert.Equal(t, 3, handlers.assertsCount, "asserts executed") assertNoException(t, m) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestPartialAuto(t *testing.T) { // init m := NewNoRels(t, nil) - defer m.Dispose() + // relations m.states["C"] = State{ Auto: true, @@ -987,19 +1364,27 @@ func TestPartialAuto(t *testing.T) { Auto: true, Require: S{"B"}, } + // logger log := "" captureLog(t, m, &log) - // run the test + + // test m.Add(S{"A"}, nil) + // assert assertStates(t, m, S{"A"}) assert.Regexp(t, `\[cancel:reject\] [C D]{3}`, log) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestTime(t *testing.T) { // init m := NewNoRels(t, nil) + // relations m.states["B"] = State{Multi: true} @@ -1044,6 +1429,10 @@ func TestTime(t *testing.T) { assertTimes(t, m, S{"A", "B", "C", "D"}, T{3, 7, 5, 1}) assert.True(t, IsTimeAfter(now, before)) assert.False(t, IsTimeAfter(before, now)) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestWhenCtx(t *testing.T) { @@ -1081,6 +1470,10 @@ func TestWhenCtx(t *testing.T) { assert.Equal(t, len(m.indexWhenTime), 0) assert.Equal(t, len(m.indexWhen), 0) assert.Equal(t, len(m.indexWhenArgs), 0) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestWhenArgs(t *testing.T) { @@ -1108,6 +1501,10 @@ func TestWhenArgs(t *testing.T) { case <-whenCh: // pass } + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestWhenTime(t *testing.T) { @@ -1140,20 +1537,54 @@ func TestWhenTime(t *testing.T) { case <-whenCh: // pass } + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +// TestNewCommon +type TestNewCommonHandlers struct { + *ExceptionHandler } +func (h *TestNewCommonHandlers) AState(e *Event) {} + func TestNewCommon(t *testing.T) { - // TODO TestConfig - t.Skip() + // init + s := Struct{"A": {}, Exception: {}} + m, err := NewCommon(context.TODO(), "foo", s, maps.Keys(s), + &TestNewCommonHandlers{}, nil, nil) + + // assert + assert.NoError(t, err) + assert.Equal(t, 1, len(m.emitters)) } +// TestTracers +type TestTracersHandlers struct { + *ExceptionHandler +} + +func (h *TestTracersHandlers) AState(e *Event) {} + func TestTracers(t *testing.T) { - // TODO TestConfig - t.Skip() + tNoop := &NoOpTracer{} + m := New(context.TODO(), Struct{"A": {}}, &Opts{ + Tracers: []Tracer{tNoop}, + }) + _ = m.BindHandlers(&TestTracersHandlers{}) + assert.Equal(t, 1, len(m.Tracers)) + assert.False(t, m.Tracers[0].Inheritable()) + m.Add1("A", nil) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestQueueLimit(t *testing.T) { - // TODO TestConfig + // TODO TestQueueLimit t.Skip() } @@ -1163,7 +1594,7 @@ func TestSubmachines(t *testing.T) { } func TestEval(t *testing.T) { - // TODO TestSubmachines + // TODO TestEval t.Skip() } @@ -1190,6 +1621,10 @@ func TestSetStates(t *testing.T) { // assert assert.ElementsMatch(t, S{"A", "B", "D", "E"}, m.activeStates) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestIs(t *testing.T) { @@ -1200,6 +1635,10 @@ func TestIs(t *testing.T) { // test assert.True(t, m.Is(S{"A", "B"}), "A B should be active") assert.False(t, m.Is(S{"A", "B", "C"}), "A B C shouldnt be active") + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestNot(t *testing.T) { @@ -1211,6 +1650,10 @@ func TestNot(t *testing.T) { assert.False(t, m.Not(S{"A", "B"}), "A B should be active") assert.False(t, m.Not(S{"A", "B", "C"}), "A B C is partially active") assert.True(t, m.Not1("D"), "D is inactive") + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestAny(t *testing.T) { @@ -1221,11 +1664,16 @@ func TestAny(t *testing.T) { // test assert.True(t, m.Any(S{"A", "B"}, S{"C"}), "A B should be active") assert.True(t, m.Any(S{"A", "B", "C"}, S{"A"}), "A B C is partially active") + + // dispose + m.Dispose() + <-m.WhenDisposed() } -func TestIsClock(t *testing.T) { +func TestClocks(t *testing.T) { // init m := NewNoRels(t, nil) + _ = m.VerifyStates(S{"A", "B", "C", "D", "Exception"}) // relations m.states["B"] = State{Multi: true} @@ -1258,6 +1706,20 @@ func TestIsClock(t *testing.T) { m.Add(S{"A", "B"}, nil) assertStates(t, m, S{"D", "A", "B"}) assertClocks(t, m, S{"A", "B", "C", "D"}, T{3, 7, 4, 1}) + + assert.Equal(t, Clocks{ + "A": 3, "B": 7, "C": 4, "D": 1, "Exception": 0, + }, m.Clocks(nil)) + + assert.Equal(t, Clocks{ + "A": 3, "B": 7, + }, m.Clocks(S{"A", "B"})) + + assert.Equal(t, uint64(3), m.Clock("A")) + + // dispose + m.Dispose() + <-m.WhenDisposed() } func TestInspect(t *testing.T) { @@ -1318,7 +1780,20 @@ func TestInspect(t *testing.T) { State: false 0 Multi: true ` - assertString(t, m, expected, names) + assertString(t, m, expected, nil) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestNilCtx(t *testing.T) { + m := New(nil, Struct{"A": {}}, nil) //nolint:all + assert.Greater(t, len(m.ID), 5) + + // dispose + m.Dispose() + <-m.WhenDisposed() } // TestWhenQueueEnds @@ -1336,16 +1811,18 @@ func TestWhenQueueEnds(t *testing.T) { // init m := NewNoRels(t, nil) defer m.Dispose() + // order err := m.VerifyStates(S{"A", "B", "C", "D", "Exception"}) if err != nil { t.Fatal(err) } + // bind handlers err = m.BindHandlers(&TestWhenQueueEndsHandlers{}) assert.NoError(t, err) - // run the test + // test readyGo := make(chan struct{}) readyMut := make(chan struct{}) var queueEnds <-chan struct{} @@ -1359,4 +1836,174 @@ func TestWhenQueueEnds(t *testing.T) { m.Add1("A", A{"readyMut": readyMut, "readyGo": readyGo}) // confirm the queue wait is closed <-queueEnds + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestGetRelationsBetween(t *testing.T) { + // init + m := NewNoRels(t, nil) + defer m.Dispose() + + // relations + m.states["A"] = State{ + Add: S{"B", "C"}, + After: S{"B"}, + } + m.states["B"] = State{ + Remove: S{"C"}, + Add: S{"A"}, + } + m.states["C"] = State{After: S{"D"}} + m.states["D"] = State{Require: S{"A"}} + + getRels := func(from, to string) []Relation { + relations, err := m.Resolver.GetRelationsBetween(from, to) + assert.NoError(t, err) + return relations + } + + // test and assert + rels := getRels("A", "B") + assert.Equal(t, RelationAdd, rels[0]) + assert.Equal(t, RelationAfter, rels[1]) + rels = getRels("B", "C") + assert.Equal(t, RelationRemove, rels[0]) + rels = getRels("C", "D") + assert.Equal(t, RelationAfter, rels[0]) + rels = getRels("D", "A") + assert.Equal(t, RelationRequire, rels[0]) + + _, err := m.Resolver.GetRelationsBetween("Unknown1", "A") + assert.Error(t, err) + + _, err = m.Resolver.GetRelationsBetween("A", "Unknown1") + assert.Error(t, err) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestString(t *testing.T) { + // init + m := NewNoRels(t, S{"A", "B"}) + _ = m.VerifyStates(S{"A", "B", "C", "D", "Exception"}) + defer m.Dispose() + + // test + assert.Equal(t, "(A:1 B:1)", m.String()) + assert.Equal(t, "(A:1 B:1)[C:0 D:0 Exception:0]", m.StringAll()) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +// TestNestedMutation +type TestNestedMutationHandlers struct { + *ExceptionHandler +} + +func (h *TestNestedMutationHandlers) AState(e *Event) { + t := e.Args["t"].(*testing.T) + + e.Machine.Add1("B", nil) + e.Machine.Add1("B", nil) + e.Machine.Add1("B", nil) + assert.Equal(t, 1, len(e.Machine.queue)) + + e.Machine.Remove1("B", nil) + assert.Equal(t, 2, len(e.Machine.queue)) + e.Machine.Remove1("B", nil) + assert.Equal(t, 2, len(e.Machine.queue)) +} + +func TestNestedMutation(t *testing.T) { + // init + m := NewNoRels(t, S{"B"}) + defer m.Dispose() + + // bind handlers + err := m.BindHandlers(&TestNestedMutationHandlers{}) + assert.NoError(t, err) + + // test + m.Add1("A", A{"t": t}) + + // assert + assertStates(t, m, S{"A"}) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestVerifyStates(t *testing.T) { + // init + m := NewNoRels(t, S{"A", "B"}) + + // test + err := m.VerifyStates(S{"A", "A", "B", "Err"}) + assert.Error(t, err) + err = m.VerifyStates(S{"A", "A"}) + assert.Error(t, err) + err = m.VerifyStates(S{"A", "B"}) + assert.Error(t, err) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestLogger(t *testing.T) { + // init + m := NewNoRels(t, nil) + + // test + m.SetTestLogger(t.Logf, LogEverything) + assert.NotNil(t, m.GetLogger()) + assert.Panics(t, func() { + m.SetTestLogger(nil, LogEverything) + }) + // coverage + m.Add1("A", nil) + m.SetLogger(nil) + m.Add1("A", nil) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestHasStateChanged(t *testing.T) { + // init + m := NewNoRels(t, S{"A", "B"}) + + // test + assert.False(t, m.HasStateChanged(S{"A", "B"})) + assert.True(t, m.HasStateChanged(S{"A"})) + + // dispose + m.Dispose() + <-m.WhenDisposed() +} + +func TestOnEventCtxDispose(t *testing.T) { + // init + m := NewNoRels(t, S{"A", "B"}) + + ctx, cancel := context.WithCancel(context.Background()) + m.OnEvent([]string{"AState"}, ctx) + time.Sleep(10 * time.Millisecond) + assert.Len(t, m.indexEventCh, 1) + cancel() + time.Sleep(10 * time.Millisecond) + assert.Len(t, m.indexEventCh, 0) + + // dispose + m.Dispose() + <-m.WhenDisposed() } diff --git a/pkg/machine/misc.go b/pkg/machine/misc.go index cfa38a8..16be728 100644 --- a/pkg/machine/misc.go +++ b/pkg/machine/misc.go @@ -605,7 +605,7 @@ func IsActiveTick(tick uint64) bool { var invalidName = regexp.MustCompile("[^a-z_0-9]+") func NormalizeID(id string) string { - return invalidName.ReplaceAllString(id, "_") + return invalidName.ReplaceAllString(strings.ToLower(id), "_") } // SMerge merges multiple state lists into one, removing duplicates. @@ -703,7 +703,11 @@ func truncateStr(s string, maxLength int) string { if len(s) <= maxLength { return s } - return s[:maxLength-3] + "..." + if maxLength < 5 { + return s[:maxLength] + } else { + return s[:maxLength-3] + "..." + } } type handlerCall struct { diff --git a/pkg/machine/misc_test.go b/pkg/machine/misc_test.go new file mode 100644 index 0000000..7d0b63c --- /dev/null +++ b/pkg/machine/misc_test.go @@ -0,0 +1,130 @@ +package machine + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestWithOpts(t *testing.T) { + // OptsWithDebug + opts := &Opts{ + DontPanicToException: false, + HandlerTimeout: 0, + } + OptsWithDebug(opts) + assert.True(t, opts.DontPanicToException) + assert.Greater(t, opts.HandlerTimeout, time.Duration(0)) + + // OptsWithTracers + tracer := &NoOpTracer{} + OptsWithTracers(opts, tracer) + + assert.Equal(t, tracer, opts.Tracers[0]) + + // OptsWithParentTracers + mach := New(context.TODO(), nil, opts) + mach.SetLogArgs(NewArgsMapper([]string{"arg"}, 10)) + OptsWithParentTracers(opts, mach) + assert.Equal(t, mach.Tracers[0], opts.Tracers[0]) + assert.NotNil(t, opts.LogArgs) +} + +func TestResultString(t *testing.T) { + assert.Equal(t, "executed", Executed.String()) + assert.Equal(t, "canceled", Canceled.String()) + assert.Equal(t, "queued", Queued.String()) +} + +func TestMutationTypeString(t *testing.T) { + assert.Equal(t, "add", MutationAdd.String()) + assert.Equal(t, "remove", MutationRemove.String()) + assert.Equal(t, "set", MutationSet.String()) + assert.Equal(t, "eval", MutationEval.String()) +} + +func TestStepTypeString(t *testing.T) { + assert.Equal(t, "rel", StepRelation.String()) + assert.Equal(t, "handler", StepHandler.String()) + assert.Equal(t, "set", StepSet.String()) + assert.Equal(t, "remove", StepRemove.String()) + assert.Equal(t, "removenotactive", StepRemoveNotActive.String()) + assert.Equal(t, "requested", StepRequested.String()) + assert.Equal(t, "cancel", StepCancel.String()) + assert.Equal(t, "", StepType(0).String()) +} + +func TestRelationString(t *testing.T) { + assert.Equal(t, "after", RelationAfter.String()) + assert.Equal(t, "add", RelationAdd.String()) + assert.Equal(t, "require", RelationRequire.String()) + assert.Equal(t, "remove", RelationRemove.String()) +} + +func TestLogLevelString(t *testing.T) { + assert.Equal(t, "nothing", LogNothing.String()) + assert.Equal(t, "nothing", LogLevel(0).String()) + assert.Equal(t, "changes", LogChanges.String()) + assert.Equal(t, "ops", LogOps.String()) + assert.Equal(t, "decisions", LogDecisions.String()) + assert.Equal(t, "everything", LogEverything.String()) +} + +func TestNewArgsMapper(t *testing.T) { + // short + mapper := NewArgsMapper([]string{"arg", "arg2"}, 2) + res := mapper(A{"arg": "foo"}) + assert.Equal(t, "fo", res["arg"]) + res = mapper(A{"arg": "foo", "arg2": "bar"}) + assert.Equal(t, "fo", res["arg"]) + assert.Equal(t, "ba", res["arg2"]) + + // long + mapper = NewArgsMapper([]string{"arg", "arg2"}, 5) + args := A{"arg": "foofoofoo"} + res = mapper(args) + assert.Equal(t, "fo...", res["arg"]) +} + +func TestParseStruct(t *testing.T) { + s := Struct{ + "A": { + Remove: S{"A", "B", "C"}, + Add: S{"C"}, + After: S{"A"}, + }, + "B": {}, + "C": {}, + } + ex := Struct{ + "A": { + Remove: S{"B"}, + Add: S{"C"}, + After: S{}, + }, + "B": {}, + "C": {}, + } + assert.Equal(t, ex, parseStruct(s)) +} + +func TestSMerge(t *testing.T) { + s := S{"A", "B", "C"} + s2 := S{"C", "D", "E"} + ex := S{"A", "B", "C", "D", "E"} + assert.Equal(t, ex, SMerge(s, s2)) + assert.Equal(t, S{}, SMerge()) +} + +func TestNormalizeID(t *testing.T) { + assert.Equal(t, "foo_bar_baz", NormalizeID("Foo Bar-Baz")) +} + +func TestIsActiveTick(t *testing.T) { + assert.True(t, IsActiveTick(1)) + assert.False(t, IsActiveTick(0)) + assert.False(t, IsActiveTick(6548734)) + assert.True(t, IsActiveTick(6548735)) +}