Index: go/src/infra/libs/epclient/epclient_test.go |
diff --git a/go/src/infra/libs/epclient/epclient_test.go b/go/src/infra/libs/epclient/epclient_test.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1b7fde910f389b506fb2bd2590aecde417e40733 |
--- /dev/null |
+++ b/go/src/infra/libs/epclient/epclient_test.go |
@@ -0,0 +1,235 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package epclient |
+ |
+import ( |
+ "bytes" |
+ "fmt" |
+ "golang.org/x/net/context" |
+ "io/ioutil" |
+ "net/http" |
+ "net/http/httptest" |
+ "testing" |
+ "time" |
+ |
+ . "github.com/smartystreets/goconvey/convey" |
+ |
+ "infra/gae/libs/endpoints" |
+) |
+ |
+func init() { |
+ backoffSlot = time.Millisecond // doesn't matter, but it should be short. |
+} |
+ |
+func TestBackoff(t *testing.T) { |
+ t.Parallel() |
+ |
+ cases := []struct { |
+ name string |
+ failures uint |
+ mult time.Duration // multiplier for slot |
+ }{ |
+ {"zero", 0, 1}, |
+ {"one", 1, 2}, |
+ {"five", 5, 2 * 2 * 2 * 2 * 2}, |
+ {"lots", 500, 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2}, |
+ } |
+ |
+ Convey("Backoff", t, func() { |
+ for _, tc := range cases { |
+ Convey(tc.name, func() { |
+ So(Backoff(tc.failures), ShouldResemble, tc.mult*backoffSlot) |
+ }) |
+ } |
+ }) |
+} |
+ |
+func TestDoer(t *testing.T) { |
+ t.Parallel() |
+ |
+ Convey("Test mkHttpDoer", t, func() { |
+ serv := httptest.NewServer(http.HandlerFunc(func(rsp http.ResponseWriter, r *http.Request) { |
+ fmt.Fprintf(rsp, "sup fool: %s", r.Method) |
+ })) |
+ defer serv.Close() |
+ |
+ doer, closer := mkHttpDoer() |
+ defer closer() |
+ |
+ req, err := http.NewRequest("GET", serv.URL, nil) |
+ So(err, ShouldBeNil) |
+ |
+ rsp, err := doer(req) |
+ So(err, ShouldBeNil) |
+ defer rsp.Body.Close() |
+ |
+ data, err := ioutil.ReadAll(rsp.Body) |
+ So(err, ShouldBeNil) |
+ So(data, ShouldResemble, []byte("sup fool: GET")) |
+ }) |
+} |
+ |
+type FakeService struct{} |
+ |
+var FakeServiceMethodInfoMap = endpoints.MethodInfoMap{ |
+ "Get": {HTTPMethod: "GET"}, |
+} |
+ |
+type CurResp struct{ Amt int } |
+ |
+type AddReq struct { |
+ CounterName string `endpoints:"required"` |
+ Amt int |
+} |
+ |
+type GetReq struct { |
+ CounterName string `endpoints:"required"` |
+ Limit int |
+ Flavor string |
+} |
+ |
+func (FakeService) Add(*http.Request, *AddReq) (*CurResp, error) { return nil, nil } |
+func (FakeService) Get(*http.Request, *GetReq) (*CurResp, error) { return nil, nil } |
+ |
+type fakeBody struct{ *bytes.Buffer } |
+ |
+func (fakeBody) Close() error { return nil } |
+ |
+type rsplet struct { |
+ body string |
+ statusCode int |
+} |
+ |
+func fakeDoer(retBody map[string][]interface{}) func() (func(*http.Request) (*http.Response, error), func()) { |
+ return func() (func(*http.Request) (*http.Response, error), func()) { |
+ return func(req *http.Request) (*http.Response, error) { |
+ lookup := fmt.Sprintf("%s %s", req.Method, req.URL) |
+ vals := retBody[lookup] |
+ val := interface{}(nil) |
+ if len(vals) > 0 { |
+ val = vals[0] |
+ retBody[lookup] = vals[1:] |
+ } |
+ if f, ok := val.(func() interface{}); ok { |
+ val = f() |
+ } |
+ switch x := val.(type) { |
+ case string: |
+ return &http.Response{ |
+ Body: fakeBody{bytes.NewBufferString(x)}, |
+ StatusCode: 200, |
+ }, nil |
+ case rsplet: |
+ return &http.Response{ |
+ Body: fakeBody{bytes.NewBufferString(x.body)}, |
+ StatusCode: x.statusCode, |
+ }, nil |
+ case error: |
+ return nil, x |
+ case *http.Response: |
+ return x, nil |
+ default: |
+ return &http.Response{ |
+ Body: fakeBody{bytes.NewBufferString( |
+ fmt.Sprintf(`{"error": {"message": "Not Found: %s"}}`, req.URL))}, |
+ StatusCode: 404, |
+ }, nil |
+ } |
+ }, func() {} |
+ } |
+} |
+ |
+func TestClient(t *testing.T) { |
+ t.Parallel() |
+ |
+ Convey("Test clients", t, func() { |
+ srv, err := endpoints.Register(FakeService{}, nil, FakeServiceMethodInfoMap) |
+ So(err, ShouldBeNil) |
+ ctx, cancel := context.WithCancel(context.Background()) |
+ c := NewClient(ctx, "", srv) |
+ So(c, ShouldNotBeNil) |
+ |
+ Convey("essentially works", func() { |
+ sc, err := c.ForService("FakeService") |
+ So(err, ShouldBeNil) |
+ |
+ c.(*client).mkHttpDoer = fakeDoer(map[string][]interface{}{ |
+ "POST /_ah/api/fakeservice/v1/add/foocounter": { |
+ `{"Amt": 18}`, |
+ rsplet{``, 500}, |
+ `{"Amt": 19}`, |
+ }, |
+ // TODO(riannucci): verify that the quotes on cats is correct |
+ `GET /_ah/api/fakeservice/v1/get/derpcounter?Limit=1&Flavor="cats"`: { |
+ `{"Amt": 9001}`, |
+ }, |
+ }) |
+ |
+ rsp := &CurResp{} |
+ So(sc.DoWithRetries("Add", &AddReq{"foocounter", 19}, rsp, 1), ShouldBeNil) |
+ So(rsp.Amt, ShouldEqual, 18) |
+ |
+ So(sc.DoWithRetries("Get", &GetReq{"derpcounter", 1, "cats"}, rsp, 1), ShouldBeNil) |
+ So(rsp.Amt, ShouldEqual, 9001) |
+ |
+ // swallows the error and retries |
+ So(sc.DoWithRetries("Add", &AddReq{"foocounter", 20}, rsp, 2), ShouldBeNil) |
+ So(rsp.Amt, ShouldEqual, 19) |
+ }) |
+ |
+ Convey("doesn't work for missing services", func() { |
+ _, err := c.ForService("Wat") |
+ So(err.Error(), ShouldContainSubstring, "no such service") |
+ |
+ err = c.DoWithRetries("Wat", "fleem", nil, nil, 1) |
+ So(err.Error(), ShouldContainSubstring, "no such service") |
+ }) |
+ |
+ Convey("doesn't work for missing methods", func() { |
+ sc, err := c.ForService("FakeService") |
+ So(err, ShouldBeNil) |
+ |
+ err = sc.DoWithRetries("fleem", nil, nil, 1) |
+ So(err.Error(), ShouldContainSubstring, "no such method") |
+ }) |
+ |
+ Convey("detects bad types", func() { |
+ sc, err := c.ForService("FakeService") |
+ So(err, ShouldBeNil) |
+ |
+ err = sc.DoWithRetries("Add", "", "", 1) |
+ So(err.Error(), ShouldContainSubstring, "mismatch for input") |
+ |
+ err = sc.DoWithRetries("Add", &AddReq{}, "", 1) |
+ So(err.Error(), ShouldContainSubstring, "mismatch for output") |
+ }) |
+ |
+ Convey("can cancel it", func() { |
+ sc, err := c.ForService("FakeService") |
+ So(err, ShouldBeNil) |
+ |
+ api := "POST /_ah/api/fakeservice/v1/add/foocounter" |
+ fakeData := map[string][]interface{}{ |
+ api: { |
+ nil, // force 404 |
+ func() interface{} { |
+ cancel() |
+ return nil |
+ }, |
+ nil, |
+ nil, |
+ }, |
+ } |
+ c.(*client).mkHttpDoer = fakeDoer(fakeData) |
+ |
+ rsp := &CurResp{} |
+ // 10 retries, but we'll never hit it |
+ err = sc.DoWithRetries("Add", &AddReq{"foocounter", 19}, rsp, 10) |
+ So(err.Error(), ShouldContainSubstring, "Not Found") |
+ So(len(fakeData[api]), ShouldEqual, 2) // should have two 'nil's left |
+ }) |
+ |
+ }) |
+} |