| // Copyright 2013 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main_test |
| |
| // This file defines a test framework for guru queries. |
| // |
| // The files beneath testdata/src contain Go programs containing |
| // query annotations of the form: |
| // |
| // @verb id "select" |
| // |
| // where verb is the query mode (e.g. "callers"), id is a unique name |
| // for this query, and "select" is a regular expression matching the |
| // substring of the current line that is the query's input selection. |
| // |
| // The expected output for each query is provided in the accompanying |
| // .golden file. |
| // |
| // (Location information is not included because it's too fragile to |
| // display as text. TODO(adonovan): think about how we can test its |
| // correctness, since it is critical information.) |
| // |
| // Run this test with: |
| // % go test golang.org/x/tools/cmd/guru -update |
| // to update the golden files. |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "go/build" |
| "go/parser" |
| "go/token" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| |
| guru "golang.org/x/tools/cmd/guru" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| func init() { |
| // This test currently requires GOPATH mode. |
| // Explicitly disabling module mode should suffix, but |
| // we'll also turn off GOPROXY just for good measure. |
| if err := os.Setenv("GO111MODULE", "off"); err != nil { |
| log.Fatal(err) |
| } |
| if err := os.Setenv("GOPROXY", "off"); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| var updateFlag = flag.Bool("update", false, "Update the golden files.") |
| |
| type query struct { |
| id string // unique id |
| verb string // query mode, e.g. "callees" |
| posn token.Position // query position |
| filename string |
| queryPos string // query position in command-line syntax |
| } |
| |
| func parseRegexp(text string) (*regexp.Regexp, error) { |
| pattern, err := strconv.Unquote(text) |
| if err != nil { |
| return nil, fmt.Errorf("can't unquote %s", text) |
| } |
| return regexp.Compile(pattern) |
| } |
| |
| // parseQueries parses and returns the queries in the named file. |
| func parseQueries(t *testing.T, filename string) []*query { |
| filedata, err := ioutil.ReadFile(filename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Parse the file once to discover the test queries. |
| fset := token.NewFileSet() |
| f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| lines := bytes.Split(filedata, []byte("\n")) |
| |
| var queries []*query |
| queriesById := make(map[string]*query) |
| |
| // Find all annotations of these forms: |
| expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp" |
| for _, c := range f.Comments { |
| text := strings.TrimSpace(c.Text()) |
| if text == "" || text[0] != '@' { |
| continue |
| } |
| posn := fset.Position(c.Pos()) |
| |
| // @verb id "regexp" |
| match := expectRe.FindStringSubmatch(text) |
| if match == nil { |
| t.Errorf("%s: ill-formed query: %s", posn, text) |
| continue |
| } |
| |
| id := match[2] |
| if prev, ok := queriesById[id]; ok { |
| t.Errorf("%s: duplicate id %s", posn, id) |
| t.Errorf("%s: previously used here", prev.posn) |
| continue |
| } |
| |
| q := &query{ |
| id: id, |
| verb: match[1], |
| filename: filename, |
| posn: posn, |
| } |
| |
| if match[3] != `"nopos"` { |
| selectRe, err := parseRegexp(match[3]) |
| if err != nil { |
| t.Errorf("%s: %s", posn, err) |
| continue |
| } |
| |
| // Find text of the current line, sans query. |
| // (Queries must be // not /**/ comments.) |
| line := lines[posn.Line-1][:posn.Column-1] |
| |
| // Apply regexp to current line to find input selection. |
| loc := selectRe.FindIndex(line) |
| if loc == nil { |
| t.Errorf("%s: selection pattern %s doesn't match line %q", |
| posn, match[3], string(line)) |
| continue |
| } |
| |
| // Assumes ASCII. TODO(adonovan): test on UTF-8. |
| linestart := posn.Offset - (posn.Column - 1) |
| |
| // Compute the file offsets. |
| q.queryPos = fmt.Sprintf("%s:#%d,#%d", |
| filename, linestart+loc[0], linestart+loc[1]) |
| } |
| |
| queries = append(queries, q) |
| queriesById[id] = q |
| } |
| |
| // Return the slice, not map, for deterministic iteration. |
| return queries |
| } |
| |
| // doQuery poses query q to the guru and writes its response and |
| // error (if any) to out. |
| func doQuery(out io.Writer, q *query, json bool) { |
| fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id) |
| |
| var buildContext = build.Default |
| buildContext.GOPATH = "testdata" |
| |
| gopathAbs, _ := filepath.Abs(buildContext.GOPATH) |
| |
| var outputMu sync.Mutex // guards outputs |
| var outputs []string // JSON objects or lines of text |
| outputFn := func(fset *token.FileSet, qr guru.QueryResult) { |
| outputMu.Lock() |
| defer outputMu.Unlock() |
| if json { |
| jsonstr := string(qr.JSON(fset)) |
| // Sanitize any absolute filenames that creep in. |
| jsonstr = strings.Replace(jsonstr, gopathAbs, "$GOPATH", -1) |
| outputs = append(outputs, jsonstr) |
| } else { |
| // suppress position information |
| qr.PrintPlain(func(_ interface{}, format string, args ...interface{}) { |
| outputs = append(outputs, fmt.Sprintf(format, args...)) |
| }) |
| } |
| } |
| |
| query := guru.Query{ |
| Pos: q.queryPos, |
| Build: &buildContext, |
| Output: outputFn, |
| } |
| |
| if err := guru.Run(q.verb, &query); err != nil { |
| fmt.Fprintf(out, "\nError: %s\n", err) |
| return |
| } |
| |
| // In a "referrers" query, references are sorted within each |
| // package but packages are visited in arbitrary order, |
| // so for determinism we sort them. Line 0 is a caption. |
| if q.verb == "referrers" { |
| sort.Strings(outputs[1:]) |
| } |
| |
| for _, output := range outputs { |
| // Replace occurrences of interface{} with any, for consistent output |
| // across go 1.18 and earlier. |
| output = strings.ReplaceAll(output, "interface{}", "any") |
| fmt.Fprintf(out, "%s\n", output) |
| } |
| |
| if !json { |
| io.WriteString(out, "\n") |
| } |
| } |
| |
| func TestGuru(t *testing.T) { |
| if testing.Short() { |
| // These tests are super slow. |
| // TODO: make a lighter version of the tests for short mode? |
| t.Skipf("skipping in short mode") |
| } |
| |
| diffCmd := "/usr/bin/diff" |
| if runtime.GOOS == "plan9" { |
| diffCmd = "/bin/diff" |
| } |
| if _, err := exec.LookPath(diffCmd); err != nil { |
| t.Skipf("skipping test: %v", err) |
| } |
| |
| for _, filename := range []string{ |
| "testdata/src/alias/alias.go", |
| "testdata/src/describe/main.go", |
| "testdata/src/freevars/main.go", |
| "testdata/src/implements/main.go", |
| "testdata/src/implements-methods/main.go", |
| "testdata/src/imports/main.go", |
| "testdata/src/referrers/main.go", |
| "testdata/src/what/main.go", |
| "testdata/src/definition-json/main.go", |
| "testdata/src/describe-json/main.go", |
| "testdata/src/implements-json/main.go", |
| "testdata/src/implements-methods-json/main.go", |
| "testdata/src/referrers-json/main.go", |
| "testdata/src/what-json/main.go", |
| } { |
| filename := filename |
| name := strings.Split(filename, "/")[2] |
| t.Run(name, func(t *testing.T) { |
| t.Parallel() |
| if filename == "testdata/src/referrers/main.go" && runtime.GOOS == "plan9" { |
| // Disable this test on plan9 since it expects a particular |
| // wording for a "no such file or directory" error. |
| t.Skip() |
| } |
| json := strings.Contains(filename, "-json/") |
| queries := parseQueries(t, filename) |
| golden := filename + "lden" |
| gotfh, err := ioutil.TempFile("", filepath.Base(filename)+"t") |
| if err != nil { |
| t.Fatal(err) |
| } |
| got := gotfh.Name() |
| defer func() { |
| gotfh.Close() |
| os.Remove(got) |
| }() |
| |
| // Run the guru on each query, redirecting its output |
| // and error (if any) to the foo.got file. |
| for _, q := range queries { |
| doQuery(gotfh, q, json) |
| } |
| |
| // Compare foo.got with foo.golden. |
| var cmd *exec.Cmd |
| switch runtime.GOOS { |
| case "plan9": |
| cmd = exec.Command(diffCmd, "-c", golden, got) |
| default: |
| cmd = exec.Command(diffCmd, "-u", golden, got) |
| } |
| testenv.NeedsTool(t, cmd.Path) |
| buf := new(bytes.Buffer) |
| cmd.Stdout = buf |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| t.Errorf("Guru tests for %s failed: %s.\n%s\n", |
| filename, err, buf) |
| |
| if *updateFlag { |
| t.Logf("Updating %s...", golden) |
| if err := exec.Command("/bin/cp", got, golden).Run(); err != nil { |
| t.Errorf("Update failed: %s", err) |
| } |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestIssue14684(t *testing.T) { |
| var buildContext = build.Default |
| buildContext.GOPATH = "testdata" |
| query := guru.Query{ |
| Pos: "testdata/src/README.txt:#1", |
| Build: &buildContext, |
| } |
| err := guru.Run("freevars", &query) |
| if err == nil { |
| t.Fatal("guru query succeeded unexpectedly") |
| } |
| if got, want := err.Error(), "testdata/src/README.txt is not a Go source file"; got != want { |
| t.Errorf("query error was %q, want %q", got, want) |
| } |
| } |