Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions benchttptest/benchttptest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package benchttptest proovides utilities for benchttp testing.
package benchttptest
143 changes: 143 additions & 0 deletions benchttptest/compare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package benchttptest

import (
"bytes"
"crypto/tls"
"io"
"net/http"
"net/url"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/benchttp/sdk/benchttp"
)

// RunnerCmpOptions is the cmp.Options used to compare benchttp.Runner.
// By default, it ignores unexported fields and includes RequestCmpOptions.
var RunnerCmpOptions = cmp.Options{
cmpopts.IgnoreUnexported(benchttp.Runner{}),
RequestCmpOptions,
}

// RequestCmpOptions is the cmp.Options used to compare *http.Request.
// It behaves as follows:
//
// - Nil and empty values are considered equal
//
// - Fields that depend on how the request was created are ignored
// to avoid false negatives when comparing requests created in different
// ways (http.NewRequest vs httptest.NewRequest vs &http.Request{})
//
// - Function fields are ignored
//
// - Body is ignored: it is compared separately
var RequestCmpOptions = cmp.Options{
cmp.Transformer("Request", instantiateNilRequest),
cmp.Transformer("Request.Header", instantiateNilHeader),
cmp.Transformer("Request.URL", stringifyURL),
cmpopts.IgnoreUnexported(http.Request{}, tls.ConnectionState{}),
cmpopts.IgnoreFields(http.Request{}, unreliableRequestFields...),
}

var unreliableRequestFields = []string{
// These fields are automatically set by NewRequest constructor
// from packages http and httptest, as a consequence they can
// trigger false positives when comparing requests that were
// created differently.
"Proto", "ProtoMajor", "ProtoMinor", "ContentLength",
"Host", "RemoteAddr", "RequestURI", "TLS", "Cancel",

// Function fields cannot be reliably compared
"GetBody",

// Body field can't be read without altering the Request, causing
// cmp-go to panic. We perform a custom comparison instead.
"Body",
}

// AssertEqualRunners fails t and shows a diff if a and b are not equal,
// as determined by RunnerCmpOptions.
func AssertEqualRunners(t *testing.T, x, y benchttp.Runner) {
t.Helper()
if !EqualRunners(x, y) {
t.Error(DiffRunner(x, y))
}
}

// EqualRunners returns true if x and y are equal, as determined by
// RunnerCmpOptions.
func EqualRunners(x, y benchttp.Runner) bool {
return cmp.Equal(x, y, RunnerCmpOptions) &&
compareRequestBody(x.Request, y.Request)
}

// DiffRunner returns a string showing the diff between x and y,
// as determined by RunnerCmpOptions.
func DiffRunner(x, y benchttp.Runner) string {
b := strings.Builder{}
b.WriteString(cmp.Diff(x, y, RunnerCmpOptions))
if x.Request != nil && y.Request != nil {
xbody := nopreadBody(x.Request)
ybody := nopreadBody(y.Request)
if !bytes.Equal(xbody, ybody) {
b.WriteString("Request.Body: ")
b.WriteString(cmp.Diff(string(xbody), string(ybody)))
}
}
return b.String()
}

// helpers

func instantiateNilRequest(r *http.Request) *http.Request {
if r == nil {
return &http.Request{}
}
return r
}

func instantiateNilHeader(h http.Header) http.Header {
if h == nil {
return http.Header{}
}
return h
}

func stringifyURL(u *url.URL) string {
if u == nil {
return ""
}
return u.String()
}

func compareRequestBody(a, b *http.Request) bool {
ba, bb := nopreadBody(a), nopreadBody(b)
return bytes.Equal(ba, bb)
}

func nopreadBody(r *http.Request) []byte {
if r == nil || r.Body == nil {
return []byte{}
}

bbuf := bytes.Buffer{}

if _, err := io.Copy(&bbuf, r.Body); err != nil {
panic("benchttptest: error reading Request.Body: " + err.Error())
}

if r.GetBody != nil {
newbody, err := r.GetBody()
if err != nil {
panic("benchttptest: Request.GetBody error: " + err.Error())
}
r.Body = newbody
} else {
r.Body = io.NopCloser(&bbuf)
}

return bbuf.Bytes()
}
189 changes: 189 additions & 0 deletions benchttptest/compare_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package benchttptest_test

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/benchttp/sdk/benchttp"
"github.com/benchttp/sdk/benchttptest"
)

