// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gensupport import ( "context" "errors" "io" "net" "net/http" "testing" ) func TestRetry(t *testing.T) { testCases := []struct { desc string respStatus []int // HTTP status codes returned (length indicates number of calls we expect). maxRetry int // Max number of calls allowed by the BackoffStrategy. wantStatus int // StatusCode of returned response. }{ { desc: "First call successful", respStatus: []int{200}, maxRetry: 3, wantStatus: 200, }, { desc: "Retry before success", respStatus: []int{500, 500, 500, 200}, maxRetry: 3, wantStatus: 200, }, { desc: "Backoff strategy abandons after 3 retries", respStatus: []int{500, 500, 500, 500}, maxRetry: 3, wantStatus: 500, }, { desc: "Backoff strategy abandons after 2 retries", respStatus: []int{500, 500, 500}, maxRetry: 2, wantStatus: 500, }, } for _, tt := range testCases { // Function consumes tt.respStatus f := func() (*http.Response, error) { if len(tt.respStatus) == 0 { return nil, errors.New("too many requests to function") } resp := &http.Response{StatusCode: tt.respStatus[0]} tt.respStatus = tt.respStatus[1:] return resp, nil } backoff := &LimitRetryStrategy{ Max: tt.maxRetry, Strategy: NoPauseStrategy, } resp, err := Retry(context.Background(), f, backoff) if err != nil { t.Errorf("%s: Retry returned err %v", tt.desc, err) } if got := resp.StatusCode; got != tt.wantStatus { t.Errorf("%s: Retry returned response with StatusCode=%d; want %d", tt.desc, got, tt.wantStatus) } if len(tt.respStatus) != 0 { t.Errorf("%s: f was not called enough; status codes remaining: %v", tt.desc, tt.respStatus) } } } type checkCloseReader struct { closed bool } func (c *checkCloseReader) Read(p []byte) (n int, err error) { return 0, io.EOF } func (c *checkCloseReader) Close() error { c.closed = true return nil } func TestRetryClosesBody(t *testing.T) { var i int responses := []*http.Response{ {StatusCode: 500, Body: &checkCloseReader{}}, {StatusCode: 500, Body: &checkCloseReader{}}, {StatusCode: 200, Body: &checkCloseReader{}}, } f := func() (*http.Response, error) { resp := responses[i] i++ return resp, nil } resp, err := Retry(context.Background(), f, NoPauseStrategy) if err != nil { t.Fatalf("Retry returned error: %v", err) } if resp != responses[2] { t.Errorf("Retry returned %v; want %v", resp, responses[2]) } for i, resp := range responses { want := i != 2 // Only the last response should not be closed. got := resp.Body.(*checkCloseReader).closed if got != want { t.Errorf("response[%d].Body closed = %t, want %t", i, got, want) } } } func TestShouldRetry(t *testing.T) { testCases := []struct { status int err error want bool }{ {status: 200, want: false}, {status: 308, want: false}, {status: 403, want: false}, {status: 429, want: true}, {status: 500, want: true}, {status: 503, want: true}, {status: 600, want: false}, {err: io.EOF, want: false}, {err: errors.New("random badness"), want: false}, {err: io.ErrUnexpectedEOF, want: true}, {err: &net.AddrError{}, want: false}, // Not temporary. {err: &net.DNSError{IsTimeout: true}, want: true}, // Temporary. } for _, tt := range testCases { if got := shouldRetry(tt.status, tt.err); got != tt.want { t.Errorf("shouldRetry(%d, %v) = %t; want %t", tt.status, tt.err, got, tt.want) } } }