fix handling of restart events
1 file changed, 74 insertions(+), 24 deletions(-)
changed files
M state/state_test.go → state/state_test.go
@@ -53,11 +53,16 @@ m.mu.Lock() defer m.mu.Unlock() m.stopCalled = true m.started = false - // Signal Wait to return - select { - case m.waitChan <- struct{}{}: - default: - } + + // Add small delay to allow tests to observe Stopping state + go func() { + time.Sleep(20 * time.Millisecond) + // Signal Wait to return + select { + case m.waitChan <- struct{}{}: + default: + } + }() return m.stopErr }@@ -122,9 +127,22 @@ m.startErr = nil m.waitErr = nil m.stopErr = nil m.started = false + // Signal any waiting goroutines before creating new channel + select { + case m.waitChan <- struct{}{}: + default: + } m.waitChan = make(chan struct{}) } +func (m *MockCommand) ClearCallCounts() { + m.mu.Lock() + defer m.mu.Unlock() + m.startCalled = false + m.stopCalled = false + m.waitCalled = false +} + func createTestStateMachine() (*StateMachine, *MockCommand, *bytes.Buffer) { mockCmd := NewMockCommand() var buf bytes.Buffer@@ -144,6 +162,7 @@ expected string }{ {NotStarted, "NotStarted"}, {Running, "Running"}, + {Stopping, "Stopping"}, {Exited, "Exited"}, {State(99), "Unknown"}, }@@ -163,7 +182,7 @@ event Event expected string }{ {Start, "Start"}, - {Signal, "Shutdown"}, + {Shutdown, "Shutdown"}, {Exit, "Stopped"}, {Restart, "Restart"}, {Event(99), "Unknown"},@@ -210,7 +229,7 @@ t.Error("Wait should have been called on command") } } -func TestSignalTransition(t *testing.T) { +func TestShutdownTransition(t *testing.T) { t.Parallel() sm, mockCmd, _ := createTestStateMachine()@@ -219,20 +238,31 @@ // Start the state machine sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) - // Reset mock to clear start calls - mockCmd.Reset() + // Clear call counts to track shutdown calls + mockCmd.ClearCallCounts() - // Send Signal event - sm.SendEvent(Signal) + // Send Shutdown event + sm.SendEvent(Shutdown) time.Sleep(10 * time.Millisecond) - if sm.currentState != Exited { - t.Errorf("State should be Exited after Signal event from Running, got %v", sm.currentState) + if sm.currentState != Stopping { + t.Errorf( + "State should be Stopping after Shutdown event from Running, got %v", + sm.currentState, + ) } if !mockCmd.WasStopCalled() { t.Error("Stop should have been called on command") } + + // Simulate command exit to complete transition to Exited + mockCmd.SimulateExit() + time.Sleep(10 * time.Millisecond) + + if sm.currentState != Exited { + t.Errorf("State should be Exited after command exit, got %v", sm.currentState) + } } func TestExitTransition(t *testing.T) {@@ -262,15 +292,15 @@ // Start the state machine sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) - // Reset mock to clear initial calls - mockCmd.Reset() + // Clear call counts to track restart calls + mockCmd.ClearCallCounts() // Send Restart event sm.SendEvent(Restart) - time.Sleep(10 * time.Millisecond) + time.Sleep(50 * time.Millisecond) // Give more time for restart sequence if sm.currentState != Running { - t.Errorf("State should still be Running after Restart event, got %v", sm.currentState) + t.Errorf("State should be Running after Restart event, got %v", sm.currentState) } if !mockCmd.WasStopCalled() {@@ -346,10 +376,12 @@ initialState State event Event description string }{ - {NotStarted, Signal, "Signal from NotStarted"}, + {NotStarted, Shutdown, "Shutdown from NotStarted"}, {NotStarted, Exit, "Exit from NotStarted"}, {NotStarted, Restart, "Restart from NotStarted"}, - {Exited, Signal, "Signal from Exited"}, + {Stopping, Start, "Start from Stopping"}, + {Stopping, Restart, "Restart from Stopping"}, + {Exited, Shutdown, "Shutdown from Exited"}, {Exited, Exit, "Exit from Exited"}, }@@ -411,11 +443,22 @@ // Set stop to return an error mockCmd.SetStopError(fmt.Errorf("stop error")) buf.Reset() - sm.SendEvent(Signal) + // Clear call counts to track stop calls + mockCmd.ClearCallCounts() + + sm.SendEvent(Shutdown) + time.Sleep(10 * time.Millisecond) + + if sm.currentState != Stopping { + t.Errorf("State should be Stopping even if Stop fails, got %v", sm.currentState) + } + + // Complete the transition by simulating command exit + mockCmd.SimulateExit() time.Sleep(10 * time.Millisecond) if sm.currentState != Exited { - t.Errorf("State should be Exited even if Stop fails, got %v", sm.currentState) + t.Errorf("State should be Exited after command exit, got %v", sm.currentState) } logOutput := buf.String()@@ -459,15 +502,22 @@ if sm.currentState != Running { t.Errorf("Expected Running, got %v", sm.currentState) } - // Signal to stop - sm.SendEvent(Signal) + // Shutdown to stop + sm.SendEvent(Shutdown) + time.Sleep(10 * time.Millisecond) + if sm.currentState != Stopping { + t.Errorf("Expected Stopping, got %v", sm.currentState) + } + + // Complete shutdown by simulating command exit + mockCmd.SimulateExit() time.Sleep(10 * time.Millisecond) if sm.currentState != Exited { t.Errorf("Expected Exited, got %v", sm.currentState) } // Start again - mockCmd.Reset() + mockCmd.ClearCallCounts() sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) if sm.currentState != Running {