2016-02-11 15:57:17 -07:00
|
|
|
// 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.
|
|
|
|
//
|
2018-04-23 10:10:15 -06:00
|
|
|
// The files beneath testdata/src contain Go programs containing
|
2016-02-11 15:57:17 -07:00
|
|
|
// 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"
|
2018-09-26 13:32:23 -06:00
|
|
|
"log"
|
2016-02-11 15:57:17 -07:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2016-02-25 15:06:15 -07:00
|
|
|
"path/filepath"
|
2016-02-11 15:57:17 -07:00
|
|
|
"regexp"
|
|
|
|
"runtime"
|
2016-04-01 13:04:45 -06:00
|
|
|
"sort"
|
2016-02-11 15:57:17 -07:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2016-04-01 13:04:45 -06:00
|
|
|
"sync"
|
2016-02-11 15:57:17 -07:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
guru "golang.org/x/tools/cmd/guru"
|
|
|
|
)
|
|
|
|
|
2018-09-26 13:32:23 -06:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-11 15:57:17 -07:00
|
|
|
var updateFlag = flag.Bool("update", false, "Update the golden files.")
|
|
|
|
|
|
|
|
type query struct {
|
|
|
|
id string // unique id
|
|
|
|
verb string // query mode, e.g. "callees"
|
2016-02-12 14:57:12 -07:00
|
|
|
posn token.Position // query position
|
2016-02-11 15:57:17 -07:00
|
|
|
filename string
|
2016-02-12 14:57:12 -07:00
|
|
|
queryPos string // query position in command-line syntax
|
2016-02-11 15:57:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2016-04-01 13:04:45 -06:00
|
|
|
func doQuery(out io.Writer, q *query, json bool) {
|
2016-02-11 15:57:17 -07:00
|
|
|
fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
|
|
|
|
|
|
|
|
var buildContext = build.Default
|
|
|
|
buildContext.GOPATH = "testdata"
|
2016-02-25 15:06:15 -07:00
|
|
|
pkg := filepath.Dir(strings.TrimPrefix(q.filename, "testdata/src/"))
|
2016-04-01 13:04:45 -06:00
|
|
|
|
2016-04-27 12:13:18 -06:00
|
|
|
gopathAbs, _ := filepath.Abs(buildContext.GOPATH)
|
|
|
|
|
2016-04-28 12:12:43 -06:00
|
|
|
var outputMu sync.Mutex // guards outputs
|
|
|
|
var outputs []string // JSON objects or lines of text
|
|
|
|
outputFn := func(fset *token.FileSet, qr guru.QueryResult) {
|
2016-04-01 13:04:45 -06:00
|
|
|
outputMu.Lock()
|
|
|
|
defer outputMu.Unlock()
|
|
|
|
if json {
|
2016-04-27 12:13:18 -06:00
|
|
|
jsonstr := string(qr.JSON(fset))
|
|
|
|
// Sanitize any absolute filenames that creep in.
|
|
|
|
jsonstr = strings.Replace(jsonstr, gopathAbs, "$GOPATH", -1)
|
2016-04-28 12:12:43 -06:00
|
|
|
outputs = append(outputs, jsonstr)
|
2016-04-01 13:04:45 -06:00
|
|
|
} else {
|
|
|
|
// suppress position information
|
|
|
|
qr.PrintPlain(func(_ interface{}, format string, args ...interface{}) {
|
2016-04-28 12:12:43 -06:00
|
|
|
outputs = append(outputs, fmt.Sprintf(format, args...))
|
2016-04-01 13:04:45 -06:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-11 15:57:17 -07:00
|
|
|
query := guru.Query{
|
|
|
|
Pos: q.queryPos,
|
|
|
|
Build: &buildContext,
|
2016-02-25 15:06:15 -07:00
|
|
|
Scope: []string{pkg},
|
2016-02-11 15:57:17 -07:00
|
|
|
Reflection: true,
|
2016-04-28 12:12:43 -06:00
|
|
|
Output: outputFn,
|
2016-02-11 15:57:17 -07:00
|
|
|
}
|
2016-04-01 13:04:45 -06:00
|
|
|
|
|
|
|
if err := guru.Run(q.verb, &query); err != nil {
|
2016-02-11 15:57:17 -07:00
|
|
|
fmt.Fprintf(out, "\nError: %s\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-04-28 12:12:43 -06:00
|
|
|
// 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 {
|
|
|
|
fmt.Fprintf(out, "%s\n", output)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !json {
|
2016-04-01 13:04:45 -06:00
|
|
|
io.WriteString(out, "\n")
|
2016-02-11 15:57:17 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGuru(t *testing.T) {
|
2018-04-26 12:12:02 -06:00
|
|
|
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")
|
|
|
|
}
|
2016-02-11 15:57:17 -07:00
|
|
|
switch runtime.GOOS {
|
|
|
|
case "android":
|
|
|
|
t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS)
|
|
|
|
case "windows":
|
|
|
|
t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, filename := range []string{
|
2017-02-09 12:54:08 -07:00
|
|
|
"testdata/src/alias/alias.go", // iff guru.HasAlias (go1.9)
|
2016-02-11 15:57:17 -07:00
|
|
|
"testdata/src/calls/main.go",
|
|
|
|
"testdata/src/describe/main.go",
|
2017-03-08 09:09:54 -07:00
|
|
|
"testdata/src/describe/main19.go", // iff go1.9
|
2016-02-11 15:57:17 -07:00
|
|
|
"testdata/src/freevars/main.go",
|
|
|
|
"testdata/src/implements/main.go",
|
|
|
|
"testdata/src/implements-methods/main.go",
|
2017-04-14 17:19:15 -06:00
|
|
|
"testdata/src/imports/main.go",
|
2016-02-11 15:57:17 -07:00
|
|
|
"testdata/src/peers/main.go",
|
|
|
|
"testdata/src/pointsto/main.go",
|
|
|
|
"testdata/src/referrers/main.go",
|
|
|
|
"testdata/src/reflection/main.go",
|
|
|
|
"testdata/src/what/main.go",
|
|
|
|
"testdata/src/whicherrs/main.go",
|
2016-11-17 14:44:22 -07:00
|
|
|
"testdata/src/softerrs/main.go",
|
2016-02-11 15:57:17 -07:00
|
|
|
// JSON:
|
|
|
|
// TODO(adonovan): most of these are very similar; combine them.
|
|
|
|
"testdata/src/calls-json/main.go",
|
|
|
|
"testdata/src/peers-json/main.go",
|
2016-04-27 12:13:18 -06:00
|
|
|
"testdata/src/definition-json/main.go",
|
2017-03-08 09:09:54 -07:00
|
|
|
"testdata/src/definition-json/main19.go",
|
2016-02-11 15:57:17 -07:00
|
|
|
"testdata/src/describe-json/main.go",
|
|
|
|
"testdata/src/implements-json/main.go",
|
|
|
|
"testdata/src/implements-methods-json/main.go",
|
|
|
|
"testdata/src/pointsto-json/main.go",
|
|
|
|
"testdata/src/referrers-json/main.go",
|
|
|
|
"testdata/src/what-json/main.go",
|
|
|
|
} {
|
2018-04-23 10:10:15 -06:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
if filename == "testdata/src/alias/alias.go" && !guru.HasAlias {
|
|
|
|
t.Skip()
|
|
|
|
}
|
|
|
|
if strings.HasSuffix(filename, "19.go") && !contains(build.Default.ReleaseTags, "go1.9") {
|
|
|
|
// TODO(adonovan): recombine the 'describe' and 'definition'
|
|
|
|
// tests once we drop support for go1.8.
|
|
|
|
t.Skip()
|
|
|
|
}
|
2016-07-18 07:54:51 -06:00
|
|
|
|
2018-04-23 10:10:15 -06:00
|
|
|
json := strings.Contains(filename, "-json/")
|
|
|
|
queries := parseQueries(t, filename)
|
|
|
|
golden := filename + "lden"
|
|
|
|
got := filename + "t"
|
|
|
|
gotfh, err := os.Create(got)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Create(%s) failed: %s", got, err)
|
|
|
|
}
|
|
|
|
defer os.Remove(got)
|
|
|
|
defer gotfh.Close()
|
2016-02-11 15:57:17 -07:00
|
|
|
|
2018-04-23 10:10:15 -06:00
|
|
|
// 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)
|
|
|
|
}
|
2016-02-11 15:57:17 -07:00
|
|
|
|
2018-04-23 10:10:15 -06:00
|
|
|
// Compare foo.got with foo.golden.
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
switch runtime.GOOS {
|
|
|
|
case "plan9":
|
|
|
|
cmd = exec.Command("/bin/diff", "-c", golden, got)
|
|
|
|
default:
|
|
|
|
cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
2016-02-11 15:57:17 -07:00
|
|
|
}
|
|
|
|
}
|
2018-04-23 10:10:15 -06:00
|
|
|
})
|
2016-02-11 15:57:17 -07:00
|
|
|
}
|
|
|
|
}
|
2016-03-07 07:47:24 -07:00
|
|
|
|
2017-03-08 09:09:54 -07:00
|
|
|
func contains(haystack []string, needle string) bool {
|
|
|
|
for _, x := range haystack {
|
|
|
|
if needle == x {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-03-07 07:47:24 -07:00
|
|
|
func TestIssue14684(t *testing.T) {
|
|
|
|
var buildContext = build.Default
|
|
|
|
buildContext.GOPATH = "testdata"
|
|
|
|
query := guru.Query{
|
|
|
|
Pos: "testdata/src/README.txt:#1",
|
|
|
|
Build: &buildContext,
|
|
|
|
}
|
2016-04-01 13:04:45 -06:00
|
|
|
err := guru.Run("freevars", &query)
|
2016-03-07 07:47:24 -07:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|