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