305 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
| // 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 oracle_test
 | |
| 
 | |
| // This file defines a test framework for oracle queries.
 | |
| //
 | |
| // The files beneath testdata/src/main 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 code.google.com/p/go.tools/oracle -update
 | |
| // to update the golden files.
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"go/build"
 | |
| 	"go/parser"
 | |
| 	"go/token"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"regexp"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.google.com/p/go.tools/importer"
 | |
| 	"code.google.com/p/go.tools/oracle"
 | |
| )
 | |
| 
 | |
| 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 // position of of query
 | |
| 	filename   string
 | |
| 	start, end int // selection of file to pass to oracle
 | |
| }
 | |
| 
 | |
| 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.
 | |
| 	var fset token.FileSet
 | |
| 	f, err := parser.ParseFile(&fset, filename, filedata,
 | |
| 		parser.DeclarationErrors|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
 | |
| 		}
 | |
| 
 | |
| 		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 := &query{
 | |
| 			id:       id,
 | |
| 			verb:     match[1],
 | |
| 			posn:     posn,
 | |
| 			filename: filename,
 | |
| 			start:    linestart + loc[0],
 | |
| 			end:      linestart + loc[1],
 | |
| 		}
 | |
| 		queries = append(queries, q)
 | |
| 		queriesById[id] = q
 | |
| 	}
 | |
| 
 | |
| 	// Return the slice, not map, for deterministic iteration.
 | |
| 	return queries
 | |
| }
 | |
| 
 | |
| // stripLocation removes a "file:line: " prefix.
 | |
| func stripLocation(line string) string {
 | |
| 	if i := strings.Index(line, ": "); i >= 0 {
 | |
| 		line = line[i+2:]
 | |
| 	}
 | |
| 	return line
 | |
| }
 | |
| 
 | |
| // doQuery poses query q to the oracle and writes its response and
 | |
| // error (if any) to out.
 | |
| func doQuery(out io.Writer, q *query, useJson bool) {
 | |
| 	fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
 | |
| 
 | |
| 	var buildContext = build.Default
 | |
| 	buildContext.GOPATH = "testdata"
 | |
| 	res, err := oracle.Query([]string{q.filename},
 | |
| 		q.verb,
 | |
| 		fmt.Sprintf("%s:#%d,#%d", q.filename, q.start, q.end),
 | |
| 		nil, // ptalog,
 | |
| 		&buildContext,
 | |
| 		true) // reflection
 | |
| 	if err != nil {
 | |
| 		fmt.Fprintf(out, "\nError: %s\n", stripLocation(err.Error()))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if useJson {
 | |
| 		// JSON output
 | |
| 		b, err := json.Marshal(res)
 | |
| 		if err != nil {
 | |
| 			fmt.Fprintf(out, "JSON error: %s\n", err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 		var buf bytes.Buffer
 | |
| 		if err := json.Indent(&buf, b, "", "\t"); err != nil {
 | |
| 			fmt.Fprintf(out, "json.Indent failed: %s", err)
 | |
| 			return
 | |
| 		}
 | |
| 		out.Write(buf.Bytes())
 | |
| 	} else {
 | |
| 		// "plain" (compiler diagnostic format) output
 | |
| 		capture := new(bytes.Buffer) // capture standard output
 | |
| 		res.WriteTo(capture)
 | |
| 		for _, line := range strings.Split(capture.String(), "\n") {
 | |
| 			fmt.Fprintf(out, "%s\n", stripLocation(line))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestOracle(t *testing.T) {
 | |
| 	switch runtime.GOOS {
 | |
| 	case "windows":
 | |
| 		t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
 | |
| 	}
 | |
| 
 | |
| 	for _, filename := range []string{
 | |
| 		"testdata/src/main/calls.go",
 | |
| 		"testdata/src/main/callgraph.go",
 | |
| 		"testdata/src/main/describe.go",
 | |
| 		"testdata/src/main/freevars.go",
 | |
| 		"testdata/src/main/implements.go",
 | |
| 		"testdata/src/main/imports.go",
 | |
| 		"testdata/src/main/peers.go",
 | |
| 		"testdata/src/main/reflection.go",
 | |
| 		// JSON:
 | |
| 		"testdata/src/main/callgraph-json.go",
 | |
| 		"testdata/src/main/calls-json.go",
 | |
| 		"testdata/src/main/peers-json.go",
 | |
| 		"testdata/src/main/describe-json.go",
 | |
| 		"testdata/src/main/referrers-json.go",
 | |
| 	} {
 | |
| 		useJson := strings.HasSuffix(filename, "-json.go")
 | |
| 		queries := parseQueries(t, filename)
 | |
| 		golden := filename + "lden"
 | |
| 		got := filename + "t"
 | |
| 		gotfh, err := os.Create(got)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("Create(%s) failed: %s", got, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer gotfh.Close()
 | |
| 
 | |
| 		// Run the oracle on each query, redirecting its output
 | |
| 		// and error (if any) to the foo.got file.
 | |
| 		for _, q := range queries {
 | |
| 			doQuery(gotfh, q, useJson)
 | |
| 		}
 | |
| 
 | |
| 		// Compare foo.got with foo.golden.
 | |
| 		cmd := exec.Command("/usr/bin/diff", "-u", golden, got) // assumes POSIX
 | |
| 		buf := new(bytes.Buffer)
 | |
| 		cmd.Stdout = buf
 | |
| 		if err := cmd.Run(); err != nil {
 | |
| 			t.Errorf("Oracle 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 TestMultipleQueries(t *testing.T) {
 | |
| 	// Importer
 | |
| 	var buildContext = build.Default
 | |
| 	buildContext.GOPATH = "testdata"
 | |
| 	imp := importer.New(&importer.Config{Build: &buildContext})
 | |
| 
 | |
| 	// Oracle
 | |
| 	filename := "testdata/src/main/multi.go"
 | |
| 	o, err := oracle.New(imp, []string{filename}, nil, true)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("oracle.New failed: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	// QueryPos
 | |
| 	pos := filename + ":#54,#58"
 | |
| 	qpos, err := oracle.ParseQueryPos(imp, pos, true)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("oracle.ParseQueryPos(%q) failed: %s", pos, err)
 | |
| 	}
 | |
| 	// SSA is built and we have the QueryPos.
 | |
| 	// Release the other ASTs and type info to the GC.
 | |
| 	imp = nil
 | |
| 
 | |
| 	// Run different query moes on same scope and selection.
 | |
| 	out := new(bytes.Buffer)
 | |
| 	for _, mode := range [...]string{"callers", "describe", "freevars"} {
 | |
| 		res, err := o.Query(mode, qpos)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("(*oracle.Oracle).Query(%q) failed: %s", pos, err)
 | |
| 		}
 | |
| 		capture := new(bytes.Buffer) // capture standard output
 | |
| 		res.WriteTo(capture)
 | |
| 		for _, line := range strings.Split(capture.String(), "\n") {
 | |
| 			fmt.Fprintf(out, "%s\n", stripLocation(line))
 | |
| 		}
 | |
| 	}
 | |
| 	want := `multi.f is called from these 1 sites:
 | |
| 	static function call from multi.main
 | |
| 
 | |
| function call (or conversion) of type ()
 | |
| 
 | |
| Free identifiers:
 | |
| var x int
 | |
| 
 | |
| `
 | |
| 	if got := out.String(); got != want {
 | |
| 		t.Errorf("Query output differs; want <<%s>>, got <<%s>>\n", want, got)
 | |
| 	}
 | |
| }
 |