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