blob: 2289c72259d1986a2d9b4a51ad244f65f6480785 [file] [log] [blame]
/*
* Copyright 2015 The Kythe Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package indexer
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"go/ast"
"go/token"
"io/ioutil"
"os"
"testing"
"kythe.io/kythe/go/test/testutil"
"kythe.io/kythe/go/util/metadata"
"kythe.io/kythe/go/util/ptypes"
"github.com/golang/protobuf/proto"
apb "kythe.io/kythe/proto/analysis_go_proto"
gopb "kythe.io/kythe/proto/go_go_proto"
spb "kythe.io/kythe/proto/storage_go_proto"
)
type memFetcher map[string]string // :: digest ā†’ content
func (m memFetcher) Fetch(path, digest string) ([]byte, error) {
if s, ok := m[digest]; ok {
return []byte(s), nil
}
return nil, os.ErrNotExist
}
func readTestFile(t *testing.T, path string) ([]byte, error) {
return ioutil.ReadFile(testutil.TestFilePath(t, path))
}
func hexDigest(data []byte) string {
h := sha256.New()
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}
// oneFileCompilation constructs a compilation unit with a single source file
// attributed to path and package pkg, whose content is given. The compilation
// is returned along with the digest of the file's content.
func oneFileCompilation(path, pkg, content string) (*apb.CompilationUnit, string) {
digest := hexDigest([]byte(content))
return &apb.CompilationUnit{
VName: &spb.VName{Language: "go", Corpus: "test", Path: pkg, Signature: "package"},
RequiredInput: []*apb.CompilationUnit_FileInput{{
VName: &spb.VName{Corpus: "test", Path: path},
Info: &apb.FileInfo{Path: path, Digest: digest},
}},
SourceFile: []string{path},
}, digest
}
func TestBuildTags(t *testing.T) {
// Make sure build tags are being respected. Synthesize a compilation with
// two trivial files, one tagged and the other not. After resolving, there
// should only be one file.
const keepFile = "// +build keepme\n\npackage foo"
const dropFile = "// +build ignore\n\npackage foo"
// Cobble together the data from two compilations into one.
u1, keepDigest := oneFileCompilation("keep.go", "foo", keepFile)
u2, dropDigest := oneFileCompilation("drop.go", "foo", dropFile)
u1.RequiredInput = append(u1.RequiredInput, u2.RequiredInput...)
u1.SourceFile = append(u1.SourceFile, u2.SourceFile...)
fetcher := memFetcher{
keepDigest: keepFile,
dropDigest: dropFile,
}
// Attach details with the build tags we care about.
info, err := ptypes.MarshalAny(&gopb.GoDetails{
BuildTags: []string{"keepme"},
})
if err != nil {
t.Fatalf("Marshaling Go details failed: %v", err)
}
u1.Details = append(u1.Details, info)
pi, err := Resolve(u1, fetcher, nil)
if err != nil {
t.Fatalf("Resolving compilation failed: %v", err)
}
// Make sure the files are what we think we want.
if n := len(pi.SourceText); n != 1 {
t.Errorf("Wrong number of source files: got %d, want 1", n)
}
for _, got := range pi.SourceText {
if got != keepFile {
t.Errorf("Wrong source:\n got: %#q\nwant: %#q", got, keepFile)
}
}
}
func TestResolve(t *testing.T) { // are you function enough not to back down?
// Test resolution on a simple two-package system:
//
// Package foo is compiled as test data from the source
// package foo
// func Foo() int { return 0 }
//
// Package bar is specified as source and imports foo.
// TODO(fromberger): Compile foo as part of the build.
foo, err := readTestFile(t, "testdata/foo.a")
if err != nil {
t.Fatalf("Unable to read foo.a: %v", err)
}
const bar = `package bar
import "test/foo"
func init() { println(foo.Foo()) }
`
unit, digest := oneFileCompilation("testdata/bar.go", "bar", bar)
fetcher := memFetcher{
hexDigest(foo): string(foo),
digest: bar,
}
unit.RequiredInput = append(unit.RequiredInput, &apb.CompilationUnit_FileInput{
VName: &spb.VName{Language: "go", Corpus: "test", Path: "foo", Signature: "package"},
Info: &apb.FileInfo{Path: "testdata/foo.a", Digest: hexDigest(foo)},
})
pi, err := Resolve(unit, fetcher, nil)
if err != nil {
t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
}
if got, want := pi.Name, "bar"; got != want {
t.Errorf("Package name: got %q, want %q", got, want)
}
if got, want := pi.VName, unit.VName; !proto.Equal(got, want) {
t.Errorf("Base vname: got %+v, want %+v", got, want)
}
if got, want := pi.ImportPath, "test/bar"; got != want {
t.Errorf("Import path: got %q, want %q", got, want)
}
if dep, ok := pi.Dependencies["test/foo"]; !ok {
t.Errorf("Missing dependency for test/foo in %+v", pi.Dependencies)
} else if pi.PackageVName[dep] == nil {
t.Errorf("Missing VName for test/foo in %+v", pi.PackageVName)
}
if got, want := len(pi.Files), len(unit.SourceFile); got != want {
t.Errorf("Source files: got %d, want %d", got, want)
}
for _, err := range pi.Errors {
t.Errorf("Unexpected resolution error: %v", err)
}
}
func TestResolveErrors(t *testing.T) {
unit, _ := oneFileCompilation("blah.a", "bogus", "package blah")
unit.SourceFile = nil
pkg, err := Resolve(unit, make(memFetcher), nil)
if err == nil {
t.Errorf("Resolving 0-source package: got %+v, wanted error", pkg)
} else {
t.Logf("Got expected error for 0-source package: %v", err)
}
}
func TestSpan(t *testing.T) {
const input = `package main
import "fmt"
func main() { fmt.Println("Hello, world") }`
unit, digest := oneFileCompilation("main.go", "main", input)
fetcher := memFetcher{digest: input}
pi, err := Resolve(unit, fetcher, nil)
if err != nil {
t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
}
tests := []struct {
key func(*ast.File) ast.Node // return a node to compute a span for
pos, end int // the expected span for the node
}{
{func(*ast.File) ast.Node { return nil }, -1, -1}, // invalid node
{func(*ast.File) ast.Node { return fakeNode{0, 2} }, -1, -1}, // invalid pos
{func(*ast.File) ast.Node { return fakeNode{5, 0} }, 4, 4}, // invalid end
{func(f *ast.File) ast.Node { return f.Name }, 8, 12}, // main
{func(f *ast.File) ast.Node { return f.Imports[0].Path }, 21, 26}, // "fmt"
{func(f *ast.File) ast.Node { return f.Decls[0] }, 14, 26}, // import "fmt"
{func(f *ast.File) ast.Node { return f.Decls[1] }, 27, len(input)}, // func main() { ... }
}
for _, test := range tests {
node := test.key(pi.Files[0])
_, pos, end := pi.Span(node)
if pos != test.pos || end != test.end {
t.Errorf("Span(%v): got pos=%d, end=%v; want pos=%d, end=%d", node, pos, end, test.pos, test.end)
}
}
}
type fakeNode struct{ pos, end token.Pos }
func (f fakeNode) Pos() token.Pos { return f.pos }
func (f fakeNode) End() token.Pos { return f.end }
func TestSink(t *testing.T) {
var facts, edges []*spb.Entry
sink := Sink(func(_ context.Context, e *spb.Entry) error {
if isEdge(e) {
edges = append(edges, e)
} else {
facts = append(facts, e)
}
return nil
})
hasFact := func(who *spb.VName, name, value string) bool {
for _, fact := range facts {
if proto.Equal(fact.Source, who) && fact.FactName == name && string(fact.FactValue) == value {
return true
}
}
return false
}
hasEdge := func(src, tgt *spb.VName, kind string) bool {
for _, edge := range edges {
if proto.Equal(edge.Source, src) && proto.Equal(edge.Target, tgt) && edge.EdgeKind == kind {
return true
}
}
return false
}
// Verify that the entries we push into the sink are preserved in encoding.
him := &spb.VName{Language: "peeps", Signature: "him"}
her := &spb.VName{Language: "peeps", Signature: "her"}
ctx := context.Background()
sink.writeFact(ctx, him, "/name", "John")
sink.writeEdge(ctx, him, her, "/friendof")
sink.writeFact(ctx, her, "/name", "Mary")
sink.writeEdge(ctx, him, him, "/loves")
sink.writeEdge(ctx, her, him, "/suspiciousof")
sink.writeFact(ctx, him, "/name/full", "Jonathan Q. Public")
sink.writeFact(ctx, her, "/name/full", "Mary M. Q. Contrary")
for _, want := range []struct {
who *spb.VName
name, value string
}{
{him, "/name", "John"},
{him, "/name/full", "Jonathan Q. Public"},
{her, "/name", "Mary"},
{her, "/name/full", "Mary M. Q. Contrary"},
} {
if !hasFact(want.who, want.name, want.value) {
t.Errorf("Missing fact %q=%q for %+v", want.name, want.value, want.who)
}
}
for _, want := range []struct {
src, tgt *spb.VName
kind string
}{
{him, her, "/friendof"},
{her, him, "/suspiciousof"},
{him, him, "/loves"},
} {
if !hasEdge(want.src, want.tgt, want.kind) {
t.Errorf("Missing edge %+v ā€•%sā†’ %+v", want.src, want.kind, want.tgt)
}
}
}
func TestComments(t *testing.T) {
// Verify that comment text is correctly escaped when translated into
// documentation nodes.
const input = `// Comment [escape] tests \t all the things.
package pkg
/*
Comment [escape] tests \t all the things.
*/
var z int
`
unit, digest := oneFileCompilation("testfile/comment.go", "pkg", input)
pi, err := Resolve(unit, memFetcher{digest: input}, &ResolveOptions{Info: XRefTypeInfo()})
if err != nil {
t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
}
var single, multi string
if err := pi.Emit(context.Background(), func(_ context.Context, e *spb.Entry) error {
if e.FactName != "/kythe/text" {
return nil
}
if e.Source.Signature == "package doc" {
if single != "" {
return fmt.Errorf("multiple package docs (%q, %q)", single, string(e.FactValue))
}
single = string(e.FactValue)
} else if e.Source.Signature == "var z doc" {
if multi != "" {
return fmt.Errorf("multiple variable docs (%q, %q)", multi, string(e.FactValue))
}
multi = string(e.FactValue)
}
return nil
}, nil); err != nil {
t.Fatalf("Emit unexpectedly failed: %v", err)
}
const want = `Comment \[escape\] tests \\t all the things.`
if single != want {
t.Errorf("Incorrect single-line comment escaping:\ngot %#q\nwant %#q", single, want)
}
if multi != want {
t.Errorf("Incorrect multi-line comment escaping:\ngot %#q\nwant %#q", multi, want)
}
}
func TestRules(t *testing.T) {
const input = "package main\n"
unit, digest := oneFileCompilation("main.go", "main", input)
unit.RequiredInput = append(unit.RequiredInput, &apb.CompilationUnit_FileInput{
VName: &spb.VName{Signature: "hey ho let's go"},
Info: &apb.FileInfo{Path: "meta"},
})
fetcher := memFetcher{digest: input}
// Resolve the compilation with a rule checker that recognizes the special
// input we added and emits a rule for the main file. This verifies we get
// the right mapping from paths back to source inputs.
pi, err := Resolve(unit, fetcher, &ResolveOptions{
CheckRules: func(ri *apb.CompilationUnit_FileInput, _ Fetcher) (*Ruleset, error) {
if ri.Info.Path == "meta" {
return &Ruleset{
Path: "main.go", // associate these rules to the main source
Rules: metadata.Rules{{
Begin: 1,
End: 2,
VName: ri.VName,
}},
}, nil
}
return nil, nil
},
})
if err != nil {
t.Fatalf("Resolve failed: %v", err)
}
// The rules should have an entry for the primary source file, and it
// should contain the rule we generated.
rs, ok := pi.Rules[pi.Files[0]]
if !ok {
t.Fatal("Missing primary source file")
}
want := metadata.Rules{{
Begin: 1,
End: 2,
VName: &spb.VName{Signature: "hey ho let's go"},
}}
if err := testutil.DeepEqual(want, rs); err != nil {
t.Errorf("Wrong rules: %v", err)
}
}
// isEdge reports whether e represents an edge.
func isEdge(e *spb.Entry) bool { return e.Target != nil && e.EdgeKind != "" }