feat: edge gateway service
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user