cmd/go/internal/webtest: web test helper library
diff --git a/vendor/cmd/go/internal/webtest/test.go b/vendor/cmd/go/internal/webtest/test.go
new file mode 100644
index 0000000..94b20a3
--- /dev/null
+++ b/vendor/cmd/go/internal/webtest/test.go
@@ -0,0 +1,314 @@
+// Copyright 2018 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.
+
+package webtest
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/hex"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"unicode/utf8"
+
+	web "cmd/go/internal/web2"
+)
+
+var mode = flag.String("webtest", "replay", "set webtest `mode` - record, replay, bypass")
+
+func Hook() {
+	if *mode == "bypass" {
+		return
+	}
+	web.SetHTTPDoForTesting(Do)
+}
+
+func Unhook() {
+	web.SetHTTPDoForTesting(nil)
+}
+
+func Print() {
+	web.SetHTTPDoForTesting(DoPrint)
+}
+
+var responses struct {
+	mu    sync.Mutex
+	byURL map[string]*respEntry
+}
+
+type respEntry struct {
+	status string
+	code   int
+	hdr    http.Header
+	body   []byte
+}
+
+func Serve(url string, status string, hdr http.Header, body []byte) {
+	if status == "" {
+		status = "200 OK"
+	}
+	code, err := strconv.Atoi(strings.Fields(status)[0])
+	if err != nil {
+		panic("bad Serve status - " + status + " - " + err.Error())
+	}
+
+	responses.mu.Lock()
+	defer responses.mu.Unlock()
+
+	if responses.byURL == nil {
+		responses.byURL = make(map[string]*respEntry)
+	}
+	responses.byURL[url] = &respEntry{status: status, code: code, hdr: web.CopyHeader(hdr), body: body}
+}
+
+func Do(req *http.Request) (*http.Response, error) {
+	if req.Method != "GET" {
+		return nil, fmt.Errorf("bad method - must be GET")
+	}
+
+	responses.mu.Lock()
+	e := responses.byURL[req.URL.String()]
+	responses.mu.Unlock()
+
+	if e == nil {
+		if *mode == "record" {
+			loaded.mu.Lock()
+			if len(loaded.did) != 1 {
+				loaded.mu.Unlock()
+				return nil, fmt.Errorf("cannot use -webtest=record with multiple loaded response files")
+			}
+			var file string
+			for file = range loaded.did {
+				break
+			}
+			loaded.mu.Unlock()
+			return doSave(file, req)
+		}
+		e = &respEntry{code: 599, status: "599 unexpected request (no canned response)"}
+	}
+	resp := &http.Response{
+		Status:     e.status,
+		StatusCode: e.code,
+		Header:     web.CopyHeader(e.hdr),
+		Body:       ioutil.NopCloser(bytes.NewReader(e.body)),
+	}
+	return resp, nil
+}
+
+func DoPrint(req *http.Request) (*http.Response, error) {
+	return doSave("", req)
+}
+
+func doSave(file string, req *http.Request) (*http.Response, error) {
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	data, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+	resp.Body = ioutil.NopCloser(bytes.NewReader(data))
+
+	var f *os.File
+	if file == "" {
+		f = os.Stderr
+	} else {
+		f, err = os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
+		if err != nil {
+			log.Fatal(err)
+		}
+		defer f.Close()
+	}
+
+	fmt.Fprintf(f, "GET %s\n", req.URL.String())
+	fmt.Fprintf(f, "%s\n", resp.Status)
+	var keys []string
+	for k := range resp.Header {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	for _, k := range keys {
+		if k == "Set-Cookie" {
+			continue
+		}
+		for _, v := range resp.Header[k] {
+			fmt.Fprintf(f, "%s: %s\n", k, v)
+		}
+	}
+	fmt.Fprintf(f, "\n")
+	if utf8.Valid(data) && !bytes.Contains(data, []byte("\nGET")) && !isHexDump(data) {
+		fmt.Fprintf(f, "%s\n\n", data)
+	} else {
+		fmt.Fprintf(f, "%s\n", hex.Dump(data))
+	}
+	return resp, err
+}
+
+var loaded struct {
+	mu  sync.Mutex
+	did map[string]bool
+}
+
+func LoadOnce(file string) {
+	loaded.mu.Lock()
+	if loaded.did[file] {
+		loaded.mu.Unlock()
+		return
+	}
+	if loaded.did == nil {
+		loaded.did = make(map[string]bool)
+	}
+	loaded.did[file] = true
+	loaded.mu.Unlock()
+
+	f, err := os.Open(file)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer f.Close()
+
+	b := bufio.NewReader(f)
+	var ungetLine string
+	nextLine := func() string {
+		if ungetLine != "" {
+			l := ungetLine
+			ungetLine = ""
+			return l
+		}
+		line, err := b.ReadString('\n')
+		if err != nil {
+			if err == io.EOF {
+				return ""
+			}
+			log.Fatalf("%s: unexpected read error: %v", file, err)
+		}
+		return line
+	}
+
+	for {
+		line := nextLine()
+		if line == "" { // EOF
+			break
+		}
+		line = strings.TrimSpace(line)
+		if strings.HasPrefix(line, "#") || line == "" {
+			continue
+		}
+		if !strings.HasPrefix(line, "GET ") {
+			log.Fatalf("%s: malformed GET line: %s", file, line)
+		}
+		url := line[len("GET "):]
+		status := nextLine()
+		if _, err := strconv.Atoi(strings.Fields(status)[0]); err != nil {
+			log.Fatalf("%s: malformed status line (after GET %s): %s", file, url, status)
+		}
+		hdr := make(http.Header)
+		for {
+			kv := strings.TrimSpace(nextLine())
+			if kv == "" {
+				break
+			}
+			i := strings.Index(kv, ":")
+			if i < 0 {
+				log.Fatalf("%s: malformed header line (after GET %s): %s", file, url, kv)
+			}
+			k, v := kv[:i], strings.TrimSpace(kv[i+1:])
+			hdr[k] = append(hdr[k], v)
+		}
+
+		var body []byte
+	Body:
+		for n := 0; ; n++ {
+			line := nextLine()
+			if n == 0 && isHexDump([]byte(line)) {
+				ungetLine = line
+				b, err := parseHexDump(nextLine)
+				if err != nil {
+					log.Fatalf("%s: malformed hex dump (after GET %s): %v", file, url, err)
+				}
+				body = b
+				break
+			}
+			if line == "" { // EOF
+				for i := 0; i < 2; i++ {
+					if len(body) > 0 && body[len(body)-1] == '\n' {
+						body = body[:len(body)-1]
+					}
+				}
+				break
+			}
+			body = append(body, line...)
+			for line == "\n" {
+				line = nextLine()
+				if strings.HasPrefix(line, "GET ") {
+					ungetLine = line
+					body = body[:len(body)-1]
+					if len(body) > 0 {
+						body = body[:len(body)-1]
+					}
+					break Body
+				}
+				body = append(body, line...)
+			}
+		}
+
+		Serve(url, status, hdr, body)
+	}
+}
+
+func isHexDump(data []byte) bool {
+	return bytes.HasPrefix(data, []byte("00000000  ")) || bytes.HasPrefix(data, []byte("0000000 "))
+}
+
+// parseHexDump parses the hex dump in text, which should be the
+// output of "hexdump -C" or Plan 9's "xd -b" or Go's hex.Dump
+// and returns the original data used to produce the dump.
+// It is meant to enable storing golden binary files as text, so that
+// changes to the golden files can be seen during code reviews.
+func parseHexDump(nextLine func() string) ([]byte, error) {
+	var out []byte
+	for {
+		line := nextLine()
+		if line == "" || line == "\n" {
+			break
+		}
+		if i := strings.Index(line, "|"); i >= 0 { // remove text dump
+			line = line[:i]
+		}
+		f := strings.Fields(line)
+		if len(f) > 1+16 {
+			return nil, fmt.Errorf("parsing hex dump: too many fields on line %q", line)
+		}
+		if len(f) == 0 || len(f) == 1 && f[0] == "*" { // all zeros block omitted
+			continue
+		}
+		addr64, err := strconv.ParseUint(f[0], 16, 0)
+		if err != nil {
+			return nil, fmt.Errorf("parsing hex dump: invalid address %q", f[0])
+		}
+		addr := int(addr64)
+		if len(out) < addr {
+			out = append(out, make([]byte, addr-len(out))...)
+		}
+		for _, x := range f[1:] {
+			val, err := strconv.ParseUint(x, 16, 8)
+			if err != nil {
+				return nil, fmt.Errorf("parsing hexdump: invalid hex byte %q", x)
+			}
+			out = append(out, byte(val))
+		}
+	}
+	return out, nil
+}