@@ -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,81 @@ 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+ 	err  :=  s .Start (tests .Context (tb ))
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+ 	require .NoError (tb , err , "service failed to start: %T" , s )
89+ 
90+ 	tb .Cleanup (func () {
91+ 		tb .Helper ()
92+ 		start  :=  time .Now ()
93+ 		err  :=  s .Close ()
94+ 		if  elapsed  :=  time .Since (start ); cfg .CloseTimeout  >  0  &&  elapsed  >  cfg .CloseTimeout  {
95+ 			tb .Errorf ("slow service close: %T.Close() took %s" , s , elapsed )
96+ 		}
97+ 		assert .NoError (tb , err , "error closing service: %T" , s )
98+ 	})
99+ }
100+ 
101+ func  (cfg  RunCfg ) waitForReady (tb  TestingT , s  services.Service , done  <- chan  struct {}) {
102+ 	for  err  :=  s .Ready (); err  !=  nil ; err  =  s .Ready () {
103+ 		select  {
104+ 		case  <- done :
105+ 			assert .NoError (tb , err , "service never ready" )
106+ 			return 
107+ 		case  <- time .After (time .Second ):
108+ 		}
109+ 	}
110+ }
111+ 
112+ func  (cfg  RunCfg ) healthCheck (tb  TestingT , s  services.Service ) {
113+ 	tb .Helper ()
44114
45115	done  :=  make (chan  struct {})
46116	tb .Cleanup (func () {
@@ -57,6 +127,9 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
57127			}
58128			return 
59129		}
130+ 		if  ! cfg .WaitForReady  {
131+ 			cfg .waitForReady (tb , s , done )
132+ 		}
60133		for  s .Ready () !=  nil  {
61134			select  {
62135			case  <- done :
@@ -77,5 +150,4 @@ func RunHealthy[S services.Service](tb TestingT, s S) S {
77150			}
78151		}
79152	}()
80- 	return  s 
81153}
0 commit comments