Skip to content

Commit 46a2402

Browse files
committed
net/http: let Transport request body writes use sendfile
net.TCPConn has the ability to send data out using system calls such as sendfile when the source data comes from an *os.File. However, the way that I/O has been laid out in the transport means that the File is actually wrapped behind two outer io.Readers, and as such the TCP stack cannot properly type-assert the reader, ensuring that it falls back to genericReadFrom. This commit does the following: * Removes transferBodyReader and moves its functionality to a new doBodyCopy helper. This is not an io.Reader implementation, but no functionality is lost this way, and it allows us to unwrap one layer from the body. * The second layer of the body is unwrapped if the original reader was wrapped with ioutil.NopCloser, which is what NewRequest wraps the body in if it's not a ReadCloser on its own. The unwrap operation passes through the existing body if there's no nopCloser. Note that this depends on change https://golang.org/cl/163737 to properly function, as the lack of ReaderFrom implementation otherwise means that this functionality is essentially walled off. Benchmarks between this commit and https://golang.org/cl/163737 (where ReaderFrom was implemented for persistConnWriter): linux/amd64: name old time/op new time/op delta FileAndServer_1KB/NoTLS-4 53.7µs ± 0% 53.5µs ± 0% -0.31% (p=0.000 n=10+10) FileAndServer_1KB/TLS-4 61.0µs ± 0% 61.1µs ± 0% ~ (p=0.165 n=10+10) FileAndServer_16MB/NoTLS-4 6.30ms ± 6% 3.81ms ± 5% -39.48% (p=0.000 n=10+10) FileAndServer_16MB/TLS-4 13.5ms ± 1% 13.4ms ± 1% ~ (p=0.631 n=10+10) FileAndServer_64MB/NoTLS-4 27.3ms ± 1% 16.4ms ± 3% -40.05% (p=0.000 n=9+10) FileAndServer_64MB/TLS-4 53.9ms ± 2% 53.4ms ± 1% -0.93% (p=0.006 n=10+9) name old speed new speed delta FileAndServer_1KB/NoTLS-4 19.1MB/s ± 0% 19.1MB/s ± 0% +0.32% (p=0.000 n=10+10) FileAndServer_1KB/TLS-4 16.8MB/s ± 0% 16.8MB/s ± 0% ~ (p=0.302 n=10+10) FileAndServer_16MB/NoTLS-4 2.67GB/s ± 6% 4.41GB/s ± 5% +65.32% (p=0.000 n=10+10) FileAndServer_16MB/TLS-4 1.25GB/s ± 1% 1.25GB/s ± 1% ~ (p=0.631 n=10+10) FileAndServer_64MB/NoTLS-4 2.45GB/s ± 1% 4.09GB/s ± 3% +66.84% (p=0.000 n=9+10) FileAndServer_64MB/TLS-4 1.25GB/s ± 2% 1.26GB/s ± 1% +0.93% (p=0.006 n=10+9) name old alloc/op new alloc/op delta FileAndServer_1KB/NoTLS-4 6.01kB ± 0% 5.03kB ± 0% -16.31% (p=0.000 n=8+10) FileAndServer_1KB/TLS-4 6.11kB ± 0% 6.11kB ± 0% ~ (p=0.127 n=10+10) FileAndServer_16MB/NoTLS-4 38.4kB ± 0% 5.1kB ± 0% -86.71% (p=0.000 n=8+8) FileAndServer_16MB/TLS-4 74.1kB ± 0% 74.1kB ± 0% ~ (p=0.781 n=10+10) FileAndServer_64MB/NoTLS-4 38.9kB ± 0% 5.6kB ± 1% -85.58% (p=0.000 n=8+10) FileAndServer_64MB/TLS-4 184kB ± 1% 184kB ± 0% ~ (p=0.489 n=9+9) name old allocs/op new allocs/op delta FileAndServer_1KB/NoTLS-4 69.0 ± 0% 70.0 ± 0% +1.45% (p=0.000 n=10+10) FileAndServer_1KB/TLS-4 71.0 ± 0% 71.0 ± 0% ~ (all equal) FileAndServer_16MB/NoTLS-4 70.0 ± 0% 70.0 ± 0% ~ (all equal) FileAndServer_16MB/TLS-4 1.11k ± 0% 1.11k ± 0% ~ (p=0.189 n=10+10) FileAndServer_64MB/NoTLS-4 71.0 ± 0% 71.0 ± 0% ~ (all equal) FileAndServer_64MB/TLS-4 4.23k ± 0% 4.24k ± 0% +0.10% (p=0.002 n=9+8) darwin/amd64: name old time/op new time/op delta FileAndServer_1KB/NoTLS-8 92.0µs ± 4% 91.6µs ± 1% ~ (p=0.601 n=10+7) FileAndServer_1KB/TLS-8 103µs ± 5% 97µs ± 1% -6.20% (p=0.000 n=10+9) FileAndServer_16MB/NoTLS-8 10.1ms ± 6% 10.2ms ± 4% ~ (p=0.315 n=10+8) FileAndServer_16MB/TLS-8 18.3ms ± 2% 17.2ms ± 2% -6.34% (p=0.000 n=9+10) FileAndServer_64MB/NoTLS-8 40.1ms ± 7% 37.7ms ± 3% -6.05% (p=0.000 n=9+9) FileAndServer_64MB/TLS-8 70.2ms ± 4% 68.1ms ± 3% -3.01% (p=0.001 n=10+10) name old speed new speed delta FileAndServer_1KB/NoTLS-8 11.1MB/s ± 4% 11.2MB/s ± 1% ~ (p=0.617 n=10+7) FileAndServer_1KB/TLS-8 9.92MB/s ± 5% 10.57MB/s ± 1% +6.53% (p=0.000 n=10+9) FileAndServer_16MB/NoTLS-8 1.67GB/s ± 5% 1.65GB/s ± 3% ~ (p=0.315 n=10+8) FileAndServer_16MB/TLS-8 910MB/s ± 5% 977MB/s ± 2% +7.40% (p=0.000 n=10+10) FileAndServer_64MB/NoTLS-8 1.67GB/s ± 6% 1.78GB/s ± 3% +6.36% (p=0.000 n=9+9) FileAndServer_64MB/TLS-8 956MB/s ± 3% 985MB/s ± 3% +3.09% (p=0.001 n=10+10) name old alloc/op new alloc/op delta FileAndServer_1KB/NoTLS-8 6.03kB ± 0% 6.03kB ± 0% -0.04% (p=0.021 n=8+10) FileAndServer_1KB/TLS-8 6.15kB ± 0% 6.14kB ± 0% -0.18% (p=0.000 n=10+9) FileAndServer_16MB/NoTLS-8 38.8kB ± 1% 38.9kB ± 1% ~ (p=0.529 n=10+10) FileAndServer_16MB/TLS-8 75.5kB ± 1% 75.5kB ± 0% ~ (p=0.481 n=10+10) FileAndServer_64MB/NoTLS-8 40.3kB ± 1% 39.3kB ± 1% -2.47% (p=0.000 n=9+10) FileAndServer_64MB/TLS-8 189kB ± 1% 189kB ± 1% ~ (p=0.684 n=10+10) name old allocs/op new allocs/op delta FileAndServer_1KB/NoTLS-8 68.0 ± 0% 68.0 ± 0% ~ (all equal) FileAndServer_1KB/TLS-8 71.0 ± 0% 71.0 ± 0% ~ (all equal) FileAndServer_16MB/NoTLS-8 69.5 ± 1% 69.6 ± 1% ~ (p=1.000 n=10+10) FileAndServer_16MB/TLS-8 1.13k ± 0% 1.14k ± 0% ~ (p=0.336 n=9+10) FileAndServer_64MB/NoTLS-8 73.6 ± 1% 71.0 ± 0% -3.47% (p=0.000 n=9+9) FileAndServer_64MB/TLS-8 4.34k ± 0% 4.34k ± 0% ~ (p=0.323 n=10+10) Benchmarks between this commit and https://golang.org/cl/163862 (where benchmarks were added): linux/amd64: name old time/op new time/op delta FileAndServer_1KB/NoTLS-4 53.3µs ± 0% 53.4µs ± 0% +0.21% (p=0.028 n=10+9) FileAndServer_1KB/TLS-4 61.2µs ± 0% 60.8µs ± 0% -0.73% (p=0.000 n=10+9) FileAndServer_16MB/NoTLS-4 25.4ms ± 5% 3.8ms ± 5% -84.99% (p=0.000 n=10+10) FileAndServer_16MB/TLS-4 33.3ms ± 2% 13.5ms ± 3% -59.35% (p=0.000 n=9+10) FileAndServer_64MB/NoTLS-4 105ms ± 2% 16ms ± 1% -84.38% (p=0.000 n=9+9) FileAndServer_64MB/TLS-4 128ms ± 2% 54ms ± 2% -58.18% (p=0.000 n=10+10) name old speed new speed delta FileAndServer_1KB/NoTLS-4 19.2MB/s ± 0% 19.2MB/s ± 0% -0.21% (p=0.018 n=10+9) FileAndServer_1KB/TLS-4 16.7MB/s ± 0% 16.8MB/s ± 0% +0.72% (p=0.000 n=10+9) FileAndServer_16MB/NoTLS-4 660MB/s ± 5% 4400MB/s ± 6% +566.83% (p=0.000 n=10+10) FileAndServer_16MB/TLS-4 504MB/s ± 2% 1241MB/s ± 2% +146.03% (p=0.000 n=9+10) FileAndServer_64MB/NoTLS-4 640MB/s ± 2% 4089MB/s ± 2% +538.71% (p=0.000 n=9+10) FileAndServer_64MB/TLS-4 524MB/s ± 2% 1254MB/s ± 2% +139.15% (p=0.000 n=10+10) name old alloc/op new alloc/op delta FileAndServer_1KB/NoTLS-4 4.94kB ± 0% 5.03kB ± 0% +1.82% (p=0.000 n=10+10) FileAndServer_1KB/TLS-4 5.07kB ± 0% 6.11kB ± 0% +20.43% (p=0.000 n=8+10) FileAndServer_16MB/NoTLS-4 5.93kB ± 8% 5.10kB ± 0% -14.06% (p=0.000 n=10+8) FileAndServer_16MB/TLS-4 141kB ± 1% 74kB ± 0% -47.53% (p=0.000 n=10+10) FileAndServer_64MB/NoTLS-4 9.66kB ±22% 5.59kB ± 1% -42.09% (p=0.000 n=10+10) FileAndServer_64MB/TLS-4 551kB ± 0% 184kB ± 0% -66.59% (p=0.000 n=9+9) name old allocs/op new allocs/op delta FileAndServer_1KB/NoTLS-4 66.0 ± 0% 70.0 ± 0% +6.06% (p=0.000 n=10+10) FileAndServer_1KB/TLS-4 70.0 ± 0% 71.0 ± 0% +1.43% (p=0.000 n=10+10) FileAndServer_16MB/NoTLS-4 67.6 ± 1% 70.0 ± 0% +3.55% (p=0.000 n=10+10) FileAndServer_16MB/TLS-4 4.19k ± 0% 1.11k ± 0% -73.59% (p=0.000 n=10+10) FileAndServer_64MB/NoTLS-4 76.3 ± 5% 71.0 ± 0% -6.95% (p=0.000 n=10+10) FileAndServer_64MB/TLS-4 16.6k ± 0% 4.2k ± 0% -74.44% (p=0.000 n=10+8) darwin/amd64: name old time/op new time/op delta FileAndServer_1KB/NoTLS-8 91.5µs ± 2% 96.8µs ± 8% +5.76% (p=0.010 n=10+9) FileAndServer_1KB/TLS-8 98.9µs ± 2% 98.9µs ± 2% ~ (p=0.968 n=10+9) FileAndServer_16MB/NoTLS-8 80.3ms ±20% 9.5ms ± 2% -88.19% (p=0.000 n=9+9) FileAndServer_16MB/TLS-8 54.3ms ±12% 17.6ms ± 4% -67.52% (p=0.000 n=9+10) FileAndServer_64MB/NoTLS-8 406ms ±19% 39ms ± 3% -90.45% (p=0.000 n=10+9) FileAndServer_64MB/TLS-8 205ms ± 6% 71ms ± 3% -65.27% (p=0.000 n=8+10) name old speed new speed delta FileAndServer_1KB/NoTLS-8 11.2MB/s ± 2% 10.6MB/s ± 9% -5.30% (p=0.010 n=10+9) FileAndServer_1KB/TLS-8 10.4MB/s ± 2% 10.4MB/s ± 2% ~ (p=0.889 n=10+9) FileAndServer_16MB/NoTLS-8 211MB/s ±17% 1770MB/s ± 2% +739.26% (p=0.000 n=9+9) FileAndServer_16MB/TLS-8 302MB/s ±26% 952MB/s ± 4% +214.93% (p=0.000 n=10+10) FileAndServer_64MB/NoTLS-8 167MB/s ±17% 1729MB/s ± 3% +938.05% (p=0.000 n=10+9) FileAndServer_64MB/TLS-8 316MB/s ±15% 944MB/s ± 3% +198.59% (p=0.000 n=10+10) name old alloc/op new alloc/op delta FileAndServer_1KB/NoTLS-8 4.96kB ± 0% 6.03kB ± 0% +21.48% (p=0.000 n=10+10) FileAndServer_1KB/TLS-8 5.10kB ± 0% 6.15kB ± 0% +20.59% (p=0.000 n=10+10) FileAndServer_16MB/NoTLS-8 7.70kB ± 1% 38.64kB ± 0% +401.51% (p=0.000 n=10+10) FileAndServer_16MB/TLS-8 137kB ± 2% 76kB ± 0% -44.73% (p=0.000 n=10+9) FileAndServer_64MB/NoTLS-8 24.1kB ±49% 40.0kB ± 2% +66.24% (p=0.000 n=10+10) FileAndServer_64MB/TLS-8 530kB ± 1% 189kB ± 1% -64.42% (p=0.000 n=10+10) name old allocs/op new allocs/op delta FileAndServer_1KB/NoTLS-8 66.0 ± 0% 68.0 ± 0% +3.03% (p=0.000 n=10+10) FileAndServer_1KB/TLS-8 70.0 ± 0% 71.0 ± 0% +1.43% (p=0.000 n=10+10) FileAndServer_16MB/NoTLS-8 71.6 ± 1% 69.0 ± 0% -3.63% (p=0.000 n=10+10) FileAndServer_16MB/TLS-8 3.96k ± 2% 1.13k ± 0% -71.34% (p=0.000 n=10+9) FileAndServer_64MB/NoTLS-8 105 ±22% 73 ± 3% -30.61% (p=0.000 n=10+10) FileAndServer_64MB/TLS-8 15.7k ± 0% 4.3k ± 0% -72.40% (p=0.000 n=10+10) Updates #30377.
1 parent 9a71015 commit 46a2402

