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