@@ -27,11 +27,7 @@ type TestingT interface {
2727// Run fails tb if the service fails to start or close.
2828func Run [R Runnable ](tb TestingT , r R ) R {
2929 tb .Helper ()
30- require .NoError (tb , r .Start (tests .Context (tb )), "service failed to start: %T" , r )
31- tb .Cleanup (func () {
32- tb .Helper ()
33- assert .NoError (tb , r .Close (), "error closing service: %T" , r )
34- })
30+ RunCfg {}.run (tb , r )
3531 return r
3632}
3733
@@ -40,7 +36,79 @@ func Run[R Runnable](tb TestingT, r R) R {
4036// - if ever ready, then health will be checked at least once, before closing
4137func RunHealthy [S services.Service ](tb TestingT , s S ) S {
4238 tb .Helper ()
43- Run (tb , s )
39+ RunCfg {Healthy : true }.Run (tb , s )
40+ return s
41+ }
42+
43+ // RunCfg specifies a test configuration for running a service.
44+ // By default, health checks are not enforced, but Start/Close timeout are.
45+ type RunCfg struct {
46+ // Healthy includes extra checks for whether the service is never ready, or is ever unhealthy (based on periodic checks).
47+ // - after starting, readiness will always be checked at least once, before closing
48+ // - if ever ready, then health will be checked at least once, before closing
49+ Healthy bool
50+ // WaitForReady blocks returning until after Ready() returns nil, after calling Start().
51+ WaitForReady bool
52+ // StartTimeout sets a limit for Start which results in an error if exceeded.
53+ StartTimeout time.Duration
54+ // StartTimeout sets a limit for Close which results in an error if exceeded.
55+ CloseTimeout time.Duration
56+ }
57+
58+ func (cfg RunCfg ) Run (tb TestingT , s services.Service ) {
59+ tb .Helper ()
60+
61+ cfg .run (tb , s )
62+
63+ if cfg .WaitForReady {
64+ ctx := tests .Context (tb )
65+ cfg .waitForReady (tb , s , ctx .Done ())
66+ }
67+
68+ if cfg .Healthy {
69+ cfg .healthCheck (tb , s )
70+ }
71+ }
72+
73+ func (cfg RunCfg ) run (tb TestingT , s Runnable ) {
74+ tb .Helper ()
75+ //TODO remove....set from built-ins? or disallow unbounded, so exceptions must be explicit?
76+ if cfg .StartTimeout == 0 {
77+ cfg .StartTimeout = time .Second
78+ }
79+ if cfg .CloseTimeout == 0 {
80+ cfg .CloseTimeout = time .Second
81+ }
82+
83+ start := time .Now ()
84+ require .NoError (tb , s .Start (tests .Context (tb )), "service failed to start: %T" , s )
85+ if elapsed := time .Since (start ); cfg .StartTimeout > 0 && elapsed > cfg .StartTimeout {
86+ tb .Errorf ("slow service start: %T.Start() took %s" , s , elapsed )
87+ }
88+
89+ tb .Cleanup (func () {
90+ tb .Helper ()
91+ start := time .Now ()
92+ assert .NoError (tb , s .Close (), "error closing service: %T" , s )
93+ if elapsed := time .Since (start ); cfg .CloseTimeout > 0 && elapsed > cfg .CloseTimeout {
94+ tb .Errorf ("slow service close: %T.Close() took %s" , s , elapsed )
95+ }
96+ })
97+ }
98+
99+ func (cfg RunCfg ) waitForReady (tb TestingT , s services.Service , done <- chan struct {}) {
100+ for err := s .Ready (); err != nil ; err = s .Ready () {
101+ select {
102+ case <- done :
103+ assert .NoError (tb , err , "service never ready" )
104+ return
105+ case <- time .After (time .Second ):
106+ }
107+ }
108+ }
109+
110+ func (cfg RunCfg ) healthCheck (tb TestingT , s services.Service ) {
111+ tb .Helper ()
44112
45113 done := make (chan struct {})
46114 tb .Cleanup (func () {
@@ -57,15 +125,9 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
57125 }
58126 return
59127 }
60- for s .Ready () != nil {
61- select {
62- case <- done :
63- if assert .NoError (tb , s .Ready (), "service never ready" ) {
64- assert .NoError (tb , hp (), "service unhealthy" )
65- }
66- return
67- case <- time .After (time .Second ):
68- }
128+ if ! cfg .WaitForReady {
129+ cfg .waitForReady (tb , s , done )
130+ assert .NoError (tb , hp (), "service unhealthy" )
69131 }
70132 for {
71133 select {
@@ -77,5 +139,4 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
77139 }
78140 }
79141 }()
80- return s
81142}
0 commit comments