GoPLS Viewer

Home|gopls/cmd/guru/guru_test.go
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.Regexperror) {
77    patternerr := strconv.Unquote(text)
78    if err != nil {
79        return nilfmt.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.Tfilename string) []*query {
86    filedataerr := 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    ferr := parser.ParseFile(fsetfilenamefiledataparser.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"posntext)
116            continue
117        }
118
119        id := match[2]
120        if prevok := queriesById[id]; ok {
121            t.Errorf("%s: duplicate id %s"posnid)
122            t.Errorf("%s: previously used here"prev.posn)
123            continue
124        }
125
126        q := &query{
127            id:       id,
128            verb:     match[1],
129            filenamefilename,
130            posn:     posn,
131        }
132
133        if match[3] != `"nopos"` {
134            selectReerr := parseRegexp(match[3])
135            if err != nil {
136                t.Errorf("%s: %s"posnerr)
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                    posnmatch[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                filenamelinestart+loc[0], linestart+loc[1])
158        }
159
160        queries = append(queriesq)
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.Writerq *queryjson bool) {
171    fmt.Fprintf(out"-------- @%s %s --------\n"q.verbq.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.FileSetqr 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(jsonstrgopathAbs"$GOPATH", -1)
188            outputs = append(outputsjsonstr)
189        } else {
190            // suppress position information
191            qr.PrintPlain(func(_ interface{}, format stringargs ...interface{}) {
192                outputs = append(outputsfmt.Sprintf(formatargs...))
193            })
194        }
195    }
196
197    query := guru.Query{
198        Pos:        q.queryPos,
199        Build:      &buildContext,
200        Scope:      []string{pkg},
201        Reflectiontrue,
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        // Replace occurrences of interface{} with any, for consistent output
219        // across go 1.18 and earlier.
220        output = strings.ReplaceAll(output"interface{}""any")
221        fmt.Fprintf(out"%s\n"output)
222    }
223
224    if !json {
225        io.WriteString(out"\n")
226    }
227}
228
229func TestGuru(t *testing.T) {
230    if testing.Short() {
231        // These tests are super slow.
232        // TODO: make a lighter version of the tests for short mode?
233        t.Skipf("skipping in short mode")
234    }
235    switch runtime.GOOS {
236    case "android":
237        t.Skipf("skipping test on %q (no testdata dir)"runtime.GOOS)
238    case "windows":
239        t.Skipf("skipping test on %q (no /usr/bin/diff)"runtime.GOOS)
240    }
241
242    for _filename := range []string{
243        "testdata/src/alias/alias.go",
244        "testdata/src/calls/main.go",
245        "testdata/src/describe/main.go",
246        "testdata/src/freevars/main.go",
247        "testdata/src/implements/main.go",
248        "testdata/src/implements-methods/main.go",
249        "testdata/src/imports/main.go",
250        "testdata/src/peers/main.go",
251        "testdata/src/pointsto/main.go",
252        "testdata/src/referrers/main.go",
253        "testdata/src/reflection/main.go",
254        "testdata/src/what/main.go",
255        "testdata/src/whicherrs/main.go",
256        "testdata/src/softerrs/main.go",
257        // JSON:
258        // TODO(adonovan): most of these are very similar; combine them.
259        "testdata/src/calls-json/main.go",
260        "testdata/src/peers-json/main.go",
261        "testdata/src/definition-json/main.go",
262        "testdata/src/describe-json/main.go",
263        "testdata/src/implements-json/main.go",
264        "testdata/src/implements-methods-json/main.go",
265        "testdata/src/pointsto-json/main.go",
266        "testdata/src/referrers-json/main.go",
267        "testdata/src/what-json/main.go",
268    } {
269        filename := filename
270        name := strings.Split(filename"/")[2]
271        t.Run(name, func(t *testing.T) {
272            t.Parallel()
273            if filename == "testdata/src/referrers/main.go" && runtime.GOOS == "plan9" {
274                // Disable this test on plan9 since it expects a particular
275                // wording for a "no such file or directory" error.
276                t.Skip()
277            }
278            json := strings.Contains(filename"-json/")
279            queries := parseQueries(tfilename)
280            golden := filename + "lden"
281            gotfherr := ioutil.TempFile(""filepath.Base(filename)+"t")
282            if err != nil {
283                t.Fatal(err)
284            }
285            got := gotfh.Name()
286            defer func() {
287                gotfh.Close()
288                os.Remove(got)
289            }()
290
291            // Run the guru on each query, redirecting its output
292            // and error (if any) to the foo.got file.
293            for _q := range queries {
294                doQuery(gotfhqjson)
295            }
296
297            // Compare foo.got with foo.golden.
298            var cmd *exec.Cmd
299            switch runtime.GOOS {
300            case "plan9":
301                cmd = exec.Command("/bin/diff""-c"goldengot)
302            default:
303                cmd = exec.Command("/usr/bin/diff""-u"goldengot)
304            }
305            testenv.NeedsTool(tcmd.Path)
306            buf := new(bytes.Buffer)
307            cmd.Stdout = buf
308            cmd.Stderr = os.Stderr
309            if err := cmd.Run(); err != nil {
310                t.Errorf("Guru tests for %s failed: %s.\n%s\n",
311                    filenameerrbuf)
312
313                if *updateFlag {
314                    t.Logf("Updating %s..."golden)
315                    if err := exec.Command("/bin/cp"gotgolden).Run(); err != nil {
316                        t.Errorf("Update failed: %s"err)
317                    }
318                }
319            }
320        })
321    }
322}
323
324func TestIssue14684(t *testing.T) {
325    var buildContext = build.Default
326    buildContext.GOPATH = "testdata"
327    query := guru.Query{
328        Pos:   "testdata/src/README.txt:#1",
329        Build: &buildContext,
330    }
331    err := guru.Run("freevars", &query)
332    if err == nil {
333        t.Fatal("guru query succeeded unexpectedly")
334    }
335    if gotwant := err.Error(), "testdata/src/README.txt is not a Go source file"got != want {
336        t.Errorf("query error was %q, want %q"gotwant)
337    }
338}
339
MembersX
parseQueries.RangeStmt_2713.BlockStmt.BlockStmt.err
doQuery
doQuery.q
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.buf
parseQueries.RangeStmt_2713.c
parseQueries.RangeStmt_2713.BlockStmt.posn
parseQueries.RangeStmt_2713.BlockStmt.match
parseQueries.queriesById
doQuery.json
doQuery.gopathAbs
doQuery._
doQuery.err
init
parseRegexp.text
parseQueries.filename
TestGuru.RangeStmt_6340.BlockStmt.filename
TestIssue14684.t
parseQueries.queries
doQuery.out
doQuery.outputs
TestIssue14684.buildContext
TestIssue14684.err
init.err
query.id
parseRegexp.err
parseQueries.t
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.RangeStmt_8068.q
TestGuru
TestGuru.t
TestGuru.RangeStmt_6340.filename
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.json
ioutil
guru
parseQueries
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.err
TestIssue14684
TestIssue14684.got
TestIssue14684.want
sync
query.queryPos
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.queries
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.BlockStmt.BlockStmt.err
parseQueries.err
parseQueries.lines
parseQueries.expectRe
testenv
parseRegexp
parseQueries.RangeStmt_2713.BlockStmt.BlockStmt.selectRe
flag
parseQueries.filedata
doQuery.BlockStmt.BlockStmt.jsonstr
doQuery.pkg
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.cmd
query
query.posn
parseQueries.RangeStmt_2713.BlockStmt.text
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.gotfh
parseRegexp.pattern
parseQueries.f
doQuery.query
testing
TestIssue14684.query
regexp
parseQueries.RangeStmt_2713.BlockStmt.q
doQuery.outputMu
parseQueries.fset
doQuery.buildContext
TestGuru.RangeStmt_6340.BlockStmt.BlockStmt.got
exec
query.verb
query.filename
runtime
parseQueries.RangeStmt_2713.BlockStmt.BlockStmt.loc
doQuery.RangeStmt_5677.output
Members
X