kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/indexer/indexer_test.go (about)

     1  /*
     2   * Copyright 2015 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package indexer
    18  
    19  import (
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/base64"
    23  	"encoding/hex"
    24  	"fmt"
    25  	"go/ast"
    26  	"go/token"
    27  	"io/ioutil"
    28  	"os"
    29  	"testing"
    30  
    31  	"kythe.io/kythe/go/test/testutil"
    32  	"kythe.io/kythe/go/util/log"
    33  	"kythe.io/kythe/go/util/metadata"
    34  	"kythe.io/kythe/go/util/ptypes"
    35  	"kythe.io/kythe/go/util/schema/edges"
    36  
    37  	"github.com/golang/protobuf/proto"
    38  
    39  	apb "kythe.io/kythe/proto/analysis_go_proto"
    40  	gopb "kythe.io/kythe/proto/go_go_proto"
    41  	mpb "kythe.io/kythe/proto/metadata_go_proto"
    42  	spb "kythe.io/kythe/proto/storage_go_proto"
    43  )
    44  
    45  type memFetcher map[string]string // :: digest → content
    46  
    47  func (m memFetcher) Fetch(path, digest string) ([]byte, error) {
    48  	if s, ok := m[digest]; ok {
    49  		return []byte(s), nil
    50  	}
    51  	return nil, os.ErrNotExist
    52  }
    53  
    54  func readTestFile(t *testing.T, path string) ([]byte, error) {
    55  	return ioutil.ReadFile(testutil.TestFilePath(t, path))
    56  }
    57  
    58  func hexDigest(data []byte) string {
    59  	h := sha256.New()
    60  	h.Write(data)
    61  	return hex.EncodeToString(h.Sum(nil))
    62  }
    63  
    64  // oneFileCompilation constructs a compilation unit with a single source file
    65  // attributed to path and package pkg, whose content is given. The compilation
    66  // is returned along with the digest of the file's content.
    67  func oneFileCompilation(path, pkg, content string) (*apb.CompilationUnit, string) {
    68  	digest := hexDigest([]byte(content))
    69  	return &apb.CompilationUnit{
    70  		VName: &spb.VName{Language: "go", Corpus: "test", Path: pkg, Signature: "package"},
    71  		RequiredInput: []*apb.CompilationUnit_FileInput{{
    72  			VName: &spb.VName{Corpus: "test", Path: path},
    73  			Info:  &apb.FileInfo{Path: path, Digest: digest},
    74  		}},
    75  		SourceFile: []string{path},
    76  	}, digest
    77  }
    78  
    79  func TestBuildTags(t *testing.T) {
    80  	// Make sure build tags are being respected. Synthesize a compilation with
    81  	// two trivial files, one tagged and the other not. After resolving, there
    82  	// should only be one file.
    83  
    84  	const keepFile = "// +build keepme\n\npackage foo"
    85  	const dropFile = "// +build ignore\n\npackage foo"
    86  
    87  	// Cobble together the data from two compilations into one.
    88  	u1, keepDigest := oneFileCompilation("keep.go", "foo", keepFile)
    89  	u2, dropDigest := oneFileCompilation("drop.go", "foo", dropFile)
    90  	u1.RequiredInput = append(u1.RequiredInput, u2.RequiredInput...)
    91  	u1.SourceFile = append(u1.SourceFile, u2.SourceFile...)
    92  
    93  	fetcher := memFetcher{
    94  		keepDigest: keepFile,
    95  		dropDigest: dropFile,
    96  	}
    97  
    98  	// Attach details with the build tags we care about.
    99  	info, err := ptypes.MarshalAny(&gopb.GoDetails{
   100  		BuildTags: []string{"keepme"},
   101  	})
   102  	if err != nil {
   103  		t.Fatalf("Marshaling Go details failed: %v", err)
   104  	}
   105  	u1.Details = append(u1.Details, info)
   106  
   107  	pi, err := Resolve(u1, fetcher, nil)
   108  	if err != nil {
   109  		t.Fatalf("Resolving compilation failed: %v", err)
   110  	}
   111  
   112  	// Make sure the files are what we think we want.
   113  	if n := len(pi.SourceText); n != 1 {
   114  		t.Errorf("Wrong number of source files: got %d, want 1", n)
   115  	}
   116  	for _, got := range pi.SourceText {
   117  		if got != keepFile {
   118  			t.Errorf("Wrong source:\n got: %#q\nwant: %#q", got, keepFile)
   119  		}
   120  	}
   121  }
   122  
   123  func TestResolve(t *testing.T) { // are you function enough not to back down?
   124  	// Test resolution on a simple two-package system:
   125  	//
   126  	// Package foo is compiled as test data from the source
   127  	//    package foo
   128  	//    func Foo() int { return 0 }
   129  	//
   130  	// Package bar is specified as source and imports foo.
   131  	// TODO(fromberger): Compile foo as part of the build.
   132  	foo, err := readTestFile(t, "testdata/foo.a")
   133  	if err != nil {
   134  		t.Fatalf("Unable to read foo.a: %v", err)
   135  	}
   136  	const bar = `package bar
   137  
   138  import "test/foo"
   139  
   140  func init() { println(foo.Foo()) }
   141  `
   142  	unit, digest := oneFileCompilation("testdata/bar.go", "bar", bar)
   143  	fetcher := memFetcher{
   144  		hexDigest(foo): string(foo),
   145  		digest:         bar,
   146  	}
   147  	unit.RequiredInput = append(unit.RequiredInput, &apb.CompilationUnit_FileInput{
   148  		VName: &spb.VName{Language: "go", Corpus: "test", Path: "foo", Signature: "package"},
   149  		Info:  &apb.FileInfo{Path: "testdata/foo.a", Digest: hexDigest(foo)},
   150  	})
   151  
   152  	pi, err := Resolve(unit, fetcher, nil)
   153  	if err != nil {
   154  		t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
   155  	}
   156  	if got, want := pi.Name, "bar"; got != want {
   157  		t.Errorf("Package name: got %q, want %q", got, want)
   158  	}
   159  	if got, want := pi.VName, unit.VName; !proto.Equal(got, want) {
   160  		t.Errorf("Base vname: got %+v, want %+v", got, want)
   161  	}
   162  	if got, want := pi.ImportPath, "test/bar"; got != want {
   163  		t.Errorf("Import path: got %q, want %q", got, want)
   164  	}
   165  	if dep, ok := pi.Dependencies["test/foo"]; !ok {
   166  		t.Errorf("Missing dependency for test/foo in %+v", pi.Dependencies)
   167  	} else if pi.PackageVName[dep] == nil {
   168  		t.Errorf("Missing VName for test/foo in %+v", pi.PackageVName)
   169  	}
   170  	if got, want := len(pi.Files), len(unit.SourceFile); got != want {
   171  		t.Errorf("Source files: got %d, want %d", got, want)
   172  	}
   173  	for _, err := range pi.Errors {
   174  		t.Errorf("Unexpected resolution error: %v", err)
   175  	}
   176  }
   177  
   178  func base64EncodeInfo(t testing.TB, msg string) string {
   179  	var gci mpb.GeneratedCodeInfo
   180  	if err := proto.UnmarshalText(msg, &gci); err != nil {
   181  		t.Fatal(err)
   182  	}
   183  	rec, err := proto.Marshal(&gci)
   184  	if err != nil {
   185  		t.Fatal(err)
   186  	}
   187  	return base64.StdEncoding.EncodeToString(rec)
   188  }
   189  
   190  func TestResolveInlineMetadata(t *testing.T) {
   191  	const gci = `
   192  meta: <
   193    edge: "%/kythe/edge/generates"
   194    vname: <
   195      signature: "IDENTIFIER:Primary"
   196      corpus: "default"
   197      path: "test/example.txt"
   198      language: "lang"
   199    >
   200    begin: 21
   201    end: 28
   202  >
   203  meta: <
   204    edge: "%/kythe/edge/generates"
   205    vname: <
   206      signature: "IDENTIFIER:Primary.my_param"
   207      corpus: "default"
   208      path: "test/example.txt"
   209      language: "lang"
   210    >
   211    begin: 41
   212    end: 48
   213  >
   214  	`
   215  	encoded := base64EncodeInfo(t, gci)
   216  	log.Infof("Encoded GeneratedCodeInfo: %s", encoded) // for testdata/basic/inline.go
   217  	subject := "package subject\n\nvar Primary = struct {\n\tMyParam bool\n}{\n\tMyParam: true,\n}\n\n//gokythe-inline-metadata:" + encoded
   218  	unit, digest := oneFileCompilation("testdata/subject.go", "subject", subject)
   219  	fetcher := memFetcher{
   220  		digest: subject,
   221  	}
   222  
   223  	featureRule := metadata.Rule{
   224  		EdgeIn:  edges.DefinesBinding,
   225  		EdgeOut: edges.Generates,
   226  		VName: &spb.VName{
   227  			Corpus:    "default",
   228  			Language:  "lang",
   229  			Signature: "IDENTIFIER:Primary",
   230  			Path:      "test/example.txt",
   231  		},
   232  		Reverse: true,
   233  		Begin:   21,
   234  		End:     28,
   235  	}
   236  
   237  	flagRule := metadata.Rule{
   238  		EdgeIn:  edges.DefinesBinding,
   239  		EdgeOut: edges.Generates,
   240  		VName: &spb.VName{
   241  			Corpus:    "default",
   242  			Language:  "lang",
   243  			Signature: "IDENTIFIER:Primary.my_param",
   244  			Path:      "test/example.txt",
   245  		},
   246  		Reverse: true,
   247  		Begin:   41,
   248  		End:     48,
   249  	}
   250  	wantRules := metadata.Rules{featureRule, flagRule}
   251  
   252  	pi, err := Resolve(unit, fetcher, nil)
   253  	if err != nil {
   254  		t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
   255  	}
   256  
   257  	gotRules := pi.Rules[pi.Files[0]]
   258  
   259  	if len(pi.Rules) != 1 {
   260  		t.Errorf("Resolve failed to load package rules in %v", unit.SourceFile)
   261  	}
   262  	if err := testutil.DeepEqual(wantRules, gotRules); err != nil {
   263  		t.Errorf("Rules diff %s", err)
   264  	}
   265  }
   266  
   267  func TestResolveErrors(t *testing.T) {
   268  	unit, _ := oneFileCompilation("blah.a", "bogus", "package blah")
   269  	unit.SourceFile = nil
   270  	pkg, err := Resolve(unit, make(memFetcher), nil)
   271  	if err == nil {
   272  		t.Errorf("Resolving 0-source package: got %+v, wanted error", pkg)
   273  	} else {
   274  		t.Logf("Got expected error for 0-source package: %v", err)
   275  	}
   276  }
   277  
   278  func TestSpan(t *testing.T) {
   279  	const input = `package main
   280  
   281  import "fmt"
   282  func main() { fmt.Println("Hello, world") }`
   283  
   284  	unit, digest := oneFileCompilation("main.go", "main", input)
   285  	fetcher := memFetcher{digest: input}
   286  	pi, err := Resolve(unit, fetcher, nil)
   287  	if err != nil {
   288  		t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
   289  	}
   290  
   291  	tests := []struct {
   292  		key      func(*ast.File) ast.Node // return a node to compute a span for
   293  		pos, end int                      // the expected span for the node
   294  	}{
   295  		{func(*ast.File) ast.Node { return nil }, -1, -1},                  // invalid node
   296  		{func(*ast.File) ast.Node { return fakeNode{0, 2} }, -1, -1},       // invalid pos
   297  		{func(*ast.File) ast.Node { return fakeNode{5, 0} }, 4, 4},         // invalid end
   298  		{func(f *ast.File) ast.Node { return f.Name }, 8, 12},              // main
   299  		{func(f *ast.File) ast.Node { return f.Imports[0].Path }, 21, 26},  // "fmt"
   300  		{func(f *ast.File) ast.Node { return f.Decls[0] }, 14, 26},         // import "fmt"
   301  		{func(f *ast.File) ast.Node { return f.Decls[1] }, 27, len(input)}, // func main() { ... }
   302  	}
   303  	for _, test := range tests {
   304  		node := test.key(pi.Files[0])
   305  		_, pos, end := pi.Span(node)
   306  		if pos != test.pos || end != test.end {
   307  			t.Errorf("Span(%v): got pos=%d, end=%v; want pos=%d, end=%d", node, pos, end, test.pos, test.end)
   308  		}
   309  	}
   310  }
   311  
   312  type fakeNode struct{ pos, end token.Pos }
   313  
   314  func (f fakeNode) Pos() token.Pos { return f.pos }
   315  func (f fakeNode) End() token.Pos { return f.end }
   316  
   317  func TestSink(t *testing.T) {
   318  	var facts, edges []*spb.Entry
   319  
   320  	sink := Sink(func(_ context.Context, e *spb.Entry) error {
   321  		if isEdge(e) {
   322  			edges = append(edges, e)
   323  		} else {
   324  			facts = append(facts, e)
   325  		}
   326  		return nil
   327  	})
   328  
   329  	hasFact := func(who *spb.VName, name, value string) bool {
   330  		for _, fact := range facts {
   331  			if proto.Equal(fact.Source, who) && fact.FactName == name && string(fact.FactValue) == value {
   332  				return true
   333  			}
   334  		}
   335  		return false
   336  	}
   337  	hasEdge := func(src, tgt *spb.VName, kind string) bool {
   338  		for _, edge := range edges {
   339  			if proto.Equal(edge.Source, src) && proto.Equal(edge.Target, tgt) && edge.EdgeKind == kind {
   340  				return true
   341  			}
   342  		}
   343  		return false
   344  	}
   345  
   346  	// Verify that the entries we push into the sink are preserved in encoding.
   347  	them := &spb.VName{Language: "peeps", Signature: "him"}
   348  	they := &spb.VName{Language: "peeps", Signature: "her"}
   349  	ctx := context.Background()
   350  	sink.writeFact(ctx, them, "/name", "Alex")
   351  	sink.writeEdge(ctx, them, they, "/friendof")
   352  	sink.writeFact(ctx, they, "/name", "Jordan")
   353  	sink.writeEdge(ctx, them, them, "/loves")
   354  	sink.writeEdge(ctx, they, them, "/suspiciousof")
   355  	sink.writeFact(ctx, them, "/name/full", "Alex Q. Public")
   356  	sink.writeFact(ctx, they, "/name/full", "Jordan M. Q. Contrary")
   357  
   358  	for _, want := range []struct {
   359  		who         *spb.VName
   360  		name, value string
   361  	}{
   362  		{them, "/name", "Alex"},
   363  		{them, "/name/full", "Alex Q. Public"},
   364  		{they, "/name", "Jordan"},
   365  		{they, "/name/full", "Jordan M. Q. Contrary"},
   366  	} {
   367  		if !hasFact(want.who, want.name, want.value) {
   368  			t.Errorf("Missing fact %q=%q for %+v", want.name, want.value, want.who)
   369  		}
   370  	}
   371  
   372  	for _, want := range []struct {
   373  		src, tgt *spb.VName
   374  		kind     string
   375  	}{
   376  		{them, they, "/friendof"},
   377  		{they, them, "/suspiciousof"},
   378  		{them, them, "/loves"},
   379  	} {
   380  
   381  		if !hasEdge(want.src, want.tgt, want.kind) {
   382  			t.Errorf("Missing edge %+v ―%s→ %+v", want.src, want.kind, want.tgt)
   383  		}
   384  	}
   385  }
   386  
   387  func TestComments(t *testing.T) {
   388  	// Verify that comment text is correctly escaped when translated into
   389  	// documentation nodes.
   390  	const input = `// Comment [escape] tests \t all the things.
   391  package pkg
   392  
   393  /*
   394    Comment [escape] tests \t all the things.
   395  */
   396  var z int
   397  `
   398  	unit, digest := oneFileCompilation("testfile/comment.go", "pkg", input)
   399  	pi, err := Resolve(unit, memFetcher{digest: input}, &ResolveOptions{Info: XRefTypeInfo()})
   400  	if err != nil {
   401  		t.Fatalf("Resolve failed: %v\nInput unit:\n%s", err, proto.MarshalTextString(unit))
   402  	}
   403  
   404  	var single, multi string
   405  	if err := pi.Emit(context.Background(), func(_ context.Context, e *spb.Entry) error {
   406  		if e.FactName != "/kythe/text" {
   407  			return nil
   408  		}
   409  		if e.Source.Signature == "package doc" {
   410  			if single != "" {
   411  				return fmt.Errorf("multiple package docs (%q, %q)", single, string(e.FactValue))
   412  			}
   413  			single = string(e.FactValue)
   414  		} else if e.Source.Signature == "var z doc" {
   415  			if multi != "" {
   416  				return fmt.Errorf("multiple variable docs (%q, %q)", multi, string(e.FactValue))
   417  			}
   418  			multi = string(e.FactValue)
   419  		}
   420  		return nil
   421  	}, nil); err != nil {
   422  		t.Fatalf("Emit unexpectedly failed: %v", err)
   423  	}
   424  
   425  	const want = `Comment \[escape\] tests \\t all the things.`
   426  	if single != want {
   427  		t.Errorf("Incorrect single-line comment escaping:\ngot  %#q\nwant %#q", single, want)
   428  	}
   429  	if multi != want {
   430  		t.Errorf("Incorrect multi-line comment escaping:\ngot  %#q\nwant %#q", multi, want)
   431  	}
   432  }
   433  
   434  func TestRules(t *testing.T) {
   435  	const input = "package main\n"
   436  	unit, digest := oneFileCompilation("main.go", "main", input)
   437  	unit.RequiredInput = append(unit.RequiredInput, &apb.CompilationUnit_FileInput{
   438  		VName: &spb.VName{Signature: "hey ho let's go"},
   439  		Info:  &apb.FileInfo{Path: "meta"},
   440  	})
   441  	fetcher := memFetcher{digest: input}
   442  
   443  	// Resolve the compilation with a rule checker that recognizes the special
   444  	// input we added and emits a rule for the main file. This verifies we get
   445  	// the right mapping from paths back to source inputs.
   446  	pi, err := Resolve(unit, fetcher, &ResolveOptions{
   447  		CheckRules: func(ri *apb.CompilationUnit_FileInput, _ Fetcher) (*Ruleset, error) {
   448  			if ri.Info.Path == "meta" {
   449  				return &Ruleset{
   450  					Path: "main.go", // associate these rules to the main source
   451  					Rules: metadata.Rules{{
   452  						Begin: 1,
   453  						End:   2,
   454  						VName: ri.VName,
   455  					}},
   456  				}, nil
   457  			}
   458  			return nil, nil
   459  		},
   460  	})
   461  	if err != nil {
   462  		t.Fatalf("Resolve failed: %v", err)
   463  	}
   464  
   465  	// The rules should have an entry for the primary source file, and it
   466  	// should contain the rule we generated.
   467  	rs, ok := pi.Rules[pi.Files[0]]
   468  	if !ok {
   469  		t.Fatal("Missing primary source file")
   470  	}
   471  	want := metadata.Rules{{
   472  		Begin: 1,
   473  		End:   2,
   474  		VName: &spb.VName{Signature: "hey ho let's go"},
   475  	}}
   476  	if err := testutil.DeepEqual(want, rs); err != nil {
   477  		t.Errorf("Wrong rules: %v", err)
   478  	}
   479  }
   480  
   481  // isEdge reports whether e represents an edge.
   482  func isEdge(e *spb.Entry) bool { return e.Target != nil && e.EdgeKind != "" }