Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions runnables/composite/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,41 +232,28 @@ func TestCompositeRunner_Run(t *testing.T) {
})

t.Run("runnable fails during startup", func(t *testing.T) {
// Setup mock runnables
mockRunnable1 := mocks.NewMockRunnable()
mockRunnable1.On("String").Return("runnable1")
mockRunnable1.On("Run", mock.Anything).Return(nil)

mockRunnable2 := mocks.NewMockRunnable()
mockRunnable2.On("String").Return("runnable2")
mockRunnable2.On("Run", mock.Anything).Return(errors.New("failed to start"))
mockRunnable := mocks.NewMockRunnable()
mockRunnable.On("String").Return("runnable1")
mockRunnable.On("Run", mock.Anything).Return(errors.New("failed to start"))

// Create entries
entries := []RunnableEntry[*mocks.Runnable]{
{Runnable: mockRunnable1, Config: nil},
{Runnable: mockRunnable2, Config: nil},
{Runnable: mockRunnable, Config: nil},
}

// Create config callback
configCallback := func() (*Config[*mocks.Runnable], error) {
return NewConfig("test", entries)
}

// Create runner
runner, err := NewRunner(configCallback)
require.NoError(t, err)

// Run
err = runner.Run(context.Background())

// Verify error and state
require.Error(t, err)
require.ErrorContains(t, err, "child runnable failed")
assert.Equal(t, finitestate.StatusError, runner.GetState())

// Verify mock expectations
mockRunnable1.AssertExpectations(t)
mockRunnable2.AssertExpectations(t)
mockRunnable.AssertExpectations(t)
})

t.Run("missing config", func(t *testing.T) {
Expand Down
77 changes: 22 additions & 55 deletions supervisor/supervisor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,48 +536,31 @@ func TestPIDZero_Reap_HandleSIGTERM(t *testing.T) {
// TestPIDZero_Reap_HandleSIGHUP tests that receiving SIGHUP triggers reload.
func TestPIDZero_Reap_HandleSIGHUP(t *testing.T) {
t.Parallel()
mockRunnable := new(mocks.Runnable)
mockRunnable := mocks.NewMockRunnable()
mockRunnable.On("Run", mock.Anything).Return(nil).Once()
mockRunnable.On("Reload").Once()
mockRunnable.On("Stop").Once()

// Setup for Stateable interface
stateChan := make(chan string)
mockRunnable.On("GetState").Return("running").Maybe()
mockRunnable.On("GetStateChan", mock.Anything).Return(stateChan).Maybe()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

pid0, err := New(WithContext(ctx), WithRunnables(mockRunnable))
require.NoError(t, err)

t.Cleanup(func() {
pid0.Shutdown()
})

pid0.listenForSignals()

execDone := make(chan error, 1)
go func() {
execDone <- pid0.Run()
}()

// Wait for services to start and send SIGHUP signal
var signalsSent int
assert.Eventually(t, func() bool {
switch signalsSent {
case 0:
pid0.signalChan <- syscall.SIGHUP
signalsSent++
case 1:
pid0.signalChan <- syscall.SIGINT // Send shutdown after reload
signalsSent++
}
return signalsSent >= 2
}, 200*time.Millisecond, 10*time.Millisecond, "Should send SIGHUP then SIGINT")
pid0.signalChan <- syscall.SIGHUP

// Wait for reload to complete before sending shutdown signal
require.Eventually(t, func() bool {
return !mockRunnable.IsMethodCallable(t, "Reload")
}, 1*time.Second, 10*time.Millisecond, "Reload was not called after SIGHUP")

pid0.signalChan <- syscall.SIGINT

// Wait for Exec to finish
select {
case err := <-execDone:
require.NoError(t, err)
Expand All @@ -588,58 +571,42 @@ func TestPIDZero_Reap_HandleSIGHUP(t *testing.T) {
mockRunnable.AssertExpectations(t)
}

// TestPIDZero_Reap_MultipleSignals tests handling multiple signals.
// TestPIDZero_Reap_MultipleSignals tests that SIGHUP triggers reload on multiple
// runnables, followed by SIGTERM triggering clean shutdown.
func TestPIDZero_Reap_MultipleSignals(t *testing.T) {
t.Parallel()
mockRunnable1 := new(mocks.Runnable)
mockRunnable2 := new(mocks.Runnable)
mockRunnable1 := mocks.NewMockRunnable()
mockRunnable2 := mocks.NewMockRunnable()
mockRunnable1.On("Run", mock.Anything).Return(nil).Once()
mockRunnable2.On("Run", mock.Anything).Return(nil).Once()

// TODO: these should be called by the hup, need to investigate!
mockRunnable1.On("Reload").Once()
mockRunnable2.On("Reload").Once()

mockRunnable1.On("Stop").Once()
mockRunnable2.On("Stop").Once()

// Setup for Stateable interface
stateChan1 := make(chan string)
stateChan2 := make(chan string)
mockRunnable1.On("GetState").Return("running").Maybe()
mockRunnable2.On("GetState").Return("running").Maybe()
mockRunnable1.On("GetStateChan", mock.Anything).Return(stateChan1).Maybe()
mockRunnable2.On("GetStateChan", mock.Anything).Return(stateChan2).Maybe()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

pid0, err := New(WithContext(ctx), WithRunnables(mockRunnable1, mockRunnable2))
require.NoError(t, err)

pid0.listenForSignals()

execDone := make(chan error, 1)
go func() {
// Start the blocking supervisor in the background
execDone <- pid0.Run()
}()

// Wait and send multiple signals
var signalsSent int
assert.Eventually(t, func() bool {
switch signalsSent {
case 0:
pid0.signalChan <- syscall.SIGHUP
signalsSent++
case 1:
pid0.signalChan <- syscall.SIGTERM
signalsSent++
}
return signalsSent >= 2
}, 200*time.Millisecond, 10*time.Millisecond, "Should send SIGHUP then SIGTERM")
pid0.signalChan <- syscall.SIGHUP

// Wait for reload to complete on both runnables before sending shutdown
require.Eventually(t, func() bool {
return !mockRunnable1.IsMethodCallable(t, "Reload") &&
!mockRunnable2.IsMethodCallable(t, "Reload")
}, 1*time.Second, 10*time.Millisecond, "Reload was not called on both runnables after SIGHUP")

pid0.signalChan <- syscall.SIGTERM

// Wait for Exec to finish due to SIGTERM
select {
case err := <-execDone:
require.NoError(t, err)
Expand Down