Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package lhttp | 5 package lhttp |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "bytes" | 8 "bytes" |
| 9 "encoding/json" | 9 "encoding/json" |
| 10 "fmt" | 10 "fmt" |
| 11 "io" | 11 "io" |
| 12 "io/ioutil" | 12 "io/ioutil" |
| 13 "net/http" | 13 "net/http" |
| 14 "net/http/httptest" | 14 "net/http/httptest" |
| 15 "sync" | |
| 15 "testing" | 16 "testing" |
| 16 | 17 |
| 17 "golang.org/x/net/context" | 18 "golang.org/x/net/context" |
| 18 | 19 |
| 19 "github.com/luci/luci-go/common/errors" | |
| 20 "github.com/luci/luci-go/common/retry" | 20 "github.com/luci/luci-go/common/retry" |
| 21 | 21 |
| 22 "github.com/maruel/ut" | 22 "github.com/maruel/ut" |
| 23 ) | 23 ) |
| 24 | 24 |
| 25 func httpReqGen(method, url string, body []byte) RequestGen { | 25 func httpReqGen(method, url string, body []byte) RequestGen { |
| 26 return func() (*http.Request, error) { | 26 return func() (*http.Request, error) { |
| 27 var bodyReader io.Reader | 27 var bodyReader io.Reader |
| 28 if body != nil { | 28 if body != nil { |
| 29 bodyReader = bytes.NewReader(body) | 29 bodyReader = bytes.NewReader(body) |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 118 defer ts.Close() | 118 defer ts.Close() |
| 119 | 119 |
| 120 httpReq := httpReqGen("GET", ts.URL, nil) | 120 httpReq := httpReqGen("GET", ts.URL, nil) |
| 121 | 121 |
| 122 clientReq := NewRequest(ctx, http.DefaultClient, fast, httpReq, func(res p *http.Response) error { | 122 clientReq := NewRequest(ctx, http.DefaultClient, fast, httpReq, func(res p *http.Response) error { |
| 123 t.Fail() | 123 t.Fail() |
| 124 return nil | 124 return nil |
| 125 }) | 125 }) |
| 126 | 126 |
| 127 status, err := clientReq() | 127 status, err := clientReq() |
| 128 » ut.AssertEqual(t, true, errors.IsTransient(err)) | 128 » ut.AssertEqual(t, "http request failed: Internal Server Error (HTTP 500) (attempts: 4)", err.Error()) |
|
mcgreevy
2016/12/12 23:57:39
Why are you removing these IsTransient checks?
djd-OOO-Apr2017
2016/12/13 00:18:09
I've changed Retry so that it now wraps the final
| |
| 129 » ut.AssertEqual(t, "http request failed: Internal Server Error (HTTP 500) ", err.Error()) | |
| 130 ut.AssertEqual(t, 500, status) | 129 ut.AssertEqual(t, 500, status) |
| 131 } | 130 } |
| 132 | 131 |
| 133 func TestGetJSON(t *testing.T) { | 132 func TestGetJSON(t *testing.T) { |
| 134 ctx := context.Background() | 133 ctx := context.Background() |
| 135 | 134 |
| 136 // First call returns HTTP 500, second succeeds. | 135 // First call returns HTTP 500, second succeeds. |
| 137 serverCalls := 0 | 136 serverCalls := 0 |
| 138 ts := httptest.NewServer(handlerJSON(t, func(body io.Reader) interface{} { | 137 ts := httptest.NewServer(handlerJSON(t, func(body io.Reader) interface{} { |
| 139 serverCalls++ | 138 serverCalls++ |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 162 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 161 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 163 serverCalls++ | 162 serverCalls++ |
| 164 w.Header().Set("Content-Type", jsonContentType) | 163 w.Header().Set("Content-Type", jsonContentType) |
| 165 _, err := io.WriteString(w, "yo") | 164 _, err := io.WriteString(w, "yo") |
| 166 ut.ExpectEqual(t, nil, err) | 165 ut.ExpectEqual(t, nil, err) |
| 167 })) | 166 })) |
| 168 defer ts.Close() | 167 defer ts.Close() |
| 169 | 168 |
| 170 actual := map[string]string{} | 169 actual := map[string]string{} |
| 171 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, &actual) | 170 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, &actual) |
| 172 » ut.AssertEqual(t, true, errors.IsTransient(err)) | 171 » ut.AssertEqual(t, "bad response "+ts.URL+": invalid character 'y' lookin g for beginning of value (attempts: 4)", err.Error()) |
| 173 » ut.AssertEqual(t, "bad response "+ts.URL+": invalid character 'y' lookin g for beginning of value", err.Error()) | |
| 174 ut.AssertEqual(t, 200, status) | 172 ut.AssertEqual(t, 200, status) |
| 175 ut.AssertEqual(t, map[string]string{}, actual) | 173 ut.AssertEqual(t, map[string]string{}, actual) |
| 176 } | 174 } |
| 177 | 175 |
| 178 func TestGetJSONBadResultIgnore(t *testing.T) { | 176 func TestGetJSONBadResultIgnore(t *testing.T) { |
| 179 ctx := context.Background() | 177 ctx := context.Background() |
| 180 | 178 |
| 181 serverCalls := 0 | 179 serverCalls := 0 |
| 182 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 180 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 183 serverCalls++ | 181 serverCalls++ |
| 184 w.Header().Set("Content-Type", jsonContentType) | 182 w.Header().Set("Content-Type", jsonContentType) |
| 185 _, err := io.WriteString(w, "yo") | 183 _, err := io.WriteString(w, "yo") |
| 186 ut.ExpectEqual(t, nil, err) | 184 ut.ExpectEqual(t, nil, err) |
| 187 })) | 185 })) |
| 188 defer ts.Close() | 186 defer ts.Close() |
| 189 | 187 |
| 190 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil) | 188 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil) |
| 191 » ut.AssertEqual(t, true, errors.IsTransient(err)) | 189 » ut.AssertEqual(t, "bad response "+ts.URL+": invalid character 'y' lookin g for beginning of value (attempts: 4)", err.Error()) |
| 192 » ut.AssertEqual(t, "bad response "+ts.URL+": invalid character 'y' lookin g for beginning of value", err.Error()) | |
| 193 ut.AssertEqual(t, 200, status) | 190 ut.AssertEqual(t, 200, status) |
| 194 } | 191 } |
| 195 | 192 |
| 196 func TestGetJSONBadContentTypeIgnore(t *testing.T) { | 193 func TestGetJSONBadContentTypeIgnore(t *testing.T) { |
| 197 ctx := context.Background() | 194 ctx := context.Background() |
| 198 | 195 |
| 199 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 196 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 200 _, err := io.WriteString(w, "{}") | 197 _, err := io.WriteString(w, "{}") |
| 201 ut.ExpectEqual(t, nil, err) | 198 ut.ExpectEqual(t, nil, err) |
| 202 })) | 199 })) |
| 203 defer ts.Close() | 200 defer ts.Close() |
| 204 | 201 |
| 205 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil) | 202 status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil) |
| 206 » ut.AssertEqual(t, "unexpected Content-Type, expected \"application/json\ ", got \"text/plain; charset=utf-8\"", err.Error()) | 203 » ut.AssertEqual(t, "unexpected Content-Type, expected \"application/json\ ", got \"text/plain; charset=utf-8\" (attempts: 4)", err.Error()) |
| 207 ut.AssertEqual(t, 200, status) | 204 ut.AssertEqual(t, 200, status) |
| 208 } | 205 } |
| 209 | 206 |
| 210 func TestPostJSON(t *testing.T) { | 207 func TestPostJSON(t *testing.T) { |
| 211 ctx := context.Background() | 208 ctx := context.Background() |
| 212 | 209 |
| 213 // First call returns HTTP 500, second succeeds. | 210 // First call returns HTTP 500, second succeeds. |
| 214 serverCalls := 0 | 211 serverCalls := 0 |
| 215 ts := httptest.NewServer(handlerJSON(t, func(body io.Reader) interface{} { | 212 ts := httptest.NewServer(handlerJSON(t, func(body io.Reader) interface{} { |
| 216 serverCalls++ | 213 serverCalls++ |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 246 serverCalls++ | 243 serverCalls++ |
| 247 })) | 244 })) |
| 248 defer ts.Close() | 245 defer ts.Close() |
| 249 | 246 |
| 250 status, err := PostJSON(ctx, fast, http.DefaultClient, ts.URL, map[strin g]string{"key": "value"}, nil, nil) | 247 status, err := PostJSON(ctx, fast, http.DefaultClient, ts.URL, map[strin g]string{"key": "value"}, nil, nil) |
| 251 ut.AssertEqual(t, nil, err) | 248 ut.AssertEqual(t, nil, err) |
| 252 ut.AssertEqual(t, 200, status) | 249 ut.AssertEqual(t, 200, status) |
| 253 ut.AssertEqual(t, 1, serverCalls) | 250 ut.AssertEqual(t, 1, serverCalls) |
| 254 } | 251 } |
| 255 | 252 |
| 253 func TestNewRequestClosesBody(t *testing.T) { | |
| 254 ctx := context.Background() | |
| 255 serverCalls := 0 | |
| 256 | |
| 257 // Return a 500 for the first 2 requests. | |
| 258 ts := httptest.NewServer( | |
| 259 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 260 defer r.Body.Close() | |
| 261 serverCalls++ | |
| 262 if serverCalls <= 2 { | |
| 263 w.WriteHeader(500) | |
| 264 } | |
| 265 fmt.Fprintf(w, "Hello World!\n") | |
| 266 })) | |
| 267 defer ts.Close() | |
| 268 | |
| 269 rt := &trackingRoundTripper{RoundTripper: http.DefaultTransport} | |
| 270 hc := &http.Client{Transport: rt} | |
| 271 httpReq := httpReqGen("GET", ts.URL, nil) | |
| 272 | |
| 273 clientCalls := 0 | |
| 274 var lastResp *http.Response | |
| 275 req := NewRequest(ctx, hc, fast, httpReq, func(resp *http.Response) erro r { | |
| 276 clientCalls++ | |
| 277 lastResp = resp | |
| 278 return resp.Body.Close() | |
| 279 }) | |
| 280 | |
| 281 status, err := req() | |
| 282 if err != nil { | |
| 283 t.Fatalf("req returned err %v, want nil", err) | |
| 284 } | |
| 285 if got, want := status, http.StatusOK; got != want { | |
|
M-A Ruel
2016/12/12 17:16:39
That's an unusual style (to me) but it's fine.
In
djd-OOO-Apr2017
2016/12/13 00:18:09
I agree. I prefer the work "actual" much more than
M-A Ruel
2016/12/13 01:55:59
That's totally bikeshedding level at that point, I
| |
| 286 t.Errorf("req returned status %d, want %d", got, want) | |
| 287 } | |
| 288 | |
| 289 // We expect only one client call, but three requests through to the ser ver. | |
| 290 if got, want := clientCalls, 1; got != want { | |
| 291 t.Errorf("handler callback invoked %d times, want %d", got, want ) | |
| 292 } | |
| 293 if got, want := len(rt.Responses), 3; got != want { | |
| 294 t.Errorf("len(Responses) = %d, want %d", got, want) | |
| 295 } | |
| 296 | |
| 297 // Check that the last response is the one we handled, and that all the bodies | |
| 298 // were closed. | |
| 299 if got, want := lastResp, rt.Responses[2]; got != want { | |
| 300 t.Errorf("Last Response did not match Response in handler callba ck.\nGot: %v\nWant: %v", got, want) | |
| 301 } | |
| 302 for i, resp := range rt.Responses { | |
| 303 rc := resp.Body.(*trackingReadCloser) | |
| 304 if !rc.Closed { | |
| 305 t.Errorf("Reponses[%d].Body was not closed", i) | |
| 306 } | |
| 307 } | |
| 308 } | |
| 309 | |
| 310 // trackingRoundTripper wraps an http.RoundTripper, keeping track of any | |
| 311 // returned Responses. Each response's Body, when set, is wrapped with a | |
| 312 // trackingReadCloser. | |
| 313 type trackingRoundTripper struct { | |
| 314 http.RoundTripper | |
| 315 Responses []*http.Response | |
| 316 mu sync.Mutex | |
|
M-A Ruel
2016/12/12 17:16:39
switch the order to make it clear what mu protects
djd-OOO-Apr2017
2016/12/13 00:18:09
Done.
| |
| 317 } | |
| 318 | |
| 319 func (t *trackingRoundTripper) RoundTrip(req *http.Request) (*http.Response, err or) { | |
| 320 resp, err := t.RoundTripper.RoundTrip(req) | |
| 321 if resp != nil && resp.Body != nil { | |
| 322 resp.Body = &trackingReadCloser{ReadCloser: resp.Body} | |
| 323 } | |
| 324 t.mu.Lock() | |
| 325 defer t.mu.Unlock() | |
| 326 t.Responses = append(t.Responses, resp) | |
| 327 return resp, err | |
| 328 } | |
| 329 | |
| 330 // trackingReadCloser wraps an io.ReadCloser, keeping track of whether Closed wa s | |
| 331 // called. | |
| 332 type trackingReadCloser struct { | |
| 333 io.ReadCloser | |
| 334 Closed bool | |
| 335 } | |
| 336 | |
| 337 func (t *trackingReadCloser) Close() error { | |
| 338 t.Closed = true | |
| 339 return t.ReadCloser.Close() | |
| 340 } | |
| 341 | |
| 256 // Private details. | 342 // Private details. |
| 257 | 343 |
| 258 func fast() retry.Iterator { | 344 func fast() retry.Iterator { |
| 259 return &retry.Limited{Retries: 3} | 345 return &retry.Limited{Retries: 3} |
| 260 } | 346 } |
| 261 | 347 |
| 262 type jsonAPI func(body io.Reader) interface{} | 348 type jsonAPI func(body io.Reader) interface{} |
| 263 | 349 |
| 264 // handlerJSON converts a jsonAPI http handler to a proper http.Handler. | 350 // handlerJSON converts a jsonAPI http handler to a proper http.Handler. |
| 265 func handlerJSON(t *testing.T, handler jsonAPI) http.Handler { | 351 func handlerJSON(t *testing.T, handler jsonAPI) http.Handler { |
| 266 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 352 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 267 //ut.ExpectEqual(t, jsonContentType, r.Header.Get("Content-Type" )) | 353 //ut.ExpectEqual(t, jsonContentType, r.Header.Get("Content-Type" )) |
| 268 defer r.Body.Close() | 354 defer r.Body.Close() |
| 269 out := handler(r.Body) | 355 out := handler(r.Body) |
| 270 if out == nil { | 356 if out == nil { |
| 271 w.WriteHeader(500) | 357 w.WriteHeader(500) |
| 272 } else { | 358 } else { |
| 273 w.Header().Set("Content-Type", jsonContentType) | 359 w.Header().Set("Content-Type", jsonContentType) |
| 274 ut.ExpectEqual(t, nil, json.NewEncoder(w).Encode(out)) | 360 ut.ExpectEqual(t, nil, json.NewEncoder(w).Encode(out)) |
| 275 } | 361 } |
| 276 }) | 362 }) |
| 277 } | 363 } |
| OLD | NEW |