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.
7 // This file defines a test framework for guru queries.
9 // The files beneath testdata/src contain Go programs containing
10 // query annotations of the form:
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.
18 // The expected output for each query is provided in the accompanying
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.)
25 // Run this test with:
26 // % go test golang.org/x/tools/cmd/guru -update
27 // to update the golden files.
50 guru "golang.org/x/tools/cmd/guru"
51 "golang.org/x/tools/internal/testenv"
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 {
61 if err := os.Setenv("GOPROXY", "off"); err != nil {
66 var updateFlag = flag.Bool("update", false, "Update the golden files.")
69 id string // unique id
70 verb string // query mode, e.g. "callees"
71 posn token.Position // query position
73 queryPos string // query position in command-line syntax
76 func parseRegexp(text string) (*regexp.Regexp, error) {
77 pattern, err := strconv.Unquote(text)
79 return nil, fmt.Errorf("can't unquote %s", text)
81 return regexp.Compile(pattern)
84 // parseQueries parses and returns the queries in the named file.
85 func parseQueries(t *testing.T, filename string) []*query {
86 filedata, err := ioutil.ReadFile(filename)
91 // Parse the file once to discover the test queries.
92 fset := token.NewFileSet()
93 f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments)
98 lines := bytes.Split(filedata, []byte("\n"))
101 queriesById := make(map[string]*query)
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] != '@' {
110 posn := fset.Position(c.Pos())
113 match := expectRe.FindStringSubmatch(text)
115 t.Errorf("%s: ill-formed query: %s", posn, text)
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)
133 if match[3] != `"nopos"` {
134 selectRe, err := parseRegexp(match[3])
136 t.Errorf("%s: %s", posn, err)
140 // Find text of the current line, sans query.
141 // (Queries must be // not /**/ comments.)
142 line := lines[posn.Line-1][:posn.Column-1]
144 // Apply regexp to current line to find input selection.
145 loc := selectRe.FindIndex(line)
147 t.Errorf("%s: selection pattern %s doesn't match line %q",
148 posn, match[3], string(line))
152 // Assumes ASCII. TODO(adonovan): test on UTF-8.
153 linestart := posn.Offset - (posn.Column - 1)
155 // Compute the file offsets.
156 q.queryPos = fmt.Sprintf("%s:#%d,#%d",
157 filename, linestart+loc[0], linestart+loc[1])
160 queries = append(queries, q)
164 // Return the slice, not map, for deterministic iteration.
168 // doQuery poses query q to the guru and writes its response and
169 // error (if any) to out.
170 func doQuery(out io.Writer, q *query, json bool) {
171 fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
173 var buildContext = build.Default
174 buildContext.GOPATH = "testdata"
175 pkg := filepath.Dir(strings.TrimPrefix(q.filename, "testdata/src/"))
177 gopathAbs, _ := filepath.Abs(buildContext.GOPATH)
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) {
183 defer outputMu.Unlock()
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)
190 // suppress position information
191 qr.PrintPlain(func(_ interface{}, format string, args ...interface{}) {
192 outputs = append(outputs, fmt.Sprintf(format, args...))
199 Build: &buildContext,
200 Scope: []string{pkg},
205 if err := guru.Run(q.verb, &query); err != nil {
206 fmt.Fprintf(out, "\nError: %s\n", err)
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:])
217 for _, output := range outputs {
218 fmt.Fprintf(out, "%s\n", output)
222 io.WriteString(out, "\n")
226 func TestGuru(t *testing.T) {
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")
232 switch runtime.GOOS {
234 t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS)
236 t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
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",
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",
267 name := strings.Split(filename, "/")[2]
268 t.Run(name, func(t *testing.T) {
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.
275 json := strings.Contains(filename, "-json/")
276 queries := parseQueries(t, filename)
277 golden := filename + "lden"
278 gotfh, err := ioutil.TempFile("", filepath.Base(filename)+"t")
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)
294 // Compare foo.got with foo.golden.
296 switch runtime.GOOS {
298 cmd = exec.Command("/bin/diff", "-c", golden, got)
300 cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
302 testenv.NeedsTool(t, cmd.Path)
303 buf := new(bytes.Buffer)
305 cmd.Stderr = os.Stderr
306 if err := cmd.Run(); err != nil {
307 t.Errorf("Guru tests for %s failed: %s.\n%s\n",
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)
321 func TestIssue14684(t *testing.T) {
322 var buildContext = build.Default
323 buildContext.GOPATH = "testdata"
325 Pos: "testdata/src/README.txt:#1",
326 Build: &buildContext,
328 err := guru.Run("freevars", &query)
330 t.Fatal("guru query succeeded unexpectedly")
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)