// 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 import ( "bytes" "context" "fmt" "go/build" "io" "net" "net/http" "os" "os/exec" "regexp" "runtime" "strings" "sync" "testing" "time" "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { if os.Getenv("GODOC_TEST_IS_GODOC") != "" { main() os.Exit(0) } // Inform subprocesses that they should run the cmd/godoc main instead of // running tests. It's a close approximation to building and running the real // command, and much less complicated and expensive to build and clean up. os.Setenv("GODOC_TEST_IS_GODOC", "1") os.Exit(m.Run()) } var exe struct { path string err error once sync.Once } func godocPath(t *testing.T) string { if !testenv.HasExec() { t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH) } exe.once.Do(func() { exe.path, exe.err = os.Executable() }) if exe.err != nil { t.Fatal(exe.err) } return exe.path } 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, ctx context.Context, cmd *exec.Cmd, addr string) { waitForServer(t, ctx, fmt.Sprintf("http://%v/", addr), "Go Documentation Server", false) } func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) { waitForServer(t, ctx, fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr), "The list of tokens.", false) } func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) { waitForServer(t, ctx, fmt.Sprintf("http://%v/pkg", addr), "Scan is not yet complete", // setting reverse as true, which means this waits // until the string is not returned in the response anymore true) } const pollInterval = 50 * time.Millisecond // waitForServer waits for server to meet the required condition, // failing the test if ctx is canceled before that occurs. func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) { start := time.Now() for { if ctx.Err() != nil { t.Helper() t.Fatalf("server failed to respond in %v", time.Since(start)) } time.Sleep(pollInterval) res, err := http.Get(url) if err != nil { continue } body, err := io.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)): return } } } // 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 TestURL(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping on plan9; fails to start up quickly enough") } bin := godocPath(t) 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 := testenv.Command(t, 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 := godocPath(t) 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) { t.Skip("slow test of to-be-deleted code (golang/go#59056)") if testing.Short() { t.Skip("skipping slow test in -short mode") } bin := godocPath(t) 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) { switch runtime.GOOS { case "plan9": t.Skip("skipping on plan9: fails to start up quickly enough") case "android", "ios": t.Skip("skipping on mobile: lacks GOROOT/api in test environment") } // 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 := testenv.Command(t, bin, args...) cmd.Dir = e.Config.Dir cmd.Env = e.Config.Env cmdOut := new(strings.Builder) cmd.Stdout = cmdOut cmd.Stderr = cmdOut cmd.Args[0] = "godoc" if err := cmd.Start(); err != nil { t.Fatalf("failed to start godoc: %s", err) } ctx, cancel := context.WithCancel(context.Background()) go func() { err := cmd.Wait() t.Logf("%v: %v", cmd, err) cancel() }() defer func() { // Shut down the server cleanly if possible. if runtime.GOOS == "windows" { cmd.Process.Kill() // Windows doesn't support os.Interrupt. } else { cmd.Process.Signal(os.Interrupt) } <-ctx.Done() t.Logf("server output:\n%s", cmdOut) }() if withIndex { waitForSearchReady(t, ctx, cmd, addr) } else { waitForServerReady(t, ctx, cmd, addr) waitUntilScanComplete(t, ctx, 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 := io.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 := godocPath(t) tempDir := t.TempDir() // Run godoc in an empty directory with module mode explicitly on, // so that 'go env GOMOD' reports os.DevNull. cmd := testenv.Command(t, 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()) } }