github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/cmd/guru/guru_test.go (about)

     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 "github.com/jhump/golang-x-tools/cmd/guru"
    51  	"github.com/jhump/golang-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  }