github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/build/build_test.go (about) 1 package build 2 3 import ( 4 "fmt" 5 gobuild "go/build" 6 "go/token" 7 "strconv" 8 "testing" 9 10 "github.com/gopherjs/gopherjs/internal/srctesting" 11 "github.com/shurcooL/go/importgraphutil" 12 ) 13 14 // Natives augment the standard library with GopherJS-specific changes. 15 // This test ensures that none of the standard library packages are modified 16 // in a way that adds imports which the original upstream standard library package 17 // does not already import. Doing that can increase generated output size or cause 18 // other unexpected issues (since the cmd/go tool does not know about these extra imports), 19 // so it's best to avoid it. 20 // 21 // It checks all standard library packages. Each package is considered as a normal 22 // package, as a test package, and as an external test package. 23 func TestNativesDontImportExtraPackages(t *testing.T) { 24 // Calculate the forward import graph for all standard library packages. 25 // It's needed for populateImportSet. 26 stdOnly := goCtx(DefaultEnv()) 27 // Skip post-load package tweaks, since we are interested in the complete set 28 // of original sources. 29 stdOnly.noPostTweaks = true 30 // We only care about standard library, so skip all GOPATH packages. 31 stdOnly.bctx.GOPATH = "" 32 forward, _, err := importgraphutil.BuildNoTests(&stdOnly.bctx) 33 if err != nil { 34 t.Fatalf("importgraphutil.BuildNoTests: %v", err) 35 } 36 37 // populateImportSet takes a slice of imports, and populates set with those 38 // imports, as well as their transitive dependencies. That way, the set can 39 // be quickly queried to check if a package is in the import graph of imports. 40 // 41 // Note, this does not include transitive imports of test/xtest packages, 42 // which could cause some false positives. It currently doesn't, but if it does, 43 // then support for that should be added here. 44 populateImportSet := func(imports []string) stringSet { 45 set := stringSet{} 46 for _, p := range imports { 47 set[p] = struct{}{} 48 switch p { 49 case "sync": 50 set["github.com/gopherjs/gopherjs/nosync"] = struct{}{} 51 } 52 transitiveImports := forward.Search(p) 53 for p := range transitiveImports { 54 set[p] = struct{}{} 55 } 56 } 57 return set 58 } 59 60 // Check all standard library packages. 61 // 62 // The general strategy is to first import each standard library package using the 63 // normal build.Import, which returns a *build.Package. That contains Imports, TestImports, 64 // and XTestImports values that are considered the "real imports". 65 // 66 // That list of direct imports is then expanded to the transitive closure by populateImportSet, 67 // meaning all packages that are indirectly imported are also added to the set. 68 // 69 // Then, github.com/gopherjs/gopherjs/build.parseAndAugment(*build.Package) returns []*ast.File. 70 // Those augmented parsed Go files of the package are checked, one file at at time, one import 71 // at a time. Each import is verified to belong in the set of allowed real imports. 72 matches, matchErr := stdOnly.Match([]string{"std"}) 73 if matchErr != nil { 74 t.Fatalf("Failed to list standard library packages: %s", err) 75 } 76 for _, pkgName := range matches { 77 pkgName := pkgName // Capture for the goroutine. 78 t.Run(pkgName, func(t *testing.T) { 79 t.Parallel() 80 81 pkg, err := stdOnly.Import(pkgName, "", gobuild.ImportComment) 82 if err != nil { 83 t.Fatalf("gobuild.Import: %v", err) 84 } 85 86 for _, pkgVariant := range []*PackageData{pkg, pkg.TestPackage(), pkg.XTestPackage()} { 87 t.Logf("Checking package %s...", pkgVariant) 88 89 // Capture the set of unmodified package imports. 90 realImports := populateImportSet(pkgVariant.Imports) 91 92 // Use parseAndAugment to get a list of augmented AST files. 93 fset := token.NewFileSet() 94 files, _, err := parseAndAugment(stdOnly, pkgVariant, pkgVariant.IsTest, fset) 95 if err != nil { 96 t.Fatalf("github.com/gopherjs/gopherjs/build.parseAndAugment: %v", err) 97 } 98 99 // Verify imports of augmented AST files. 100 for _, f := range files { 101 fileName := fset.File(f.Pos()).Name() 102 for _, imp := range f.Imports { 103 importPath, err := strconv.Unquote(imp.Path.Value) 104 if err != nil { 105 t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err) 106 } 107 if importPath == "github.com/gopherjs/gopherjs/js" { 108 continue 109 } 110 if _, ok := realImports[importPath]; !ok { 111 t.Errorf("augmented package %q imports %q in file %v, but real %q doesn't:\nrealImports = %v", 112 pkgVariant, importPath, fileName, pkgVariant.ImportPath, realImports) 113 } 114 } 115 } 116 } 117 }) 118 } 119 } 120 121 // stringSet is used to print a set of strings in a more readable way. 122 type stringSet map[string]struct{} 123 124 func (m stringSet) String() string { 125 s := make([]string, 0, len(m)) 126 for v := range m { 127 s = append(s, v) 128 } 129 return fmt.Sprintf("%q", s) 130 } 131 132 func TestOverlayAugmentation(t *testing.T) { 133 tests := []struct { 134 desc string 135 src string 136 noCodeChange bool 137 want string 138 expInfo map[string]overrideInfo 139 }{ 140 { 141 desc: `remove function`, 142 src: `func Foo(a, b int) int { 143 return a + b 144 }`, 145 noCodeChange: true, 146 expInfo: map[string]overrideInfo{ 147 `Foo`: {}, 148 }, 149 }, { 150 desc: `keep function`, 151 src: `//gopherjs:keep-original 152 func Foo(a, b int) int { 153 return a + b 154 }`, 155 noCodeChange: true, 156 expInfo: map[string]overrideInfo{ 157 `Foo`: {keepOriginal: true}, 158 }, 159 }, { 160 desc: `remove constants and values`, 161 src: `import "time" 162 163 const ( 164 foo = 42 165 bar = "gopherjs" 166 ) 167 168 var now = time.Now`, 169 noCodeChange: true, 170 expInfo: map[string]overrideInfo{ 171 `foo`: {}, 172 `bar`: {}, 173 `now`: {}, 174 }, 175 }, { 176 desc: `remove types`, 177 src: `type ( 178 foo struct {} 179 bar int 180 ) 181 182 type bob interface {}`, 183 noCodeChange: true, 184 expInfo: map[string]overrideInfo{ 185 `foo`: {}, 186 `bar`: {}, 187 `bob`: {}, 188 }, 189 }, { 190 desc: `remove methods`, 191 src: `type Foo struct { 192 bar int 193 } 194 195 func (x *Foo) GetBar() int { return x.bar } 196 func (x *Foo) SetBar(bar int) { x.bar = bar }`, 197 noCodeChange: true, 198 expInfo: map[string]overrideInfo{ 199 `Foo`: {}, 200 `Foo.GetBar`: {}, 201 `Foo.SetBar`: {}, 202 }, 203 }, { 204 desc: `remove generics`, 205 src: `import "cmp" 206 207 type Pointer[T any] struct {} 208 209 func Sort[S ~[]E, E cmp.Ordered](x S) {} 210 211 // this is a stub for "func Equal[S ~[]E, E any](s1, s2 S) bool {}" 212 func Equal[S ~[]E, E any](s1, s2 S) bool {}`, 213 noCodeChange: true, 214 expInfo: map[string]overrideInfo{ 215 `Pointer`: {}, 216 `Sort`: {}, 217 `Equal`: {}, 218 }, 219 }, { 220 desc: `prune an unused import`, 221 src: `import foo "some/other/bar"`, 222 want: ``, 223 expInfo: map[string]overrideInfo{}, 224 }, { 225 desc: `purge function`, 226 src: `//gopherjs:purge 227 func Foo(a, b int) int { 228 return a + b 229 }`, 230 want: ``, 231 expInfo: map[string]overrideInfo{ 232 `Foo`: {}, 233 }, 234 }, { 235 desc: `purge struct removes an import`, 236 src: `import "bytes" 237 import "math" 238 239 //gopherjs:purge 240 type Foo struct { 241 bar *bytes.Buffer 242 } 243 244 const Tau = math.Pi * 2.0`, 245 want: `import "math" 246 247 const Tau = math.Pi * 2.0`, 248 expInfo: map[string]overrideInfo{ 249 `Foo`: {purgeMethods: true}, 250 `Tau`: {}, 251 }, 252 }, { 253 desc: `purge whole type decl`, 254 src: `//gopherjs:purge 255 type ( 256 Foo struct {} 257 bar interface{} 258 bob int 259 )`, 260 want: ``, 261 expInfo: map[string]overrideInfo{ 262 `Foo`: {purgeMethods: true}, 263 `bar`: {purgeMethods: true}, 264 `bob`: {purgeMethods: true}, 265 }, 266 }, { 267 desc: `purge part of type decl`, 268 src: `type ( 269 Foo struct {} 270 271 //gopherjs:purge 272 bar interface{} 273 274 //gopherjs:purge 275 bob int 276 )`, 277 want: `type ( 278 Foo struct {} 279 )`, 280 expInfo: map[string]overrideInfo{ 281 `Foo`: {}, 282 `bar`: {purgeMethods: true}, 283 `bob`: {purgeMethods: true}, 284 }, 285 }, { 286 desc: `purge all of a type decl`, 287 src: `type ( 288 //gopherjs:purge 289 Foo struct {} 290 )`, 291 want: ``, 292 expInfo: map[string]overrideInfo{ 293 `Foo`: {purgeMethods: true}, 294 }, 295 }, { 296 desc: `remove and purge values`, 297 src: `import "time" 298 299 const ( 300 foo = 42 301 //gopherjs:purge 302 bar = "gopherjs" 303 ) 304 305 //gopherjs:purge 306 var now = time.Now`, 307 want: `const ( 308 foo = 42 309 )`, 310 expInfo: map[string]overrideInfo{ 311 `foo`: {}, 312 `bar`: {}, 313 `now`: {}, 314 }, 315 }, { 316 desc: `purge all value names`, 317 src: `//gopherjs:purge 318 var foo, bar int 319 320 //gopherjs:purge 321 const bob, sal = 12, 42`, 322 want: ``, 323 expInfo: map[string]overrideInfo{ 324 `foo`: {}, 325 `bar`: {}, 326 `bob`: {}, 327 `sal`: {}, 328 }, 329 }, { 330 desc: `imports not confused by local variables`, 331 src: `import ( 332 "cmp" 333 "time" 334 ) 335 336 //gopherjs:purge 337 func Sort[S ~[]E, E cmp.Ordered](x S) {} 338 339 func SecondsSince(start time.Time) int { 340 cmp := time.Now().Sub(start) 341 return int(cmp.Second()) 342 }`, 343 want: `import ( 344 "time" 345 ) 346 347 func SecondsSince(start time.Time) int { 348 cmp := time.Now().Sub(start) 349 return int(cmp.Second()) 350 }`, 351 expInfo: map[string]overrideInfo{ 352 `Sort`: {}, 353 `SecondsSince`: {}, 354 }, 355 }, { 356 desc: `purge generics`, 357 src: `import "cmp" 358 359 //gopherjs:purge 360 type Pointer[T any] struct {} 361 362 //gopherjs:purge 363 func Sort[S ~[]E, E cmp.Ordered](x S) {} 364 365 // stub for "func Equal[S ~[]E, E any](s1, s2 S) bool" 366 func Equal() {}`, 367 want: `// stub for "func Equal[S ~[]E, E any](s1, s2 S) bool" 368 func Equal() {}`, 369 expInfo: map[string]overrideInfo{ 370 `Pointer`: {purgeMethods: true}, 371 `Sort`: {}, 372 `Equal`: {}, 373 }, 374 }, { 375 desc: `remove unsafe and embed if not needed`, 376 src: `import "unsafe" 377 import "embed" 378 379 //gopherjs:purge 380 var eFile embed.FS 381 382 //gopherjs:purge 383 func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)`, 384 want: ``, 385 expInfo: map[string]overrideInfo{ 386 `SwapPointer`: {}, 387 `eFile`: {}, 388 }, 389 }, { 390 desc: `keep unsafe and embed for directives`, 391 src: `import "unsafe" 392 import "embed" 393 394 //go:embed hello.txt 395 var eFile embed.FS 396 397 //go:linkname runtimeNano runtime.nanotime 398 func runtimeNano() int64`, 399 want: `import _ "unsafe" 400 import "embed" 401 402 //go:embed hello.txt 403 var eFile embed.FS 404 405 //go:linkname runtimeNano runtime.nanotime 406 func runtimeNano() int64`, 407 expInfo: map[string]overrideInfo{ 408 `eFile`: {}, 409 `runtimeNano`: {}, 410 }, 411 }, 412 } 413 414 for _, test := range tests { 415 t.Run(test.desc, func(t *testing.T) { 416 const pkgName = "package testpackage\n\n" 417 if test.noCodeChange { 418 test.want = test.src 419 } 420 421 f := srctesting.New(t) 422 fileSrc := f.Parse("test.go", pkgName+test.src) 423 424 overrides := map[string]overrideInfo{} 425 augmentOverlayFile(fileSrc, overrides) 426 pruneImports(fileSrc) 427 428 got := srctesting.Format(t, f.FileSet, fileSrc) 429 430 fileWant := f.Parse("test.go", pkgName+test.want) 431 want := srctesting.Format(t, f.FileSet, fileWant) 432 433 if got != want { 434 t.Errorf("augmentOverlayFile and pruneImports got unexpected code:\n"+ 435 "returned:\n\t%q\nwant:\n\t%q", got, want) 436 } 437 438 for key, expInfo := range test.expInfo { 439 if gotInfo, ok := overrides[key]; !ok { 440 t.Errorf(`%q was expected but not gotten`, key) 441 } else if expInfo != gotInfo { 442 t.Errorf(`%q had wrong info, got %+v`, key, gotInfo) 443 } 444 } 445 for key, gotInfo := range overrides { 446 if _, ok := test.expInfo[key]; !ok { 447 t.Errorf(`%q with %+v was not expected`, key, gotInfo) 448 } 449 } 450 }) 451 } 452 } 453 454 func TestOriginalAugmentation(t *testing.T) { 455 tests := []struct { 456 desc string 457 info map[string]overrideInfo 458 src string 459 want string 460 }{ 461 { 462 desc: `do not affect function`, 463 info: map[string]overrideInfo{}, 464 src: `func Foo(a, b int) int { 465 return a + b 466 }`, 467 want: `func Foo(a, b int) int { 468 return a + b 469 }`, 470 }, { 471 desc: `change unnamed sync import`, 472 info: map[string]overrideInfo{}, 473 src: `import "sync" 474 475 var _ = &sync.Mutex{}`, 476 want: `import sync "github.com/gopherjs/gopherjs/nosync" 477 478 var _ = &sync.Mutex{}`, 479 }, { 480 desc: `change named sync import`, 481 info: map[string]overrideInfo{}, 482 src: `import foo "sync" 483 484 var _ = &foo.Mutex{}`, 485 want: `import foo "github.com/gopherjs/gopherjs/nosync" 486 487 var _ = &foo.Mutex{}`, 488 }, { 489 desc: `remove function`, 490 info: map[string]overrideInfo{ 491 `Foo`: {}, 492 }, 493 src: `func Foo(a, b int) int { 494 return a + b 495 }`, 496 want: ``, 497 }, { 498 desc: `keep original function`, 499 info: map[string]overrideInfo{ 500 `Foo`: {keepOriginal: true}, 501 }, 502 src: `func Foo(a, b int) int { 503 return a + b 504 }`, 505 want: `func _gopherjs_original_Foo(a, b int) int { 506 return a + b 507 }`, 508 }, { 509 desc: `remove types and values`, 510 info: map[string]overrideInfo{ 511 `Foo`: {}, 512 `now`: {}, 513 `bar1`: {}, 514 }, 515 src: `import "time" 516 517 type Foo interface{ 518 bob(a, b string) string 519 } 520 521 var now = time.Now 522 const bar1, bar2 = 21, 42`, 523 want: `const bar2 = 42`, 524 }, { 525 desc: `remove in multi-value context`, 526 info: map[string]overrideInfo{ 527 `bar`: {}, 528 }, 529 src: `const foo, bar = func() (int, int) { 530 return 24, 12 531 }()`, 532 want: `const foo, _ = func() (int, int) { 533 return 24, 12 534 }()`, 535 }, { 536 desc: `full remove in multi-value context`, 537 info: map[string]overrideInfo{ 538 `bar`: {}, 539 }, 540 src: `const _, bar = func() (int, int) { 541 return 24, 12 542 }()`, 543 want: ``, 544 }, { 545 desc: `remove methods`, 546 info: map[string]overrideInfo{ 547 `Foo.GetBar`: {}, 548 `Foo.SetBar`: {}, 549 }, 550 src: ` 551 func (x Foo) GetBar() int { return x.bar } 552 func (x *Foo) SetBar(bar int) { x.bar = bar }`, 553 want: ``, 554 }, { 555 desc: `purge struct and methods`, 556 info: map[string]overrideInfo{ 557 `Foo`: {purgeMethods: true}, 558 }, 559 src: `type Foo struct{ 560 bar int 561 } 562 563 func (f Foo) GetBar() int { return f.bar } 564 func (f *Foo) SetBar(bar int) { f.bar = bar } 565 566 func NewFoo(bar int) *Foo { return &Foo{bar: bar} }`, 567 // NewFoo is not removed automatically since 568 // only functions with Foo as a receiver are removed. 569 want: `func NewFoo(bar int) *Foo { return &Foo{bar: bar} }`, 570 }, { 571 desc: `remove generics`, 572 info: map[string]overrideInfo{ 573 `Pointer`: {}, 574 `Sort`: {}, 575 `Equal`: {}, 576 }, 577 src: `import "cmp" 578 579 // keeps the isOnlyImports from skipping what is being tested. 580 func foo() {} 581 582 type Pointer[T any] struct {} 583 584 func Sort[S ~[]E, E cmp.Ordered](x S) {} 585 586 // overlay had stub "func Equal() {}" 587 func Equal[S ~[]E, E any](s1, s2 S) bool {}`, 588 want: `// keeps the isOnlyImports from skipping what is being tested. 589 func foo() {}`, 590 }, { 591 desc: `purge generics`, 592 info: map[string]overrideInfo{ 593 `Pointer`: {purgeMethods: true}, 594 `Sort`: {}, 595 `Equal`: {}, 596 }, 597 src: `import "cmp" 598 599 // keeps the isOnlyImports from skipping what is being tested. 600 func foo() {} 601 602 type Pointer[T any] struct {} 603 func (x *Pointer[T]) Load() *T {} 604 func (x *Pointer[T]) Store(val *T) {} 605 606 func Sort[S ~[]E, E cmp.Ordered](x S) {} 607 608 // overlay had stub "func Equal() {}" 609 func Equal[S ~[]E, E any](s1, s2 S) bool {}`, 610 want: `// keeps the isOnlyImports from skipping what is being tested. 611 func foo() {}`, 612 }, { 613 desc: `prune an unused import`, 614 info: map[string]overrideInfo{}, 615 src: `import foo "some/other/bar" 616 617 // keeps the isOnlyImports from skipping what is being tested. 618 func foo() {}`, 619 want: `// keeps the isOnlyImports from skipping what is being tested. 620 func foo() {}`, 621 }, { 622 desc: `override signature of function`, 623 info: map[string]overrideInfo{ 624 `Foo`: { 625 overrideSignature: srctesting.ParseFuncDecl(t, 626 `package whatever 627 func Foo(a, b any) (any, bool) {}`), 628 }, 629 }, 630 src: `func Foo[T comparable](a, b T) (T, bool) { 631 if a == b { 632 return a, true 633 } 634 return b, false 635 }`, 636 want: `func Foo(a, b any) (any, bool) { 637 if a == b { 638 return a, true 639 } 640 return b, false 641 }`, 642 }, { 643 desc: `override signature of method`, 644 info: map[string]overrideInfo{ 645 `Foo.Bar`: { 646 overrideSignature: srctesting.ParseFuncDecl(t, 647 `package whatever 648 func (r *Foo) Bar(a, b any) (any, bool) {}`), 649 }, 650 }, 651 src: `func (r *Foo[T]) Bar(a, b T) (T, bool) { 652 if r.isSame(a, b) { 653 return a, true 654 } 655 return b, false 656 }`, 657 want: `func (r *Foo) Bar(a, b any) (any, bool) { 658 if r.isSame(a, b) { 659 return a, true 660 } 661 return b, false 662 }`, 663 }, { 664 desc: `empty file removes all imports`, 665 info: map[string]overrideInfo{ 666 `foo`: {}, 667 }, 668 src: `import . "math/rand" 669 func foo() int { 670 return Int() 671 }`, 672 want: ``, 673 }, { 674 desc: `empty file with directive`, 675 info: map[string]overrideInfo{ 676 `foo`: {}, 677 }, 678 src: `//go:linkname foo bar 679 import _ "unsafe"`, 680 want: `//go:linkname foo bar 681 import _ "unsafe"`, 682 }, { 683 desc: `multiple imports for directives`, 684 info: map[string]overrideInfo{ 685 `A`: {}, 686 `C`: {}, 687 }, 688 src: `import "unsafe" 689 import "embed" 690 691 //go:embed hello.txt 692 var A embed.FS 693 694 //go:embed goodbye.txt 695 var B string 696 697 var C unsafe.Pointer 698 699 // override Now with hardcoded time for testing 700 //go:linkname timeNow time.Now 701 func timeNow() time.Time { 702 return time.Date(2012, 8, 6, 0, 0, 0, 0, time.UTC) 703 }`, 704 want: `import _ "unsafe" 705 import _ "embed" 706 707 //go:embed goodbye.txt 708 var B string 709 710 // override Now with hardcoded time for testing 711 //go:linkname timeNow time.Now 712 func timeNow() time.Time { 713 return time.Date(2012, 8, 6, 0, 0, 0, 0, time.UTC) 714 }`, 715 }, 716 } 717 718 for _, test := range tests { 719 t.Run(test.desc, func(t *testing.T) { 720 pkgName := "package testpackage\n\n" 721 importPath := `math/rand` 722 f := srctesting.New(t) 723 fileSrc := f.Parse("test.go", pkgName+test.src) 724 725 augmentOriginalImports(importPath, fileSrc) 726 augmentOriginalFile(fileSrc, test.info) 727 pruneImports(fileSrc) 728 729 got := srctesting.Format(t, f.FileSet, fileSrc) 730 731 fileWant := f.Parse("test.go", pkgName+test.want) 732 want := srctesting.Format(t, f.FileSet, fileWant) 733 734 if got != want { 735 t.Errorf("augmentOriginalImports, augmentOriginalFile, and pruneImports got unexpected code:\n"+ 736 "returned:\n\t%q\nwant:\n\t%q", got, want) 737 } 738 }) 739 } 740 }