func TestAssertEqualRunners(t *testing.T) {
for _, tc := range []struct {
name string
pass bool
a, b benchttp.Runner
}{
{
name: "pass if runners are equal",
pass: true,
a: benchttp.Runner{Requests: 1},
b: benchttp.Runner{Requests: 1},
},
{
name: "fail if runners are not equal",
pass: false,
a: benchttp.Runner{Requests: 1},
b: benchttp.Runner{Requests: 2},
},
} {
t.Run(tc.name, func(t *testing.T) {
tt := &testing.T{}

benchttptest.AssertEqualRunners(tt, tc.a, tc.b)
if tt.Failed() == tc.pass {
t.Fail()
}
})
}
}

func TestEqualRunners(t *testing.T) {
for _, tc := range []struct {
name string
want bool
a, b benchttp.Runner
}{
{
name: "equal runners",
want: true,
a: benchttp.Runner{Requests: 1},
b: benchttp.Runner{Requests: 1},
},
{
name: "different runners",
want: false,
a: benchttp.Runner{Requests: 1},
b: benchttp.Runner{Requests: 2},
},
{
name: "consider zero requests equal",
want: true,
a: benchttp.Runner{Request: nil},
b: benchttp.Runner{Request: &http.Request{}},
},
{
name: "consider zero request headers equal",
want: true,
a: benchttp.Runner{Request: &http.Request{Header: nil}},
b: benchttp.Runner{Request: &http.Request{Header: http.Header{}}},
},
{
name: "consider zero request bodies equal",
want: true,
a: benchttp.Runner{Request: &http.Request{Body: nil}},
b: benchttp.Runner{Request: &http.Request{Body: http.NoBody}},
},
{
name: "zero request vs non zero request",
want: false,
a: benchttp.Runner{Request: &http.Request{Method: "GET"}},
b: benchttp.Runner{Request: nil},
},
{
name: "different request field values",
want: false,
a: benchttp.Runner{Request: &http.Request{Method: "GET"}},
b: benchttp.Runner{Request: &http.Request{Method: "POST"}},
},
{
name: "ignore unreliable request fields",
want: true,
a: benchttp.Runner{
Request: httptest.NewRequest( // sets Proto, ContentLength, ...
"POST",
"https://example.com",
nil,
),
},
b: benchttp.Runner{
Request: &http.Request{
Method: "POST",
URL: mustParseRequestURI("https://example.com"),
},
},
},
{
name: "equal request bodies",
want: true,
a: benchttp.Runner{
Request: &http.Request{
Body: io.NopCloser(strings.NewReader("hello")),
},
},
b: benchttp.Runner{
Request: &http.Request{
Body: io.NopCloser(strings.NewReader("hello")),
},
},
},
{
name: "different request bodies",
want: false,
a: benchttp.Runner{
Request: &http.Request{
Body: io.NopCloser(strings.NewReader("hello")),
},
},
b: benchttp.Runner{
Request: &http.Request{
Body: io.NopCloser(strings.NewReader("world")),
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
if benchttptest.EqualRunners(tc.a, tc.b) != tc.want {
t.Error(benchttptest.DiffRunner(tc.a, tc.b))
}
})
}

t.Run("restore request body", func(t *testing.T) {
a := benchttp.Runner{
Request: httptest.NewRequest(
"POST",
"https://example.com",
strings.NewReader("hello"),
),
}
b := benchttp.Runner{
Request: &http.Request{
Method: "POST",
URL: mustParseRequestURI("https://example.com"),
Body: io.NopCloser(bytes.NewReader([]byte("hello"))),
},
}

_ = benchttptest.EqualRunners(a, b)

ba, bb := mustRead(a.Request.Body), mustRead(b.Request.Body)
want := []byte("hello")
if !bytes.Equal(want, ba) || !bytes.Equal(want, bb) {
t.Fail()
}
})
}

// helpers

func mustParseRequestURI(s string) *url.URL {
u, err := url.ParseRequestURI(s)
if err != nil {
panic(err)
}
return u
}

func mustRead(r io.Reader) []byte {
b, err := io.ReadAll(r)
if err != nil {
panic("mustRead: " + err.Error())
}
return b
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ require (
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
gopkg.in/yaml.v3 v3.0.1
)

require github.com/google/go-cmp v0.5.9
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down