Skip to content

Commit

Permalink
Responder generated by httpmock can now be used concurrently
Browse files Browse the repository at this point in the history
Closes #93

Signed-off-by: Maxime Soulé <[email protected]>
  • Loading branch information
maxatome committed Aug 8, 2020
1 parent b71b92c commit a88cbab
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 7 deletions.
49 changes: 42 additions & 7 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,28 @@ func (r Responder) Trace(fn func(...interface{})) Responder {
}
}

// ResponderFromResponse wraps an *http.Response in a Responder
// ResponderFromResponse wraps an *http.Response in a Responder.
//
// Be careful, except for responses generated by httpmock
// (NewStringResponse and NewBytesResponse functions) for which there
// is no problems, it is the caller responsibility to ensure the
// response body can be read several times and concurrently if needed,
// as it is shared among all Responder returned responses.
//
// For home-made responses, NewRespBodyFromString and
// NewRespBodyFromBytes functions can be used to produce response
// bodies that can be read several times and concurrently.
func ResponderFromResponse(resp *http.Response) Responder {
return func(req *http.Request) (*http.Response, error) {
res := new(http.Response)
*res = *resp
res := *resp

// Our stuff: generate a new io.ReadCloser instance sharing the same buffer
if body, ok := resp.Body.(*dummyReadCloser); ok {
res.Body = body.copy()
}

res.Request = req
return res, nil
return &res, nil
}
}

Expand Down Expand Up @@ -244,20 +259,39 @@ func NewXmlResponderOrPanic(status int, body interface{}) Responder { // nolint:
// NewRespBodyFromString creates an io.ReadCloser from a string that is suitable for use as an
// http response body.
func NewRespBodyFromString(body string) io.ReadCloser {
return &dummyReadCloser{strings.NewReader(body)}
return &dummyReadCloser{orig: body}
}

// NewRespBodyFromBytes creates an io.ReadCloser from a byte slice that is suitable for use as an
// http response body.
func NewRespBodyFromBytes(body []byte) io.ReadCloser {
return &dummyReadCloser{bytes.NewReader(body)}
return &dummyReadCloser{orig: body}
}

type dummyReadCloser struct {
body io.ReadSeeker
orig interface{} // string or []byte
body io.ReadSeeker // instanciated on demand from orig
}

// copy returns a new instance resetting d.body to nil.
func (d *dummyReadCloser) copy() *dummyReadCloser {
return &dummyReadCloser{orig: d.orig}
}

// setup ensures d.body is correctly initialized.
func (d *dummyReadCloser) setup() {
if d.body == nil {
switch body := d.orig.(type) {
case string:
d.body = strings.NewReader(body)
case []byte:
d.body = bytes.NewReader(body)
}
}
}

func (d *dummyReadCloser) Read(p []byte) (n int, err error) {
d.setup()
n, err = d.body.Read(p)
if err == io.EOF {
d.body.Seek(0, 0) // nolint: errcheck
Expand All @@ -266,6 +300,7 @@ func (d *dummyReadCloser) Read(p []byte) (n int, err error) {
}

func (d *dummyReadCloser) Close() error {
d.setup()
d.body.Seek(0, 0) // nolint: errcheck
return nil
}
35 changes: 35 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"io/ioutil"
"net/http"
"strings"
"sync"
"testing"
)

Expand Down Expand Up @@ -340,3 +342,36 @@ func TestResponder(t *testing.T) {
chk(rt, resp, "")
chkCalled()
}

func TestParallelResponder(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil)
if err != nil {
t.Fatal("Error creating request")
}

body := strings.Repeat("ABC-", 1000)

for _, r := range []Responder{
NewStringResponder(200, body),
NewBytesResponder(200, []byte(body)),
} {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, _ := r(req)
b, err := ioutil.ReadAll(resp.Body)
switch {
case err != nil:
t.Errorf("ReadAll error: %s", err)
case len(b) != 4000:
t.Errorf("ReadAll read only %d bytes", len(b))
case !strings.HasPrefix(string(b), "ABC-"):
t.Errorf("ReadAll does not read the right prefix: %s", string(b)[0:4])
}
}()
}
wg.Wait()
}
}

0 comments on commit a88cbab

Please sign in to comment.