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
7// This file defines a test framework for guru queries.
8//
9// The files beneath testdata/src contain Go programs containing
10// query annotations of the form:
11//
12//   @verb id "select"
13//
14// where verb is the query mode (e.g. "callers"), id is a unique name
15// for this query, and "select" is a regular expression matching the
16// substring of the current line that is the query's input selection.
17//
18// The expected output for each query is provided in the accompanying
19// .golden file.
20//
21// (Location information is not included because it's too fragile to
22// display as text.  TODO(adonovan): think about how we can test its
23// correctness, since it is critical information.)
24//
25// Run this test with:
26// 	% go test golang.org/x/tools/cmd/guru -update
27// to update the golden files.
28
29import (
30	"bytes"
31	"flag"
32	"fmt"
33	"go/build"
34	"go/parser"
35	"go/token"
36	"io"
37	"io/ioutil"
38	"log"
39	"os"
40	"os/exec"
41	"path/filepath"
42	"regexp"
43	"runtime"
44	"sort"
45	"strconv"
46	"strings"
47	"sync"
48	"testing"
49
50	guru "golang.org/x/tools/cmd/guru"
51	"golang.org/x/tools/internal/testenv"
52)
53
54func init() {
55	// This test currently requires GOPATH mode.
56	// Explicitly disabling module mode should suffix, but
57	// we'll also turn off GOPROXY just for good measure.
58	if err := os.Setenv("GO111MODULE", "off"); err != nil {
59		log.Fatal(err)
60	}
61	if err := os.Setenv("GOPROXY", "off"); err != nil {
62		log.Fatal(err)
63	}
64}
65
66var updateFlag = flag.Bool("update", false, "Update the golden files.")
67
68type query struct {
69	id       string         // unique id
70	verb     string         // query mode, e.g. "callees"
71	posn     token.Position // query position
72	filename string
73	queryPos string // query position in command-line syntax
74}
75
76func parseRegexp(text string) (*regexp.Regexp, error) {
77	pattern, err := strconv.Unquote(text)
78	if err != nil {
79		return nil, fmt.Errorf("can't unquote %s", text)
80	}
81	return regexp.Compile(pattern)
82}
83
84// parseQueries parses and returns the queries in the named file.
85func parseQueries(t *testing.T, filename string) []*query {
86	filedata, err := ioutil.ReadFile(filename)
87	if err != nil {
88		t.Fatal(err)
89	}
90
91	// Parse the file once to discover the test queries.
92	fset := token.NewFileSet()
93	f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments)
94	if err != nil {
95		t.Fatal(err)
96	}
97
98	lines := bytes.Split(filedata, []byte("\n"))
99
100	var queries []*query
101	queriesById := make(map[string]*query)
102
103	// Find all annotations of these forms:
104	expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp"
105	for _, c := range f.Comments {
106		text := strings.TrimSpace(c.Text())
107		if text == "" || text[0] != '@' {
108			continue
109		}
110		posn := fset.Position(c.Pos())
111
112		// @verb id "regexp"
113		match := expectRe.FindStringSubmatch(text)
114		if match == nil {
115			t.Errorf("%s: ill-formed query: %s", posn, text)
116			continue
117		}
118
119		id := match[2]
120		if prev, ok := queriesById[id]; ok {
121			t.Errorf("%s: duplicate id %s", posn, id)
122			t.Errorf("%s: previously used here", prev.posn)
123			continue
124		}
125
126		q := &query{
127			id:       id,
128			verb:     match[1],
129			filename: filename,
130			posn:     posn,
131		}
132
133		if match[3] != `"nopos"` {
134			selectRe, err := parseRegexp(match[3])
135			if err != nil {
136				t.Errorf("%s: %s", posn, err)
137				continue
138			}
139
140			// Find text of the current line, sans query.
141			// (Queries must be // not /**/ comments.)
142			line := lines[posn.Line-1][:posn.Column-1]
143
144			// Apply regexp to current line to find input selection.
145			loc := selectRe.FindIndex(line)
146			if loc == nil {
147				t.Errorf("%s: selection pattern %s doesn't match line %q",
148					posn, match[3], string(line))
149				continue
150			}
151
152			// Assumes ASCII. TODO(adonovan): test on UTF-8.
153			linestart := posn.Offset - (posn.Column - 1)
154
155			// Compute the file offsets.
156			q.queryPos = fmt.Sprintf("%s:#%d,#%d",
157				filename, linestart+loc[0], linestart+loc[1])
158		}
159
160		queries = append(queries, q)
161		queriesById[id] = q
162	}
163
164	// Return the slice, not map, for deterministic iteration.
165	return queries
166}
167
168// doQuery poses query q to the guru and writes its response and
169// error (if any) to out.
170func doQuery(out io.Writer, q *query, json bool) {
171	fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
172
173	var buildContext = build.Default
174	buildContext.GOPATH = "testdata"
175	pkg := filepath.Dir(strings.TrimPrefix(q.filename, "testdata/src/"))
176
177	gopathAbs, _ := filepath.Abs(buildContext.GOPATH)
178
179	var outputMu sync.Mutex // guards outputs
180	var outputs []string    // JSON objects or lines of text
181	outputFn := func(fset *token.FileSet, qr guru.QueryResult) {
182		outputMu.Lock()
183		defer outputMu.Unlock()
184		if json {
185			jsonstr := string(qr.JSON(fset))
186			// Sanitize any absolute filenames that creep in.
187			jsonstr = strings.Replace(jsonstr, gopathAbs, "$GOPATH", -1)
188			outputs = append(outputs, jsonstr)
189		} else {
190			// suppress position information
191			qr.PrintPlain(func(_ interface{}, format string, args ...interface{}) {
192				outputs = append(outputs, fmt.Sprintf(format, args...))
193			})
194		}
195	}
196
197	query := guru.Query{
198		Pos:        q.queryPos,
199		Build:      &buildContext,
200		Scope:      []string{pkg},
201		Reflection: true,
202		Output:     outputFn,
203	}
204
205	if err := guru.Run(q.verb, &query); err != nil {
206		fmt.Fprintf(out, "\nError: %s\n", err)
207		return
208	}
209
210	// In a "referrers" query, references are sorted within each
211	// package but packages are visited in arbitrary order,
212	// so for determinism we sort them.  Line 0 is a caption.
213	if q.verb == "referrers" {
214		sort.Strings(outputs[1:])
215	}
216
217	for _, output := range outputs {
218		fmt.Fprintf(out, "%s\n", output)
219	}
220
221	if !json {
222		io.WriteString(out, "\n")
223	}
224}
225
226func TestGuru(t *testing.T) {
227	if testing.Short() {
228		// These tests are super slow.
229		// TODO: make a lighter version of the tests for short mode?
230		t.Skipf("skipping in short mode")
231	}
232	switch runtime.GOOS {
233	case "android":
234		t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS)
235	case "windows":
236		t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
237	}
238
239	for _, filename := range []string{
240		"testdata/src/alias/alias.go",
241		"testdata/src/calls/main.go",
242		"testdata/src/describe/main.go",
243		"testdata/src/freevars/main.go",
244		"testdata/src/implements/main.go",
245		"testdata/src/implements-methods/main.go",
246		"testdata/src/imports/main.go",
247		"testdata/src/peers/main.go",
248		"testdata/src/pointsto/main.go",
249		"testdata/src/referrers/main.go",
250		"testdata/src/reflection/main.go",
251		"testdata/src/what/main.go",
252		"testdata/src/whicherrs/main.go",
253		"testdata/src/softerrs/main.go",
254		// JSON:
255		// TODO(adonovan): most of these are very similar; combine them.
256		"testdata/src/calls-json/main.go",
257		"testdata/src/peers-json/main.go",
258		"testdata/src/definition-json/main.go",
259		"testdata/src/describe-json/main.go",
260		"testdata/src/implements-json/main.go",
261		"testdata/src/implements-methods-json/main.go",
262		"testdata/src/pointsto-json/main.go",
263		"testdata/src/referrers-json/main.go",
264		"testdata/src/what-json/main.go",
265	} {
266		filename := filename
267		name := strings.Split(filename, "/")[2]
268		t.Run(name, func(t *testing.T) {
269			t.Parallel()
270			if filename == "testdata/src/referrers/main.go" && runtime.GOOS == "plan9" {
271				// Disable this test on plan9 since it expects a particular
272				// wording for a "no such file or directory" error.
273				t.Skip()
274			}
275			json := strings.Contains(filename, "-json/")
276			queries := parseQueries(t, filename)
277			golden := filename + "lden"
278			gotfh, err := ioutil.TempFile("", filepath.Base(filename)+"t")
279			if err != nil {
280				t.Fatal(err)
281			}
282			got := gotfh.Name()
283			defer func() {
284				gotfh.Close()
285				os.Remove(got)
286			}()
287
288			// Run the guru on each query, redirecting its output
289			// and error (if any) to the foo.got file.
290			for _, q := range queries {
291				doQuery(gotfh, q, json)
292			}
293
294			// Compare foo.got with foo.golden.
295			var cmd *exec.Cmd
296			switch runtime.GOOS {
297			case "plan9":
298				cmd = exec.Command("/bin/diff", "-c", golden, got)
299			default:
300				cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
301			}
302			testenv.NeedsTool(t, cmd.Path)
303			buf := new(bytes.Buffer)
304			cmd.Stdout = buf
305			cmd.Stderr = os.Stderr
306			if err := cmd.Run(); err != nil {
307				t.Errorf("Guru tests for %s failed: %s.\n%s\n",
308					filename, err, buf)
309
310				if *updateFlag {
311					t.Logf("Updating %s...", golden)
312					if err := exec.Command("/bin/cp", got, golden).Run(); err != nil {
313						t.Errorf("Update failed: %s", err)
314					}
315				}
316			}
317		})
318	}
319}
320
321func TestIssue14684(t *testing.T) {
322	var buildContext = build.Default
323	buildContext.GOPATH = "testdata"
324	query := guru.Query{
325		Pos:   "testdata/src/README.txt:#1",
326		Build: &buildContext,
327	}
328	err := guru.Run("freevars", &query)
329	if err == nil {
330		t.Fatal("guru query succeeded unexpectedly")
331	}
332	if got, want := err.Error(), "testdata/src/README.txt is not a Go source file"; got != want {
333		t.Errorf("query error was %q, want %q", got, want)
334	}
335}
336