File tree

2 files changed

+222
-19
lines changed

2 files changed

+222
-19
lines changed

src/net/http/transfer.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,6 @@ func (br *byteReader) Read(p []byte) (n int, err error) {
5353
return 1, io.EOF
5454
}
5555

56-
// transferBodyReader is an io.Reader that reads from tw.Body
57-
// and records any non-EOF error in tw.bodyReadError.
58-
// It is exactly 1 pointer wide to avoid allocations into interfaces.
59-
type transferBodyReader struct{ tw *transferWriter }
60-
61-
func (br transferBodyReader) Read(p []byte) (n int, err error) {
62-
n, err = br.tw.Body.Read(p)
63-
if err != nil && err != io.EOF {
64-
br.tw.bodyReadError = err
65-
}
66-
return
67-
}
68-
6956
// transferWriter inspects the fields of a user-supplied Request or Response,
7057
// sanitizes them without changing the user object and provides methods for
7158
// writing the respective header, body and trailer in wire format.
@@ -347,15 +334,18 @@ func (t *transferWriter) writeBody(w io.Writer) error {
347334
var err error
348335
var ncopy int64
349336

350-
// Write body
337+
// Write body. We "unwrap" the body first if it was wrapped in a
338+
// nopCloser. This is to ensure that we can take advantage of
339+
// OS-level optimizations in the event that the body is an
340+
// *os.File.
351341
if t.Body != nil {
352-
var body = transferBodyReader{t}
342+
var body = t.unwrapBody()
353343
if chunked(t.TransferEncoding) {
354344
if bw, ok := w.(*bufio.Writer); ok && !t.IsResponse {
355345
w = &internal.FlushAfterChunkWriter{Writer: bw}
356346
}
357347
cw := internal.NewChunkedWriter(w)
358-
_, err = io.Copy(cw, body)
348+
_, err = t.doBodyCopy(cw, body)
359349
if err == nil {
360350
err = cw.Close()
361351
}
@@ -364,14 +354,14 @@ func (t *transferWriter) writeBody(w io.Writer) error {
364354
if t.Method == "CONNECT" {
365355
dst = bufioFlushWriter{dst}
366356
}
367-
ncopy, err = io.Copy(dst, body)
357+
ncopy, err = t.doBodyCopy(dst, body)
368358
} else {
369-
ncopy, err = io.Copy(w, io.LimitReader(body, t.ContentLength))
359+
ncopy, err = t.doBodyCopy(w, io.LimitReader(body, t.ContentLength))
370360
if err != nil {
371361
return err
372362
}
373363
var nextra int64
374-
nextra, err = io.Copy(ioutil.Discard, body)
364+
nextra, err = t.doBodyCopy(ioutil.Discard, body)
375365
ncopy += nextra
376366
}
377367
if err != nil {
@@ -402,6 +392,31 @@ func (t *transferWriter) writeBody(w io.Writer) error {
402392
return err
403393
}
404394

395+
// doBodyCopy wraps a copy operation, with any resulting error also
396+
// being saved in bodyReadError.
397+
//
398+
// This function is only intended for use in writeBody.
399+
func (t *transferWriter) doBodyCopy(dst io.Writer, src io.Reader) (n int64, err error) {
400+
n, err = io.Copy(dst, src)
401+
if err != nil && err != io.EOF {
402+
t.bodyReadError = err
403+
}
404+
return
405+
}
406+
407+
// unwrapBodyReader unwraps the body's inner reader if it's a
408+
// nopCloser. This is to ensure that body writes sourced from local
409+
// files (*os.File types) are properly optimized.
410+
//
411+
// This function is only intended for use in writeBody.
412+
func (t *transferWriter) unwrapBody() io.Reader {
413+
if reflect.TypeOf(t.Body) == nopCloserType {
414+
return reflect.ValueOf(t.Body).Field(0).Interface().(io.Reader)
415+
}
416+
417+
return t.Body
418+
}
419+
405420
type transferReader struct {
406421
// Input
407422
Header Header

src/net/http/transfer_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ package http
77
import (
88
"bufio"
99
"bytes"
10+
"crypto/rand"
11+
"fmt"
1012
"io"
1113
"io/ioutil"
14+
"os"
15+
"reflect"
1216
"strings"
1317
"testing"
1418
)
@@ -90,3 +94,187 @@ func TestDetectInMemoryReaders(t *testing.T) {
9094
}
9195
}
9296
}
97+
98+
type mockTransferWriter struct {
99+
CalledReader io.Reader
100+
WriteCalled bool
101+
}
102+
103+
var _ io.ReaderFrom = (*mockTransferWriter)(nil)
104+
105+
func (w *mockTransferWriter) ReadFrom(r io.Reader) (int64, error) {
106+
w.CalledReader = r
107+
return io.Copy(ioutil.Discard, r)
108+
}
109+
110+
func (w *mockTransferWriter) Write(p []byte) (int, error) {
111+
w.WriteCalled = true
112+
return ioutil.Discard.Write(p)
113+
}
114+
115+
func TestTransferWriterWriteBodyReaderTypes(t *testing.T) {
116+
fileType := reflect.TypeOf(&os.File{})
117+
bufferType := reflect.TypeOf(&bytes.Buffer{})
118+
119+
nBytes := int64(1 << 10)
120+
newFileFunc := func() (r io.Reader, done func(), err error) {
121+
f, err := ioutil.TempFile("", "net-http-newfilefunc")
122+
if err != nil {
123+
return nil, nil, err
124+
}
125+
126+
// Write some bytes to the file to enable reading.
127+
if _, err := io.CopyN(f, rand.Reader, nBytes); err != nil {
128+
return nil, nil, fmt.Errorf("failed to write data to file: %v", err)
129+
}
130+
if _, err := f.Seek(0, 0); err != nil {
131+
return nil, nil, fmt.Errorf("failed to seek to front: %v", err)
132+
}
133+
134+
done = func() {
135+
f.Close()
136+
os.Remove(f.Name())
137+
}
138+
139+
return f, done, nil
140+
}
141+
142+
newBufferFunc := func() (io.Reader, func(), error) {
143+
return bytes.NewBuffer(make([]byte, nBytes)), func() {}, nil
144+
}
145+
146+
cases := []struct {
147+
name string
148+
bodyFunc func() (io.Reader, func(), error)
149+
method string
150+
contentLength int64
151+
transferEncoding []string
152+
limitedReader bool
153+
expectedReader reflect.Type
154+
expectedWrite bool
155+
}{
156+
{
157+
name: "file, non-chunked, size set",
158+
bodyFunc: newFileFunc,
159+
method: "PUT",
160+
contentLength: nBytes,
161+
limitedReader: true,
162+
expectedReader: fileType,
163+
},
164+
{
165+
name: "file, non-chunked, size set, nopCloser wrapped",
166+
method: "PUT",
167+
bodyFunc: func() (io.Reader, func(), error) {
168+
r, cleanup, err := newFileFunc()
169+
return ioutil.NopCloser(r), cleanup, err
170+
},
171+
contentLength: nBytes,
172+
limitedReader: true,
173+
expectedReader: fileType,
174+
},
175+
{
176+
name: "file, non-chunked, negative size",
177+
method: "PUT",
178+
bodyFunc: newFileFunc,
179+
contentLength: -1,
180+
expectedReader: fileType,
181+
},
182+
{
183+
name: "file, non-chunked, CONNECT, negative size",
184+
method: "CONNECT",
185+
bodyFunc: newFileFunc,
186+
contentLength: -1,
187+
expectedReader: fileType,
188+
},
189+
{
190+
name: "file, chunked",
191+
method: "PUT",
192+
bodyFunc: newFileFunc,
193+
transferEncoding: []string{"chunked"},
194+
expectedWrite: true,
195+
},
196+
{
197+
name: "buffer, non-chunked, size set",
198+
bodyFunc: newBufferFunc,
199+
method: "PUT",
200+
contentLength: nBytes,
201+
limitedReader: true,
202+
expectedReader: bufferType,
203+
},
204+
{
205+
name: "buffer, non-chunked, size set, nopCloser wrapped",
206+
method: "PUT",
207+
bodyFunc: func() (io.Reader, func(), error) {
208+
r, cleanup, err := newBufferFunc()
209+
return ioutil.NopCloser(r), cleanup, err
210+
},
211+
contentLength: nBytes,
212+
limitedReader: true,
213+
expectedReader: bufferType,
214+
},
215+
{
216+
name: "buffer, non-chunked, negative size",
217+
method: "PUT",
218+
bodyFunc: newBufferFunc,
219+
contentLength: -1,
220+
expectedWrite: true,
221+
},
222+
{
223+
name: "buffer, non-chunked, CONNECT, negative size",
224+
method: "CONNECT",
225+
bodyFunc: newBufferFunc,
226+
contentLength: -1,
227+
expectedWrite: true,
228+
},
229+
{
230+
name: "buffer, chunked",
231+
method: "PUT",
232+
bodyFunc: newBufferFunc,
233+
transferEncoding: []string{"chunked"},
234+
expectedWrite: true,
235+
},
236+
}
237+
238+
for _, tc := range cases {
239+
t.Run(tc.name, func(t *testing.T) {
240+
body, cleanup, err := tc.bodyFunc()
241+
if err != nil {
242+
t.Fatal(err)
243+
}
244+
defer cleanup()
245+
246+
mw := &mockTransferWriter{}
247+
tw := &transferWriter{
248+
Body: body,
249+
ContentLength: tc.contentLength,
250+
TransferEncoding: tc.transferEncoding,
251+
}
252+
253+
if err := tw.writeBody(mw); err != nil {
254+
t.Fatal(err)
255+
}
256+
257+
if tc.expectedReader != nil {
258+
if mw.CalledReader == nil {
259+
t.Fatal("did not call ReadFrom")
260+
}
261+
262+
var actualReader reflect.Type
263+
lr, ok := mw.CalledReader.(*io.LimitedReader)
264+
if ok && tc.limitedReader {
265+
actualReader = reflect.TypeOf(lr.R)
266+
} else {
267+
actualReader = reflect.TypeOf(mw.CalledReader)
268+
}
269+
270+
if tc.expectedReader != actualReader {
271+
t.Fatalf("got reader %T want %T", actualReader, tc.expectedReader)
272+
}
273+
}
274+
275+
if tc.expectedWrite && !mw.WriteCalled {
276+
t.Fatal("did not invoke Write")
277+
}
278+
})
279+
}
280+
}

0 commit comments

Comments
 (0)