diff --git a/pkg/services/servicetest/run.go b/pkg/services/servicetest/run.go index f09f58965a..c1a20e82bd 100644 --- a/pkg/services/servicetest/run.go +++ b/pkg/services/servicetest/run.go @@ -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 } @@ -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() { @@ -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 { @@ -77,5 +139,4 @@ func RunHealthy[S services.Service](tb TestingT, s S) S { } } }() - return s }