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.
25 "golang.org/x/tools/go/packages/packagestest"
26 "golang.org/x/tools/internal/testenv"
29 // buildGodoc builds the godoc executable.
30 // It returns its path, and a cleanup function.
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()) {
37 if runtime.GOARCH == "arm" {
38 t.Skip("skipping test on arm platforms; too slow")
40 if runtime.GOOS == "android" {
41 t.Skipf("the dependencies are not available on android")
43 testenv.NeedsTool(t, "go")
45 tmp, err := ioutil.TempDir("", "godoc-regtest-")
50 if cleanup == nil { // probably, go build failed.
55 bin = filepath.Join(tmp, "godoc")
56 if runtime.GOOS == "windows" {
59 cmd := exec.Command("go", "build", "-o", bin)
60 if err := cmd.Run(); err != nil {
61 t.Fatalf("Building godoc: %v", err)
64 return bin, func() { os.RemoveAll(tmp) }
67 func serverAddress(t *testing.T) string {
68 ln, err := net.Listen("tcp", "127.0.0.1:0")
70 ln, err = net.Listen("tcp6", "[::1]:0")
76 return ln.Addr().String()
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",
87 if err := <-ch; err != nil {
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.",
100 if err := <-ch; err != nil {
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",
111 // setting reverse as true, which means this waits
112 // until the string is not returned in the response anymore
115 if err := <-ch; err != nil {
120 const pollInterval = 200 * time.Millisecond
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)
133 res, err := http.Get(url)
137 body, err := ioutil.ReadAll(res.Body)
139 if err != nil || res.StatusCode != http.StatusOK {
143 case !reverse && bytes.Contains(body, []byte(match)),
144 reverse && !bytes.Contains(body, []byte(match)):
149 ch <- fmt.Errorf("server failed to respond in %v", timeout)
152 // hasTag checks whether a given release tag is contained in the current version
154 func hasTag(t string) bool {
155 for _, v := range build.Default.ReleaseTags {
163 func killAndWait(cmd *exec.Cmd) {
168 func TestURL(t *testing.T) {
169 if runtime.GOOS == "plan9" {
170 t.Skip("skipping on plan9; fails to start up quickly enough")
172 bin, cleanup := buildGodoc(t)
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)
179 args := []string{fmt.Sprintf("-url=%s", url)}
180 cmd := exec.Command(bin, args...)
183 cmd.Args[0] = "godoc"
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",
194 if err := cmd.Run(); err != nil {
195 t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
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)
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"))
208 // Basic integration test for godoc HTTP interface.
209 func TestWeb(t *testing.T) {
210 bin, cleanup := buildGodoc(t)
212 for _, x := range packagestest.All {
213 t.Run(x.Name(), func(t *testing.T) {
214 testWeb(t, x, bin, false)
219 // Basic integration test for godoc HTTP interface.
220 func TestWebIndex(t *testing.T) {
222 t.Skip("skipping test in -short mode")
224 bin, cleanup := buildGodoc(t)
226 testWeb(t, packagestest.GOPATH, bin, true)
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")
235 // Write a fake GOROOT/GOPATH with some third party packages.
236 e := packagestest.Export(t, x, []packagestest.Module{
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"`,
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"`,
256 addr := serverAddress(t)
257 args := []string{fmt.Sprintf("-http=%s", addr)}
259 args = append(args, "-index", "-index_interval=-1s")
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"
268 if err := cmd.Start(); err != nil {
269 t.Fatalf("failed to start godoc: %s", err)
271 defer killAndWait(cmd)
274 waitForSearchReady(t, cmd, addr)
276 waitForServerReady(t, cmd, addr)
277 waitUntilScanComplete(t, addr)
282 contains []string // substring
283 match []string // regexp
286 releaseTag string // optional release tag that must be in go/build.ReleaseTags
291 "Go Documentation Server",
293 "These packages are part of the Go Project but outside the main Go tree.",
298 contains: []string{"Package fmt implements formatted I/O"},
302 contains: []string{"scan_test.go"},
305 path: "/src/fmt/print.go",
306 contains: []string{"// Println formats using"},
312 "Package fmt implements formatted I/O",
314 "Package a is a package in godoc.test/repo1.",
316 notContains: []string{
325 "Package fmt implements formatted I/O",
326 "internal/syscall/?m=all",
328 notContains: []string{
333 path: "/search?q=ListenAndServe",
337 notContains: []string{
343 path: "/pkg/strings/",
345 `href="/src/strings/strings.go"`,
349 path: "/cmd/compile/internal/amd64/",
351 `href="/src/cmd/compile/internal/amd64/ssa.go"`,
355 path: "/pkg/math/bits/",
363 `// IPv6 scoped addressing zone; added in Go 1.1`,
367 path: "/pkg/net/http/httptrace/",
369 `Got1xxResponse.*// Go 1\.11`,
371 releaseTag: "go1.11",
373 // Verify we don't add version info to a struct field added the same time
374 // as the struct itself:
376 path: "/pkg/net/http/httptrace/",
378 `(?m)GotFirstResponseByte func\(\)\s*$`,
381 // Remove trailing periods before adding semicolons:
383 path: "/pkg/database/sql/",
385 "The number of connections currently in use; added in Go 1.11",
386 "The number of idle connections; added in Go 1.11",
388 releaseTag: "go1.11",
391 // Third party packages.
393 path: "/pkg/godoc.test/repo1/a",
394 contains: []string{`const <span id="Name">Name</span> = "repo1a"`},
397 path: "/pkg/godoc.test/repo2/b",
398 contains: []string{`const <span id="Name">Name</span> = "repo2b"`},
401 for _, test := range tests {
402 if test.needIndex && !withIndex {
405 url := fmt.Sprintf("http://%s%s", addr, test.path)
406 resp, err := http.Get(url)
408 t.Errorf("GET %s failed: %s", url, err)
411 body, err := ioutil.ReadAll(resp.Body)
412 strBody := string(body)
415 t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
418 for _, substr := range test.contains {
419 if test.releaseTag != "" && !hasTag(test.releaseTag) {
422 if !bytes.Contains(body, []byte(substr)) {
423 t.Errorf("GET %s: wanted substring %q in body", url, substr)
427 for _, re := range test.match {
428 if test.releaseTag != "" && !hasTag(test.releaseTag) {
431 if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
433 t.Fatalf("Bad regexp %q: %v", re, err)
435 t.Errorf("GET %s: wanted to match %s in body", url, re)
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)
446 t.Errorf("GET %s: got:\n%s", url, body)
451 // Test for golang.org/issue/35476.
452 func TestNoMainModule(t *testing.T) {
454 t.Skip("skipping test in -short mode")
456 if runtime.GOOS == "plan9" {
457 t.Skip("skipping on plan9; for consistency with other tests that build godoc binary")
459 bin, cleanup := buildGodoc(t)
461 tempDir, err := ioutil.TempDir("", "godoc-test-")
465 defer os.RemoveAll(tempDir)
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=/")
471 cmd.Env = append(os.Environ(), "GO111MODULE=on")
472 var stderr bytes.Buffer
476 t.Fatalf("godoc command failed: %v\nstderr=%q", err, stderr.String())
478 if strings.Contains(stderr.String(), "go mod download") {
479 t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String())
483 // Basic integration test for godoc -analysis=type (via HTTP interface).
484 func TestTypeAnalysis(t *testing.T) {
485 bin, cleanup := buildGodoc(t)
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.
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
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{
502 Files: map[string]interface{}{
506 func main() { print(lib.V) }
512 Files: map[string]interface{}{
518 func (T) F() int { return C }
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)
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()
542 cmd.Args[0] = "godoc"
543 if err := cmd.Start(); err != nil {
544 t.Fatalf("failed to start godoc: %s", err)
546 defer killAndWait(cmd)
547 waitForServerReady(t, cmd, addr)
549 // Wait for type analysis to complete.
550 reader := bufio.NewReader(stderr)
552 s, err := reader.ReadString('\n') // on Plan 9 this fails
556 fmt.Fprint(os.Stderr, s)
557 if strings.Contains(s, "Type analysis complete.") {
561 go io.Copy(os.Stderr, reader)
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.
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"},
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"},
579 url := fmt.Sprintf("http://%s%s", addr, test.url)
580 resp, err := http.Get(url)
582 t.Errorf("GET %s failed: %s", url, err)
585 body, err := ioutil.ReadAll(resp.Body)
588 t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
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)
598 time.Sleep(10 * time.Millisecond)
602 match, err := regexp.Match(test.pattern, body)
604 t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
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))