mirror of
https://github.com/golang/net.git
synced 2026-03-31 10:27:08 +09:00
This change makes it easier to move x/net/http2 into std. Moving the http2 package into std and importing it from net/http (rather than bundling it as net/http/h2_bundle.go) requires removing the http2->net/http dependency. Moving tests into the http2_test package allows them to continue importing net/http without creating a cycle. Change-Id: If0799a94a6d2c90f02d7f391e352e14e6a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/749280 Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Nicholas Husin <husin@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Nicholas Husin <nsh@golang.org>
573 lines
14 KiB
Go
573 lines
14 KiB
Go
// Copyright 2024 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Infrastructure for testing ClientConn.RoundTrip.
|
|
// Put actual tests in transport_test.go.
|
|
|
|
package http2_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"sync/atomic"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
. "golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/hpack"
|
|
"golang.org/x/net/internal/gate"
|
|
)
|
|
|
|
// TestTestClientConn demonstrates usage of testClientConn.
|
|
func TestTestClientConn(t *testing.T) { synctestTest(t, testTestClientConn) }
|
|
func testTestClientConn(t testing.TB) {
|
|
// newTestClientConn creates a *ClientConn and surrounding test infrastructure.
|
|
tc := newTestClientConn(t)
|
|
|
|
// tc.greet reads the client's initial SETTINGS and WINDOW_UPDATE frames,
|
|
// and sends a SETTINGS frame to the client.
|
|
//
|
|
// Additional settings may be provided as optional parameters to greet.
|
|
tc.greet()
|
|
|
|
// Request bodies must either be constant (bytes.Buffer, strings.Reader)
|
|
// or created with newRequestBody.
|
|
body := tc.newRequestBody()
|
|
body.writeBytes(10) // 10 arbitrary bytes...
|
|
body.closeWithError(io.EOF) // ...followed by EOF.
|
|
|
|
// tc.roundTrip calls RoundTrip, but does not wait for it to return.
|
|
// It returns a testRoundTrip.
|
|
req, _ := http.NewRequest("PUT", "https://dummy.tld/", body)
|
|
rt := tc.roundTrip(req)
|
|
|
|
// tc has a number of methods to check for expected frames sent.
|
|
// Here, we look for headers and the request body.
|
|
tc.wantHeaders(wantHeader{
|
|
streamID: rt.streamID(),
|
|
endStream: false,
|
|
header: http.Header{
|
|
":authority": []string{"dummy.tld"},
|
|
":method": []string{"PUT"},
|
|
":path": []string{"/"},
|
|
},
|
|
})
|
|
// Expect 10 bytes of request body in DATA frames.
|
|
tc.wantData(wantData{
|
|
streamID: rt.streamID(),
|
|
endStream: true,
|
|
size: 10,
|
|
multiple: true,
|
|
})
|
|
|
|
// tc.writeHeaders sends a HEADERS frame back to the client.
|
|
tc.writeHeaders(HeadersFrameParam{
|
|
StreamID: rt.streamID(),
|
|
EndHeaders: true,
|
|
EndStream: true,
|
|
BlockFragment: tc.makeHeaderBlockFragment(
|
|
":status", "200",
|
|
),
|
|
})
|
|
|
|
// Now that we've received headers, RoundTrip has finished.
|
|
// testRoundTrip has various methods to examine the response,
|
|
// or to fetch the response and/or error returned by RoundTrip
|
|
rt.wantStatus(200)
|
|
rt.wantBody(nil)
|
|
}
|
|
|
|
// A testClientConn allows testing ClientConn.RoundTrip against a fake server.
|
|
//
|
|
// A test using testClientConn consists of:
|
|
// - actions on the client (calling RoundTrip, making data available to Request.Body);
|
|
// - validation of frames sent by the client to the server; and
|
|
// - providing frames from the server to the client.
|
|
//
|
|
// testClientConn manages synchronization, so tests can generally be written as
|
|
// a linear sequence of actions and validations without additional synchronization.
|
|
type testClientConn struct {
|
|
t testing.TB
|
|
|
|
tr *Transport
|
|
fr *Framer
|
|
cc *ClientConn
|
|
testConnFramer
|
|
|
|
encbuf bytes.Buffer
|
|
enc *hpack.Encoder
|
|
|
|
roundtrips []*testRoundTrip
|
|
|
|
netconn *synctestNetConn
|
|
}
|
|
|
|
func newTestClientConnFromClientConn(t testing.TB, tr *Transport, cc *ClientConn) *testClientConn {
|
|
tc := &testClientConn{
|
|
t: t,
|
|
tr: tr,
|
|
cc: cc,
|
|
}
|
|
|
|
// srv is the side controlled by the test.
|
|
var srv *synctestNetConn
|
|
if tconn := cc.TestNetConn(); tconn == nil {
|
|
// If cc.tconn is nil, we're being called with a new conn created by the
|
|
// Transport's client pool. This path skips dialing the server, and we
|
|
// create a test connection pair here.
|
|
var cli *synctestNetConn
|
|
cli, srv = synctestNetPipe()
|
|
cc.TestSetNetConn(cli)
|
|
} else {
|
|
// If cc.tconn is non-nil, we're in a test which provides a conn to the
|
|
// Transport via a TLSNextProto hook. Extract the test connection pair.
|
|
if tc, ok := tconn.(*tls.Conn); ok {
|
|
// Unwrap any *tls.Conn to the underlying net.Conn,
|
|
// to avoid dealing with encryption in tests.
|
|
tconn = tc.NetConn()
|
|
cc.TestSetNetConn(tconn)
|
|
}
|
|
srv = tconn.(*synctestNetConn).peer
|
|
}
|
|
|
|
srv.SetReadDeadline(time.Now())
|
|
srv.autoWait = true
|
|
tc.netconn = srv
|
|
tc.enc = hpack.NewEncoder(&tc.encbuf)
|
|
tc.fr = NewFramer(srv, srv)
|
|
tc.testConnFramer = testConnFramer{
|
|
t: t,
|
|
fr: tc.fr,
|
|
dec: hpack.NewDecoder(InitialHeaderTableSize, nil),
|
|
}
|
|
tc.fr.SetMaxReadFrameSize(10 << 20)
|
|
t.Cleanup(func() {
|
|
tc.closeWrite()
|
|
})
|
|
|
|
return tc
|
|
}
|
|
|
|
func (tc *testClientConn) readClientPreface() {
|
|
tc.t.Helper()
|
|
// Read the client's HTTP/2 preface, sent prior to any HTTP/2 frames.
|
|
buf := make([]byte, len(ClientPreface))
|
|
if _, err := io.ReadFull(tc.netconn, buf); err != nil {
|
|
tc.t.Fatalf("reading preface: %v", err)
|
|
}
|
|
if !bytes.Equal(buf, []byte(ClientPreface)) {
|
|
tc.t.Fatalf("client preface: %q, want %q", buf, ClientPreface)
|
|
}
|
|
}
|
|
|
|
func newTestClientConn(t testing.TB, opts ...any) *testClientConn {
|
|
t.Helper()
|
|
|
|
tt := newTestTransport(t, opts...)
|
|
const singleUse = false
|
|
_, err := tt.tr.TestNewClientConn(nil, singleUse, nil)
|
|
if err != nil {
|
|
t.Fatalf("newClientConn: %v", err)
|
|
}
|
|
|
|
return tt.getConn()
|
|
}
|
|
|
|
// hasFrame reports whether a frame is available to be read.
|
|
func (tc *testClientConn) hasFrame() bool {
|
|
return len(tc.netconn.Peek()) > 0
|
|
}
|
|
|
|
// isClosed reports whether the peer has closed the connection.
|
|
func (tc *testClientConn) isClosed() bool {
|
|
return tc.netconn.IsClosedByPeer()
|
|
}
|
|
|
|
// closeWrite causes the net.Conn used by the ClientConn to return a error
|
|
// from Read calls.
|
|
func (tc *testClientConn) closeWrite() {
|
|
tc.netconn.Close()
|
|
}
|
|
|
|
// closeWrite causes the net.Conn used by the ClientConn to return a error
|
|
// from Write calls.
|
|
func (tc *testClientConn) closeWriteWithError(err error) {
|
|
tc.netconn.loc.setReadError(io.EOF)
|
|
tc.netconn.loc.setWriteError(err)
|
|
}
|
|
|
|
// testRequestBody is a Request.Body for use in tests.
|
|
type testRequestBody struct {
|
|
tc *testClientConn
|
|
gate gate.Gate
|
|
|
|
// At most one of buf or bytes can be set at any given time:
|
|
buf bytes.Buffer // specific bytes to read from the body
|
|
bytes int // body contains this many arbitrary bytes
|
|
|
|
err error // read error (comes after any available bytes)
|
|
}
|
|
|
|
func (tc *testClientConn) newRequestBody() *testRequestBody {
|
|
b := &testRequestBody{
|
|
tc: tc,
|
|
gate: gate.New(false),
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *testRequestBody) unlock() {
|
|
b.gate.Unlock(b.buf.Len() > 0 || b.bytes > 0 || b.err != nil)
|
|
}
|
|
|
|
// Read is called by the ClientConn to read from a request body.
|
|
func (b *testRequestBody) Read(p []byte) (n int, _ error) {
|
|
if err := b.gate.WaitAndLock(context.Background()); err != nil {
|
|
return 0, err
|
|
}
|
|
defer b.unlock()
|
|
switch {
|
|
case b.buf.Len() > 0:
|
|
return b.buf.Read(p)
|
|
case b.bytes > 0:
|
|
if len(p) > b.bytes {
|
|
p = p[:b.bytes]
|
|
}
|
|
b.bytes -= len(p)
|
|
for i := range p {
|
|
p[i] = 'A'
|
|
}
|
|
return len(p), nil
|
|
default:
|
|
return 0, b.err
|
|
}
|
|
}
|
|
|
|
// Close is called by the ClientConn when it is done reading from a request body.
|
|
func (b *testRequestBody) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// writeBytes adds n arbitrary bytes to the body.
|
|
func (b *testRequestBody) writeBytes(n int) {
|
|
defer synctest.Wait()
|
|
b.gate.Lock()
|
|
defer b.unlock()
|
|
b.bytes += n
|
|
b.checkWrite()
|
|
synctest.Wait()
|
|
}
|
|
|
|
// Write adds bytes to the body.
|
|
func (b *testRequestBody) Write(p []byte) (int, error) {
|
|
defer synctest.Wait()
|
|
b.gate.Lock()
|
|
defer b.unlock()
|
|
n, err := b.buf.Write(p)
|
|
b.checkWrite()
|
|
return n, err
|
|
}
|
|
|
|
func (b *testRequestBody) checkWrite() {
|
|
if b.bytes > 0 && b.buf.Len() > 0 {
|
|
b.tc.t.Fatalf("can't interleave Write and writeBytes on request body")
|
|
}
|
|
if b.err != nil {
|
|
b.tc.t.Fatalf("can't write to request body after closeWithError")
|
|
}
|
|
}
|
|
|
|
// closeWithError sets an error which will be returned by Read.
|
|
func (b *testRequestBody) closeWithError(err error) {
|
|
defer synctest.Wait()
|
|
b.gate.Lock()
|
|
defer b.unlock()
|
|
b.err = err
|
|
}
|
|
|
|
// roundTrip starts a RoundTrip call.
|
|
//
|
|
// (Note that the RoundTrip won't complete until response headers are received,
|
|
// the request times out, or some other terminal condition is reached.)
|
|
func (tc *testClientConn) roundTrip(req *http.Request) *testRoundTrip {
|
|
ctx, cancel := context.WithCancel(req.Context())
|
|
req = req.WithContext(ctx)
|
|
rt := &testRoundTrip{
|
|
t: tc.t,
|
|
donec: make(chan struct{}),
|
|
cancel: cancel,
|
|
}
|
|
tc.roundtrips = append(tc.roundtrips, rt)
|
|
go func() {
|
|
defer close(rt.donec)
|
|
rt.resp, rt.respErr = tc.cc.TestRoundTrip(req, func(streamID uint32) {
|
|
rt.id.Store(streamID)
|
|
})
|
|
}()
|
|
synctest.Wait()
|
|
|
|
tc.t.Cleanup(func() {
|
|
if !rt.done() {
|
|
return
|
|
}
|
|
res, _ := rt.result()
|
|
if res != nil {
|
|
res.Body.Close()
|
|
}
|
|
})
|
|
|
|
return rt
|
|
}
|
|
|
|
func (tc *testClientConn) greet(settings ...Setting) {
|
|
tc.wantFrameType(FrameSettings)
|
|
tc.wantFrameType(FrameWindowUpdate)
|
|
tc.writeSettings(settings...)
|
|
tc.writeSettingsAck()
|
|
tc.wantFrameType(FrameSettings) // acknowledgement
|
|
}
|
|
|
|
// makeHeaderBlockFragment encodes headers in a form suitable for inclusion
|
|
// in a HEADERS or CONTINUATION frame.
|
|
//
|
|
// It takes a list of alternating names and values.
|
|
func (tc *testClientConn) makeHeaderBlockFragment(s ...string) []byte {
|
|
if len(s)%2 != 0 {
|
|
tc.t.Fatalf("uneven list of header name/value pairs")
|
|
}
|
|
tc.encbuf.Reset()
|
|
for i := 0; i < len(s); i += 2 {
|
|
tc.enc.WriteField(hpack.HeaderField{Name: s[i], Value: s[i+1]})
|
|
}
|
|
return tc.encbuf.Bytes()
|
|
}
|
|
|
|
// inflowWindow returns the amount of inbound flow control available for a stream,
|
|
// or for the connection if streamID is 0.
|
|
func (tc *testClientConn) inflowWindow(streamID uint32) int32 {
|
|
w, err := tc.cc.TestInflowWindow(streamID)
|
|
if err != nil {
|
|
tc.t.Error(err)
|
|
}
|
|
return w
|
|
}
|
|
|
|
// testRoundTrip manages a RoundTrip in progress.
|
|
type testRoundTrip struct {
|
|
t testing.TB
|
|
resp *http.Response
|
|
respErr error
|
|
donec chan struct{}
|
|
id atomic.Uint32
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// streamID returns the HTTP/2 stream ID of the request.
|
|
func (rt *testRoundTrip) streamID() uint32 {
|
|
id := rt.id.Load()
|
|
if id == 0 {
|
|
panic("stream ID unknown")
|
|
}
|
|
return id
|
|
}
|
|
|
|
// done reports whether RoundTrip has returned.
|
|
func (rt *testRoundTrip) done() bool {
|
|
select {
|
|
case <-rt.donec:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// result returns the result of the RoundTrip.
|
|
func (rt *testRoundTrip) result() (*http.Response, error) {
|
|
t := rt.t
|
|
t.Helper()
|
|
synctest.Wait()
|
|
select {
|
|
case <-rt.donec:
|
|
default:
|
|
t.Fatalf("RoundTrip is not done; want it to be")
|
|
}
|
|
return rt.resp, rt.respErr
|
|
}
|
|
|
|
// response returns the response of a successful RoundTrip.
|
|
// If the RoundTrip unexpectedly failed, it calls t.Fatal.
|
|
func (rt *testRoundTrip) response() *http.Response {
|
|
t := rt.t
|
|
t.Helper()
|
|
resp, err := rt.result()
|
|
if err != nil {
|
|
t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr)
|
|
}
|
|
if resp == nil {
|
|
t.Fatalf("RoundTrip returned nil *Response and nil error")
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// err returns the (possibly nil) error result of RoundTrip.
|
|
func (rt *testRoundTrip) err() error {
|
|
t := rt.t
|
|
t.Helper()
|
|
_, err := rt.result()
|
|
return err
|
|
}
|
|
|
|
// wantStatus indicates the expected response StatusCode.
|
|
func (rt *testRoundTrip) wantStatus(want int) {
|
|
t := rt.t
|
|
t.Helper()
|
|
if got := rt.response().StatusCode; got != want {
|
|
t.Fatalf("got response status %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// readBody reads the contents of the response body.
|
|
func (rt *testRoundTrip) readBody() ([]byte, error) {
|
|
t := rt.t
|
|
t.Helper()
|
|
return io.ReadAll(rt.response().Body)
|
|
}
|
|
|
|
// wantBody indicates the expected response body.
|
|
// (Note that this consumes the body.)
|
|
func (rt *testRoundTrip) wantBody(want []byte) {
|
|
t := rt.t
|
|
t.Helper()
|
|
got, err := rt.readBody()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error reading response body: %v", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("unexpected response body:\ngot: %q\nwant: %q", got, want)
|
|
}
|
|
}
|
|
|
|
// wantHeaders indicates the expected response headers.
|
|
func (rt *testRoundTrip) wantHeaders(want http.Header) {
|
|
t := rt.t
|
|
t.Helper()
|
|
res := rt.response()
|
|
if diff := diffHeaders(res.Header, want); diff != "" {
|
|
t.Fatalf("unexpected response headers:\n%v", diff)
|
|
}
|
|
}
|
|
|
|
// wantTrailers indicates the expected response trailers.
|
|
func (rt *testRoundTrip) wantTrailers(want http.Header) {
|
|
t := rt.t
|
|
t.Helper()
|
|
res := rt.response()
|
|
if diff := diffHeaders(res.Trailer, want); diff != "" {
|
|
t.Fatalf("unexpected response trailers:\n%v", diff)
|
|
}
|
|
}
|
|
|
|
func diffHeaders(got, want http.Header) string {
|
|
// nil and 0-length non-nil are equal.
|
|
if len(got) == 0 && len(want) == 0 {
|
|
return ""
|
|
}
|
|
// We could do a more sophisticated diff here.
|
|
// DeepEqual is good enough for now.
|
|
if reflect.DeepEqual(got, want) {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("got: %v\nwant: %v", got, want)
|
|
}
|
|
|
|
// A testTransport allows testing Transport.RoundTrip against fake servers.
|
|
// Tests that aren't specifically exercising RoundTrip's retry loop or connection pooling
|
|
// should use testClientConn instead.
|
|
type testTransport struct {
|
|
t testing.TB
|
|
tr *Transport
|
|
|
|
ccs []*testClientConn
|
|
}
|
|
|
|
func newTestTransport(t testing.TB, opts ...any) *testTransport {
|
|
tt := &testTransport{
|
|
t: t,
|
|
}
|
|
|
|
tr := &Transport{}
|
|
for _, o := range opts {
|
|
switch o := o.(type) {
|
|
case func(*http.Transport):
|
|
o(tr.TestTransport())
|
|
case func(*Transport):
|
|
o(tr)
|
|
case *Transport:
|
|
tr = o
|
|
}
|
|
}
|
|
tt.tr = tr
|
|
|
|
tr.TestSetNewClientConnHook(func(cc *ClientConn) {
|
|
tc := newTestClientConnFromClientConn(t, tr, cc)
|
|
tt.ccs = append(tt.ccs, tc)
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
synctest.Wait()
|
|
if len(tt.ccs) > 0 {
|
|
t.Fatalf("%v test ClientConns created, but not examined by test", len(tt.ccs))
|
|
}
|
|
})
|
|
|
|
return tt
|
|
}
|
|
|
|
func (tt *testTransport) hasConn() bool {
|
|
return len(tt.ccs) > 0
|
|
}
|
|
|
|
func (tt *testTransport) getConn() *testClientConn {
|
|
tt.t.Helper()
|
|
if len(tt.ccs) == 0 {
|
|
tt.t.Fatalf("no new ClientConns created; wanted one")
|
|
}
|
|
tc := tt.ccs[0]
|
|
tt.ccs = tt.ccs[1:]
|
|
synctest.Wait()
|
|
tc.readClientPreface()
|
|
synctest.Wait()
|
|
return tc
|
|
}
|
|
|
|
func (tt *testTransport) roundTrip(req *http.Request) *testRoundTrip {
|
|
rt := &testRoundTrip{
|
|
t: tt.t,
|
|
donec: make(chan struct{}),
|
|
}
|
|
go func() {
|
|
defer close(rt.donec)
|
|
rt.resp, rt.respErr = tt.tr.RoundTrip(req)
|
|
}()
|
|
synctest.Wait()
|
|
|
|
tt.t.Cleanup(func() {
|
|
if !rt.done() {
|
|
return
|
|
}
|
|
res, _ := rt.result()
|
|
if res != nil {
|
|
res.Body.Close()
|
|
}
|
|
})
|
|
|
|
return rt
|
|
}
|