// 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. // TODO(adonovan): improve coverage: // - output of @callgraph is nondeterministic. // - as are lists of labels. import ( "bytes" "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/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) { fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id) capture := new(bytes.Buffer) // capture standard output var buildContext = build.Default buildContext.GOPATH = "testdata" err := oracle.Main([]string{q.filename}, q.verb, fmt.Sprintf("%s %d-%d", q.filename, q.start, q.end), /*PTA-log=*/ nil, capture, &buildContext) for _, line := range strings.Split(capture.String(), "\n") { fmt.Fprintf(out, "%s\n", stripLocation(line)) } if err != nil { fmt.Fprintf(out, "Error: %s\n", stripLocation(err.Error())) } } 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/describe.go", "testdata/src/main/freevars.go", "testdata/src/main/implements.go", "testdata/src/main/imports.go", "testdata/src/main/peers.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) } // Compare foo.got with foo.golden. cmd := exec.Command("/usr/bin/diff", "-u3", 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) } } } } }