395 lines
11 KiB
Go
395 lines
11 KiB
Go
// Copyright 2023 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package svcs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestController(t *testing.T) {
|
|
t.Run("NewController", func(t *testing.T) {
|
|
c := NewController()
|
|
require.NotNil(t, c)
|
|
})
|
|
t.Run("Stop", func(t *testing.T) {
|
|
t.Run("CalledBeforeStart", func(t *testing.T) {
|
|
c := NewController()
|
|
c.Stop()
|
|
require.Error(t, c.Start(context.Background()))
|
|
require.NoError(t, c.WaitForStart())
|
|
require.NoError(t, c.WaitForStop())
|
|
})
|
|
t.Run("ReturnsFirstError", func(t *testing.T) {
|
|
c := NewController()
|
|
ctx := context.Background()
|
|
err := errors.New("first")
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return errors.New("second") },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return err },
|
|
}))
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.ErrorIs(t, c.Start(ctx), err)
|
|
require.ErrorIs(t, c.WaitForStop(), err)
|
|
wg.Wait()
|
|
})
|
|
})
|
|
t.Run("EmptyServices", func(t *testing.T) {
|
|
c := NewController()
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.NoError(t, c.Start(ctx))
|
|
require.NoError(t, c.WaitForStop())
|
|
wg.Wait()
|
|
})
|
|
t.Run("Register", func(t *testing.T) {
|
|
t.Run("AfterStartCalled", func(t *testing.T) {
|
|
c := NewController()
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
require.Error(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
c.Stop()
|
|
}()
|
|
require.NoError(t, c.Start(ctx))
|
|
require.NoError(t, c.WaitForStop())
|
|
wg.Wait()
|
|
})
|
|
})
|
|
t.Run("Start", func(t *testing.T) {
|
|
t.Run("CallsInitInOrder", func(t *testing.T) {
|
|
c := NewController()
|
|
var inited []int
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 0)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 1)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 2)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.NoError(t, c.Start(ctx))
|
|
require.NoError(t, c.WaitForStop())
|
|
require.Equal(t, inited, []int{0, 1, 2})
|
|
wg.Wait()
|
|
})
|
|
t.Run("StopsCallingInitOnFirstError", func(t *testing.T) {
|
|
err := errors.New("first error")
|
|
c := NewController()
|
|
var inited []int
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 0)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 1)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
return err
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
inited = append(inited, 2)
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.ErrorIs(t, c.WaitForStart(), err)
|
|
c.Stop()
|
|
}()
|
|
require.ErrorIs(t, c.Start(ctx), err)
|
|
require.ErrorIs(t, c.WaitForStop(), err)
|
|
require.Equal(t, inited, []int{0, 1})
|
|
wg.Wait()
|
|
})
|
|
t.Run("CallsStopWhenInitErrors", func(t *testing.T) {
|
|
err := errors.New("first error")
|
|
c := NewController()
|
|
var stopped []int
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
stopped = append(stopped, 0)
|
|
return nil
|
|
},
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
stopped = append(stopped, 1)
|
|
return nil
|
|
},
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
return err
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
stopped = append(stopped, 2)
|
|
return nil
|
|
},
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error {
|
|
return nil
|
|
},
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
stopped = append(stopped, 3)
|
|
return nil
|
|
},
|
|
}))
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.ErrorIs(t, c.WaitForStart(), err)
|
|
c.Stop()
|
|
}()
|
|
require.ErrorIs(t, c.Start(ctx), err)
|
|
require.ErrorIs(t, c.WaitForStop(), err)
|
|
require.Equal(t, stopped, []int{1, 0})
|
|
wg.Wait()
|
|
})
|
|
t.Run("RunsServices", func(t *testing.T) {
|
|
c := NewController()
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) { wg.Done() },
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) { wg.Done() },
|
|
StopF: func(RunState) error { return nil },
|
|
}))
|
|
ctx := context.Background()
|
|
var cwg sync.WaitGroup
|
|
cwg.Add(1)
|
|
go func() {
|
|
defer cwg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.NoError(t, c.Start(ctx))
|
|
require.NoError(t, c.WaitForStop())
|
|
wg.Wait()
|
|
cwg.Wait()
|
|
})
|
|
t.Run("StopsAllServices", func(t *testing.T) {
|
|
c := NewController()
|
|
var wg sync.WaitGroup
|
|
err := errors.New("first error")
|
|
wg.Add(2)
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
wg.Done()
|
|
return errors.New("second error")
|
|
},
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return nil },
|
|
RunF: func(context.Context) {},
|
|
StopF: func(RunState) error {
|
|
wg.Done()
|
|
return err
|
|
},
|
|
}))
|
|
ctx := context.Background()
|
|
var cwg sync.WaitGroup
|
|
cwg.Add(1)
|
|
go func() {
|
|
defer cwg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.ErrorIs(t, c.Start(ctx), err)
|
|
require.ErrorIs(t, c.WaitForStop(), err)
|
|
wg.Wait()
|
|
cwg.Wait()
|
|
})
|
|
})
|
|
t.Run("RunState", func(t *testing.T) {
|
|
t.Run("RunInvokedOnNormalStop", func(t *testing.T) {
|
|
c := NewController()
|
|
var receivedState RunState = -1
|
|
require.NoError(t, c.Register(&AnonService{
|
|
RunF: func(context.Context) {},
|
|
StopF: func(rs RunState) error { receivedState = rs; return nil },
|
|
}))
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
require.NoError(t, c.WaitForStart())
|
|
c.Stop()
|
|
}()
|
|
require.NoError(t, c.Start(ctx))
|
|
require.NoError(t, c.WaitForStop())
|
|
require.Equal(t, RunInvoked, receivedState)
|
|
wg.Wait()
|
|
})
|
|
t.Run("RunNotInvokedOnInitFailure", func(t *testing.T) {
|
|
c := NewController()
|
|
var receivedState RunState = -1
|
|
initErr := errors.New("init failed")
|
|
require.NoError(t, c.Register(&AnonService{
|
|
StopF: func(rs RunState) error { receivedState = rs; return nil },
|
|
}))
|
|
require.NoError(t, c.Register(&AnonService{
|
|
InitF: func(context.Context) error { return initErr },
|
|
}))
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c.WaitForStart()
|
|
c.Stop()
|
|
}()
|
|
require.ErrorIs(t, c.Start(ctx), initErr)
|
|
require.Equal(t, RunNotInvoked, receivedState)
|
|
wg.Wait()
|
|
})
|
|
})
|
|
t.Run("RunStopsControllerExample", func(t *testing.T) {
|
|
// |Run| has no way to return an error, but it *can* use the
|
|
// controller itself to coordinate a shutdown of all the
|
|
// services and to ensure that an error is returned from its
|
|
// Service's |Close| method.
|
|
c := NewController()
|
|
ctx := context.Background()
|
|
|
|
expectedErr := errors.New("error set from run")
|
|
errCh := make(chan error)
|
|
var runErr error
|
|
var runWg sync.WaitGroup
|
|
runWg.Add(1)
|
|
failingService := &AnonService{
|
|
RunF: func(context.Context) {
|
|
runErr = <-errCh
|
|
// Do this in the background, since it will block on StopF down below being completed.
|
|
go c.Stop()
|
|
runWg.Done()
|
|
},
|
|
StopF: func(rs RunState) error {
|
|
if rs == RunInvoked {
|
|
runWg.Wait()
|
|
}
|
|
return runErr
|
|
},
|
|
}
|
|
c.Register(failingService)
|
|
|
|
// See how we do not call |Stop| on the controller here. The
|
|
// "failing" Run method of the failingService will call it.
|
|
var cwg sync.WaitGroup
|
|
cwg.Add(1)
|
|
go func() {
|
|
defer cwg.Done()
|
|
require.ErrorIs(t, c.Start(ctx), expectedErr)
|
|
}()
|
|
require.NoError(t, c.WaitForStart())
|
|
errCh <- expectedErr
|
|
require.ErrorIs(t, c.WaitForStop(), expectedErr)
|
|
cwg.Wait()
|
|
})
|
|
}
|