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
5package main_test
6
7import (
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
26// buildGodoc builds the godoc executable.
27// It returns its path, and a cleanup function.
28//
29// TODO(adonovan): opt: do this at most once, and do the cleanup
30// exactly once.  How though?  There's no atexit.
31func buildGodoc(t *testing.T) (bin string, cleanup func()) {
32	if runtime.GOARCH == "arm" {
33		t.Skip("skipping test on arm platforms; too slow")
34	}
35	tmp, err := ioutil.TempDir("", "godoc-regtest-")
36	if err != nil {
37		t.Fatal(err)
38	}
39	defer func() {
40		if cleanup == nil { // probably, go build failed.
41			os.RemoveAll(tmp)
42		}
43	}()
44
45	bin = filepath.Join(tmp, "godoc")
46	if runtime.GOOS == "windows" {
47		bin += ".exe"
48	}
49	cmd := exec.Command("go", "build", "-o", bin)
50	if err := cmd.Run(); err != nil {
51		t.Fatalf("Building godoc: %v", err)
52	}
53
54	return bin, func() { os.RemoveAll(tmp) }
55}
56
57func serverAddress(t *testing.T) string {
58	ln, err := net.Listen("tcp", "127.0.0.1:0")
59	if err != nil {
60		ln, err = net.Listen("tcp6", "[::1]:0")
61	}
62	if err != nil {
63		t.Fatal(err)
64	}
65	defer ln.Close()
66	return ln.Addr().String()
67}
68
69func waitForServerReady(t *testing.T, addr string) {
70	waitForServer(t,
71		fmt.Sprintf("http://%v/", addr),
72		"The Go Programming Language",
73		15*time.Second,
74		false)
75}
76
77func waitForSearchReady(t *testing.T, addr string) {
78	waitForServer(t,
79		fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
80		"The list of tokens.",
81		2*time.Minute,
82		false)
83}
84
85func waitUntilScanComplete(t *testing.T, addr string) {
86	waitForServer(t,
87		fmt.Sprintf("http://%v/pkg", addr),
88		"Scan is not yet complete",
89		2*time.Minute,
90		true,
91	)
92	// setting reverse as true, which means this waits
93	// until the string is not returned in the response anymore
94}
95
96const pollInterval = 200 * time.Millisecond
97
98func waitForServer(t *testing.T, url, match string, timeout time.Duration, reverse bool) {
99	// "health check" duplicated from x/tools/cmd/tipgodoc/tip.go
100	deadline := time.Now().Add(timeout)
101	for time.Now().Before(deadline) {
102		time.Sleep(pollInterval)
103		res, err := http.Get(url)
104		if err != nil {
105			continue
106		}
107		rbody, err := ioutil.ReadAll(res.Body)
108		res.Body.Close()
109		if err == nil && res.StatusCode == http.StatusOK {
110			if bytes.Contains(rbody, []byte(match)) && !reverse {
111				return
112			}
113			if !bytes.Contains(rbody, []byte(match)) && reverse {
114				return
115			}
116		}
117	}
118	t.Fatalf("Server failed to respond in %v", timeout)
119}
120
121// hasTag checks whether a given release tag is contained in the current version
122// of the go binary.
123func hasTag(t string) bool {
124	for _, v := range build.Default.ReleaseTags {
125		if t == v {
126			return true
127		}
128	}
129	return false
130}
131
132func killAndWait(cmd *exec.Cmd) {
133	cmd.Process.Kill()
134	cmd.Wait()
135}
136
137// Basic integration test for godoc HTTP interface.
138func TestWeb(t *testing.T) {
139	testWeb(t, false)
140}
141
142// Basic integration test for godoc HTTP interface.
143func TestWebIndex(t *testing.T) {
144	if testing.Short() {
145		t.Skip("skipping test in -short mode")
146	}
147	testWeb(t, true)
148}
149
150// Basic integration test for godoc HTTP interface.
151func testWeb(t *testing.T, withIndex bool) {
152	if runtime.GOOS == "plan9" {
153		t.Skip("skipping on plan9; files to start up quickly enough")
154	}
155	bin, cleanup := buildGodoc(t)
156	defer cleanup()
157	addr := serverAddress(t)
158	args := []string{fmt.Sprintf("-http=%s", addr)}
159	if withIndex {
160		args = append(args, "-index", "-index_interval=-1s")
161	}
162	cmd := exec.Command(bin, args...)
163	cmd.Stdout = os.Stderr
164	cmd.Stderr = os.Stderr
165	cmd.Args[0] = "godoc"
166
167	// Set GOPATH variable to non-existing path
168	// and GOPROXY=off to disable module fetches.
169	// We cannot just unset GOPATH variable because godoc would default it to ~/go.
170	// (We don't want the indexer looking at the local workspace during tests.)
171	cmd.Env = append(os.Environ(),
172		"GOPATH=does_not_exist",
173		"GOPROXY=off",
174		"GO111MODULE=off")
175
176	if err := cmd.Start(); err != nil {
177		t.Fatalf("failed to start godoc: %s", err)
178	}
179	defer killAndWait(cmd)
180
181	if withIndex {
182		waitForSearchReady(t, addr)
183	} else {
184		waitForServerReady(t, addr)
185		waitUntilScanComplete(t, addr)
186	}
187
188	tests := []struct {
189		path        string
190		contains    []string // substring
191		match       []string // regexp
192		notContains []string
193		needIndex   bool
194		releaseTag  string // optional release tag that must be in go/build.ReleaseTags
195	}{
196		{
197			path:     "/",
198			contains: []string{"Go is an open source programming language"},
199		},
200		{
201			path:     "/pkg/fmt/",
202			contains: []string{"Package fmt implements formatted I/O"},
203		},
204		{
205			path:     "/src/fmt/",
206			contains: []string{"scan_test.go"},
207		},
208		{
209			path:     "/src/fmt/print.go",
210			contains: []string{"// Println formats using"},
211		},
212		{
213			path: "/pkg",
214			contains: []string{
215				"Standard library",
216				"Package fmt implements formatted I/O",
217			},
218			notContains: []string{
219				"internal/syscall",
220				"cmd/gc",
221			},
222		},
223		{
224			path: "/pkg/?m=all",
225			contains: []string{
226				"Standard library",
227				"Package fmt implements formatted I/O",
228				"internal/syscall/?m=all",
229			},
230			notContains: []string{
231				"cmd/gc",
232			},
233		},
234		{
235			path: "/search?q=ListenAndServe",
236			contains: []string{
237				"/src",
238			},
239			notContains: []string{
240				"/pkg/bootstrap",
241			},
242			needIndex: true,
243		},
244		{
245			path: "/pkg/strings/",
246			contains: []string{
247				`href="/src/strings/strings.go"`,
248			},
249		},
250		{
251			path: "/cmd/compile/internal/amd64/",
252			contains: []string{
253				`href="/src/cmd/compile/internal/amd64/ssa.go"`,
254			},
255		},
256		{
257			path: "/pkg/math/bits/",
258			contains: []string{
259				`Added in Go 1.9`,
260			},
261		},
262		{
263			path: "/pkg/net/",
264			contains: []string{
265				`// IPv6 scoped addressing zone; added in Go 1.1`,
266			},
267		},
268		{
269			path: "/pkg/net/http/httptrace/",
270			match: []string{
271				`Got1xxResponse.*// Go 1\.11`,
272			},
273			releaseTag: "go1.11",
274		},
275		// Verify we don't add version info to a struct field added the same time
276		// as the struct itself:
277		{
278			path: "/pkg/net/http/httptrace/",
279			match: []string{
280				`(?m)GotFirstResponseByte func\(\)\s*$`,
281			},
282		},
283		// Remove trailing periods before adding semicolons:
284		{
285			path: "/pkg/database/sql/",
286			contains: []string{
287				"The number of connections currently in use; added in Go 1.11",
288				"The number of idle connections; added in Go 1.11",
289			},
290			releaseTag: "go1.11",
291		},
292	}
293	for _, test := range tests {
294		if test.needIndex && !withIndex {
295			continue
296		}
297		url := fmt.Sprintf("http://%s%s", addr, test.path)
298		resp, err := http.Get(url)
299		if err != nil {
300			t.Errorf("GET %s failed: %s", url, err)
301			continue
302		}
303		body, err := ioutil.ReadAll(resp.Body)
304		strBody := string(body)
305		resp.Body.Close()
306		if err != nil {
307			t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
308		}
309		isErr := false
310		for _, substr := range test.contains {
311			if test.releaseTag != "" && !hasTag(test.releaseTag) {
312				continue
313			}
314			if !bytes.Contains(body, []byte(substr)) {
315				t.Errorf("GET %s: wanted substring %q in body", url, substr)
316				isErr = true
317			}
318		}
319		for _, re := range test.match {
320			if test.releaseTag != "" && !hasTag(test.releaseTag) {
321				continue
322			}
323			if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
324				if err != nil {
325					t.Fatalf("Bad regexp %q: %v", re, err)
326				}
327				t.Errorf("GET %s: wanted to match %s in body", url, re)
328				isErr = true
329			}
330		}
331		for _, substr := range test.notContains {
332			if bytes.Contains(body, []byte(substr)) {
333				t.Errorf("GET %s: didn't want substring %q in body", url, substr)
334				isErr = true
335			}
336		}
337		if isErr {
338			t.Errorf("GET %s: got:\n%s", url, body)
339		}
340	}
341}
342
343// Basic integration test for godoc -analysis=type (via HTTP interface).
344func TestTypeAnalysis(t *testing.T) {
345	if runtime.GOOS == "plan9" {
346		t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below
347	}
348
349	// Write a fake GOROOT/GOPATH.
350	tmpdir, err := ioutil.TempDir("", "godoc-analysis")
351	if err != nil {
352		t.Fatalf("ioutil.TempDir failed: %s", err)
353	}
354	defer os.RemoveAll(tmpdir)
355	for _, f := range []struct{ file, content string }{
356		{"goroot/src/lib/lib.go", `
357package lib
358type T struct{}
359const C = 3
360var V T
361func (T) F() int { return C }
362`},
363		{"gopath/src/app/main.go", `
364package main
365import "lib"
366func main() { print(lib.V) }
367`},
368	} {
369		file := filepath.Join(tmpdir, f.file)
370		if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
371			t.Fatalf("MkdirAll(%s) failed: %s", filepath.Dir(file), err)
372		}
373		if err := ioutil.WriteFile(file, []byte(f.content), 0644); err != nil {
374			t.Fatal(err)
375		}
376	}
377
378	// Start the server.
379	bin, cleanup := buildGodoc(t)
380	defer cleanup()
381	addr := serverAddress(t)
382	cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type")
383	cmd.Env = os.Environ()
384	cmd.Env = append(cmd.Env, fmt.Sprintf("GOROOT=%s", filepath.Join(tmpdir, "goroot")))
385	cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(tmpdir, "gopath")))
386	cmd.Env = append(cmd.Env, "GO111MODULE=off")
387	cmd.Env = append(cmd.Env, "GOPROXY=off")
388	cmd.Stdout = os.Stderr
389	stderr, err := cmd.StderrPipe()
390	if err != nil {
391		t.Fatal(err)
392	}
393	cmd.Args[0] = "godoc"
394	if err := cmd.Start(); err != nil {
395		t.Fatalf("failed to start godoc: %s", err)
396	}
397	defer killAndWait(cmd)
398	waitForServerReady(t, addr)
399
400	// Wait for type analysis to complete.
401	reader := bufio.NewReader(stderr)
402	for {
403		s, err := reader.ReadString('\n') // on Plan 9 this fails
404		if err != nil {
405			t.Fatal(err)
406		}
407		fmt.Fprint(os.Stderr, s)
408		if strings.Contains(s, "Type analysis complete.") {
409			break
410		}
411	}
412	go io.Copy(os.Stderr, reader)
413
414	t0 := time.Now()
415
416	// Make an HTTP request and check for a regular expression match.
417	// The patterns are very crude checks that basic type information
418	// has been annotated onto the source view.
419tryagain:
420	for _, test := range []struct{ url, pattern string }{
421		{"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"},
422		{"/src/lib/lib.go", "L3.*type .*type info for T.*struct"},
423		{"/src/lib/lib.go", "L5.*var V .*type T struct"},
424		{"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"},
425
426		{"/src/app/main.go", "L2.*package .*Package docs for app"},
427		{"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"},
428		{"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"},
429	} {
430		url := fmt.Sprintf("http://%s%s", addr, test.url)
431		resp, err := http.Get(url)
432		if err != nil {
433			t.Errorf("GET %s failed: %s", url, err)
434			continue
435		}
436		body, err := ioutil.ReadAll(resp.Body)
437		resp.Body.Close()
438		if err != nil {
439			t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
440			continue
441		}
442
443		if !bytes.Contains(body, []byte("Static analysis features")) {
444			// Type analysis results usually become available within
445			// ~4ms after godoc startup (for this input on my machine).
446			if elapsed := time.Since(t0); elapsed > 500*time.Millisecond {
447				t.Fatalf("type analysis results still unavailable after %s", elapsed)
448			}
449			time.Sleep(10 * time.Millisecond)
450			goto tryagain
451		}
452
453		match, err := regexp.Match(test.pattern, body)
454		if err != nil {
455			t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err)
456			continue
457		}
458		if !match {
459			// This is a really ugly failure message.
460			t.Errorf("GET %s: body doesn't match %q, got:\n%s",
461				url, test.pattern, string(body))
462		}
463	}
464}
465