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 +}