Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions pkg/services/servicetest/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ type TestingT interface {
// Run fails tb if the service fails to start or close.
func Run[R Runnable](tb TestingT, r R) R {
tb.Helper()
require.NoError(tb, r.Start(tests.Context(tb)), "service failed to start: %T", r)
tb.Cleanup(func() {
tb.Helper()
assert.NoError(tb, r.Close(), "error closing service: %T", r)
})
RunCfg{}.run(tb, r)
return r
}

Expand All @@ -40,7 +36,79 @@ func Run[R Runnable](tb TestingT, r R) R {
// - if ever ready, then health will be checked at least once, before closing
func RunHealthy[S services.Service](tb TestingT, s S) S {
tb.Helper()
Run(tb, s)
RunCfg{Healthy: true}.Run(tb, s)
return s
}

// RunCfg specifies a test configuration for running a service.
// By default, health checks are not enforced, but Start/Close timeout are.
type RunCfg struct {
// Healthy includes extra checks for whether the service is never ready, or is ever unhealthy (based on periodic checks).
// - after starting, readiness will always be checked at least once, before closing
// - if ever ready, then health will be checked at least once, before closing
Healthy bool
// WaitForReady blocks returning until after Ready() returns nil, after calling Start().
WaitForReady bool
// StartTimeout sets a limit for Start which results in an error if exceeded.
StartTimeout time.Duration
// StartTimeout sets a limit for Close which results in an error if exceeded.
CloseTimeout time.Duration
}

func (cfg RunCfg) Run(tb TestingT, s services.Service) {
tb.Helper()

cfg.run(tb, s)

if cfg.WaitForReady {
ctx := tests.Context(tb)
cfg.waitForReady(tb, s, ctx.Done())
}

if cfg.Healthy {
cfg.healthCheck(tb, s)
}
}

func (cfg RunCfg) run(tb TestingT, s Runnable) {
tb.Helper()
//TODO remove....set from built-ins? or disallow unbounded, so exceptions must be explicit?
if cfg.StartTimeout == 0 {
cfg.StartTimeout = time.Second
}
if cfg.CloseTimeout == 0 {
cfg.CloseTimeout = time.Second
}

start := time.Now()
require.NoError(tb, s.Start(tests.Context(tb)), "service failed to start: %T", s)
if elapsed := time.Since(start); cfg.StartTimeout > 0 && elapsed > cfg.StartTimeout {
tb.Errorf("slow service start: %T.Start() took %s", s, elapsed)
}

tb.Cleanup(func() {
tb.Helper()
start := time.Now()
assert.NoError(tb, s.Close(), "error closing service: %T", s)
if elapsed := time.Since(start); cfg.CloseTimeout > 0 && elapsed > cfg.CloseTimeout {
tb.Errorf("slow service close: %T.Close() took %s", s, elapsed)
}
})
}

func (cfg RunCfg) waitForReady(tb TestingT, s services.Service, done <-chan struct{}) {
for err := s.Ready(); err != nil; err = s.Ready() {
select {
case <-done:
assert.NoError(tb, err, "service never ready")
return
case <-time.After(time.Second):
}
}
}

func (cfg RunCfg) healthCheck(tb TestingT, s services.Service) {
tb.Helper()

done := make(chan struct{})
tb.Cleanup(func() {
Expand All @@ -57,15 +125,9 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
}
return
}
for s.Ready() != nil {
select {
case <-done:
if assert.NoError(tb, s.Ready(), "service never ready") {
assert.NoError(tb, hp(), "service unhealthy")
}
return
case <-time.After(time.Second):
}
if !cfg.WaitForReady {
cfg.waitForReady(tb, s, done)
assert.NoError(tb, hp(), "service unhealthy")
}
for {
select {
Expand All @@ -77,5 +139,4 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
}
}
}()
return s
}
Loading