github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/cmd/godoc/godoc_test.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"go/build"
    12  	"io/ioutil"
    13  	"net"
    14  	"net/http"
    15  	"os"
    16  	"os/exec"
    17  	"regexp"
    18  	"runtime"
    19  	"strings"
    20  	"sync"
    21  	"testing"
    22  	"time"
    23  
    24  	"golang.org/x/tools/go/packages/packagestest"
    25  	"golang.org/x/tools/internal/testenv"
    26  )
    27  
    28  func TestMain(m *testing.M) {
    29  	if os.Getenv("GODOC_TEST_IS_GODOC") != "" {
    30  		main()
    31  		os.Exit(0)
    32  	}
    33  
    34  	// Inform subprocesses that they should run the cmd/godoc main instead of
    35  	// running tests. It's a close approximation to building and running the real
    36  	// command, and much less complicated and expensive to build and clean up.
    37  	os.Setenv("GODOC_TEST_IS_GODOC", "1")
    38  
    39  	os.Exit(m.Run())
    40  }
    41  
    42  var exe struct {
    43  	path string
    44  	err  error
    45  	once sync.Once
    46  }
    47  
    48  func godocPath(t *testing.T) string {
    49  	switch runtime.GOOS {
    50  	case "js", "ios":
    51  		t.Skipf("skipping test that requires exec")
    52  	}
    53  
    54  	exe.once.Do(func() {
    55  		exe.path, exe.err = os.Executable()
    56  	})
    57  	if exe.err != nil {
    58  		t.Fatal(exe.err)
    59  	}
    60  	return exe.path
    61  }
    62  
    63  func serverAddress(t *testing.T) string {
    64  	ln, err := net.Listen("tcp", "127.0.0.1:0")
    65  	if err != nil {
    66  		ln, err = net.Listen("tcp6", "[::1]:0")
    67  	}
    68  	if err != nil {
    69  		t.Fatal(err)
    70  	}
    71  	defer ln.Close()
    72  	return ln.Addr().String()
    73  }
    74  
    75  func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
    76  	waitForServer(t, ctx,
    77  		fmt.Sprintf("http://%v/", addr),
    78  		"Go Documentation Server",
    79  		false)
    80  }
    81  
    82  func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
    83  	waitForServer(t, ctx,
    84  		fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
    85  		"The list of tokens.",
    86  		false)
    87  }
    88  
    89  func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) {
    90  	waitForServer(t, ctx,
    91  		fmt.Sprintf("http://%v/pkg", addr),
    92  		"Scan is not yet complete",
    93  		// setting reverse as true, which means this waits
    94  		// until the string is not returned in the response anymore
    95  		true)
    96  }
    97  
    98  const pollInterval = 50 * time.Millisecond
    99  
   100  // waitForServer waits for server to meet the required condition,
   101  // failing the test if ctx is canceled before that occurs.
   102  func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) {
   103  	start := time.Now()
   104  	for {
   105  		if ctx.Err() != nil {
   106  			t.Helper()
   107  			t.Fatalf("server failed to respond in %v", time.Since(start))
   108  		}
   109  
   110  		time.Sleep(pollInterval)
   111  		res, err := http.Get(url)
   112  		if err != nil {
   113  			continue
   114  		}
   115  		body, err := ioutil.ReadAll(res.Body)
   116  		res.Body.Close()
   117  		if err != nil || res.StatusCode != http.StatusOK {
   118  			continue
   119  		}
   120  		switch {
   121  		case !reverse && bytes.Contains(body, []byte(match)),
   122  			reverse && !bytes.Contains(body, []byte(match)):
   123  			return
   124  		}
   125  	}
   126  }
   127  
   128  // hasTag checks whether a given release tag is contained in the current version
   129  // of the go binary.
   130  func hasTag(t string) bool {
   131  	for _, v := range build.Default.ReleaseTags {
   132  		if t == v {
   133  			return true
   134  		}
   135  	}
   136  	return false
   137  }
   138  
   139  func TestURL(t *testing.T) {
   140  	if runtime.GOOS == "plan9" {
   141  		t.Skip("skipping on plan9; fails to start up quickly enough")
   142  	}
   143  	bin := godocPath(t)
   144  
   145  	testcase := func(url string, contents string) func(t *testing.T) {
   146  		return func(t *testing.T) {
   147  			stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
   148  
   149  			args := []string{fmt.Sprintf("-url=%s", url)}
   150  			cmd := testenv.Command(t, bin, args...)
   151  			cmd.Stdout = stdout
   152  			cmd.Stderr = stderr
   153  			cmd.Args[0] = "godoc"
   154  
   155  			// Set GOPATH variable to a non-existing absolute path
   156  			// and GOPROXY=off to disable module fetches.
   157  			// We cannot just unset GOPATH variable because godoc would default it to ~/go.
   158  			// (We don't want the indexer looking at the local workspace during tests.)
   159  			cmd.Env = append(os.Environ(),
   160  				"GOPATH=/does_not_exist",
   161  				"GOPROXY=off",
   162  				"GO111MODULE=off")
   163  
   164  			if err := cmd.Run(); err != nil {
   165  				t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
   166  			}
   167  
   168  			if !strings.Contains(stdout.String(), contents) {
   169  				t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout)
   170  			}
   171  		}
   172  	}
   173  
   174  	t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree."))
   175  	t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O"))
   176  }
   177  
   178  // Basic integration test for godoc HTTP interface.
   179  func TestWeb(t *testing.T) {
   180  	bin := godocPath(t)
   181  
   182  	for _, x := range packagestest.All {
   183  		t.Run(x.Name(), func(t *testing.T) {
   184  			testWeb(t, x, bin, false)
   185  		})
   186  	}
   187  }
   188  
   189  // Basic integration test for godoc HTTP interface.
   190  func TestWebIndex(t *testing.T) {
   191  	if testing.Short() {
   192  		t.Skip("skipping slow test in -short mode")
   193  	}
   194  	bin := godocPath(t)
   195  	testWeb(t, packagestest.GOPATH, bin, true)
   196  }
   197  
   198  // Basic integration test for godoc HTTP interface.
   199  func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) {
   200  	switch runtime.GOOS {
   201  	case "plan9":
   202  		t.Skip("skipping on plan9: fails to start up quickly enough")
   203  	case "android", "ios":
   204  		t.Skip("skipping on mobile: lacks GOROOT/api in test environment")
   205  	}
   206  
   207  	// Write a fake GOROOT/GOPATH with some third party packages.
   208  	e := packagestest.Export(t, x, []packagestest.Module{
   209  		{
   210  			Name: "godoc.test/repo1",
   211  			Files: map[string]interface{}{
   212  				"a/a.go": `// Package a is a package in godoc.test/repo1.
   213  package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`,
   214  				"b/b.go": `package b; const Name = "repo1b"`,
   215  			},
   216  		},
   217  		{
   218  			Name: "godoc.test/repo2",
   219  			Files: map[string]interface{}{
   220  				"a/a.go": `package a; const Name = "repo2a"`,
   221  				"b/b.go": `package b; const Name = "repo2b"`,
   222  			},
   223  		},
   224  	})
   225  	defer e.Cleanup()
   226  
   227  	// Start the server.
   228  	addr := serverAddress(t)
   229  	args := []string{fmt.Sprintf("-http=%s", addr)}
   230  	if withIndex {
   231  		args = append(args, "-index", "-index_interval=-1s")
   232  	}
   233  	cmd := testenv.Command(t, bin, args...)
   234  	cmd.Dir = e.Config.Dir
   235  	cmd.Env = e.Config.Env
   236  	cmdOut := new(strings.Builder)
   237  	cmd.Stdout = cmdOut
   238  	cmd.Stderr = cmdOut
   239  	cmd.Args[0] = "godoc"
   240  
   241  	if err := cmd.Start(); err != nil {
   242  		t.Fatalf("failed to start godoc: %s", err)
   243  	}
   244  	ctx, cancel := context.WithCancel(context.Background())
   245  	go func() {
   246  		err := cmd.Wait()
   247  		t.Logf("%v: %v", cmd, err)
   248  		cancel()
   249  	}()
   250  	defer func() {
   251  		// Shut down the server cleanly if possible.
   252  		if runtime.GOOS == "windows" {
   253  			cmd.Process.Kill() // Windows doesn't support os.Interrupt.
   254  		} else {
   255  			cmd.Process.Signal(os.Interrupt)
   256  		}
   257  		<-ctx.Done()
   258  		t.Logf("server output:\n%s", cmdOut)
   259  	}()
   260  
   261  	if withIndex {
   262  		waitForSearchReady(t, ctx, cmd, addr)
   263  	} else {
   264  		waitForServerReady(t, ctx, cmd, addr)
   265  		waitUntilScanComplete(t, ctx, addr)
   266  	}
   267  
   268  	tests := []struct {
   269  		path        string
   270  		contains    []string // substring
   271  		match       []string // regexp
   272  		notContains []string
   273  		needIndex   bool
   274  		releaseTag  string // optional release tag that must be in go/build.ReleaseTags
   275  	}{
   276  		{
   277  			path: "/",
   278  			contains: []string{
   279  				"Go Documentation Server",
   280  				"Standard library",
   281  				"These packages are part of the Go Project but outside the main Go tree.",
   282  			},
   283  		},
   284  		{
   285  			path:     "/pkg/fmt/",
   286  			contains: []string{"Package fmt implements formatted I/O"},
   287  		},
   288  		{
   289  			path:     "/src/fmt/",
   290  			contains: []string{"scan_test.go"},
   291  		},
   292  		{
   293  			path:     "/src/fmt/print.go",
   294  			contains: []string{"// Println formats using"},
   295  		},
   296  		{
   297  			path: "/pkg",
   298  			contains: []string{
   299  				"Standard library",
   300  				"Package fmt implements formatted I/O",
   301  				"Third party",
   302  				"Package a is a package in godoc.test/repo1.",
   303  			},
   304  			notContains: []string{
   305  				"internal/syscall",
   306  				"cmd/gc",
   307  			},
   308  		},
   309  		{
   310  			path: "/pkg/?m=all",
   311  			contains: []string{
   312  				"Standard library",
   313  				"Package fmt implements formatted I/O",
   314  				"internal/syscall/?m=all",
   315  			},
   316  			notContains: []string{
   317  				"cmd/gc",
   318  			},
   319  		},
   320  		{
   321  			path: "/search?q=ListenAndServe",
   322  			contains: []string{
   323  				"/src",
   324  			},
   325  			notContains: []string{
   326  				"/pkg/bootstrap",
   327  			},
   328  			needIndex: true,
   329  		},
   330  		{
   331  			path: "/pkg/strings/",
   332  			contains: []string{
   333  				`href="/src/strings/strings.go"`,
   334  			},
   335  		},
   336  		{
   337  			path: "/cmd/compile/internal/amd64/",
   338  			contains: []string{
   339  				`href="/src/cmd/compile/internal/amd64/ssa.go"`,
   340  			},
   341  		},
   342  		{
   343  			path: "/pkg/math/bits/",
   344  			contains: []string{
   345  				`Added in Go 1.9`,
   346  			},
   347  		},
   348  		{
   349  			path: "/pkg/net/",
   350  			contains: []string{
   351  				`// IPv6 scoped addressing zone; added in Go 1.1`,
   352  			},
   353  		},
   354  		{
   355  			path: "/pkg/net/http/httptrace/",
   356  			match: []string{
   357  				`Got1xxResponse.*// Go 1\.11`,
   358  			},
   359  			releaseTag: "go1.11",
   360  		},
   361  		// Verify we don't add version info to a struct field added the same time
   362  		// as the struct itself:
   363  		{
   364  			path: "/pkg/net/http/httptrace/",
   365  			match: []string{
   366  				`(?m)GotFirstResponseByte func\(\)\s*$`,
   367  			},
   368  		},
   369  		// Remove trailing periods before adding semicolons:
   370  		{
   371  			path: "/pkg/database/sql/",
   372  			contains: []string{
   373  				"The number of connections currently in use; added in Go 1.11",
   374  				"The number of idle connections; added in Go 1.11",
   375  			},
   376  			releaseTag: "go1.11",
   377  		},
   378  
   379  		// Third party packages.
   380  		{
   381  			path:     "/pkg/godoc.test/repo1/a",
   382  			contains: []string{`const <span id="Name">Name</span> = &#34;repo1a&#34;`},
   383  		},
   384  		{
   385  			path:     "/pkg/godoc.test/repo2/b",
   386  			contains: []string{`const <span id="Name">Name</span> = &#34;repo2b&#34;`},
   387  		},
   388  	}
   389  	for _, test := range tests {
   390  		if test.needIndex && !withIndex {
   391  			continue
   392  		}
   393  		url := fmt.Sprintf("http://%s%s", addr, test.path)
   394  		resp, err := http.Get(url)
   395  		if err != nil {
   396  			t.Errorf("GET %s failed: %s", url, err)
   397  			continue
   398  		}
   399  		body, err := ioutil.ReadAll(resp.Body)
   400  		strBody := string(body)
   401  		resp.Body.Close()
   402  		if err != nil {
   403  			t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
   404  		}
   405  		isErr := false
   406  		for _, substr := range test.contains {
   407  			if test.releaseTag != "" && !hasTag(test.releaseTag) {
   408  				continue
   409  			}
   410  			if !bytes.Contains(body, []byte(substr)) {
   411  				t.Errorf("GET %s: wanted substring %q in body", url, substr)
   412  				isErr = true
   413  			}
   414  		}
   415  		for _, re := range test.match {
   416  			if test.releaseTag != "" && !hasTag(test.releaseTag) {
   417  				continue
   418  			}
   419  			if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
   420  				if err != nil {
   421  					t.Fatalf("Bad regexp %q: %v", re, err)
   422  				}
   423  				t.Errorf("GET %s: wanted to match %s in body", url, re)
   424  				isErr = true
   425  			}
   426  		}
   427  		for _, substr := range test.notContains {
   428  			if bytes.Contains(body, []byte(substr)) {
   429  				t.Errorf("GET %s: didn't want substring %q in body", url, substr)
   430  				isErr = true
   431  			}
   432  		}
   433  		if isErr {
   434  			t.Errorf("GET %s: got:\n%s", url, body)
   435  		}
   436  	}
   437  }
   438  
   439  // Test for golang.org/issue/35476.
   440  func TestNoMainModule(t *testing.T) {
   441  	if testing.Short() {
   442  		t.Skip("skipping test in -short mode")
   443  	}
   444  	if runtime.GOOS == "plan9" {
   445  		t.Skip("skipping on plan9; for consistency with other tests that build godoc binary")
   446  	}
   447  	bin := godocPath(t)
   448  	tempDir := t.TempDir()
   449  
   450  	// Run godoc in an empty directory with module mode explicitly on,
   451  	// so that 'go env GOMOD' reports os.DevNull.
   452  	cmd := testenv.Command(t, bin, "-url=/")
   453  	cmd.Dir = tempDir
   454  	cmd.Env = append(os.Environ(), "GO111MODULE=on")
   455  	var stderr bytes.Buffer
   456  	cmd.Stderr = &stderr
   457  	err := cmd.Run()
   458  	if err != nil {
   459  		t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String())
   460  	}
   461  	if strings.Contains(stderr.String(), "go mod download") {
   462  		t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String())
   463  	}
   464  }