| // 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 <span id="Name">Name</span> = "repo1a"`}, | 
 | 		}, | 
 | 		{ | 
 | 			path:     "/pkg/godoc.test/repo2/b", | 
 | 			contains: []string{`const <span id="Name">Name</span> = "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()) | 
 | 	} | 
 | } |