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> = "repo1a"`}, 395 }, 396 { 397 path: "/pkg/godoc.test/repo2/b", 398 contains: []string{`const <span id="Name">Name</span> = "repo2b"`}, 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 }