// Copyright 2013 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 main_test import ( "bufio" "bytes" "fmt" "go/build" "io" "io/ioutil" "net" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "testing" "time" ) // buildGodoc builds the godoc executable. // It returns its path, and a cleanup function. // // TODO(adonovan): opt: do this at most once, and do the cleanup // exactly once. How though? There's no atexit. func buildGodoc(t *testing.T) (bin string, cleanup func()) { if runtime.GOARCH == "arm" { t.Skip("skipping test on arm platforms; too slow") } tmp, err := ioutil.TempDir("", "godoc-regtest-") if err != nil { t.Fatal(err) } defer func() { if cleanup == nil { // probably, go build failed. os.RemoveAll(tmp) } }() bin = filepath.Join(tmp, "godoc") if runtime.GOOS == "windows" { bin += ".exe" } cmd := exec.Command("go", "build", "-o", bin) if err := cmd.Run(); err != nil { t.Fatalf("Building godoc: %v", err) } return bin, func() { os.RemoveAll(tmp) } } // Basic regression test for godoc command-line tool. func TestCLI(t *testing.T) { bin, cleanup := buildGodoc(t) defer cleanup() tests := []struct { args []string matches []string // regular expressions dontmatch []string // regular expressions }{ { args: []string{"fmt"}, matches: []string{ `import "fmt"`, `Package fmt implements formatted I/O`, }, }, { args: []string{"io", "WriteString"}, matches: []string{ `func WriteString\(`, `WriteString writes the contents of the string s to w`, }, }, { args: []string{"nonexistingpkg"}, matches: []string{ `cannot find package`, }, }, { args: []string{"fmt", "NonexistentSymbol"}, matches: []string{ `No match found\.`, }, }, { args: []string{"-src", "syscall", "Open"}, matches: []string{ `func Open\(`, }, dontmatch: []string{ `No match found\.`, }, }, } for _, test := range tests { cmd := exec.Command(bin, test.args...) cmd.Args[0] = "godoc" out, err := cmd.CombinedOutput() if err != nil { t.Errorf("Running with args %#v: %v", test.args, err) continue } for _, pat := range test.matches { re := regexp.MustCompile(pat) if !re.Match(out) { t.Errorf("godoc %v =\n%s\nwanted /%v/", strings.Join(test.args, " "), out, pat) } } for _, pat := range test.dontmatch { re := regexp.MustCompile(pat) if re.Match(out) { t.Errorf("godoc %v =\n%s\ndid not want /%v/", strings.Join(test.args, " "), out, pat) } } } } func serverAddress(t *testing.T) string { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { ln, err = net.Listen("tcp6", "[::1]:0") } if err != nil { t.Fatal(err) } defer ln.Close() return ln.Addr().String() } func waitForServerReady(t *testing.T, addr string) { waitForServer(t, fmt.Sprintf("http://%v/", addr), "The Go Programming Language", 15*time.Second, false) } func waitForSearchReady(t *testing.T, addr string) { waitForServer(t, fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr), "The list of tokens.", 2*time.Minute, false) } func waitUntilScanComplete(t *testing.T, addr string) { waitForServer(t, fmt.Sprintf("http://%v/pkg", addr), "Scan is not yet complete", 2*time.Minute, true, ) // setting reverse as true, which means this waits // until the string is not returned in the response anymore } const pollInterval = 200 * time.Millisecond func waitForServer(t *testing.T, url, match string, timeout time.Duration, reverse bool) { // "health check" duplicated from x/tools/cmd/tipgodoc/tip.go deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { time.Sleep(pollInterval) res, err := http.Get(url) if err != nil { continue } rbody, err := ioutil.ReadAll(res.Body) res.Body.Close() if err == nil && res.StatusCode == http.StatusOK { if bytes.Contains(rbody, []byte(match)) && !reverse { return } if !bytes.Contains(rbody, []byte(match)) && reverse { return } } } t.Fatalf("Server failed to respond in %v", timeout) } // hasTag checks whether a given release tag is contained in the current version // of the go binary. func hasTag(t string) bool { for _, v := range build.Default.ReleaseTags { if t == v { return true } } return false } func killAndWait(cmd *exec.Cmd) { cmd.Process.Kill() cmd.Wait() } // Basic integration test for godoc HTTP interface. func TestWeb(t *testing.T) { testWeb(t, false) } // Basic integration test for godoc HTTP interface. func TestWebIndex(t *testing.T) { if testing.Short() { t.Skip("skipping test in -short mode") } testWeb(t, true) } // Basic integration test for godoc HTTP interface. func testWeb(t *testing.T, withIndex bool) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; files to start up quickly enough") } bin, cleanup := buildGodoc(t) defer cleanup() addr := serverAddress(t) args := []string{fmt.Sprintf("-http=%s", addr)} if withIndex { args = append(args, "-index", "-index_interval=-1s") } cmd := exec.Command(bin, args...) cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr cmd.Args[0] = "godoc" // Set GOPATH variable to non-existing path // and GOPROXY=off to disable module fetches. // We cannot just unset GOPATH variable because godoc would default it to ~/go. // (We don't want the indexer looking at the local workspace during tests.) cmd.Env = append(os.Environ(), "GOPATH=does_not_exist", "GOPROXY=off", "GO111MODULE=off") if err := cmd.Start(); err != nil { t.Fatalf("failed to start godoc: %s", err) } defer killAndWait(cmd) if withIndex { waitForSearchReady(t, addr) } else { waitForServerReady(t, addr) waitUntilScanComplete(t, addr) } tests := []struct { path string contains []string // substring match []string // regexp notContains []string needIndex bool releaseTag string // optional release tag that must be in go/build.ReleaseTags }{ { path: "/", contains: []string{"Go is an open source programming language"}, }, { path: "/pkg/fmt/", contains: []string{"Package fmt implements formatted I/O"}, }, { path: "/src/fmt/", contains: []string{"scan_test.go"}, }, { path: "/src/fmt/print.go", contains: []string{"// Println formats using"}, }, { path: "/pkg", contains: []string{ "Standard library", "Package fmt implements formatted I/O", }, notContains: []string{ "internal/syscall", "cmd/gc", }, }, { path: "/pkg/?m=all", contains: []string{ "Standard library", "Package fmt implements formatted I/O", "internal/syscall/?m=all", }, notContains: []string{ "cmd/gc", }, }, { path: "/search?q=ListenAndServe", contains: []string{ "/src", }, notContains: []string{ "/pkg/bootstrap", }, needIndex: true, }, { path: "/pkg/strings/", contains: []string{ `href="/src/strings/strings.go"`, }, }, { path: "/cmd/compile/internal/amd64/", contains: []string{ `href="/src/cmd/compile/internal/amd64/ssa.go"`, }, }, { path: "/pkg/math/bits/", contains: []string{ `Added in Go 1.9`, }, }, { path: "/pkg/net/", contains: []string{ `// IPv6 scoped addressing zone; added in Go 1.1`, }, }, { path: "/pkg/net/http/httptrace/", match: []string{ `Got1xxResponse.*// Go 1\.11`, }, releaseTag: "go1.11", }, // Verify we don't add version info to a struct field added the same time // as the struct itself: { path: "/pkg/net/http/httptrace/", match: []string{ `(?m)GotFirstResponseByte func\(\)\s*$`, }, }, // Remove trailing periods before adding semicolons: { path: "/pkg/database/sql/", contains: []string{ "The number of connections currently in use; added in Go 1.11", "The number of idle connections; added in Go 1.11", }, releaseTag: "go1.11", }, } for _, test := range tests { if test.needIndex && !withIndex { continue } url := fmt.Sprintf("http://%s%s", addr, test.path) resp, err := http.Get(url) if err != nil { t.Errorf("GET %s failed: %s", url, err) continue } body, err := ioutil.ReadAll(resp.Body) strBody := string(body) resp.Body.Close() if err != nil { t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp) } isErr := false for _, substr := range test.contains { if test.releaseTag != "" && !hasTag(test.releaseTag) { continue } if !bytes.Contains(body, []byte(substr)) { t.Errorf("GET %s: wanted substring %q in body", url, substr) isErr = true } } for _, re := range test.match { if test.releaseTag != "" && !hasTag(test.releaseTag) { continue } if ok, err := regexp.MatchString(re, strBody); !ok || err != nil { if err != nil { t.Fatalf("Bad regexp %q: %v", re, err) } t.Errorf("GET %s: wanted to match %s in body", url, re) isErr = true } } for _, substr := range test.notContains { if bytes.Contains(body, []byte(substr)) { t.Errorf("GET %s: didn't want substring %q in body", url, substr) isErr = true } } if isErr { t.Errorf("GET %s: got:\n%s", url, body) } } } // Basic integration test for godoc -analysis=type (via HTTP interface). func TestTypeAnalysis(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below } // Write a fake GOROOT/GOPATH. tmpdir, err := ioutil.TempDir("", "godoc-analysis") if err != nil { t.Fatalf("ioutil.TempDir failed: %s", err) } defer os.RemoveAll(tmpdir) for _, f := range []struct{ file, content string }{ {"goroot/src/lib/lib.go", ` package lib type T struct{} const C = 3 var V T func (T) F() int { return C } `}, {"gopath/src/app/main.go", ` package main import "lib" func main() { print(lib.V) } `}, } { file := filepath.Join(tmpdir, f.file) if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { t.Fatalf("MkdirAll(%s) failed: %s", filepath.Dir(file), err) } if err := ioutil.WriteFile(file, []byte(f.content), 0644); err != nil { t.Fatal(err) } } // Start the server. bin, cleanup := buildGodoc(t) defer cleanup() addr := serverAddress(t) cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type") cmd.Env = os.Environ() cmd.Env = append(cmd.Env, fmt.Sprintf("GOROOT=%s", filepath.Join(tmpdir, "goroot"))) cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(tmpdir, "gopath"))) cmd.Env = append(cmd.Env, "GO111MODULE=off") cmd.Env = append(cmd.Env, "GOPROXY=off") cmd.Stdout = os.Stderr stderr, err := cmd.StderrPipe() if err != nil { t.Fatal(err) } cmd.Args[0] = "godoc" if err := cmd.Start(); err != nil { t.Fatalf("failed to start godoc: %s", err) } defer killAndWait(cmd) waitForServerReady(t, addr) // Wait for type analysis to complete. reader := bufio.NewReader(stderr) for { s, err := reader.ReadString('\n') // on Plan 9 this fails if err != nil { t.Fatal(err) } fmt.Fprint(os.Stderr, s) if strings.Contains(s, "Type analysis complete.") { break } } go io.Copy(os.Stderr, reader) t0 := time.Now() // Make an HTTP request and check for a regular expression match. // The patterns are very crude checks that basic type information // has been annotated onto the source view. tryagain: for _, test := range []struct{ url, pattern string }{ {"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"}, {"/src/lib/lib.go", "L3.*type .*type info for T.*struct"}, {"/src/lib/lib.go", "L5.*var V .*type T struct"}, {"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"}, {"/src/app/main.go", "L2.*package .*Package docs for app"}, {"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"}, {"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"}, } { url := fmt.Sprintf("http://%s%s", addr, test.url) resp, err := http.Get(url) if err != nil { t.Errorf("GET %s failed: %s", url, err) continue } body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp) continue } if !bytes.Contains(body, []byte("Static analysis features")) { // Type analysis results usually become available within // ~4ms after godoc startup (for this input on my machine). if elapsed := time.Since(t0); elapsed > 500*time.Millisecond { t.Fatalf("type analysis results still unavailable after %s", elapsed) } time.Sleep(10 * time.Millisecond) goto tryagain } match, err := regexp.Match(test.pattern, body) if err != nil { t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err) continue } if !match { // This is a really ugly failure message. t.Errorf("GET %s: body doesn't match %q, got:\n%s", url, test.pattern, string(body)) } } }