269 lines
5.9 KiB
Go
269 lines
5.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/gateway/internal/config"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAppRunWaitsForCancellationWithoutComponents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
application := New(config.Config{ShutdownTimeout: 50 * time.Millisecond})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
resultCh := make(chan error, 1)
|
|
go func() {
|
|
resultCh <- application.Run(ctx)
|
|
}()
|
|
|
|
select {
|
|
case err := <-resultCh:
|
|
require.FailNowf(t, "Run() returned early", "error=%v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-resultCh:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "Run() did not return after cancellation")
|
|
}
|
|
}
|
|
|
|
func TestAppRunCancelsComponentsAndCallsShutdownOnce(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
first := newLifecycleComponent()
|
|
second := newLifecycleComponent()
|
|
|
|
application := New(
|
|
config.Config{ShutdownTimeout: time.Second},
|
|
first,
|
|
second,
|
|
)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
resultCh := make(chan error, 1)
|
|
go func() {
|
|
resultCh <- application.Run(ctx)
|
|
}()
|
|
|
|
first.waitStarted(t)
|
|
second.waitStarted(t)
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-resultCh:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "Run() did not return after cancellation")
|
|
}
|
|
|
|
first.waitRunExited(t)
|
|
second.waitRunExited(t)
|
|
|
|
assert.Equal(t, 1, first.shutdownCalls())
|
|
assert.Equal(t, 1, second.shutdownCalls())
|
|
}
|
|
|
|
func TestAppRunReturnsComponentErrorAndStillShutsDown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runErr := errors.New("boom")
|
|
failing := newFailingComponent(runErr)
|
|
blocking := newLifecycleComponent()
|
|
|
|
application := New(
|
|
config.Config{ShutdownTimeout: time.Second},
|
|
failing,
|
|
blocking,
|
|
)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
resultCh := make(chan error, 1)
|
|
go func() {
|
|
resultCh <- application.Run(ctx)
|
|
}()
|
|
|
|
failing.waitStarted(t)
|
|
blocking.waitStarted(t)
|
|
failing.releaseRun()
|
|
|
|
select {
|
|
case err := <-resultCh:
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, runErr)
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "Run() did not return after component failure")
|
|
}
|
|
|
|
failing.waitRunExited(t)
|
|
blocking.waitRunExited(t)
|
|
|
|
assert.Equal(t, 1, failing.shutdownCalls())
|
|
assert.Equal(t, 1, blocking.shutdownCalls())
|
|
}
|
|
|
|
// lifecycleComponent blocks in Run until the application calls Shutdown.
|
|
type lifecycleComponent struct {
|
|
startedCh chan struct{}
|
|
runDoneCh chan struct{}
|
|
stopCh chan struct{}
|
|
shutdownMu sync.Mutex
|
|
shutdownCnt int
|
|
}
|
|
|
|
// newLifecycleComponent builds a component that exits Run only after Shutdown
|
|
// signals its stop channel.
|
|
func newLifecycleComponent() *lifecycleComponent {
|
|
return &lifecycleComponent{
|
|
startedCh: make(chan struct{}),
|
|
runDoneCh: make(chan struct{}),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Run marks the component as started, waits for cancellation, and then blocks
|
|
// until Shutdown releases the stop channel.
|
|
func (c *lifecycleComponent) Run(ctx context.Context) error {
|
|
close(c.startedCh)
|
|
defer close(c.runDoneCh)
|
|
|
|
<-ctx.Done()
|
|
<-c.stopCh
|
|
return nil
|
|
}
|
|
|
|
// Shutdown records the call and releases the run loop.
|
|
func (c *lifecycleComponent) Shutdown(context.Context) error {
|
|
c.shutdownMu.Lock()
|
|
defer c.shutdownMu.Unlock()
|
|
|
|
c.shutdownCnt++
|
|
if c.shutdownCnt == 1 {
|
|
close(c.stopCh)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// waitStarted blocks until Run has started or fails the test on timeout.
|
|
func (c *lifecycleComponent) waitStarted(t *testing.T) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case <-c.startedCh:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "component did not start")
|
|
}
|
|
}
|
|
|
|
// waitRunExited blocks until Run exits or fails the test on timeout.
|
|
func (c *lifecycleComponent) waitRunExited(t *testing.T) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case <-c.runDoneCh:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "component run did not exit")
|
|
}
|
|
}
|
|
|
|
// shutdownCalls returns the number of observed Shutdown invocations.
|
|
func (c *lifecycleComponent) shutdownCalls() int {
|
|
c.shutdownMu.Lock()
|
|
defer c.shutdownMu.Unlock()
|
|
|
|
return c.shutdownCnt
|
|
}
|
|
|
|
// failingComponent returns a predefined error once released by the test and
|
|
// still tracks shutdown calls.
|
|
type failingComponent struct {
|
|
startedCh chan struct{}
|
|
releaseCh chan struct{}
|
|
runDoneCh chan struct{}
|
|
shutdownMu sync.Mutex
|
|
shutdownCnt int
|
|
err error
|
|
}
|
|
|
|
// newFailingComponent builds a component whose Run returns err after release.
|
|
func newFailingComponent(err error) *failingComponent {
|
|
return &failingComponent{
|
|
startedCh: make(chan struct{}),
|
|
releaseCh: make(chan struct{}),
|
|
runDoneCh: make(chan struct{}),
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
// Run waits until the test releases it and then returns the configured error.
|
|
func (c *failingComponent) Run(context.Context) error {
|
|
close(c.startedCh)
|
|
defer close(c.runDoneCh)
|
|
|
|
<-c.releaseCh
|
|
return c.err
|
|
}
|
|
|
|
// Shutdown records that the application attempted graceful shutdown.
|
|
func (c *failingComponent) Shutdown(context.Context) error {
|
|
c.shutdownMu.Lock()
|
|
defer c.shutdownMu.Unlock()
|
|
|
|
c.shutdownCnt++
|
|
return nil
|
|
}
|
|
|
|
// waitStarted blocks until Run has started or fails the test on timeout.
|
|
func (c *failingComponent) waitStarted(t *testing.T) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case <-c.startedCh:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "failing component did not start")
|
|
}
|
|
}
|
|
|
|
// releaseRun allows Run to return its configured error.
|
|
func (c *failingComponent) releaseRun() {
|
|
close(c.releaseCh)
|
|
}
|
|
|
|
// waitRunExited blocks until Run exits or fails the test on timeout.
|
|
func (c *failingComponent) waitRunExited(t *testing.T) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case <-c.runDoneCh:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "failing component run did not exit")
|
|
}
|
|
}
|
|
|
|
// shutdownCalls returns the number of observed Shutdown invocations.
|
|
func (c *failingComponent) shutdownCalls() int {
|
|
c.shutdownMu.Lock()
|
|
defer c.shutdownMu.Unlock()
|
|
|
|
return c.shutdownCnt
|
|
}
|