174 lines
3.8 KiB
Go
174 lines
3.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/lobby/internal/config"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type fakeComponent struct {
|
|
runErr error
|
|
shutdownErr error
|
|
runHook func(context.Context) error
|
|
shutdownHook func(context.Context) error
|
|
runCount atomic.Int32
|
|
downCount atomic.Int32
|
|
blockForCtx bool
|
|
}
|
|
|
|
func (component *fakeComponent) Run(ctx context.Context) error {
|
|
component.runCount.Add(1)
|
|
if component.runHook != nil {
|
|
return component.runHook(ctx)
|
|
}
|
|
if component.blockForCtx {
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
|
|
return component.runErr
|
|
}
|
|
|
|
func (component *fakeComponent) Shutdown(ctx context.Context) error {
|
|
component.downCount.Add(1)
|
|
if component.shutdownHook != nil {
|
|
return component.shutdownHook(ctx)
|
|
}
|
|
|
|
return component.shutdownErr
|
|
}
|
|
|
|
func newCfg() config.Config {
|
|
return config.Config{ShutdownTimeout: time.Second}
|
|
}
|
|
|
|
func TestAppValidateRejectsNonPositiveTimeout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := New(config.Config{}, &fakeComponent{blockForCtx: true})
|
|
err := app.Run(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "shutdown timeout must be positive")
|
|
}
|
|
|
|
func TestAppValidateRejectsNilComponent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := New(newCfg(), nil)
|
|
err := app.Run(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "component 0 is nil")
|
|
}
|
|
|
|
func TestAppRunCancelledByContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
component := &fakeComponent{blockForCtx: true}
|
|
app := New(newCfg(), component)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- app.Run(ctx)
|
|
}()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("app did not stop after cancellation")
|
|
}
|
|
|
|
assert.Equal(t, int32(1), component.runCount.Load())
|
|
assert.Equal(t, int32(1), component.downCount.Load())
|
|
}
|
|
|
|
func TestAppRunPropagatesComponentError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
failing := &fakeComponent{runErr: errors.New("boom")}
|
|
blocking := &fakeComponent{blockForCtx: true}
|
|
app := New(newCfg(), failing, blocking)
|
|
|
|
err := app.Run(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "boom")
|
|
assert.Equal(t, int32(1), blocking.downCount.Load())
|
|
}
|
|
|
|
func TestAppRunEarlyCleanExit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
short := &fakeComponent{} // returns immediately without error
|
|
blocking := &fakeComponent{blockForCtx: true}
|
|
app := New(newCfg(), short, blocking)
|
|
|
|
err := app.Run(context.Background())
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "exited without error before shutdown")
|
|
}
|
|
|
|
func TestAppRunShutdownCollectsErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
component := &fakeComponent{
|
|
blockForCtx: true,
|
|
shutdownErr: errors.New("shutdown-boom"),
|
|
}
|
|
app := New(newCfg(), component)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- app.Run(ctx)
|
|
}()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
|
|
err := <-errCh
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "shutdown-boom")
|
|
}
|
|
|
|
func TestAppRunNoComponentsBlocksUntilCancel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := New(newCfg())
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- app.Run(ctx)
|
|
}()
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("app did not exit after cancel")
|
|
}
|
|
}
|
|
|
|
func TestAppRunNilContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := New(newCfg(), &fakeComponent{blockForCtx: true})
|
|
err := app.Run(nil) //nolint:staticcheck // test exercises the nil-context guard.
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "nil context")
|
|
}
|