1
2
3
4
5 package main
6
7 import (
8 "bytes"
9 "context"
10 "fmt"
11 "go/build"
12 "io"
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
35
36
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 if !testenv.HasExec() {
50 t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
51 }
52
53 exe.once.Do(func() {
54 exe.path, exe.err = os.Executable()
55 })
56 if exe.err != nil {
57 t.Fatal(exe.err)
58 }
59 return exe.path
60 }
61
62 func serverAddress(t *testing.T) string {
63 ln, err := net.Listen("tcp", "127.0.0.1:0")
64 if err != nil {
65 ln, err = net.Listen("tcp6", "[::1]:0")
66 }
67 if err != nil {
68 t.Fatal(err)
69 }
70 defer ln.Close()
71 return ln.Addr().String()
72 }
73
74 func waitForServerReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
75 waitForServer(t, ctx,
76 fmt.Sprintf("http://%v/", addr),
77 "Go Documentation Server",
78 false)
79 }
80
81 func waitForSearchReady(t *testing.T, ctx context.Context, cmd *exec.Cmd, addr string) {
82 waitForServer(t, ctx,
83 fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
84 "The list of tokens.",
85 false)
86 }
87
88 func waitUntilScanComplete(t *testing.T, ctx context.Context, addr string) {
89 waitForServer(t, ctx,
90 fmt.Sprintf("http://%v/pkg", addr),
91 "Scan is not yet complete",
92
93
94 true)
95 }
96
97 const pollInterval = 50 * time.Millisecond
98
99
100
101 func waitForServer(t *testing.T, ctx context.Context, url, match string, reverse bool) {
102 start := time.Now()
103 for {
104 if ctx.Err() != nil {
105 t.Helper()
106 t.Fatalf("server failed to respond in %v", time.Since(start))
107 }
108
109 time.Sleep(pollInterval)
110 res, err := http.Get(url)
111 if err != nil {
112 continue
113 }
114 body, err := io.ReadAll(res.Body)
115 res.Body.Close()
116 if err != nil || res.StatusCode != http.StatusOK {
117 continue
118 }
119 switch {
120 case !reverse && bytes.Contains(body, []byte(match)),
121 reverse && !bytes.Contains(body, []byte(match)):
122 return
123 }
124 }
125 }
126
127
128
129 func hasTag(t string) bool {
130 for _, v := range build.Default.ReleaseTags {
131 if t == v {
132 return true
133 }
134 }
135 return false
136 }
137
138 func TestURL(t *testing.T) {
139 if runtime.GOOS == "plan9" {
140 t.Skip("skipping on plan9; fails to start up quickly enough")
141 }
142 bin := godocPath(t)
143
144 testcase := func(url string, contents string) func(t *testing.T) {
145 return func(t *testing.T) {
146 stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
147
148 args := []string{fmt.Sprintf("-url=%s", url)}
149 cmd := testenv.Command(t, bin, args...)
150 cmd.Stdout = stdout
151 cmd.Stderr = stderr
152 cmd.Args[0] = "godoc"
153
154
155
156
157
158 cmd.Env = append(os.Environ(),
159 "GOPATH=/does_not_exist",
160 "GOPROXY=off",
161 "GO111MODULE=off")
162
163 if err := cmd.Run(); err != nil {
164 t.Fatalf("failed to run godoc -url=%q: %s\nstderr:\n%s", url, err, stderr)
165 }
166
167 if !strings.Contains(stdout.String(), contents) {
168 t.Errorf("did not find substring %q in output of godoc -url=%q:\n%s", contents, url, stdout)
169 }
170 }
171 }
172
173 t.Run("index", testcase("/", "These packages are part of the Go Project but outside the main Go tree."))
174 t.Run("fmt", testcase("/pkg/fmt", "Package fmt implements formatted I/O"))
175 }
176
177
178 func TestWeb(t *testing.T) {
179 bin := godocPath(t)
180
181 for _, x := range packagestest.All {
182 t.Run(x.Name(), func(t *testing.T) {
183 testWeb(t, x, bin, false)
184 })
185 }
186 }
187
188
189 func TestWebIndex(t *testing.T) {
190 t.Skip("slow test of to-be-deleted code (golang/go#59056)")
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
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
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
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
252 if runtime.GOOS == "windows" {
253 cmd.Process.Kill()
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
271 match []string
272 notContains []string
273 needIndex bool
274 releaseTag string
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
362
363 {
364 path: "/pkg/net/http/httptrace/",
365 match: []string{
366 `(?m)GotFirstResponseByte func\(\)\s*$`,
367 },
368 },
369
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
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 := io.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
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
451
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 }
465
View as plain text