Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

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()
})
}