@@ -17,11 +17,15 @@ limitations under the License.
17
17
package main
18
18
19
19
import (
20
+ "context"
20
21
"fmt"
22
+ "io"
21
23
"net"
24
+ "net/http"
22
25
"os"
23
26
"reflect"
24
27
"testing"
28
+ "time"
25
29
)
26
30
27
31
func TestMain (t * testing.T ) {
@@ -80,3 +84,97 @@ func TestTrapClosedConnErr(t *testing.T) {
80
84
}
81
85
}
82
86
}
87
+
88
+ func TestServeMetrics (t * testing.T ) {
89
+ // Open random test port
90
+ l , err := net .Listen ("tcp" , "127.0.0.1:0" )
91
+ if err != nil {
92
+ t .Fatalf ("listen: %v" , err )
93
+ }
94
+
95
+ // Start serveMetrics in background
96
+ errCh := make (chan error , 1 )
97
+ go func () { errCh <- serveMetrics (l ) }()
98
+
99
+ // Build URL
100
+ url := "http://" + l .Addr ().String () + "/metrics"
101
+
102
+ // Client timeout for each request
103
+ client := & http.Client {Timeout : 500 * time .Millisecond }
104
+
105
+ // Poll with 3-second deadlines until server is ready
106
+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
107
+ defer cancel ()
108
+
109
+ done := make (chan struct {})
110
+ go func (ctx context.Context ) {
111
+ defer close (done )
112
+ for {
113
+ req , _ := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
114
+ // Execute probe, expecting status 200
115
+ resp , err := client .Do (req )
116
+ if err == nil {
117
+ if resp .StatusCode == http .StatusOK {
118
+ resp .Body .Close ()
119
+ return
120
+ }
121
+ resp .Body .Close ()
122
+ }
123
+ // Abort probe if context deadline is expired or canceled
124
+ select {
125
+ case <- ctx .Done ():
126
+ return
127
+ default :
128
+ time .Sleep (20 * time .Millisecond )
129
+ }
130
+ }
131
+ }(ctx )
132
+
133
+ // Wait for readiness or fail on timeout
134
+ select {
135
+ case <- done :
136
+ case <- ctx .Done ():
137
+ t .Fatalf ("server not ready: %v" , ctx .Err ())
138
+ }
139
+
140
+ // Perform the request
141
+ req , _ := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
142
+ resp , err := client .Do (req )
143
+ if err != nil {
144
+ t .Fatalf ("Get /metrics: %v" , err )
145
+ }
146
+ t .Cleanup (func () {
147
+ if resp != nil && resp .Body != nil {
148
+ _ = resp .Body .Close ()
149
+ }
150
+ })
151
+
152
+ // Check for HTTP status 200
153
+ if resp .StatusCode != http .StatusOK {
154
+ t .Fatalf ("unexpected status: %d" , resp .StatusCode )
155
+ }
156
+ // Validate response body is non-empty
157
+ body , err := io .ReadAll (resp .Body )
158
+ if err != nil {
159
+ t .Fatalf ("read body: %v" , err )
160
+ }
161
+ // Validate response body is non-empty
162
+ if len (body ) == 0 {
163
+ t .Fatalf ("empty metrics body" )
164
+ }
165
+
166
+ // Trigger graceful shutdown
167
+ if err := l .Close (); err != nil {
168
+ t .Fatalf ("close listener %v" , err )
169
+ }
170
+
171
+ // Fail if errCh exits non-graceful or is not closed after 2 sec
172
+ select {
173
+ case err := <- errCh :
174
+ if err != nil {
175
+ t .Fatalf ("serveMetrics error after close: %v" , err )
176
+ }
177
+ case <- time .After (2 * time .Second ):
178
+ t .Fatalf ("serveMetrics did not exit after listener close" )
179
+ }
180
+ }
0 commit comments