github.com/april1989/origin-go-tools@v0.0.32/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  	"bufio"
     9  	"bytes"
    10  	"fmt"
    11  	"go/build"
    12  	"io"
    13  	"io/ioutil"
    14  	"net"
    15  	"net/http"
    16  	"os"
    17  	"os/exec"
    18  	"path/filepath"
    19  	"regexp"
    20  	"runtime"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/april1989/origin-go-tools/go/packages/packagestest"
    26  	"github.com/april1989/origin-go-tools/internal/testenv"
    27  )
    28  
    29  // buildGodoc builds the godoc executable.
    30  // It returns its path, and a cleanup function.
    31  //
    32  // TODO(adonovan): opt: do this at most once, and do the cleanup
    33  // exactly once.  How though?  There's no atexit.
    34  func buildGodoc(t *testing.T) (bin string, cleanup func()) {
    35  	t.Helper()
    36  
    37  	if runtime.GOARCH == "arm" {
    38  		t.Skip("skipping test on arm platforms; too slow")
    39  	}
    40  	if runtime.GOOS == "android" {
    41  		t.Skipf("the dependencies are not available on android")
    42  	}
    43  	testenv.NeedsTool(t, "go")
    44  
    45  	tmp, err := ioutil.TempDir("", "godoc-regtest-")
    46  	if err != nil {
    47  		t.Fatal(err)
    48  	}
    49  	defer func() {
    50  		if cleanup == nil { // probably, go build failed.
    51  			os.RemoveAll(tmp)
    52  		}
    53  	}()
    54  
    55  	bin = filepath.Join(tmp, "godoc")
    56  	if runtime.GOOS == "windows" {
    57  		bin += ".exe"
    58  	}
    59  	cmd := exec.Command("go", "build", "-o", bin)
    60  	if err := cmd.Run(); err != nil {
    61  		t.Fatalf("Building godoc: %v", err)
    62  	}
    63  
    64  	return bin, func() { os.RemoveAll(tmp) }
    65  }
    66  
    67  func serverAddress(t *testing.T) string {
    68  	ln, err := net.Listen("tcp", "127.0.0.1:0")
    69  	if err != nil {
    70  		ln, err = net.Listen("tcp6", "[::1]:0")
    71  	}
    72  	if err != nil {
    73  		t.Fatal(err)
    74  	}
    75  	defer ln.Close()
    76  	return ln.Addr().String()
    77  }
    78  
    79  func waitForServerReady(t *testing.T, cmd *exec.Cmd, addr string) {
    80  	ch := make(chan error, 1)
    81  	go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }()
    82  	go waitForServer(t, ch,
    83  		fmt.Sprintf("http://%v/", addr),
    84  		"Go Documentation Server",
    85  		15*time.Second,
    86  		false)
    87  	if err := <-ch; err != nil {
    88  		t.Fatal(err)
    89  	}
    90  }
    91  
    92  func waitForSearchReady(t *testing.T, cmd *exec.Cmd, addr string) {
    93  	ch := make(chan error, 1)
    94  	go func() { ch <- fmt.Errorf("server exited early: %v", cmd.Wait()) }()
    95  	go waitForServer(t, ch,
    96  		fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
    97  		"The list of tokens.",
    98  		2*time.Minute,
    99  		false)
   100  	if err := <-ch; err != nil {
   101  		t.Fatal(err)
   102  	}
   103  }
   104  
   105  func waitUntilScanComplete(t *testing.T, addr string) {
   106  	ch := make(chan error)
   107  	go waitForServer(t, ch,
   108  		fmt.Sprintf("http://%v/pkg", addr),
   109  		"Scan is not yet complete",
   110  		2*time.Minute,
   111  		// setting reverse as true, which means this waits
   112  		// until the string is not returned in the response anymore
   113  		true,
   114  	)
   115  	if err := <-ch; err != nil {
   116  		t.Fatal(err)
   117  	}
   118  }
   119  
   120  const pollInterval = 200 * time.Millisecond
   121  
   122  // waitForServer waits for server to meet the required condition.
   123  // It sends a single error value to ch, unless the test has failed.
   124  // The error value is nil if the required condition was met within
   125  // timeout, or non-nil otherwise.
   126  func waitForServer(t *testing.T, ch chan<- error, url, match string, timeout time.Duration, reverse bool) {
   127  	deadline := time.Now().Add(timeout)
   128  	for time.Now().Before(deadline) {
   129  		time.Sleep(pollInterval)
   130  		if t.Failed() {
   131  			return
   132  		}
   133  		res, err := http.Get(url)
   134  		if err != nil {
   135  			continue
   136  		}
   137  		body, err := ioutil.ReadAll(res.Body)
   138  		res.Body.Close()
   139  		if err != nil || res.StatusCode != http.StatusOK {
   140  			continue
   141  		}
   142  		switch {
   143  		case !reverse && bytes.Contains(body, []byte(match)),
   144  			reverse && !bytes.Contains(body, []byte(match)):
   145  			ch <- nil
   146  			return
   147  		}
   148  	}
   149  	ch <- fmt.Errorf("server failed to respond in %v", timeout)
   150  }
   151  
   152  // hasTag checks whether a given release tag is contained in the current version
   153  // of the go binary.
   154  func hasTag(t string) bool {
   155  	for _, v := range build.Default.ReleaseTags {
   156  		if t == v {
   157  			return true
   158  		}
   159  	}
   160  	return false
   161  }
   162  
   163  func killAndWait(cmd *exec.Cmd) {
   164  	cmd.Process.Kill()
   165  	cmd.Process.Wait()
   166  }
   167  
   168  func TestURL(t *testing.T) {
   169  	if runtime.GOOS == "plan9" {
   170  		t.Skip("skipping on plan9; fails to start up quickly enough")
   171  	}
   172  	bin, cleanup := buildGodoc(t)
   173  	defer cleanup()
   174  
   175  	testcase := func(url string, contents string) func(t *testing.T) {
   176  		return func(t *testing.T) {
   177  			stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
   178  
   179  			args := []string{fmt.Sprintf("-url=%s", url)}
   180  			cmd := exec.Command(bin, args...)
   181  			cmd.Stdout = stdout
   182  			cmd.Stderr = stderr
   183  			cmd.Args[0] = "godoc"
   184  
   185  			// Set GOPATH variable to a non-existing absolute path
   186  			// and GOPROXY=off to disable module fetches.
   187  			// We cannot just unset GOPATH variable because godoc would default it to ~/go.
   188  			// (We don't want the indexer looking at the local workspace during tests.)
   189  			cmd.Env = append(os.Environ(),
   190  				"GOPATH=/does_not_exist",
   191  				"GOPROXY=off",
   192  				"GO111MODULE=off")
   193  
   194  			if err := cmd.Run(); err != nil {
   195  				t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
   196  			}
   197  
   198  			if !strings.Contains(stdout.String(), contents) {
   199  				t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout)
   200  			}
   201  		}
   202  	}
   203  
   204  	t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree."))
   205  	t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O"))
   206  }
   207  
   208  // Basic integration test for godoc HTTP interface.
   209  func TestWeb(t *testing.T) {
   210  	bin, cleanup := buildGodoc(t)
   211  	defer cleanup()
   212  	for _, x := range packagestest.All {
   213  		t.Run(x.Name(), func(t *testing.T) {
   214  			testWeb(t, x, bin, false)
   215  		})
   216  	}
   217  }
   218  
   219  // Basic integration test for godoc HTTP interface.
   220  func TestWebIndex(t *testing.T) {
   221  	if testing.Short() {
   222  		t.Skip("skipping test in -short mode")
   223  	}
   224  	bin, cleanup := buildGodoc(t)
   225  	defer cleanup()
   226  	testWeb(t, packagestest.GOPATH, bin, true)
   227  }
   228  
   229  // Basic integration test for godoc HTTP interface.
   230  func testWeb(t *testing.T, x packagestest.Exporter, bin string, withIndex bool) {
   231  	if runtime.GOOS == "plan9" {
   232  		t.Skip("skipping on plan9; fails to start up quickly enough")
   233  	}
   234  
   235  	// Write a fake GOROOT/GOPATH with some third party packages.
   236  	e := packagestest.Export(t, x, []packagestest.Module{
   237  		{
   238  			Name: "godoc.test/repo1",
   239  			Files: map[string]interface{}{
   240  				"a/a.go": `// Package a is a package in godoc.test/repo1.
   241  package a; import _ "godoc.test/repo2/a"; const Name = "repo1a"`,
   242  				"b/b.go": `package b; const Name = "repo1b"`,
   243  			},
   244  		},
   245  		{
   246  			Name: "godoc.test/repo2",
   247  			Files: map[string]interface{}{
   248  				"a/a.go": `package a; const Name = "repo2a"`,
   249  				"b/b.go": `package b; const Name = "repo2b"`,
   250  			},
   251  		},
   252  	})
   253  	defer e.Cleanup()
   254  
   255  	// Start the server.
   256  	addr := serverAddress(t)
   257  	args := []string{fmt.Sprintf("-http=%s", addr)}
   258  	if withIndex {
   259  		args = append(args, "-index", "-index_interval=-1s")
   260  	}
   261  	cmd := exec.Command(bin, args...)
   262  	cmd.Dir = e.Config.Dir
   263  	cmd.Env = e.Config.Env
   264  	cmd.Stdout = os.Stderr
   265  	cmd.Stderr = os.Stderr
   266  	cmd.Args[0] = "godoc"
   267  
   268  	if err := cmd.Start(); err != nil {
   269  		t.Fatalf("failed to start godoc: %s", err)
   270  	}
   271  	defer killAndWait(cmd)
   272  
   273  	if withIndex {
   274  		waitForSearchReady(t, cmd, addr)
   275  	} else {
   276  		waitForServerReady(t, cmd, addr)
   277  		waitUntilScanComplete(t, addr)
   278  	}
   279  
   280  	tests := []struct {
   281  		path        string
   282  		contains    []string // substring
   283  		match       []string // regexp
   284  		notContains []string
   285  		needIndex   bool
   286  		releaseTag  string // optional release tag that must be in go/build.ReleaseTags
   287  	}{
   288  		{
   289  			path: "/",
   290  			contains: []string{
   291  				"Go Documentation Server",
   292  				"Standard library",
   293  				"These packages are part of the Go Project but outside the main Go tree.",
   294  			},
   295  		},
   296  		{
   297  			path:     "/pkg/fmt/",
   298  			contains: []string{"Package fmt implements formatted I/O"},
   299  		},
   300  		{
   301  			path:     "/src/fmt/",
   302  			contains: []string{"scan_test.go"},
   303  		},
   304  		{
   305  			path:     "/src/fmt/print.go",
   306  			contains: []string{"// Println formats using"},
   307  		},
   308  		{
   309  			path: "/pkg",
   310  			contains: []string{
   311  				"Standard library",
   312  				"Package fmt implements formatted I/O",
   313  				"Third party",
   314  				"Package a is a package in godoc.test/repo1.",
   315  			},
   316  			notContains: []string{
   317  				"internal/syscall",
   318  				"cmd/gc",
   319  			},
   320  		},
   321  		{
   322  			path: "/pkg/?m=all",
   323  			contains: []string{
   324  				"Standard library",
   325  				"Package fmt implements formatted I/O",
   326  				"internal/syscall/?m=all",
   327  			},
   328  			notContains: []string{
   329  				"cmd/gc",
   330  			},
   331  		},
   332  		{
   333  			path: "/search?q=ListenAndServe",
   334  			contains: []string{
   335  				"/src",
   336  			},
   337  			notContains: []string{
   338  				"/pkg/bootstrap",
   339  			},
   340  			needIndex: true,
   341  		},
   342  		{
   343  			path: "/pkg/strings/",
   344  			contains: []string{
   345  				`href="/src/strings/strings.go"`,
   346  			},
   347  		},
   348  		{
   349  			path: "/cmd/compile/internal/amd64/",
   350  			contains: []string{
   351  				`href="/src/cmd/compile/internal/amd64/ssa.go"`,
   352  			},
   353  		},
   354  		{
   355  			path: "/pkg/math/bits/",
   356  			contains: []string{
   357  				`Added in Go 1.9`,
   358  			},
   359  		},
   360  		{
   361  			path: "/pkg/net/",
   362  			contains: []string{
   363  				`// IPv6 scoped addressing zone; added in Go 1.1`,
   364  			},
   365  		},
   366  		{
   367  			path: "/pkg/net/http/httptrace/",
   368  			match: []string{
   369  				`Got1xxResponse.*// Go 1\.11`,
   370  			},
   371  			releaseTag: "go1.11",
   372  		},
   373  		// Verify we don't add version info to a struct field added the same time
   374  		// as the struct itself:
   375  		{
   376  			path: "/pkg/net/http/httptrace/",
   377  			match: []string{
   378  				`(?m)GotFirstResponseByte func\(\)\s*$`,
   379  			},
   380  		},
   381  		// Remove trailing periods before adding semicolons:
   382  		{
   383  			path: "/pkg/database/sql/",
   384  			contains: []string{
   385  				"The number of connections currently in use; added in Go 1.11",
   386  				"The number of idle connections; added in Go 1.11",
   387  			},
   388  			releaseTag: "go1.11",
   389  		},
   390  
   391  		// Third party packages.
   392  		{
   393  			path:     "/pkg/godoc.test/repo1/a",
   394  			contains: []string{`const <span id="Name">Name</span> = &#34;repo1a&#34;`},
   395  		},
   396  		{
   397  			path:     "/pkg/godoc.test/repo2/b",
   398  			contains: []string{`const <span id="Name">Name</span> = &#34;repo2b&#34;`},
   399  		},
   400  	}
   401  	for _, test := range tests {
   402  		if test.needIndex && !withIndex {
   403  			continue
   404  		}
   405  		url := fmt.Sprintf("http://%s%s", addr, test.path)
   406  		resp, err := http.Get(url)
   407  		if err != nil {
   408  			t.Errorf("GET %s failed: %s", url, err)
   409  			continue
   410  		}
   411  		body, err := ioutil.ReadAll(resp.Body)
   412  		strBody := string(body)
   413  		resp.Body.Close()
   414  		if err != nil {
   415  			t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
   416  		}
   417  		isErr := false
   418  		for _, substr := range test.contains {
   419  			if test.releaseTag != "" && !hasTag(test.releaseTag) {
   420  				continue
   421  			}
   422  			if !bytes.Contains(body, []byte(substr)) {
   423  				t.Errorf("GET %s: wanted substring %q in body", url, substr)
   424  				isErr = true
   425  			}
   426  		}
   427  		for _, re := range test.match {
   428  			if test.releaseTag != "" && !hasTag(test.releaseTag) {
   429  				continue
   430  			}
   431  			if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
   432  				if err != nil {
   433  					t.Fatalf("Bad regexp %q: %v", re, err)
   434  				}
   435  				t.Errorf("GET %s: wanted to match %s in body", url, re)
   436  				isErr = true
   437  			}
   438  		}
   439  		for _, substr := range test.notContains {
   440  			if bytes.Contains(body, []byte(substr)) {
   441  				t.Errorf("GET %s: didn't want substring %q in body", url, substr)
   442  				isErr = true
   443  			}
   444  		}
   445  		if isErr {
   446  			t.Errorf("GET %s: got:\n%s", url, body)
   447  		}
   448  	}
   449  }
   450  
   451  // Test for golang.org/issue/35476.
   452  func TestNoMainModule(t *testing.T) {
   453  	if testing.Short() {
   454  		t.Skip("skipping test in -short mode")
   455  	}
   456  	if runtime.GOOS == "plan9" {
   457  		t.Skip("skipping on plan9; for consistency with other tests that build godoc binary")
   458  	}
   459  	bin, cleanup := buildGodoc(t)
   460  	defer cleanup()
   461  	tempDir, err := ioutil.TempDir("", "godoc-test-")
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	defer os.RemoveAll(tempDir)
   466  
   467  	// Run godoc in an empty directory with module mode explicitly on,
   468  	// so that 'go env GOMOD' reports os.DevNull.
   469  	cmd := exec.Command(bin, "-url=/")
   470  	cmd.Dir = tempDir
   471  	cmd.Env = append(os.Environ(), "GO111MODULE=on")
   472  	var stderr bytes.Buffer
   473  	cmd.Stderr = &stderr
   474  	err = cmd.Run()
   475  	if err != nil {
   476  		t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String())
   477  	}
   478  	if strings.Contains(stderr.String(), "go mod download") {
   479  		t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String())
   480  	}
   481  }
   482  
   483  // Basic integration test for godoc -analysis=type (via HTTP interface).
   484  func TestTypeAnalysis(t *testing.T) {
   485  	bin, cleanup := buildGodoc(t)
   486  	defer cleanup()
   487  	testTypeAnalysis(t, packagestest.GOPATH, bin)
   488  	// TODO(golang.org/issue/34473): Add support for type, pointer
   489  	// analysis in module mode, then enable its test coverage here.
   490  }
   491  func testTypeAnalysis(t *testing.T, x packagestest.Exporter, bin string) {
   492  	if runtime.GOOS == "plan9" {
   493  		t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below
   494  	}
   495  
   496  	// Write a fake GOROOT/GOPATH.
   497  	// TODO(golang.org/issue/34473): This test uses import paths without a dot in first
   498  	// path element. This is not viable in module mode; import paths will need to change.
   499  	e := packagestest.Export(t, x, []packagestest.Module{
   500  		{
   501  			Name: "app",
   502  			Files: map[string]interface{}{
   503  				"main.go": `
   504  package main
   505  import "lib"
   506  func main() { print(lib.V) }
   507  `,
   508  			},
   509  		},
   510  		{
   511  			Name: "lib",
   512  			Files: map[string]interface{}{
   513  				"lib.go": `
   514  package lib
   515  type T struct{}
   516  const C = 3
   517  var V T
   518  func (T) F() int { return C }
   519  `,
   520  			},
   521  		},
   522  	})
   523  	goroot := filepath.Join(e.Temp(), "goroot")
   524  	if err := os.Mkdir(goroot, 0755); err != nil {
   525  		t.Fatalf("os.Mkdir(%q) failed: %v", goroot, err)
   526  	}
   527  	defer e.Cleanup()
   528  
   529  	// Start the server.
   530  	addr := serverAddress(t)
   531  	cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type")
   532  	cmd.Dir = e.Config.Dir
   533  	// Point to an empty GOROOT directory to speed things up
   534  	// by not doing type analysis for the entire real GOROOT.
   535  	// TODO(golang.org/issue/34473): This test optimization may not be viable in module mode.
   536  	cmd.Env = append(e.Config.Env, fmt.Sprintf("GOROOT=%s", goroot))
   537  	cmd.Stdout = os.Stderr
   538  	stderr, err := cmd.StderrPipe()
   539  	if err != nil {
   540  		t.Fatal(err)
   541  	}
   542  	cmd.Args[0] = "godoc"
   543  	if err := cmd.Start(); err != nil {
   544  		t.Fatalf("failed to start godoc: %s", err)
   545  	}
   546  	defer killAndWait(cmd)
   547  	waitForServerReady(t, cmd, addr)
   548  
   549  	// Wait for type analysis to complete.
   550  	reader := bufio.NewReader(stderr)
   551  	for {
   552  		s, err := reader.ReadString('\n') // on Plan 9 this fails
   553  		if err != nil {
   554  			t.Fatal(err)
   555  		}
   556  		fmt.Fprint(os.Stderr, s)
   557  		if strings.Contains(s, "Type analysis complete.") {
   558  			break
   559  		}
   560  	}
   561  	go io.Copy(os.Stderr, reader)
   562  
   563  	t0 := time.Now()
   564  
   565  	// Make an HTTP request and check for a regular expression match.
   566  	// The patterns are very crude checks that basic type information
   567  	// has been annotated onto the source view.
   568  tryagain:
   569  	for _, test := range []struct{ url, pattern string }{
   570  		{"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"},
   571  		{"/src/lib/lib.go", "L3.*type .*type info for T.*struct"},
   572  		{"/src/lib/lib.go", "L5.*var V .*type T struct"},
   573  		{"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"},
   574  
   575  		{"/src/app/main.go", "L2.*package .*Package docs for app"},
   576  		{"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"},
   577  		{"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"},
   578  	} {
   579  		url := fmt.Sprintf("http://%s%s", addr, test.url)
   580  		resp, err := http.Get(url)
   581  		if err != nil {
   582  			t.Errorf("GET %s failed: %s", url, err)
   583  			continue
   584  		}
   585  		body, err := ioutil.ReadAll(resp.Body)
   586  		resp.Body.Close()
   587  		if err != nil {
   588  			t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
   589  			continue
   590  		}
   591  
   592  		if !bytes.Contains(body, []byte("Static analysis features")) {
   593  			// Type analysis results usually become available within
   594  			// ~4ms after godoc startup (for this input on my machine).
   595  			if elapsed := time.Since(t0); elapsed > 500*time.Millisecond {
   596  				t.Fatalf("type analysis results still unavailable after %s", elapsed)
   597  			}
   598  			time.Sleep(10 * time.Millisecond)
   599  			goto tryagain
   600  		}
   601  
   602  		match, err := regexp.Match(test.pattern, body)
   603  		if err != nil {
   604  			t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
   605  			continue
   606  		}
   607  		if !match {
   608  			// This is a really ugly failure message.
   609  			t.Errorf("GET %s: body doesn't match %q, got:\n%s",
   610  				url, test.pattern, string(body))
   611  		}
   612  	}
   613  }