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 | |
5 | package 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 | |
29 | import ( |
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 | |
54 | func 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 | |
66 | var updateFlag = flag.Bool("update", false, "Update the golden files.") |
67 | |
68 | type 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 | |
76 | func 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. |
85 | func 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. |
170 | func 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 | // 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 | |
229 | func 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(t, filename) |
280 | golden := filename + "lden" |
281 | gotfh, err := 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(gotfh, q, json) |
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", golden, got) |
302 | default: |
303 | cmd = exec.Command("/usr/bin/diff", "-u", golden, got) |
304 | } |
305 | testenv.NeedsTool(t, cmd.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 | filename, err, buf) |
312 | |
313 | if *updateFlag { |
314 | t.Logf("Updating %s...", golden) |
315 | if err := exec.Command("/bin/cp", got, golden).Run(); err != nil { |
316 | t.Errorf("Update failed: %s", err) |
317 | } |
318 | } |
319 | } |
320 | }) |
321 | } |
322 | } |
323 | |
324 | func 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 got, want := err.Error(), "testdata/src/README.txt is not a Go source file"; got != want { |
336 | t.Errorf("query error was %q, want %q", got, want) |
337 | } |
338 | } |
339 |
Members