Home | History | Annotate | Download | only in http
      1 // Copyright 2010 The Go Authors.  All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package http
      6 
      7 import (
      8 	"bytes"
      9 	"errors"
     10 	"fmt"
     11 	"io"
     12 	"io/ioutil"
     13 	"net/url"
     14 	"strings"
     15 	"testing"
     16 )
     17 
     18 type reqWriteTest struct {
     19 	Req  Request
     20 	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
     21 
     22 	// Any of these three may be empty to skip that test.
     23 	WantWrite string // Request.Write
     24 	WantProxy string // Request.WriteProxy
     25 
     26 	WantError error // wanted error from Request.Write
     27 }
     28 
     29 var reqWriteTests = []reqWriteTest{
     30 	// HTTP/1.1 => chunked coding; no body; no trailer
     31 	{
     32 		Req: Request{
     33 			Method: "GET",
     34 			URL: &url.URL{
     35 				Scheme: "http",
     36 				Host:   "www.techcrunch.com",
     37 				Path:   "/",
     38 			},
     39 			Proto:      "HTTP/1.1",
     40 			ProtoMajor: 1,
     41 			ProtoMinor: 1,
     42 			Header: Header{
     43 				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
     44 				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
     45 				"Accept-Encoding":  {"gzip,deflate"},
     46 				"Accept-Language":  {"en-us,en;q=0.5"},
     47 				"Keep-Alive":       {"300"},
     48 				"Proxy-Connection": {"keep-alive"},
     49 				"User-Agent":       {"Fake"},
     50 			},
     51 			Body:  nil,
     52 			Close: false,
     53 			Host:  "www.techcrunch.com",
     54 			Form:  map[string][]string{},
     55 		},
     56 
     57 		WantWrite: "GET / HTTP/1.1\r\n" +
     58 			"Host: www.techcrunch.com\r\n" +
     59 			"User-Agent: Fake\r\n" +
     60 			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
     61 			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
     62 			"Accept-Encoding: gzip,deflate\r\n" +
     63 			"Accept-Language: en-us,en;q=0.5\r\n" +
     64 			"Keep-Alive: 300\r\n" +
     65 			"Proxy-Connection: keep-alive\r\n\r\n",
     66 
     67 		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
     68 			"Host: www.techcrunch.com\r\n" +
     69 			"User-Agent: Fake\r\n" +
     70 			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
     71 			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
     72 			"Accept-Encoding: gzip,deflate\r\n" +
     73 			"Accept-Language: en-us,en;q=0.5\r\n" +
     74 			"Keep-Alive: 300\r\n" +
     75 			"Proxy-Connection: keep-alive\r\n\r\n",
     76 	},
     77 	// HTTP/1.1 => chunked coding; body; empty trailer
     78 	{
     79 		Req: Request{
     80 			Method: "GET",
     81 			URL: &url.URL{
     82 				Scheme: "http",
     83 				Host:   "www.google.com",
     84 				Path:   "/search",
     85 			},
     86 			ProtoMajor:       1,
     87 			ProtoMinor:       1,
     88 			Header:           Header{},
     89 			TransferEncoding: []string{"chunked"},
     90 		},
     91 
     92 		Body: []byte("abcdef"),
     93 
     94 		WantWrite: "GET /search HTTP/1.1\r\n" +
     95 			"Host: www.google.com\r\n" +
     96 			"User-Agent: Go-http-client/1.1\r\n" +
     97 			"Transfer-Encoding: chunked\r\n\r\n" +
     98 			chunk("abcdef") + chunk(""),
     99 
    100 		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
    101 			"Host: www.google.com\r\n" +
    102 			"User-Agent: Go-http-client/1.1\r\n" +
    103 			"Transfer-Encoding: chunked\r\n\r\n" +
    104 			chunk("abcdef") + chunk(""),
    105 	},
    106 	// HTTP/1.1 POST => chunked coding; body; empty trailer
    107 	{
    108 		Req: Request{
    109 			Method: "POST",
    110 			URL: &url.URL{
    111 				Scheme: "http",
    112 				Host:   "www.google.com",
    113 				Path:   "/search",
    114 			},
    115 			ProtoMajor:       1,
    116 			ProtoMinor:       1,
    117 			Header:           Header{},
    118 			Close:            true,
    119 			TransferEncoding: []string{"chunked"},
    120 		},
    121 
    122 		Body: []byte("abcdef"),
    123 
    124 		WantWrite: "POST /search HTTP/1.1\r\n" +
    125 			"Host: www.google.com\r\n" +
    126 			"User-Agent: Go-http-client/1.1\r\n" +
    127 			"Connection: close\r\n" +
    128 			"Transfer-Encoding: chunked\r\n\r\n" +
    129 			chunk("abcdef") + chunk(""),
    130 
    131 		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
    132 			"Host: www.google.com\r\n" +
    133 			"User-Agent: Go-http-client/1.1\r\n" +
    134 			"Connection: close\r\n" +
    135 			"Transfer-Encoding: chunked\r\n\r\n" +
    136 			chunk("abcdef") + chunk(""),
    137 	},
    138 
    139 	// HTTP/1.1 POST with Content-Length, no chunking
    140 	{
    141 		Req: Request{
    142 			Method: "POST",
    143 			URL: &url.URL{
    144 				Scheme: "http",
    145 				Host:   "www.google.com",
    146 				Path:   "/search",
    147 			},
    148 			ProtoMajor:    1,
    149 			ProtoMinor:    1,
    150 			Header:        Header{},
    151 			Close:         true,
    152 			ContentLength: 6,
    153 		},
    154 
    155 		Body: []byte("abcdef"),
    156 
    157 		WantWrite: "POST /search HTTP/1.1\r\n" +
    158 			"Host: www.google.com\r\n" +
    159 			"User-Agent: Go-http-client/1.1\r\n" +
    160 			"Connection: close\r\n" +
    161 			"Content-Length: 6\r\n" +
    162 			"\r\n" +
    163 			"abcdef",
    164 
    165 		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
    166 			"Host: www.google.com\r\n" +
    167 			"User-Agent: Go-http-client/1.1\r\n" +
    168 			"Connection: close\r\n" +
    169 			"Content-Length: 6\r\n" +
    170 			"\r\n" +
    171 			"abcdef",
    172 	},
    173 
    174 	// HTTP/1.1 POST with Content-Length in headers
    175 	{
    176 		Req: Request{
    177 			Method: "POST",
    178 			URL:    mustParseURL("http://example.com/"),
    179 			Host:   "example.com",
    180 			Header: Header{
    181 				"Content-Length": []string{"10"}, // ignored
    182 			},
    183 			ContentLength: 6,
    184 		},
    185 
    186 		Body: []byte("abcdef"),
    187 
    188 		WantWrite: "POST / HTTP/1.1\r\n" +
    189 			"Host: example.com\r\n" +
    190 			"User-Agent: Go-http-client/1.1\r\n" +
    191 			"Content-Length: 6\r\n" +
    192 			"\r\n" +
    193 			"abcdef",
    194 
    195 		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
    196 			"Host: example.com\r\n" +
    197 			"User-Agent: Go-http-client/1.1\r\n" +
    198 			"Content-Length: 6\r\n" +
    199 			"\r\n" +
    200 			"abcdef",
    201 	},
    202 
    203 	// default to HTTP/1.1
    204 	{
    205 		Req: Request{
    206 			Method: "GET",
    207 			URL:    mustParseURL("/search"),
    208 			Host:   "www.google.com",
    209 		},
    210 
    211 		WantWrite: "GET /search HTTP/1.1\r\n" +
    212 			"Host: www.google.com\r\n" +
    213 			"User-Agent: Go-http-client/1.1\r\n" +
    214 			"\r\n",
    215 	},
    216 
    217 	// Request with a 0 ContentLength and a 0 byte body.
    218 	{
    219 		Req: Request{
    220 			Method:        "POST",
    221 			URL:           mustParseURL("/"),
    222 			Host:          "example.com",
    223 			ProtoMajor:    1,
    224 			ProtoMinor:    1,
    225 			ContentLength: 0, // as if unset by user
    226 		},
    227 
    228 		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
    229 
    230 		// RFC 2616 Section 14.13 says Content-Length should be specified
    231 		// unless body is prohibited by the request method.
    232 		// Also, nginx expects it for POST and PUT.
    233 		WantWrite: "POST / HTTP/1.1\r\n" +
    234 			"Host: example.com\r\n" +
    235 			"User-Agent: Go-http-client/1.1\r\n" +
    236 			"Content-Length: 0\r\n" +
    237 			"\r\n",
    238 
    239 		WantProxy: "POST / HTTP/1.1\r\n" +
    240 			"Host: example.com\r\n" +
    241 			"User-Agent: Go-http-client/1.1\r\n" +
    242 			"Content-Length: 0\r\n" +
    243 			"\r\n",
    244 	},
    245 
    246 	// Request with a 0 ContentLength and a 1 byte body.
    247 	{
    248 		Req: Request{
    249 			Method:        "POST",
    250 			URL:           mustParseURL("/"),
    251 			Host:          "example.com",
    252 			ProtoMajor:    1,
    253 			ProtoMinor:    1,
    254 			ContentLength: 0, // as if unset by user
    255 		},
    256 
    257 		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
    258 
    259 		WantWrite: "POST / HTTP/1.1\r\n" +
    260 			"Host: example.com\r\n" +
    261 			"User-Agent: Go-http-client/1.1\r\n" +
    262 			"Transfer-Encoding: chunked\r\n\r\n" +
    263 			chunk("x") + chunk(""),
    264 
    265 		WantProxy: "POST / HTTP/1.1\r\n" +
    266 			"Host: example.com\r\n" +
    267 			"User-Agent: Go-http-client/1.1\r\n" +
    268 			"Transfer-Encoding: chunked\r\n\r\n" +
    269 			chunk("x") + chunk(""),
    270 	},
    271 
    272 	// Request with a ContentLength of 10 but a 5 byte body.
    273 	{
    274 		Req: Request{
    275 			Method:        "POST",
    276 			URL:           mustParseURL("/"),
    277 			Host:          "example.com",
    278 			ProtoMajor:    1,
    279 			ProtoMinor:    1,
    280 			ContentLength: 10, // but we're going to send only 5 bytes
    281 		},
    282 		Body:      []byte("12345"),
    283 		WantError: errors.New("http: ContentLength=10 with Body length 5"),
    284 	},
    285 
    286 	// Request with a ContentLength of 4 but an 8 byte body.
    287 	{
    288 		Req: Request{
    289 			Method:        "POST",
    290 			URL:           mustParseURL("/"),
    291 			Host:          "example.com",
    292 			ProtoMajor:    1,
    293 			ProtoMinor:    1,
    294 			ContentLength: 4, // but we're going to try to send 8 bytes
    295 		},
    296 		Body:      []byte("12345678"),
    297 		WantError: errors.New("http: ContentLength=4 with Body length 8"),
    298 	},
    299 
    300 	// Request with a 5 ContentLength and nil body.
    301 	{
    302 		Req: Request{
    303 			Method:        "POST",
    304 			URL:           mustParseURL("/"),
    305 			Host:          "example.com",
    306 			ProtoMajor:    1,
    307 			ProtoMinor:    1,
    308 			ContentLength: 5, // but we'll omit the body
    309 		},
    310 		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
    311 	},
    312 
    313 	// Request with a 0 ContentLength and a body with 1 byte content and an error.
    314 	{
    315 		Req: Request{
    316 			Method:        "POST",
    317 			URL:           mustParseURL("/"),
    318 			Host:          "example.com",
    319 			ProtoMajor:    1,
    320 			ProtoMinor:    1,
    321 			ContentLength: 0, // as if unset by user
    322 		},
    323 
    324 		Body: func() io.ReadCloser {
    325 			err := errors.New("Custom reader error")
    326 			errReader := &errorReader{err}
    327 			return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
    328 		},
    329 
    330 		WantError: errors.New("Custom reader error"),
    331 	},
    332 
    333 	// Request with a 0 ContentLength and a body without content and an error.
    334 	{
    335 		Req: Request{
    336 			Method:        "POST",
    337 			URL:           mustParseURL("/"),
    338 			Host:          "example.com",
    339 			ProtoMajor:    1,
    340 			ProtoMinor:    1,
    341 			ContentLength: 0, // as if unset by user
    342 		},
    343 
    344 		Body: func() io.ReadCloser {
    345 			err := errors.New("Custom reader error")
    346 			errReader := &errorReader{err}
    347 			return ioutil.NopCloser(errReader)
    348 		},
    349 
    350 		WantError: errors.New("Custom reader error"),
    351 	},
    352 
    353 	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
    354 	// and doesn't add a User-Agent.
    355 	{
    356 		Req: Request{
    357 			Method:     "GET",
    358 			URL:        mustParseURL("/foo"),
    359 			ProtoMajor: 1,
    360 			ProtoMinor: 0,
    361 			Header: Header{
    362 				"X-Foo": []string{"X-Bar"},
    363 			},
    364 		},
    365 
    366 		WantWrite: "GET /foo HTTP/1.1\r\n" +
    367 			"Host: \r\n" +
    368 			"User-Agent: Go-http-client/1.1\r\n" +
    369 			"X-Foo: X-Bar\r\n\r\n",
    370 	},
    371 
    372 	// If no Request.Host and no Request.URL.Host, we send
    373 	// an empty Host header, and don't use
    374 	// Request.Header["Host"]. This is just testing that
    375 	// we don't change Go 1.0 behavior.
    376 	{
    377 		Req: Request{
    378 			Method: "GET",
    379 			Host:   "",
    380 			URL: &url.URL{
    381 				Scheme: "http",
    382 				Host:   "",
    383 				Path:   "/search",
    384 			},
    385 			ProtoMajor: 1,
    386 			ProtoMinor: 1,
    387 			Header: Header{
    388 				"Host": []string{"bad.example.com"},
    389 			},
    390 		},
    391 
    392 		WantWrite: "GET /search HTTP/1.1\r\n" +
    393 			"Host: \r\n" +
    394 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    395 	},
    396 
    397 	// Opaque test #1 from golang.org/issue/4860
    398 	{
    399 		Req: Request{
    400 			Method: "GET",
    401 			URL: &url.URL{
    402 				Scheme: "http",
    403 				Host:   "www.google.com",
    404 				Opaque: "/%2F/%2F/",
    405 			},
    406 			ProtoMajor: 1,
    407 			ProtoMinor: 1,
    408 			Header:     Header{},
    409 		},
    410 
    411 		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
    412 			"Host: www.google.com\r\n" +
    413 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    414 	},
    415 
    416 	// Opaque test #2 from golang.org/issue/4860
    417 	{
    418 		Req: Request{
    419 			Method: "GET",
    420 			URL: &url.URL{
    421 				Scheme: "http",
    422 				Host:   "x.google.com",
    423 				Opaque: "//y.google.com/%2F/%2F/",
    424 			},
    425 			ProtoMajor: 1,
    426 			ProtoMinor: 1,
    427 			Header:     Header{},
    428 		},
    429 
    430 		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
    431 			"Host: x.google.com\r\n" +
    432 			"User-Agent: Go-http-client/1.1\r\n\r\n",
    433 	},
    434 
    435 	// Testing custom case in header keys. Issue 5022.
    436 	{
    437 		Req: Request{
    438 			Method: "GET",
    439 			URL: &url.URL{
    440 				Scheme: "http",
    441 				Host:   "www.google.com",
    442 				Path:   "/",
    443 			},
    444 			Proto:      "HTTP/1.1",
    445 			ProtoMajor: 1,
    446 			ProtoMinor: 1,
    447 			Header: Header{
    448 				"ALL-CAPS": {"x"},
    449 			},
    450 		},
    451 
    452 		WantWrite: "GET / HTTP/1.1\r\n" +
    453 			"Host: www.google.com\r\n" +
    454 			"User-Agent: Go-http-client/1.1\r\n" +
    455 			"ALL-CAPS: x\r\n" +
    456 			"\r\n",
    457 	},
    458 
    459 	// Request with host header field; IPv6 address with zone identifier
    460 	{
    461 		Req: Request{
    462 			Method: "GET",
    463 			URL: &url.URL{
    464 				Host: "[fe80::1%en0]",
    465 			},
    466 		},
    467 
    468 		WantWrite: "GET / HTTP/1.1\r\n" +
    469 			"Host: [fe80::1]\r\n" +
    470 			"User-Agent: Go-http-client/1.1\r\n" +
    471 			"\r\n",
    472 	},
    473 
    474 	// Request with optional host header field; IPv6 address with zone identifier
    475 	{
    476 		Req: Request{
    477 			Method: "GET",
    478 			URL: &url.URL{
    479 				Host: "www.example.com",
    480 			},
    481 			Host: "[fe80::1%en0]:8080",
    482 		},
    483 
    484 		WantWrite: "GET / HTTP/1.1\r\n" +
    485 			"Host: [fe80::1]:8080\r\n" +
    486 			"User-Agent: Go-http-client/1.1\r\n" +
    487 			"\r\n",
    488 	},
    489 }
    490 
    491 func TestRequestWrite(t *testing.T) {
    492 	for i := range reqWriteTests {
    493 		tt := &reqWriteTests[i]
    494 
    495 		setBody := func() {
    496 			if tt.Body == nil {
    497 				return
    498 			}
    499 			switch b := tt.Body.(type) {
    500 			case []byte:
    501 				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
    502 			case func() io.ReadCloser:
    503 				tt.Req.Body = b()
    504 			}
    505 		}
    506 		setBody()
    507 		if tt.Req.Header == nil {
    508 			tt.Req.Header = make(Header)
    509 		}
    510 
    511 		var braw bytes.Buffer
    512 		err := tt.Req.Write(&braw)
    513 		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
    514 			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
    515 			continue
    516 		}
    517 		if err != nil {
    518 			continue
    519 		}
    520 
    521 		if tt.WantWrite != "" {
    522 			sraw := braw.String()
    523 			if sraw != tt.WantWrite {
    524 				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
    525 				continue
    526 			}
    527 		}
    528 
    529 		if tt.WantProxy != "" {
    530 			setBody()
    531 			var praw bytes.Buffer
    532 			err = tt.Req.WriteProxy(&praw)
    533 			if err != nil {
    534 				t.Errorf("WriteProxy #%d: %s", i, err)
    535 				continue
    536 			}
    537 			sraw := praw.String()
    538 			if sraw != tt.WantProxy {
    539 				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
    540 				continue
    541 			}
    542 		}
    543 	}
    544 }
    545 
    546 type closeChecker struct {
    547 	io.Reader
    548 	closed bool
    549 }
    550 
    551 func (rc *closeChecker) Close() error {
    552 	rc.closed = true
    553 	return nil
    554 }
    555 
    556 // TestRequestWriteClosesBody tests that Request.Write does close its request.Body.
    557 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
    558 // inside a NopCloser, and that it serializes it correctly.
    559 func TestRequestWriteClosesBody(t *testing.T) {
    560 	rc := &closeChecker{Reader: strings.NewReader("my body")}
    561 	req, _ := NewRequest("POST", "http://foo.com/", rc)
    562 	if req.ContentLength != 0 {
    563 		t.Errorf("got req.ContentLength %d, want 0", req.ContentLength)
    564 	}
    565 	buf := new(bytes.Buffer)
    566 	req.Write(buf)
    567 	if !rc.closed {
    568 		t.Error("body not closed after write")
    569 	}
    570 	expected := "POST / HTTP/1.1\r\n" +
    571 		"Host: foo.com\r\n" +
    572 		"User-Agent: Go-http-client/1.1\r\n" +
    573 		"Transfer-Encoding: chunked\r\n\r\n" +
    574 		// TODO: currently we don't buffer before chunking, so we get a
    575 		// single "m" chunk before the other chunks, as this was the 1-byte
    576 		// read from our MultiReader where we stiched the Body back together
    577 		// after sniffing whether the Body was 0 bytes or not.
    578 		chunk("m") +
    579 		chunk("y body") +
    580 		chunk("")
    581 	if buf.String() != expected {
    582 		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
    583 	}
    584 }
    585 
    586 func chunk(s string) string {
    587 	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
    588 }
    589 
    590 func mustParseURL(s string) *url.URL {
    591 	u, err := url.Parse(s)
    592 	if err != nil {
    593 		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
    594 	}
    595 	return u
    596 }
    597 
    598 type writerFunc func([]byte) (int, error)
    599 
    600 func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
    601 
    602 // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
    603 func TestRequestWriteError(t *testing.T) {
    604 	failAfter, writeCount := 0, 0
    605 	errFail := errors.New("fake write failure")
    606 
    607 	// w is the buffered io.Writer to write the request to.  It
    608 	// fails exactly once on its Nth Write call, as controlled by
    609 	// failAfter. It also tracks the number of calls in
    610 	// writeCount.
    611 	w := struct {
    612 		io.ByteWriter // to avoid being wrapped by a bufio.Writer
    613 		io.Writer
    614 	}{
    615 		nil,
    616 		writerFunc(func(p []byte) (n int, err error) {
    617 			writeCount++
    618 			if failAfter == 0 {
    619 				err = errFail
    620 			}
    621 			failAfter--
    622 			return len(p), err
    623 		}),
    624 	}
    625 
    626 	req, _ := NewRequest("GET", "http://example.com/", nil)
    627 	const writeCalls = 4 // number of Write calls in current implementation
    628 	sawGood := false
    629 	for n := 0; n <= writeCalls+2; n++ {
    630 		failAfter = n
    631 		writeCount = 0
    632 		err := req.Write(w)
    633 		var wantErr error
    634 		if n < writeCalls {
    635 			wantErr = errFail
    636 		}
    637 		if err != wantErr {
    638 			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
    639 			continue
    640 		}
    641 		if err == nil {
    642 			sawGood = true
    643 			if writeCount != writeCalls {
    644 				t.Fatalf("writeCalls constant is outdated in test")
    645 			}
    646 		}
    647 		if writeCount > writeCalls || writeCount > n+1 {
    648 			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
    649 		}
    650 	}
    651 	if !sawGood {
    652 		t.Fatalf("writeCalls constant is outdated in test")
    653 	}
    654 }
    655