github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/oracle/oracle_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 oracle_test
     6  
     7  // This file defines a test framework for oracle queries.
     8  //
     9  // The files beneath testdata/src/main 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/oracle -update
    27  // to update the golden files.
    28  
    29  import (
    30  	"bytes"
    31  	"encoding/json"
    32  	"flag"
    33  	"fmt"
    34  	"go/build"
    35  	"go/parser"
    36  	"go/token"
    37  	"io"
    38  	"io/ioutil"
    39  	"os"
    40  	"os/exec"
    41  	"regexp"
    42  	"runtime"
    43  	"strconv"
    44  	"strings"
    45  	"testing"
    46  
    47  	"golang.org/x/tools/oracle"
    48  )
    49  
    50  var updateFlag = flag.Bool("update", false, "Update the golden files.")
    51  
    52  type query struct {
    53  	id       string         // unique id
    54  	verb     string         // query mode, e.g. "callees"
    55  	posn     token.Position // position of of query
    56  	filename string
    57  	queryPos string // value of -pos flag
    58  }
    59  
    60  func parseRegexp(text string) (*regexp.Regexp, error) {
    61  	pattern, err := strconv.Unquote(text)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("can't unquote %s", text)
    64  	}
    65  	return regexp.Compile(pattern)
    66  }
    67  
    68  // parseQueries parses and returns the queries in the named file.
    69  func parseQueries(t *testing.T, filename string) []*query {
    70  	filedata, err := ioutil.ReadFile(filename)
    71  	if err != nil {
    72  		t.Fatal(err)
    73  	}
    74  
    75  	// Parse the file once to discover the test queries.
    76  	fset := token.NewFileSet()
    77  	f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments)
    78  	if err != nil {
    79  		t.Fatal(err)
    80  	}
    81  
    82  	lines := bytes.Split(filedata, []byte("\n"))
    83  
    84  	var queries []*query
    85  	queriesById := make(map[string]*query)
    86  
    87  	// Find all annotations of these forms:
    88  	expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp"
    89  	for _, c := range f.Comments {
    90  		text := strings.TrimSpace(c.Text())
    91  		if text == "" || text[0] != '@' {
    92  			continue
    93  		}
    94  		posn := fset.Position(c.Pos())
    95  
    96  		// @verb id "regexp"
    97  		match := expectRe.FindStringSubmatch(text)
    98  		if match == nil {
    99  			t.Errorf("%s: ill-formed query: %s", posn, text)
   100  			continue
   101  		}
   102  
   103  		id := match[2]
   104  		if prev, ok := queriesById[id]; ok {
   105  			t.Errorf("%s: duplicate id %s", posn, id)
   106  			t.Errorf("%s: previously used here", prev.posn)
   107  			continue
   108  		}
   109  
   110  		q := &query{
   111  			id:       id,
   112  			verb:     match[1],
   113  			filename: filename,
   114  			posn:     posn,
   115  		}
   116  
   117  		if match[3] != `"nopos"` {
   118  			selectRe, err := parseRegexp(match[3])
   119  			if err != nil {
   120  				t.Errorf("%s: %s", posn, err)
   121  				continue
   122  			}
   123  
   124  			// Find text of the current line, sans query.
   125  			// (Queries must be // not /**/ comments.)
   126  			line := lines[posn.Line-1][:posn.Column-1]
   127  
   128  			// Apply regexp to current line to find input selection.
   129  			loc := selectRe.FindIndex(line)
   130  			if loc == nil {
   131  				t.Errorf("%s: selection pattern %s doesn't match line %q",
   132  					posn, match[3], string(line))
   133  				continue
   134  			}
   135  
   136  			// Assumes ASCII. TODO(adonovan): test on UTF-8.
   137  			linestart := posn.Offset - (posn.Column - 1)
   138  
   139  			// Compute the file offsets.
   140  			q.queryPos = fmt.Sprintf("%s:#%d,#%d",
   141  				filename, linestart+loc[0], linestart+loc[1])
   142  		}
   143  
   144  		queries = append(queries, q)
   145  		queriesById[id] = q
   146  	}
   147  
   148  	// Return the slice, not map, for deterministic iteration.
   149  	return queries
   150  }
   151  
   152  // WriteResult writes res (-format=plain) to w, stripping file locations.
   153  func WriteResult(w io.Writer, q *oracle.Query) {
   154  	capture := new(bytes.Buffer) // capture standard output
   155  	q.WriteTo(capture)
   156  	for _, line := range strings.Split(capture.String(), "\n") {
   157  		// Remove a "file:line: " prefix.
   158  		if i := strings.Index(line, ": "); i >= 0 {
   159  			line = line[i+2:]
   160  		}
   161  		fmt.Fprintf(w, "%s\n", line)
   162  	}
   163  }
   164  
   165  // doQuery poses query q to the oracle and writes its response and
   166  // error (if any) to out.
   167  func doQuery(out io.Writer, q *query, useJson bool) {
   168  	fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
   169  
   170  	var buildContext = build.Default
   171  	buildContext.GOPATH = "testdata"
   172  	query := oracle.Query{
   173  		Mode:       q.verb,
   174  		Pos:        q.queryPos,
   175  		Build:      &buildContext,
   176  		Scope:      []string{q.filename},
   177  		Reflection: true,
   178  	}
   179  	if err := oracle.Run(&query); err != nil {
   180  		fmt.Fprintf(out, "\nError: %s\n", err)
   181  		return
   182  	}
   183  
   184  	if useJson {
   185  		// JSON output
   186  		b, err := json.MarshalIndent(query.Serial(), "", "\t")
   187  		if err != nil {
   188  			fmt.Fprintf(out, "JSON error: %s\n", err.Error())
   189  			return
   190  		}
   191  		out.Write(b)
   192  		fmt.Fprintln(out)
   193  	} else {
   194  		// "plain" (compiler diagnostic format) output
   195  		WriteResult(out, &query)
   196  	}
   197  }
   198  
   199  func TestOracle(t *testing.T) {
   200  	switch runtime.GOOS {
   201  	case "android":
   202  		t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS)
   203  	case "windows":
   204  		t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
   205  	}
   206  
   207  	for _, filename := range []string{
   208  		"testdata/src/calls/main.go",
   209  		"testdata/src/describe/main.go",
   210  		"testdata/src/freevars/main.go",
   211  		"testdata/src/implements/main.go",
   212  		"testdata/src/implements-methods/main.go",
   213  		"testdata/src/imports/main.go",
   214  		"testdata/src/peers/main.go",
   215  		"testdata/src/pointsto/main.go",
   216  		"testdata/src/referrers/main.go",
   217  		"testdata/src/reflection/main.go",
   218  		"testdata/src/what/main.go",
   219  		"testdata/src/whicherrs/main.go",
   220  		// JSON:
   221  		// TODO(adonovan): most of these are very similar; combine them.
   222  		"testdata/src/calls-json/main.go",
   223  		"testdata/src/peers-json/main.go",
   224  		"testdata/src/describe-json/main.go",
   225  		"testdata/src/implements-json/main.go",
   226  		"testdata/src/implements-methods-json/main.go",
   227  		"testdata/src/pointsto-json/main.go",
   228  		"testdata/src/referrers-json/main.go",
   229  		"testdata/src/what-json/main.go",
   230  	} {
   231  		useJson := strings.Contains(filename, "-json/")
   232  		queries := parseQueries(t, filename)
   233  		golden := filename + "lden"
   234  		got := filename + "t"
   235  		gotfh, err := os.Create(got)
   236  		if err != nil {
   237  			t.Errorf("Create(%s) failed: %s", got, err)
   238  			continue
   239  		}
   240  		defer gotfh.Close()
   241  		defer os.Remove(got)
   242  
   243  		// Run the oracle on each query, redirecting its output
   244  		// and error (if any) to the foo.got file.
   245  		for _, q := range queries {
   246  			doQuery(gotfh, q, useJson)
   247  		}
   248  
   249  		// Compare foo.got with foo.golden.
   250  		var cmd *exec.Cmd
   251  		switch runtime.GOOS {
   252  		case "plan9":
   253  			cmd = exec.Command("/bin/diff", "-c", golden, got)
   254  		default:
   255  			cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
   256  		}
   257  		buf := new(bytes.Buffer)
   258  		cmd.Stdout = buf
   259  		cmd.Stderr = os.Stderr
   260  		if err := cmd.Run(); err != nil {
   261  			t.Errorf("Oracle tests for %s failed: %s.\n%s\n",
   262  				filename, err, buf)
   263  
   264  			if *updateFlag {
   265  				t.Logf("Updating %s...", golden)
   266  				if err := exec.Command("/bin/cp", got, golden).Run(); err != nil {
   267  					t.Errorf("Update failed: %s", err)
   268  				}
   269  			}
   270  		}
   271  	}
   272  }