2013-08-27 16:49:13 -06: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.
|
|
|
|
|
2013-08-27 15:58:26 -06:00
|
|
|
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"
|
2013-09-03 13:29:02 -06:00
|
|
|
"encoding/json"
|
2013-08-27 15:58:26 -06:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"go/build"
|
|
|
|
"go/parser"
|
|
|
|
"go/token"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"regexp"
|
2013-08-28 22:04:05 -06:00
|
|
|
"runtime"
|
2013-08-27 15:58:26 -06:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
go.tools/oracle: refactor Oracle API to allow repeated queries on same scope.
The existing standalone Query function builds an importer, ssa.Program, oracle,
and query position, executes the query and returns the result.
For clients (such as Frederik Zipp's web-based github.com/fzipp/pythia tool)
that wish to load the program once and make several queries, we now expose
these as separate operations too. Here's a client, in pseudocode:
o := oracle.New(...)
for ... {
qpos := o.ParseQueryPos(...)
res := o.Query(mode, qpos)
print result
}
NB: this is a slight deoptimisation in the one-shot case since we have to
build the entire SSA program with debug info, not just the query package,
since we now don't know the query package at that time.
The 'exact' param to ParseQueryPos needs more thought since its
ideal value is a function of the query mode. This will do for now.
Details:
- expose Oracle type, New() func and Query() method.
- expose QueryPos type and ParseQueryPos func.
- improved package doc comment.
- un-exposed the "needs" bits.
- added test.
R=crawshaw
CC=frederik.zipp, golang-dev
https://golang.org/cl/13810043
2013-09-23 13:02:18 -06:00
|
|
|
"code.google.com/p/go.tools/importer"
|
2013-08-27 15:58:26 -06:00
|
|
|
"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.
|
2013-09-03 13:29:02 -06:00
|
|
|
func doQuery(out io.Writer, q *query, useJson bool) {
|
2013-08-27 15:58:26 -06:00
|
|
|
fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id)
|
|
|
|
|
|
|
|
var buildContext = build.Default
|
|
|
|
buildContext.GOPATH = "testdata"
|
2013-09-03 13:29:02 -06:00
|
|
|
res, err := oracle.Query([]string{q.filename},
|
2013-08-27 15:58:26 -06:00
|
|
|
q.verb,
|
2013-09-08 20:10:11 -06:00
|
|
|
fmt.Sprintf("%s:#%d,#%d", q.filename, q.start, q.end),
|
2013-09-23 14:13:01 -06:00
|
|
|
nil, // ptalog,
|
|
|
|
&buildContext,
|
|
|
|
true) // reflection
|
2013-09-03 13:29:02 -06:00
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(out, "\nError: %s\n", stripLocation(err.Error()))
|
|
|
|
return
|
2013-08-27 15:58:26 -06:00
|
|
|
}
|
|
|
|
|
2013-09-03 13:29:02 -06:00
|
|
|
if useJson {
|
|
|
|
// JSON output
|
2013-09-24 13:08:14 -06:00
|
|
|
b, err := json.MarshalIndent(res.Serial(), "", "\t")
|
2013-09-03 13:29:02 -06:00
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(out, "JSON error: %s\n", err.Error())
|
|
|
|
return
|
|
|
|
}
|
2013-09-24 13:08:14 -06:00
|
|
|
out.Write(b)
|
2013-09-03 13:29:02 -06:00
|
|
|
} 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))
|
|
|
|
}
|
2013-08-27 15:58:26 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOracle(t *testing.T) {
|
2013-08-28 22:04:05 -06:00
|
|
|
switch runtime.GOOS {
|
|
|
|
case "windows":
|
|
|
|
t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
|
|
|
|
}
|
|
|
|
|
2013-08-27 15:58:26 -06:00
|
|
|
for _, filename := range []string{
|
|
|
|
"testdata/src/main/calls.go",
|
2013-09-03 13:29:02 -06:00
|
|
|
"testdata/src/main/callgraph.go",
|
2013-08-27 15:58:26 -06:00
|
|
|
"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",
|
go.tools/pointer: reflection, part 1: maps, and some core features.
Core:
reflect.TypeOf
reflect.ValueOf
reflect.Zero
reflect.Value.Interface
Maps:
(reflect.Value).MapIndex
(reflect.Value).MapKeys
(reflect.Value).SetMapIndex
(*reflect.rtype).Elem
(*reflect.rtype).Key
+ tests:
pointer/testdata/mapreflect.go.
oracle/testdata/src/main/reflection.go.
Interface objects (T, V...) have been renamed "tagged objects".
Abstraction: we model reflect.Value similar to
interface{}---as a pointer that points only to tagged
objects---but a reflect.Value may also point to an "indirect
tagged object", one in which the payload V is of type *T not T.
These are required because reflect.Values can hold lvalues,
e.g. when derived via Field() or Elem(), though we won't use
them till we get to structs and pointers.
Solving: each reflection intrinsic defines a new constraint
and resolution rule. Because of the nature of reflection,
generalizing across types, the resolution rules dynamically
create additional complex constraints during solving, where
previously only simple (copy) constraints were created.
This requires some solver changes:
The work done before the main solver loop (to attach new
constraints to the graph) is now done before each iteration,
in processNewConstraints.
Its loop over constraints is broken into two passes:
the first handles base (addr-of) constraints,
the second handles simple and complex constraints.
constraint.init() has been inlined. The only behaviour that
varies across constraints is ptr()
Sadly this will pessimize presolver optimisations, when we get
there; such is the price of reflection.
Objects: reflection intrinsics create objects (i.e. cause
memory allocations) with no SSA operation. We will represent
them as the cgnode of the instrinsic (e.g. reflect.New), so we
extend Labels and node.data to represent objects as a product
(not sum) of ssa.Value and cgnode and pull this out into its
own type, struct object. This simplifies a number of
invariants and saves space. The ntObject flag is now
represented by obj!=nil; the other flags are moved into
object.
cgnodes are now always recorded in objects/Labels for which it
is appropriate (all but those for globals, constants and the
shared contours for functions).
Also:
- Prepopulate the flattenMemo cache to consider reflect.Value
a fake pointer, not a struct.
- Improve accessors and documentation on type Label.
- @conctypes assertions renamed @types (since dyn. types needn't be concrete).
- add oracle 'describe' test on an interface (missing, an oversight).
R=crawshaw
CC=golang-dev
https://golang.org/cl/13418048
2013-09-16 07:49:10 -06:00
|
|
|
"testdata/src/main/reflection.go",
|
2013-09-03 13:29:02 -06:00
|
|
|
// 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",
|
2013-09-10 12:11:42 -06:00
|
|
|
"testdata/src/main/referrers-json.go",
|
2013-08-27 15:58:26 -06:00
|
|
|
} {
|
2013-09-03 13:29:02 -06:00
|
|
|
useJson := strings.HasSuffix(filename, "-json.go")
|
2013-08-27 15:58:26 -06:00
|
|
|
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 {
|
2013-09-03 13:29:02 -06:00
|
|
|
doQuery(gotfh, q, useJson)
|
2013-08-27 15:58:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Compare foo.got with foo.golden.
|
2013-08-28 22:31:39 -06:00
|
|
|
cmd := exec.Command("/usr/bin/diff", "-u", golden, got) // assumes POSIX
|
2013-08-27 15:58:26 -06:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
go.tools/oracle: refactor Oracle API to allow repeated queries on same scope.
The existing standalone Query function builds an importer, ssa.Program, oracle,
and query position, executes the query and returns the result.
For clients (such as Frederik Zipp's web-based github.com/fzipp/pythia tool)
that wish to load the program once and make several queries, we now expose
these as separate operations too. Here's a client, in pseudocode:
o := oracle.New(...)
for ... {
qpos := o.ParseQueryPos(...)
res := o.Query(mode, qpos)
print result
}
NB: this is a slight deoptimisation in the one-shot case since we have to
build the entire SSA program with debug info, not just the query package,
since we now don't know the query package at that time.
The 'exact' param to ParseQueryPos needs more thought since its
ideal value is a function of the query mode. This will do for now.
Details:
- expose Oracle type, New() func and Query() method.
- expose QueryPos type and ParseQueryPos func.
- improved package doc comment.
- un-exposed the "needs" bits.
- added test.
R=crawshaw
CC=frederik.zipp, golang-dev
https://golang.org/cl/13810043
2013-09-23 13:02:18 -06:00
|
|
|
|
|
|
|
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"
|
2013-09-23 14:13:01 -06:00
|
|
|
o, err := oracle.New(imp, []string{filename}, nil, true)
|
go.tools/oracle: refactor Oracle API to allow repeated queries on same scope.
The existing standalone Query function builds an importer, ssa.Program, oracle,
and query position, executes the query and returns the result.
For clients (such as Frederik Zipp's web-based github.com/fzipp/pythia tool)
that wish to load the program once and make several queries, we now expose
these as separate operations too. Here's a client, in pseudocode:
o := oracle.New(...)
for ... {
qpos := o.ParseQueryPos(...)
res := o.Query(mode, qpos)
print result
}
NB: this is a slight deoptimisation in the one-shot case since we have to
build the entire SSA program with debug info, not just the query package,
since we now don't know the query package at that time.
The 'exact' param to ParseQueryPos needs more thought since its
ideal value is a function of the query mode. This will do for now.
Details:
- expose Oracle type, New() func and Query() method.
- expose QueryPos type and ParseQueryPos func.
- improved package doc comment.
- un-exposed the "needs" bits.
- added test.
R=crawshaw
CC=frederik.zipp, golang-dev
https://golang.org/cl/13810043
2013-09-23 13:02:18 -06:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|