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 != "" }