| 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
|
| + })
|
| +
|
| + })
|
| +}
|
|
|