golang.org/x/tools@v0.21.0/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"
    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  	if !testenv.HasExec() {
    50  		t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
    51  	}
    52  
    53  	exe.once.Do(func() {
    54  		exe.path, exe.err = os.Executable()
    55  	})
    56  	if exe.err != nil {
    57  		t.Fatal(exe.err)
    58  	}
    59  	return exe.path
    60  }
    61  
    62  func serverAddress(t *testing.T) string {
    63  	ln, err := net.Listen("tcp", "127.0.0.1:0")
    64  	if err != nil {
    65  		ln, err = net.Listen("tcp6", "[::1]:0")
    66  	}
    67  	if err != nil {
    68  		t.Fatal(err)
    69  	}
    70  	defer ln.Close()
    71  	return ln.Addr().String()
    72  }
    73  
    74  func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
    75  	waitForServer(t, ctx,
    76  		fmt.Sprintf("http://%v/", addr),
    77  		"Go Documentation Server",
    78  		false)
    79  }
    80  
    81  func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
    82  	waitForServer(t, ctx,
    83  		fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
    84  		"The list of tokens.",
    85  		false)
    86  }
    87  
    88  func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) {
    89  	waitForServer(t, ctx,
    90  		fmt.Sprintf("http://%v/pkg", addr),
    91  		"Scan is not yet complete",
    92  		// setting reverse as true, which means this waits
    93  		// until the string is not returned in the response anymore
    94  		true)
    95  }
    96  
    97  const pollInterval = 50 * time.Millisecond
    98  
    99  // waitForServer waits for server to meet the required condition,
   100  // failing the test if ctx is canceled before that occurs.
   101  func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) {
   102  	start := time.Now()
   103  	for {
   104  		if ctx.Err() != nil {
   105  			t.Helper()
   106  			t.Fatalf("server failed to respond in %v", time.Since(start))
   107  		}
   108  
   109  		time.Sleep(pollInterval)
   110  		res, err := http.Get(url)
   111  		if err != nil {
   112  			continue
   113  		}
   114  		body, err := io.ReadAll(res.Body)
   115  		res.Body.Close()
   116  		if err != nil || res.StatusCode != http.StatusOK {
   117  			continue
   118  		}
   119  		switch {
   120  		case !reverse && bytes.Contains(body, []byte(match)),
   121  			reverse && !bytes.Contains(body, []byte(match)):
   122  			return
   123  		}
   124  	}
   125  }
   126  
   127  // hasTag checks whether a given release tag is contained in the current version
   128  // of the go binary.
   129  func hasTag(t string) bool {
   130  	for _, v := range build.Default.ReleaseTags {
   131  		if t == v {
   132  			return true
   133  		}
   134  	}
   135  	return false
   136  }
   137  
   138  func TestURL(t *testing.T) {
   139  	if runtime.GOOS == "plan9" {
   140  		t.Skip("skipping on plan9; fails to start up quickly enough")
   141  	}
   142  	bin := godocPath(t)
   143  
   144  	testcase := func(url string, contents string) func(t *testing.T) {
   145  		return func(t *testing.T) {
   146  			stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
   147  
   148  			args := []string{fmt.Sprintf("-url=%s", url)}
   149  			cmd := testenv.Command(t, bin, args...)
   150  			cmd.Stdout = stdout
   151  			cmd.Stderr = stderr
   152  			cmd.Args[0] = "godoc"
   153  
   154  			// Set GOPATH variable to a non-existing absolute path
   155  			// and GOPROXY=off to disable module fetches.
   156  			// We cannot just unset GOPATH variable because godoc would default it to ~/go.
   157  			// (We don't want the indexer looking at the local workspace during tests.)
   158  			cmd.Env = append(os.Environ(),
   159  				"GOPATH=/does_not_exist",
   160  				"GOPROXY=off",
   161  				"GO111MODULE=off")
   162  
   163  			if err := cmd.Run(); err != nil {
   164  				t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
   165  			}
   166  
   167  			if !strings.Contains(stdout.String(), contents) {
   168  				t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout)
   169  			}
   170  		}
   171  	}
   172  
   173  	t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree."))
   174  	t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O"))
   175  }
   176  
   177  // Basic integration test for godoc HTTP interface.
   178  func TestWeb(t *testing.T) {
   179  	bin := godocPath(t)
   180  
   181  	for _, x := range packagestest.All {
   182  		t.Run(x.Name(), func(t *testing.T) {
   183  			testWeb(t, x, bin, false)
   184  		})
   185  	}
   186  }
   187  
   188  // Basic integration test for godoc HTTP interface.
   189  func TestWebIndex(t *testing.T) {
   190  	t.Skip("slow test of to-be-deleted code (golang/go#59056)")
   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 := io.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  }