// 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" "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/internal/testenv" ) // 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()) { t.Helper() if runtime.GOARCH == "arm" { t.Skip("skipping test on arm platforms; too slow") } if runtime.GOOS == "android" { t.Skipf("the dependencies are not available on android") } testenv.NeedsTool(t, "go") 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) } } 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, cmd *exec.Cmd, addr string) { ch := make(chan error, 1) go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }() go waitForServer(t, ch, fmt.Sprintf("http://%v/", addr), "Go Documentation Server", 15*time.Second, false) if err := <-ch; err != nil { t.Fatal(err) } } func waitForSearchReady(t *testing.T, cmd *exec.Cmd, addr string) { ch := make(chan error, 1) go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }() go waitForServer(t, ch, fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr), "The list of tokens.", 2*time.Minute, false) if err := <-ch; err != nil { t.Fatal(err) } } func waitUntilScanComplete(t *testing.T, addr string) { ch := make(chan error) go waitForServer(t, ch, fmt.Sprintf("http://%v/pkg", addr), "Scan is not yet complete", 2*time.Minute, // setting reverse as true, which means this waits // until the string is not returned in the response anymore true, ) if err := <-ch; err != nil { t.Fatal(err) } } const pollInterval = 200 * time.Millisecond // waitForServer waits for server to meet the required condition. // It sends a single error value to ch, unless the test has failed. // The error value is nil if the required condition was met within // timeout, or non-nil otherwise. func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout time.Duration, reverse bool) { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { time.Sleep(pollInterval) if t.Failed() { return } res, err := http.Get(url) if err != nil { continue } body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil || res.StatusCode != http.StatusOK { continue } switch { case !reverse && bytes.Contains(body, []byte(match)), reverse && !bytes.Contains(body, []byte(match)): ch <- nil return } } ch <- fmt.Errorf("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.Process.Wait() } func TestURL(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; fails to start up quickly enough") } bin, cleanup := buildGodoc(t) defer cleanup() testcase := func(url string, contents string) func(t *testing.T) { return func(t *testing.T) { stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) args := []string{fmt.Sprintf("-url=%s", url)} cmd := exec.Command(bin, args...) cmd.Stdout = stdout cmd.Stderr = stderr cmd.Args[0] = "godoc" // Set GOPATH variable to a non-existing absolute 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.Run(); err != nil { t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr) } if !strings.Contains(stdout.String(), contents) { t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout) } } } t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree.")) t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O")) } // Basic integration test for godoc HTTP interface. func TestWeb(t *testing.T) { bin, cleanup := buildGodoc(t) defer cleanup() for _, x := range packagestest.All { t.Run(x.Name(), func(t *testing.T) { testWeb(t, x, bin, false) }) } } // Basic integration test for godoc HTTP interface. func TestWebIndex(t *testing.T) { if testing.Short() { t.Skip("skipping test in -short mode") } bin, cleanup := buildGodoc(t) defer cleanup() testWeb(t, packagestest.GOPATH, bin, true) } // Basic integration test for godoc HTTP interface. func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; fails to start up quickly enough") } // Write a fake GOROOT/GOPATH with some third party packages. e := packagestest.Export(t, x, []packagestest.Module{ { Name: "godoc.test/repo1", Files: map[string]interface{}{ "a/a.go": `// Package a is a package in godoc.test/repo1. package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`, "b/b.go": `package b; const Name = "repo1b"`, }, }, { Name: "godoc.test/repo2", Files: map[string]interface{}{ "a/a.go": `package a; const Name = "repo2a"`, "b/b.go": `package b; const Name = "repo2b"`, }, }, }) defer e.Cleanup() // Start the server. 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.Dir = e.Config.Dir cmd.Env = e.Config.Env cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr cmd.Args[0] = "godoc" if err := cmd.Start(); err != nil { t.Fatalf("failed to start godoc: %s", err) } defer killAndWait(cmd) if withIndex { waitForSearchReady(t, cmd, addr) } else { waitForServerReady(t, cmd, 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 Documentation Server", "Standard library", "These packages are part of the Go Project but outside the main Go tree.", }, }, { 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", "Third party", "Package a is a package in godoc.test/repo1.", }, 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", }, // Third party packages. { path: "/pkg/godoc.test/repo1/a", contains: []string{`const Name = "repo1a"`}, }, { path: "/pkg/godoc.test/repo2/b", contains: []string{`const Name = "repo2b"`}, }, } 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) } } } // Test for golang.org/issue/35476. func TestNoMainModule(t *testing.T) { if testing.Short() { t.Skip("skipping test in -short mode") } if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; for consistency with other tests that build godoc binary") } bin, cleanup := buildGodoc(t) defer cleanup() tempDir, err := ioutil.TempDir("", "godoc-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(tempDir) // Run godoc in an empty directory with module mode explicitly on, // so that 'go env GOMOD' reports os.DevNull. cmd := exec.Command(bin, "-url=/") cmd.Dir = tempDir cmd.Env = append(os.Environ(), "GO111MODULE=on") var stderr bytes.Buffer cmd.Stderr = &stderr err = cmd.Run() if err != nil { t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String()) } if strings.Contains(stderr.String(), "go mod download") { t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String()) } } // Basic integration test for godoc -analysis=type (via HTTP interface). func TestTypeAnalysis(t *testing.T) { bin, cleanup := buildGodoc(t) defer cleanup() testTypeAnalysis(t, packagestest.GOPATH, bin) // TODO(golang.org/issue/34473): Add support for type, pointer // analysis in module mode, then enable its test coverage here. } func testTypeAnalysis(t *testing.T, x packagestest.Exporter, bin string) { if runtime.GOOS == "plan9" { t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below } // Write a fake GOROOT/GOPATH. // TODO(golang.org/issue/34473): This test uses import paths without a dot in first // path element. This is not viable in module mode; import paths will need to change. e := packagestest.Export(t, x, []packagestest.Module{ { Name: "app", Files: map[string]interface{}{ "main.go": ` package main import "lib" func main() { print(lib.V) } `, }, }, { Name: "lib", Files: map[string]interface{}{ "lib.go": ` package lib type T struct{} const C = 3 var V T func (T) F() int { return C } `, }, }, }) goroot := filepath.Join(e.Temp(), "goroot") if err := os.Mkdir(goroot, 0755); err != nil { t.Fatalf("os.Mkdir(%q) failed: %v", goroot, err) } defer e.Cleanup() // Start the server. addr := serverAddress(t) cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type") cmd.Dir = e.Config.Dir // Point to an empty GOROOT directory to speed things up // by not doing type analysis for the entire real GOROOT. // TODO(golang.org/issue/34473): This test optimization may not be viable in module mode. cmd.Env = append(e.Config.Env, fmt.Sprintf("GOROOT=%s", goroot)) 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, cmd, 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)) } } }